/* ════════════════════════════════════════════════════════════════════════════
   Athar docs — the chrome for site/docs.html.

   The landing (site/index.html) is self-contained on purpose — its CSS is
   inline, so it stays one file to move. This stylesheet carries only what
   docs.html needs: the shared header/footer chrome, the chapter rail, the
   markdown body, and the code block.

   The tokens below are the LANDING's tokens, name for name and value for
   value, including its light theme, so a reader who sets the light theme on
   the landing and clicks "Docs" doesn't land on a differently-lit page. Both
   pages read `athar-site-theme` from localStorage before first paint and
   resolve to the same surface.

   COLOUR — one accent, per brand/logo.svg: AMBER #E9A23B, the ring mark's
   colour on an INK ground. Everything else is neutral.

   MEASURED, not guessed:
     amber on the dark ground (#0B0B0D)  9.08:1 — safe as text and as a fill.
     amber on white/paper                ~2.1:1 — a fine FILL, an unusable
       INK. So amber-coloured TEXT (links, the active rail item, the eyebrow)
       resolves to --amber-ink: #9A5710 in the light theme (5.4:1 on the
       paper ground), and to --amber (#E9A23B) itself in the dark theme.
     --t3 (the faintest text colour) is #948F9B on dark and #6B665E on
       light — both nudged up across successive passes that measured under
       4.5:1, which is why this file states the number instead of eyeballing
       it. t3 is set on --panel and --panel-2 as well as the page background
       (the chrome address-bar label, the seam-node captions, the docs rail
       group headers), so the number that has to clear 4.5:1 is the worst of
       the three, not just t3-on-bg: dark measures 6.24:1 on bg, 5.72:1 on
       panel, 5.38:1 on panel-2; light measures 5.46:1 on bg, 5.70:1 on
       panel, 5.06:1 on panel-2. These are the same values site/index.html
       uses, so the landing and the docs stay one lit surface. An earlier
       dark value (#807D86) passed
       against bg (4.87:1) but failed against panel (4.46:1) and panel-2
       (4.20:1) — exactly the kind of miss a single spot-check against the
       page background, rather than every surface the colour actually sits
       on, produces.

   CODE — a fixed palette shared across the Vulos suite, independent of page
   theme (the code surface stays dark in both): see the block below `.hljs`.
   Every one of its colours clears 4.5:1 against #0C1116, comments included:
   the comment tone is #6C7D94 (4.51:1), not the #6B7C93 (4.45:1) an earlier
   pass carried, which was the one token on this page that missed the floor.

   TYPE — identical to the landing: Fraunces (display, headings) / Schibsted
   Grotesk (body) / IBM Plex Mono (code), all self-hosted under ./fonts, none
   shared with a sibling Vulos landing's font choice. See site/index.html's
   stylesheet header for the full rationale.
   ════════════════════════════════════════════════════════════════════════════ */

@font-face{font-family:'Fraunces Variable';font-style:normal;font-weight:100 900;font-display:swap;src:url('./fonts/fraunces-latin-wght-normal.woff2') format('woff2-variations');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;}
@font-face{font-family:'Fraunces Variable';font-style:italic;font-weight:100 900;font-display:swap;src:url('./fonts/fraunces-latin-wght-italic.woff2') format('woff2-variations');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;}
@font-face{font-family:'Schibsted Grotesk Variable';font-style:normal;font-weight:400 900;font-display:swap;src:url('./fonts/schibsted-grotesk-latin-wght-normal.woff2') format('woff2-variations');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;src:url('./fonts/ibm-plex-mono-latin-400-normal.woff2') format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:600;font-display:swap;src:url('./fonts/ibm-plex-mono-latin-600-normal.woff2') format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;}

*,*::before,*::after{box-sizing:border-box}

:root{
  /* Dark theme = near-black ground, warmed a touch to sit near the mark's
     own #0E0E11 rather than a cold, bluish black. */
  --bg:#0B0B0D; --bg-2:#111114; --panel:#16161A; --panel-2:#1C1C21; --sunken:#08080A;
  --line:rgba(255,255,255,.09); --line-2:rgba(255,255,255,.16); --hair:rgba(255,255,255,.06);
  --t1:#F5F3EF; --t2:#A6A3AC; --t3:#948F9B;
  --amber:#E9A23B; --amber-ink:#E9A23B;
  --amber-wash:rgba(233,162,59,.12); --amber-edge:rgba(233,162,59,.32); --amber-glow:rgba(233,162,59,.45);
  --err:#FF8A8A; --err-wash:rgba(239,68,68,.10); --err-edge:rgba(239,68,68,.30);
  --ok:#34D399;
  --r:14px; --r-sm:9px; --max:1220px;
  /* Docs shell geometry. --hdr-h must equal .nav's height: the desktop rail is
     pinned to the viewport from directly under the header to its bottom edge,
     so the two numbers are one number. --rail-w is the rail's full width now
     that it is flush to the viewport's left edge rather than a card floating
     inside a centred wrapper. */
  --hdr-h:64px; --rail-w:288px; --doc-gap:48px;
  --quick:130ms; --base:220ms; --ease:cubic-bezier(.2,0,.2,1);
  --display:'Fraunces Variable',ui-serif,Georgia,'Times New Roman',serif;
  --sans:'Schibsted Grotesk Variable',-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,Roboto,Helvetica,Arial,sans-serif;
  --mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,"SF Mono","Cascadia Mono",Menlo,Consolas,monospace;
  color-scheme:dark;
}
:root[data-theme=light]{
  --bg:#FBFAF8; --bg-2:#F4F1EC; --panel:#FFFFFF; --panel-2:#F4F1EC; --sunken:#EFEBE3;
  --line:#E4DFD5; --line-2:#D6CFC0; --hair:rgba(20,18,14,.06);
  --t1:#16151A; --t2:#5B5750; --t3:#6B665E;
  --amber-ink:#9A5710;
  --amber-wash:#FBF1E3; --amber-edge:rgba(154,87,16,.28); --amber-glow:rgba(233,162,59,.35);
  --err:#C81E1E; --err-wash:rgba(200,30,30,.07); --err-edge:rgba(200,30,30,.26);
  --ok:#0C7049;
  color-scheme:light;
}

html{scroll-behavior:smooth;scroll-padding-top:80px}
body{
  margin:0;font-family:var(--sans);background:var(--bg);color:var(--t1);
  line-height:1.62;-webkit-font-smoothing:antialiased;font-size:16px;overflow-x:hidden;
}
a{color:inherit;text-decoration:none}
img{max-width:100%;display:block}
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
svg{display:block}
::selection{background:var(--amber-glow);color:#1A1204}
:where(a,button,summary,[tabindex],input):focus-visible{outline:2px solid var(--amber);outline-offset:2px;border-radius:6px}
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important}
}

.wrap{width:100%;max-width:var(--max);margin:0 auto;padding:0 26px}
@media(max-width:560px){.wrap{padding:0 18px}}

.skip{
  position:absolute;left:14px;top:-70px;z-index:200;padding:11px 18px;border-radius:10px;
  background:var(--amber);color:#1A1204;font-weight:750;font-size:14px;
  transition:top var(--quick) var(--ease);
}
.skip:focus{top:14px}

/* ── Header ──────────────────────────────────────────────────────────────── */
.site-header{
  position:sticky;top:0;z-index:50;
  background:color-mix(in srgb,var(--bg) 82%,transparent);
  backdrop-filter:blur(14px) saturate(1.3);
  border-bottom:1px solid var(--line);
}
.nav{display:flex;align-items:center;gap:18px;height:64px}
.brand{display:inline-flex;align-items:center;gap:10px;font-family:var(--display);font-weight:700;letter-spacing:-.02em;font-size:19px;flex:none;color:var(--t1);min-height:44px}
.brand .tile{
  width:30px;height:30px;border-radius:9px;display:block;flex:none;
  box-shadow:0 4px 14px -5px var(--amber-glow);
}
.brand .tile svg{width:100%;height:100%;display:block}
.nav-links{display:flex;align-items:center;gap:3px;margin-left:auto}
.nav-links a{
  display:inline-flex;align-items:center;min-height:44px;
  padding:0 11px;border-radius:8px;color:var(--t2);font-size:14px;font-weight:550;
  transition:color var(--quick) var(--ease),background var(--quick) var(--ease);
}
.nav-links a:hover{color:var(--t1);background:var(--panel-2)}
.nav-links a.active{color:var(--amber-ink);background:var(--amber-wash)}
.nav-act{display:flex;align-items:center;gap:8px;margin-left:auto;flex:none}
.nav-links+.nav-act{margin-left:14px}
.iconbtn{
  width:44px;height:44px;border-radius:11px;border:1px solid var(--line);background:var(--panel);
  display:grid;place-items:center;color:var(--t2);flex:none;
  transition:color var(--quick) var(--ease),border-color var(--quick) var(--ease);
}
.iconbtn:hover{color:var(--t1);border-color:var(--line-2)}
.iconbtn svg{width:17px;height:17px}:root[data-theme=light] .moon{display:none}
:root[data-theme=dark] .sun{display:none}
.ghlink{
  display:inline-flex;align-items:center;justify-content:center;gap:7px;height:44px;min-width:44px;
  padding:0 13px;border-radius:11px;
  border:1px solid var(--line);background:var(--panel);font-size:14px;font-weight:600;color:var(--t2);
  transition:color var(--quick) var(--ease),border-color var(--quick) var(--ease);
}
.ghlink:hover{color:var(--t1);border-color:var(--line-2)}
.ghlink svg{width:16px;height:16px}
.ghlink-icon{width:44px;padding:0;gap:0}
.ghlink-icon svg{width:17px;height:17px}
@media(max-width:900px){.nav-links{display:none}}
@media(max-width:560px){.ghlink span{display:none}.ghlink{padding:0}}

/* ── Status strip ────────────────────────────────────────────────────────
   Laid out as flowing prose, not as flex items. It is one sentence ending in
   a link, and as a flex row the link became a blockified flex item — a
   21px-tall "control" that the tap-target floor then had to grow, when what
   it actually is is an inline link at the end of a sentence. */
.status-strip{border-bottom:1px solid var(--line);background:var(--amber-wash)}
.status-strip .wrap{
  padding-block:9px;font-size:13px;color:var(--t2);
}
.status-strip b{color:var(--amber-ink);font-weight:700}
.status-strip a{color:var(--amber-ink);font-weight:600}
.status-strip a:hover{text-decoration:underline}

/* ── Docs shell ──────────────────────────────────────────────────────────
   `padding-block`, not the `padding` shorthand: this element is also a
   `.wrap`, and a later single-class rule setting `padding:32px 0 76px` beat
   `.wrap`'s own `padding:0 26px` on source order and silently deleted the
   page gutter. Below 1200px that put the rail and every line of prose flush
   against the viewport edge. The same collision existed on .foot and on the
   landing's .colophon; all three are stated as padding-block now.

   THE BREAKPOINT IS ONE NUMBER, STATED ONCE PER SIDE. It used to be written
   as `max-width:900px` for the collapsed shape and `min-width:901px` for the
   rail, which leaves the half-open interval (900px, 901px) matching NEITHER —
   and a fractional CSS viewport is not exotic, it is what browser zoom
   produces (a 991px window at 110% is 900.909px). In that gap the summary
   kept `display:none` from the base rule while docs.html's own
   `matchMedia('(min-width: 901px)')` read false and closed the <details>, so
   the rail rendered as an empty box with the disclosure chevron — the only
   control that could have reopened it — not drawn at all. Both sides now key
   off 900px, `min-width:900px` / `max-width:899.98px`, which tile the line
   with no hole, and docs.html's matchMedia string is the same 900px.

   THE SHELL IS NOT A CENTRED WRAPPER ON DESKTOP. `.docs-shell` is also a
   `.wrap`, so the rail used to start 136px in from the left of a 1440px
   viewport and read as floating. Above the breakpoint the wrapper is undone
   (max-width:none, margin:0) and the column is stated as padding instead, so
   the document's left edge, the status strip's and the footer's are one line
   at `--rail-w + --doc-gap` and the rail owns everything to the left of it. */
.docs-shell{display:grid;grid-template-columns:minmax(0,1fr);gap:18px;padding-block:16px 56px}
@media(min-width:900px){
  .wrap.docs-shell{
    display:block;max-width:none;margin:0;padding-block:32px 76px;
    padding-left:calc(var(--rail-w) + var(--doc-gap));padding-right:var(--doc-gap);
  }
  .status-strip .wrap,.site-footer .wrap{
    max-width:none;margin:0;
    padding-left:calc(var(--rail-w) + var(--doc-gap));padding-right:var(--doc-gap);
  }
}

/* ── The chapter rail ────────────────────────────────────────────────────
   ONE list, two shapes.

   ≥900px it is a rail PINNED TO THE VIEWPORT'S LEFT EDGE — position:fixed,
   left:0, from directly under the header to the bottom of the screen. Not
   sticky-inside-a-centred-wrapper, which is what it was: at 1440 that put its
   left edge at x=136 and made a navigation rail read as a floating card that
   drifted with the page. Fixed means it does not move at all while the
   document scrolls, and flush-left means the eye has one edge to return to.
   It keeps athar's own treatment inside — four named groups, every chapter
   numbered 01–12, a head that states the extent ("12 chapters"), the amber
   active row. The numbering is the point: it makes the SHAPE of the
   documentation legible at a glance (where you are, how much is behind you,
   how much is left) instead of presenting twelve equal-weight links.

   <900px the same list collapses into a single 52px disclosure bar that
   states the chapter you are on and its position ("03 / 12 · Self-hosting").
   The rail used to be 698px tall at 390px — identical to desktop — so a
   phone reader scrolled an entire desktop navigation rail before reaching a
   word of prose. Now prose starts immediately, and because the bar is sticky
   under the header the whole table of contents stays one tap away at any
   scroll position rather than only at the very top of the document.

   The <details> ships with `open` so that with JavaScript off the full list
   is simply present (docs.html needs JS to render a chapter at all, but a
   nav that vanishes without it would still be the wrong failure). docs.html
   closes it on narrow viewports at load, on chapter selection, and on
   crossing the breakpoint. */
.docs-rail{min-width:0}
.toc{border:1px solid var(--line);border-radius:var(--r);background:var(--panel)}
.toc-bar{
  display:none;align-items:center;gap:12px;padding:0 14px;min-height:52px;
  cursor:pointer;list-style:none;border-radius:calc(var(--r) - 1px);
}
.toc-bar::-webkit-details-marker{display:none}
.toc-bar .toc-txt{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1}
.toc-bar .toc-kicker{font-family:var(--mono);font-size:11px;letter-spacing:.13em;text-transform:uppercase;color:var(--t3);font-weight:600}
.toc-bar .toc-now{font-size:15px;font-weight:650;color:var(--t1);line-height:1.3;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* ── The disclosure indicator ────────────────────────────────────────────
   The chevron is the whole of this control's affordance, so it is drawn as a
   control: a 36px well with the same border and radius vocabulary as .iconbtn
   in the header, rather than an 18px hairline floating in the bar's right-hand
   whitespace. The glyph keeps its explicit `fill="none" stroke="currentColor"`
   presentation attributes (an SVG chevron that states neither fills black and
   renders as a solid triangle) and is stated at stroke-width 2 — the width
   every other line icon on this page uses; it was alone at 2.2. The rotation
   names its own origin: a bare `rotate(180deg)` inherits 50% 50% from the
   initial value today, but the moment anything sets transform-origin higher up
   the chevron pivots off-centre, and this is one declaration to prevent it. */
.toc-bar .toc-ind{
  display:grid;place-items:center;flex:none;width:36px;height:36px;
  border-radius:11px;border:1px solid var(--line);background:var(--bg-2);color:var(--t2);
  transition:color var(--quick) var(--ease),border-color var(--quick) var(--ease);
}
.toc-bar .toc-chev{
  width:19px;height:19px;flex:none;
  transform-origin:50% 50%;transition:transform var(--base) var(--ease);
}
.toc[open] .toc-bar .toc-chev{transform:rotate(180deg)}
.toc-bar:hover{background:var(--panel-2)}
.toc-bar:hover .toc-ind{color:var(--t1);border-color:var(--line-2)}
.rail-head{
  display:flex;align-items:baseline;justify-content:space-between;gap:10px;
  margin:0;padding:13px 14px;border-bottom:1px solid var(--line);
  font-family:var(--mono);font-size:11px;letter-spacing:.11em;text-transform:uppercase;color:var(--t3);font-weight:700;
}
.rail-head span{font-weight:600;letter-spacing:.06em;opacity:.85}
.docs-nav{padding:8px}
.docs-nav .grp{padding:14px 12px 4px;font-size:11.5px;text-transform:uppercase;letter-spacing:.09em;color:var(--t3);font-weight:800}
.docs-nav a{
  display:flex;align-items:center;gap:11px;padding:9px 12px;border-radius:8px;color:var(--t2);min-height:44px;
  font-size:14.5px;font-weight:550;transition:color var(--quick) var(--ease),background var(--quick) var(--ease);
}
.docs-nav a .ix{
  font-family:var(--mono);font-size:11px;font-weight:600;letter-spacing:.04em;
  color:var(--t3);flex:none;font-variant-numeric:tabular-nums;
}
.docs-nav a:hover{background:var(--panel-2);color:var(--t1)}
.docs-nav a:hover .ix{color:var(--t2)}
.docs-nav a.active{background:var(--amber-wash);color:var(--amber-ink);font-weight:650;box-shadow:inset 2px 0 0 0 var(--amber)}
.docs-nav a.active .ix{color:var(--amber-ink)}

@media(min-width:900px){
  /* Pinned, not sticky. `bottom:0` rather than a max-height keeps the rail
     exactly as tall as the screen below the header at every viewport height,
     so the list scrolls inside itself and the page never scrolls it away. */
  .docs-rail{
    position:fixed;left:0;top:var(--hdr-h);bottom:0;width:var(--rail-w);z-index:44;
    overflow-y:auto;overscroll-behavior:contain;
    background:var(--bg-2);border-right:1px solid var(--line);
    padding-bottom:32px;
    scrollbar-width:thin;scrollbar-color:var(--line-2) transparent;
  }
  .docs-rail::-webkit-scrollbar{width:9px}
  .docs-rail::-webkit-scrollbar-track{background:transparent}
  .docs-rail::-webkit-scrollbar-thumb{background:var(--line-2);border-radius:9px}
  /* Flush to an edge, the card treatment is wrong: no second border a pixel
     inside the rail's own, no radius against a straight viewport edge. */
  .toc{border:0;border-radius:0;background:transparent}
  .rail-head{position:sticky;top:0;z-index:1;background:var(--bg-2);padding:17px 22px 13px}
  .docs-nav{padding:10px 14px 0 16px}
}
@media(max-width:899.98px){
  .rail-head{display:none}
  .docs-rail{position:sticky;top:64px;z-index:40}
  /* OPAQUE. This bar was `color-mix(... 94%, transparent)` over a
     `backdrop-filter:blur(12px)`, and 6% of a blurred document is not a
     texture — at 390 and at 768, in both themes, whole words of prose were
     legible THROUGH the bar and straight through the chevron as the page
     scrolled under it. A sticky navigation bar has to be a surface. Dropping
     backdrop-filter also drops its side effect: an element with one becomes
     the containing block for fixed-position descendants. */
  .toc{background:var(--panel);box-shadow:0 10px 26px -18px rgba(0,0,0,.75)}
  .toc-bar{display:flex}
  /* Even opened, the list never becomes a 698px wall again: it is a panel
     with its own scroll, capped well short of the viewport. */
  .toc-nav{max-height:min(56vh,420px);overflow-y:auto;overscroll-behavior:contain;border-top:1px solid var(--line)}
  .docs-nav .grp{padding-top:12px}
}

.docs-main{min-width:0}
.docs-crumb{font-size:12.5px;color:var(--t3);margin-bottom:10px;font-weight:600;letter-spacing:.02em;display:flex;gap:8px;align-items:center}
/* A 44px hit area on a 20px row: the padding is cancelled by a matching
   negative vertical margin, so the crumb keeps its height and gains a target. */
.docs-crumb a{color:var(--t3);display:inline-flex;align-items:center;justify-content:center;min-width:44px;padding-block:12px;padding-inline:6px;margin-block:-12px;margin-inline:-6px}
.docs-crumb a:hover{color:var(--amber-ink)}
.docs-error{
  color:var(--err);background:var(--err-wash);border:1px solid var(--err-edge);
  padding:14px 16px;border-radius:var(--r-sm);
}
.docs-pager{display:flex;gap:14px;margin-top:52px;padding-top:24px;border-top:1px solid var(--line)}
.docs-pager a{
  flex:1;min-width:0;border:1px solid var(--line);border-radius:var(--r-sm);padding:14px 16px;
  transition:border-color var(--quick) var(--ease),background var(--quick) var(--ease);
}
.docs-pager a:hover{border-color:var(--amber-edge);background:var(--amber-wash)}
.docs-pager .to{font-size:11.5px;text-transform:uppercase;letter-spacing:.08em;color:var(--t3);display:block;margin-bottom:4px}
.docs-pager .nm{font-size:15px;font-weight:650;color:var(--t1)}
.docs-pager .next{text-align:right}
@media(max-width:560px){.docs-pager{flex-direction:column}.docs-pager .next{text-align:left}}

/* ── Markdown body ───────────────────────────────────────────────────────── */
.markdown{font-size:16.2px;min-width:0}
.markdown>*:first-child{margin-top:0}
.markdown h1,.markdown h2,.markdown h3,.markdown h4{font-family:var(--display);letter-spacing:-.02em;font-weight:650;line-height:1.15;scroll-margin-top:80px}
.markdown h1{font-size:clamp(28px,4.2vw,38px);margin:.2em 0 .5em;padding-bottom:.32em;border-bottom:1px solid var(--line)}
.markdown h2{font-size:clamp(21px,3vw,26px);margin:1.8em 0 .6em;padding-bottom:.26em;border-bottom:1px solid var(--line)}
.markdown h3{font-size:19px;margin:1.6em 0 .5em}
.markdown h4{font-size:16.2px;margin:1.3em 0 .4em}
.markdown p,.markdown li{color:var(--t2)}
.markdown strong,.markdown b{color:var(--t1);font-weight:700}
.markdown em{color:var(--t1)}
.markdown a{color:var(--amber-ink);font-weight:600;text-underline-offset:2px}
.markdown a:hover{text-decoration:underline}
.markdown code{
  font-family:var(--mono);font-size:.87em;background:var(--sunken);
  border:1px solid var(--line);padding:1px 5px;border-radius:5px;color:var(--t1);
}
/* Inline code is the one thing here that must never force a horizontal
   scroll: docs prose is full of long identifiers, DSNs and file paths, and
   at 390px an unbroken 40-character token inside a <p> would take the whole
   page with it. Code inside <pre> is excluded — that block already scrolls
   on its own axis, and wrapping it would corrupt the command it holds. */
.markdown :is(p,li,td,th,h1,h2,h3,h4,blockquote) code{overflow-wrap:anywhere;word-break:break-word}
.markdown ul,.markdown ol{padding-left:1.3em}
.markdown li+li{margin-top:.32em}
.markdown li::marker{color:var(--t3)}
.markdown blockquote{
  margin:1.2em 0;padding:.55em 1.1em;border-left:3px solid var(--amber);
  background:var(--amber-wash);color:var(--t2);border-radius:0 var(--r-sm) var(--r-sm) 0;
}
.markdown blockquote>*:first-child{margin-top:0}
.markdown blockquote>*:last-child{margin-bottom:0}
/* Tables wrap rather than pan. `white-space:nowrap` on every cell was what
   forced these into a horizontal scroll — the reader could reach the last
   column but had to drag to do it, and lost the first column on the way.
   Only the first column, which is the row's label, still holds its line. */
.markdown table{border-collapse:collapse;width:100%;margin:1.3em 0;font-size:14.3px}
.markdown .tblwrap{overflow-x:auto;margin:1.3em 0;border-radius:var(--r-sm);border:1px solid var(--line)}
.markdown .tblwrap table{margin:0;border:0}
.markdown th,.markdown td{border-bottom:1px solid var(--line);padding:9px 13px;text-align:left;color:var(--t2);white-space:normal;vertical-align:top}
.markdown td:first-child,.markdown th:first-child{padding-left:14px}
.markdown td:first-child{white-space:nowrap}
.markdown th{background:var(--panel-2);font-weight:700;color:var(--t1);position:sticky;top:0}
.markdown tr:last-child td{border-bottom:0}
/* Narrow: the row label can wrap too rather than set the table's floor. */
@media(max-width:620px){
  .markdown td:first-child{white-space:normal}
  .markdown th,.markdown td{padding:8px 10px;font-size:13.6px}
}
.markdown img{border-radius:10px;margin:1.2em 0;border:1px solid var(--line)}
.markdown hr{border:none;border-top:1px solid var(--line);margin:2.2em 0}

/* ── Code blocks ─────────────────────────────────────────────────────────
   TWO BEHAVIOURS, chosen per block from the code itself (see classifyCode()
   in docs.html), because "make it scroll" and "make it wrap" are each wrong
   for half of what is in these docs.

   SOFT-WRAP is the default. A shell command, a script tag, a JSON body or an
   nginx block carries no column alignment — nothing is lost by folding a long
   line, and at 390px folding is the difference between reading it and dragging
   it. Every source line is its own block element (span.cl, built by
   decorateCode) so each one can carry its OWN hanging indent: the fold is
   pushed 2.6ch right of the line it belongs to, which is what keeps a wrapped
   continuation visually subordinate instead of looking like the next command.
   A plain `text-indent:-2.6ch` on the <code> could not do this — text-indent
   applies to the first formatted line of a block only, so it would have
   indented the block once and shifted every real line the wrong way.

   HORIZONTAL SCROLL is kept for the blocks where alignment IS the content:
   the architecture box diagram, the aligned `=` of the identity derivation,
   the aligned `//` comments in the tracker API list. Folding those destroys
   the only thing they were laid out to show. Those get .is-wide, and with it
   a stated affordance — a "↔ scroll" chip in the chrome bar, a right-edge
   fade that disappears once you reach the end, and a scrollbar that is
   always visible rather than one that fades out and leaves no signal at all.

   The line split keeps the real "\n" between the spans as a text node, and
   the <code> is white-space:normal so those collapse away rather than
   printing blank lines. That is deliberate: code.textContent therefore still
   contains the original newlines, so the copy button and an ordinary text
   selection both still yield the real, unfolded source. */
.markdown .codeblock{
  background:#0C1116;border:1px solid var(--line-2);border-radius:12px;
  margin:1.3em 0;overflow:hidden;box-shadow:0 1px 0 0 var(--hair) inset;
}
.markdown .codeblock .cb-bar{
  display:flex;align-items:center;gap:10px;padding:4px 6px 4px 14px;
  border-bottom:1px solid rgba(255,255,255,.07);background:rgba(255,255,255,.02);
}
.markdown .codeblock .cb-lang{
  font-family:var(--mono);font-size:11px;letter-spacing:.09em;text-transform:uppercase;
  color:#8195AC;font-weight:600;
}
/* The affordance chip. Only ever present on a block that really does scroll,
   so it is a statement of fact rather than decoration. */
.markdown .codeblock .cb-pan{
  display:none;align-items:center;gap:5px;font-family:var(--mono);font-size:11px;
  letter-spacing:.07em;text-transform:uppercase;color:#93C5FD;font-weight:600;
  border:1px solid rgba(147,197,253,.30);background:rgba(147,197,253,.10);
  padding:2px 8px;border-radius:99px;
}
.markdown .codeblock.is-wide .cb-pan{display:inline-flex}
.markdown .codeblock .cb-copy{
  margin-left:auto;display:inline-flex;align-items:center;justify-content:center;gap:6px;
  min-height:44px;min-width:44px;padding:0 12px;border-radius:9px;
  border:1px solid rgba(255,255,255,.12);color:#C8D4E3;font-size:12px;font-weight:600;
  transition:background var(--quick) var(--ease),border-color var(--quick) var(--ease),color var(--quick) var(--ease);
}
.markdown .codeblock .cb-copy:hover{background:rgba(255,255,255,.08);border-color:rgba(255,255,255,.22)}
.markdown .codeblock .cb-copy svg{width:13px;height:13px;flex:none}
.markdown .codeblock .cb-copy.copied{color:#5EEAD4;border-color:rgba(94,234,212,.4)}
@media(max-width:420px){.markdown .codeblock .cb-copy span{display:none}.markdown .codeblock .cb-copy{padding:0}}

.markdown .cb-view{position:relative}
.markdown pre{margin:0;padding:16px 18px;overflow-x:hidden}
.markdown pre code{
  background:none;border:0;padding:0;font-family:var(--mono);font-size:13.4px;
  --cl-lh:1.72;line-height:var(--cl-lh);
  color:#C8D4E3;white-space:normal;tab-size:2;
}
/* One block per source line — see the note above. */
.markdown pre code .cl{
  display:block;min-height:calc(1em * var(--cl-lh));
  white-space:pre-wrap;overflow-wrap:anywhere;
  /* --hang is written per line by decorateCode: that line's own leading
     indent plus one 2.6ch step, so a fold lands one step INSIDE the line it
     continues rather than back at the left margin. */
  padding-left:var(--hang,2.6ch);text-indent:calc(-1 * var(--hang,2.6ch));
}
/* Alignment-critical: hold the line, scroll the box, and say so. */
.markdown .codeblock.is-wide pre{overflow-x:auto;overscroll-behavior-x:contain;padding-bottom:10px}
.markdown .codeblock.is-wide pre code .cl{white-space:pre;overflow-wrap:normal;padding-left:0;text-indent:0}
/* An overlay scrollbar that fades to nothing is not an affordance. Where the
   engine lets us, force a permanently drawn one; `scrollbar-width` is the
   Firefox path only, and is scoped away from Chromium because setting it
   there makes the engine ignore the ::-webkit-scrollbar rules entirely. */
@supports not selector(::-webkit-scrollbar){
  .markdown .codeblock.is-wide pre{scrollbar-width:thin;scrollbar-color:#4A5A6E transparent}
}
.markdown .codeblock.is-wide pre::-webkit-scrollbar{height:10px}
.markdown .codeblock.is-wide pre::-webkit-scrollbar-track{background:rgba(255,255,255,.04);border-radius:99px}
.markdown .codeblock.is-wide pre::-webkit-scrollbar-thumb{background:#4A5A6E;border-radius:99px;border:3px solid #0C1116}
.markdown .codeblock.is-wide .cb-view::after{
  content:"";position:absolute;top:0;right:0;bottom:0;width:54px;pointer-events:none;
  background:linear-gradient(to left,#0C1116 12%,rgba(12,17,22,0));
  opacity:1;transition:opacity var(--base) var(--ease);
}
/* Reached the right-hand end: there is nothing further to reveal, so the
   fade that promised more content has to stop promising it. */
.markdown .codeblock.is-wide.at-end .cb-view::after{opacity:0}

/* ═══ Shared code-token palette ═══════════════════════════════════════════
   One fixed palette across the Vulos suite, deliberately independent of the
   page theme — the code surface (#0C1116) stays dark whether the reader is
   in light or dark mode, the way a terminal window would.

   TEN ROLES, and every class the vendored build can emit is assigned to one
   of them. The vendored bundle is site/assets/vendor/highlight.min.js: the
   hljs core plus bash/go/sql/json/yaml/html+xml/javascript/nginx/ini/
   plaintext. Its complete token vocabulary — every `className` string in the
   bundle plus the two names the keyword machinery emits — is these 27:

     attr attribute built_in bullet class comment doctag function keyword
     literal meta meta-keyword meta-string name number operator params
     regexp section string subst symbol tag template-variable title type
     variable

   All 27 appear below. NOTHING falls through to the .hljs default: that
   colour is reached only by text highlight.js did not tokenise at all.
   Re-derive the list with:
     grep -oE '(className|scope):"[A-Za-z._-]+"' site/assets/vendor/highlight.min.js

   MEASURED against the fixed #0C1116 code surface (WCAG 2.1, sRGB):
     default     #C8D4E3  12.63:1      function    #7DD3FC  11.37:1
     comment     #6C7D94   4.51:1      property    #93C5FD  10.52:1
     keyword     #C4B5FD  10.27:1      variable    #FDA4AF  10.03:1
     string      #5EEAD4  12.82:1      meta        #FDBA74  11.24:1
     number      #FBBF24  11.36:1      punctuation #8195AC   6.17:1
   Every role clears 4.5:1 as normal-weight body text. The comment tone is
   the tight one at 4.51:1 and is the reason it is #6C7D94 rather than the
   #6B7C93 an earlier pass used, which measured 4.45:1 and failed. */
.hljs{color:#C8D4E3}
/* comment — also hljs's own doc-tag and quote scopes */
.hljs-comment,.hljs-quote,.hljs-doctag{color:#6C7D94;font-style:italic}
/* keyword — plus type names and the language constants. `literal` moved here
   from the number role deliberately: true/false/null/on/off are keywords of
   the language, not quantities, and sharing amber with `number` was costing
   JSON, YAML, ini and nginx blocks a whole colour they could have had. */
.hljs-keyword,.hljs-class,.hljs-type,.hljs-literal,.hljs-symbol{color:#C4B5FD}
/* string */
.hljs-string,.hljs-regexp,.hljs-meta-string{color:#5EEAD4}
/* number */
.hljs-number{color:#FBBF24}
/* function — callables and the shell built-ins */
.hljs-function,.hljs-title,.hljs-built_in,.hljs-section{color:#7DD3FC}
/* property — object keys, HTML/YAML/ini attributes, nginx directives */
.hljs-attr,.hljs-attribute,.hljs-property{color:#93C5FD}
/* variable — including a signature's parameter list */
.hljs-variable,.hljs-template-variable,.hljs-subst,.hljs-params{color:#FDA4AF}
/* meta — preprocessor/doctype lines and element names */
.hljs-meta,.hljs-meta-keyword,.hljs-name{color:#FDBA74}
/* punctuation — brackets, operators, list bullets */
.hljs-tag,.hljs-operator,.hljs-bullet,.hljs-punctuation{color:#8195AC}
.hljs-emphasis{font-style:italic}
.hljs-strong{font-weight:700}

/* ── Footer ──────────────────────────────────────────────────────────────
   `padding-block`, never the `padding` shorthand — .foot is also a .wrap,
   and the shorthand deleted the page gutter (see .docs-shell above). This
   page's footer had a second and worse version of the same fault: it carried
   no .wrap at all, so its one line sat hard against x=0 at every width. */
.site-footer{border-top:1px solid var(--line);margin-top:20px}
.foot{padding-block:30px 42px;display:flex;flex-wrap:wrap;gap:14px 20px;align-items:center;justify-content:space-between}
.foot .left{display:flex;align-items:center;gap:11px;color:var(--t3);font-size:14px;min-width:0}
.foot .left img{width:24px;height:24px;border-radius:7px}
.foot .links{display:flex;gap:2px 18px;flex-wrap:wrap;font-size:14px;color:var(--t2)}
.foot .links a{display:inline-flex;align-items:center;min-height:44px;padding:0 10px;margin:0 -10px;border-radius:8px}
.foot .links a:hover{color:var(--t1);background:var(--panel-2)}
/* No negative margin on .links itself: the anchors already carry
   `margin:0 -10px` to cancel their own hit-area padding, and stacking a
   second one on the container pushed the first link to x=-2 — the exact
   overflow shape the note on .foot .links a warns about. */
@media(max-width:560px){
  .foot{flex-direction:column;align-items:flex-start;gap:10px}
}


/* ── Vulos suite chrome — identical on every product landing ───────────────
   Two elements, and only two: a logo-only home button in the top bar, and
   one line in the footer. Deliberately free of repo variables so the block
   is byte-identical across the suite and can be diffed as one thing. */
.vulos-home { display: inline-flex; align-items: center; justify-content: center; width: 34px; height: 34px; border-radius: 9px; flex: 0 0 auto; opacity: .72; transition: opacity .15s ease, transform .15s ease; }
.vulos-home:hover, .vulos-home:focus-visible { opacity: 1; transform: translateY(-1px); }
.vulos-home img { display: block; width: 22px; height: 22px; border-radius: 6px; }
.vulos-foot { display: inline-flex; align-items: center; gap: 9px; font-size: 13.5px; line-height: 1.45; text-decoration: none; color: inherit; opacity: .85; transition: opacity .15s ease; }
.vulos-foot:hover { opacity: 1; text-decoration: none; }
.vulos-foot img { display: block; width: 18px; height: 18px; border-radius: 5px; flex: 0 0 auto; }

/* Dark-ground correction for the mark above. vulos-logo.png is flat #2E383A
   on transparency — ink drawn for paper — so composited on this surface it
   measures about 1.3:1 and reads as a smudge, not as a dimmed logo. The
   artwork carries its whole shape in the alpha channel, so brightness(0)
   invert(1) recolours it to the surface's foreground with the glyph intact,
   and the opacity holds it at the same secondary weight the light theme
   gives it. Styling only: the chrome markup stays byte-identical suite-wide. */
:root:not([data-theme="light"]) .vulos-home img,
:root:not([data-theme="light"]) .vulos-foot img { filter: brightness(0) invert(1); opacity: .66; }

/* Athar's tap-target floor, applied OUTSIDE the shared block above so that
   block stays byte-identical across the suite and can still be diffed as one
   thing. The shared home button is 34px and the shared footer line is 20px
   tall; this site holds standalone controls to 44px, so both are grown here.
   The footer line's extra height is cancelled by a matching negative margin,
   so it gains a target without moving the rule above it. */
.vulos-home{width:44px;height:44px;border-radius:11px}
.vulos-foot{min-height:44px;padding-block:12px;margin-block:-12px}
