/* ═══════════════════════════════════════════════════════════════════════════
   openrate — site stylesheet.

   Shared by index.html and docs.html so the marketing surface and the docs
   surface are the same object. Same art direction as the app itself
   ("Banknote Instrument", see web/src/styles.css): security-print engraving on
   an ink canvas, with a warm banknote-paper light theme.

   Type is Geist Sans (display + UI) and Geist Mono (every figure) — drawn for
   developer tools, with real tabular figures, rather than the editorial-serif
   register the site launched with. openrate is a rate *engine*, and the type
   should read like an instrument, not a magazine.

   Fonts are vendored in ./fonts — never fetched from Google Fonts. The site is
   served from the repo and from a static mirror, and neither should leak a
   reader's IP to a third party for the sake of a typeface.
   ═══════════════════════════════════════════════════════════════════════════ */

@font-face {
  font-family: 'Geist Sans'; font-style: normal; font-weight: 400; font-display: swap;
  src: url('./fonts/geist-sans-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+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Geist Sans'; font-style: normal; font-weight: 500; font-display: swap;
  src: url('./fonts/geist-sans-latin-500-normal.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Geist Sans'; font-style: normal; font-weight: 600; font-display: swap;
  src: url('./fonts/geist-sans-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+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Geist Sans'; font-style: normal; font-weight: 700; font-display: swap;
  src: url('./fonts/geist-sans-latin-700-normal.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Geist Mono'; font-style: normal; font-weight: 400; font-display: swap;
  src: url('./fonts/geist-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+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Geist Mono'; font-style: normal; font-weight: 500; font-display: swap;
  src: url('./fonts/geist-mono-latin-500-normal.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Geist Mono'; font-style: normal; font-weight: 600; font-display: swap;
  src: url('./fonts/geist-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+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}

/* Fraunces — the display face. One variable file per style (wght 100-900,
   plus the family's own optical-size axis baked into the "standard" build)
   stands in for what used to be four static weight files, so a whole
   register — headline, eyebrow-italic, receipt-numeral flourish — comes
   from two files instead of ten. Latin subset only: this site is English
   copy, and the vietnamese/latin-ext subsets Fontsource ships alongside it
   would be dead weight. See LICENSE-fraunces.txt (OFL-1.1, same licence as
   the Geist faces above). */
@font-face {
  font-family: 'Fraunces Variable'; font-style: normal; font-weight: 100 900; font-display: swap;
  src: url('./fonts/fraunces-variable-latin-standard-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+0304,U+0308,U+0329,U+2000-206F,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-variable-latin-standard-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+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}

:root {
  /* Ink is the default, and it has to be declared HERE and not only in the
     <meta name="color-scheme">. The meta says which schemes the page supports
     and lets the reader's machine pick — which is right until the reader pins
     a choice: with only the meta, a reader who chose ink on a light machine
     got the dark page inside a light scrollbar, light form controls and a
     light overscroll gutter. The property below follows data-theme instead,
     so the UA-painted furniture follows the page rather than the machine. */
  color-scheme: dark;
  --ink: #06090E; --ink-2: #0B0F16; --paper: #0E141D; --paper-2: #131A25; --paper-3: #1A2230;
  --line: #1B222D; --line-2: #252E3C; --line-3: #354152;
  /* --text-4 lightened from #616D7E — it isn't only a decorative label colour,
     it's also the seal-card explainer copy ("Fresh, directly quoted, and
     corroborated..."), the run-it step descriptions and the footer nav links,
     all real body-sized text. The original measured 3.52-3.80:1 against this
     theme's three page backgrounds, under the 4.5:1 AA text floor; this
     measures 4.53-4.89:1 against the same three. */
  --text: #ECEFF4; --text-2: #B8C1CE; --text-3: #8B96A6; --text-4: #717F92; --text-5: #44505F;
  --blue: #4C8DFF; --teal: #2DD4BF; --amber: #E0A63C; --red: #F2665C;
  --engrave: rgba(255,255,255,.028);
  --grain-o: .035;
  --shadow-1: 0 1px 0 rgba(255,255,255,.03) inset, 0 14px 34px -20px rgba(0,0,0,.9);
  --shadow-2: 0 1px 0 rgba(255,255,255,.04) inset, 0 44px 100px -44px rgba(0,0,0,.95), 0 12px 34px -22px rgba(0,0,0,.75);
  /* Fraunces for every headline — a banknote's serif, not a SaaS sans — Geist
     Sans stays for UI chrome and running copy (--ui, and --brand for the
     wordmark specifically, so the small header logotype doesn't inherit a
     display face tuned for 60px+), Geist Mono for every figure. Three faces,
     each doing one job, is the point: a rate engine's numbers should read
     like an instrument, its headlines like something printed and signed. */
  --display: 'Fraunces Variable', ui-serif, Georgia, 'Times New Roman', serif;
  --brand: 'Geist Sans', ui-sans-serif, system-ui, -apple-system, sans-serif;
  --ui: 'Geist Sans', ui-sans-serif, system-ui, -apple-system, sans-serif;
  --mono: 'Geist Mono', ui-monospace, 'SF Mono', monospace;
  --r-xs: 4px; --r-sm: 7px; --r: 11px; --r-lg: 16px; --r-xl: 22px;
  --max: 1180px; --gut: clamp(18px, 4vw, 40px);
  --ease: cubic-bezier(.22,.72,.18,1);
  --ease-out: cubic-bezier(.16,1,.3,1);

  /* ── code palette ─────────────────────────────────────────────────────
     Twelve roles, shared with the sibling Vulos sites so the family reads as
     one system. Deliberately OUTSIDE the [data-theme="light"] override below:
     a code block is a lit instrument panel, not a page surface, so it stays on
     the same dark glass whether the reader is on paper or on ink.

     Measured against --code-bg (#0C1116), sRGB relative luminance, WCAG 2.x:

       text        #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
       addition    #86EFAC  13.51:1      deletion    #F87171   6.86:1

     Every one clears the 4.5:1 AA floor for body text. --code-comment is the
     tight one at 4.51:1 — it is the family's value and it does pass, but it
     has almost no headroom, so it is used ONLY on --code-bg itself. The one
     place a comment-coloured token sits on a lighter surface (the .code bar's
     title, on --code-bg lifted 4% toward white) uses --code-punct instead,
     which measures 5.69:1 there where --code-comment would measure 4.17:1. */
  --code-bg: #0C1116;
  --code-text: #C8D4E3;
  --code-comment: #6C7D94;
  --code-keyword: #C4B5FD;
  --code-string: #5EEAD4;
  --code-number: #FBBF24;
  --code-function: #7DD3FC;
  --code-property: #93C5FD;
  --code-variable: #FDA4AF;
  --code-meta: #FDBA74;
  --code-punct: #8195AC;
  --code-addition: #86EFAC;
  --code-deletion: #F87171;
}

[data-theme="light"] {
  color-scheme: light;
  --ink: #F2EFE6; --ink-2: #EAE6DA; --paper: #FBF9F4; --paper-2: #FFFFFF; --paper-3: #F1EDE2;
  --line: #E0DACB; --line-2: #D2CAB6; --line-3: #B8AF98;
  /* --text-4 darkened from #7C8695 — see the matching note in :root above;
     the same real-content uses apply here. Original measured 2.95-3.50:1
     against this theme's three page backgrounds; this measures 4.51-5.35:1. */
  --text: #10151C; --text-2: #333B46; --text-3: #5A6472; --text-4: #5F6876; --text-5: #9AA3B0;
  /* teal/amber darkened from the original #0E8E7C/#A9741A — those read fine as
     decorative accents but are also the literal grade-A and grade-C seal
     letters and the colour of every inline <code> span, both real text a
     reader has to read. Against this theme's own backgrounds (paper/ink/ink-2)
     the originals measured 3.24-3.85:1, under the 4.5:1 WCAG AA text floor;
     these measure 4.51-5.39:1 against the same three, same hue, just darker.
     Red nudged the same way for the same reason (grade-D seal letter): the
     seal glyph actually sits on --paper tinted 11% with its own colour (see
     .seal below), and against THAT composited backdrop the original
     #C33A31 measured 4.28:1 — just under the floor, and under by more than a
     flat-paper estimate would suggest, because the self-tint pulls the
     backdrop toward the glyph's own hue. This measures 4.51:1 against the
     same composited backdrop. */
  --blue: #1F5FD8; --teal: #0B7465; --amber: #8A5F15; --red: #BD382F;
  --engrave: rgba(24,32,22,.035);
  --grain-o: .05;
  --shadow-1: 0 1px 0 rgba(255,255,255,.7) inset, 0 12px 28px -20px rgba(60,50,30,.5);
  --shadow-2: 0 1px 0 rgba(255,255,255,.8) inset, 0 34px 80px -42px rgba(60,50,30,.45), 0 10px 28px -20px rgba(60,50,30,.35);
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; overflow-x: clip; scroll-padding-top: 84px; -webkit-text-size-adjust: 100%; }
body {
  position: relative; min-height: 100dvh;
  background: var(--ink); color: var(--text);
  font-family: var(--ui); font-size: 16px; line-height: 1.65;
  -webkit-font-smoothing: antialiased; overflow-x: clip;
  transition: background .45s var(--ease), color .45s var(--ease);
}
body::before {
  content: ""; position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background:
    repeating-linear-gradient(90deg, var(--engrave) 0 1px, transparent 1px 88px),
    repeating-linear-gradient(0deg, var(--engrave) 0 1px, transparent 1px 88px);
  mask-image: radial-gradient(ellipse 120% 90% at 50% 0%, #000 0%, transparent 78%);
}
body::after {
  content: ""; position: fixed; inset: 0; z-index: 1; pointer-events: none;
  opacity: var(--grain-o); mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='.55'/%3E%3C/svg%3E");
}
a { color: inherit; text-decoration: none; }
/* height:auto is not optional next to max-width:100%. The shots carry explicit
   width/height attributes (so the layout reserves the right box before they
   load, and nothing shifts), but without this the attribute height is applied
   literally while max-width shrinks the width — every screenshot renders
   stretched vertically. */
img, svg { display: block; max-width: 100%; height: auto; }
button { font: inherit; color: inherit; }
::selection { background: color-mix(in srgb, var(--blue) 30%, transparent); }
:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; border-radius: var(--r-xs); }

.page { position: relative; z-index: 2; }
.wrap { max-width: var(--max); margin: 0 auto; padding: 0 var(--gut); }

/* ── type ─────────────────────────────────────────────────────────────── */
.lbl {
  font-family: var(--mono); font-size: 12px; font-weight: 500;
  letter-spacing: .13em; text-transform: uppercase; color: var(--text-4); line-height: 1;
}
.eyebrow { display: inline-flex; align-items: center; gap: 10px; }
.eyebrow::before { content: ""; width: 5px; height: 5px; background: var(--teal); transform: rotate(45deg); flex: none; }
.eyebrow::after { content: ""; width: clamp(20px, 5vw, 54px); height: 1px; background: linear-gradient(90deg, var(--line-2), transparent); }

/* Fraunces carries its own optical-size axis (font-optical-sizing: auto is
   the CSS default and needs no extra rule) — the same variable file draws a
   sharper, higher-contrast cut at 80px than it does at 15px, the way metal
   type once needed a different punch cut per size. Weight sits at 460-560
   rather than a heavier "headline bold": Fraunces' contrast does the work a
   heavier weight would elsewhere, and going heavier here reads closer to
   Bricolage than to a banknote. */
h1, h2, h3 { font-family: var(--display); letter-spacing: -.012em; line-height: 1.04; }
h1 { font-size: clamp(2.9rem, 7.4vw, 5.4rem); line-height: .98; font-weight: 480; letter-spacing: -.018em; }
h2 { font-size: clamp(2rem, 4.3vw, 3rem); font-weight: 480; letter-spacing: -.014em; }
h3 { font-family: var(--ui); font-size: clamp(1.2rem, 2.1vw, 1.48rem); font-weight: 600; letter-spacing: -.012em; }
/* em carries the second colour AND the second face — italic Fraunces at a
   register between the roman headline and body text is the "signed by hand"
   beat this page uses to mark the one clause per headline worth lingering
   on. */
em { font-family: var(--display); font-style: italic; color: var(--teal); font-weight: 460; }
p { color: var(--text-3); }
.lede { font-size: 1.09rem; line-height: 1.75; max-width: 62ch; color: var(--text-3); }
.lede b, p b { color: var(--text-2); font-weight: 600; }
.num { font-family: var(--mono); font-variant-numeric: tabular-nums; }
/* Relative sizing is right — inline code should track the text it sits in —
   but .88em of an already-small parent goes under the page's 11px floor
   (`corroboration` inside a 11.5px caption measured 10.1px). Same floor and
   same reasoning as .md code, which already carries one. */
code { font-family: var(--mono); font-size: max(11px, .88em); color: var(--teal); }

/* ── guilloché ────────────────────────────────────────────────────────── */
.guilloche { position: absolute; pointer-events: none; z-index: 0; color: var(--blue); }
.guilloche svg { width: 100%; height: 100%; overflow: visible; }
.guilloche .rose { animation: spin 150s linear infinite; transform-origin: 50% 50%; }
.guilloche .rose.rev { animation-duration: 210s; animation-direction: reverse; }
@keyframes spin { to { transform: rotate(360deg); } }

/* ── header ───────────────────────────────────────────────────────────── */
.hdr {
  position: sticky; top: 0; z-index: 60; height: 66px;
  display: flex; align-items: center; gap: 14px;
  padding: 0 var(--gut);
  background: color-mix(in srgb, var(--ink) 82%, transparent);
  backdrop-filter: blur(16px) saturate(150%);
  border-bottom: 1px solid var(--line);
}
/* min-height, not just the glyph height: the wordmark is a real navigation
   target and was measuring 26px against the 44px tap-target floor. */
.brand { display: flex; align-items: center; gap: 10px; flex: none; min-height: 44px; }
.brand svg { width: 26px; height: 26px; transition: transform .5s var(--ease); }
.brand:hover svg { transform: rotate(180deg); }
.brand .word { font-family: var(--brand); font-size: 21px; font-weight: 600; letter-spacing: -.02em; line-height: 1; }
.brand .word i { font-style: normal; color: var(--teal); }
.hdr .sp { flex: 1; }
.hdr nav { display: flex; gap: 4px; }
.hdr nav a {
  display: inline-flex; align-items: center; min-height: 44px;  /* was 9px padding = 40.3px, under the tap-target floor */
  padding: 0 12px; font-size: 13.5px; font-weight: 500; color: var(--text-3);
  transition: color .2s var(--ease);
}
.hdr nav a:hover, .hdr nav a.on { color: var(--text); }
/* The header is 82% paper over a 16px blur, so what is behind it is part of
   what the nav is read against — and the code panels are FIXED dark in both
   themes (see the code palette in :root). Measured on the composited pixels
   rather than on the token: with a code panel scrolled under the glass the
   backdrop lands on rgb(200,199,194), and --text-3 on that is 3.54:1 — under
   the 4.5:1 AA floor, on seven real navigation links. Against flat --paper the
   same token measures 5.2:1 and looks fine, which is exactly why this had to
   be measured composited. --text-2 on the same backdrop measures 6.62:1, and
   6.99:1 on flat paper. Only the light theme needs it: on ink the panels are
   the same colour as everything else behind the glass, and --text-3 there
   measures 5.9:1 composited. */
[data-theme="light"] .hdr nav a { color: var(--text-2); }
@media (max-width: 860px) { .hdr nav { display: none; } }
/* 320px, measured in a browser rather than guessed at: the bar's own contents
   — the wordmark plus three 44px controls — came to 330px inside a 320px
   viewport on both pages, so the header, and only the header, was pushing the
   document 10px over. Neither of the obvious fixes is available: 44px is the
   ratified tap-target floor and the Vulos mark is ratified suite chrome, so
   nothing here may shrink below 44 and nothing may be dropped. What can give
   is the space between them and the size of the wordmark, which is set at
   21px for a 1440px header and does not need to be at 320.

   `overflow-x: clip` on body means this never showed up as a scrollbar, and it
   would not show up in a scrollWidth check on <html> either — the same trap a
   sibling landing hid a doubled-over viewport behind. It was found by walking
   every element's bounding box against the viewport, which is what actually
   answers the question. */
@media (max-width: 380px) {
  .hdr { gap: 8px; }
  .brand { gap: 7px; }
  .brand svg { width: 23px; height: 23px; }
  .brand .word { font-size: 18px; }
}

/* ── the header's three utility controls ──────────────────────────────────
   They used to be three different objects standing shoulder to shoulder: the
   Vulos mark a bare image at 72% opacity, GitHub a bare glyph, and the theme
   switch a bordered circle. Three treatments for three adjacent controls —
   and the first of them all but invisible, because the suite mark is a single
   flat ink colour drawn for paper and at .72 on #06090E it measured 1.6:1.

   One object now: a single hairline-ruled group of three 44px cells, divided
   by the same hairline the stat rail and the instrument cells are divided by.
   It reads as one cluster of utilities rather than three strays, it separates
   them from the seven text links beside them, and it is narrower than three
   free-standing controls were — which matters at 320, where this bar is the
   only thing on either page that has ever pushed the document over. */
.tools {
  display: flex; align-items: center; flex: none;
  border: 1px solid var(--line-2); border-radius: 100px; overflow: hidden;
  background: color-mix(in srgb, var(--paper) 55%, transparent);
}
.tools > * {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; flex: none;  /* was 34px — under the 44px tap-target floor */
  border: 0; background: transparent; color: var(--text-3); cursor: pointer;
  transition: color .2s var(--ease), background .2s var(--ease);
}
.tools > * + * { border-left: 1px solid var(--line-2); }
/* The theme button names the theme ONE CLICK AWAY: a sun on ink, a moon on
   paper. Both glyphs ship in the markup and CSS picks between them, rather
   than site.js writing the right one in — site.js is deferred, so it runs
   AFTER the first paint, and a reader whose machine asked for paper would
   watch the button start on the wrong glyph and correct itself. The head
   script settles data-theme before anything is painted, so this selector is
   already true by then. */
[data-theme-toggle] .ico-moon { display: none; }
[data-theme="light"] [data-theme-toggle] .ico-sun { display: none; }
[data-theme="light"] [data-theme-toggle] .ico-moon { display: block; }
.tools > *:hover, .tools > *:focus-visible { color: var(--text); background: var(--paper-3); }
/* The shared .vulos-home block below sets its own box, opacity and hover
   lift; inside the group those are the group's job, so they are overridden
   here by specificity rather than by being edited out of the block that has
   to stay identical across the suite. */
.tools .vulos-home { width: 44px; height: 44px; border-radius: 0; opacity: 1; }
.tools .vulos-home:hover, .tools .vulos-home:focus-visible { transform: none; }
.tools .vulos-home img { width: 18px; height: 18px; border-radius: 0; }
/* On ink the mark has to be the surface's foreground, not its own paper ink —
   the same treatment patala's landing already gives it. brightness(0) flattens
   the glyph to black and invert(1) lifts it to white; the shape is untouched,
   and the opacity lands it at the same weight as the two glyphs beside it. */
:root:not([data-theme="light"]) .tools .vulos-home img,
:root:not([data-theme="light"]) .vulos-foot img { filter: brightness(0) invert(1); opacity: .66; }
:root:not([data-theme="light"]) .tools .vulos-home:hover img,
:root:not([data-theme="light"]) .vulos-foot:hover img { opacity: .92; }

.btn {
  display: inline-flex; align-items: center; gap: 9px;
  padding: 12px 20px; min-height: 44px; border-radius: 100px;  /* min-height keeps .btn.sm on the tap-target floor too */
  font-size: 14.5px; font-weight: 600;
  border: 1px solid transparent; cursor: pointer;
  transition: transform .18s var(--ease), box-shadow .22s var(--ease), border-color .2s var(--ease), color .2s var(--ease);
}
.btn:hover { transform: translateY(-1px); }
.btn-solid { background: var(--text); color: var(--ink); box-shadow: 0 10px 26px -14px rgba(0,0,0,.85); }
.btn-solid:hover { box-shadow: 0 16px 34px -14px rgba(0,0,0,.9); }
.btn-ghost { border-color: var(--line-2); color: var(--text-2); }
.code pre { margin: 0; padding: 16px 18px; overflow-x: auto; font-family: var(--mono); font-size: 12.5px; line-height: 1.8; color: var(--code-text); }
/* The shell prompt is chrome, not content — it used to take --code-string,
   which is the colour of every quoted argument on the same line. */
.code .prompt { color: var(--code-punct); user-select: none; }

/* ── how code behaves when the column is narrower than the line ───────────
   Two behaviours, chosen per block by what the block IS, rather than one
   compromise for both:

   • A block in a real language soft-wraps. site.js re-wraps every highlighted
     block so each source line is its own <span class="cl"> block box, which is
     what makes a HANGING INDENT possible: a continuation is set 2.6ch in from
     its own line's start, so a wrap is never mistaken for a newline and the
     leading whitespace that carries a JSON or Go block's structure still lines
     up. (text-indent on the <pre> itself cannot do this — a <pre> is one block
     box, so it would indent the first line of the whole listing and nothing
     else.) Nothing scrolls; the reader reads it.

   • A block whose columns ARE its meaning — the unfenced plaintext listings,
     where the second column starts at a fixed offset on every row — keeps its
     lines intact and scrolls horizontally on purpose, with the affordance
     below so the scroll is visible rather than discovered.

   The line boxes exist in both cases so a block can switch between them
   without re-rendering, and so `.cl` numbering/striping stays available. */
.cl { display: block; }
pre { tab-size: 4; }   /* site.js counts a tab as four columns; keep them agreed */
pre.cb-wrap code, .code pre code { white-space: pre-wrap; overflow-wrap: break-word; }
/* --in is the line's own leading whitespace, published by site.js. The pair
   cancels on the first line — which therefore starts exactly where its own
   indentation puts it — and leaves every continuation 2.6ch inside that. */
/* The indent is capped at 30% of the column. Tracking the source indentation
   exactly is right until the indentation is most of the column: at 320 the C
   example's deepest line reserved 215px of a 250px box and wrapped the string
   literal into a 36px ribbon four characters wide. The cap is a proportion
   rather than a breakpoint so it only engages where it is actually needed —
   at 1440 it sits around 36ch and nothing in these docs reaches it. */
pre.cb-wrap .cl, .code pre .cl {
  --in-capped: min(var(--in, 0ch), 30%);
  padding-left: calc(var(--in-capped) + 2.6ch);
  text-indent: calc(-1 * (var(--in-capped) + 2.6ch));
}
/* A blank line is an empty block box, which would otherwise collapse to zero
   height and close up the paragraphing the author put in the listing. 1lh is
   exactly one line box; the em value is the fallback for engines without it. */
.cl:empty { min-height: 1.8em; min-height: 1lh; }
pre.cb-wrap { overflow-x: hidden; }

/* Deliberate horizontal scroll, made visible. Two layers of background:
   the `local` gradients scroll with the content and cover the ends, the
   `scroll` shadows are pinned to the box — so a shadow only shows on a side
   there is genuinely more content on. No JavaScript and no scroll listener.
   Paired with a scrollbar that is always drawn, because the platform default
   on macOS is an overlay bar that appears only once you are already
   scrolling — which is exactly the reader who has not realised they can. */
.or-scroll {
  --edge-face: var(--code-bg);
  --edge-mark: rgba(148,178,214,.34);
  /* The stack is a custom property so a more specific rule elsewhere can
     apply the same affordance without restating four gradients. */
  --edge:
    linear-gradient(90deg, var(--edge-face) 42%, transparent) 0 0 / 40px 100% no-repeat local,
    linear-gradient(270deg, var(--edge-face) 42%, transparent) 100% 0 / 40px 100% no-repeat local,
    linear-gradient(90deg, var(--edge-mark), transparent) 0 0 / 22px 100% no-repeat scroll,
    linear-gradient(270deg, var(--edge-mark), transparent) 100% 0 / 22px 100% no-repeat scroll,
    var(--edge-face);
  overflow-x: auto;
  background: var(--edge);
}
/* No scrollbar-width / scrollbar-color here on purpose. Setting either of the
   standard properties makes Chromium ignore the ::-webkit-scrollbar rules
   below and fall back to the platform's overlay scrollbar — which on macOS is
   drawn only while you are already scrolling, i.e. never for the reader who
   has not yet realised there is anything to scroll. Dropping them gets a real
   9px bar that takes layout space and is always on screen. Firefox has no
   overlay default on desktop and draws its own either way. */
.or-scroll::-webkit-scrollbar { height: 10px; }
.or-scroll::-webkit-scrollbar-track { background: color-mix(in srgb, var(--edge-face) 84%, #8FA3BC 16%); border-radius: 10px; }
.or-scroll::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--edge-face) 30%, #8FA3BC 70%); border-radius: 10px; }
.or-scroll::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--edge-face) 10%, #A9BCD3 90%); }
/* The same affordance over a page surface rather than the code surface. */
.or-scroll.on-page { --edge-face: var(--ink); --edge-mark: color-mix(in srgb, var(--line-3) 92%, transparent); }

/* ── landing-page components ──────────────────────────────────────────────
   The hero, stat rail, screenshots, split rows, seals, code plates, steps,
   endpoint table and every hand-drawn illustration used to live here. They
   are index.html's alone, docs.html never referenced one of them, and the
   landing page was redesigned onto a different architecture — so they now
   live in ./landing.css, which only index.html loads. What stays in this
   file is strictly what the two surfaces share: fonts, tokens, the base
   reset, type, the guilloché, the header, buttons, the hljs mapping, the
   footer, the docs viewer and the reveal. */

/* ── syntax highlighting (hljs) ───────────────────────────────────────────
   One mapping, shared by the landing's terminal/response plates (.code pre)
   and the docs viewer's rendered markdown (.md pre) — both sit on the same
   fixed --code-bg.

   The vendored bundle (assets/vendor/highlight.min.js) registers exactly four
   grammars — bash/shell/sh, json, go/golang, plaintext — and the classes it
   can emit were dumped by highlighting every fenced block in every doc page
   and both landing plates, plus synthetic snippets driving the grammar paths
   the corpus does not reach. That vocabulary is:

     punctuation attr string number keyword comment built_in literal
     type variable function params meta title

   `hljs-function` is the one that used to fall through: the bundled Go grammar
   still wraps a whole `func Name(args) ret` construct in it (the pre-scope
   className form), nothing in this file matched, and its own text — the
   parens and separators between the highlighted children — painted as plain
   --code-text. It is mapped with the other delimiters below.

   Two more were collisions rather than gaps. `hljs-literal` shared amber with
   `hljs-number`, so JSON's true/false/null and Go's nil/iota were indistinct
   from numerals; they are language constants, so they now take the variable
   role, which is where hljs's own `variable.constant_` lives. `hljs-type`
   shared orange with `hljs-meta`, so Go's `string`/`int` read as preprocessor
   directives; it now takes the property blue and --code-meta is left for real
   meta (a shebang, a build tag).

   Classes beyond that vocabulary are still mapped, so a grammar added to the
   bundle later never silently falls through to unstyled default text. */
.code pre, .md pre { background: var(--code-bg); }
.code code, .md pre code { color: var(--code-text); }
.hljs-comment, .hljs-quote { color: var(--code-comment); font-style: italic; }
.hljs-keyword, .hljs-selector-tag, .hljs-subst, .hljs-doctag, .hljs-formula, .hljs-template-tag { color: var(--code-keyword); }
.hljs-string, .hljs-regexp, .hljs-char.escape_ { color: var(--code-string); }
.hljs-number, .hljs-symbol { color: var(--code-number); }
.hljs-title, .hljs-title.function_, .hljs-title.class_, .hljs-section, .hljs-name, .hljs-built_in { color: var(--code-function); }
.hljs-attr, .hljs-attribute, .hljs-property, .hljs-type, .hljs-selector-class, .hljs-selector-id { color: var(--code-property); }
.hljs-variable, .hljs-template-variable, .hljs-params, .hljs-literal, .hljs-variable.language_, .hljs-variable.constant_ { color: var(--code-variable); }
.hljs-meta, .hljs-meta .hljs-keyword, .hljs-meta .hljs-string, .hljs-tag, .hljs-selector-pseudo, .hljs-bullet, .hljs-link { color: var(--code-meta); }
.hljs-punctuation, .hljs-operator, .hljs-function { color: var(--code-punct); }
/* Semantic by definition — a diff's two colours are the information. The
   bundle ships no diff grammar, so nothing emits these today; they are the
   last two of the family's twelve roles and are declared so the mapping is
   complete rather than nearly complete. */
.hljs-addition { color: var(--code-addition); }
.hljs-deletion { color: var(--code-deletion); }

/* ── footer ───────────────────────────────────────────────────────────── */
.ftr { border-top: 1px solid var(--line); background: var(--ink-2); position: relative; overflow: clip; }
.ftr .guilloche { bottom: -320px; left: -180px; width: 620px; height: 620px; opacity: .2; }
.ftr-in { position: relative; z-index: 1; max-width: var(--max); margin: 0 auto; padding: clamp(48px,6vw,76px) var(--gut) 28px; }
.ftr-grid { display: grid; grid-template-columns: 2.2fr 1fr 1fr 1fr; gap: clamp(24px,4vw,52px); padding-bottom: clamp(32px,4vw,52px); border-bottom: 1px solid var(--line); }
.ftr-grid p { font-size: 13.5px; line-height: 1.7; max-width: 40ch; margin: 14px 0 18px; color: var(--text-4); }
.ftr-col ul { list-style: none; display: flex; flex-direction: column; gap: 10px; margin-top: 15px; }
.ftr-col a { font-size: 13px; color: var(--text-4); transition: color .15s var(--ease); }
.ftr-col a:hover { color: var(--teal); }
.tag {
  display: inline-flex; align-items: center; gap: 8px; min-height: 44px;  /* was 36.6px */
  border: 1px solid var(--line-2); border-radius: 100px; padding: 7px 16px;
  font-size: 12.5px; color: var(--text-3);
  transition: border-color .18s var(--ease), color .18s var(--ease);
}
.tag:hover { border-color: var(--line-3); color: var(--text); }
.ftr-btm { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 14px; padding-top: 26px; }
.ftr-btm span { font-family: var(--mono); font-size: 12px; color: var(--text-4); }  /* --text-5 measured 2.0-2.3:1 here; it is a rule colour, not a text colour */
.ftr-btm a { color: var(--text-4); text-decoration: underline; text-underline-offset: 3px; }
@media (max-width: 760px) { .ftr-grid { grid-template-columns: 1fr 1fr; } .ftr-brand { grid-column: 1 / -1; } }
@media (max-width: 480px) { .ftr-grid { grid-template-columns: 1fr; } }

/* ═══ docs ═════════════════════════════════════════════════════════════ */
/* ── the shell, and why every track is filled ─────────────────────────────
   This grid used to be `256px minmax(0, 1fr)` with `.docs-main` capped at
   `max-width: 920px`. At 1440 that resolved to tracks of 256px and 1184px
   with the main column occupying 920 of the second one — leaving 264px of
   reserved, permanently empty track down the right-hand side. It read as a
   deliberate side panel that had failed to render, because that is exactly
   what a column-shaped hole looks like.

   The rule now is that the grid decides the widths and nothing inside it
   holds back: there is no track that something does not fill.

     ≥1380   three tracks — index, document, and the on-this-page rail that
             now occupies what the hole used to be. The rail is bounded by a
             clamp rather than a fraction, so it cannot itself become the
             next too-wide-and-mostly-empty column on a very large display;
             the document track takes every pixel the rail does not.
     ≤1379   two tracks. The rail cannot earn its keep under ~230px, so it is
             not drawn, and the document track takes the whole remainder
             instead of leaving a smaller version of the same hole.
     ≤980    one track, and the index becomes the disclosure bar (see below).

   1380 is where the third track starts paying: 256 index + 920 document
   (the width overview.md's repository map needs, see .docs-main) + 232 rail
   at the clamp's floor. At 1440 the clamp lands on 259px and the document
   track gets 925 — still wider than the 920 the map needs, so the one
   listing on the site that scrolls still does not have to at desktop width.

   .no-rail is the guard for a page too short to justify a rail: it drops the
   grid back to two tracks rather than leaving an empty third. Every page in
   the corpus carries at least four headings, so it does not fire today — but
   an empty track is the exact defect this block exists to remove, and it
   should not be able to come back by way of a new page. */
.docs {
  display: grid;
  grid-template-columns: 256px minmax(0, 1fr) clamp(232px, 18vw, 300px);
  align-items: start;
}
.docs.no-rail { grid-template-columns: 256px minmax(0, 1fr); }
@media (max-width: 1379.98px) {
  .docs { grid-template-columns: 256px minmax(0, 1fr); }
  .docs-rail { display: none; }
}
/* Pinned to the far left of the viewport and fixed in place while the
   document scrolls — the shared spec the sibling docs sites are being brought
   onto. It is the first track of a grid that starts at the page edge, so its
   left edge IS the viewport's; sticky under the 66px header holds it there.

   `height`, not `max-height`, and that is the difference between a rail that
   is pinned and one that merely starts at zero. Sized to its content, the
   border-right stopped a few hundred pixels down — a stub of a rule beside a
   document that then scrolled past it, which reads as a box that happens to
   be at the top left rather than as an edge the page is anchored to. Given
   the full height below the header the rule runs the whole viewport, the
   separation is unambiguous, and the panel visibly stays while the text
   moves. overflow-y still earns its keep: nine titles plus the search box
   outgrow a short window. */
.docs-side {
  position: sticky; top: 66px; align-self: start; z-index: 40;
  height: calc(100dvh - 66px); overflow-y: auto;
  padding: 30px 20px 40px var(--gut);
  border-right: 1px solid var(--line);
}
.docs-side::-webkit-scrollbar { width: 7px; }
.docs-side::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 7px; }
.toc-panel { display: flex; flex-direction: column; gap: 4px; }
.toc-panel > .lbl { margin-bottom: 8px; }
/* The desktop rail's disclosure button. Hidden here, and the whole reason the
   panel is a separate element: below 980px this button IS the navigation and
   the panel drops out of flow behind it. */
.toc-btn { display: none; }
.docs-side nav { display: flex; flex-direction: column; gap: 2px; }

/* ── the index's groups ───────────────────────────────────────────────────
   Fourteen pages in one flat column is a wall, not an index: nothing tells a
   reader that "Zero network" belongs beside "Go library" rather than beside
   "Interest rates", and there is no way to put away a part of the product you
   are not using. So the pages sit in five groups, each a real <section> with a
   real <button>, and the button collapses it.

   The heading is a button and not a bare label, which means it has to look
   like neither a page link nor a page title. It takes the same mono/uppercase
   treatment .lbl uses everywhere else on the site, sits at --text-4 rather
   than --text-3 so the page titles under it stay the brighter thing, and only
   the chevron moves on hover — a group header that highlighted like a link
   would read as a fifteenth page. */
.nav-group + .nav-group { margin-top: 10px; }
.nav-grp {
  display: flex; align-items: center; gap: 8px; width: 100%;
  min-height: 34px; padding: 4px 12px 4px 12px; margin: 0;
  background: none; border: none; cursor: pointer; text-align: left;
  font-family: var(--mono); font-size: 11px; font-weight: 500;
  letter-spacing: .13em; text-transform: uppercase; color: var(--text-4);
}
.nav-grp span { flex: 1 1 auto; min-width: 0; }
.nav-grp .gc {
  flex: none; width: 13px; height: 13px; opacity: .7;
  transition: transform .2s var(--ease), opacity .15s var(--ease);
}
.nav-grp:hover .gc { opacity: 1; }
.nav-grp[aria-expanded="false"] .gc { transform: rotate(-90deg); }
.nav-items { display: flex; flex-direction: column; gap: 2px; }
.nav-group.collapsed .nav-items { display: none; }

/* ── in-page headings, nested under the page they belong to ───────────────
   The on-this-page rail owns this job above 1380px, in its own column. Below
   that the rail is not drawn (see the .docs grid), and until now that left a
   phone, a tablet and most laptop windows with no way to see the shape of a
   document or move within it. This is the same list, in the one column that
   always exists, and the media query below is deliberately the SAME breakpoint
   as the rail's so the two can never both be on screen.

   Indented from a rule rather than from padding alone: the list has to read as
   belonging to the link above it, and at this width a bare indent looks like a
   misaligned sibling.

   Every selector here carries `.docs-side` for specificity and not for style:
   `.docs-side nav a` above sets a 44px floor, a radius and --text-3 on every
   anchor in this rail, and a bare `.nav-heads a` loses to it. */
.docs-side .nav-heads {
  list-style: none; margin: 2px 0 4px 22px; padding: 0;
  border-left: 1px solid var(--line-2);
}
.docs-side .nav-heads li { margin: 0; padding: 0; }
.docs-side .nav-heads a {
  display: flex; align-items: center; min-height: 40px;
  padding: 6px 10px 6px 12px; margin-left: -1px;
  border-left: 2px solid transparent; border-radius: 0;
  font-size: 12.5px; line-height: 1.45; color: var(--text-4);
  transition: color .15s var(--ease), border-color .15s var(--ease);
}
.docs-side .nav-heads a:hover { color: var(--text-2); background: none; }
.docs-side .nav-heads a.here { color: var(--teal); border-left-color: var(--teal); }
@media (min-width: 1380px) { .docs-side .nav-heads { display: none; } }
/* `.docs-side nav a`, not `.docs-side a` — the search results are anchors in
   the same aside, and a display:flex meant for a one-line nav row laid their
   title and their excerpt side by side instead of stacked. */
.docs-side nav a {
  display: flex; align-items: center; min-height: 44px;  /* was 7px padding = 36.3px, under the tap-target floor */
  padding: 7px 12px; font-size: 13.5px; color: var(--text-3);
  border-left: 2px solid transparent; border-radius: 0 var(--r-xs) var(--r-xs) 0;
  transition: color .15s var(--ease), border-color .15s var(--ease), background .15s var(--ease);
}
.docs-side nav a:hover { color: var(--text); background: var(--paper-3); }
/* 5%, not 7%: at 7% the tint pulled the row ground to rgb(226,230,221) in
   light theme and --teal measured 4.485:1 on it — under the floor, and
   invisible to a token audit because the token is fine and the row it sits
   on is what moved. 5% measures 4.61:1 and is not tellable apart. */
.docs-side nav a.active { color: var(--teal); border-left-color: var(--teal); background: color-mix(in srgb, var(--teal) 5%, transparent); }

.docs-search {
  width: 100%; margin-bottom: 12px; min-height: 44px;  /* was 35px */
  background: var(--ink-2); border: 1px solid var(--line); border-radius: 100px;
  padding: 8px 16px; color: var(--text); font-family: var(--mono); font-size: 13px;
}
.docs-search::placeholder { color: var(--text-4); }
.docs-search:focus { outline: none; border-color: color-mix(in srgb, var(--blue) 50%, transparent); }
.hits { display: flex; flex-direction: column; gap: 2px; margin-bottom: 10px; }
.hit { display: block; min-height: 44px; padding: 9px 12px; border-radius: var(--r-sm); background: var(--paper-3); }
/* .on is the KEYBOARD cursor, moved with the arrow keys while focus stays in
   the search box (role=combobox + aria-activedescendant). It has to be visible
   as strongly as hover, because it is the only thing on screen telling a
   keyboard reader which result Enter will open. */
.hit:hover, .hit.on { background: var(--paper-2); }
.hit.on { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--blue) 55%, transparent); }
.hit b { display: block; font-size: 12.5px; color: var(--text); font-weight: 600; }
.hit span { display: block; font-size: 12px; color: var(--text-4); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hits-none { padding: 10px 12px; font-size: 12.5px; color: var(--text-4); }

/* No max-width, on purpose: this column IS its grid track, and the track is
   sized by the rule at the top of this section. A cap here is what put the
   264px hole down the right of the page — the track was 1184 and the column
   politely took 920 of it, and the 264 it left behind belonged to nobody.

   The number that used to be that cap has not gone away, it has moved into
   the grid: the widest thing the docs carry is overview.md's repository map —
   an unfenced listing whose second column starts at a fixed offset on every
   row, so it keeps its lines intact and scrolls rather than wraps — and it
   needs 840px of content box, i.e. 920 including this padding. That is now
   the figure the 1380px breakpoint above is derived from, so the map still
   fits without scrolling at 1440.

   Prose does not stretch when the track is wider than that: .md p and .md li
   cap themselves at 72ch regardless. */
.docs-main { min-width: 0; padding: clamp(30px,4vw,48px) var(--gut) clamp(56px,8vw,96px); }
.crumb { margin-bottom: 18px; }

/* ── previous / next ──────────────────────────────────────────────────────
   Two cards closing the document, matching the sidebar's own idiom: the
   direction in mono caps as a label, the destination title in text. Auto-fit
   rather than two fixed columns so the first and last documents — which have
   only one neighbour — get a single full-width card instead of a card and a
   hole. The `next` card pushes itself to the right-hand track when it is the
   only one, so "next" never sits where "previous" would. */
.docs-pager {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: 12px; margin-top: clamp(40px, 6vw, 64px);
  padding-top: clamp(24px, 3vw, 32px); border-top: 1px solid var(--line);
  max-width: 72ch;
}
.docs-pager a {
  display: flex; flex-direction: column; gap: 6px; min-height: 44px;
  padding: 14px 16px; border: 1px solid var(--line); border-radius: 10px;
  background: var(--paper-2); color: var(--text-2);
  transition: border-color .15s var(--ease), background .15s var(--ease), color .15s var(--ease);
}
.docs-pager a:hover { border-color: var(--line-3); background: var(--paper-3); color: var(--text); }
.docs-pager a:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.docs-pager .dir {
  font-family: var(--mono); font-size: 11px; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase; color: var(--text-3);
}
.docs-pager .t { font-size: 14.5px; line-height: 1.35; }
.docs-pager .next { text-align: right; grid-column: -2; }

/* ── the on-this-page rail ────────────────────────────────────────────────
   What now occupies the space the phantom column used to reserve. The docs
   are long enough to want one — overview.md is sixteen headings and about
   eleven screens at 1440 — and a reader who has scrolled into the middle of
   it otherwise has no way to see where they are or what else is on the page
   without going back to the top.

   It is built by the viewer, not written in the markup, because the headings
   come from markdown that is fetched and rendered client-side. A reader
   without JavaScript therefore never sees it, which is correct: they never
   see the document it indexes either, and the grid falls back to two tracks
   because .no-rail is applied on the same code path.

   Sticky rather than fixed, and offset by the header, so it holds its place
   while the document scrolls and still scrolls internally when a page has
   more headings than the viewport has room for. */
.docs-rail {
  position: sticky; top: 66px; align-self: start;
  max-height: calc(100dvh - 66px); overflow-y: auto;
  padding: 30px var(--gut) 40px 4px;
}
.docs-rail::-webkit-scrollbar { width: 7px; }
.docs-rail::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 7px; }
.docs-rail > .lbl { display: block; margin-bottom: 10px; }
.docs-rail ol { list-style: none; margin: 0; padding: 0; border-left: 1px solid var(--line); }
.docs-rail li { margin: 0; padding: 0; }
/* A rail entry is a real navigation target, so it carries the same 44px floor
   the index rail and the header nav do. Multi-line titles simply grow past
   it — "Verify a release before you run it" is three lines at this width. */
.docs-rail a {
  display: flex; align-items: center; min-height: 44px;
  padding: 7px 10px 7px 14px; margin-left: -1px;
  border-left: 2px solid transparent;
  font-size: 12.5px; line-height: 1.45; color: var(--text-4);
  transition: color .15s var(--ease), border-color .15s var(--ease);
}
.docs-rail a:hover { color: var(--text-2); }
.docs-rail a.here { color: var(--teal); border-left-color: var(--teal); }
/* An h3 is a step inside its h2, and reads as one. */
.docs-rail a.sub { padding-left: 26px; font-size: 12px; }

/* rendered markdown */
.md { font-size: 15.5px; }
.md > *:first-child { margin-top: 0; }
.md h1 { font-size: clamp(2rem,4.2vw,2.9rem); margin: 0 0 18px; }
.md h2 {
  font-size: clamp(1.5rem,2.8vw,2rem); margin: 46px 0 14px;
  padding-top: 22px; border-top: 1px solid var(--line);
}
.md h3 { font-size: 1.16rem; margin: 30px 0 10px; }
/* Several docs title a section with an endpoint — "## `GET /api/v1/rates`".
   Inline code inherits the heading's size via the .86em rule, which turns a
   section title into a 2rem code block; pin it to something readable and let
   it sit on the baseline of the heading instead. */
.md h1 code, .md h2 code, .md h3 code {
  font-size: .82em; padding: 2px 8px; vertical-align: baseline;
  background: var(--ink-2); border-color: var(--line-2);
}
.md h2 code { font-size: .72em; }
/* Many canonical docs put a --- before every section heading; without this the
   hr and the heading's own top rule stack into a double line with dead space. */
.md hr + h2 { border-top: none; padding-top: 0; margin-top: 26px; }
.md h2:first-child { border-top: none; padding-top: 0; margin-top: 0; }
.md h4 { font-family: var(--ui); font-size: .96rem; font-weight: 650; color: var(--text); margin: 22px 0 8px; }
/* A bare URL or a long `code` span is one unbreakable word to the line
   breaker, and one of them is all it takes to push a 354px column to 365px.
   Breaking inside a word is ugly; being unable to read the paragraph is
   worse, and this only ever fires on a word that genuinely cannot fit. */
.md p, .md li, .md blockquote, .md dd { overflow-wrap: break-word; }
.md td, .md th { overflow-wrap: break-word; }
/* `anywhere` rather than `break-word` inside a table that is still a table,
   and the difference matters: only `anywhere` lowers a cell's min-content
   width, and min-content is what decides how wide the table insists on being.
   Two of sources.md's tables carry unbreakable tokens —
   `SarbWebApi/WebIndicators/HomePageRates`, `api.frankfurter.dev/v1/latest` —
   and those single words were holding an eight-column table at 1150px on a
   1440px desktop that had 840px to give it.

   Scoped twice over, because breaking words is a cost and not a default.
   Scoped to inline code inside a cell rather than the cell's prose: the tokens
   holding those tables open are `code` spans, while the text around them is
   ordinary English that already has break opportunities. Applied to the whole
   cell it read as a fix and looked like a bug — the same table then "fitted"
   by breaking coinbas/e and autho/ritati/ve, and with the headings in too it
   managed SOUR/CE and DEF/AUL/T.

   And scoped to tables the viewer has measured and found too wide (.brk). A
   table that fits should never break a word: lowering min-content also lowers
   what the auto layout will hand a column, so switching this on unconditionally
   made configuration.md — which fits at every width — split OPENRATE_ADD/R and
   -/interest-/sources for nothing. See applyStacking() in docs.html. */
.md table.brk td code { overflow-wrap: anywhere; }
.md p { margin: 13px 0; line-height: 1.75; max-width: 72ch; }
.md a { color: var(--blue); text-decoration: underline; text-underline-offset: 3px; text-decoration-color: color-mix(in srgb, var(--blue) 35%, transparent); }
.md a:hover { text-decoration-color: currentColor; }
.md b, .md strong { color: var(--text); font-weight: 650; }
.md ul, .md ol { margin: 14px 0; padding-left: 4px; list-style: none; }
.md ol { counter-reset: md; }
.md li { position: relative; padding-left: 24px; margin: 9px 0; color: var(--text-3); line-height: 1.72; max-width: 72ch; }
.md ul > li::before { content: ""; position: absolute; left: 4px; top: .66em; width: 5px; height: 5px; background: var(--teal); transform: rotate(45deg); }
.md ol > li { counter-increment: md; }
/* --text-4, not --text-5. A list marker is the only thing on the page that
   tells a reader which of five numbered steps they are looking at, so it is
   text in every sense that matters. --text-5 measured 2.43:1 on --ink (dark)
   and 2.22:1 on --ink (light) — the docs body is --ink in both themes — where
   --text-4 measures 4.89:1 and 4.90:1. See scripts/check-contrast.mjs. */
.md ol > li::before { content: counter(md) "."; position: absolute; left: 0; top: 0; font-family: var(--mono); font-size: 12px; color: var(--text-4); }
.md li > ul, .md li > ol { margin: 8px 0; }
.md code {
  font-family: var(--mono); font-size: .86em; color: var(--teal);
  background: var(--ink-2); border: 1px solid var(--line); border-radius: var(--r-xs); padding: 1px 6px;
  /* .86em of an already-reduced parent (a <sub>, a caption) lands at 11.1px,
     under the 12px floor the rest of the page keeps. Relative sizing is right —
     inline code should track the text it sits in — so keep it and add a floor
     rather than pinning an absolute size. */
  font-size: max(12px, .86em);
}
/* ── code blocks in the docs ──────────────────────────────────────────────
   Each <pre> is wrapped by the viewer in a .codeblock with a chrome bar, the
   same object the landing's terminal and response plates already are. The bar
   is not decoration: it is where the copy control goes. The control used to
   float in the block's top-right corner at 48.8x31px — under the tap-target
   floor, invisible until hover (so unusable by touch, which has no hover),
   and sitting on top of the first line of code. In the bar it gets a real
   44px box, it is always visible, and it covers nothing. The bar also names
   the language, which the reader previously had to infer. */
.codeblock {
  margin: 18px 0; border: 1px solid var(--line-2); border-radius: var(--r);
  background: var(--code-bg); box-shadow: var(--shadow-1); overflow: hidden;
}
.cb-bar {
  display: flex; align-items: center; gap: 10px; padding: 0 8px 0 16px;
  min-height: 40px; background: color-mix(in srgb, var(--code-bg) 100%, white 4%);
  border-bottom: 1px solid rgba(255,255,255,.08);
}
.cb-lang {
  font-family: var(--mono); font-size: 11px; font-weight: 500; letter-spacing: .14em;
  text-transform: uppercase; color: var(--code-punct); flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Named for what it is: this block keeps its lines and scrolls, and the
   reader is told so rather than left to find out by dragging. The viewer adds
   .on only when the block is genuinely wider than its column at the current
   width — a listing that happens to fit should not advertise a scroll it does
   not have. */
.cb-hint {
  display: none; flex: none; align-items: center; gap: 6px;
  font-family: var(--mono); font-size: 11px; letter-spacing: .08em; color: var(--code-punct);
}
.cb-hint.on { display: inline-flex; }
.md .codeblock pre {
  position: static; margin: 0; padding: 14px 16px; border: none; border-radius: 0;
  box-shadow: none;
  /* var(--edge) is set by .or-scroll on the scrolling blocks and undefined on
     the wrapping ones, so this one declaration carries the edge affordance
     where there is one and lands on the plain code surface where there isn't
     — without this rule having to lose a specificity fight with .or-scroll. */
  background: var(--edge, var(--code-bg));
  font-family: var(--mono); font-size: 12.5px; line-height: 1.75; color: var(--code-text);
}
.md pre {
  position: relative; margin: 18px 0; padding: 16px 18px; overflow-x: auto;
  background: var(--code-bg); border: 1px solid var(--line-2); border-radius: var(--r);
  font-family: var(--mono); font-size: 12.5px; line-height: 1.75; color: var(--code-text);
  box-shadow: var(--shadow-1);
}
.md pre code { background: none; border: none; padding: 0; color: inherit; font-size: inherit; }
.md pre code.hljs { display: block; }
.md blockquote {
  margin: 18px 0; padding: 2px 0 2px 18px;
  border-left: 2px solid color-mix(in srgb, var(--teal) 45%, transparent);
  color: var(--text-4);
}
/* ── tables ───────────────────────────────────────────────────────────────
   The table itself is a table again. It used to carry `display: block;
   overflow-x: auto` directly, which is the usual shorthand for "let it
   scroll" and has a cost that is easy to miss: a <table> laid out as a block
   sizes its anonymous inner table to max-content, so it took its preferred
   width — 1150px for sources.md's eight-column feed matrix — and scrolled
   even when the column was wide enough for it to fit. The scroll now lives on
   a wrapper the viewer adds, and the table is free to lay out at 100% and
   only overflow when its columns genuinely cannot compress further. */
.md .table-scroll { margin: 18px 0; }
.md table { width: 100%; border-collapse: collapse; margin: 18px 0; font-size: 14px; }
.md .table-scroll > table { margin: 0; }
.md th, .md td { text-align: left; padding: 11px 14px; border-bottom: 1px solid var(--line); vertical-align: top; }
/* A dense matrix earns tighter gutters. sources.md's eight-column table spends
   224px — a quarter of the column it is in — on cell padding alone, and that
   was the whole of the 7px it overflowed a 1440px desktop by. */
.md table[data-cols="6"] :is(th, td),
.md table[data-cols="7"] :is(th, td),
.md table[data-cols="8"] :is(th, td),
.md table[data-cols="9"] :is(th, td) { padding-left: 10px; padding-right: 10px; }
/* The first column hangs on the text column's own left edge, the way the
   landing's ledger does — but only while the table is a table. In a stacked
   card the first cell is inside the card, and pulling it out left it as the
   one row not lining up with the rest. */
.md table[data-cols]:not(.stack) :is(th, td):first-child { padding-left: 0; }
.md th {
  font-family: var(--mono); font-size: 12px; font-weight: 500;
  letter-spacing: .1em; text-transform: uppercase; color: var(--text-4);
}
.md td { color: var(--text-3); }
.md tbody tr:last-child td { border-bottom: none; }
.md hr { border: none; height: 1px; background: var(--line); margin: 40px 0; }
.md img { display: inline-block; margin: 4px 0; }
.md p[align="center"], .md div[align="center"] { text-align: center; }

/* Always visible, never on top of the code, and a real target. Opacity-0-
   until-hover is a desktop idiom; a touch reader has no hover and so simply
   never had a copy button at all. */
.copy-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  flex: none; min-width: 44px; min-height: 44px; padding: 0 12px;
  background: transparent; border: 1px solid transparent; border-radius: var(--r-sm);
  color: var(--code-punct); font-family: var(--mono); font-size: 11.5px; letter-spacing: .06em;
  cursor: pointer;
  transition: color .16s var(--ease), border-color .16s var(--ease), background .16s var(--ease);
}
.copy-btn svg { width: 13px; height: 13px; flex: none; }
.copy-btn:hover { color: var(--code-string); border-color: color-mix(in srgb, var(--code-string) 40%, transparent); background: rgba(255,255,255,.04); }
.copy-btn.done { color: var(--code-string); }

.docs-err {
  border: 1px solid color-mix(in srgb, var(--red) 32%, transparent);
  background: color-mix(in srgb, var(--red) 10%, transparent);
  color: var(--red); padding: 14px 16px; border-radius: var(--r); font-size: 14px;
}

/* #content's markup default is the literal string "Loading…" (see docs.html)
   so there's something in the DOM before the fetch resolves — but a reader
   with JavaScript disabled never runs the fetch, so that placeholder would
   otherwise sit there forever, contradicting the <noscript> message right
   above it that already explains scripting is off. Same gating principle as
   .rv/.dial elsewhere: html.js is only added by a script, so this only
   hides #content for the population that actually has JS to eventually
   replace it. */
html:not(.js) #content { display: none; }

/* ── the documentation index below 980px ──────────────────────────────────
   What this replaced: the desktop rail was laid on its side into a single
   non-wrapping row of nine pills with overflow-x: auto. On a 390px phone that
   made ASIDE.docs-side an 855px-wide box inside a 390px window — the reader
   had to drag the navigation sideways to discover that six of the nine pages
   existed, with nothing on screen to say they were there, and the search box
   was hidden outright because it had nowhere to go.

   It is now a disclosure. The bar shows one line — the page you are on —
   which is the single fact a reader in a document most needs, and opening it
   drops the whole index over the page: all nine titles at once, laid out in a
   grid that fits the width instead of running past it, with the search box
   back. Nothing is off-screen and nothing is dragged. The bar is sticky under
   the header, so the index is one tap away from anywhere in a long document
   rather than a scroll back to the top.

   The panel is a plain element toggled by a button rather than <details>: the
   rail has to be permanently open above 980px, and forcing a <details> open
   from CSS is not reliably possible now that engines render its contents
   through ::details-content. html:not(.js) keeps the whole index visible for
   a reader without scripting, the same gate #content and .rv already use. */
@media (max-width: 980px) {
  /* minmax(0, 1fr), not a bare 1fr: a bare 1fr's automatic minimum is its
     content's min-content size, so any non-wrapping child raises the track
     above the viewport width and takes the whole page into horizontal scroll
     with it. That is how the old pill bar blew the page out to 887px on a
     768px viewport.

     `.docs.no-rail` is named here as well as `.docs`, and it has to be. A
     media query adds no specificity: `.docs.no-rail` above is (0,2,0) and
     beats a bare `.docs` inside any @media, at any width. With scripting on
     the viewer removes `no-rail` as soon as it has a rail to show, so this
     rule won this argument by accident. With scripting OFF `no-rail` is the
     shipped default and never leaves, and the two-track desktop grid survived
     into a 390px phone: the index sat in a 256px column with the document
     crushed into the ~110px beside it, one word per line. Readable with
     JavaScript off is a requirement of these pages, and this was the one
     place it was not met. */
  .docs, .docs.no-rail { grid-template-columns: minmax(0, 1fr); }
  .docs-side {
    position: sticky; top: 66px; z-index: 45;
    /* height:auto undoes the desktop rail's full-viewport height — here the
       element is a one-line bar and must be exactly as tall as that bar. */
    height: auto; max-height: none; overflow: visible;
    padding: 0 var(--gut); border-right: none; border-bottom: 1px solid var(--line);
    background: color-mix(in srgb, var(--ink) 88%, transparent);
    backdrop-filter: blur(16px) saturate(150%);
  }
  .toc-btn {
    display: flex; align-items: center; gap: 12px; width: 100%;
    min-height: 54px; padding: 0; background: none; border: none; cursor: pointer;
    text-align: left; color: var(--text-3);
  }
  .toc-btn .toc-k {
    flex: none; font-family: var(--mono); font-size: 11px; font-weight: 500;
    letter-spacing: .14em; text-transform: uppercase; color: var(--text-4);
  }
  .toc-btn .toc-here {
    flex: 1 1 auto; min-width: 0; font-size: 14.5px; font-weight: 600; color: var(--text);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .toc-btn .toc-chev { flex: none; width: 16px; height: 16px; color: var(--text-4); transition: transform .22s var(--ease); }
  .toc-btn[aria-expanded="true"] .toc-chev { transform: rotate(180deg); }
  .toc-btn:hover .toc-here { color: var(--teal); }

  .toc-panel {
    position: absolute; left: 0; right: 0; top: 100%; z-index: 46;
    display: none; gap: 0;
    max-height: calc(100dvh - 122px); overflow-y: auto;
    padding: 16px var(--gut) 22px;
    background: var(--ink-2); border-bottom: 1px solid var(--line-2);
    box-shadow: 0 28px 64px -28px rgba(0,0,0,.85);
  }
  .docs-side.open .toc-panel, html:not(.js) .toc-panel { display: flex; }
  html:not(.js) .toc-panel { position: static; max-height: none; box-shadow: none; padding: 16px 0 22px; }
  .toc-panel > .lbl { display: none; }
  /* Two columns wherever two columns fit, so a group's pages are one glance
     rather than one scroll. auto-fit + a 150px floor means it is one column
     on the narrowest phones without a second breakpoint to keep in step.
     The grid is on .nav-items, not on nav: the groups themselves stack, so
     each heading still sits directly above the pages it names — laying the
     GROUPS out in two columns would put "Embedding" beside the pages of
     "Start here". */
  .docs-side nav { display: flex; flex-direction: column; gap: 2px; }
  .docs-side .nav-items { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 2px; }
  .docs-side nav a { border-left: none; border-radius: var(--r-sm); padding: 8px 12px; }
  .docs-side nav a.active {
    border-left: none; color: var(--teal);
    /* Same correction as the desktop rule above; the inset ring below is what
       carries the emphasis here, not the fill. */
    background: color-mix(in srgb, var(--teal) 6%, transparent);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--teal) 32%, transparent);
  }
  /* The heading list is a nested list under one link, so it never joins the
     two-column grid — it spans it, and keeps its rule and its indent. */
  .docs-side .nav-heads { grid-column: 1 / -1; margin-left: 14px; }
  .docs-side .nav-heads a {
    border-left: 2px solid transparent; border-radius: 0; padding: 6px 10px 6px 12px;
  }
  .docs-side .nav-heads a.here { border-left-color: var(--teal); background: none; box-shadow: none; }
  .docs-main { max-width: none; }
}

/* ── wide tables on a phone ───────────────────────────────────────────────
   sources.md's feed matrix is eight columns of prose. At 354px there is no
   column width that makes it a table a person can read: laid out to fit, each
   column is 44px; laid out to be legible, it is a 1150px box dragged past the
   window three times over. So past a point a table stops being a grid and
   becomes one card per row, each cell labelled with its own column heading —
   the heading row is carried onto every cell as data-th by the viewer, so
   nothing is lost and nothing has to be remembered from a header that
   scrolled away.

   Where that point is depends on the column count, which is why the switch is
   a matchMedia pair in docs.html rather than a media query here: five columns
   or more give up at 900px, three or four at 620px, and a two-column
   key/value table never does because it already reads correctly at any width.

   Tables that stay tabular keep the .or-scroll affordance on their wrapper. */
.md .table-scroll.stacked { overflow-x: visible; background: none; }
.md table.stack { display: block; font-size: 14px; }
.md table.stack thead { display: none; }
.md table.stack tbody, .md table.stack tr, .md table.stack td, .md table.stack th { display: block; }
.md table.stack tbody tr {
  padding: 4px 0 10px; margin: 0 0 12px;
  border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--paper);
}
.md table.stack tbody tr:last-child { margin-bottom: 0; }
/* The label sits above its value rather than beside it. Beside it would be a
   two-track grid, and a grid blockifies every direct child — which turns an
   inline <code> chip into a full-width bar and splits `127.0.0.1 port` across
   two rows. A cell has to stay a normal inline formatting context; only the
   label is a block. */
.md table.stack td, .md table.stack tbody th {
  display: block; border-bottom: none; padding: 8px 14px 0;
  font-weight: inherit; color: var(--text-3);
}
.md table.stack td::before, .md table.stack tbody th::before {
  content: attr(data-th); display: block; margin-bottom: 2px;
  font-family: var(--mono); font-size: 11px; font-weight: 500; letter-spacing: .1em;
  text-transform: uppercase; color: var(--text-4);
}
/* An empty cell in a stacked row is a row of nothing but its own label. */
.md table.stack td:empty { display: none; }
.md table.stack tbody tr > *:first-child { padding-top: 11px; color: var(--text); }
.md table.stack tbody tr > *:first-child::before { color: var(--teal); }

/* ── reveal ───────────────────────────────────────────────────────────────
   Progressive enhancement, not a requirement: .rv only starts hidden once
   html.js is present, and that class is set by an inline script in <head>
   before first paint. A reader with JavaScript disabled never gets the "js"
   class, so .rv keeps its default (visible, static) state forever — the
   content is never gated on a script that might not run. Same principle the
   guilloché already followed ("if scripting is off nothing here is missed"),
   now applied here too. */
.rv { transition: opacity .7s var(--ease-out), transform .7s var(--ease-out); }
html.js .rv { opacity: 0; transform: translateY(16px); }
html.js .rv.in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  html.js .rv { opacity: 1; transform: none; transition: none; }
  .guilloche .rose { animation: none; }
  *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
}


/* ── 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. */
/* 44px, not 34: both of these are standalone controls — a link to another
   site, in the header and again in the footer — and both measured under the
   tap-target floor at every width. The mark itself is unchanged at 22px and
   18px; only the box around it grew. */
.vulos-home { display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; border-radius: 12px; 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; min-height: 44px; 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; }
