/*
 * HouseChalk — adaptive decisions mockups, shared design system.
 *
 * THE SPACING LADDER, ADDED 2026-08-06.
 *
 *     0 · 2 · 4 · 8 · 12 · 16 · 24 · 32 · 40 · 48 · 56 · 64
 *
 * Every padding, margin, gap and box dimension in _ds/*.css is on it. The set
 * used to carry 51 distinct spacing values, measured one screen at a time,
 * and the difference between most neighboring pairs — 13 against 14, 21
 * against 22 — was not a decision anybody had made. They are snapped now:
 * nearest rung, ties rounding up.
 *
 * 12 is the one half-step above 8 and it is load-bearing. `.card +
 * .card-note` hugs at 12 where every other block pair takes 24
 * (components.css:133), and a ladder without it cannot say that.
 *
 * NOT ON THE LADDER, and not to be put on it: hairlines and rings (1px,
 * 1.5px, 2px), border-radius (its own --r-* scale), font sizes and
 * line-heights (their own ramp above), letter-spacing, and device dimensions
 * — the 390px phone, the 844px screen, the 940px page. A 1.5px ring rounded
 * to a rung stops being a hairline, and a phone is not a spacing decision.
 *
 * Prose in the comments below sometimes quotes a pre-snap measurement — "gap:
 * 14px" where the rule now reads 16. That is the measurement the value came
 * from and it is left as provenance, not as a contradiction.
 *
 * packages/hc_design carries the same ladder in HcSpace, so the deck and the
 * Flutter library agree rather than approximate each other.
 *
 * THE TOKENS BELOW ARE THE DESIGN SYSTEM'S, NOT THIS FILE'S OWN.
 * They come from the registered brand `user:housechalk` — the skill package at
 * `.claude/skills/housechalk-app-design-system/`.
 *
 * WHAT THE MOVE IS, AS OF 2026-09-05. Pigment #283EC4 is the identity anchor
 * AND the next product action; Aqua Forest #59A686 is status — settled, live,
 * on site — and nothing else. Green never becomes a primary action.
 *
 * This is the SECOND repalette in two days and the first one was wrong. It put
 * this file on Congress Blue #0B4B88 with Fraunces and DM Sans, from a package
 * `main` had replaced four hours earlier — values captured from this very deck,
 * so applying them moved nothing. docs/OPEN_QUESTIONS.md Q179 carries the
 * ancestry check. What survives from that pass is the part that was never about
 * the hue: --success stopped being an alias of --primary, so the deck no longer
 * draws "done" in the same color it draws "do this".
 *
 * STAGE 1 OF EIGHT was values only — the palette, the one face and the ramp.
 * STAGE 2 was this file's spacing and radius ladders and the removal of every
 * texture: both grids and the page glow are gone, and --page-ground is now the
 * desk rather than a second name for a zone.
 * STAGE 3 is the tinted grounds. Six tokens and three card classes are gone,
 * and 142 call sites across 34 pages are cards. What a tint used to say is now
 * a word: an eyebrow, a chip's ink, a glyph, a figure's value, a dot. See
 * docs/DESIGN_SYSTEM_REALIGNMENT.md.
 *
 * The earlier provenance, kept because the type ramp and the elevation still
 * come from it: the claude.ai/design project "HouseChalk Design System
 * Revised" (0cba408e-e513-4afe-9083-3f8721909963).
 *
 * WHAT WAS AND WAS NOT CHANGED. The token NAMES are still mostly untouched —
 * the pages keep reading --surface, --on-surface-variant, --primary and the
 * rest, so the palette moved without a page-by-page sweep. What changed is what
 * those names RESOLVE TO. Radii and spacing are deliberately left alone: they
 * are not what was wrong, and moving them would reflow every frame in the set.
 *
 * THE EXCEPTION, AND IT IS THE POINT OF THE CHANGE. Where a rule in these
 * sheets meant SETTLED and reached for --primary-deep on --success-surface, it
 * was re-pointed to --success-deep. Those sites were swept by hand, one at a
 * time, because only reading the rule says which of the two meanings it wanted:
 * `.step.now` is an action and stayed blue, `.step.done` is status and went
 * green, and no automatic rewrite could have told them apart.
 *
 * --accent-ink AND --accent-line SPLIT FOR A REASON THAT MAY HAVE EXPIRED.
 * They exist because the old dark accent, Azure #33689C, measured 2.92:1 on
 * Mirage and could not be small text — so the label went white and the blue
 * survived only as the underline. The dark accent is #AEB8F3 now and the
 * arithmetic is different, so both are pointed at the blue again. If
 * ./scripts/check-deck-contrast.sh disagrees, --accent-ink goes back to
 * --on-surface and the split earns its keep a second time. Keep both names
 * either way: in light they are one value and the split is a no-op, which is
 * exactly how --on-primary and --on-hero hid their own difference until dark
 * mode separated them.
 *
 * THE ONE COLLISION, AND HOW IT WAS RESOLVED. This file's --sage, --lavender
 * and --clay were TINT FILLS (a card ground) where the design system's tokens
 * of the same three names are ACCENTS, so the design system's raw names were
 * deliberately not imported here. Stage 3 settled it by deletion rather than
 * by translation: there are no tinted grounds, so there is nothing left for
 * the three names to collide over. The rule that outlived it is the general
 * one — if you are copying a rule out of the design system, translate it into
 * this file's names; do not paste it.
 *
 * DARK MODE. Set data-theme="dark" on <html>. The first dark block was built
 * from the system's one dark surface and its on-dark ramp; that compressed the
 * whole application into a near-black band. The block below is the revised
 * five-level HouseChalk ladder, while retaining the system's stated rules,
 * chiefly "accents go bright" and Button.jsx's onDark variant.
 */
/* ONE FACE. Figtree sets every word in the product — titles, body, figures,
   chips, the coach's quoted words. Hierarchy is size, weight and tracking, in
   that order, and never a second family.

   Served from this directory rather than a CDN, and that is not provenance.
   With no network Chrome waits an @import out before painting, which is what
   made a page take 5s once and five minutes the next in the contrast gate —
   and worse, it paints the FALLBACK and the gate reads clean numbers off the
   wrong face with no error anywhere. The skill package shipped exactly that
   bug until 2026-09-05.

   DM Sans stays as the local fallback, not a second voice. Fraunces is gone:
   the system consolidated to one face, and a serif display step is now a
   larger, tighter, lighter treatment of the same family. */
@font-face {
  font-family: 'Figtree';
  src: url('fonts/Figtree-VariableFont_wght.ttf') format('truetype-variations');
  font-weight: 300 900; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Figtree';
  src: url('fonts/Figtree-Italic-VariableFont_wght.ttf') format('truetype-variations');
  font-weight: 300 900; font-style: italic; font-display: swap;
}
@font-face {
  font-family: 'DM Sans';
  src: url('fonts/DMSans-VariableFont_opsz_wght.ttf') format('truetype-variations');
  font-weight: 100 1000; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'DM Sans';
  src: url('fonts/DMSans-Italic-VariableFont_opsz_wght.ttf') format('truetype-variations');
  font-weight: 100 1000; font-style: italic; font-display: swap;
}

:root {
  /* ---- Surfaces. Three levels, and the order is the point: a card sits
     ABOVE the page and a well sits BELOW it. --surface-lowest is the well
     (options, muted callouts), not a third card. ---------------------------- */
  --surface:            #F2F2F2;  /* cool neutral — the page, never white */
  --surface-card:       #FFFFFF;  /* an object you can act on or cite */
  --surface-container:  #E8E8E8;  /* a zone: a region of the screen, edge to edge */
  /* THE WELL IS LIGHTER THAN THE PAGE NOW, AND THAT IS NOT A MISTAKE. A well is
     a recess inside a CARD, and the card is #FFFFFF — so #F5F5F5 sits below the
     card while never touching the page. Under warm paper the page was
     near-white, so the well had to go darker than it to read at all; a cool
     #F2F2F2 page makes white a real step up, which is the entire argument for
     the cool ground, and the well follows the card rather than the page. */
  --surface-lowest:     #F5F5F5;  /* a well INSIDE a card, never a third card */

  --on-surface:         #15171A;  /* near-black, never pure black — 16.0:1 */
  --on-surface-variant: #5B6068;  /* the muted tone at 14px and up — 5.6:1 */
  /* AS TEXT GETS SMALLER, MUTED GETS DARKER. --on-surface-variant is the muted
     tone at 14px and up; --on-surface-dense is the muted tone at 13px and
     below. That is a measurement, not a second gray for taste, and the package
     makes the same split.

THREE TOKENS WERE BROKEN BY THIS COMMENT AND THE ONE BELOW IT. The prose
     used to close its comment and then keep writing, so ~13 lines of English
     sat in the middle of `:root` as invalid CSS — and a browser recovering from
     that skips to the next `;`, which SWALLOWED the declaration underneath.
     Measured in headless Chrome against main: --on-surface-dense (47 call
     sites) and --success both resolved to the empty string, and --success took
     --sage with it, because the settled tint was a color-mix that read it and a
     mix with an invalid argument is itself invalid — sixteen pages drew --sage
     at the time. (--sage is deleted now; the failure mode is not.)

     Nothing caught it, and the reason is worth keeping: `var()` on a dead token
     is invalid-at-computed-value-time, so a `color` INHERITS instead — those 47
     sites drew --on-surface, which is darker, so the contrast gate measured
     MORE contrast and passed. A gate that reads rendered pixels cannot see a
     token that is not there. `npm run deck:orphans` is the check that can. */
  --on-surface-dense:   #494E56;  /* the muted tone at 13px and below — 7.4:1 */

  /* ---- The blue.
     Pigment is BOTH the identity mark and the next product action, which is the
     system's single largest claim — "Pigment is what you can do; Aqua Forest is
     what is already true". It is between cobalt and ultramarine, deliberately
     away from corporate navy and deliberately not near-black, because a filled
     action has to look pressable.

     The --primary / --primary-flat split survives every repalette because it
     was never about the hue: --primary is the TEXT-BEARING fill, --primary-flat
     is the raw accent for 4px bars, status dots and anything with nothing
     written on top. Never write on --primary-flat.

     Pigment carries pure #FFFFFF — 8.15:1. The Romance off-white went with
     Congress Blue. */
  --primary:            #283EC4;  /* pigment — the text-bearing fill, 8.15:1 */
  --primary-flat:       #5A68DA;  /* bars, dots, indicators. NO TEXT. */
  --primary-deep:       #1B2A8F;  /* ink on the blue wash */
  --on-primary:         #FFFFFF;
  --primary-surface:    #E8EAF9;  /* the ONE remaining wash: selected navigation */
  /* Evidence is a SETTLED thing, so these follow --success, and they are
     color-mix rather than a frozen rgba: BRAND_ALIGNMENT_REVIEW.md §"Proposed
     order of work" names the old hardcoded sage rgba as the trap — a value
     swap on --primary would have moved every other accent and left these two
     behind, still green, with nothing pointing at them.

     THEY ARE STILL LITERALS, AND THE REASON IS AN ALPHA. The review asks for
     color-mix off --success, and it is not available here.
     `packages/hc_design/tool/gen_tokens.dart` DOES resolve color-mix — --sage,
     --lavender and --clay were formulas, until stage 3 deleted all three — but
     only between two OPAQUE `var()` colors whose weights sum to 100%.
     `color-mix(..., transparent)` is an alpha rather than a mix, `_channels`
     takes `#RRGGBB` and nothing else, and `dart tool/gen_tokens.dart --check`
     exits 255 on it. `rgba()` the generator does handle, and these two are
     alphas, so they are Aqua Forest #59A686 written out at the two values with
     the derivation named here rather than encoded. */
  --evidence-fill:      rgba(40,62,196,0.08);    /* pigment @ 8% */
  --evidence-halo:      rgba(40,62,196,0.12);    /* pigment @ 12% */

  /* ---- THE ACCENT AS TEXT, AND THE ACCENT AS A LINE.
     One color in light, and historically two in dark — see the header. Every
     link, every accented word and every eyebrow in this sheet reads
     --accent-ink; anything that draws the accent as a rule, an underline or a
     ring reads --accent-line. An eyebrow is a DRAWING, not a place: uppercase
     at 12px/600 with eyebrow tracking IS an eyebrow wherever it appears, and
     every one of them takes accent ink. */
  --accent-ink:         #283EC4;
  --accent-line:        #283EC4;

  /* ---- THE INK ON A NON-PRIMARY ACCENT FILL, and why --on-primary cannot be
     it any more. --on-primary FLIPS AGAIN under Pigment: the dark accent
     #8B98EC is bright, so its ink goes near-black, exactly as the old sage
     accent did and exactly as Congress Blue did not. That does not put
     --on-accent-fill out of a job — it is the ink for a NON-primary accent fill,
     indigo or amber, which go bright on their own schedule. Keeping the two
     names is what stopped five pages drawing white on #E39A76 (2.28:1) the last
     time --on-primary changed its mind. */
  --on-accent-fill:     #FFFFFF;

  /* ---- The green, and it did not move.
     Aqua Forest #59A686 is settled, success, live and on site. It is never a
     primary action and it never carries a label. The package carries it across
     unchanged, which makes it the one value in this block that survived the
     repalette — everything around it is Pigment now.

     THESE THREE USED TO BE ALIASES OF --primary. `--success: var(--primary)`
     meant the deck drew "done" in the same color it drew "do this", and the
     system exists partly to stop that.

     AND IT IS STILL NOT A DOT ON A LIGHT GROUND — it is worse on this ground
     than the last one. Measured, Aqua Forest was 2.79:1 on warm paper; on the
     cool #F2F2F2 page it is 2.6:1, and on the #E8E8E8 zone lower still. It
     fails the 3:1 separation bar before anything is written on it, so a bare
     green dot on a light screen is not a discernible mark. It is a FILL you
     write near-black on, and the ground of the green wash. A green DOT or BAR
     takes --success-deep. Q179 flagged this before the ground moved and the
     arithmetic agrees; ./scripts/check-deck-contrast.sh is what settles it. */
  --success:            #59A686;  /* aqua forest — a fill under near-black */
  --success-deep:       #2A6349;  /* the ink and the dot — 7.0:1 on white */
  /* A MARK's disc only: a tick, never a panel. */
  --success-surface:    #E4F2EB;

  /* ---- INK ONLY, AND THAT IS NOW LITERAL: THE MID-STEPS ARE GONE.
     --amber, --indigo and --terracotta were the deck's own base accents and
     the design system declares NONE of them -- `tokens/colors.css` names only
     the three `-deep` inks, and says why on the same line: "There is
     deliberately no --amber-surface or --indigo-surface to reach for." A mid
     step with no ink token to write on it is a fill waiting to happen, and it
     was: two conversation pills were filled in --indigo for an ACTION, and
     --on-amber existed to be written on an amber fill that the system does not
     have. --insight-fg went with them -- it was a fourth hue for "what
     HouseChalk inferred", which is not one of the four meanings, and its eight
     call sites were prose. ---- */
  /* People, position, advocacy. The mark is what carries it. */
  --indigo-deep:        #4E4784;

  /* Time and money, and amber carries both: a clock, or a costed figure. */
  --amber-deep:         #8A5A17;

  /* ---- Conflict. THIS IS ROSE, NOT CLAY, and the choice is deliberate.
     The old --terracotta was #BD6940, which is byte-identical to the system's
     --clay — so mapping terracotta onto clay would have been the tidy move and
     would have quietly destroyed the chip vocabulary, collapsing "a price over
     its allowance" and "two things claimed on one wall" into one color. The
     system's rose is documented as "caution, never alarm", which is exactly
     what a clash in this product is. What HAS moved since this was written is
     the fill half: the system declares --terracotta-deep and nothing above it,
     so rose is an ink and the mid step is gone with the other two. */
  --terracotta-deep:    #9A4747;  /* rose-ink */

  /* ---- Destructive, and it is NOT terracotta. The design system declares this
     pair with its own rule -- "Only ever an outline action, never a filled
     primary" -- and the deck had no destructive color at all, so Material's
     `error` slot was taking --terracotta, a STATUS ink meaning "a genuine
     clash". Deleting a seat and disagreeing with a quote are not the same
     thing and must not be the same red. --signal-red is the package's own
     name for the first value. ---- */
  --danger:             #DC2828;
  --danger-deep:        #A81D1D;

  /* ---- The hero band, and why its ink is its own token --------------------
     The hero is a DARK GREEN GRADIENT IN BOTH THEMES. It is not a surface that
     flips; it is the one place the product raises its voice, and it stays dark
     when the lights go down because everything around it has gone dark too.

     Its ink therefore stays LIGHT in both themes — and that is exactly why it
     cannot be --on-primary. In the light theme those two agree (white on the
     sage fill, white on the hero) and the agreement hid the fact that they are
     different jobs. In dark they part company: --primary goes bright sage and
     its ink has to go dark, while the hero stays dark and its ink has to stay
     light. Sharing one token meant the hero's headline turned #1E2824 on
     #1E2824 and the whole card went blank.

     If a surface does not flip with the theme, its ink does not either, and it
     needs a name of its own.

     The hero is now BLUE in both themes. It used to be one of five surfaces
     allowed to carry the blueprint grid; there is no grid to carry, and the
     gradient alone is what makes it a brand surface. */
  --hero-start:         #283EC4;  /* pigment */
  --hero-end:           #1B2A8F;  /* pigment-ink */
  --on-hero:            #FFFFFF;
  /* THE BLUEPRINT PLATE — the drawing a decision card carries when its room has
     no illustration of its own (Landing.dc.html frame A). A plate, not a
     texture: it fills one 16:9 slot and nothing sits on it but its own lines. */
  --plate-blueprint:    #0B4B88;
  --on-plate-blueprint: #EDF3F9;
  /* THE PLACEHOLDER PLATE — what a card whose head is a picture draws when it
     has none (Space Groups.dc.html turn 6). Gray, not blueprint: on a day-one
     board most spaces have no picture, and the blue made the product's one
     large color most of the screen. The ink draws the grid (at 55%) and the
     house line drawing. */
  --plate-placeholder:    #EDEEF0;
  --on-plate-placeholder: #B9BDC3;
  /* THE SAME PLATE RUN AS A SCREEN-OPENING BAND (Binder.dc.html frame AA).
     The house screen opens on this ground full-bleed behind its claim, which
     needs three inks a 16:9 drawing never carries: the small-caps label, the
     support sentence under the title, and the settled figure's green.
     --success-deep is 3.1:1 on this ground and fails; --success-on-plate is
     measured at the 27-33px semibold figure step and passes. Both inks are
     solid rather than alpha-muted white, per the contrast rule.

     --plate-figure is the one tinted ground the design system admits, and the
     narrow rule that admits it: a tint is still not a rung on the page ground;
     inside the band a 12% white well is how a FIGURE gets a tile, and nothing
     but a figure goes in one. */
  --on-plate-blueprint-muted:   #B9CFE4;
  --on-plate-blueprint-support: #D6E6F2;
  --success-on-plate:           #8ECBAF;
  --plate-figure:               rgba(255,255,255,0.12);
  /* THE LAUNCH SURFACE, AND IT IS NOT ONLY A DECK TOKEN.
     `scripts/build-theme-boot-assets.mjs` reads --boot-ground out of this file
     and writes the native launch surface from it: web/index.html's theme-color
     and body background, manifest.json, android colors.xml, boot-assets.json.
     `check-theme-token-generation.mjs` asserts every one of those against it.
     So it moves as one closed-set change across seven files, or not at all —
     it was deliberately left behind by the deck-only pass for exactly that
     reason.

     --on-boot is a different case and does NOT move with the palette: it is
     the EXACT fill of logo-mark-white.svg, and the gate asserts the two are
     equal. It changes when the asset does. */
  --boot-ground:        #283EC4;  /* pigment — the invariant launch surface */
  --on-boot:            #EBEBEB;  /* exact fill of the canonical white brand mark */

  /* ---- CHROME, AND IT IS NOT THE PAGE. Four sheets hardcoded a chrome color
     because there was no token for one: role-colors.css and xnav.css each
     carried `--nav-shell:#181C25` twice and `#0B4B88` once, so the dock and the
     desktop rail sat out of the palette entirely and survived a repalette
     untouched. That is what put the builder shell 4.35:1 against a 4.5 floor
     after everything else had moved, and it breaks the system's own "no raw
     color literals in component code" rule.

     --rail-surface is a SOLID DARK NEUTRAL, deliberately not the action color:
     the rail is permanent furniture and pigment is what you can DO, so a blue
     rail spends the action color on the one element that is never the next
     action. Dark also lets the content beside it stay light with no border
     between them — the value change is the edge.

     --shell IS the action color, and only the builder product runs on it. */
  --rail-surface:       #1E2126;
  --on-rail:            #F7F7F8;
  --on-rail-muted:      #9AA0A8;
  --shell:              #283EC4;
  --on-shell:           #FFFFFF;

  --outline:            #E2E2E2;  /* a card's own edge — the one inset ring */
  --outline-strong:     #CBCBCB;  /* the drawn edge on a thing with no fill */
  /* IT MOVES WITH THE GROUNDS UNDER IT, and they have just all moved. The
     ratios that chose this value were measured on warm paper, Carrara and the
     Carrara-derived well, none of which exist any more. It is HELD at its old
     value, and the gate has now run on the new grounds: it clears 3:1 against
     all four light surfaces. The old ratios described a different page; do not
     quote them, re-run ./scripts/check-deck-contrast.sh. */
  --control-outline:    #7B7C80;  /* 4.17:1 on --surface-card — Q245 */

  /* ---- The type scale, from tokens/typography.css -----------------------
     THIS IS THE PART THE FIRST PASS LEFT BEHIND, and it was the part that
     mattered most. The palette and the elevation moved to the design system
     while every font-size in this file stayed where it was, so the screens
     had the system's color and the old sheet's proportions — which is why
     they still did not look like the thing they were retokened from.

     The gap was not subtle. An option title was 14px against the system's 19,
     a supporting line 12.5 against 15, a headline 22 against 26. Roughly a
     quarter smaller everywhere, which is the whole of "the body text is
     really quite small".

     Reach for a step. Sixteen distinct body sizes were typed by hand across
     the set before the ramp existed; there is no reason to type a
     seventeenth. */
  --text-3xl:  42px;  /* the arrival title — a screen with one thing on it */
  --text-2xl:  33px;
  --text-xl:   27px;  /* the screen title */
  --text-lg:   21px;  /* the block title — cards and bare blocks alike */
  --text-md:   18px;  /* a lead sentence, an option name */
  --text-base: 16px;  /* body */
  --text-sm:   15px;  /* a supporting sentence */
  --text-xs:   13.5px; /* metadata */
  --text-2xs:  12px;  /* chips, the eyebrow */
  --text-3xs:  11px;

  --leading-display:  1.04;
  --leading-tight:    1.14;
  --leading-snug:     1.28;
  --leading-body:     1.55;
  --leading-relaxed:  1.62;

  /* AS TYPE GETS LARGER IT GETS TIGHTER, and there are two steps of it now.
     A 42px heading at default tracking looks loose and unresolved. --tracking-
     display is for --text-2xl and above; --tracking-title is the gentler one
     for --text-lg and --text-xl, which used to take no tracking at all. */
  --tracking-display: -0.03em;
  --tracking-title:   -0.02em;
  --tracking-eyebrow: 0.13em;

  /* WEIGHT CARRIES HIERARCHY, so the steps are named rather than typed. --text-
     lg is the block-title step and it is ALWAYS 700. Emphasis lives INSIDE a
     sentence: --weight-bold 800 inside a title, where the surrounding type is
     regular at 27 or 42px, and --weight-semibold 600 inside an already-muted
     support line, because 800 at 16px on a quiet line reads as shouting. */
  --weight-regular:   400;
  --weight-medium:    500;
  --weight-semibold:  600;
  --weight-bold:      800;

  /* BOTH NAMES RESOLVE TO ONE FACE, and they survive only as job names: display
     is the larger, tighter, lighter treatment; body is the operational one.
     They used to be Fraunces and DM Sans. If you find yourself wanting a second
     family, the answer is a step on the ramp above or a weight, not a new face.

     Keeping both names is deliberate. Re-pointing them costs one line here;
     collapsing them to one would have edited ~90 call sites in a change that is
     supposed to be a value swap, and would have thrown away the record of which
     of the two jobs each site is doing — which is exactly what a future pass
     needs if the system ever takes a display face again. */
  /* The stack is the package's, character for character. It used to carry
     'Helvetica Neue' between Segoe UI and Arial; that is one more name in a
     fallback chain nothing reaches, and keeping it made the two stacks
     unequal for no gain. */
  --font-display: "Figtree","DM Sans",system-ui,-apple-system,"Segoe UI",Arial,sans-serif;
  --font-body:    "Figtree","DM Sans",system-ui,-apple-system,"Segoe UI",Arial,sans-serif;
  /* The one place a face other than Figtree is correct: a code, a one-time
     passcode, a reference number — a value read character by character rather
     than as a word. Written out as `ui-monospace, monospace` at three sites
     before this token existed. */
  --font-mono:    ui-monospace, SFMono-Regular, Menlo, monospace;

  /* ---- Motion. ONE CURVE, and it is the only motion token this deck earns.
     The package also declares --motion-duration, --motion, --lift-hover and
     --lift-press; they are deliberately NOT here, because the deck has thirteen
     transition declarations and all of them are in xnav.css with their own
     literal durations. Declaring four more tokens nothing wears is what
     `.blueprint` was — defined, documented at length, never once applied.
     Add each one in the change that gives it a call site.

     --nav-ease is re-pointed at this rather than replaced at its 24 call sites.
     It held cubic-bezier(0.22,0.61,0.36,1) — the same curve, rounded. */
  --motion-ease:  cubic-bezier(0.215,0.61,0.355,1);

  /* ---- Radius. THE SIGNATURE MOVED, and it is the value most likely to be
     put back by somebody quoting the old layer: 24px is the card, not 20px.
     Every step shifted with it — controls to 18, the large step to 28, the
     teardrop to 24/14 — so a card and a control no longer differ by 4px, which
     was too little to read as a distinction and too much to read as a mistake.

     --r-zone IS NOT DECORATION, it is one of the three things that tell a zone
     from a card. A zone runs to the screen edge, is flat, and is square; a card
     is inset, raised and rounded. Declared as a token rather than written as 0
     at each site so a zone can be recognized by what it reads. */
  --r-sm:          12px;
  --r-base:        18px;   /* controls */
  --r-card:        24px;   /* the signature */
  --r-lg:          28px;
  --r-full:        999px;
  --r-zone:        0px;    /* a region of the screen is square, always */
  --r-teardrop:    24px 14px 14px 14px;
  --border-weight: 1px;

  /* ---- Spacing. THE DECK HAD NO SPACING SCALE AT ALL until this stage — not
     one token — while every padding and gap in it was a literal. The ladder is
       0 · 2 · 4 · 8 · 12 · 16 · 24 · 32 · 40 · 48 · 56 · 64
     and 12 is the load-bearing half-step: a card note hugs its card at 12 where
     every other block pair takes 24.

     NOT ON THE LADDER, and not to be put on it: hairlines and rings (1, 1.5,
     2px), border-radius, font sizes, line-heights, letter-spacing, and device
     dimensions. A 1.5px ring rounded to a rung stops being a hairline. */
  --sp-0:  0px;
  --sp-2:  2px;
  --sp-4:  4px;
  --sp-8:  8px;
  --sp-12: 12px;
  --sp-16: 16px;
  --sp-24: 24px;
  --sp-32: 32px;
  --sp-40: 40px;
  --sp-48: 48px;
  --sp-56: 56px;
  --sp-64: 64px;

  /* The named steps. A component never sets its own external margin — the
     container owns the gap — and a component owns every gap inside itself. If
     two elements need a gap smaller than the block rhythm, they are one
     component and need a wrapper.

     THESE THREE MOVED HERE FROM components.css, WHERE THEY WERE DECLARED ON
     `body.hc-air` RATHER THAN :root. That scope was correct while they were
     the component library's own scale and wrong the moment the rest of the
     ladder existed — a page that is not `.hc-air` would resolve `var(--sp-block)`
     to nothing, and an invalid var() takes the whole declaration with it. Every
     one of the ~580 callers happens to be on an `.hc-air` page today, so
     nothing was broken; nothing was protecting it either.

     TWO OF THEM ARE DELIBERATELY OFF THE LADDER above, and that is the
     package's decision rather than a slip: 6 is below the smallest rung and 28
     sits between 24 and 32. A gap inside one component and the rhythm between
     two blocks are the two measurements the 8px ladder cannot express — 4 is
     touching and 8 is a gap; 24 separates two cards, whose edges do most of
     the work, and does not separate two blocks where one has no edges. */
  --sp-tight:     6px;    /* a label and the thing it labels, inside a component */
  --sp-snug:      12px;   /* parts of one component; a card and its explaining line */
  --sp-block:     28px;   /* the gap between two blocks in a scroll — was 22px */
  --state-padding: 24px;  /* a state block is padded like a card, not a control */
  --card-padding:  24px;
  --card-gap:      24px;
  --section-gap:   48px;  /* the air above a named run, on top of --sp-block */
  --page-pad:      24px;
  --touch-min:     44px;  /* never smaller */
  --dock-reserve: 112px;  /* clear air a scroll keeps under a floating dock */

  /* ---- Elevation. Every step is two-part: a tight contact shadow and a wide
     diffuse one. A single-shadow lift is the tell that a surface is floating
     rather than resting on something. */
  --shadow-whisper: 0 1px 2px rgba(21,23,26,0.035), 0 8px 20px -8px rgba(21,23,26,0.07);
  --shadow-soft:    0 1px 2px rgba(21,23,26,0.04), 0 14px 30px -10px rgba(21,23,26,0.10);
  --shadow-card:    0 2px 4px rgba(21,23,26,0.05), 0 26px 52px -14px rgba(21,23,26,0.14);
  --shadow-float:   0 6px 14px rgba(21,23,26,0.06), 0 40px 76px -22px rgba(21,23,26,0.18);
  --shadow-glow:    0 10px 24px -12px rgba(40,62,196,0.40);
  /* THE ONE RING THAT WAS WRITTEN AS A LITERAL, AND THE ONLY ONE THAT DID NOT
     RE-POINT. A 26% white ring reads on --primary in light and is inert in
     dark, where the fill is #8B98EC and already the lightest thing near it —
     1.25:1 against it. It mixes --on-primary instead, which is the ink ALREADY
     sitting on that fill: white in light, near-black in dark. Declared once,
     here, because a custom property resolves at use: the dark block's
     --on-primary carries it without a second declaration. Q244 Ruling 1. */
  --ring-selected:  inset 0 0 0 1.5px color-mix(in oklch, var(--on-primary) 26%, transparent);
  /* A WHITE CARD'S HAIRLINE IS AN INSET RING COMPOSED INTO THE SHADOW LIST,
     never a border: a real border moves content in by a pixel and reflows every
     card. Written out at ~40 sites before this token existed. */
  --ring-hairline:  inset 0 0 0 1px var(--outline);
  /* A CONTROL'S RESTING EDGE, AND THE HAIRLINE IS NOT IT. --ring-hairline is
     1.30:1 light and 1.16:1 dark — enough to divide two rows inside a card,
     nowhere near the 3:1 WCAG 1.4.11 asks of an interactive boundary. Anything
     you can press that has no fill of its own draws this instead. Q245. */
  --ring-control:   inset 0 0 0 1px var(--control-outline);
  --ring-focus:     0 0 0 3px color-mix(in oklch, var(--primary) 34%, transparent);

  /* ---- The desk, and there is no texture on it.
     `tokens/patterns.css` is the whole rule and it is one sentence: "THERE IS
     NO PATTERN." Both grids are gone — the 64px page whisper (--grid-line, on
     body and every phone) and the 32px blueprint (--blueprint-*, --on-shell-
     grid), along with the glow the page grid sat over (--page-glow,
     --glow-from, --glow-to). Nine tokens and six declarations.

     WHY, in the package's own words: it competed with every surface it sat
     behind, it forced page grounds to set background-COLOR rather than the
     shorthand or silently lose their texture, and on a dense screen it added a
     third thing for the eye to resolve before reading a word. The surface
     ladder does the work the grid was decoration for.

     `.blueprint` and `.blueprint-on-shell` went with them, and it is worth
     recording that removing THOSE changed nothing: measured 2026-09-05, both
     had zero call sites in the deck's markup. The plan named them for three
     weeks as the thing to delete, and deleting them would have left every grid
     a reader can actually see exactly where it was.

     --page-ground SURVIVES, and it is no longer the same color as a zone. It
     is the desk a phone or a document is drawn ON — not a surface inside the
     product, which is why it has no ink token and nothing reads on it. It was
     #E8E8E8, which is --surface-container: the desk and a zone were literally
     the same value, so a zone drawn near a frame edge had no edge. */
  --page-ground: #DEDEDE;

  /* ---- The undecided room ---------------------------------------------
     The recolorable kitchen drawing reads every surface as var(--r-NAME,
     <literal>), and a page paints a surface by setting the variable inline.
     What was never declared anywhere is the OTHER half: the color a surface
     takes when nobody has decided it. Each drawing carried its own literal
     fallback, which is how the illustrations stayed paper-white on the dark
     theme and sat on the page as lit rectangles.

     So the undecided values are tokens now, and they follow the theme. That
     is not just a dark-mode repair — it is what the drawing was always
     saying. An undecided surface is supposed to render as the app's own
     neutral, and on the dark theme the app's own neutral is dark. Mockup 09's
     "Nothing settled" frame is still entirely undecided and still entirely
     neutral; it is simply neutral in the right key.

     A DECIDED SURFACE IS NEVER TOUCHED BY THIS. Sea Salt, Pewter Green, warm
     oak and honed quartz are paint and stone: they are set inline, they win
     over these defaults, and they look the same in both themes because a
     color somebody chose does not change when the lights go down. */
  --r-wall:           #E8E2D6;
  --r-ceiling:        #F2EDE4;
  --r-trim:           #F7F4EE;
  --r-cabinet-upper:  #D6D2C7;
  --r-cabinet-lower:  #D6D2C7;
  --r-island:         #D6D2C7;
  --r-counter:        #EDEAE3;
  --r-backsplash:     #EFEBE2;
  --r-floor:          #DED5C6;
  --r-appliance:      #C9C6C0;
  --r-fixture:        #B8B4AC;

  /* Floor-plan editorial roles from docs/PLAN_FLATTENING.md §6. */
  --plan-ground:      #F7F3EC;
  --plan-wall:        #33302B;
  --plan-opening:     #6E685E;
  --plan-cabinet:     #B99B72;
  --plan-counter:     #E3D9C6;
  --plan-fixture:     #C4BCAD;
  --plan-appliance:   #ADA69A;
  --plan-floor:       #F1EADC;
  --plan-furniture:   #C9C2B5;
  --plan-landscape:   #94A583;
  --plan-annotation:  #CFC8B9;
}

/* ==========================================================================
   DARK
   Set data-theme="dark" on <html>. Only values move; not one selector below
   this block is aware of the theme, which is the whole reason the token names
   were left alone.
   ========================================================================== */
:root[data-theme="dark"] {
  /* THESE VALUES ARE TAKEN, NOT DERIVED. The package ships a whole dark table
     in tokens/colors-dark.css, including the recessed well this deck needs and
     the previous source did not. Deriving a parallel set would produce values
     close to these and equal to none of them, which is how two systems start
     disagreeing. Take dark from the package; do not re-derive it here. */
  /* COOL NEAR-BLACK, NOT BLUE-BLACK. Mirage #181C25 was a blue-tinted canvas,
     and the package rejects that in one line: "a blue-black canvas under a blue
     action makes the action disappear". The light appearance is a neutral cool
     gray, so its counterpart is the same neutral inverted.

     AND THE ZONE CROSSED THE PAGE. --surface-container used to be LIGHTER than
     the page here — a raised "working panel" — while in light it is darker. A
     component that is a zone in light became a card in dark, so it had to know
     which appearance it was in. It is below the page in both now, which is the
     point of the ladder: above / beside / below, and the same order either way.
     This is the one change in this block that is not a value swap, and screens
     with zones will read differently in dark because of it. */
  --surface:            #18191C;  /* the page */
  --surface-card:       #232529;  /* above the page */
  --surface-container:  #121316;  /* a zone: below the page, as in light */
  --surface-lowest:     #0E0F11;  /* a well inside a card */

  /* The on-dark ramp. */
  --on-surface:         #F7F7F8;
  /* SMALLER TEXT, SO LIGHTER HERE. The rule is "more contrast as text gets
     smaller", and in dark that means moving TOWARD the ink, not away from it —
     which is why -dense is lighter than -variant here and darker in light. */
  --on-surface-dense:   #BFC3C9;  /* 13px and below */
  --on-surface-variant: #A8ACB3;  /* 14px and up */

  /* ACCENTS GO BRIGHT AND THE FILL'S INK GOES DARK. The darkening that makes
     white safe on pigment in light makes the fill vanish here, so a lighter
     blue becomes the fill and a near-black becomes the text on it.

     --accent-ink IS THE BLUE AGAIN. It was white because Azure on Mirage
     measured 2.92:1 and the old rule forbade blue as small text in dark. The
     dark accent is #AEB8F3 now, and the gate has run: 86 of 86 page/theme
     pairs clean, so every eyebrow and every link in dark reads at AA on the
     blue. If that ever stops being true the fix is to put this back to
     --on-surface, not to lighten the palette. */
  --primary:            #8B98EC;
  --primary-flat:       #6E7DE0;  /* bars and dots, no text on top */
  --primary-deep:       #AEB8F3;  /* ink on the blue wash */
  --on-primary:         #0B0E1F;  /* near-black on the bright fill */
  --primary-surface:    #22253A;
  --accent-ink:         #AEB8F3;
  --accent-line:        #6E7DE0;
  --on-accent-fill:     #1C1B1A;  /* 7.16:1 on the bright amber, 5.68 on indigo */
  --evidence-fill:      rgba(139,152,236,0.10);  /* the dark accent @ 10% */
  --evidence-halo:      rgba(139,152,236,0.14);  /* the dark accent @ 14% */

  /* Aqua Forest does not move between themes — it is the one color the package
     carries across both appearances unchanged, and the one value the 2026-09-05
     repalette did not touch. It is a fill and a dot here; settled TEXT on a
     dark card takes --success-deep, which is why these two are different values
     in dark as well as in light. The contrast gate is clean on both. */
  --success:            #59A686;  /* aqua forest — colorSuccess */
  --success-deep:       #8ECBAF;  /* the ink and the dot */
  --success-surface:    #1A2A23;

  --indigo-deep:        #B0A8E0;
  --amber-deep:         #E0A472;
  --terracotta-deep:    #E09B9B;

  --danger:             #E96A6A;
  --danger-deep:        #F0908F;

  /* The hero stays deep and its ink stays light. --on-hero is deliberately NOT
     re-pointed here — it is the same white in either theme, which is the whole
     point of it being a separate token from --on-primary, and --on-primary has
     just gone back to flipping. */
  --hero-start:         #232A5C;
  --hero-end:           #151833;

  /* The plate goes graphite with its grid dialed back (frame E); the room
     plates stay warm, so the plate family reads the same in both. */
  --plate-blueprint:    #14161A;
  --on-plate-blueprint: #E8E9EA;
  /* One step above --surface-card, so the plate still reads as a head on the
     card (Space Groups.dc.html turn 6). */
  --plate-placeholder:    #2B2E33;
  --on-plate-placeholder: #4A4E55;
  /* On graphite the band's inks are neutral rather than blue — the ground has
     no hue left to agree with — and the green does not move: Aqua Forest's ink
     step is the same value in both appearances. The figure well drops to 8%:
     12% white reads as a rung of its own on a ground this dark. */
  --on-plate-blueprint-muted:   #9198A0;
  --on-plate-blueprint-support: #C3C6CB;
  --success-on-plate:           #8ECBAF;
  --plate-figure:               rgba(255,255,255,0.08);

  /* Dark mode uses LUMINANCE EDGES where light mode uses shadow, so the
     hairline carries the separation — and both edges come from the app layer
     too, rather than from a white veil. --control-outline is this deck's: the
     table has no control edge, and a control needs 3:1 where a decorative
     hairline does not. */
  /* The rail barely moves between appearances — it is already dark in light,
     which is the point of it. The shell does move: the builder's chrome is the
     action color in light and the canvas in dark, so nothing has to know which
     appearance it is in. */
  --rail-surface:       #101215;
  --on-rail:            #F7F7F8;
  --on-rail-muted:      #8E949C;
  --shell:              #18191C;
  --on-shell:           #F7F7F8;

  --outline:            #2E3035;
  --outline-strong:     #43464C;
  --control-outline:    #82858C;  /* 4.15:1 on --surface-card. It was #7C8698 at
                                     3.46:1 — over the 3:1 bar but with no margin,
                                     on the appearance where --shadow-whisper is
                                     0 0 #0000 and the edge is the ONLY boundary a
                                     control has. Q245. */

  /* The page ground and its ramp are further down, where this block already
     declared them. They are NOT repeated here: a second declaration above the
     original is dead, and a dead declaration reads like a decision. The
     generator caught this one — dark.glowFrom came out #26312B, the old
     green-black, while a re-pointed copy sat five lines above doing nothing. */
  /* --shadow-glow is NOT re-pointed here. It is set to `0 0 #0000` a few lines
     below with the other two, because dark surfaces separate by value and a
     hairline rather than by shadow. A blue glow declared here would be dead:
     the later declaration wins, and a dead declaration reads like a decision. */

  /* Resting surfaces use value and a hairline. Only genuinely floating
     surfaces use the two deeper shadows below. Kept as declared tokens rather
     than removed so no component selector has to know which theme it is in. */
  /* `0 0 #0000`, NOT `none`, and the difference is load-bearing. These tokens
     get COMPOSED — `.card` is `inset 0 0 0 1px var(--outline), var(--shadow-soft)`
     — and `none` is only legal as the whole value, never as one entry in a
     shadow list. Written as `none` the comma-separated declaration is invalid,
     the browser drops all of it, and the card silently loses its hairline in
     dark while light keeps it. A transparent zero-size shadow composes. */
  --shadow-whisper: 0 0 #0000;
  --shadow-soft:    0 0 #0000;
  --shadow-card:    0 24px 48px -12px rgba(0,0,0,0.55);
  --shadow-float:   0 36px 72px -20px rgba(0,0,0,0.6);
  --shadow-glow:    0 0 #0000;  /* Button.jsx onDark: no glow at rest. Composed on
                                   .mic-lg, so it takes the same treatment. */

  /* The desk in the dark key. This is the one value the package states for a
     dark page ground and it already matched, so it is unchanged. */
  --page-ground: #0C0D0F;

  /* The undecided room, in the dark key. Same ladder as the light values —
     ceiling lightest, then trim, wall, counter, cabinets, floor, fixtures
     darkest — so the drawing keeps its depth and its readability; only the
     key moves. Kept a touch warm rather than neutral gray, because the room
     is still lit and a flatly desaturated drawing reads as switched off. */
  /* The SPREAD matters as much as the key. A first pass mapped these into a
     four-point band and the room went flat — every surface the same value, the
     drawing legible only as a silhouette. Light spans 34 points from fixture
     to ceiling, so dark spans a comparable range rather than hugging the
     shell. Measured lightest to darkest: ceiling, trim, counter, backsplash,
     wall, appliance, cabinets, floor, with fixtures brightest because they are
     the metal and they catch the light in both themes. */
  --r-wall:           #333D36;
  --r-ceiling:        #47524A;
  --r-trim:           #414B44;
  --r-cabinet-upper:  #29322D;
  --r-cabinet-lower:  #29322D;
  --r-island:         #29322D;
  --r-counter:        #3D4740;
  --r-backsplash:     #39433C;
  --r-floor:          #222925;
  --r-appliance:      #2E3833;
  --r-fixture:        #5A6459;

  --plan-ground:      #1B2320;
  --plan-wall:        #D8D2C6;
  --plan-opening:     #9AA39A;
  --plan-cabinet:     #8A7256;
  --plan-counter:     #3D4740;
  --plan-fixture:     #5A6459;
  --plan-appliance:   #4A544D;
  --plan-floor:       #232B27;
  --plan-furniture:   #444E48;
  --plan-landscape:   #5E7359;
  --plan-annotation:  #3A443E;
}

* { box-sizing: border-box; margin: 0; padding: 0; }

/* THE DECK PAGE IS A DESK WITH PHONES ON IT, which is the one place
   --page-ground belongs. Flat: the grid that used to sit here is gone. */
body {
  font-family: var(--font-body);
  background-color: var(--page-ground);
  color: var(--on-surface);
  -webkit-font-smoothing: antialiased;
  padding: var(--sp-56) var(--sp-24) var(--sp-64);
}

/* THE PAGE INSIDE A PHONE IS --surface AND NOTHING ELSE. This rule used to
   compose four background layers — a 64px grid, a radial glow and a two-stop
   vertical gradient — because two classes each setting background-image cannot
   combine and they all had to be declared together. `patterns.css` replaces
   the whole of it with one declaration:

       .hc-ground{background-color:var(--surface)}

   The old comment here warned never to use the `background` shorthand, since
   it would reset background-image and silently take the texture off. That
   warning is now void: there is no image to lose. */
.hc-ground,
.phone,
body.hc-air .canvas {
  background-color: var(--surface);
}

/* ---- Page chrome ---- */
.page-head { max-width: 940px; margin: 0 auto 24px; text-align: center; }
.page-head h1 {
  font-family: var(--font-display);
  font-size: 34px; font-weight: 400; letter-spacing: -0.5px;
}
.page-head h1 em { color: var(--accent-ink); font-style: italic; }
.page-head p {
  margin-top: 12px; color: var(--on-surface-variant);
  font-size: 15px; line-height: 1.6;
}
.page-nav {
  max-width: 940px; margin: 0 auto 48px;
  display: flex; gap: 8px; justify-content: center; flex-wrap: wrap;
}
.page-nav a {
  font-size: 13px; text-decoration: none; color: var(--on-surface-variant);
  background: var(--surface-card); padding: 8px 16px; border-radius: var(--r-full);
  box-shadow: var(--shadow-whisper);
}
.page-nav a.on { background: var(--primary); color: var(--on-primary); font-weight: 500; }

.stage {
  display: flex; gap: 48px; justify-content: center;
  align-items: flex-start; flex-wrap: wrap;
}
.frame-wrap { width: 390px; }
.frame-label {
  text-transform: uppercase; font-size: 11px; font-weight: 700;
  letter-spacing: 2.75px; color: var(--on-surface-variant);
  margin-bottom: 8px; text-align: center;
}
.frame-sub {
  text-align: center; font-size: 13px; color: var(--on-surface-variant);
  margin-bottom: 16px; line-height: 1.5;
}

/* ---- Phone frame ---- */
.phone {
  width: 390px; height: 844px;
  /* background-COLOR. The shorthand here reset the background-image set above
     and took the grid and the glow straight back off. */
  background-color: var(--surface);
  border-radius: 46px;
  box-shadow: 0 40px 80px -24px rgba(0,0,0,0.42), 0 0 0 11px #14110F, 0 0 0 13px #2C2823;
  overflow: hidden; position: relative;
  display: flex; flex-direction: column;
}
.notch {
  position: absolute; top: 0; left: 50%; transform: translateX(-50%);
  width: 128px; height: 32px; background: #14110F;
  border-radius: 0 0 18px 18px; z-index: 50;
}
.appbar {
  display: flex; align-items: center; justify-content: space-between;
  padding: 48px 24px 8px; flex-shrink: 0;
}
.deskbar {
  display: flex; align-items: center; justify-content: space-between;
  flex-shrink: 0;
}
.brand { display: flex; align-items: center; gap: 8px; }
.brand-mark {
  width: 32px; height: 32px; border-radius: 10px;
  background: var(--primary); color: var(--on-primary);
  display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 14px;
}
.brand-name { font-weight: 700; font-size: 17px; color: var(--accent-ink); }
.btn-area {
   display: flex; align-items: center; justify-content: center;
   gap: 8px;
}
.icon-btn {
  width: 40px; height: 40px; border-radius: var(--r-full);
  background: var(--surface-card); box-shadow: var(--shadow-whisper);
  display: flex; align-items: center; justify-content: center;
  color: var(--on-surface-variant);
}
.scroll { flex: 1; overflow: hidden; padding: 8px 24px 24px; }

/* ==========================================================================
   WHAT A HUE MEANS. Fixed here once so a page does not re-derive it, and so a
   reader who has learned one screen has learned all of them. A COLOR IS A
   MARK, AND A SENTENCE NEVER TAKES ONE — it is a claim about the KIND of thing
   it marks, spent on an eyebrow, a chip's ink, a glyph, a figure's value or a
   dot, never on the ground under prose:

     blue / --primary              HouseChalk, and the next action
     green / --success-deep        settled, agreed, done, live, on site
     amber / --amber-deep          time and money; a clock running
     indigo / --indigo-deep        people, position, advocacy
     terracotta / --terracotta-deep  conflict, over, nobody has this
     --surface-container           neutral; not yet, unclaimed, a record

   Two washes survive and neither is a ground for prose: --primary-surface is
   selected navigation, and --success-surface is a mark's disc — a tick, never
   a panel.

     BLUE AND GREEN CHANGED PLACES, and it is the whole of this retheme. Green
     used to be the accent and blue used to mean proof and uploads; now blue is
     HouseChalk and the thing to do, green is the thing already done, and proof
     takes the neutral panel because a document is a record rather than a
     status. A settled chip and a primary button are no longer one color.

   NEVER USE A BASE ACCENT AS ITS OWN FOREGROUND. The -deep tone of each pair
   is the ink and the base is the fill; on the washes these two used to have,
   --terracotta measured 3.32:1 and --amber no better. Every foreground below
   is the -deep half of a pair that already exists in this file.

   AND NEVER DIM AN ON-CONTAINER TONE WITH OPACITY. An on-container tone is
   already the quiet end of its pair, so an alpha on top of it is a second quiet
   step nobody chose — the pages had four of them (.75, .8, .9, .95) and
   --amber-deep at .75 on the old clay measured 3.46:1. If a quieter step is
   genuinely needed, mix toward the surface it lands on instead:

       color-mix(in srgb, var(--amber-deep) 88%, var(--surface-container))

   which stays a color rather than a transparency, and verify it is still
   >= 4.5:1 against that surface.
   ========================================================================== */

/* ---- Type ---- */
/* THE TRACKING IS THE TOKEN, and the literal it replaces was one step's value
   stranded on the step above. `-0.4px` is what -0.02em comes to at --text-lg
   (21px x -0.02 = -0.42); this rule is --text-xl (27px), where the token comes
   to -0.54. The deck reached for var(--tracking-title) at all four --text-lg
   titles and typed the number by hand at all three --text-xl ones — money.css
   did both, seventeen lines apart. Same cause as Q233: the literal was typed
   when the consolidated deck started (`578561a17`) and --tracking-title did not
   exist until `ed3d89ff8`, so the migration that moved everything else could
   not see it. An em token would have followed the step; a pixel cannot. Q247. */
.screen-title {
  font-family: var(--font-display); font-size: var(--text-xl);
  letter-spacing: var(--tracking-title); margin-bottom: 4px;
}
/* THIS IS THE EYEBROW, and it is now the design system's own — Eyebrow.jsx:
   12px / 600 / uppercase / --tracking-eyebrow, in ACCENT INK rather than gray.
   The pages have invented 1.4, 1.6, 1.8, 2.0 and 2.75px tracking for the same
   primitive, which is five spellings of one word. If an eyebrow needs a
   different color, set the color; the metrics do not move.

   Why it grew. At 10px/700/gray it was too faint to lead a section on its own,
   so the pages kept parking it inside a gray well to give it presence — which
   is what made a screen full of them read as a form. Colored and two sizes up
   it holds a section by itself, and the wells can come off.

   AND THE TRACKING IS THE TOKEN NOW, WHICH IT SHOULD ALWAYS HAVE BEEN. This
   rule hardcoded 0.14em from the day the consolidated deck started
   (`578561a17`, 2026-08-02) — before --tracking-eyebrow existed. `ed3d89ff8`
   then moved the deck's type onto the package's and took that token from
   0.14em DOWN TO 0.13em, and could not carry this rule with it, because a
   literal is invisible to a token migration. The eyebrow is the deck's
   most-used single class, so the one declaration that never referenced the
   token is also the one that decides what 1645 renders look like: every page
   kept drawing 0.14em while the token, `Eyebrow.jsx`, every other package
   component and hc_text.dart had moved to 0.13em. `Q233`. */
.micro-label {
  text-transform: uppercase; font-size: 12px; font-weight: 600;
  letter-spacing: var(--tracking-eyebrow); color: var(--accent-ink);
}

/* ---- The body ramp --------------------------------------------------------
   Four steps, matching the shipped Flutter ramp (bodyLarge 18 / bodyMedium 16 /
   bodySmall 14, caption 13). Until these existed there was nothing between
   .screen-title and .micro-label to reach for, so every page typed a number:
   the set carries 16 distinct body sizes and one page sets three of them on
   three consecutive lines. Reach for a step. If none of the four is right, the
   content is probably the wrong shape rather than the ramp being too coarse.

   Note where the muted tone changes hands: .t-sub is 14px and takes
   --on-surface, .t-meta is 13px and takes --on-surface-dense, per the rule at
   the top of this file. */
.t-lead { font-size: var(--text-md);   font-weight: 500; line-height: var(--leading-snug);    color: var(--on-surface); }
.t-body { font-size: var(--text-base); font-weight: 400; line-height: var(--leading-relaxed); color: var(--on-surface); }
.t-sub  { font-size: var(--text-sm);   font-weight: 400; line-height: var(--leading-body);    color: var(--on-surface); }
/* .t-sub is body ink because it is a supporting SENTENCE, not metadata. The
   quiet variant is the same sentence when it sits under a title that is
   already carrying the emphasis — a subtitle rather than a subordinate line.
   It exists because 34 sites had re-declared exactly this color inline,
   which is one missing variant being re-decided rather than 34 decisions. */
.t-sub.quiet { color: var(--on-surface-variant); }
.t-meta { font-size: var(--text-xs);   font-weight: 400; line-height: var(--leading-body);    color: var(--on-surface-dense); }
/* Any figure meant to be compared down a column. Three cards in a row priced
   $2,100 / $8,650 / $21,400 do not compare unless the digits are the same
   width. Put it on the figure, not on the sentence around it. */
.t-num  { font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; }

/* THE AIR ABOVE IT IS THE DIVIDER, so the header is CENTERED AND UNRULED.
   A named run is separated by --section-gap, and once that gap is there the
   rule is redundant decoration: a quiet label for what follows rather than a
   line drawn across the screen.

   --section-gap has been declared 48px on both sides of this design and read by
   NOBODY. Measured before this changed: the lead-in above the deck's 388
   headers was 40 on 319 of them, 28 on 51, 0 on 11, 24 on 4 and 32 on 3 — five
   lead-ins for one component, 69 of them written inline at the call site, which
   is the "sixteen distinct body sizes" pathology of the type ramp comment above
   applied to spacing. It is two now: the gap, or `flush`.

   AND THE GAP IS 76, NOT 48. The token's own comment says --section-gap is the
   air above a named run "ON TOP OF --sp-block", and the package composes it
   that way: its Scroll sets `gap: var(--sp-block)` and its SectionHeader adds
   `paddingTop: var(--section-gap)` inside that. This sheet's .scroll has no
   gap — every block's lead-in is its own margin — so the sum is written here
   instead. Taking 48 alone would have been the package's conclusion with half
   its premise, which is how a rule stops carrying its own weight.

   IT STAYS A MARGIN. The package owns this as the component's own padding, and
   that is right where a call site cannot reach in and change it — but four of
   the inline overrides here are measured page exceptions (19-now-the-week's
   fixed 860 canvas clips in silence, and 40 put a card 10px past the edge), and
   padding would ADD to those rather than replace them. */
.section-header {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  margin: calc(var(--sp-block) + var(--section-gap)) 0 16px;
}
/* The first header on a screen, under a ScreenHead that has already provided
   the air. */
.section-header.flush { margin-top: 0; }
/* `left` restores the ruled version, and it is for ONE case: a run whose
   trailing chip counts the rows beneath it, where the eye wants the count on
   the same line as the section it counts and the rule to carry it there.
   One hairline, and --rule-ink is it. This was --outline at opacity .6, which
   resolves to #F0EEEA on white and to a fixed warm gray that all but vanishes
   on anything but white; --rule-ink is 9% of the page's own ink, so it
   resolves to #EBEAEA on white and follows whatever it lands on. */
.section-header.left { justify-content: normal; gap: 12px; }
.section-header .rule { flex: 1; height: 1px; background: var(--rule-ink); }
.coach-subtitle {
  font-family: var(--font-display); font-style: italic;
  font-size: var(--text-sm); color: var(--on-surface-variant); line-height: 1.5;
}

/* ---- Cards ----------------------------------------------------------------
   A CARD TAKES A HAIRLINE, AND THERE IS NO OTHER KIND OF CARD. The rule used
   to read "hairline whites, borderless tints", which stopped meaning anything
   on 2026-09-05 when the tinted cards became cards. A card carries 1px of
   --outline. This file used to have no border on anything, on the argument
   that surface plus a soft shadow is the separation.

   It is drawn as an INSET ring rather than a border so the box keeps its size
   — a real 1px border would move every card's content in by a pixel and
   reflow the set. .card-absent replaces the whole box-shadow with `none`, so
   the ring comes off with the declaration and it stays flat without needing to
   say so twice. .card-quiet is misnamed and never was one: it sets no
   background, so it is a card wearing a quieter shadow, and it keeps the ring.

   It carries its weight in dark for free. --outline resolves to
   rgba(255,255,255,0.12) there and --shadow-soft flattens to a transparent
   zero-size shadow, so the same declaration that draws a hairline on paper
   draws the luminance edge the shell needs — which is the thing a shadow
   cannot do on the dark content plane.

   Two other white cards carry the same rule by a different mechanism: .room
   and .hero are mostly photograph, and an inset shadow paints below content.
   They use `outline` with a negative offset. See the note at .room. */
.card {
  background: var(--surface-card); border-radius: var(--r-card);
  box-shadow: inset 0 0 0 1px var(--outline), var(--shadow-soft);
  padding: 16px 16px;
}
.card + .card { margin-top: 12px; }
/* Not a tint. It sets no background, so it is a white card at a quieter
   lift — it keeps the ring. */
.card-quiet { box-shadow: inset 0 0 0 1px var(--outline), var(--shadow-whisper); }

/* ---- Chat ----
   Bubbles hug their content and cap at 80%. Text inside stays left-aligned in
   both directions: only the bubble sits right, never the words. A full-width
   bubble with right-aligned text is the tell that these are not real bubbles.

   A bubble is white and takes no hairline, and that is deliberate rather than
   an omission. The hairline rule is about cards — a surface holding a record.
   A bubble is speech: it is already bounded by its own asymmetric corner, and
   a ring around it reads as a box someone drew around what was said. Inputs
   are the other exclusion, for the same reason in reverse: an input's edge is
   its affordance and it carries --control-outline directly, not as a card
   would. */
.coach-bubble {
  width: fit-content; max-width: 80%;
  background: var(--surface-card); border-radius: 18px 18px 18px 5px;
  box-shadow: var(--shadow-whisper); padding: 12px 16px;
  font-size: var(--text-sm); line-height: var(--leading-body); margin: 0 0 8px 0;
}
.me-bubble {
  width: fit-content; max-width: 80%;
  background: var(--primary-surface); border-radius: 18px 18px 5px 18px;
  padding: 12px 16px; font-size: var(--text-sm); line-height: var(--leading-body);
  color: var(--on-surface); margin: 0 0 8px auto;
}

/* ---- Voice capture. Mirrors the shipped three-stage panel: idle, recording,
   preview. The pulsing mic is the accent, never red, per voice_capture_panel.dart. */
.wave { display: flex; align-items: center; justify-content: center; gap: 4px; height: 40px; }
.wave i { width: 4px; border-radius: 2px; background: var(--primary); opacity: .85; }
.mic-lg {
  width: 80px; height: 80px; border-radius: var(--r-full);
  background: var(--primary); color: var(--on-primary);
  display: flex; align-items: center; justify-content: center;
  margin: 0 auto; box-shadow: 0 0 0 10px rgba(11,75,136,.10), var(--shadow-glow);
}
.timer { font-size: 30px; font-family: var(--font-display); letter-spacing: -0.5px; text-align: center; }
.textarea {
  background: var(--surface-card); border-radius: var(--r-card);
  box-shadow: var(--shadow-soft); padding: 16px 16px;
  font-size: var(--text-sm); line-height: 1.6; color: var(--on-surface); min-height: 190px;
}
.caret { display:inline-block; width: 2px; height: 16px; background:var(--primary); vertical-align:-3px; margin-left: 1px; }
.said-block {
  background: var(--surface-container); border-radius: 18px;
  padding: 12px 16px; margin-bottom: 12px;
}

/* ---- Bits ---- */
.pill {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: var(--text-2xs); padding: 8px 12px; border-radius: var(--r-full);
  background: var(--surface-container); color: var(--on-surface-dense);
}
/* ONE UNSELECTED GROUND, AND CHOSEN IS AN ACTION. A pill answers a question
   the reader asked, so the only distinction it needs is picked or not picked —
   .pill-idg's indigo went with the tints and there is nothing for a second
   unselected tone to say. Chosen is --primary rather than the settled green:
   green would report a status somebody else could give this pill, and blue
   says you picked it. */
.pill-on { background: var(--primary); color: var(--on-primary); font-weight: 500; }
.tag {
  font-size: 9.5px; letter-spacing: 1.3px; text-transform: uppercase;
  font-weight: 700; padding: 4px 8px; border-radius: var(--r-full);
}

/* ---- Chips ----------------------------------------------------------------
   A CHIP MUST SAY WHAT KIND OF THING IT IS. This is the largest craft gap the
   set had: one page ran a single badge class for a trace index, a status, a
   placement and a conflict, and all four rendered identically, so a letter that
   exists only to help you follow a record across four frames looked exactly
   like product state. A badge that does not distinguish its subject is
   decoration with a border-radius.

   .chip is shape only — the pill, the gap, the size. A kind class supplies the
   fill and the ink, always as a container/on-container pair that already exists
   above. Measured on the fill each one carries:

     RE-MEASURED against the design system's palette. The numbers that used to
     sit here were taken on the old one and every pair in them is now a color
     this file no longer contains, which made the block read as verified when
     none of it had been checked since the tokens moved. Light theme:

     .chip-count               #5B6270 on #F2F1EE   5.43:1
     .chip-status.is-settled   #24413A on #E7F1EB   9.61:1
     .chip-status.is-open      #5B6270 on #F2F1EE   5.43:1
     .chip-status.is-waiting   #54487A on #EBE7F7   6.70:1
     .chip-time                #9E4E2B on #F9E6D6   4.84:1
     .chip-money               #9E4E2B on #F9E6D6   4.84:1
     .chip-person              #54487A on #EBE7F7   6.70:1
     .chip-code                #5B6270 on #F2F1EE   5.43:1
     .chip-conflict            #A24B4B on #F5EAEA   4.90:1

     Every one clears AA, and three of them clear it by less than the old set
     did — .chip-time at 4.84 is the tightest in the file. Treat that as the
     floor: if a mark ink moves, this is the pair that fails first.

     Dark theme: the on-dark ramp puts the neutral chips near 10:1, and the
     tightest colored pair is .chip-time at #E39A76 on #33261D, 6.37:1.

   The kinds are not interchangeable and picking between them is the design
   work. A count is a number. A status is where a record stands. Time is a date,
   a lead time or a deadline. A person is who. A code is something somebody has
   to type into a paint counter. A conflict is over, clashing, or nobody's.
   A PLACEMENT IS NOT A CHIP — "Kitchen · walls" is the answer to "where", and
   it belongs on the row's own sub-line, not in a badge beside it. */
/* ONE DRAWN SHAPE, AND THE INK CARRIES THE KIND. Every chip is the same
   hairline pill and only its ink moves. Eight tinted grounds used to do this
   job, which put eight colored panels on a screen that already has a surface
   ladder — and a chip is small, non-prose notation, which is exactly what
   color SHOULD be spent on. So the color moved from the fill to the text.
   The hairline is what gives a chip a shape on white and on the page alike,
   where a fill this pale would vanish on one or the other. */
/* SEMIBOLD, BECAUSE A CHIP IS NOT PROSE. 12px carrying a count, a code or a
   date is read as notation rather than as a word, and 500 at that size reads
   as small body text that happens to have an edge round it. The design package
   has drawn it at 600 all along; nothing measurable argues back, so it wins the
   row. `Q230`. */
.chip {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: var(--text-2xs); font-weight: 600;
  /* A FIXED BOX, NOT ONE DERIVED FROM THE TYPE. `Q240` diagnosed this correctly
     and treated it one step short: it pinned the line box to `--text-2xs`, which
     still sizes the pill off the type inside it. Design ruled the box itself —
     24px, vertical padding zero — so the height is stated once and no face can
     move it. `Q245`.

     The measurement `Q240` took is why the type step does not move instead:
     in the browser at 4x, mono renders optically LARGER than Figtree at the same
     nominal size (cap height 8.91 against 8.54, +4.3%), where at 11 it is 8.16,
     -4.4%. Going up a step flips the mismatch's sign rather than closing it. */
  box-sizing: border-box; height: 24px; line-height: 1;
  padding: 0 12px; border-radius: var(--r-full); white-space: nowrap;
  box-shadow: inset 0 0 0 1px var(--outline-strong);
}
/* a number */
.chip-count { color: var(--on-surface-dense);
  font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; }
/* where a record stands */
.chip-status.is-settled { color: var(--success-deep); }
.chip-status.is-open    { color: var(--on-surface-dense); }
.chip-status.is-waiting { color: var(--indigo-deep); }
/* a date, a lead time, a deadline */
.chip-time { color: var(--amber-deep);
  font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; }
/* an amount, including one over an allowance. The amber pair carries money as
   well as time, so this renders exactly as .chip-time and exists to be picked
   by name — an author reaching for a chip should say which the value IS.
   Deliberately NOT .chip-conflict: a price above an allowance is a fact about
   an option, not a verdict on it. Coloring the delta as a clash tells the
   reader this option is the wrong one before they have weighed it, which is
   the argument mockup 20 exists to make — its dearest candidate is the right
   one and its cheapest is wrong, for reasons that have nothing to do with
   either price. Terracotta is for a genuine clash: two things on one wall,
   a decision nobody holds. */
.chip-money { color: var(--amber-deep);
  font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; }
/* who */
.chip-person { color: var(--indigo-deep); }
/* a product code, an identifier — the one chip somebody reads a character at a
   time, which is the whole reason it is monospaced and tracked out */
/* CODE KEEPS THE ROW'S TYPE STEP AND TIGHTENS ITS SIDES INSTEAD. It drew
   --text-3xs here, one step below every other chip, which is the mismatch
   `Q240` measured and then treated by pinning the line box. Design ruled the
   other way round: the box is fixed at 24 for every tone, so the step can stay
   at --text-2xs and only the side padding moves, because mono is wider per
   character. `Q245`. */
.chip-code { color: var(--on-surface-dense);
  font-family: var(--font-mono); letter-spacing: .2px; padding: 0 8px; }
/* over, clashing, nobody has this. The 13% terracotta wash went with the other
   chip fills: one drawn shape, and terracotta is the loudest ink on it. */
.chip-conflict { color: var(--terracotta-deep); }
/* A run of chips. Wraps as a row, never mid-chip — .chip's own white-space
   nowrap is what guarantees that. */
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
/* IT SITS BARE ON THE GROUND, AND THAT IS RUNG 0, NOT THE OUTLINE RUNG. The
   outline rung is for a container-shaped absence or a set — a slot for
   something not there yet, a group of controls — and a one-sentence aside is
   neither. Boxed at the card radius it took the same footprint as the options
   above it and read as a fourth option the reader had failed to select. "An
   aside is not an object" is not the test; "does this need a surface at all"
   is, and the answer here is no.

   SO THE RADIUS AND THE BOX PADDING ARE GONE, and neither was doing anything
   the reader could see. `border-radius` here painted NOTHING: no rule on this
   sheet or on any page gives a callout a background, so the radius was rounding
   a corner that was never drawn. The 12px box padding was worse than nothing —
   it is the footprint of the surface that is not there, and the scroll's own
   block gap is already what separates an aside from what it is about.
   `41-the-time-axis.html:660` had already written this down as settled and the
   declaration had not caught up.

   4px inline, and that is an optical nudge rather than a box: it keeps the
   glyph off the page gutter's exact edge. Nothing vertical. */
.callout {
  padding-inline: 4px;
  display: flex; gap: 12px; align-items: flex-start;
}
.callout p { font-size: var(--text-sm); line-height: var(--leading-body); margin: 0;
  color: var(--on-surface-variant); }
/* A LABEL, NOT A LEAD SENTENCE, and that one distinction sets both numbers.
   The paragraph below is the content and the heading names it, so the gap is
   --sp-tight -- literally "a label and the thing it labels" where it is
   declared -- and NOT the --sp-snug every state title takes. It does not follow
   the states; it is a different relationship reaching a different named step.
   The weight does not come down with the step: 600 holds, because regular
   weight only begins at --text-lg. This drew 4px and 500, and the 4px was the
   last unnamed number in the pair. Q253. */
.callout .t { font-size: var(--text-base); font-weight: 600;
  margin: 0 0 var(--sp-tight); color: var(--on-surface); }
/* THE TONE COLORS THE GLYPH AND NOTHING ELSE. Four tinted grounds used to
   carry it, which made an aside the loudest thing on the screen while the
   glyph was already saying which kind of aside it is. A callout is a mark on
   prose: the sentence takes the page's own ink on every tone. */
.callout > svg { flex: none; color: var(--on-surface-variant); }
.c-amber > svg { color: var(--amber-deep); }
.c-idg   > svg { color: var(--indigo-deep); }
.c-mute  > svg { color: var(--on-surface-variant); }
/* Coach voice leaning in. Used when the coach opens a thread rather than
   reporting one, e.g. an extracted word too vague to keep. */
.c-coach > svg { color: var(--primary); }

/* THE FIGURE IS THE HEADLINE AND MUST OUTRANK ITS LABEL. Both halves used to
   inherit this row's own --text-sm and declare no size of their own, so a
   label and the number it names came out at 15px/400 apiece and only ink told
   them apart. components.json calls this component "one headline number on a
   card"; a headline the same size as its label is not one, and the reader's
   eye had nothing to land on. Measured across the deck, 16 rows on 7 files
   read that way -- see docs/METRIC_ROW_AUDIT.md.

   The step is `fact`'s, deliberately: 13.5 -> 16 is already proven below on
   155 instances, and taking a second ratio for a component the eye reads as
   fact's sibling would make the family speak twice. The weight is what
   separates them -- a fact is one of a settled run and stays 400; this is a
   headline and takes 500. That pair is also exactly what `HcKv` already drew
   in Dart, so the sheet and the library now agree rather than approximate. */
/* THE ONE HEADLINE FIGURE ON A CARD — `components/figures/KeyValue`. It was a
   label and a figure pushed to OPPOSITE EDGES at --text-base, which is a row;
   the component stacks, left-aligned, and gives the number a real display step,
   because on a card holding one figure the figure IS the title and the label is
   a caption above it. That is also what tells this apart from a `.fact` at a
   glance. Q180 ranked the two halves and recorded the size gap it did not
   close; `data-hierarchy` is what closes it — the rule is that the answer takes
   the display step and its inputs collapse into one line beneath.

   IT CENTERS IN WHATEVER HEIGHT ITS CARD GETS. Pinned to the ceiling of a
   stretched card it read as broken, so it fills and centers. That is not a
   license to pair it with anything; the arrangement is the fix. */
.kv {
  display: grid; gap: var(--sp-8); align-content: center; justify-items: start;
  min-height: 88px; padding: 0;
}
/* ONE LABEL TREATMENT FOR EVERY CAPTION-ABOVE-A-THING. Fact, KeyValue,
   FigureTrio, LatePair and Field all name the thing beneath them, so the design
   system draws all five identically: 12px, semibold, sentence case,
   --on-surface-dense, no tracking. They were on three treatments across four
   sheets and a page-local block — 13.5px at 400 and at 500, all in
   --on-surface-variant — and "three treatments on one sheet of data cards is
   what makes them look unresolved". The ink goes DARKER as the type goes
   smaller, which is the ink card's own rule. */
.kv .k { font-size: var(--text-2xs); font-weight: 600; line-height: 1.3; color: var(--on-surface-dense); }
.kv .v {
  font-size: var(--text-2xl); font-weight: 600; line-height: 1;
  letter-spacing: var(--tracking-display); color: var(--on-surface);
  font-variant-numeric: tabular-nums; text-align: left;
}
/* THE FIGURE'S TONE, and the only place a status color touches a kv. Clash ink
   on the answer is what `colors-status` means by "a figure's value" — the
   figure is over, and the reader should not have to read the label to know it.
   The label and the working line stay their own ink: a sentence never takes a
   status color. */
.kv .v.clash { color: var(--terracotta-deep); }
/* THE WORKING, folded into ONE subordinate line. The figures the answer was
   computed from go here — "$18,000 set aside · Reed Millwork quoted $21,400" —
   rather than beside it at the same weight, which hands the reader two numbers
   and leaves them to arrive at the third. */
.kv .w {
  margin-top: 2px; font-size: var(--text-xs); line-height: var(--leading-body);
  color: var(--on-surface-dense); text-wrap: pretty;
}

/* A RUN WHERE THE ANSWER OWNS THE RIGHT EDGE — `components/rows/AnswerRow`.
   Not a `.row`: a row's `.ct` is --text-xs in the variant ink, a QUALIFIER on
   the name — "Their quote, 2 June" — and putting an amount there demotes the
   one thing the reader came for. Here the answer sits at the name's own step,
   tabular, on one right edge, so a column of them compares vertically without
   the eye re-measuring each line.

   WHAT THE ANSWER IS "OF" GOES UNDER THE ANSWER, not into the name's caption.
   "$22,000 set aside" beside "Primary bath" is a second fact about the room;
   "left of $22,000" under "$6,800" is the same number doing its actual job,
   which is telling you what the answer was measured against.

   CLASH INK MARKS THE SINGLE ROW THAT IS OVER. Color on every row is five
   alarms, and a reader who sees five stops reading any of them. */
.arow { display: flex; align-items: baseline; gap: var(--sp-16); padding: var(--sp-snug) 0; }
.arow + .arow { box-shadow: inset 0 1px 0 var(--rule-ink); }
/* BOTH CAPTIONS ARE OPTIONAL AND THEY ARE NOT THE SAME CAPTION. Under the
   ANSWER goes what the answer is measured against — "left of $22,000". Under
   the NAME goes who and when — "Reed Millwork · 14 May" — which is a qualifier
   on the thing, not on the number. A run uses one or the other, never both:
   two captions on one line is the reader choosing which to read. */
.arow .tx { flex: 1; min-width: 0; }
.arow .tx .n { font-size: var(--text-md); line-height: 1.3; display: block; }
.arow .tx .c { margin-top: 4px; font-size: var(--text-xs); line-height: 1.4; color: var(--on-surface-dense); display: block; }
/* 3px, the same gap `.pair` puts between a key and its value — a figure and the
   caption naming it are one object, and the ladder's 4 opens a seam in it. */
.arow .a { flex: none; display: grid; gap: 3px; justify-items: end; text-align: right; }
.arow .a .v {
  font-size: var(--text-md); font-weight: 600; line-height: 1.2;
  color: var(--on-surface); font-variant-numeric: tabular-nums;
}
.arow .a .v.clash { color: var(--terracotta-deep); }
.arow .a .c {
  font-size: var(--text-xs); line-height: 1.4;
  color: var(--on-surface-dense); font-variant-numeric: tabular-nums;
}

/* Settled facts. IT STACKS, AND IT NEVER PUTS A SENTENCE IN A COLUMN.
   `Q225`, ruled on this deck's own content: of the 144 `.fact` instances on the
   46 pages, 53 have a value longer than 25 characters and 41 carry a comma or a
   sentence-ending period, the longest running to 119 characters — while the
   longest KEY is 23 characters and the median is 9. The fixed column was
   argued here as "right for a run of settled facts about different things",
   and that argument describes a Fact whose value is a figure; a third of this
   deck's are not. On the 390px frame the columned value got
   294 - 96 - 12 = 186px where stacked it gets the card's full 294 — 58% more —
   for a label that needs nine characters.

   THE VALUE IS THE CONTENT, SO IT TAKES THE STEP ABOVE THE LABEL. The label
   goes to the caption step and the value to the lead step, which is the
   smallest gap that reads as a hierarchy; at 13.5px muted over 16px regular the
   two were near enough the same size that a data card read as gray mush.

   ITS BLOCK PADDING IS SYMMETRIC. A Fact cannot see that it is last, so the
   only spacing it can own safely is the same on both sides — `8px 0` plus a
   `12px` top on every subsequent one is that asymmetry, and it put more air
   under a run than over it.

   THE HAIRLINE MARKS A SETTLED RUN, so the first item does not take one. The
   comment that stood here named `--rule-ink` and the declaration set only
   `padding-top`: the rule was LOST rather than decided against, and the app has
   been drawing it all along.

   Tabular is unconditional rather than left to a `t-num` on the instance —
   `components.json`'s own first rule for this component, and the app has never
   made it optional. */
.fact { display: grid; gap: 4px; padding: 12px 0; }
.fact + .fact { box-shadow: inset 0 1px 0 var(--rule-ink); }
.fact .k { font-size: var(--text-2xs); font-weight: 600; color: var(--on-surface-dense); line-height: 1.3; }
.fact .v {
  font-size: var(--text-md); font-weight: 500; color: var(--on-surface);
  line-height: 1.4; font-variant-numeric: tabular-nums; text-wrap: pretty;
}

/* ---- A PAIR IS A COMPARISON, NOT A KEY COLUMN ------------------------------
   `guidelines/choices-and-pairs.card.html`: "Stack each pair so a long key
   never squeezes its value. Lead with the value that changed, one step up, and
   let the one it replaces follow at body weight. Dates, file names and review
   history go into a single caption line."

   It names what was here as the thing to avoid, and names the exact key that
   made it fail: "A key column with the values floating opposite it: the reader
   is handed both and left to work out which one is news, and a key like
   'Earlier package answer' wraps to two lines while its value sits alone."
   That was `.fact`, whose key was a fixed 88px column. `Q225` has since ruled
   the same way for `.fact` itself, on this deck's own content, so both stack
   now. Whether that leaves two components doing one job is open and recorded
   in OPEN_QUESTIONS.md; the pair still leads with the value that changed, one
   step up, which is the thing a run of settled facts does not do.

   THE KEY IS THE CAPTION TREATMENT, NOT AN EYEBROW, and this is the one place
   the new card and README.md read differently. The card's specimen draws it
   uppercase and tracked — but through a `const eyebrow` it shares with the
   specimen page's own "Not this" chrome, and README.md is categorical twice
   over: "an eyebrow is accent ink naming a section, not a caption naming a
   number", and "anything which should not be accent ink must not be drawn as
   one". A pair's key names the value under it, so it takes the caption
   treatment the other five captions take. Recorded in OPEN_QUESTIONS.md. */
.pair { display: grid; gap: 3px; padding: 12px 0; }
.pair + .pair { box-shadow: inset 0 1px 0 var(--rule-ink); }
.pair:first-child { padding-top: 0; }
.pair:last-child { padding-bottom: 0; }
.pair .k {
  font-size: var(--text-2xs); font-weight: 600; color: var(--on-surface-dense);
  line-height: 1.35;
}
/* The one that changed, one step up. */
.pair .v { font-size: var(--text-base); line-height: 1.3; }
.pair.lead .v { font-size: var(--text-lg); font-weight: 600; }
/* Dates, file names and review history, in ONE line. */
.pair-note {
  margin: 8px 0 0; font-size: var(--text-xs); line-height: 1.4;
  color: var(--on-surface-dense);
}

/* Always-available composer. Going off script uses the same door as the
   opening description, so this is persistent, not a separate entry point. */
.composer {
  flex-shrink: 0; padding: 12px 24px 24px;
  display: flex; align-items: center; gap: 8px;
  background: var(--surface);
}
/* The composer's own input, and NOT the form field. forms.css says as much in
   its opening paragraph — neither .textarea nor this is a form field — and for
   a while both were called .field, which made `field` count 46 instances where
   the component has 43. Renamed so the component owns its name. */
.composer .entry {
  flex: 1; background: var(--surface-card); box-shadow: var(--shadow-whisper);
  border-radius: var(--r-full); padding: 12px 16px;
  font-size: var(--text-sm); color: var(--on-surface-variant);
}
.composer .mic {
  width: 40px; height: 40px; border-radius: var(--r-full);
  background: var(--primary); color: var(--on-primary); flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
}

.steps { display: flex; align-items: center; gap: 8px; }
.step {
  width: 24px; height: 24px; border-radius: var(--r-full);
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; font-weight: 700;
  background: var(--surface-container); color: var(--on-surface-variant);
}
.step.done { background: var(--success-surface); color: var(--success-deep); }
.step.now  { background: var(--primary); color: var(--on-primary); }

/* An option is RAISED, not sunk. It used to be a well on the page — a gray
   rectangle on a gray ground — and a well says "this is a record" where an
   option needs to say "you can act on this". Elevation is what carries that
   difference in this system, so the two blocks on a question screen that are
   NOT actionable keep the flat treatment and these do not.
   The selected one goes further: a saturated fill plus --ring-selected, an
   inset hairline drawn for exactly that job. It is NOT a white one: it mixes
   --on-primary, so it is white on the light fill and near-black on the dark,
   which is the only way it stays visible in both. Q244 Ruling 1. */
/* A CHOICE AMONG A CARD'S OWN ROWS — `components/rows/PickRow`. `.pickrow` was
   in the vocabulary with no rule behind it: thirteen instances across 27 and 48
   inherited `.mark` from icons.css and nothing else, so the marker stacked
   ABOVE the name it marks.

   WHICH SHAPE IS A QUESTION ABOUT THE SURFACE, not about which looks better.
   On the ground a choice is `.opt`/`Option` and selected is a fill. Inside a
   card it cannot be — a raised, filled block within a card is a second card,
   and the ladder does not stack — so here the marker carries selection, on one
   surface with --rule-ink between rows and plain ink at every state.

   The chosen name goes to 600 and stays plain ink. All thirteen rows set
   `color:var(--primary-deep)` on it, and blue text is a link everywhere else in
   this deck, so the picked option read as somewhere to navigate. */
/* --sp-snug, not the 14 PickRow.jsx hardcodes: the package's own spacing.css
   declares no 14 (…8, 12, 16…), and a component literal is not a step. 12
   above and below a 21.6px line clears the 44px target with room. */
.pickrow {
  display: flex; gap: var(--sp-snug); align-items: flex-start;
  width: 100%; padding: var(--sp-snug) 0;
  background: none; border: 0; text-align: left; color: inherit;
  font-family: var(--font-body); cursor: pointer;
}
/* The hairline belongs BETWEEN rows, so the first one does not open with a
   rule under the card's own head. */
.pickrow + .pickrow { box-shadow: inset 0 1px 0 var(--rule-ink); }
/* SelectionMarker's own md step — 19px, the size icons.css already draws it at,
   and the ring is --rail-ink at 1.5px rather than the 9% separator: the empty
   state is a RING, not a filled gray disc, because a disc with no tick in it is
   the one shape a reader can mistake for a chosen row at a glance. Transparent
   under the ring, so the row's own surface shows through and the marker never
   reads as a second ground. */
.pickrow .mark {
  align-self: flex-start;
  width: 19px; height: 19px; margin-top: var(--sp-2);
  background: transparent;
  box-shadow: inset 0 0 0 1.5px var(--rail-ink);
  color: var(--on-primary);
}
.pickrow .mark > svg { width: 11px; height: 11px; }
.pickrow.on .mark { background: var(--primary); box-shadow: none; }
.pickrow .n { font-size: var(--text-base); font-weight: 400; line-height: 1.35; margin: 0; }
.pickrow.on .n { font-weight: 600; }
.pickrow .a { margin: var(--sp-4) 0 0; font-size: var(--text-xs); line-height: 1.4; color: var(--on-surface-dense); }

/* A CARD RADIUS AND A CARD'S AIR, because an option is never anything but its
   own surface: 60 of the 60 real `.opt` instances across 13 pages are
   standalone, and not one of them sits inside a `.card`. `.cand` — this same
   component under the name `43-deciding-against-the-picture` gives it — has
   drawn `--r-card` and 24px of horizontal pad all along, so the deck was
   already drawing an option at the card step whenever it stopped calling it an
   option. `Q229`. */
.opt {
  border-radius: var(--r-card); padding: 24px;
  background: var(--surface-card); margin-bottom: 12px;
  box-shadow: var(--ring-control), var(--shadow-whisper);  /* Q245 */
}
.opt.sel {
  background: var(--primary); color: var(--on-primary);
  box-shadow: var(--ring-selected), var(--shadow-soft);
}
/* THERE IS NO WARN TONE, and there were no call sites for the one that stood
   here — an amber option said "this one costs more" in a color the reader had
   to learn, where the annotation says it in words they already know. */
.opt .n { font-size: var(--text-md); font-weight: 500; margin: 0; }
.opt .a { font-size: var(--text-sm); line-height: var(--leading-body); color: var(--on-surface-variant); margin: 4px 0 0; }
.opt.sel .n { color: var(--on-primary); }
/* NOT QUIETENED AT ALL, and the measurement is what decided it. The obvious
   move is a softer tone for the sub-line, but on the dark theme the selected
   fill is bright sage carrying dark ink. A separate muted foreground would add
   another semantic action-ink token, so the sub-line takes the full on-primary
   and the hierarchy is carried by weight and size instead. The light theme has
   room for a softer tone; one rule serves both, so it does not get one. */
.opt.sel .a { color: var(--on-primary); }

.cta {
  display: block; text-align: center;
  background: var(--primary); color: var(--on-primary);
  text-decoration: none;
  font-weight: 600; font-size: var(--text-base); padding: 16px; border-radius: var(--r-full);
  box-shadow: var(--shadow-glow);
}
/* The quiet way out. Button.jsx variant="ghost": the primary's geometry, a
   transparent ground and accent ink, so it carries a button's weight without
   competing with the primary. It was a white card, which made it read as a
   second equal action. Centered, because hung off the left margin it reads as a
   stray link rather than a way out. */
.cta-ghost {
  /* THE TARGET IS 44px AND THE PADDING NO LONGER GETS IT THERE. 12px of padding
     around a 15px line cleared 44 under the old ramp's leading and lands on 42
     under this one — which is the point of measuring geometry rather than
     declaring it. inline-flex with a min-height rather than more padding,
     because the visual size is not the target size and growing the padding
     would also move the text away from the sentence above it. */
  /* FLEX, NOT INLINE-FLEX, and the difference is the centering this component
     is named for. `margin-inline: auto` computes to zero on an inline-level
     box, so for four months the ghost centered only where a parent happened to
     center its text and sat hard against the left margin everywhere else --
     16 of the 26 on 04-onboarding, measured. Block-level shrink-to-fit is what
     the comment above always described. */
  display: flex; align-items: center; justify-content: center;
  min-height: 44px;
  width: fit-content; margin-inline: auto; text-align: center;
  background: transparent; color: var(--accent-ink);
  text-decoration: none;
  font-size: var(--text-sm); font-weight: 600; padding: 12px 24px;
  border: 0; border-radius: var(--r-full); box-shadow: none;
  font-family: inherit; cursor: pointer;
}

/* A committing action that must remain visibly a button without taking the
   filled primary weight. Full width so its boundary—not a warning color—does
   the work; used for destructive actions after their consequence is stated. */
.cta-outline {
  display: block; width: 100%; box-sizing: border-box; text-align: center;
  background: transparent; color: var(--accent-ink);
  border: 1px solid var(--accent-line); text-decoration: none;
  font-size: var(--text-base); font-weight: 600; padding: 15px;
  border-radius: var(--r-full); box-shadow: none;
}

.note {
  max-width: 940px; margin: 40px auto 0; background: var(--surface-card);
  border-radius: var(--r-card); padding: 24px 24px;
  box-shadow: inset 0 0 0 1px var(--outline), var(--shadow-soft);
}
.note h3 { font-family: var(--font-display); font-size: 18px; font-weight: 600; margin-bottom: 12px; }
.note p, .note li { font-size: var(--text-base); line-height: var(--leading-relaxed); color: var(--on-surface-variant); }
.note ul { margin: 8px 0 0 16px; }
/* :not([class]) IS LOAD-BEARING, and a browser found out why. `.note a` is
   (0,1,1) and beats `.cta` at (0,1,0), so a CTA quoted inside a closing note —
   31-the-four-types does exactly that — took the accent as its TEXT while
   keeping the accent as its FILL: 1.00:1, the same color on itself. That
   predates the repalette; at HEAD both sides resolved to sage #3D6655 and the
   ratio was identical. It contradicts the rule stated two comments below, that
   everything which is a link but not prose carries a class and therefore wins,
   so the fix is to stop competing with classed links rather than to name them. */
.note a:not([class]) { color: var(--accent-ink); text-decoration-color: var(--accent-line); }

/* Prose links, everywhere else. The rule above was the only link color in the
   sheet, so a cross-reference written in a .page-head or an .annot — which is
   where most of them are — fell through to the browser default #0000EE. In
   light that is merely off-palette; on the dark body it measures 1.86:1 and is
   unreadable. Ten of them were shipping that way.

   Declared on the bare element so it catches a link wherever prose puts it,
   rather than adding a fourth scoped selector that the fifth prose container
   would escape. Everything that is a link but not prose — .page-nav a, the
   .xnav tiles, the index's card wrappers — carries a class and therefore wins
   on specificity; that was verified against all 1,738 links in the set rather
   than assumed. */
/* THE ACCENT AS TEXT. --accent-ink, not --primary, and the underline carries
   the blue separately. The two are one value in both appearances again under
   Pigment; the split is kept because it cost nothing and it is what will hold
   if a future dark accent cannot be small text. */
a { color: var(--accent-ink); text-decoration-color: var(--accent-line); }


/* ==========================================================================
   THE CURRENT LAYER
   Promoted out of the deck's old binder page, which was the reference
   implementation and which the old binder-nav and colors pages had each
   hand-copied. This is now the one copy. Those three are the PRE-RENAME set
   and they live at docs/_archive/2026-08-03/adaptive-mockups/ —
   13-the-binder.html, 15-binder-nav.html, 16-colours-and-the-space.html,
   the last one still carrying the British spelling. This comment used to
   name them as if they sat in docs/mockups/, where none of them has been
   since the renumber.

   HOW TO USE IT — one line:

       <body class="hc-air">

   WHY IT IS SCOPED AND NOT THE DEFAULT.
   The rhythm below redefines .card and .card + .card, and those two are
   load-bearing on fourteen older pages. It redefined .fact and .section-header
   too, until `Q225` and `Q220` put each of them on one rule: a stacked Fact has
   no label column to widen, and a section header now reads --section-gap, which
   is one number rather than a base and an override. 02, 08 and 09 in
   particular take their card padding straight from the base .card above
   (16px 17px, measured in the browser); 01, 04 and 14 override it themselves
   to 13px 15px. Changing the base rule would silently reflow all of them. So
   the whole promoted layer hangs off body.hc-air: a page opts in, and a page
   that has not opted in is byte-for-byte unaffected. Every selector here is
   prefixed. Do not remove the prefix.

   The tokens are the exception and sit unprefixed in :root below, because they
   are new custom-property names — nothing in 01..14 or index.html declares or
   reads any of them, so they cannot collide.

   COLLISIONS FOUND, AND WHY THE PREFIX RESOLVES ALL OF THEM RATHER THAN A
   RENAME. Besides the four rhythm selectors: .rooms is 07's chip-wrap, .row is
   09's and 14's two-column list row, .scrim is 11's photo-caption gradient
   (a bare .scrim carrying `inset: 0` would have dropped 11's gradient over the
   whole photo instead of its bottom edge). Under body.hc-air none of the three
   is reachable from those pages, so the names stay as 13 wrote them and no
   markup had to change.
   ========================================================================== */

:root {
  /* THERE ARE NO TINTED GROUNDS, AND THERE IS NO TOKEN LEFT TO REACH FOR.
     --sage, --lavender and --clay were the core device of this set: a tinted
     card carried the kind, sage settled, clay money and timing, lavender
     people. Six tokens went with them on 2026-09-05 — those three and the
     --amber-surface / --indigo-surface / --insight-surface washes they mixed
     from. What a tint used to say is now a word: an eyebrow, a chip's ink, a
     glyph, a figure's value, a dot. Small, non-prose notation, which is what
     color is good at, and the surface ladder does the separation.
     --success-surface survives and is a MARK's disc only — a tick, never a
     panel. See tokens/colors.css in the design-system package. */
  /* NOT A STEP DOWN FROM --outline ANY MORE. This was --outline at 78%, on the
     argument that a card's edge is the full strength and a smaller bounded
     thing wants less. The design system declares --hairline as --outline
     outright, and it had exactly one caller here — a chip on page 23 — so the
     distinction cost a token and bought one chip. Still not for row rules,
     which are --rule-ink. */
  --hairline: var(--outline);
  /* a hairline that follows its ground rather than being fixed against white */
  --rule-ink: color-mix(in srgb, var(--on-surface) 9%, transparent);
  /* A rail is not a rule. --rule-ink separates two rows and is meant to be
     felt rather than seen; --rail-ink is the line your eye follows down a
     column of dates, and the ring on an empty node — the only mark saying a
     node is there at all. Read along its length, 9% is not enough. */
  --rail-ink: color-mix(in srgb, var(--on-surface) 22%, transparent);
  /* One step larger than --r-teardrop's 24/14. NOT "the marketing teardrop",
     which is what this said: `sites/` draws it zero times and all 47 deck pages
     carry `.hc-air`, so it is the app's shape and only the app's. The figures
     in the old note (16/24 and 12/20) were not this token's either. Q239. */
  --r-teardrop-lg: 28px 18px 18px 18px;
}

/* ---- Air ------------------------------------------------------------------
   THE PART THAT MATTERS MOST, and the part the older pages get wrong.
   AppSpacing.cardPadding is 32, cardGap is 24 and sectionGap is 48. Earlier
   revisions of 13 ignored all three to force five sections into an 844pt
   phone. That was the wrong trade. The spacing is the design.

   A FRAME IS ALLOWED TO RUN PAST THE BOTTOM OF THE PHONE, AND NOTHING IS TO BE
   COMPRESSED TO FIT. A phone scrolls; a frame that fits eight crushed cards is
   less honest than one that shows a few roomy cards and lets you scroll for
   the rest. If a retrofit is tempted to shave these numbers to make a frame
   end at 844, that is the wrong instinct — let it overrun.

   More air above a section header than below it, so a heading belongs to what
   follows it rather than floating between two things. */
body.hc-air .card           { padding: 24px 24px; }
body.hc-air .card + .card   { margin-top: 24px; }

/* The card for a thing the app cannot do. Flat, quiet, no lift, no hairline.
   It is not a badge and it does not say "not built": the copy inside says the
   honest thing in the homeowner's own terms. From 16. */
body.hc-air .card-absent { background: var(--surface-container); box-shadow: none; }

/* ---- Room card ------------------------------------------------------------
   A drawing of the room filling the top of the card at 4:3, then a roomy white
   panel with the name and, quieter under it, the count. With the hero card
   this is the only shape that takes the large teardrop. From 13. */
body.hc-air .rooms { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
/* A white card takes the hairline, and a room card is a white card — it just
   happens to be mostly photograph. The ring cannot be an inset box-shadow
   here: an inset shadow paints above the background but BELOW content, so the
   full-bleed <img> would cover every edge but the bottom panel's. `outline`
   paints after content and takes no space, so it draws the same 1px over the
   photo without reflowing anything. Different mechanism, same rule. */
body.hc-air .room {
  border-radius: var(--r-teardrop-lg); overflow: hidden;
  background: var(--surface-card);
  box-shadow: var(--shadow-soft);
  outline: 1px solid var(--outline); outline-offset: -1px;
}
body.hc-air .room img { display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover; }
body.hc-air .room .p { padding: 16px 16px 24px; }
body.hc-air .room .p .n { font-size: var(--text-base); font-weight: 600; line-height: 1.3; }
body.hc-air .room .p .c { font-size: var(--text-xs); color: var(--on-surface-variant); line-height: 1.35; margin-top: 4px; }
/* The room with no drawing yet. Flat, quiet, both columns, still a room. */
/* A room with no drawing is the .card-absent of this family: flat, quiet, no
   lift and NO hairline. It must clear .room's outline as well as its shadow. */
body.hc-air .room.empty {
  background: var(--surface-container); box-shadow: none; outline: none;
}
body.hc-air .room.full  { grid-column: 1 / -1; }

/* ---- Hero card ------------------------------------------------------------
   A room screen opens on the room, with its colors and materials as chips
   directly under it. From 13. */
body.hc-air .hero {
  border-radius: var(--r-teardrop-lg); overflow: hidden;
  background: var(--surface-card); box-shadow: var(--shadow-soft);
  /* Same white panel as .room, same photograph problem — see the note there. */
  outline: 1px solid var(--outline); outline-offset: -1px;
}
body.hc-air .hero img { display: block; width: 100%; aspect-ratio: 16 / 10; object-fit: cover; }
body.hc-air .hero .b { padding: 24px 24px 24px; }
body.hc-air .hero .b .micro-label { display: block; margin-bottom: 16px; }

/* ---- Swatch strip ---------------------------------------------------------
   A color, a material or a finish is shown, never spelled. The pending fill
   is the one that has not been chosen yet. From 13. */
body.hc-air .sw-row { display: flex; gap: 12px; }
body.hc-air .sw-row .s { flex: 1; min-width: 0; }
body.hc-air .sw-row .s .p { height: 48px; border-radius: var(--r-sm); box-shadow: var(--shadow-whisper); }
body.hc-air .sw-row .s .p.pending {
  background: repeating-linear-gradient(135deg,
    var(--surface-container) 0 6px, var(--surface-lowest) 6px 12px);
}
body.hc-air .sw-row .s .n { font-size: var(--text-xs); font-weight: 500; line-height: 1.3; margin-top: 12px; }
body.hc-air .sw-row .s .c { font-size: var(--text-2xs); color: var(--on-surface-variant); line-height: 1.3; margin-top: 4px; }

/* ---- Index row ------------------------------------------------------------
   A glyph, a name, and the count hard right. Hairline between rows only,
   inside the card. Never around it. From 13. */
body.hc-air .irow { display: flex; align-items: center; gap: 16px; padding: 12px 0; }
body.hc-air .irow + .irow { padding-top: 16px; }
body.hc-air .irow .ic { flex-shrink: 0; line-height: 0; color: var(--on-surface-variant); }
body.hc-air .irow .n  { flex: 1; min-width: 0; font-size: var(--text-base); line-height: 1.35; }
body.hc-air .irow .ct { flex-shrink: 0; font-size: var(--text-xs); color: var(--on-surface-variant); }

/* Stacked row, for the lists that carry a second line. .ct and .go are
   alternative right-hand ends: a quiet count, or a chevron. From 13, with
   .row .ct folded in from 16. */
body.hc-air .row { display: flex; align-items: center; gap: 16px; padding: 12px 0; }
body.hc-air a.row { color: inherit; text-decoration: none; }
body.hc-air .row + .row { padding-top: 16px; }
body.hc-air .row .ic { flex-shrink: 0; line-height: 0; color: var(--on-surface-variant); }
body.hc-air .row .tx { flex: 1; min-width: 0; }
body.hc-air .row .tx .n { font-size: var(--text-base); font-weight: 600; line-height: 1.35; display: block; }
body.hc-air .row .tx .c { font-size: var(--text-xs); color: var(--on-surface-variant); line-height: 1.4; margin-top: 4px; display: block; }
body.hc-air .row .ct { flex-shrink: 0; font-size: var(--text-xs); color: var(--on-surface-variant); text-decoration: none; }
body.hc-air .row .go { flex-shrink: 0; line-height: 0; }

/* ---- The dated spine ------------------------------------------------------
   A run of moments down a continuous rail, each one a node and a block of text
   beside it. It is the shape mockups 18 and 19 both need: 18 draws it small to
   compare three arrangements of the time axis, 19 draws it full size as the
   Build tab, which is why it is shared rather than page-local.

   The rail is a flex child of the node column rather than a border on the
   list, so it stretches to whatever sits beside it. That is what lets one node
   open up and hold whole cards inside the rail without the line breaking, and
   it is why the last node can drop the rail entirely instead of trailing into
   nothing.

   Three node states, and none of them is a percentage. `.done` is a filled
   emerald disc, `.now` is a ring with a filled center, and a bare node is a
   hollow ring on the track color. That is the same vocabulary a payment
   schedule uses, and it counts moments rather than measuring progress.

   EVERY SELECTOR IN THIS BLOCK IS A CHILD COMBINATOR, AND IT HAS TO BE. The
   node column is `.n`, and `.n` is also the title of a stacked `.row` — and an
   opened node holds whole cards full of stacked rows inside the rail. Written
   as descendants, `.spine .sp .n` reached every one of those titles at the same
   specificity as `.row .tx .n` and, being declared later, won: every title in
   an opened node became a 20px-wide centering flex column and sat up to 68px
   right of its own sub-line. `>` scopes this block to the node column, which is
   the only thing it was ever about. */
body.hc-air .spine { position: relative; }
body.hc-air .spine > .sp { display: flex; gap: 16px; align-items: flex-start; }
body.hc-air .spine > .sp > .n {
  flex: 0 0 20px; display: flex; flex-direction: column; align-items: center;
  align-self: stretch; padding-top: 4px;
}
body.hc-air .spine > .sp > .n i {
  width: 12px; height: 12px; border-radius: var(--r-full); flex-shrink: 0;
  box-shadow: inset 0 0 0 2px var(--rail-ink); background: var(--surface-card);
}
body.hc-air .spine > .sp > .n i.done { background: var(--success-deep); box-shadow: none; }
body.hc-air .spine > .sp > .n i.now  { background: var(--surface-card); box-shadow: inset 0 0 0 4px var(--primary); }
/* The rail. It grows to the height of whatever sits beside it.

   --rail-ink, NOT --rule-ink. A rule between rows is a separator and 9% is
   right for it; a rail is structure — it is the thing your eye follows down a
   column of dates — and it is read along its length rather than noticed at a
   crossing. Those are different jobs, so they get different weights, and the
   node rings below take the rail's weight rather than the rule's for the same
   reason: an empty ring is the only mark saying a node exists. */
body.hc-air .spine > .sp > .n b { flex: 1; width: 2px; background: var(--rail-ink); margin: 8px 0 0; }
body.hc-air .spine > .sp:last-child > .n b { display: none; }
body.hc-air .spine > .sp > .bd { flex: 1; min-width: 0; padding-bottom: 24px; }
body.hc-air .spine > .sp:last-child > .bd { padding-bottom: 0; }
/* Each line of a node is its own block. They are spans in the markup so a node
   stays one element deep, so every one of them has to be told. Child
   combinators again, for the reason above: `.s` is also a swatch and `.t` and
   `.d` are cheap enough class names to collide with anything nested. */
body.hc-air .spine > .sp > .bd > .d,
body.hc-air .spine > .sp > .bd > .t,
body.hc-air .spine > .sp > .bd > .s,
body.hc-air .spine > .sp > .bd > .in { display: block; }
body.hc-air .spine > .sp > .bd > .d { font-size: 11px; font-weight: 700; letter-spacing: 1.4px; color: var(--on-surface-variant); line-height: 1.3; }
body.hc-air .spine > .sp > .bd > .t { font-size: var(--text-md); font-weight: 500; line-height: 1.35; margin-top: 8px; }
body.hc-air .spine > .sp > .bd > .s { font-size: var(--text-sm); color: var(--on-surface-variant); line-height: 1.5; margin-top: 8px; }
/* A node that is carrying something: the open phase, with its own contents
   nested under it and inside the rail rather than beside it. */
body.hc-air .spine > .sp > .bd > .in { margin-top: 16px; }

/* ---- Color chips ---------------------------------------------------------
   Surface chip. The values a surface-assignment vocabulary allows, plus the
   ones it does not: .absent takes a dashed outline, which is a chip treatment
   rather than a card border, and reads as unfinished without a badge. From 16. */
body.hc-air .surfs { display: flex; flex-wrap: wrap; gap: 8px; }
body.hc-air .surf {
  font-size: var(--text-sm); padding: 8px 16px; border-radius: var(--r-full);
  background: var(--surface-lowest); color: var(--on-surface); line-height: 1;
}
body.hc-air .surf.on { background: var(--primary); color: var(--on-primary); font-weight: 500; }
/* NOT AMBER. `components/surfaces/Card.d.ts` settles what an absent thing looks
   like: "the shape of a card with nothing in it yet, drawn as an outline
   because there is no object to raise. THERE ARE NO TINTED TONES: the kind of
   thing is carried by the words and the marks in them." Amber means a clock is
   running, and a surface nobody has chosen has no clock on it. The dash stays
   -- a dash is a shape, and it is what says unfinished without a badge. */
body.hc-air .surf.absent {
  background: transparent; color: var(--on-surface-variant);
  outline: 1px dashed var(--outline-strong);
  outline-offset: -1px;
}

/* Palette row. A color in the room: the swatch, its name, its brand and code,
   and the surface it sits on. The pending hatch is the same one the swatch
   strip uses, so the set spells "nothing here yet" one way. From 16. */
body.hc-air .prow { display: flex; align-items: center; gap: 16px; padding: 12px 0; }
body.hc-air .prow + .prow,
body.hc-air .prow.ruled { padding-top: 16px; }
body.hc-air .prow .sw { width: 40px; height: 40px; border-radius: 11px; flex-shrink: 0; box-shadow: var(--shadow-whisper); }
body.hc-air .prow .sw.pending {
  background: repeating-linear-gradient(135deg,
    var(--surface-lowest) 0 6px, var(--surface-container) 6px 12px);
}
body.hc-air .prow .tx { flex: 1; min-width: 0; }
body.hc-air .prow .tx .n { font-size: var(--text-base); line-height: 1.35; display: block; }
body.hc-air .prow .tx .c { font-size: var(--text-xs); color: var(--on-surface-variant); line-height: 1.4; margin-top: 4px; display: block; }
/* THE SECOND ONE, AND IT WAS ALSO NOT AN EYEBROW. This drew "Cabinet" — the
   surface a color goes on — uppercased at 10px with tracking, which is an
   eyebrow's drawing in an ink no eyebrow may take. It names what the row is
   FOR, not a section, and the markup already writes it in sentence case; only
   the CSS was shouting. Same caption treatment as every other label above a
   thing. The .need variant keeps --terracotta-deep below: a chip's ink is one
   of the five places a status color may appear. */
body.hc-air .stag {
  flex-shrink: 0; font-size: var(--text-2xs); font-weight: 600;
  color: var(--on-surface-dense);
}
/* --terracotta-deep, not --terracotta. This file forbids the base accent as its
   own foreground a few rules above and then did it here: 3.83:1 on the page
   surface, which fails AA at any size. Caught by an agent reading the rule and
   then reading the sheet. */
body.hc-air .stag.need { color: var(--terracotta-deep); }
/* The quiet way out of a conflict. A sentence and one action, no dialog. */
body.hc-air .resolve { display: flex; align-items: center; gap: 12px; padding: 16px 0 2px; margin-top: 12px; }
body.hc-air .resolve p { flex: 1; font-size: var(--text-sm); line-height: 1.45; color: var(--on-surface); }
/* THE ACTION SLOT IS A REAL CONTROL, AND UNTIL Q242 FORTY-NINE OF THE
   FIFTY-FOUR WERE A <span>. A span carries no role, is not in the tab order and
   cannot be activated from a keyboard, so the one element on a state block that
   has to be reachable was the one element a keyboard could not reach -- 91% of
   the sites, measured. `DeadLink.jsx` names that exact failure as the reason it
   exists. Every one is a <button type="button"> now; the count is the gate:

     grep -oh '<[a-z]*[^>]*class="[^"]*\bact\b' docs/mockups/[0-9]*.html \
       | grep -o '^<[a-z]*' | sort | uniq -c

   THE ROLE CHANGED AND THE DRAWING DID NOT. Q223 ruled the split: the package
   wins the role, the deck wins the look. The reset below is what keeps the
   second half true -- a <button> arrives wearing ButtonFace, a border and the
   UA's 13.33px Arial, and every one of those would be the filled button this
   component exists not to be. */
button.act {
  appearance: none; -webkit-appearance: none;
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; color: inherit; text-align: inherit; cursor: pointer;
}

/* 44px OF TOUCHABLE HEIGHT, AND THE TEXT DOES NOT MOVE. DESIGN.md's floor is
   44px and these drew at 17-20px: "Undo" was a 33x17 target, which is the one
   control a reader reaches for in a hurry. The link keeps its link weight --
   the rule above says why it is not a button -- so the box grows around the
   text instead of the text growing.

   The arithmetic is why margin-top goes to 0 here. The text used to sit 12px
   of margin plus half an 18px line box from the top, so its center was 21px
   down; centered inside 44px it is 22px down. Same place, twice the target.

   AND THAT CENTERING HAD NEVER RUN. This was `a.act` (0,1,1) while `.say .act`
   above declared `display: inline-block` (0,2,0), so the more specific rule won
   the display and `align-items: center` was inert: measured in Chrome, the
   label sat at the TOP of the 44px box, its center 9px down rather than 22.
   The target grew downward and the text moved UP 12px, which is the opposite of
   what the paragraph above promises. Dropping the `display` from `.say .act` is
   what makes this rule's own arithmetic true.

   SCOPED TO THE TWO ELEMENTS THAT ARE CONTROLS, not to `.act`. A bare `.act`
   (0,1,0) would also match `.asof.act` -- where `act` is a modifier meaning
   "there is something to do on this surface", not the action slot -- and being
   later in the file it would beat `.asof`'s own `display: flex` and re-lay a
   staleness paragraph as an inline flex box. `ct` stays anchor-scoped for the
   original reason: it is also a <span> money figure in the same rows, and a
   figure is not a target. */
button.act, a.act {
  display: inline-flex; align-items: center; justify-content: flex-start;
  min-height: 44px; min-width: 44px;
  /* UNDERLINED, SO COLOR IS NOT THE ONLY SIGNAL. `Q242` took the pill off this
     control on a contrast measurement and left the blue label doing the
     identifying alone — which is exactly the thing a reader who cannot separate
     blue from the body ink has no way to see. Design ruled the underline back
     on when it ruled the pill off. `Q245`. */
  text-decoration: underline;
  text-decoration-thickness: 1px; text-underline-offset: 4px;
}
a.ct {
  display: inline-flex; align-items: center; justify-content: flex-end;
  min-height: 44px; min-width: 44px;
}
body.hc-air .resolve .act { flex-shrink: 0; font-size: var(--text-sm); font-weight: 500; color: var(--accent-ink); }

/* ---- Nav dock, and its scroll reserve -------------------------------------
   HcNavMenu shrink-wraps its height and sizes itself to its container, so a
   host pins it in a Stack — Positioned(bottom/left/right) for an upward menu,
   (top/left/right) for a downward one. This is that Positioned. The 16px inset
   is what makes the container 358pt inside a 390pt phone, and that number
   matters: the widget's narrow/wide switch reads the CONTAINER, not the
   viewport, so a phone frame resolves narrow even on a desktop page.

   .nav-reserve is the scroll reserve. A docked menu floats over the scroller,
   so the scroller has to hold clear air at the bottom or the last card sits
   under the pill. 120px, which is DESIGN.md's figure, not a taste: the spec
   names "120px bottom clearance above floating mobile navigation". This sat at
   112px on 177 scrollers until scripts/check-deck-posture.sh measured it --
   8px short, a multiple of 8, and invisible to every gate the deck had. Put it
   on the .scroll of every frame that carries a dock. From 13 and 15. */
body.hc-air .navdock { position: absolute; left: 16px; right: 16px; bottom: 16px; z-index: 40; }
body.hc-air .navdock.bottom { bottom: 16px; }
body.hc-air .navdock.top    { top: 24px; bottom: auto; left: 24px; right: 24px; }
body.hc-air .scroll.nav-reserve { padding-bottom: 120px; }

/* The page receding behind an open panel. From 15.
   IT IS BLACK, NOT --on-surface, and that is the whole point of this comment.
   It used to be the ink at 26% on the reasoning that a scrim should be the
   same color the text is — which reads correctly on paper and inverts the
   moment the ink does: on the dark theme --on-surface is #F4F1EA, so the
   receding page was lit by a white veil instead of being dimmed by a dark one.
   A scrim is not ink. It is the absence of light, and it darkens in both
   themes or it is not a scrim. */
body.hc-air .scrim {
  position: absolute; inset: 0; z-index: 30; pointer-events: none;
  background: rgba(0,0,0,0.30);
}

/* The wide canvas, for showing a component over a desktop layout. Not a phone
   bezel and not pretending to be a browser: just the page area. 1160 wide with
   a 20px dock inset gives a 1120pt container, which is HcNavMenuMetrics
   .wideMaxWidth exactly. From 15. */
body.hc-air .frame-wrap.wide { width: 1160px; max-width: 100%; }
body.hc-air .canvas {
  position: relative; overflow: hidden;
  height: 860px; border-radius: var(--r-lg);
  /* background-COLOR, for the reason given at .hc-ground. */
  background-color: var(--surface); box-shadow: var(--shadow-card);
  padding: 112px 40px 40px;
}
body.hc-air .canvas .cols { display: flex; gap: 24px; align-items: flex-start; }
body.hc-air .canvas .cols > * { flex: 1; min-width: 0; }

/* ---- Build-state annotation -----------------------------------------------
   Sits under the phone, outside the bezel, because the convention in this set
   is that meta never appears inside the device. It carries no color on
   purpose: index.html already spends green on "in the six-week proof" and
   indigo on "deferred", and a badge reusing either for "exists in code" would
   mean two different things on two pages. From 13. */
body.hc-air .annot { margin-top: 16px; }
body.hc-air .annot .g + .g { margin-top: 12px; }
body.hc-air .annot .micro-label { display: block; margin-bottom: 4px; }
body.hc-air .annot p { font-size: 12.5px; line-height: 1.55; color: var(--on-surface-variant); }
body.hc-air .annot code,
body.hc-air .note code {
  font-family: var(--font-mono); font-size: .92em;
  background: var(--surface-container); padding: 1px 4px; border-radius: 5px;
  /* A file path is one unbreakable word, so a long one pushes the page wider
     than the phone it is describing. Page 15 was the only page in the set
     overflowing at 390px and every other page is one long path away from the
     same bug — so the fix belongs here rather than in whichever page trips it
     next. */
  overflow-wrap: anywhere;
}

/* ---- Page nav -------------------------------------------------------------
   The pill list at the top of every page is rendered by _ds/pagenav.js into
   <div class="page-nav" data-pagenav></div>. The base .page-nav and .page-nav a
   rules are above and unchanged, so a page that still carries a hand-written
   list keeps its old look. What follows is only the group scaffolding the
   script emits, and the quieter treatment for the superseded run. */
.page-nav[data-pagenav] {
  display: block; text-align: center;
}
.page-nav .pn-group {
  display: inline-flex; align-items: center; flex-wrap: wrap;
  gap: 8px; margin: 0 8px 12px;
  vertical-align: middle;
}
/* Air between one run and the next, so a label reads as belonging to the pills
   after it rather than the pill before it. */
.page-nav .pn-group + .pn-group { margin-left: 24px; }
.page-nav .pn-label {
  text-transform: uppercase; font-size: 9.5px; font-weight: 700;
  letter-spacing: 2px; color: var(--on-surface-variant);
  opacity: .5; margin-right: 2px; white-space: nowrap;
}
/* Superseded pages are still reachable and still readable. They are just not
   where the argument is any more, so they lose the card and most of the
   contrast rather than being hidden. */
.page-nav .pn-group.pn-past .pn-label { opacity: .35; }
.page-nav .pn-group.pn-past a {
  background: transparent; box-shadow: none;
  color: color-mix(in srgb, var(--on-surface-variant) 58%, transparent);
  padding: 8px 12px;
}
.page-nav .pn-group.pn-past a.on {
  background: color-mix(in srgb, var(--primary) 16%, transparent);
  color: var(--accent-ink); font-weight: 500;
}

/* ---- The theme switch -----------------------------------------------------
   Appended to the page nav by _ds/pagenav.js. It is set apart from the page
   pills rather than dressed as one of them, because it does not navigate: the
   run of pills answers "which screen", and this answers "which theme", and a
   control that looks like its neighbors is claiming to be one of them. */
/* Its own line, not the tail of the last run. As an inline group it wrapped
   onto the end of whichever run had room — which happened to be the superseded
   one, so the theme switch sat among the dead pages looking like another of
   them. A block of its own is also the honest layout: it is not a sibling of
   the page pills. The margin-left reset undoes .pn-group + .pn-group, which
   would otherwise indent it off center. */
.page-nav .pn-group.pn-theme {
  display: block; margin: 16px 0 0; margin-left: 0;
}
.page-nav [data-theme-toggle] {
  font: inherit; font-size: 12px; font-weight: 600; letter-spacing: 0.08em;
  text-transform: uppercase; cursor: pointer;
  color: var(--on-surface-variant);
  background: transparent;
  border: 1px solid var(--outline);
  border-radius: var(--r-full);
  padding: 8px 16px;
}
.page-nav [data-theme-toggle]:hover {
  color: var(--accent-ink);
  border-color: color-mix(in srgb, var(--accent-line) 45%, transparent);
}
