/*
 * Gimorra landing page — the Terminal Design System.
 *
 * The console's DEFAULT_APPEARANCE, reproduced: `copperplate` in dark, `porcelain` in
 * light, accent dial `green` (apps/dashboard/src/lib/themes.ts:133). Every hex below is
 * lifted verbatim from that palette's --ds-* block in apps/dashboard/src/app/globals.css;
 * a token the palette does not restate falls back to the reference base, exactly as a
 * palette-as-a-diff does there.
 *
 * Two derivations are carried over rather than hand-rolled, because they are what makes
 * the console legible rather than decoration:
 *   · the DARK LEGIBILITY LIFT (globals.css:1071) — every surface, rule and neutral fill
 *     mixed a few percent toward the palette's OWN ink, so the ladder opens rather than
 *     sliding up as a block, and it stays on-hue (copperplate warms).
 *   · the LIGHT ACCENT-INK RULE (globals.css:885) — on a pale surface the raw dial hue is
 *     a wash, so the text form deepens 58% toward the ink and the FILL 90%, each on its
 *     own axis rather than drifting grey.
 *
 * The ISOMETRIC PLATES are the reference design's own figures, pasted unmodified, and they
 * stroke themselves from `--og-*`. Those names are published as an adapter at the foot of
 * this block - derived from the palette, never restating it - so a figure lifted out of the
 * reference arrives on the console's colours rather than on the reference's `terminal`
 * register. The palette here is the console's and stays the console's.
 *
 * Mode follows the visitor's system setting until the nav's switch overrides it, and the
 * override is one attribute: every token below resolves through `light-dark()`, so the used
 * `color-scheme` of `:root` — `light dark` by default, pinned by `[data-theme]` — is the only
 * switch there is. One declaration per token carries BOTH palettes, so a palette can never be
 * half-restated: a token that light does not move is written once, not twice.
 * `theme.js` sets the attribute in the <head>, before the first paint.
 */

:root {
  /* ── copperplate (dark, with the legibility lift) / porcelain (light) ────── */
  /* Light needs no lift — those palettes already open their ladder — so it is the flat hex
     in each pair and dark is the mix toward its own ink. */
  --text-1: light-dark(#33302b, #ead9c0);
  --bg-app: light-dark(#efece8, color-mix(in oklab, #14100b 97.5%, var(--text-1)));
  --bg-terminal: light-dark(#fbf9f6, color-mix(in oklab, #1a150e 96%, var(--text-1)));
  --bg-raised: light-dark(#ffffff, color-mix(in oklab, #241d14 95.5%, var(--text-1)));
  --bg-inset: light-dark(#f4f1ec, color-mix(in oklab, #100c08 95.5%, var(--text-1)));
  --bg-hover: light-dark(#e9e5df, color-mix(in oklab, #2c2418 95%, var(--text-1)));
  --border-1: light-dark(#ddd8d0, color-mix(in oklab, #332917 91%, var(--text-1)));
  --border-2: light-dark(#c2bab0, color-mix(in oklab, #54432a 92.5%, var(--text-1)));
  /* The two mid-text steps take the SAME 78% / 55% walk toward the ink in both modes, so only
     the neutral they start from switches. */
  --text-2: color-mix(in oklab, light-dark(#6b655c, #b59c7a) 78%, var(--text-1));
  --text-dim: color-mix(in oklab, light-dark(#a39c90, #77664c) 55%, var(--text-1));

  /* Semantic hues, from the palette's own block. */
  --green: light-dark(#3d8b3d, #a4c26f);
  --red: light-dark(#c53a3a, #e0705a);
  --yellow: light-dark(#a37414, #d8a24a);
  --orange: light-dark(#bb6a1e, #cf8236);
  --blue: light-dark(#3565c0, #8aa8bd);
  /* copperplate declares no ansi cyan, so the phase glyphs take the reference base's
     badge teal — a real token, and the one hue that separates cleanly from `★` green. */
  --cyan: light-dark(#175f68, #7fd6e4);

  /* Accent dial: `green`. Three forms — the raw hue for a glow, a FILL for buttons and
     nodes, an FG for text and rules — because light mode moves them by different amounts:
     the accent-ink rule deepens the text form 58% toward the ink and the fill 90%, each on
     its own axis rather than drifting grey. Dark leaves both at the raw dial. */
  --accent: #78d670;
  --accent-fg: light-dark(color-mix(in oklab, var(--accent) 58%, var(--text-1)), var(--accent));
  --accent-fill: light-dark(color-mix(in oklab, var(--accent) 90%, var(--text-1)), var(--accent));
  /*
   * The hover halo, and the ONE place its strength is set - the nav lights its mark, its
   * version, its sections and both of its buttons from this token, so they cannot drift apart.
   *
   * A COLOR, not a shadow: `light-dark()` is defined for colors only, so the two themes are
   * chosen here and every rule composes its own `0 0 Npx var(--accent-glow)` around it. Weaker
   * in light mode on purpose - a glow is light thrown onto a ground, and the same strength that
   * reads as a lit terminal on the dark page reads as a green smudge on the white one.
   *
   * Built from raw `--accent`, never `--accent-fg`: the foreground mix is pulled toward the ink
   * so text stays legible ON the page, and a halo pulled the same way is a grey blur.
   */
  --accent-glow: light-dark(
    color-mix(in oklab, var(--accent) 38%, transparent),
    color-mix(in oklab, var(--accent) 58%, transparent)
  );
  --accent-soft: light-dark(
    color-mix(in oklab, var(--accent) 16%, var(--bg-terminal)),
    color-mix(in oklab, var(--accent) 22%, var(--bg-terminal))
  );

  /*
   * The button lift. The two TONES are separate tokens because `light-dark()` is defined for
   * colors only - the same rule `--accent-glow` above carries - so wrapping a whole shadow in
   * it is invalid at computed-value time and resolves `box-shadow` to `none`, silently. The
   * geometry around them is a token too: an unregistered custom property is a token stream
   * substituted before `box-shadow` parses, so the `light-dark()` calls still land in colour
   * positions. Use `var(--lift)`; the one site that needs more appends to it.
   *
   * Two passes rather than one: the wide one carries the distance and the tight one carries
   * the edge, which is what keeps a 1px-bordered control from reading as a sticker. Deeper
   * in dark mode, because a shadow is an absence of light and there is less to take away.
   */
  --lift-far: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.34));
  --lift-near: light-dark(rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.22));
  --lift: 0 4px 8px var(--lift-far), 0 4px 4px var(--lift-near);



  /*
   * ── the plates' tokens, DERIVED from the palette above ────────────────────
   *
   * Every drawing on this page is one of the reference design's isometric figures, pasted
   * unmodified, and a figure strokes and fills itself from `--og-*` inline. Those names are
   * published here rather than re-keyed into each plate, for two reasons: a plate lifted out
   * of the reference needs no edit and so cannot drift from it, and the palette stays the
   * ONE above - the console's, not the reference's own `terminal` register.
   *
   * So this block is an adapter and nothing else. Not one hex is stated in it: every line
   * resolves to a token already declared above, which is what makes a palette change still
   * one edit. Where the reference's ladder is finer than the console's - four text steps to
   * three, three border steps to two - the missing rung is a mix between its neighbours and
   * says so.
   */
  --og-page-bg: var(--bg-app);
  --og-surface: var(--bg-terminal);
  --og-surface-raised: var(--bg-raised);
  --og-surface-sunken: var(--bg-inset);
  --og-border: var(--border-1);
  /* The reference's faintest rule, used for a plate's isometric floor grid. The console
     publishes no step under `border-1`, so it is that rule half-dissolved into the surface
     it is drawn on - which is what a floor grid has to be to stay behind the drawing. */
  --og-border-subtle: color-mix(in oklab, var(--border-1) 55%, var(--bg-terminal));
  --og-border-strong: var(--border-2);
  --og-text: var(--text-1);
  --og-text-body: var(--text-2);
  --og-text-muted: var(--text-dim);
  /* A fourth text step the console does not carry: a plate uses it for the one label that
     has gone quiet (a task not yet picked up), so it sits under `text-dim` rather than
     beside it. */
  --og-text-faint: color-mix(in oklab, var(--text-dim) 62%, var(--bg-terminal));
  --og-accent: var(--accent-fill);
  --og-accent-fg: var(--accent-fg);
  --og-accent-soft-bg: var(--accent-soft);
  --og-positive: var(--green);
  --og-positive-soft-bg: color-mix(in oklab, var(--green) 18%, var(--bg-terminal));
  --og-negative: var(--red);
  --og-negative-soft-bg: color-mix(in oklab, var(--red) 18%, var(--bg-terminal));
  --og-warning: var(--yellow);
  --og-neutral-soft-bg: var(--bg-hover);
  --og-neutral-soft-fg: var(--text-2);

  /* JetBrains Mono leads, then the platform's own. It is the console's `font: 'jetbrains'`
     (apps/dashboard/src/lib/themes.ts:137), and no webfont is loaded for it: the page fetches
     nothing but its own assets, so this is the face if the visitor has it and `ui-monospace`
     if they do not. */
  --mono:
    "JetBrains Mono", ui-monospace, "SF Mono", SFMono-Regular, "Fira Code", Menlo, Consolas,
    monospace;
  --og-font-mono: var(--mono);

  /*
   * ONE measure for the whole page, not a reading width plus a wider hero.
   *
   * The two-width version made the hero a break-out: its frame started 90px left of every
   * rule below it, so the first thing under the fold was a step in the page's left edge. The
   * reference design holds a single 1320px column from the nav to the footer and lets the
   * COLUMN COUNT carry the difference - a reading band is a 62ch paragraph inside the wide
   * track, never a narrower track. The two prose pages narrow the token itself (see `.doc`).
   */
  --maxw: 1320px;

  /*
   * THE EDGE, and it is square — `0`, not a small radius.
   *
   * The reference design's `terminal` register sets every one of its four radius tokens
   * (frame, card, control, pill) to zero, and that is the whole of how the register reads
   * as a terminal rather than as a product UI: a page whose structure is nothing but
   * hairline rectangles has no soft corner anywhere to contradict it. At 6px this page was
   * hedging - a rounded card holding a square-cornered SVG plate, a pill-shaped badge two
   * inches from a terminal prompt - and the hedge was visible in every screenshot.
   *
   * The token stays a token rather than being inlined, because the register is a CHOICE:
   * one line here moves the nav, the buttons, the cards, the code boxes, the frames and the
   * badges together, which is the only way a change like this can stay coherent.
   *
   * Two things are exempt and neither is a box. LEDs and graph nodes keep `50%` — they are
   * dots, and the reference draws its status dots round for the same reason. The brand badge
   * keeps its `22%` because that arc lives in the PNG's own alpha, so a CSS corner here cuts
   * a second, tighter curve through the artwork.
   */
  --r: 0px;

  /* The vertical rhythm, as one number. Bands are separated by 2× this and nothing else -
     no full-bleed rules, no 88px troughs: structure comes from the card borders. */
  --band: 22px;

  /*
   * The page's one timing curve. Nine `animation` shorthands and one `transition` read it and
   * NOTHING declared it, which is worse than it sounds: an undefined custom property makes
   * its whole shorthand invalid at computed-value time, so each of those nine fell back to
   * `animation-name: none` and simply never ran. The scope ring, the box edges, the candidate
   * chip, the three verdict pills and the whole cleanup ledger were all dead on this one
   * missing line.
   *
   * Fast out of the gate and a long settle, which is the shape that suits a thing ARRIVING -
   * a chip landing in a lane, a boundary closing - and every one of the nine is that.
   */
  --ease: cubic-bezier(0.22, 0.68, 0.24, 1);

  /* The one duration for every entrance on the page - the hero's on load, and each band's as
     it scrolls up. See §entrance. Held here beside `--ease` so the two are read together:
     they are the same move, and a page whose opening beats are timed apart reads as several
     animations rather than one page arriving. */
  --enter: 0.62s;

  /* Both, in system order: no stored choice means the visitor's setting decides, and every
     `light-dark()` above reads this. The two rules under it are the whole of the override. */
  color-scheme: light dark;
}

:root[data-theme="light"] {
  color-scheme: light;
}
:root[data-theme="dark"] {
  color-scheme: dark;
}

* {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
  /* The sticky bar is 64px; the rest is the air an anchored heading wants above it. */
  scroll-padding-top: 80px;
}

/*
 * 13.5px, not 14. The reference design's body step, and the one that lets a card's copy run
 * three lines at the width the grid gives it rather than four - which is what makes a row of
 * cards land on the same baseline without any of them being padded out to get there.
 *
 * The page ground is FLAT. There was a fixed 48px grid and a radial mask painted behind
 * everything here; both are gone. A texture behind a page whose every band is already a
 * hairline-ruled grid competes with the rules it sits under, and the mask meant the top of
 * the page was busier than the bottom for no reason a reader could name.
 */
body {
  margin: 0;
  background: var(--bg-app);
  color: var(--text-1);
  font-family: var(--mono);
  font-size: 13.5px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

a {
  color: var(--accent-fg);
  text-decoration: none;
}
a:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}

h1,
h2,
h3 {
  font-weight: 600;
  letter-spacing: -0.02em;
  margin: 0;
}

p {
  margin: 0 0 1em;
  color: var(--text-2);
}

code,
pre {
  font-family: var(--mono);
}

/* ── layout ────────────────────────────────────────────────────────────────── */

.wrap {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 24px;
  position: relative;
  z-index: 1;
  min-width: 0;
}

@media (max-width: 620px) {
  .wrap {
    padding: 0 16px;
  }
}

/*
 * A band is `--band` of air above and below, and NOTHING else: no full-bleed rule between
 * sections, no 88px trough.
 *
 * The rules were the problem. Every band already draws its own hairline grid, so a page-wide
 * rule every 176px added a second, competing horizontal - the eye read the page as nine
 * stacked slabs rather than as one column of objects. Removing the rule and halving the
 * trough is what makes the card borders the only structure on the page, which is the whole
 * idea the reference design is built on.
 */
section {
  padding: var(--band) 0;
  position: relative;
  z-index: 1;
}

/*
 * The section label, written as a source comment.
 *
 * The `//` is a PSEUDO rather than two characters in the markup: it is ornament, and a screen
 * reader announcing "slash slash the bargain" is worse than silence. One place to change it,
 * and the markup stays a plain sentence.
 */
.eyebrow {
  font-size: 11.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin: 0 0 16px;
}
.eyebrow::before {
  content: "// ";
  color: var(--accent-fg);
  opacity: 0.75;
}

h2 {
  font-size: clamp(19px, 2.5vw, 24px);
  line-height: 1.28;
  margin-bottom: 12px;
}

/* The reading band is a MEASURE inside the wide column, never a narrower column. */
.lede {
  max-width: min(66ch, 100%);
  font-size: 13.5px;
}

/*
 * ── nav ─────────────────────────────────────────────────────────────────────
 *
 * THE SHAPE, and it is one row read left to right: an identity, then a `/`, then the
 * sections, then the actions hard against the right edge. Nothing floats in the middle.
 *
 * The previous bar put four plain links and a pill-shaped console badge in a single
 * right-hand cluster, which gave the row two competing centres of gravity - a rounded
 * brown pill next to a rounded brown version pill, with the links squeezed between them -
 * and no way to tell a section anchor from an outbound route. The fix is the split this
 * page already uses everywhere else: NAVIGATION on the left with the thing it belongs to,
 * ACTIONS on the right in the page's own button geometry (`--r`, a hairline, 34px tall),
 * so the only two round things left in the bar are the brand badge and the live LED.
 *
 * Links are uppercase and tracked out because they are LABELS, not prose: at 11.5px in a
 * mono face, small-caps tracking is what separates a nav item from the body copy it sits
 * above, and it lets four of them run in the space three sentence-case words needed.
 */

header.nav {
  position: sticky;
  top: 0;
  z-index: 40;
  backdrop-filter: blur(14px);
  /* Nearly opaque, and no rule of its own: the bar separates from the page by its GROUND.
     The line belongs to `.nav-inner` — see below. */
  background: color-mix(in oklab, var(--bg-app) 92%, transparent);
}

/*
 * The rule sits on the INNER element, so it starts and stops at the column's own edges
 * rather than running the full width of the viewport.
 *
 * This is the reference design's move and it is a structural claim, not a detail: the page
 * is one 1320px sheet, and a rule that overshoots it on both sides says the bar is a
 * separate chrome bolted above the sheet. Aligned, the nav's underline lands on exactly the
 * left edge the hero's first character does, and the whole page reads as ruled paper.
 */
.nav-inner {
  position: relative;
  display: flex;
  align-items: center;
  gap: 14px;
  height: 64px;
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 24px;
}
/* Drawn as a pseudo rather than as `border-bottom`, because a border spans the PADDING box
   and would overshoot the hero's first character by the 24px gutter on each side - which is
   the one thing this rule exists to line up with. `inset` repeats the gutter so the line
   ends where the content does. `.wrap`'s own 620px step is mirrored below. */
.nav-inner::after {
  content: "";
  position: absolute;
  inset: auto 24px 0;
  height: 1px;
  background: var(--border-1);
}

@media (max-width: 620px) {
  .nav-inner {
    padding: 0 16px;
  }
  .nav-inner::after {
    inset: auto 16px 0;
  }
}

/*
 * THE NAV LIGHTS UP. Every item in the bar - the mark, the version, the four sections and both
 * buttons - answers a hover by going accent and throwing a halo in it, so the bar reads as one
 * lit strip rather than as five controls that each found their own hover.
 *
 * `:focus-visible` is paired with `:hover` on every one of them. A keyboard is not a lesser
 * pointer, and a bar whose whole hover language is a colour would otherwise have nothing at
 * all to say to a tab key.
 */
.mark {
  display: flex;
  align-items: center;
  gap: 9px;
  color: var(--text-1);
  font-weight: 600;
  letter-spacing: 0.02em;
  transition:
    color 0.16s,
    text-shadow 0.16s;
}
.mark:hover,
.mark:focus-visible {
  color: var(--accent-fg);
  text-shadow: 0 0 12px var(--accent-glow);
  text-decoration: none;
  outline: none;
}
/* The badge is the one item with a halo at REST - it is the brand mark, and it carries its own
   light on the page. Hovering raises that light rather than introducing it. */
.mark:hover .glyph,
.mark:focus-visible .glyph {
  box-shadow: 0 0 22px color-mix(in oklab, var(--accent) 60%, transparent);
}
/* The step up to 15px and the tighter tracking are the NAV's, not the footer's: the same
   wordmark sits in a 12px credits row down there, where a wordmark a step above body size
   would out-shout the line it belongs to. */
.nav-inner .mark {
  flex: none;
  font-size: 15px;
  letter-spacing: -0.01em;
}

/* The divider between the identity and the sections. A literal slash rather than a 1px
   rule: the page is a terminal, and a path separator is the character that already means
   "what follows is inside what precedes". Decorative, so it is a pseudo — see `.eyebrow`. */
.nav-sep {
  color: var(--text-dim);
  font-size: 15px;
  user-select: none;
}

/* The version rides with the wordmark as quiet text, not as a bordered pill. It is a fact
   about the build, never an affordance, and a pill promised a click that was never there. */
.ver {
  flex: none;
  font-size: 11.5px;
  color: var(--text-dim);
  letter-spacing: 0.02em;
  white-space: nowrap;
  transition:
    color 0.16s,
    text-shadow 0.16s;
}
/*
 * It lights with the MARK as well as on its own hover, because the badge, the wordmark and the
 * build are one identity cluster and lighting a third of it is worse than lighting none.
 * `+`, not a wrapper: `bun run bump-version` finds this element by its own markup and refuses
 * a file it cannot match, so the glow had to be built around the existing sibling pair.
 *
 * Still NOT an affordance, and the cursor is what keeps that honest - it stays the text cursor,
 * so the version lights up without ever claiming, as the old pill did, that it can be clicked.
 */
.ver:hover,
.mark:hover + .ver,
.mark:focus-visible + .ver {
  color: var(--accent-fg);
  text-shadow: 0 0 12px var(--accent-glow);
}

/* The brand badge - `/logo.png`, the same raster the console and the reports wear.
   No border and no radius here: the badge carries its own edge and its corners are
   rounded in the PNG's alpha, so a CSS radius on top would cut a second, tighter
   arc through the artwork. `block` because an inline image sits on the baseline and
   would drop the mark a few pixels below the wordmark it is centred against. */
.mark .glyph {
  display: block;
  width: 22px;
  height: 22px;
  /* The halo takes the RAW dial hue, never either mix: a wash muted toward the ink
     lands a step off the page and reads as a smudge rather than as light. */
  box-shadow: 0 0 14px color-mix(in oklab, var(--accent) 34%, transparent);
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px 8px;
  border: 1px solid var(--border-2);
  border-radius: var(--r);
  font-size: 11px;
  color: var(--text-2);
  background: var(--bg-raised);
  white-space: nowrap;
}

.nav-links {
  display: flex;
  align-items: center;
  gap: 22px;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}
.nav-links::-webkit-scrollbar {
  display: none;
}
.nav-links a {
  color: var(--text-2);
  font-size: 11.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
  transition:
    color 0.16s,
    text-shadow 0.16s;
}
/* Accent, not `--text-1`. These used to just brighten a step, which at 11.5px uppercase was a
   change a reader had to look for - the halo is what makes the target unmistakable without
   adding an underline to a bar that has no other rules in it. */
.nav-links a:hover,
.nav-links a:focus-visible {
  color: var(--accent-fg);
  text-shadow: 0 0 12px var(--accent-glow);
  text-decoration: none;
  outline: none;
}

/* The right-hand cluster. `margin-left: auto` lives HERE and not on `.nav-links`, which is
   what moves the sections back beside the mark. */
.nav-act {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
}

/*
 * The two actions share one geometry - 34px tall, `--r` corners, a hairline - so they read
 * as a pair rather than as a badge beside an icon. `.navbtn` is deliberately NOT `.btn`:
 * that one is the page's 13px call-to-action and is a step too heavy for a 64px bar.
 */
.navbtn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 34px;
  padding: 0 14px;
  border: 1px solid var(--border-2);
  border-radius: var(--r);
  background: var(--bg-terminal);
  box-shadow: var(--lift);
  color: var(--text-1);
  font-size: 12.5px;
  white-space: nowrap;
  transition:
    color 0.16s,
    background 0.16s,
    border-color 0.16s,
    box-shadow 0.16s;
}
/* A BOX halo where the text items take a text one: these have an edge, so the light comes off
   the edge. The ring goes accent with it - a glow around a grey hairline reads as a rendering
   artefact rather than as the button answering. */
.navbtn:hover,
.navbtn:focus-visible {
  text-decoration: none;
  background: var(--bg-hover);
  border-color: var(--accent-fg);
  color: var(--accent-fg);
  box-shadow: 0 0 18px var(--accent-glow);
  outline: none;
}
/* The bar's one action, and the same filled slab as `.btn.primary` one size down: the page
   has exactly one primary treatment, and the nav is not the place to invent a second. */
.navbtn.live {
  font-weight: 500;
}
/* It is already the page's full ink at rest, so its hover has to go FURTHER than the others'
   or the one button the bar wants clicked would be the one that answers least. */
.navbtn.live:hover,
.navbtn.live:focus-visible {
  box-shadow: var(--lift), 0 0 24px color-mix(in oklab, var(--accent) 38%, transparent);
}

/*
 * Narrow. The bar sheds in the order it can afford to: the deep-link sections first, then
 * the version, then the `/` that no longer separates anything, then the console's label -
 * which keeps its LED, so the route survives as a lit square rather than disappearing.
 */
@media (max-width: 900px) {
  .nav-links a.hide-sm {
    display: none;
  }
  .nav-links {
    gap: 16px;
  }
  .ver {
    display: none;
  }
}
@media (max-width: 560px) {
  .nav-sep {
    display: none;
  }
  .navbtn.live .lbl {
    display: none;
  }
  .navbtn.live {
    padding: 0 11px;
  }
}

/*
 * ── the theme switch ───────────────────────────────────────────────────────
 *
 * One glyph, and it shows the theme a click MOVES TO, not the one in force: a sun means
 * "go light". `theme.js` writes that destination to `data-to`, so the swap is an attribute
 * the script already has to set rather than a second class to keep in sync.
 *
 * Hidden until the script has run, because a switch that cannot switch is worse than none.
 * `data-js` lands in the <head>, before the first paint, so the nav never reflows for it.
 */
.themebtn {
  display: none;
}
:root[data-js] .themebtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 34px;
  height: 34px;
  padding: 0;
  background: none;
  /* A visible hairline, not a transparent one: it is the second half of the action pair
     beside the console button and has to hold the same box even at rest. */
  border: 1px solid var(--border-2);
  border-radius: var(--r);
  color: var(--text-2);
  cursor: pointer;
  transition:
    color 0.16s,
    background 0.16s,
    border-color 0.16s,
    box-shadow 0.16s;
}
/*
 * The second half of the action pair, so it answers the way the first one does - same ring,
 * same halo. Its glyph is drawn in `currentColor`, so the sun and the moon light with it.
 *
 * `:root[data-js]`, and that prefix is the bug rather than a flourish. The button's resting
 * `color` and `background` are set by `:root[data-js] .themebtn` above - (0,3,0) - so a plain
 * `.themebtn:hover` at (0,2,0) LOSES to them and the button had no hover state at all: the old
 * rule's `--text-1` and `--bg-hover` had never once reached the screen. Anything hovering this
 * control has to be written at the same depth as the rule that dressed it.
 */
:root[data-js] .themebtn:hover {
  color: var(--accent-fg);
  background: var(--bg-hover);
  border-color: var(--accent-fg);
  box-shadow: 0 0 18px var(--accent-glow);
}
/* This one keeps a real OUTLINE rather than trading it for the halo. It is a control with no
   label - a halo alone would say "warm" where a focused icon button has to say exactly which
   square the keyboard is on. */
:root[data-js] .themebtn:focus-visible {
  outline: 2px solid var(--accent-fg);
  outline-offset: 2px;
  color: var(--accent-fg);
  box-shadow: 0 0 18px var(--accent-glow);
}
.themebtn .moon {
  display: none;
}
.themebtn[data-to="dark"] .moon {
  display: block;
}
.themebtn[data-to="dark"] .sun {
  display: none;
}

/* ── buttons ───────────────────────────────────────────────────────────────── */

/*
 * The secondary action: the page's own surface, a hairline rim, body ink. It is the
 * reference landing page's pairing - one filled slab against one outlined box - and the two
 * resolve at a glance in a way two outlines differing only in hue never did.
 *
 * Both carry a soft lift, which is the one place on this page anything is raised off the
 * ground. A button is the one element a reader is meant to press, so it is the one element
 * that may look pressable; every other surface stays flat.
 */
.btn {
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
  gap: 8px;
  padding: 9px 16px;
  border-radius: var(--r);
  border: 1px solid var(--border-2);
  background: var(--bg-terminal);
  color: var(--text-2);
  font-size: 13px;
  box-shadow: var(--lift);
  transition:
    border-color 0.16s,
    background 0.16s,
    color 0.16s;
}
.btn:hover {
  text-decoration: none;
  border-color: var(--accent-fg);
  color: var(--text-1);
}
/*
 * The primary action is a SOLID slab of the page's own ink, with the page's ground as its
 * label - the reference landing page's treatment, and the clearest hierarchy two buttons
 * side by side can have.
 *
 * It had been an accent RING on the inset ground, on the argument that a saturated fill was
 * noise next to a page of hairline rectangles. That argument holds for a saturated fill and
 * not for this one: filling with `--text-1` adds no hue at all, only contrast, so the button
 * reads as the one SOLID thing on a page of outlines rather than as the one coloured thing.
 * It also inverts with the mode for free, because the ink does - a cream slab with dark type
 * in dark mode, the mirror of a dark slab with cream type in light.
 *
 * The rim is one step off the fill rather than a contrasting edge, which is the whole job a
 * border has on a filled control: catch the boundary where the slab meets a ground of
 * similar weight.
 */
.btn.primary,
.navbtn.live {
  background: var(--accent-soft);
  border-color: color-mix(in oklab, var(--accent-fg) 70%, transparent);
  color: var(--accent-fg);
  font-weight: 600;
}
.btn.primary:hover,
.navbtn.live:hover,
.navbtn.live:focus-visible {
  background: color-mix(in oklab, var(--accent) 30%, var(--bg-terminal));
  border-color: var(--accent-fg);
  color: var(--accent-fg);
}
/* ONE flex item for the label, so it cannot be split between an anonymous text node and a
   span - two items that competed for the line and left the button 75px tall. `min-width: 0`
   is what lets it clip rather than overflow, since `.btn` sets `white-space: nowrap`. */
.btn .lbl {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* On a filled slab the LED is the accent at full strength - it is the one hue on the button,
   and it has the page's ink behind it rather than its ground. */
.btn.primary .livedot,
.navbtn.live .livedot {
  background: var(--accent-fill);
}
/* The trailing external-link glyph is a hint, not a second label, so it sits back until the
   cursor is on the button. */
.btn .go {
  opacity: 0.5;
  transition: opacity 0.16s;
}
.btn:hover .go {
  opacity: 0.9;
}

/* Icons ride inside the label, never replace it: a glyph alone is a guess. */
.btn svg,
.copybtn svg,
.themebtn svg,
.replay svg {
  width: 15px;
  height: 15px;
  flex: none;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.copybtn svg,
.replay svg {
  width: 12px;
  height: 12px;
}
.replay svg {
  fill: currentColor;
}

/* A dot that pings, so "live" is asserted by the page rather than only claimed by a word. */
.livedot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent-fill);
  flex: none;
  position: relative;
}
.livedot::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: inherit;
  animation: livePing 2.1s ease-out infinite;
}
@keyframes livePing {
  0% {
    transform: scale(1);
    opacity: 0.65;
  }
  100% {
    transform: scale(2.8);
    opacity: 0;
  }
}

/* Wherever the live console is a text link — nav, footer — it carries the dot and the
   accent, so the one thing on the page a reader can USE reads as live in both places. */
a.live {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  color: var(--accent-fg);
  font-weight: 600;
}

/* In the NAV it is a BUTTON, not a link — see `.navbtn.live`. The footer's copy stays a
   plain link, where a bordered box would be the loudest thing in a 12px row. */
.foot a.live {
  color: var(--accent-fg);
}

/* ── hero ──────────────────────────────────────────────────────────────────── */

/*
 * The fold: a claim on the left, three checkable numbers on the right.
 *
 * The terminal replay used to live here, which made the fold 648px of frame plus a heading
 * and cost the page its opening sentence - a reader met a running dashboard before they had
 * been told what the thing does. The replay is now its own band, where a caption can sit
 * beside it and say what it is. What the fold gets instead is the stat strip: three figures,
 * every one of them greppable in this repo, which is a stronger opening than a screenshot.
 */
.hero {
  padding: 34px 0 var(--band);
}

/*
 * ONE CENTRED COLUMN, bounded, with the plate at the top of it.
 *
 * The fold was a two-column grid - claim left, plate right - and both halves were the worse
 * for it: the drawing had a 0.92fr track to live in and the sentence beside it had to be
 * long enough to balance a 288px plate. Stacked, each gets its own measure. The plate takes
 * the full width of the column and the copy takes as much of it as a line should hold.
 *
 * `--hero-w`, not `--maxw`. The page's 1320 is a measure for a GRID of cards; a centred
 * headline set that wide is a line no reader tracks to the end of, and the plate stops
 * gaining long before it. Everything in this band reads from one number so the drawing, the
 * headline and the action row share an edge.
 */
.hero-stack {
  text-align: center;
}

/*
 * The action row: the install command and the one link that is not an install.
 *
 * `center` with `wrap`, because the two items have very different widths - the copy block
 * is as wide as a 56-character command and the button is as wide as two words - and a row
 * that distributes them lands the button against the column's right edge with a hole in the
 * middle. Centred, they read as a pair. `stretch` matches their heights exactly, which is
 * the whole reason the primary button borrows the copy block's geometry.
 */
.hero-act {
  display: flex;
  align-items: stretch;
  justify-content: center;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 22px;
}

/*
 * ONE LINE, and the type is sized to keep it one.
 *
 * The headline and the standfirst under it are each a single sentence, and each is set to
 * run unbroken across the full 1320 column - a centred claim that wraps reads as two claims,
 * and in monospace the second line of a 42px block is an orphan however the measure is
 * tuned. There is no ragging to adjust here, so the lever is the SIZE: `2.05vw` tracks the
 * viewport and tops out at 28px, which is 56 characters inside 1320 with room to spare.
 *
 * Below 900px it wraps, deliberately. Sizing a 56-character line to fit a phone puts it
 * under 12px, which is smaller than the body copy under it - at that width two readable
 * lines beat one unreadable one.
 */
.hero h1 {
  font-size: clamp(19px, 2.05vw, 28px);
  line-height: 1.2;
  letter-spacing: -0.015em;
  max-width: 100%;
  margin-bottom: 14px;
}

@media (min-width: 900px) {
  .hero h1 {
    white-space: nowrap;
  }
}

.hero h1 .hl {
  color: var(--accent-fg);
}

/* ── the hero's plate ──────────────────────────────────────────────────────── */

/*
 * The fold's drawing, and the first thing on the page under the bar.
 *
 * It takes its height from the PLATE rather than carrying one of its own, which is the
 * opposite of what `.framed` does and right for the opposite reason: a card's frame has a
 * row of siblings to line its label up with, and this one has none. `960×400` is a plate the
 * reference draws at full panel width, so letting it fill this box edge to edge is the shape
 * it was drawn for - a fixed height here would letterbox it and put 300px of dead ground
 * either side of the one drawing a reader meets first.
 */
.hero-fig {
  position: relative;
  /* The TEXT takes the full column so its two sentences stay on one line each; the drawing
     does not follow it out there. Past ~1180px the plate is the same picture with longer
     lanes, and a drawing wider than the sentence it illustrates reads as a separate band. */
  max-width: 1180px;
  margin-inline: auto;
  padding: 20px 24px;
  margin-bottom: 30px;
  border: 1px solid var(--border-1);
  border-radius: var(--r);
  background: var(--bg-terminal);
  overflow: hidden;
}
.hero-fig .iso {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 960 / 400;
}

@media (max-width: 760px) {
  /* At phone width the plate's own labels fall under 6px. It is a drawing, not a diagram
     the reader has to parse, so it keeps its place and loses its side padding. */
  .hero-fig {
    padding: 12px;
    margin-bottom: 24px;
  }
}

/* copy block */
/* Shrink-wrapped, not column-wide: stretched to the copy column the box carried ~180px of
   dead ground between the end of the command and the divider, which read as a gap in the
   component rather than as breathing room. `max-width: 100%` keeps it inside the column when
   the command is the wider of the two, and the `.cmd` scroller then takes over. */
.copyblock {
  display: flex;
  align-items: stretch;
  width: fit-content;
  max-width: 100%;
  background: var(--bg-inset);
  border: 1px solid var(--border-1);
  border-radius: var(--r);
  box-shadow: var(--lift);
  padding: 0 0 0 10px;
  font-size: 12px;
  overflow: hidden;
}
/* `nowrap` plus its own scroller: the install line is one command and a command that has
   been wrapped is one a reader cannot trust they have copied whole. */
.copyblock .cmd {
  align-self: center;
  /* The scroller only works if the flex item is allowed below its content width. */
  min-width: 0;
  padding: 9px 10px 9px 0;
  overflow-x: auto;
  white-space: nowrap;
  color: var(--text-1);
  scrollbar-width: thin;
}
.copyblock .cmd .dollar {
  color: var(--green);
  user-select: none;
}
/*
 * An ATTACHED segment, not a pill floating inside the box.
 *
 * The button used to be its own `--bg-raised` plate with its own border and radius, sitting
 * in the middle of the inset ground with air on all four sides: two nested rounded boxes of
 * near-identical value, which at 10.5px read as a blob rather than a control. So it drops
 * its fill and its outline and instead takes the full height of the block, separated from
 * the command by the SAME hairline the block is drawn with - the field/affordance pairing
 * every install box uses, and the only edge in the component that has to be seen.
 *
 * Its ground arrives on hover rather than at rest: the surface change is then the whole of
 * the feedback, and the resting state has one less rectangle in it.
 */
.copybtn {
  flex: none;
  align-self: stretch;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  background: none;
  border: 0;
  border-left: 1px solid var(--border-1);
  color: var(--text-dim);
  border-radius: 0;
  padding: 0 10px;
  font-family: var(--mono);
  font-size: 11px;
  cursor: pointer;
  transition:
    background 0.16s,
    color 0.16s;
}
.copybtn:hover {
  background: color-mix(in oklab, var(--accent-fg) 13%, transparent);
  color: var(--text-1);
}
.copybtn:active {
  background: color-mix(in oklab, var(--accent-fg) 20%, transparent);
}
/* The button has no resting outline, so focus has to draw its own; inset, because the block
   clips its overflow and an outward ring would be cut off on the right edge. */
.copybtn:focus-visible {
  outline: 2px solid var(--accent-fg);
  outline-offset: -2px;
}
.copybtn.done {
  background: color-mix(in oklab, var(--green) 15%, transparent);
  color: var(--green);
}

/* ── terminal ──────────────────────────────────────────────────────────────── */

/*
 * No drop shadow, and the shared radius. Every other box on the page is a flat hairline
 * rectangle; a lifted one here read as a floating window pasted onto a flat page.
 *
 * The frame takes the FULL column and stands alone - the title and fact list that used to
 * sit under it are gone. A `.pair` held the two side by side at 1.35fr / 0.65fr before that,
 * which is a shape for a drawing whose height its copy can match, and this one's cannot:
 * `--term-h` is a fixed 648px board and the copy was about 330, so the narrow track was half
 * empty however it was aligned. The rule died with the last `.pair` on the page.
 */
.term {
  background: var(--bg-terminal);
  border: 1px solid var(--border-1);
  border-radius: var(--r);
  overflow: hidden;
  margin-top: 24px;
}

/*
 * The bar is a CAPTION LINE, not a window chrome plate.
 *
 * It used to be three grey dots, a boxed badge and an outlined button on a `--bg-raised`
 * strip - four borders and a second surface above a frame that already has one. None of it
 * carried meaning: the dots are macOS furniture, and the box around `illustrative` shouted
 * the one word on the frame that is only a disclaimer. So the strip is the terminal's own
 * background with a single hairline under it, and everything on it is set in type: the
 * command left, its state and control right, separated by weight and case alone.
 */
.term-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 16px 10px;
  border-bottom: 1px solid var(--border-1);
  font-size: 11px;
  color: var(--text-dim);
}
.term-bar .title {
  color: var(--text-2);
  letter-spacing: 0.01em;
}
.term-bar .title .s {
  color: var(--text-dim);
}
/* The replay's numbers are invented, and this is where that is said. It was two lines of
   caption under the frame; the claim belongs on the frame that makes it. The `·` is drawn
   here rather than typed so the gap either side is the bar's, not the font's. */
.term-bar .demo {
  margin-right: auto;
  font-size: 9.5px;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--text-dim);
  cursor: help;
}
.term-bar .demo::before {
  content: "·";
  margin-right: 10px;
  letter-spacing: 0;
}
/* `display` on the class would beat the UA rule for [hidden], so state it explicitly —
   otherwise the tag says "live" over the typed command, before anything is running. */
.term-bar .tag[hidden] {
  display: none;
}
.term-bar .tag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 10px;
  color: var(--yellow);
}
.term-bar .tag i {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
  animation: kidPulse 1.1s ease-in-out infinite;
}
.term-bar .tag.settled {
  color: var(--green);
}
.term-bar .tag.settled i {
  animation: none;
}
/* Held, not live: the dot stops pinging, because a pulsing dot beside the word `paused` is the
   bar contradicting itself. */
.term-bar .tag.held {
  color: var(--text-dim);
}
.term-bar .tag.held i {
  animation: none;
}
/* The one control on the bar, and it reads as a word rather than a plate: an outlined pill
   next to an outlined badge made the bar look like a toolbar. The glyph carries the affordance
   and the colour carries the hover. */
.replay {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: none;
  border: 0;
  color: var(--text-dim);
  padding: 2px 0;
  font-family: var(--mono);
  font-size: 10.5px;
  cursor: pointer;
  transition: color 0.16s;
}
.replay:hover {
  color: var(--accent-fg);
}
.replay:focus-visible {
  outline: 1px solid var(--accent-fg);
  outline-offset: 3px;
  border-radius: var(--r);
}
.replay svg {
  opacity: 0.75;
}
.replay:hover svg {
  opacity: 1;
}

/*
 * The pause toggle. One button carrying two glyphs, switched on `aria-pressed` - so the state
 * the screen reader is given and the state the eye is given are the SAME attribute and cannot
 * drift, which two buttons or a class would both allow.
 *
 * `[hidden]` is spelled out for the reason the `.tag` rule above gives: `.replay` sets
 * `display: inline-flex`, which outranks the UA's `[hidden] { display: none }`, so `app.js`
 * hiding this under reduced motion would otherwise do nothing at all.
 */
.pausebtn[hidden] {
  display: none;
}
.pausebtn .ri,
.pausebtn[aria-pressed="true"] .pi {
  display: none;
}
.pausebtn[aria-pressed="true"] .ri {
  display: block;
}
/* Pressed reads as ON, not as hover: the label is the run's state, so it holds the accent while
   the board is stopped rather than only while the pointer is over it. */
.pausebtn[aria-pressed="true"] {
  color: var(--accent-fg);
}

.term-body {
  padding: 16px 16px 18px;
  font-size: 12.5px;
  line-height: 1.85;
  min-height: 340px;
  overflow-x: auto;
  white-space: pre;
  scrollbar-width: thin;
}

/*
 * The hero frame is a FIXED screen, not a box that grows with its contents.
 *
 * `min-height` let the frame stand at one size while the command typed and then jump taller
 * the moment the dashboard replaced it - the page reflowed under the reader every eight
 * seconds. A terminal does not resize when a program prints; it scrolls. So the height is
 * fixed, the overflow is hidden, and the TRANSCRIPT takes whatever is left over - which is
 * also how the real board allocates (`MIN_STREAM`, apps/cli/src/tui/dashboard-layout.ts:73):
 * every other region is measured first and the stream gets the remainder.
 */
#term {
  /* Sized to the board's fixed regions (four field rows, two child rows, six findings, the
     two-line footer) plus the transcript. `app.js`'s KID_ROWS/FINDS/PHASES tables are the other
     half of this number - grow any of them and the frame clips.

     Grown from 648px, but only just - and the small number is the point. What actually
     decides how much run you can see is `TAIL_ROWS` in `app.js` (6 → 10 → 13), not this; the
     region is the frame's only `flex: 1 1 auto`, so it was ALREADY taller than the six lines
     being put in it. Adding height without raising that cap buys nothing but void, which is
     what the first attempt at this did: 806px left the stream eighteen rows to hold ten, and
     a replay caught early in its loop was a header over 300px of empty ground.

     So this is sized to `TAIL_ROWS` plus about one: thirteen lines fill it, and the fourteenth
     row of slack is what puts the oldest line under `.tail::after`'s fade, so the region reads
     as scrollback running off the top rather than as a list that happens to fit. The height did
     NOT move when the children region went from five rows to two - those three rows went to the
     stream, which is why `TAIL_ROWS` went 10 → 13 in the same change. The twelve-phase strip
     stays one row at desktop width and wraps to two on a narrow screen, which the extra row
     also absorbs. Grow `TAIL_ROWS`, `KID_ROWS` or `FINDS` and this has to move with them. */
  --term-h: 754px;
}
#term .term-body {
  height: var(--term-h);
  min-height: 0;
  overflow: hidden;
}
/* Not scoped to `#term`, on the same argument the `.dash-on` rule below states: the rule
   travels with the class. Every region holds its measured height and only the stream gives
   ground - said as one exception rather than as a rule plus a higher-specificity override. */
.term-body.dash-on .dash {
  height: 100%;
  display: flex;
  flex-direction: column;
}
.dash > *:not(.rgn-tail) {
  flex: none;
}
.dash > .rgn-tail {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
/* The dashboard is laid out, not printed, so the frame drops `white-space: pre` while it
   is up. Deliberately NOT scoped to #term: the rule travels with the class, so moving the
   frame cannot leave the dashboard rendering as preformatted text. */
.term-body.dash-on {
  white-space: normal;
  overflow-x: hidden;
}

@media (max-width: 560px) {
  #term {
    /* Taller than the desktop frame, which looks wrong and is not: the phase strip wraps to
       four rows here against one at desktop width, and `.dash` shrinks its font but not its
       row COUNT - two child rows and six findings are two and six lines wide or narrow. The
       stream is the only region that gives ground, so on a phone it is the one that pays for
       the wrap unless this number does. Sized, like the desktop one, to `TAIL_ROWS` plus
       about a row at this breakpoint's 10.5px line box. */
    --term-h: 840px;
  }
  #term .term-body {
    padding: 14px 12px;
  }
  /* The bar carries five things at this width - name, `illustrative`, the state tag, pause and
     replay - and once the tag appears mid-run they no longer fit on one line. Left to shrink,
     the flex items squeezed the NAME, so `gimorra · scan` broke across two lines while the
     controls kept their width: the frame appeared to rename itself the moment the run started.
     Wrapping moves the controls down as a group instead, and `nowrap` on the title is what
     says the name is the item that must not be the one that gives. */
  .term-bar {
    flex-wrap: wrap;
    row-gap: 6px;
  }
  .term-bar .title {
    white-space: nowrap;
  }
  .dash {
    font-size: 10.5px;
  }
  .dash .rgn {
    padding: 7px 0;
  }
  /* On a phone the child's task and its dollar cost are the first two things worth giving
     up - the name, the turns and the state are what a reader is scanning for. A truncated
     dollar figure is worse than no dollar figure. */
  .kid {
    grid-template-columns: 9px minmax(0, 1fr) 30px 64px;
    gap: 6px;
  }
  .kid .task,
  .kid .cst {
    display: none;
  }
  .find {
    grid-template-columns: 68px minmax(0, 1fr) 56px 26px;
    gap: 6px;
  }
  .fld {
    grid-template-columns: 11px 48px minmax(0, 1fr);
  }
  /* The tail still FLEXES here - a fixed height would put the frame back to changing size
     the moment a region above it wrapped onto another line. */
}

.term-body .ln {
  display: block;
  opacity: 0;
  transform: translateY(3px);
  animation: lineIn 0.26s ease forwards;
}

@keyframes lineIn {
  to {
    opacity: 1;
    transform: none;
  }
}

.cursor {
  display: inline-block;
  width: 7px;
  height: 14px;
  background: var(--text-1);
  vertical-align: -2px;
  animation: blink 1.05s steps(2, start) infinite;
}
@keyframes blink {
  to {
    visibility: hidden;
  }
}

.c-dim {
  color: var(--text-dim);
}
.c-mut {
  color: var(--text-2);
}
.c-green {
  color: var(--green);
}
.c-red {
  color: var(--red);
}
.c-cyan {
  color: var(--cyan);
}
.c-yellow {
  color: var(--yellow);
}
.c-orange {
  color: var(--orange);
}
.c-ink {
  color: var(--text-1);
}

/* ── the live dashboard inside the hero frame ──────────────────────────────── */

/*
 * A real TTY packs its rows. `line-height: 1.75` gave every one of the board's ~35 lines
 * three quarters of a blank line under it, which is what pushed the frame past a laptop
 * screen - so the fix is the leading and the region padding, not fewer regions.
 */
.dash {
  font-size: 11.5px;
  line-height: 1.5;
  animation: dashIn 0.34s ease both;
}
@keyframes dashIn {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
}

.dash .rgn {
  padding: 5px 0;
  border-top: 1px dashed var(--border-1);
}
.dash .rgn-top {
  padding-top: 0;
  padding-bottom: 4px;
  border-top: none;
  display: flex;
  align-items: center;
  gap: 7px;
  flex-wrap: wrap;
}
.dash .rgn-top .ttl {
  font-weight: 600;
}

/*
 * The header FIELDS block - `headerBlock`'s own rows (apps/cli/src/tui/scan-dashboard.tsx:408):
 * a glyph, a label padded to a fixed column, and the value. `scope` prints its rules inline
 * because a carve-out the operator cannot see is one they cannot confirm landed.
 */
.fields {
  padding-bottom: 6px;
  border-bottom: 1px solid var(--border-1);
}
.fld {
  display: grid;
  grid-template-columns: 12px 58px minmax(0, 1fr);
  gap: 6px;
  align-items: baseline;
}
.fld .fl {
  color: var(--text-dim);
}
.fld .fv {
  color: var(--text-2);
}
.dash .rgn-h {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 9.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin-bottom: 3px;
}
.dash .rgn-h::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border-1);
}

/* phases — the strip that updates in place rather than reprinting */
.pstrip {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
}
.pstrip .p {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  color: var(--text-dim);
  transition: color 0.25s;
}
.pstrip .p.done {
  color: var(--green);
}
.pstrip .p.now {
  color: var(--cyan);
}
.pstrip .p.now .nm {
  border-bottom: 1px solid currentColor;
}
.pstrip .mk {
  font-size: 10px;
  width: 8px;
  display: inline-block;
}
/* A phase that just completed pops once, so the strip reads as moving rather than as a list. */
.pstrip .p.pop {
  animation: phasePop 0.6s ease-out;
}
@keyframes phasePop {
  0% {
    transform: translateY(-3px);
    text-shadow: 0 0 10px currentColor;
  }
  100% {
    transform: none;
    text-shadow: none;
  }
}

/*
 * Every board column that can outgrow its track clips to one line. Written once: the diff
 * that added the fields, children and findings rows spelled it out four separate times.
 */
.fld .fv,
.kid .task,
.kid .tn,
.kid .cst,
.find .t,
.find .sv,
.find .cv,
.foot-keys {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* `⊕ surface` — one PIPELINE row's body on the real board, not a region of its own
   (`surfaceRecapLine`, apps/cli/src/scan-console.ts:331), so it sits under the phase bar.
   It wraps rather than truncates: the `out of scope` clause is the end of the sentence and
   the end is the point. */
.surf {
  margin-top: 5px;
  line-height: 1.5;
}

.bar {
  height: 3px;
  margin-top: 8px;
  background: var(--bg-hover);
  overflow: hidden;
}
.bar i {
  display: block;
  position: relative;
  overflow: hidden;
  height: 100%;
  width: 0;
  background: linear-gradient(90deg, var(--accent-fill), var(--cyan));
  box-shadow: 0 0 8px color-mix(in oklab, var(--accent) 55%, transparent);
}
/* A travelling highlight while work is in flight — and none once the run has settled. */
.bar i::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    transparent,
    color-mix(in oklab, var(--text-1) 60%, transparent),
    transparent
  );
  animation: barShine 1.7s linear infinite;
}
.bar.done i::after {
  display: none;
}
@keyframes barShine {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(100%);
  }
}

/* children — one row per specialist the goal supervisor spawned */
/*
 * TWO rows, and they are a WINDOW over five children rather than the roster - `KID_ROWS` and
 * `kidWindow` in `app.js` own which two, and the region header carries the `+N more`.
 */
.kid {
  display: grid;
  grid-template-columns: 10px minmax(0, 92px) minmax(0, 1fr) 30px 62px 72px;
  gap: 8px;
  align-items: baseline;
  padding: 1px 0;
}
/* A row whose OCCUPANT changed, which on a two-row window happens about six times a run. The
   fade is what separates "a different child is here now" from "this child's state changed" -
   without it a name swapping under a settled dot reads as a glitch. Short on purpose: the row
   is replacing information the reader may be mid-way through, not introducing it. */
.kid.swapped {
  animation: kidSwap 0.26s ease-out;
}
@keyframes kidSwap {
  from {
    opacity: 0;
    transform: translateY(-3px);
  }
}
/* Four decimals on a child, two on the run - the real board's own split, because a child's
   share of a scan is often under a cent. `tabular-nums` belongs to the two NUMBER columns;
   the diff had sprayed it onto `.task`, which is prose. */
.kid .tn,
.kid .cst {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.kid .dotk {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--border-2);
  transition: background 0.25s;
}
.kid.running .dotk {
  background: var(--yellow);
  animation: kidPulse 1.1s ease-in-out infinite;
}
.kid.done .dotk {
  background: var(--green);
}
@keyframes kidPulse {
  50% {
    opacity: 0.35;
  }
}
.kid .nm {
  color: var(--text-2);
}
.kid.done .nm,
.kid.running .nm {
  color: var(--text-1);
}
.kid .stt {
  color: var(--text-dim);
  white-space: nowrap;
  text-align: right;
}
.kid.running .stt {
  color: var(--yellow);
}
.kid.done .stt {
  color: var(--green);
}

/*
 * findings — the tally rides in the region's own header, which is `inFlight`'s place on the
 * real board (apps/cli/src/tui/scan-dashboard.tsx:864), and flashes on the frame it moves.
 */
.rgn-h .flash {
  animation: ctFlash 0.65s ease-out;
}
@keyframes ctFlash {
  0% {
    text-shadow: 0 0 12px currentColor;
    transform: translateY(-2px);
  }
  100% {
    text-shadow: none;
    transform: none;
  }
}

/*
 * The findings LIST. `FindingLine` is the shape both surfaces that draw findings take
 * (apps/cli/src/tui/scan-dashboard.tsx:786), so the board shows what a finding IS - a
 * severity, a title and the one column that is its state - rather than only how many there
 * are. Rows are appended as proof settles them and never re-rendered.
 */
.finds {
  margin-top: 4px;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.find {
  display: grid;
  grid-template-columns: 76px minmax(0, 1fr) 62px 30px;
  gap: 8px;
  align-items: baseline;
  animation: tailIn 0.34s ease both;
}
/* The ONE column that is the finding's state. `open` is not a lesser green - it is a
   different answer, and a class with no deterministic validator never leaves it. */
.find .stt {
  color: var(--green);
}
.find.open .stt {
  color: var(--yellow);
}
.find .t {
  color: var(--text-1);
}
.find .sv.high {
  color: var(--red);
}
.find .sv.medium {
  color: var(--orange);
}
.find .sv.low {
  color: var(--blue);
}
.find .cv {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
/* `PROPERTY_PROOF_LEGEND`, packages/reporting/src/model.ts:367. */
.legend-prop {
  margin-top: 5px;
  font-size: 10px;
}

/*
 * The footer, never given up at any height: what it cost against the pool, the token split,
 * the wall clock and the one status word (apps/cli/src/tui/scan-dashboard.tsx:1654). The key
 * hints under it are `scanKeyHints` (apps/cli/src/tui/keys.ts:535).
 */
.foot-st {
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid var(--border-1);
  display: flex;
  align-items: baseline;
  gap: 5px;
  flex-wrap: wrap;
  font-variant-numeric: tabular-nums;
}
/* A class, not the `data-k` registry key - that attribute exists so `buildDash` can populate
   the `el` map, and renaming a key should not move the status word. */
.foot-st .f-status {
  margin-left: auto;
}
.foot-keys {
  font-size: 10px;
  margin-top: 3px;
}

/* transcript — the oldest line scrolls off the top, and the region takes the leftover
   height rather than declaring one, so the frame never changes size. */
.dash .rgn-tail {
  padding-bottom: 0;
}
.tail {
  flex: 1 1 auto;
  /* Three rows, which is `MIN_STREAM` (apps/cli/src/tui/dashboard-layout.ts:73): the real
     allocator reserves the stream that much before handing height to anything else. */
  min-height: 3lh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  position: relative;
}
/*
 * The region's leftover height is not a whole number of line boxes, so the oldest line can
 * sit half inside it. Fading the top edge makes that read as scrollback running off the
 * screen - which is what it is - rather than as a line the renderer sliced.
 *
 * An opaque gradient over the ground, not `mask-image` on the region: a mask pushes the whole
 * subtree into a transparency layer on every paint, and this subtree is the one that churns -
 * a line is appended and another removed ~13 times per replay, each running a 0.3s entry
 * animation. Absolutely positioned, so it is not a flex item of its own container, and it
 * works in Safari, which never had the masked version.
 */
.tail::after {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 15px;
  background: linear-gradient(to bottom, var(--bg-terminal), transparent);
  pointer-events: none;
}
/* A line is drawn whole or not at all. */
.tail div {
  flex: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  animation: tailIn 0.3s ease both;
}
@keyframes tailIn {
  from {
    opacity: 0;
    transform: translateY(5px);
  }
}

/* ── generic cards / grids ─────────────────────────────────────────────────── */

/* ── the figure box ────────────────────────────────────────────────────────── */

/* A drawing too wide to sit in a card gets a hairline box of its own. No plate number: the
   drawing is the only thing in the box, so nothing needs to name it. */
.figure {
  position: relative;
  padding: 22px;
  border: 1px solid var(--border-1);
  border-radius: var(--r);
  background: var(--bg-terminal);
  min-width: 0;
}
.figure svg {
  display: block;
  width: 100%;
  height: auto;
}
/* The plate's one line of caption. It sits UNDER the drawing rather than inside its viewBox,
   because the two plates have different viewBoxes and a caption drawn in SVG units would set
   at a different size in each. */
.figure .fcap {
  display: block;
  margin-top: 10px;
}

/*
 * Two figures across the FULL column, with the argument under them rather than beside them.
 *
 * Both plates carry the same 320×220 viewBox on purpose, so the two boxes are the same
 * height without either one being told what its height is. A pair at different proportions
 * would need a fixed height here, and that is the rule `.framed` had to invent for the cards.
 *
 * The drawing is capped and centred inside its box. Left uncapped, a 320-unit plate in a
 * 650px half renders its 8.5px caption at 16px and its hairlines at 2 - the reference art is
 * drawn for a plate, and past about 500px it stops reading as a diagram and starts reading as
 * a poster. The BOX still spans its half, so the pair keeps the same shoulders as the text
 * under it; only the art inside stops growing.
 *
 * It collapses a long way down - 620px, later than `.factgrid` under it - because the two plates
 * are one argument seen twice and stacking them at the first opportunity separates the
 * halves by a screen of copy.
 */
.figpair {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px;
  margin-top: 24px;
  min-width: 0;
}
.figpair .figure {
  display: flex;
  justify-content: center;
}
.figpair .figure svg {
  max-width: 500px;
}
@media (max-width: 620px) {
  .figpair {
    grid-template-columns: minmax(0, 1fr);
  }
}

/*
 * The argument under a full-width object, in two columns: the claim and its paragraph left,
 * the facts that back it right. Two bands wear it - the orchestration pair and the replay
 * frame - and both for the same reason: the object's height is set by what the object IS,
 * and no column of three facts is ever going to match it.
 *
 * `align-items: start` and NOT centred - these two columns are read top to bottom as one
 * block, so their first lines have to share a baseline. Centring floated the heading half a
 * fact down from the first bullet it is introducing.
 */
/*
 * The orchestration band's four facts, under its plates and across the full column.
 *
 * They were the right half of a two-column split whose left half was the band's title. The
 * title heads the band now, like every other one, so the facts have the width to themselves -
 * and at 1320px a single column of four one-line facts is a very long, very thin list. Two
 * columns of two is the same four facts at a measure a reader actually tracks.
 */
.factgrid {
  margin-top: 30px;
}
.factgrid .facts {
  margin-top: 0;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px 34px;
}
@media (max-width: 860px) {
  .factgrid .facts {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ── the fact list ─────────────────────────────────────────────────────────── */

/* A marker column and a hanging indent, so a fact that wraps stays clear of its own
   marker. The bold lead-in is the claim; what follows it is why. */
.facts {
  margin-top: 20px;
  display: grid;
  gap: 10px;
}
.fact {
  display: grid;
  grid-template-columns: 20px minmax(0, 1fr);
  gap: 10px;
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--text-2);
}
.fact .m {
  color: var(--accent-fg);
}
.fact b {
  color: var(--text-1);
  font-weight: 600;
}

/* ── the numbered step card ───────────────────────────────────────────────── */

/*
 * The page's one repeating object: a FRAMED illustration, a label, three lines of copy.
 * Every capability on the page is one of these, which is what lets a reader learn the shape
 * once and then scan.
 */
.steps {
  display: grid;
  gap: 14px;
  margin-top: 26px;
}
.steps > * {
  min-width: 0;
}
/*
 * `auto-fit`/`minmax`, not a fixed column count. The two grids on this page want three and
 * four across at full width, and both want to become two and then one as the viewport
 * narrows - which is one rule per grid here instead of two media queries each.
 */
.s3 {
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
}
.s4 {
  grid-template-columns: repeat(auto-fit, minmax(272px, 1fr));
}

/*
 * No number gutter. `STEP / 01 … 07` ran across two grids in two different bands, so the
 * sequence it implied was one the page did not have - 04 did not follow 03 in time, or in
 * anything else. A card that is one of three peers does not need an ordinal, and dropping it
 * gives the illustration back the 68px the gutter was holding.
 */
.step {
  padding: 18px;
  border: 1px solid var(--border-1);
  border-radius: var(--r);
  background: var(--bg-terminal);
  transition: border-color 220ms var(--ease);
  min-width: 0;
}
.step:hover {
  border-color: var(--border-2);
}

.step .sbody {
  min-width: 0;
}

/*
 * The illustration gets a FRAME of its own - a sunken panel inside the card, which is what
 * separates a drawing from the prose under it without a caption having to say so.
 *
 * A fixed height rather than an aspect ratio: the framed illustrations carry three different
 * viewBoxes and two of the frames hold markup instead, and a row of frames that each solved
 * for their own art's proportion would step up and down across the row. One height, art
 * centred inside it, and every frame in a grid agrees.
 */
.framed {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 190px;
  padding: 12px;
  margin-bottom: 16px;
  border: 1px solid var(--border-1);
  border-radius: var(--r);
  /*
   * The plate's own ground, and the same one `.figure` gives the two orchestration plates.
   * A plate strokes its asset grid in `--og-border`, a hairline chosen to sit on the surface
   * this names; dropped onto the sunken step instead it loses most of its contrast and
   * the drawing reads as one lit element floating over nothing. The frame still separates
   * from the card it sits in - that is what its border is for.
   */
  background: var(--bg-terminal);
  overflow: hidden;
}
/* A frame that carries a caption reserves the caption's band in its own padding. Without
   it the art is centred in the WHOLE frame and the bottom of the drawing runs under the
   text - which on the scope card put the caption straight across the boundary ring. */
.framed:has(.fcap) {
  padding-bottom: 28px;
}
.framed:has(> .artchip),
.framed:has(> .lanepills) {
  padding-bottom: 44px;
}
/* The card's label: uppercase mono, tracked out, which is how this design system names a
   thing. Deliberately NOT a sentence - the sentence is the paragraph under it. */
.step h3 {
  margin: 0 0 7px;
  font-size: 12.5px;
  line-height: 1.4;
  font-weight: 400;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-1);
}
.step p {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--text-2);
}

/*
 * ONE band header, and every band wears it: an eyebrow, then the title. Nothing else - the
 * page's last lede was the controls band's and it is gone, so there is no third part left
 * anywhere and no reason for this to be anything but a stack.
 *
 * It is LEFT-ALIGNED and shares the column's left edge with the cards under it. Centred (it
 * was, briefly) the header is an island: a 1320px band whose cards run edge to edge,
 * announced by a column of text half that wide with 300px of empty page each side. The fold
 * is centred because a fold is a title page; a band is not.
 *
 * The three parts had drifted into three arrangements before this - the controls band
 * stacked all three with an inline `margin-top` on its lede, the orchestration band left its
 * title UNDER its drawings in a two-column split, and the replay band carried a bare eyebrow.
 * The shape is the same everywhere now, so a reader learns where a band announces itself once.
 */
.bandhead {
  margin: 0 0 28px;
}
.bandhead .eyebrow {
  margin-bottom: 14px;
}

/*
 * ONE LINE. The title was two - the first stepped back in `--text-2` and the second in full
 * ink - which read as a claim and its answer, and at the width of this column put a two-line
 * block above every band where a sentence would do. The `.a`/`.b` pair went with it; a title
 * that fits on one line has no halves to colour differently.
 */
.band-title {
  margin: 0;
  font-size: clamp(19px, 2.6vw, 26px);
  line-height: 1.26;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text-1);
}

/*
 * The frame's bottom strip - a caption, a mono chip, or a row of verdict pills.
 *
 * All three ride INSIDE the frame rather than under it, and that is what keeps a row of
 * cards aligned. A chip stacked beneath the frame pushes its own card's title 40px lower
 * than its neighbour's, so three cards in a row had their labels at three different
 * heights; inside the frame the art slot is one fixed height for every card whatever it
 * holds. `:has(.fcap)` and friends reserve the strip's band in the frame's own padding, so
 * the drawing is centred in what is LEFT rather than running under the strip.
 */
.fcap,
.framed > .artchip,
.framed > .lanepills {
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: 9px;
  margin: 0;
}
.fcap {
  text-align: center;
  font-size: 10.5px;
  color: var(--text-dim);
  pointer-events: none;
}
.framed > .lanepills {
  justify-content: center;
}

/* The mono chip a card drops under its illustration (a real command, a real event). */
.artchip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 14px;
  padding: 7px 10px;
  border: 1px solid var(--border-1);
  border-radius: var(--r);
  background: var(--bg-app);
  font-size: 10.5px;
  color: var(--text-2);
  max-width: 100%;
  overflow: hidden;
  white-space: nowrap;
}
.artchip i {
  width: 7px;
  height: 13px;
  flex: none;
  background: var(--accent-fg);
  animation: v2-caret 1s steps(1) infinite;
}

@keyframes v2-caret {
  50% {
    opacity: 0;
  }
}

/* ── the caveat note ───────────────────────────────────────────────────────── */

/* `.thesis-note` and `.thesis-note.warn` stood here and went with the only element that wore
   them, the scope-guard caveat under the controls band. Deleted rather than left named: a
   class nothing wears is the same kind of quiet lie as a keyframe nothing defines, and this
   file had six of those. `git log -S thesis-note` has the rule if the note comes back. */

/* ── footer ────────────────────────────────────────────────────────────────── */

/*
 * The closing box. Flat `--bg-terminal`, not a gradient: the gradient was the one surface on
 * the page that was not a single colour, which made the last thing a reader saw the one
 * thing that did not match. The accent lives in the button, which is the point of the box.
 */
.cta {
  border: 1px solid var(--border-1);
  border-radius: var(--r);
  background: var(--bg-terminal);
  padding: 30px 34px 34px;
  display: flex;
  align-items: center;
  gap: 24px;
  flex-wrap: wrap;
}
/* The band is a strip now that its plate is gone, so the line is the only thing in it
   carrying weight - it keeps the band-title scale rather than dropping to a label's. */
.cta .t {
  font-size: clamp(19px, 2.6vw, 26px);
  line-height: 1.25;
  letter-spacing: -0.01em;
  color: var(--text-1);
}
.cta .s {
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--text-2);
  margin-top: 8px;
  max-width: 62ch;
}
/* Sized up with the band's line, and pushed to the far edge: the claim and the action are
   the strip's only two items, so the gap between them is the strip. */
.cta .btn {
  margin-left: auto;
  padding: 13px 22px;
  font-size: 14px;
}

footer {
  border-top: 1px solid var(--border-1);
  margin-top: var(--band);
  padding: 20px 0 32px;
  font-size: 11.5px;
  color: var(--text-dim);
  position: relative;
  z-index: 1;
}
.foot {
  display: flex;
  gap: 16px;
  flex-wrap: wrap;
  align-items: center;
}
.foot a {
  color: var(--text-2);
}
.foot .sp {
  margin-left: auto;
}

/* ── entrance ──────────────────────────────────────────────────────────────── */

/*
 * TWO entrances, and the difference between them is the whole of this section.
 *
 * The fold cannot wait for JavaScript. `app.js` is the last element in the body, so anything
 * gated on it starts a beat after the first paint - which on the hero is exactly the beat a
 * reader is looking at it. So the nav and the hero run a CSS `animation` instead: it starts
 * at the first frame the stylesheet is applied, with no script involved at all. Everything
 * BELOW the fold is a scroll reveal, because there a transition is right - it has to fire
 * when the band arrives, and only `app.js` knows when that is.
 *
 * Both use `--ease` and `--enter` from the token block at the top of this file - the same
 * curve the nine illustration animations already run on, which is described there as the
 * shape that suits a thing ARRIVING. A band arriving is that.
 */

@keyframes enter {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
}

/*
 * `backwards` fill, not `both`: the element holds the 0% frame through its delay (without it
 * a staggered item paints solid, then blinks out when its turn comes) and is released the
 * instant it lands, so nothing on this page keeps a `transform` it is no longer using - a
 * lingering one is a containing block, and `.hero-fig` holds a plate that animates inside.
 *
 * `[data-js]` is `theme.js`'s head-set attribute; here it means only "a browser that got this
 * far", which keeps the rule with the reveal's own guard below rather than special-casing it.
 */
@media (prefers-reduced-motion: no-preference) {
  html[data-js] .nav,
  html[data-js] .hero-stack > * {
    animation: enter var(--enter) var(--ease) backwards;
  }

  /* The ladder, and it runs top to bottom now that the fold is one column: the plate, the
     sentence it illustrates, then the command. Reading order and arrival order are the same
     thing here, which is the whole advantage a stacked fold has over a split one. */
  html[data-js] .hero-stack > .hero-fig {
    animation-delay: 0.05s;
  }
  html[data-js] .hero-stack > h1 {
    animation-delay: 0.13s;
  }
  html[data-js] .hero-stack > .hero-act {
    animation-delay: 0.21s;
  }
}

/* ── reveal ────────────────────────────────────────────────────────────────── */

/*
 * Scoped to `[data-js]`, and that guard is load-bearing. `app.js` is what adds `.in`; an
 * unguarded `opacity: 0` means a blocked, failed or still-parsing `app.js` leaves every band
 * below the fold invisible - a blank page that reads as a broken deploy rather than as a page
 * with no animation. `theme.js` is parser-blocking in the head, so the attribute is on
 * `<html>` before the first paint and the hidden state never flashes in late.
 */
html[data-js] .reveal {
  opacity: 0;
  transition: opacity var(--enter) ease;
}
html[data-js] .reveal > * {
  transform: translateY(12px);
  transition: transform var(--enter) var(--ease);
}
/*
 * The visible side carries the SAME guard, and that is not tidiness - it is the bug. A bare
 * `.reveal.in` scores (0,2,0) against the hidden rule's (0,2,1), so `opacity: 0` outranks it
 * and every band below the fold stays invisible with `.in` sitting on it. Keep both sides of
 * this section at equal depth.
 */
html[data-js] .reveal.in {
  opacity: 1;
}
html[data-js] .reveal.in > * {
  transform: none;
}

/*
 * The band's children ladder in rather than the whole slab arriving at once. A `.reveal` wrap
 * has three to five direct children - eyebrow, title, lede, the card row - so the ladder is
 * short and lands well inside the parent's own fade. Six is every case on the page; past that
 * the tail would be arriving long after the reader has read the top of the band.
 */
html[data-js] .reveal > :nth-child(2) {
  transition-delay: 0.05s;
}
html[data-js] .reveal > :nth-child(3) {
  transition-delay: 0.1s;
}
html[data-js] .reveal > :nth-child(4) {
  transition-delay: 0.15s;
}
html[data-js] .reveal > :nth-child(5) {
  transition-delay: 0.2s;
}
html[data-js] .reveal > :nth-child(n + 6) {
  transition-delay: 0.25s;
}

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.001s !important;
    transition-duration: 0.001s !important;
  }
  html[data-js] .reveal,
  html[data-js] .reveal > * {
    opacity: 1;
    transform: none;
  }

  /*
   * The plates land on their BASE frame, not their last one, and the distinction is the
   * whole rule. The blanket `animation-duration: 0.001s` above lands everything else on its
   * 100% keyframe, which is right for a drawing that ARRIVES somewhere - but a plate loops,
   * so its 100% frame is the moment before it starts again: `iso-prog` ends at `opacity: 0`,
   * `iso-in2` and `iso-belt` do too. Collapsed that way the scheduler's progress bars, the
   * verification's tick and the conveyor's parcels are all simply absent. Unsetting the
   * animation instead leaves each element exactly as the markup draws it - every bar full,
   * every label present - which is the finished reading of all eleven.
   */
  .iso * {
    animation: none !important;
  }

  /* The verdict lanes rest lit: the card's claim is which one is STORED, and a row of three
     chips all at 32% says only that none of them happened. */
  .lanepills .pill {
    opacity: 1;
  }
}

/* ── /security page ────────────────────────────────────────────────────────── */

/*
 * The two prose pages narrow the WHOLE column, nav included.
 *
 * `--maxw` is 1320px because the landing page's grids fill it. A page that is one 76ch
 * measure does not: capping only `.doc` left the prose against the left edge of a 1320px
 * track with 500px of nothing beside it, and the nav mark floating a long way off the text
 * it belongs to. Re-declaring the token on the body moves the nav, the doc and the footer
 * together, which is the only version where the three line up.
 */
body:has(.doc) {
  --maxw: 880px;
}
.doc {
  max-width: min(76ch, 100%);
  padding: 44px 0 24px;
}
.doc h1 {
  font-size: clamp(23px, 3.6vw, 30px);
  line-height: 1.2;
  margin-bottom: 10px;
}
.doc h2 {
  margin-top: 40px;
  font-size: 18px;
}
.doc h3 {
  margin-top: 26px;
  font-size: 14px;
  color: var(--text-1);
}
.doc p,
.doc li {
  font-size: 13.5px;
  color: var(--text-2);
}
.doc ul {
  padding-left: 20px;
}
.doc li {
  margin-bottom: 6px;
}
.doc pre {
  background: var(--bg-inset);
  border: 1px solid var(--border-1);
  border-radius: var(--r);
  padding: 14px;
  font-size: 12.5px;
  overflow-x: auto;
  color: var(--text-1);
}
.doc table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12.5px;
  margin: 14px 0;
}
.doc th,
.doc td {
  text-align: left;
  padding: 8px 10px;
  border-bottom: 1px solid var(--border-1);
  vertical-align: top;
}
.doc th {
  color: var(--text-dim);
  font-weight: 500;
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.doc td:first-child {
  color: var(--text-1);
  white-space: nowrap;
}
.callout {
  border: 1px solid color-mix(in oklab, var(--orange) 32%, var(--border-1));
  background: color-mix(in oklab, var(--orange) 7%, var(--bg-terminal));
  border-radius: var(--r);
  padding: 14px 18px;
  margin: 20px 0;
  font-size: 13px;
  color: var(--text-2);
}
.callout b {
  color: var(--text-1);
}

/*
 * The plates' keyframes, verbatim from the reference design's own stylesheet.
 *
 * These are the fifteen the ten plates on this page actually name, not the reference's
 * whole set: an unused `@keyframes` is bytes every visitor downloads to animate nothing, and
 * a figure added later brings its own. The names are the reference's, so a plate pasted in
 * from it runs without an edit — which is the whole arrangement this file keeps up its end of.
 */
@keyframes iso-rise { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }
@keyframes iso-draw { 0% { stroke-dashoffset: 600; opacity: 1; } 55% { stroke-dashoffset: 0; opacity: 1; } 88% { stroke-dashoffset: 0; opacity: 1; } 96%,100% { stroke-dashoffset: 0; opacity: 0; } }
@keyframes iso-dash { to { stroke-dashoffset: -32; } }
@keyframes iso-prog { 0% { transform: scaleX(0); opacity: 1; } 70% { transform: scaleX(1); opacity: 1; } 88% { transform: scaleX(1); opacity: 1; } 96%,100% { transform: scaleX(1); opacity: 0; } }
@keyframes iso-in1 { 0%,10% { opacity: 0; } 16%,88% { opacity: 1; } 95%,100% { opacity: 0; } }
@keyframes iso-row { 0% { opacity: 0; transform: translateX(-4px); } 6% { opacity: 1; transform: translateX(0); } 80% { opacity: 1; } 90%,100% { opacity: 0; } }
@keyframes iso-in2 { 0%,26% { opacity: 0; } 32%,88% { opacity: 1; } 95%,100% { opacity: 0; } }
@keyframes iso-blink { 0%,100% { opacity: .25; } 50% { opacity: 1; } }
@keyframes iso-step { 0%,20% { transform: translateX(0); } 25%,45% { transform: translateX(-30px); } 50%,70% { transform: translateX(-60px); } 75%,100% { transform: translateX(-90px); } }
@keyframes iso-belt { 0% { transform: translate(0,0); opacity: 0; } 8% { opacity: 1; } 92% { opacity: 1; } 100% { transform: translate(52px,-30px); opacity: 0; } }
@keyframes iso-slide { from { transform: translateX(-84px); } to { transform: translateX(72px); } }
@keyframes iso-redact { 0% { transform: scaleX(0); opacity: 1; } 10% { transform: scaleX(1); } 86% { transform: scaleX(1); opacity: 1; } 92%,100% { transform: scaleX(1); opacity: 0; } }
@keyframes iso-block { 0% { transform: translate(0,0); opacity: 0; } 6% { opacity: 1; } 50% { transform: translate(69px,-40px); opacity: 1; } 56% { transform: translate(64px,-37px); opacity: 1; } 70%,100% { transform: translate(64px,-37px); opacity: 0; } }
@keyframes iso-pass { 0% { transform: translate(0,0); opacity: 0; } 6% { opacity: 1; } 92% { transform: translate(138px,-80px); opacity: 1; } 100% { transform: translate(138px,-80px); opacity: 0; } }
@keyframes iso-move { to { offset-distance: 100%; } }

/* ═══ the isometric plates ═════════════════════════════════════════════════
 *
 * Every drawing on this page is one of the reference design's own isometric figures,
 * pasted unmodified. That is deliberate and it is what this section exists to support: a
 * plate strokes and fills itself from `--og-*` inline, and the register block at the top of
 * this file publishes those tokens, so a figure lifted out of the reference needs no re-key
 * and cannot drift from it. The only thing added here is the class `iso`, which carries the
 * motion contract below.
 *
 * `--spd` scales every duration (each plate writes `calc(Ns * var(--spd, 1))`). One knob for
 * the whole page's tempo, and the plates then keep their RELATIVE timings, which is the half
 * that matters: a scheduler whose bars fill faster than its dashes travel is a broken drawing.
 */
.iso {
  --spd: 1;
  font-family: var(--og-font-mono);
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

/*
 * `fill-box`, and it is what makes a progress bar grow from its own left edge rather than
 * from the SVG's origin: without it `scaleX` is measured against the whole viewBox and a
 * 16px bar sweeps the width of the drawing.
 *
 * `left`, not the reference stylesheet's `center`. Of the twenty `.tb` elements here, the
 * seven that run a `scaleX` keyframe (`iso-prog`, `iso-redact`) all want `left` and each one
 * used to say so inline; the other thirteen run translate-only keyframes, where the origin
 * cannot be observed. `center` was therefore the value no element on this page wanted, held
 * by the shared rule and overridden seven times.
 *
 * The plates also inherit their face from here rather than restating it per `<g>`, which is
 * what the pasted markup did fifty-two times over - `body` already sets `--mono` and SVG
 * `<text>` inherits it, so every one of those was a no-op. On the class it is at least a
 * real insulation: a future change to `body`'s face leaves the drawings alone.
 */
.iso .tb {
  transform-box: fill-box;
  transform-origin: left;
}

/*
 * THE MOTION CONTRACT, and the `!important` is load-bearing rather than lazy.
 *
 * `app.js` adds `.play` to a `[data-play]` host on enter and REMOVES it on exit, so only the
 * plate or two actually on screen ever animates. A plate declares its animation with the
 * `animation` SHORTHAND, inline — which both resets `animation-play-state` to `running` and
 * outranks any stylesheet rule that is not `!important`. So both sides of the gate carry it
 * and specificity decides between them, exactly as it would without the flag.
 *
 * Pausing rather than unsetting is the point: a plate resumes mid-cycle where it left off,
 * so scrolling back to a drawing does not restart its story from the top.
 */
.iso * {
  animation-play-state: paused !important;
}
.play .iso * {
  animation-play-state: running !important;
}

/* ═══ the proof verdicts ═══════════════════════════════════════════════════
 *
 * The three lanes a candidate can land in, under the proof-engine card. They are not part
 * of a plate and never were: the conveyor above them says a candidate is INSPECTED, and
 * these say what inspecting it can conclude - which is the half of that card the drawing
 * cannot carry. Only `confirmed` stays lit, because only `confirmed` is stored.
 */

.pill {
  font-size: 11px;
  padding: 2px 8px;
  border-radius: var(--r);
  border: 1px solid var(--border-2);
  color: var(--text-2);
}
.pill.ok {
  color: var(--green);
  border-color: color-mix(in oklab, var(--green) 45%, var(--border-2));
  background: color-mix(in oklab, var(--green) 10%, transparent);
}
.pill.no {
  color: var(--red);
  border-color: color-mix(in oklab, var(--red) 45%, var(--border-2));
  background: color-mix(in oklab, var(--red) 10%, transparent);
}
.pill.maybe {
  color: var(--yellow);
  border-color: color-mix(in oklab, var(--yellow) 45%, var(--border-2));
  background: color-mix(in oklab, var(--yellow) 10%, transparent);
}

.lanepills {
  display: flex;
  gap: 6px;
  margin: 0 0 14px;
  flex-wrap: wrap;
}
.lanepills .pill {
  opacity: 0.32;
}
.play .lanepills .pill {
  animation: v2-lane 3.6s var(--ease) infinite;
  animation-delay: var(--d);
}
.play .lanepills .pill.ok {
  animation-name: v2-lane-win;
}

@keyframes v2-lane {
  0%,
  52% {
    opacity: 0.32;
  }
  62%,
  74% {
    opacity: 1;
  }
  84%,
  100% {
    opacity: 0.32;
  }
}

@keyframes v2-lane-win {
  0%,
  52% {
    opacity: 0.32;
  }
  62%,
  100% {
    opacity: 1;
  }
}

/* ═══ nav + misc v2 chrome ═════════════════════════════════════════════════ */

.nav-inner .glyph {
  border-radius: 22%;
}

/* The install line is a placeholder while gimorra is in alpha: no URL, nothing to copy. */
.copyblock .cmd .soon {
  color: var(--text-2);
}
