/* ============================================================
   oku · chrome.css
   Shared visual chrome for long-form HTML artifacts.
   Per-page overrides: define your own --accent / --accent-soft
   in a small <style> block on the page itself.
   ============================================================ */

/* Web fonts, from the copy that travels with the kit.

   This was an @import of fonts.googleapis.com, on every page of every
   built tree. Three costs, and the first is the one that decided it:
   every reader of every delivered document announced themselves — IP,
   referrer, page URL — to a third party, which is the thing the
   rebuild button was deliberately built NOT to do ("a document that
   calls home when a colleague opens it is worse than the bug it would
   report"). Second, a page opened with no network lost the typography
   the layout is measured in. Third, 330 ms on the critical path, since
   an @import inside a render-blocking stylesheet is a second serial
   round trip before the first paint.

   `oku vendor` fetches four variable woff2 (Inter + JetBrains Mono,
   latin + latin-ext — Turkish needs latin-ext, and this kit ships
   Turkish pages) into `vendor/fonts/` beside this file. The URLs below
   are relative to THIS stylesheet, so they resolve under `oku serve`
   and in `dist/site` without anything being injected; the standalone
   build rewrites them to its own `_oku/vendor/` copy, and drops these
   blocks entirely when the fonts were never fetched, so a page can
   never ship a font URL that reaches outside itself.

   Absent the vendored files the browser falls back to the system stack
   named in each font-family declaration, which is why `font-display`
   is swap and why no @font-face here names a network host. */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("vendor/fonts/inter-latin-wght-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+2074, U+20AC, U+2122, U+2191, U+2193, U+2212,
    U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("vendor/fonts/inter-latin-ext-wght-normal.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
    U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 100 800;
  font-display: swap;
  src: url("vendor/fonts/jetbrains-mono-latin-wght-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+2074, U+20AC, U+2122, U+2191, U+2193, U+2212,
    U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 100 800;
  font-display: swap;
  src: url("vendor/fonts/jetbrains-mono-latin-ext-wght-normal.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
    U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ============ Tokens — light ============ */
:root {
  /* The reader's text scale. chrome-boot.js overwrites it before first
     paint from localStorage; declared here so a page whose script never
     ran still resolves every `zoom` below to 1 rather than to `normal`. */
  --oku-text-scale: 1;
  --bg: #fafaf9;
  --surface: #ffffff;
  /* Quiet raised/recessed surface — table header band, chart tracks,
     code background, TOC hover. It was #f5f3ff, which carries 12 points
     of blue over red and reads as lavender. That looks intentional
     beside the default indigo accent and looks like an unrelated hue
     beside any other one, on 38 surfaces at once. Same family, a
     quarter of the saturation. */
  --surface-2: #f5f4f8;
  --text: #1e1b29;
  --text-soft: #4c4760;
  --text-faint: #8b85a3;
  --border: #e4e1ee;
  --border-soft: #f0edf7;
  --primary: #1e1b29;
  --accent: #6366f1;
  --accent-soft: #e0e7ff;
  --accent-strong: #4338ca;
  --warning: #b45309;
  --warning-soft: #fef3c7;
  --danger: #b91c1c;
  --danger-soft: #fee2e2;
  --success: #15803d;
  --success-soft: #dcfce7;
  --on-accent: #ffffff;
  --max-width: 960px;
  /* Reader-controlled content width (D3 + P7). The default is the
     'comfortable' reading width — a hair wider than the prior 860px
     so visual primitives (kpi-grid, charts, tables) don't crowd at
     full-token zoom. Narrow / wide / max are picked via the top-right
     chrome button and persist to localStorage. */
  --content-width: 960px;
  /* Line-length cap for running prose. Visual primitives (table, chart,
     kpi-grid, compare-grid, step-flow, diagram, code, annotated-code)
     always fill --content-width; only paragraphs and lists clamp here.
     Expressed in `ch` and re-declared per content-width mode below, so
     the width toggle still moves prose in the same direction as
     everything else — just on a curve suited to reading rather than to
     a chart. The per-mode values are the source of truth; this is the
     fallback for a page with no body attribute yet. */
  --prose-width: 62ch;
  /* One source of truth for the Contents panel's width: page-nav is
     this wide, and the pinned inset is exactly this much. Two numbers
     that had to agree would drift the first time one was tuned. */
  --oku-drawer-w: min(320px, 88vw);
  --shadow-strong: rgba(30, 27, 41, 0.18);
  --shadow-soft:   rgba(30, 27, 41, 0.06);
  --ctrl-bg:        rgba(255, 255, 255, 0.85);
  --ctrl-bg-hover:  rgba(245, 243, 255, 0.95);
  --line-strong:    #cbc7dd;
  --scrim:          rgba(30, 27, 41, 0.45);
  /* Code block fill — clearly separated from the page bg so fenced
     code reads as a distinct surface at a glance. */
  --code-bg:        #eef0f6;
  /* Extended chart palette — light theme. The 'series-N' tokens are
     used when authors don't pin a colour on individual series; the
     chart renderer rotates through them for series 6+ that the
     5-name palette (accent/warn/danger/success/muted) can't cover. */
  --series-1: #0f766e;
  --series-2: #b45309;
  --series-3: #4338ca;
  --series-4: #c2410c;
  --series-5: #15803d;
  --series-6: #be185d;
  --series-7: #0369a1;
  /* One step darker than the rest of the ramp was written at: every
     other light-theme series is a 700, this one was a 600, and it was
     the only colour in either theme under the 3:1 WCAG asks of a
     graphical object — 2.82 against --surface-2. At 700 it clears on
     every surface. See test_colour_contrast.py. */
  --series-8: #4d7c0f;
  --series-9: #7c2d12;
  --series-10: #4b5563;
  /* The fill that goes with each series colour. A figure that groups
     nodes by category needs a plate to write on, and the ramp above is
     for marks: `fill:var(--series-3)` under `color:var(--text)` is
     dark-on-dark in one theme and light-on-light in the other. These
     are the tint of the same hue — pale here, deep in the dark theme —
     so `fill:var(--series-N-soft), stroke:var(--series-N),
     color:var(--text)` reads in both. It is the vocabulary a mermaid
     `classDef` has to use, since Mermaid's grammar takes CSS values and
     not CSS functions, so `color-mix()` is not available to it. */
  --series-1-soft: #ccfbf1;
  --series-2-soft: #fef3c7;
  --series-3-soft: #e0e7ff;
  --series-4-soft: #ffedd5;
  --series-5-soft: #dcfce7;
  --series-6-soft: #fce7f3;
  --series-7-soft: #e0f2fe;
  --series-8-soft: #ecfccb;
  --series-9-soft: #fed7aa;
  --series-10-soft: #e5e7eb;
}
:root[data-theme="light"] { color-scheme: light; }
/* Not in print. Paper is white, and every token below is chosen for a
   dark screen — a page printed while the reader had dark mode on came
   out with `pre` at #e8e6f5 on a white body (1.13:1, code blocks
   blank) and cards painted #15122a with black text on them (1.19:1).
   The print block underneath repainted `body` and the headings and
   could not reach anything token-driven, which is most of the page.
   `@media not print` is how the light values on :root above win back,
   without a second copy of thirty-six tokens for someone to keep in
   step. */
@media not print {
:root[data-theme="dark"] {
  --bg: #0e0c1c;
  --surface: #1a1730;
  --surface-2: #15122a;
  --text: #e8e6f5;
  --text-soft: #c9c4e0;
  --text-faint: #7a7493;
  --border: #2e2a4a;
  --border-soft: #1f1b3a;
  --primary: #e8e6f5;
  --accent: #a5b4fc;
  --accent-soft: #1e1b4b;
  --accent-strong: #c7d2fe;
  --warning: #fbbf24;
  --warning-soft: #422006;
  --danger: #fca5a5;
  --danger-soft: #450a0a;
  --success: #86efac;
  --success-soft: #052e16;
  --on-accent: #0e0c1c;
  --shadow-strong: rgba(0, 0, 0, 0.5);
  --shadow-soft:   rgba(0, 0, 0, 0.25);
  --ctrl-bg:        rgba(26, 23, 48, 0.78);
  --ctrl-bg-hover:  rgba(46, 42, 74, 0.92);
  --line-strong:    #4e4870;
  --scrim:          rgba(0, 0, 0, 0.55);
  --code-bg:        #14121f;
  /* Extended chart palette — dark theme. Lighter, more saturated
     versions of the light-theme ramp so contrast against the dark
     surface stays comfortable. */
  --series-1: #5eead4;
  --series-2: #fcd34d;
  --series-3: #c7d2fe;
  --series-4: #fdba74;
  --series-5: #86efac;
  --series-6: #f9a8d4;
  --series-7: #7dd3fc;
  --series-8: #bef264;
  --series-9: #fca5a5;
  --series-10: #d1d5db;
  /* Soft plates, dark theme. Deep rather than pale, and lifted off
     --surface far enough to read as a plate: 1.52 to 1.99 against it,
     with --text at 7.1 to 9.3 on top. See test_colour_contrast.py. */
  --series-1-soft: #134e4a;
  --series-2-soft: #78350f;
  --series-3-soft: #312e81;
  --series-4-soft: #7c2d12;
  --series-5-soft: #14532d;
  --series-6-soft: #831843;
  --series-7-soft: #0c4a6e;
  --series-8-soft: #365314;
  --series-9-soft: #7f1d1d;
  --series-10-soft: #374151;
  color-scheme: dark;
}
}

/* ============ Base layout ============ */
* { box-sizing: border-box; }
html {
  scroll-behavior: smooth;
  /* Lock the text-size to what the CSS declares. Without this, mobile
     Safari and some desktop zoom states scale text to fit the viewport,
     producing the "page is downscaled" effect when narrowing the
     window. Content should reflow, not shrink. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  /* Reserve the scrollbar's gutter whether or not one is showing. Three
     paths lock the body's scroll — the lightbox, the search modal and
     the modal drawer — and on a system with classic (non-overlay)
     scrollbars, taking the scrollbar away widens the viewport by its
     gutter, so the centred column jumps sideways at the moment a panel
     opens over it. Reserving it costs one gutter on a page short enough
     not to scroll; the alternative is a jump on every reader whose OS
     does not use overlay scrollbars, which is every Windows machine and
     any Mac set to "Show scroll bars: Always". Ignored by browsers that
     do not support it, which leaves them exactly where they were. */
  scrollbar-gutter: stable;
  /* The top rail's height at rest. Declared here rather than on
     `.okt-rail` because it is not only the rail's business: the rail
     is opaque and fixed, so anything else that pins itself to the top
     of the viewport — a sticky table header — starts this far down.
     The rail's hover grows it in its own scope; nothing else follows
     that, on purpose (a header that jumps when the pointer nears the
     rail is a header that moves). */
  --okt-rail-h: 12px;
}
body {
  margin: 0; padding: 0;
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  font-size: 16px; line-height: 1.65;
  letter-spacing: 0.005em;
  color: var(--text); background: var(--bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Inter's polished variants: ss01 (open digits), ss03 (lowercase l
     with hook → clearer in long runs), cv11 (single-storey a). Tnum
     keeps numerics monospaced inside tabular/data contexts. */
  font-feature-settings: 'cv11', 'ss01', 'ss03';
  /* Long URLs / unbroken tokens shouldn't push the page wider than the
     viewport — break inside the word if there's nowhere else to break. */
  overflow-wrap: break-word;
  word-break: normal;
  transition: background-color .18s ease, color .18s ease;
}
code, pre, .mono {
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 13px;
  /* JetBrains Mono ligatures (->, =>, !=, ===, etc.) make code read
     closer to how it reasons. zero gives a slashed/dotted 0 so it's
     distinct from O at small sizes. */
  font-feature-settings: 'calt', 'zero';
  font-variant-ligatures: contextual;
}
/* Tabular numerics across all table cells so numbers align column-wise
   without depending on the cell padding. */
table { font-variant-numeric: tabular-nums; }
.okt-stats, .okt-group-count, .okt-chip-count {
  font-variant-numeric: tabular-nums;
}

/* Layout — one centred content column. The sidebar (page-nav, which
   adopts page-toc at boot) is an OVERLAY drawer at every width, opened
   by the "Contents" button in the top-left chrome strip. Content never
   shifts: a reader who opens the contents to jump somewhere gets the
   same measure back when it closes, and on a wide window the drawer
   sits in the empty gutter beside the centred column. The previous
   model — a permanent grid column with a full-height edge handle that
   both resized and collapsed it — drew a line down the page and moved
   the text every time it was used. */
.layout { display: block; }

/* Scrim + scroll lock belong to the MODAL state only — the narrow
   viewport, where the panel covers the page and the page behind it is
   not usable anyway. A peek is transient and pointer-driven, and a
   pinned panel is something you read alongside; darkening the page or
   freezing its scroll in either case would fight the reader. */
body.drawer-modal::before {
  content: '';
  position: fixed; inset: 0;
  background: rgba(0,0,0,0.35);
  z-index: 90;
  animation: htmldoc-fade-in 0.2s ease;
}
@keyframes htmldoc-fade-in { from { opacity: 0; } to { opacity: 1; } }
body.drawer-modal { overflow: hidden; }

/* The pinned inset. The panel is still `position: fixed` — it has to
   stay put while the document scrolls under it — so the column is moved
   by padding the layout, and `main`'s `margin-inline: auto` re-centres
   it in what is left. Gated at 900px because below that the remainder
   is narrower than the narrow measure; chrome.js will not enter the
   pinned state there and unpins if the window shrinks into it. */
@media (min-width: 900px) {
  body.drawer-pinned .layout { padding-left: var(--oku-drawer-w); }
  /* The rail is a map OF the document, so it belongs over the document
     and not over the panel beside it. Its marks are percentages of its
     own width, so they rescale; chrome.js rebuilds it on the state
     change because a class flip fires no resize event. */
  body.drawer-pinned .okt-rail { left: var(--oku-drawer-w); }
}

/* The Contents button opens the drawer at every width — the sidebar's
   only affordance. It takes the same 44px icon box as every other chrome
   button; the name lives in `aria-label` and `title`, not in ink above
   the cover. */

/* The reader's text scale, and the whole of it: `zoom` takes text,
   inline SVG, Mermaid's output, images and rules together, and reflows
   rather than stretching. The full reasoning — why not font-size, why
   the column does not move, and the two things it breaks — is in the
   "Reader text scale" comment in chrome.js.

   `zoom` divides the available width, so main's rendered box stays the
   physical width the layout gave it and the measure narrows instead.
   The 44px chrome buttons, the rail and the sidebar's own width are
   outside it on purpose: they are the frame, not the document. */
main { zoom: var(--oku-text-scale, 1); }
/* The tree and the table of contents are text a reader reads, so the
   complaint covers them. The scroll region only — the build footer is a
   stamp, not reading material, and page-nav itself owns the surface's
   height contract (invariant 1), which a zoom would scale out of the
   viewport. */
.page-nav-scroll { zoom: var(--oku-text-scale, 1); }

main {
  /* Divided by the scale, so the frame around the column is the same
     number of PHYSICAL pixels at every scale. Magnifying the padding
     with the text is what a browser's own zoom does, and on a phone it
     is the thing that hurts: the column is already clamped to the
     viewport, so every pixel the padding gains comes off the figure.
     Measured at 360px, uncompensated: a chart went from 286px wide to
     256px when the reader stepped the text up one notch — they asked
     for more and the picture got smaller. Compensated it holds at 286.
     Vertically it is the same argument plus one: the top padding is
     what keeps the first heading clear of the 44px chrome buttons, and
     that clearance is measured in physical pixels too. */
  padding:
    calc(56px  / var(--oku-text-scale, 1))
    calc(64px  / var(--oku-text-scale, 1))
    calc(120px / var(--oku-text-scale, 1))
    calc(64px  / var(--oku-text-scale, 1));
  max-width: var(--content-width, var(--max-width));
  /* Centred, always. The sidebar overlays rather than occupying a
     column, so nothing competes for the horizontal space and the
     measure never moves when the contents open. The reader-controlled
     width modes (narrow/comfortable/max) live on body via
     --content-width; --max-width is the unmodified default. */
  margin-inline: auto;
}

/* ============ TOC sidebar section ============ *
 * Stacks under page-nav inside the single left column. On wide
 * Stacks under the site tree inside the Contents drawer, which owns
 * the panel edge and the scroll context.
 * --------------------------------------------------------------- */
nav.toc, page-toc {
  align-self: start;
  z-index: 60;
  padding: 12px 24px 36px 24px;
  /* No border-right: the drawer owns the panel edge. */
  background: var(--surface);
  font-size: 13.5px; line-height: 1.5;
  display: block;
  transition: padding 0.25s ease, border-color 0.25s ease;
}
.toc-header { margin: 0 0 14px 0; }
.toc-header h2 {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--text-faint); font-weight: 700; margin: 0;
}
.toc-list { list-style: none; margin: 0; padding: 0; }
.toc-list > li.toc-h2 { margin: 2px 0; }
.toc-head {
  display: flex; align-items: baseline; gap: 6px;
  padding: 5px 4px; cursor: pointer; user-select: none;
  border-radius: 4px;
}
.toc-head:hover { background: var(--surface-2); }
/* The chevron is a <button> where a section has subsections and a
   <span> where it does not, so the rule has to describe both: a button
   brings its own font, padding, border and background, none of which
   belong in a 10px column. */
.toc-head .chevron {
  font-size: 9px; color: var(--text-faint);
  transition: transform 0.2s; width: 10px; flex-shrink: 0; text-align: center;
  font-family: inherit; line-height: inherit;
  background: none; border: 0; padding: 0; margin: 0;
  cursor: pointer;
}
.toc-head button.chevron:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}
.toc-head .chevron.hidden { visibility: hidden; }
.toc-h2.expanded > .toc-head .chevron { transform: rotate(90deg); }
.toc-head .num {
  color: var(--text-faint); font-variant-numeric: tabular-nums;
  font-weight: 600; flex-shrink: 0; min-width: 18px;
}
.toc-list a { color: var(--text); text-decoration: none; flex: 1; }
.toc-head a:hover { color: var(--accent); }
.toc-h2.active > .toc-head > a,
.toc-h2.active > .toc-head > .num { color: var(--accent); font-weight: 700; }
.toc-sub {
  list-style: none; display: none;
  padding-left: 26px; margin: 2px 0 6px 12px;
  border-left: 1px solid var(--border-soft);
}
.toc-h2.expanded > .toc-sub { display: block; }
.toc-sub li { margin: 0; }
.toc-sub a {
  color: var(--text-soft); font-size: 12.5px;
  display: block; padding: 4px 8px;
  border-left: 2px solid transparent; margin-left: -10px;
  line-height: 1.4;
}
.toc-sub a:hover { color: var(--accent); }
.toc-sub a.active { color: var(--accent); border-left-color: var(--accent); font-weight: 600; }

/* ============ Chrome buttons (TOC + theme + back-to-top) ============ */
.ctrl-btn {
  position: fixed; z-index: 200;
  width: 44px; height: 44px;
  border-radius: 12px;
  background: var(--ctrl-bg);
  color: var(--text);
  border: 1px solid var(--border);
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; padding: 0; font: inherit;
  box-shadow: 0 6px 18px var(--shadow-strong), 0 0 0 1px var(--shadow-soft);
  -webkit-backdrop-filter: blur(12px) saturate(140%);
  backdrop-filter: blur(12px) saturate(140%);
  isolation: isolate;
  transition: background .18s ease, border-color .18s ease, box-shadow .18s ease, transform .12s ease, opacity .2s ease;
}
.ctrl-btn:hover { background: var(--ctrl-bg-hover); border-color: var(--line-strong); }
/* Pinch-zoom stability: each fixed-position chrome button follows the
   visual viewport via CSS variables that chrome.js syncs. */
.ctrl-btn { transform: translate(var(--vv-left, 0), var(--vv-top, 0)); }
.ctrl-btn:active { transform: translate(var(--vv-left, 0), var(--vv-top, 0)) scale(0.96); }
.ctrl-btn svg { width: 20px; height: 20px; display: block; }
/* Proximity dimming. The chrome buttons are permanent, fixed, and sit
   over the top of the reading column at every scroll position — four
   opaque 44px boxes competing with the cover for the first thing the eye
   lands on. They fade back to a hint until the pointer approaches, then
   come up to full strength; the falloff is per-button distance
   (chrome.js), so walking toward the theme cycler brings the theme
   cycler up and leaves the Contents button where it was.
   Three things keep this from becoming a hidden control:
   - the floor is 0.32, not 0 — the button is always visible, always
     click-targetable, and its box never moves (invariant 2);
   - hover and focus-visible pin it to full regardless of the computed
     distance, so the keyboard path is unaffected;
   - the whole mechanism only arms when a fine pointer has actually
     moved (body[data-ctrl-proximity]) — a touch device, a screen
     reader, or a browser where the script never ran sees full opacity.
   The warning indicator is out: an alert that dims itself is a bug. */
body[data-ctrl-proximity="1"] .ctrl-btn:not(.warning-indicator) {
  opacity: var(--okt-ctrl-near, 1);
}
body[data-ctrl-proximity="1"] .ctrl-btn:hover,
body[data-ctrl-proximity="1"] .ctrl-btn:focus-visible,
body[data-ctrl-proximity="1"] .ctrl-btn[aria-expanded="true"] {
  opacity: 1;
}
/* back-to-top carries its own visibility (scroll position decides
   whether it exists at all). Proximity must not resurrect it. */
body[data-ctrl-proximity="1"] .back-to-top:not(.visible) { opacity: 0; }

.drawer-toggle { top: 16px; left: 16px; }

/* ---- the top-right cluster ---- *
 * One flex row, not five buttons each computing its own `right:`.
 *
 * The arithmetic version did not survive its own growth. Five buttons
 * want this corner (personalize, search, warning, width, theme), three
 * of them appear conditionally and at different times, and each carried
 * a hand-computed offset plus a `:has()` rule to compact when a
 * neighbour was absent. `.width-toggle` and `.search-toggle` both
 * resolved to right: 76px on any page with search and no warning — two
 * 44px boxes at exactly [1320, 16, 44, 44], the width toggle
 * unreachable underneath. Nothing was wrong with either declaration on
 * its own; the offset table was simply never re-derived when search
 * was added, which is what an offset table always does.
 *
 * A flex row has no table to re-derive. A button that is absent, or
 * `display: none` (the warning indicator's default), takes no space and
 * the rest close up on their own. Adding a sixth costs an `order`.
 * Pinned by test_invariants.py::test_the_top_right_chrome_never_overlaps. */
.okt-chrome-cluster {
  position: fixed;
  top: 16px; right: 16px;
  z-index: 210;
  display: flex;
  align-items: center;
  gap: 16px;
  /* Pinch-zoom stability moves here, once, instead of onto each child —
     see the .ctrl-btn note above. */
  transform: translate(var(--vv-left, 0), var(--vv-top, 0));
}
.okt-chrome-cluster .ctrl-btn {
  position: static;
  transform: none;
}
.okt-chrome-cluster .ctrl-btn:active {
  transform: scale(0.96);
}
/* Visual order is fixed by `order`, so it does not depend on which
   button was created first — search and the warning indicator arrive
   whenever their subsystem initialises.

   Three things reach this row now. Theme, width, language and
   personalize moved into the presentation menu, which is what the
   `menu-toggle` opens; what is left is what a reader uses WHILE
   reading. The menu sits hard right: it is the one that changes what
   the reader is looking at rather than how it is lit, so it gets the
   corner they can hit without aiming — the position the width cycler
   used to hold, for the same reason. */
.okt-chrome-cluster .search-toggle      { order: 20; }
.okt-chrome-cluster .warning-indicator  { order: 30; }
.okt-chrome-cluster .menu-toggle        { order: 60; }
/* The sliders icon's knobs are holes punched in its lines, so they are
   painted in the button's own background — which means they have to
   follow it when it changes. The `fill` attribute on the icon is the
   no-stylesheet fallback; at rest the two agree, and on hover they did
   not: measured in dark, a knob at rgba(26,23,48,.78) sitting on a
   button at rgba(46,42,74,.92), which reads as the page showing through
   rather than as a knob on a slider. */
.menu-toggle svg circle { fill: var(--ctrl-bg); }
.menu-toggle:hover svg circle { fill: var(--ctrl-bg-hover); }

/* Content-width modes (D3). The CSS variable wins because <main> reads
   it via max-width; the body attribute is the persisted source of truth. */
/* The `ch` values are calibrated, not chosen: `ch` is the width of "0",
   which in Inter is wider than the average character in running prose,
   so the rendered line runs ~1.1x the nominal figure. Measured medians
   at these settings are 63 / 67 / 78 characters, against a readable
   range of 45-75. Re-measure if the body font changes. */
body[data-content-width="narrow"]      { --content-width: 860px;  --prose-width: 54ch; }
body[data-content-width="comfortable"] { --content-width: 1100px; --prose-width: 62ch; }
/* `100%`, not `100vw`. A viewport unit resolves against the initial
   containing block, which the reader's text scale (`zoom` on main) does
   not touch, so at 1.25 this mode asked for 125% of the viewport and the
   page scrolled sideways. A percentage resolves against main's own
   containing block, which IS zoom-adjusted, so it means the same thing
   at every scale — and the same thing it meant before, since main's
   containing block was already the full width here. */
body[data-content-width="max"]         { --content-width: 100%;   --prose-width: none; }
/* Wide-screen bumps, so the default actually USES the room on a
   1600px+ monitor instead of leaving two hand-widths of margin. */
@media (min-width: 1500px) {
  body[data-content-width="comfortable"] { --content-width: 1240px; }
}
@media (min-width: 1900px) {
  body[data-content-width="comfortable"] { --content-width: 1400px; }
}

.back-to-top { bottom: 18px; right: 18px; opacity: 0; pointer-events: none; }
.back-to-top.visible { opacity: 1; pointer-events: auto; }

/* Inline-personalization panel — opens near the gear, modal-like but
   inline so the rest of the page stays scrollable. */
.personalize-panel {
  position: fixed;
  top: 70px;
  right: 16px;
  width: 320px;
  max-width: calc(100vw - 32px);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 16px 18px 14px 18px;
  box-shadow: 0 12px 32px var(--shadow-strong);
  z-index: 220;
  animation: tip-in 0.18s ease;
}
.personalize-panel header {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 6px;
}
.personalize-panel header strong { font-size: 14px; color: var(--text); }
.personalize-panel .personalize-close {
  background: none; border: none; cursor: pointer;
  font-size: 22px; line-height: 1; color: var(--text-soft);
  padding: 0 4px;
  transition: color 0.15s ease;
}
.personalize-panel .personalize-close:hover { color: var(--accent); }
.personalize-panel p { font-size: 12px; color: var(--text-soft); margin: 0 0 12px 0; line-height: 1.5; }
.personalize-fields {
  display: grid; gap: 10px;
  max-height: 50vh; overflow-y: auto;
}
.personalize-field {
  display: grid; gap: 4px;
}
.personalize-label {
  font-size: 11px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--text-soft);
}
.personalize-field input {
  font: 500 13px 'JetBrains Mono', monospace;
  padding: 7px 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text);
  outline: none;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.personalize-field input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.personalize-panel footer {
  margin-top: 12px;
  display: flex; justify-content: flex-end;
}
.personalize-reset {
  font: 600 11px 'Inter', sans-serif;
  padding: 5px 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text-soft);
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.personalize-reset:hover { border-color: var(--danger); color: var(--danger); }

/* Personalized-value spans inside code blocks: a subtle accent tint so
   the reader sees instantly which characters are theirs vs. literal. */
.okc-personalized {
  background: var(--accent-soft);
  color: var(--accent-strong);
  border-radius: 3px;
  padding: 0 2px;
  transition: background 0.18s ease;
  cursor: help;
}
.okc-personalized:hover { background: color-mix(in srgb, var(--accent) 30%, transparent); }

/* ============ The presentation menu ============ *
 * The panel behind the corner's `menu-toggle`: one row per choice a
 * reader makes about how the page is presented. See the module comment
 * in chrome.js for why these five left the corner.
 *
 * It is a <body> child, not a child of the cluster: the cluster is a
 * flex row of 44px boxes and a 300px panel inside it would size the row.
 * Position is fixed and it carries the same visual-viewport translate
 * the cluster does, so a pinch-zoomed page keeps the panel under the
 * button that opened it.
 *
 * Every row is one grid with two tracks — label, control — so the
 * controls line up down a single right edge whatever the labels say in
 * whatever language. A flex row per line would pack each control against
 * its own label and leave five different left edges.
 * ---------------------------------------------------------------- */
.okt-chrome-menu {
  position: fixed;
  top: 68px; right: 16px;
  z-index: 209;
  /* 340, not 320: at 320 the longest label the kit ships wrapped to two
     lines while its neighbours sat on one, which makes the rows ragged
     down the column the reader is scanning. Sized to the widest row the
     kit can produce — label plus the three width stops — and capped to
     the viewport so a phone gets a panel and not a scroll. */
  width: min(340px, calc(100vw - 32px));
  display: grid;
  gap: 4px;
  padding: 10px;
  border-radius: 14px;
  background: var(--ctrl-bg);
  border: 1px solid var(--border);
  box-shadow: 0 12px 32px var(--shadow-strong), 0 0 0 1px var(--shadow-soft);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  backdrop-filter: blur(14px) saturate(140%);
  transform: translate(var(--vv-left, 0), var(--vv-top, 0));
}
.okt-chrome-menu[hidden] { display: none; }
.okt-chrome-menu:focus { outline: none; }
.okt-menu-row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 12px;
  padding: 6px 6px 6px 8px;
  border-radius: 9px;
}
.okt-menu-row + .okt-menu-row { border-top: 1px solid var(--line); }
.okt-menu-label {
  font-size: 12.5px; font-weight: 600; color: var(--text-soft);
  /* An author's language is not this kit's, so a label can be twice as
     long as the English it was measured against. */
  min-width: 0; overflow-wrap: anywhere;
}

/* A segmented control: the stop the page is in is pressed, and the
   others are one click away. This is the half the corner could not do —
   an icon-only cycler has to be hovered to be read and announces its
   stops in a tooltip nobody opens. */
.okt-menu-seg {
  display: flex;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  background: var(--surface);
}
.okt-menu-seg button {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 9px;
  border: 0; background: none; cursor: pointer;
  font: 600 11.5px/1.2 inherit; color: var(--text-soft);
  white-space: nowrap;
  transition: background .15s ease, color .15s ease;
}
.okt-menu-seg button + button { border-left: 1px solid var(--border); }
.okt-menu-seg button:hover { background: var(--ctrl-bg-hover); color: var(--text); }
.okt-menu-seg button[aria-pressed="true"] {
  background: var(--accent-soft, var(--surface-2));
  color: var(--accent);
}
.okt-menu-seg-code button {
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: 0.04em;
}
.okt-menu-glyph { display: inline-flex; }
.okt-menu-glyph svg { width: 13px; height: 13px; display: block; }

/* The text-size stepper. The readout in the middle IS the reset — a
   fourth box would crowd a row that is already three wide, and at 100%
   a separate Reset is a control that does nothing, which is the
   disabled state this kit does not ship. */
.okt-menu-step {
  display: flex; align-items: center;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  background: var(--surface);
}
.okt-menu-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 26px;
  border: 0; background: none; cursor: pointer; padding: 0;
  color: var(--text-soft);
  transition: background .15s ease, color .15s ease;
}
.okt-menu-btn:hover { background: var(--ctrl-bg-hover); color: var(--text); }
.okt-menu-btn svg { width: 14px; height: 14px; display: block; }
/* An end of the ladder is stated, not hidden: the button stays in place
   and stays clickable, it just says that it has nothing left to give. */
.okt-menu-btn[aria-disabled="true"] { opacity: 0.35; cursor: default; }
.okt-menu-btn[aria-disabled="true"]:hover { background: none; color: var(--text-soft); }
.okt-menu-readout {
  /* One column track, sized to the widest reading the ladder can
     produce, so the two step buttons do not move as the number changes
     width — the thing you are clicking repeatedly must stay put. */
  min-width: 46px;
  padding: 0 4px;
  border: 0; border-left: 1px solid var(--border); border-right: 1px solid var(--border);
  background: none; cursor: pointer;
  font: 600 11.5px/26px 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--text);
  text-align: center;
}
.okt-menu-readout:hover { background: var(--ctrl-bg-hover); }

/* A row whose control opens something else — the personalize panel. */
.okt-menu-open {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 26px;
  border: 1px solid var(--border); border-radius: 8px;
  background: var(--surface); cursor: pointer; padding: 0;
  color: var(--text-soft);
}
.okt-menu-open:hover { background: var(--ctrl-bg-hover); color: var(--text); }
.okt-menu-open svg { width: 14px; height: 14px; display: block; }

/* ============ Reading aids ============ *
 * The rail: a 12px strip across the top that answers three questions
 * without the reader asking any of them — how far in am I, what shape
 * is this page, and can I get back to that table.
 *
 * Two rules govern every declaration below.
 *
 * 1. NO BUTTON BOX EVER MOVES. The hover swell, the current-position
 *    highlight and focus all act on the mark's ::before via `transform`
 *    and colour. The <button> hit boxes are fixed at 14x13 and the
 *    strip is fixed at 12px, at every state. A dock that reflows makes
 *    you chase the thing you were aiming at; this one cannot.
 * 2. NOTHING ON THE PAGE MOVES EITHER. The strip is `position: fixed`
 *    and the label is absolutely positioned inside it with
 *    `pointer-events: none`, so revealing either cannot displace a
 *    single element of the document.
 * Pinned by test_page_rail.py. */
.okt-rail {
  position: fixed; top: 0; left: 0; right: 0;
  /* Tall enough to CONTAIN the marks, including magnified. At 3px they
     had to hang below the strip onto whatever text was scrolling under
     it, where a 2px tick is a speck. Painted --bg, so the effect is
     that page content ends 12px early — what any fixed header does, at
     a fraction of the height. Rest height inherited from :root. */
  /* One multiplier drives every mark's size, so at rest the strip is a
     hairline and under the pointer it is a legible map. It composes with
     the per-mark dock swell (`--okt-mag`) rather than replacing it: the
     neighbourhood you are pointing at is the biggest thing on the rail,
     inside a rail that has already grown. */
  --okt-rail-scale: 1;
  height: var(--okt-rail-h);
  transition: height 0.16s ease;
  z-index: 250;
  background: var(--bg);
  /* The strip takes the pointer so the swell can start BEFORE the
     pointer reaches a mark — a dock that only responds once you are
     already on target is backwards. Nothing interactive lives in the
     top 12px (the chrome buttons start at y=16), and the strip is
     opaque, so nothing is being covered. */
}
/* Hover opens the map. The strip grows DOWNWARD from a fixed top edge
   and it is `position: fixed`, so nothing in the document moves and no
   mark changes its x or its top — the growth is the one direction that
   cannot displace anything, including the mark you are aiming at.
   32px, because the tallest thing on the rail is the title bar at
   9px x 2 (open scale) x 1.6 (swell at the pointer) = 28.8px, and it
   has to stay inside the strip rather than hang onto the text scrolling
   under it. Those three numbers are one constraint — change any and
   re-check the other two against the 9px bar.
   `focus-within` is not decoration: the keyboard path through the marks
   is a roving tabindex, and a 3px mark you cannot see is not a target. */
.okt-rail:hover,
.okt-rail:focus-within {
  --okt-rail-h: 32px;
  --okt-rail-scale: 2;
  background: var(--surface);
  box-shadow: 0 4px 14px var(--shadow-soft);
}
/* A touch pointer has no hover. Expanding on tap would grow the strip
   under the finger that just committed to a target. */
@media (hover: none) {
  .okt-rail:hover { --okt-rail-h: inherit; --okt-rail-scale: 1; background: var(--bg); box-shadow: none; }
}
/* The track. It has to be visible or the ticks read as a row of
   floating marks with nothing joining them — and the line joining them
   is the page. --border-soft against --bg was a 4-value step and
   disappeared entirely. */
.okt-rail::before {
  content: '';
  position: absolute; top: 0; left: 0; right: 0; height: 2px;
  background: var(--border);
}
/* Read progress. Still `.progress-bar` / `#progress-bar` — the id is
   what initReadingAids drives and what the print sheet hides. */
.progress-bar {
  position: absolute; top: 0; left: 0;
  height: 2px; width: 0;
  background: var(--accent);
  transition: width 0.1s linear; pointer-events: none;
}
.okt-rail-marks { position: absolute; inset: 0; pointer-events: none; }
/* A landmark is a button with a constant 14x13 box and no visuals of
   its own. Everything visible is the ::before, so the target can be
   comfortably large while the mark stays a hairline. */
.okt-rail-mark {
  position: absolute;
  /* The box follows the strip so an expanded rail is also a bigger
     target. It only ever grows downward from top: 0 — the x and the top
     of every mark are the same in every state. */
  top: 0; height: max(13px, var(--okt-rail-h, 12px)); width: 14px; margin-left: -7px;
  padding: 0; border: 0; background: none;
  -webkit-appearance: none; appearance: none;
  cursor: pointer; pointer-events: auto;
}
.okt-rail-mark::before {
  content: '';
  position: absolute; left: 50%; top: 0;
  /* The swell. `transform` only — it never triggers layout, so the
     pointermove handler is cheap and the boxes above stay put.
     transform-origin at the top centre grows the mark downward from
     the track, the way a tick lengthens rather than floats.
     Two multipliers: the rail-wide one (hover opens the map) and the
     per-mark dock one (the pointer picks a neighbourhood out of it). */
  transform: translateX(-50%) scale(calc(var(--okt-mag, 1) * var(--okt-rail-scale, 1)));
  transform-origin: 50% 0;
  transition: background-color 0.18s ease, opacity 0.18s ease, transform 0.12s ease;
}
/* ---- shapes: what kind of thing is at this position ---- */
/* Headings — a bar crossing the track, and the bar gets THICKER as the
   heading gets more important. Depth alone (the old two heights) says
   "there is a hierarchy"; depth plus weight says which level you are
   looking at without measuring it against its neighbour. Three tiers,
   matching the three the page can have. */
.okt-rail-mark[data-kind="title"]::before {
  width: 3px; height: 9px; border-radius: 0 0 1px 1px;
  background: var(--text); opacity: 1;
}
.okt-rail-mark[data-kind="section"]::before {
  width: 2px; height: 7px; border-radius: 0 0 1px 1px;
  background: var(--text-soft); opacity: 0.9;
}
.okt-rail-mark[data-kind="sub"]::before {
  width: 1.5px; height: 4px; border-radius: 0 0 1px 1px;
  background: var(--text-soft); opacity: 0.5;
}
/* Figures sit below the track, quieter than any heading — the shape of
   the page is its headings; figures are the things you go back for. */
.okt-rail-mark[data-kind="figure"]::before {
  top: calc(5px * var(--okt-rail-scale, 1));
  background: var(--text-faint); opacity: 0.62;
}
/* The three kinds a reader actually hunts for by name get a silhouette
   of their own, and none of them is shared with anything else:
   a table is a filled square (a grid of cells),
   a chart is a filled circle (a point on an axis),
   a diagram is a diamond (a topology, a square turned off-axis).
   Everything else — KPI grids, comparison grids, step flows, snippets,
   annotated code, example pairs — is a hollow square: recognisably "a
   figure", recognisably not one of the three. At 4px this is the whole
   budget; the hover expansion is what makes the four resolve, and the
   tooltip carries the name. */
.okt-rail-mark[data-shape="square"]::before { width: 4px; height: 4px; border-radius: 1px; }
.okt-rail-mark[data-shape="round"]::before  { width: 4px; height: 4px; border-radius: 50%; }
.okt-rail-mark[data-shape="hollow"]::before {
  width: 4px; height: 4px; border-radius: 1px;
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--text-faint);
}
/* Diamond — a square on its corner. The rotation composes with both
   scales rather than replacing them, so a magnified diamond stays a
   diamond. */
.okt-rail-mark[data-shape="angle"]::before {
  width: 4px; height: 4px; border-radius: 1px;
  transform: translateX(-50%) scale(calc(var(--okt-mag, 1) * var(--okt-rail-scale, 1))) rotate(45deg);
}
.okt-rail-mark[data-shape="hollow"]:hover::before,
.okt-rail-mark[data-shape="hollow"]:focus-visible::before,
.okt-rail-mark[data-shape="hollow"].is-current::before {
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--accent);
}
.okt-rail-mark:hover::before,
.okt-rail-mark:focus-visible::before,
.okt-rail-mark.is-current::before {
  background: var(--accent); opacity: 1;
}
.okt-rail-mark:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 0;
  border-radius: 3px;
}
@media (prefers-reduced-motion: reduce) {
  .okt-rail-mark::before { transition: background-color 0.18s ease, opacity 0.18s ease; }
}
/* ---- the label ---- */
/* Absolutely positioned and pointer-transparent, so revealing it cannot
   move the rail, the marks, or anything on the page. */
.okt-rail-tip {
  /* Below the strip at whatever height the strip currently is — the tip
     only ever shows while the rail is open, so this resolves to 34px in
     practice and never lands on top of a mark. */
  position: absolute; top: calc(var(--okt-rail-h, 12px) + 6px);
  transform: translateX(-50%);
  max-width: min(300px, 66vw);
  padding: 7px 10px;
  border-radius: 8px;
  background: var(--ctrl-bg, var(--surface));
  border: 1px solid var(--border);
  box-shadow: 0 6px 18px var(--shadow-strong);
  -webkit-backdrop-filter: blur(12px) saturate(140%);
  backdrop-filter: blur(12px) saturate(140%);
  font: 500 11.5px/1.35 'Inter', -apple-system, sans-serif;
  color: var(--text);
  opacity: 0; pointer-events: none;
  transition: opacity 0.14s ease;
}
.okt-rail-tip .okt-rail-tip-line {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.okt-rail-tip .okt-rail-tip-kind {
  color: var(--text-soft);
  font-weight: 600; font-size: 10px;
  text-transform: uppercase; letter-spacing: 0.07em;
  margin-right: 6px;
}
/* Preview. A rendered SVG clones inertly — no scripts, no
   custom-element lifecycle — so a chart or a diagram can show its real
   silhouette rather than a description of one. */
.okt-rail-tip-preview {
  margin-top: 6px;
  font-size: 10.5px;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}
.okt-rail-tip-thumb {
  width: 100%; height: 88px;
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
  border-radius: 5px;
  background: var(--bg);
  border: 1px solid var(--border-soft);
}
.okt-rail-tip-thumb svg { max-width: 100%; max-height: 84px; display: block; }
.okt-rail-tip.visible { opacity: 1; }

.okt-rail[data-scrollable="0"] { display: none; }
.skip-link {
  position: absolute; left: -9999px; top: auto;
  background: var(--primary); color: var(--bg);
  padding: 10px 16px; border-radius: 4px;
  font-size: 14px; font-weight: 600;
  text-decoration: none; z-index: 1000;
}
.skip-link:focus { left: 16px; top: 16px; }
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px; border-radius: 4px;
}
button:focus-visible, a:focus-visible { outline-offset: 3px; }

@keyframes flash-highlight {
  0%   { background-color: var(--accent-soft); }
  100% { background-color: transparent; }
}
:target { animation: flash-highlight 1.5s ease-out; scroll-margin-top: 80px; }

.permalink {
  opacity: 0; margin-left: 10px;
  color: var(--text-faint); text-decoration: none;
  font-size: 0.7em; font-weight: 400;
  transition: opacity 0.15s, color 0.15s;
}
h2:hover .permalink, h3:hover .permalink, .permalink:focus-visible { opacity: 1; }
.permalink:hover { color: var(--accent); }

pre {
  position: relative;
  background: var(--code-bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  /* padding-top is generous (30px) to reserve space for the language
     pill that sits at top-left and the chrome bar (copy / wrap) that
     sits at top-right. Both are absolutely positioned INSIDE this
     reserved band so they never overlap the first code line.
     padding-bottom stays smaller — nothing floats at the bottom. */
  padding: 30px 18px 16px;
  /* Word-wrap by default — long lines fold instead of horizontal-
     scrolling out of the column. Authors who need single-line code
     can override with pre.okt-nowrap (rare). */
  overflow-x: auto;
  white-space: pre-wrap;
  word-break: break-word;
  overflow-wrap: anywhere;
  margin: 16px 0;
}
pre.okt-nowrap {
  white-space: pre;
  word-break: normal;
  overflow-wrap: normal;
}
pre code {
  white-space: inherit;
}
/* Per-line layout — each .okt-code-line is a grid row carrying three
   cells: [line-number] [fold-marker] [code-content]. Numbers + fold
   markers ride with their code line: when word-wrap is enabled and a
   logical line spans multiple visual rows, the cells stay at the row
   top (align-items: start) while the content grows. Replaces the old
   absolute-positioned gutter, which couldn't track wrapped lines.
   Order matches IntelliJ-style editors: number first (left), then
   the fold-marker column to the right of the number. */
pre.okt-line-numbered { padding-left: 12px; }
pre.okt-line-numbered code {
  display: block;
  font-family: inherit;
  font-size: inherit;
  line-height: inherit;
}
pre.okt-line-numbered .okt-code-line {
  display: grid;
  grid-template-columns: 36px 14px minmax(0, 1fr);
  column-gap: 0;
  align-items: start;
}
pre .okt-code-line.okt-line-hidden { display: none; }
pre.okt-line-numbered .okt-code-content {
  min-width: 0;
  padding-left: 8px;
}
/* Line numbers — UNIFORM across all rows: same color, same size, same
   weight regardless of foldability. */
pre .okt-code-ln {
  display: block;
  text-align: right;
  padding-right: 4px;
  color: var(--text-faint);
  opacity: 0.55;
  user-select: none;
  -webkit-user-select: none;
  font-variant-numeric: tabular-nums;
}
/* Render the line number via ::before so it does NOT contribute to
   <code>.textContent — Prism's autoloader fires `complete` twice (once
   before the language module arrives, once after) and the second pass
   re-reads textContent; if numbers leaked in, every line would render
   with a duplicated "1uv …" / "2# …" prefix. */
pre .okt-code-ln::before { content: attr(data-ln); }
/* Fold marker column — sits to the RIGHT of the line number (IntelliJ
   convention). Empty for non-foldable lines; shows ▼ (expanded) or
   ▶ (collapsed) for foldable ones. Dim grey at rest; brighter on
   hover/focus. Pointer-events only on actually-foldable markers so
   non-foldable rows don't intercept code selection. */
pre .okt-fold-marker {
  display: block;
  text-align: center;
  font-size: 10px;
  line-height: inherit;
  color: var(--text-faint);
  opacity: 0;
  user-select: none;
  -webkit-user-select: none;
  pointer-events: none;
  transition: opacity 0.15s ease, color 0.15s ease;
}
pre .okt-fold-marker.okt-foldable {
  pointer-events: auto;
  cursor: pointer;
  opacity: 0.45;
}
pre .okt-fold-marker.okt-foldable:hover,
pre .okt-fold-marker.okt-foldable:focus-visible {
  opacity: 1;
  color: var(--text-soft);
}
pre .okt-fold-marker.okt-foldable:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-radius: 2px;
}
/* IDE-style triangle arrows: ▼ expanded, ▶ collapsed. No transform
   so the glyph stays crisp at small sizes. */
pre .okt-fold-marker.okt-foldable::before { content: '\25BC'; }  /* ▼ */
pre .okt-fold-marker.okt-foldable.okt-folded::before { content: '\25B6'; }  /* ▶ */

/* Language pill — a static label, NOT a button. No hover state, no
   accent transitions. Pinned to the top-left corner of the pre with
   only the bottom-right corner rounded so it reads as a flag/tab on
   the block edge rather than a floating chip. Inside the pre's
   reserved padding-top band so it never overlaps the gutter (which
   starts at top: 30px) or the first code line. */
pre > .okt-code-lang {
  position: absolute;
  top: 0;
  left: 0;
  font: 500 10.5px 'JetBrains Mono', ui-monospace, monospace;
  letter-spacing: 0.04em;
  color: var(--text-faint);
  background: var(--surface-2);
  border: 0;
  border-bottom: 1px solid var(--border-soft);
  border-right: 1px solid var(--border-soft);
  border-radius: 0 0 6px 0;
  padding: 3px 9px 3px 10px;
  user-select: none;
  pointer-events: none;
  z-index: 2;
  transition: opacity 0.18s ease;
}

/* Smart-hide round 2: when the page contains code blocks in only
   ONE language, the pills are redundant chrome — the reader already
   knows what they're looking at. Reveal-on-hover-of-pre so the pill
   stays discoverable. Multi-language pages keep pills visible at
   rest. */
html[data-okt-single-lang] pre > .okt-code-lang {
  opacity: 0.18;
}
html[data-okt-single-lang] pre:hover > .okt-code-lang,
html[data-okt-single-lang] pre:focus-within > .okt-code-lang {
  opacity: 1;
}

/* When at least one fold is collapsed, hint at it via the marker
   triangle. Subtle accent tint on the opening line's number cell so
   the reader notices where work is hidden — without changing
   number size/weight (which would break the grid alignment). */
pre.okt-has-folds .okt-code-line:has(.okt-fold-marker.okt-folded) .okt-code-ln {
  color: var(--accent);
  opacity: 0.8;
}
/* Pre-host: a non-scrolling containing block wrapping every <pre>.
   The copy / wrap icon buttons live on the host, NOT inside the
   scrolling pre — without this they'd ride the pre's horizontal scroll
   and disappear off-screen when the reader scrolls a long line. The
   host carries pre's outer margin so the visible spacing is unchanged. */
.okt-pre-host {
  position: relative;
  margin: 16px 0;
}
.okt-pre-host > pre { margin: 0; }
/* Icon-only Copy on every <pre>. Same shape as the shared toolbar
   used on diagrams and charts (see .okt-bar below) so the affordance
   is identical wherever copy/screenshot live. Anchored to the host
   so horizontal scroll of the pre doesn't push the button away. */
.copy-btn {
  position: absolute; top: 8px; right: 8px;
  width: 30px; height: 30px;
  background: var(--surface); color: var(--text-soft);
  border: 1px solid var(--border); border-radius: 6px;
  padding: 0; cursor: pointer; opacity: 0;
  display: inline-flex; align-items: center; justify-content: center;
  transition: opacity 0.15s, color 0.15s, border-color 0.15s, background 0.15s;
}
.copy-btn svg { width: 14px; height: 14px; }
.okt-pre-host:hover .copy-btn, .copy-btn:focus-visible { opacity: 1; }
.copy-btn:hover { color: var(--accent-strong); border-color: var(--accent); background: var(--accent-soft); }
.copy-btn.copied { color: var(--success); border-color: var(--success); background: var(--success-soft); }
.copy-btn.error  { color: var(--danger);  border-color: var(--danger);  background: var(--danger-soft); }

/* Wrap toggle — same shape and reveal behaviour as .copy-btn, sits to
   its left. Active state (line wrapping on) tints the button so the
   reader can see at a glance whether the current block is in wrap mode. */
.okt-wrap-btn {
  position: absolute; top: 8px; right: 46px;  /* 8 + 30 (copy) + 8 gap */
  width: 30px; height: 30px;
  background: var(--surface); color: var(--text-soft);
  border: 1px solid var(--border); border-radius: 6px;
  padding: 0; cursor: pointer; opacity: 0;
  display: inline-flex; align-items: center; justify-content: center;
  transition: opacity 0.15s, color 0.15s, border-color 0.15s, background 0.15s;
}
.okt-wrap-btn svg { width: 14px; height: 14px; }
.okt-pre-host:hover .okt-wrap-btn, .okt-wrap-btn:focus-visible, .okt-wrap-btn.active { opacity: 1; }
.okt-wrap-btn:hover { color: var(--accent-strong); border-color: var(--accent); background: var(--accent-soft); }
.okt-wrap-btn.active { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }

/* Wrap-on state — flip horizontal scroll into wrapping. Apply to the
   <code> element so the gutter (a sibling positioned absolutely) keeps
   its layout independent of the wrap. Long lines wrap at word
   boundaries; very long tokens (URLs, hashes) break inside via
   overflow-wrap so the block never overflows horizontally. */
pre.okt-wrap { overflow-x: visible; }
pre.okt-wrap .okt-code-content {
  white-space: pre-wrap;
  word-break: break-word;
  overflow-wrap: anywhere;
}

/* ============ Image expand affordance + shared lightbox ============ *
 * Reveals a small chip on image hover; click opens the lightbox overlay.
 * Same chip shape as .copy-btn / .okt-wrap-btn so the affordance reads
 * the same across code, image, chart, diagram. */
.okt-img-host {
  position: relative;
  display: inline-block;
  max-width: 100%;
}
.okt-img-host img { display: block; max-width: 100%; }
.okt-img-expand {
  position: absolute;
  top: 8px; right: 8px;
  width: 30px; height: 30px;
  background: var(--surface); color: var(--text-soft);
  border: 1px solid var(--border); border-radius: 6px;
  padding: 0; cursor: pointer; opacity: 0;
  display: inline-flex; align-items: center; justify-content: center;
  transition: opacity 0.15s, color 0.15s, border-color 0.15s, background 0.15s;
  backdrop-filter: blur(4px);
}
.okt-img-expand svg { width: 14px; height: 14px; }
.okt-img-host:hover .okt-img-expand,
.okt-img-host:focus-within .okt-img-expand,
.okt-img-expand:focus-visible { opacity: 1; }
.okt-img-expand:hover {
  color: var(--accent-strong);
  border-color: var(--accent);
  background: var(--accent-soft);
}

/* Lightbox overlay — full-viewport modal. Backdrop dims the page; the
   frame centers content and provides a close button. Reused by image,
   chart, diagram, and (future) mermaid expand actions. */
.okt-lightbox {
  position: fixed; inset: 0;
  z-index: 1000;
  display: none;
  align-items: center; justify-content: center;
}
.okt-lightbox.open { display: flex; }
html.okt-lightbox-open { overflow: hidden; }    /* lock body scroll */
.okt-lightbox-backdrop {
  position: absolute; inset: 0;
  background: rgba(0, 0, 0, 0.78);
  backdrop-filter: blur(4px);
  cursor: zoom-out;
}
.okt-lightbox-frame {
  position: relative;
  /* Explicit size, not a cap — the frame fills the FULL viewport so
     expanded charts / diagrams get every available pixel. A max-*
     alone shrinks the frame to its content, which left small diagrams
     smaller in the "expanded" view than on the page. */
  width: 100vw; height: 100vh;
  width: 100dvw; height: 100dvh;
  display: flex; flex-direction: column;
  z-index: 1;
}
.okt-lightbox-close {
  position: absolute;
  top: 10px; right: 10px;
  z-index: 3;
  width: 36px; height: 36px;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: 8px;
  padding: 0; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.okt-lightbox-close:hover {
  color: var(--accent-strong);
  border-color: var(--accent);
  background: var(--accent-soft);
}
.okt-lightbox-close svg { width: 18px; height: 18px; }
.okt-lightbox-content {
  display: flex; align-items: center; justify-content: center;
  background: var(--surface);
  border: none;
  border-radius: 0;
  padding: 8px;
  flex: 1;
  width: 100%;
  min-width: 0; min-height: 0;
  outline: none;
  overflow: auto;
}
/* Pan / zoom stage — fills the lightbox; its inner is transformed
   on wheel / drag / pinch. The stage clips overflow so wildly
   zoomed content stays inside the frame, and the inner uses
   transform-origin top-left so the math in __okuPanZoom is
   straightforward. */
.okt-lightbox-content > .okt-lightbox-pz {
  position: relative;
  flex: 1;
  width: 100%; height: 100%;
  overflow: hidden;
  cursor: grab;
  display: block;
  touch-action: none;
}
.okt-lightbox-pz.panning { cursor: grabbing; }
.okt-lightbox-pz-inner {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  transform-origin: 0 0;
  display: flex; align-items: center; justify-content: center;
}
.okt-lightbox-pz-inner > * {
  max-width: 100%; max-height: 100%;
  width: auto; height: auto;
}
.okt-lightbox-pz-toolbar {
  position: absolute;
  right: 12px;
  bottom: 12px;
  display: inline-flex;
  align-items: center;
  border-radius: 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: 0 4px 12px var(--shadow-soft);
  z-index: 5;
  overflow: hidden;
}
.okt-lightbox-pz-toolbar button {
  background: none;
  border: none;
  width: 32px; height: 32px;
  font: 600 16px 'Inter', sans-serif;
  color: var(--text);
  cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
}
.okt-lightbox-pz-toolbar button + button { border-left: 1px solid var(--border); }
.okt-lightbox-pz-toolbar button:hover {
  background: var(--accent-soft);
  color: var(--accent-strong);
}
/* Picture-in-picture minimap — appears in the top-left corner of
   the lightbox stage when the chart is zoomed in past ~1.1x. Shows
   a static thumbnail of the chart with a coloured rect indicating
   the currently visible viewport region. Pointer-events:none so
   it doesn't intercept pan / zoom gestures. */
.okt-lightbox-pip {
  position: absolute;
  left: 12px;
  top: 12px;
  width: 160px;
  height: 100px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 4px 12px var(--shadow-soft);
  overflow: hidden;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.18s ease;
  z-index: 6;
}
.okt-lightbox-pip.visible {
  opacity: 1;
}
.okt-lightbox-pip-inner {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.okt-lightbox-pip-inner svg {
  width: 100%;
  height: 100%;
}
.okt-lightbox-pip-viewport {
  position: absolute;
  border: 2px solid var(--accent);
  background: rgba(0, 0, 0, 0);
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.12);
}
.okt-lightbox-content > img,
.okt-lightbox-content > svg,
.okt-lightbox-content > .okt-lightbox-img {
  display: block;
  width: 100%; height: 100%;
  max-width: 100%; max-height: 100%;
  object-fit: contain;
}
/* Table fullscreen: the moved .okt-table-wrap stretches to fill the
   lightbox; its scroll viewport scrolls both axes. Original margin /
   border-radius look right at body width, not at full-viewport — neutralise. */
.okt-lightbox-content > .okt-table-wrap {
  width: 100%; height: 100%;
  margin: 0;
  display: flex; flex-direction: column;
}
.okt-lightbox-content > .okt-table-wrap > .okt-table-scroll,
.okt-lightbox-content > .okt-table-wrap > .okt-table-cards,
.okt-lightbox-content > .okt-table-wrap > .okt-table-list,
.okt-lightbox-content > .okt-table-wrap > .okt-table-board {
  flex: 1;
  min-height: 0;
  overflow: auto;
}

/* ---- Markdown viewer ----------------------------------------------
   A linked .md file rendered in the lightbox frame. The frame centres
   its child and scrolls itself, which is right for a figure and wrong
   for a document — a tall document centred in a scroll container has
   its first line above the top edge. So the viewer STRETCHES to the
   frame and owns its own scrolling: the bar stays put, the body
   scrolls, and the document starts where a document starts.

   No `:has()` on the frame. The stretch belongs to the thing being
   stretched, and a rule that reaches up to the parent would have to be
   re-read every time the frame's own layout is touched. */
.okt-lightbox-content > .okt-mdview {
  align-self: stretch;
  flex: 1;
  min-width: 0; min-height: 0;
  display: flex; flex-direction: column;
  text-align: left;
}
.okt-mdview-bar {
  display: flex;
  align-items: center;
  gap: 12px;
  flex: 0 0 auto;
  /* The lightbox close button is absolutely positioned at top: 10px /
     right: 10px over this bar. Reserve its 36px box plus its inset so
     the last action never lands underneath it. */
  padding: 8px 56px 8px 14px;
  border-bottom: 1px solid var(--border);
  background: var(--surface-2);
}
/* The path answers "what am I looking at", so it gets the slack — but
   the FILENAME is the part that answers it. Two spans rather than one
   truncated string: the directory absorbs the ellipsis, the filename
   never shrinks. (The usual one-span trick for this is `direction: rtl`,
   which reorders the leading slash of an absolute path and prints
   `docs/notes/plan.md/`.) */
.okt-mdview-path {
  display: flex;
  align-items: baseline;
  flex: 1 1 auto;
  min-width: 0;
  font: 0.82rem 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--text-faint);
}
.okt-mdview-dir {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.okt-mdview-file {
  flex: 0 0 auto;
  color: var(--text-soft);
  font-weight: 500;
}
.okt-mdview-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
}
.okt-mdview-bar .okt-view-btn {
  padding: 4px 10px;
  height: 26px;
  border: none;
  background: transparent;
  color: var(--text-soft);
  font-size: 0.76rem;
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease;
}
.okt-mdview-bar .okt-view-btn + .okt-view-btn { border-left: 1px solid var(--border); }
.okt-mdview-bar .okt-view-btn:hover { background: var(--accent-soft); color: var(--accent-strong); }
.okt-mdview-bar .okt-view-btn.active { background: var(--accent); color: var(--on-accent); }
.okt-mdview-bar .okt-view-btn[disabled] { opacity: 0.4; cursor: default; }
.okt-mdview-copy,
.okt-mdview-open {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 26px;
  padding: 0 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text-soft);
  font-size: 0.76rem;
  text-decoration: none;
  cursor: pointer;
  transition: color 0.12s ease, border-color 0.12s ease, background 0.12s ease;
}
.okt-mdview-copy svg { width: 13px; height: 13px; }
.okt-mdview-copy:hover,
.okt-mdview-open:hover {
  color: var(--accent-strong);
  border-color: var(--accent);
  background: var(--accent-soft);
}
.okt-mdview-copy.copied { color: var(--success); border-color: currentColor; }
.okt-mdview-body {
  flex: 1;
  min-height: 0;
  overflow: auto;
  padding: 28px 24px 64px;
  /* A viewed file is a document being read, so the reader's text scale
     applies to it. The BODY carries it rather than the frame: the frame
     is chrome — the path, the Rendered/Source switch, the copy button —
     and this region already scrolls, so a scale that outgrows it scrolls
     rather than overflowing the overlay. */
  zoom: var(--oku-text-scale, 1);
}
/* One reading column, same measure the page uses. The width control
   changes --content-width on <body>, so the viewer follows it. */
.okt-mdview-rendered,
.okt-mdview-source {
  max-width: var(--content-width, 960px);
  margin: 0 auto;
}
.okt-mdview[data-view="rendered"] .okt-mdview-source { display: none; }
.okt-mdview[data-view="source"] .okt-mdview-rendered { display: none; }
.okt-mdview-source {
  white-space: pre-wrap;
  word-break: break-word;
}
/* The viewed file's own title, from its front-matter. Same relationship
   to the body as a page cover's h1 — larger than any heading inside the
   document, and the summary under it quieter than the prose. */
/* What the FILE says about itself, grouped and ruled off from the file.
   Ungrouped, the summary reads as the document's own first paragraph —
   same left edge, same measure, one line apart — and the reader
   attributes the kit's framing to the author. */
.okt-mdview-head {
  margin-bottom: 22px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--border-soft);
}
.okt-mdview-title {
  margin: 0;
  font-size: 1.75rem;
  line-height: 1.2;
  letter-spacing: -0.02em;
}
.okt-mdview-summary {
  margin: 6px 0 0;
  color: var(--text-soft);
  font-size: 1.02rem;
  text-align: left;   /* a one-line subtitle has nothing to justify */
}

/* The viewed file's first heading opens a <section>, which carries the
   page's own top margin — unwanted as the first thing under the bar. */
.okt-mdview-rendered > :first-child { margin-top: 0; }
.okt-mdview-rendered > section:first-child > h2:first-child { margin-top: 0; }
.okt-mdview-fail { margin: 0; }
.okt-mdview-inert { margin: 0 0 16px; }

@media (max-width: 640px) {
  /* The path and the actions cannot share one line at phone width, and
     the path is what identifies the file — it keeps the full row. */
  .okt-mdview-bar { flex-wrap: wrap; padding-right: 14px; }
  .okt-mdview-path { flex: 1 0 100%; padding-right: 42px; }
  .okt-mdview-body { padding: 20px 14px 48px; }
}

/* Shared toolbar for diagrams + charts. Lives top-right of the host
   element; reveals on hover/focus-within. Each button is the same
   shape as .copy-btn for a single mental model. */
.okt-bar {
  position: absolute; top: 8px; right: 8px;
  display: flex; gap: 4px;
  opacity: 0; transition: opacity 0.15s;
  z-index: 10;
  pointer-events: none;
}
.okt-host:hover .okt-bar,
.okt-host:focus-within .okt-bar { opacity: 1; pointer-events: auto; }
.okt-bar button {
  width: 30px; height: 30px;
  border-radius: 6px;
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text-soft);
  cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0;
  transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.okt-bar button:hover {
  background: var(--accent-soft);
  color: var(--accent-strong);
  border-color: var(--accent);
}
.okt-bar button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.okt-bar button svg { width: 14px; height: 14px; }
.okt-bar-sep {
  display: inline-block;
  width: 1px;
  height: 18px;
  align-self: center;
  background: var(--border);
  margin: 0 4px;
}

/* Theme-swap guard — when applyTheme flips the data-theme attribute,
   this class lands on <html> for two animation frames. Every
   element inside has its transitions silenced so the page repaints
   in ONE pass instead of every transition animating independently.
   Without this guard each `transition: background`, `transition:
   color`, etc. animates on its own clock and the reader sees a
   visible cascade. */
.okt-theme-swapping,
.okt-theme-swapping *,
.okt-theme-swapping *::before,
.okt-theme-swapping *::after {
  transition: none !important;
  animation-duration: 0s !important;
}
.okt-bar button.flash-ok {
  background: var(--success-soft);
  color: var(--success);
  border-color: var(--success);
}
.okt-bar button.flash-fail {
  background: var(--danger-soft);
  color: var(--danger);
  border-color: var(--danger);
}

/* ============ Chart-config popover ============
   Floating panel anchored to the gear button on a chart's toolbar.
   Position:fixed so it overlays everything; z-index above the
   tooltip (1100) so it pins above a hovered slice's readout. */
.okc-config-popover {
  position: fixed;
  z-index: 1150;
  min-width: 260px;
  max-width: 320px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 12px 36px var(--shadow-strong);
  padding: 12px 14px;
  font: 13px/1.4 'Inter', sans-serif;
  color: var(--text);
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.okc-config-popover[hidden] { display: none; }
.okc-cfg-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 2px;
}
.okc-cfg-title {
  font-weight: 600;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-soft);
}
.okc-cfg-close {
  background: transparent;
  border: 0;
  width: 22px;
  height: 22px;
  padding: 0;
  border-radius: 4px;
  color: var(--text-soft);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.okc-cfg-close:hover { background: var(--surface-2); color: var(--text); }
.okc-cfg-close svg { width: 12px; height: 12px; }
.okc-cfg-row {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 26px;
}
.okc-cfg-label {
  flex: 0 0 90px;
  font-size: 12px;
  color: var(--text-soft);
}
.okc-cfg-hint {
  flex-direction: column;
  align-items: flex-start;
  font-size: 12px;
  color: var(--text-soft);
}
.okc-cfg-hint code {
  background: var(--surface-2);
  padding: 1px 5px;
  border-radius: 3px;
  font: 12px 'JetBrains Mono', ui-monospace, monospace;
}
.okc-cfg-select {
  flex: 1 1 auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 4px 8px;
  font: inherit;
  color: var(--text);
}
.okc-cfg-num {
  flex: 0 0 90px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 4px 8px;
  font: inherit;
  color: var(--text);
}
.okc-cfg-chips {
  flex: 1 1 auto;
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
}
.okc-cfg-chip {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 2px 10px;
  font: 12px 'Inter', sans-serif;
  color: var(--text-soft);
  cursor: pointer;
  transition: background 0.12s, border-color 0.12s, color 0.12s;
}
.okc-cfg-chip:hover { border-color: var(--accent); color: var(--text); }
.okc-cfg-chip.on {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent-strong);
}

/* ============ Table-config popover ============
   Same shell as the chart-config popover, plus an explicit row
   layout for the moved-in view-toggle and group-by select. The
   .okt-config-stashed span (in the table toolbar) is hidden at
   rest; the popover shows it when open. */
.okt-config-stashed[hidden] { display: none; }
.okt-config-popover {
  position: fixed;
  z-index: 1150;
  min-width: 280px;
  max-width: 360px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 12px 36px var(--shadow-strong);
  padding: 12px 14px;
  font: 13px/1.4 'Inter', sans-serif;
  color: var(--text);
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.okt-config-popover[hidden] { display: none; }
.okt-config-popover .okt-cfg-row {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 28px;
}
.okt-config-popover .okt-cfg-label {
  flex: 0 0 80px;
  font-size: 12px;
  color: var(--text-soft);
}
.okt-config-popover .okt-cfg-field {
  flex: 1 1 auto;
  display: inline-flex;
  align-items: center;
}
/* Re-style the moved-in view buttons + group-by select so they
   sit inside the popover rather than the toolbar context. */
.okt-config-popover .okt-view-group {
  display: inline-flex;
  gap: 4px;
}
.okt-config-popover .okt-view-btn {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  color: var(--text-soft);
}
.okt-config-popover .okt-view-btn.active {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent-strong);
}
.okt-config-popover .okt-groupby-select {
  flex: 1 1 auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 4px 8px;
  font: inherit;
  color: var(--text);
  min-width: 0;
}
.okt-cfg-section {
  border-top: 1px solid var(--border-soft);
  padding-top: 8px;
  margin-top: 4px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.okt-cfg-subtitle {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-faint);
  margin-bottom: 2px;
}
.okt-cfg-colfilter {
  flex: 1 1 auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 4px 8px;
  font: inherit;
  color: var(--text);
  min-width: 0;
}

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

/* ============ Cover header ============ *
 * Card-like cover with two stacked radial washes (top-right + bottom-
 * left) that fade naturally into transparency, so the accent color
 * never cuts off abruptly at a hard edge. Subtle border + 1px shadow
 * give the cover its own elevation tier against the page bg. The old
 * heavy 3px primary underline is replaced by the card border + extra
 * margin so the visual seam between cover and content is quiet.
 * --------------------------------------------------------------- */
header.cover {
  position: relative;
  overflow: hidden;
  margin: 0 0 48px 0;
  padding: 44px 42px 48px 42px;
  border-radius: 20px;
  background:
    /* Diagonal wash present at every width — the two corner glows alone
       wash out on a wide desktop cover and the hero reads as flat white.
       The linear layer keeps the accent visible across the whole block
       (the way the corner radials already do on a narrow mobile card). */
    linear-gradient(118deg, var(--accent-soft) 0%, transparent 50%),
    radial-gradient(820px 460px at 108% -18%, var(--accent-soft) 0%, transparent 62%),
    radial-gradient(560px 320px at -12% 118%, var(--accent-soft) 0%, transparent 70%),
    var(--surface);
  border: 1px solid var(--border-soft);
  box-shadow: 0 1px 2px var(--shadow-soft);
}
header.cover > * { position: relative; z-index: 1; }
/* Title only — no eyebrow, no subtitle, no meta line. The full panel
   padding leaves the heading stranded in the top third of a mostly
   empty tinted rectangle, so the block shrinks to fit what it holds. */
header.cover.cover-bare { padding: 30px 42px 32px 42px; margin-bottom: 36px; }
header.cover.cover-bare h1 { margin-bottom: 0; }
header.cover .eyebrow {
  font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.18em;
  color: var(--accent-strong); font-weight: 700; margin-bottom: 16px;
}
header.cover h1 {
  font-size: 44px; line-height: 1.08; margin: 0 0 14px 0;
  color: var(--text); font-weight: 800; letter-spacing: -0.025em;
}
header.cover .subtitle {
  font-size: 17px; color: var(--text-soft); margin: 0;
  font-weight: 400; line-height: 1.55;
  max-width: 62ch;
}
header.cover .meta {
  margin-top: 24px;
  font-size: 12.5px;
  color: var(--text-soft);
  letter-spacing: 0.02em;
}
header.cover .meta-updated {
  margin-top: 4px;
  font-size: 11.5px;
  color: var(--text-faint);
  font-style: italic;
  letter-spacing: 0.02em;
}
@media (max-width: 700px) {
  header.cover { padding: 32px 24px 36px 24px; border-radius: 14px; }
  header.cover h1 { font-size: 34px; }
}

/* ============ Section headings & text ============ */
h2 {
  font-size: 28px; color: var(--primary);
  margin: 64px 0 8px 0; padding-top: 16px;
  font-weight: 800; letter-spacing: -0.015em;
}
h2 .num {
  display: inline-block; font-size: 14px;
  background: var(--accent); color: var(--on-accent);
  padding: 2px 10px; border-radius: 12px; vertical-align: middle;
  margin-right: 10px; letter-spacing: 0.05em; font-weight: 700;
  position: relative; top: -4px;
}
.section-lead { font-size: 17px; color: var(--text-soft); margin: 0 0 28px 0; line-height: 1.6; }
h3 { font-size: 21px; color: var(--text); margin: 36px 0 12px 0; font-weight: 700; letter-spacing: -0.01em; }
h4 {
  font-size: 14px; color: var(--text-soft);
  margin: 22px 0 10px 0; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em;
}
/* Titles that name a figure or a callout, and are NOT headings.
 *
 * `##` opens a section, so every one of these sat under an h2 and an h4
 * skipped a level — six breaks on docs/architecture.html. h3 is not the
 * fix either: buildTOC collects every h3 in a section, so promoting
 * them would list 43 card titles in the on-page contents of
 * docs/charts.md. They are labels for a box, not sections of the
 * document, so they are <p> and the box carries the name through
 * aria-labelledby.
 *
 * They still have to LOOK like what they replaced, which is what this
 * rule restores: the weight came from the h4 element rule above, and
 * the alignment has to be won back from the justify list — `main
 * .callout p` outranks a bare class, so these selectors carry `main`
 * too. A one-line title stretched to both edges is the exact failure
 * the justify scope was drawn to avoid. */
main .callout .callout-title,
main .okt-card-title,
main .bar-chart-title,
main .okt-chart-grid-title {
  font-weight: 700;
  text-align: start;
  -webkit-hyphens: manual;
  hyphens: manual;
}
p { margin: 0 0 18px 0; }
ul, ol { padding-left: 22px; margin: 0 0 18px 0; }
li { margin: 8px 0; }
li.task-item { list-style: none; margin-left: -18px; }
li.task-item input[type="checkbox"] { accent-color: var(--accent); vertical-align: -2px; }
dl.md-dl { margin: 0 0 18px 0; }
dl.md-dl dt { font-weight: 700; margin-top: 10px; }
dl.md-dl dd { margin: 4px 0 4px 22px; color: var(--text-soft); }
strong { color: var(--text); font-weight: 700; }
em { color: var(--text-soft); font-style: italic; }
del { color: var(--text-soft); text-decoration-color: var(--border); }

/* Inline image from `![alt](src)`. Never wider than its column, and it
   keeps the baseline of the line it sits on. */
img.okt-inline-img {
  max-width: 100%;
  height: auto;
  border-radius: 6px;
  vertical-align: middle;
}

/* Footnotes: the reference is a quiet superscript, the section at the
   end of the page is one step down in weight from body prose. */
sup.okt-fn-ref { line-height: 0; font-size: 0.75em; }
sup.okt-fn-ref a {
  text-decoration: none;
  padding: 0 2px;
  color: var(--accent-strong);
}
section.okt-footnotes { font-size: 0.94em; color: var(--text-soft); }
section.okt-footnotes ol { padding-left: 20px; }
section.okt-footnotes li { margin: 6px 0; }
section.okt-footnotes li:target { background: var(--surface-2); }
a.okt-fn-back { text-decoration: none; color: var(--text-faint); }
code:not(pre code) {
  background: var(--surface-2);
  border: 1px solid var(--border-soft);
  border-radius: 4px;
  padding: 1px 6px;
  font-size: 0.88em;
  color: var(--accent-strong);
}

/* ============ Wide tables — scroll wrap + view selector + expand ============ *
 * chrome.js wraps every <table> in .okt-table-wrap with:
 *   .okt-table-controls  → top-right view buttons + expand
 *   .okt-table-scroll    → horizontal-scroll viewport for the original table
 *   .okt-table-cards     → row-per-card grid (auto-generated if <thead>)
 *   .okt-table-list      → definition-list per row (auto-generated)
 * Edge-fade shadows on the scroll viewport hint at overflow; the expand
 * button breaks the table out of <main>'s max-width via negative margins. */
.okt-table-wrap {
  position: relative;
  margin: 32px 0;
  padding: 10px 12px 12px 12px;
  border: 1px solid var(--border-soft);
  border-radius: 12px;
  background: var(--surface);
  transition: margin 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
.okt-table-wrap:hover,
.okt-table-wrap:focus-within {
  border-color: var(--border);
}
/* Inner chrome reads as parts of a single card. */
.okt-table-wrap .okt-table-controls { margin-bottom: 10px; }
.okt-table-wrap .okt-chips {
  margin: 0 0 12px 0;
  background: var(--code-bg);
  border-color: var(--border-soft);
}
.okt-table-wrap .okt-table-scroll { background: var(--bg); }
/* The expand affordance now opens the table in the shared lightbox
   overlay (same path as image / chart / diagram expand). The old
   in-place bleed via .okt-table-wrap.expanded was visually heavy and
   collided with the sidebar grid — removed. */
.okt-table-controls {
  display: flex;
  gap: 4px;
  align-items: center;
  justify-content: flex-end;
  /* Allow secondary rows when the cumulative width of filter + stats
     + view buttons + expand + groupby exceeds the wrap's inner
     width. Without wrapping, justify-content: flex-end pushes
     overflow off the START edge, putting the filter outside the
     table border.
     DOM order: filter → stats → view-toggle → expand → groupby.
     groupby is LAST in DOM so the wrap break drops it (and only
     it) to a second line. CSS order puts groupby visually between
     stats and view-toggle when everything fits on one line. */
  flex-wrap: wrap;
  margin-bottom: 8px;
  /* Gone until asked for — the same contract as the copy button on a
     code block and the expand chip on a diagram. A table is content;
     its chrome should not be competing with the first row for the
     reader's attention. The bar keeps its layout box so revealing it
     never reflows the table, and `pointer-events: none` stops an
     invisible control from swallowing a click.
     Hover or keyboard focus on the wrap brings it back, and any active
     filter / chip / non-default view pins it open — the reader must
     always be able to see the state they are already in.

     This was raised once as the cause of "the tables have lost their
     controls" and it was not: what had actually gone was the toolbar
     of every table of six rows or fewer, hidden outright by a size
     rule with no hover to bring it back. Hover-to-reveal is the
     wanted behaviour and stays. Touch keeps them visible (no hover to
     reveal with) under the `@media (hover: none)` rule below. */
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s ease;
}
/* Left: what narrows the data, and the count that answers it.
   Right: the three buttons that act on the table as a whole — copy,
   configure, expand. Copy belongs with them; sitting between the
   filter and its own count it split a pair that is read together. */
.okt-table-controls .okt-filter   { order: 0; }
.okt-table-controls .okt-stats    { order: 1; }
.okt-table-controls .okt-groupby  { order: 2; }
.okt-table-controls .okt-view-group { order: 3; }
.okt-table-controls [data-copy]   { order: 4; }
.okt-table-controls [data-cfg]    { order: 5; }
.okt-table-controls [data-expand] { order: 6; }
/* When groupby wraps to a new line, push it to the LEFT edge
   instead of inheriting justify-content: flex-end. */
.okt-table-controls .okt-groupby { margin-right: auto; }
.okt-table-wrap:hover .okt-table-controls,
.okt-table-wrap:focus-within .okt-table-controls {
  opacity: 1;
  pointer-events: auto;
}
/* A touch device has no hover, so hover-to-reveal would make the
   controls unreachable. Keep them visible there. */
@media (hover: none) {
  .okt-table-controls { opacity: 1; pointer-events: auto; }
  /* Same reasoning, and the wrap toggle is the control that matters
     most here: it is the fix for a code block scrolling sideways, on
     the device where that hurts. It was revealed only by
     `.okt-pre-host:hover`, so on a touch screen it was a 30x30 box at
     opacity 0 that still took clicks. `.copy-btn` had a rescue keyed
     off `max-width: 768px` — a WIDTH proxy for a HOVER question, which
     leaves a touch tablet at 1024px with neither. */
  .copy-btn, .okt-wrap-btn { opacity: 1; }
}
/* A short table spans the column like every other block. It once sized
   to its content — three short cells stretched over 970px was read as a
   layout accident — but the card that shrank with it then ended short of
   the paragraph above and the code block below, and a right edge that
   steps in and out down the page reads as the bigger fault. Same rule as
   prose, callouts and diagrams: one column, one right edge. Columns
   distribute the slack; `table-layout: auto` on a full-width table gives
   each column its content share, so the cells stay legible.
   Pinned by test_presentation_measure.py and
   test_presentation_defaults.py::test_a_three_row_table_shares_the_column_edge. */
/* Force-show whenever the table is in a non-default state — an
   active filter (input has text), an active chip, or a view other
   than the default table view. The user must always be able to see
   the state they're already in, even without hovering. */
.okt-table-wrap:has(.okt-filter input:not(:placeholder-shown)) .okt-table-controls,
.okt-table-wrap:has(.okt-chip.active) .okt-table-controls,
.okt-table-wrap[data-view]:not([data-view="table"]) .okt-table-controls {
  opacity: 1;
  pointer-events: auto;
}
.okt-table-controls button {
  font: 600 11px 'Inter', sans-serif;
  letter-spacing: 0.02em;
  background: var(--surface);
  color: var(--text-soft);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 4px 10px;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.okt-table-controls button[data-expand],
.okt-table-controls button[data-copy]    { padding: 4px 6px; }
.okt-table-controls button[data-expand] svg,
.okt-table-controls button[data-cfg] svg,
.okt-table-controls button[data-copy] svg { width: 14px; height: 14px; }
.okt-table-controls button[data-cfg]     { padding: 4px 6px; }
.okt-table-controls button.okt-flash-ok {
  background: var(--success-soft, color-mix(in srgb, var(--success) 24%, var(--surface)));
  border-color: var(--success);
  color: var(--success);
}
.okt-table-controls button.okt-flash-fail {
  background: color-mix(in srgb, var(--danger) 24%, var(--surface));
  border-color: var(--danger);
  color: var(--danger);
}
.okt-table-controls button.active,
.okt-table-controls button:hover {
  background: var(--accent-soft);
  color: var(--accent-strong);
  border-color: var(--accent);
}
.okt-table-controls button:active { transform: scale(0.95); }
.okt-table-controls button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.okt-ctrl-sep {
  width: 1px;
  height: 18px;
  background: var(--border);
  margin: 0 4px;
}

/* Compact view-toggle cluster — segmented icon-only buttons. The
   group has a single rounded border; each button is borderless and
   shares the surrounding chrome. Active button reads the accent.
   Tooltip + aria-label carry the textual name. */
.okt-view-group {
  display: inline-flex;
  align-items: stretch;
  margin: 0 4px 0 0;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  overflow: hidden;
}
.okt-table-controls .okt-view-group .okt-view-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--text-soft);
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease;
}
.okt-table-controls .okt-view-group .okt-view-btn + .okt-view-btn {
  border-left: 1px solid var(--border);
}
.okt-table-controls .okt-view-group .okt-view-btn:hover {
  background: var(--accent-soft);
  color: var(--accent-strong);
}
.okt-table-controls .okt-view-group .okt-view-btn.active {
  background: var(--accent);
  color: var(--on-accent);
}
.okt-table-controls .okt-view-group .okt-view-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* Filter input — left side of the controls bar; pushes the rest right. */
.okt-filter {
  display: inline-flex;
  align-items: center;
  margin-right: 8px;
  gap: 6px;
  padding: 4px 10px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text-soft);
  transition: border-color 0.15s ease, background 0.15s ease;
  /* Flex items default to min-width: min-content; without this the
     filter refuses to shrink and overflows the wrap on narrow widths. */
  min-width: 0;
  flex-shrink: 1;
}
/* Stats lives next to the filter — they're read together. Pure
   numerics ("5" or "3/5") so the visual language stays neutral. */
.okt-table-controls .okt-stats {
  margin-left: 0;
  margin-right: auto;
}

/* Group-by selector — pick any column at runtime to bucket the rows. */
.okt-groupby {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-right: 6px;
  font: 500 11px 'Inter', sans-serif;
  letter-spacing: 0.02em;
  color: var(--text-soft);
}
.okt-groupby-label {
  text-transform: uppercase;
  font-weight: 700;
  letter-spacing: 0.06em;
}
.okt-groupby-select {
  font: inherit;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 3px 6px;
  cursor: pointer;
  max-width: 160px;
}
.okt-groupby-select:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.okt-filter:focus-within {
  border-color: var(--accent);
  color: var(--accent-strong);
  background: var(--surface);
}
.okt-filter svg { width: 14px; height: 14px; flex-shrink: 0; }
.okt-filter input {
  border: none; background: transparent; outline: none;
  font: 500 12px 'Inter', sans-serif;
  color: var(--text);
  min-width: 0;
  width: 140px;
  /* Lets the input shrink (down to min-width) when the controls bar
     is crowded — paired with .okt-filter { min-width: 0 }. */
  flex: 1 1 0;
}
.okt-filter input::placeholder { color: var(--text-faint); }

/* Sort indicators on <thead th> when the cell is bound as sortable.
   Position comes from `table thead th` — every sortable cell is one —
   and `position: sticky` is also a containing block for the
   absolutely-positioned rank badge, so it doubles for both jobs. It
   used to be restated here with `top: 0`, a second copy of the offset
   that the rail rule above would then have had to be told about. */
.okt-sortable {
  cursor: pointer;
  user-select: none;
  transition: background 0.12s ease, color 0.12s ease;
}
.okt-sortable:hover { background: var(--accent-soft) !important; color: var(--accent-strong) !important; }
/* The arrow belongs to the label, so it sits next to the label. Pinned
   to the cell's right edge it drifted a whole column away on a wide
   prose column — a control floating in white space, with the word it
   acts on at the other end of it — and it collided with the multi-sort
   rank badge, which is pinned there for real. The badge keeps the edge
   (it is a rank across columns, and ranks line up); only a cell that
   has one reserves the room. */
.okt-sortable::after {
  content: '⇅';
  margin-left: 6px;
  font-size: 11px;
  opacity: 0.35;
  transition: opacity 0.15s ease, color 0.15s ease;
}
.okt-sortable[data-sort-rank] { padding-right: 22px !important; }
.okt-sortable:hover::after { opacity: 0.65; }
.okt-sortable.okt-sort-asc::after  { content: '↑'; opacity: 1; color: var(--accent); }
.okt-sortable.okt-sort-desc::after { content: '↓'; opacity: 1; color: var(--accent); }
/* Multi-column sort rank badge — only shown when more than one
   column is active. Small accent-coloured numeral after the arrow
   so the reader can see "this is sort #2" at a glance. */
.okt-sortable[data-sort-rank]::before {
  content: attr(data-sort-rank);
  position: absolute;
  top: 50%;
  right: 4px;
  transform: translateY(-50%);
  background: var(--accent);
  color: var(--bg);
  font: 700 9px 'Inter', sans-serif;
  border-radius: 7px;
  min-width: 13px;
  height: 13px;
  padding: 0 3px;
  line-height: 13px;
  text-align: center;
  pointer-events: none;
}
.okt-sortable[data-sort-rank] { padding-right: 36px !important; }
.okt-sortable:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
/* CSS-driven view switching — the wrapper's data-view attribute selects
   which container is visible. Avoids the [hidden]-attribute vs
   display:grid conflict where cards.hidden was overridden. */
/* View-toggle visibility. Each [data-view] hides the three NON-active
   variants. Board is the fourth alternate; default (attribute absent)
   shows the table view. */
.okt-table-wrap[data-view="table"] .okt-table-cards,
.okt-table-wrap[data-view="table"] .okt-table-list,
.okt-table-wrap[data-view="table"] .okt-table-board,
.okt-table-wrap[data-view="list"]  .okt-table-scroll,
.okt-table-wrap[data-view="list"]  .okt-table-cards,
.okt-table-wrap[data-view="list"]  .okt-table-board,
.okt-table-wrap[data-view="cards"] .okt-table-scroll,
.okt-table-wrap[data-view="cards"] .okt-table-list,
.okt-table-wrap[data-view="cards"] .okt-table-board,
.okt-table-wrap[data-view="board"] .okt-table-scroll,
.okt-table-wrap[data-view="board"] .okt-table-cards,
.okt-table-wrap[data-view="board"] .okt-table-list { display: none !important; }
.okt-table-wrap:not([data-view]) .okt-table-cards,
.okt-table-wrap:not([data-view]) .okt-table-list,
.okt-table-wrap:not([data-view]) .okt-table-board { display: none !important; }

/* Chip filter rack — tabular layout: column 1 is the label cell, column
   2 is the chip cell. Multiple filterable columns stack vertically; the
   labels right-align so the chip flush-left edge is shared. Each chip
   is a toggle and its inline count badge previews how many rows would
   survive if it were toggled (within current other-column filters). */
.okt-chips {
  display: grid;
  /* `max-content` on the label track sizes the whole rack from the
     longest column name, and a table's column name is prose an author
     writes — `B: open-source Spark 3.5.4 + open-source Iceberg 1.10.0`
     measured 381px against a 360px viewport, so the page scrolled
     sideways. minmax(0, max-content) keeps the label column exactly as
     wide as it wants while it fits and lets it wrap when it does not,
     which is the same repair the `1fr` tracks took. */
  grid-template-columns: minmax(0, max-content) minmax(0, 1fr);
  column-gap: 14px;
  row-gap: 10px;
  align-items: center;
  margin: 0 0 12px 0;
  padding: 10px 14px;
  border: 1px dashed var(--border-soft);
  border-radius: 8px;
  background: var(--surface);
}
.okt-chip-group {
  display: contents;
}
.okt-chip-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}
.okt-chip-label {
  text-align: right;
  font: 600 11px 'Inter', sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-soft);
  overflow-wrap: anywhere;
}
/* Below this, a label and its chips side by side leave neither enough
   room: the label wraps to four lines and the chips to a 60px column.
   The rack stacks instead — label above its own chips — which is the
   same information in the order it is read. */
@media (max-width: 560px) {
  .okt-chips {
    grid-template-columns: minmax(0, 1fr);
    row-gap: 6px;
  }
  .okt-chip-label {
    text-align: left;
    margin-top: 4px;
  }
}
.okt-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* 24px is the pointer-target minimum; the chip rack is one of the
     most-clicked surfaces in a table, and 3px padding left it at 23. */
  min-height: 24px;
  padding: 3px 10px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text);
  font: 500 12px 'Inter', sans-serif;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease, opacity 0.15s ease, transform 0.1s ease;
}
.okt-chip:hover {
  border-color: var(--accent);
  color: var(--accent-strong);
}
.okt-chip:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.okt-chip:active { transform: scale(0.97); }
.okt-chip.active {
  background: var(--accent);
  color: var(--surface);
  border-color: var(--accent);
}
.okt-chip-count {
  font: 600 10.5px 'Inter', sans-serif;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent-strong);
  min-width: 14px;
  text-align: center;
}
.okt-chip.active .okt-chip-count {
  background: rgba(255,255,255,0.22);
  color: inherit;
}
.okt-chip.okt-chip-empty {
  opacity: 0.4;
}
.okt-chip-clear {
  display: none;
  font: 600 14px 'Inter', sans-serif;
  width: 22px;
  height: 22px;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--text-soft);
  cursor: pointer;
  border-radius: 999px;
  align-items: center;
  justify-content: center;
  line-height: 1;
}
.okt-chip-group.has-active .okt-chip-clear { display: inline-flex; }
.okt-chip-clear:hover { color: var(--accent-strong); background: var(--accent-soft); }

.okt-table-scroll {
  /* Default: horizontal scroll on overflow so cells keep their 9ch
     readable floor even when a many-column table's natural width
     exceeds the container. Cells still word-wrap inside their column
     (white-space: normal + overflow-wrap: anywhere on td); the scroll
     only kicks in when even the minimum column widths can't fit.
     This box is a SCROLL CONTAINER, and that is what a sticky header
     sticks to — `overflow-x: auto` makes the y axis a scroll axis
     too, so the `position: sticky` on every thead th pinned it to a
     box that grows with the table and never scrolls. It used to say
     this was acceptable "since the wrapper is tall enough that thead
     still feels sticky"; measured, the labels left with the first
     row. So a table that FITS its column gives the container up:
     chrome.js measures and sets `data-fit`, the box stops clipping,
     and the header sticks to the viewport natively. A wider table
     keeps it and gets `.okt-table-ghost` below instead. */
  overflow-x: auto;
  border-radius: 8px;
  background: var(--surface);
}
/* The header a wide table shows while its own has scrolled away. A
   clone of the thead, OUTSIDE the scroll container so it can stick to
   the viewport, sized cell by cell from the real one and scrolled in
   step with the box — a scroller of its own with the bar hidden, not a
   clipped box with a transform, so a wheel over the stuck header moves
   the rows under it and nothing in it is out of the reader's reach. It sits
   in flow over the real thead (own height, pulled back by the same
   margin) and is invisible and untouchable there — the author's header
   answers the pointer at rest; the ghost only shows once chrome.js
   sees it stuck above the real one. The lightbox and the alternate
   views have no page scroll under them, so no ghost there.

   It is a `display: table` div, not a `<table>`. A second table in the
   wrap is the first one every `querySelector('table')` finds — the
   rail's thumbnail read "0 × 0" off it — and a `role` cannot take an
   element out of a selector. The thead is cloned into it as-is, so
   the cell rules below name both hosts. */
.okt-table-ghost {
  position: sticky;
  top: calc(var(--okt-rail-h) / var(--oku-text-scale, 1));
  z-index: 3;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  visibility: hidden;
  pointer-events: none;
  background: var(--bg);
}
.okt-table-ghost::-webkit-scrollbar { display: none; }
.okt-table-ghost[data-stuck="1"] { visibility: visible; pointer-events: auto; }
.okt-table-ghost[hidden],
.okt-table-wrap[data-view]:not([data-view="table"]) > .okt-table-ghost,
.okt-lightbox-content > .okt-table-wrap > .okt-table-ghost { display: none; }
.okt-table-ghost > div {
  display: table; table-layout: fixed; border-collapse: collapse;
  font-size: 14.5px; line-height: 1.5;
}
/* `relative`, not `static`: the multi-sort rank badge is absolutely
   positioned against its cell. Not `sticky`: this box clips, so a
   sticky cell would climb inside it. */
.okt-table-ghost th { position: relative; top: auto; }
.okt-table-ghost .okt-col-resize { display: none; }
.okt-table-scroll:has(> table.okt-nowrap) {
  overflow-x: auto;
  /* Edge fade shadows hint at horizontal overflow on opt-in tables. */
  background:
    linear-gradient(to right, var(--surface) 30%, transparent),
    linear-gradient(to right, transparent, var(--surface) 70%) 100% 0,
    radial-gradient(farthest-side at 0   50%, var(--shadow-strong), transparent),
    radial-gradient(farthest-side at 100% 50%, var(--shadow-strong), transparent) 100% 0;
  background-repeat: no-repeat;
  background-color: var(--surface);
  background-size: 40px 100%, 40px 100%, 14px 100%, 14px 100%;
  background-attachment: local, local, scroll, scroll;
}
/* After the nowrap rule: it re-declares `overflow-x: auto` at the
   same specificity, and a nowrap table that happens to fit is still a
   table that fits. */
.okt-table-scroll[data-fit="1"] { overflow-x: visible; }
.okt-table-scroll > table {
  margin: 0;
  /* Default: fit the wrap container so cell content wraps inside its
     column instead of forcing horizontal scroll. Tables with many
     genuinely-unbreakable columns can opt in to single-line layout
     with the okt-nowrap class — that's when the scroll container
     earns its keep. */
  width: 100%;
  table-layout: auto;
}
.okt-table-scroll > table.okt-nowrap {
  width: max-content;
  min-width: 100%;
}

/* Cards view — auto-fit grid, key/value stack per cell.
   The container is a FIELD, not more card. Both this and the list
   view used to paint --surface behind cards that are themselves
   --surface, which left the border as the only thing separating a
   card from the space around it — and a hairline on a same-coloured
   background is exactly the boundary a reader cannot find. The table
   view already solved this: .okt-table-scroll sits on a darker field.
   Same treatment here. */
.okt-table-cards,
.okt-table-list {
  background: var(--surface-2);
  border-radius: 10px;
  padding: 12px;
}
/* `min(Npx, 100%)`, never a bare `Npx`, as the floor of an auto-fill
   track. A bare floor is a width the track REFUSES to go below, so a
   container narrower than it overflows — and the container is now
   narrower than it looks: the reader's text scale divides the available
   width, so at 2x on a 360px phone a grid has 136px to lay out in.
   Measured before the fix: eight compare cards each 400px wide inside a
   360px viewport, and the page scrolled sideways.
   `min(..., 100%)` keeps the intended floor everywhere it fits and
   yields to the container everywhere it does not. */
.okt-table-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(260px, 100%), 1fr));
  gap: 12px;
  /* Size each card to its own content. Stretching a row to its tallest
     member was invisible while the card edge was — but now that the
     edge reads, the slack shows up as an empty panel. */
  align-items: start;
}
/* Group rows from the source table become full-width subheaders in
   the cards grid. Same vocabulary in the list view below. */
.okt-cards-group {
  grid-column: 1 / -1;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--accent-strong);
  margin: 10px 0 -2px 0;
  padding-bottom: 6px;
  border-bottom: 2px solid var(--accent-soft);
}
.okt-cards-group:first-child { margin-top: 0; }
/* Border defines the edge, a tight two-layer shadow lifts it off the
   field. Diffuse shadows read as fog; the 1px contact layer is what
   actually makes an edge legible. No hover state — a card that lifts
   under the pointer promises a click it does not handle. */
.okt-card {
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: 10px;
  padding: 14px 16px;
  box-shadow: 0 1px 2px var(--shadow-soft), 0 4px 10px -6px var(--shadow-strong);
}
/* ============ The raise: light moves, content does not ============ *
 * Every card kind used to answer the pointer with
 * `transform: translateY(-2px)`. That takes the card's CONTENT with it.
 * The sentence the reader is on jumps two pixels and settles — too
 * small to read as an effect, exactly the right size to read as a
 * rendering fault — and it fires on every card the pointer crosses on
 * its way somewhere else.
 *
 * The raise is made of light now: a ring grows outward from the card's
 * edge and the shadow deepens under it. Both are box-shadow layers, so
 * nothing is laid out and nothing reflows; the text stays at the pixel
 * the reader was reading it at. Anything that changes a card's box —
 * transform, padding, border-WIDTH, font-size — is the defect, not the
 * effect. Border COLOUR is free, and so is opacity.
 *
 * The resting state carries the same LAYER COUNT as the hover state,
 * collapsed to zero spread and zero blur so it paints nothing.
 * box-shadow only interpolates layer-for-layer; a rule that omits the
 * ring at rest snaps it on instead of growing it. */
:root {
  --okt-ring-off: 0 0 0 0 var(--accent-soft);
  --okt-ring-on:  0 0 0 3px var(--accent-soft);
}

/* Anchor / clickable cards — match the table-row affordance: pointer
   cursor, the whole card is the click target, no link blue. */
a.okt-card,
.okt-card[onclick],
.okt-card[role="button"] {
  cursor: pointer;
  text-decoration: none;
  color: inherit;
  display: block;
  box-shadow: var(--okt-ring-off), 0 1px 2px var(--shadow-soft), 0 4px 10px -6px var(--shadow-strong);
  transition: border-color 0.15s ease, box-shadow 0.18s ease;
}
a.okt-card:hover,
.okt-card[onclick]:hover,
.okt-card[role="button"]:hover {
  border-color: var(--accent);
  box-shadow: var(--okt-ring-on), 0 10px 24px -6px var(--shadow-strong), 0 4px 10px -6px var(--shadow-strong);
}
a.okt-card:focus-visible,
.okt-card[tabindex]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.okt-card-row { margin-bottom: 10px; }
.okt-card-row:last-child { margin-bottom: 0; }
.okt-card-key {
  display: block;
  font-size: 10.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--text-soft);
  margin-bottom: 2px;
}
.okt-card-val { display: block; font-size: 13.5px; color: var(--text); line-height: 1.45; }

/* List view — every row is its own 2-col <table class="okt-list-card">.
   The first column is the header label; the second column is the cell
   value. Same vocabulary as the main table, just rotated 90°.
   Group rows become section headings (.okt-list-group) at the same
   visual weight as the cards-view group header. Rows that carry an
   onclick/href get wrapped in .okt-list-row so the affordance moves
   across all three views. */
.okt-table-list { display: flex; flex-direction: column; gap: 18px; }
.okt-list-group {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: 11px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--accent-strong);
  margin: 18px 0 0 0;
  padding-bottom: 6px;
  border-bottom: 2px solid var(--accent-soft);
}
.okt-list-group:first-child { margin-top: 0; }
/* Each list item: clear visual card boundary so two consecutive items
   never blur together. Outer border is --line-strong against the
   --surface-2 field on .okt-table-list, plus the same two-layer
   contact shadow the cards view uses. No hover state — the card is
   not a button, and deepening its edge under the pointer said it was. */
.okt-list-card {
  width: 100%;
  border-collapse: collapse;
  margin: 0;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: 10px;
  overflow: hidden;
  box-shadow: 0 1px 2px var(--shadow-soft), 0 4px 10px -6px var(--shadow-strong);
}

/* ====== Board (kanban) view ====== *
 * One lane per active group; each lane stacks its rows as compact
 * cards. Lanes flow horizontally and scroll when they overflow the
 * available width — never stack vertically (a wrapping kanban defeats
 * the purpose of the metaphor). Tight lane width (260px) keeps the
 * card density readable on a typical viewport. */
.okt-table-board {
  display: flex;
  flex-direction: row;
  gap: 16px;
  overflow-x: auto;
  align-items: flex-start;
  padding-bottom: 8px;
}
.okt-board-lane {
  flex: 0 0 260px;
  display: flex;
  flex-direction: column;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 12px;
  max-height: 70vh;
}
.okt-board-lane-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 14px 8px;
  border-bottom: 1px solid var(--line);
  position: sticky; top: 0;
  background: inherit;
  border-radius: 12px 12px 0 0;
}
.okt-board-lane-title {
  font-size: 12px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--accent-strong);
}
.okt-board-lane-count {
  font: 700 11px 'JetBrains Mono', monospace;
  color: var(--text-soft);
  background: var(--accent-soft);
  border-radius: 999px;
  padding: 1px 8px;
  min-width: 22px; text-align: center;
}
.okt-board-lane-body {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 10px 12px 12px;
  display: flex; flex-direction: column;
  gap: 10px;
}
.okt-board-card {
  display: block;
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: 8px;
  padding: 10px 12px;
  text-decoration: none;
  color: inherit;
  box-shadow: var(--okt-ring-off), 0 1px 2px var(--shadow-soft);
  transition: box-shadow 0.18s ease, border-color 0.18s ease;
}
.okt-board-card:hover {
  border-color: var(--accent);
  box-shadow: var(--okt-ring-on), 0 2px 6px var(--shadow-strong, var(--shadow-soft));
}
.okt-board-card:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.okt-board-card-row {
  display: grid;
  grid-template-columns: 80px minmax(0, 1fr);
  gap: 6px;
  font-size: 12.5px;
  line-height: 1.45;
  padding: 2px 0;
}
.okt-board-card-key {
  font-size: 10.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--text-soft);
  align-self: start;
}
.okt-board-card-val {
  color: var(--text);
  word-wrap: break-word;
}
.okt-list-card > tbody > tr + tr > th,
.okt-list-card > tbody > tr + tr > td {
  border-top: 1px solid var(--border-soft);
}
.okt-list-card th[scope="row"] {
  text-align: left;
  vertical-align: top;
  font-size: 10.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--text-soft);
  padding: 8px 12px;
  width: 1%;
  white-space: nowrap;
  background: var(--surface-strong, var(--surface));
}
.okt-list-card td {
  padding: 8px 12px;
  font-size: 14px;
  color: var(--text);
  line-height: 1.5;
  vertical-align: top;
}
.okt-list-row {
  display: block;
  text-decoration: none;
  color: inherit;
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.15s ease;
}
.okt-list-row.okt-list-row-static { cursor: default; }
/* A filled row is cue enough; the 2px nudge that used to come with it
   moved the row's text. See "The raise" above. */
.okt-list-row:not(.okt-list-row-static):hover {
  background: var(--accent-soft);
}
.okt-list-row:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Stats span — sits at the far right of the controls bar so the reader
   always knows what they're looking at. Goes accent-strong while a
   filter or chip set is narrowing the visible rows. */
.okt-stats {
  margin-left: 8px;
  font: 500 11px 'Inter', sans-serif;
  letter-spacing: 0.02em;
  color: var(--text-faint);
  transition: color 0.15s ease;
  white-space: nowrap;
}
.okt-stats.okt-stats-filtered { color: var(--accent-strong); }
.okt-stats.okt-stats-empty { color: var(--text-soft); font-style: italic; }

/* Collapsible group chevron — sits before the title in every view.
   Rotates -90° (points right) when the group is collapsed; clicking
   it (or the group header band) toggles. The badge keeps the live
   row count for the group regardless of collapse state, so the
   reader still sees "this group has N rows" while folded. */
.okt-group-chevron {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Box sized to the pointer-target minimum; the glyph inside stays
     12px so the header rhythm is unchanged. */
  width: 24px;
  height: 24px;
  margin-right: 4px;
  border-radius: 4px;
  color: var(--text-soft);
  cursor: pointer;
  transition: transform 0.18s ease, background 0.15s ease, color 0.15s ease;
  vertical-align: middle;
}
.okt-group-chevron svg { width: 12px; height: 12px; }
.okt-group-chevron:hover { background: var(--accent-soft); color: var(--accent-strong); }
.okt-group-chevron:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.okt-collapsed .okt-group-chevron { transform: rotate(-90deg); }
.okt-collapsible { cursor: pointer; }

/* Group count badge — sits between the chevron and the title in every
   view so the numbers align at the same x across rows. Numeric pill
   shape carries the meaning; no English word required. */
.okt-group-count {
  display: inline-block;
  margin-right: 8px;
  padding: 1px 8px;
  min-width: 22px;
  text-align: center;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent-strong);
  font: 600 10.5px 'Inter', sans-serif;
  letter-spacing: 0.02em;
  text-transform: none;
  vertical-align: baseline;
}
.okt-cards-group { display: flex; align-items: baseline; gap: 4px; }
.okt-group-title { display: inline; }

/* ============ Synced highlight pairs · [data-bind] ============ *
 * Any two elements sharing `data-bind="<key>"` flash together when
 * either is hovered or focused. Inline elements get a subtle accent
 * background + boxed outline; block elements get a stronger ring.
 * Always animated so the binding feels live. */
[data-bind] {
  cursor: pointer;
  border-radius: 3px;
  transition: background 0.18s ease, box-shadow 0.18s ease, color 0.18s ease;
}
[data-bind].bind-active {
  background: var(--accent-soft);
  box-shadow: 0 0 0 3px var(--accent-soft);
  color: var(--accent-strong);
}
/* Inside <pre>, the active style needs to outscale a bit so it's visible
   against the syntax tokens — use a real outline instead of a box-shadow
   (which would clip when overflow-x scrolls). */
pre [data-bind].bind-active,
.okt-table-scroll [data-bind].bind-active {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  background: var(--accent-soft);
}

/* ============ <oku-annotated-code> — numbered code-side annotations ============ */
oku-annotated-code {
  display: block;
  margin: 24px 0;
  border: 1px solid var(--border);
  border-radius: 12px;
  overflow: hidden;
  background: var(--surface);
}
oku-annotated-code > pre {
  margin: 0;
  border: none;
  border-radius: 0;
  border-bottom: 1px solid var(--border-soft);
}
.okc-anno-marker {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px; height: 18px;
  font: 700 10.5px 'JetBrains Mono', monospace;
  border-radius: 50%;
  background: var(--accent);
  color: var(--bg);
  border: none;
  cursor: pointer;
  padding: 0;
  margin: 0 2px;
  vertical-align: 1px;
  transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1),
              box-shadow 0.18s ease;
}
.okc-anno-marker:hover,
.okc-anno-marker:focus-visible,
.okc-anno-marker.hovered {
  transform: scale(1.3);
  box-shadow: 0 0 0 3px var(--accent-soft);
  outline: none;
}
/* Per-line annotation slot for annotated-code. Lives in its OWN grid
   column, placed adjacent to the code (after the line number + fold
   marker) so the marker reads as "this circle annotates the line on
   its right" rather than floating in the far gutter. Tooltip on hover
   shows the annotation body so the reader can preview without jumping
   to the list below. */
/* [num 32px] [fold 14px] [anno 22px] [content 1fr]
 *
 * Keyed off the line HAVING the annotation cell, not off a class on the
 * host. The host class only says "this block wants a gutter"; whether a
 * given line carries the cell is a separate fact, and the two came
 * apart. Any later pass that re-wraps the code lines (the page's own
 * line-number pass, a second highlight) rebuilds them from cloned text
 * and the slots go with it — while `okc-anno-gutter-on` stayed on. The
 * content cell then landed in the 22px annotation column, and a 27-
 * character line of Python wrapped to 450px tall.
 *
 * :has() makes the rule true by construction: no cell, no 4th column. */
.okc-anno-wrap.okc-anno-gutter-on pre.okt-line-numbered
  .okt-code-line:has(> .okc-anno-line-marker) {
  grid-template-columns: 32px 14px 22px minmax(0, 1fr);
}
.okc-anno-wrap.okc-anno-gutter-on .okc-anno-line-marker {
  position: relative;          /* anchor for absolute tooltip */
  display: inline-flex;
  align-items: start;
  justify-content: center;
  user-select: none;
  -webkit-user-select: none;
  pointer-events: none;       /* per-marker re-enables it */
}
.okc-anno-wrap.okc-anno-gutter-on .okc-anno-line-marker .okc-anno-marker {
  margin: 0;
  pointer-events: auto;
  /* Smaller than the legacy 18px chip so it fits the 22px gutter
     column without dominating. */
  width: 14px; height: 14px;
  font-size: 9px;
  font-weight: 700;
  /* Centre the marker's vertical mid-line with the line-number's
     text mid-line. The line-number text sits at row top with
     line-height ≈ 22px → its visual center is ~11px from the top;
     the 14px circle's center is 7px from its own top, so a 4px
     margin-top brings the two centres into alignment. Stays at the
     row's first visual line when content wraps, so the marker
     remains attached to the logical line it annotates. */
  margin-top: 4px;
}
/* Tooltip on marker hover/focus. Sits BELOW the marker's line so it
   never blocks the code the annotation refers to. Anchored to the
   slot's left edge; `width: max-content` lets it grow to its content
   up to max-width based on the annotation body. The line itself is
   highlighted via .okt-anno-target so the reader can see at a glance
   which row(s) the chip points at. */
/* Tooltip always renders ABOVE the trigger marker so it never
   shifts the surrounding code lines downward. The setHover wiring
   in chrome.js now uses position: fixed + viewport coords so the
   tooltip escapes the annotated-code wrapper's clipping context
   and stays visible against the viewport edge. The CSS default
   stays anchored 'above' for the static (no-JS-positioning) case. */
.okc-anno-tip {
  position: fixed;
  display: none;
  z-index: 9999;
  width: max-content;
  max-width: 360px;
  min-width: 220px;
  padding: 10px 14px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  box-shadow: 0 6px 18px var(--shadow-strong, var(--shadow-soft));
  font: 400 13.5px 'Inter', system-ui, sans-serif;
  letter-spacing: 0;
  line-height: 1.5;
  color: var(--text);
  pointer-events: none;        /* hover-only preview; not interactive */
  white-space: normal;
  word-break: normal;
  overflow-wrap: anywhere;
  transform: translate(-50%, -100%);
  margin-top: -10px;
}
.okc-anno-tip code {
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 12px;
  background: var(--surface-2, var(--code-bg));
  padding: 1px 4px;
  border-radius: 3px;
}

/* Hover-target highlight on the line(s) the annotation refers to.
   The accent-soft band runs full-row width and never obscures the
   text — so the reader sees which line(s) belong to the chip the
   moment they hover the marker. Activated by JS toggling
   .okt-anno-target on the relevant .okt-code-line spans. */
.okc-anno-wrap pre.okt-line-numbered .okt-code-line.okt-anno-target {
  background: var(--accent-soft);
  transition: background 0.12s ease;
}
/* Substring mark for `annotation.match` — narrow, inline highlight
   so the chip can point at a token rather than a whole line. Light
   underline at rest; the row's background lift applies via the line-
   level rule above when the line is also in the target set. */
.okc-anno-substr {
  background: transparent;
  color: inherit;
  border-bottom: 1px dashed var(--accent);
  border-radius: 2px;
  padding: 0 1px;
  transition: background 0.12s ease, color 0.12s ease;
}
.okc-anno-substr.hovered {
  background: var(--accent);
  color: var(--bg);
  border-bottom-color: transparent;
}
/* Substring-anchored auto-marker — sits inline immediately before the
   first highlighted occurrence. Smaller than the gutter chip so it
   doesn't crowd the surrounding tokens. */
.okc-anno-marker.okc-anno-marker-substr {
  display: inline-flex;
  vertical-align: baseline;
  margin: 0 4px 0 0;
  height: 1.25em;
  min-width: 1.25em;
  padding: 0 4px;
  font-size: 10.5px;
  line-height: 1;
  border-radius: 50%;
}
/* JS-positioned: setHover toggles display:block + viewport coords
   (see _wireInteractivity in chrome.js). The CSS-only hover rule
   no longer fires because position:fixed without coords would
   render at (0,0). */

.okc-anno-list {
  list-style: none;
  margin: 0;
  padding: 8px 0;
  background: var(--surface-2);
}
.okc-anno-item {
  display: flex;
  gap: 12px;
  padding: 10px 16px;
  border-left: 3px solid transparent;
  transition: background 0.15s ease, border-left-color 0.15s ease, transform 0.15s ease;
  cursor: pointer;
}
.okc-anno-item:hover,
.okc-anno-item:focus-visible,
.okc-anno-item.hovered {
  background: var(--accent-soft);
  border-left-color: var(--accent);
  outline: none;
}
.okc-anno-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--bg);
  font: 700 11px 'JetBrains Mono', monospace;
  flex-shrink: 0;
  margin-top: 2px;
}
/* `flex: 1` sets a zero basis and leaves `min-width: auto`, which is
   the part that refuses to shrink below the longest unbreakable run in
   the annotation — and an annotation is prose about code, so it names
   identifiers. */
.okc-anno-body { font-size: 14px; line-height: 1.55; color: var(--text); flex: 1; min-width: 0; overflow-wrap: anywhere; }
.okc-anno-body p:first-child { margin-top: 0; }
.okc-anno-body p:last-child  { margin-bottom: 0; }
.okc-anno-body code {
  background: var(--surface);
  border: 1px solid var(--border-soft);
  padding: 1px 4px;
  border-radius: 3px;
  font-size: 0.92em;
}

/* ============ Prism syntax tokens (themed via chrome vars) ============ *
 * Prism is lazy-loaded from a CDN by chrome.js when at least one
 * <code class="language-..."> block exists. Tokens derive from the same
 * --accent/--text-soft/etc. variables that drive the rest of the kit,
 * so light/dark theme swaps without a second stylesheet.
 * --------------------------------------------------------------------- */
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
  background: transparent;
  color: var(--text);
  font-family: 'JetBrains Mono', monospace;
  text-shadow: none;
}
.token.comment, .token.prolog, .token.doctype, .token.cdata {
  color: var(--text-faint); font-style: italic;
}
.token.punctuation { color: var(--text-soft); }
.token.namespace   { opacity: 0.7; }
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.property,
.token.deleted     { color: var(--accent); }
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted    { color: var(--success); }
.token.operator,
.token.entity,
.token.url         { color: var(--text-soft); }
.token.atrule,
.token.attr-value,
.token.keyword     { color: var(--accent-strong); font-weight: 500; }
.token.function,
.token.class-name  { color: var(--warning); }
.token.regex,
.token.important,
.token.variable    { color: var(--danger); }
.token.important, .token.bold { font-weight: 700; }
.token.italic      { font-style: italic; }
.token.entity      { cursor: help; }

/* ============ TL;DR block ============ */
/* The far stop is --surface, not --surface-2: a gradient wants to end
   on a colour with no hue of its own, and --surface-2 is a tinted
   surface even after the de-saturation above. */
.tldr {
  margin: 0 0 40px 0; padding: 28px 32px;
  background: linear-gradient(135deg, var(--accent-soft) 0%, var(--surface) 100%);
  border: 1px solid var(--border); border-radius: 12px;
}
/* Kept in the accessibility tree and in the TOC, out of the visual
   flow. Not `display: none` — that would drop it from both. */
.okt-sr-only {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%);
  overflow: hidden; white-space: nowrap;
}
.tldr .tldr-label {
  font-size: 11px; font-weight: 800; letter-spacing: 0.15em;
  text-transform: uppercase; color: var(--accent-strong);
  margin-bottom: 12px; display: inline-block;
  padding: 4px 10px; background: var(--surface); border-radius: 12px;
}
.tldr h2 { margin: 0 0 16px 0; border: none; padding-top: 0; font-size: 22px; }
.tldr .one-line { font-size: 17px; line-height: 1.55; font-weight: 600; color: var(--primary); margin: 0 0 16px 0; }
.tldr ul { margin-bottom: 0; }
.tldr li { font-size: 15.5px; }

/* ============ Callouts ============ */
.callout {
  position: relative;
  margin: 22px 0; padding: 18px 22px 18px 48px; border-radius: 10px;
  border-left: 5px solid var(--accent); background: var(--accent-soft);
}
/* Inline-SVG glyph per callout type, masked with the accent colour
   so it inherits the per-type tint. No circle wrapper — the icon
   sits flat in the padding gutter like a modern docs callout. */
.callout::before {
  content: '';
  position: absolute;
  left: 16px;
  top: 18px;
  width: 20px;
  height: 20px;
  background-color: var(--accent-strong);
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: 20px 20px;
  mask-size: 20px 20px;
  -webkit-mask-image: var(--callout-icon, none);
  mask-image: var(--callout-icon, none);
}
/* Per-type icons — encoded inline so there's no extra HTTP fetch
   and the kit stays single-file-bootable. Feather-style line icons. */
.callout.info,
.callout.note { --callout-icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='10'/><line x1='12' y1='16' x2='12' y2='12'/><line x1='12' y1='8' x2='12.01' y2='8'/></svg>"); }
.callout.tip { --callout-icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M9 18h6'/><path d='M10 22h4'/><path d='M12 2a7 7 0 0 0-4 12.7c1 .7 1.5 1.8 1.5 3V17h5v-.7c0-1.2.5-2.3 1.5-3A7 7 0 0 0 12 2z'/></svg>"); }
.callout.warn,
.callout.warning,
.callout.caution { --callout-icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path 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'/><line x1='12' y1='9' x2='12' y2='13'/><line x1='12' y1='17' x2='12.01' y2='17'/></svg>"); }
.callout.danger { --callout-icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='10'/><line x1='4.93' y1='4.93' x2='19.07' y2='19.07'/></svg>"); }
.callout.success { --callout-icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M22 11.08V12a10 10 0 1 1-5.93-9.14'/><polyline points='22 4 12 14.01 9 11.01'/></svg>"); }
.callout.neutral { --callout-icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='9 11 12 14 22 4'/><path d='M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11'/></svg>"); }
/* IMPORTANT is one of the five standard GFM types and had no entry
   here. With no --callout-icon the ::before falls back to `mask-image:
   none`, and a 20x20 box painted --accent-strong with no mask renders
   as a solid square — a filled block where the glyph should be. Speech
   bubble with an exclamation, matching GitHub's own reading of the
   type: this is a remark you must not miss. */
.callout.important { --callout-icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z'/><line x1='12' y1='7.5' x2='12' y2='11'/><line x1='12' y1='13.6' x2='12.01' y2='13.6'/></svg>"); }
/* Per-type tint on the icon — picks up the same colour family as
   the heading text below it. */
.callout.warn::before,
.callout.warning::before,
.callout.caution::before { background-color: var(--warning); }
.callout.danger::before  { background-color: var(--danger); }
.callout.success::before,
.callout.tip::before     { background-color: var(--success); }
.callout.neutral::before { background-color: var(--text-soft); }
.callout.info::before,
.callout.note::before    { background-color: var(--accent-strong); }
/* Note: schema accepts "warn"; ".warning" kept as alias for legacy HTML. */
.callout.warn,
.callout.warning,
.callout.caution { border-left-color: var(--warning); background: var(--warning-soft); }
.callout.danger  { border-left-color: var(--danger);  background: var(--danger-soft); }
.callout.success,
.callout.tip     { border-left-color: var(--success); background: var(--success-soft); }
.callout.neutral { border-left-color: var(--text-soft); background: var(--surface-2); }
.callout.note,
.callout.info    { border-left-color: var(--accent); background: var(--accent-soft); }
.callout .callout-title { margin: 0 0 8px 0; text-transform: none; letter-spacing: 0; font-size: 16px; color: var(--accent-strong); }
.callout.warn .callout-title,
.callout.warning .callout-title,
.callout.caution .callout-title { color: var(--warning); }

/* Insight — pull-quote for a key takeaway. Bigger than a callout. */
.insight {
  display: block;
  background: linear-gradient(90deg, var(--accent-soft) 0%, transparent 100%);
  border-left: 4px solid var(--accent);
  padding: 14px 18px;
  margin: 18px 0;
  font-size: 16px;
  line-height: 1.5;
  color: var(--text);
  font-weight: 500;
  letter-spacing: -0.005em;
}
.insight strong { color: var(--accent-strong); }

/* Info-tip — inline expandable detail wrapped around native <details>. */
details.info-tip {
  margin: 14px 0;
  border-left: 3px solid var(--accent);
  background: var(--surface-2);
  padding: 12px 14px;
  border-radius: 0 8px 8px 0;
}
details.info-tip summary {
  cursor: pointer;
  font-weight: 600;
  color: var(--accent-strong);
  list-style: none;
  font-size: 14px;
}
details.info-tip summary::-webkit-details-marker { display: none; }
details.info-tip summary::before {
  content: '▸ ';
  color: var(--accent);
  transition: transform 0.2s ease;
  display: inline-block;
}
details.info-tip[open] summary::before { transform: rotate(90deg); }
details.info-tip[open] summary { margin-bottom: 10px; }
details.info-tip > *:not(summary) { font-size: 13.5px; line-height: 1.6; color: var(--text); }
details.info-tip > *:not(summary) p { margin: 0 0 10px 0; }
details.info-tip > *:not(summary) p:last-child { margin-bottom: 0; }

/* Figure — embedded image (data: URI / URL) or inline author SVG, with
   optional caption. SVG scales via its own viewBox; raster caps at the
   content width. Both center and pick up a quiet themed frame. */
figure.okt-figure {
  margin: 18px 0;
  text-align: center;
}
figure.okt-figure img {
  max-width: 100%;
  height: auto;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: var(--surface);
}
figure.okt-figure.okt-svg .okt-svg-holder { width: 100%; }
figure.okt-figure.okt-svg svg {
  max-width: 100%;
  height: auto;
  display: block;
  margin: 0 auto;
}
figure.okt-figure figcaption {
  margin-top: 8px;
  font-size: 13px;
  color: var(--text-soft);
  font-style: italic;
}

/* ============ Hand-drawn diagrams: the theme vocabulary ============ *
 * An author who draws their own SVG — in an `oku-svg` block or an HTML
 * island — needs somewhere to get colour from. Without a vocabulary the
 * only thing to reach for is a hex literal, and a hex literal is how a
 * figure ends up invisible in the theme nobody was looking at: dark ink
 * on the dark surface, a pale box on the pale one. `oku check` already
 * says so (`island-hand-styled`); these classes are what it is telling
 * the author to use.
 *
 * The whole set is presentation attributes driven by the kit's tokens,
 * so a figure follows the page accent and both themes for free, and
 * `--series-1..10` keeps a hand-drawn figure in the same palette as
 * every chart around it.
 *
 * Sized in absolute px on purpose: an SVG scales through its viewBox,
 * so a diagram set in `em` re-sizes its own text against the drawing
 * whenever the surrounding measure changes. */
.okt-diag-node {
  fill: var(--surface-2);
  stroke: var(--border);
  stroke-width: 1;
}
.okt-diag-node.accent { fill: var(--accent-soft); stroke: var(--accent); }
.okt-diag-node.ok     { fill: var(--success-soft); stroke: var(--success); }
.okt-diag-node.warn   { fill: var(--warning-soft); stroke: var(--warning); }
.okt-diag-node.fail   { fill: var(--danger-soft);  stroke: var(--danger); }
.okt-diag-node.info   { fill: var(--accent-soft);  stroke: var(--accent-strong); }
.okt-diag-node.plain  { fill: var(--surface); stroke: var(--border-soft); }
/* A region behind a group of nodes — the box that says "these three are
   one subsystem". Quieter than a node, or it competes with its own
   contents. */
.okt-diag-group {
  fill: var(--surface-2);
  stroke: var(--border-soft);
  stroke-width: 1;
  opacity: 0.55;
}

.okt-diag-edge {
  fill: none;
  stroke: var(--text-faint);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.okt-diag-edge.strong { stroke: var(--accent); stroke-width: 2; }
.okt-diag-edge.ok     { stroke: var(--success); }
.okt-diag-edge.fail   { stroke: var(--danger); }
.okt-diag-edge.dashed { stroke-dasharray: 5 4; }
.okt-diag-edge.faint  { stroke: var(--border); }
/* Arrowheads live inside <marker>, which the stroke colour does not
   reach — a marker paints with its own fill. Give the head this class
   and it tracks the same token the line does. */
.okt-diag-arrow      { fill: var(--text-faint); stroke: none; }
.okt-diag-arrow.strong { fill: var(--accent); }
.okt-diag-arrow.ok   { fill: var(--success); }
.okt-diag-arrow.fail { fill: var(--danger); }

.okt-diag-label {
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  font-size: 12px;
  fill: var(--text);
}
.okt-diag-label.strong { font-weight: 650; }
.okt-diag-label.soft   { font-size: 11px; fill: var(--text-soft); }
.okt-diag-label.faint  { font-size: 10.5px; fill: var(--text-faint); }
.okt-diag-label.mono {
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 10.5px;
  fill: var(--text-soft);
}
.okt-diag-label.accent { fill: var(--accent); }
.okt-diag-label.ok     { fill: var(--success); }
.okt-diag-label.warn   { fill: var(--warning); }
.okt-diag-label.fail   { fill: var(--danger); }

/* The chart ramp, for a figure that encodes a category per shape. */
.okt-diag-fill-1  { fill: var(--series-1); }
.okt-diag-fill-2  { fill: var(--series-2); }
.okt-diag-fill-3  { fill: var(--series-3); }
.okt-diag-fill-4  { fill: var(--series-4); }
.okt-diag-fill-5  { fill: var(--series-5); }
.okt-diag-fill-6  { fill: var(--series-6); }
.okt-diag-fill-7  { fill: var(--series-7); }
.okt-diag-fill-8  { fill: var(--series-8); }
.okt-diag-fill-9  { fill: var(--series-9); }
.okt-diag-fill-10 { fill: var(--series-10); }

/* Bar-chart — row-per-item horizontal bars. Layout-safe by structure. */
.bar-chart { display: grid; gap: 6px; margin: 24px 0; }
.bar-row {
  display: grid;
  /* Adaptive columns: label shrinks with available width, track
     consumes the remainder (minmax(0,...) lets it actually shrink
     to zero in tight containers like compare-card previews), value
     auto-fits. Previously the label was a fixed 160px which made
     the track collapse to 0 inside ~145px compare-card mini-renders
     — bars were invisible. */
  /* `fit-content(42%)`, not `minmax(40px, min(max-content, 42%))`:
     `min()` takes lengths and percentages, never an intrinsic keyword,
     so that spelling is invalid — and an invalid track list drops the
     whole declaration, leaving a one-column grid where every element
     is full width. `fit-content(<pct>)` is the primitive for exactly
     this: max-content, capped. */
  grid-template-columns: fit-content(42%) minmax(0, 1fr) auto;
  align-items: center;
  gap: 12px;
  font-size: 13.5px;
}
/* Shared origin (comparison correctness): each row is its own grid, so
   by default the label column is sized to THAT row's label — and bars
   start at different x, making lengths impossible to compare. Pull the
   columns up to the chart and have every row inherit them via subgrid:
   the label column becomes the widest label across all rows and every
   bar starts at the same x. Scoped to horizontal charts; vertical bars
   keep their own layout. */
.bar-chart:not(.bar-chart-vertical),
.bar-chart-multi:not(.bar-chart-vertical) {
  /* The label column is capped, and the cap is the load-bearing part.
     `max-content` alone does not shrink: when the row runs out of room
     it is the `1fr` track that pays, because 1fr floors at 0 while a
     max-content column holds its ground. Measured at a 360px viewport
     before this cap — label 143px, value 49px, gaps 24px, track 106px
     of a 316px chart; with labels a little longer the same arithmetic
     put every bar between 1px and 4px. A track is a length the reader
     compares against other lengths, so a track that has been squeezed
     to single digits is not a small chart, it is no chart. */
  /* `fit-content(42%)`, not `minmax(40px, min(max-content, 42%))`:
     `min()` takes lengths and percentages, never an intrinsic keyword,
     so that spelling is invalid — and an invalid track list drops the
     whole declaration, leaving a one-column grid where every element
     is full width. `fit-content(<pct>)` is the primitive for exactly
     this: max-content, capped. */
  grid-template-columns: fit-content(42%) minmax(0, 1fr) auto;
  column-gap: 12px;
  /* Named so the row layout can key off the CHART's width rather than
     the viewport's. A viewport query answers the wrong question: the
     same chart collapses in a narrow grid cell on a 1440px desktop, and
     stays fine on a phone if its labels are short. The name also keeps
     the compare-card mini-renders out of it — they clear the container
     below, and a query with no matching container simply never fires. */
  container-type: inline-size;
  container-name: okbar;
}
.bar-chart:not(.bar-chart-vertical) > .bar-row,
.bar-chart-multi:not(.bar-chart-vertical) > .bar-row {
  grid-template-columns: subgrid;
  grid-column: 1 / -1;
}
/* Below the width where three columns can still give the track
   something to say, the row becomes two lines: the label with its value
   at the end, and the track full-width beneath. 340px is where the
   guarantee stops being satisfiable — at the 42% label cap and a ~50px
   value, 0.58 x 340 - 73 is about 120, and 120px is roughly the floor
   at which a length still reads as a length. The row stops being a
   subgrid here on purpose: it has its own two columns now, and every
   track is full-width, so bars still share an origin. */
@container okbar (max-width: 340px) {
  /* The two gaps have to disagree. A row is now two lines, so the space
     inside a row and the space between rows are both vertical, and at
     the base 6px they were the same measurement — six labels and six
     tracks evenly spaced, with nothing saying which track belonged to
     which label. Inside tightens to 4px, between widens to 6+10.

     The 10px is a margin on the ROW and not `row-gap` on the chart,
     which is the one thing a container query cannot do: the chart is
     the container, and an element may not be styled by a query against
     its own size. The rows are descendants, so they can be. */
  .bar-chart:not(.bar-chart-vertical) > .bar-row,
  .bar-chart-multi:not(.bar-chart-vertical) > .bar-row {
    grid-template-columns: minmax(0, 1fr) auto;
    row-gap: 4px;
  }
  .bar-chart:not(.bar-chart-vertical) > .bar-row:not(:last-child),
  .bar-chart-multi:not(.bar-chart-vertical) > .bar-row:not(:last-child) {
    margin-bottom: 10px;
  }
  .bar-row > .bar-label { grid-column: 1; grid-row: 1; }
  .bar-row > .bar-value { grid-column: 2; grid-row: 1; }
  .bar-row > .bar-track { grid-column: 1 / -1; grid-row: 2; }
}
.bar-chart:not(.bar-chart-vertical) > .bar-chart-title,
.bar-chart:not(.bar-chart-vertical) > .bar-chart-legend,
.bar-chart-multi:not(.bar-chart-vertical) > .bar-chart-title,
.bar-chart-multi:not(.bar-chart-vertical) > .bar-chart-legend {
  grid-column: 1 / -1;
}
.bar-row .bar-label { font-weight: 500; color: var(--text); }
.bar-row .bar-track {
  height: 20px;
  background: var(--surface-2);
  border-radius: 4px;
  overflow: hidden;
}
.bar-row .bar-fill {
  height: 100%;
  background: var(--accent);
  border-radius: 4px;
  transition: width 0.25s ease;
}
.bar-row .bar-fill.warn    { background: var(--warning); }
.bar-row .bar-fill.danger  { background: var(--danger); }
.bar-row .bar-fill.success { background: var(--success); }
.bar-row .bar-value {
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 12px;
  font-weight: 600;
  text-align: right;
  color: var(--text-soft);
}

/* The unit line under a bar chart, from `x_label`. Column 2, not the
   full width: it names the unit the BARS are drawn in, so it starts
   where the bars start. Spanning from column 1 put it under the labels,
   at the far left of a chart whose numbers are at the far right, and it
   read as a stray word rather than as an axis unit — visible the first
   time it shipped on a real page. */
.bar-chart:not(.bar-chart-vertical) > .bar-chart-unit {
  grid-column: 2 / -1;
}
.bar-chart > .bar-chart-unit {
  margin-top: 6px;
  font: 500 11.5px 'Inter', sans-serif;
  color: var(--text-faint);
}

/* Multi-series bars — stacked or grouped variants of the horizontal
   bar chart. The track is a flex container so each series's bar can
   sit either next to (stacked) or above/below (grouped) its siblings. */
.bar-chart-multi .bar-chart-title {
  margin: 0 0 6px 0;
  font: 600 14px 'Inter', sans-serif;
  color: var(--text);
}
.bar-chart-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 12px 16px;
  margin: 0 0 12px 0;
  font: 500 12px 'Inter', sans-serif;
  color: var(--text-soft);
}
/* Legend chip is now a real <button> so it announces as clickable
   to screen readers + picks up focus styling. */
.bar-chart-legend-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px 6px;
  border: none;
  background: transparent;
  border-radius: 4px;
  font: inherit;
  color: inherit;
  cursor: pointer;
  transition: background 0.12s ease, opacity 0.12s ease;
}
.bar-chart-legend-chip:hover { background: var(--surface-2); }
.bar-chart-legend-chip:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.bar-chart-legend-chip.off { opacity: 0.42; text-decoration: line-through; }
.bar-chart-legend-chip.off .bar-chart-legend-swatch { opacity: 0.4; }
.bar-chart-legend-swatch {
  display: inline-block;
  width: 12px;
  height: 12px;
  border-radius: 3px;
  background: var(--accent);
}
.bar-chart-legend-chip.warn .bar-chart-legend-swatch    { background: var(--warning); }
.bar-chart-legend-chip.danger .bar-chart-legend-swatch  { background: var(--danger); }
.bar-chart-legend-chip.success .bar-chart-legend-swatch { background: var(--success); }
.bar-chart-legend-chip.muted .bar-chart-legend-swatch   { background: var(--text-soft); }
/* Bar-fills toggled off via legend chip dim to faint outlines so
   the layout doesn't reflow when a series is hidden. */
.bar-fill.dim {
  opacity: 0.16;
  pointer-events: none;
}

/* Stacked: each series fills its share next to the previous one on
   the same row. No gap between segments so the totals read as a
   single bar. */
.bar-chart-stacked-bar .bar-row .bar-track {
  display: flex;
  align-items: stretch;
}
.bar-chart-stacked-bar .bar-row .bar-fill {
  border-radius: 0;
  transition: width 0.25s ease;
}
.bar-chart-stacked-bar .bar-row .bar-fill:first-child {
  border-radius: 4px 0 0 4px;
}
.bar-chart-stacked-bar .bar-row .bar-fill:last-child {
  border-radius: 0 4px 4px 0;
}
.bar-chart-stacked-bar .bar-row .bar-fill:only-child {
  border-radius: 4px;
}

/* Grouped: each series gets its own mini-bar above / below the
   others in the same row. The track stays the same height per
   row but inner bars stack vertically with a thin gap. */
.bar-chart-grouped-bar .bar-row .bar-track {
  display: flex;
  flex-direction: column;
  gap: 2px;
  background: transparent;
  height: auto;
  min-height: 20px;
  padding: 2px 0;
}
.bar-chart-grouped-bar .bar-row .bar-fill {
  height: 6px;
  border-radius: 3px;
  /* Don't set background here. The parent `.bar-row .bar-fill`
     rule (`background: var(--accent)`) is the default; token
     variants (`.warn`, `.danger`, `.success`) apply through the
     same-specificity `.bar-row .bar-fill.<token>` rule. Earlier
     this rule set `background: var(--accent)` unconditionally,
     which has the same specificity as the token rules but ships
     LATER in the cascade — so every grouped-bar fill rendered
     in accent regardless of its series's color. Inline
     `style.background = 'var(--series-N)'` for unnamed series
     still wins because inline > class. */
}

/* Step-flow — numbered horizontal flow that wraps to a stack. */
.step-cards { display: grid; gap: 12px; margin: 24px 0; }
/* Unordered variant (ordered:false) — a 2-up grid of parallel link
   cards (navigation, option lists) instead of a numbered single-column
   sequence. No step-num column, so the card body spans the full width.
   Collapses to one column on narrow viewports like the other grids. */
.step-cards-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.step-cards-grid .step-card { grid-template-columns: minmax(0, 1fr); }
@media (max-width: 768px) { .step-cards-grid { grid-template-columns: minmax(0, 1fr); } }
.step-card {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 16px;
  align-items: start;
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 16px 18px;
  background: var(--surface);
  box-shadow: var(--okt-ring-off), 0 0 0 var(--shadow-strong);
  transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.step-card:hover {
  border-color: var(--accent);
  box-shadow: var(--okt-ring-on), 0 10px 24px -6px var(--shadow-strong);
}
.step-card .step-num {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 700;
  font-size: 14px;
}
.step-card .okt-card-title {
  margin: 0 0 4px 0;
  font-size: 15px;
  text-transform: none;
  letter-spacing: 0;
  color: var(--text);
}
.step-card .step-meta {
  font-size: 12px;
  color: var(--text-soft);
  margin-bottom: 8px;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
}
.step-card p {
  margin: 0;
  font-size: 14px;
  line-height: 1.55;
  color: var(--text);
}
/* When the step card is itself an <a> (href on the step), reset
   anchor styling so the card chrome wins — only the hover affordance
   below differentiates it from a static card. */
a.step-card,
a.step-card:visited {
  color: inherit;
  text-decoration: none;
}
a.step-card.step-card-link { cursor: pointer; }
a.step-card.step-card-link::after {
  content: '→';
  position: absolute;
  top: 14px;
  right: 16px;
  font-size: 14px;
  color: var(--text-soft);
  opacity: 0;
  transition: opacity 0.18s ease, transform 0.18s ease;
}
a.step-card.step-card-link { position: relative; }
a.step-card.step-card-link:hover::after,
a.step-card.step-card-link:focus-visible::after {
  opacity: 1;
  transform: translateX(2px);
}
a.step-card.step-card-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.callout.danger .callout-title { color: var(--danger); }
.callout.success .callout-title,
.callout.tip .callout-title { color: var(--success); }
.callout.neutral .callout-title { color: var(--text); }
.callout.note .callout-title,
.callout.info .callout-title { color: var(--accent-strong); }
.callout p:last-child, .callout ul:last-child { margin-bottom: 0; }

/* ============ Tables, pills, KPI tiles, comparison cards, details ============ */
table { width: 100%; border-collapse: collapse; margin: 18px 0; font-size: 14.5px; line-height: 1.5; }
/* A column label is a label, not a heading: small, uppercase, tracked
   out, and quieter than the data under it. The filled band it used to
   sit in made the header the loudest row in the table, which is exactly
   backwards — the reader came for the cells. The 2px rule carries the
   separation the fill used to.

   The background stays declared because these headers are `sticky`:
   without an opaque fill the rows scroll THROUGH the label. It is the
   scroll area's own colour, so it reads as no fill at rest and only
   does its job once the reader scrolls a long table. */
:is(table, .okt-table-ghost) th {
  text-align: left; background: var(--bg);
  padding: 10px 14px; border-bottom: 2px solid var(--border);
  font-weight: 650; color: var(--text-soft); font-size: 12.5px;
  text-transform: uppercase; letter-spacing: 0.04em;
}
/* Sticky column headers — only on top-level <thead> rows. Scoped this
   way so row-headers in the list-card view (<th scope="row">) don't
   inherit sticky positioning, which would scramble that layout. */
table thead th {
  position: sticky;
  /* Under the rail, which is opaque and fixed. The reading column is
     `zoom`ed by the text-scale control and `top` scales with it, so
     the rail's height is divided back out. */
  top: calc(var(--okt-rail-h) / var(--oku-text-scale, 1));
  z-index: 2;
}
/* Inside the lightbox — a fullscreened table, the markdown viewer —
   the scrollport is the lightbox's own box and the rail is under it. */
.okt-lightbox-content table thead th { top: 0; }
.okt-lightbox-content .okt-table-ghost { top: 0; }
table td {
  padding: 12px 14px;
  border-bottom: 1px solid var(--border-soft);
  vertical-align: top;
  /* Word-wrap by default so long cell content folds inside the
     column instead of forcing horizontal scroll. Tables that want
     single-line cells can opt out per-row via the okt-nowrap class.
     Each cell has a readable floor (~9ch ≈ 70 px) so many-column
     tables don't squash columns to one-char lines — when the natural
     minimum exceeds container width, the wrapper's horizontal scroll
     takes over instead of producing illegible word-breaks. */
  white-space: normal;
  /* `break-word`, NOT `anywhere`. They paint the same; they size
     differently, and the sizing is the whole point. `anywhere` lets the
     auto table layout count a one-character break as a legal minimum,
     so a column holding `read_time` reports a min-content width of one
     character, the algorithm hands the slack to a prose column, and the
     identifier renders as `read_ti / me`. `break-word` keeps
     min-content at the longest word, so the column claims the width it
     needs while there is slack to claim, and still breaks the token
     when there genuinely is not — which is what the paragraph below
     always said this did. */
  overflow-wrap: break-word;
  word-break: normal;
  min-width: 9ch;
}
/* The last row's rule doubled the wrap's own border into a 2px edge. */
table tbody tr:last-child td { border-bottom: none; }
/* An identifier in a cell is one token, and a column label is written
   to be read on one line. Both used to fold mid-word to save pixels the
   table had to spare. */
table td > code,
table td > a > code { white-space: nowrap; }
:is(table, .okt-table-ghost) th {
  /* Headers earn the same readable floor so labelled columns don't
     collapse below the label length. */
  min-width: 9ch;
}
table tr.okt-nowrap td,
table.okt-nowrap td { white-space: nowrap; overflow-wrap: normal; }
/* Semantic status pill (header `status` value→verdict map). Same colour
   language as compare-grid verdicts so state reads at a glance instead of
   as bare text. Soft fill + strong text; tokens carry their own light/dark
   pairs. The pill is purely visual — the cell's text value is unchanged. */
.okt-status {
  display: inline-block;
  padding: 2px 10px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
  line-height: 1.6;
  white-space: nowrap;
}
.okt-status-good    { background: var(--success-soft); color: var(--success); }
.okt-status-warn    { background: var(--warning-soft); color: var(--warning); }
.okt-status-bad     { background: var(--danger-soft);  color: var(--danger); }
.okt-status-info    { background: var(--accent-soft);  color: var(--accent-strong); }
.okt-status-neutral { background: var(--surface-2);    color: var(--text-soft); border: 1px solid var(--border); }
/* Per-column wrap: true (header object). Cells in a wrap-flagged
   column honour explicit newlines from the source so authors can
   author multi-paragraph cell content with `\n` instead of inlining
   `<br>` tags or splitting across multiple columns. */
table td[data-wrap="1"] { white-space: pre-line; }
/* Hover tint ONLY where the row actually does something. A tint that
   follows the pointer down a table reads as "this row is selectable",
   and when the click does nothing the reader stops trusting the whole
   surface — they hesitate before clicking anything. The affordance and
   the behaviour have to agree, so the tint is scoped to the same
   markers `classify()` in chrome.js treats as clickable, and the
   pointer cursor comes with it. A cell that merely contains a link is
   not an interactive row; the link carries its own affordance. */
table tbody tr[onclick]:hover > td,
table tbody tr[data-href]:hover > td,
table tbody tr[role="button"]:hover > td,
table tbody tr[role="link"]:hover > td,
table tbody tr[tabindex]:hover > td {
  background: var(--surface-2);
}
table tbody tr[onclick],
table tbody tr[data-href],
table tbody tr[role="button"],
table tbody tr[role="link"],
table tbody tr[tabindex] {
  cursor: pointer;
}
/* Column-resize grabber — thin right-edge strip on every thead <th>.
   Invisible until hovered, then a 3px accent line + ew-resize
   cursor. While dragging, the table gains .okt-table-resizing to
   silence pointer events on body text and force the resize cursor
   across the whole document. */
/* The grab zone sits INSIDE its own cell, and that is the whole rule.
   Straddling the boundary (`right: -3px`) reads as the obvious way to
   write it — the pointer is aiming at a line between two columns — and
   it does not work: a `th` is positioned, later siblings paint over
   earlier ones, so the next cell covered the outer two-thirds. Of six
   pixels the reader could see, two answered; the visible grip line was
   in the dead part, and a click on it sorted the next column instead of
   resizing this one. Ten pixels inside the cell is both a real target
   and one nothing can paint over. */
.okt-col-resize {
  position: absolute;
  top: 0;
  right: 0;
  width: 10px;
  height: 100%;
  cursor: ew-resize;
  z-index: 3;
  user-select: none;
}
.okt-col-resize::after {
  content: '';
  position: absolute;
  top: 6px;
  right: 1px;
  bottom: 6px;
  width: 2px;
  background: transparent;
  border-radius: 1px;
  transition: background 0.12s ease;
}
.okt-col-resize:hover::after { background: var(--accent); }
.okt-table-resizing,
.okt-table-resizing * { cursor: ew-resize !important; user-select: none !important; }

.pill {
  display: inline-block; padding: 3px 10px; border-radius: 14px;
  font-size: 12px; font-weight: 700; margin-right: 4px; white-space: nowrap;
  box-shadow: 0 0 0 var(--shadow-soft);
  transition: box-shadow 0.18s ease;
}
/* No ring here: a pill already carries its own semantic fill, and an
   accent ring around a red one is two colours arguing. Depth only. */
.pill:hover {
  box-shadow: 0 2px 6px var(--shadow-soft);
}
.pill.ok    { background: var(--success-soft); color: var(--success); }
.pill.warn  { background: var(--warning-soft); color: var(--warning); }
.pill.fail  { background: var(--danger-soft);  color: var(--danger); }
.pill.info  { background: var(--accent-soft);  color: var(--accent-strong); }
.pill.muted { background: var(--border-soft);  color: var(--text-soft); }

.kpi-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; margin: 24px 0; }
.kpi {
  text-align: center; padding: 22px 14px;
  background: var(--surface); border-radius: 10px; border: 1px solid var(--border);
  box-shadow: var(--okt-ring-off), 0 0 0 var(--shadow-strong);
  transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.kpi:hover {
  border-color: var(--accent);
  box-shadow: var(--okt-ring-on), 0 10px 24px -6px var(--shadow-strong);
}
.kpi .num {
  font-size: 30px; font-weight: 800; color: var(--accent);
  line-height: 1.1; letter-spacing: -0.02em;
  /* An inline-block sizes to max-content, so a value that is not a bare
     numeral — `1.2M req/s`, a build stamp, a version string — leaves the
     tile and takes the viewport with it. Measured at 1440px: one tile
     put the page's scrollWidth at 1818. `anywhere` rather than
     `break-word` because only the former lowers the min-content size the
     grid track measures. */
  display: inline-block;
  max-width: 100%;
  overflow-wrap: anywhere;
}
.kpi .label { font-size: 12.5px; color: var(--text-soft); margin-top: 6px; line-height: 1.4; }
/* Optional per-tile icon — a semantic glyph above the number so a grid of
   metrics reads as a spec sheet, not a wall of numbers. */
.kpi-icon { display: flex; justify-content: center; margin-bottom: 10px; color: var(--accent); opacity: 0.9; }
.kpi-icon svg { display: block; }
.kpi:hover .kpi-icon { opacity: 1; }

/* ============ Timeline ============ *
 * An ordered sequence where each entry carries a STATE. The neighbour
 * is step-flow, and the difference is worth stating because picking the
 * wrong one is the usual mistake: a step-flow is a procedure the reader
 * is meant to follow, so every step is equally true. A timeline is a
 * record of what happened, and an entry on it can be a claim that was
 * later dropped. That is why the dot carries a status and the step
 * numeral does not.
 *
 * One rail down the left, one dot per entry, laid out by CSS on
 * ::before — the DOM is only the reading order, so the list still reads
 * correctly with the stylesheet off, and nothing has to be kept in
 * sync. The rail stops 6px short at both ends so it reads as spanning
 * the entries rather than running off into the prose. */
ol.okt-timeline {
  position: relative;
  list-style: none;
  margin: 24px 0;
  padding: 0 0 0 26px;
}
ol.okt-timeline::before {
  content: '';
  position: absolute;
  left: 5px;
  top: 6px;
  bottom: 6px;
  width: 2px;
  border-radius: 1px;
  background: var(--border);
}
.okt-tl-item { position: relative; margin: 0 0 18px 0; }
.okt-tl-item:last-child { margin-bottom: 0; }
/* The dot sits ON the rail, not beside it. The rail spans x 5..7 from
   the list's left edge, centre 6. The item's box starts at the padding
   edge, so `left: -26px` puts the 12px dot at x 0..12 — centre 6, the
   same number, which is why the narrow-width override moves the padding
   and the offset together and never one alone. Filled from the page
   background so the rail cannot show through a hollow one. */
.okt-tl-item::before {
  content: '';
  position: absolute;
  left: -26px;
  top: 4px;
  width: 12px;
  height: 12px;
  box-sizing: border-box;
  border-radius: 50%;
  background: var(--bg);
  border: 2px solid var(--text-faint);
}
/* Filled = settled. Hollow = still a question, in one of two ways. */
.okt-tl-done::before    { border-color: var(--success); background: var(--success); }
.okt-tl-open::before    { border-color: var(--warning); }
.okt-tl-dropped::before { border-color: var(--danger); }
.okt-tl-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 3px;
}
.okt-tl-title { font-weight: 650; font-size: 15px; color: var(--text); min-width: 0; overflow-wrap: anywhere; }
/* The chip carries the author's own word for the entry; the dot carries
   the state. Two jobs, two places — a chip that had to encode both
   would need one colour per phrase. */
.okt-tl-chip {
  flex: none;
  font: 600 11.5px 'Inter', sans-serif;
  letter-spacing: 0.01em;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--border-soft);
  color: var(--text-soft);
  /* The chip is `flex: none`, so it is already as wide as its label and
     a two-word one never wraps — the cap is what forces a break, and it
     only binds when the label is wider than the entry. `nowrap` here
     could not be saved by `overflow-wrap`: with wrapping switched off
     there is no break opportunity for it to take. */
  max-width: 100%;
  overflow-wrap: anywhere;
}
.okt-tl-done .okt-tl-chip    { background: var(--success-soft); color: var(--success); }
.okt-tl-open .okt-tl-chip    { background: var(--warning-soft); color: var(--warning); }
.okt-tl-dropped .okt-tl-chip { background: var(--danger-soft);  color: var(--danger); }
.okt-tl-body { color: var(--text-soft); font-size: 14.5px; }
.okt-tl-body > :first-child { margin-top: 0; }
.okt-tl-body > :last-child { margin-bottom: 0; }
@media (max-width: 560px) {
  ol.okt-timeline { padding-left: 22px; }
  .okt-tl-item::before { left: -22px; }
}

.compare-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; margin: 24px 0; }
/* A grid item defaults to min-width: auto, so it refuses to shrink below
   the widest unbreakable token it contains — one long path, URL or
   percent-encoded string in a card and the whole PAGE scrolls sideways
   at 360px. min-width: 0 lets the item track the column; the global
   overflow-wrap then breaks the token inside it. */
.compare-grid > *, .kpi-grid > *, .step-cards > * { min-width: 0; }
.compare-card {
  padding: 22px; border-radius: 10px;
  border: 1px solid var(--border); background: var(--surface);
  box-shadow: var(--okt-ring-off), 0 0 0 var(--shadow-strong);
  transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.compare-card:hover {
  border-color: var(--accent);
  box-shadow: var(--okt-ring-on), 0 10px 24px -6px var(--shadow-strong);
}
/* Clickable variant — whole card is an <a>. Strip default link
   styling so the card reads as a tile, not as a link-coloured
   block of text. Underline cancellation + inherited colour so
   inner text doesn't pick up the link colour. */
a.compare-card,
a.compare-card-link {
  color: inherit;
  text-decoration: none;
  cursor: pointer;
  display: block;
}
a.compare-card-link:hover {
  border-color: var(--accent);
}
/* Pick-by-family mini-card layout — clickable variant tiles in the
   Pick-by-family grid. Compact padding, label on top, tiny render
   in the middle, click anywhere on the card to jump to the variant
   anchor. pointer-events:none on the inner chart so hover doesn't
   pin the mini's tooltip when the user clearly meant to click the
   card.

   Cards are normalised to a fixed height so every family row in
   the picker grid shares the same visual rhythm. Without this,
   square shapes (chord, polar-area, sunburst, scatter-matrix) push
   their row to ~178px while wide-flat shapes (bullet, sparkline,
   calendar-heatmap) sit in a 136-143px row — a 42px stagger that
   reads as "some charts are bigger and thicker than others".
   With a fixed 170px and `align-items:center` on the chart slot,
   the SVG inside each card centres in the same visual frame
   regardless of its native aspect. */
.compare-card.compare-card-link {
  padding: 12px 14px 16px;
  height: 220px;
  display: grid;
  grid-template-rows: auto 1fr;
  align-items: stretch;
}
.compare-card.compare-card-link > oku-chart,
.compare-card.compare-card-link > .bar-chart,
.compare-card.compare-card-link > .bar-chart-multi,
.compare-card.compare-card-link > oku-diagram {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0; /* allow shrinking inside the grid track */
  width: 100%;
  overflow: hidden;
}
.compare-card.compare-card-link .okt-card-title,
.compare-card.compare-card-link .bar-chart-title {
  margin: 0 0 6px;
  font-size: 12px;
  text-transform: lowercase;
  letter-spacing: 0.02em;
  color: var(--text);
  text-align: center;
  font-weight: 600;
}
.compare-card.compare-card-link oku-chart,
.compare-card.compare-card-link .bar-chart,
.compare-card.compare-card-link .bar-chart-multi,
.compare-card.compare-card-link oku-diagram {
  margin: 0;
  pointer-events: none;
}
.compare-card.compare-card-link svg.okc-svg,
.compare-card.compare-card-link .okc-sparkline-row {
  max-height: 160px;
  display: block;
  margin: 0 auto;
}
.compare-card.compare-card-link .okt-bar {
  display: none;
}
/* Picker-preview styling:
   - No legends. The picker label IS the chart name; in-chart
     legends are noise at this size.
   - Thicker strokes + bigger dots so the structural shape reads
     at a glance from across the page. Strokes/dots get bumped
     proportionally per chart family.
   - Hide axis ticks + small labels — preview is about SHAPE, not
     exact values. */
.compare-card.compare-card-link .okc-series-legend,
.compare-card.compare-card-link .okc-gauge-zone-chip,
.compare-card.compare-card-link .okc-radar-legend,
.compare-card.compare-card-link .okc-donut-legend,
.compare-card.compare-card-link .okc-waffle-legend,
.compare-card.compare-card-link .okc-legend,
.compare-card.compare-card-link .okc-legend-backdrop,
.compare-card.compare-card-link .okc-title,
.compare-card.compare-card-link .okc-tick,
.compare-card.compare-card-link .okc-axis-label,
.compare-card.compare-card-link .okc-quadrant-label,
.compare-card.compare-card-link .okc-slope-col,
.compare-card.compare-card-link .okc-slope-readout,
.compare-card.compare-card-link .okc-radar-label,
.compare-card.compare-card-link .okc-polar-area-label,
.compare-card.compare-card-link .okc-bump-end-label,
.compare-card.compare-card-link .okc-bullet-label,
.compare-card.compare-card-link .okc-bullet-readout,
.compare-card.compare-card-link .okc-bar-chart-title,
.compare-card.compare-card-link .bar-chart-legend,
.compare-card.compare-card-link .bar-chart-title {
  display: none !important;
}
/* The bar family's row label and value readout are the exception, and it
   is a layout fact rather than a preference: they are the first and
   third columns of the chart's grid, and the track is the `1fr` between
   them. Taken out of the flow, the track loses its column and collapses
   — the three bar cards rendered a 9px speck at the far left of a 170px
   box. `visibility` hides the writing and keeps the columns. */
.compare-card.compare-card-link .bar-label,
.compare-card.compare-card-link .bar-value {
  visibility: hidden !important;
}
/* And for the same reason the preview opts out of the narrow-width
   two-line row. A card is ~145px wide, so it is under every threshold
   worth setting — but its label and value are hidden rather than
   removed, and stacking would spend a line of a 90px-tall card drawing
   nothing. Clearing the container name is enough: a `@container okbar`
   query with no okbar ancestor never matches. */
.compare-card.compare-card-link .bar-chart,
.compare-card.compare-card-link .bar-chart-multi {
  container-type: normal;
  container-name: none;
}
/* Thicker strokes + bigger dots for preview legibility.
   The radius is in viewBox units and the card shows the viewBox at about
   0.27, so `r: 5` — the value this block carried before anything actually
   rendered in a card — reached the reader as a 2.7px speck: scatter,
   bubble and quadrant read as empty cards, which is the defect this
   whole feature exists to fix. 11 lands near 6px. */
.compare-card.compare-card-link .okc-svg .okc-dot,
.compare-card.compare-card-link .okc-svg .okc-bump-dot {
  r: 11;
}
/* A bubble's radius IS its third variable. Normalising it draws a
   scatter plot and labels it bubble, so this one keeps its own sizes. */
.compare-card.compare-card-link .okt-compare-preview[data-oku-figure="bubble"] .okc-dot {
  r: auto;
}
/* Legend swatches survive their hidden backdrop and read as data — four
   coloured squares in the corner of a scatter preview are indistinguishable
   from four data points. */
.compare-card.compare-card-link .okc-legend-swatch { display: none !important; }
.compare-card.compare-card-link .okc-svg path,
.compare-card.compare-card-link .okc-svg polyline,
.compare-card.compare-card-link .okc-svg line:not(.okc-axis):not(.okc-quadrant):not(.okc-bullet-target) {
  stroke-width: 2.5 !important;
}
/* Tighter grid for the family picker — 5 columns on wide viewports,
   responsive auto-fit fallback. */
.compare-grid:has(> a.compare-card-link) {
  grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 1fr));
  gap: 14px;
}

/* Small multiples — repeat the same chart across N panels in a grid.
   The grid is responsive auto-fit by default; the renderer can lock
   to an explicit column count via `style="grid-template-columns: …"`
   inline when block.cols is provided. */
.okt-chart-grid {
  margin: 18px 0;
}
.okt-chart-grid-title {
  margin: 0 0 12px 0;
  font: 600 15px 'Inter', sans-serif;
  color: var(--text);
}
.okt-chart-grid-cells {
  display: grid;
  gap: 16px;
}
.okt-chart-grid-cell {
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: 8px;
  padding: 8px;
  min-width: 0; /* allow flex/grid children to shrink */
}
.okt-chart-grid-cell oku-chart,
.okt-chart-grid-cell .bar-chart,
.okt-chart-grid-cell .bar-chart-multi {
  display: block;
}
.okt-chart-grid-cell svg.okc-svg {
  max-height: 180px;
  width: 100%;
}
.compare-card.bad,
.compare-card.danger  { border-top: 4px solid var(--danger); }
.compare-card.bad:hover,
.compare-card.danger:hover  { border-color: var(--danger); }
.compare-card.good,
.compare-card.success { border-top: 4px solid var(--success); }
.compare-card.good:hover,
.compare-card.success:hover { border-color: var(--success); }
.compare-card.in   { border-top: 4px solid var(--success); }
.compare-card.in:hover   { border-color: var(--success); }
.compare-card.out,
.compare-card.muted { border-top: 4px solid var(--text-soft); }
.compare-card.out:hover,
.compare-card.muted:hover  { border-color: var(--text-soft); }
.compare-card.warn { border-top: 4px solid var(--warning); }
.compare-card.warn:hover { border-color: var(--warning); }
.compare-card.accent { border-top: 4px solid var(--accent); }
.compare-card.accent:hover { border-color: var(--accent); }
.compare-card.in .okt-card-title  { color: var(--success); }
.compare-card.out .okt-card-title { color: var(--text-soft); }
.compare-card .okt-card-title { margin: 0 0 12px 0; text-transform: none; letter-spacing: 0; font-size: 16px; color: var(--text); }
/* Verdict-icon header: a check / cross / triangle / ring at the title makes
   each card readable at a glance instead of a uniform block of text. */
.compare-card.caution { border-top: 4px solid var(--warning); }
.compare-card.caution:hover { border-color: var(--warning); }
.compare-card-head { display: flex; align-items: center; gap: 9px; margin: 0 0 12px 0; }
/* `min-width: auto` is the initial value for a flex ITEM as well as a
   grid track, so a title holding one long unbreakable run — a class
   name, a config key, a path — refuses to shrink and leaves the card,
   the column and finally the viewport, and the page scrolls sideways at
   360px. `break-word` does not save it: that wraps a word only once the
   box is narrow, and the box is sized FROM the word. Every kit box that
   holds author text says it may be narrower than its longest word;
   `overflow-wrap: anywhere` is what then makes the word break, because
   unlike `break-word` it also lowers the min-content size the parent
   measures. */
.compare-card-head .okt-card-title { margin: 0; min-width: 0; overflow-wrap: anywhere; }
/* A card that links to a figure shows that figure's silhouette (see
   wireComparePreviews). The box is a fixed height so a row of cards
   keeps one baseline whatever the aspect ratios behind them are — the
   figures on docs/charts.md run from 5:1 to 1:1.1, and letting each card
   size to its own clone would step the row's bottom edge up and down.
   Nothing here is interactive: the clone is decoration, and the card
   itself is the link. */
.okt-compare-preview {
  height: 104px;
  margin: 0 0 12px 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  pointer-events: none;
  /* The clone is a reduction of a figure drawn for four times the width,
     so it carries more ink per pixel than anything else on the card. At
     full strength it out-shouts the title it belongs to. */
  opacity: .82;
}
.compare-card:hover .okt-compare-preview { opacity: 1; }
/* The inert <oku-chart> shell exists only so the chart's own rules match
   (see __okuBuildComparePreview). It is given a real box rather than
   `display: contents`: contents produces no box at all, so anything that
   measures an `oku-chart` on this page reads 0x0 — a trap for the next
   pass that walks figures, and one an audit here already fell into. */
.okt-compare-preview > oku-chart {
  display: block;
  width: 100%;
  height: 100%;
  margin: 0;
  min-width: 0;
  /* The shell is a scoping device, not a figure. Giving it a real box
     brought `oku-chart`'s own surface with it — border, background,
     padding, radius — and the preview appeared inside a second card. */
  background: none;
  border: 0;
  border-radius: 0;
  padding: 0;
  box-shadow: none;
}
.okt-compare-preview svg { width: 100%; height: 100%; display: block; }
/* The bar family has no viewBox; it is fitted by transform in JS, which
   means it must first be ALLOWED to lay out at its natural width. The
   `main .bar-chart { max-width: 100% }` rule that keeps every block
   inside one right edge would otherwise cap the clone at the card's
   width and the transform would shrink that again — measured at 39px
   wide inside a 170px box. Two classes outrank `main .bar-chart`, so
   this wins on specificity and needs no !important. */
.okt-compare-preview > .bar-chart { flex: 0 0 auto; max-width: none; }
.compare-card-icon { flex: 0 0 auto; display: inline-flex; color: var(--text-soft); }
.compare-card-icon svg { display: block; }
.compare-card.good .compare-card-icon,
.compare-card.success .compare-card-icon,
.compare-card.in .compare-card-icon { color: var(--success); }
.compare-card.bad .compare-card-icon,
.compare-card.danger .compare-card-icon { color: var(--danger); }
.compare-card.warn .compare-card-icon,
.compare-card.caution .compare-card-icon { color: var(--warning); }
.compare-card.accent .compare-card-icon { color: var(--accent); }
/* Items list (was scope-col ul) — sits below the optional `content` body. */
.compare-card ul {
  margin: 0;
  padding-left: 20px;
  font-size: 14px;
  line-height: 1.6;
}
.compare-card ul li { margin: 4px 0; }

details.card {
  margin: 14px 0; padding: 16px 20px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 10px; border-left: 4px solid var(--accent);
}
details.card[open] { box-shadow: 0 4px 16px rgba(0,0,0,0.04); }
details.card summary { cursor: pointer; font-weight: 600; list-style: none; position: relative; padding-right: 28px; color: var(--text); }
details.card summary::after { content: "▾"; position: absolute; right: 4px; top: 2px; color: var(--text-faint); transition: transform 0.2s; }
details.card[open] summary::after { transform: rotate(180deg); }
details.card summary::-webkit-details-marker { display: none; }
details.card .body { margin-top: 12px; font-size: 14.5px; line-height: 1.6; }

/* ============ Glossary tooltips ============ */
.g { border-bottom: 1px dotted var(--accent); cursor: help; color: inherit; }
.g-wrap { position: relative; display: inline-block; }
.g:focus { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }
.g-tip {
  display: none; position: absolute;
  bottom: calc(100% + 8px); left: 0; z-index: 50;
  background: var(--primary); color: var(--bg);
  padding: 12px 14px; border-radius: 8px;
  font-size: 13.5px; line-height: 1.5; font-weight: 400;
  text-align: left; letter-spacing: 0;
  width: 320px; max-width: calc(100vw - 32px);
  box-shadow: 0 12px 32px rgba(0,0,0,0.25);
  white-space: normal;
}
.g-tip::after {
  content: ''; position: absolute; top: 100%; left: 16px;
  border: 6px solid transparent; border-top-color: var(--primary);
}
.g-wrap:hover .g-tip, .g-wrap.open .g-tip { display: block; }
@media (max-width: 600px) {
  .g-tip {
    position: fixed; left: 16px; right: 16px; width: auto; max-width: none;
    bottom: auto; top: 50%; transform: translateY(-50%);
  }
  .g-tip::after { display: none; }
}

/* ============ Footer ============ */
footer {
  margin-top: 80px; padding-top: 24px;
  border-top: 1px solid var(--border);
  font-size: 13px; color: var(--text-faint);
}

/* ============ Responsive ============ */
/* ≤768px — phone tuning only. The Contents drawer behaves the same at
   every width; see the layout note at the top of this file. */
@media (max-width: 768px) {
  /* The drawer, its scrim and the Contents button are base layout now;
     this width only tightens the reading surface. Top padding clears
     the Contents button (16px inset + 44px tall) before the cover. */
  main {
    padding:
      calc(76px / var(--oku-text-scale, 1))
      calc(22px / var(--oku-text-scale, 1))
      calc(96px / var(--oku-text-scale, 1))
      calc(22px / var(--oku-text-scale, 1));
  }
  header.cover h1 { font-size: 30px; }
  header.cover .subtitle { font-size: 16px; }
  h2 { font-size: 24px; }
  .kpi-grid, .compare-grid { grid-template-columns: minmax(0, 1fr); }
  /* Font and gap only. This rule used to set `110px 1fr 50px` as well,
     and that declaration never once applied: every row the kit builds
     sits inside `.bar-chart`, whose `> .bar-row` subgrid rule outranks a
     bare `.bar-row` in a media query. It read as the narrow-width bar
     tuning while the real columns came from the chart, which is why the
     collapse it looks like it prevents was reported anyway. The columns
     are handled where they resolve — see the @container block. */
  .bar-row { font-size: 12.5px; gap: 8px; }
  /* `.copy-btn { opacity: 1 }` used to sit here — a WIDTH proxy for a
     HOVER question, which rescued a phone and left a touch tablet at
     1024px with an invisible button. It moved to the `(hover: none)`
     block, beside the wrap toggle that never had one. */
  .permalink { display: none; }
}

/* ============ Print / PDF ============ */
@media print {
  .ctrl-btn, .okt-chrome-cluster, .okt-rail, .progress-bar, .skip-link,
  .copy-btn, .permalink, .okt-copy-actions,
  /* A path chip prints as the path. The button that copies it is
     one of the two things paper cannot do. */
  .okt-fp-copy,
  /* Live table and code chrome. Measured on the built pages: 39
     language pills, 25 fold markers, 10 column-resize bars and a full
     table toolbar — filter box, stats counter, both copy buttons —
     printed onto paper, where none of them can be operated. */
  .okt-code-lang, .okt-fold-marker, .okt-col-resize,
  .okt-chrome-menu,
  .okt-table-controls, .okt-chip-rack, .okt-view-switch { display: none !important; }
  .okt-sortable::after { content: none !important; }
  /* page-nav is listed because of the pinned state. Off-canvas it was
     invisible on paper for free; at left: 0 it would print, and its
     inset would print with it. */
  nav.toc, page-toc, page-nav { display: none !important; }
  .layout { display: block; padding-left: 0 !important; }
  body { background: white; color: black; font-size: 11pt; }
  /* The reader's text scale is about a screen they are sitting in front
     of. Paper has a fixed page width and its own scale control in the
     print dialog, and a page printed at 200% is four times the sheets
     for the same words. */
  main { padding: 0; max-width: none; zoom: 1; }
  header.cover { page-break-after: avoid; }
  h1, h2, h3, h4 { page-break-after: avoid; color: black; }
  pre, table, .callout, .compare-grid, .kpi-grid, .compare-card, details.card, .tldr,
  .okt-copy-region { page-break-inside: avoid; }
  a { color: black; text-decoration: underline; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 9pt; color: #555; }
  details { display: block; }
  details > *:not(summary) { display: block !important; }
  details summary::after { content: " (expanded)"; font-size: 9pt; color: #888; }
  /* Paper does not scroll, so an `overflow: auto` box simply clips.
     Measured: a table board 1088px wide inside a 742px page lost 346px
     with nothing on the page to say so. */
  .okt-table-scroll, .okt-table-board, pre { overflow: visible !important; }
  pre, pre code { white-space: pre-wrap; word-break: break-word; }
}

/* ============================================================
   <glossary-term>, <ext-ref>, and the tooltip popup
   (v2 interactive learning primitives)
   ============================================================ */
/* ---------------------------------------------------------------- *
   filepath — an inline path the reader can look inside

   The chip has to read as a path FIRST. An author writes one where
   they would have written a code span, and a control that announced
   itself louder than the text around it would make every mention of a
   file the loudest thing in its paragraph. So: the same monospace
   face and the same recessed ground inline code already has, plus one
   affordance — a dotted underline under the path — and a copy button
   that only appears when the pointer is on the chip.
   ---------------------------------------------------------------- */
.okt-fp {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  max-width: 100%;
  padding: 1px 5px 1px 4px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--surface-2);
  vertical-align: baseline;
  line-height: 1.35;
}
.okt-fp:hover,
.okt-fp:focus-within {
  border-color: var(--accent);
}
.okt-fp-icon {
  display: inline-flex;
  flex: 0 0 auto;
  color: var(--text-faint);
}
.okt-fp-icon svg { width: 13px; height: 13px; }
.okt-fp[data-status="ok"] .okt-fp-icon { color: var(--accent); }
.okt-fp-label {
  font: 500 0.88em 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--text);
  background: none;
  padding: 0;
  border: 0;
  overflow-wrap: anywhere;
  transition: color 0.15s;
}
/* The chip already carries a border, an icon and a button, which is
   enough to read as a control — the dotted underline glossary terms use
   is their ONLY mark and would be a third one here. The pointer is
   answered with colour instead, which is also the rule that keeps a
   hover from moving anything. */
.okt-fp[data-status="ok"] .okt-fp-label { cursor: pointer; }
.okt-fp[data-status="ok"]:hover .okt-fp-label { color: var(--accent-strong); }
.okt-fp:not([data-status="ok"]) {
  /* A file that is not there is not a whole thing. Dashed says so at a
     glance and costs no layout, and the icon carries the same word in
     the same colour. */
  border-style: dashed;
}
.okt-fp:not([data-status="ok"]) .okt-fp-label { cursor: help; }
.okt-fp:not([data-status="ok"]) .okt-fp-icon { color: var(--warning); }
.okt-fp-label:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--accent);
  border-radius: 2px;
}

/* The copy button is quiet until the pointer arrives, on the rule the
   table toolbar already follows: chrome that is always at full weight
   competes with the content it hangs off. It keeps its box either way,
   so nothing on the line moves when it appears. */
.okt-fp-copy {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  padding: 0;
  border: 0;
  border-radius: 3px;
  background: none;
  color: var(--text-faint);
  cursor: pointer;
  /* A floor, not zero — unlike the table toolbar above, which has a
     whole card to hover. This button sits inside a line of prose, so
     at zero the reserved slot reads as a gap the author left in the
     path rather than as a control waiting to be used. */
  opacity: 0.38;
  transition: opacity 0.15s, color 0.15s;
}
.okt-fp-copy svg { width: 12px; height: 12px; }
/* The glyph is 12px because it sits on a line of prose and a taller box
   would push the line apart. The TARGET is not: the pseudo-element
   pushes the hit area out to 26px in both directions without changing
   the layout box, so the pointer floor is met and nothing on the line
   moves. */
.okt-fp-copy::after {
  content: '';
  position: absolute;
  inset: -5px;
}
.okt-fp:hover .okt-fp-copy,
.okt-fp-copy:focus-visible { opacity: 1; }
.okt-fp-copy:hover { color: var(--accent); }
.okt-fp-copy.okt-flash-ok { opacity: 1; color: var(--success); }
.okt-fp-copy.okt-flash-fail { opacity: 1; color: var(--danger); }

/* ---- hover card ---- */
.okt-fp-card { max-width: 380px; }
.okt-fp-card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 6px;
}
.okt-fp-card-name {
  font: 600 12.5px 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--text);
  overflow-wrap: anywhere;
}
.okt-fp-card-meta {
  flex: 0 0 auto;
  font-size: 11px;
  color: var(--text-faint);
  white-space: nowrap;
}
.okt-fp-card-pre {
  margin: 0;
  padding: 7px 9px;
  border: 1px solid var(--border-soft);
  border-radius: 5px;
  background: var(--surface-2);
  max-height: 220px;
  overflow: hidden;
}
.okt-fp-card-pre code {
  font: 400 11px/1.5 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--text-soft);
  white-space: pre;
}
.okt-fp-card-more,
.okt-fp-card-hint {
  margin-top: 5px;
  font-size: 10.5px;
  color: var(--text-faint);
}
.okt-fp-card-why {
  font-size: 12px;
  color: var(--warning);
}
.okt-fp-media {
  display: block;
  max-width: 100%;
  border-radius: 5px;
}
.okt-fp-media-small { max-height: 190px; }

/* ---- popup ---- */
.okt-fp-view {
  display: flex;
  flex-direction: column;
  min-height: 0;
  /* A three-line file in a full-screen overlay reads as a dialog that
     failed to load. The floor gives the frame a shape; the ceiling
     keeps a long file inside the viewport with its own scrollbar. */
  min-width: min(560px, 90vw);
  max-width: min(1100px, 94vw);
  max-height: 86vh;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}
/* The reading-aids pass wraps a <pre> in its own host with 16px of
   block margin, which split the frame into a bar and a separate card
   floating under it. Inside this frame they are one surface. */
.okt-fp-view .okt-pre-host { margin: 0; }
.okt-fp-view .okt-pre-host pre { border: 0; border-radius: 0; margin: 0; }
.okt-fp-view-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
  background: var(--surface-2);
}
.okt-fp-view-path {
  font: 500 12.5px 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--text);
  overflow-wrap: anywhere;
}
.okt-fp-view-meta {
  font-size: 11.5px;
  color: var(--text-faint);
  white-space: nowrap;
}
.okt-fp-view-pre {
  margin: 0;
  padding: 12px 14px;
  overflow: auto;
  min-height: 0;
}
.okt-fp-view-cut {
  padding: 10px 14px;
  font-size: 12px;
  color: var(--text-faint);
}

.oku-gloss,
.oku-extref {
  border-bottom: 1px dotted var(--accent-strong);
  cursor: help;
  color: var(--text);
  font-weight: 500;
  padding: 0 2px;
  border-radius: 3px;
  transition: background 0.15s;
}
.oku-gloss:hover,
.oku-gloss:focus-visible,
.oku-extref:hover,
.oku-extref:focus-visible {
  background: var(--accent-soft);
  outline: none;
}
.oku-gloss:focus-visible,
.oku-extref:focus-visible {
  box-shadow: 0 0 0 2px var(--accent);
}
.oku-gloss.unknown,
.oku-extref.unknown {
  border-bottom: 1px dotted var(--warning);
  color: var(--warning);
}

/* Type-themed underlines on the inline cite-text (the visible word the
   reader sees in flow). Different source kinds carry different colors so
   the page hints at WHAT kind of source backs each claim before the
   reader hovers. */
.oku-extref[data-cite-type="paper"]   { border-bottom-color: var(--series-3); }
.oku-extref[data-cite-type="rfc"]     { border-bottom-color: var(--text-soft); }
.oku-extref[data-cite-type="release"] { border-bottom-color: var(--success); }
.oku-extref[data-cite-type="blog"]    { border-bottom-color: var(--warning); }
.oku-extref[data-cite-type="other"]   { border-bottom-color: var(--accent); }

/* Ext-refs are pure hover-tooltip triggers — clicking the host pins
   the tooltip but never navigates. The destination URL lives as a
   clickable link inside the citation card instead. The arrow that
   previously hinted at outbound navigation lived alongside a duplicate
   "Learn more →" row and read as an action affordance on the host;
   both have been retired. */

/* The citation card itself — rendered inside the tooltip body. */
.okt-cite {
  display: grid;
  gap: 6px;
  font-family: 'Inter', sans-serif;
}
.okt-cite-head {
  display: flex;
  align-items: center;
  gap: 8px;
}
.okt-cite-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px; height: 26px;
  border-radius: 6px;
  background: var(--surface-2);
  color: var(--accent-strong);
  flex-shrink: 0;
}
.okt-cite-icon svg { width: 16px; height: 16px; }
.okt-cite-title {
  font-size: 14px;
  font-weight: 700;
  color: var(--text);
  line-height: 1.3;
}
/* Domain row inside the citation card. When the underlying ext-ref
   carries a `link`, this element is an `<a>` instead of a `<div>` —
   reading the URL should look like a link, not a code chip. The
   styling is tuned for "follow the source" affordance: accent
   underline on hover, faint outbound-arrow glyph appended inline by
   the JS so screen readers see "domain ↗". */
.okt-cite-domain {
  display: inline-block;
  font-size: 12.5px;
  color: var(--accent-strong);
  text-decoration: none;
  border-bottom: 1px dashed var(--accent);
  letter-spacing: 0;
  padding: 1px 0;
  align-self: start;
}
a.okt-cite-domain:hover,
a.okt-cite-domain:focus-visible {
  color: var(--accent);
  border-bottom-style: solid;
  outline: none;
}
/* When the citation card already exposes its own clickable domain,
   the generic "Learn more →" footer is redundant — hide it so the
   tooltip has a single navigation affordance. The `:has()` selector
   is widely supported in browsers oku already requires. */
.oku-tooltip:has(.okt-cite) .okt-link { display: none; }
.okt-cite-meta {
  font-size: 11.5px;
  color: var(--text-soft);
  display: flex; gap: 8px; flex-wrap: wrap;
  margin-top: 2px;
}
.okt-cite-author {
  font-style: italic;
}
.okt-cite-date::before {
  content: '· ';
  color: var(--text-faint);
}
.okt-cite-summary {
  font-size: 13px;
  color: var(--text);
  line-height: 1.5;
  margin-top: 4px;
}

/* Each type colors the icon background distinctly so the card "feels" the
   right kind even before reading the title. Every one of these reads a
   token: `paper` was the literal #4338ca, which is --series-3's LIGHT
   value, so the one citation kind a research page uses most kept the
   light palette on a dark page while its four siblings inverted. */
.okt-cite[data-cite-type="paper"]   .okt-cite-icon { background: color-mix(in srgb, var(--series-3) 14%, var(--surface)); color: var(--series-3); }
.okt-cite[data-cite-type="rfc"]     .okt-cite-icon { background: var(--surface-2); color: var(--text); }
.okt-cite[data-cite-type="release"] .okt-cite-icon { background: var(--success-soft); color: var(--success); }
.okt-cite[data-cite-type="blog"]    .okt-cite-icon { background: var(--warning-soft); color: var(--warning); }
.okt-cite[data-cite-type="other"]   .okt-cite-icon { background: var(--accent-soft); color: var(--accent-strong); }

.oku-tooltip {
  position: absolute;
  max-width: 340px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: 10px;
  padding: 14px 16px;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--text);
  box-shadow: 0 8px 24px var(--shadow-strong);
  font-family: 'Inter', sans-serif;
  font-weight: 400;
  letter-spacing: 0;
  z-index: 300;
  pointer-events: auto;
  animation: okt-in 0.18s ease;
}
@keyframes okt-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}
.oku-tooltip.pinned {
  border-color: var(--accent);
  box-shadow: 0 8px 24px var(--shadow-strong), 0 0 0 2px var(--accent-soft);
}
.oku-tooltip .okt-body { margin: 0; }
.oku-tooltip .okt-body p { margin: 0 0 8px 0; }
.oku-tooltip .okt-body p:last-child { margin-bottom: 0; }
.oku-tooltip .okt-body code { font-family: 'JetBrains Mono', monospace; font-size: 12.5px; background: var(--surface-2); padding: 1px 4px; border-radius: 3px; }
.oku-tooltip .okt-link {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
  font-size: 12.5px;
}
.oku-tooltip .okt-link a {
  color: var(--accent-strong);
  font-weight: 600;
  text-decoration: none;
  border-bottom: 1px solid var(--accent);
}
.oku-tooltip .okt-link a:hover { background: var(--accent-soft); }
.oku-tooltip .okt-lang {
  display: inline-block;
  font-size: 10.5px;
  color: var(--warning);
  background: var(--warning-soft);
  padding: 1px 6px;
  border-radius: 3px;
  margin-top: 8px;
  font-family: 'JetBrains Mono', monospace;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.oku-tooltip .okt-pin-hint {
  display: block;
  margin-top: 8px;
  font-size: 11px;
  color: var(--text-soft);
  font-style: italic;
}
.oku-tooltip.pinned .okt-pin-hint {
  color: var(--accent-strong);
  font-style: normal;
  font-weight: 600;
}
.oku-tooltip.pinned .okt-pin-hint::before { content: '📌  '; font-style: normal; }
.oku-tooltip:not(.pinned) .okt-pin-hint::before { content: '↳ '; }

/* ============================================================
   <page-nav> — site-tree sidebar
   ============================================================ */
page-nav {
  /* Parked off-canvas, slid in by body.drawer-open. `left` rather than a
     transform so position:sticky inside it (the panel headers) keeps a
     containing block. Two rows: the scroll region grows, the footer pins
     to the bottom edge. Fixed in every state, pinned included — a panel
     that scrolls away with the document is not one you can read
     alongside it. */
  display: flex;
  flex-direction: column;
  position: fixed;
  top: 0;
  left: -100%;
  width: var(--oku-drawer-w);
  height: 100vh;
  height: 100dvh;
  z-index: 99;
  overflow: hidden;
  overscroll-behavior: contain;
  background: var(--surface);
  border-right: 1px solid var(--border-soft);
  box-shadow: 4px 0 24px rgba(0,0,0,0.18);
  padding-top: 56px;
  font-size: 13.5px;
  line-height: 1.5;
  transition: left 0.25s ease, background 0.25s, border-color 0.25s;
}
body.drawer-open page-nav { left: 0; }
/* Pinned, the panel is not floating over anything — the column moved
   aside for it. A drop shadow there would draw a seam down a layout
   that has none. The border stays; that IS the seam. */
body.drawer-pinned page-nav { box-shadow: none; }
/* The scroll region: takes ALL space between the top of the sidebar
   and the footer at the bottom. Holds the boxed site tree AND the
   adopted TOC. Footer never scrolls with this content. */
page-nav .page-nav-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* Site-tree panel: a subtle bordered card so it reads as a distinct
   group from the TOC stacked below. Sits below the chrome strip via
   its own 76px top margin. */
page-nav .page-nav-panel {
  margin: 14px 14px 18px 14px;
  padding: 14px 14px;
  background: var(--surface-2);
  border: 1px solid var(--border-soft);
  border-radius: 8px;
  min-width: 0;
  transition: opacity 0.2s ease, margin 0.25s, padding 0.25s,
              background 0.25s, border-color 0.25s;
}
/* The TOC sits below the boxed site tree inside the scroll region —
   shares the same sidebar padding so the heading aligns with the
   site-tree labels rather than the panel-box edge. */
page-nav .page-nav-scroll > page-toc {
  display: block;
  padding: 0 22px 18px 22px;
}
/* Footer locked to the literal bottom edge of the sidebar via
   flex-shrink:0. Dimmed, so it does not compete for attention. A 1px
   top border separates it from the scroll region above.

   `--text-soft`, not `--text-faint`. Faint at this footer's 0.85
   opacity measures 2.8:1 light / 3.2:1 dark, which was defensible while
   the only thing down here was a decorative version number nobody had
   to read. It stopped being defensible when the build stamp moved in:
   a reader is meant to compare that string against a terminal, and a
   fact below the contrast floor is not a fact they have. Soft is still
   visibly quieter than body text, which is the register this wants.

   The blanket `opacity: 0.85` went with it. A container's opacity is
   inherited by every child and cannot be raised back by one, so the
   drift line — the one line in here that has to be READ, and the only
   one that is ever coloured — sat at 3.86:1 in light with no way out
   except a different hue. An alert that dims itself is a bug. The
   quietness lives in the token now, where a child can decline it. */
page-nav .page-nav-footer {
  flex: 0 0 auto;
  padding: 10px 22px;
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--text-soft);
  background: var(--surface);
  border-top: 1px solid var(--border-soft);
  user-select: none;
}
page-nav .page-nav-footer .page-nav-version {
  font-variant-numeric: tabular-nums;
}
/* The build block. Proximity: the facts about which build a reader is
   looking at, and the control that replaces it, sit on the version
   that was already here — not in a menu the reader would have to
   suspect exists. Hierarchy: it stays in footer-dim ink, because a
   document is not about its own provenance. Only the drift line takes
   a colour, and only when there is a real mismatch to name; a warning
   tint on a page that is fine is how a reader learns to skip the line
   that matters. */
page-nav .page-nav-stamp {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
page-nav .page-nav-freshness:empty { display: none; }
page-nav .page-nav-freshline {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin-top: 3px;
}
page-nav .page-nav-age { min-width: 0; }
page-nav .page-nav-rebuild {
  flex: 0 0 auto;
  font: inherit;
  letter-spacing: inherit;
  color: var(--text-soft);
  background: none;
  border: 1px solid var(--border-soft);
  border-radius: 4px;
  padding: 1px 7px;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}
page-nav .page-nav-rebuild:hover,
page-nav .page-nav-rebuild:focus-visible {
  color: var(--accent);
  border-color: var(--accent);
}
/* `:not([hidden])` rather than a bare `.page-nav-cmd`, because `code`
   is inline and needs `display: block` here — and an author rule
   setting `display` outranks the UA stylesheet's `[hidden]`, so the
   bare form ships the command permanently open. */
page-nav .page-nav-cmd:not([hidden]) {
  display: block;
  margin-top: 6px;
  padding: 5px 7px;
  font-size: 10.5px;
  line-height: 1.45;
  color: var(--text-soft);
  background: var(--surface-2);
  border-radius: 4px;
  /* `break-word`, not `anywhere`: a deep source path is one long token
     and has to break somewhere, but this keeps the break out of
     `&& oku build`, which fits on its own. Wrapping rather than
     scrolling because the whole command has to be selectable by hand —
     that is the fallback when a browser refuses the clipboard, and a
     fallback you have to drag-scroll through is not one. */
  overflow-wrap: break-word;
  /* The footer is user-select:none so a drag across the sidebar picks
     up nothing. This one string is the exception — it exists to be
     taken, and hand-selecting it is the fallback for every browser
     that refuses the clipboard. */
  user-select: text;
  -webkit-user-select: text;
}
page-nav .page-nav-drift {
  margin-top: 4px;
  color: var(--warning);
}
page-nav .page-nav-tree { list-style: none; margin: 0; padding: 0; }
page-nav .page-nav-tree ol { list-style: none; margin: 0; padding-left: 12px; }
page-nav .page-nav-tree ol.depth-0 { padding-left: 0; }
page-nav .page-nav-item { margin: 2px 0; }
page-nav .page-nav-item > a {
  display: block;
  padding: 4px 8px;
  color: var(--text);
  text-decoration: none;
  border-radius: 4px;
  border-left: 2px solid transparent;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.18s ease;
}
/* Fill, colour and the accent edge — no padding shift. The 8→12px
   nudge this used to carry slid the label out from under the pointer
   on every item the reader passed over. See "The raise" above. */
page-nav .page-nav-item > a:hover {
  background: var(--surface-2);
  color: var(--accent);
  border-left-color: var(--accent-soft);
}
page-nav .page-nav-item.active > a {
  color: var(--accent);
  font-weight: 600;
  border-left-color: var(--accent);
  background: var(--accent-soft);
}
page-nav .page-nav-folder {
  padding: 4px 8px;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-soft);
}
page-nav .page-nav-loading,
page-nav .page-nav-empty {
  padding: 8px 4px;
  font-size: 12px;
  color: var(--text-soft);
  font-style: italic;
}
/* Pane edge tabs (page-nav left, page-toc right) are fixed-positioned
   FULL-HEIGHT rails — the entire inner edge of the panel is clickable.
   The visible chevron sits in a centered thumb pill so the affordance
   remains a familiar shape, but the click target spans the whole edge.
   Position is fixed and tracks each pane's inner edge via
   --nav-tab-left / --toc-tab-right (synced by chrome.js from
   getBoundingClientRect), so:
   - horizontal scroll of overflowing main content doesn't move them,
   - one pane's collapse animates the tab with that pane only,
   - browser zoom and pinch-zoom (via --vv-*) keep them in place.
   At narrow widths (drawer mode) the @media block below anchors them
   to the viewport edges so they remain reachable when a drawer is shut. */
page-nav .page-nav-tab {
  position: fixed;
  top: 0;
  bottom: 0;
  left: var(--nav-tab-left, 248px);
  z-index: 50;
  width: 22px;
  height: 100vh;
  background: transparent;
  border: none;
  cursor: pointer;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.18s ease;
  transform: translate(var(--vv-left, 0), var(--vv-top, 0));
}
page-nav .page-nav-tab::before {
  /* Subtle accent stripe on the inner edge — reinforces "this whole
     side is clickable" without screaming. Brightens to accent on hover. */
  content: '';
  position: absolute;
  top: 0; bottom: 0; left: 0;
  width: 3px;
  background: transparent;
  transition: background 0.18s ease;
  pointer-events: none;
}
page-nav .page-nav-tab:hover,
page-nav .page-nav-tab:focus-visible {
  background: linear-gradient(90deg, var(--accent-soft) 0%, transparent 80%);
  outline: none;
}
page-nav .page-nav-tab:hover::before,
page-nav .page-nav-tab:focus-visible::before { background: var(--accent); }
.page-nav-tab-thumb {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 64px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-left: none;
  border-radius: 0 6px 6px 0;
  box-shadow: 0 4px 12px var(--shadow-soft);
  color: var(--text-soft);
  transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease,
              transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1);
  pointer-events: none;
}
page-nav .page-nav-tab:hover .page-nav-tab-thumb,
page-nav .page-nav-tab:focus-visible .page-nav-tab-thumb {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent-strong);
  transform: translateX(2px) scaleX(1.3);
}
page-nav .page-nav-tab:active .page-nav-tab-thumb {
  transform: translateX(2px) scaleX(1.2) scale(0.96);
  transition-duration: 0.08s;
}
.page-nav-tab-thumb svg { width: 14px; height: 14px; transition: transform 0.2s; }
/* The pane edge tabs are no longer rendered by page-nav / page-toc in
   the single-sidebar layout. If older HTML still contains them, hide. */
page-nav .page-nav-tab,
page-toc .page-toc-tab { display: none !important; }

/* In the single-sidebar layout, page-toc stacks under page-nav and its
   inner panel only needs a small top padding (page-nav above already
   clears the chrome-button strip). The outer page-toc has no padding
   so the section line-up is visually clean. */
.layout:has(page-nav) page-toc {
  display: block;
  align-self: start;
  padding: 0;
}
.layout:has(page-nav) page-toc .page-toc-panel {
  padding: 16px 22px 36px 22px;
  border-top: 1px solid var(--border-soft);
  overflow-y: auto;
  transition: opacity 0.2s ease, padding 0.25s;
}
body.toc-collapsed .layout:has(page-nav) page-toc .page-toc-panel {
  opacity: 0;
  padding-left: 0;
  padding-right: 0;
  pointer-events: none;
}
.layout:has(page-nav) page-toc .page-toc-tab {
  position: fixed;
  top: 0;
  bottom: 0;
  right: var(--toc-tab-right, 228px);
  z-index: 50;
  width: 22px;
  height: 100vh;
  background: transparent;
  border: none;
  cursor: pointer;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.18s ease;
  transform: translate(var(--vv-left, 0), var(--vv-top, 0));
}
.layout:has(page-nav) page-toc .page-toc-tab::before {
  content: '';
  position: absolute;
  top: 0; bottom: 0; right: 0;
  width: 3px;
  background: transparent;
  transition: background 0.18s ease;
  pointer-events: none;
}
.layout:has(page-nav) page-toc .page-toc-tab:hover,
.layout:has(page-nav) page-toc .page-toc-tab:focus-visible {
  background: linear-gradient(270deg, var(--accent-soft) 0%, transparent 80%);
  outline: none;
}
.layout:has(page-nav) page-toc .page-toc-tab:hover::before,
.layout:has(page-nav) page-toc .page-toc-tab:focus-visible::before { background: var(--accent); }
.page-toc-tab-thumb {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 64px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-right: none;
  border-radius: 6px 0 0 6px;
  box-shadow: 0 4px 12px var(--shadow-soft);
  color: var(--text-soft);
  transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease,
              transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1);
  pointer-events: none;
}
.layout:has(page-nav) page-toc .page-toc-tab:hover .page-toc-tab-thumb,
.layout:has(page-nav) page-toc .page-toc-tab:focus-visible .page-toc-tab-thumb {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent-strong);
  transform: translateX(-2px) scaleX(1.3);
}
.layout:has(page-nav) page-toc .page-toc-tab:active .page-toc-tab-thumb {
  transform: translateX(-2px) scaleX(1.2) scale(0.96);
  transition-duration: 0.08s;
}
.page-toc-tab-thumb svg { width: 14px; height: 14px; transition: transform 0.2s; }
body.toc-collapsed .page-toc-tab-thumb svg { transform: rotate(180deg); }

/* Narrow-viewport drawer rules live in the single @media (max-width: 768px)
   block earlier in this file. A previous (pre-page-nav-adopts-page-toc)
   design had a parallel block here for max-width: 1024px that treated
   page-toc as a separate right-edge drawer; both that drawer and the
   `.open` class it toggled were dropped along with the adoption refactor. */

/* ============================================================
   Forward-compat warning indicator
   ============================================================ */
/* Position comes from .okt-chrome-cluster; only the alarm colours are
   this button's own. It ships `display: none` and the cluster closes up
   around it, which is why there is no compaction rule here any more. */
.ctrl-btn.warning-indicator {
  color: var(--warning);
  background: var(--warning-soft);
  border-color: var(--warning);
  /* The count badge below is absolutely positioned against this button.
     `.okt-chrome-cluster .ctrl-btn` sets `position: static` so the row
     lays the buttons out, which left the badge resolving against the
     CLUSTER instead — it landed at the cluster's top-right corner, over
     the width toggle, reading as that button's badge. Same specificity
     as the cluster rule and later in the file, so this wins on order.
     Pinned by test_the_warning_count_sits_on_the_warning_button. */
  position: relative;
}
.ctrl-btn.warning-indicator::after {
  content: attr(data-count);
  position: absolute;
  top: -4px;
  right: -4px;
  min-width: 18px;
  height: 18px;
  border-radius: 9px;
  background: var(--danger);
  /* --danger inverts across themes (#b91c1c → #fca5a5) and a literal
     `white` does not: 6.5:1 in light, 1.9:1 in dark. This is the one
     control the chrome-dimming floor exempts, because an alert that
     dims itself is a bug — an alert whose count cannot be read is the
     same bug by another route. */
  color: var(--on-accent);
  font-size: 10px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 4px;
  font-family: 'JetBrains Mono', monospace;
}

.warning-panel {
  position: fixed;
  top: 70px;
  right: 16px;
  width: 360px;
  max-width: calc(100vw - 32px);
  max-height: 60vh;
  background: var(--surface);
  border: 1px solid var(--warning);
  border-radius: 12px;
  padding: 14px 16px;
  z-index: 220;
  box-shadow: 0 8px 24px var(--shadow-strong);
  overflow-y: auto;
  font-family: 'Inter', sans-serif;
  font-size: 13px;
  color: var(--text);
}
.warning-panel header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border);
}
.warning-panel header strong { color: var(--warning); font-size: 13px; }
.warning-panel-dismiss {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-soft);
  border-radius: 4px;
  padding: 4px 10px;
  font-size: 11px;
  cursor: pointer;
  font-family: inherit;
}
.warning-panel-dismiss:hover { background: var(--surface-2); color: var(--text); }
.warning-panel ul { list-style: none; margin: 0; padding: 0; }
.warning-panel li { padding: 8px 0; border-bottom: 1px solid var(--border-soft); line-height: 1.4; }
.warning-panel li:last-child { border-bottom: 0; }
.warning-panel li.level-error { color: var(--danger); }
.warning-panel li.level-warn { color: var(--warning); }
.warning-panel li code {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  background: var(--surface-2);
  color: var(--text-soft);
  padding: 1px 5px;
  border-radius: 3px;
  margin-right: 6px;
}
.warning-panel-jump {
  display: inline-block;
  margin-left: 8px;
  padding: 1px 8px;
  font-size: 11px;
  color: var(--accent);
  border: 1px solid var(--accent-soft);
  border-radius: 4px;
  text-decoration: none;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.warning-panel-jump:hover {
  background: var(--accent-soft);
  color: var(--accent-strong);
  border-color: var(--accent);
}
/* Brief flash on the warning's target element when the reader clicks
   "Jump to". Quiet — a soft ring, no flicker — so it locates the
   element without disrupting reading. */
.oku-flash {
  animation: oku-flash 1.6s ease-out 1;
}
@keyframes oku-flash {
  0%   { box-shadow: 0 0 0 4px var(--accent); }
  60%  { box-shadow: 0 0 0 4px var(--accent-soft); }
  100% { box-shadow: 0 0 0 0 transparent; }
}

/* In-place diagram error card — replaces mermaid's half-rendered SVG
   when the source fails to parse. Source is still reachable via the
   toolbar; the warning panel also lists this entry with a Jump-to. */
.okd-render.okd-error {
  display: block;
  padding: 14px;
  background: var(--surface-2);
  border: 1px dashed var(--warning);
  border-radius: 8px;
}
.okd-error-card {
  font-size: 13px;
  color: var(--text-soft);
  line-height: 1.5;
}
/* Same card, used by the renderer when a typed block draws nothing.
   It needs its own frame because it is not sitting inside an
   .okd-render the way the diagram error is. */
.okt-block-error {
  display: block;
  margin: 24px 0;
  padding: 14px 16px;
  background: var(--surface-2);
  border: 1px dashed var(--warning);
  border-radius: 8px;
}
.okd-error-card .okd-error-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 8px;
}
.okd-error-card strong {
  color: var(--warning);
  font-size: 13px;
}
.okd-error-card .okd-error-msg {
  font-family: 'JetBrains Mono', monospace;
  font-size: 12px;
  color: var(--text-faint);
}
.okd-error-card .okd-error-source {
  margin: 0 0 8px 0;
  padding: 10px 12px;
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: 6px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 12px;
  color: var(--text);
  white-space: pre;
  overflow-x: auto;
  max-height: 360px;
  overflow-y: auto;
}
.okd-error-card details.okd-error-detail summary {
  cursor: pointer;
  color: var(--text-faint);
  font-size: 12px;
  user-select: none;
}
.okd-error-card details.okd-error-detail[open] summary { margin-bottom: 6px; }
.okd-error-card details.okd-error-detail pre {
  margin: 0;
  padding: 8px 10px;
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: 4px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 11.5px;
  color: var(--text-soft);
  white-space: pre-wrap;
  word-break: break-word;
}

/* ============================================================
   <oku-chart> — scatter / line SVG charts
   ============================================================ */
/* Fullscreen lift — the host carries its scoped rules with it into
   the lightbox stage. Sizing overrides relax inline width/height so
   the chart fills the stage instead of the original column. */
oku-chart.okc-fullscreen,
oku-chart.okc-fullscreen .okc-svg {
  width: 100%;
  height: auto;
  max-width: none;
  max-height: none;
}
oku-chart.okc-fullscreen .okc-svg { display: block; }
/* Sparkline normally renders inline at 120x28; relax that in fullscreen
   so the chart actually fills the lightbox instead of looking like a
   thumbnail floating in dead space. The row wrapper switches from
   inline-flex to a block flow with the SVG stretching to viewport. */
oku-chart.okc-fullscreen .okc-sparkline-row { display: block; }
oku-chart.okc-fullscreen .okc-sparkline-row svg.okc-sparkline {
  width: 100%;
  height: auto;
  min-height: 320px;
  max-width: none;
  max-height: none;
  flex: none;
}
oku-chart {
  display: block;
  margin: 24px 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 12px 14px;
  position: relative;
}
oku-chart .okc-svg { display: block; width: 100%; height: auto; }
oku-chart .okc-title {
  fill: var(--text);
  font: 600 14px 'Inter', sans-serif;
}
oku-chart .okc-axis {
  stroke: var(--line-strong);
  stroke-width: 1;
  fill: none;
}
oku-chart .okc-axis-label {
  fill: var(--text-soft);
  font: 500 11px 'Inter', sans-serif;
  letter-spacing: 0.02em;
}
/* Gantt row labels sit in a gutter whose width okuFitLabelGutter()
   estimates from this font size. Leaving the font unset let it
   inherit the SVG default (16px), so the gutter and the glyphs
   disagreed and long labels ran off the left edge. */
oku-chart .okc-gantt-label {
  fill: var(--text-soft);
  font: 500 11px 'Inter', sans-serif;
}
oku-chart .okc-tick {
  fill: var(--text-soft);
  font: 400 10.5px 'JetBrains Mono', monospace;
}
/* Category labels drawn in a chart's own gutter, and the point labels a
   connected scatter hangs off its dots. One rule because they are one
   thing — the name of the row, written beside it — and because the way
   they broke was one thing too: an SVG <text> with no `fill` paints
   BLACK. Not "a bit dark". Black, on a #0e0c1c page. Eight renderers
   emitted a labelled <text> and none of them ever had a CSS rule, so
   every one of these charts lost its row names in dark mode while
   looking correct in light. Measured on docs/charts.html before the
   fix: fill rgb(0,0,0) on rgb(26,23,48) — contrast 1.21:1.

   The font is not cosmetic either. Unset, an SVG <text> inherits the
   16px default, and these renderers size their left gutter for 11px
   (`pad.left = 140` and friends) — so the labels also ran off the left
   edge of the viewBox and were clipped mid-word. Same trap the gantt
   label hit; see the note above it. */
oku-chart .okc-dot-plot-label,
oku-chart .okc-violin-label,
oku-chart .okc-lollipop-label,
oku-chart .okc-dumbbell-label,
oku-chart .okc-polar-area-label,
oku-chart .okc-pop-cat,
oku-chart .okc-conn-label,
oku-chart .okc-horizon-label {
  fill: var(--text-soft);
  font: 500 11px 'Inter', sans-serif;
}
/* The bump chart's series names are the same thing — a row name written
   beside the row — and were the one member of the family that never got
   a rule. They rendered at the inherited 16px against a gutter sized for
   11, and ran off BOTH edges of the viewBox, because a bump chart writes
   each name at both ends of its line.

   Font only, no fill: the renderer paints each name in its own series
   colour with a `fill` PRESENTATION ATTRIBUTE, and any CSS `fill` here —
   even `var(--text-soft)` — outranks it and would collapse every name to
   one colour. That colour is the only thing tying a name to its line. */
oku-chart .okc-bump-end-label {
  font: 500 11px 'Inter', sans-serif;
}
/* The same absence on two guide lines. Both carry a width (and the
   waterfall bridge a dash pattern) in the markup, which is how you can
   tell they were meant to be seen — an SVG <line> with no `stroke`
   defaults to none and simply never paints. The dot-plot's connector is
   the one its own source comment describes as running "from axis-left to
   dot (light) so the row reads as a single beat". */
oku-chart .okc-dot-plot-track {
  /* 1px, not 2. This line runs from the left edge of the plot to the
     dot, and the left edge of a dot plot is the data's minimum, not
     zero — drawn with any weight it reads as a bar measured from a
     baseline that does not exist. At a hairline it reads as what it is,
     a leader from the label to its mark. */
  stroke: var(--line-strong);
  stroke-width: 1;
}
oku-chart .okc-waterfall-bridge {
  stroke: var(--text-faint);
  stroke-width: 1;
}
oku-chart .okc-line { stroke-linejoin: round; stroke-linecap: round; }
oku-chart .okc-area { stroke: none; }
/* Quadrant chart — two reference lines dividing the plot into four
   regions, plus optional corner labels. The reference lines are
   dashed and faint so they read as guides, not as data. */
oku-chart .okc-quadrant {
  stroke: var(--text-faint);
  stroke-width: 1;
  stroke-dasharray: 4 4;
  opacity: 0.6;
  fill: none;
}
/* Quadrant region labels (QUICK WINS / RE-EVALUATE / etc.). Rendered
   OUTSIDE the plot bounds — top labels above the top axis, bottom
   labels below the x-tick row — so a data point can never overlap
   them at rest. No more backing pill or .okc-label-overlay fade —
   labels live in their own gutter. */
oku-chart .okc-quadrant-label {
  fill: var(--text-soft);
  font: 600 10.5px 'Inter', sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
oku-chart .okc-legend {
  /* Same halo trick for legend chip text — keeps the label readable
     when a data point happens to scatter under the chip. */
  stroke: var(--bg);
  stroke-width: 3;
  stroke-linejoin: round;
  paint-order: stroke fill;
}
/* Donut chart — slices, centre readout, side legend. */
/* Global: never show the browser default focus outline on chart
   interactive shapes. Each shape has its own per-state visual
   below (scale, stroke, opacity). */
oku-chart [tabindex]:focus,
oku-chart [tabindex]:focus-visible {
  outline: none;
}
oku-chart .okc-donut .okc-slice {
  stroke: var(--bg);
  stroke-width: 2;
  cursor: pointer;
  transition: opacity 0.12s ease, transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1), stroke-width 0.12s ease;
  transform-origin: 140px 160px;
}
oku-chart .okc-donut .okc-slice:hover,
oku-chart .okc-donut .okc-slice:focus-visible {
  transform: scale(1.04);
  stroke-width: 3;
  outline: none;
}
/* Pinned slice — stronger raise + accent ring so the click-pin
   state reads at a glance after the cursor moves away. */
oku-chart .okc-donut .okc-slice.okc-pinned {
  transform: scale(1.06);
  stroke: var(--accent);
  stroke-width: 3;
}

/* Sunburst rings — same outward-grow hover as donut. transform-
   origin centers on the wheel's own centre (cx, cy in renderer). */
oku-chart .okc-sunburst .okc-sunburst-arc {
  stroke: var(--bg);
  stroke-width: 1;
  cursor: pointer;
  transition: opacity 0.12s ease, transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1), stroke-width 0.12s ease;
  transform-box: fill-box;
  transform-origin: center;
}
oku-chart .okc-sunburst .okc-sunburst-arc:hover,
oku-chart .okc-sunburst .okc-sunburst-arc:focus-visible {
  stroke-width: 2.5;
  outline: none;
  filter: brightness(1.12);
}
oku-chart .okc-sunburst .okc-sunburst-arc.okc-pinned {
  stroke: var(--accent);
  stroke-width: 2.5;
}

/* Polar-area sectors — match donut/sunburst hover semantics. */
oku-chart .okc-polar-area .okc-polar-area-sector {
  stroke: var(--bg);
  stroke-width: 1;
  cursor: pointer;
  transition: opacity 0.12s ease, transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1), stroke-width 0.12s ease, fill-opacity 0.18s ease;
}
oku-chart .okc-polar-area .okc-polar-area-sector:hover,
oku-chart .okc-polar-area .okc-polar-area-sector:focus-visible {
  fill-opacity: 0.95;
  stroke-width: 2.5;
  outline: none;
}
oku-chart .okc-polar-area .okc-polar-area-sector.okc-pinned {
  stroke: var(--accent);
  stroke-width: 2.5;
}
oku-chart .okc-donut-total {
  fill: var(--text);
  font: 800 22px 'Inter', sans-serif;
  letter-spacing: -0.02em;
}
oku-chart .okc-donut-caption {
  fill: var(--text-soft);
  font: 500 10.5px 'Inter', sans-serif;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
/* Interactive non-Cartesian legends — clicking a legend item toggles
   the matching slice / cell / area's visibility. Hidden shapes fade
   out; the legend item shows the off state via line-through label. */
oku-chart .okc-donut-legend,
oku-chart .okc-waffle-legend,
oku-chart .okc-radar-legend {
  cursor: pointer;
  transition: opacity 0.15s ease;
}
oku-chart .okc-donut-legend:hover,
oku-chart .okc-waffle-legend:hover,
oku-chart .okc-radar-legend:hover {
  opacity: 0.85;
}
oku-chart .okc-donut-legend.okc-legend-off text,
oku-chart .okc-waffle-legend.okc-legend-off text,
oku-chart .okc-radar-legend.okc-legend-off text {
  text-decoration: line-through;
  opacity: 0.55;
}
oku-chart .okc-donut-legend.okc-legend-off rect,
oku-chart .okc-waffle-legend.okc-legend-off rect,
oku-chart .okc-radar-legend.okc-legend-off rect {
  opacity: 0.35;
}
oku-chart .okc-hidden,
oku-chart .okc-donut .okc-slice.okc-hidden,
oku-chart .okc-waffle .okc-waffle-cell.okc-hidden,
oku-chart .okc-radar .okc-radar-series.okc-hidden {
  opacity: 0.08;
  pointer-events: none;
  transition: opacity 0.18s ease;
}

/* Radar polygon hover — pop the hovered series to the front + dim
   the rest, so a multi-series radar reads as one shape at a time
   when comparing. Same emphasis pattern as slope/parcoord. */
oku-chart .okc-radar .okc-radar-series {
  cursor: pointer;
  transition: opacity 0.15s ease, fill-opacity 0.18s ease, stroke-width 0.12s ease;
}
oku-chart .okc-radar:has(.okc-radar-series:hover) .okc-radar-series {
  opacity: 0.25;
}
oku-chart .okc-radar:has(.okc-radar-series:hover) .okc-radar-series:hover,
oku-chart .okc-radar:has(.okc-radar-series:focus-visible) .okc-radar-series:focus-visible {
  opacity: 1;
  fill-opacity: 0.45;
  stroke-width: 2.6;
  outline: none;
  filter: drop-shadow(0 0 4px var(--accent));
}

/* Legend hover highlight — **emphasize** the hovered series rather
   than dim the others. Earlier behaviour faded the non-matching
   series to opacity 0.18; the reader's attention then tracked the
   change-points (the dimming shapes) instead of the target (which
   stayed stationary). Inverted to keep the other series stable so
   only the target shifts: the hovered series gets a thicker stroke
   + larger dot + accent drop-shadow, everything else stays as-is.

   Wiring: the chart's `<oku-chart>` host gets `.okc-legend-hovering`
   + `data-legend-hover="<idx>"` while a legend chip is hovered or
   focused. Cartesian shapes carry `data-series-idx` on the parent
   `.okc-series`; non-Cartesian shapes use their own index attr. */

/* Cartesian — emphasize the matching series. */
oku-chart.okc-legend-hovering .okc-series {
  transition: filter 0.15s ease;
}
oku-chart.okc-legend-hovering[data-legend-hover="0"] .okc-series[data-series-idx="0"],
oku-chart.okc-legend-hovering[data-legend-hover="1"] .okc-series[data-series-idx="1"],
oku-chart.okc-legend-hovering[data-legend-hover="2"] .okc-series[data-series-idx="2"],
oku-chart.okc-legend-hovering[data-legend-hover="3"] .okc-series[data-series-idx="3"],
oku-chart.okc-legend-hovering[data-legend-hover="4"] .okc-series[data-series-idx="4"],
oku-chart.okc-legend-hovering[data-legend-hover="5"] .okc-series[data-series-idx="5"],
oku-chart.okc-legend-hovering[data-legend-hover="6"] .okc-series[data-series-idx="6"],
oku-chart.okc-legend-hovering[data-legend-hover="7"] .okc-series[data-series-idx="7"] {
  filter: drop-shadow(0 0 5px var(--accent)) drop-shadow(0 0 2px var(--accent));
}
oku-chart.okc-legend-hovering[data-legend-hover="0"] .okc-series[data-series-idx="0"] path,
oku-chart.okc-legend-hovering[data-legend-hover="1"] .okc-series[data-series-idx="1"] path,
oku-chart.okc-legend-hovering[data-legend-hover="2"] .okc-series[data-series-idx="2"] path,
oku-chart.okc-legend-hovering[data-legend-hover="3"] .okc-series[data-series-idx="3"] path,
oku-chart.okc-legend-hovering[data-legend-hover="4"] .okc-series[data-series-idx="4"] path,
oku-chart.okc-legend-hovering[data-legend-hover="5"] .okc-series[data-series-idx="5"] path,
oku-chart.okc-legend-hovering[data-legend-hover="6"] .okc-series[data-series-idx="6"] path,
oku-chart.okc-legend-hovering[data-legend-hover="7"] .okc-series[data-series-idx="7"] path {
  stroke-width: 3.5;
}
oku-chart.okc-legend-hovering[data-legend-hover="0"] .okc-series[data-series-idx="0"] circle,
oku-chart.okc-legend-hovering[data-legend-hover="1"] .okc-series[data-series-idx="1"] circle,
oku-chart.okc-legend-hovering[data-legend-hover="2"] .okc-series[data-series-idx="2"] circle,
oku-chart.okc-legend-hovering[data-legend-hover="3"] .okc-series[data-series-idx="3"] circle,
oku-chart.okc-legend-hovering[data-legend-hover="4"] .okc-series[data-series-idx="4"] circle,
oku-chart.okc-legend-hovering[data-legend-hover="5"] .okc-series[data-series-idx="5"] circle,
oku-chart.okc-legend-hovering[data-legend-hover="6"] .okc-series[data-series-idx="6"] circle,
oku-chart.okc-legend-hovering[data-legend-hover="7"] .okc-series[data-series-idx="7"] circle {
  r: 6;
}

/* Non-Cartesian: emphasize the matching shape with a brighter
   ring + accent halo. Other slices/cells/areas stay as-is. */
oku-chart.okc-legend-hovering .okc-slice,
oku-chart.okc-legend-hovering .okc-waffle-cell,
oku-chart.okc-legend-hovering .okc-radar-series {
  transition: filter 0.15s ease, stroke-width 0.15s ease;
}
oku-chart.okc-legend-hovering[data-legend-hover="0"] .okc-slice[data-slice-idx="0"],
oku-chart.okc-legend-hovering[data-legend-hover="1"] .okc-slice[data-slice-idx="1"],
oku-chart.okc-legend-hovering[data-legend-hover="2"] .okc-slice[data-slice-idx="2"],
oku-chart.okc-legend-hovering[data-legend-hover="3"] .okc-slice[data-slice-idx="3"],
oku-chart.okc-legend-hovering[data-legend-hover="4"] .okc-slice[data-slice-idx="4"],
oku-chart.okc-legend-hovering[data-legend-hover="5"] .okc-slice[data-slice-idx="5"],
oku-chart.okc-legend-hovering[data-legend-hover="6"] .okc-slice[data-slice-idx="6"],
oku-chart.okc-legend-hovering[data-legend-hover="7"] .okc-slice[data-slice-idx="7"],
oku-chart.okc-legend-hovering[data-legend-hover="0"] .okc-waffle-cell[data-segment-idx="0"],
oku-chart.okc-legend-hovering[data-legend-hover="1"] .okc-waffle-cell[data-segment-idx="1"],
oku-chart.okc-legend-hovering[data-legend-hover="2"] .okc-waffle-cell[data-segment-idx="2"],
oku-chart.okc-legend-hovering[data-legend-hover="3"] .okc-waffle-cell[data-segment-idx="3"],
oku-chart.okc-legend-hovering[data-legend-hover="0"] .okc-radar-series[data-series-idx="0"],
oku-chart.okc-legend-hovering[data-legend-hover="1"] .okc-radar-series[data-series-idx="1"],
oku-chart.okc-legend-hovering[data-legend-hover="2"] .okc-radar-series[data-series-idx="2"],
oku-chart.okc-legend-hovering[data-legend-hover="3"] .okc-radar-series[data-series-idx="3"] {
  /* Earlier this rule used `drop-shadow(0 0 6px var(--accent))` —
     the halo bled across the inter-slice gap into the neighbour's
     color and made the unit boundary harder to see, not easier.
     Replaced with a non-bleeding emphasis:
       - accent stroke (paints the slice's own edge — doesn't extend
         past it)
       - brightness(1.12) on the fill (pops the colour without
         leaking sideways)
       - stroke-width matched to the default bg-stroke so the slice
         keeps its visual proportions
     Adjacent slices stay put; the inter-slice gap reads at full
     contrast. */
  stroke: var(--accent);
  stroke-width: 2.5;
  filter: brightness(1.12);
}

/* General label-overlay collision avoidance — any chart label that
   could obscure a data point (quadrant corner labels, future axis
   titles in tight charts) tags itself with .okc-label-overlay.
   The label stays click-transparent (pointer-events:none) and
   fades when the chart :has() a hovered data point OR an active
   tooltip — the reader's interaction with data wins over a
   stationary label. */
oku-chart .okc-label-overlay {
  pointer-events: none;
  transition: opacity 0.18s ease;
}

/* Generic vertical cursor (box-plot, histogram, candlestick,
   density, beeswarm, dot-plot, …). Sibling concept to the
   Cartesian charts' .okc-cartesian-cursor. Dashed accent line
   that follows the pointer's x-position within the plot region. */
oku-chart .okc-generic-cursor {
  stroke: var(--accent);
  stroke-width: 2;
  stroke-opacity: 0.85;
  stroke-dasharray: 4 3;
  pointer-events: none;
}
/* Bar chart vertical cursor (DIV-based variant of .okc-generic-
   cursor). Same visual semantic — dashed accent line following
   the pointer's x. The chart wrap is position:relative so an
   absolutely-positioned full-height line lands inside it. */
.bar-chart,
.bar-chart-multi {
  position: relative;
}
.okc-bar-cursor {
  position: absolute;
  /* top/bottom are set in JS to match the plot area so the cursor
     doesn't bleed over the title row or the legend rack. */
  width: 0;
  border-left: 2px dashed var(--accent);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s ease;
  z-index: 1;
}

/* Vertical (column) bar layout — same DOM, flipped axis. The
   renderer adds .bar-chart-vertical when block.orientation is
   'vertical'. CSS overrides the grid to lay rows out in columns
   and makes the track + fill grow upward via height instead of
   width. The `--bar-pct` CSS var (set by the renderer alongside
   inline width) drives the vertical height. */
.bar-chart-vertical {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(min(28px, 100%), 1fr);
  gap: 12px;
  align-items: end;
  min-height: 220px;
}
.bar-chart-vertical .bar-row {
  display: flex;
  flex-direction: column;
  align-items: center;
  height: 100%;
  gap: 4px;
  grid-template-columns: none;
}
.bar-chart-vertical .bar-row .bar-value {
  text-align: center;
  order: 0;
}
.bar-chart-vertical .bar-row .bar-track {
  order: 1;
  height: 100%;
  width: 24px;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}
.bar-chart-vertical .bar-row .bar-label {
  order: 2;
  text-align: center;
  font-size: 12px;
}
.bar-chart-vertical .bar-row .bar-fill {
  width: 100% !important;
  height: calc(var(--bar-pct, 0) * 1%);
  align-self: stretch;
}
/* Stacked vertical: bars accumulate from the bottom up. */
.bar-chart-vertical.bar-chart-stacked-bar .bar-row .bar-track {
  flex-direction: column-reverse;
}
.bar-chart-vertical.bar-chart-stacked-bar .bar-row .bar-fill {
  height: calc(var(--bar-pct, 0) * 1%);
  border-radius: 0;
}
.bar-chart-vertical.bar-chart-stacked-bar .bar-row .bar-fill:first-child {
  border-radius: 0 0 4px 4px;
}
.bar-chart-vertical.bar-chart-stacked-bar .bar-row .bar-fill:last-child {
  border-radius: 4px 4px 0 0;
}
/* Grouped vertical: bars sit side-by-side in the row. */
.bar-chart-vertical.bar-chart-grouped-bar .bar-row .bar-track {
  flex-direction: row;
  align-items: flex-end;
  gap: 2px;
  width: auto;
  min-width: 60px;
}
.bar-chart-vertical.bar-chart-grouped-bar .bar-row .bar-fill {
  width: 8px !important;
  height: calc(var(--bar-pct, 0) * 1%);
}
oku-chart:has(.okc-dot:hover) .okc-label-overlay,
oku-chart:has(.okc-bubble:hover) .okc-label-overlay,
oku-chart:has(.okc-slice:hover) .okc-label-overlay,
oku-chart:has(.okc-waffle-cell:hover) .okc-label-overlay,
oku-chart:has(.okc-treemap-cell:hover) .okc-label-overlay,
oku-chart:has(.okc-funnel-band:hover) .okc-label-overlay,
oku-chart:has(.okc-network-node:hover) .okc-label-overlay,
oku-chart:has(.okc-pinned) .okc-label-overlay,
oku-chart:has(.okc-tooltip.visible) .okc-label-overlay {
  opacity: 0.15;
}
oku-chart .okc-donut-legend-label {
  fill: var(--text);
  font: 500 12px 'Inter', sans-serif;
  dominant-baseline: middle;
}

/* ============ Tier-1 / Tier-3 chart variants ============ *
 * heatmap · sparkline · waffle · gauge · radar · box-plot · bullet · slope.
 * Visual tokens come from the same accent / warn / danger / success /
 * muted vocabulary as donut. Each rule scopes inside .okc-<type> to
 * isolate the SVG-class space from the Cartesian renderer. */

oku-chart .okc-heatmap .okc-heatmap-cell {
  stroke: var(--surface);
  stroke-width: 0.5;
  transition: fill-opacity 0.12s ease;
}
oku-chart .okc-heatmap .okc-heatmap-cell:hover { stroke: var(--accent-strong); stroke-width: 1.5; }
oku-chart .okc-heatmap .okc-heatmap-label {
  fill: var(--text-soft);
  font: 500 11px 'Inter', sans-serif;
  dominant-baseline: middle;
}

.okc-sparkline-row {
  display: inline-flex; align-items: center; gap: 6px;
  line-height: 1;
  /* An inline-flex sizes to max-content, so a 120px sparkline plus its
     end label is a width this box REFUSES to go below and the whole page
     scrolls sideways behind it. Two declarations, and they go together:
     the cap here, and a track below that is allowed to give. Found at
     360px with the reader's text scale at 2x — 407px of content in a
     360px viewport — but the same box overflows at 1x the moment the end
     label is long enough, which is why the fix is the cap and not a
     media query. */
  max-width: 100%;
}
.okc-sparkline-row svg.okc-sparkline {
  width: 120px; height: 28px;
  /* Shrinkable, not fixed: `0 0 auto` is what made the row's minimum the
     full 120px. A sparkline is a shape, and a narrower one is still the
     shape — an absent one is not. */
  flex: 0 1 120px;
  min-width: 0;
  /* sparkline inherits theme color; .okc-sparkline-* classes pick from it. */
  color: var(--accent);
}
.okc-sparkline-row .okc-sparkline-end-label {
  font: 500 12px 'Inter', sans-serif;
  color: var(--text-soft);
  min-width: 0;
  overflow-wrap: anywhere;
}
oku-chart .okc-sparkline .okc-sparkline-line {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.okc-sparkline .okc-sparkline-cursor {
  stroke: var(--accent-strong);
  stroke-width: 0.5;
  stroke-dasharray: 2 2;
  opacity: 0.7;
}
.okc-sparkline .okc-sparkline-cursor-dot {
  fill: var(--accent-strong);
}
oku-chart .okc-sparkline .okc-sparkline-area {
  fill: currentColor;
  fill-opacity: 0.16;
  stroke: none;
}
oku-chart .okc-sparkline .okc-sparkline-bar { fill: currentColor; }
oku-chart .okc-sparkline .okc-sparkline-endpoint {
  fill: currentColor;
  stroke: var(--surface);
  stroke-width: 1;
}

oku-chart .okc-waffle .okc-waffle-cell {
  stroke: var(--surface);
  stroke-width: 0.6;
  transition: fill-opacity 0.12s ease;
}
oku-chart .okc-waffle .okc-waffle-cell:hover { stroke: var(--accent-strong); stroke-width: 1.4; }
oku-chart .okc-waffle .okc-waffle-legend-label {
  fill: var(--text);
  font: 500 12px 'Inter', sans-serif;
  dominant-baseline: middle;
}

oku-chart .okc-gauge .okc-gauge-bg { /* background arc — uses inline fill */ }
oku-chart .okc-gauge .okc-gauge-zone { /* zone band — uses inline fill + opacity */ }
oku-chart .okc-gauge .okc-gauge-value { /* value arc — accent */ }
oku-chart .okc-gauge .okc-gauge-target {
  stroke: var(--text);
  stroke-width: 2;
}
oku-chart .okc-gauge .okc-gauge-value-text {
  fill: var(--text);
  font: 700 30px 'Inter', sans-serif;
}
oku-chart .okc-gauge .okc-gauge-label {
  fill: var(--text-soft);
  font: 500 11.5px 'Inter', sans-serif;
}

oku-chart .okc-radar .okc-radar-ring {
  fill: none;
  stroke: var(--border-soft);
  stroke-width: 0.6;
}
oku-chart .okc-radar .okc-radar-axis {
  stroke: var(--border);
  stroke-width: 0.6;
}
oku-chart .okc-radar .okc-radar-label {
  fill: var(--text);
  font: 500 12px 'Inter', sans-serif;
  dominant-baseline: middle;
}
oku-chart .okc-radar .okc-radar-series { cursor: default; transition: fill-opacity 0.18s ease; }
oku-chart .okc-radar .okc-radar-series:hover { fill-opacity: 0.4; }
oku-chart .okc-radar .okc-radar-legend-label {
  fill: var(--text-soft);
  font: 500 12px 'Inter', sans-serif;
  dominant-baseline: middle;
}

oku-chart .okc-boxplot .okc-boxplot-label {
  fill: var(--text);
  font: 500 12px 'Inter', sans-serif;
}
oku-chart .okc-boxplot .okc-boxplot-whisker {
  stroke: var(--text-soft);
  stroke-width: 1.2;
}
oku-chart .okc-boxplot .okc-boxplot-iqr { stroke-width: 1; }
oku-chart .okc-boxplot .okc-boxplot-outlier {
  stroke: var(--surface);
  stroke-width: 1;
}

oku-chart .okc-bullet .okc-bullet-label {
  fill: var(--text);
  font: 500 12px 'Inter', sans-serif;
}
oku-chart .okc-bullet .okc-bullet-bg {
  fill: var(--code-bg, rgba(127,127,127,0.12));
}
oku-chart .okc-bullet .okc-bullet-target {
  stroke: var(--text);
  stroke-width: 2.4;
}
oku-chart .okc-bullet .okc-bullet-readout {
  fill: var(--text);
  font: 600 12px 'Inter', sans-serif;
  dominant-baseline: middle;
}

oku-chart .okc-slope .okc-slope-col {
  fill: var(--text-soft);
  font: 600 12px 'Inter', sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
oku-chart .okc-slope .okc-slope-readout {
  fill: var(--text);
  font: 500 12px 'Inter', sans-serif;
  dominant-baseline: middle;
}
/* Slope-line hover — same emphasis pattern as Cartesian series.
   The line thickens + brightens; un-hovered siblings dim so the
   target reads as the selected line. tabindex on the renderer makes
   it keyboard-focusable too. */
oku-chart .okc-slope .okc-slope-line {
  cursor: pointer;
  transition: stroke-width 0.12s ease, opacity 0.12s ease;
}
oku-chart .okc-slope:has(.okc-slope-line:hover) .okc-slope-line {
  opacity: 0.35;
}
oku-chart .okc-slope:has(.okc-slope-line:hover) .okc-slope-line:hover,
oku-chart .okc-slope:has(.okc-slope-line:focus-visible) .okc-slope-line:focus-visible {
  stroke-width: 4;
  opacity: 1;
  outline: none;
  filter: drop-shadow(0 0 4px var(--accent));
}

/* Parallel-coordinates polylines — same hover pattern as slope.
   The record being read pops; the rest fade so the trail through
   each axis reads cleanly. */
oku-chart .okc-parcoord .okc-parcoord-line {
  cursor: pointer;
  transition: stroke-width 0.12s ease, opacity 0.12s ease;
  fill: none;
}
oku-chart .okc-parcoord:has(.okc-parcoord-line:hover) .okc-parcoord-line {
  opacity: 0.18;
}
oku-chart .okc-parcoord:has(.okc-parcoord-line:hover) .okc-parcoord-line:hover,
oku-chart .okc-parcoord:has(.okc-parcoord-line:focus-visible) .okc-parcoord-line:focus-visible {
  stroke-width: 3;
  opacity: 1;
  outline: none;
  filter: drop-shadow(0 0 3px var(--accent));
}

oku-chart .okc-histogram .okc-histogram-bar {
  transition: fill-opacity 0.12s ease;
}
oku-chart .okc-histogram .okc-histogram-bar:hover { fill-opacity: 1; }

oku-chart .okc-calendar .okc-calendar-cell {
  stroke: var(--surface);
  stroke-width: 0.5;
  transition: fill-opacity 0.12s ease;
}
oku-chart .okc-calendar .okc-calendar-cell:hover { stroke: var(--accent-strong); stroke-width: 1.4; }
oku-chart .okc-calendar .okc-calendar-label {
  fill: var(--text-soft);
  font: 500 10.5px 'Inter', sans-serif;
  dominant-baseline: middle;
}
oku-chart .okc-calendar .okc-calendar-month {
  fill: var(--text-soft);
  font: 500 10.5px 'Inter', sans-serif;
  letter-spacing: 0.06em;
}

oku-chart .okc-treemap .okc-treemap-cell rect {
  stroke: var(--surface);
  stroke-width: 1;
  transition: fill-opacity 0.12s ease;
}
oku-chart .okc-treemap .okc-treemap-cell:hover rect { fill-opacity: 1; }
oku-chart .okc-treemap .okc-treemap-label {
  fill: var(--surface);
  font: 700 13px 'Inter', sans-serif;
  paint-order: stroke fill;
  stroke: rgba(0,0,0,0.32);
  stroke-width: 0.6;
}
oku-chart .okc-treemap .okc-treemap-value {
  fill: var(--surface);
  font: 500 11.5px 'Inter', sans-serif;
  fill-opacity: 0.88;
}

oku-chart .okc-ridgeline .okc-ridgeline-curve {
  transition: fill-opacity 0.18s ease;
}
oku-chart .okc-ridgeline .okc-ridgeline-curve:hover { fill-opacity: 0.55; }
oku-chart .okc-ridgeline .okc-ridgeline-label {
  fill: var(--text);
  font: 600 12px 'Inter', sans-serif;
}
/* Parallel cursor — vertical line + numeric readout that follows the
   pointer across every ridge so the reader can compare a given X
   value across distributions in one glance. */
oku-chart .okc-ridgeline .okc-ridge-cursor {
  stroke: var(--accent-strong);
  stroke-dasharray: 3 3;
  opacity: 0.75;
}
/* Same idea for Cartesian charts (scatter / line / area / bubble).
   The synced cursor spans the plot region; tooltip shows every
   series' value at the cursor's nearest x. */
oku-chart .okc-cartesian-cursor {
  stroke: var(--accent-strong);
  stroke-width: 1;
  stroke-dasharray: 3 3;
  opacity: 0.6;
  pointer-events: none;
}
/* Gauge zone-chip row (bottom of the gauge, never on the arc). */
oku-chart .okc-gauge-zone-chip text {
  fill: var(--text-soft);
}
oku-chart .okc-gauge-cursor {
  fill: var(--accent-strong);
  pointer-events: none;
}
oku-chart .okc-gauge-cursor-line {
  stroke: var(--accent-strong);
  stroke-width: 1.5;
  stroke-dasharray: 2 2;
  pointer-events: none;
}
oku-chart .okc-ridgeline .okc-ridge-cursor-label {
  fill: var(--accent-strong);
  font: 600 11px 'JetBrains Mono', monospace;
  font-variant-numeric: tabular-nums;
}

oku-chart .okc-funnel .okc-funnel-band {
  transition: fill-opacity 0.18s ease;
}
oku-chart .okc-funnel .okc-funnel-band:hover { fill-opacity: 1; }
oku-chart .okc-funnel .okc-funnel-label {
  fill: var(--text);
  font: 500 12px 'Inter', sans-serif;
  dominant-baseline: middle;
}
/* Value + percentage now sit in their own right-anchored columns so
   digits stack visually. Tabular-nums keeps proportional fonts from
   wobbling between rows. Same family as labels for visual unity. */
oku-chart .okc-funnel .okc-funnel-value,
oku-chart .okc-funnel .okc-funnel-pct {
  fill: var(--text-soft);
  font: 500 12px 'JetBrains Mono', ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  dominant-baseline: middle;
}
oku-chart .okc-funnel .okc-funnel-pct { fill: var(--text); }
/* Legacy readout class — earlier rendering combined value+pct into
   one cell. Kept so older standalone exports still style cleanly. */
oku-chart .okc-funnel .okc-funnel-readout {
  fill: var(--text-soft);
  font: 500 12px 'Inter', sans-serif;
  dominant-baseline: middle;
}

oku-chart .okc-dot {
  stroke: var(--surface);
  stroke-width: 1.5;
  cursor: pointer;
  transition: r 0.12s ease, stroke-width 0.12s ease, opacity 0.18s ease;
}
oku-chart .okc-dot:hover,
oku-chart .okc-dot:focus,
oku-chart .okc-dot.hovered {
  r: 6;
  stroke-width: 2;
  outline: none;
}
oku-chart .okc-dot:focus-visible {
  stroke: var(--accent-strong);
}
oku-chart .okc-point-label {
  fill: var(--text-soft);
  font: 400 10.5px 'Inter', sans-serif;
  cursor: pointer;
  /* Paint a halo behind the text so labels stay readable when they brush
     against dots or grid lines — paint-order draws stroke first, fill on
     top, giving a clean outline. The halo color is the surface color so
     it blends with the chart frame. */
  paint-order: stroke fill;
  stroke: var(--surface);
  stroke-width: 3px;
  stroke-linejoin: round;
  transition: fill 0.15s ease, font-weight 0.15s ease;
}
oku-chart .okc-point-label:hover,
oku-chart .okc-point-label:focus,
oku-chart .okc-point-label.hovered {
  fill: var(--accent-strong);
  font-weight: 600;
  outline: none;
}
/* Collision fallback: labels that survived the stagger pass keep their
   y-shift; labels that couldn't fit anywhere are hidden until their
   dot is hovered/focused. The existing setHover(key,on) toggles the
   .hovered class on the matching label, which reveals it. */
oku-chart .okc-point-label.okc-label-shifted {
  /* Visual hint that the label has been moved off the line. */
  fill: var(--text-faint);
}
oku-chart .okc-point-label.okc-label-shifted.hovered {
  fill: var(--accent-strong);
}
oku-chart .okc-point-label.okc-label-hidden {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s ease;
}
oku-chart .okc-point-label.okc-label-hidden.hovered {
  opacity: 1;
  pointer-events: auto;
  font-weight: 600;
}

/* Series dimming via legend toggle */
oku-chart .okc-series { transition: opacity 0.2s ease; }
oku-chart .okc-series.dim { opacity: 0.15; }

/* Legend chips — clickable, focusable */
oku-chart .okc-legend-chip { cursor: pointer; }
/* Legend backdrop. Opaque surface so a data point that lands
   underneath gets visually masked instead of bleeding through the
   chip text — fixes the user-reported "legends overlap data". */
oku-chart .okc-legend-bg {
  fill: var(--surface);
  fill-opacity: 0.94;
  transition: fill 0.12s ease;
}
/* Cluster-level backdrop — sits behind every legend chip in the
   top-right column. Fully opaque so a giant bubble or dense data
   region underneath is FULLY hidden (a partial-opacity backdrop
   bled the bubble outline through the chip labels — see the user's
   "Auth" overlap report). */
oku-chart .okc-legend-backdrop {
  fill: var(--surface);
  fill-opacity: 1;
  stroke: var(--border-soft);
  stroke-width: 1;
}
oku-chart .okc-legend-chip:hover .okc-legend-bg,
oku-chart .okc-legend-chip:focus-visible .okc-legend-bg {
  fill: var(--accent-soft);
}
oku-chart .okc-legend-chip:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-radius: 4px;
}
oku-chart .okc-legend-chip.off .okc-legend-swatch { opacity: 0.25; }
oku-chart .okc-legend-chip.off .okc-legend { fill: var(--text-faint); text-decoration: line-through; }

/* Hover tooltip — chart-local; styling shares the kit's surface tokens
   but doesn't reuse the glossary tooltip controller (no "pin" affordance,
   no "Learn more" link — wrong shape for a data tooltip). Selector is
   intentionally bare-class so it works inside oku-chart, .bar-chart,
   .bar-chart-multi, treemap, funnel, and any future host that wants it. */
.okc-tooltip {
  /* `position: fixed` so the tooltip can render above the lightbox
     overlay when a chart is expanded. Position math in chrome.js
     uses viewport coords (dot.getBoundingClientRect()), so no host
     ancestor needs to be a positioning context.

     Width is clamped (min + max) so pin / unpin doesn't visually
     resize the card — the hint footer changes between states but
     the bounding box stays stable. */
  position: fixed;
  transform: translate(-50%, -100%);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: 6px;
  padding: 8px 28px 8px 10px;
  font-size: 12px;
  color: var(--text);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.12s ease;
  box-shadow: 0 6px 16px var(--shadow-strong);
  white-space: normal;
  word-wrap: break-word;
  z-index: 1100;
  min-width: 200px;
  max-width: 240px;
}
.okc-tooltip.visible { opacity: 1; }
.okc-tooltip .okc-tt-series {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--accent-strong);
  margin-bottom: 2px;
}
.okc-tooltip .okc-tt-label { font-weight: 600; }
.okc-tooltip .okc-tt-coords {
  color: var(--text-soft);
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  margin-top: 2px;
}
/* Key/value rows inside the rich tooltip — small two-column grid so
   "value", "share", "drop-off" lines stack cleanly. */
.okc-tooltip .okc-tt-kv {
  margin: 4px 0 0 0;
  display: grid;
  grid-template-columns: auto auto;
  column-gap: 10px;
  row-gap: 1px;
  font-size: 11.5px;
}
.okc-tooltip .okc-tt-kv dt {
  color: var(--text-soft);
  margin: 0;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 10.5px;
}
.okc-tooltip .okc-tt-kv dd {
  margin: 0;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  text-align: right;
}
/* The crossing read: one line per bar the vertical cursor passes
   through, so the list is as long as the comparison the cursor drew.
   Three columns — name, value, share — because a value column that
   sometimes carries a percentage after it stops lining up, and lining
   up is the whole reason a reader scans a list like this. The names
   keep their own case: these are the row labels off the chart, and
   uppercasing them makes them read as headings for the numbers rather
   than as the labels beside them. */
.okc-tooltip.okc-tt-wide { max-width: min(360px, 84vw); }
.okc-tooltip .okc-tt-kv.okc-tt-cross {
  /* The names give way, the numbers do not: a label can ellipsise and
     still be recognised, a value cannot. */
  grid-template-columns: minmax(0, 1fr) auto auto;
  column-gap: 12px;
  row-gap: 2px;
  max-height: 60vh;
  overflow-y: auto;
}
.okc-tooltip .okc-tt-cross .okc-tt-name {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  text-transform: none;
  letter-spacing: 0;
  font-size: 11.5px;
}
.okc-tooltip .okc-tt-cross .okc-tt-nametext {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.okc-tooltip .okc-tt-cross .okc-tt-val,
.okc-tooltip .okc-tt-cross .okc-tt-share { white-space: nowrap; }
.okc-tooltip .okc-tt-cross .okc-tt-swatch {
  width: 8px;
  height: 8px;
  border-radius: 2px;
  flex: none;
}
.okc-tooltip .okc-tt-cross .okc-tt-share {
  color: var(--text-faint);
  font-size: 10.5px;
}
/* The row under the pointer. It is one of the set, not a different
   kind of thing, so it gets weight — not a colour of its own. */
.okc-tooltip .okc-tt-cross .is-cursor {
  color: var(--text);
  font-weight: 700;
}

/* The chart wrap (or its outer host) needs relative positioning for
   the absolute tooltip to anchor correctly. */
.bar-chart,
.bar-chart-multi { position: relative; }
oku-chart .okc-legend {
  fill: var(--text);
  font: 500 11.5px 'Inter', sans-serif;
}

/* ============================================================
   <oku-diagram> — Mermaid renders
   ============================================================ */
oku-diagram {
  display: block;
  margin: 24px 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 16px;
  position: relative;
}
/* `overflow: hidden` clips any mermaid layout overspill (boxes that
   extend past the SVG's viewBox bounds — happens when Mermaid lays
   out content wider than the computed viewBox). Without this the
   overflowing box leaks into / past the column edge and reads as a
   "trim". The fullscreen lightbox shows the full diagram for cases
   where the inline view ends up clipping anything important. */
oku-diagram .okd-render { display: block; min-height: 60px; color: var(--text-soft); font-size: 13px; overflow-x: auto; overflow-y: hidden; }

/* Mermaid node hover.
   What this is NOT: a click affordance. Every node used to take
   `cursor: pointer` and an accent drop-shadow on hover, which is the
   vocabulary of a button — and nothing happens on click unless the
   author wrote mermaid's `click NodeId "url"`. The reader ends up
   probing a diagram for behaviour it does not have.
   What it IS: a reading aid. Hovering a node dims the rest of the
   graph and keeps the node with its incident edges and neighbours
   loud (see `_wireNeighborHighlight` below), which answers "what is
   this connected to" on a graph too dense to trace by eye. That is
   information, so it stays — with the arrow cursor, and without the
   glow. `cursor: pointer` is restored further down for the nodes
   mermaid itself marks `.clickable`. */
oku-diagram .okd-render svg .node,
oku-diagram .okd-render svg [class*="node"],
oku-diagram .okd-render svg .actor,
oku-diagram .okd-render svg .actor-box,
oku-diagram .okd-render svg .statediagram-state,
oku-diagram .okd-render svg .classGroup,
oku-diagram .okd-render svg .entityBox,
oku-diagram .okd-render svg .task,
oku-diagram .okd-render svg .task-rect,
oku-diagram .okd-render svg .pieCircle,
/* Mermaid pie's interactive shapes are the wedges, not the outer
   circle. The `slice` class is the actual fan-shaped <path>. */
oku-diagram .okd-render svg .slice,
oku-diagram .okd-render svg .pieTitleText + path,
oku-diagram .okd-render svg .section,
/* Gantt + journey sections come through as .section0 / .section1 /
   .section2 etc. (no hyphen), so a plain `.section` selector misses
   them. Match anything that starts with `section` followed by a
   digit. */
oku-diagram .okd-render svg [class*="section"],
oku-diagram .okd-render svg .mindmap-node,
/* Timeline emits .timeline-node + .taskWrapper / .lineWrapper /
   .eventWrapper — none of which match anything above. */
oku-diagram .okd-render svg .timeline-node,
oku-diagram .okd-render svg .taskWrapper,
oku-diagram .okd-render svg .note {
  cursor: default;
  transition: filter 0.15s ease;
}
/* Mermaid tags a node `.clickable` when the source binds a click to
   it. That is the only case where the pointer cursor is true. */
oku-diagram .okd-render svg .clickable,
oku-diagram .okd-render svg .clickable * {
  cursor: pointer;
}
/* The accent glow and the accent stroke that used to fire on plain
   hover are gone. On a flowchart the neighbour highlight below is the
   feedback, and it says something ("these are the connected nodes").
   On the diagram types that carry no graph structure — sequence, pie,
   gantt, timeline — hover now does nothing, which is the truth. */
oku-diagram .okd-render svg .clickable:hover {
  filter: drop-shadow(0 0 4px var(--accent));
}

/* Neighbor highlight — wired by `_wireNeighborHighlight` on flowchart
   nodes (those that carry data-id). When any node is active, the SVG
   gets data-okd-has-active="1"; the hovered node, its incident edges,
   and the neighbor nodes get data-okd-active="1" individually. CSS
   dims everything else so the active subgraph reads loud while the
   rest fades back. Falls back gracefully on diagram types without
   data-id (sequence, gantt, etc.) — neither attribute appears, the
   hover-only rule above still applies. */
oku-diagram .okd-render svg[data-okd-has-active] .node:not([data-okd-active]),
oku-diagram .okd-render svg[data-okd-has-active] .flowchart-link:not([data-okd-active]),
oku-diagram .okd-render svg[data-okd-has-active] .edge-thickness-normal:not([data-okd-active]),
oku-diagram .okd-render svg[data-okd-has-active] .edgeLabel:not([data-okd-active]),
oku-diagram .okd-render svg[data-okd-has-active] .edgeLabels > g:not([data-okd-active]) {
  /* Recede, don't disappear. 0.25 put dimmed labels below the
     readability floor on the dark theme — the reader could no longer
     see the rest of the graph they were navigating. The active subgraph
     is carried by the accent stroke and the 2–2.5px stroke-width above;
     the dim only has to push the rest back one layer. */
  opacity: 0.55;
  transition: opacity 0.18s ease;
}
oku-diagram .okd-render svg .node[data-okd-active] > rect,
oku-diagram .okd-render svg .node[data-okd-active] > circle,
oku-diagram .okd-render svg .node[data-okd-active] > polygon,
oku-diagram .okd-render svg .node[data-okd-active] > path {
  stroke: var(--accent) !important;
  stroke-width: 2px;
}
oku-diagram .okd-render svg .flowchart-link[data-okd-active],
oku-diagram .okd-render svg path.edge-thickness-normal[data-okd-active] {
  stroke: var(--accent) !important;
  stroke-width: 2.5px;
}
/* Keyboard focus ring on flowchart nodes (we add tabindex=0 to them so
   keyboard users can step through the graph). Drop the default browser
   outline since it floats outside the SVG's path; use a visible
   inner-stroke instead. */
oku-diagram .okd-render svg .node:focus {
  outline: none;
}
oku-diagram .okd-render svg .node:focus > rect,
oku-diagram .okd-render svg .node:focus > circle,
oku-diagram .okd-render svg .node:focus > polygon,
oku-diagram .okd-render svg .node:focus > path {
  stroke: var(--accent) !important;
  stroke-width: 2.5px;
}
oku-diagram .okd-render svg {
  /* width:100% on a fixed-aspect-ratio SVG would stretch a tall
     portrait diagram (mindmap, vertical state machine) into a
     ~2000px scroll-wall. Use auto+max-* instead so the browser
     fits the SVG inside BOTH dimensions while preserving the
     viewBox aspect ratio — wide flowcharts hit max-width first,
     tall diagrams hit max-height first. The lightbox (below)
     drops the height cap so fullscreen reads at full size. */
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 70vh;
  margin: 0 auto;
}
/* Clamp text size inside Mermaid SVGs so state / mindmap diagrams
   don't render their few labels at 30px+. Mermaid's `fontSize`
   init option doesn't propagate to every diagram type; the CSS
   floor catches the rest. Lightbox view (below) lifts the cap so
   the fullscreen reader can read at full size. */
oku-diagram .okd-render svg text,
oku-diagram .okd-render svg foreignObject,
oku-diagram .okd-render svg foreignObject span {
  font-size: 13px !important;
}
/* Fullscreen lightbox: let the diagram grow to fill the pan/zoom
   stage. `.okt-lightbox-pz-inner > *` sets width:auto so images shrink
   to their natural size; a diagram host has no natural size (its SVG
   is width:100% of the host), so auto collapses it to a few hundred
   px in the middle of a 1700px stage. Pin the host to the stage. */
.okt-lightbox-pz-inner > oku-diagram.okd-fullscreen {
  width: 100%;
  max-height: 100%;
}
.okt-lightbox-pz oku-diagram .okd-render svg,
.okt-lightbox-content oku-diagram .okd-render svg {
  width: 100%;
  height: auto;
  max-width: none;
  max-height: 100%;
}
/* Drop the inline clamp inside the lightbox — the reader opened the
   fullscreen view explicitly, so text should scale with the SVG. */
.okt-lightbox-pz oku-diagram .okd-render svg text,
.okt-lightbox-content oku-diagram .okd-render svg text,
.okt-lightbox-pz oku-diagram .okd-render svg foreignObject,
.okt-lightbox-content oku-diagram .okd-render svg foreignObject {
  font-size: inherit !important;
}
oku-diagram .okd-fallback {
  background: var(--surface-2);
  padding: 12px;
  border-radius: 6px;
  font-size: 12px;
  white-space: pre-wrap;
  overflow-x: auto;
}
oku-diagram .okd-caption {
  display: block;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border-soft);
  font-size: 12.5px;
  color: var(--text-soft);
  text-align: center;
  font-style: italic;
}

/* ============================================================
   <oku-snippet> — editable HTML/CSS/JS playground
   ============================================================ */
oku-snippet {
  display: block;
  margin: 24px 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  overflow: hidden;
}
oku-snippet .okt-snippet-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: var(--surface-2);
  border-bottom: 1px solid var(--border);
  padding: 8px 14px;
  font-size: 12px;
  color: var(--text-soft);
}
oku-snippet .okt-snippet-label {
  font-weight: 600;
  color: var(--text);
  min-width: 0;
  overflow-wrap: anywhere;
}
oku-snippet .okt-snippet-reset {
  /* The label is the elastic half of this header; the button is not.
     Without this the button is the thing pushed off the right edge. */
  flex: none;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 4px;
  min-height: 24px;
  padding: 3px 10px;
  font-size: 11px;
  color: var(--text-soft);
  cursor: pointer;
  font-family: inherit;
}
oku-snippet .okt-snippet-reset:hover { background: var(--surface); color: var(--text); border-color: var(--line-strong); }
oku-snippet .okt-snippet-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  min-height: 320px;
}
/* When nested inside the example-pair render column, the grid is
   tight (~half content-width). Stack editor + preview unconditionally
   in that context so neither gets squashed to a sliver. */
.example-output oku-snippet .okt-snippet-body {
  grid-template-columns: minmax(0, 1fr);
  min-height: 400px;
}
.example-output oku-snippet .okt-snippet-editor,
.example-output oku-snippet .okt-snippet-preview {
  min-height: 200px;
}
/* Editor pane: a `<textarea>` overlaid on top of a Prism-highlighted
   shadow `<pre><code>`. The textarea has transparent text + visible
   caret; the shadow renders the colored tokens directly below. Both
   share the exact same font/size/line-height/padding so glyphs align
   per-pixel with what the user types. The textarea drives layout
   (so resize: vertical works); the shadow is absolutely positioned
   over the wrap and scroll-synced from the textarea. */
oku-snippet .okt-snippet-editor-wrap {
  position: relative;
  background: var(--surface);
  border-right: 1px solid var(--border);
  overflow: hidden;
}
oku-snippet .okt-snippet-editor,
oku-snippet .okt-snippet-editor-shadow {
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12.5px;
  line-height: 1.55;
  padding: 12px 14px;
  margin: 0;
  border: 0;
  letter-spacing: normal;
  tab-size: 2;
  white-space: pre;
  word-spacing: normal;
}
oku-snippet .okt-snippet-editor {
  display: block;
  width: 100%;
  min-height: 200px;
  background: transparent;
  color: transparent;
  caret-color: var(--text);
  resize: vertical;
  outline: none;
  position: relative;
  z-index: 1;
  /* Selection stays visible even with transparent text. */
  -webkit-text-fill-color: transparent;
}
oku-snippet .okt-snippet-editor::selection { background: var(--accent-soft); color: transparent; }
oku-snippet .okt-snippet-editor-shadow {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: auto;
  /* Shadow gets the same scroll metrics as the textarea via JS sync. */
  background: transparent;
  color: var(--text);
}
oku-snippet .okt-snippet-editor-shadow code {
  display: block;
  background: transparent;
  font: inherit;
  padding: 0;
  line-height: inherit;
  white-space: pre;
}
oku-snippet .okt-snippet-editor-wrap:focus-within { background: var(--surface-2); }
oku-snippet .okt-snippet-preview {
  background: white;
  border: 0;
  width: 100%;
  min-height: 200px;
}
@media (max-width: 700px) {
  oku-snippet .okt-snippet-body { grid-template-columns: minmax(0, 1fr); }
  oku-snippet .okt-snippet-editor-wrap { border-right: 0; border-bottom: 1px solid var(--border); }
}

/* ============================================================
   Pagefind search — button + modal
   ============================================================ */
/* The search button is positioned by .okt-chrome-cluster and needs no
   rule of its own. It used to carry right: 136px plus a pair of
   `:has()` rules that pulled it to 76px when the warning indicator was
   absent — where .width-toggle already was. */

.search-modal {
  position: fixed;
  inset: 0;
  z-index: 400;
  display: none;
  align-items: flex-start;
  justify-content: center;
  padding-top: 8vh;
}
.search-modal.open { display: flex; }
.search-modal .search-backdrop {
  position: absolute;
  inset: 0;
  background: var(--scrim);
  backdrop-filter: blur(2px);
}
.search-modal .search-panel {
  position: relative;
  width: 92%;
  max-width: 620px;
  max-height: 70vh;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: 14px;
  box-shadow: 0 24px 64px rgba(0,0,0,0.25);
  overflow: hidden;
  animation: search-in 0.15s ease;
}
@keyframes search-in {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: translateY(0); }
}
.search-header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--border);
}
.search-input {
  flex: 1;
  border: 0;
  background: transparent;
  font: 500 16px 'Inter', sans-serif;
  color: var(--text);
  outline: none;
  padding: 4px 0;
}
.search-input::placeholder { color: var(--text-faint); }
.search-close {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 4px 8px;
  font: 600 11px 'JetBrains Mono', monospace;
  color: var(--text-soft);
  cursor: pointer;
}
.search-close:hover { background: var(--accent-soft); color: var(--accent-strong); }
.search-status {
  padding: 8px 16px;
  font-size: 12px;
  color: var(--text-soft);
  border-bottom: 1px solid var(--border);
}
.search-status:empty { display: none; }
.search-status code {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  background: var(--surface-2);
  padding: 1px 5px;
  border-radius: 3px;
  color: var(--text);
}
.search-results {
  list-style: none;
  margin: 0;
  padding: 0;
  overflow-y: auto;
}
.search-result a {
  display: block;
  padding: 12px 16px;
  border-bottom: 1px solid var(--border-soft);
  color: var(--text);
  text-decoration: none;
}
.search-result a:hover { background: var(--surface-2); }
.search-result-title {
  font-weight: 600;
  color: var(--accent-strong);
  margin-bottom: 4px;
}
.search-result-excerpt {
  font-size: 13px;
  line-height: 1.45;
  color: var(--text-soft);
}
.search-result mark {
  background: var(--accent-soft);
  color: var(--accent-strong);
  padding: 0 2px;
  border-radius: 2px;
}

/* ============ Example pair (P3) ============
 * Two-column code+render layout used to document primitives in
 * reference.json. Each side gets a small label badge so the reader
 * knows which is source vs result. Stacks vertically below 900px so
 * the code stays readable on narrow viewports.
 * --------------------------------------------------------------- */
.example-pair {
  display: grid;
  /* Output-dominant 2:3. The rendered block is the thing the reader
     came to see; the JSON beside it is reference. A 50/50 split let the
     source out-shout its own result (worst on the charts page, where a
     chart should lead and its payload should footnote). */
  grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
  gap: 12px;
  /* Each column lays out from the top — the rendered block keeps
     its natural height instead of stretching to match the source
     column. Was stretch; the visual side ended up edge-anchored
     (bars / kpi tiles spread far apart vertically). */
  align-items: start;
  margin: 18px 0 26px 0;
  border: 1px solid var(--border-soft);
  border-radius: 12px;
  padding: 12px;
  background: var(--surface);
  box-shadow: 0 1px 3px var(--shadow-soft);
}
.example-pair .example-title {
  grid-column: 1 / -1;
  font: 600 12px 'Inter', sans-serif;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--accent-strong);
}
.example-pair .example-code,
.example-pair .example-output {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.example-pair .example-col-label {
  font: 600 10.5px 'Inter', sans-serif;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-soft);
  margin-bottom: 6px;
}
/* Each column's content sits at the top, natural height. The label
   has a small bottom margin; the body (second flex child) explicitly
   resets its top margin so the flex column gap doesn't double up
   against the label. Stretch was retired — visual primitives rendered
   edge-anchored (bars + tiles ballooned vertically). */
/* Source pre + render-side first child both flush against the column
   label so their box-TOPS line up regardless of what the render is.
   This is the universal rule the user has been asking for: when
   render is a chart card, callout, tldr, kpi-grid, compare-grid,
   table, info-tip, annotated-code, live-snippet, code pre, etc. —
   the visible top edge of the box matches the visible top edge of
   the source <pre> box. */
.example-pair .example-code > *:not(.example-col-label),
.example-pair .example-output > *:not(.example-col-label) {
  margin-top: 0 !important;
  margin-bottom: 0 !important;
}
@media (max-width: 900px) {
  .example-pair { grid-template-columns: minmax(0, 1fr); }
}

/* ============ Copy region ============
 * A block the reader is meant to take away, optionally paired with the
 * text it replaces. The two regions share one card because they are one
 * instruction, not two quotations that happen to be adjacent.
 * --------------------------------------------------------------- */
.okt-copy {
  margin: 22px 0;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  box-shadow: 0 1px 3px var(--shadow-soft);
  overflow: hidden;
}
/* The card's own surface, not the recessed one. Grey means exactly one
   thing inside this card — "this is the text being replaced" — and a
   head band sharing it made the card read as three stripes of
   ambiguous rank instead of a caption over a pair. */
.okt-copy-head {
  padding: 9px 14px;
  border-bottom: 1px solid var(--border);
  font: 600 12.5px 'Inter', sans-serif;
  color: var(--text-soft);
}
.okt-copy-region + .okt-copy-region { border-top: 1px solid var(--border); }
/* The old text is quoted, not offered. It sits on the recessed surface
   so the region below it reads as the one to take away. */
.okt-copy-was { background: var(--surface-2); }

.okt-copy-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;
  padding: 8px 12px 0 14px;
}
/* A lone region has no role word, and space-between would then park the
   buttons on the left, under the body text instead of over its edge. */
.okt-copy-bar > .okt-copy-actions:only-child { margin-left: auto; }
.okt-copy-role {
  font: 600 10.5px 'Inter', sans-serif;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-faint);
}
.okt-copy-actions { display: inline-flex; gap: 6px; }
.okt-copy-btn {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 4px 9px;
  font: 500 11.5px 'Inter', sans-serif;
  color: var(--text-soft);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.okt-copy-btn:hover { color: var(--accent-strong); border-color: var(--accent); background: var(--accent-soft); }
.okt-copy-btn.is-copied { color: var(--success); border-color: var(--success); background: var(--success-soft); }
.okt-copy-btn.is-failed { color: var(--danger);  border-color: var(--danger);  background: var(--danger-soft); }
.okt-copy-icon { display: inline-flex; }
.okt-copy-icon svg { width: 12px; height: 12px; }

.okt-copy-body { padding: 6px 14px 14px 14px; }
.okt-copy-body > *:first-child { margin-top: 0; }
.okt-copy-body > *:last-child { margin-bottom: 0; }

/* Removed and added words. Colour AND a line, because a diff that a
   reader who cannot separate the two hues can still read is a diff;
   one keyed on hue alone is a decoration they have to take on trust. */
.okt-copy-del {
  background: var(--danger-soft);
  color: var(--text);
  text-decoration: line-through;
  text-decoration-color: var(--danger);
  border-radius: 3px;
  padding: 0 1px;
}
.okt-copy-ins {
  background: var(--success-soft);
  color: var(--text);
  text-decoration: underline;
  text-decoration-color: var(--success);
  text-underline-offset: 2px;
  border-radius: 3px;
  padding: 0 1px;
}

/* ============ Section / heading card boundaries (P3) ============
 * Card-style surround for reference subsections so the eye can
 * trace where one primitive ends and the next begins. Only applies
 * inside [data-page-id="reference"] so other pages don't pick up
 * the chrome by accident.
 * --------------------------------------------------------------- */
[data-page-id="reference"] section > h2 + *,
[data-page-id="reference"] section section {
  /* No double-card; outer section already gets bookend spacing. */
}
[data-page-id="reference"] section section,
[data-page-id="reference"] section[id] {
  margin-top: 28px;
}
[data-page-id="reference"] section section > h3 {
  margin-top: 0;
}
[data-page-id="reference"] .ref-card {
  border: 1px solid var(--border-soft);
  border-radius: 12px;
  padding: 18px 20px 4px;
  background: var(--surface);
  margin: 22px 0;
}
[data-page-id="reference"] .ref-card > h3:first-child,
[data-page-id="reference"] .ref-card > h4:first-child {
  margin-top: 0;
}

/* ============ Tier 3 charts (sankey / network / scatter-matrix / parallel-coordinates) ============ */

/* Sankey — left→right flow ribbons + column-stacked node rectangles. */
oku-chart .okc-sankey .okc-sankey-link {
  transition: fill-opacity 0.18s ease;
  cursor: pointer;
}
oku-chart .okc-sankey .okc-sankey-link:hover { fill-opacity: 0.62; }
oku-chart .okc-sankey .okc-sankey-node { cursor: pointer; }
oku-chart .okc-sankey .okc-sankey-label {
  fill: var(--text);
  font: 500 11px 'Inter', sans-serif;
  dominant-baseline: middle;
}

/* Network — node-link diagram with force-relaxed layout. */
oku-chart .okc-network .okc-network-edge {
  stroke: var(--text-faint);
  stroke-width: 1;
  opacity: 0.55;
}
oku-chart .okc-network .okc-network-node {
  cursor: grab;
  transition: filter 0.15s ease;
  touch-action: none;
}
oku-chart .okc-network .okc-network-node:hover circle {
  filter: brightness(1.1);
  stroke: var(--accent-strong);
  stroke-width: 2;
}
oku-chart .okc-network .okc-network-node.okc-dragging {
  cursor: grabbing;
}
oku-chart .okc-network .okc-network-node.okc-dragging circle {
  filter: brightness(1.15);
  stroke: var(--accent-strong);
  stroke-width: 2;
}
oku-chart .okc-network .okc-network-label {
  fill: var(--text);
  font: 500 10.5px 'Inter', sans-serif;
  stroke: var(--bg);
  stroke-width: 3;
  stroke-linejoin: round;
  paint-order: stroke fill;
}

/* Scatter-matrix — N×N grid of mini scatters. */
oku-chart .okc-scatter-matrix .okc-sm-cell {
  fill: var(--surface);
  stroke: var(--border);
  stroke-width: 0.5;
  cursor: pointer;
  transition: fill 0.12s ease, stroke 0.12s ease;
}
oku-chart .okc-scatter-matrix .okc-sm-cell:hover,
oku-chart .okc-scatter-matrix .okc-sm-cell:focus-visible {
  fill: var(--surface-2);
  stroke: var(--accent);
  outline: none;
}
/* Diagonal cells label the axis — quieter background since no
   data plot. */
oku-chart .okc-scatter-matrix .okc-sm-cell-diag {
  fill: var(--surface-2);
  cursor: default;
}
oku-chart .okc-scatter-matrix .okc-sm-cell-diag:hover {
  fill: var(--surface-2);
  stroke: var(--border);
}
oku-chart .okc-scatter-matrix .okc-sm-label {
  fill: var(--text);
  font: 600 12px 'Inter', sans-serif;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
/* Correlation coefficient chip in each off-diagonal cell. Faint
   when weak (|r| < 0.7), accent-strong when the relationship
   reaches the conventional "strong" threshold so the matrix
   reads as a heatmap of strength at a glance. */
oku-chart .okc-scatter-matrix .okc-sm-corr {
  fill: var(--text-faint);
  font: 500 9.5px 'JetBrains Mono', ui-monospace, monospace;
  pointer-events: none;
}
oku-chart .okc-scatter-matrix .okc-sm-corr.strong {
  fill: var(--accent-strong);
  font-weight: 700;
}

/* Parallel-coordinates — multi-axis polylines. */
oku-chart .okc-parcoord .okc-parcoord-axis {
  stroke: var(--text-faint);
  stroke-width: 1;
}
oku-chart .okc-parcoord .okc-parcoord-label {
  fill: var(--text-soft);
  font: 600 10.5px 'Inter', sans-serif;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
oku-chart .okc-parcoord .okc-parcoord-line {
  fill: none;
  stroke-width: 3.6;
  stroke-opacity: 0.7;
  transition: stroke-opacity 0.15s ease, stroke-width 0.15s ease;
  cursor: pointer;
}
/* When ANY line is being hovered, fade the rest so the highlighted
   record stands out instead of being one of N at the same opacity. */
oku-chart .okc-parcoord:has(.okc-parcoord-line:hover) .okc-parcoord-line {
  stroke-opacity: 0.15;
}
oku-chart .okc-parcoord .okc-parcoord-line:hover,
oku-chart .okc-parcoord .okc-parcoord-line.hovered {
  stroke-opacity: 1 !important;
  stroke-width: 5;
}


/* ============ Content width ============
 * Visual components fill the reader's selected content width, set
 * per the body[data-content-width] value and the wide-screen
 * @media bumps near the top of this file.
 *
 * Running prose does not. A fully symmetric model was tried first —
 * every block kind, prose included, filling --content-width — on the
 * grounds that the width toggle should act uniformly. It does act
 * uniformly, and the cost lands on the default: at 'comfortable' on
 * a 1440px window, body paragraphs ran 77-83 characters per line
 * against a readable range of 45-75.
 *
 * The cap is expressed in `ch` and re-declared per mode, so the
 * toggle still moves prose in the same direction — narrow 58ch,
 * comfortable 66ch, wide 78ch, max uncapped. A reader who wants
 * full-bleed prose still has 'max'.
 * ---------------------------------------------------------------- */
main .okt-table-wrap,
main .compare-grid,
main .kpi-grid,
main .step-cards,
main .bar-chart,
main .bar-chart-multi,
main oku-chart,
main oku-diagram,
main oku-snippet,
main oku-annotated-code,
main pre,
main .example-pair {
  max-width: 100%;
}

/* NOTHING here caps a block below --content-width, and that is the
   rule: one column, one right edge, for prose, callouts, the TL;DR
   panel, code, tables and diagrams alike.
   `--prose-width` is declared in :root and per mode, and the kit does
   not apply it. That looks like dead weight and is not — it is there
   for a caller that wants a per-block line-length cap.
   The history is worth keeping, because it is easy to reopen: body
   prose runs long at the comfortable width (a 77-83 character median
   when the cap was proposed), and shortening it IS better for the
   paragraph in isolation. It was tried. What the reader sees is not
   one paragraph though, it is a column where the text stops ~200px
   short of the cover, the code block and the diagram — and a right
   edge that moves from block to block reads as a rendering fault,
   which costs more than the long line saves.
   The measure belongs to --content-width, and the reader shortens it
   with the width toggle, which moves every block together.
   Pinned by test_presentation_measure.py — every block in a section
   shares one right edge, at every width. */

/* Running prose is justified, and the hyphenation is not optional.
   Justify alone at this measure opens rivers — the browser can only
   stretch word spaces, so one long word at the end of a line drags a
   whole line apart. `hyphens: auto` gives it break points and the
   inter-word spacing stays close to the ragged-right value.
   Scope is deliberately a list, not `main`: a justified table cell, axis
   label, chip or caption is a short string stretched to a box edge,
   which looks broken. Only blocks that run several lines of sentence
   text are in. Code is never justified — `pre` keeps its own spacing. */
main > section > p:not(.okt-card-title),
main > section > ul li,
main > section > ol li,
main > section > blockquote p,
main .callout p:not(.callout-title):not(.okt-card-title),
main .callout li,
main .tldr li,
main .insight p:not(.okt-card-title),
main .example-pair p:not(.okt-card-title):not(.callout-title):not(.bar-chart-title) {
  text-align: justify;
  -webkit-hyphens: auto;
  hyphens: auto;
  /* Cap the damage on the pathological line: a long unbreakable token
     (a URL, an identifier) can still force one over-stretched line, and
     without a limit the browser will take the word spaces anywhere. */
  text-justify: inter-word;
}
/* A single-line block needs no exception: `justify` leaves the LAST line
   of a block ragged, and a one-line paragraph is all last line. Nothing
   short gets stretched. */

/* The one block that broke that rule without anyone writing a line of
   CSS: the kit never styled <blockquote>, so it took the UA default
   `margin: 1em 40px` and ended 40px short of every sibling. The left
   indent is the convention and stays; the right inset is what put a
   step in the column edge. Geometry only — the kit still declares no
   colour, border or type for a quote, which is a separate gap. */
main blockquote {
  margin: 1em 0 1em 40px;
}


/* ============ Tier 3 — chord + geo ============ */
oku-chart .okc-chord .okc-chord-arc {
  stroke: var(--bg);
  stroke-width: 1.5;
  transition: filter 0.18s ease;
}
oku-chart .okc-chord .okc-chord-arc:hover { filter: brightness(1.05); }
oku-chart .okc-chord .okc-chord-ribbon {
  transition: fill-opacity 0.18s ease;
  cursor: pointer;
}
oku-chart .okc-chord .okc-chord-ribbon:hover { fill-opacity: 0.55; }
oku-chart .okc-chord .okc-chord-label {
  fill: var(--text);
  font: 600 11px 'Inter', sans-serif;
}

oku-chart .okc-geo .okc-geo-cell-on rect {
  stroke: var(--border);
  stroke-width: 0.5;
  cursor: pointer;
  transition: filter 0.15s ease, stroke-width 0.15s ease;
}
oku-chart .okc-geo .okc-geo-cell-on:hover rect,
oku-chart .okc-geo .okc-geo-cell-on:focus-visible rect {
  filter: brightness(1.1);
  stroke: var(--accent-strong);
  stroke-width: 1.5;
}
oku-chart .okc-geo .okc-geo-cell-off {
  fill: var(--surface-2);
  stroke: var(--border-soft);
  stroke-width: 0.5;
}
oku-chart .okc-geo .okc-geo-code {
  fill: var(--text);
  font: 700 11.5px 'JetBrains Mono', ui-monospace, monospace;
  letter-spacing: 0.04em;
}
oku-chart .okc-geo .okc-geo-value {
  fill: var(--text);
  font: 500 10px 'JetBrains Mono', ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
}
oku-chart .okc-geo .okc-geo-code-off {
  fill: var(--text-faint);
  font: 500 10px 'JetBrains Mono', ui-monospace, monospace;
}
oku-chart .okc-geo .okc-geo-legend-tick {
  fill: var(--text-soft);
  font: 500 10.5px 'JetBrains Mono', ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
}

/* ============ Arc diagram — nodes on baseline, arcs above ============ */
oku-chart .okc-arc-diagram .okc-arc-link {
  cursor: pointer;
  transition: stroke-opacity 0.15s ease, stroke-width 0.15s ease;
}
oku-chart .okc-arc-diagram .okc-arc-link:hover,
oku-chart .okc-arc-diagram .okc-arc-link:focus-visible {
  stroke-opacity: 0.95;
  outline: none;
}
oku-chart .okc-arc-diagram .okc-arc-node {
  cursor: pointer;
  transition: r 0.15s ease, stroke-width 0.15s ease;
}
oku-chart .okc-arc-diagram .okc-arc-node:hover,
oku-chart .okc-arc-diagram .okc-arc-node:focus-visible {
  stroke-width: 2.5;
  outline: none;
}
oku-chart .okc-arc-diagram .okc-arc-label {
  fill: var(--text);
  font: 500 10.5px 'Inter', sans-serif;
  stroke: var(--bg);
  stroke-width: 3;
  stroke-linejoin: round;
  paint-order: stroke fill;
}

/* ============ Range bar — [low, high] interval per row ============ */
oku-chart .okc-range-bar .okc-range-band {
  cursor: pointer;
  transition: fill-opacity 0.15s ease;
}
oku-chart .okc-range-bar .okc-range-band:hover,
oku-chart .okc-range-bar .okc-range-band:focus-visible {
  fill-opacity: 0.70;
  outline: none;
}
oku-chart .okc-range-bar .okc-range-row-label {
  fill: var(--text);
  font: 500 11.5px 'Inter', sans-serif;
}
oku-chart .okc-range-bar .okc-range-readout {
  fill: var(--text-soft);
  font: 500 10.5px 'JetBrains Mono', ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
}

/* ============ Pareto — bars + cumulative-% line ============ */
oku-chart .okc-pareto .okc-pareto-bar {
  cursor: pointer;
  transition: fill-opacity 0.15s ease;
}
oku-chart .okc-pareto .okc-pareto-bar:hover,
oku-chart .okc-pareto .okc-pareto-bar:focus-visible {
  fill-opacity: 0.95;
  outline: none;
}
oku-chart .okc-pareto .okc-pareto-line {
  stroke-linejoin: round;
  stroke-linecap: round;
  pointer-events: none;
}
oku-chart .okc-pareto .okc-pareto-line-dot {
  cursor: pointer;
}
oku-chart .okc-pareto .okc-pareto-cat-label {
  fill: var(--text-soft);
  font: 500 10.5px 'Inter', sans-serif;
}
oku-chart .okc-pareto .okc-pareto-80-label {
  font: 500 10px 'JetBrains Mono', ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}


/* ============ Chart tooltip pin-hint + pinned state ============ */
.okc-tooltip .okc-tt-pin-hint {
  display: block;
  margin-top: 4px;
  font-size: 9.5px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-faint);
  opacity: 0.7;
}
.okc-tooltip.pinned {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-soft), 0 6px 16px var(--shadow-strong);
  pointer-events: auto;
}
/* When pinned the click-to-pin hint becomes the Esc reminder. The
   close button (top-right) is the primary close affordance. */
.okc-tooltip.pinned .okc-tt-pin-hint {
  /* Same line height as unpinned so the card height stays stable. */
  font-size: 9.5px;
}
.okc-tooltip.pinned .okc-tt-pin-hint::before {
  content: 'Esc / × to close';
}
.okc-tooltip.pinned .okc-tt-pin-hint::after {
  content: '';
}
.okc-tooltip.pinned .okc-tt-pin-hint {
  /* hide the original "click to pin" text by collapsing its content
     into a span that's transparent — the ::before replaces it. */
  color: transparent;
  position: relative;
}
.okc-tooltip.pinned .okc-tt-pin-hint::before {
  position: absolute;
  left: 0; top: 0;
  color: var(--text-faint);
  white-space: nowrap;
}
/* Close × button — only visible when the tooltip is pinned. */
.okc-tooltip .okc-tt-close {
  position: absolute;
  top: 4px;
  right: 6px;
  width: 18px;
  height: 18px;
  display: none;
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
  color: var(--text-faint);
  font-size: 16px;
  line-height: 1;
  border-radius: 4px;
  align-items: center;
  justify-content: center;
}
.okc-tooltip.pinned .okc-tt-close {
  display: inline-flex;
}
.okc-tooltip .okc-tt-close:hover,
.okc-tooltip .okc-tt-close:focus-visible {
  background: var(--surface-2);
  color: var(--text);
  outline: none;
}
