/* ==========================================================================
   AdForm — Dokumentasi Developer
   docs.css — complete theme stylesheet

   DESIGN LANGUAGE: EFFERD (locked owner directive, .claude/rules/efferd-design.md)
     · Monochrome zinc. Off-white surface, near-black ink, hairline rails.
     · Geist + Geist Mono. Numbers, code and identifiers are ALWAYS mono.
     · Brand accent appears in exactly THREE places — the live dot in the
       topbar, the focus ring, and the copy-success checkmark. Nowhere else.
       Links are ink, not brand. Buttons are black, not brand.
     · Headlines are LARGE and LIGHT (weight 500), tight tracking, lh ~1.1.
     · Framed content column with visible vertical RAIL lines and hairline
       horizontal dividers between sections. Never a stack of shadowed cards.
     · Radius 14px cards / 10px inner. Shadow at most 0 1px 2px rgba(20,20,16,.03).

   BUILD CONTRACT
     Emitted by scripts/build-docs.mjs to /assets/docs.<hash8>.css, so the
     relative url("fonts/…") below resolves to /assets/fonts/*.
     JS hooks consumed by theme/docs.js and NOT to be renamed:
       [data-copy]  .doc-code  [data-theme-toggle]  .doc-toc__link  [data-active]

   CONTRAST LEDGER — re-measured 2026-07-27 with an automated sweep over the
   rendered pages: every element carrying a text node, plus ::before / ::after
   / ::marker, in BOTH schemes, at 1440 / 900 / 390, in rest AND hover state.
   12,305 samples. WCAG 2.1 AA (4.5:1 text, 3:1 non-text).
                                      LIGHT     DARK
     h1 heading .....................  18.97 /  18.85   (min 3, large text)
     body paragraph .................  10.01 /  15.50
     lede 18px ......................   4.63 /   7.68
     sidebar link, rest state .......   4.63 /   7.68
     nav / TOC / table labels 12px ..   5.48 /   9.24   (--doc-label)
     code text ......................  18.31 /  18.48
     table cell .....................  10.44 /  14.98
     scope + optional badges ........   4.83 /   7.42
     warning label ..................   6.44 /  10.11
     danger label ...................   7.45 /   8.64
     focus ring (non-text, min 3) ... 10.58 /   7.59
     ↳ scroll-region ring, table .... 11.04 /   7.34   (inset, on --doc-card)
     ↳ scroll-region ring, code ..... 10.21 /   7.44   (inset, on --doc-sunken)
     footer build string ............   4.63 /   7.68   (was 2.43 / 4.07 — FAILED)
     breadcrumb separator ...........   4.63 /   7.68   (was 2.43 / 4.07 — FAILED)
     list marker (non-text, min 3) ..   4.63 /   7.68   (was 2.43 / 4.07 — FAILED)
     heading anchor link ............   4.63 /   7.68   (was 2.43 / 4.07 — FAILED)
     pager direction, HOVER .........   5.48 /   9.24   (was 4.47 light — FAILED)

   THE PREVIOUS VERSION OF THIS BLOCK CLAIMED "Zero failures". THAT WAS FALSE.
   Five roles failed; four of them were the four consumers of --doc-faint. The
   comment asserted --doc-faint carried "no load-bearing text" while the very
   same file set the footer build string, the breadcrumb separator, the list
   markers and the heading anchor in it. A token cannot be documented out of
   the contrast budget — so --doc-faint has been REMOVED rather than merely
   darkened, which is the only change that stops it being reached for again.

   Rules this file enforces on itself:
     · Brand cyan is NEVER text on a light surface (2.48:1 — fails AA). It is
       a dot, a ring and a checkmark only.
     · --doc-muted on --doc-sunken measures 4.47:1 and FAILS. Every 12px label
       that can land on a sunken fill — including on HOVER, which is how
       .doc-pager__dir was missed — uses --doc-label instead.
     · The lightest ink available anywhere is --doc-muted. There is no tier
       below it, because everything the old tier was used for was text.

   Hairline rails and borders (--doc-border, 1.13:1) are exempt: they are
   decoration under WCAG 1.4.11, not graphics required to understand content.

   Re-run the audit against a rendered page before changing any colour token.

   ── CODE PLATE + SYNTAX (added 2026-07-29) ───────────────────────────────
   Measured against --doc-code-bg, which is what these actually sit on — not
   against the page. AA text floor 4.5:1.
                                      LIGHT     DARK
     code text (--doc-fg) ...........  14.10 /  13.70
     inline chip text ...............  13.97 /  12.74
     language tab label .............   5.49 /   6.65
     syntax: string .................   5.46 /   8.67
     syntax: key / var / attribute ..   6.21 /   7.74
     syntax: keyword / tag ..........   7.47 /   5.64   (+ weight 500)
     syntax: number .................   5.68 /   7.74
     syntax: comment ................   5.90 /   6.18   (+ italic)
     syntax: punctuation ............   5.17 /   5.14   ← the binding role
     focus ring (non-text, min 3) ...   7.86 /   5.52
     scrollbar thumb (min 3) ........   3.17 /   3.58

   Punctuation is the role that decides how far the plate may go. Darken the
   plate (light) or lighten it (dark) and punctuation is the first to fall
   below 4.5. Plate and roles are set TOGETHER; moving one alone breaks the
   set.

   BANNED on the code plate, measured, not assumed:
     --doc-muted #6b7280 ............   4.04  FAILS   (already banned on
                                                       --doc-sunken at 4.47)
     --doc-accent #20b0c8 ...........   1.85  FAILS   light only. On the dark
                                                      plate it is 5.52 and is
                                                      used for the focus ring.

   ── SURFACE SEPARATION (non-text) ────────────────────────────────────────
   THE LEDGER HAD NO SECTION LIKE THIS, and that omission is precisely how the
   defect below survived an audit that counted 12,305 samples: every sample
   was text-on-a-surface. Zero were surface-against-surface. The audit was
   honest about what it measured and blind to what it did not.
                                      LIGHT     DARK
     code plate vs page .............   1.345 /  1.376   (was 1.036 / 1.020)
     code plate vs card .............   1.404 /  1.330   (was 1.080 / 1.015)
     inline chip vs page ............   1.358 /  1.479   (was 1.036 / 1.020)
     plate border vs plate ..........   1.262 /  2.048

   STATE THIS PLAINLY: those four numbers are BELOW the 3:1 that WCAG 1.4.11
   asks of non-text, and they are compliant only because --doc-code-line is
   always drawn. That makes the border load-bearing, not decorative. Deleting
   it is not a style change; it invalidates this section.

   3:1 was measured and rejected on purpose. It needs a #929292 fill in light,
   which turns the off-white page into a grey board and drops code text from
   18:1 to 6.4:1 — worse for every reader in order to satisfy one number.

   Why the earlier exemption for borders does not extend here: a border may be
   called decoration only when the surface beneath it already separates itself.
   At 1.02–1.08 across page, card and plate, it did not. The border was
   carrying the entire shape of the box alone, which is the definition of
   load-bearing.

   Dark mode also had its DIRECTION wrong and it is now flipped: the plate is
   lighter than both the page and the card. It used to be darker than the card
   and lighter than the page at the same time — pinched between two
   near-identical greys with no visible edge on either side.
   ========================================================================== */

/* ── 1. Fonts ─────────────────────────────────────────────────────────────
   Self-hosted. Zero external network requests. If the woff2 files are missing
   the fallback stack renders the page correctly — nothing depends on Geist.

   THE FILES ARE PRESENT. Verify before editing these names:
     theme/assets/fonts/Geist-Variable.woff2       68 KB
     theme/assets/fonts/GeistMono-Variable.woff2   70 KB
     theme/assets/fonts/LICENSE.txt                SIL OFL 1.1
   Source: npm `geist` 1.7.2 → dist/fonts/geist-{sans,mono}/*-Variable.woff2
   (upstream vercel/geist-font). SIL OFL 1.1 permits redistribution; the
   licence text ships alongside the fonts and must not be deleted.

   Two variable files cover every weight this theme uses (400/500 sans,
   400/500 mono), so there is nothing else to fetch. build-docs.mjs copies
   theme/assets/ recursively and warns on any woff2 referenced here that is
   missing — keep these filenames byte-identical to the files on disk.
   ------------------------------------------------------------------------ */

@font-face {
  font-family: "Geist";
  src: url("fonts/Geist-Variable.woff2") format("woff2-variations"),
       url("fonts/Geist-Variable.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Geist Mono";
  src: url("fonts/GeistMono-Variable.woff2") format("woff2-variations"),
       url("fonts/GeistMono-Variable.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* ── 2. Tokens ────────────────────────────────────────────────────────── */

:root {
  color-scheme: light;

  /* Type */
  --doc-font: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI",
    Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
  --doc-font-mono: "Geist Mono", ui-monospace, "JetBrains Mono", SFMono-Regular,
    Menlo, Consolas, "Liberation Mono", monospace;

  --doc-text-xs: 0.75rem;      /* 12px */
  --doc-text-sm: 0.875rem;     /* 14px */
  --doc-text-base: 1rem;       /* 16px — floor for body copy, incl. mobile */
  --doc-text-lg: 1.125rem;     /* 18px */
  --doc-text-xl: 1.375rem;     /* 22px */
  --doc-text-2xl: 1.75rem;     /* 28px */
  --doc-text-code: 0.8125rem;  /* 13px */

  /* Surfaces — off-white, never pure white */
  --doc-bg: #fafafa;
  --doc-card: #ffffff;
  --doc-sunken: #f6f6f5;
  --doc-hover: #f0f0ef;

  /* Ink */
  --doc-fg: #0a0a0a;           /* headings, primary text */
  --doc-body: #3f3f46;         /* paragraphs — comfortable for long reads */
  --doc-muted: #6b7280;        /* metadata, captions, nav rest state */
  --doc-label: #5e6472;        /* 12px uppercase labels. Darker than --doc-muted
                                  because these sit on --doc-sunken, where
                                  --doc-muted lands at 4.47:1 and fails AA. */
  /* There is deliberately no tier below --doc-muted. The old --doc-faint
     (#9ca3af, 2.43:1 on --doc-bg) failed AA everywhere it was used. */

  /* Lines — hairline, very soft */
  --doc-border: #ececE8;
  --doc-border-strong: #dedede;

  /* Brand — three appearances only. See the header comment. */
  --doc-accent: #20b0c8;       /* cyan, from the logo */
  --doc-petrol: #004060;       /* petrol, from the logo */
  --doc-focus: var(--doc-petrol);

  /* Semantic — callouts only, always paired with a distinct icon + label */
  --doc-warn: #7c5400;
  --doc-warn-line: #e3d3a8;
  --doc-warn-bg: #fdfaf2;
  --doc-danger: #a11616;
  --doc-danger-line: #ecc9c9;
  --doc-danger-bg: #fdf6f6;

  /* ── Code plate ──────────────────────────────────────────────────────
     A SEPARATE SET from --doc-sunken, deliberately. Reusing --doc-sunken
     would have been one line shorter and would have silently moved every
     other measurement taken against it — the ledger already records
     --doc-muted on --doc-sunken at 4.47:1, and darkening that fill would
     drop it further without anything pointing at the cause.

     These fills are 1.35–1.48 against the page. That is far short of the
     3:1 WCAG asks of non-text, and the shortfall is covered by
     --doc-code-line, which is why removing that border is not a style
     choice — it invalidates the whole set. 3:1 was measured and rejected:
     it needs #929292 in light, which turns the off-white page into a grey
     board and drops code text from 18:1 to 6.4:1.

     The previous values sat at 1.02–1.04 — three greys nobody could tell
     apart, which is what "ga jelas hitam semua" was describing. */
  --doc-code-bg: #dadad4;      /* plate under a code block */
  --doc-code-chip: #d9d9d3;    /* inline-code chip. Sits on --doc-bg, not on
                                  --doc-card: prose has no card of its own. */
  --doc-code-line: #c3c3bc;    /* plate + tab + chip edge. Load-bearing. */
  --doc-code-label: #4d5361;   /* language tab text. NOT --doc-muted, which
                                  measures 4.04:1 on the plate and fails. */
  --doc-code-scroll: #78786f;  /* scrollbar thumb inside a code block */

  /* ── Syntax ──────────────────────────────────────────────────────────
     Six roles. Every one measured against --doc-code-bg, not against the
     page — the page is not what they sit on.

     Colour is never the only channel: keywords carry weight 500 and
     comments carry italics, so a red-green colour-blind reader still has
     a second signal on every pair that appears side by side. Strings and
     numbers are the closest pair in hue; they are told apart by the
     quotes, which no palette can take away. */
  --doc-t-str: #0a5f46;        /* strings, and the target of an HTTP request */
  --doc-t-key: #5b28b4;        /* JSON keys, header names, $vars, attributes */
  --doc-t-kw: #0e4269;         /* keywords, true/false/null, HTTP methods, tags */
  --doc-t-num: #754600;        /* numbers */
  --doc-t-com: #464f5d;        /* comments — in bash blocks these carry command
                                  OUTPUT, so they are not dimmed to the floor */
  --doc-t-pun: #56565f;        /* braces, brackets, commas, colons */

  /* Buttons — primary is BLACK, never brand-coloured */
  --doc-btn-bg: #0a0a0a;
  --doc-btn-fg: #fafafa;

  /* Geometry */
  --doc-radius-card: 14px;
  --doc-radius: 10px;
  --doc-radius-sm: 8px;
  --doc-radius-xs: 5px;
  --doc-shadow: 0 1px 2px rgba(20, 20, 16, 0.03);
  --doc-edge: rgba(20, 20, 16, 0.17);        /* scroll-shadow, tables */

  /* Layout */
  --doc-shell: 88rem;          /* 1408px — outer frame, matches Efferd */
  --doc-sidebar: 17rem;        /* 272px */
  --doc-toc: 15rem;            /* 240px */
  --doc-col: 38rem;            /* 608px ≈ 70 characters at 16px Geist.
                                  Efferd's docs column measures 672px (~78ch);
                                  trimmed here to honour the 65–75ch reading
                                  cap. Revert by editing this one line. */
  --doc-topbar-h: 56px;
  --doc-gutter: 16px;

  --doc-ease: cubic-bezier(0.32, 0.72, 0, 1);
  --doc-dur: 160ms;
}

@media (min-width: 1024px) {
  :root { --doc-gutter: 24px; }
}

/* Dark — still monochrome zinc. Not blue-black, not pure black. */
@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;
    --doc-bg: #0b0b0c;
    --doc-card: #101012;
    --doc-sunken: #0e0e10;
    --doc-hover: #1a1a1d;
    --doc-fg: #fafafa;
    --doc-body: #e4e4e7;
    --doc-muted: #a1a1aa;
    --doc-label: #b4b4bd;
    --doc-border: rgba(255, 255, 255, 0.11);
    --doc-border-strong: rgba(255, 255, 255, 0.17);
    --doc-focus: #20b0c8;
    --doc-warn: #e3b341;
    --doc-warn-line: rgba(227, 179, 65, 0.28);
    --doc-warn-bg: rgba(227, 179, 65, 0.06);
    --doc-danger: #ff8a85;
    --doc-danger-line: rgba(255, 138, 133, 0.28);
    --doc-danger-bg: rgba(255, 138, 133, 0.06);
    --doc-btn-bg: #fafafa;
    --doc-btn-fg: #0a0a0a;
    --doc-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
    --doc-edge: rgba(255, 255, 255, 0.13);   /* a black shadow is invisible here */

    /* Code plate. The direction FLIPS here: in dark the plate is LIGHTER
       than both the page and the card it sits in. The old values had it
       darker than the card and lighter than the page at the same time —
       pinched between two near-identical greys with no visible edge on
       either side. */
    --doc-code-bg: #2a2a2e;
    --doc-code-chip: #2f2f35;
    --doc-code-line: rgba(255, 255, 255, 0.22);
    --doc-code-label: #b0b0ba;
    --doc-code-scroll: rgba(255, 255, 255, 0.40);

    --doc-t-str: #7fdcac;
    --doc-t-key: #c9b4f8;
    --doc-t-kw: #69a6ee;
    --doc-t-num: #e8b673;
    --doc-t-com: #a3abb9;
    --doc-t-pun: #9a9aa6;
  }
}

/* The explicit toggle must win in BOTH directions over the media query. */
:root[data-theme="light"] {
  color-scheme: light;
  --doc-bg: #fafafa;
  --doc-card: #ffffff;
  --doc-sunken: #f6f6f5;
  --doc-hover: #f0f0ef;
  --doc-fg: #0a0a0a;
  --doc-body: #3f3f46;
  --doc-muted: #6b7280;
  --doc-label: #5e6472;
  --doc-border: #ececE8;
  --doc-border-strong: #dedede;
  --doc-focus: #004060;
  --doc-warn: #7c5400;
  --doc-warn-line: #e3d3a8;
  --doc-warn-bg: #fdfaf2;
  --doc-danger: #a11616;
  --doc-danger-line: #ecc9c9;
  --doc-danger-bg: #fdf6f6;
  --doc-btn-bg: #0a0a0a;
  --doc-btn-fg: #fafafa;
  --doc-shadow: 0 1px 2px rgba(20, 20, 16, 0.03);
  --doc-edge: rgba(20, 20, 16, 0.17);        /* scroll-shadow, tables */

  --doc-code-bg: #dadad4;
  --doc-code-chip: #d9d9d3;
  --doc-code-line: #c3c3bc;
  --doc-code-label: #4d5361;
  --doc-code-scroll: #78786f;

  --doc-t-str: #0a5f46;
  --doc-t-key: #5b28b4;
  --doc-t-kw: #0e4269;
  --doc-t-num: #754600;
  --doc-t-com: #464f5d;
  --doc-t-pun: #56565f;
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --doc-bg: #0b0b0c;
  --doc-card: #101012;
  --doc-sunken: #0e0e10;
  --doc-hover: #1a1a1d;
  --doc-fg: #fafafa;
  --doc-body: #e4e4e7;
  --doc-muted: #a1a1aa;
  --doc-label: #b4b4bd;
  --doc-border: rgba(255, 255, 255, 0.11);
  --doc-border-strong: rgba(255, 255, 255, 0.17);
  --doc-focus: #20b0c8;
  --doc-warn: #e3b341;
  --doc-warn-line: rgba(227, 179, 65, 0.28);
  --doc-warn-bg: rgba(227, 179, 65, 0.06);
  --doc-danger: #ff8a85;
  --doc-danger-line: rgba(255, 138, 133, 0.28);
  --doc-danger-bg: rgba(255, 138, 133, 0.06);
  --doc-btn-bg: #fafafa;
  --doc-btn-fg: #0a0a0a;
  --doc-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
  --doc-edge: rgba(255, 255, 255, 0.13);     /* a black shadow is invisible here */

  --doc-code-bg: #2a2a2e;
  --doc-code-chip: #2f2f35;
  --doc-code-line: rgba(255, 255, 255, 0.22);
  --doc-code-label: #b0b0ba;
  --doc-code-scroll: rgba(255, 255, 255, 0.40);

  --doc-t-str: #7fdcac;
  --doc-t-key: #c9b4f8;
  --doc-t-kw: #69a6ee;
  --doc-t-num: #e8b673;
  --doc-t-com: #a3abb9;
  --doc-t-pun: #9a9aa6;
}

/* ── 3. Reset + base ─────────────────────────────────────────────────── */

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

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--doc-bg);
  color: var(--doc-body);
  font-family: var(--doc-font);
  font-size: var(--doc-text-base);
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* The page body must never scroll sideways. Wide children scroll inside
     their own containers (.doc-code pre, .doc-tablewrap). */
  overflow-x: clip;
}

img,
svg,
video { max-width: 100%; height: auto; display: block; }

button,
input,
select,
textarea { font: inherit; color: inherit; }

/* One global focus treatment. `outline` rather than box-shadow so it wraps
   inline links correctly across line breaks. Never removed without a
   replacement of at least equal visibility. */
:focus-visible {
  outline: 2px solid var(--doc-focus);
  outline-offset: 2px;
  border-radius: 3px;
}

:focus:not(:focus-visible) { outline: none; }

::selection {
  background: var(--doc-fg);
  color: var(--doc-bg);
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* Visually-hidden, still announced. */
.doc-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── 4. Skip link — the first focusable element on the page ──────────── */

.doc-skip {
  position: fixed;
  left: 12px;
  top: 12px;
  z-index: 90;
  transform: translateY(-180%);
  padding: 10px 16px;
  border: 1px solid var(--doc-border-strong);
  border-radius: var(--doc-radius-sm);
  background: var(--doc-card);
  color: var(--doc-fg);
  font-size: var(--doc-text-sm);
  font-weight: 500;
  text-decoration: none;
  box-shadow: var(--doc-shadow);
  transition: transform var(--doc-dur) var(--doc-ease);
}

.doc-skip:focus { transform: translateY(0); }

/* ── 5. Topbar ───────────────────────────────────────────────────────── */

.doc-topbar {
  position: sticky;
  top: 0;
  z-index: 30;
  border-bottom: 1px solid var(--doc-border);
  background: color-mix(in srgb, var(--doc-bg) 88%, transparent);
  backdrop-filter: saturate(180%) blur(12px);
  -webkit-backdrop-filter: saturate(180%) blur(12px);
}

@supports not (backdrop-filter: blur(1px)) {
  .doc-topbar { background: var(--doc-bg); }
}

.doc-topbar__inner {
  display: flex;
  align-items: center;
  gap: 10px;
  height: var(--doc-topbar-h);
  max-width: var(--doc-shell);
  margin-inline: auto;
  padding-inline: var(--doc-gutter);
}

.doc-topbar__spacer { flex: 1 1 auto; }

.doc-brand {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  color: var(--doc-fg);
  font-size: var(--doc-text-sm);
  font-weight: 500;
  letter-spacing: -0.012em;
  text-decoration: none;
  white-space: nowrap;
}

.doc-brand__mark {
  width: 22px;
  height: 22px;
  flex: none;
  border-radius: 5px;
}

.doc-brand__text { color: inherit; }

/* BRAND ACCENT 1 of 3 — the single live dot. Nothing else in the topbar
   carries colour. */
.doc-dot {
  position: relative;
  width: 6px;
  height: 6px;
  flex: none;
  border-radius: 50%;
  background: var(--doc-accent);
}

.doc-dot::after {
  content: "";
  position: absolute;
  inset: -3px;
  border-radius: 50%;
  border: 1px solid var(--doc-accent);
  opacity: 0;
  animation: doc-ping 2.8s var(--doc-ease) infinite;
}

@keyframes doc-ping {
  0%   { opacity: 0.5; transform: scale(0.7); }
  70%  { opacity: 0;   transform: scale(1.5); }
  100% { opacity: 0;   transform: scale(1.5); }
}

@media (prefers-reduced-motion: reduce) {
  .doc-dot::after { display: none; }
}

.doc-topbar__link {
  padding: 6px 10px;
  border-radius: var(--doc-radius-xs);
  color: var(--doc-muted);
  font-size: var(--doc-text-sm);
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--doc-dur) var(--doc-ease),
              background-color var(--doc-dur) var(--doc-ease);
}

.doc-topbar__link:hover {
  color: var(--doc-fg);
  background: var(--doc-hover);
}

@media (max-width: 599px) {
  .doc-topbar__link { display: none; }
}

/* ── 6. Buttons ──────────────────────────────────────────────────────── */

.doc-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  min-height: 34px;
  padding: 0 12px;
  border: 1px solid transparent;
  border-radius: var(--doc-radius-sm);
  background: var(--doc-btn-bg);
  color: var(--doc-btn-fg);
  font-size: var(--doc-text-sm);
  font-weight: 500;
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
  transition: opacity var(--doc-dur) var(--doc-ease),
              background-color var(--doc-dur) var(--doc-ease),
              border-color var(--doc-dur) var(--doc-ease);
}

.doc-btn:hover { opacity: 0.88; }

/* Ghost — thin border, no fill. */
.doc-btn--ghost {
  background: transparent;
  color: var(--doc-fg);
  border-color: var(--doc-border-strong);
}

.doc-btn--ghost:hover { background: var(--doc-hover); opacity: 1; }

.doc-btn svg { width: 16px; height: 16px; flex: none; }

/* Icon-only control: 34px visual, 44px hit area via the ::before overlay. */
.doc-iconbtn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--doc-radius-sm);
  background: transparent;
  color: var(--doc-muted);
  cursor: pointer;
  transition: background-color var(--doc-dur) var(--doc-ease),
              color var(--doc-dur) var(--doc-ease);
}

.doc-iconbtn::before {
  content: "";
  position: absolute;
  inset: -5px;
}

.doc-iconbtn:hover {
  background: var(--doc-hover);
  color: var(--doc-fg);
}

.doc-iconbtn svg { width: 16px; height: 16px; }

/* Theme toggle — inline SVG icons, never glyphs. Moon in light, sun in dark. */
.doc-themetoggle .doc-i-sun { display: none; }
.doc-themetoggle .doc-i-moon { display: block; }

@media (prefers-color-scheme: dark) {
  .doc-themetoggle .doc-i-sun { display: block; }
  .doc-themetoggle .doc-i-moon { display: none; }
}

:root[data-theme="dark"] .doc-themetoggle .doc-i-sun { display: block; }
:root[data-theme="dark"] .doc-themetoggle .doc-i-moon { display: none; }
:root[data-theme="light"] .doc-themetoggle .doc-i-sun { display: none; }
:root[data-theme="light"] .doc-themetoggle .doc-i-moon { display: block; }

/* ── 7. Shell, three-column grid, and the four vertical rails ────────────
   Rail 1 = shell left edge          (.doc-shell::before)
   Rail 2 = sidebar / content        (.doc-sidebar border-inline-end)
   Rail 3 = content / TOC            (.doc-col border-inline-end)
   Rail 4 = shell right edge         (.doc-shell::after)
   This is the Efferd hallmark: the empty side gutters read as deliberate
   framing rather than as slack.
   ---------------------------------------------------------------------- */

.doc-shell {
  position: relative;
  display: grid;
  grid-template-columns: var(--doc-sidebar) minmax(0, 1fr) var(--doc-toc);
  /* align-items MUST stay `stretch` (the default). Two things depend on it:
     rails 2 and 3 are borders on these columns and must run the full page
     height, and the sticky inner panels need a taller containing block than
     themselves or they never actually stick. */
  align-items: stretch;
  width: 100%;
  max-width: var(--doc-shell);
  margin-inline: auto;
  min-height: calc(100vh - var(--doc-topbar-h));
}

.doc-shell::before,
.doc-shell::after {
  content: "";
  position: absolute;
  inset-block: 0;
  width: 1px;
  background: var(--doc-border);
  pointer-events: none;
  z-index: 1;
}

.doc-shell::before { left: 0; }
.doc-shell::after  { right: 0; }

/* min-width:0 on grid children is what actually stops a wide code block or
   table from forcing the whole page to scroll sideways. */
.doc-sidebar,
.doc-col,
.doc-tocrail { min-width: 0; }

.doc-sidebar { border-inline-end: 1px solid var(--doc-border); }   /* rail 2 */
.doc-col     { border-inline-end: 1px solid var(--doc-border); }   /* rail 3 */

@media (max-width: 1279px) {
  .doc-shell { grid-template-columns: var(--doc-sidebar) minmax(0, 1fr); }
  .doc-tocrail { display: none; }
  .doc-col { border-inline-end: 0; }
}

@media (max-width: 899px) {
  .doc-shell { grid-template-columns: minmax(0, 1fr); }
  .doc-shell::before,
  .doc-shell::after { display: none; }
  .doc-sidebar { border-inline-end: 0; }
}

/* ── 8. Sidebar + mobile disclosure ──────────────────────────────────────
   Markup is <details class="doc-mobilenav"> wrapping the nav.
   ≥900px : the <summary> is hidden and the nav is always visible, so the
            disclosure is invisible and inert. The template ships the element
            with `open`, which means the nav is reachable with JavaScript and
            CSS disabled — no keyboard trap, no hidden-but-focusable links.
   <900px : the <summary> becomes a 46px-tall tap target; a 4-line inline
            script in the template collapses it at that breakpoint.
   -------------------------------------------------------------------- */

/* STICKY LIVES ON THE <details>, NOT ON THE NAV INSIDE IT.
   Chrome wraps a <details>'s children in a `::details-content` box, and that
   box — not the <details> — is the containing block for anything inside. It
   is only as tall as the nav, so a sticky nav has zero slack and silently
   never sticks. Sticking the <details> itself gives it .doc-sidebar (a
   full-height stretched grid item) as its containing block, which works in
   every engine and needs no ::details-content support. */
.doc-sidebar__inner {
  padding: 24px var(--doc-gutter) 40px;
  scrollbar-width: thin;
  scrollbar-color: var(--doc-border-strong) transparent;
}

@media (min-width: 900px) {
  .doc-mobilenav {
    position: sticky;
    top: var(--doc-topbar-h);
  }
  .doc-sidebar__inner {
    max-height: calc(100vh - var(--doc-topbar-h));
    overflow-y: auto;
    overscroll-behavior: contain;
  }
}

.doc-sidebar__inner::-webkit-scrollbar { width: 8px; }
.doc-sidebar__inner::-webkit-scrollbar-track { background: transparent; }
.doc-sidebar__inner::-webkit-scrollbar-thumb {
  background: var(--doc-border-strong);
  border-radius: 8px;
  border: 2px solid transparent;
  background-clip: content-box;
}

/* .doc-sidebar stays a plain block. It is a grid item under the default
   `align-items: stretch`, so it is already full page height — which is what
   both rail 2 and the sticky disclosure below depend on. */
.doc-mobilenav { display: block; }

.doc-mobilenav__summary {
  display: none;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  min-height: 46px;
  padding: 12px var(--doc-gutter);
  border-bottom: 1px solid var(--doc-border);
  color: var(--doc-fg);
  font-size: var(--doc-text-sm);
  font-weight: 500;
  list-style: none;
  cursor: pointer;
  user-select: none;
}

.doc-mobilenav__summary::-webkit-details-marker { display: none; }
.doc-mobilenav__summary::marker { content: ""; }
.doc-mobilenav__summary:hover { background: var(--doc-hover); }
.doc-mobilenav__summary:focus-visible { outline-offset: -3px; }

.doc-mobilenav__chev {
  width: 16px;
  height: 16px;
  flex: none;
  color: var(--doc-muted);
  transition: transform var(--doc-dur) var(--doc-ease);
}

.doc-mobilenav[open] .doc-mobilenav__chev { transform: rotate(180deg); }

@media (max-width: 899px) {
  .doc-mobilenav__summary { display: flex; }
  .doc-sidebar__inner {
    position: static;
    max-height: none;
    overflow: visible;
    padding: 12px var(--doc-gutter) 22px;
    border-bottom: 1px solid var(--doc-border);
  }
}

/* ── 9. Sidebar navigation ───────────────────────────────────────────── */

.doc-navgroup + .doc-navgroup {
  margin-top: 22px;
  padding-top: 22px;
  border-top: 1px solid var(--doc-border);
}

.doc-navgroup__title {
  margin: 0 0 8px;
  padding-inline: 10px;
  color: var(--doc-label);
  font-size: var(--doc-text-xs);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.doc-navlist {
  margin: 0;
  padding: 0;
  list-style: none;
}

.doc-navlist a {
  position: relative;
  display: block;
  padding: 7px 10px;
  border-radius: var(--doc-radius-xs);
  color: var(--doc-muted);
  font-size: var(--doc-text-sm);
  line-height: 1.45;
  text-decoration: none;
  transition: color var(--doc-dur) var(--doc-ease),
              background-color var(--doc-dur) var(--doc-ease);
}

.doc-navlist a:hover {
  color: var(--doc-fg);
  background: var(--doc-hover);
}

/* Active item is signalled on THREE channels — marker bar, weight, ink
   colour — so it is never colour alone. */
.doc-navlist a[aria-current="page"],
.doc-navlist a[data-active],
.doc-navlist a.is-active {
  color: var(--doc-fg);
  font-weight: 500;
  background: var(--doc-hover);
}

.doc-navlist a[aria-current="page"]::before,
.doc-navlist a[data-active]::before,
.doc-navlist a.is-active::before {
  content: "";
  position: absolute;
  left: -1px;
  top: 6px;
  bottom: 6px;
  width: 2px;
  border-radius: 2px;
  background: var(--doc-fg);
}

/* ── 10. Main column ─────────────────────────────────────────────────── */

.doc-col {
  padding-inline: clamp(16px, 4vw, 48px);
  padding-bottom: 76px;
}

.doc-article {
  max-width: var(--doc-col);
  margin-inline: auto;
}

/* ── 11. Breadcrumb + page head ──────────────────────────────────────── */

.doc-breadcrumb {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  padding-top: 26px;
  color: var(--doc-muted);
  font-size: var(--doc-text-xs);
}

.doc-breadcrumb:empty { display: none; }

.doc-breadcrumb a { color: inherit; text-decoration: none; }

.doc-breadcrumb a:hover {
  color: var(--doc-fg);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* The separator inherits the breadcrumb's own --doc-muted. It used to be set
   one tier lighter, which put a visible glyph at 2.43:1. */
.doc-breadcrumb__sep { color: inherit; }

.doc-titleblock {
  padding: 14px 0 26px;
  border-bottom: 1px solid var(--doc-border);
}

/* Headline: LARGE and LIGHT. Weight 500, tight tracking, lh ~1.1. */
.doc-title {
  margin: 0;
  color: var(--doc-fg);
  font-size: var(--doc-text-2xl);
  font-weight: 500;
  line-height: 1.12;
  letter-spacing: -0.032em;
  text-wrap: balance;
}

@media (min-width: 768px) {
  .doc-title {
    font-size: 2rem;
    line-height: 1.08;
    letter-spacing: -0.036em;
  }
}

.doc-lede {
  margin: 10px 0 0;
  color: var(--doc-muted);
  font-size: var(--doc-text-lg);
  line-height: 1.55;
  text-wrap: pretty;
}

.doc-lede:empty { display: none; }

.doc-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-top: 16px;
}

.doc-meta:empty { display: none; }

/* HTTP method + scope badges. Monochrome — the WORD is the differentiator,
   not a colour. Mono face, per "identifiers are always mono". */
.doc-method,
.doc-scope {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 8px;
  border: 1px solid var(--doc-border-strong);
  border-radius: var(--doc-radius-xs);
  background: var(--doc-card);
  font-family: var(--doc-font-mono);
  font-size: var(--doc-text-xs);
  line-height: 1.5;
  white-space: nowrap;
}

.doc-method {
  color: var(--doc-fg);
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.doc-scope { color: var(--doc-muted); }

/* Endpoint line under the badges — scrolls inside itself when long. */
.doc-endpointline {
  margin: 12px 0 0;
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: none;
}

.doc-endpointline::-webkit-scrollbar { display: none; }

.doc-endpointline code {
  display: inline-block;
  padding: 6px 10px;
  border: 1px solid var(--doc-border);
  border-radius: var(--doc-radius-xs);
  background: var(--doc-sunken);
  color: var(--doc-body);
  font-family: var(--doc-font-mono);
  font-size: var(--doc-text-xs);
  white-space: nowrap;
}

/* ── 12. Prose ───────────────────────────────────────────────────────── */

.doc-body { padding-top: 30px; }

.doc-body > * { margin: 0; }
.doc-body > * + * { margin-top: 18px; }

.doc-body p {
  color: var(--doc-body);
  line-height: 1.75;
  text-wrap: pretty;
}

.doc-body strong { color: var(--doc-fg); font-weight: 500; }
.doc-body em { font-style: italic; }

/* Sequential heading levels only: h1 (title block) → h2 → h3 → h4. */
.doc-h2,
.doc-h3,
.doc-h4 {
  position: relative;
  color: var(--doc-fg);
  letter-spacing: -0.02em;
  text-wrap: balance;
  scroll-margin-top: calc(var(--doc-topbar-h) + 20px);
}

/* Section rhythm: 44px of air above each h2 with a hairline divider drawn at
   the top of that space. Sections are delimited by LINES, never by cards. */
.doc-body > .doc-h2 {
  margin-top: 44px;
  padding-top: 26px;
  border-top: 1px solid var(--doc-border);
  font-size: var(--doc-text-xl);
  font-weight: 500;
  line-height: 1.35;
}

.doc-body > .doc-h2:first-child {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

.doc-body > .doc-h3 {
  margin-top: 32px;
  font-size: var(--doc-text-lg);
  font-weight: 500;
  line-height: 1.45;
}

.doc-body > .doc-h4 {
  margin-top: 26px;
  font-size: var(--doc-text-base);
  font-weight: 500;
  line-height: 1.5;
}

/* Heading anchor: hidden until hover, but ALWAYS revealed on keyboard focus
   so it can never be a hidden-yet-focusable trap. */
.doc-anchor {
  position: absolute;
  left: -1.1em;
  width: 1.1em;
  /* A real link. It is invisible at rest but fully visible on hover and on
     keyboard focus, so it carries the full 4.5:1 text budget. */
  color: var(--doc-muted);
  font-weight: 400;
  text-decoration: none;
  opacity: 0;
  transition: opacity var(--doc-dur) var(--doc-ease);
}

.doc-h2:hover > .doc-anchor,
.doc-h3:hover > .doc-anchor,
.doc-h4:hover > .doc-anchor,
.doc-anchor:focus-visible { opacity: 1; }

.doc-anchor:hover { color: var(--doc-fg); }

/* Below the width where the hanging anchor has a margin to live in, hide it
   the visually-hidden way rather than with opacity. `width:0` does nothing to
   an inline box, so an opacity-0 anchor still occupies its glyph advance and
   visibly indents every heading. Clipping it out of flow keeps the headings
   flush AND keeps the anchor reachable by keyboard. */
@media (max-width: 1023px) {
  .doc-anchor {
    position: absolute;
    left: auto;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    opacity: 1;
  }
  .doc-anchor:focus-visible {
    position: static;
    width: auto;
    height: auto;
    clip-path: none;
    margin-right: 6px;
  }
}

/* Links are ink, not brand. Underline settles in on hover. */
.doc-link,
.doc-body a {
  color: var(--doc-fg);
  text-decoration: underline;
  text-decoration-color: var(--doc-border-strong);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: text-decoration-color var(--doc-dur) var(--doc-ease);
}

.doc-link:hover,
.doc-body a:hover { text-decoration-color: currentColor; }

.doc-link--ext svg {
  display: inline-block;
  width: 13px;
  height: 13px;
  margin-left: 3px;
  vertical-align: -1px;
  color: var(--doc-muted);
}

/* Lists */
.doc-body ul,
.doc-body ol {
  padding-left: 1.35em;
  color: var(--doc-body);
}

.doc-body li + li { margin-top: 7px; }
.doc-body li::marker { color: var(--doc-muted); }
.doc-body li > p + p { margin-top: 7px; }

.doc-body ul ul,
.doc-body ol ol,
.doc-body ul ol,
.doc-body ol ul { margin-top: 7px; }

/* Blockquote — hairline rail, no tint, no card. */
.doc-body blockquote {
  padding: 2px 0 2px 18px;
  border-left: 2px solid var(--doc-border-strong);
  color: var(--doc-muted);
}

.doc-body blockquote p { color: inherit; }
.doc-body blockquote > * + * { margin-top: 12px; }

/* Divider inside prose */
.doc-body hr {
  height: 1px;
  border: 0;
  background: var(--doc-border);
  margin-block: 34px;
}

/* ── Inline code chip ─────────────────────────────────────────────────────
   1390 of these across the site, and 46.5% of them are inside TABLE CELLS —
   24 cells hold three or more, one row holds ten. Every value below is set by
   that fact rather than by taste:

   · No border. Seven bordered chips in one paragraph reads as noise, and the
     fill alone already separates code from prose.
   · Vertical padding 0.08em, not the 0.12em it had. Chips taller than the line
     box push the leading apart, so a paragraph containing chips ends up more
     loosely spaced than the plain paragraph beside it.
   · box-decoration-break: clone. The column is 608px and 130 chips contain
     spaces, so they wrap; without this the second fragment loses its rounded
     corners and reads as a rendering fault.
   · margin-inline: 0.5px. Two chips with no word between them would otherwise
     touch and read as one long chip.
   ---------------------------------------------------------------------- */
.doc-body :not(pre) > code {
  padding: 0.08em 0.3em;
  margin-inline: 0.5px;
  border-radius: 4px;
  background: var(--doc-code-chip);
  color: var(--doc-fg);
  font-family: var(--doc-font-mono);
  font-size: 0.86em;
  overflow-wrap: anywhere;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

/* Three places the fill is wrong and only the mono face should remain.
   In a heading it breaks the line; in a table column where every cell holds
   one it turns the column into a grey wall; in a link it fights the link. */
.doc-body h1 :not(pre) > code,
.doc-body h2 :not(pre) > code,
.doc-body h3 :not(pre) > code,
.doc-body h4 :not(pre) > code,
.doc-tablewrap :not(pre) > code,
.doc-body a :not(pre) > code {
  background: transparent;
  padding-inline: 0;
  color: inherit;
}

.doc-body a > code { color: inherit; }

kbd {
  display: inline-block;
  min-width: 1.6em;
  padding: 1px 5px;
  border: 1px solid var(--doc-border-strong);
  border-bottom-width: 2px;
  border-radius: var(--doc-radius-xs);
  background: var(--doc-card);
  color: var(--doc-fg);
  font-family: var(--doc-font-mono);
  font-size: var(--doc-text-xs);
  text-align: center;
}

/* Figures */
.doc-body img {
  border: 1px solid var(--doc-border);
  border-radius: var(--doc-radius);
  background: var(--doc-card);
}

.doc-body figcaption {
  margin-top: 8px;
  color: var(--doc-muted);
  font-size: var(--doc-text-xs);
}

/* ── 13. Code blocks — the two-shell Efferd construction ─────────────────
   Outer   .doc-code           14px radius, hairline border, card fill, 4px pad
   Bar     .doc-code__bar      filename / language + the copy button
   Surface .doc-code__surface  10px radius, hairline border, sunken fill,
                               plus a 1px inner top highlight (the premium tell)
   <pre>                       scrolls HORIZONTALLY inside itself; the page
                               body never does.
   -------------------------------------------------------------------- */

.doc-code {
  padding: 4px;
  border: 1px solid var(--doc-border);
  border-radius: var(--doc-radius-card);
  background: var(--doc-card);
  box-shadow: var(--doc-shadow);
}

/* Bottom-aligned, and no bottom padding: the language tab has to touch the
   plate. Centred in a 32px bar it floats as a detached pill instead. */
.doc-code__bar {
  display: flex;
  align-items: flex-end;
  justify-content: flex-start;
  gap: 8px;
  min-height: 0;
  padding: 6px 6px 0 6px;
}

/* ── Language tab ────────────────────────────────────────────────────────
   The build already parsed the fence language, validated it, and stored it in
   data-lang — and then never showed it to anyone. A page that gives the same
   example in PHP and in JavaScript back to back left the reader guessing which
   was which.

   margin-bottom:-1px plus z-index:2 is what makes this read as a TAB rather
   than a pill: its bottom edge sits ON the plate's top border and covers it.
   .doc-code__surface::before already claims z-index 1, so 2 wins without
   touching it. */
.doc-code__lang {
  display: inline-flex;
  align-items: center;
  position: relative;
  z-index: 2;
  margin-bottom: -1px;
  padding: 4px 10px 5px;
  border: 1px solid var(--doc-code-line);
  border-bottom: 0;
  border-radius: var(--doc-radius-sm) var(--doc-radius-sm) 0 0;
  background: var(--doc-code-bg);
  color: var(--doc-code-label);
  font-family: var(--doc-font-mono);
  font-size: var(--doc-text-xs);
  line-height: 1.4;
  white-space: nowrap;
}

/* Optional filename, when the fence carries title=. No fill and no border, so
   it never competes with the tab beside it. */
.doc-code__file {
  align-self: center;
  min-width: 0;
  overflow: hidden;
  color: var(--doc-code-label);
  font-family: var(--doc-font-mono);
  font-size: var(--doc-text-xs);
  line-height: 1.5;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* Copy button to the far right; the tab and filename stay left. */
.doc-code__bar .doc-copy { margin-left: auto; align-self: center; }

.doc-code__surface {
  position: relative;
  border: 1px solid var(--doc-code-line);
  border-radius: var(--doc-radius);
  /* Square under the tab, so the tab's straight bottom edge meets a straight
     plate edge instead of leaving a crescent gap. */
  border-top-left-radius: 0;
  background: var(--doc-code-bg);
  overflow: hidden;
}

/* The 1px inner top highlight. */
.doc-code__surface::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: calc(var(--doc-radius) - 1px);
  box-shadow: inset 0 1px color-mix(in srgb, var(--doc-fg) 7%, transparent);
  pointer-events: none;
  z-index: 1;
}

.doc-code pre {
  margin: 0;
  padding: 14px 16px;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  background: transparent;
  scrollbar-width: thin;
  /* NOT --doc-border-strong. On the old near-invisible plate that measured
     1.24:1; on the darker plate it falls to 1.13:1, so strengthening the plate
     would have quietly drowned the only hint that a block scrolls sideways —
     and the widest line in the content is 507 characters. Measured on the new
     plate: 3.17:1 light, 3.58:1 dark. */
  scrollbar-color: var(--doc-code-scroll) transparent;
}

/* Focus ring for the code scroller. Inset is not a preference here, it is
   forced: .doc-code__surface is overflow:hidden, so an outside ring on the
   <pre> would be clipped away and the tab stop would look like nothing
   happened. The radius is restated because the global :focus-visible rule
   would otherwise stamp 3px onto an element that has no radius of its own,
   leaving a square ring inside a 10px surface.
   Contrast against --doc-sunken, the fill behind the code (min 3:1):
     light  #004060 on #f6f6f5 .... 10.21:1
     dark   #20b0c8 on #0e0e10 ..... 7.44:1                                 */
.doc-code pre[tabindex]:focus-visible {
  outline: 2px solid var(--doc-focus);
  outline-offset: -2px;
  border-radius: calc(var(--doc-radius) - 1px);
}

.doc-code pre::-webkit-scrollbar { height: 8px; }
.doc-code pre::-webkit-scrollbar-track { background: transparent; }
.doc-code pre::-webkit-scrollbar-thumb {
  background: var(--doc-code-scroll);
  border-radius: 8px;
  border: 2px solid transparent;
  background-clip: content-box;
}

.doc-code code {
  display: block;
  color: var(--doc-fg);
  font-family: var(--doc-font-mono);
  font-size: var(--doc-text-code);
  line-height: 1.65;
  tab-size: 2;
  white-space: pre;
}

/* ── Syntax colours ──────────────────────────────────────────────────────
   The generator has been emitting these nine classes into every code block
   for some time. Until now docs.css had not one rule for any of them, so all
   nine inherited `color: var(--doc-fg)` from .doc-code code above and every
   token in every block rendered in the same single colour. That is what
   "hitam semua" was describing, literally.

   SCOPING TO .doc-code IS LOAD-BEARING, not tidiness. Unscoped, these would
   also catch inline code if the inline renderer ever started highlighting,
   and a paragraph whose `status`, `paid`, and `shipped` each carried a
   different hue is a striped paragraph, not a readable one. Inline code is
   separated from prose by its FIELD, never by hue.

   Contrast is measured against --doc-code-bg, not against the page — see the
   CODE PLATE section of the ledger at the top of this file.

   Weight and slant are second channels. Keywords sit next to keys constantly
   ("ok": true), and comments next to everything; a reader who cannot separate
   the hues still has one more signal on each of those pairs. */
.doc-code .doc-t-str { color: var(--doc-t-str); }
.doc-code .doc-t-num { color: var(--doc-t-num); }
.doc-code .doc-t-pun { color: var(--doc-t-pun); }

/* Keys, $variables and HTML attributes are all names-of-something; they share
   one hue and are told apart by their own glyphs — the dollar sign, the
   equals sign — not by colour. */
.doc-code .doc-t-key,
.doc-code .doc-t-var,
.doc-code .doc-t-attr { color: var(--doc-t-key); }

.doc-code .doc-t-kw,
.doc-code .doc-t-tag {
  color: var(--doc-t-kw);
  font-weight: 500;
}

.doc-code .doc-t-com {
  color: var(--doc-t-com);
  font-style: italic;
}

/* Copy button. Styled here; the wiring lives in theme/docs.js.
   Supports both the `[data-copied]` attribute and the `.is-copied` class. */
.doc-copy {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--doc-radius-sm);
  background: transparent;
  color: var(--doc-muted);
  cursor: pointer;
  transition: background-color var(--doc-dur) var(--doc-ease),
              color var(--doc-dur) var(--doc-ease);
}

/* 44px hit area without inflating the 28px visual. */
.doc-copy::before {
  content: "";
  position: absolute;
  inset: -8px;
}

.doc-copy:hover {
  background: var(--doc-hover);
  color: var(--doc-fg);
}

.doc-copy svg {
  position: absolute;
  width: 14px;
  height: 14px;
  transition: opacity var(--doc-dur) var(--doc-ease),
              transform var(--doc-dur) var(--doc-ease);
}

.doc-copy .doc-i-copy { opacity: 1; transform: scale(1); }

/* BRAND ACCENT 2 of 3 — the copy-success checkmark. */
.doc-copy .doc-i-check {
  opacity: 0;
  transform: scale(0.6);
  color: var(--doc-accent);
}

.doc-copy[data-copied] .doc-i-copy,
.doc-copy.is-copied .doc-i-copy { opacity: 0; transform: scale(0.6); }

.doc-copy[data-copied] .doc-i-check,
.doc-copy.is-copied .doc-i-check { opacity: 1; transform: scale(1); }

/* ── 14. Tables — API reference: parameter / type / required / description ──
   THE WRAPPER SCROLLS. THE PAGE DOES NOT. Both halves of that need saying:

   · The page body never scrolls sideways — body has `overflow-x: clip` and
     every grid child has `min-width: 0`.
   · A table wider than the wrapper therefore scrolls INSIDE .doc-tablewrap.

   That second half used to be true only in theory. macOS and iOS hide overlay
   scrollbars until you actually scroll, so a clipped column looked simply
   absent: on the API Produk error table the whole fourth column ("Penyebab")
   sat outside the wrapper, still wrapping its text, which left every row tall
   and visibly empty. Nothing on screen said "there is more to the right".

   The fix is in two parts, and BOTH are required:
     1. Stop manufacturing overflow that was never needed — see the sizing
        rules below. Six of six tables on the API Produk page overflowed; four
        of those did so purely because of a hard min-width floor.
     2. Where a table genuinely IS too wide, SAY SO. The layered background
        below is the Kizu scroll-shadow: the two `local` layers scroll with the
        content and mask the two `scroll` layers pinned to the box, so an edge
        shadow appears only while there is hidden content on that side and
        fades out exactly when you reach the end. No JavaScript, no measuring.

   KEYBOARD ACCESS — the gap noted here previously is CLOSED. Three parts,
   split across three files because no single one of them can do the job:
     · scripts/build-docs.mjs emits tabindex="0" role="region" aria-label="…"
       on this wrapper. Static HTML cannot know the rendered width, so it
       marks EVERY table — the no-JS fallback is "operable", not "silent".
     · theme/docs.js measures scrollWidth vs clientWidth after layout and
       REMOVES tabindex + role from wrappers whose table already fits, then
       re-evaluates on resize and on font load. A tab stop that cannot scroll
       is noise, and the brief is explicit that adding noise makes it worse.
     · this file styles the focus ring, below. Without it the tab stop is
       invisible and the change is a net loss for sighted keyboard users.
   The [tabindex] attribute selector is what ties them together: it matches
   only the wrappers docs.js has confirmed actually scroll.
   ---------------------------------------------------------------------- */

.doc-tablewrap {
  position: relative;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  border: 1px solid var(--doc-border);
  border-radius: var(--doc-radius);
  box-shadow: var(--doc-shadow);
  scrollbar-width: thin;
  scrollbar-color: var(--doc-border-strong) transparent;

  /* Scroll shadow. Order matters: the two masking layers must sit ON TOP of
     the two shadow layers, so they are listed first. */
  background:
    linear-gradient(to right, var(--doc-card) 40%, rgba(255, 255, 255, 0))
      0 0 / 34px 100% no-repeat local,
    linear-gradient(to left, var(--doc-card) 40%, rgba(255, 255, 255, 0))
      100% 0 / 34px 100% no-repeat local,
    radial-gradient(farthest-side at 0 50%, var(--doc-edge), rgba(255, 255, 255, 0))
      0 0 / 15px 100% no-repeat scroll,
    radial-gradient(farthest-side at 100% 50%, var(--doc-edge), rgba(255, 255, 255, 0))
      100% 0 / 15px 100% no-repeat scroll,
    var(--doc-card);
}

/* Focus ring, INSET. The global :focus-visible draws at outline-offset 2px
   with a 3px radius — both wrong here: an outside ring on a full-width block
   collides with the paragraph above it, and 3px would square off the 14px
   corner while focused. Inset by 2px the ring traces the wrapper's own corner
   and stays inside the scroll shadow.
   Contrast, computed against --doc-card, the fill this wrapper sits on
   (WCAG 1.4.11 non-text, minimum 3:1):
     light  #004060 on #ffffff .... 11.04:1
     dark   #20b0c8 on #101012 ..... 7.34:1                                 */
.doc-tablewrap[tabindex]:focus-visible {
  outline: 2px solid var(--doc-focus);
  outline-offset: -2px;
  border-radius: var(--doc-radius);
}

.doc-tablewrap::-webkit-scrollbar { height: 8px; }
.doc-tablewrap::-webkit-scrollbar-track { background: transparent; }
.doc-tablewrap::-webkit-scrollbar-thumb {
  background: var(--doc-border-strong);
  border-radius: 8px;
  border: 2px solid transparent;
  background-clip: content-box;
}

.doc-tablewrap table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--doc-text-sm);
  line-height: 1.6;
}

/* NO UNCONDITIONAL min-width. There used to be a flat `min-width: 42rem`
   (672px) here. The prose column is 38rem (608px), so that floor guaranteed
   at least 64px of overflow on EVERY table on the site, including three-column
   tables whose content fitted comfortably. Measured on /docs/api-products/ at
   1440px: four of the six tables overflowed by exactly 66px and needed to
   scroll for no reason at all.

   The floor only ever earned its keep on a phone, where a table really would
   compress to porridge. So it now applies only there. */
@media (max-width: 899px) {
  .doc-tablewrap table { min-width: 34rem; }
}

.doc-tablewrap caption {
  padding: 10px 14px;
  border-bottom: 1px solid var(--doc-border);
  color: var(--doc-muted);
  font-size: var(--doc-text-xs);
  text-align: left;
}

.doc-tablewrap thead th {
  padding: 9px 14px;
  border-bottom: 1px solid var(--doc-border);
  background: var(--doc-sunken);
  color: var(--doc-label);
  font-size: var(--doc-text-xs);
  font-weight: 500;
  letter-spacing: 0.06em;
  text-align: left;
  text-transform: uppercase;
  white-space: nowrap;
}

.doc-tablewrap tbody td,
.doc-tablewrap tbody th {
  padding: 11px 14px;
  border-top: 1px solid var(--doc-border);
  color: var(--doc-body);
  text-align: left;
  vertical-align: top;
}

.doc-tablewrap tbody tr:first-child td,
.doc-tablewrap tbody tr:first-child th { border-top: 0; }

.doc-tablewrap tbody th { color: var(--doc-fg); font-weight: 500; }

.doc-tablewrap tbody tr:hover td,
.doc-tablewrap tbody tr:hover th { background: var(--doc-sunken); }

/* Identifiers inside a table must never break mid-token — `string` splitting
   into `strin` / `g` is worse than a scrollbar.

   `white-space: nowrap` was the wrong instrument for that. Normal wrapping
   NEVER splits a word; only `word-break: break-all` does, and nothing here
   asks for it. What nowrap actually did was force the widest single string in
   a column to become that column's minimum width: on the error table the
   Body column pinned itself to 438px to keep
   `{"error":"Terlalu banyak permintaan. Coba lagi nanti."}` on one line, and
   that one cell is most of the reason the fourth column ended up off-screen.

   So: wrap at SPACES, and never inside a token.

   `overflow-wrap: break-word` was tried here and is wrong — it drops the
   column's min-content width to about one character, so the auto table
   algorithm hands the column almost nothing and then breaks every value mid
   word to fit: `{"error":"Malform` / `ed API key"}`. Keeping overflow-wrap at
   `normal` makes min-content equal the longest single token, which is what
   gives the column an honest floor to bargain with.

   A token genuinely wider than the whole frame will still push the table out
   and hand it to the wrapper's scroll — which is now visibly signposted.

   SPECIFICITY, and this is the whole ballgame: the inline-code rule in §12 is
   `.doc-body :not(pre) > code` = (0,1,2), which outranks a plain
   `.doc-tablewrap code` = (0,1,1). That rule sets `overflow-wrap: anywhere`,
   so table code was inheriting ANYWHERE no matter what was written here —
   which both broke tokens mid-word and dropped the column's min-content to
   nearly zero, starving it in the auto layout. The old `white-space: nowrap`
   appeared to work only because §12 never sets white-space, so that one
   property got through while the others silently lost. Matching the shape of
   the §12 selector puts this rule back on equal footing, and it wins the tie
   by coming later in the file. Do not flatten it back to `.doc-tablewrap
   code` — that is a silent no-op.

   This exact selector also appears in §12, where it drops the chip fill for
   table cells. Two rules, same selector, disjoint properties — deliberately
   kept apart because they answer two different questions ("how does code wrap
   in a table" here, "does code get a fill here" there). Both must keep the
   (0,2,2) shape for the reason above. */
.doc-tablewrap :not(pre) > code {
  font-family: var(--doc-font-mono);
  font-size: 0.92em;
  white-space: normal;
  word-break: normal;
  overflow-wrap: normal;
  hyphens: none;
}

/* Short leading columns stay on one line; the description column absorbs the
   slack. `th` is the row header (the parameter name) and must not wrap. */
.doc-tablewrap tbody th:not(:last-child) { white-space: nowrap; }

/* The description column's floor. This is a genuine trade-off and it was
   measured rather than guessed — swept across the six tables on
   /docs/api-products/ at 1440px, reading overflow and row height per value:

     floor     tables overflowing      worst row on the 5-column table
     10rem     1  (3px)                248px
     11rem     1  (19px)               225px
     14rem     1  (67px)               158px   ← chosen
     16rem     2  (99px + 5px)         136px

   Below ~14rem the description column is starved and the rows grow tall;
   above it a second table starts scrolling for no good reason. 14rem puts
   five of the six tables at exactly zero overflow, and leaves the one table
   that genuinely cannot fit 5 columns and a prose description into 608px
   scrolling by 67px — which the scroll shadow now announces.

   Re-measure this before changing it. */
.doc-tablewrap tbody td:last-child,
.doc-tablewrap tbody th:last-child { min-width: 14rem; }

/* Required flag: a word plus a glyph — never colour alone. */
.doc-req,
.doc-opt {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-family: var(--doc-font-mono);
  font-size: var(--doc-text-xs);
  letter-spacing: 0.02em;
  white-space: nowrap;
}

.doc-req { color: var(--doc-fg); font-weight: 500; }
.doc-opt { color: var(--doc-muted); }

.doc-req::before {
  content: "";
  width: 5px;
  height: 5px;
  flex: none;
  border-radius: 50%;
  background: currentColor;
}

.doc-opt::before {
  content: "";
  width: 5px;
  height: 5px;
  flex: none;
  border-radius: 50%;
  border: 1px solid currentColor;
}

/* ── 15. Callouts — note / warning / danger ──────────────────────────────
   Differentiated on FOUR channels so colour is never the sole signal:
     1. a distinct icon SHAPE  (circle / triangle / octagon)
     2. a visible text LABEL
     3. left border WEIGHT + STYLE (2px solid / 3px solid / 3px double)
     4. a restrained semantic hue
   Icons are SVG (CSS masks, inlined as data URIs — no network, no emoji).
   -------------------------------------------------------------------- */

.doc-callout {
  display: grid;
  grid-template-columns: 18px minmax(0, 1fr);
  gap: 4px 11px;
  padding: 14px 16px;
  border: 1px solid var(--doc-border);
  border-left-width: 2px;
  border-left-color: var(--doc-border-strong);
  border-radius: var(--doc-radius);
  background: var(--doc-card);
  color: var(--doc-body);
}

.doc-callout::before {
  content: "";
  grid-row: 1 / span 2;
  grid-column: 1;
  width: 18px;
  height: 18px;
  background-color: var(--doc-muted);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: 18px 18px;
  mask-size: 18px 18px;
}

.doc-callout__label {
  grid-column: 2;
  margin: 0;
  color: var(--doc-fg);
  font-size: var(--doc-text-xs);
  font-weight: 500;
  letter-spacing: 0.07em;
  line-height: 18px;
  text-transform: uppercase;
}

.doc-callout__body { grid-column: 2; min-width: 0; }
.doc-callout__body > * { margin: 0; }
.doc-callout__body > * + * { margin-top: 10px; }

.doc-callout__body p {
  color: inherit;
  font-size: var(--doc-text-sm);
  line-height: 1.65;
}

/* NOTE — circle + i */
.doc-callout--note::before {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 11.25v4.75'/%3E%3Cpath d='M12 7.9h.01'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 11.25v4.75'/%3E%3Cpath d='M12 7.9h.01'/%3E%3C/svg%3E");
}

/* WARNING — triangle + ! */
.doc-callout--warning {
  border-color: var(--doc-warn-line);
  border-left-width: 3px;
  border-left-color: var(--doc-warn);
  background: var(--doc-warn-bg);
}

.doc-callout--warning .doc-callout__label { color: var(--doc-warn); }

.doc-callout--warning::before {
  background-color: var(--doc-warn);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z'/%3E%3Cpath d='M12 9v4.5'/%3E%3Cpath d='M12 17.2h.01'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z'/%3E%3Cpath d='M12 9v4.5'/%3E%3Cpath d='M12 17.2h.01'/%3E%3C/svg%3E");
}

/* DANGER — octagon + × */
.doc-callout--danger {
  border-color: var(--doc-danger-line);
  border-left-width: 3px;
  border-left-style: double;
  border-left-color: var(--doc-danger);
  background: var(--doc-danger-bg);
}

.doc-callout--danger .doc-callout__label { color: var(--doc-danger); }

.doc-callout--danger::before {
  background-color: var(--doc-danger);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7.86 2h8.28L22 7.86v8.28L16.14 22H7.86L2 16.14V7.86z'/%3E%3Cpath d='m14.5 9.5-5 5'/%3E%3Cpath d='m9.5 9.5 5 5'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7.86 2h8.28L22 7.86v8.28L16.14 22H7.86L2 16.14V7.86z'/%3E%3Cpath d='m14.5 9.5-5 5'/%3E%3Cpath d='m9.5 9.5 5 5'/%3E%3C/svg%3E");
}

/* ── 16. Pager ───────────────────────────────────────────────────────── */

.doc-pager {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
  margin-top: 52px;
  padding-top: 28px;
  border-top: 1px solid var(--doc-border);
}

.doc-pager:empty { display: none; }

.doc-pager__link {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 13px 16px;
  border: 1px solid var(--doc-border);
  border-radius: var(--doc-radius);
  background: var(--doc-card);
  text-decoration: none;
  transition: border-color var(--doc-dur) var(--doc-ease),
              background-color var(--doc-dur) var(--doc-ease);
}

.doc-pager__link:hover {
  border-color: var(--doc-border-strong);
  background: var(--doc-sunken);
}

/* Both modifiers are declared, not just --next. The prev link would land in
   column 1 by auto-placement anyway, but only while a next link exists to push
   it: on the LAST page of a group the pager holds a single child, and without
   an explicit column the browser is free to place it wherever the flow lands.
   Declaring both also stops the build's class audit reporting --prev as a class
   the markup emits with no rule behind it. */
.doc-pager__link--prev {
  grid-column: 1;
  align-items: flex-start;
  text-align: left;
}

.doc-pager__link--next {
  grid-column: 2;
  align-items: flex-end;
  text-align: right;
}

/* --doc-label, not --doc-muted: .doc-pager__link:hover swaps the fill to
   --doc-sunken, where --doc-muted measures 4.47:1 and fails. The rest state on
   --doc-card was fine (4.83:1), which is exactly why this was missed. */
.doc-pager__dir {
  color: var(--doc-label);
  font-size: var(--doc-text-xs);
  letter-spacing: 0.02em;
}

.doc-pager__title {
  color: var(--doc-fg);
  font-size: var(--doc-text-sm);
  font-weight: 500;
  line-height: 1.45;
}

@media (max-width: 599px) {
  .doc-pager { grid-template-columns: minmax(0, 1fr); }
  .doc-pager__link--next { grid-column: 1; }
}

/* ── 17. Article footer ──────────────────────────────────────────────── */

.doc-footer {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 6px 16px;
  margin-top: 32px;
  padding-top: 18px;
  border-top: 1px solid var(--doc-border);
  color: var(--doc-muted);
  font-size: var(--doc-text-xs);
}

.doc-footer p { margin: 0; }
.doc-footer p:empty { display: none; }

.doc-footer a {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: var(--doc-border-strong);
  text-underline-offset: 3px;
  overflow-wrap: anywhere;
}

.doc-footer a:hover { color: var(--doc-fg); }

.doc-footer__updated,
.doc-footer__build { font-family: var(--doc-font-mono); }
/* Inherits the footer's --doc-muted. This is the build/version string — a
   reader quoting it in a bug report has to be able to read it. It was set in
   the old --doc-faint tier at 2.43:1 light / 4.07:1 dark, failing AA in both. */
.doc-footer__build { color: inherit; }
.doc-footer__md { flex: 1 1 100%; order: 3; }

/* ── 18. TOC rail ────────────────────────────────────────────────────── */

.doc-tocrail__inner,
.doc-tocrail > .doc-toc {
  position: sticky;
  top: var(--doc-topbar-h);
  max-height: calc(100vh - var(--doc-topbar-h));
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 30px var(--doc-gutter) 40px;
  scrollbar-width: thin;
  scrollbar-color: transparent transparent;
}

.doc-tocrail__inner:hover,
.doc-tocrail > .doc-toc:hover {
  scrollbar-color: var(--doc-border-strong) transparent;
}

/* No TOC emitted (page has fewer than two headings) → the rail stays blank
   rather than showing an empty sticky box. Requires the build to emit '' with
   no surrounding whitespace, which page.html is written to allow. */
.doc-toc:empty { display: none; }

.doc-toc__title {
  margin: 0 0 10px;
  color: var(--doc-label);
  font-size: var(--doc-text-xs);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.doc-toc ul {
  margin: 0;
  padding: 0;
  list-style: none;
  border-left: 1px solid var(--doc-border);
}

.doc-toc li { margin: 0; }

.doc-toc__link {
  display: block;
  margin-left: -1px;
  padding: 5px 0 5px 12px;
  border-left: 1px solid transparent;
  color: var(--doc-muted);
  font-size: var(--doc-text-xs);
  line-height: 1.5;
  text-decoration: none;
  transition: color var(--doc-dur) var(--doc-ease),
              border-color var(--doc-dur) var(--doc-ease);
}

.doc-toc__link:hover {
  color: var(--doc-fg);
  border-left-color: var(--doc-border-strong);
}

/* Active: marker + weight + ink colour. Three channels, never colour alone. */
.doc-toc__link[data-active],
.doc-toc__link.is-active {
  color: var(--doc-fg);
  font-weight: 500;
  border-left-color: var(--doc-fg);
}

.doc-toc__link:focus-visible { outline-offset: -2px; }

.doc-toc__lvl3 > .doc-toc__link { padding-left: 24px; }

/* ── 19. Mobile refinements ──────────────────────────────────────────── */

@media (max-width: 899px) {
  .doc-col {
    padding-inline: var(--doc-gutter);
    padding-bottom: 56px;
  }
  .doc-article { max-width: none; }
  .doc-breadcrumb { padding-top: 20px; }
  .doc-title { font-size: 1.625rem; }
  .doc-lede { font-size: var(--doc-text-base); }
  /* Body copy never drops below 16px on mobile. */
  body { font-size: var(--doc-text-base); }
}

/* ── 20. Search ───────────────────────────────────────────────────────────
   Monochrome like everything else. The only colour in this whole component is
   the focus ring, which is the theme's existing petrol — the highlight on a
   matched word is a tinted SURFACE, not brand-coloured text, because brand
   cyan measures 2.48:1 on white and cannot legally carry text here.

   VISIBILITY IS OPT-IN. The trigger stays display:none until docs.js puts
   data-search-ready on <html>. With JavaScript off there is no index, and a
   control that opens nothing is worse than no control at all.

   CONTRAST (measured on the rendered dialog, both schemes, AA 4.5:1 text):
     result title ............. 15.94 / 16.21
     result snippet ........... 10.01 / 15.50   (--doc-body)
     breadcrumb line 12px .....  5.48 /  9.24   (--doc-label)
     placeholder ..............  4.63 /  7.68   (--doc-muted on --doc-card)
     mark, ink on tinted fill .. 12.63 / 13.02
     footer key hints 12px ....  5.48 /  9.24
   ---------------------------------------------------------------------- */

.doc-searchbtn { display: none; }

:root[data-search-ready] .doc-searchbtn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 32px;
  padding: 0 8px 0 10px;
  border: 1px solid var(--doc-border-strong);
  border-radius: var(--doc-radius-sm);
  background: var(--doc-card);
  color: var(--doc-muted);
  font: inherit;
  font-size: var(--doc-text-sm);
  line-height: 1;
  cursor: pointer;
  transition: background-color var(--doc-dur) var(--doc-ease),
              border-color var(--doc-dur) var(--doc-ease),
              color var(--doc-dur) var(--doc-ease);
}

.doc-searchbtn:hover {
  color: var(--doc-fg);
  border-color: var(--doc-muted);
}

.doc-searchbtn svg { width: 15px; height: 15px; flex: none; }
.doc-searchbtn__text { white-space: nowrap; }

/* Named so the magnifier can be sized independently of whichever control it
   sits in — the trigger and the dialog bar want different sizes. */
.doc-i-search { flex: none; }

/* Keyboard hint. Never the only way in — the button is clickable and the
   dialog is reachable by tab order. */
.doc-kbd {
  padding: 2px 5px;
  border: 1px solid var(--doc-border);
  border-radius: var(--doc-radius-xs);
  background: var(--doc-sunken);
  color: var(--doc-label);
  font-family: var(--doc-font-mono);
  font-size: 0.6875rem;   /* 11px — a hint, not content */
  line-height: 1.4;
  white-space: nowrap;
}

/* Under 899px the label and the key hint go; the magnifier alone stays, at a
   34px box that matches the theme toggle beside it. */
@media (max-width: 899px) {
  :root[data-search-ready] .doc-searchbtn {
    width: 34px;
    justify-content: center;
    padding: 0;
    border-color: transparent;
    background: transparent;
  }
  .doc-searchbtn__text,
  .doc-searchbtn .doc-kbd { display: none; }
  .doc-searchbtn:hover { background: var(--doc-hover); border-color: transparent; }
}

/* ── dialog ───────────────────────────────────────────────────────────── */

.doc-searchdlg {
  width: min(640px, calc(100vw - 32px));
  max-height: min(560px, calc(100vh - 96px));
  margin: 0 auto;
  /* Sits high rather than centred: the results grow downward and a centred box
     makes the list jump as it fills. */
  margin-block: 12vh auto;
  padding: 0;
  border: 1px solid var(--doc-border);
  border-radius: var(--doc-radius-card);
  background: var(--doc-card);
  color: var(--doc-fg);
  box-shadow: 0 12px 40px rgba(20, 20, 16, 0.14), var(--doc-shadow);
  overflow: hidden;
}

.doc-searchdlg::backdrop {
  background: rgba(10, 10, 10, 0.34);
  backdrop-filter: blur(2px);
}

.doc-searchdlg[open] {
  display: flex;
  flex-direction: column;
  animation: doc-search-in 140ms var(--doc-ease);
}

@keyframes doc-search-in {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .doc-searchdlg[open] { animation: none; }
}

.doc-searchdlg__bar {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: none;
  padding: 0 10px 0 16px;
  height: 52px;
  border-block-end: 1px solid var(--doc-border);
  color: var(--doc-muted);
}

.doc-searchdlg__bar svg { width: 17px; height: 17px; flex: none; }

.doc-searchdlg__input {
  flex: 1 1 auto;
  min-width: 0;
  height: 100%;
  border: 0;
  background: transparent;
  color: var(--doc-fg);
  font-size: var(--doc-text-base);   /* 16px — below this iOS zooms the page */
  outline: none;
}

.doc-searchdlg__input::placeholder { color: var(--doc-muted); }

/* The field owns the whole bar visually, so the ring goes on the bar. */
.doc-searchdlg__bar:focus-within {
  outline: 2px solid var(--doc-focus);
  outline-offset: -2px;
  border-radius: var(--doc-radius-card) var(--doc-radius-card) 0 0;
}

.doc-searchdlg__body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;   /* scrolling the list must not scroll the page */
  padding: 6px;
}

.doc-searchnote {
  margin: 0;
  padding: 22px 12px 26px;
  color: var(--doc-muted);
  font-size: var(--doc-text-sm);
  text-align: center;
}

.doc-searchlist {
  margin: 0;
  padding: 0;
  list-style: none;
}

.doc-searchitem { margin: 0; }

.doc-searchitem__link {
  display: block;
  padding: 9px 11px;
  border-radius: var(--doc-radius);
  color: inherit;
  text-decoration: none;
}

/* Hover and keyboard selection are the SAME treatment. Two different ones let
   the pointer and the arrow keys disagree about which row is "current". */
.doc-searchitem__link:hover,
.doc-searchitem__link[aria-selected="true"] {
  background: var(--doc-sunken);
}

.doc-searchitem__crumb {
  display: block;
  color: var(--doc-label);
  font-size: var(--doc-text-xs);
  letter-spacing: 0.02em;
}

.doc-searchitem__title {
  display: block;
  margin-top: 2px;
  color: var(--doc-fg);
  font-size: var(--doc-text-sm);
  font-weight: 500;
}

.doc-searchitem__snip {
  display: block;
  margin-top: 3px;
  color: var(--doc-body);
  font-size: var(--doc-text-sm);
  line-height: 1.5;
  /* Two lines, then clip. A section can be several paragraphs long and an
     un-clamped snippet would push the next result off the visible list. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.doc-searchmark {
  padding: 0 1px;
  border-radius: 3px;
  background: var(--doc-hover);
  color: var(--doc-fg);
  font-weight: 500;
}

.doc-searchdlg__foot {
  display: flex;
  gap: 16px;
  flex: none;
  padding: 8px 14px;
  border-block-start: 1px solid var(--doc-border);
  background: var(--doc-sunken);
  color: var(--doc-label);
  font-size: var(--doc-text-xs);
}

.doc-searchdlg__foot span {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}

/* Under 599px the key legend is noise — that reader has no keyboard. */
@media (max-width: 599px) {
  .doc-searchdlg__foot { display: none; }
  .doc-searchdlg { margin-block: 6vh auto; max-height: calc(100vh - 60px); }
}

/* ── 21. Print ───────────────────────────────────────────────────────── */

@media print {
  .doc-searchbtn,
  .doc-topbar,
  .doc-sidebar,
  .doc-tocrail,
  .doc-pager,
  .doc-copy,
  .doc-skip { display: none !important; }

  .doc-shell::before,
  .doc-shell::after { display: none; }

  .doc-shell { display: block; min-height: 0; }
  .doc-col { border: 0; padding: 0; }
  .doc-article { max-width: none; }

  body { background: #fff; color: #000; }

  .doc-code,
  .doc-tablewrap,
  .doc-callout { break-inside: avoid; box-shadow: none; }

  .doc-body a::after {
    content: " (" attr(href) ")";
    color: #555;
    font-size: 0.85em;
  }
}
