/* ==========================================================================
   What's My R? v5 "Warm Clarity" — styles.css (OWNER: styles.css only, no JS/HTML here)
   Source of truth: docs/DESIGN-SYSTEM.md — §1 palette, §2 type, §3 spacing/radius/
   elevation/borders, §4 components, §8 motion, §9 iOS. Supersedes BUILD-SPEC §4
   (tokens/classes) for everything DESIGN-SYSTEM.md names; §4 remains authoritative
   for any class this file does not mention.

   NO class name emitted by a screen changes. Every selector that existed in v4
   still exists here; only its declarations moved onto the token system.

   Every contrast ratio quoted in a comment below was RECOMPUTED in this pass with a
   WCAG 2.x relative-luminance implementation over the FINAL surfaces in this file.
   Ratios measured against the old (inverted) --card2 are stale and were not carried
   forward. Recompute before changing any value.
   ========================================================================== */

/* ---- 1. Tokens (DESIGN-SYSTEM §1.1 / §2.1 / §2.3 / §3.1 / §3.3 / §3.4) --------
   THE FILE SHIPS TWO THEMES (LIGHT-SPEC §1). §1a is everything that does NOT change with
   the ground: type, spacing, radius, the tint MIX, motion, the iOS insets. §1b is LIGHT on
   bare :root — the default. §1c is DARK. The delta between them is 31 custom properties and one
   `color-scheme`, counted from this file, and that is the whole point of the v5.0 token
   refactor: nothing else had to move. (LIGHT-SPEC §1.10 said 24 because it predated
   --gold-display, --s-mid, the two --fan-* alphas and --chevron; each of those five exists
   because a value that could not follow its token was found in a component or a script.)

   HOW A THEME RESOLVES, AND WHY NOTHING FLASHES:
     no signal             bare :root — LIGHT, painted from the first frame.
     system prefers dark   @media (prefers-color-scheme:dark) — resolved by the CSS engine
                           before first paint, with no script involved at all.
     user override         :root[data-theme="light"] / :root[data-theme="dark"], stamped on
                           <html> by app/theme.js, which index.html loads as a PARSER-
                           BLOCKING classic <script> in <head>. It runs before the parser
                           reaches <body> and therefore before the first paint. No inline
                           script is needed, which matters: the CSP has no script-src, so it
                           falls back to default-src 'self' and an inline stamp is blocked.
     The dark media block is guarded :root:not([data-theme="light"]) so an explicit light
     choice beats a dark system preference. The two override blocks repeat the same bodies at
     (0,2,0) so they win wherever source order would otherwise decide.
     tests/theme-contrast.test.mjs asserts the four blocks agree and recomputes every pair in
     BOTH themes — including --bg2 as a surface, which no audit before this one measured. */

/* ---- 1a. Theme-independent ---------------------------------------------------- */
:root{
  /* type — stacks (§2.1). -apple-system must LEAD or an Apple device with Segoe
     installed loses SF. "Segoe UI Variable Text" must precede "Segoe UI" or Win 11
     pins the old static face. ui-serif is deliberately ABSENT: on Windows Chromium it
     resolves to Times New Roman and would beat Palatino Linotype. */
  --font-sans:-apple-system,BlinkMacSystemFont,"Segoe UI Variable Text","Segoe UI",
    system-ui,Roboto,"Helvetica Neue",Arial,sans-serif;
  --font-serif:"Iowan Old Style","Palatino Linotype",Charter,Constantia,Georgia,"Noto Serif",serif;
  --font-mono:ui-monospace,"SF Mono","Cascadia Mono",Consolas,"Roboto Mono",monospace;
  --font-num:var(--font-sans);            /* a numeral NEVER takes the serif (§2.6 rule 5) */

  /* type — scale (§2.3). Base is 16px: the reader is in their fifties and is being
     asked to trust the number. --t-xs 12px is the floor; nothing renders below it. */
  --t-xs:12px; --t-sm:14px; --t-base:16px; --t-md:18px;
  --t-lg:21px; --t-xl:27px; --t-2xl:34px; --t-3xl:clamp(34px,6.2vw,56px);
  /* LIGHT-SPEC §6.3 — the hero's two new steps, and the only two added by v5.1. The display
     clamp tops out at 64px, not 72px (D19): at 64px the measured Palatino advance is 980px in
     a 1408px box, so the headline tolerates a 43.7% wider serif before it takes a second line,
     where 72px tolerated 27.7%. That removes the fold budget's dependence on an Iowan Old
     Style / Noto Serif advance width nobody has measured. */
  --t-display:clamp(40px,6.4vw,64px);
  /* numeral steps — all five are sans 700 + tabular-nums, letter-spacing 0 */
  --t-num-sm:15px; --t-num-md:20px; --t-num-lg:27px; --t-num-xl:clamp(34px,8vw,46px);
  --t-num-2xl:clamp(40px,5.4vw,64px);

  /* spacing — 4px base, nine steps, and no value outside them (§3.1) */
  --sp-1:4px; --sp-2:8px; --sp-3:12px; --sp-4:16px; --sp-5:24px;
  --sp-6:32px; --sp-7:48px; --sp-8:64px; --sp-9:96px;

  /* radius — four values, nine legacy names kept alive so every var(--r13) across
     41 files snaps to the system with zero edits (§3.3). --r-full is permitted on
     round things and bar ends ONLY, never on a text container. */
  --r-sm:4px; --r-md:8px; --r-lg:12px; --r-full:999px;
  --r9:4px; --r10:4px; --r11:4px; --r12:8px; --r13:8px; --r14:8px; --r16:12px; --r22:12px; --r999:999px;

  /* elevation — three levels, exactly one of which casts a shadow (§3.4).
     At most ONE Floating element per view. Cards separate by the bg→card value step
     plus a --line2 hairline, never by shadow. --e-floating is THEME-SPECIFIC and lives in
     §1b/§1c: 55% black is a dark-ground value and reads as a smear over an off-white page. */
  --e-flush:none;
  --e-bounded:none;
  --shadow:none;                          /* was 0 18px 50px rgba(0,0,0,.35) — V8 */

  /* tinted fills — V10. Declared ONCE, never re-mixed inline, and THEME-INDEPENDENT as
     declarations: because the mix names var(--card) as its base, re-pointing the theme tokens
     re-mixes all four for free. That is the property that made the light theme 28 lines.
       DARK  #352923 / #2E2D2B / #352D22 / #2F302A — accent on its own tint 4.79–7.37,
             every ink step incl. --tertiary >=4.50, --line2 3.48–3.68 (it was 2.91–3.07,
             two of them under the 3.0 floor, until D6 re-pointed --line2).
       LIGHT #F1EAE8 / #E9EEF3 / #F2EEE8 / #E9F1EE — accent on its own tint 4.87–8.49,
             every ink step incl. --tertiary >=4.51, --line2 3.80–3.93.
     Both columns are recomputed from the shipped hexes by tests/theme-contrast.test.mjs
     rather than quoted, so a token move re-runs the measurement instead of staling this
     comment. The 3px accent LEFT RULE stays in both themes: the component must look the same
     in both, and it is the only boundary a tinted block has. */
  --tint-red:color-mix(in srgb, var(--red) 10%, var(--card));
  --tint-blue:color-mix(in srgb, var(--blue) 10%, var(--card));
  --tint-gold:color-mix(in srgb, var(--gold) 10%, var(--card));
  --tint-green:color-mix(in srgb, var(--green) 10%, var(--card));

  /* motion (§8.1). The focus RING is theme-specific and lives in §1b/§1c — two rings,
     because one ring cannot clear a saturated fill, and in light the inner ring cannot be
     --bg: an off-white spacer on a white card measures 1.11:1 and defeats the construction. */
  --ease:cubic-bezier(.22,1,.36,1);

  /* iOS safe areas (§9.2). These resolve to 0 until index.html carries
     viewport-fit=cover — see the F1 report, integrator item I-5. */
  --safe-t:env(safe-area-inset-top,0px);  --safe-b:env(safe-area-inset-bottom,0px);
  --safe-l:env(safe-area-inset-left,0px); --safe-r:env(safe-area-inset-right,0px);
}

/* ---- 1b. THEME: LIGHT — the default, on bare :root (LIGHT-SPEC §1, §6.1) ---------
   Every ratio below was recomputed over these exact hexes in this pass, on FOUR surfaces,
   because --bg2 is a real surface that no previous audit gave a column:

     on bg / card / card2 / bg2        LIGHT                 DARK
     --ink      #27221B               14.25 15.78 12.90 11.13   17.11 14.50 12.62 10.75
     --ink2     #524B43                7.75  8.58  7.02  6.05   11.91 10.10  8.79  7.48
     --muted    #6C665C                5.13  5.69  4.65  4.01x   7.90  6.70  5.83  4.96
     --tertiary #6E685F                4.98  5.51  4.51  3.89x   6.51  5.52  4.80  4.09x
     --red      #702D1C                9.11 10.09  8.25  7.12    6.56  5.56  4.84  4.12x
     --blue     #275084                7.39  8.18  6.69  5.77    7.70  6.52  5.68  4.83
     --gold     #7B5514                6.02  6.66  5.45  4.70    9.07  7.69  6.69  5.70
     --green    #247556                5.05  5.59  4.58  3.95x  10.66  9.03  7.86  6.70
     --violet   #7B3A8F                6.67  7.38  6.04  5.21    7.32  6.21  5.40  4.60

   THE --bg2 RULE, SPLIT BY FLOOR, ENFORCED IN BOTH THEMES (D02/D10/D21). A token is
   permitted as TEXT on --bg2 only if it clears 4.5 in EVERY shipped theme, which is a
   different and smaller set than either theme alone: --ink, --ink2, --blue, --gold and
   --violet. BANNED as text: --muted (4.01 light), --tertiary and its alias --ink3 (3.89 /
   4.09), --green (3.95 light) and --red (4.12 DARK — a pre-existing dark failure nobody had
   listed, and the reason the gate runs in both themes). A NON-TEXT indicator takes the 3.0
   floor, which is what lets .toast.good/.warn/.bad keep their tone left-rules, --red
   included. tests/theme-contrast.test.mjs derives that permit set by computation, then
   enumerates every descendant of every --bg2 ground and PINS the list, because the v5.0 audit
   wrote "all three live --bg2 sites pass, and the one non-ink descendant is --gold" and every
   number in that sentence was wrong. Re-derived from this file, there are FOUR grounds:

     .toast                       --bg2 always; color --ink                    11.13 / 10.75
       .toast-text                inherits --ink                               11.13 / 10.75
       .toast-action              --gold                                        4.70 /  5.70
       .toast-x                   --ink2 (was --muted: 4.01 LIGHT, 4.96 dark)   6.05 /  7.48
       .toast.good/.warn/.bad     tone left rules, INDICATORS at floor 3.0
                                  --green 3.95/6.70 · --gold 4.70/5.70 · --red 7.12/4.12
       edge                       --line2                                       3.18 /  3.17
     .seg-btn:not(.on):hover      re-declares color --ink in the same block     11.13 / 10.75
     .unit-toggle button:not(.on):hover   same                                  11.13 / 10.75
     .btn:hover                   inherits .btn's --ink; hover edge --ink2      11.13 / 10.75
                                                                                 6.05 /  7.48

   The two :hover segment rules are NOT failures and a selector-level ban would call them one:
   their --muted resting colour is painted on .seg's --card2 (4.65 / 5.83), and the only rule
   that paints --bg2 re-colours them in the same declaration block. That is why the gate
   measures the pair a rule RENDERS instead of banning a token near a surface.

   THE TONE LADDER INVERTS AND KEEPS ITS RANK (§1.4). §1.8's proposed light accents all
   landed at luminance 0.132-0.133 — four identical greys to a deuteranope. These are pinned
   to different contrasts instead: greyscale #424242 < #4F4F4F < #5C5C5C < #686868, monotonic
   red < blue < gold < green, spread 2.55x (dark 1.73x). On a light ground rung 1 is the
   darkest and therefore the HIGHEST-contrast colour; the ladder encodes rank, not loudness. */
:root{
  --bg:#F6F3ED; --card:#FFFFFF; --card2:#ECE8DF; --bg2:#DED8CD;
  --ink:#27221B; --ink2:#524B43; --muted:#6C665C; --tertiary:#6E685F; --ink3:#6E685F;
  --line:#E3DFD8; --line2:#7F756A;
  --red:#702D1C; --blue:#275084; --gold:#7B5514; --green:#247556; --violet:#7B3A8F;
  --sunset:#B3400E;
  /* H1 accent only, large text (>=40px/700), floor 3.0: 3.67 on --bg, 4.06 on --card.
     It exists because --gold at 6.02 is the body-text value and the hero wants the warmer
     one; it is NEVER permitted on body text, and the test pins that to one selector. */
  --gold-display:#A87411;
  /* white on every accent fill; --ink on an accent fill stays FORBIDDEN (1.56-2.82 light) */
  --ink-on-red:#FFFFFF;                   /* 10.09 on --red   */
  --ink-on-blue:#FFFFFF;                  /*  8.18 on --blue  */
  --ink-on-gold:#FFFFFF;                  /*  6.66 on --gold  */
  --ink-on-green:#FFFFFF;                 /*  5.59 on --green */
  /* categorical series (§1.9), rebuilt from scratch for a white ground: the dark five
     measure 1.22-4.66 on #FFFFFF and three of them fail the 3.0 mark floor outright.
     On --card 11.99 / 7.93 / 5.64 / 4.22 / 3.37; greyscale #373737 < #515151 < #676767 <
     #7B7B7B < #8C8C8C. --s-mid is rung 3, split out of --blue because the tone ladder and
     the series ladder cannot share one blue on white without colliding rungs 2 and 3.
     --s-other at 3.37 on --card is 2.76 on --card2, which is why .fan and .bd-bar moved. */
  --s-tax:#532C22; --s-salary:#6C4A28; --s-mid:#3468AC; --s-ss:#438682; --s-other:#789553;
  /* the fan chart's two band alphas — tokens, not JS constants, because charts.js cannot
     read the theme and a constant is exactly the value that cannot follow its token.
     .18/.50 light and .12/.30 dark reproduce each other: outer/ground 1.29 in both,
     inner/outer 1.94 in both. The inner band composites OVER the outer one. */
  --fan-outer:.18; --fan-inner:.50;
  /* the <select> chevron is a data: URI and a URI cannot read a custom property, so the
     literal is per-theme and the token carries it. muted/card2 is 4.65 light, 5.83 dark. */
  --chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%236C665C' fill='none' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  /* inner ring is --card, not --bg: --bg on a white card is 1.11:1, an invisible spacer.
     ring-to-ring 6.66; the outer gold clears 3.0 on all four grounds (6.02/6.66/5.45/4.70) */
  --focus-ring:0 0 0 2px var(--card), 0 0 0 4px var(--gold);
  --e-floating:0 8px 24px rgba(39,34,27,.16), 0 1px 3px rgba(39,34,27,.12);
  --e-raised:0 1px 2px rgba(39,34,27,.05), 0 6px 20px rgba(39,34,27,.07);
  /* §S3 onboarding pagelet — the full-viewport dimmed backdrop behind #/begin and #/start's
     modal-shell card (see "onboarding pagelet shell" at the end of this file). Built from --ink
     (light's darkest token) so it inverts with the theme instead of a fixed hex staying wrong in
     one of them. It never resolves to a single hex (color-mix's second stop is the `transparent`
     keyword, not a token) and carries no text, so it sits outside every token-pair floor
     tests/theme-contrast.test.mjs enforces — nothing is ever read off a scrim. */
  --scrim:color-mix(in srgb, var(--ink) 50%, transparent);
  color-scheme:light;
}

/* ---- 1c. THEME: DARK — v5.0's shipped values, unchanged except --line2 (D6) and the
   three tokens light introduced. Guarded so an explicit light choice beats the system. -- */
@media (prefers-color-scheme:dark){ :root:not([data-theme="light"]){
  --bg:#100E0B; --card:#241F1A; --card2:#2F2A24; --bg2:#3B352E;
  --ink:#F4F1EC; --ink2:#CFCBC3; --muted:#ACA59B; --tertiary:#9C958D; --ink3:#9C958D;
  --line:#3A342E; --line2:#8A8178;
  --red:#CC856F; --blue:#8BA6C8; --gold:#CBAF6C; --green:#96CCB7; --violet:#B992C5;
  --sunset:#FF9A62;
  --gold-display:#CBAF6C;                 /* dark keeps --gold — 9.07 on --bg, no change */
  --ink-on-red:#34150D;                   /* 5.68 on --red   */
  --ink-on-blue:#0E1E37;                  /* 6.66 on --blue  */
  --ink-on-gold:#261D0A;                  /* 7.83 on --gold  */
  --ink-on-green:#09231A;                 /* 9.17 on --green */
  --s-tax:#B05D48; --s-salary:#BB8046; --s-mid:#8BA6C8; --s-ss:#9FCFCC; --s-other:#E4EBDB;
  --fan-outer:.12; --fan-inner:.30;
  --chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23ACA59B' fill='none' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --focus-ring:0 0 0 2px var(--bg), 0 0 0 4px var(--gold);
  --e-floating:0 8px 24px rgba(0,0,0,.55), 0 1px 2px rgba(0,0,0,.4);
  --e-raised:0 1px 2px rgba(0,0,0,.35), 0 6px 20px rgba(0,0,0,.28);
  /* §S3 — dark's own darkest token is --bg, not --ink (ink is the near-white body-text colour
     here), so the scrim is built from that instead. See the light block's comment above it. */
  --scrim:color-mix(in srgb, var(--bg) 62%, transparent);
  color-scheme:dark;
} }

/* The two override blocks. They repeat the bodies above at (0,2,0) so a stamped choice wins
   whatever the source order, and tests/theme-contrast.test.mjs asserts each override is
   byte-equal to the block it mirrors — the duplication cannot drift. */
:root[data-theme="dark"]{
  --bg:#100E0B; --card:#241F1A; --card2:#2F2A24; --bg2:#3B352E;
  --ink:#F4F1EC; --ink2:#CFCBC3; --muted:#ACA59B; --tertiary:#9C958D; --ink3:#9C958D;
  --line:#3A342E; --line2:#8A8178;
  --red:#CC856F; --blue:#8BA6C8; --gold:#CBAF6C; --green:#96CCB7; --violet:#B992C5;
  --sunset:#FF9A62;
  --gold-display:#CBAF6C;
  --ink-on-red:#34150D;
  --ink-on-blue:#0E1E37;
  --ink-on-gold:#261D0A;
  --ink-on-green:#09231A;
  --s-tax:#B05D48; --s-salary:#BB8046; --s-mid:#8BA6C8; --s-ss:#9FCFCC; --s-other:#E4EBDB;
  --fan-outer:.12; --fan-inner:.30;
  --chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23ACA59B' fill='none' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --focus-ring:0 0 0 2px var(--bg), 0 0 0 4px var(--gold);
  --e-floating:0 8px 24px rgba(0,0,0,.55), 0 1px 2px rgba(0,0,0,.4);
  --e-raised:0 1px 2px rgba(0,0,0,.35), 0 6px 20px rgba(0,0,0,.28);
  --scrim:color-mix(in srgb, var(--bg) 62%, transparent);
  color-scheme:dark;
}
:root[data-theme="light"]{
  --bg:#F6F3ED; --card:#FFFFFF; --card2:#ECE8DF; --bg2:#DED8CD;
  --ink:#27221B; --ink2:#524B43; --muted:#6C665C; --tertiary:#6E685F; --ink3:#6E685F;
  --line:#E3DFD8; --line2:#7F756A;
  --red:#702D1C; --blue:#275084; --gold:#7B5514; --green:#247556; --violet:#7B3A8F;
  --sunset:#B3400E;
  --gold-display:#A87411;
  --ink-on-red:#FFFFFF;
  --ink-on-blue:#FFFFFF;
  --ink-on-gold:#FFFFFF;
  --ink-on-green:#FFFFFF;
  --s-tax:#532C22; --s-salary:#6C4A28; --s-mid:#3468AC; --s-ss:#438682; --s-other:#789553;
  --fan-outer:.18; --fan-inner:.50;
  --chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%236C665C' fill='none' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --focus-ring:0 0 0 2px var(--card), 0 0 0 4px var(--gold);
  --e-floating:0 8px 24px rgba(39,34,27,.16), 0 1px 3px rgba(39,34,27,.12);
  --e-raised:0 1px 2px rgba(39,34,27,.05), 0 6px 20px rgba(39,34,27,.07);
  --scrim:color-mix(in srgb, var(--ink) 50%, transparent);
  color-scheme:light;
}

/* THE WEIGHT LADDER (§2.2, V2). Three weights, all real faces:
     400 — body, captions, prose, ALL serif headings        (Segoe UI Regular)
     600 — labels, buttons, table headers, nav, row labels  (Segoe UI Semibold)
     700 — the single most important word or figure in a view, and every numeral
   500, 650, 750, 800, 850 and 900 are DELETED. On Windows 800/850/900 all land on
   Segoe UI Black, a display face that was setting 11px uppercase micro-labels.
   Lint gate (§11.3 #25):
     grep -rhoE "font-weight:\s*[0-9]+" styles.css app/ | grep -vE ":(400|600|700)$"
   must return nothing. */

/* ---- 2. Reset + base ------------------------------------------------------ */
*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth;-webkit-text-size-adjust:100%}   /* rotation must not resize type */
body{
  background:var(--bg);                   /* V8: both radial-gradient()s deleted, incl. the #15224a literal */
  color:var(--ink);
  font-family:var(--font-sans);
  font-size:var(--t-base);line-height:1.6;
  -webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;
  -webkit-tap-highlight-color:transparent;               /* the grey iOS flash reads as a rendering bug */
  overscroll-behavior:none;                              /* no white rubber-band behind a dark app */
  min-height:100vh;min-height:100dvh;                    /* dvh: the keyboard-open fix (§9.3) */
}
input,select,button,textarea{font-family:inherit}
::selection{background:var(--gold);color:var(--ink-on-gold)}   /* 7.83:1 — was rgba(70,211,154,.35) */
img{max-width:100%}
[hidden]{display:none!important}
svg{display:block}

/* touch behaviour a Capacitor shell needs from the CSS (§9.4). The killed tap
   highlight and the :active state always ship together, or a button feels dead. */
button,a,.row[role="button"],.chip,.pill.btn,.tier-card[role="button"],.lt-marker{
  touch-action:manipulation;-webkit-user-select:none;user-select:none}
.num,.table,p,li{-webkit-user-select:text;user-select:text}
button:active,.chip:active,.row[role="button"]:active{background:var(--card2)}

/* ---- 3. Accessibility utilities ------------------------------------------ */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.skip{position:absolute;left:var(--sp-3);top:-60px;z-index:200;display:inline-flex;align-items:center;
  background:var(--green);color:var(--ink-on-green);        /* 9.17:1 */
  font-weight:600;font-size:var(--t-sm);padding:0 var(--sp-4);min-height:44px;
  border-radius:var(--r-md);transition:top .18s var(--ease)}
.skip:focus{top:var(--sp-3)}

a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,
[tabindex]:focus-visible,summary:focus-visible{
  outline:none;box-shadow:var(--focus-ring);border-radius:var(--r-sm);
}

/* ---- 4. Chrome: header/brand, toast, nav --------------------------------- */
header{text-align:center;padding:calc(var(--sp-6) + var(--safe-t)) var(--sp-5) var(--sp-2)}
.brand{display:inline-flex;align-items:center;gap:var(--sp-3)}

/* V6: the wordmark was a gradient clipped to text, so `color` computed to transparent
   and it could never be a favicon or an App Store icon. It is now ONE FLAT INK SERIF
   with the trailing R in --gold — the existing .dot hook. Works at 16px, works in
   light mode, survives greyscale. */
.brand h1{font-family:var(--font-serif);font-size:20px;font-weight:400;letter-spacing:-.01em;
  background:none;-webkit-background-clip:border-box;background-clip:border-box;
  color:var(--ink);-webkit-text-fill-color:var(--ink)}
.brand .dot{color:var(--gold);-webkit-text-fill-color:var(--gold)}
/* 2026-09-25 — the Open Path mark replaces the "R" glyph (app/ui.js's monogram(), the favicon
   and apple-touch-icon.png all cut from the same 64-unit geometry). No font-* left to set: the
   mark is a stroked SVG path now, sized as a percentage of this box, not a text node. */
.monogram{width:44px;height:44px;border-radius:var(--r-md);display:grid;place-items:center;
  color:var(--ink-on-gold);background:var(--gold);box-shadow:none}
.monogram.sm{width:32px;height:32px}
.tag{color:var(--ink2);margin-top:var(--sp-3);font-size:var(--t-md);line-height:1.5;
  max-width:34ch;margin-left:auto;margin-right:auto;text-wrap:balance}
.tag b{color:var(--ink);font-weight:600}

/* toast (app.js builds div.toast(.good|.warn|.bad) > span.toast-text + button.toast-action? + button.toast-x)
   The ONE Floating element in the system (§3.4/§4.23): --bg2 ground, --line2 edge,
   --e-floating. Tone is a 3px left rule, never a tinted border. The --bg2 allowlist is
   split by floor (see :root): text 4.5, non-text indicator 3.0. Edge --line2 on --bg2 is
   3.17:1 after D6's re-point (it was 2.64:1) and 5.04:1 against the --bg behind it. */
#toast-root{position:fixed;z-index:300;display:flex;flex-direction:column;gap:var(--sp-2);
  align-items:center;pointer-events:none;
  left:calc(var(--sp-4) + var(--safe-l));right:calc(var(--sp-4) + var(--safe-r));
  bottom:calc(var(--sp-4) + var(--safe-b))}
@media(min-width:768px){#toast-root{left:auto;right:calc(var(--sp-5) + var(--safe-r));
  bottom:calc(var(--sp-5) + var(--safe-b));align-items:flex-end}}
.toast{pointer-events:auto;display:flex;align-items:center;gap:var(--sp-3);max-width:420px;width:100%;
  background:var(--bg2);border:1px solid var(--line2);border-left:3px solid var(--line2);
  border-radius:var(--r-md);box-shadow:var(--e-floating);
  padding:var(--sp-3) var(--sp-4);font-size:var(--t-sm);line-height:1.5;color:var(--ink)}
.toast.good{border-left-color:var(--green)}
.toast.warn{border-left-color:var(--gold)}
.toast.bad{border-left-color:var(--red)}
.toast-text{flex:1;min-width:0;line-height:1.5}
.toast-action{flex:none;background:none;border:none;color:var(--gold);font-weight:600;font-size:var(--t-sm);
  cursor:pointer;padding:0 var(--sp-2);min-width:44px;min-height:44px}
/* D7 — --muted, not --ink2, until this pass. On the toast's --bg2 ground that is 4.96:1 in
   dark (a pass, which is why it survived the v5.0 audit) and 4.01:1 in light (a fail). The
   token that is safe in BOTH themes is --ink2: 7.48 dark / 6.05 light. The dismiss X is a
   control with a text glyph in it, so its floor is 4.5, not 3.0. */
.toast-x{flex:none;background:none;border:none;color:var(--ink2);font-size:var(--t-md);line-height:1;
  cursor:pointer;width:44px;height:44px;min-width:44px;min-height:44px;display:grid;place-items:center;
  border-radius:var(--r-sm)}
.toast-x:hover{color:var(--ink);background:var(--card2)}

/* nav (app.js updateNav -> #nav > div.menubar > div.menu (button.menu-trigger + div.menu-panel >
   button.menu-item(.on)) + div.menubar-chips). 2026-09-26: one dropdown replaces the desktop tab
   row and the fixed phone tab bar. */
.menubar{max-width:1180px;margin:0 auto;display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-2);
  padding:calc(var(--sp-2) + var(--safe-t)) calc(var(--sp-4) + var(--safe-r)) var(--sp-2) calc(var(--sp-4) + var(--safe-l));
  border-bottom:1px solid var(--line)}
.menubar-chips{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-2);margin-left:auto;min-width:0}
.menubar-chips .filechip,.menubar-chips .authchip,.menubar-chips .authrow{margin:0}
.menubar-chips .authchip{max-width:none;flex:none}
.menu{position:relative}
.menu-trigger{display:inline-flex;align-items:center;gap:var(--sp-2);min-height:44px;padding:0 var(--sp-3);
  background:var(--card);border:1px solid var(--line2);border-radius:var(--r-md);color:var(--ink);
  font-size:var(--t-sm);font-weight:600;cursor:pointer;transition:.12s var(--ease)}
.menu-trigger:hover{border-color:var(--ink2)}
.menu-trigger:focus-visible,.menu-item:focus-visible{outline:none;box-shadow:var(--focus-ring)}
.menu-trigger .nav-icon{width:18px;height:18px;flex:none}
.menu-current{color:var(--muted);font-weight:600;padding-left:var(--sp-2);border-left:1px solid var(--line2)}
.menu-caret{color:var(--muted);font-size:var(--t-xs)}
.menu-panel{position:absolute;top:calc(100% + 6px);left:0;z-index:200;min-width:230px;
  display:flex;flex-direction:column;padding:var(--sp-1);background:var(--card);
  border:1px solid var(--line2);border-radius:var(--r-md);box-shadow:var(--shadow)}
.menu-item{position:relative;display:flex;align-items:center;gap:var(--sp-3);min-height:44px;padding:0 var(--sp-3);
  background:none;border:none;border-radius:var(--r-sm);color:var(--muted);font-size:var(--t-sm);font-weight:600;
  text-align:left;cursor:pointer}
.menu-item:hover{color:var(--ink);background:var(--card2)}
.menu-item.on{color:var(--ink);background:var(--card2)}
/* selected is a SHAPE change as well as a colour one, so it survives greyscale */
.menu-item.on::before{content:"";position:absolute;left:0;top:10px;bottom:10px;width:3px;border-radius:var(--r-full);background:var(--gold)}
.menu-item .nav-icon{width:18px;height:18px;flex:none}
.nav-dot{position:absolute;top:50%;right:var(--sp-3);margin-top:-3.5px;width:7px;height:7px;border-radius:50%;background:currentColor}
@media (max-width:600px){
  .menubar-chips{margin-left:0;width:100%}
  .menu-current{max-width:40vw;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
}

/* ---- 5. Layout ------------------------------------------------------------ */
/* §3.6: the two-column split moves from 920 to 1100. At 920 each column was ~430px —
   too narrow for the hero meter plus the tier trio. */
.layout{max-width:1080px;margin:var(--sp-5) auto 0;display:grid;grid-template-columns:1fr;gap:var(--sp-5);
  padding-left:calc(var(--sp-4) + var(--safe-l));padding-right:calc(var(--sp-4) + var(--safe-r));
  padding-bottom:calc(var(--sp-9) + var(--safe-b))}
@media(min-width:768px){.layout{padding-bottom:var(--sp-8)}}
@media(min-width:1100px){.layout{grid-template-columns:1.12fr .88fr;align-items:start}}
@media(min-width:1440px){.layout{max-width:1180px;gap:var(--sp-6)}}

.screen{display:block}
/* .s-<id> — screen root hook only; each screen module injects its own scoped
   `.s-<id> ...` rules via ui.style() per §3.1/§3.2. No shared rules live here. */

.col-result{order:-1}
@media(min-width:1100px){.col-result{position:sticky;top:calc(var(--hdr-h,72px) + var(--sp-5));order:0}}

/* ---- 6. Cards -------------------------------------------------------------- */
/* Bounded (§3.4): a 1px --line2 edge at 4.27:1 (was 3.56:1 — D6 re-point) and NO shadow. A card may not contain
   a card (§3.5) — an inner block is --card2 at --r-md with no border and no shadow. */
.card{background:var(--card);border:1px solid var(--line2);border-radius:var(--r-lg);box-shadow:var(--e-bounded)}
.card-pad{padding:var(--sp-5)}
@media(max-width:599.98px){.card-pad{padding:var(--sp-4)}}
.card.raised{box-shadow:var(--e-floating)}
/* a tint is a 3px accent LEFT RULE at full token value on an untinted ground
   (5.49–7.37:1); the three linear-gradient washes are deleted */
.card.tint-gold,.card.tint-green,.card.tint-blue{background:var(--card);border-left-width:3px}
.card.tint-gold{border-left-color:var(--gold)}
.card.tint-green{border-left-color:var(--green)}
.card.tint-blue{border-left-color:var(--blue)}

/* §4.2 — .step stops being a filled green pill and becomes a gold eyebrow ABOVE the
   title: a step number is wayfinding, not a call to action, and a saturated fill in
   front of a heading was outranking the heading. Markup is unchanged. */
.sec-h{display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--sp-1) var(--sp-3);margin-bottom:var(--sp-1)}
.sec-h .step{flex:0 0 100%;font-size:var(--t-xs);font-weight:600;letter-spacing:.06em;text-transform:uppercase;
  color:var(--gold);background:none;border:none;padding:0}
.sec-h h2{font-family:var(--font-serif);font-size:var(--t-lg);font-weight:400;letter-spacing:-.01em;
  line-height:1.35;text-wrap:balance}
.sec-h .sec-h-action{margin-left:auto}
.sec-sub{color:var(--muted);font-size:var(--t-sm);margin:var(--sp-2) 0 var(--sp-4);max-width:62ch}

.group-label{display:flex;align-items:center;gap:var(--sp-2);margin:var(--sp-5) 0 var(--sp-2);
  font-size:var(--t-xs);font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
.group-label::after{content:"";flex:1;height:1px;background:var(--line)}
.group-label .gi{width:16px;height:16px;flex:none;line-height:1}

.divider{height:1px;background:var(--line);margin:var(--sp-4) 0;border:none}

/* ---- 7. Rows & fields ------------------------------------------------------ */
/* §4.4 — THE DEAD-RULE FIX. v4 styled `.row-txt label`; ui.js:428 emits `span.lbl`,
   so every row label across all 24 screens rendered as unstyled inline body text,
   distinguishable from its own hint only by colour. One retargeted selector. */
.row{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-3);padding:var(--sp-3);
  border:1px solid transparent;border-radius:var(--r-md);min-height:44px;transition:.12s var(--ease)}
.row:hover{background:var(--card2);border-color:var(--line)}
.row-main{display:flex;align-items:center;gap:var(--sp-3);min-width:0;flex:1}
.row .ic{width:40px;height:40px;flex:none;border-radius:var(--r-sm);display:grid;place-items:center;
  font-size:var(--t-md);background:var(--card2);border:1px solid var(--line);color:var(--muted)}
.row-txt{min-width:0}
.row-txt .lbl{display:block;font-size:var(--t-md);font-weight:600;color:var(--ink);line-height:1.35;cursor:pointer}
.row-sub{display:block;font-size:var(--t-sm);color:var(--muted);margin-top:2px}
/* a bolded fragment inside a hint is emphasis, not a funded state — colour is for facts */
.row-sub b{color:var(--ink2);font-weight:600}
.row-tags{display:flex;gap:var(--sp-2);flex-wrap:wrap;margin-top:var(--sp-2)}
.row-tags:empty{display:none}

.row-input{display:flex;align-items:center;gap:var(--sp-2);flex:none}
/* V7: 16px on an <input> is NOT a style choice — under 16px iOS Safari zooms the page
   on focus and does not zoom back. Focus moves green → gold so it is one colour
   everywhere and never collides with the funded state. */
/* 46px = a 44px INPUT plus the field's own two 1px borders. The tap-target rule is
   measured on the <input>, not on its wrapper, so the input stretches to fill the
   field and the field grows by exactly its border. */
.field{display:flex;align-items:center;background:var(--card2);border:1px solid var(--line2);
  border-radius:var(--r-md);padding:0 var(--sp-3);min-height:46px;width:124px;transition:.12s var(--ease)}
.field.full{width:100%}
.field:focus-within,.field.focus{border-color:var(--gold);box-shadow:var(--focus-ring)}
/* 2026-09-29 (owner: "the text fields don't display cleanly") — the currency sign is the same
   size as the number and sits right against it; numbers read from the left like the text. */
.field .cur{color:var(--ink2);font-family:var(--font-num);font-size:16px;font-weight:600;flex:none}
.field input{background:none;border:none;outline:none;color:var(--ink);
  font-family:var(--font-num);font-size:16px;font-weight:600;font-variant-numeric:tabular-nums;
  width:100%;text-align:left;align-self:stretch;min-height:44px;padding:0 0 0 2px}
.field input::placeholder{color:var(--tertiary);font-weight:400}   /* 4.80:1 on --card2 — was 2.92:1 */
/* .field-toggle — the Show/Hide control lives INSIDE the field it toggles, a flex sibling of
   the input rather than a button in a row underneath (which is what produced the overlap: a
   separate row pulled up with a negative margin to close the gap, landing its border on top of
   the field's own bottom-right corner). align-self:stretch matches the field's 46px so the
   44px tap-target floor is met on both axes without a fixed height fighting min-height. */
.field .field-toggle{flex:none;align-self:stretch;display:inline-flex;align-items:center;
  justify-content:center;background:none;border:none;margin:0;padding:0 0 0 var(--sp-3);
  min-width:44px;color:var(--muted);font-size:var(--t-xs);font-weight:600;cursor:pointer;
  white-space:nowrap;transition:.12s var(--ease)}
.field .field-toggle:hover{color:var(--ink)}

.frow{display:flex;gap:var(--sp-3);margin-top:var(--sp-3);flex-wrap:wrap}
.ffield{flex:1;min-width:140px}
.ffield label{display:block;font-size:var(--t-sm);font-weight:600;color:var(--ink2);margin-bottom:var(--sp-2)}
.ffield label small,.ffield small{color:var(--muted);font-weight:400}
.ffield .field{width:100%}

/* the data: URI chevron cannot read a custom property, so the literal moved INTO the theme
   blocks as --chevron and this rule names the token. Before v5.1 it was one hard-coded
   %23ACA59B — --muted's DARK value — which on the light --card2 ground measures 1.88:1: a
   control's only affordance, invisible, and exactly the class of defect a literal is. Now
   muted/card2 = 4.65 light / 5.83 dark, because the URI follows the theme. */
.select{appearance:none;-webkit-appearance:none;-moz-appearance:none;width:100%;
  background:var(--card2) var(--chevron) no-repeat right var(--sp-3) center;
  border:1px solid var(--line2);border-radius:var(--r-md);color:var(--ink);
  font-size:16px;font-weight:600;padding:11px 32px 11px var(--sp-3);
  min-height:44px;cursor:pointer;transition:.12s var(--ease)}
.select:hover{border-color:var(--ink2)}

/* ---- 8. Controls ------------------------------------------------------------ */
/* §4.13 — the selected segment goes INK-ON-GROUND (--ink2 on --bg, 11.91:1). A
   segmented control is a view switch, not a value, and it was spending two of the
   five accent colours on furniture. */
.seg,.unit-toggle{display:inline-flex;background:var(--card2);border:1px solid var(--line2);
  border-radius:var(--r-md);overflow:hidden;flex:none}
.seg-btn,.unit-toggle button{background:none;border:none;color:var(--muted);
  font-size:var(--t-xs);font-weight:600;padding:0 var(--sp-3);min-height:44px;cursor:pointer;
  transition:.12s var(--ease)}
.seg-btn.on,.unit-toggle button.on{background:var(--ink2);color:var(--bg)}
.seg-btn:not(.on):hover,.unit-toggle button:not(.on):hover{color:var(--ink);background:var(--bg2)}

/* §4.8 — the pill is an OUTLINE TAG, not a filled lozenge. Six hand-mixed literals
   (#bcd4ff #e3c9ff #a9eccf #f7d38f #f3a9ad) are deleted; --r-full is dropped because a
   999px radius around a word reads as consumer candy and 4px reads as a tag on a
   document. Accent-on-card is 5.56–9.03:1.
   TAP TARGET: span.pill is a LABEL and is exempt from 44px. button.pill.btn is a
   CONTROL and is not. */
.pill{display:inline-flex;align-items:center;gap:var(--sp-1);font-size:var(--t-xs);font-weight:600;
  padding:3px var(--sp-2);border-radius:var(--r-sm);border:1px solid var(--line2);
  background:none;color:var(--muted);white-space:nowrap;line-height:1.4}
.pill.ord{color:var(--blue);border-color:var(--blue)}
.pill.inv{color:var(--violet);border-color:var(--violet)}
.pill.free,.pill.grow{color:var(--green);border-color:var(--green)}
.pill.ss{color:var(--gold);border-color:var(--gold)}
.pill.fixed{color:var(--red);border-color:var(--red)}
.pill.btn{cursor:pointer;min-height:44px;padding:0 var(--sp-3);transition:.12s var(--ease)}
.pill.btn:hover{background:var(--card2)}     /* was filter:brightness(1.18) — an unmeasurable colour */

/* §4.15 — selected is a DOUBLED GOLD BORDER, not a blue fill: a shape change as well
   as a colour change, so aria-pressed has a visual twin that survives greyscale.
   ui.js:486's inline el.style.borderColor override still applies and is unaffected. */
.chips,.chip-row{display:flex;gap:var(--sp-2);flex-wrap:wrap;margin-top:var(--sp-4)}
.chip{background:var(--card2);border:1px solid var(--line2);color:var(--ink2);
  font-size:var(--t-sm);font-weight:600;padding:0 var(--sp-4);min-height:44px;
  display:inline-flex;align-items:center;border-radius:var(--r-md);cursor:pointer;transition:.12s var(--ease)}
.chip:hover{color:var(--ink);border-color:var(--ink2)}
.chip.on{background:none;border-color:var(--gold);color:var(--gold);box-shadow:inset 0 0 0 1px var(--gold)}

/* inner block (§3.5): --card2, --r-md, no border of its own inside a bordered card */
.adj{margin-top:var(--sp-4);background:var(--card2);border:1px solid var(--line);
  border-radius:var(--r-md);padding:var(--sp-4)}
.adj-head{display:flex;align-items:center;gap:var(--sp-3);min-height:44px}
.adj-head .ti{width:18px;height:18px;flex:none;font-size:var(--t-base);line-height:1;color:var(--muted)}
.adj-head .tl{font-size:var(--t-base);font-weight:600}
.adj-head .sum{margin-left:auto;font-size:var(--t-xs);color:var(--muted);font-weight:600}
.adj-body{margin-top:var(--sp-4);transition:.18s var(--ease)}
/* D2 — THE `.adj.off` RULES ARE DELETED, not merely de-faded. The original
   `opacity:.35;filter:grayscale(.5)` over this --card2 inner block composited --ink to
   2.89:1, --ink2 to 2.35:1 and --muted to 1.93:1 (measured against --card, as the audit
   did, --ink lands on exactly 3.00:1). Round 1 swapped that for `opacity:1;color:--ink2`
   plus a dashed --line2 boundary, which fixed the CONTRAST half and left the other half
   standing: `pointer-events:none` was still the only inactivity mechanism, and CSS cannot
   take a control out of the tab order. WCAG 1.4.3 exempts an INACTIVE component; that one
   was merely mouse-inert, so its inputs still tabbed, still focused, and still announced
   as enabled — a keyboard user could land on a control that cannot be operated.
   And it was dead: nothing in app/ emits `.adj` / `.adj-head` / `.adj-body` (ui.js:1171
   emits only the unrelated `.adj-note` below). A comment cannot stop a future reviver from
   pasting the rule back, so the rule is gone instead of annotated. legacy/index-v3.html
   keeps its own inline copy of the old styles and does not link this file — nothing there
   changes, and nothing rendered by the app changes, because this state rendered nowhere.
   TO REVIVE: make the state genuinely inactive in the EMITTER — `disabled` on every
   control, or `aria-disabled` plus removal from the tab order, or `inert` on the block.
   Only a component that is actually inactive AND unfocusable may be dimmed at all.
   THE STANDING RULE, which outlives this component: an opacity dim toward the ground may
   never be the mechanism for de-emphasising READABLE CONTENT. Swap the token (--ink →
   --ink2 / --muted), add a border, or add a label — never fade. */
.adj-note{color:var(--muted);font-size:var(--t-sm);line-height:1.6;margin-top:var(--sp-3);max-width:62ch}
.adj-note b{color:var(--ink);font-weight:600}

/* §4.14 — the off state gains a --line2 border (3.72:1 after D6's re-point, was 3.10:1) so
   the track is visible as a control and not just a darker smudge; the knob moves off the
   #fff literal. The ::before inset is the SANCTIONED pattern for a 62x44 target without a
   62x44 visual.
   D3 — THE KNOB, MEASURED IN BOTH STATES, because the audit filed this as a live failure
   ("--muted on a checked --green track = 1.35:1") and it is not one. The base --muted knob
   is only ever painted on the OFF track: --muted on --card2 = 5.83:1. The instant the
   input is checked, `.switch input:checked ~ .knob` (0,3,0) outranks `.switch .knob`
   (0,2,0) and repaints it --ink-on-green = 9.17:1 on --green. The `~` reaches it because
   ui.js:774 emits `label.switch > input + span.track + span.knob` — the knob IS a
   following sibling of the input. Both pairs are above the 3:1 non-text floor, in both
   states; 1.35:1 is a pair this component never renders. Do not "fix" it by deleting the
   base --muted: that is the OFF knob, and it is the passing one. */
.switch{position:relative;width:44px;height:26px;flex:none;cursor:pointer;display:inline-block}
.switch::before{content:"";position:absolute;inset:-9px}
.switch input{opacity:0;width:0;height:0;position:absolute}
.switch .track{position:absolute;inset:0;background:var(--card2);border:1px solid var(--line2);
  border-radius:var(--r-full);transition:.18s var(--ease)}
.switch .knob{position:absolute;top:4px;left:4px;width:18px;height:18px;border-radius:50%;
  background:var(--muted);transition:.18s var(--ease)}
.switch input:checked ~ .track{background:var(--green);border-color:var(--green)}
.switch input:checked ~ .knob{transform:translateX(18px);background:var(--ink-on-green)}
.switch input:focus-visible ~ .track{box-shadow:var(--focus-ring)}

.slider-row{display:flex;flex-direction:column;gap:var(--sp-2);margin-top:var(--sp-3)}
.slider-row:first-child{margin-top:0}
/* 2026-09-27 — label left, value right, on every slider (screens used to add this one by one). */
.slider-row .st{display:flex;align-items:baseline;justify-content:space-between;gap:var(--sp-3);font-size:var(--t-sm);color:var(--ink)}
.slider-val{font-family:var(--font-num);font-weight:700;color:var(--gold);flex:none;
  font-size:var(--t-num-sm);font-variant-numeric:tabular-nums;letter-spacing:0}
/* 2026-09-29 (owner) — every slider's value is also a box you can type in. At rest it reads like
   the old value (a dashed underline says it can be edited); focused it becomes a plain field. */
.slider-edit{background:transparent;border:0;border-bottom:1px dashed var(--line2);border-radius:0;
  text-align:right;padding:2px 0;margin:0;min-width:4ch;max-width:60vw;cursor:text;min-height:32px}
.slider-edit:hover{border-bottom-color:var(--gold)}
.slider-edit:focus{outline:none;border:1px solid var(--gold);border-radius:var(--r-sm);background:var(--card2);
  color:var(--ink);padding:2px 8px;box-shadow:var(--focus-ring)}
/* the green→gold→red track was the only rainbow left in the kit and it encoded nothing:
   a slider track is furniture. Neutral track, single-token variants.

   §9.5 — THE CONTROL IS 44px TALL; THE VISIBLE TRACK STAYS 6px. Three things changed:

   1. The selector. `ui.sliderRow` (ui.js:960-970) emits a BARE <input type=range>, or
      `input.cool|.gold|.plain` when tinted — it never emits the `.range` class. So the
      old `.range` / `.range.cool` rules matched NOTHING the app renders, which is why six
      screen css blocks re-declare the whole slider from scratch. The base now hangs off
      `input[type=range]`, where the markup actually is; `.range` is kept as an alias so
      any hand-written markup carrying it still styles.
   2. `min-height`, not `height`, is the load-bearing word. min-height clamps used height
      whatever the specificity of the rule that set `height`, so a screen block's
      `input[type=range]{height:40px}` (plan-assumptions, plan-people, four more) can no
      longer shrink the tap target. Only another `min-height` can, which is what the lint
      gate is for.
   3. `height:6px` moves off the input and onto the track pseudo-element, so growing the
      control to 44px does not grow the furniture. Thumb margin-top:-6px centres a 20px
      thumb on the 8px track box (6px content + 2 x 1px border): (8 - 20) / 2. */
/* 2026-09-27 (owner: "make the sliders beautiful, easy to move") — a 28px thumb on a 44px-tall
   control, an 8px track that fills behind the thumb (`--pct`, kept current by ui.js rangePct), and
   a thumb that grows while held. The tint classes only change the fill colour. */
input[type=range],.range{-webkit-appearance:none;appearance:none;width:100%;
  height:44px;min-height:44px;padding:0;background:none;border:0;
  border-radius:var(--r-full);outline:none;cursor:pointer;touch-action:pan-y;
  --range-fill:var(--gold);--range-edge:var(--line2);--pct:0%}
input[type=range].cool,.range.cool{--range-fill:var(--blue)}
input[type=range].gold,.range.gold{--range-fill:var(--gold)}
input[type=range].plain,.range.plain{--range-fill:var(--muted)}
input[type=range]::-webkit-slider-runnable-track,.range::-webkit-slider-runnable-track{
  height:8px;border-radius:var(--r-full);border:1px solid var(--range-edge);
  background:linear-gradient(to right,var(--range-fill) 0 var(--pct),var(--card2) var(--pct) 100%)}
input[type=range]::-moz-range-track,.range::-moz-range-track{
  height:8px;border-radius:var(--r-full);border:1px solid var(--range-edge);background:var(--card2)}
input[type=range]::-moz-range-progress,.range::-moz-range-progress{height:8px;border-radius:var(--r-full);background:var(--range-fill)}
input[type=range]::-webkit-slider-thumb,.range::-webkit-slider-thumb{-webkit-appearance:none;
  width:28px;height:28px;border-radius:50%;background:var(--card);border:3px solid var(--range-fill);
  box-shadow:0 1px 4px color-mix(in srgb,var(--ink) 30%,transparent);cursor:grab;margin-top:-11px;
  transition:transform .12s var(--ease)}
input[type=range]::-moz-range-thumb,.range::-moz-range-thumb{width:22px;height:22px;border-radius:50%;
  background:var(--card);border:3px solid var(--range-fill);box-shadow:0 1px 4px color-mix(in srgb,var(--ink) 30%,transparent);
  cursor:grab;transition:transform .12s var(--ease)}
input[type=range]:hover::-webkit-slider-thumb,.range:hover::-webkit-slider-thumb{transform:scale(1.06)}
input[type=range]:active::-webkit-slider-thumb,.range:active::-webkit-slider-thumb{transform:scale(1.15);cursor:grabbing}
input[type=range]:active::-moz-range-thumb,.range:active::-moz-range-thumb{transform:scale(1.15);cursor:grabbing}
input[type=range]:focus-visible,.range:focus-visible{box-shadow:var(--focus-ring)}

/* §4.9 — three fixes: .btn:hover{color:#fff} deleted (a literal, and 1.60:1 on a
   .primary fill); .btn.danger:hover's literal #2a0c0e becomes var(--ink-on-red); and
   filter:brightness(1.08) deleted — brightening a token is an unmeasurable colour.
   .btn.sm stops being 36px: a small button is NARROWER, never shorter.
   §9.5 is 44x44, TWO AXES. min-height alone left a short label under target on the
   cross axis: button.btn.sm labelled "Fix" measured 40.8 x 44 at 390px (four of them
   on /survivor). min-width:44px is the same pattern .toast-action and .toast-x
   already use above. .btn.full still wins the main axis with width:100%. */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--sp-2);
  border:1px solid var(--line2);background:var(--card2);color:var(--ink);
  padding:0 var(--sp-4);min-height:44px;min-width:44px;border-radius:var(--r-md);
  font-family:var(--font-sans);font-weight:600;font-size:var(--t-sm);cursor:pointer;
  transition:.12s var(--ease);text-decoration:none}
  /* v4.5 ACCOUNTS-SPEC §15 — ui.linkButton is the first <a class="btn"> in this app (every
     earlier .btn was a real <button>, which never carried an underline to begin with); the reset
     above is what keeps the two tags visually identical. */
.btn:hover{border-color:var(--ink2);background:var(--bg2)}
.btn.primary{background:var(--green);border-color:var(--green);color:var(--ink-on-green)}
.btn.primary:hover{background:var(--green);border-color:var(--ink-on-green);color:var(--ink-on-green);filter:none}
.btn.ghost{background:none}
.btn.ghost:hover{border-color:var(--ink2)}
.btn.danger{background:none;border-color:var(--red);color:var(--red)}
.btn.danger:hover{background:var(--red);border-color:var(--red);color:var(--ink-on-red)}   /* 5.68:1 */
.btn.sm{padding:0 var(--sp-3);font-size:var(--t-xs);min-height:44px}
.btn.full{width:100%}
/* D01 — the disabled button is the one place an opacity dim would have been DEFENSIBLE
   (a `disabled` control is out of the tab order, so WCAG 1.4.3's inactive-component
   exemption really does apply) and it is still wrong, because `opacity:.45` composited
   --ink over --card2 to 1.96:1 in dark and 2.53:1 in light: an operator cannot read which
   button is dead. Tokens instead, and they are the same tokens the tone ladder already
   spends on "present but not the point": --tertiary on --card2 is 4.51 light / 4.80 dark,
   a genuine reading, with a --line2 edge at 3.69 / 3.72 so the box is still a control.
   R2 — AND THE PRECEDENCE IS NOW EXPLICIT RATHER THAN POSITIONAL. `.btn:hover` (0,2,0) and
   `.btn:disabled` (0,2,0) are equal, so a hovered disabled button resolved on LINE ORDER
   alone: it lands on --tertiary over --card2 (4.51 light / 4.80 dark) only because
   :disabled is declared later in the file. Swap the two blocks and the same button silently
   becomes --tertiary on --bg2 — 3.89 light / 4.09 dark, under the 4.5 floor in BOTH themes,
   and --tertiary is on this file's own banned-as-text list for --bg2. `.btn:disabled:hover`
   is (0,3,0) and outranks `.btn:hover` outright, so no reordering can resurrect that pair.
   `pointer-events:none` is the other half and stays: :hover cannot match an element that
   takes no pointer events, which is what keeps the (0,3,0) variant hovers
   (`.btn.primary:hover`, `.btn.danger:hover`) off a disabled button — a disabled danger
   button must never paint the live red fill, which is a worse defect than a ratio. */
.btn:disabled,.btn:disabled:hover{background:var(--card2);border-color:var(--line2);
  color:var(--tertiary);pointer-events:none}

/* §4.19 — no fill; the 3px blue left rule already says "aside". .tip b moves off --blue. */
.tip{display:flex;gap:var(--sp-3);margin-top:var(--sp-4);padding:var(--sp-3) var(--sp-4);
  border-radius:var(--r-md);background:none;border:1px solid var(--line);border-left:3px solid var(--blue);
  color:var(--ink2);font-size:var(--t-sm);line-height:1.6}
.tip > span:first-child{flex:none;width:18px;height:18px;line-height:1;color:var(--blue)}
.tip b{color:var(--ink);font-weight:600}

/* ---- 9. Result column ------------------------------------------------------- */
.verdict{text-align:center;padding:var(--sp-5)}

/* §4.10a — V4, FIXED AT THE SOURCE. v4 rotated `.ring` -90deg, but ui.js:601 returns
   div.ring > svg, so .ring-center (the percentage text) rotated too — and home.js:470
   and survivor.js:437 each carried a private un-rotate. The rotation now lives on the
   SVG, where it belongs. BOTH LOCAL PATCHES MUST BE DELETED IN THE SAME COMMIT
   (§10.5 coupling A): with this file alone those screens double-rotate.
   210px fixed → fluid max-width:180px (at 390px the fixed ring ate ~40% of the first
   screen); stroke 15→10; linecap round→butt, because a rounded cap at 0% still draws a
   visible dot, which reads as progress that does not exist. */
.ringwrap{position:relative;width:100%;max-width:180px;aspect-ratio:1;margin:0 auto var(--sp-2)}
.ring{position:relative;width:100%;max-width:180px;aspect-ratio:1;margin-inline:auto;transform:none}
.ring > svg{display:block;width:100%;height:auto;transform:rotate(-90deg)}
/* D04/D27 — THE LAST METER THAT DID NOT STATE ITS OWN EXTENT, now fixed rather than excused.
   The track was --card2: 1.22:1 on the --card it sits on in light, 1.15:1 in dark, so the
   reader could see the green arc but not the domain it is a fraction of — the same defect
   .rail-track, .compare-bar, .bd-bar and .tseg-track were each given a 1px --line2 border for,
   and .gauge-track and .donut-track a --line2 stroke. This one was left out because an SVG
   stroke cannot take a border, so BOTH test files carried it as written residue pointing here.
   Both exemptions are deleted in this commit; do not re-add them.

   --line2 on --card is 4.51:1 light / 4.27:1 dark, so the domain now reads. That alone would
   move the failure one pair over — the fill against the track is --green on --line2 at 1.24:1
   light / 2.12:1 dark — and no single track colour closes both pairs (§ the .gauge-keyline
   note in charts.js). The keyline is the construction that does: the same circle, the same
   10-unit stroke, painted --card, drawn 3 units longer than the fill and underneath it, so a
   3-unit --card gap sits at the arc's terminus. Measured in both themes, every tone ui.ring
   accepts, fill-vs-keyline: green 5.59/9.03 · gold 6.66/7.69 · red 10.09/5.56 ·
   blue 8.18/6.52 · violet 7.38/6.21 · muted 5.69/6.70. Keyline-vs-track 4.51/4.27.
   Source order is load-bearing — track, keyline, fill — and lives in ui.js's ring(). */
.ring-track{fill:none;stroke:var(--line2);stroke-width:10}
.ring-keyline{fill:none;stroke:var(--card);stroke-width:10;stroke-linecap:butt;
  transition:stroke-dasharray .7s var(--ease)}
.ring-fill{fill:none;stroke:var(--green);stroke-width:10;stroke-linecap:butt;
  transition:stroke-dashoffset .7s var(--ease),stroke .4s}
.ring-center{position:absolute;inset:0;display:grid;place-content:center;text-align:center}
.ring-pct{font-family:var(--font-num);font-size:var(--t-num-xl);font-weight:700;letter-spacing:0;
  font-variant-numeric:tabular-nums;line-height:1}
.ring-pct .sfx{font-size:.55em;color:var(--tertiary)}
.ring-cap{font-size:var(--t-xs);font-weight:600;letter-spacing:.06em;text-transform:uppercase;
  color:var(--muted);margin-top:var(--sp-2)}

/* §4.10b — ui.rail, the V1 fix. The gate is a FIXED 75% of the track at every value and
   on every screen, the scale is linear, and the fill never lies about being full.
   pos(v) = min(v / 1.3333, 1) is written by ui.js to --rail-pos. */
.rail{display:flex;flex-direction:column;gap:var(--sp-2)}
.rail-pct{font-family:var(--font-num);font-size:var(--t-num-xl);font-weight:700;letter-spacing:0;
  font-variant-numeric:tabular-nums;color:var(--ink);line-height:1}
.rail-pct .sfx{font-size:.55em;color:var(--tertiary)}
/* An empty reading is an em dash, and "—%" is not a reading — it is a unit with nothing in
   front of it, which is what /_kitchen's pending ring printed. `num()` already marks the empty
   case with `.num-empty` (ui.js:354), so the suffix hides itself off that class rather than off
   `pending`: EVERY empty value loses the stray %, not just the ones a caller remembered to flag.
   `display:none` removes it from the accessibility tree too, so a screen reader hears "—" and
   not "dash percent" — the same result as not emitting the span, without editing the frozen
   ring/rail markup that tests/ui.test.mjs pins. */
.ring-pct .num-empty + .sfx,.rail-pct .num-empty + .sfx{display:none}
.rail-cap{font-size:var(--t-sm);color:var(--muted)}
.rail-track{--rail-gate:75%;position:relative;height:12px;border-radius:var(--r-full);
  background:var(--card2);border:1px solid var(--line2)}
.rail-fill{display:block;height:100%;width:var(--rail-pos,0%);background:var(--green);
  border-radius:var(--r-full);transition:width .7s var(--ease)}
/* the 100% gate — a fixed constant, taller than the track so it reads as a threshold */
.rail-track::after{content:"";position:absolute;top:-4px;bottom:-4px;left:var(--rail-gate);
  width:1px;background:var(--ink2)}
/* beyond the scale the cap changes SHAPE: a flat end means "this is the value", a
   notched end means "the axis stops here, the number does not" */
.rail-fill.over-max{clip-path:polygon(0 0,calc(100% - 6px) 0,100% 50%,calc(100% - 6px) 100%,0 100%)}

.v-title{font-family:var(--font-serif);font-size:var(--t-xl);font-weight:400;letter-spacing:-.01em;
  line-height:1.25;margin-top:var(--sp-2);text-wrap:balance}
.v-msg{color:var(--ink2);font-size:var(--t-base);margin:var(--sp-2) auto 0;max-width:42ch;line-height:1.6}
.v-msg b{color:var(--ink);font-weight:600}
.v-msg .good{color:var(--green)}
.v-msg .bad{color:var(--red)}

/* inner block (§3.5). The headline figure is --ink: the tone lives in the meter beside
   it and in the words, never in a neon numeral (§1.6 — colour is the third channel). */
.rbig{margin:var(--sp-5) var(--sp-3) var(--sp-1);padding:var(--sp-4);border-radius:var(--r-md);
  background:var(--card2);border:1px solid var(--line);
  display:flex;align-items:center;justify-content:space-between;gap:var(--sp-3);text-align:left}
.rbig-label{font-size:var(--t-sm);font-weight:400;letter-spacing:0;text-transform:none;color:var(--muted)}
.rbig-amt{font-family:var(--font-num);font-size:var(--t-num-lg);font-weight:700;letter-spacing:0;
  font-variant-numeric:tabular-nums;color:var(--ink);line-height:1.1;margin-top:var(--sp-1)}
.rbig-sub{font-size:var(--t-sm);color:var(--muted);margin-top:2px}

.flow{margin:var(--sp-4) var(--sp-3) 0;display:flex;align-items:stretch;gap:var(--sp-2)}
.flow .stp{flex:1;min-width:0;background:var(--card2);border:1px solid var(--line);
  border-radius:var(--r-md);padding:var(--sp-2) var(--sp-3);text-align:left}
.flow .stp .k{font-size:var(--t-xs);font-weight:600;letter-spacing:.06em;text-transform:uppercase;
  color:var(--muted);white-space:nowrap}
.flow .stp .v{font-family:var(--font-num);font-size:var(--t-num-md);font-weight:700;letter-spacing:0;
  margin-top:2px;font-variant-numeric:tabular-nums}
.flow .stp.gross .v{color:var(--muted)}
.flow .stp.net .v{color:var(--green)}
.flow .arr{display:grid;place-items:center;color:var(--muted);font-size:var(--t-base);flex:none}

/* tinted component (§1.5): --tint-gold fill + a 3px gold left rule. --line2 on this fill
   was 2.96:1, under the 3.0 floor, which is why the boundary was the rule and not a border;
   after D6's re-point it is 3.54:1, so the rule is now a consistency choice. It stays. */
.fire{margin:var(--sp-4) var(--sp-3) 0;padding:var(--sp-4);background:var(--tint-gold);
  border:1px solid var(--line);border-left:3px solid var(--gold);border-radius:var(--r-md)}
.fire-top{display:flex;justify-content:space-between;align-items:center;font-size:var(--t-xs);
  font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--gold)}
.fire-top .r25{color:var(--muted);font-weight:600}
.fire-num{font-family:var(--font-num);font-size:var(--t-num-lg);font-weight:700;letter-spacing:0;
  font-variant-numeric:tabular-nums;margin-top:var(--sp-2);color:var(--ink);line-height:1.1}
.fire-sub{font-size:var(--t-sm);color:var(--muted);margin-top:var(--sp-1)}
/* the track states its own extent with a --line2 edge, not a --line one: a --card body on
   the --tint-gold ground above it is 1.16:1 light / 1.19:1 dark and --line adds 1.21 / 1.36,
   so before this the reader could see the gold fill and not the domain it was a fraction of
   (§5.3, WCAG 1.4.11 — the same defect .gauge-track was fixed for). --line2 is 3.90 / 3.54. */
.fire-bar{height:8px;border-radius:var(--r-full);background:var(--card);overflow:hidden;
  margin-top:var(--sp-3);border:1px solid var(--line2)}
.fire-fill{height:100%;border-radius:var(--r-full);background:var(--gold);transition:width .7s var(--ease)}
.fire-prog{font-size:var(--t-xs);margin-top:var(--sp-2);display:flex;justify-content:space-between;color:var(--muted)}
.fire-prog b{color:var(--ink);font-weight:600}

/* phase timeline (v3 .tl/.tl-h/.bdg/.tseg .age/.mid/.track/.meta/.cov, flattened per §4) */
.tl{margin:var(--sp-4) var(--sp-3) 0;padding:var(--sp-4);background:var(--card2);
  border:1px solid var(--line);border-radius:var(--r-md)}
.tl-head{display:flex;justify-content:space-between;align-items:center;gap:var(--sp-2);
  font-size:var(--t-xs);font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
.tl-badge{font-weight:600}
.tseg{display:flex;gap:var(--sp-3);padding:var(--sp-3) 0;border-bottom:1px solid var(--line)}
.tseg:last-child{border-bottom:none}
.tseg-age{flex:none;width:54px;text-align:center}
.tseg-age .rng{font-family:var(--font-num);font-size:var(--t-num-sm);font-weight:700;letter-spacing:0;
  font-variant-numeric:tabular-nums}
.tseg-age .ic{font-size:var(--t-base);margin-top:2px;line-height:1}
.tseg-body{flex:1;min-width:0}
.tseg-body .lbl{font-size:var(--t-sm);color:var(--ink);line-height:1.5}
/* the same 1px --line2 edge .rail-track, .fire-bar and .compare-bar carry, for the same
   reason: a --card body on the --card2 block above it is 1.22:1 light / 1.15:1 dark, so the
   track had no perceivable extent and the segment's fill was a fraction of nothing. The BODY
   stays --card, which is what keeps every tone fill legible inside it. */
.tseg-track{height:8px;border-radius:var(--r-full);background:var(--card);overflow:hidden;
  margin-top:var(--sp-2);border:1px solid var(--line2)}
.tseg-track .fill{height:100%;border-radius:var(--r-full);transition:width .7s var(--ease)}
.tseg-meta{font-size:var(--t-xs);color:var(--muted);margin-top:var(--sp-1)}
.tseg-pct{flex:none;width:46px;text-align:right;font-family:var(--font-num);font-size:var(--t-num-sm);
  font-weight:700;letter-spacing:0;font-variant-numeric:tabular-nums}
.tl-note{color:var(--muted);font-size:var(--t-sm);margin-top:var(--sp-3);line-height:1.6;max-width:62ch}
.tl-note b{color:var(--ink);font-weight:600}

/* breakdown bar (v3 .bd/.bar/.legend → .bd/.bd-bar/.bd-legend) */
.bd{padding:var(--sp-4) var(--sp-5) var(--sp-5);border-top:1px solid var(--line)}
.bd-h{font-size:var(--t-xs);font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);
  margin-bottom:var(--sp-3);display:flex;justify-content:space-between;gap:var(--sp-2)}
.bd-h .sub{font-weight:400;text-transform:none;letter-spacing:0;color:var(--tertiary)}  /* 5.52:1 — was 2.68:1 */
/* D03 / L1 — THE GROUND MOVES TO --card, and it is not cosmetic. .bd-bar is a stacked
   SERIES bar, so it is a chart, and the light series ladder only closes if charts render on
   --card and nowhere else: --s-other measures 3.37:1 on --card and 2.76:1 on --card2. The
   old §1.9 claim "charts already render on --card" was false in BOTH builds — this rule and
   .fan were the two containers contradicting it. With the ground on --card the bar needs its
   --line2 edge to state its extent (card2/card is 1.22 light, 1.15 dark: a track with no
   perceivable domain), which it already had.
   D05/D11 — AND THE SEGMENTS GET A KEYLINE. Adjacent rungs of a five-rung ladder measure
   1.25-1.51 light and 1.33-1.46 dark against EACH OTHER; the SVG charts are saved by
   `.chart-seg{stroke:var(--card);stroke-width:2}` and .bd-bar is DOM, so the house pattern
   never reached it. The same 2px --card keyline measures 3.37-11.99 light / 3.50-13.38 dark
   against every rung it separates. box-sizing is border-box, so it costs width, not layout;
   charts.js's own minimum-segment floor is what keeps a thin slice from being all keyline. */
.bd-bar{display:flex;height:16px;border-radius:var(--r-full);overflow:hidden;background:var(--card);
  border:1px solid var(--line2)}
.bd-bar i{display:block;height:100%;transition:width .7s var(--ease);
  border-right:2px solid var(--card)}
.bd-bar i:last-of-type{border-right:none}
.bd-bar .empty{flex:1;display:grid;place-items:center;font-size:var(--t-xs);color:var(--tertiary)}
.bd-legend{display:grid;grid-template-columns:1fr 1fr;gap:var(--sp-2) var(--sp-4);margin-top:var(--sp-4)}
@media(max-width:420px){.bd-legend{grid-template-columns:1fr}}
.bd-legend .leg{display:flex;align-items:center;gap:var(--sp-2);font-size:var(--t-sm)}
.bd-legend .leg .sw{width:10px;height:10px;border-radius:var(--r-sm);flex:none}
.bd-legend .leg .ln{color:var(--muted);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.bd-legend .leg .lv{font-family:var(--font-num);font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:0}
.bd-legend .leg .lp{color:var(--muted);font-size:var(--t-xs);width:38px;text-align:right;font-variant-numeric:tabular-nums}
/* charts.js:560 emits a bare i.legend-dot inside .bd-legend .leg — a different shape
   under the same class as ui.legendDot's span (§4.16) */
.bd-legend .leg .legend-dot{width:10px;height:10px;border-radius:var(--r-sm);flex:none;display:block}

/* the semantic pair (§1.6). 1.25:1 under tritanopia is NOT a pass and this file does
   not claim one: surplus and shortfall are never distinguished by colour. Every
   instance carries a sign (fmt.signedMoney), a word ("to spare" / "short") and a
   direction. Colour is the third channel, never the first. */
.gap{margin-top:var(--sp-4);padding:var(--sp-4);border-radius:var(--r-md);
  font-size:var(--t-base);line-height:1.6;border:1px solid var(--line);border-left:3px solid var(--line2)}
.gap.need{background:var(--tint-red);border-left-color:var(--red)}
.gap.spare{background:var(--tint-green);border-left-color:var(--green)}
.gap .gv{font-weight:700;font-variant-numeric:tabular-nums}
.gap.need .gv{color:var(--red)}      /* 4.79:1 on --tint-red */
.gap.spare .gv{color:var(--green)}   /* 7.37:1 on --tint-green */

.sscmp{display:flex;gap:var(--sp-2);margin-top:var(--sp-3)}
.sscell{flex:1;min-width:0;text-align:center;background:var(--card2);border:1px solid var(--line);
  border-radius:var(--r-md);padding:var(--sp-2) var(--sp-1);transition:.12s var(--ease)}
/* selected is a doubled border — a shape change, so it survives greyscale */
.sscell.on{border-color:var(--green);box-shadow:inset 0 0 0 1px var(--green);background:var(--tint-green)}
.sscell .sa{font-size:var(--t-xs);font-weight:600;color:var(--muted)}
.sscell .sm{font-family:var(--font-num);font-size:var(--t-num-sm);font-weight:700;letter-spacing:0;
  margin-top:2px;font-variant-numeric:tabular-nums}
.sscell .sp{font-size:var(--t-xs);font-weight:600;margin-top:2px}
.sscell .sp.up{color:var(--green)}
.sscell .sp.dn{color:var(--red)}
.sscell .sp.fra{color:var(--blue)}
.ss-hint{font-size:var(--t-sm);color:var(--muted)}

.proj{margin-top:var(--sp-3);padding:var(--sp-3) var(--sp-4);border-radius:var(--r-md);
  background:var(--tint-green);border:1px solid var(--line);border-left:3px solid var(--green);
  font-size:var(--t-sm);line-height:1.6}
.proj .pn{font-family:var(--font-num);font-size:var(--t-num-md);font-weight:700;letter-spacing:0;
  font-variant-numeric:tabular-nums;color:var(--ink)}
.proj small{color:var(--muted)}

/* ---- 10. New components (v4 only) -------------------------------------------- */

/* §4.5 — THE KPI STOPS BEING A TILE AND BECOMES A DEFINITION ROW. Four tiles 109px
   wide and 222px tall, with 11px/800 uppercase labels wrapping to 3–4 lines and one
   tile height forced by the worst case: tuning the tile could not fix that, removing
   the tile could. Label left in sentence case, figure right, caption under both, a
   hairline between rows. The class names and the markup are untouched, so
   home.js:310's el.querySelectorAll('.kpi')[2] still resolves. */
.kpi{background:none;border:none;border-top:1px solid var(--line);border-radius:0;
  padding:var(--sp-3) 0;display:grid;grid-template-columns:1fr auto;column-gap:var(--sp-4);align-items:baseline}
.kpi-label{grid-column:1;font-size:var(--t-sm);font-weight:400;letter-spacing:0;text-transform:none;color:var(--muted)}
.kpi-value{grid-column:2;grid-row:1;justify-self:end;display:block;font-family:var(--font-num);
  font-size:var(--t-num-md);font-weight:700;letter-spacing:0;font-variant-numeric:tabular-nums;color:var(--ink)}
.kpi-cap{grid-column:1/-1;font-size:var(--t-xs);color:var(--tertiary);margin-top:2px;max-width:48ch}

/* callout — inline feedback card (tone: good/warn/bad/neutral).
   §4.6 — the tone TITLE stops being coloured: a coloured heading plus a coloured rule
   plus a coloured icon is three signals for one fact. The rule and the icon carry it. */
.callout{display:flex;gap:var(--sp-3);padding:var(--sp-4);border-radius:var(--r-md);
  font-size:var(--t-base);line-height:1.6;background:var(--card2);border:1px solid var(--line)}
.callout-title{font-family:var(--font-serif);font-size:var(--t-lg);font-weight:400;color:var(--ink)}
.callout-body{color:var(--ink2);margin-top:var(--sp-1)}
.callout.good,.callout.warn,.callout.bad{background:var(--card);border:1px solid var(--line);border-left:3px solid}
.callout.good{border-left-color:var(--green)}
.callout.warn{border-left-color:var(--gold)}
.callout.bad{border-left-color:var(--red)}
.callout.neutral{background:var(--card2);border-color:var(--line)}

/* banner — dismissible notice strip. Same tone treatment as .callout. */
.banner{display:flex;align-items:center;gap:var(--sp-3);padding:var(--sp-3) var(--sp-4);
  border-radius:var(--r-md);background:var(--card2);border:1px solid var(--line);margin-bottom:var(--sp-4)}
.banner-title{font-weight:600;font-size:var(--t-base)}
.banner-body{color:var(--ink2);font-size:var(--t-sm);margin-top:2px}
.banner.good,.banner.warn,.banner.bad{background:var(--card);border-left:3px solid}
.banner.good{border-left-color:var(--green)}
.banner.warn{border-left-color:var(--gold)}
.banner.bad{border-left-color:var(--red)}
.banner-x{flex:none;background:none;border:none;color:var(--muted);font-size:var(--t-md);line-height:1;
  cursor:pointer;width:44px;height:44px;min-width:44px;min-height:44px;display:grid;place-items:center;
  border-radius:var(--r-sm);margin-left:auto}
.banner-x:hover{color:var(--ink);background:var(--card2)}

/* empty state — one of only two places centred text is permitted (§2.5) */
.empty{text-align:center;padding:var(--sp-7) var(--sp-5);color:var(--muted)}
.empty-icon{width:24px;height:24px;margin:0 auto;font-size:var(--t-lg);line-height:1;color:var(--tertiary)}
.empty-title{font-family:var(--font-serif);font-size:var(--t-lg);font-weight:400;color:var(--ink);margin-top:var(--sp-3)}
.empty-body{font-size:var(--t-base);margin-top:var(--sp-2);max-width:42ch;margin-left:auto;margin-right:auto;line-height:1.6}

/* table — .table-wrap{overflow-x:auto} IS the no-sideways-scroll guarantee for wide
   tables (§11.4 #31) and may not be removed */
.table-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;overscroll-behavior-x:contain}
.table{width:100%;border-collapse:collapse;font-size:var(--t-num-sm);white-space:nowrap}
.table th{text-align:left;font-size:var(--t-xs);font-weight:600;letter-spacing:.06em;text-transform:uppercase;
  color:var(--muted);padding:var(--sp-2) var(--sp-3);border-bottom:1px solid var(--line2)}
.table th[data-align="right"],.table td[data-align="right"]{text-align:right}
.table td{padding:var(--sp-3);border-bottom:1px solid var(--line);font-variant-numeric:tabular-nums;color:var(--ink2)}
.table tr:last-child td{border-bottom:none}
.table tr:hover td{background:var(--card2)}
.table tr.dead td{color:var(--tertiary)}          /* 5.52:1 — was 2.68:1 */
.table tr.dead td.age{color:var(--muted);text-decoration:line-through}

/* accordion */
.accordion{display:flex;flex-direction:column;gap:var(--sp-2)}
.acc-item{background:var(--card);border:1px solid var(--line2);border-radius:var(--r-lg);overflow:hidden}
.acc-head{width:100%;display:flex;align-items:center;justify-content:space-between;gap:var(--sp-3);
  background:none;border:none;color:var(--ink);text-align:left;padding:var(--sp-4) var(--sp-5);
  min-height:44px;cursor:pointer}
.acc-head:hover{background:var(--card2)}
.acc-head-title{font-family:var(--font-serif);font-size:var(--t-lg);font-weight:400}
.acc-head-caption{font-size:var(--t-sm);color:var(--muted);font-weight:400;margin-top:2px}
.acc-head-chevron{width:14px;height:14px;flex:none;color:var(--muted);transition:transform .18s var(--ease)}
.acc-item.open .acc-head-chevron{transform:rotate(180deg)}
.acc-body{display:none;padding:0 var(--sp-5) var(--sp-5)}
.acc-item.open .acc-body{display:block}
.acc-pinned{margin-bottom:var(--sp-4)}

/* stale — data from a prior computation shown while a fresher one loads (§8.4).
   grayscale(.3) is deleted: on a warm palette desaturating destroys the product's
   whole temperature and reads as BROKEN rather than as one moment out of date.

   D1 — AND SO IS opacity:.5, FOR THE SAME REASON ONE LAYER DOWN. Composited over --card it
   took --ink2 to 3.58:1, --muted to 2.71:1 and --tertiary to 2.40:1; over --card2 (.rbig,
   .adj, every inner block) --ink itself fell to 4.36:1 and --muted to 2.58:1. Floor 4.5.
   WCAG 1.4.3's inactive-component exemption cannot be claimed here by definition: this
   class's own docstring is "data from a prior computation SHOWN while a fresher one loads"
   — it is content the reader is reading, and a stale reading is still a reading.

   Dimming toward the ground is also the one construction that INVERTS between themes, so a
   value tuned on charcoal cannot be carried to white: the same .5 takes --ink to 3.15:1 in
   light. There is no opacity that is right in both. A token is.

   THE STANDING RULE: an opacity dim toward the ground is never the mechanism for
   de-emphasising readable content. Swap the token, add a border, or add a label.
   Here it is a border, because .stale is toggled onto arbitrary subtrees (a figure, a KPI
   row, a whole card, the gauge wrap) and a blanket colour rule would reach ink it does not
   own. A 1px dashed --line2 ring — 4.27:1 on --card, 5.04:1 on --bg, 3.72:1 on --card2,
   all above the 3.0 non-text floor — says "not settled yet" in the same idiom
   .filechip.never already uses, survives greyscale, and costs no layout: `outline` is not
   in the box model, so nothing reflows when a result lands. Every ratio INSIDE the subtree
   stays exactly what the token table says it is, in both themes, which is the whole point.
   THE ONE FADE THAT OUTRANKED THIS RULE IS GONE, DELETED IN THIS PASS: start-parts.js's
   `.s-start .ob-strip.stale{opacity:.55}` (0,3,0) no longer exists, so that strip inherits
   this rule; on --card2 every ink step clears 4.5, lowest --tertiary 4.51/4.80 (light/dark). */
.stale{opacity:1;filter:none;outline:1px dashed var(--line2);outline-offset:2px}
.stale-label{font-family:var(--font-mono);font-size:var(--t-xs);letter-spacing:.06em;
  text-transform:uppercase;color:var(--muted)}

/* badge — small tier tag, tone words match .tone-*. A LABEL (a span), so it is exempt
   from the 44px rule. --r-full is dropped per §3.3. Accent on its own tint: 4.79–7.37. */
.badge{display:inline-flex;align-items:center;font-size:var(--t-xs);font-weight:600;
  padding:3px var(--sp-2);border-radius:var(--r-sm);border:1px solid var(--line);line-height:1.4}
.badge.green{color:var(--green);background:var(--tint-green)}
.badge.gold{color:var(--gold);background:var(--tint-gold)}
.badge.blue{color:var(--blue);background:var(--tint-blue)}
.badge.red{color:var(--red);background:var(--tint-red)}
.badge.muted{color:var(--muted);background:var(--card2)}

/* fan — MC fan-chart wrapper (chart + trial-count chips). D03 / L1: it WAS an inner block on
   --card2 (§3.5), and it is a chart ground, so it was the second of the two containers that
   falsified "charts render on --card and nowhere else" — --s-other is 2.76:1 on --card2.
   Moving the ground to --card costs it the surface step that told the eye it was a block, so
   the boundary is promoted --line to --line2 (4.51 light / 4.27 dark): a bounded region has
   to be bounded by something, and §1.4 is explicit that a sole boundary is never --line. */
.fan{margin-top:var(--sp-4);padding:var(--sp-4);background:var(--card);border:1px solid var(--line2);
  border-radius:var(--r-md)}

/* compare — ranked horizontal bars (charts.compareBars) */
.compare{display:flex;flex-direction:column;gap:var(--sp-3);margin-top:var(--sp-2)}
.compare-row{display:flex;flex-direction:column;gap:var(--sp-1)}
.compare-row-top{display:flex;justify-content:space-between;gap:var(--sp-2);font-size:var(--t-sm)}
.compare-row-top .lab{color:var(--ink);font-weight:600}
.compare-row-top .val{font-family:var(--font-num);font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:0}
/* same rule as .rail-track and .fire-bar, for the same reason: this is a meter, so its track
   is half the reading. --card2 on --card is 1.22 light / 1.15 dark and --line is 1.33 / 1.33;
   --line2 is 4.51 / 4.27. A ranked bar whose longest row is also the full width of the track
   is unreadable if the track has no edge. */
.compare-bar{height:10px;border-radius:var(--r-full);background:var(--card2);border:1px solid var(--line2);overflow:hidden}
.compare-bar i{display:block;height:100%;border-radius:var(--r-full);background:var(--blue);transition:width .7s var(--ease)}
.compare-row.best .compare-bar i{background:var(--green)}
.compare-row.highlight .compare-row-top .lab{color:var(--gold)}

/* hatched — diagonal-stripe overlay for a stacked-bar/segment (V9's second channel;
   --s-tax is hatched in EVERY chart) */
.hatched{position:relative}
.hatched::before{content:"";position:absolute;inset:0;
  background-image:repeating-linear-gradient(135deg,currentColor 0 3px,transparent 3px 6px);opacity:.6}

/* num — every formatted numeric value (ui.num). UNTOUCHED in JS: ui.js:342 throws
   without src and every figure keeps data-src/data-kind. Styling only. */
.num{font-family:var(--font-num);font-variant-numeric:tabular-nums;letter-spacing:0}
.num.nominal{border-bottom:1px dotted var(--tertiary);cursor:help}
.num-empty{color:var(--tertiary)}

/* chart — generic SVG chart wrapper (charts.js builders) */
.chart{width:100%}
.chart svg{display:block;width:100%;height:auto}
.chart title{pointer-events:none}

/* legend — generic colour-key row (donut/lineChart/fanChart), distinct from .bd-legend.
   §4.16 MARKUP MISMATCH FIX: v4 styled the SPAN as the 9px swatch while ui.js:646 puts
   the colour on the <i>, so the swatch was the whole label row. Style the <i>.
   plan-accounts.js's local .legend-dot workaround must be DELETED IN THE SAME COMMIT
   (§10.5 coupling B). */
.legend{display:flex;flex-wrap:wrap;gap:var(--sp-2) var(--sp-4);margin-top:var(--sp-3)}
.legend-item{display:flex;align-items:center;gap:var(--sp-2);font-size:var(--t-sm);color:var(--muted)}
.legend-dot{display:inline-flex;align-items:center;gap:var(--sp-2);font-size:var(--t-sm);color:var(--muted);
  width:auto;height:auto;border-radius:0;background:none}
.legend-dot > i{width:10px;height:10px;flex:none;border-radius:var(--r-sm);display:block}

/* spinner */
.spinner{width:18px;height:18px;border-radius:50%;border:2px solid var(--line2);border-top-color:var(--gold);
  animation:wmr-spin .8s linear infinite;flex:none}
.spinner-row{display:inline-flex;align-items:center;gap:var(--sp-2);font-size:var(--t-sm);color:var(--muted)}
@keyframes wmr-spin{to{transform:rotate(360deg)}}

/* 2026-09-26 — the router's loading placeholder and load-failure card (app.js `loadingNode`,
   `failureCard`). The placeholder fades in after a beat, so a screen already in cache never
   flashes it; a route-focused heading (app.js `focusScreenHeading`) takes no focus ring, since
   it was focused by the router rather than reached by the keyboard. */
.app-loading{display:flex;flex-direction:column;align-items:center;gap:var(--sp-3);
  padding:var(--sp-5) var(--sp-4);color:var(--muted);font-size:var(--t-sm);
  animation:wmr-loading-in .2s var(--ease) .25s both}
.app-loading-bar{width:120px;max-width:60%;height:4px;border-radius:var(--r-full);background:var(--line);
  overflow:hidden;position:relative}
.app-loading-bar::after{content:"";position:absolute;inset:0;width:40%;border-radius:inherit;background:var(--gold);
  animation:wmr-loading-slide 1.1s var(--ease) infinite alternate}
@keyframes wmr-loading-in{from{opacity:0}to{opacity:1}}
@keyframes wmr-loading-slide{from{transform:translateX(0)}to{transform:translateX(150%)}}
/* Before app.js has run at all (its own imports are still arriving), an empty <main> shows the same
   bar with no words; `loadingNode` adds the announced line the moment the script starts. */
main#app:empty::before{content:"";display:block;width:120px;height:4px;margin:var(--sp-5) auto;
  border-radius:var(--r-full);background:var(--line);animation:wmr-loading-in .2s var(--ease) .25s both}
.load-failed{max-width:560px;margin:var(--sp-5) auto;display:flex;flex-direction:column;gap:var(--sp-3);align-items:flex-start}
.route-focus:focus{outline:none}

/* stepper — onboarding step-of-4 indicator. Progress is wayfinding, not a funded
   state, so the dot is gold (§4.2's argument, applied consistently). */
.stepper{display:flex;align-items:center;justify-content:center;gap:var(--sp-3);margin-bottom:var(--sp-4);
  font-size:var(--t-xs);color:var(--muted);font-weight:600}
.step-dots{display:flex;gap:var(--sp-2)}
.step-dots span{width:7px;height:7px;border-radius:50%;background:var(--line2);transition:.18s var(--ease)}
.step-dots span.on{background:var(--gold);width:20px;border-radius:var(--r-full)}

/* ---- 11. Tone mapping (one place; capacity/fundedShare ≥1 green, ≥.8 gold, ≥.5 blue, else red)
   D-23: this is a RANKED SEVERITY SCALE, so §1.5 orders it by luminance. It survives
   greyscale (#979797 → #A4A4A4 → #B2B2B2 → #C1C1C1) and every CVD. ------------------ */
.tone-green{color:var(--green)}
.tone-gold{color:var(--gold)}
.tone-blue{color:var(--blue)}
.tone-red{color:var(--red)}
.tone-muted{color:var(--muted)}

/* ---- 12. Responsive (§3.6 — five tiers, was one) -------------------------------- */
/* Phone            < 600  single column, bottom tab bar, card pad 16, section gap 48
   Phone L / small  >= 600 KPI list 2-up, .frow side by side, card pad 24
   Tablet portrait  >= 768 top nav replaces the bottom bar
   Two-column       >=1100 .layout 1.12fr .88fr, .col-result sticky
   Desktop          >=1440 content capped at 1180, section gap 64 */
@media(max-width:767.98px){
  main#app{padding-bottom:var(--sp-2)}
}

/* ---- 13. Reduced motion (§8.2). DO NOT SIMPLIFY: .01ms rather than `none` keeps
   transitionend firing for any JS that awaits it. ---------------------------------- */
@media (prefers-reduced-motion: reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{
    animation-duration:.01ms!important;
    animation-iteration-count:1!important;
    transition-duration:.01ms!important;
    scroll-behavior:auto!important;
  }
  /* a frozen spinner is a bug, not a reduced animation: the global kill snaps it to
     360deg and leaves a static ring that reads as a broken control */
  .spinner{animation:none!important;border-color:var(--line2);border-top-color:var(--gold);opacity:1}
  /* every bar arrives at its final width with no sweep */
  .rail-fill,.tier-bar i,.fire-fill,.tseg-track .fill,.bd-bar i,.compare-bar i,
  .ring-fill,.ring-keyline,.gauge-fill,.gauge-keyline{transition:none!important}
}

/* ==== v4.1 product layer (integrator; PRODUCT-DECISIONS P1/P2/P10/P13) ==================
   Everything below is additive. Screen agents file class requests here via the integrator. */

/* header brand is now a link to the homepage; the trailing R of "WhatsMyR" is the brand mark */
/* `color` is not decoration here: without it the link inherits the UA's default link colour
   (Chrome's dark-mode rgb(158,158,255)), the only element in the app computing one. Nothing
   paints purple today only because both children set their own colour — any bare text node,
   ::before or future sibling inside the brand link would, in the header, on every screen. */
a.brand{text-decoration:none;cursor:pointer;color:var(--ink)}
a.brand:focus-visible{outline:none;box-shadow:var(--focus-ring);border-radius:var(--r-sm)}

/* disclaimer footer (P10/P13) — rendered by the shell on every screen */
footer.disclaimer{max-width:1080px;margin:var(--sp-6) auto var(--sp-9);text-align:center;
  color:var(--muted);font-size:var(--t-sm);line-height:1.6;
  padding-left:calc(var(--sp-5) + var(--safe-l));padding-right:calc(var(--sp-5) + var(--safe-r))}
footer.disclaimer p{margin:0 auto;max-width:62ch}
/* 2026-10-09 (owner) — the disclaimer must be explicitly prominent: a bordered notice in full
   ink at body size, not small grey print. */
footer.disclaimer .disclaimer-main{max-width:68ch;padding:var(--sp-3) var(--sp-4);border:1px solid var(--line2);
  border-left:4px solid var(--gold);border-radius:12px;background:var(--card);color:var(--ink);
  font-size:var(--t-base);text-align:left}
footer.disclaimer .disclaimer-main strong{color:var(--ink)}
/* the one-time acknowledgement bar above every screen (app.js updateDisclaimerAck) */
#disclaimer-ack:empty{display:none}
.disc-ack{max-width:1080px;margin:var(--sp-3) auto 0;box-sizing:border-box;
  padding:var(--sp-3) var(--sp-4);border:1px solid var(--line2);border-left:4px solid var(--gold);
  border-radius:12px;background:var(--card);color:var(--ink);display:flex;gap:var(--sp-3);align-items:center;flex-wrap:wrap;
  font-size:var(--t-sm);line-height:1.55}
.disc-ack p{margin:0;flex:1 1 320px}
.disc-ack .btn{flex:none}
@media(max-width:1128px){.disc-ack{margin-left:calc(var(--sp-4) + var(--safe-l));margin-right:calc(var(--sp-4) + var(--safe-r))}}
footer.disclaimer .disclaimer-sub{margin-top:var(--sp-2);font-size:var(--t-xs);color:var(--tertiary)}
@media(min-width:768px){footer.disclaimer{margin-bottom:var(--sp-7)}}

/* Phase-0 mismatch fix: ui.js emits short class names (.ci/.ct/.cb callout,
   .bt/.bb/.ba banner, .ei/.et/.eb empty, .at/.ac/.av accordion head) while the rules
   above were written for the long names. Alias the short names here.
   DEC-2: these spans keep their markup byte-for-byte; only their CONTENT changes from a
   text node to an inline <svg>, which is why every rule below sizes a box and sets
   `color` rather than `font-size` alone — ui.icon paints in currentColor. */
.callout .ct{font-family:var(--font-serif);font-size:var(--t-lg);font-weight:400;color:var(--ink)}
.callout .cb{color:var(--ink2);margin-top:var(--sp-1)}
.callout .ci{flex:none;width:18px;height:18px;margin-top:2px;font-size:var(--t-base);line-height:1.1;color:var(--muted)}
.callout.good .ci{color:var(--green)}
.callout.warn .ci{color:var(--gold)}
.callout.bad .ci{color:var(--red)}
.banner .bt{font-weight:600;font-size:var(--t-base)}
.banner .bb{color:var(--ink2);font-size:var(--t-sm);margin-top:2px}
.banner .ba{margin-left:var(--sp-2);background:none;border:1px solid var(--line2);color:var(--ink);
  border-radius:var(--r-sm);padding:0 var(--sp-3);min-height:44px;font-size:var(--t-sm);font-weight:600;cursor:pointer}
.banner .ba:hover{background:var(--card2);border-color:var(--ink2)}
.empty .ei{width:24px;height:24px;margin:0 auto;font-size:var(--t-lg);line-height:1;color:var(--tertiary)}
.empty .et{font-family:var(--font-serif);font-size:var(--t-lg);font-weight:400;color:var(--ink);margin-top:var(--sp-3)}
.empty .eb{font-size:var(--t-base);margin-top:var(--sp-2);max-width:42ch;margin-left:auto;margin-right:auto;line-height:1.6}
.empty .btn{margin-top:var(--sp-4)}
.acc-head .at{font-family:var(--font-serif);font-size:var(--t-lg);font-weight:400}
.acc-head .ac{display:block;font-size:var(--t-sm);color:var(--muted);font-weight:400;margin-top:2px}
.acc-head .av{width:14px;height:14px;flex:none;color:var(--muted);transition:transform .18s var(--ease)}
.acc-item.open .acc-head .av{transform:rotate(180deg)}

/* ---- §7.4 shared classes the module agents' own `css` exports do NOT emit -------------
   widgets.js and charts.js each export a `css` string that app.js injects once at boot
   (boot(): ui.style('widgets'|'charts', mod.css)), and between them they already ship every
   other row of the §7.4 table: .tier-* / .snap-strip / .qcard* / .explain* / .gap-row +
   .gap-sev / .gap-fix / .evt-* / .sample-tag / .perm / .convo* (widgets.js) and the whole
   .lt-* life timeline incl. its half of the narrow-screen scroll (charts.js). Those stay in
   their own modules; only what neither emits is typed here.
   `.tier-trio.stale` deliberately has no rule of its own — §7.4 asks for "the §4 `.stale`
   treatment" and §4's `.stale` (opacity, above) already matches the trio's own element,
   so a duplicate here would be a second source for one look. */

/* decision grid (§4.4): ten question cards. The 700px breakpoint joins the tier list at
   600px (§3.6). The answer card itself (.qcard / .qcard-verdict / .qcard-metrics /
   .qcard-chip) comes from widgets.js. */
.q-grid{display:grid;grid-template-columns:1fr;gap:var(--sp-2);margin-top:var(--sp-4)}
@media(min-width:600px){.q-grid{grid-template-columns:repeat(2,1fr)}}
.q-ask{display:block;font-size:var(--t-sm);color:var(--muted);margin-top:2px;line-height:1.5;white-space:normal}

/* gap severity accent (§7.4): the dot itself is widgets.js's `.gap-sev.badge.<tone>`; the row's
   own high/medium/low rule is the integrator's half, so the two never drift apart. */
.gap-row.high{border-left:3px solid var(--gold)}
.gap-row.medium{border-left:3px solid var(--blue)}
.gap-row.low{border-left:3px solid var(--line2)}

/* ==== v4.2 file layer (integrator; FILE-SPEC §7.5) =====================================
   Additive, and the only shared rule this work needs: every other class the four new screens
   emit is scoped under `.s-open` / `.s-update` / `.pf-file` / `.s-start` and ships inside that
   screen's own `css` export, which app.js and plan.js inject as `screen-<id>`.

   Four states, one per §5.4 row. `saved` is the bare class and carries no modifier, because the
   quiet state is the resting state. `never` and `problem` are dashed and gold-bordered
   respectively — never red, never a dot, never animated: a status that shouts is a nag.

   §4.22 — BOTH CHIPS ARE CONTROLS (they are <button>s), so both take the 44px minimum. They
   were 28px and ~26px. They still ship centred here; moving them into a right-aligned utility
   row inside the top nav is §7.1's job and needs app.js's data-screen line first.

   The border is --line2, NOT the --line the §4.22 code block prints. §4.22's snippet contradicts
   §1.4, and §1.4 is the rule that is stated as absolute: "if a boundary tells you where to click,
   it is --line2". These two chips are unhoused — background:none, no container — so their 1px
   border is their ONLY boundary, and --line #3A342E over that ground is 1.33:1 on --card and
   1.57:1 on --bg: below the 3:1 UI floor and, in practice, invisible. --line2 #8A8178 measures
   5.04:1 on --bg (the ground they actually sit on, under the header) and 4.27:1 on --card, so
   both clear the floor on either surface. (Those were 4.21 and 3.56 at the old #7D746B; D6's
   re-point raised them.) --line stays what its own token comment says it is:
   a decorative divider inside an already-bounded region, never a sole boundary.

   Hover therefore cannot stay --line2 any more (it would now be a no-op against the resting
   state). It goes to --ink2, which is exactly what every other bounded control in this file
   already does on hover: .chip, .btn, .btn.ghost, .select, .synccard-choice, .banner .ba. */
.filechip,.authchip{display:inline-flex;align-items:center;min-height:44px;padding:0 var(--sp-3);
  border-radius:var(--r-sm);border:1px solid var(--line2);background:none;color:var(--muted);
  font-size:var(--t-xs);font-weight:600;cursor:pointer;transition:.12s var(--ease);
  max-width:calc(100% - 32px);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.filechip{margin:var(--sp-2) auto 0}
.authchip{margin:0 auto var(--sp-2)}
.filechip:hover,.authchip:hover{color:var(--ink);border-color:var(--ink2)}
.filechip.dirty{color:var(--ink)}
.filechip.never,.filechip.unsaved,.authchip.signedOut{border-style:dashed}
.filechip.problem,.authchip.offline{color:var(--gold);border-color:var(--gold)}
.authchip.syncing{color:var(--ink2)}
.filechip:focus-visible,.authchip:focus-visible{outline:none;box-shadow:var(--focus-ring)}
@media (max-width:600px){.filechip,.authchip{margin-bottom:var(--sp-2)}}
/* The identity chip and its Sign out button, side by side and centred like the chip alone was. */
.authrow{display:flex;justify-content:center;align-items:center;gap:var(--sp-2);margin:0 auto var(--sp-2);max-width:calc(100% - 32px)}
.authrow .authchip{margin:0;max-width:none;min-width:0}
.authchip.signout{flex:none;color:var(--ink2)}

/* ==== v4.4 account layer (ACCOUNTS-SPEC §8.6, §6.5, §9.4) ================================= */

/* The two unskippable cards (§6.5's conflict, §9.4's claim). Three choices, equal visual weight,
   none preselected — F27's rule made visible: nothing here reads as "the one you were going to
   press anyway", because the whole point is that only the person can answer it. */
.synccard-host:empty{display:none}
.synccard{max-width:640px;margin:0 auto var(--sp-4);border-left-width:3px;border-left-color:var(--gold)}
.synccard-t{margin:0 0 var(--sp-2);font-family:var(--font-serif);font-size:var(--t-lg);font-weight:400}
.synccard-b{margin:0 0 var(--sp-1);color:var(--ink2);line-height:1.6;max-width:62ch}
.synccard-d{margin:0 0 var(--sp-3);color:var(--ink3);font-size:var(--t-xs)}
.synccard-choice{display:block;width:100%;margin-top:var(--sp-2);padding:var(--sp-3) var(--sp-4);
  text-align:left;min-height:44px;border:1px solid var(--line2);border-radius:var(--r-md);
  background:var(--card2);cursor:pointer;transition:.12s var(--ease)}
.synccard-choice:hover{border-color:var(--ink2)}
.synccard-choice:focus-visible{outline:none;box-shadow:var(--focus-ring)}
.synccard-choice-t{display:block;color:var(--ink);font-weight:600;font-size:var(--t-sm)}
.synccard-choice-h{display:block;margin-top:2px;color:var(--ink3);font-size:var(--t-xs);line-height:1.5}

/* The account screen's rows. */
.s-account .acct-wrap{max-width:620px;margin:0 auto;padding:0 var(--sp-1)}
.s-account .acct-card{margin-bottom:var(--sp-4)}
.s-account .acct-h{margin:0 0 var(--sp-2);font-family:var(--font-serif);font-size:var(--t-lg);font-weight:400}
.s-account .acct-p{margin:0 0 var(--sp-3);color:var(--ink2);font-size:var(--t-sm);line-height:1.6;max-width:62ch}
.s-account .acct-meta{margin:0 0 var(--sp-3);color:var(--ink3);font-size:var(--t-xs)}
.s-account .acct-list{margin:0;padding:0;list-style:none}
.s-account .acct-row{display:flex;justify-content:space-between;gap:var(--sp-3);padding:var(--sp-2) 0;
  border-top:1px solid var(--line);font-size:var(--t-sm)}
.s-account .acct-row:first-child{border-top:0}
.s-account .acct-new{color:var(--gold)}
.s-account .acct-error:empty{display:none}

/* ==== §6.5 / §7.1 — THE GLOBAL HEADER, gated on body[data-screen] ==========================
   Added by the SHELL pass (index.html + app/app.js), because these three rules and app.js's
   `document.body.dataset.screen = screen.id` are one change: neither half does anything alone,
   and the CSS block at line ~877 above says in as many words that this work "needs app.js's
   data-screen line first". If the styles pass also lands §6.5 verbatim, these rules are the
   identical declarations and either copy may be deleted — but only one of them.

   THE NEGATION IS THE POINT. `body:not([data-screen="landing"])` matches a <body> that carries no
   attribute at all, which is every body before the first `renderRoute()` has run. So the
   marketing header is hidden from the FIRST PAINT rather than from the first frame after boot,
   and a cold load of #/home never flashes 165px of monogram-wordmark-tagline above the user's own
   portfolio. Measured signed-in chrome above the content: 287px (header 165 + #nav 122) → 56px.

   On the landing the same element becomes the 56px utility bar of §6.6's wireframe: flush left,
   centred in the same 1180px measure as .topnav, no longer a centred marketing block. The
   tagline is not hidden here because it is no longer in the markup — index.html deleted it, so it
   is said once, by the hero, on the one page that sells (V6). */
/* 2026-09-25 — signin/signup ADDED to the header allowlist (the recover door was removed, ACCOUNTS-SPEC §17). They are pre-auth: a visitor
   there has not reached the signed-in app the 56px-bar rule exists to keep clear for, and a bare
   card floating with no logo, no way back and nothing tying it to the site was a real gap, not a
   design choice — every one of them was one click from the marketing header a moment earlier.
   `#header-actions` stays empty here (only landing.js populates it), so the bar shows brand-only:
   no menu, no search, no "Get started" while a visitor is mid sign-up. */
/* 2026-09-29 (owner) — the header (logo, name, the slogan in the middle) shows on EVERY screen and
   stays in place while the page scrolls. */
header{
  display:flex;align-items:center;justify-content:space-between;
  max-width:1180px;margin-inline:auto;text-align:left;min-height:56px;
  padding:calc(var(--sp-2) + var(--safe-t)) calc(var(--sp-4) + var(--safe-r))
          var(--sp-2) calc(var(--sp-4) + var(--safe-l))}
body[data-screen="landing"] .monogram,body[data-screen="signin"] .monogram,
body[data-screen="signup"] .monogram{
  width:32px;height:32px;font-size:16px}

/* 2026-09-25 — the header's menu / search / "Get started" (#header-actions, landing.js only).
   The parent `header` rule above is already `justify-content:space-between`, so this block is
   simply the second flex child pushed to the far right; nothing here touches the bar's height.
   Every hit target below uses the brand link's own trick (min-height:44px, margin-block to give
   the extra back to the bar's padding) rather than growing the 56px bar itself. Tokens only. */
.hdr-actions{display:flex;align-items:center;gap:var(--sp-4)}
.hdr-menu{display:none;align-items:center;gap:var(--sp-1)}
@media(min-width:820px){.hdr-menu{display:flex}}
.hdr-link{font:inherit;font-size:var(--t-sm);font-weight:600;color:var(--ink2);background:none;
  border:none;cursor:pointer;padding:var(--sp-2) var(--sp-2);border-radius:var(--r-sm);
  min-height:44px;margin-block:-6px}
.hdr-link:hover{color:var(--ink);background:var(--card2)}
.hdr-search{position:relative;display:flex;align-items:center}
.hdr-search-btn{display:grid;place-items:center;width:44px;height:44px;margin:-6px;
  background:none;border:none;border-radius:var(--r-sm);color:var(--ink2);cursor:pointer}
.hdr-search-btn:hover{color:var(--ink);background:var(--card2)}
.hdr-search-btn[aria-expanded="true"]{color:var(--ink);background:var(--card2)}
.hdr-search-panel{position:absolute;top:calc(100% + var(--sp-2));right:0;z-index:120;
  width:min(320px,80vw);background:var(--bg2);border:1px solid var(--line2);
  border-radius:var(--r-lg);box-shadow:var(--e-floating);padding:var(--sp-3)}
.hdr-search-input{width:100%;font-size:16px;padding:var(--sp-2) var(--sp-3);
  background:var(--card);border:1px solid var(--line2);border-radius:var(--r-md);color:var(--ink)}
.hdr-results{margin-top:var(--sp-2);max-height:60vh;overflow:auto;display:flex;flex-direction:column;gap:2px}
.hdr-result{display:flex;flex-direction:column;align-items:flex-start;gap:2px;text-align:left;
  width:100%;background:none;border:none;border-radius:var(--r-sm);padding:var(--sp-2) var(--sp-2);
  cursor:pointer;min-height:44px;justify-content:center}
.hdr-result:hover{background:var(--card2)}
.hdr-result-h{font-size:var(--t-sm);font-weight:600;color:var(--ink)}
/* --ink2, not --muted: --muted on --bg2 is 4.01:1 in light (floor 4.5), the same failure the
   toast dismiss-X already hit on this exact surface — --ink2 is the token proven safe on --bg2
   in both themes (7.48 dark / 6.05 light, see the toast-x comment above). */
.hdr-result-g{font-size:var(--t-xs);color:var(--ink2)}
.hdr-empty{font-size:var(--t-sm);color:var(--ink2);padding:var(--sp-2)}

/* §9.2 — the footer's own bottom inset. Every other full-bleed container already spends
   --safe-b (.layout:247, .bottomnav:226, #toast-root:188); the disclaimer, which is a SIBLING of
   <main> and therefore outside .layout's padding, spent none — so on a notched iPhone its last
   line sat under the home indicator. It needs the inset at both breakpoints: below 768 the fixed
   tab bar is over it, and above 768 the only iPhones that wide are in landscape, where the home
   indicator is still there. */
footer.disclaimer{margin-bottom:calc(var(--sp-9) + var(--safe-b))}
@media(min-width:768px){footer.disclaimer{margin-bottom:calc(var(--sp-7) + var(--safe-b))}}

/* ==== FREEDOM v1 — app/choice-parts.js: the stop pair and the phase strip ==================
   Shared by the landing, home and choose screens, so the sheet lives here rather than in one
   screen's `css`. Tokens only; numerals take --font-num; every paint is an ink token on a
   --card / --card2 ground the contrast matrix already covers; weights 400/600/700; nothing
   under 12px (--t-xs). The big figure is --ink — the tone lives in the rail beside it (§1.6). */
.stop-pair{display:grid;grid-template-columns:1fr;gap:var(--sp-3)}
@media(min-width:640px){.stop-pair{grid-template-columns:1fr 1fr}}
.stop-card{background:var(--card);border:1px solid var(--line2);border-radius:var(--r-lg);
  padding:var(--sp-4);text-align:left;min-width:0}
.stop-tag{font-size:var(--t-xs);font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
.stop-label{font-size:var(--t-sm);color:var(--muted);margin-top:var(--sp-3)}
.stop-big{font-family:var(--font-num);font-size:var(--t-num-xl);font-weight:700;letter-spacing:0;
  font-variant-numeric:tabular-nums;color:var(--ink);line-height:1.05;margin-top:var(--sp-1)}
.stop-unit{font-size:var(--t-sm);color:var(--muted);margin-top:2px}
.stop-why{font-size:var(--t-xs);color:var(--tertiary);margin-top:var(--sp-1);max-width:36ch;line-height:1.45}
.stop-funded{font-size:var(--t-base);font-weight:600;color:var(--ink);margin-top:var(--sp-3)}
.stop-years{font-size:var(--t-sm);color:var(--ink2);margin-top:var(--sp-1)}
.stop-years .stop-yearslabel{color:var(--muted)}
.stop-mc{margin-top:var(--sp-3)}
.stop-mc .rail-pct{font-size:var(--t-num-lg)}
.stop-tax{display:flex;justify-content:space-between;align-items:baseline;gap:var(--sp-2);
  font-size:var(--t-sm);color:var(--ink2);margin-top:var(--sp-3);padding-top:var(--sp-2);border-top:1px solid var(--line)}
.stop-tax .num{font-weight:600;color:var(--ink)}
.stop-est{font-size:var(--t-xs);color:var(--tertiary);margin:var(--sp-2) 0 0;line-height:1.45}
/* the hook line above the pair (freedom.compareDelta) — the one thing on the landing centrepiece
   meant to be read before the two cards are, so it is the largest text in the section besides the
   hero itself; the gold numerals are the only accent colour on an otherwise ink-on-card section. */
.stop-hook{font-size:var(--t-lg);font-weight:700;color:var(--ink);text-align:center;
  max-width:52ch;margin:0 auto var(--sp-4);line-height:1.35}
.stop-hook .num{font-family:var(--font-num);font-variant-numeric:tabular-nums;color:var(--gold)}
.phase-strip{display:grid;grid-template-columns:1fr;gap:var(--sp-2);margin-top:var(--sp-4)}
@media(min-width:640px){.phase-strip{grid-template-columns:repeat(3,1fr)}}
.phase{background:var(--card2);border:1px solid var(--line);border-radius:var(--r-md);
  padding:var(--sp-3);text-align:left;min-width:0}
.phase-name{font-size:var(--t-sm);font-weight:600;color:var(--ink)}
/* 2026-09-27 — each kind of retired year carries its timeline colour (app/lifechart.js). */
.phase.phase-gogo{border-top:4px solid var(--green)}
.phase.phase-slowgo{border-top:4px solid var(--gold)}
.phase.phase-nogo{border-top:4px solid var(--blue)}
.phase-range{font-size:var(--t-xs);color:var(--muted);margin-top:2px}
.phase-sent{font-size:var(--t-sm);color:var(--ink2);margin:var(--sp-2) 0 0;line-height:1.5}
.phase-need{font-family:var(--font-num);font-size:var(--t-num-md);font-weight:700;color:var(--ink);
  margin-top:var(--sp-2);font-variant-numeric:tabular-nums;letter-spacing:0}
.phase-need .phase-needlabel{font-family:var(--font-sans);font-size:var(--t-xs);font-weight:400;color:var(--muted)}
.phase-need .phase-needunit{font-family:var(--font-sans);font-size:var(--t-xs);font-weight:400;color:var(--muted);margin-left:2px}
.phase-note{grid-column:1/-1;font-size:var(--t-xs);color:var(--tertiary);margin:0;line-height:1.45}

/* ==== drawdown — FREEDOM-SPEC §F5: the Draw-from column, its flag chips, the Roth-window line
   and the paycheck card's "This month, draw from". Shared by screens/withdraw-years.js and
   withdraw-paycheck.js, so the sheet lives here; each screen's own `css` carries only layout.
   Tokens only; weights 400/600/700; nothing under --t-xs; no opacity. A flag chip is a LABEL
   (span), exempt from the 44px target like span.pill. The draw figures inherit the table cell's
   ink so a survivor (`tr.dead`) row stays muted in this column too. The window line grounds on
   --card2 with a --line2 edge, and its link is --blue, which the matrix measures on --card2. */
.dd-draws{display:flex;flex-direction:column;gap:2px}
.dd-draw{display:flex;align-items:baseline;justify-content:space-between;gap:var(--sp-2);white-space:nowrap}
.dd-bucket{font-size:var(--t-xs);color:var(--muted)}
.dd-draw .num{font-variant-numeric:tabular-nums}
.dd-note{font-size:var(--t-xs);color:var(--tertiary)}
.dd-flags{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}
.dd-flags:empty{display:none}
.dd-flag{display:inline-flex;align-items:center;font-size:var(--t-xs);font-weight:600;line-height:1.4;
  padding:1px var(--sp-1);border-radius:var(--r-sm);border:1px solid var(--line2);background:none;
  color:var(--muted);white-space:nowrap}
.dd-flag.penalty{color:var(--red);border-color:var(--red)}
.dd-flag.irmaa{color:var(--gold);border-color:var(--gold)}
.dd-flag.rmd{color:var(--blue);border-color:var(--blue)}
.dd-flag.ss{color:var(--violet);border-color:var(--violet)}
.dd-flag.window{color:var(--green);border-color:var(--green)}
.dd-window{display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--sp-1) var(--sp-3);margin:0 0 var(--sp-3);
  padding:var(--sp-3);border:1px solid var(--line2);border-radius:var(--r-md);background:var(--card2);
  font-size:var(--t-sm);color:var(--ink2);line-height:1.5}
.dd-window-text{flex:1 1 40ch;min-width:0}
.dd-window .num{font-weight:600;color:var(--ink)}
.dd-window-unit{font-size:var(--t-xs);color:var(--muted)}
.dd-window-link{display:inline-flex;align-items:center;min-height:44px;padding:0 var(--sp-2);font-weight:600;
  color:var(--blue);text-decoration:none;white-space:nowrap}
.dd-window-link:hover,.dd-window-link:focus-visible{color:var(--ink);text-decoration:underline}
.dd-penalty{margin:0 0 var(--sp-3);font-size:var(--t-sm);color:var(--ink2);line-height:1.5;max-width:72ch;white-space:normal}
.dd-penalty .num{font-weight:600;color:var(--ink)}
.dd-legend{margin-top:var(--sp-2);font-size:var(--t-xs);color:var(--tertiary);line-height:1.5}
.dd-legend b{font-weight:600;color:var(--muted)}
.dd-month{margin-top:var(--sp-4);padding-top:var(--sp-3);border-top:1px solid var(--line)}
.dd-month-title{font-size:var(--t-xs);font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);margin-bottom:var(--sp-2)}
.dd-month .dd-draws{gap:4px}
.dd-month .dd-draw{justify-content:flex-start;gap:var(--sp-3);font-size:var(--t-base);color:var(--ink)}
.dd-month .dd-draw .num{font-weight:600}
.dd-month .dd-bucket{font-size:var(--t-sm);min-width:9ch}
.dd-month-unit{font-size:var(--t-xs);color:var(--tertiary);margin-top:var(--sp-1)}
.dd-none{font-size:var(--t-sm);color:var(--muted);margin:0}
@media (max-width:520px){
  .dd-window{flex-direction:column;align-items:flex-start;gap:var(--sp-1)}
  .dd-month .dd-bucket{min-width:0}
}

/* ==== freedom hero — FREEDOM-SPEC §F2 item 1 (app/screens/landing.js, landing-parts.js) =====
   The "Freedom..." sequence: a static word and ONE phrase element whose text a timer swaps every
   PHRASE_MS, faded through `.fh-out` (opacity 0 — removal, not a dim; FADE_MS ≤ 300 ms in
   landing-parts.js), then the finale held for FINALE_MS with the sub-headline revealed beneath.
   Tokens only; three weights; nothing under 12px; no @keyframes and no animation property — the
   one moving thing is the phrase's opacity TRANSITION, which the global reduced-motion sweep
   (line ~1097) collapses to .01ms, and under reduced motion the timer never runs at all.

   THE GEOMETRY IS FIXED AT EVERY WIDTH, which is the structural rule the nugget band broke. The
   h1 is a flex column of the word row and the phrase slot; the slot reserves TWO phrase lines
   below 1100px and one above it (the longest phrase, "...to master a forgotten hobby.", is 31
   characters: ~830px at 56px on the serif, one line at 1100+, two at 390 where the step is 34px),
   and the h1's own min-height is the word row plus that slot, so a phrase that wraps changes
   nothing outside the slot. The finale REPLACES the whole line (the word takes [hidden], the
   phrase carries the finale at the word's size and weight) and centres inside the same box: 68px
   in a 140px box at 1440, two lines of 42px in a 128px box at 390. The sub-headline sits in
   .fh-subslot, whose min-height is reserved from first paint (two lines at --t-lg plus .lp-sub's
   own top margin), so revealing it moves neither the CTA row under it nor anything below.

   Colour: the word inherits --ink from .lp-h1; the phrase takes .lp-h1-a's --gold-display, the
   landing's one licensed large-text accent (3.67:1 on --bg in light at 34-64px, measured as
   large text by tests/theme-contrast.test.mjs), and nothing in this block paints it. */
.fh-seq{display:flex;flex-direction:column;align-items:center;text-align:center;width:100%;
  min-width:0}
.fh-h1{display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:var(--sp-2);width:100%;
  min-height:calc(var(--t-display) * 1.06 + var(--sp-2) + var(--t-3xl) * 1.15 * 2)}
.fh-word{display:block}
/* The phrase: book serif, one step under the word (--t-3xl is clamp 34→56px), weight 400, its
   own two-line slot. text-wrap:balance keeps a wrapped phrase's two lines even. */
.fh-phrase{display:block;font-size:var(--t-3xl);font-weight:400;line-height:1.15;
  letter-spacing:-.01em;min-height:calc(var(--t-3xl) * 1.15 * 2);text-wrap:balance;
  transition:opacity 240ms var(--ease)}
.fh-phrase.fh-out{opacity:0}
/* The finale takes the word's own step and weight — "Freedom to be at Peace." at display size —
   and the slot's floor comes off so it can centre in the box the two rows reserved. */
.fh-h1[data-finale="1"] .fh-phrase{font-size:var(--t-display);font-weight:700;line-height:1.06;
  letter-spacing:-.02em;min-height:0}
/* The sub-headline's reserved slot. .lp-sub (landing.js) sets the type, colour, measure and its
   top margin; this only holds the room for two lines of it from first paint. */
.fh-subslot{display:flex;flex-direction:column;align-items:center;width:100%;
  min-height:calc(var(--sp-4) + var(--t-lg) * 1.45 * 2)}
@media(max-width:767.98px){
  .fh-subslot{min-height:calc(var(--sp-3) + var(--t-md) * 1.45 * 2)}
}
@media(min-width:1100px){
  .fh-phrase{min-height:calc(var(--t-3xl) * 1.15)}
  .fh-h1{min-height:calc(var(--t-display) * 1.06 + var(--sp-2) + var(--t-3xl) * 1.15)}
}

/* ==== choose — FREEDOM-SPEC §F4 (app/screens/choose.js, choose-parts.js) ===================
   The screen exports NO css of its own: every rule for #/choose lives here, scoped to .s-choose,
   the root hook app.js sets on the host. The stop cards are the shared stop-pair block above;
   the phase controls are kit fields and sliders; the Tax Valley is charts.stackedBars with the
   Roth window shaded by charts.css's own .fan-band.fan-outer (measured in both themes with its
   --line2 edge), so this block declares LAYOUT and ink only. Every ink is --ink / --ink2 /
   --muted / --tertiary on the --card ground the column card paints, or --blue for the one link
   (the same paint .dd-window-link uses); borders are --line / --line2; weights 400/600/700;
   nothing under --t-xs; no opacity; 44px on the chips (kit), the link and the segmented. */
.s-choose .layout{grid-template-columns:1fr}
.s-choose .ch-root{display:flex;flex-direction:column;gap:var(--sp-5);min-width:0}
.s-choose .ch-cols{display:grid;grid-template-columns:1fr;gap:var(--sp-4);min-width:0}
@media(min-width:700px){.s-choose .ch-cols{grid-template-columns:1fr 1fr}}
.s-choose .ch-col{display:flex;flex-direction:column;gap:var(--sp-2);min-width:0}
.s-choose .ch-colname{font-size:var(--t-md);font-weight:700;color:var(--ink);margin:0 0 var(--sp-1)}
.s-choose .ch-lbl{font-size:var(--t-sm);font-weight:600;color:var(--ink2)}
.s-choose .ch-why{font-size:var(--t-xs);color:var(--muted);line-height:1.45;margin:0;max-width:62ch}
.s-choose .ch-col .chip-row{margin-top:0}
.s-choose .ch-col .frow{margin-top:0}
.s-choose .ch-col .divider{margin:var(--sp-2) 0}
/* the closed "More" that holds the phase controls (2026-09-26) — a 44px summary row */
.s-choose .ch-more{border-top:1px solid var(--line);margin-top:var(--sp-2)}
.s-choose .ch-more>summary{cursor:pointer;min-height:44px;display:flex;align-items:center;
  font-size:var(--t-sm);font-weight:600;color:var(--muted)}
.s-choose .ch-more>summary:hover{color:var(--ink)}
.s-choose .ch-more>summary::-webkit-details-marker{display:none}
.s-choose .ch-more>summary::before{content:"\25B8";margin-right:var(--sp-2)}
.s-choose .ch-more[open]>summary::before{content:"\25BE"}
.s-choose .ch-more[open]{display:flex;flex-direction:column;gap:var(--sp-2)}
/* the .st header ui.sliderRow emits (label + live value) — styles.css ships .slider-row and
   .slider-val but not this row, so the label and the value would glue together (plan-cashflow
   carries the same local rule). */
.s-choose .slider-row .st{display:flex;align-items:baseline;justify-content:space-between;gap:var(--sp-2);
  font-size:var(--t-sm);font-weight:600;color:var(--muted)}
.s-choose .ch-adopt{display:flex;flex-direction:column;align-items:flex-start;gap:var(--sp-2);
  margin-top:var(--sp-3);padding-top:var(--sp-3);border-top:1px solid var(--line)}
.s-choose .ch-valley .sec-h{flex-wrap:wrap;gap:var(--sp-2)}
.s-choose .ch-valley .seg{margin-left:auto}
/* the chart ground is --card (charts.js CHART_GROUND); the wrap scrolls sideways inside itself
   rather than the page (§3.6). */
.s-choose .tv-wrap{background:var(--card);max-width:100%;overflow-x:auto;margin-top:var(--sp-3)}
.s-choose .tv-legend{display:flex;align-items:center;flex-wrap:wrap;gap:var(--sp-1) var(--sp-2);
  font-size:var(--t-xs);color:var(--muted);margin-top:var(--sp-2)}
.s-choose .tv-legend .num{color:var(--ink2);font-weight:600}
.s-choose .tv-swatch{width:18px;height:12px;flex:none}
.s-choose .tv-unit{font-size:var(--t-xs);color:var(--tertiary);margin:var(--sp-2) 0 0;line-height:1.45}
.s-choose .tv-none{font-size:var(--t-sm);color:var(--muted);margin:var(--sp-3) 0 0;line-height:1.5}
.s-choose .tv-link{display:inline-flex;align-items:center;min-height:44px;margin-top:var(--sp-2);
  padding:0 var(--sp-2);font-size:var(--t-sm);font-weight:600;color:var(--blue);text-decoration:none}
.s-choose .tv-link:hover,.s-choose .tv-link:focus-visible{color:var(--ink);text-decoration:underline;
  text-underline-offset:3px}
.s-choose .tv-link:focus-visible{outline:none;box-shadow:var(--focus-ring)}

/* ==== onboarding pagelet shell — SETTINGS-SPEC v1 §S3 (app/screens/begin.js, start.js,
   start-parts.js) ============================================================================
   #/begin and #/start already ask the right questions in the right order — this section changes
   only the CHROME they render into, from a plain full-bleed page to a small, present, popped-
   open card over a dimmed backdrop. Both screens build this shell exactly ONCE, inside `mount()`
   (`start-parts.js`'s `mountPagelet`, and a smaller copy inline in begin.js — this file is held
   to one importer, and `#/begin` is not it), and every later `render()` writes into
   `.pagelet-card-body` only; the scrim and the card itself are never rebuilt. That is what keeps
   the pop-in below a once-per-visit event instead of something a single keystroke replays.

   TOKENS. `--scrim` is declared per theme in the four token blocks above (§1b/§1c and their two
   overrides), alongside every other themed value, rather than here — nothing in the shipped app
   painted a full-viewport dimmer before this pass. Each theme builds it from ITS OWN darkest
   token (--ink in light, --bg in dark — dark's --ink is the near-white body-text colour, the
   wrong end of the ladder to dim toward) mixed with the `transparent` keyword rather than a
   literal, so it inverts with the theme instead of a fixed hex staying wrong in one of them. It
   does not resolve to a single hex the way --tint-* does (color-mix's second stop is a keyword,
   not a token) and it carries no text of its own, so no pair is ever measured off it — correct,
   not a gap: the whole point of a scrim is that nothing is meant to be read through it. The
   card's own surface is --card, the same token every other card in the product already uses, and
   its shadow is --e-floating, the one the toast already spends — no new elevation tier, because
   §3.4's "one Floating element per view" is a per-VIEW budget and a first-run dialog and a
   transient toast are never the two things on screen at once.

   MOTION. `.pagelet-card--enter` is the pre-paint state — opacity 0, scaled down 4% — added
   before the card is appended and removed two animation frames later so the browser has a real
   first frame to transition FROM (one frame is not always enough: some engines coalesce a class
   add and a same-tick remove into one style recalc and the transition never fires). Both declared
   opacity values are exactly 0 and 1, the two values the opacity sweep (D01, same file) exempts
   as unable to hold readable content at rest, so this motion never touches a token composited
   over its ground the way the retired `.stale` fade did. `prefers-reduced-motion` needs no rule
   here at all: the global sweep at line ~1097 already collapses every transition-duration in the
   document to .01ms, so a reduced-motion visitor gets the settled card with nothing to see move.

   NO CLICK-OUTSIDE DISMISS (§S3, verbatim): the scrim carries no click handler in either screen's
   JS and no close control lives here — this is the required first-run flow, not a dialog a person
   can back out of by accident. Keyboard focus is trapped to the card by each screen's own keydown
   handler (`start-parts.js`'s `trapTab`, and begin.js's smaller copy of the same shape); this
   file only supplies what a trap needs somewhere to land — a card that is reachable, and a scrim
   that is not. */
.pagelet-scrim{position:fixed;inset:0;z-index:250;display:flex;align-items:center;justify-content:center;
  padding:var(--sp-5) var(--sp-4) calc(var(--sp-5) + var(--safe-b));background:var(--scrim);
  overflow-y:auto;-webkit-overflow-scrolling:touch}
.pagelet-card{position:relative;width:100%;max-width:640px;max-height:calc(100dvh - var(--sp-6));
  overflow-y:auto;background:var(--card);border-radius:var(--r-lg);box-shadow:var(--e-floating);
  padding:var(--sp-5);opacity:1;transform:scale(1);
  transition:opacity .22s var(--ease),transform .22s var(--ease)}
.pagelet-card:focus{outline:none}
.pagelet-card.pagelet-card--enter{opacity:0;transform:scale(.96)}
.pagelet-card-body{min-width:0}
/* the sheet's own grabber — phone only, aria-hidden in the markup, pure decoration. --line2 on
   the card's --card ground is >=3.0 on every surface in both themes already (D09, same file). */
.pagelet-handle{display:none}
/* 2026-09-27 — the pagelet's close control, top right of every card. */
.pagelet-top{display:flex;justify-content:flex-end;margin:0 0 var(--sp-2)}
.pagelet-close{display:inline-flex;align-items:center;gap:4px;min-height:44px;padding:0 var(--sp-3);
  background:none;border:1px solid var(--line2);border-radius:var(--r-md);color:var(--muted);
  font:inherit;font-size:var(--t-sm);font-weight:600;cursor:pointer}
.pagelet-close:hover{color:var(--ink);border-color:var(--ink2)}
.pagelet-close:focus-visible{outline:none;box-shadow:var(--focus-ring)}
@media (max-width:600px){
  .pagelet-scrim{align-items:flex-end;padding:var(--sp-3)}
  .pagelet-card{max-width:none;max-height:calc(100dvh - var(--sp-5));
    border-radius:var(--r-lg) var(--r-lg) 0 0;
    padding:var(--sp-4) var(--sp-4) calc(var(--sp-4) + var(--safe-b))}
  .pagelet-handle{display:block;width:36px;height:4px;margin:0 auto var(--sp-3);
    border-radius:var(--r-full);background:var(--line2)}
}

/* 2026-09-26 — the page gutter on the screens that had none (Plan ran edge to edge; Settings,
   Timeline and the sign-in doors sat 0-4px from the edge on a phone). Same measure as Home. */
.s-plan,.s-timeline,.s-account,.s-signin,.s-signup,.s-admin{
  max-width:1080px;margin-left:auto;margin-right:auto;
  padding-left:calc(var(--sp-4) + var(--safe-l));padding-right:calc(var(--sp-4) + var(--safe-r));
  padding-top:var(--sp-4)}
/* Plan's results strip stays in view while the editor scrolls under it (tablet and up; on a
   phone it would cover half the screen). 2026-10-02 — it sticks UNDER the sticky header
   (--hdr-h, published by app.js) and stacks BELOW it (header is z 40): at top:0 / z 50 it slid over
   the header and painted on top of the open Menu panel, which lives inside the header's stacking
   context and so can never out-rank a sibling above 40. */
@media (min-width:768px){ .s-plan .wi-strip{position:sticky;top:var(--hdr-h,72px);z-index:30} }
/* 2026-09-26 — Privacy · Terms under the disclaimer. 44px tall targets, same quiet tone. */
footer.disclaimer .footer-links a{display:inline-flex;align-items:center;min-height:44px;padding:0 var(--sp-2);color:var(--muted);text-decoration:underline;text-underline-offset:3px}
footer.disclaimer .footer-links a:hover{color:var(--ink)}

/* 2026-09-27 — "Edit my answers" above Plan's results strip. */
.s-plan .plan-edit{display:flex;justify-content:flex-end;margin-bottom:var(--sp-2)}

/* 2026-09-29 — the unlock card (app/billing.js): what the feature found, the price, one button */
.unlock{display:flex;flex-direction:column;gap:8px;padding:14px 16px;border:1px solid var(--line2);border-left:4px solid var(--gold);border-radius:var(--r-md);background:var(--card)}
.unlock-t{margin:0;font-weight:700;color:var(--ink)}
.unlock-teaser{margin:0;color:var(--ink);font-weight:600;line-height:1.5}
.unlock-b{margin:0;color:var(--ink2);font-size:var(--t-sm);line-height:1.5}
.unlock .btn{align-self:flex-start}
.unlock-n{margin:0;color:var(--muted);font-size:var(--t-xs)}
.dq-verdict-free{margin:0 0 10px;font-weight:600;color:var(--ink)}

/* 2026-09-29 (owner) — the slogan beside the name, in the header on every page */
.brand-words{display:inline-flex;align-items:baseline;gap:10px;flex-wrap:wrap;min-width:0}
.brand-slogan{font-size:var(--t-sm);color:var(--ink2);letter-spacing:.01em;white-space:nowrap}
.brand-slogan b{color:var(--gold);font-weight:700;letter-spacing:.04em}
@media (max-width:640px){
  .brand-words{flex-direction:column;align-items:flex-start;gap:0}
  .brand-slogan{font-size:var(--t-xs);line-height:1.2}
}

/* 2026-09-29 — the sunset logo (a couple walking at sunset) replaces the gold monogram in the header */
.brand .brand-logo{width:44px;height:44px;flex:none;display:block}
@media (max-width:640px){.brand .brand-logo{width:38px;height:38px}}
@media (max-width:480px){
  .brand{gap:var(--sp-2);min-width:0}
  .brand .brand-logo{width:34px;height:34px}
  .brand-slogan{white-space:normal;max-width:15ch}
}

/* 2026-09-29 (owner) — the header stays while scrolling; the slogan is its centre, as a heading */
header{position:sticky;top:0;z-index:40;background:var(--bg);border-bottom:1px solid var(--line2);
  display:grid !important;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);align-items:center;gap:var(--sp-3);
  max-width:none !important;width:100%;box-sizing:border-box}
header .brand{justify-self:start}
header .header-actions{justify-self:end}
.hdr-slogan{margin:0;justify-self:center;text-align:center;font-family:var(--font-serif);font-weight:400;
  font-size:clamp(17px,2.1vw,26px);line-height:1.2;color:var(--ink);letter-spacing:-.005em;text-wrap:balance}
.hdr-slogan b{color:var(--sunset);font-weight:700;letter-spacing:0}
@media (max-width:860px){
  header{grid-template-columns:minmax(0,1fr) auto;row-gap:2px}
  .hdr-slogan{grid-column:1 / -1;grid-row:2;font-size:17px;padding-bottom:2px}
}
/* 2026-09-29 (owner) — "the header, tagline and logo need to be a little bigger" */
header .brand .brand-logo{width:60px;height:60px}
header .brand h1{font-size:26px}
.hdr-slogan{font-size:clamp(20px,2.6vw,32px)}
@media (max-width:860px){
  header .brand .brand-logo{width:46px;height:46px}
  header .brand h1{font-size:22px}
  .hdr-slogan{font-size:19px}
}
@media (max-width:360px){ .hdr-slogan{font-size:17px} }
/* 2026-09-29 (owner: "alignment of the header to the Menu is messed up") — the bar still spans the
   screen, but its contents line up with the page's 1180px content column the Menu row uses, and the
   slogan takes the flexible middle so links and buttons never run into it. */
header{grid-template-columns:auto minmax(0,1fr) auto !important;
  padding-inline:calc(max(0px, (100% - 1180px) / 2) + var(--sp-4) + var(--safe-l)) calc(max(0px, (100% - 1180px) / 2) + var(--sp-4) + var(--safe-r)) !important}
.hdr-slogan{min-width:0}
.hdr-link{white-space:nowrap}
header .hdr-menu{display:none}   /* the slogan owns the middle; the sections are on the page itself */
@media (max-width:860px){
  header{grid-template-columns:minmax(0,1fr) auto !important}
}

/* 2026-09-29 (owner) — the menu stays with the header, and reads like a regular website menu:
   page links in a row (the current one underlined), no divider; a three-line button on phones. */
#nav{position:sticky;top:var(--hdr-h,72px);z-index:39;background:var(--bg)}
.menubar{padding-block:4px !important}
.topnav-links{display:flex;flex-wrap:wrap;align-items:center;gap:2px}
.topnav-link{position:relative;display:inline-flex;align-items:center;gap:6px;min-height:44px;padding:0 12px;background:none;border:0;
  color:var(--ink2);font:inherit;font-size:var(--t-sm);font-weight:600;cursor:pointer;border-radius:var(--r-sm)}
.topnav-link:hover{color:var(--ink);background:var(--card2)}
.topnav-link.on{color:var(--ink)}
.topnav-link.on{text-decoration:underline;text-decoration-color:var(--sunset);text-decoration-thickness:3px;text-underline-offset:9px}
.topnav-link:focus-visible{outline:none;box-shadow:var(--focus-ring)}
.nav-dot-inline{width:7px;height:7px;border-radius:50%;background:currentColor;flex:none}
.menu-current{border-left:0 !important;padding-left:0 !important;color:var(--ink)}
.menu-trigger{display:none}
@media (max-width:860px){
  .topnav-links{display:none}
  .menu-trigger{display:inline-flex;border:0;background:none;padding:0 6px}
}
@media (min-width:861px){ .menu-panel{display:none} }

/* 2026-09-29 (owner) — the slogan centred over the page: equal side columns; the account corner and
   the homepage's buttons share the right one. The first menu link lines up with the logo. */
header{grid-template-columns:minmax(0,1fr) auto minmax(0,1fr) !important}
header > .header-actions, header > .header-account{grid-column:3;grid-row:1;justify-self:end;display:flex;align-items:center;gap:var(--sp-2)}
header > .header-account:empty{display:none}
.topnav-links{margin-left:-12px}
@media (max-width:860px){
  header{grid-template-columns:minmax(0,1fr) auto !important}
  header > .header-actions, header > .header-account{grid-column:2}
}
/* 2026-09-29 (owner: "all input boxes have a weird shadow") — no glow on focus (a crisp border
   instead) and no browser autofill tint */
.field:focus-within,.field.focus{box-shadow:0 0 0 1px var(--gold) !important}
input:-webkit-autofill,input:-webkit-autofill:hover,input:-webkit-autofill:focus{
  -webkit-box-shadow:0 0 0 1000px var(--card2) inset !important;-webkit-text-fill-color:var(--ink) !important;transition:background-color 9999s ease-out 0s}

/* 2026-09-29 (owner) — the drop-down "Menu" sits beside the logo in the one pinned header */
#nav{display:none}
.hdr-left{display:flex;align-items:center;gap:var(--sp-3);min-width:0;justify-self:start}
.header-menu .menu-trigger{display:inline-flex !important;border:1px solid var(--line2);background:var(--card);padding:0 var(--sp-3)}
.header-menu .menu-panel{display:flex !important}
.header-menu:empty{display:none}
@media (max-width:860px){
  .header-menu .menu-label{display:none}
  .header-menu .menu-trigger{padding:0 10px}
  .hdr-left{gap:var(--sp-2)}
}
/* 2026-09-29 — the left side (logo, name, Menu) and the account corner take what they need; the
   slogan fills the middle, centred in it, so nothing overlaps or clips. */
header{grid-template-columns:auto minmax(0,1fr) auto !important}
.header-account .authchip,.header-account .authrow{white-space:nowrap;flex:none;max-width:none}
@media (max-width:860px){
  header{grid-template-columns:minmax(0,1fr) auto !important}
  .header-menu .menu-panel{position:fixed;left:12px;right:12px;top:calc(var(--hdr-h,110px) + 4px);min-width:0}
}

/* 2026-09-29 (owner: "keep the Menu at the top right") — the right side holds the homepage buttons or
   the account control, then the Menu; its list opens from the right edge. */
.hdr-right{grid-column:3;grid-row:1;justify-self:end;display:flex;align-items:center;gap:var(--sp-2);min-width:0}
.hdr-right > .header-actions,.hdr-right > .header-account{display:flex;align-items:center;gap:var(--sp-2)}
.header-menu .menu-panel{left:auto;right:0}
@media (max-width:860px){ .hdr-right{grid-column:2} }

/* 2026-09-30 (owner) — Ask R: a floating button, bottom right, on every app screen */
.ask-fab{position:fixed;right:calc(18px + var(--safe-r, 0px));bottom:calc(18px + var(--safe-b, 0px));z-index:60;display:inline-flex;align-items:center;gap:8px;padding:12px 18px;border:0;border-radius:var(--r-full);background:var(--sunset);color:var(--ink-on-gold);font:inherit;font-size:var(--t-sm);font-weight:700;cursor:pointer;box-shadow:var(--shadow)}
.ask-fab:hover{filter:brightness(1.05)}
.ask-fab:focus-visible{outline:none;box-shadow:var(--focus-ring)}
@media (max-width:560px){.ask-fab{padding:12px;right:calc(12px + var(--safe-r, 0px));bottom:calc(12px + var(--safe-b, 0px))}.ask-fab-t{display:none}}
@media print{.ask-fab{display:none}}

/* 2026-09-30 — Choose: Plan A is the plan's own age, said under the title */
.s-choose .ch-same{margin:0;font-size:var(--t-sm);line-height:1.5;color:var(--ink2);max-width:70ch}

/* 2026-09-30 (owner) — the overall status light beside a page title (Cash flow, Health care) */
.st-head{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:10px 16px}
.st-head > :first-child{flex:1;min-width:0}
.st-pill{display:inline-flex;align-items:center;gap:8px;padding:8px 14px;border-radius:var(--r-full);font-size:var(--t-sm);font-weight:700;color:var(--ink);background:var(--card);border:1px solid var(--line2);white-space:nowrap}
.st-dot{width:16px;height:16px;border-radius:var(--r-full);flex:none}
.st-pill.green{background:var(--tint-green)}
.st-pill.green .st-dot{background:var(--green)}
.st-pill.orange{background:var(--tint-gold)}
.st-pill.orange .st-dot{background:var(--sunset)}
.st-pill.red{background:var(--tint-red)}
.st-pill.red .st-dot{background:var(--red)}

/* ==== 2026-09-30 (owner: "the site is dull, the menu is dull: make it polished and ready to pay
   for") — one finishing pass over the chrome and the surfaces. Tokens only. ==================== */

/* the header: a light, raised bar that the page scrolls under */
header{background:color-mix(in srgb, var(--bg) 86%, transparent);
  -webkit-backdrop-filter:saturate(1.3) blur(12px);backdrop-filter:saturate(1.3) blur(12px);
  border-bottom:1px solid var(--line);box-shadow:var(--e-raised)}
header .brand h1{font-weight:600;letter-spacing:-.01em}

/* header controls: pills */
.hdr-right .btn,.hdr-right .authchip,.header-account button,.header-menu .menu-trigger{border-radius:var(--r-full) !important}
.header-menu .menu-trigger{border:1px solid var(--line) !important;background:var(--card) !important;box-shadow:var(--e-raised);
  min-height:44px;padding:0 16px 0 14px !important;gap:8px;font-weight:600;transition:border-color .15s var(--ease), box-shadow .15s var(--ease)}
.header-menu .menu-trigger:hover,.header-menu .menu-trigger[aria-expanded="true"]{border-color:var(--ink2) !important}
.header-menu .menu-caret{font-size:var(--t-xs);color:var(--muted)}

/* the drop-down: a proper panel — icon tiles, a line on what each page is for */
.header-menu .menu-panel{width:360px;max-width:calc(100vw - 24px);max-height:calc(100vh - var(--hdr-h,90px) - 24px);overflow-y:auto;
  padding:8px;gap:2px;border:1px solid var(--line);border-radius:18px;background:var(--card);box-shadow:var(--e-floating);
  flex-direction:column;opacity:1;transform:none;transition:opacity .16s var(--ease), transform .16s var(--ease)}
@starting-style{ .header-menu .menu-panel{opacity:0;transform:translateY(-6px)} }
.header-menu .menu-item{min-height:56px;padding:8px 10px;gap:12px;border-radius:12px;align-items:center;text-align:left;color:var(--ink)}
.header-menu .menu-item:hover{background:var(--card2)}
.header-menu .menu-item.on{background:var(--tint-gold)}
.header-menu .menu-item.on::before{display:none}
.header-menu .menu-item .nav-icon{width:36px;height:36px;padding:8px;box-sizing:border-box;border-radius:10px;background:var(--card2);color:var(--ink)}
.header-menu .menu-item.on .nav-icon{background:var(--sunset);color:var(--ink-on-gold)}
.menu-text{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1}
.menu-text .nav-label{font-size:var(--t-sm);font-weight:600;color:var(--ink)}
.menu-desc{font-size:var(--t-xs);color:var(--muted);line-height:1.35;white-space:normal}
.menu-sep{height:1px;background:var(--line);margin:6px 10px}
@media (max-width:860px){
  .header-menu .menu-panel{width:auto}
}

/* surfaces: softer edges and a little depth */
.card{border-color:var(--line);box-shadow:var(--e-raised)}
.s-health .hy-year,.s-cashflow .cf-year,.s-roadmap .rm-card,.s-roadmap .rm-hero,.s-wishes .wl-sec,
.s-todo .td-year,.s-cashflow .cf-detail,.s-timeline .tl-list{border-color:var(--line);box-shadow:var(--e-raised);border-radius:16px}
.s-health .hy-year,.s-cashflow .cf-year{border-left-width:6px}

/* buttons: clean white secondaries that lift on hover; a confident primary */
.btn{border-radius:12px;background:var(--card);border-color:var(--line2)}
.btn:hover{background:var(--card);border-color:var(--ink);box-shadow:var(--e-raised)}
.btn.primary{box-shadow:var(--e-raised)}
.btn.primary:hover{box-shadow:var(--e-floating)}

/* page titles: a real title, not a section heading */
.scr-head .sec-h h2{font-size:clamp(26px,3.2vw,36px);font-weight:600;letter-spacing:-.015em;line-height:1.15}
.scr-head .sec-sub{font-size:var(--t-base);max-width:62ch}

/* focus moved to the page by the router is not a keyboard focus ring */
main:focus,[tabindex="-1"]:focus{outline:none;box-shadow:none}

/* the page ground: a faint warm wash at the top, so the white cards sit on something */
body{background:var(--bg)}

/* 2026-09-30 — the header Upgrade pill (signed in, payments on, not paid yet) */
.authchip.upgrade{background:var(--sunset);border-color:var(--sunset);color:var(--ink-on-gold);font-weight:700}
.authchip.upgrade:hover{filter:brightness(1.06)}
@media (max-width:560px){ .authchip.upgrade{display:none} }

/* 2026-09-30 (owner: "colour the menu icons") — each page its own accent, on a tint of it */
.header-menu .menu-item .nav-icon{color:var(--mi, var(--ink));background:color-mix(in srgb, var(--mi, var(--ink)) 14%, var(--card))}
.header-menu .menu-item.on .nav-icon{background:var(--mi, var(--sunset));color:var(--card)}
.mi-home{--mi:var(--sunset)}
.mi-cashflow{--mi:var(--green)}
.mi-health{--mi:var(--red)}
.mi-wishes{--mi:var(--violet)}
.mi-housing{--mi:var(--gold)}
.mi-roadmap{--mi:var(--blue)}
.mi-survivor{--mi:var(--s-ss)}
.mi-todo{--mi:var(--green)}
.mi-onepage{--mi:var(--s-mid)}
.mi-choose{--mi:var(--blue)}
.mi-timeline{--mi:var(--gold)}
.mi-withdraw{--mi:var(--s-salary)}
.mi-plan{--mi:var(--ink2)}
.mi-decide{--mi:var(--violet)}
.mi-account{--mi:var(--muted)}
.mi-admin{--mi:var(--red)}

/* 2026-09-30 (owner) — Optimize beside the answer's headline */
.sim-a-head{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px 16px}
.sim-a-head .sim-a-big{flex:1 1 320px;min-width:0}
.sim-a-head .btn{flex:none}

/* 2026-09-30 (owner: "on a phone the header name and caption are covered by Sign in and Get
   started") — on a phone the homepage header keeps one button (Sign in / Sign out); the hero's own
   big button starts a plan. The brand shrinks rather than slide under the buttons. */
.hdr-left{min-width:0}
header .brand{min-width:0}
header .brand h1{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@media (max-width:560px){
  #header-actions .btn.primary{display:none}
  .hdr-right{gap:6px}
  .hdr-right .btn{padding:0 12px}
}

/* 2026-09-30 — the homepage header's Pricing link (wide screens only) */
.hdr-pricing{display:inline-flex;align-items:center;min-height:44px;padding:0 10px;background:none;border:0;font:inherit;font-size:var(--t-sm);font-weight:600;color:var(--ink);cursor:pointer;border-radius:var(--r-full)}
.hdr-pricing:hover{background:var(--card2)}
@media (max-width:860px){ .hdr-pricing{display:none} }
