/* ═══════════════════════════════════════════════════════════════════
   ORBOX · Canonical tokens  —  drop-in :root + base classes
   Single source of truth mirror of orbox.brand.json.
   Transcribed 1:1 from Claude Design project 019e3093 (styles.css :root + brand.md).
   Include this file first; build every screen from these tokens only.
   ═══════════════════════════════════════════════════════════════════ */

/* ── Display font (place the two cuts in assets/fonts/) ── */
@font-face {
  font-family: "Shapiro";
  src: url("../assets/fonts/Shapiro-35Feather.ttf") format("truetype");
  font-weight: 300; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Shapiro";
  src: url("../assets/fonts/Shapiro-45Welter.ttf") format("truetype");
  font-weight: 500; font-style: normal; font-display: swap;
}

:root {
  /* ── Ink ladder (warm charcoal: text + dark surfaces + all hairlines) ── */
  --ink: #403D38;
  --ink-90: #4a463f;
  --ink-pressed: #2a2823;          /* pressed/darker ink — mirrors the live :root */
  --ink-70: rgba(64,61,56,0.80);   /* body copy — 5.19 on --bg, 5.72 on --card (was .72 = 4.21/4.58) */
  --ink-60: rgba(64,61,56,0.75);   /* kicker, captions — 4.53 on --bg, the minimum alpha that clears AA */
  --ink-40: rgba(64,61,56,0.40);   /* NON-TEXT ONLY: marks, dividers. 2.03 on --bg. Never a word. */
  --ink-20: rgba(64,61,56,0.18);   /* borders + outlines on LIGHT surfaces */
  --ink-10: rgba(64,61,56,0.10);   /* hairlines */
  --ink-06: rgba(64,61,56,0.06);   /* ghost fills */

  /* ── Surfaces (warm sand) ── */
  --bg: #EBE8DF;
  --bg-soft: #E1DDD0;
  --bg-deep: #D8D2C2;
  --panel: #F2EFE6;
  --panel-2: #EAE5D7;
  --card: #FBF8EF;
  --card-2: #F4F0E3;

  /* ── Dark surfaces + the ladder that lives on them ───────────────────
     Orbox is a light system with dark SECTIONS, not a dark theme. There is no
     site-wide inversion: a dark band is a deliberate beat (a statement, a CTA,
     an editorial pull-quote, a footer), and it flips only what sits inside it.
     Never put an --ink-* value on a dark surface: warm charcoal at 18% alpha
     over #403D38 is invisible. Use the on-dark ladder below.

     Alphas are measured against the ground the ladder actually LANDS on, not
     against the bare band. A card on a dark section is
     `background: var(--fill-on-dark)` — an 8% cream wash — so the real ground
     under most on-dark text is #4E4B46, not #403D38, and every step of the
     ladder gives up roughly a fifth of its ratio there. The old alphas were
     specified against #403D38 and looked safe: 60% read 4.56 on the bare band
     and 3.90 on a card, so --on-dark-60 — kickers, captions, placeholders,
     .filter-label, .footer-legal, .stat-caption — failed AA inside the single
     most common container in the system. Not an edge case: the designed case.

     Measured against the washed ground (#4E4B46):
       100% = 7.58 (AAA) · 82% = 5.74 (AA) · 70% = 4.68 (AA, the floor for
       11px mono) · 52% = 3.35 (non-text marks only).
     On the bare band the same steps read 9.48 / 6.99 / 5.59 / 3.85.

     The number in a token name is a TIER INDEX, not an alpha — exactly as
     --ink-70 carries 0.80 and --ink-60 carries 0.75 on the light side. v1.3
     raised the light ladder past its names to clear AA and left the dark twin
     untouched; this is that same correction. The two lower gaps (0.12 and
     0.18) are the ones the original ladder used, so the visual hierarchy is
     the designed one — only the top gap compresses, and it had 3 points of
     headroom to give. Do not lower them.
     And note: --brand as TEXT on dark is 3.23 and fails. Use --brand-soft
     (4.86) for bronze type on a dark band. --brand stays a fill, not a word. */
  --surface-dark: #403D38;              /* the standard dark section (= --ink) */
  --surface-night: #12161C;             /* deep, rare: bookends and footers    */
  --on-dark: #F4F0E3;                   /* text and marks on dark (= --card-2) */
  --on-dark-70: rgba(244,240,227,0.82); /* body copy on dark        */
  --on-dark-60: rgba(244,240,227,0.70); /* kickers, captions        */
  --on-dark-40: rgba(244,240,227,0.52); /* subtle marks, disabled   */
  --line-on-dark: rgba(244,240,227,0.20); /* borders and outlines   */
  --fill-on-dark: rgba(244,240,227,0.08); /* ghost fills            */

  /* ── Night: the app-shell surface family (v1.4) ──────────────────────
     A dark BAND is a beat inside a light page — that is --surface-dark, and
     the rule above still holds. An app SHELL is a different problem: a whole
     product living on dark for hours, with a canvas, raised panels, fields
     and hover states that must separate from each other without any of them
     turning into a band. --surface-dark cannot do that job: it is --ink, so
     everything raised above it collides with the text colour.

     These four are measured off the shipping OrboxOS build (sign-in, chat,
     settings) and then RE-DERIVED as the pack's own wash arithmetic so they
     are reproducible rather than four sampled numbers:
         --night-raised = --night + 4%  --on-dark
         --night-field  = --night + 8%  --on-dark   (exactly one --fill-on-dark)
         --night-hover  = --night + 12% --on-dark
     Measured on the product: #272320 / #2F2C27 / #36332E. Derived: #28241F /
     #302D27 / #39352F. The gap is one step per channel — inside JPEG noise.
     The derived values ship, because a ladder you can recompute is a system
     and a ladder you sampled is a screenshot.

     They ship as RESOLVED HEX, not as stacked translucent fills, because the
     system rule is that ghost fills do not stack: one wash is a plane, two is
     a bug. A field inside a raised panel must not compound to a third colour.

     This family is WARM (#1F1C17 is the sand palette with the light taken
     out). --surface-night #12161C is COOL blue-black and is NOT its darkest
     step — it stays what it always was: the deep, rare bookend and footer
     tone for marketing pages. Two different jobs, two different tokens; do
     not collapse them.

     The whole on-dark ladder was re-measured on all four grounds and clears
     its tiers everywhere (worst case, on --night-hover): --on-dark 10.68 ·
     -70 7.75 · -60 6.15 · -40 4.13. Status words on --night-hover: pos 5.32 ·
     warn 5.40 · info 5.37 · neg 5.33 · --brand-soft 5.48. --brand as a WORD
     is 3.64 there and still fails — the bronze stays a fill on night too. */
  --night:        #1F1C17;   /* app canvas                                 */
  --night-raised: #28241F;   /* panels, cards, menus, sheets     (+4%)     */
  --night-field:  #302D27;   /* inputs, wells, code blocks       (+8%)     */
  --night-hover:  #39352F;   /* hover / pressed on the above     (+12%)    */

  /* ── Brand (bronze / taupe-brown) ── */
  --brand: #A5876B;
  --brand-deep: #7B5B3D;
  --brand-soft: #C7A985;
  --brand-glow: #E8DDC8;
  /* Text that sits ON a --brand fill. White on bronze is 3.35 and fails AA; this deep
     ink is 5.42 and passes, identically on a light or a dark ground because the ratio
     is text-to-fill, not text-to-page. The bronze is a fill, not a word — so when a
     word must sit on it, the word goes dark. */
  --on-brand: #12161C;

  /* ── Accent palette ── */
  --gold: #A5976B;
  --taupe: #A6A19B;
  --sky: #5075A4;
  --sky-soft: #75ABC1;
  --rose: #C3AFBE;
  --olive: #9CA69B;
  --sage: #D8DDD3;
  --sage-deep: #C3CAB9;
  --sage-ink: #3A4036;

  /* ── Status (palette + opacity = state; also chart series) ── */
  --pos: #6E8A5F;
  --pos-soft: #9CA69B;
  --warn: #B8862C;
  --info: #5075A4;
  --info-soft: #75ABC1;
  --neg: #A55353;
  --neg-bright: #F16060;
  --red: #A55353;
  --red-bright: #F16060;
  /* Status FILLS above; status WORDS below. Measured on --bg / --card / --surface-dark:
     pos 3.14 · warn 2.64 · info 3.88 · neg 4.33 — every one of them fails AA as text on
     light, and all four fail on dark. A status colour is a dot, a bar or a pill fill.
     When the state has to be spoken, use the -ink pair on light and the -on-dark pair on
     dark. Both are measured, not eyeballed. */
  --pos-ink:  #546A47;   /* 4.86 on --bg · 5.60 on --card */
  --warn-ink: #805C1E;   /* 4.94 · 5.70 */
  --info-ink: #44648D;   /* 4.96 · 5.73 */
  --neg-ink:  #9A4D4D;   /* 4.84 · 5.58 */
  --pos-on-dark:  #9EB194;   /* 4.72 on --surface-dark · 7.41 on --night */
  --warn-on-dark: #CDA763;   /* 4.79 · 7.52 */
  --info-on-dark: #98AECB;   /* 4.76 · 7.48 */
  --neg-on-dark:  #CFA0A0;   /* 4.73 · 7.43 */
  /* Text that sits ON a --neg fill — the destructive twin of --on-brand.
     Cream reads 4.65 on #A55353 and passes AA; --ink reads 3.42 and does not,
     which is the opposite of the bronze case, because --neg is dark and
     --brand is not. So: dark word on bronze, cream word on red. */
  --on-neg: #F4F0E3;
  /* Destructive hover on LIGHT darkens the fill, which RAISES the label to
     6.02 — hover must never cost contrast (the .btn-primary:hover 2.94 defect
     fixed in v1.3.1 was exactly that mistake). On NIGHT the destructive
     control is a ghost: --neg-on-dark as the word (7.43 on --night), filling
     to --neg on hover. Never raw --neg as a WORD on a dark ground — that is
     the 3.40:1 "Sign out" in the shipping OrboxOS menu. */
  --neg-hover: #8E4444;

  /* ── Placeholder + chart scope (swapped wholesale by .on-dark, never hardcoded
     inside an SVG — a grid stroke written as a literal cannot follow the ground) ── */
  --placeholder: var(--ink-60);
  --chart-grid: var(--ink-10);
  --chart-axis: var(--ink-20);
  --chart-tick: var(--ink-60);
  --chart-halo: rgba(165,135,107,0.22);

  /* ── Lines ── */
  --line: rgba(64,61,56,0.10);
  --line-2: rgba(64,61,56,0.18);

  /* ── Control boundaries (v1.4) — a separate job from a hairline ───────
     --line and --line-2 draw EDGES: a card rim, a section rule, a table
     divider. They measure 1.19 and 1.37 on --card. That is correct for an
     edge — a divider is allowed to whisper — and those values do not change.

     A form control is not an edge. WCAG 1.4.11 requires 3:1 for "visual
     information required to identify user interface components and states",
     and for a text input the boundary IS that information: remove it and the
     field stops being a field. v1.3.1 drew .input with --line on --card —
     1.19:1, a field with no perceptible edge, sitting on a card of the same
     colour. That is the single defect this pair exists to close, and it is
     the same defect measured in the shipping OrboxOS sign-in screen.

     One alpha per side, chosen to clear 3.0 on EVERY ground in the system,
     so a field can be dropped into any surface without a re-check.

     v1.4 raised the light alpha from 0.62 to 0.72, because 0.62 was chosen
     against the wrong backdrop. background-clip is border-box by default, so
     a control's own fill paints UNDER its border and a translucent edge
     composites over that fill, not over the page. Every control in this file
     is --card-filled, and --card is lighter than four of the seven grounds —
     so the edge was being lightened before it was ever measured. Under the
     old model 0.62 landed --bg-deep at exactly 3.00; under the real one it
     lands at 2.47, and every input, select, textarea, checkbox and radio in
     the pack has been below 1.4.11 on the three deepest surfaces since 1.3.
     The numbers below are measured the way the border actually renders:
       light 0.72 over a --card fill →
                    card 4.58 · card-2 4.27 · panel 4.23 · panel-2 3.87 ·
                    bg 3.97 · bg-soft 3.58 · bg-deep 3.22
       dark  0.47 over the worst of the three fills a dark control carries
             (transparent, --fill-on-dark, --night-field) →
                    night 4.30 · raised 4.14 · field 3.92 · hover 3.48 ·
                    surface-dark 3.09 · night(cool) 4.34
     The dark side survived its re-measurement — a cream edge over a ghost
     fill on a dark ground is darkened by the fill, not lightened — so only
     the light alpha moved. Use these ONLY on interactive boundaries.
     Painting a card with them makes the page look like a wireframe. */
  --line-control: rgba(64,61,56,0.72);
  --line-control-on-dark: rgba(244,240,227,0.47);

  /* ── Type families ── */
  --font-display: "Shapiro", "Inter", system-ui, sans-serif;
  --font-text: "Inter", system-ui, sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, monospace;

  /* ── Type scale (nine steps; fluid ranges as clamp) ──
     The three display steps are true fluid ramps between a 375px and a 1440px viewport:
     `preferred` = rem intercept + vw slope, so the size still responds to the user's browser
     font size (WCAG 1.4.4) instead of being pure vw, which does not.

     They used to be `clamp(56px, 5vw, 64px)` / a flat `46px` / `clamp(30px, 6vw, 84px)`, and
     that inverted the hierarchy on a phone. 5vw only reaches 56px at a 1120px viewport, so the
     hero sat pinned at its minimum — 56px — across every phone and tablet, while 6vw pinned the
     big number at 30px below 500px and the section title never moved off 46px at all. The
     desktop order 84 > 64 > 46 became 30 < 46 < 56: the headline outsized the number it was
     introducing. Nothing was wrong with any single value; the ramps simply never overlapped the
     widths people read on. */
  --fs-hero: clamp(40px, 1.97rem + 2.25vw, 64px);
  --fs-section: clamp(32px, 1.69rem + 1.31vw, 46px);
  --fs-card-title: clamp(22px, 2vw, 24px);
  --fs-sub-title: clamp(18px, 1.6vw, 20px);
  --fs-body-lead: 16px;
  --fs-body: 14.5px;
  --fs-caption: 12.5px;
  --fs-kicker: 11px;
  --fs-big-number: clamp(44px, 1.87rem + 3.76vw, 84px);

  /* ── Radii ── */
  --r-xs: 6px;
  --r-sm: 10px;
  --r-md: 14px;
  --r-lg: 20px;
  --r-xl: 28px;
  --r-pill: 999px;
  --r-tag: 4px;

  /* ── Control masks (v1.4) ────────────────────────────────────────────
     The checkbox mark, as a MASK rather than a background-image, so its colour
     comes from a token (--card-2 on light, --ink on dark) instead of being
     baked into the data URI. A baked mark is the reason inverted checkboxes
     show a cream tick on a cream box the moment a band flips.
     Geometry is Phosphor's own `check` and `minus` paths, unmodified — the
     same file that ships in assets/icons/regular/, so the mark inside a
     control and the icon beside it are literally the same drawing. */
  --mask-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath d='M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z'/%3E%3C/svg%3E");
  --mask-minus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath d='M224,128a8,8,0,0,1-8,8H40a8,8,0,0,1,0-16H216A8,8,0,0,1,224,128Z'/%3E%3C/svg%3E");

  /* ── Elevation (soft, warm-tinted) ── */
  --shadow-1: 0 1px 0 rgba(64,61,56,0.04), 0 1px 2px rgba(64,61,56,0.04);
  --shadow-2: 0 2px 12px rgba(64,61,56,0.05), 0 1px 2px rgba(64,61,56,0.04);
  --shadow-3: 0 14px 40px -18px rgba(64,61,56,0.18), 0 1px 2px rgba(64,61,56,0.04);
  --shadow-4: 0 30px 80px -30px rgba(64,61,56,0.28), 0 4px 12px rgba(64,61,56,0.06);

  /* The scrim (v1.4). Elevation's other half: shadows say "above", the scrim says
     "and nothing below is live". It is --night at 0.62, warm for the same reason the
     four shadows above are warm — a neutral black-out over a sand page goes grey and
     cold exactly at the seam where the page still shows past the dialog, which is the
     one place anybody looks. 0.62 is derived, not chosen: the boundary between the
     dialog and the dimmed page crosses 1.4.11's 3:1 at 0.48, and 0.62 is the first
     step at which even the worst ground — a dialog opened over a --card region, the
     lightest thing the scrim has to darken — clears 4.5 (4.65, rising to 5.81 over
     --bg-deep). A modal's edge is the last boundary in the system that should be
     marginal. See the Dialog block for what this does NOT solve on a dark ground. */
  --scrim: rgba(31,28,23,0.62);

  /* ── Motion ── */
  --ease-ui: cubic-bezier(.22,.61,.36,1);
  --dur-fast: 180ms;
  --dur-base: 280ms;
  --dur-slow: 420ms;
  --stagger: 90ms;
  --loop-sphere: 12s;
  --lift-hover: -2px;
  --press-scale: 0.985;
  /* The four durations MOTION.md names that are not UI transitions. They live here so the
     "no literal durations" rule is satisfiable: a caret blink and a data ping are still
     system values, they just sit outside the 180-420ms band on purpose. */
  --ease-ping: cubic-bezier(0,0,.2,1);
  --loop-ping: 2.4s;        /* the live-data ripple, one per 2.4s — never faster */
  --dur-caret: 1s;          /* typewriter caret blink, linear */
  --type-in: 45ms;          /* typewriter, per character in  */
  --type-out: 25ms;         /* typewriter, per character out */
  --type-hold: 1.6s;        /* typewriter, hold at full string */

  /* ── Layout constants ── */
  --content-max: 1216px;
  --app-shell-max: 1640px;
  --page-pad: 32px;
  --section-pad: 112px;

  /* ── Live knobs (Tweaks panel) ── */
  --scale: 1;
  --density: 1;
  --grain-opacity: 0.5;
}

/* ── Focus ────────────────────────────────────────────────────────────
   One bronze value cannot serve both grounds: brand-deep is 5.03 on light
   and 1.75 on dark; brand-soft is the reverse (1.82 / 4.86). So the ring
   is a PAIR, swapped by .on-dark. Never outline:none without a replacement,
   never let a focus rule touch geometry, and never let a wash be the whole
   indicator — before v1.3 every ring in the system measured 1.07 to 1.82. */
:root {
  --focus-ring: var(--brand-deep);
  --focus-width: 2px;
  --focus-offset: 2px;
}
.on-dark { --focus-ring: var(--brand-soft); }

:focus-visible {
  outline: var(--focus-width) solid var(--focus-ring);
  outline-offset: var(--focus-offset);
}

/* ── Base ── */

/* v1.5 (Z1-box-sizing). Neither line declared this, in either the pack or rc.
   Without it `.input { width: 100% }` plus 18px of padding plus a 1px border
   lays out 38px wider than its rail, and every padded box in the system is
   quietly the wrong size. It is one line and it is load-bearing: the whole
   measured geometry of this pack — the 1216 rail, the 44px targets, the
   48/52px affix compensation — assumes the border-box model. */
*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--ink-70);
  font-family: var(--font-text);
  font-size: var(--fs-body);
  line-height: 1.6;
  letter-spacing: -0.005em;
  font-feature-settings: "tnum" 1;      /* tabular numbers by default */
  -webkit-font-smoothing: antialiased;
}

/* ── Type-scale utility classes (sentence case only) ── */
/* 300, not 500. The pack bundles Shapiro 35 Feather specifically as the large light display cut
   and this is the only role that asks for it — with .t-hero hardcoded at 500 the file was
   downloaded on every page and never rendered a glyph. Feather holds down to the 40px floor of
   the clamp above; below that it would go wispy, which is why the floor is 40 and not 32. */
.t-hero        { font-family: var(--font-display); font-size: var(--fs-hero);       font-weight: 300; letter-spacing: -0.03em;  line-height: 1.02; color: var(--ink); text-wrap: balance; }
.t-section     { font-family: var(--font-display); font-size: var(--fs-section);    font-weight: 500; letter-spacing: -0.028em; line-height: 1.05; color: var(--ink); text-wrap: balance; }
.t-card-title  { font-family: var(--font-display); font-size: var(--fs-card-title); font-weight: 500; letter-spacing: -0.025em; line-height: 1.15; color: var(--ink); }
.t-sub-title   { font-family: var(--font-display); font-size: var(--fs-sub-title);  font-weight: 500; letter-spacing: -0.02em;  line-height: 1.2;  color: var(--ink); }
.t-body-lead   { font-family: var(--font-text); font-size: var(--fs-body-lead); font-weight: 400; line-height: 1.6; color: var(--ink-70); text-wrap: pretty; }
.t-body        { font-family: var(--font-text); font-size: var(--fs-body);      font-weight: 400; line-height: 1.6; color: var(--ink-70); }
.t-caption     { font-family: var(--font-text); font-size: var(--fs-caption);   font-weight: 400; line-height: 1.5; color: var(--ink-60); }
.t-big-number  { font-family: var(--font-display); font-size: var(--fs-big-number); font-weight: 500; letter-spacing: -0.03em; line-height: 0.95; color: var(--ink); font-feature-settings: "tnum" 1; }
.kicker        { font-family: var(--font-mono); font-size: var(--fs-kicker); font-weight: 500; letter-spacing: 0; line-height: 1.4; color: var(--ink-60); text-transform: none; }
.st-accent     { color: var(--brand-deep); }   /* second-line emphasis — never italic, never another font */

/* ── Layout ── */
.content-rail { max-width: var(--content-max); margin: 0 auto; padding-inline: var(--page-pad); }
.app-shell    { max-width: var(--app-shell-max); margin: 0 auto; padding-inline: var(--page-pad); }
.section      { padding-block: var(--section-pad); }
/* The head is a content atom. It is placed *inside* a rail, so it does not carry one.
   It used to: `max-width: var(--content-max)` + `margin: 0 auto` + `padding-inline:
   var(--page-pad)` — a second, private copy of the rail — while all 29 call sites in this
   pack nest it inside `.content-rail`. Two rails stacked means two paddings. Measured at
   1440: the rail's content edge is x=144, and every section title on the landing sat at
   x=176 — 32px inboard of the cards beneath it, on all seven sections, and on all 22 in
   the gallery. The divider still looked right, because the *box* filled the parent and only
   the text was inset; that is why it survived every review. And the private 1216 cap would
   have clipped the head 424px narrower than its own content on the 1640 app shell.
   A component that carries its own rail can only ever be correct on one rail. This one now
   inherits whichever rail it is placed in, so it is correct on both. */
.section-head-row {
  display: grid; grid-template-columns: 1fr auto; gap: 32px; align-items: end;
  padding-bottom: 20px; border-bottom: 1px solid var(--line); margin-bottom: 28px;
}
.section-head-row .sub { max-width: 38ch; text-align: right; color: var(--ink-60); }

/* ── Card ── */
.card {
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-lg); box-shadow: var(--shadow-1);
  padding: 32px;
  transition: transform var(--dur-base) var(--ease-ui), box-shadow var(--dur-base) var(--ease-ui);
}
@media (hover: hover) and (pointer: fine) {
  .card:hover { transform: translateY(var(--lift-hover)); box-shadow: var(--shadow-2); }
}
.card--dark { background: var(--ink); color: var(--card-2); border: none; }

/* The head *inside* a card — kicker + title left, one meta element right. This is the second
   head shape the system has, and it is not a smaller `.section-head-row`: that one divides a
   page section and owns a rule; this one opens a card and owns nothing.
   It lived in the dashboard example's local stylesheet until now, which is a call site
   inventing a component — the next app screen would have invented it again, differently. Two
   details are load-bearing and are the reason it is worth promoting rather than re-deriving:
   `flex-wrap: wrap` because `.card` sets `overflow: hidden`, so an unwrapped head silently
   clips the right-hand pill below ~340px instead of dropping it under the title; and
   `align-items: flex-start`, because the title is the taller element and a pill centred
   against two lines of title reads as misaligned. */
.card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px 24px; margin-bottom: 24px; flex-wrap: wrap; }
.card-head .kicker { margin-bottom: 8px; }

/* ── Acronym tag (DLD/RERA/VARA) ──────────────────────────────────────
   Mono, fit-content, never widened, never used for emphasis (that is the
   accent tag). Three weights of the same quiet device:
     .tag-acronym          outline   — the default, and what you reach for
     .tag-acronym--fill    ghost fill — for dense lists, where 6 outlines buzz
     .tag-acronym--solid   inverted   — one per view, marks the verified one   */
/* The `text-decoration: none` below is not decoration hygiene, it is the fifth instance of one
   bug. Any of these chips can be a link — the gallery makes all three of them anchors, to show
   the focus ring — and an anchor that is a box gets the browser's underline drawn through the
   whole inline box, padding included, in the UA's link blue. Rendered, the fact-pill read as a
   blue underlined pill. `.pager-item` had this hole, then `.btn`, then these three. The gate in
   scripts/verify_orbox.py now derives the required set from the markup: any pack class that
   builds a box and lands on an `<a>` must kill the underline. Prose links keep theirs. */
.tag-acronym {
  display: inline-block; width: fit-content;
  padding: 3px 8px; border: 1px solid var(--ink-20); background: transparent;
  border-radius: var(--r-tag); color: var(--ink-70); text-decoration: none;
  font-family: var(--font-mono); font-size: var(--fs-kicker); font-weight: 500; line-height: 1.2;
  letter-spacing: 0;   /* mono roles carry zero tracking; without this it inherits body -0.005em */
}
.tag-acronym--fill  { background: var(--ink-06); border-color: transparent; }
.tag-acronym--solid { background: var(--ink); border-color: transparent; color: var(--card-2); }

/* ── Accent tags (display, pill, rare) ── */
.atag { display: inline-flex; align-items: center; font-family: var(--font-display); font-weight: 500;
        border-radius: var(--r-pill); padding: 4px 12px; font-size: 13px; line-height: 1.2;
        text-decoration: none; }   /* a chip that is a link is still a chip — see .tag-acronym above */
.atag-brand { background: var(--brand); color: var(--on-brand); }
.atag-ink   { background: var(--ink); color: var(--card-2); }
.atag-sage  { background: var(--sage); color: var(--sage-ink); }
.atag-ghost { background: transparent; color: var(--ink); border: 1px solid var(--ink-20); }

/* ── Buttons (13.5px/500 + arrow › glyph by default, visible hover) ──
   The arrow is a default and not a law: `.btn-danger` withdraws it and
   `.btn--noarrow` opts out — see the destructive-control block below. */
.btn {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--font-text); font-size: 13.5px; font-weight: 500;
  /* The second time this exact line was missing. `.pager-item` had the same hole and got the
     same fix last version; this one is worse, because `.btn` on an `<a>` is the primary CTA —
     the landing's "Request beta access" shipped underlined. A `<button>` hides the bug, which
     is why it survives review: the specimen you look at is usually the button. There is now a
     gate for it (`--pack`), keyed off the classes the examples actually put on an `<a>`, so
     the third instance fails the build instead of shipping. */
  text-decoration: none;
  border: none; border-radius: var(--r-pill); padding: 11px 18px; cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-ui), background var(--dur-fast) var(--ease-ui);
}
.btn::after { content: "›"; font-size: 1.1em; line-height: 0; }
@media (hover: hover) and (pointer: fine) {
  .btn:hover { transform: translateY(var(--lift-hover)); }
  /* Colour hovers belong inside the gate too. Outside it, a tapped button on iOS keeps
     the hover fill until you tap elsewhere — the sticky-hover bug, in the loudest
     possible place. */
  /* The hover fill darkens past the point where the dark label still works:
     --on-brand (#12161C) on --brand is 5.42, on --brand-deep it is 2.94. The fill
     was correct and the label came along unchanged, so the loudest control in the
     system dropped below AA in the one state a pointer user sees most. Cream on
     --brand-deep is 5.41, so the label flips with the fill — the same pairing
     .btn-dark already uses for a cream word on a dark plane. Both endpoints of the
     transition clear AA; nothing in between is a resting state.
     .btn-brand is documented as an alias of .btn-primary and had no colour hover at
     all, which made the claim false on the one band where the two are meant to match.
     It hovers with it now — and only here: inside .on-dark the two deliberately part
     company (primary inverts to light, brand keeps the bronze), so brand hovers to
     --brand-deep on a dark band too, which is still 5.41 with the cream label. */
  .btn-primary:hover,
  .btn-brand:hover   { background: var(--brand-deep); color: var(--card-2); }
  .btn-dark:hover    { background: var(--ink-pressed); }
  .btn-ghost:hover   { background: var(--ink-10); }
  /* Colour restated, not inherited by source order: the light-band rule above is
     (0,2,0) and so is `.on-dark .btn-primary` further down, so which one paints the
     hover label depended on which line came last. That is how the .card--dark bug
     got in. This is (0,3,0) and it says what it means. */
  .on-dark .btn-primary:hover { background: var(--card); color: var(--ink); }
  .on-dark .btn-dark:hover,
  .on-dark .btn-ghost:hover   { background: rgba(244,240,227,0.16); }
}
.btn:active  { transform: scale(var(--press-scale)); }
/* Vocabulary follows the live app (iron rule: live wins). Before v1.3 the pack called
   the ink button .btn-primary, the live app calls it .btn-dark and gives
   .btn-primary the bronze fill. A delegate reading the pack shipped the wrong colour. */
.btn-primary { background: var(--brand); color: var(--on-brand); box-shadow: var(--shadow-2); }
.btn-dark    { background: var(--ink); color: var(--card-2); }
.btn-brand   { background: var(--brand); color: var(--on-brand); }   /* alias of primary, kept for existing markup */
.btn-ghost   { background: var(--ink-06); color: var(--ink); }
.btn[disabled], .btn:disabled, .btn[disabled]:hover, .btn:disabled:hover {
  cursor: not-allowed;
  /* never opacity: it lands on dead grey. Keep the label readable. */
  background: var(--ink-06); color: var(--ink-70); box-shadow: none; transform: none;
}

/* ── Dark sections ────────────────────────────────────────────────────
   Wrap a band in .on-dark and the components inside it flip themselves.
   You never hand-write an inverted variant at the call site, and you never
   inline a hex to fake one. Add .on-dark--night for the deep surface.

     <section class="section on-dark"> … </section>

   What flips: type colour, hairlines, ghost fills, acronym tags, accent
   ghost tags, buttons, the input and its focus ring, fact pills, cards.
   What does NOT flip: --brand stays --brand. The bronze is the constant
   across both surfaces; that is what makes a dark band read as Orbox and
   not as a different product.                                            */
.on-dark { background: var(--surface-dark); color: var(--on-dark-70); }
.on-dark--night { background: var(--surface-night); }

/* ── .on-dark--shell — the app-shell modifier (v1.4) ─────────────────────
   Written as a MODIFIER of .on-dark (same convention as .on-dark--night), so
   it inherits all 60-odd .on-dark rules instead of forking them. Markup is
   `class="on-dark on-dark--shell"`. Nothing below re-implements a component;
   it only repoints the surface variables at the night family.

   The wash re-bases per container instead of stacking. The shell canvas hands
   its children --fill-on-dark = --night-raised; a card re-bases it to
   --night-field for anything nested inside it. So a ghost button is always
   exactly one rung above whatever it sits on, and never two washes deep.
   This is the "ghost fills do not stack" rule expressed as inheritance. */
.on-dark--shell {
  background: var(--night);
  --surface-dark: var(--night);       /* so .topbar's color-mix follows the shell */
  --fill-on-dark: var(--night-raised);
}
/* Nothing else belongs in this block. Every other .on-dark--shell rule in this
   file sits immediately AFTER the `.on-dark X` rule it modifies — .input in the
   field block, .textarea, .select, .menu, .dialog and the whole hover set. That
   placement is not tidiness, it is the only thing holding those rules up:
   `.on-dark--shell .card` and `.on-dark .card` are both (0,2,0), and a tie at
   equal specificity is decided by source order alone. The card, card--dark and
   nav rules were first drafted here, above the bases they modify, and lost the
   cascade silently for two versions. They now live beside their bases, each with
   the note that belongs to it. Keep only the variable re-bases above. */

.on-dark .t-hero,
.on-dark .t-section,
.on-dark .t-card-title,
.on-dark .t-sub-title,
.on-dark .t-big-number { color: var(--on-dark); }
.on-dark .t-body-lead,
.on-dark .t-body { color: var(--on-dark-70); }
.on-dark .t-caption,
.on-dark .kicker { color: var(--on-dark-60); }
.on-dark .st-accent { color: var(--brand-soft); }

.on-dark .card { background: var(--fill-on-dark); border-color: var(--line-on-dark);
                 color: var(--on-dark-70); box-shadow: none; }
.on-dark .card--dark { background: var(--surface-night); }

/* The shell's two card rules, and why they are HERE rather than up in the
   .on-dark--shell intro where they were written. Both pairs tie exactly —
   `.on-dark--shell .card` vs `.on-dark .card`, `.on-dark--shell .card--dark` vs
   `.on-dark .card--dark`, (0,2,0) against (0,2,0) — so the later rule wins, and
   for two versions the later rule was the base. What made it hard to see is that
   the failure was PARTIAL: custom properties cascade per property, so the
   `--fill-on-dark: var(--night-field)` re-base declared in the losing block still
   reached the element, and `.on-dark .card`'s `background: var(--fill-on-dark)`
   then resolved to it. A shell card painted --night-field: not unstyled, not
   obviously broken, just one rung too light, everywhere, forever. .card--dark was
   worse — it fell through to --surface-night #12161C, which is the COOL dark and
   not a member of the warm night family at all. Both were found by reading
   getComputedStyle off a rendered shell, not by reading the file. */
/* `.panel` used to be in this selector list beside `.card`. It was removed in
   v1.4 because the pack has no `.panel` component and never did — the landing's
   `.band-panel` is a local artifact class, and `--panel` / `--panel-2` are band
   grounds, not a surface component. A rule in the SSOT for a component that does
   not exist is a claim the pack cannot honour. */
.on-dark--shell .card {
  background: var(--night-raised);
  border-color: var(--line-on-dark);
  --fill-on-dark: var(--night-field); /* re-base: children step up one rung */
}
.on-dark--shell .card--dark { background: var(--night); }

/* .card--dark is a dark surface that arrives WITHOUT the .on-dark wrapper, so not one
   of the sixty `.on-dark X` flips above reaches inside it. Its own `color: var(--card-2)`
   only wins where a child sets no colour of its own — and .t-card-title, .t-body,
   .t-sub-title and .kicker all set one. The result was --ink text on an --ink card:
   1:1, six lines of it, in the gallery's own specimen for this variant. It survived
   every static gate because every rule involved is individually correct; it took
   scripts/render_check.py resolving the cascade in a real browser to see it.
   Kept as a repair rather than a deletion because the variant is documented and used.
   The scope is text only: for controls on a dark card — buttons, inputs, chips —
   write `class="card card--dark on-dark"` and the full ladder applies. */
.card--dark .t-hero,
.card--dark .t-section,
.card--dark .t-card-title,
.card--dark .t-sub-title,
.card--dark .t-big-number { color: var(--on-dark); }
.card--dark .t-body-lead,
.card--dark .t-body       { color: var(--on-dark-70); }
.card--dark .t-caption,
.card--dark .stat-caption,
.card--dark .kicker       { color: var(--on-dark-60); }
.card--dark .st-accent    { color: var(--brand-soft); }

.on-dark .tag-acronym { border-color: var(--line-on-dark); color: var(--on-dark-70); }
.on-dark .tag-acronym--fill  { background: var(--fill-on-dark); border-color: transparent; }
.on-dark .tag-acronym--solid { background: var(--on-dark); border-color: transparent; color: var(--ink); }

.on-dark .atag-ghost { color: var(--on-dark); border-color: var(--line-on-dark); }
.on-dark .atag-ink   { background: var(--on-dark); color: var(--ink); }

/* Primary commit inverts to light; brand keeps the bronze and becomes the
   loud one — this is the block COMPONENTS.md means by "CTAs in dark blocks". */
.on-dark .btn-primary { background: var(--on-dark); color: var(--ink); }
.on-dark .btn-ghost   { background: var(--fill-on-dark); color: var(--on-dark); }
/* Attribute selector on purpose: .on-dark .btn-primary is (0,2,0) and would otherwise
   repaint a disabled button bronze-inverted. This is (0,3,0) and wins on every variant.
   And the label is --on-dark-70 (5.74 on this button's own --fill-on-dark ground), not
   --on-dark-40 (3.35) — the on-dark twin of "--ink-40 carries no words" is
   "--on-dark-40 carries no words". */
.on-dark .btn[disabled], .on-dark .btn:disabled,
.on-dark .btn[disabled]:hover, .on-dark .btn:disabled:hover {
  background: var(--fill-on-dark); color: var(--on-dark-70); box-shadow: none; transform: none;
}
.on-dark .btn-dark { background: var(--surface-night); color: var(--on-dark); }

/* v1.4: --line-on-dark is 1.81 on a dark band — an edge, not a control. Same
   correction as the light side, same reason. */
.on-dark .input { background: var(--fill-on-dark); border-color: var(--line-control-on-dark);
                  color: var(--on-dark); }
.on-dark .input::placeholder { color: var(--on-dark-60); }   /* 4.68 on the input's own
   --fill-on-dark ground — a placeholder is text, and it is measured where it lands.
   On the shell it lands on --night-field and reads 6.73. The shipping OrboxOS
   placeholder measures 3.29 — that is the defect this token exists to prevent. */
.on-dark .input:focus { border-color: var(--focus-ring);
                        box-shadow: 0 0 0 4px rgba(199,169,133,0.16); }
.on-dark--shell .input { background: var(--night-field); }

@media (hover: hover) and (pointer: fine) {
  /* no shadow step on dark: a card lifts with the 8% wash and the 20% hairline */
  .on-dark .card:hover { transform: translateY(var(--lift-hover));
                         background: rgba(244,240,227,0.12); border-color: rgba(244,240,227,0.28); }
  .on-dark .nav a:hover      { color: var(--on-dark); background: var(--fill-on-dark); }
  .on-dark .pager-item:hover { color: var(--on-dark); background: var(--fill-on-dark); }
  .on-dark .footer-link:hover { color: var(--on-dark); }
  /* --brand-deep is 5.03 on light and 1.75 on dark. Same swap as .st-accent. */
  .on-dark .brandmark:hover .wordmark { color: var(--brand-soft); }
}
.on-dark .section-head-row { border-bottom-color: var(--line-on-dark); }
.on-dark .section-head-row .sub { color: var(--on-dark-60); }
.on-dark { --placeholder: var(--on-dark-60);
           --chart-grid: var(--line-on-dark); --chart-axis: rgba(244,240,227,0.28);
           --chart-tick: var(--on-dark-60);   --chart-halo: rgba(199,169,133,0.26); }
.on-dark .status-chip--pos  { color: var(--pos-on-dark);  border-color: var(--line-on-dark); }
.on-dark .status-chip--warn { color: var(--warn-on-dark); border-color: var(--line-on-dark); }
.on-dark .status-chip--info { color: var(--info-on-dark); border-color: var(--line-on-dark); }
.on-dark .status-chip--neg  { color: var(--neg-on-dark);  border-color: var(--line-on-dark); }
.on-dark .chip { border-color: var(--line-on-dark); color: var(--on-dark-70); }
.on-dark .chip[aria-pressed="true"] { background: var(--on-dark); color: var(--ink); border-color: transparent; }
.on-dark .stat-caption { color: var(--on-dark-60); }
/* The switch on dark, mirroring .on-dark .check > input token for token. Every
   rule below is written one selector deeper than the light rule it replaces —
   `.on-dark .switch:checked` is (0,3,0) against `.switch:checked`'s (0,2,0) —
   for the reason the status-chip block spells out: at equal specificity the
   later rule wins, and .switch:checked lives ~650 lines further down this
   file, so a (0,2,0) dark override would silently lose and repaint every
   checked switch in every dark band bronze. Measured on the six dark grounds:
   the unchecked edge --line-control-on-dark reads 3.44 at worst, the checked
   --on-dark track 9.48, and the --ink knob on it 9.48. */
.on-dark .switch { background: transparent; border-color: var(--line-control-on-dark); }
.on-dark .switch::after { background: var(--on-dark); }
.on-dark .switch:checked { background-color: var(--on-dark); border-color: var(--on-dark); }
.on-dark .switch:checked::after { background: var(--ink); }
.on-dark .switch:disabled { background: var(--fill-on-dark); border-color: var(--line-on-dark); }
.on-dark .switch:disabled::after { background: var(--on-dark-60); }
.on-dark .switch-row { color: var(--on-dark); }
.on-dark .switch-row:has(> .switch:disabled) { color: var(--on-dark-60); }

.on-dark .fact-pill { border-color: var(--line-on-dark); }
.on-dark .fact-pill .label { color: var(--on-dark-60); }
.on-dark .fact-pill .value { color: var(--on-dark); }

/* ── Shell atoms on dark ──────────────────────────────────────────────
   Same contract as everything above: the band flips them, the call site never does.
   Note that every one of these is written one selector deeper than the light rule it
   replaces. That is not style — this dark block sits ABOVE the light definitions in
   this file, so an override at equal specificity would lose the cascade on source
   order and render exactly like no override at all. The harness checks the winner,
   not the existence. */
.on-dark .topbar { background: color-mix(in srgb, var(--surface-dark) 88%, transparent);
                   border-bottom-color: var(--line-on-dark); }
.on-dark .brandmark { color: var(--on-dark); }
.on-dark .nav a { color: var(--on-dark-70); }
.on-dark .nav a[aria-current="page"] { background: var(--fill-on-dark); color: var(--on-dark); }
/* Same tie, same cure: both selectors are (0,3,1), so this must follow the line
   above it. Inside the shell the current page needs --night-field, one rung above
   the --night-raised topbar it sits in; when it lost, it inherited the shell's
   --fill-on-dark and painted --night-raised — the current tab and the bar behind
   it exactly the same colour, which reads as no current tab at all. */
.on-dark--shell .nav a[aria-current="page"] { background: var(--night-field); color: var(--on-dark); }
.on-dark .account .who { color: var(--on-dark-70); }

.on-dark .search .icon { color: var(--on-dark-60); }
.on-dark .filter-label { color: var(--on-dark-60); }

.on-dark .pager-item { color: var(--on-dark-70); }
/* A light island, and the one licensed --ink inside a dark band: the current page paints
   itself --on-dark (#F4F0E3) first, so the ink is sitting on 0.88 luminance, not on the
   band. Same move as .on-dark .chip[aria-pressed="true"] above. */
.on-dark .pager-item[aria-current="page"]   { background: var(--on-dark); color: var(--ink); }
.on-dark .pager-item[aria-disabled="true"]  { background: var(--fill-on-dark); color: var(--on-dark-70); }
.on-dark .pager-gap { color: var(--on-dark-60); }

.on-dark .footer-link  { color: var(--on-dark-70); }
.on-dark .footer-base  { border-top-color: var(--line-on-dark); }
.on-dark .footer-legal { color: var(--on-dark-60); }

/* The warm lens is a ground, so it re-grounds: sand and glow on light, night and a
   stronger bronze bloom on dark. The bronze is the constant across both. */
.on-dark .lens {
  background-color: var(--surface-night);
  background-image:
    radial-gradient(120% 92% at 20% 8%,  color-mix(in srgb, var(--brand) 62%, transparent) 0%, transparent 58%),
    radial-gradient(96% 78% at 88% 100%, color-mix(in srgb, var(--brand-glow) 16%, transparent) 0%, transparent 64%);
}
.on-dark .lens::after {
  background: linear-gradient(200deg,
    color-mix(in srgb, var(--brand-glow) 20%, transparent) 0%, transparent 48%,
    color-mix(in srgb, var(--surface-night) 55%, transparent) 100%);
}
/* No shadow on dark. The sphere steps up with the wash and the hairline, like the card. */
.on-dark .orb--thinking { box-shadow: none; }
.on-dark .band-rule { background: var(--line-on-dark); }

/* ── Input (pill, brand focus ring, typewriter placeholder) ── */
/* v1.4: the border was --line (1.19 on --card). A card rim may whisper; the
   edge of a text field may not — it is the only thing that says "field", and
   1.4.11 puts that at 3:1. --line-control composites over the field's own
   --card fill and still reads 4.58 there, 3.22 on the deepest ground in the
   system. Nothing else about the control changed. */
.input {
  width: 100%; border: 1px solid var(--line-control); background: var(--card);
  border-radius: var(--r-pill); padding: 12px 18px;
  font-family: var(--font-text); font-size: var(--fs-body); color: var(--ink);
  transition: box-shadow var(--dur-base) var(--ease-ui), border-color var(--dur-base) var(--ease-ui);
}
/* The 8% bronze wash is a brand signature, not an indicator. It stays, and the
   real ring sits on top of it.
   v1.4: the focused border was --brand-soft, which measures 2.09 on --card —
   the focused state was drawn in a colour weaker than the resting one. It now
   uses --focus-ring, which is already --brand-deep on light (5.81 on --card)
   and --brand-soft on dark (7.63 on --night, 4.86 on --surface-dark). One
   token, correct on both sides, and focus can no longer cost contrast.
   v1.4: the two rules below were in the opposite order, and the comment on the
   outline:none line said ":focus-visible below" while :focus-visible was above it.
   Both are (0,2,0), so the later one won and .input was the one control in the
   system whose keyboard ring never painted — measured `outline: none 0px` on a
   focused field. .textarea and .select were always written in this order and were
   always correct; .input has been brought into line with them rather than given a
   rule of its own. The render harness's focus walk did not catch it, because the
   border-colour change and the wash are themselves a difference from rest: a walk
   can only ask whether focus changes anything, and something did change. What
   caught it was reading the two idioms side by side. */
.input:focus { outline: none; }   /* :focus-visible below puts the real ring back */
.input:focus-visible, .input:focus:not(:focus-visible) {
  border-color: var(--focus-ring);
  box-shadow: 0 0 0 4px rgba(165,135,107,0.08);
}
.input:focus-visible { outline: var(--focus-width) solid var(--focus-ring); outline-offset: var(--focus-offset); }
/* (0,4,0) — the light pointer-focus rule above is (0,3,0) and declared later, so without
   this the dark band silently lost its border and its wash. */
.on-dark .input:focus:not(:focus-visible) { border-color: var(--focus-ring); box-shadow: 0 0 0 4px rgba(199,169,133,0.16); }
.input::placeholder { color: var(--placeholder); }

/* ══════════════════════════════════════════════════════════════════════
   FORM LAYER (v1.4)
   Derived, not invented. Every control below was measured off the shipping
   OrboxOS build (sign-in and settings) and then rebuilt on this pack's own
   tokens, which is why six contrast failures in that build disappear here
   rather than being carried forward — along with the seven more that sit at
   the line. AUDIT-ORBOXOS.md carries all fourteen findings, two readings
   each. A form is where a design system is actually tested: it is the only
   place where colour, type, motion, focus, state, error and assistive text
   all have to agree at once.

   Structure is always the same three rows, and the wiring is not optional:
     <div class="field">
       <label class="field-label" for="ID">Label <span class="req"
              aria-hidden="true">*</span></label>
       <input class="input" id="ID" required aria-describedby="ID-msg"
              aria-invalid="true">
       <p class="field-msg field-msg--error" id="ID-msg">…</p>
     </div>
   label[for] → the control. aria-describedby → the message. aria-invalid on
   the control, never colour alone. The form carries `novalidate` and renders
   its own messages: the native bubble cannot be styled, cannot be read where
   the error is, and in the shipping build it lands ~350px left and ~320px
   above the field it refers to, on top of an unrelated nav item.
   ══════════════════════════════════════════════════════════════════════ */

.field { display: grid; gap: 6px; }
.field-label {
  font-family: var(--font-text); font-size: 13.5px; font-weight: 500;
  line-height: 1.35; color: var(--ink);
}
.field-label .req { color: var(--neg-ink); margin-inline-start: 2px; }
/* Help and error are the same slot, so a field never changes height when it
   goes invalid — the row is reserved, only the words change. */
.field-msg {
  display: flex; align-items: flex-start; gap: 6px;
  font-family: var(--font-text); font-size: 12.5px; line-height: 1.45;
  color: var(--ink-60); margin: 0;
}
.field-msg .icon { margin-block-start: 1px; }
.field-msg--error   { color: var(--neg-ink); }   /* 5.58 on --card · 4.84 on --bg */
.field-msg--success { color: var(--pos-ink); }   /* 5.60 · 4.86 */
.on-dark .field-label       { color: var(--on-dark); }
.on-dark .field-label .req  { color: var(--neg-on-dark); }
.on-dark .field-msg         { color: var(--on-dark-60); }
.on-dark .field-msg--error  { color: var(--neg-on-dark); }  /* 7.43 on --night */
.on-dark .field-msg--success{ color: var(--pos-on-dark); }

/* ── Field states ─────────────────────────────────────────────────────
   Invalid is drawn on the border AND spoken in the message AND flagged on
   aria-invalid. Three channels, because colour is one. */
.input[aria-invalid="true"], .textarea[aria-invalid="true"], .select[aria-invalid="true"] {
  border-color: var(--neg-ink);                    /* 5.58 on --card */
  box-shadow: 0 0 0 4px rgba(154,77,77,0.10);
}
.on-dark .input[aria-invalid="true"], .on-dark .textarea[aria-invalid="true"],
.on-dark .select[aria-invalid="true"] {
  border-color: var(--neg-on-dark);                /* 4.73 on --surface-dark */
  box-shadow: 0 0 0 4px rgba(207,160,160,0.14);
}
/* Disabled is exempt from 1.4.3, which is not a licence to make it illegible —
   it is a licence to stop fighting for 4.5. Never opacity: it lands on dead
   grey and drags the border under 1:1. */
.input:disabled, .textarea:disabled, .select:disabled {
  background: var(--ink-06); color: var(--ink-60);
  border-color: var(--line-2); cursor: not-allowed; box-shadow: none;
}
.on-dark .input:disabled, .on-dark .textarea:disabled, .on-dark .select:disabled {
  /* --on-dark-60, not -40: the -40 tier is a non-text mark on both ladders and
     may never carry a word, disabled or not. The light twin uses --ink-60 for
     the same reason. Disabled is exempt from 1.4.3; it is not exempt from the
     system's own rule about which steps are allowed to be read. */
  background: var(--fill-on-dark); color: var(--on-dark-60); border-color: var(--line-on-dark);
}
/* Readonly is NOT disabled. The value is still content, still focusable, still
   read out — it keeps the full 4.5 and keeps its control boundary. Only the
   ground changes, to say "you cannot type here". */
.input[readonly], .textarea[readonly] {
  background: var(--panel-2); color: var(--ink); border-color: var(--line-control);
}
/* border-color has to be restated, not inherited. The light rule above sets it to
   --line-control, which is dark ink at 0.72 — on --surface-dark that edge measures
   1.25 and the field simply is not there. A dark override that repaints the fill and
   leaves the edge behind is the most common way a control goes missing on a dark
   band, and it is invisible to a specificity check: the override does win, it just
   does not say enough. check_dark_edge_coverage now gates the whole shape. */
.on-dark .input[readonly], .on-dark .textarea[readonly] {
  background: var(--fill-on-dark); color: var(--on-dark);
  border-color: var(--line-control-on-dark);
}

/* ── Affixes: icon in, action out ─────────────────────────────────────
   The leading icon is decoration and gets aria-hidden. The trailing one is a
   real <button> with a real label — a password reveal that is only an <svg>
   is a control with no name. Icon colour is --ink-60 (4.5+ on the field's own
   ground); the shipping build draws these at 2.86 and 1.71. */
.field-affix { position: relative; display: block; }
.field-affix > .icon {
  position: absolute; inset-block-start: 50%; translate: 0 -50%;
  inset-inline-start: 18px; color: var(--ink-60); pointer-events: none;
}
.field-affix > .icon.is-trailing { inset-inline-start: auto; inset-inline-end: 18px; }
.on-dark .field-affix > .icon { color: var(--on-dark-60); }
.input--lead, .select { padding-inline-start: 48px; }
.input--trail { padding-inline-end: 52px; }
.field-action {
  position: absolute; inset-block-start: 50%; translate: 0 -50%; inset-inline-end: 6px;
  display: inline-flex; align-items: center; justify-content: center;
  inline-size: 44px; block-size: 44px; padding: 0;
  border: 0; background: none; border-radius: var(--r-pill);
  color: var(--ink-60); cursor: pointer;
  transition: color var(--dur-fast) var(--ease-ui), background var(--dur-fast) var(--ease-ui);
}
.field-action:focus-visible { outline: var(--focus-width) solid var(--focus-ring); outline-offset: var(--focus-offset); }
.on-dark .field-action { color: var(--on-dark-60); }

/* ── Textarea ─────────────────────────────────────────────────────────
   Inherits the field, drops the pill: a pill on three lines of text puts the
   first and last character inside the curve. resize stays vertical — a
   user-resizable box is a 1.4.4 ally, and horizontal resize breaks the rail. */
.textarea {
  width: 100%; border: 1px solid var(--line-control); background: var(--card);
  border-radius: var(--r-md); padding: 14px 18px; min-block-size: 120px; resize: vertical;
  font-family: var(--font-text); font-size: var(--fs-body); line-height: 1.55; color: var(--ink);
  transition: box-shadow var(--dur-base) var(--ease-ui), border-color var(--dur-base) var(--ease-ui);
}
.textarea::placeholder { color: var(--placeholder); }
.textarea:focus { outline: none; }
.textarea:focus-visible, .textarea:focus:not(:focus-visible) {
  border-color: var(--focus-ring); box-shadow: 0 0 0 4px rgba(165,135,107,0.08);
}
.textarea:focus-visible { outline: var(--focus-width) solid var(--focus-ring); outline-offset: var(--focus-offset); }
.on-dark .textarea { background: var(--fill-on-dark); border-color: var(--line-control-on-dark); color: var(--on-dark); }
.on-dark .textarea::placeholder { color: var(--on-dark-60); }
.on-dark--shell .textarea { background: var(--night-field); }

/* ── Select ───────────────────────────────────────────────────────────
   A native <select> with appearance:none. The chevron is a sibling .icon with
   pointer-events:none, never a background-image — a baked-in image cannot
   follow the token when the ground flips. */
.select {
  width: 100%; appearance: none; -webkit-appearance: none;
  border: 1px solid var(--line-control); background: var(--card);
  border-radius: var(--r-pill); padding: 12px 48px 12px 18px;
  font-family: var(--font-text); font-size: var(--fs-body); color: var(--ink); cursor: pointer;
  transition: box-shadow var(--dur-base) var(--ease-ui), border-color var(--dur-base) var(--ease-ui);
}
.select:focus { outline: none; }
.select:focus-visible, .select:focus:not(:focus-visible) {
  border-color: var(--focus-ring); box-shadow: 0 0 0 4px rgba(165,135,107,0.08);
}
.select:focus-visible { outline: var(--focus-width) solid var(--focus-ring); outline-offset: var(--focus-offset); }
.on-dark .select { background: var(--fill-on-dark); border-color: var(--line-control-on-dark); color: var(--on-dark); }
.on-dark--shell .select { background: var(--night-field); }
/* The dropdown list itself is OS-drawn; only its option colours are ours, and
   only some browsers honour them. Stated so nobody files it as a bug. */
.select option { background: var(--card); color: var(--ink); }
.on-dark .select option { background: var(--surface-dark); color: var(--on-dark); }

/* ── Checkbox / radio ─────────────────────────────────────────────────
   The whole row is the <label>, so the target is the row, not the 20px box —
   min-block-size 44px clears both WCAG 2.5.8 (24px) and the Apple HIG 44px
   without padding a single component. The mark is a mask, not a baked image,
   so it takes its colour from a token like every other glyph.
   The checked BOX is bronze but the boundary is --brand-deep: a bronze fill
   alone reads 2.73 on --bg and would fail 1.4.11 on four of seven grounds. */
.check {
  display: flex; align-items: flex-start; gap: 12px;
  min-block-size: 44px; padding-block: 10px; cursor: pointer;
  font-family: var(--font-text); font-size: var(--fs-body); line-height: 1.5; color: var(--ink);
}
.check > input {
  appearance: none; -webkit-appearance: none; margin: 0; flex: 0 0 auto;
  inline-size: 20px; block-size: 20px; margin-block-start: 2px;
  border: 1px solid var(--line-control); background: var(--card);
  border-radius: var(--r-xs); cursor: pointer;
  transition: background var(--dur-fast) var(--ease-ui), border-color var(--dur-fast) var(--ease-ui);
}
.check > input[type="radio"] { border-radius: var(--r-pill); }
/* The checked fill is --brand-deep, not --brand. Cream on --brand is 3.2 and
   the mark would fail; on --brand-deep it is 5.41, the same pairing the pack
   already uses for .btn-brand:hover. And --brand-deep clears 3:1 as a boundary
   on all seven light grounds (worst 4.09), which --brand does not (2.22). */
.check > input:checked, .check > input:indeterminate {
  background-color: var(--brand-deep); border-color: var(--brand-deep);
}
/* the glyph, drawn as a mask over the fill so it takes a token, not a baked hex */
.check > input[type="checkbox"]::after,
.check > input[type="radio"]::after { content: ""; display: block; block-size: 100%; inline-size: 100%; }
.check > input[type="checkbox"]:checked::after {
  background-color: var(--card-2);
  -webkit-mask: var(--mask-check) center / 14px 14px no-repeat;
          mask: var(--mask-check) center / 14px 14px no-repeat;
}
.check > input[type="checkbox"]:indeterminate::after {
  background-color: var(--card-2);
  -webkit-mask: var(--mask-minus) center / 14px 14px no-repeat;
          mask: var(--mask-minus) center / 14px 14px no-repeat;
}
.check > input[type="radio"]:checked::after {
  background-color: var(--card-2);
  -webkit-mask: radial-gradient(circle at 50% 50%, #000 0 3.5px, transparent 3.6px);
          mask: radial-gradient(circle at 50% 50%, #000 0 3.5px, transparent 3.6px);
}
.check > input:focus-visible { outline: var(--focus-width) solid var(--focus-ring); outline-offset: var(--focus-offset); }
.check > input:disabled { background: var(--ink-06); border-color: var(--line-2); cursor: not-allowed; }
.check:has(> input:disabled) { color: var(--ink-60); cursor: not-allowed; }
.check > input[aria-invalid="true"] { border-color: var(--neg-ink); }
.on-dark .check { color: var(--on-dark); }
.on-dark .check > input { background: transparent; border-color: var(--line-control-on-dark); }
.on-dark .check > input:checked, .on-dark .check > input:indeterminate {
  background-color: var(--on-dark); border-color: var(--on-dark);
}
.on-dark .check > input:checked::after, .on-dark .check > input:indeterminate::after { background-color: var(--ink); }
.on-dark .check > input:disabled { background: var(--fill-on-dark); border-color: var(--line-on-dark); }
.on-dark .check:has(> input:disabled) { color: var(--on-dark-60); }

/* ── Fieldset / legend ────────────────────────────────────────────────
   A radio group is one control with several options, and the question is the
   legend. Three radios with a <p> above them are three unrelated controls to
   a screen reader. Use a real <fieldset>; the reset below only removes the
   browser's box, it does not remove the semantics. */
.fieldset { border: 0; padding: 0; margin: 0; min-inline-size: 0; display: grid; gap: 4px; }
.legend {
  padding: 0; font-family: var(--font-text); font-size: 13.5px; font-weight: 500;
  color: var(--ink); margin-block-end: 4px;
}
.on-dark .legend { color: var(--on-dark); }

/* ── Form scaffolding ─────────────────────────────────────────────────
   24px between fields is the smallest gap at which a label still reads as
   belonging to the control below it rather than the one above. */
.form        { display: grid; gap: 24px; }
.form-row    { display: grid; gap: 24px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.form-actions{ display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }
.form-actions--end { justify-content: flex-end; }

/* ── Destructive control (v1.4) ───────────────────────────────────────
   Light: a filled red button, cream label, 4.65 at rest and 6.02 on hover —
   hover raises the ratio, never lowers it. Dark: a ghost, because raw --neg as
   a WORD on dark measures 3.40 (this is the shipping "Sign out"), and as a
   FILL it measures 3.20 on --night — inside tolerance but with nothing left
   for a hover. So on dark the word is --neg-on-dark (7.43) and the fill only
   appears under the pointer, where the label flips to cream. */
.btn-danger { background: var(--neg); color: var(--on-neg); }
.on-dark .btn-danger {
  background: transparent; color: var(--neg-on-dark);
  border: 1px solid var(--line-control-on-dark);
}

/* `.btn::after { content: "›" }` is unconditional, and a cold reviewer named the
   consequence before this pack could produce it: "Delete account permanently ›".
   v1.4 shipped .btn-danger and made it real — nine specimens reading "Delete
   listing ›", a chevron promising forward motion on the one control that ends
   something. The arrow is a direction, and destroying a row has no direction; a
   dialog's "Keep it" and "Stay" have one, and it points the other way.

   Withdrawn from the destructive control unconditionally, and available as an
   opt-out anywhere the action is a dismissal rather than a step. Opt-out and not
   opt-in only because the arrow is right on the large majority of buttons here,
   and inverting the default would mean editing every call site to get back what
   it already had. `content: none` and not `content: ""`: the empty string still
   generates a box, which still takes the ::after's font-size and the flex gap
   around it, so the label would sit off-centre by the width of a gap on every
   destructive button in the system. */
.btn-danger::after,
.btn--noarrow::after { content: none; }

/* ── Menu / popover (v1.4) ────────────────────────────────────────────
   The host for a destructive item, and the shape the OrboxOS account dropdown
   already is. It is a real listbox/menu: the trigger owns aria-expanded and
   aria-haspopup, the panel owns role="menu", each row role="menuitem". A div
   that merely looks like a menu is not one.
   Elevation is --shadow-3 on light. On a dark ground the pack forbids shadows
   (they land as a black smear on charcoal), so separation comes from the
   raised surface plus a hairline instead. */
.menu {
  min-inline-size: 200px; padding: 4px;
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-md); box-shadow: var(--shadow-3);
}
.menu-item {
  display: flex; align-items: center; gap: 10px; inline-size: 100%;
  min-block-size: 44px; padding: 8px 12px;
  border: 0; background: none; border-radius: var(--r-sm);
  font-family: var(--font-text); font-size: var(--fs-body); line-height: 1.4;
  color: var(--ink); text-align: start; text-decoration: none; cursor: pointer;
  transition: background var(--dur-fast) var(--ease-ui), color var(--dur-fast) var(--ease-ui);
}
.menu-item .icon { color: var(--ink-60); }
.menu-item .spacer { margin-inline-start: auto; }
.menu-item[aria-checked="true"] .icon-check { color: var(--brand-deep); }   /* 5.81 on --card;
   the shipping build draws this checkmark at 3.11 — passing, but with nothing in hand. */
.menu-item:focus-visible { outline: var(--focus-width) solid var(--focus-ring); outline-offset: -2px; }
.menu-sep { block-size: 1px; margin: 6px 4px; background: var(--line-2); border: 0; }
.on-dark .menu { background: var(--surface-dark); border-color: var(--line-on-dark); box-shadow: none; }
.on-dark--shell .menu { background: var(--night-raised); }
.on-dark .menu-item { color: var(--on-dark); }
.on-dark .menu-item .icon { color: var(--on-dark-60); }
.on-dark .menu-item[aria-checked="true"] .icon-check { color: var(--brand-soft); }
.on-dark .menu-sep { background: var(--line-on-dark); }

/* The destructive row, and the reason it is written HERE and not up beside
   .btn-danger where it was first drafted. `.menu-item--danger` and `.menu-item`
   are both (0,1,0); `.on-dark .menu-item--danger` and `.on-dark .menu-item` are
   both (0,2,0). A modifier that ties on specificity is decided by source order
   alone, and the base rules above are the later ones — so for two versions the
   danger row rendered at --ink on light and --on-dark on dark. Not a wrong
   colour: NO colour, on the one row in a menu that must not look like the
   others. Nothing static caught it, because nothing static was wrong; it was
   found by reading getComputedStyle off a rendered menu. Keep these last. */
.menu-item--danger { color: var(--neg-ink); }
.on-dark .menu-item--danger { color: var(--neg-on-dark); }

/* ── Dialog + scrim (v1.4) ────────────────────────────────────────────
   A real <dialog>, opened with .showModal(). The focus trap, Escape, the page
   behind going inert and the stacking are all the browser's — and they are the
   four things a hand-rolled modal gets wrong, roughly in that order.

   Note what is NOT in this block: there is no z-index in it anywhere. A modal
   <dialog> paints in the TOP LAYER, which sits above every stacking context on
   the page regardless of what any of them declared, so the z-index arms race
   that hand-rolled modals always end in never starts. If a dialog here ever
   seems to need a z-index, the bug is somewhere else.

   Open it with showModal(), never by setting the `open` attribute. `<dialog open>`
   is a NON-modal dialog — no scrim, no trap, no inert page, no top layer — and it
   is pixel-identical in a screenshot, which is exactly why that bug ships. The one
   sanctioned `open` is .dialog-stage at the end of this block, which is
   documentation furniture and says so.

   ── the scrim, and the one thing it cannot do ────────────────────────────
   On LIGHT the plane separates. --card against the scrimmed page is 4.65 at its
   worst — worst being a dialog opened over a --card region, the lightest ground
   the scrim has to darken — and 5.81 over --bg-deep. Comfortably past the 3:1
   that 1.4.11 asks of a boundary you have to see to use the thing.

   On DARK the scrim separates nothing, and no alpha fixes it. The night family
   lives within a hair of itself in luminance: --night-raised on a scrimmed
   --night is 1.10, and --surface-dark, the lightest dark surface the pack owns,
   only reaches 1.06. Darkening the ground moves those by hundredths — and
   downward, because the dialog is the lighter of the two. So on dark the
   BOUNDARY does the work the plane does on light: a --line-control-on-dark
   hairline resolves to #88847b over --night-raised and measures 3.89–4.67
   against the scrimmed grounds, #959188 over --surface-dark for 4.61–5.54. It
   is the same trade the menu makes two blocks up, for the same reason — no
   shadows on dark, so an edge is all there is.

   ── where the dark class goes ───────────────────────────────────────────
   Selectors match the DOM, not the top layer. A <dialog> parked at the end of
   <body> is not a descendant of the shell it belongs to, so `.on-dark .dialog`
   never fires for it however dark the app around it looks. Put the class on the
   dialog itself — `<dialog class="dialog on-dark on-dark--shell">` — which also
   hands it the right --focus-ring. Both forms work below; the element's own
   class is the one to reach for.

   ── focus ───────────────────────────────────────────────────────────────
   The browser moves focus in. WHERE it lands is still the author's problem.
   `autofocus` goes on the least destructive control — Cancel, never Delete —
   and never on the ✕, which starts the reader at the end of the dialog and
   makes the first Tab feel like a wrap. If the content is long enough to
   scroll, autofocus goes nowhere: the dialog takes tabindex="-1" and the reader
   starts at the top of the text instead of at a button underneath it. And a
   .dialog-body that actually scrolls takes tabindex="0" plus an accessible
   name, because a scroll region no key can reach is a 2.1.1 failure that
   browsers only paper over inconsistently.

   ── motion ──────────────────────────────────────────────────────────────
   Entry and exit are a transition, and `overlay` is in the list on purpose.
   Leave it out and the exit still runs — but the element drops out of the top
   layer on the first frame, so the dialog fades over an already-undimmed page
   with the scrim gone. It reads as a rendering fault and it is a one-word
   omission. `display` is there for the same reason at the other end; neither
   moves without allow-discrete.
   Under reduced motion the transition is dropped outright rather than
   shortened. The global damper would take the duration to ~0 anyway, but this
   is the one place where "the damper will catch it" is not good enough: with no
   transition there is nothing for allow-discrete to sequence, and whether the
   element is in the top layer stops depending on a timer at all. */
.dialog {
  margin: auto;                                   /* the top layer centres nothing for you */
  inline-size: min(100% - 32px, 520px);
  max-block-size: min(84svh, 720px);
  padding: 0;
  background: var(--card);
  color: var(--ink-70);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-4);
  opacity: 0;
  transform: translateY(8px) scale(0.99);
  transition: opacity var(--dur-fast) var(--ease-ui),
              transform var(--dur-fast) var(--ease-ui),
              display var(--dur-fast) allow-discrete,
              overlay var(--dur-fast) allow-discrete;
}
/* display lives on [open] and nowhere else: the UA sheet hides a closed dialog with
   `display: none`, and one unconditional author `display` on .dialog would beat it and
   leave every closed dialog on the page in flow, scrimless and untrapped. */
.dialog[open] {
  display: grid; grid-template-rows: auto minmax(0, 1fr) auto;
  opacity: 1; transform: none;
}
@starting-style {
  .dialog[open] { opacity: 0; transform: translateY(8px) scale(0.99); }
}
/* The literal repeats --scrim, and only here. ::backdrop did not inherit from its
   originating element until 2024, and in a browser where it still does not, var(--scrim)
   resolves to nothing and the modal opens with NO scrim at all — the failure is total and
   silent. The duplicate is a second source of truth, so the pack self-check compares this
   literal against brand.json's `scrim` byte for byte and fails if they ever drift. */
.dialog::backdrop {
  background: rgba(31,28,23,0.62);
  background: var(--scrim);
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease-ui),
              display var(--dur-fast) allow-discrete,
              overlay var(--dur-fast) allow-discrete;
}
.dialog[open]::backdrop { opacity: 1; }
@starting-style {
  .dialog[open]::backdrop { opacity: 0; }
}
.dialog--wide  { inline-size: min(100% - 32px, 720px); }
/* An alertdialog asks a question, and every way out has to be an answer — so it has no ✕.
   A ✕ beside two buttons is a third, unlabelled option, and the one people click when the
   two labelled ones both look expensive. Escape still closes it, because the browser owns
   that key, and Escape must do exactly what Cancel does. */
.dialog--alert { inline-size: min(100% - 32px, 420px); }

.dialog-head  { display: flex; align-items: flex-start; gap: 16px;
                padding: 20px 24px; border-block-end: 1px solid var(--line-2); }
.dialog-title { margin: 0; }                       /* the size comes from .t-sub-title */
.dialog-desc  { margin: 8px 0 0; }
.dialog-close { margin-inline-start: auto; margin-block: -12px -8px; margin-inline-end: -12px; }
.dialog-body  { padding: 24px; overflow: auto; overscroll-behavior: contain; }
.dialog-foot  { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center;
                gap: 12px; padding: 16px 24px; border-block-start: 1px solid var(--line-2); }
/* A secondary action pushed to the far left — "Learn more", never a second commit. */
.dialog-foot .dialog-aside { margin-inline-end: auto; }

.on-dark .dialog, .dialog.on-dark {
  background: var(--surface-dark); color: var(--on-dark-70);
  border-color: var(--line-control-on-dark); box-shadow: none;
}
.on-dark--shell .dialog, .dialog.on-dark--shell { background: var(--night-raised); }
.on-dark .dialog-head, .on-dark .dialog-foot { border-color: var(--line-on-dark); }

/* The page behind a modal must not scroll. No JS: :modal is true only for a dialog opened
   with showModal(), so a non-modal <dialog open> — and .dialog-stage below — leave the
   page alone, which is the whole difference between the two and the reason to key on it. */
html:has(dialog:modal) { overflow: hidden; }

@media (prefers-reduced-motion: reduce) {
  .dialog, .dialog::backdrop { transition: none; }
}

/* Documentation furniture, and the only sanctioned `open`. A modal cannot be shown on a
   static page — showModal() needs a script and a click — so a gallery either mocks the
   dialog up out of divs, which measures a drawing of the component rather than the
   component, or it stages the real element. This paints the real --scrim as its own ground
   and un-positions the real <dialog>, so what a screenshot and the render harness measure
   is the actual surface on the actual dimmed ground. Never ship it in a product. */
.dialog-stage { display: grid; place-items: center; padding: 32px 16px;
                border-radius: var(--r-lg); background: var(--scrim); }
.dialog-stage > .dialog { position: static; }

/* ══ Hover (v1.4 additions) ════════════════════════════════════════════
   Gated, like every other hover in this file: a hover that is not behind
   `(hover: hover) and (pointer: fine)` sticks to the last-tapped element on a
   touch screen, and a stuck hover on a destructive item is the worst place to
   have one. */
@media (hover: hover) and (pointer: fine) {
  .btn-danger:hover  { background: var(--neg-hover); }   /* label 4.65 -> 6.02: hover RAISES it */
  /* the ghost fills, and the word flips to cream on the red instead of staying
     a red word on a dark ground */
  .on-dark .btn-danger:hover { background: var(--neg); color: var(--on-neg); border-color: var(--neg); }
  .menu-item:hover { background: var(--ink-06); }
  .menu-item--danger:hover { background: rgba(165,83,83,0.10); color: var(--neg-ink); }
  .on-dark .menu-item:hover { background: var(--fill-on-dark); }
  .on-dark .menu-item--danger:hover { background: rgba(207,160,160,0.14); color: var(--neg-on-dark); }
  .on-dark--shell .menu-item:hover { background: var(--night-hover); }
  .field-action:hover { color: var(--ink); background: var(--ink-06); }
  .on-dark .field-action:hover { color: var(--on-dark); background: var(--fill-on-dark); }
  .icon-btn:hover { color: var(--ink); background: var(--ink-06); }
  .on-dark .icon-btn:hover { color: var(--on-dark); background: var(--fill-on-dark); }
  .on-dark--shell .icon-btn:hover { background: var(--night-raised); }
  .on-dark--shell .btn-ghost:hover,
  .on-dark--shell .btn-dark:hover  { background: var(--night-hover); }
  .on-dark--shell .nav a:hover     { background: var(--night-raised); }
}

/* ══ ICON LAYER (v1.4) — Phosphor, one set, no mixing ══════════════════
   Set: Phosphor 2.1.1 (MIT), Regular + Fill, in assets/icons/. Phosphor ships
   each glyph as a single filled path on a 256 viewBox with fill="currentColor"
   already set, so an inlined icon takes its colour from the text token with no
   further work — and no stroke-width compensation, which is a Lucide problem.
   Inline the <svg>; do not <img> it. An <img> cannot inherit currentColor, so
   it cannot follow the ground when a band flips, and that is precisely how an
   icon ends up at 1.71:1.

   FILL IS A STATE, NOT A STYLE. Regular = idle, Fill = active/selected. Never
   both as decoration on one surface, and never a second icon family anywhere.

   Sizes are 16 / 20 / 24 / 32. Nothing in between, ever. Default is 20.
   A decorative icon takes aria-hidden="true". An icon that is the only content
   of a control takes a real accessible name on the control.
   An icon that carries meaning must clear 3:1 against its own ground — it is a
   graphical object under 1.4.11, and --ink-40 / --on-dark-40 are non-text marks
   only. Both harnesses gate this. */
.icon {
  display: inline-block; flex: 0 0 auto;
  inline-size: 20px; block-size: 20px;
  fill: currentColor; color: inherit;
}
.icon-16 { inline-size: 16px; block-size: 16px; }
.icon-20 { inline-size: 20px; block-size: 20px; }
.icon-24 { inline-size: 24px; block-size: 24px; }
.icon-32 { inline-size: 32px; block-size: 32px; }
/* Cap-height alignment: an icon centred on the line box floats above the text.
   inline-flex + align-items:center puts it on the optical midline. */
.icon-row { display: inline-flex; align-items: center; gap: 8px; }
/* An icon-only control: 20px of glyph, 44px of target. */
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  inline-size: 44px; block-size: 44px; padding: 0;
  border: 0; background: none; border-radius: var(--r-pill);
  color: var(--ink-70); cursor: pointer;
  transition: color var(--dur-fast) var(--ease-ui), background var(--dur-fast) var(--ease-ui);
}
.icon-btn:focus-visible { outline: var(--focus-width) solid var(--focus-ring); outline-offset: var(--focus-offset); }
.icon-btn[aria-pressed="true"] { color: var(--brand-deep); }
.on-dark .icon-btn { color: var(--on-dark-70); }
.on-dark .icon-btn[aria-pressed="true"] { color: var(--brand-soft); }
@media (prefers-reduced-motion: reduce) {
  .icon-btn, .field-action, .check > input { transition: none; }
}

/* ── Fact pill (two-cell: mono label | value) ── */
.fact-pill { display: inline-flex; align-items: stretch; border: 1px solid var(--line); border-radius: var(--r-pill); overflow: hidden;
             text-decoration: none; }   /* rendered as an <a> this was a blue underlined pill — see .tag-acronym above */
.fact-pill .label { font-family: var(--font-mono); font-size: var(--fs-kicker); letter-spacing: 0; color: var(--ink-60); padding: 5px 10px; border-right: 1px solid var(--line); }
.fact-pill .value { font-family: var(--font-text); font-size: 13px; color: var(--ink); padding: 5px 12px; }

/* ── Sphere / orb wrapper ── */
.orb { aspect-ratio: 1 / 1; flex-shrink: 0; display: block; }

/* ── Chip (filter / segmented choice) ─────────────────────────────────
   The quiet interactive pill. Not a tag: a tag states a fact, a chip takes a
   click. State lives on aria-pressed, never on colour alone. */
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--font-text); font-size: 13.5px; font-weight: 500; line-height: 1.2;
  padding: 7px 14px; border-radius: var(--r-pill);
  border: 1px solid var(--ink-20); background: transparent; color: var(--ink-70);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-ui), border-color var(--dur-fast) var(--ease-ui);
}
.chip[aria-pressed="true"] { background: var(--ink); color: var(--card-2); border-color: transparent; }
.chip:active { transform: scale(var(--press-scale)); }
@media (hover: hover) and (pointer: fine) {
  .chip:hover { background: var(--ink-06); }
  .chip[aria-pressed="true"]:hover { background: var(--ink-pressed); }
}

/* ── Status chip ──────────────────────────────────────────────────────
   A state, spoken. The dot carries the status FILL; the word carries the
   measured -ink pair. Never colour-only: the label is the signal, the dot is
   the accent — that is WCAG 1.4.1 and it is also just clearer. */
.status-chip {
  display: inline-flex; align-items: center; gap: 7px;
  font-family: var(--font-mono); font-size: var(--fs-kicker); font-weight: 500; letter-spacing: 0;
  padding: 4px 10px; border: 1px solid var(--line-2); border-radius: var(--r-pill);
  color: var(--ink-70);
}
.status-chip::before { content: ""; width: 6px; height: 6px; border-radius: 50%;
                       background: currentColor; flex: none; }
.status-chip--pos  { color: var(--pos-ink); }
.status-chip--warn { color: var(--warn-ink); }
.status-chip--info { color: var(--info-ink); }
.status-chip--neg  { color: var(--neg-ink); }
/* The dark base. Only the four modifiers had an .on-dark override, so a plain <span
   class="status-chip">Live</span> inside a dark band kept --ink-70 on --surface-dark — contrast
   1.06, a word you cannot read — and kept --line-2, a light hairline on a dark ground.

   :where() on purpose. This rule is (0,1,0), not (0,2,0): at (0,2,0) it ties with the four
   .on-dark .status-chip--* rules above and, being later in the file, repaints all four of them
   --on-dark-70 — every status chip in every dark band goes cream and the colour signal is gone.
   Scoring the scope at zero puts this rule below them by specificity rather than by luck of
   ordering, so it cannot be broken by someone moving a block. */
:where(.on-dark) .status-chip { color: var(--on-dark-70); border-color: var(--line-on-dark); }

/* ── Switch (v1.4: rebuilt on the checkbox's contrast rules) ──────────
   This control shipped in 1.3 with the exact three defects the checkbox block
   above spends four lines forbidding, which is what happens when a component
   is written once and never given a specimen: --ink-20 as the track boundary
   measures 1.32 on --bg-deep where 1.4.11 asks 3.0, --brand as the checked
   fill measures 2.22 where the same clause asks 3.0, and there was no focus
   ring at all. The fill cannot rescue the edge either — --ink-06 over the
   worst light ground is 1.09 against that ground, so an unchecked switch IS
   its border. Hence --line-control (3.00 worst) unchecked and --brand-deep
   (4.09 worst) checked, the same two tokens .check > input already uses.

   The knob is not decoration: it is the entire state signal, so it is held to
   1.4.11 against the track it sits on. --ink on an --ink-06 track reads 6.55
   at worst; --card-2 on --brand-deep reads 5.41. The reflex knob, --on-brand,
   is the trap — 5.42 on --brand but only 2.94 on --brand-deep, so moving the
   fill without moving the knob would have traded one failure for another.

   Geometry: the knob is 18×18 inside a 40×24 padding box with 3px of air on
   every side, which makes the travel exactly 16px and the knob an actual
   circle. The old 16px-wide, 18px-tall knob was an ellipse at --r-pill.

   Use role="switch" on the input. Without it AT says "checkbox, checked";
   with it, "switch, on" — which is what the control means. */
.switch {
  appearance: none; -webkit-appearance: none; margin: 0; flex: 0 0 auto;
  inline-size: 40px; block-size: 24px; cursor: pointer; position: relative;
  border-radius: var(--r-pill); border: 1px solid var(--line-control); background: var(--ink-06);
  transition: background var(--dur-fast) var(--ease-ui), border-color var(--dur-fast) var(--ease-ui);
}
.switch::after {
  content: ""; position: absolute; inset-block: 3px; left: 3px; inline-size: 18px;
  border-radius: var(--r-pill); background: var(--ink);
  transition: translate var(--dur-fast) var(--ease-ui);
}
.switch:checked { background-color: var(--brand-deep); border-color: var(--brand-deep); }
.switch:checked::after { translate: 16px 0; background: var(--card-2); }
.switch:focus-visible { outline: var(--focus-width) solid var(--focus-ring); outline-offset: var(--focus-offset); }
/* Disabled is 1.4.11-exempt, which is the only reason --ink-40 (a non-text
   mark token, 2.03 on --bg) is allowed to be the knob here — and the reason
   it has to be: a knob left at full --ink reads as a live control. */
.switch:disabled { background: var(--ink-06); border-color: var(--line-2); cursor: not-allowed; }
.switch:disabled::after { background: var(--ink-40); }

/* The row, because a switch with no label is a toggle for nothing. Same 44px
   target and same <label>-wraps-everything contract as .check — but its own
   class, not .check, because `.check > input` is (0,1,1) and would beat
   `.switch` (0,1,0) and repaint the track as a 20px square. Switch trails,
   label leads: the auto margin does that without the call site knowing.
   The 8px is not .check's 10px: the switch is 26px tall against the checkbox
   row's 24px line box, so 8px lands this row on exactly 44px where 10px would
   overshoot to 46px — and 8 is on the 4px grid, so the closed controlPadding
   carve-out does not have to grow to admit it. */
.switch-row {
  display: flex; align-items: center; gap: 16px;
  min-block-size: 44px; padding-block: 8px; cursor: pointer;
  font-family: var(--font-text); font-size: var(--fs-body); line-height: 1.5; color: var(--ink);
}
.switch-row > .switch { margin-inline-start: auto; }
.switch-row .switch-text { display: grid; gap: 4px; }
.switch-row:has(> .switch:disabled) { color: var(--ink-60); cursor: not-allowed; }

/* ── Stat caption ─────────────────────────────────────────────────────
   Every number gets one. A figure without a caption is a claim without a
   source — this is the atom that makes that rule cheap to obey. */
.stat-caption { font-family: var(--font-mono); font-size: var(--fs-kicker); font-weight: 500;
                letter-spacing: 0; line-height: 1.4; color: var(--ink-60); }

/* ── Chart scope ──────────────────────────────────────────────────────
   Reference the vars from inside the SVG (stroke="var(--chart-grid)"); .on-dark
   swaps all four at once. The line, the area gradient and the end dot keep
   --brand on both grounds — a fill carries its own contrast. */
.chart-grid { stroke: var(--chart-grid); }
.chart-axis { stroke: var(--chart-axis); }
.chart-tick { fill: var(--chart-tick); font-family: var(--font-mono); font-size: var(--fs-kicker); }

/* ── Data ping — a ripple, not a blink ── */
.dot-ping { position: relative; }
.dot-ping::after {
  content: ""; position: absolute; inset: 0; border-radius: 50%;
  background: var(--brand); transform-origin: 50% 50%;
  animation: orb-ping var(--loop-ping) var(--ease-ping) infinite;
}
@keyframes orb-ping {
  0%   { transform: scale(0.55); opacity: 0.55; }
  100% { transform: scale(2.7);  opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  /* the ripple stops and rests as a halo, so the dot still reads as live data */
  .dot-ping::after { animation: none; transform: scale(1.8); opacity: 1;
                     background: var(--chart-halo); }
}

/* ═══════════════════════════════════════════════════════════════════════
   SHELL ATOMS — the parts of a page that are not the content
   Everything above this line is something you put *in* a screen. Everything
   below is the screen itself: the bar across the top, the way you search it,
   the way you page through it, the band that breaks it, the footer that ends
   it. They were the pack's largest gap — DESIGN.md described a topbar and a
   footer, ui.jsx shipped them, and the tokens file had neither, so every
   delegate invented one and no two agreed.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Topbar ───────────────────────────────────────────────────────────
   Sticky, translucent, one hairline. Compose it with a rail, don't rebuild
   one: <header class="topbar"><div class="app-shell topbar-inner"> … */
.topbar {
  position: sticky; top: 0; z-index: 20;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line);
}
.topbar-inner {
  display: grid; grid-template-columns: auto 1fr auto;
  align-items: center; gap: 24px; min-height: 64px;
}

/* ── Brandmark (symbol + wordmark lockup) ─────────────────────────────
   The symbol is assets/logo/orbox-symbol.svg, placed verbatim. Never redraw
   the disc or its highlight, never recolour them, never letter-space the
   wordmark. Below 860px the word drops and the mark stays — see the
   responsive block at the end of this file. */
.brandmark { display: inline-flex; align-items: center; gap: 10px;
             color: var(--ink); text-decoration: none; }
.brandmark .symbol   { inline-size: 24px; block-size: 24px; flex-shrink: 0; display: block; }
.brandmark .wordmark { font-family: var(--font-display); font-weight: 500;
                       font-size: var(--fs-sub-title); letter-spacing: -0.02em; line-height: 1; }

/* ── Nav ──────────────────────────────────────────────────────────────
   Current page is aria-current, not a colour. A nav that marks "you are
   here" with fill alone tells a screen-reader user nothing at all. */
.nav { display: flex; align-items: center; gap: 4px; justify-self: center; }
.nav a {
  font-family: var(--font-text); font-size: 13.5px; font-weight: 500; line-height: 1.2;
  color: var(--ink-70); text-decoration: none;
  padding: 8px 12px; border-radius: var(--r-pill);
  transition: color var(--dur-fast) var(--ease-ui), background-color var(--dur-fast) var(--ease-ui);
}
.nav a[aria-current="page"] { color: var(--ink); background: var(--ink-06); }
.nav a:active { transform: scale(var(--press-scale)); }

/* ── Account ── */
.account { display: inline-flex; align-items: center; gap: 10px; }
.account .who { font-family: var(--font-text); font-size: 13.5px; font-weight: 500; color: var(--ink-70); }
/* Initials on bronze: the label goes --on-brand (5.42), never white (3.35). */
.avatar {
  inline-size: 32px; block-size: 32px; border-radius: 50%; flex-shrink: 0;
  display: grid; place-items: center;
  background: var(--brand); color: var(--on-brand);
  font-family: var(--font-mono); font-size: var(--fs-kicker); font-weight: 500; letter-spacing: 0;
}

/* ── Search ───────────────────────────────────────────────────────────
   Not a new control — an .input with room for a mark. The focus ring, the
   wash and the placeholder all come from .input unchanged, which is the
   whole point: no :focus-within rule here, nothing that could blank the
   ring on the element that actually has focus. */
.search { position: relative; display: block; }
.search .input { padding-inline-start: 44px; }
.search .icon {
  position: absolute; inset-block: 0; inset-inline-start: 16px; margin-block: auto;
  inline-size: 16px; block-size: 16px; color: var(--ink-60); pointer-events: none;
}

/* ── Filter row ───────────────────────────────────────────────────────
   A set, not seven loose buttons. The row carries role="group" (or
   role="radiogroup" when the choice is exclusive) and an aria-label naming
   what is being filtered; the chips inside carry aria-pressed / aria-checked.
   Without the group name a screen reader announces "Sale, pressed" with no
   answer to "sale of what". */
.filter-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.filter-label { font-family: var(--font-mono); font-size: var(--fs-kicker); font-weight: 500;
                letter-spacing: 0; line-height: 1.4; color: var(--ink-60); margin-inline-end: 4px; }

/* ── Pagination ───────────────────────────────────────────────────────
   Same doctrine as the chip: state lives on an ARIA attribute. The current
   page is aria-current="page"; a spent prev/next is aria-disabled="true" on
   a ghost fill with --ink-70 type — never opacity, which drags the label to
   2.1 and calls it a state. */
.pager { display: flex; align-items: center; gap: 6px; }
.pager-item {
  display: inline-grid; place-items: center;
  min-inline-size: 34px; block-size: 34px; padding-inline: 10px;
  border-radius: var(--r-pill); border: 1px solid transparent; background: transparent;
  font-family: var(--font-mono); font-size: var(--fs-caption); font-weight: 500;
  letter-spacing: 0; color: var(--ink-70); cursor: pointer;
  /* A pager item is nearly always an <a>, and an underlined row of page numbers reads as six
     separate links rather than one control. The nav, the brandmark and the footer link all
     kill it too; this one was missed until the specimen was rendered and looked at. */
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-ui), background-color var(--dur-fast) var(--ease-ui);
}
.pager-item[aria-current="page"]  { background: var(--ink); color: var(--card-2); }
.pager-item[aria-disabled="true"] { background: var(--ink-06); color: var(--ink-70); cursor: not-allowed; }
.pager-item:active { transform: scale(var(--press-scale)); }
/* The ellipsis is read aloud, so it is text and takes a text value, not --ink-40. */
.pager-gap { font-family: var(--font-mono); font-size: var(--fs-caption);
             color: var(--ink-60); padding-inline: 4px; }

/* ── Footer ───────────────────────────────────────────────────────────
   Usually the last dark band on the page: <footer class="section on-dark on-dark--night">
   wrapping a .content-rail > .footer. */
.footer { display: grid; grid-template-columns: 1.4fr repeat(3, minmax(0, 1fr));
          gap: 48px 32px; }
.footer-col { display: flex; flex-direction: column; gap: 10px; }
.footer-link { font-family: var(--font-text); font-size: var(--fs-body); line-height: 1.7;
               color: var(--ink-70); text-decoration: none; width: fit-content;
               transition: color var(--dur-fast) var(--ease-ui); }
.footer-base { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: center;
               justify-content: space-between; gap: 16px;
               border-top: 1px solid var(--line); padding-top: 24px; }
.footer-legal { font-family: var(--font-mono); font-size: var(--fs-kicker); font-weight: 500;
                letter-spacing: 0; color: var(--ink-60); }

/* ── Dark-band composition grid ───────────────────────────────────────
   A dark band is one idea and one focal element. This is the grid that says
   so: copy on one side, the focal object on the other, nothing in the middle
   competing. .band-grid--focal-left mirrors it; on mobile both collapse to
   copy-then-focal, because a sphere above an unread headline is decoration. */
.band-grid { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
             gap: 64px; align-items: center; }
.band-focal { justify-self: end; inline-size: 100%; max-inline-size: 420px; }
.band-grid--focal-left > .band-focal { order: -1; justify-self: start; }
.band-rule { block-size: 1px; background: var(--line); border: 0; margin: 0; }

/* ── Warm lens ────────────────────────────────────────────────────────
   The imagery stand-in the skill points at: a --panel block with a warm
   grade, used instead of a stock photo when the real one is not available.
   With an <img> inside it becomes the grade itself — soft contrast, a
   restrained warm cast, one bloom. .lens--flat drops the overlay when the
   photograph is already graded. */
.lens {
  position: relative; overflow: hidden; isolation: isolate;
  border-radius: var(--r-lg);
  background-color: var(--panel);
  /* The first draft of this used --brand-glow over --panel with --panel-2 beneath. All three
     sit within a couple of percent of --bg's luminance, so rendered on a light page it was an
     almost invisible rectangle that read as a missing image rather than as a treatment — and it
     contradicted this system's own claim that the bronze is what stays constant across grounds.
     The bronze is in it now: --brand at 18% carries the light, --brand-glow softens the falloff,
     --panel-2 grounds the far corner. Same three-term structure as the .on-dark version below. */
  background-image:
    radial-gradient(120% 92% at 20% 8%,  color-mix(in srgb, var(--brand) 18%, transparent) 0%, transparent 62%),
    radial-gradient(88% 70% at 34% 22%,  var(--brand-glow) 0%, transparent 56%),
    radial-gradient(96% 78% at 88% 100%, var(--panel-2)   0%, transparent 64%);
}
.lens > img { display: block; inline-size: 100%; block-size: 100%; object-fit: cover; }
.lens::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(200deg,
    color-mix(in srgb, var(--brand-glow) 42%, transparent) 0%, transparent 46%,
    color-mix(in srgb, var(--ink) 26%, transparent) 100%);
}
.lens--flat::after { content: none; }

/* ── Orb: the three behaviours ────────────────────────────────────────
   .orb stays the wrapper — aspect ratio and nothing else, so the caller owns
   the size. The modifiers are the three behaviours in brand.json → sphere.

   --symbol is the logo mark and therefore an <svg>/<img> child, never CSS:
   assets/logo/orbox-symbol.svg is geometry that must not be redrawn.
   --thinking is the CSS stand-in for a generated sphere frame, the same
   bargain as .lens standing in for a photograph. It breathes on --loop-sphere
   (12s, the ambient band) and drifts its inner bloom while it does — quiet,
   not Siri-busy.
   --image is the warm lens cropped round. */
.orb > svg, .orb > img { inline-size: 100%; block-size: 100%; display: block; }
.orb--symbol { color: var(--brand); }
.orb--thinking {
  border-radius: 50%; box-shadow: var(--shadow-3);
  background-color: var(--brand);
  background-image:
    radial-gradient(circle at 32% 26%, var(--brand-glow) 0%, var(--brand-soft) 30%, var(--brand) 62%, var(--brand-deep) 100%),
    radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--brand-glow) 60%, transparent) 0%, transparent 52%);
  background-size: 100% 100%, 165% 165%;
  background-position: 0 0, 26% 74%;
  animation: orb-breathe var(--loop-sphere) var(--ease-ui) infinite;
}
@keyframes orb-breathe {
  0%, 100% { transform: scale(1);     background-position: 0 0, 26% 74%; }
  50%      { transform: scale(1.035); background-position: 0 0, 66% 30%; }
}
.orb--image { border-radius: 50%; }
@media (prefers-reduced-motion: reduce) {
  /* it stops mid-breath and stays a sphere — the object is the point, the motion is not */
  .orb--thinking { animation: none; transform: none; }
}

/* Every shell hover sits behind the pointer gate, same as the card and the button:
   a hover state applied on tap sticks until you touch something else. */
@media (hover: hover) and (pointer: fine) {
  .nav a:hover        { color: var(--ink); background: var(--ink-06); }
  .pager-item:hover   { color: var(--ink); background: var(--ink-06); }
  .footer-link:hover  { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
  .brandmark:hover .wordmark { color: var(--brand-deep); }
}

/* ── Motion: entrances animate FROM hidden; end-state is the base ── */
.reveal { opacity: 1; transform: none;          /* base IS the end state */
           transition: opacity var(--dur-slow) var(--ease-ui),
                       transform var(--dur-slow) var(--ease-ui); }
.reveal[data-pending] { opacity: 0; transform: translateY(12px); }

@media (prefers-reduced-motion: reduce) {
  /* the damper kills duration; it cannot un-hide something authored hidden, so the
     pending state is released explicitly */
  .reveal[data-pending] { opacity: 1; transform: none; }
  *, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; }
}

@media print {
  /* The third path to a broken observer, and the one MOTION.md §2 did not close. "No JS"
     and "reduced motion" both land on a visible page. Print does not: the observer runs,
     but only ever for what the reader actually scrolled past, so every section still below
     the fold goes to paper still pending. Measured before this line existed: six of the
     landing's eight printed pages came out blank — not degraded, blank. The same hole
     swallows every other full-page capture, which is how it was found: OG images, PDF
     exports, visual-regression screenshots. A capture pipeline does not scroll.
     This rule must stay AFTER the rule it releases, and nothing downstream may re-declare
     the pending state without re-declaring this too — same specificity, so source order is
     the whole cascade. scripts/verify_orbox.py enforces both halves. */
  .reveal[data-pending] { opacity: 1; transform: none; }
}

/* ── Responsive: mobile section padding + grid collapse ── */
/* One breakpoint, 860px, for the whole system. Not because 860 is magic but because a
   second breakpoint is a second set of rules to keep true, and the type already scales
   continuously — the clamps do the work between the columns collapsing. */
@media (max-width: 860px) {
  :root { --section-pad: 64px; --page-pad: 24px; }
  .section-head-row { grid-template-columns: 1fr; }
  .section-head-row .sub { text-align: left; }

  /* Responsive logo: in the topbar the word drops and the mark stays. The disc is the
     recognisable half and it survives at 24px; the wordmark at phone width either
     crowds the nav or shrinks below its own minimum, and a squeezed wordmark reads as
     a broken logo.
     Scoped to `.topbar` on purpose. The unscoped version of this rule also stripped the
     word off the *footer* lockup, where none of that reasoning applies — the footer
     brandmark has a full column to itself and no nav to crowd, and a lone bronze disc
     above a tagline reads as a failed image, not as a logo. A responsive rule inherits
     the constraint of the place it was written for; carrying it everywhere the class
     appears is how you get a fix that breaks the component somewhere else. */
  .topbar .brandmark .wordmark { display: none; }
  .account .who { display: none; }

  /* The nav moves to its own row instead of staying a third column.
     The first version of this block kept the desktop `auto 1fr auto` and only added
     `overflow-x: auto` to the nav, on the theory that a scroller is cheaper than a
     second row. It deleted the nav. `overflow-x` other than `visible` sets a grid
     item's automatic minimum size to zero, so the 1fr track was free to collapse —
     and it did, because the account column is `auto` and a search input's intrinsic
     width is about twenty characters. Measured at 390px: nav width 0, its three links
     laid out past the topbar's own right edge, invisible. Every gate still passed,
     because no gate can see that a box is the wrong size.
     Two rows costs 45px of header. A missing primary nav costs the navigation. */
  .topbar-inner {
    grid-template-columns: auto minmax(0, 1fr);
    gap: 8px 12px; min-height: 56px; padding-block: 8px;
  }
  .brandmark { grid-area: 1 / 1; }
  .account   { grid-area: 1 / 2; min-inline-size: 0; }
  .search    { flex: 1 1 auto; min-inline-size: 0; }
  .nav       { grid-area: 2 / 1 / 3 / -1; }

  /* The nav, the filter row and the pager scroll instead of wrapping. All three are
     single-line sets: a wrapped pager reads as two paginations, a wrapped filter row
     loses the "these are one choice" reading, and a nav that wraps changes the header
     height as you navigate. The pager was overflowing the *document* before this —
     435px of scroll width in a 390px viewport, which drags the whole page sideways. */
  .nav, .filter-row, .pager { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
                              justify-self: stretch; }
  .nav::-webkit-scrollbar, .filter-row::-webkit-scrollbar,
  .pager::-webkit-scrollbar { display: none; }
  /* Inside a scroller the items must keep their size; shrinking them to fit defeats
     the scroller and drags the 34px hit target below the 34px it is specified at. */
  .pager-item, .pager-gap, .filter-row .chip { flex-shrink: 0; }

  .footer { grid-template-columns: 1fr 1fr; gap: 32px 24px; }
  .band-grid { grid-template-columns: 1fr; gap: 40px; }
  .band-focal { justify-self: start; max-inline-size: 320px; }
  /* copy first on a phone, whichever side the focal sits on at desktop */
  .band-grid--focal-left > .band-focal { order: 0; }
}

/* ── end of ORBOX canonical tokens ──────────────────────────────────────
   This line is a structural anchor, not decoration. Each example in examples/
   pastes this whole file verbatim into its <style> block so it stays a single
   double-clickable artifact, and scripts/sync_examples.py + the --pack gate use
   the banner at the top and this comment at the bottom to find that paste.
   Anything an artifact adds of its own goes below this line. Do not delete it,
   do not move it, and do not put a rule after it. ────────────────────────── */
