/* ══ SELF-HOSTED WEB FONTS (security lockdown 2 · audit I1, 2026-09-03) ═══════════════════════════
 * Hearth used to pull these faces from fonts.googleapis.com / fonts.gstatic.com. That was the ONE
 * third-party origin in the whole client — the single reason the CSP could not be `self` for styles
 * and fonts, and the reason a FIRST load was not offline-capable. The exact woff2 files Google was
 * serving are now in `web/fonts/` (provenance + the request that produced them: web/fonts/README.md),
 * and the block below is Google’s own generated CSS with the urls repointed and nothing else changed:
 * the same 20 faces, the same weights, the same per-face `unicode-range`, the same `font-display: swap`.
 * Keeping the ranges matters — dropping them would make every page download the latin-ext file it does
 * not need, which is the opposite of what self-hosting is for.
 * ⚠ These files are in the service worker’s SHELL list. If you add, remove or re-version a face here,
 * add, remove or re-version it there in the same edit, or the SW precache will 404 and INSTALL WILL
 * FAIL — which takes the whole offline shell with it.
 */
/* latin-ext */
@font-face {
  font-family: 'DM Mono';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url(../fonts/dmmono-v16-aFTR7PB1QTsUX8KYvrGyEY2tbZX9.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'DM Mono';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url(../fonts/dmmono-v16-aFTR7PB1QTsUX8KYvrGyEYOtbQ.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
  font-family: 'DM Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(../fonts/dmmono-v16-aFTU7PB1QTsUX8KYthSQBLyM.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'DM Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(../fonts/dmmono-v16-aFTU7PB1QTsUX8KYthqQBA.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
  font-family: 'DM Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(../fonts/dmmono-v16-aFTR7PB1QTsUX8KYvumzEY2tbZX9.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'DM Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(../fonts/dmmono-v16-aFTR7PB1QTsUX8KYvumzEYOtbQ.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url(../fonts/dmsans-v17-rP2Yp2ywxg089UriI5-g4vlH9VoD8Cmcqbu6-K6h9Q.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url(../fonts/dmsans-v17-rP2Yp2ywxg089UriI5-g4vlH9VoD8Cmcqbu0-K4.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(../fonts/dmsans-v17-rP2Yp2ywxg089UriI5-g4vlH9VoD8Cmcqbu6-K6h9Q.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(../fonts/dmsans-v17-rP2Yp2ywxg089UriI5-g4vlH9VoD8Cmcqbu0-K4.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(../fonts/dmsans-v17-rP2Yp2ywxg089UriI5-g4vlH9VoD8Cmcqbu6-K6h9Q.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(../fonts/dmsans-v17-rP2Yp2ywxg089UriI5-g4vlH9VoD8Cmcqbu0-K4.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(../fonts/dmsans-v17-rP2Yp2ywxg089UriI5-g4vlH9VoD8Cmcqbu6-K6h9Q.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(../fonts/dmsans-v17-rP2Yp2ywxg089UriI5-g4vlH9VoD8Cmcqbu0-K4.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* greek */
@font-face {
  font-family: 'Syne';
  font-style: normal;
  font-weight: 700;
  font-display: block;   /* the wordmark must never paint in a fallback and resize (2026-09-07) */
  src: url(../fonts/syne-v24-8vIH7w4qzmVxm2NL9Hz_.woff2) format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* latin-ext */
@font-face {
  font-family: 'Syne';
  font-style: normal;
  font-weight: 700;
  font-display: block;   /* the wordmark must never paint in a fallback and resize (2026-09-07) */
  src: url(../fonts/syne-v24-8vIH7w4qzmVxm25L9Hz_.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Syne';
  font-style: normal;
  font-weight: 700;
  font-display: block;   /* the wordmark must never paint in a fallback and resize (2026-09-07) */
  src: url(../fonts/syne-v24-8vIH7w4qzmVxm2BL9A.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* greek */
@font-face {
  font-family: 'Syne';
  font-style: normal;
  font-weight: 800;
  font-display: block;   /* the wordmark must never paint in a fallback and resize (2026-09-07) */
  src: url(../fonts/syne-v24-8vIH7w4qzmVxm2NL9Hz_.woff2) format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* latin-ext */
@font-face {
  font-family: 'Syne';
  font-style: normal;
  font-weight: 800;
  font-display: block;   /* the wordmark must never paint in a fallback and resize (2026-09-07) */
  src: url(../fonts/syne-v24-8vIH7w4qzmVxm25L9Hz_.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Syne';
  font-style: normal;
  font-weight: 800;
  font-display: block;   /* the wordmark must never paint in a fallback and resize (2026-09-07) */
  src: url(../fonts/syne-v24-8vIH7w4qzmVxm2BL9A.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* The wordmark's STAND-IN (2026-09-07): a local face scaled so "Tobsy" is the same width in it as in Syne 800
   (measured: Syne 111.23px vs the local face 90.77px at 153% → 187.5%; local('Arial') matches the regular face and synthesises the bold, which is why it is not the naive 153%). With font-display: block the stand-in is never SEEN, but its box
   is what the header lays out against for the few ms before Syne arrives — matched, so nothing beside the
   name can jump. Arial/Helvetica/Roboto have near-identical advance widths for these letters. */
@font-face {
  font-family: 'Syne Standin';
  src: local('Arial'), local('Helvetica Neue'), local('Roboto'), local('Helvetica');
  size-adjust: 187.5%;
}

/* Hearth styles (static shell, Phase 2). Migrated from user-script/Styles.html — this file is now the source. */
  /* Hearth colour tokens — Bible §24 (exact). */
  :root {
    --gut: 26px;    /* Slice A (ledger panel): the reserved gap the row's flow line keeps before the amount column (human: "26 points") */
    --bg-base: #0c0c12;
    --bg-surface: #13131e;
    --bg-elevated: #1a1a28;
    --bg-input: #1e1e2e;
    --bg-hover: #22223a;
    --border-subtle: #1e1e30;
    --border-default: #2a2a40;
    --border-strong: #3a3a58;
    --text-primary: #f0f0f8;
    --text-secondary: #a0a0c0;
    --text-tertiary: #6a6a88;
    --accent-primary: #7c6aff;          /* purple — brand, household, human moments */
    --accent-glow: rgba(124,106,255,0.18);
    --accent-warm: #e8834a;             /* amber — DESTRUCTIVE actions + error/status feedback (Bible §3). NB: outgoing
                                           MONEY moved to --accent-out (lavender trial, 2026-06-23) — amber kept here so
                                           Remove/destructive never collides with the purple primary. */
    --accent-out: #da82ff;              /* TRIAL (2026-06-23, human-directed) — outgoing money amounts. Calmer than the
                                           old amber, but COLLIDES with purple (brand/household/primary); revisit (maybe
                                           pink). See tone-and-language.md. Was amber #e8834a. */
    --accent-teal: #3ecfaa;             /* teal — income, positive, goals */
    --accent-red: #f06070;              /* red — genuine alerts ONLY */
    --accent-gold: #e8c547;             /* gold — milestones */
    /* ── THE PERSON / BUCKET IDENTIFIER PALETTE (human-ratified 2026-08-13, from the colour-ideas mock
       `dist-live/mocks/color-ideas.html`) ──────────────────────────────────────────────────────────────
       SIX identifier-ONLY hues, in this exact slot order. They are NEVER semantic: an identifier says
       "this is Dev's bar", never "this is income" or "this is spending". This RETIRES the old arrangement
       where the money colours (teal / lavender --accent-out / gold) doubled as person colours — the human's
       reason being that a person wearing income-teal is unreadable next to an income series. The old
       id4/id5/id6 hexes (#3ecc3e grass · #c87eaf rose · #a5998d taupe) are retired with it; rose survives
       the cull but MOVES from slot 5 to slot 6.
       MACHINE-VALIDATED on the dark surface #13131e: worst adjacent pair ΔE 16.6 under protanopia and
       ΔE 22.9 under normal vision, every slot ≥3:1 contrast against the surface.
       ⚠ THE SKY SLOT IS AN EXPLICIT HUMAN-RATIFIED EXCEPTION TO THE APP'S NO-BLUE RULE (CLAUDE.md
       non-negotiable #4 / Bible §24). It is legal ONLY as a person/bucket IDENTITY colour and must never
       be used semantically — no money amount, no state, no chrome ever wears it. */
    --accent-id1: #2e7d4f;              /* forest — identifier only */
    --accent-id2: #5da9e8;              /* sky — THE one sanctioned blue, identity-only (see the note above) */
    --accent-id3: #eda57e;              /* peach — identifier only */
    --accent-id4: #a5486e;              /* berry — identifier only */
    --accent-id5: #b8c92e;              /* chartreuse — identifier only */
    --accent-id6: #c87eaf;              /* rose — identifier only (was id5 before 2026-08-13) */
    /* THE HOUSEHOLD PAIR (same ratification). A household compared alongside people is not a seventh
       person — it wears its own validated pair (ΔE 18 apart under CVD) so "the two of us together" reads
       as a different KIND of row. At most two households can exist, hence exactly two tokens. */
    --accent-hh1: #9d8cff;              /* periwinkle — household identity only */
    --accent-hh2: #6a4fc9;              /* deep violet — household identity only */
    --on-accent-primary: #ffffff;       /* derived: text on purple primary buttons */
    /* CHART INK (v366, SIXTH REVIEW ③ — human-ratified 2026-08-15, "great point there"). Axis labels sat on
       --text-tertiary #6a6a88 over the deep #08080d chart card = 3.83:1, under the 4.5:1 readability line and
       the one place text is small AND on the darkest surface in the app. This is a NEUTRAL TEXT SHADE, not a
       meaning-bearing colour — it says nothing about income, spending or state (Bible §3 is untouched). It sits
       deliberately between tertiary and --text-secondary (#a0a0c0, 7.78:1) so the axis is readable while still
       reading as quieter chrome than the data: measured 6.38:1 on #08080d. SCREEN ONLY — the printed report has
       its own palette (p.faint on white, already fine) in js/report-analysis.js, which must not follow this.
       v389 — THE TOKEN IS NOW EVERY CHART'S AXIS INK, not Analysis's alone (the human: the drift/usual-ranges
       and forecast day-by-day charts "feel hazy… most information feels hazy"). Measured on the real screens
       before anything moved: those charts sit on a `.card` (--bg-surface #13131e), where --text-tertiary is
       3.54:1 — WORSE than the 3.83:1 that earned this token in the first place. Same ink here reads 5.89:1 on
       that surface. The NAME is left alone deliberately: renaming it would touch .ana-axis, AXIS_FONT_PX and
       report-analysis.js's literal duplicate for no behaviour, and a second token holding the same hex is the
       thing the palette rule exists to stop. Read it as "chart axis ink"; it is not Analysis-only any more. */
    --ana-axis-ink: #8f8fae;
    --on-accent-warm: #1a0f06;          /* derived: dark ink for text on amber buttons (not a semantic colour) */
    /* calendar "paper zones" (§31, Option 2): the PAST vs FUTURE half tones meeting at the "now" seam */
    --cal-zone-past: #191926;           /* PAST/today — a raised, lighter surface (ink; it happened) */
    --cal-zone-past-border: #2e2e46;
    --cal-zone-future: #0e0e16;         /* FUTURE — a sunken, darker surface (pencil; expected) */
    --cal-zone-hatch: rgba(160, 150, 190, 0.24);  /* FUTURE cells' faint diagonal "projected" stroke (§31, ④a) */
    --cal-zone-empty-border: #16161f;   /* empty days recede — transparent bg + this faint border */
    --radius: 14px;
    --gap: 16px;
  }

  * { box-sizing: border-box; }
  /* lock the page to the viewport width: nothing may cause a horizontal PAGE scroll on a phone
     (the nav becomes its own scrollable strip below — the screen itself never pans sideways). */
  /* overflow-x: CLIP (not hidden) still blocks sideways scroll but does NOT create a scroll container, so
     position:sticky descendants (the Bulk live-ledger) keep working. */
  html, body { margin: 0; padding: 0; overflow-x: clip; max-width: 100%;
    /* a horizontal swipe is OURS (swipe between tabs on mobile) — don't let the browser hijack it as a
       back/forward navigation gesture (edge-swipe). Inner scrollers (nav strip, CSV columns) still scroll. */
    overscroll-behavior-x: none;
    /* RESERVE the vertical scrollbar's space permanently (Windows/classic scrollbars take ~15px). Without this the
       centred .app shifts sideways whenever a tab is tall enough to need a scrollbar vs not — and the per-switch
       re-render toggles it = a left/right FLICKER on every tab change (only visible once the frame stopped jumping).
       Overlay scrollbars (mobile) reserve nothing, so phones are unaffected. */
    scrollbar-gutter: stable; }

  /* ── APP-NATIVE SCROLLBARS, EVERYWHERE — STANDING RULE (human-directed 2026-08-26, v406) ──────────────
     Their words, on finding a stock Windows scrollbar under the Intentions "Your picture" card: *"it really
     shouldn't need the scroll bar anyway, and if ever a scroll bar has to come then it has to be app
     native."* Both halves are the rule. The first half is per-bug (kill the overflow — see `.intent-footer`
     below). This is the second half, and it is a DEFAULT rather than a list, because a list is a thing you
     forget to add to: `scrollbar-width` and `scrollbar-color` are INHERITED properties, so setting them on
     the root dresses every scroll container the app will ever grow, including ones nobody remembered.
     The webkit pseudo-elements do NOT inherit, so they get one universal rule beside it.
     WHAT THIS DELIBERATELY DOES NOT TOUCH: a scrollbar that is hidden ON PURPOSE (`.nav`, the analysis
     shape strip) or dimmed-until-hover on purpose (`.ledger-side`, the CSV column lists) sets its own
     value with a CLASS selector, which outranks both rules here — so those stay exactly as they were.
     Every one was checked before this block was written; do the same before adding another. */
  html { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
  *::-webkit-scrollbar { width: 10px; height: 10px; }
  *::-webkit-scrollbar-track { background: transparent; }
  *::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 999px; }
  *::-webkit-scrollbar-thumb:hover { background: var(--text-tertiary); }
  *::-webkit-scrollbar-corner { background: transparent; }

  body {
    background: var(--bg-base);
    color: var(--text-primary);
    font-family: 'DM Sans', system-ui, sans-serif;
    font-weight: 400;
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
    padding-bottom: env(safe-area-inset-bottom);
  }
  /* ONE fixed app FRAME at every tab (human-ratified: cap 1500, adaptive). It fills the window up to the cap,
     then centres so a big monitor/TV gets calm gutters instead of a sprawled app. The nav/header span this frame
     and never jump between tabs — only the CONTENT (.view) re-caps per screen. Never binds on a phone. */
  .app { width: min(100%, 1500px); margin: 0 auto; padding: 24px 20px 64px; }
  /* CONTENT width per screen, centred inside the steady frame. Forms read best as a calm narrow column; data
     screens use more (.app--wide); dense grids fill the frame (.app--bulk/.app--csv, below). This is the ONLY
     thing that changes between tabs — the frame above stays put. (conventions.md HYBRID rule, now frame-steady.) */
  .view { max-width: 680px; margin: 0 auto; }
  .app--wide .view { max-width: 1100px; }
  /* MOBILE swipe between tabs: new content glides in from the swiped-toward edge so the gesture feels native
     instead of snapping (the human's ask). transform + opacity only (compositor-cheap); the page's overflow-x:clip
     hides the brief off-screen slide so no horizontal scrollbar appears. A tab CLICK stays instant (no class added). */
  @keyframes hearthViewNext { from { opacity: 0; transform: translateX(26px); } to { opacity: 1; transform: none; } }
  @keyframes hearthViewPrev { from { opacity: 0; transform: translateX(-26px); } to { opacity: 1; transform: none; } }
  .view--enter-next { animation: hearthViewNext 210ms cubic-bezier(0.22, 0.61, 0.36, 1); }
  .view--enter-prev { animation: hearthViewPrev 210ms cubic-bezier(0.22, 0.61, 0.36, 1); }
  @media (prefers-reduced-motion: reduce) { .view--enter-next, .view--enter-prev { animation: none; } }
  /* ── v417 ⑦ — AN ENTER ANIMATION IS A TRANSFORM, AND A TRANSFORM RE-PARENTS EVERY FIXED CHILD ───────
     The human: arriving at Ledger or Analysis BY SWIPE flashes the side edge-tabs as a full-height line
     for a frame before they settle; arriving by tab CLICK never does. Both halves of that are exactly
     right, and the reason is the animation above.

     MEASURED (poc/_probe-v417.mjs swipe, a per-frame recorder over a real CDP touch-swipe). During the
     210ms slide, `#view` carries a transform — which makes it the containing block for every
     `position: fixed` DESCENDANT. The Ledger's slide-in panel is `top:0; bottom:0; translateX(-103%)`,
     so against a 1808px-tall `#view` instead of the 844px viewport it became a 320 x 1808 slab whose
     right edge sat at x = +31: a 31px-wide, taller-than-the-screen strip of panel down the left margin.
     Its edge tab travelled with it to y = 896, off the bottom. At the animation's end the transform
     goes and everything snaps to where it belongs. Frame log, shipped tree:
        t=9529  ledger-side@-289,119 320x1808   ledger-tab@40,896
        t=9720  (no transform) ledger-side@-330,0 320x844   ledger-tab@0,334
     A CLICK adds no class, so there is no transform and no re-parenting — hence "tab-click never does".

     THE FIX IS THE ONE THE SYMPTOM ASKS FOR: an edge tab is invisible until it is positioned. For the
     210ms the containing block is wrong, these overlays stand down; when the animation ends they are
     already where they belong, so they appear correct rather than settling. `visibility` (not
     `display`, not `opacity`) so nothing is re-laid-out and nothing fades — the reader simply never
     sees the misplaced frame.

     THE LIST IS EXHAUSTIVE AND IT IS ASSERTED, not hoped: seven elements, on two screens, are every
     `position: fixed` descendant `#view` holds on a normal render across all ten views (measured, same
     probe — every other fixed thing in the app is a popover, tip, modal or toast, none of which can
     exist during a 210ms gesture animation, and the ⛶ overlay is `display: none` until opened).
     verify-v417-polish ⑦ re-enumerates them at runtime and fails if a new one appears uncovered.
     ⚠ STANDING RULE for the next `position: fixed` overlay that lives inside `#view`: add it here. */
  .view--enter-next .ledger-side, .view--enter-prev .ledger-side,
  .view--enter-next .ledger-tab, .view--enter-prev .ledger-tab,
  .view--enter-next .ledger-scrim, .view--enter-prev .ledger-scrim,
  .view--enter-next .ana-lpanel, .view--enter-prev .ana-lpanel,
  .view--enter-next .ana-lpanel-handle, .view--enter-prev .ana-lpanel-handle,
  .view--enter-next .ana-tpanel, .view--enter-prev .ana-tpanel,
  .view--enter-next .ana-tpanel-handle, .view--enter-prev .ana-tpanel-handle,
  /* v423 — the Settings/Help thumb-bar joins the list under the standing rule above. It is
     `position: fixed; bottom: 0` inside #view, so during the 210ms transform it would resolve against
     the full height of the settings content and paint its bar somewhere down the page instead of on
     the glass. Its sheet and scrim are its CHILDREN, so one entry covers all three. */
  .view--enter-next .snav, .view--enter-prev .snav { visibility: hidden; }
  /* ── v378 — THE HEADER'S POPUPS OUTRANK CONTENT-LEVEL PINNED BARS ──────────────────────────────────
     The human's screenshot: on the phone Ledger, the open ACTIVE-CONTEXT dropdown was painted over by the
     pinned strip — "Dev" half-hidden under "320 shown of 1,393". `.dropdown__list` carries z-index 30 and
     the header created NO stacking context, so it competed at the root against `.ledger-strip` (sticky,
     z-index 30) and lost on DOM order. This is a CLASS, not one screen: `.cal-sticky` pins at 40 on the
     Calendar and would have done exactly the same. One stacking context on the header settles all of them.
     50 is deliberately chosen: ABOVE every content-level pinned bar (30/40), and BELOW the ledger panel /
     tab / scrim (61/60/59 — which are full-height overlays that SHOULD cover the header) and below every
     modal and popover layer (100 / 200 / 1200). Every one of those orderings is what it already was. */
  .app__header { display: flex; align-items: center; gap: 12px 16px; margin-bottom: 24px; flex-wrap: wrap;
    position: relative; z-index: 50; }
  .brand { font-family: 'Syne', 'Syne Standin', sans-serif; font-weight: 800; font-size: 28px; letter-spacing: -0.01em; margin: 0;
    color: var(--accent-primary); }   /* purple = brand colour (Bible §3) */
  /* the bear beside the wordmark (2026-09-07). One flex lock so the mark and the name move as a unit and the
     rest of the header line (picker, bell) measures against the same box it always did. */
  .brandlock { display: flex; align-items: center; gap: 10px; min-width: 0; }
  .brandmark { width: 34px; height: 40px; flex: none; display: block; }   /* the bear alone, no tile: its own box is 352×412 (mark PS, pocket included) */

  /* header right cluster: active-context dropdown + nav */
  .app__headerright { display: flex; align-items: center; gap: 16px; margin-left: auto; flex-wrap: wrap; }
  .app__headerright .dropdown { width: auto; }
  .app__headerright .dropdown__trigger { padding: 8px 10px; font-size: 14px; }
  .app__headerright .dropdown__list { min-width: 180px; }
  /* v395 — THE CONTEXT CHOOSER'S MENU OPENS LEFTWARD, FROM ITS OWN RIGHT EDGE.
     The shared `.combo__list` sets `left: 0; right: 0`, and with `min-width: 180px` the over-constrained
     box resolves in LTR by ignoring `right` — so the menu grew RIGHTWARD from the trigger's left edge.
     Under a SHORT name ("Dev", a 69px trigger sitting 62px from the frame's right edge) that put its right
     side at 439px on a 390px phone: 69px of the profile list hanging off the screen, silently CLIPPED
     rather than scrolled because the page is `overflow-x: clip`. It is the tap you make to switch person,
     so it is the tap that showed it. `left: auto` hands the anchoring to `right: 0` — the menu now hangs
     from the same edge the control does, which is what a right-side control's menu should always have
     done — and the viewport clamp is the belt beside that brace for any future wider list. */
  .ctxslot .dropdown__list { left: auto; right: 0; max-width: calc(100vw - 24px); }
  .ctxslot { display: flex; align-items: center; gap: 8px; }
  .ctxslot__label { font-size: 12px; color: var(--text-tertiary); text-transform: uppercase; letter-spacing: 0.06em; }
  .nav { display: flex; gap: 4px; }
  .nav__btn {
    appearance: none; background: none; border: 1px solid transparent; cursor: pointer; white-space: nowrap;
    color: var(--text-secondary); font-family: inherit; font-size: 14px; padding: 6px 12px; border-radius: 8px;
  }
  /* ⚠ v421 ③ — THE DOUBLE-LIT TAB PILL (BUG, human-screenshot 2026-09-01; root-caused first-hand).
   * Their screenshot: Calendar open, and Calendar AND Goals & Plans both wearing a pill. It was not
   * the active class and it was not a focus style — MEASURED on a 390px touch page: tap Calendar,
   * swipe to Goals & Plans, and the bar reads
   *     calendar   active:false  hover:TRUE  bg #22223a
   *     goalsplans active:true   hover:false bg #1a1a28
   * A touch screen has no pointer to move away, so Chrome leaves `:hover` STUCK on the last thing
   * tapped; a swipe moves the active class and nothing clears the hover. Worse, `--bg-hover` is
   * LIGHTER than `--bg-elevated`, so the stale chip reads MORE lit than the real one — which is why
   * the human read the wrong tab as current.
   * THE CUT: hover is a POINTER state, so it is stated only where a pointer exists. The pill then
   * means exactly one thing — the current view — and nothing else paints a filled chip.
   * Keyboard users lose nothing: they get their own RING (never the pill), so "where am I" and
   * "where is the caret" stay two different marks. */
  @media (hover: hover) {
    .nav__btn:hover { color: var(--text-primary); background: var(--bg-hover); }
  }
  .nav__btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-glow); }
  .nav__btn--active { color: var(--text-primary); background: var(--bg-elevated); border-color: var(--border-default); }

  /* calm offline indicator (slim, neutral — not amber/red; this is information, not alarm) */
  .offlinebar {
    background: var(--bg-elevated); border: 1px solid var(--border-default); color: var(--text-secondary);
    border-radius: 10px; padding: 8px 12px; margin-bottom: var(--gap); font-size: 13px; text-align: center;
  }

  /* profiles — the 6-slot pool */
  .slot {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    background: var(--bg-surface); border: 1px solid var(--border-subtle);
    border-radius: 10px; padding: 14px 16px; margin-bottom: 10px;
  }
  .slot--empty { border-style: dashed; border-color: var(--border-default); }
  .slot--confirm { border-color: var(--border-strong); background: var(--bg-elevated); flex-wrap: wrap; }
  .slot__name { font-size: 16px; font-weight: 500; }
  .slot__confirm { display: flex; flex-direction: column; gap: 2px; flex: 1 1 240px; }
  .slot__actions { display: flex; gap: 8px; align-items: center; }
  .slot__add { display: flex; gap: 8px; align-items: center; flex: 1; }
  .slot__add input { flex: 1; min-width: 0; }

  /* NB: ALL .btn--* modifiers live AFTER the base .btn (below) so they actually win — .btn--sm sat
     here (before .btn) and was silently overridden: every "small" button rendered FULL-SIZE. */

  /* "＋ Add someone" — reveals the next empty seat (progressive, not all 6 at once) */
  .addbtn {
    appearance: none; width: 100%; cursor: pointer;
    background: none; border: 1px dashed var(--border-default); color: var(--text-secondary);
    font-family: inherit; font-size: 14px; padding: 12px; border-radius: 10px; margin-bottom: 10px;
  }
  .addbtn:hover { color: var(--text-primary); background: var(--bg-hover); border-color: var(--border-strong); }

  /* row action icons — calm grey at rest, intent colour on hover (purple = neutral, amber = destructive) */
  .iconbtn {
    appearance: none; background: none; border: none; cursor: pointer;
    color: var(--text-tertiary); padding: 7px; border-radius: 8px; line-height: 0;
    display: inline-flex; align-items: center; justify-content: center;
  }
  .iconbtn:hover { background: var(--bg-hover); color: var(--text-secondary); }
  .iconbtn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-glow); }
  .iconbtn:disabled { opacity: 0.5; cursor: default; }
  .iconbtn svg { display: block; }
  .iconbtn--edit:hover { color: var(--accent-primary); }   /* purple = neutral action */
  .iconbtn--danger:hover { color: var(--accent-warm); }    /* amber = destructive */
  .iconbtn--locked, .iconbtn--locked:hover { color: var(--accent-primary); } /* purple = a PIN is set (the intimate layer) */

  /* modal overlay (reusable) */
  .modal__overlay {
    position: fixed; inset: 0; z-index: 100;
    background: rgba(6,6,12,0.66);
    display: flex; align-items: flex-start; justify-content: center;
    padding: 32px 16px; overflow-y: auto;
  }
  .modal {
    width: 100%; max-width: 420px;
    background: var(--bg-surface); border: 1px solid var(--border-default);
    border-radius: var(--radius); box-shadow: 0 24px 60px rgba(0,0,0,0.5);
  }
  .modal__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 18px 18px 0; }
  .modal__head h2 { margin: 0; }
  .modal__body { padding: 14px 18px 20px; }
  .modal__body .field { margin-bottom: 12px; }
  .modal__body > .btn { width: 100%; margin-top: 4px; }

  /* PIN flows */
  .pin-menu { display: flex; flex-direction: column; gap: 8px; }
  .pin-menu > .btn { width: 100%; }
  .pin-menu--row { flex-direction: row; flex-wrap: wrap; margin-top: 8px; }
  .pin-menu--row > .btn { width: auto; }
  .pin-recovery {
    text-align: center; font-size: 32px; letter-spacing: 0.18em;
    color: var(--accent-primary); background: var(--bg-elevated);
    border: 1px solid var(--border-default); border-radius: 10px; padding: 18px; margin: 6px 0;
  }
  .pin-or { text-align: center; color: var(--text-tertiary); font-size: 13px; margin: 16px 0 8px; }
  .admin-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-top: 4px; }

  /* households */
  .hh-card__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
  .hh-card__head h2 { margin: 0 0 2px; }
  .hh-card__head p { margin: 0; }
  .split__row { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
  .split__name { flex: 1; min-width: 0; color: var(--text-secondary); }
  .split__row input { width: 84px; }
  .split__pct { color: var(--text-tertiary); }
  /* per-category split overrides (Bible §6) */
  .hh-card__ov { margin-top: 4px; }
  .hh-card__customise { margin-top: 8px; padding: 0; }
  .hhcat { max-height: 52vh; overflow-y: auto; margin: 8px 0 4px; border: 1px solid var(--border-default); border-radius: 10px; }
  .hhcat__group { padding: 8px 12px 4px; font-size: 12px; letter-spacing: .04em; text-transform: uppercase; color: var(--text-tertiary); background: var(--bg-elevated); position: sticky; top: 0; z-index: 1; }
  .hhcat-row { display: flex; align-items: center; gap: 10px; padding: 8px 12px; border-top: 1px solid var(--border-default); }
  .hhcat-row__name { flex: 1; min-width: 0; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .hhcat-row__ctrl { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
  .hhcat-row__base { color: var(--text-tertiary); }
  .hhcat-row--on { background: var(--accent-glow); }
  .hhcat-mini { width: 54px; padding: 6px 8px; }
  .hhcat-mini__lbl { color: var(--text-tertiary); font-size: 13px; }
  /* PROPORTIONAL_INCOME notable-swing prompt (§6) */
  .hh-prop-swing { margin-top: 12px; padding: 12px; border: 1px solid var(--accent-primary); border-radius: 10px; background: var(--accent-glow); }
  .hh-prop-swing__msg { margin: 0 0 10px; color: var(--text-primary); }
  .hh-prop-swing__actions { display: flex; gap: 8px; flex-wrap: wrap; }
  /* pre-save income-split choice (in the create/edit modal): a selectable pair of pills, "use" pre-selected */
  .hh-prop-choice { display: flex; gap: 8px; flex-wrap: wrap; margin: 10px 0 2px; }
  .hh-prop-choice__opt { appearance: none; font-family: inherit; font-size: 14px; cursor: pointer;
    padding: 9px 14px; border-radius: 999px; border: 1px solid var(--border-default);
    background: var(--bg-elevated); color: var(--text-secondary); transition: background .12s, border-color .12s, color .12s; }
  .hh-prop-choice__opt:hover { color: var(--text-primary); background: var(--bg-hover); }
  .hh-prop-choice__opt.is-selected { background: var(--accent-primary); color: var(--on-accent-primary); border-color: var(--accent-primary); }
  @media (max-width: 480px) {
    .hhcat-row { flex-wrap: wrap; }
    .hhcat-row__name { flex: 1 0 100%; white-space: normal; }
  }
  .readonly { padding: 11px 12px; color: var(--text-primary); background: var(--bg-elevated);
    border: 1px solid var(--border-subtle); border-radius: 10px; }

  /* categories */
  .cat-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin: 2px 0 16px; }
  .cat-tab { appearance: none; background: none; border: 1px solid var(--border-default); color: var(--text-secondary);
    font-family: inherit; font-size: 14px; padding: 8px 14px; border-radius: 999px; cursor: pointer; }
  .cat-tab:hover { color: var(--text-primary); background: var(--bg-hover); }
  .cat-tab--active { color: var(--on-accent-primary); background: var(--accent-primary); border-color: var(--accent-primary); }
  /* groups (headers) read larger; categories sit indented beneath them for a clear hierarchy */
  .cat-header__line { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 8px; }
  .cat-header__namewrap { display: flex; align-items: center; gap: 8px; min-width: 0; }
  .cat-header__name { font-size: 20px; margin: 0; }
  .cat-lock { display: inline-flex; color: var(--accent-primary); }
  .cat-lock svg { display: block; }
  .cat-locked { display: flex; align-items: center; gap: 12px; padding: 8px 2px 2px; }
  .cat-locked p { margin: 0; flex: 1; }
  .cat-row { display: flex; align-items: center; justify-content: space-between; gap: 12px;
    padding: 9px 2px 9px 14px; border-top: 1px solid var(--border-subtle); }
  .cat-row--edit { padding-left: 2px; }
  .cat-row--managed .slot__name { color: var(--text-secondary); }
  .cat-managed { gap: 8px; }
  .cat-managed__t { font-size: .82rem; }
  .cat-row .slot__add { flex: 1; }
  .cat-inline { flex: 1; display: flex; flex-direction: column; gap: 6px; min-width: 0; }
  .cat-inline__err { font-size: 13px; }
  .cat-addrow { padding-top: 10px; }
  .cat-add { margin-top: 10px; margin-bottom: 0; }
  .cat-archived { margin-top: 8px; padding-top: 6px; }
  .cat-archived__row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 6px 2px; }
  .move-opt { margin-bottom: 16px; }
  .move-opt > .btn { width: 100%; }
  .move-opt p { margin: 6px 0 0; }

  /* change history */
  .history-link { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: var(--gap); }
  .history-filters { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: var(--gap); }
  .history-filters .field { margin-bottom: 0; flex: 1; min-width: 140px; }
  .history-row { padding: 11px 2px; border-top: 1px solid var(--border-subtle); }
  .history-row:first-child { border-top: none; }
  .history-row__desc { font-size: 15px; color: var(--text-primary); }
  .history-row__meta { display: flex; justify-content: space-between; gap: 12px; margin-top: 3px; }
  .history-row__who { font-size: 13px; color: var(--text-secondary); }
  .history-row__when { font-size: 12px; color: var(--text-tertiary); }

  h2 { font-family: 'Syne', sans-serif; font-weight: 700; font-size: 20px; margin: 0 0 4px; }
  .muted { color: var(--text-secondary); }
  .tertiary { color: var(--text-tertiary); font-size: 13px; }

  .card {
    background: var(--bg-surface);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius);
    padding: 20px;
    margin-bottom: var(--gap);
  }

  .field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
  .field > label { font-size: 13px; color: var(--text-secondary); font-weight: 500; }
  select, input {
    background: var(--bg-input);
    color: var(--text-primary);
    border: 1px solid var(--border-default);
    border-radius: 10px;
    padding: 11px 12px;
    font-family: inherit;
    font-size: 15px;
    width: 100%;
  }
  select:focus, input:focus { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-glow); }
  /* number inputs: no ±spinner steppers (people type the amount; nudging by a cent is noise) */
  input[type="number"]::-webkit-outer-spin-button,
  input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
  input[type="number"] { -moz-appearance: textfield; appearance: textfield; }

  /* searchable combobox (currency / country / time zone) */
  .combo { position: relative; }
  .combo__list {
    position: absolute; z-index: 30; left: 0; right: 0; top: calc(100% + 4px);
    margin: 0; padding: 4px; list-style: none; max-height: 260px; overflow-y: auto;
    background: var(--bg-elevated); border: 1px solid var(--border-default);
    border-radius: 10px; box-shadow: 0 10px 28px rgba(0,0,0,0.45);
  }
  .combo__opt { padding: 9px 10px; border-radius: 7px; cursor: pointer; font-size: 14px; color: var(--text-primary); }
  .combo__opt:hover, .combo__opt[aria-selected="true"] { background: var(--bg-hover); }
  .combo__empty { padding: 9px 10px; font-size: 13px; color: var(--text-tertiary); }
  /* THEMED scrollbar for the popup lists (the default Windows bar is bright white against the dark popup) */
  .combo__list, .lcat-box { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }

  /* tag input (Phase 5): chips + autocomplete, looks like one text field; stores a comma-separated string */
  .taginput {
    position: relative; display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
    padding: 6px 8px; min-height: 44px; cursor: text;
    background: var(--bg-surface); border: 1px solid var(--border-default); border-radius: 10px;
  }
  .taginput:focus-within { border-color: var(--border-strong); }
  .taginput__chip {
    display: inline-flex; align-items: center; gap: 2px;
    padding: 3px 3px 3px 10px; border-radius: 999px; font-size: 13px;
    background: var(--bg-elevated); border: 1px solid var(--border-default); color: var(--text-primary);
  }
  .taginput__chipx {
    appearance: none; background: none; border: 0; padding: 0; cursor: pointer;
    width: 18px; height: 18px; line-height: 1; font-size: 15px; border-radius: 999px;
    color: var(--text-tertiary); display: inline-flex; align-items: center; justify-content: center;
  }
  .taginput__chipx:hover { color: var(--accent-warm); background: var(--bg-hover); }
  .taginput__field {
    flex: 1 1 90px; min-width: 90px; border: 0; background: none; padding: 4px 2px; margin: 0;
    color: var(--text-primary); font-size: 14px; outline: none; box-shadow: none;
  }
  /* keyboard-highlighted suggestion (↑/↓ nav) — matches the combobox hover treatment */
  .taginput__opt--hi { background: var(--bg-hover); }

  /* tag chips shown on a ledger row (read-only) */
  .tx__tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px; }
  .tx__tag { font-size: 11px; padding: 2px 9px; border-radius: 999px; background: var(--bg-elevated); border: 1px solid var(--border-subtle); color: var(--text-secondary); }
  .tx__tag::before { content: '#'; opacity: 0.5; }
  .combo__list::-webkit-scrollbar, .lcat-box::-webkit-scrollbar { width: 10px; }
  .combo__list::-webkit-scrollbar-track, .lcat-box::-webkit-scrollbar-track { background: transparent; }
  .combo__list::-webkit-scrollbar-thumb, .lcat-box::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 999px; border: 2px solid var(--bg-elevated); background-clip: content-box; }
  .combo__list::-webkit-scrollbar-thumb:hover, .lcat-box::-webkit-scrollbar-thumb:hover { background: var(--text-tertiary); background-clip: content-box; }

  /* app-native dropdown (short fixed lists) — reuses the combobox popup look (.combo__list / .combo__opt) */
  .dropdown { position: relative; }
  .dropdown__trigger {
    display: flex; align-items: center; justify-content: space-between; gap: 8px;
    width: 100%; text-align: left; line-height: 1.4;
    background: var(--bg-input); color: var(--text-primary);
    border: 1px solid var(--border-default); border-radius: 10px;
    padding: 11px 12px; font-family: inherit; font-size: 15px; cursor: pointer;
    -webkit-appearance: none; appearance: none;
  }
  .dropdown__trigger:focus-visible { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-glow); }
  .dropdown--open .dropdown__trigger { border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-glow); }
  .dropdown__value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .dropdown__chevron { flex: none; display: flex; color: var(--text-tertiary); transition: transform 0.15s ease; }
  .dropdown--open .dropdown__chevron { transform: rotate(180deg); }
  .dropdown__opt--active { background: var(--bg-hover); }
  .dropdown__opt[aria-selected="true"] { font-weight: 600; }
  .dropdown__opt[aria-disabled="true"] { color: var(--text-tertiary); opacity: 0.5; cursor: not-allowed; }
  .dropdown__opt[aria-disabled="true"]:hover { background: transparent; }

  /* the ONE grouped category picker (single-select popup; same grouped look as the Ledger filter box) */
  .pick__placeholder { color: var(--text-tertiary); }
  /* typable trigger (click → the value is selected so you can type to filter) + a fully scrollable grouped popup
     (the popup inherits .combo__list's overflow-y:auto, so it scrolls through every category). */
  .catpick__box { max-height: 320px; }
  .catpick__input { width: 100%; box-sizing: border-box; padding-right: 38px; cursor: text; }
  /* the chevron is a real toggle target (pointer-events:auto) with a comfortable 36px hit area down the right
     edge, so tapping it while open closes the picker (you no longer have to click off the field to dismiss). */
  .catpick__chev { position: absolute; right: 0; top: 0; bottom: 0; width: 36px; display: flex; align-items: center; justify-content: center; pointer-events: auto; cursor: pointer; color: var(--text-tertiary); }
  .catpick.dropdown--open .catpick__chev { transform: rotate(180deg); }
  .catpick__h { cursor: default; }
  .catpick__h:hover { background: transparent; }
  .catpick__opt { justify-content: space-between; }
  .catpick__opt > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .catpick__opt--active { background: var(--bg-hover); }
  /* ── v412 — THE ONE MERCHANT COMBO. Wears the app's existing popup (.combo__list) with no colour of
     its own; the wrap is what a host sizes, because the control ATTACHES to an input the host already
     had (and every one of those inputs is `width: 100%` by the global rule, so it fills the wrap).
     `left: 0; right: 0` with NO `min-width` — an over-constrained absolute box drops `right` in LTR
     and grows off the right edge of a phone, silently, because the page is `overflow-x: clip` (v395). */
  .mcombo { position: relative; }
  .mcombo > input { width: 100%; box-sizing: border-box; }
  .mcombo__box { z-index: 45; max-height: 280px; max-width: calc(100vw - 24px); padding: 4px; }
  .mcombo--up .mcombo__box { top: auto; bottom: calc(100% + 4px); }
  .mcombo__opt { padding: 9px 10px; border-radius: 7px; cursor: pointer; font-size: 14px; color: var(--text-primary); overflow-wrap: break-word; }
  .mcombo__opt:hover, .mcombo__opt--hi { background: var(--bg-hover); }
  /* ＋Add is the one site whose merchant field ALREADY has a visible list under it — the honeycomb.
     Two merchant lists on one screen is the thing the round exists to avoid, and worse, a hex under
     an open popup is a mis-tap waiting to happen. `visibility: hidden` stands the comb down while the
     list is up: it keeps the layout still (no shift when the popup opens) AND removes the tiles from
     hit-testing, which `opacity: 0` would not. */
  .entry-merch--picking .entry-hive { visibility: hidden; }
  .entry-merch__row .mcombo { flex: 1 1 auto; min-width: 0; }
  .bulk-cell--merch .mcombo { width: 100%; }
  .bulkbar-field .mcombo { flex: 1 1 140px; min-width: 0; }
  .csv-r__l2 > .mcombo { flex: 0 0 120px; min-width: 0; }
  .csv-grp__rename .mcombo { flex: 1 1 auto; min-width: 0; }
  /* MULTI picker (the checkbox "selector box"): checkbox + label sit together (not space-between); headers are
     clickable select-all; flat lists (no header, e.g. merchants) aren't indented; a "Clear all" row sits on top. */
  .catpick--multi .catpick__opt { justify-content: flex-start; }
  .catpick--multi .catpick__opt > span:first-child { overflow: visible; text-overflow: clip; white-space: normal; }
  .catpick--multi .catpick__opt > span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
  .catpick--multi .catpick__opt--flat { padding-left: 10px; }
  .catpick--multi .catpick__h { cursor: pointer; }
  .catpick--multi .catpick__h:hover { background: var(--bg-hover); }
  .catpick__clear { padding: 8px 10px; font-size: 12px; color: var(--text-secondary); cursor: pointer; border-bottom: 1px solid var(--border-subtle); }
  .catpick__clear:hover { background: var(--bg-hover); color: var(--accent-primary); }
  .catpick__check { flex: none; display: flex; color: var(--accent-primary); }

  /* themed date picker (replaces the OS-drawn <input type=date> popup) */
  .datepick__pop {
    position: absolute; z-index: 30; top: calc(100% + 4px); left: 0; width: 272px; max-width: 86vw;
    background: var(--bg-elevated); border: 1px solid var(--border-default);
    border-radius: 12px; box-shadow: 0 10px 28px rgba(0,0,0,0.45); padding: 12px;
  }
  /* (horizontal placement is clamped inline by dateInput's positionPop — stays inside card + viewport) */
  .datepick__head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
  .datepick__title { font-weight: 600; font-size: 14px; color: var(--text-primary); }
  /* v393 — the title is the FAST-TRAVEL door: tap it and the day grid becomes the month/year picker.
     It keeps the exact look it had as a span (a control that shouts would be the loudest thing in a
     calendar), and only picks up a small chevron + a hover to say it can be pressed. The month panel
     itself reuses the `.hmonthpop__*` rules verbatim — nothing new is defined for it here. */
  .datepick__title--btn { display: inline-flex; align-items: center; gap: 4px; appearance: none;
    background: none; border: none; padding: 3px 6px; margin-left: -6px; border-radius: 8px;
    font-family: inherit; font-weight: 600; font-size: 14px; color: var(--text-primary); cursor: pointer; }
  .datepick__title--btn:hover { background: var(--bg-hover); }
  .datepick__titlechev { display: inline-flex; line-height: 0; color: var(--text-tertiary); transition: transform .12s ease; }
  .datepick__title--open .datepick__titlechev { transform: rotate(180deg); color: var(--accent-primary); }
  .datepick__months { margin-top: 2px; }
  @media (prefers-reduced-motion: reduce) { .datepick__titlechev { transition: none; } }
  .datepick__nav { display: flex; gap: 4px; }
  .datepick__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
  .datepick__wd { text-align: center; font-size: 11px; color: var(--text-tertiary); padding: 4px 0; }
  .datepick__day { appearance: none; border: none; background: none; color: var(--text-primary);
    font-family: inherit; font-size: 13px; padding: 7px 0; border-radius: 8px; cursor: pointer; text-align: center; }
  .datepick__day:hover { background: var(--bg-hover); }
  .datepick__day--out { color: var(--text-tertiary); opacity: 0.55; }
  .datepick__day--disabled { color: var(--text-tertiary); opacity: 0.28; cursor: default; }   /* out of min/max range */
  .datepick__day--disabled:hover { background: none; }
  .datepick__day--today { box-shadow: inset 0 0 0 1px var(--border-strong); }
  .datepick__day--active { background: var(--bg-hover); }
  .datepick__day--sel { background: var(--accent-primary); color: var(--on-accent-primary); font-weight: 600; }
  .datepick__day--sel:hover { background: var(--accent-primary); }
  .datepick__foot { display: flex; justify-content: space-between; margin-top: 8px; }
  /* typeable date field (opt-in): a borderless input + a calendar button, inside the trigger frame */
  .datepick__trigwrap { display: flex; align-items: center; padding: 0; }
  .datepick__trigwrap:focus-within { border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-glow); }
  .datepick__input { flex: 1; min-width: 0; border: none; background: none; box-shadow: none; outline: none;
    padding: 10px 12px; font-family: inherit; font-size: 14px; color: var(--text-primary); }
  .datepick__input:focus { border: none; box-shadow: none; outline: none; }
  .datepick__iconbtn { flex: none; background: none; border: none; color: var(--text-secondary); padding: 0 10px;
    cursor: pointer; display: flex; align-items: center; }
  .datepick__iconbtn:hover { color: var(--accent-primary); }
  .datepick__err { font-size: 12px; color: var(--accent-warm); margin-top: 5px; }   /* "that's not a date" feedback */

  .hint { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; margin-top: 6px; font-size: 12px; color: var(--text-tertiary); }
  .linkbtn { appearance: none; background: none; border: none; padding: 0; cursor: pointer;
    color: var(--accent-primary); font-family: inherit; font-size: 12px; }
  .linkbtn:hover { text-decoration: underline; }

  /* ── top-bar consolidation (2026-07-24) ─────────────────────────────────────────────────────── */
  /* ＋Add header: title/intro left, the two quiet entry-door pills right; pills wrap below on a phone. */
  .entry-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
  .entry-head__text { flex: 1 1 240px; min-width: 0; }
  .entry-head__links { display: flex; gap: 8px; flex-wrap: wrap; }
  /* quiet pill: primary-purple text on a hairline border, purple glow on hover — no new colours (palette law) */
  .entry-modelink { display: inline-flex; align-items: center; gap: 6px; font-family: inherit; font-size: 13px;
    color: var(--accent-primary); background: none; border: 1px solid var(--border-default); border-radius: 999px;
    padding: 5px 12px; cursor: pointer; white-space: nowrap; }
  .entry-modelink:hover { background: var(--accent-glow); }

  /* merged Goals & Plans view: Reserves & Goals (left) + Pay-over-time plans (right); stacks below ~900px. */
  .goalsplans { display: grid; grid-template-columns: 1fr; gap: 18px; }
  .goalsplans__col { min-width: 0; } /* the standing flex/grid overflow guard — long content wraps, never spills */
  @media (min-width: 900px) { .goalsplans { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } }

  /* divider between the profiles pool and the household card in the merged Settings "Profiles" panel */
  .settings-divider { border: none; border-top: 1px solid var(--border-default); margin: 22px 0; }

  /* numbers / amounts always in mono — Bible §25 */
  .mono, .preview__value { font-family: 'DM Mono', ui-monospace, monospace; }

  .preview {
    display: flex; flex-wrap: wrap; gap: 20px;
    background: var(--bg-elevated);
    border: 1px solid var(--border-subtle);
    border-radius: 10px; padding: 14px 16px; margin-top: 4px;
  }
  .preview__item { display: flex; flex-direction: column; gap: 2px; }
  .preview__label { font-size: 12px; color: var(--text-tertiary); }
  .preview__value { font-size: 16px; color: var(--text-primary); }
  .preview__value--amount { color: var(--accent-out); }   /* outgoing amount (lavender trial; was amber) */

  /* primary CTA = purple (brand/neutral actions: Save, Add, Continue). Bible §3. */
  .btn {
    appearance: none; border: none; cursor: pointer;
    background: var(--accent-primary); color: var(--on-accent-primary);
    font-family: 'DM Sans', sans-serif; font-weight: 600; font-size: 15px;
    padding: 12px 20px; border-radius: 10px;
    transition: filter .12s ease, transform .06s ease; /* tactile feedback on hover/press (app-wide) */
  }
  .btn:hover { filter: brightness(1.08); }
  .btn:active { filter: brightness(0.92); transform: translateY(1px); }
  .btn:disabled { opacity: 0.6; cursor: default; filter: none; transform: none; }
  /* ghost = a subtle, secondary action (Cancel, Use different…) — outlined, not a loud purple primary.
     MUST sit after the base .btn (same specificity, later source) so its transparent background wins. */
  .btn--ghost { background: none; border: 1px solid var(--border-default); color: var(--text-secondary); }
  .btn--ghost:hover { color: var(--text-primary); background: var(--bg-hover); filter: none; }
  .btn--ghost:active { background: var(--bg-hover); transform: translateY(1px); }
  /* amber variant = money-outgoing & destructive actions only (e.g. Remove). Bible §3. */
  .btn--warm { background: var(--accent-warm); color: var(--on-accent-warm); }
  /* small variant for row-level / secondary placements (MUST sit after .btn — see the NB above) */
  .btn--sm { padding: 8px 12px; font-size: 13px; }

  .toast { margin-top: 14px; font-size: 14px; min-height: 20px; }
  .toast--ok { color: var(--accent-teal); }
  /* ERROR TEXT IS AMBER, NOT RED (human-ratified 2026-07-30 — "go with (a) for the reds").
     This one rule is EVERY error message in the app: 42 call sites, 17 written strings + server prose, from
     the "Something went wrong" load-failure card down to "Type a merchant name first". It had been --accent-red
     since it was written, which contradicted the palette rule three ways: red is reserved for a genuine
     zero-or-below boundary (non-negotiable #4 / tone-and-language.md); --accent-warm is defined ten lines above
     as "DESTRUCTIVE actions + error/status feedback" in as many words; and its own sibling .hearth-toast--err
     (the FLOATING toast, line ~881) was ALREADY amber — so the same message rendered two different colours
     depending only on whether it appeared inline or floated.
     The audit that settled it: tools/red-text-audit.src.html -> /mocks/red-text. Of the ten --accent-red uses,
     the other NINE are correct and were deliberately left alone (the zero line, below-zero amounts, the zero
     marks, and intentions.trackKeptNegative "Spent {amount} more than your income" — the one sentence that
     SHOULD be red). Red now appears ONLY at a zero-or-below boundary, which is what the rule always said. */
  .toast--err { color: var(--accent-warm); }

  /* ── settings section: the LAPTOP SIDEBAR + content panel (v423, human-ratified from the round-2 mock)
     One quiet line per section — icon BESIDE label, left-aligned, the current line wearing the purple
     inset edge. It replaced a 78px icon RAIL (icon over a 10.5px label); the rail was compact but it
     read as a wall of glyphs, and the mock's sketch is a list you can scan. While Help is the open
     section its ten guides indent beneath it (`.settings-navsub`), so a wide screen keeps everything
     visible at once — which is the whole reason the phone gets a different control rather than this
     one squeezed. Below the breakpoint this whole sidebar is `display: none` and the bottom thumb-bar
     takes over; it stays IN THE DOM there (not un-built) so every deep link and every path that
     reaches a sub-tab through it keeps working on a phone. */
  .settings-section { display: flex; gap: var(--gap); align-items: flex-start; }
  .settings-subnav { display: flex; flex-direction: column; gap: 2px; flex: none; width: 168px; }
  .settings-navbtn {
    appearance: none; background: none; border: none; cursor: pointer;
    width: 100%; border-radius: 8px; font-family: inherit; color: var(--text-secondary);
    display: flex; flex-direction: row; align-items: center; gap: 10px;
    padding: 9px 10px; text-align: left; font-size: 13px; line-height: 1.25;
  }
  .settings-navbtn__icon { display: flex; flex: none; color: var(--text-tertiary); }
  .settings-navbtn__icon svg { width: 17px; height: 17px; }
  .settings-navbtn__lbl { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  @media (hover: hover) {
    .settings-navbtn:hover { background: var(--bg-hover); color: var(--text-primary); }
    .settings-navbtn:hover .settings-navbtn__icon { color: var(--text-primary); }
  }
  .settings-navbtn--active {
    background: var(--bg-surface); color: var(--accent-primary); font-weight: 600;
    box-shadow: inset 3px 0 0 var(--accent-primary);
  }
  .settings-navbtn--active .settings-navbtn__icon { color: var(--accent-primary); }
  .settings-navbtn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-glow); }

  /* the ten Help guides, indented beneath the Help line while you are in Help */
  .settings-navsub { display: flex; flex-direction: column; gap: 1px; padding: 2px 0 2px 14px; }
  .settings-navsub__btn {
    appearance: none; background: none; border: none; cursor: pointer; width: 100%;
    border-radius: 7px; font-family: inherit; color: var(--text-tertiary); font-size: 12px; line-height: 1.3;
    display: flex; align-items: baseline; gap: 8px; padding: 6px 8px; text-align: left;
  }
  .settings-navsub__n { flex: none; width: 13px; font-family: 'DM Mono', ui-monospace, monospace; font-size: 10.5px; }
  /* the guide names WRAP rather than ellipsing. Caught by eye on the first laptop screenshot: seven of
     the ten read "Importing & screen…", which is a list you cannot scan. A sidebar has height to spare
     and no width to spare, so height is what gives. (The nine SECTION names all fit on one line at
     168px, so they keep `nowrap` above and the column stays tidy.) */
  .settings-navsub__lbl { min-width: 0; white-space: normal; }
  @media (hover: hover) { .settings-navsub__btn:hover { background: var(--bg-hover); color: var(--text-primary); } }
  .settings-navsub__btn--active { color: var(--text-primary); font-weight: 600; box-shadow: inset 2px 0 0 var(--accent-primary); }
  .settings-navsub__btn--active .settings-navsub__n { color: var(--accent-primary); }
  .settings-navsub__btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-glow); }

  .settings-panel { flex: 1; min-width: 0; }
  /* the global form caps its own content width (wide forms read worse) — the SECTION frame stays
     stable across sub-tabs; data sub-tabs (archive, change log, merchant rules) use the full panel */
  .settings-panel--narrow { max-width: 680px; }

  /* ══ v423 — THE THUMB-BAR (phone posture of the section nav) ═══════════════════════════════════════
     Built from the approved round-2 mock. It exists ONLY on Settings/Help — no other screen in the app
     gains bottom chrome — and it is `display: none` above the section's own 640px breakpoint, where the
     sidebar is the control. Layer: 40, the content-level pinned-bar tier (below the header's 50, per the
     v378 ordering, so the header's own popups still outrank it). `data-noswipe` on the wrap is what keeps
     it out of the swipe-nav's way: the gesture handler walks ancestors, so bar, sheet and scrim are all
     covered by the one attribute, and the sheet's own scroll is vertical. */
  /* v424 (human review of v423): the bar sits FLUSH at the viewport bottom — no gap underneath (scrolled
     content used to show in the 10px slot below it), and its edges align with the app frame's 20px card
     inset instead of spilling 8px past the card on each side. The safe-area inset lives INSIDE the bar
     (its own opaque padding), so on gesture-bar phones nothing can peek through beneath it either. */
  .snav { display: none; position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
    padding: 0 20px; pointer-events: none; }
  .snav > * { pointer-events: auto; }
  /* ⚠ THE LAYER MOVES WHEN THE SHEET OPENS, and both halves are deliberate (found by eye on the first
     390px screenshot: the scrim dimmed the whole page EXCEPT the header, which reads as a bug).
     CLOSED the bar is furniture on the content-level pinned tier (40) — under the header's own 50, so
     the context picker and the bell still paint over it, which is the v378 ordering. OPEN it is a
     dialog with a scrim, so it takes 90: above the header and above the ledger/analysis side panels
     (60/61), and still BELOW the modal overlay (100), which must always win. */
  .snav--open { z-index: 90; }
  .snav__bar {
    position: relative; z-index: 3; appearance: none; cursor: pointer; width: 100%;
    display: flex; align-items: center; gap: 8px; font-family: inherit; font-size: 15px; text-align: left;
    background: var(--bg-elevated); border: 1px solid var(--border-default); border-bottom: none;
    border-radius: 16px 16px 0 0;
    padding: 11px 14px calc(11px + env(safe-area-inset-bottom));
    color: var(--text-primary); box-shadow: 0 -4px 20px rgba(0, 0, 0, .35);
  }
  .snav__ic { display: flex; flex: none; color: var(--text-tertiary); }
  .snav__ic svg { width: 17px; height: 17px; }
  .snav__crumb, .snav__sep { color: var(--text-tertiary); font-size: 13px; flex: none; }
  .snav__cur { flex: 1; min-width: 0; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .snav__car { display: flex; flex: none; color: var(--text-tertiary); transition: transform .18s ease; }
  .snav--open .snav__car { transform: rotate(180deg); }
  @media (prefers-reduced-motion: reduce) { .snav__car { transition: none; } }

  .snav__scrim { display: none; position: fixed; inset: 0; z-index: 1; background: rgba(4, 4, 8, .55); }
  .snav--open .snav__scrim { display: block; }
  /* the sheet drops UP from the bar: absolutely positioned against the fixed wrap, so it needs no
     second viewport calculation and can never disagree with where the bar actually is */
  .snav__sheet { display: none; position: absolute; left: 20px; right: 20px; bottom: 100%; z-index: 2;
    margin-bottom: 8px; background: var(--bg-surface); border: 1px solid var(--border-default);
    border-radius: 16px; padding: 8px; max-height: 62vh; overflow-y: auto; overflow-x: hidden;
    box-shadow: 0 -8px 34px rgba(0, 0, 0, .5);
    opacity: 0; transform: translateY(10px); transition: opacity .16s ease, transform .16s ease; }
  .snav--open .snav__sheet { display: block; }
  .snav--shown .snav__sheet { opacity: 1; transform: none; }
  @media (prefers-reduced-motion: reduce) { .snav__sheet { transition: none; } }
  .snav__sheethead { display: flex; align-items: center; gap: 10px; padding: 4px 8px 8px; }
  .snav__sheettitle { flex: 1; min-width: 0; font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
    color: var(--text-tertiary); }
  .snav__back { appearance: none; background: none; border: none; cursor: pointer; font-family: inherit;
    font-size: 12.5px; color: var(--text-secondary); display: flex; align-items: center; gap: 4px; padding: 4px 6px;
    border-radius: 7px; }
  .snav__backarr { color: var(--text-tertiary); }
  .snav__back:focus-visible, .snav__opt:focus-visible, .snav__bar:focus-visible {
    outline: none; box-shadow: 0 0 0 3px var(--accent-glow); }

  /* ⚠ THUMB ORDER (human-ratified, and it LOOKS LIKE A BUG if you don't know): item 1 renders LOWEST,
     nearest the thumb, and the list reads UPWARD. `flex-wrap: wrap-reverse` stacks the LINES bottom-up
     while leaving each line reading left-to-right — so the DOM order stays 1..N (tab order, screen
     readers and the guide numbering are all unharmed) and only the visual rows are flipped. Reversing
     the ARRAY instead would also have flipped the cells within each row (…3 2 1), which is not what
     "reads upward" means. The render site in settings.js carries the same warning. */
  .snav__grid { display: flex; flex-wrap: wrap-reverse; gap: 6px; }
  .snav__opt { appearance: none; cursor: pointer; background: none; border: none; font-family: inherit;
    flex: 0 0 calc((100% - 12px) / 3); display: flex; flex-direction: column; align-items: center; gap: 6px;
    padding: 11px 5px 10px; border-radius: 12px; color: var(--text-secondary); font-size: 12px; line-height: 1.2;
    text-align: center; }
  .snav__optic { display: flex; align-items: center; justify-content: center; height: 20px; color: var(--text-tertiary); }
  .snav__optic svg { width: 19px; height: 19px; }
  .snav__optic--n { font-family: 'DM Mono', ui-monospace, monospace; font-size: 13px; }
  .snav__optlbl { display: block; }
  .snav__opt--on { background: var(--bg-elevated); color: var(--text-primary); font-weight: 600;
    box-shadow: inset 0 0 0 1px var(--accent-primary); }
  .snav__opt--on .snav__optic { color: var(--accent-primary); }

  /* phone: the sidebar stands down entirely and the thumb-bar is the section nav */
  @media (max-width: 640px) {
    .settings-section { flex-direction: column; }
    /* once stacked, the panel must FILL the column — without this it sizes to its max-content and a wide data
       panel (e.g. the CSV review tab-strip) blows past the viewport and clips on the right. width:100% +
       min-width:0 keeps it at the section width; its own wide children (the tabs) scroll within. */
    .settings-panel { width: 100%; min-width: 0; }
    /* v423: the nine-tile wall is GONE from the top of the screen — content starts directly under the
       screen title. The sidebar stays in the DOM (hidden) so nothing that navigates through it breaks. */
    .settings-subnav { display: none; }
    .snav { display: block; }
    /* room for the bar, so the last card is never parked underneath it */
    .settings-section { padding-bottom: calc(74px + env(safe-area-inset-bottom)); }

    /* THE CONDENSED PHONE HEADER (v385, human-directed 2026-08-20). It used to cost THREE lines — the logo,
       then "ACTIVE {picker} {bell}", then the tab strip — before a single figure appeared. Now it costs two.
       The mechanism is `display: contents` on the right cluster: its BOX dissolves, so its three children
       become direct flex children of `.app__header`, and the brand + the context picker + the bell share one
       line while `.nav` (width: 100%) wraps to the next. No DOM change, so the DESKTOP header is untouched —
       which is the whole reason this is a CSS-only fix rather than a re-parenting.
       ⚠ Nothing may `position` against `.app__headerright` below this breakpoint any more — a
       `display: contents` element generates no box. The notification popup anchors on `.app__header`
       instead (see the 640px block further down); `.app__header` is already `position: relative`. */
    .app__headerright { display: contents; }
    .app__header { gap: 10px; margin-bottom: 14px; }
    .brand { font-size: 23px; }
    .brandlock { gap: 8px; }
    .brandmark { width: 28px; height: 33px; }
    /* the picker + bell ride to the far end of the logo's line */
    .ctxslot { margin-left: auto; min-width: 0; }
    /* "the name suggests what it is" — the ACTIVE label word is what a 390px line cannot afford */
    .ctxslot__label { display: none; }
    /* a long profile / household name must ELLIPSIS, never push the bell off the line or wrap it.
       .dropdown__value already carries the ellipsis rule; a flex item needs min-width: 0 to honour it.
       v395 — THE CAP IS NOW THE SPACE THAT ACTUALLY EXISTS, not a flat 42vw. The line is
       [ 20 pad | Hearth ~126 | 10 gap | CHOOSER | 10 gap | bell 32 | 20 pad ], so what the chooser may
       take is `100vw − 218`; the 236 leaves a further 18px of air so a long household name does not sit
       up against the wordmark (at 390 that is 154px, where a flat 42vw was 164 and left 8px). The clamp's
       floor keeps it legible on a very narrow phone and its 42vw ceiling keeps it from sprawling on a
       small tablet. The brand's width is what the middle term encodes, so the header measurements in
       verify-v395 assert the two invariants directly — a gap to the wordmark, and a bell that never
       moves — rather than trusting the arithmetic to stay true if the wordmark is ever resized. */
    .ctxslot { min-width: 0; }
    .ctxslot .dropdown { min-width: 0; }
    /* 2026-09-07: the bear (28px + 8px gap) joined the wordmark, so the middle term grew by 36px → 270.
       Measured at 390: [ 20 | bear 28 | 8 | Tobsy 111 | 10 | CHOOSER | 10 | bell 32 | 20 ] = 239 + chooser;
       the clamp gives the chooser 120 here, which leaves ~31px of air — more than the v395 sum had. */
    .ctxslot .dropdown__trigger { max-width: clamp(90px, calc(100vw - 270px), 42vw); padding: 7px 9px; }
    .ctxslot .dropdown__value { min-width: 0; }
    /* the top nav becomes a full-width, horizontally-SCROLLABLE strip (the TABS scroll, not the page). */
    .nav {
      width: 100%; flex-wrap: nowrap; overflow-x: auto; padding-bottom: 2px;
      -webkit-overflow-scrolling: touch;            /* momentum scroll on iOS */
      scrollbar-width: none; -ms-overflow-style: none; /* hide the scrollbar (Firefox/IE) */
    }
    .nav::-webkit-scrollbar { display: none; }       /* hide the scrollbar (WebKit) */
    .nav__btn { flex: 0 0 auto; }                    /* keep intrinsic width; let the strip scroll */
  }

  /* merchant rules */
  .merchant-add { margin-bottom: var(--gap); }

  /* toolbar: search · sort · add. Wraps on a phone; the search keeps the width it can. */
  .mrule-tools { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: var(--gap); }
  .mrule-search { flex: 1 1 220px; min-width: 0; position: relative; }
  .mrule-search input { width: 100%; padding-left: 34px; }
  .mrule-search__icon { position: absolute; left: 11px; top: 50%; transform: translateY(-50%);
    color: var(--text-tertiary); line-height: 0; pointer-events: none; }
  .mrule-search input::-webkit-search-cancel-button { filter: invert(0.6); }
  .mrule-tools .dropdown { flex: 0 0 auto; }
  .mrule-tools .btn { flex: 0 0 auto; }

  .mrule-list { padding: 0; overflow: hidden; }
  .mrule-empty { padding: 24px 16px; text-align: center; }
  /* By-category sort renders group headers; keep them quiet — they label, they don't compete. */
  .mrule-grouphead { padding: 9px 16px; background: var(--bg-base); color: var(--text-tertiary);
    font-size: 11.5px; letter-spacing: .07em; text-transform: uppercase;
    border-bottom: 1px solid var(--border-subtle); }

  .mrule { display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
    padding: 12px 16px; border-top: 1px solid var(--border-subtle); }
  .mrule:first-child { border-top: none; }
  .mrule-grouphead + .mrule { border-top: none; }
  .mrule__main { min-width: 0; flex: 1 1 200px; }
  .mrule__pattern { font-size: 15px; color: var(--text-primary); word-break: break-word; }
  .mrule__usage { font-size: 12.5px; color: var(--text-tertiary); margin-top: 3px;
    font-variant-numeric: tabular-nums; }
  .mrule__cat { flex: none; font-size: 12.5px; color: var(--text-primary); white-space: nowrap;
    border: 1px solid var(--border-default); background: var(--bg-elevated); border-radius: 999px; padding: 4px 10px; }
  .mrule__archived { font-size: 12px; color: var(--text-tertiary); font-style: italic; }
  .mrule__acts { display: flex; align-items: center; gap: 2px; flex: none; }

  /* The BEHAVIOUR chip — the rule's confidence level, now the user's to set (tap → the level popover).
     Colour carries the meaning: teal = it just works · gold = it will come back to you · muted = only a
     suggestion. Never amber (destructive) and never red (the zero line only). */
  .mrule-behav { flex: none; display: inline-flex; align-items: center; gap: 5px; white-space: nowrap;
    border: 1px solid var(--border-default); background: var(--bg-elevated); border-radius: 999px;
    padding: 5px 10px; font-family: inherit; font-size: 12.5px; color: var(--text-secondary); cursor: pointer; }
  .mrule-behav:hover, .mrule-behav:focus-visible { border-color: var(--accent-primary); outline: none; }
  .mrule-behav__car { line-height: 0; opacity: .55; }
  .mrule-behav__car svg { width: 12px; height: 12px; }
  .mrule-behav--auto { color: var(--accent-teal); border-color: rgba(62, 207, 170, .35); }
  .mrule-behav--ask { color: var(--accent-gold); border-color: rgba(232, 197, 71, .32); }
  .mrule-behav--sug { color: var(--text-tertiary); }

  /* the level popover — themed like every other Hearth popup, positioned from the chip it belongs to */
  .mrule-pop { position: fixed; z-index: 200; min-width: 240px; max-width: min(320px, calc(100vw - 16px));
    max-height: calc(100vh - 16px); overflow-y: auto;   /* a very short landscape viewport scrolls it, never clips it */
    background: var(--bg-elevated); border: 1px solid var(--border-strong); border-radius: 12px; padding: 6px;
    box-shadow: 0 16px 40px rgba(0, 0, 0, .6); }
  .mrule-pop__o { display: block; width: 100%; text-align: left; background: none; border: 0;
    color: var(--text-primary); font-family: inherit; font-size: 14px; padding: 10px 11px;
    border-radius: 8px; cursor: pointer; }
  .mrule-pop__o:hover, .mrule-pop__o:focus-visible { background: var(--bg-hover); outline: none; }
  .mrule-pop__hint { display: block; color: var(--text-tertiary); font-size: 12px; margin-top: 2px; }
  .mrule-pop__o[aria-selected="true"] .mrule-pop__lbl { color: var(--accent-primary); font-weight: 500; }
  .mrule-pop__o[aria-selected="true"] .mrule-pop__lbl::before { content: '✓  '; }

  /* the same three levels inside the add/edit modal, as a stacked choice */
  .mrule-seg { display: flex; flex-direction: column; gap: 6px; }
  .mrule-seg__o { text-align: left; background: var(--bg-input); border: 1px solid var(--border-default);
    border-radius: 10px; color: var(--text-primary); font-family: inherit; font-size: 14px;
    padding: 10px 12px; cursor: pointer; }
  .mrule-seg__o:hover { background: var(--bg-hover); }
  .mrule-seg__hint { display: block; color: var(--text-tertiary); font-size: 12px; margin-top: 2px; }
  .mrule-seg__o[aria-checked="true"] { border-color: var(--accent-primary); background: var(--accent-glow); }
  /* Renaming a merchant rule offers to rename PAST entries too.
     NO WARNING LINE — dropped 2026-07-30 on the human's reading, which the code bears out: the rewrite writes
     one column and deletes nothing, so changing it back is editing the rule name and ticking the same box.
     It carried an amber "it can't be undone" line, which was simply untrue. The count line above the tick is
     the whole disclosure. (The one genuinely one-way part is a MERGE of two spellings into one — they can't
     be told apart afterwards — and the human chose not to surface even that.)
     Bordered top AND bottom so the offer reads as its own section — without the bottom rule it butts straight
     against the next field's "Category" label (caught in the 390px screenshot). */
  .mrename { border-top: 1px solid var(--border-subtle); border-bottom: 1px solid var(--border-subtle);
    margin: 12px 0 14px; padding: 10px 0 12px; }
  .mrename:empty { border: none; margin: 0; padding: 0; }
  .mrename__t { font-size: 13px; font-weight: 500; margin-bottom: 4px; }
  .mrename__l { margin: 0 0 4px; font-size: 13px; }
  .mrename__opt { display: flex; gap: 8px; align-items: center; font-size: 13px;
    min-height: 40px; cursor: pointer; }
  /* the shared .hui-check carries margin-top:4px to sit on the first line of a two-line CSV row; this label
     is single-line and vertically centred, so that offset has to come back off. */
  .mrename__opt .hui-check { margin-top: 0; }


  /* phone: the name + usage take their own line; category, behaviour and the actions wrap beneath it */
  @media (max-width: 640px) {
    .mrule { padding: 12px 14px; gap: 9px; }
    .mrule__main { flex: 1 1 100%; }
    .mrule__acts { margin-left: auto; }
  }

  /* ＋Add nav button — the daily core, emphasised (purple); solid pill when it's the active tab */
  .nav__btn--add { color: var(--accent-primary); border-color: var(--border-default); font-weight: 600; }
  /* v421 ③ — pointer-only, for the reason stated at .nav__btn:hover above (a tapped ＋Add kept a lit
     glow after a swipe carried the pill away, same sticky-hover mechanism, its own colour) */
  @media (hover: hover) {
    .nav__btn--add:hover { background: var(--accent-glow); color: var(--accent-primary); }
  }
  .nav__btn--add.nav__btn--active { background: var(--accent-primary); color: var(--on-accent-primary); border-color: var(--accent-primary); }

  /* quick manual entry */
  .entry-types { display: flex; gap: 8px; margin-bottom: 8px; }
  /* v398 rider — the "Set aside is not income" hint, under the type row (＋Add) and at the head of the
     goals section (bulk). Tertiary register, full width, wraps freely: it is a sentence to read once,
     not a warning to be stopped by. */
  .entry-asidehint, .bulk-asidehint { margin: 0 0 10px; line-height: 1.45; max-width: 62ch; }
  .entry-type { flex: 1; padding: 10px; border-radius: 10px; border: 1px solid var(--border-default);
    background: none; color: var(--text-secondary); font-family: inherit; font-size: 15px; cursor: pointer; }
  .entry-type:hover { background: var(--bg-hover); }
  .entry-type--active { background: var(--accent-primary); color: var(--on-accent-primary); border-color: var(--accent-primary); }
  /* phone: 4 type pills can't fit one row (the long "Reimbursement" word overflows the card with no
     h-scroll) — wrap them to a 2×2 grid. Placed AFTER the base .entry-type so it wins the cascade. */
  @media (max-width: 640px) {
    .entry-types { flex-wrap: wrap; }
    .entry-type { flex: 1 1 calc(50% - 4px); }
  }
  /* v418 ⑤ — NO `text-overflow: ellipsis` HERE. The whole amount is always visible: `fitDisplay` shrinks
   * the type until it fits (measured fractionally, entry.js), and an ellipsis is not a fallback for that
   * — it is the failure this control exists to prevent, wearing a tidy face. Removed so the outcome is
   * unreachable rather than merely unlikely. `overflow: hidden` stays: it keeps a runaway figure inside
   * the card instead of shoving the currency chip off the row. */
  .numpad__display { font-family: 'DM Mono', ui-monospace, monospace; font-size: 34px; text-align: right;
    padding: 12px 10px 6px; color: var(--text-primary); overflow: hidden; }
  .numpad__display--in { color: var(--accent-teal); }
  .numpad__display--reimb { color: var(--accent-gold); }
  .numpad__display--neutral { color: var(--text-secondary); }   /* transfer = neutral movement, muted (§5.1) */
  /* FX (§9) — currency chip beside the amount + the live conversion preview */
  .entry-amountrow { display: flex; align-items: center; gap: 8px; }
  .entry-amountrow .numpad__display { flex: 1; min-width: 0; }
  .entry-ccychip { flex: 0 0 auto; padding: 6px 12px; border-radius: 20px; border: 1px solid var(--border-default);
    background: none; color: var(--text-secondary); font-family: inherit; font-size: 14px; font-weight: 600; cursor: pointer; }
  .entry-ccychip:hover { background: var(--bg-hover); }
  .entry-ccychip--on { color: var(--accent-primary); border-color: var(--accent-primary); background: var(--accent-glow); }
  .entry-ccypick { margin: 6px 0 2px; }
  /* date chip (§22 past-data entry) — compact "Today ▾" pill on the LEFT of the amount row; mirrors .entry-ccychip */
  .entry-amountrow .entry-datechip { flex: 0 0 auto; }
  .entry-datechip .dropdown__trigger { width: auto; gap: 5px; padding: 6px 10px 6px 12px; border-radius: 20px;
    background: none; color: var(--text-secondary); font-size: 14px; font-weight: 600; }
  .entry-datechip .dropdown__trigger:hover { background: var(--bg-hover); }
  .entry-datechip .dropdown__chevron svg { width: 16px; height: 16px; }
  .entry-fx { margin: 2px 0 8px; padding: 8px 12px; border-radius: 10px; background: var(--bg-surface); }
  .entry-fx__line { font-size: 15px; color: var(--text-primary); }
  .entry-fx__note, .entry-fx__attr { font-size: 12px; margin-top: 3px; }
  .numpad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin: 4px 0 6px; }
  .numpad__key { padding: 15px 0; font-size: 20px; font-family: 'DM Mono', ui-monospace, monospace;
    background: var(--bg-elevated); border: 1px solid var(--border-subtle); border-radius: 10px;
    color: var(--text-primary); cursor: pointer; }
  .numpad__key:hover { background: var(--bg-hover); }
  .numpad__key:active { transform: translateY(1px); }
  .numpad__key--back { color: var(--accent-warm); }
  .entry-suggest { display: inline-block; margin-top: 8px; font-size: 13px; font-family: inherit; }
  .entry-suggest--applied { background: none; border: none; padding: 0;
    color: var(--accent-teal); }   /* the chosen category MATCHES the suggestion — a calm confirmation (no action needed) */
  .entry-suggest--chip { appearance: none; cursor: pointer; padding: 6px 12px; border-radius: 16px;
    border: 1px solid var(--accent-primary); background: var(--accent-glow); color: var(--accent-primary);
    font-family: inherit; }
  .entry-suggest--chip:hover { background: var(--accent-primary); color: var(--on-accent-primary); }
  .entry-label { display: block; font-size: 13px; color: var(--text-secondary); margin: 14px 0 8px; }
  .cat-tiles { display: flex; flex-wrap: wrap; gap: 8px; }
  .cat-tile { padding: 9px 14px; border-radius: 20px; border: 1px solid var(--border-default);
    background: none; color: var(--text-secondary); font-family: inherit; font-size: 14px; cursor: pointer; }
  .cat-tile:hover { background: var(--bg-hover); }
  .cat-tile--active { background: var(--accent-primary); color: var(--on-accent-primary); border-color: var(--accent-primary); }
  .cat-tile--more { font-style: italic; }
  .entry-more { margin-top: 10px; }
  .entry-chosen { margin-top: 10px; font-size: 13px; color: var(--accent-primary); }
  .entry-hh { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border-subtle); }
  .entry-hh__split { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 8px;
    font-size: 14px; color: var(--text-secondary); }
  .entry-hh__q { font-size: 13px; color: var(--text-secondary); margin: 14px 0 8px; }
  .entry-hh__btns { display: flex; flex-wrap: wrap; gap: 8px; }
  .entry-hh__hint { margin-top: 8px; font-size: 12px; color: var(--text-tertiary); }
  .entry-hh__override { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 10px; }
  .entry-hh__field { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 110px; }
  .entry-hh__field > label { font-size: 12px; color: var(--text-tertiary); }
  .entry-pill { appearance: none; cursor: pointer; padding: 9px 16px; border-radius: 20px;
    border: 1px solid var(--border-default); background: none; color: var(--text-secondary);
    font-family: inherit; font-size: 14px; }
  .entry-pill:hover { background: var(--bg-hover); }
  .entry-pill--active { background: var(--accent-primary); color: var(--on-accent-primary); border-color: var(--accent-primary); }
  .entry-pill--ghost { border-style: dashed; font-size: 13px; }
  .entry-log { width: 100%; margin-top: 16px; }

  /* ── v394: the MERCHANT block — label left, typing field right, the honeycomb beneath ─────────────
     The label and the field share one row so the comb gets the card's full width for names, which is
     where the height saved in the approved mock's round 2 came from. */
  .entry-merch { margin-top: 14px; }
  .entry-merch__row { display: flex; align-items: center; gap: 10px; }
  .entry-merch__lbl { flex: 0 0 auto; font-size: 13px; color: var(--text-secondary); }
  .entry-merch__in { flex: 1 1 auto; min-width: 0; }
  /* THE HONEYCOMB. Every tile is absolutely placed from JS (buildHive) because a tessellation is
     arithmetic, not flow — the box's own width/height are set from the same numbers, including the half
     tile an OFFSET row reaches past the flush ones. Nothing here is a new colour: the resting tile is
     --bg-elevated on a --border-default rim, and a selected tile wears the purple primary. */
  .entry-hive { position: relative; margin: 8px 0 0; max-width: 100%; }
  .hex { position: absolute; padding: 0; border: 0; background: var(--border-default); cursor: pointer;
    clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
    transition: filter .12s ease, transform .06s ease; }
  .hex__in { position: absolute; inset: 1.5px; background: var(--bg-elevated); color: var(--text-secondary);
    clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    padding: 0 9%; text-align: center; }
  .hex:hover .hex__in { background: var(--bg-hover); color: var(--text-primary); }
  .hex:active { transform: translateY(1px); }
  .hex__emoji { font-size: 14px; line-height: 1.15; }
  .hex__name { font-family: 'DM Sans', sans-serif; font-weight: 600; line-height: 1.12; width: 100%;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
    word-break: break-word; hyphens: auto; }
  .hex__sub { line-height: 1.1; color: var(--text-tertiary); width: 100%;
    display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
  .hex--on { background: var(--accent-primary); filter: drop-shadow(0 0 7px var(--accent-glow)); }
  .hex--on .hex__in { background: var(--accent-primary); color: var(--on-accent-primary); }
  .hex--on .hex__sub { color: var(--on-accent-primary); opacity: .78; }

  /* ── v394: the STICKY LOG DOCK ────────────────────────────────────────────────────────────────────
     The LAST child of .view, so it floats over the form while you are scrolled up it and settles into
     its own space at the end — it never permanently covers content and needs no fixed-overlay geometry.
     Full-bleed across .app's 20px gutters with an opaque base so the form cannot peek past its edges as
     it scrolls under (the .cal-sticky rule). Sticky works because html/body use overflow-x: clip.
     Log is order:99 and grows, so it stays last and full-width on whichever row the wrap leaves it. */
  .entry-dock { position: sticky; bottom: 0; z-index: 5; display: flex; flex-wrap: wrap; gap: 8px;
    align-items: center; margin: 14px -20px 0; padding: 10px 20px;
    padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
    background: var(--bg-base); border-top: 1px solid var(--border-subtle);
    box-shadow: 0 -10px 24px rgba(0, 0, 0, .55); }
  .entry-dock .entry-log { order: 99; flex: 1 1 132px; min-width: 132px; width: auto; margin-top: 0;
    padding: 12px 14px; }
  .entry-dock__mini { flex: 0 0 auto; appearance: none; cursor: pointer; white-space: nowrap;
    background: none; border: 1px solid var(--border-default); border-radius: 19px;
    color: var(--text-secondary); font-family: inherit; font-size: 12.5px; padding: 10px 11px; }
  .entry-dock__mini:hover { color: var(--text-primary); background: var(--bg-hover); }
  .entry-dock__mini--on { color: var(--accent-primary); border-color: var(--accent-primary); background: var(--accent-glow); }
  /* the dock floats over the bottom of the viewport, so anything it might scroll a reader to keeps a
     landing strip clear of it */
  .entry .field, .entry .entry-more, .entry .entry-amort { scroll-margin-bottom: 96px; }
  /* reimbursement entry (5d·5): the linkable-expense candidate list + the over-ask */
  .reimb-cands { display: flex; flex-direction: column; gap: 6px; margin-top: 4px; }
  .reimb-cand { display: flex; justify-content: space-between; align-items: center; gap: 10px; width: 100%;
    text-align: left; padding: 8px 12px; border-radius: 10px; border: 1px solid var(--border-default);
    background: none; color: var(--text-secondary); font-family: inherit; font-size: 14px; cursor: pointer; }
  .reimb-cand:hover { background: var(--bg-hover); }
  .reimb-cand__main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
  .reimb-cand__t { color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .reimb-cand__m { font-size: 12px; color: var(--text-tertiary); }
  .reimb-cand__r { flex: 0 0 auto; font-size: 12px; color: var(--accent-gold); }
  /* shared reimbursement→expense finder: a category filter + working search above a scrollable list (no cap) */
  .reimb-finder { display: flex; flex-direction: column; gap: 8px; }
  .reimb-finder__controls { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
  .reimb-finder__controls > * { flex: 1 1 150px; min-width: 0; }
  .reimb-finder__search { font: inherit; padding: 9px 12px; border-radius: 10px; border: 1px solid var(--border-default);
    background: var(--bg-input); color: var(--text-primary); }
  .reimb-finder__search:focus { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-glow); }
  /* locked-category chip: the refund's category is fixed (bulk/Ledger finish) — shown, not pickable */
  .reimb-finder__fixedcat { flex: 0 1 auto; display: inline-flex; align-items: center; padding: 8px 12px; border-radius: 10px;
    background: rgba(124,106,255,.12); color: var(--text-primary); font-weight: 600; font-size: 0.92em;
    border: 1px solid var(--border-subtle); }
  .reimb-finder__hint { margin: 0; font-size: 0.85em; }
  .reimb-finder .reimb-cands { max-height: 300px; overflow-y: auto; }
  .entry-overask { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border-subtle); }
  .entry-overask .entry-hh__btns { margin-top: 8px; }
  /* §28 pattern double-log nudge — calm awareness (purple-tinted), never amber; only at commit */
  .entry-patdup { margin-top: 14px; padding: 10px 12px; background: var(--accent-glow); border: 1px solid var(--border-default); border-radius: 10px; }
  .entry-patdup .entry-hh__btns { margin-top: 8px; }

  /* amortisation — "pay over time" (§4.12): the inline config on the expense form */
  .entry-amort { margin-top: 14px; padding: 12px 14px; border-radius: 12px;
    border: 1px solid var(--border-default); background: var(--bg-surface); }
  .entry-amort__head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
  .entry-amort__title { font-size: 14px; font-weight: 600; color: var(--text-primary); }
  .entry-amort__sub { font-size: 12px; color: var(--text-tertiary); }
  .entry-amort__close { margin-left: auto; }
  .entry-amort .entry-hh__btns { margin-top: 10px; }
  .entry-amort__preview { margin-top: 10px; font-size: 13px; color: var(--accent-primary); }
  /* compound selector beats the base .tx__note (which sets italic) regardless of source order */
  .tx__note.tx__note--amort { font-style: normal; color: var(--text-tertiary); }
  /* §8 pre-funded draw-down — gold = money previously set aside (tone-and-language.md) */
  .tx__note.tx__note--fund { font-style: normal; color: var(--accent-gold); }
  .txd__amort { margin-top: 10px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

  /* pay-over-time plans surface */
  .plan { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex-wrap: wrap;
    padding: 14px 2px; border-top: 1px solid var(--border-subtle); }
  .plan:first-child { border-top: none; }
  .plan--inactive { opacity: 0.7; }
  .plan__main { min-width: 0; flex: 1; }
  .plan__titlerow { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
  .plan__title { font-size: 15px; color: var(--text-primary); word-break: break-word; }
  .plan__shared { font-size: 12px; color: var(--accent-primary); }  /* a household plan seen in a personal view (your share) */
  .plan__sub { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; margin-top: 3px;
    font-size: 13px; color: var(--text-secondary); }
  .plan__type { color: var(--accent-primary); }
  .plan__prog { font-size: 12px; margin-top: 4px; }
  .plan__next { font-size: 12px; margin-top: 2px; }
  .plan__right { display: flex; flex-direction: column; align-items: flex-end; gap: 5px; flex: none; }
  .plan__total { font-size: 15px; color: var(--text-primary); }
  .plan__status { font-size: 11px; padding: 2px 9px; border-radius: 20px; border: 1px solid var(--border-default); }
  .plan__status--active { color: var(--accent-teal); border-color: var(--accent-teal); }
  .plan__status--complete { color: var(--accent-primary); border-color: var(--accent-primary); }
  .plan__status--cancelled { color: var(--text-tertiary); }
  .plan__cancel { font-size: 12px; }
  .plan__confirm { flex-basis: 100%; display: flex; align-items: center; justify-content: space-between; gap: 10px;
    flex-wrap: wrap; margin-top: 8px; padding: 8px 12px; border-radius: 10px; background: var(--bg-surface); }
  .plan__confirmq { font-size: 13px; color: var(--text-secondary); }
  .plan__confirmbtns { display: flex; gap: 8px; flex: none; }

  /* reserves & goals surface (§8) — teal = kept/goals, gold = a milestone reached, purple = shared */
  .reserve { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex-wrap: wrap;
    padding: 14px 2px; border-top: 1px solid var(--border-subtle); }
  .reserve:first-child { border-top: none; }
  .reserve--inactive { opacity: 0.72; }
  .reserve__main { min-width: 0; flex: 1; }
  .reserve__titlerow { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
  .reserve__title { font-size: 15px; color: var(--text-primary); word-break: break-word; }
  .reserve__kind { font-size: 11px; color: var(--accent-teal); border: 1px solid var(--accent-teal);
    border-radius: 20px; padding: 1px 8px; }
  .reserve__shared { font-size: 12px; color: var(--accent-primary); }
  .reserve__reached { font-size: 11px; color: var(--accent-gold); border: 1px solid var(--accent-gold);
    border-radius: 20px; padding: 1px 8px; }   /* a goal reaching 100% — gold milestone celebration (§3/§8) */
  /* v388 — THE FUND BAR, rebuilt in the INTENTIONS register (the human's ask: "the progress bar's track is
     invisible and rounded — I want the whole bar visible, straight, like the intentions one"). Same fix, same
     reasoning as .intent-track__bar: the old track was --bg-surface on a --bg-surface card, so only the fill
     was ever on screen and a nearly-empty fund showed a 3% dot against nothing. The track now carries its own
     background and border, and the corners are SQUARE. Gold throughout — this is fund money (§3). */
  .reserve__bar { position: relative; height: 14px; border-radius: 0; background: var(--bg-input);
    border: 1px solid var(--border-default); overflow: hidden; margin: 10px 0 5px; max-width: 420px; }
  .reserve__barfill { height: 100%; background: var(--accent-gold); border-radius: 0; transition: width .25s ease; }
  .reserve__barfill--done { background: var(--accent-gold); }   /* target reached — gold (§3 milestone) */
  .reserve__barlbl { display: flex; justify-content: space-between; gap: 12px; font-size: 12px;
    color: var(--text-tertiary); font-variant-numeric: tabular-nums; max-width: 420px; }
  .reserve__emoji { font-size: 17px; line-height: 1; flex: none; }
  /* v391: one notch down (19→18 / 17→16 on a phone) — the human read the headline as shouting beside the
     card's other figures. Weight, gold and tabular-nums are unchanged; only the size moves.
     v392 — AND THE WEIGHT COMES OFF THE FAUX BOLD. The human reported the figure as "hazy" on their phone
     and it was not the size: `.reserve__holds` also carries `mono`, i.e. DM Mono, which SHIPS NO WEIGHT
     ABOVE 500. Asking for 600 makes the browser synthesize a bold by smearing the 500 glyphs — crisp text
     beside a blurred number, exactly the standing "cap DM Mono at 500" rule this codebase already writes
     down at .csv-dupr__amt / .csv-counter__big / .ana-mtab td.mono. 500 is its real heaviest; the desktop
     size drops one more notch (18→17) so losing the weight does not read as the figure growing. */
  /* v406 ⑥ — the amount left the title row for its own line directly under the name, so the old
     `margin-left: auto` (which right-floated it against the actions column) goes with it. Size, weight and
     gold are unchanged: this is a move, not a restyle. */
  .reserve__holdsrow { display: flex; margin-top: 3px; }
  .reserve__holds { font-size: 17px; font-weight: 500; color: var(--accent-gold);
    font-variant-numeric: tabular-nums; }
  .reserve__sub { font-size: 12.5px; color: var(--text-tertiary); margin: 2px 0 0; }
  /* v406 ④ — quiet, tertiary, and it simply is not there when the fund has no stamp to show. */
  .reserve__since { font-size: 12px; margin: 2px 0 0; }
  .reserve__pace { font-size: 13.5px; color: var(--text-secondary); margin: 7px 0 0; }
  .reserve__pace b { color: var(--text-primary); font-weight: 600; }
  .reserve__multiera { font-size: 12.5px; margin: 8px 0 0; max-width: 56ch; }
  /* the step ladder. v403 gave it AXES ("otherwise it's just a line"): a LEFT GUTTER carrying the top of the
     money scale and the zero line's own value, and the two end months in a row under the plot. All four
     labels are HTML, never SVG <text> — the svg is preserveAspectRatio='none', so anything inside it is
     stretched with the box. The money labels are absolutely positioned at the same y the line uses (set
     inline, as a %), which is why the gutter is the plot's padding rather than a flex column: the label has
     to be free to sit anywhere vertically. Sparkline register throughout — tertiary, tiny, no ticks. */
  .reserve__spark { margin: 12px 0 2px; max-width: 420px; }
  .reserve__sparkplot { position: relative; padding-left: 54px; }
  .reserve__sparksvg { width: 100%; height: 56px; display: block; }
  .reserve__sparkline { stroke: var(--accent-gold); stroke-width: 2; }
  .reserve__sparkbase { stroke: var(--text-tertiary); stroke-width: 1; }   /* the zero line — soft grey (v347), never red */
  .reserve__sparkval { position: absolute; left: 0; width: 48px; text-align: right; line-height: 1;
    font-size: 10.5px; color: var(--text-tertiary); font-variant-numeric: tabular-nums; }
  /* zero sits ON the baseline, so its label hangs ABOVE the rule rather than dropping into the month row */
  .reserve__sparkval--zero { transform: translateY(-100%); }
  .reserve__sparkaxis { display: flex; justify-content: space-between; gap: 10px; padding-left: 54px;
    margin-top: 3px; font-size: 10.5px; color: var(--text-tertiary); font-variant-numeric: tabular-nums; }
  .reserve__sparkcap { font-size: 11.5px; margin: 2px 0 0; max-width: 56ch; }
  /* the fund's own recent story — gold IN (money set aside), lavender OUT (spending, --accent-out) */
  .reserve__events { margin: 12px 0 0; border-top: 1px solid var(--border-subtle); padding-top: 8px; max-width: 460px; }
  .reserve__ev { display: flex; align-items: baseline; gap: 10px; font-size: 13px; padding: 4px 0; }
  .reserve__evd { color: var(--text-tertiary); flex: 0 0 auto; min-width: 68px; }
  /* the label WRAPS rather than ellipsising. At 390px "Paid from this fund — Bunnings" was being cut to
     "Paid from t…", which is not a shortened label, it is a deleted one — and the merchant name is the whole
     point of the line. Two short lines beat one meaningless one. (Found at the eyeball gate.) */
  .reserve__evl { flex: 1 1 auto; min-width: 0; color: var(--text-secondary); overflow-wrap: anywhere; }
  .reserve__evamt { flex: 0 0 auto; font-variant-numeric: tabular-nums; }
  .reserve__evamt--in { color: var(--accent-gold); }
  .reserve__evamt--out { color: var(--accent-out); }
  .reserve__seeall { display: inline-block; margin-top: 7px; font-size: 13px; color: var(--accent-primary); }
  .reserve__delete { font-size: 12px; color: var(--accent-warm); }   /* destructive = amber (§3), never red */
  .reserve__undo { flex-basis: 100%; display: flex; align-items: center; justify-content: space-between; gap: 10px;
    margin-top: 8px; padding: 9px 12px; border-radius: 10px; background: var(--bg-surface);
    border: 1px solid var(--border-subtle); }
  .reserve__undoq { font-size: 13px; color: var(--text-secondary); }
  .reserve__undogo { font-size: 13px; color: var(--accent-primary); font-weight: 600; }
  .reserve__busy { margin-top: 4px; }
  .reserve__emojibtn { margin: 0; }
  @media (max-width: 560px) {
    .reserve__holds { font-size: 16px; }
    .reserve__evd { min-width: 58px; }
    /* the phone card is narrow: the gutter gives back 4px and the labels drop half a point, so the plot
       keeps its width and the two month captions never meet in the middle. 44px is not arbitrary —
       "A$128.5k", the widest tick under a million, measures 41px here (asserted in the phone arm). */
    .reserve__sparkplot, .reserve__sparkaxis { padding-left: 50px; }
    .reserve__sparkval { width: 44px; }
    .reserve__sparkval, .reserve__sparkaxis { font-size: 10px; }
  }
  .reserve__bartick { position: absolute; top: 0; width: 2px; height: 100%; background: var(--bg-elevated); }
  .reserve__bartick--hit { background: var(--accent-gold); }    /* a 25/50/75% milestone reached */
  .reserve__prog { font-size: 13px; color: var(--text-secondary); }
  .reserve__people { font-size: 12px; margin-top: 4px; word-break: break-word; }
  .reserve__mine { font-size: 12px; color: var(--accent-primary); margin-top: 2px; }
  .reserve__drawn { font-size: 12px; margin-top: 2px; }
  .reserve__right { display: flex; flex-direction: column; align-items: flex-end; gap: 5px; flex: none; }
  .reserve__status { font-size: 11px; padding: 2px 9px; border-radius: 20px; border: 1px solid var(--border-default); }
  .reserve__status--active { color: var(--accent-teal); border-color: var(--accent-teal); }
  .reserve__status--closed { color: var(--text-tertiary); }
  .reserve__status--released { color: var(--accent-primary); border-color: var(--accent-primary); }
  .reserve__give, .reserve__editbtn, .reserve__release, .reserve__archive { font-size: 12px; }
  .reserve__archive { color: var(--accent-warm); }   /* Remove = amber, destructive (Bible §3) */
  .reserve__form { flex-basis: 100%; margin-top: 8px; padding: 12px; border-radius: 10px; background: var(--bg-surface); }
  .reserve__formlabel { display: block; font-size: 13px; color: var(--text-secondary); margin-bottom: 6px; }
  .reserve__form input[type="number"] { width: 100%; }
  .reserve__who { font-size: 13px; color: var(--text-secondary); margin: 10px 0 6px; }
  .reserve__formbtns { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; }
  .reserve__confirm { flex-basis: 100%; display: flex; align-items: center; justify-content: space-between; gap: 10px;
    flex-wrap: wrap; margin-top: 8px; padding: 8px 12px; border-radius: 10px; background: var(--bg-surface); }
  .reserve__confirmq { font-size: 13px; color: var(--text-secondary); }
  .reserve__confirmbtns { display: flex; gap: 8px; flex: none; }
  .reserve__confirm { flex-wrap: wrap; }
  /* min-width 100% (NOT max-width) — a flex item's max-width CLAMPS its flex base size, so a 62ch cap let the
     "full-width" line slip up beside the buttons on a wide strip instead of wrapping. min wins the clamp.
     Secondary (not tertiary) text — the explainers must be clearly readable (human review, 2026-07-15). */
  .reserve__relinfo { flex-basis: 100%; min-width: 100%; margin: 0; font-size: 12.5px; color: var(--text-secondary); }
  .reserve__howlink { display: block; margin-top: 8px; }
  /* `.reserve__started` RETIRED v406 ⑤ — the standalone named-split line is gone; the named split now rides
     the sub-line (`.reserve__sub`) as the fund's ONE starting sentence. */
  .reserve__starthelp { font-size: 12.5px; margin: -4px 0 12px; max-width: 52ch; }
  .reserve__unrelease { font-size: 12px; color: var(--accent-teal); }
  .reserve__howinfo { max-width: 62ch; margin: 6px 0 0; font-size: 13px; color: var(--text-secondary); }
  /* expanded list of a reserve's linked draw-down expenses (each jumps to its ledger row) */
  .reserve__draws { flex-basis: 100%; margin-top: 8px; padding: 4px 12px; border-radius: 10px; background: var(--bg-surface); }
  .reserve__draw { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-top: 1px solid var(--border-subtle); }
  .reserve__draw:first-child { border-top: none; }
  .reserve__drawmain { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 1px; }
  .reserve__drawt { font-size: 13px; color: var(--text-primary); word-break: break-word; }
  .reserve__drawm { font-size: 11px; }
  .reserve__drawamt { font-size: 13px; color: var(--text-secondary); flex: none; }
  .reserve__drawgo { font-size: 12px; flex: none; }
  /* money input with a base-currency prefix (the Target field) */
  .reserve__money { display: flex; align-items: center; gap: 4px; border: 1px solid var(--border-default);
    border-radius: 10px; padding: 0 10px; background: var(--bg-surface); }
  .reserve__money:focus-within { border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-glow); }
  .reserve__moneysym { flex: none; color: var(--text-secondary); font-size: 14px; }
  .reserve__money input { flex: 1; min-width: 0; border: none; background: none; box-shadow: none; outline: none; padding: 10px 0; }
  .reserve__money input:focus { border: none; box-shadow: none; outline: none; }

  /* ledger / transaction list */
  .tx { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
    padding: 12px 2px; border-top: 1px solid var(--border-subtle); }
  .tx:first-child { border-top: none; }
  .tx--pending { opacity: 0.72; }
  .tx__main { min-width: 0; flex: 1; }
  .tx__title { font-size: 15px; color: var(--text-primary); word-break: break-word; }
  .tx__sub { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; margin-top: 3px; }
  .tx__cat { font-size: 13px; color: var(--text-secondary); }
  .tx__date { font-size: 12px; color: var(--text-tertiary); }
  .tx__pending { font-size: 12px; color: var(--accent-primary); }
  .tx__note { font-size: 13px; color: var(--text-secondary); font-style: italic; margin-top: 3px; word-break: break-word; }
  /* Slice A — the ledger panel row. Line 2 (.tx__meta) FLOWS: category, badges, tags and the note inline,
     wrapping only when the next item would reach the reserved gap. The gap itself is the info column's right
     padding (var(--gut) = 26px, ratified) — content wraps at that content-box edge, so the amounts always keep
     their breathing room. Scoped to .ledger-list so the embedded/peek/arc rows are untouched. */
  .ledger-list .tx { align-items: flex-start; }
  .ledger-list .tx__main { padding-right: var(--gut); }
  /* ── v384 — A NAME NEVER BREAKS MID-WORD; THE AMOUNT TAKES ITS OWN LINE INSTEAD ────────────────────
     The defect, off the human's own phone shot: "Groce/ries" beside a nine-digit amount. Row amounts are
     the RECORD and are exact to the cent by ratified rule (v382), so a huge one is legitimately ~150px
     wide — and with `flex: 1` + `min-width: 0` the info column's hypothetical size was ZERO, so it never
     asked the row to wrap. It just kept shrinking until a word had to be cut in half.
     TWO changes, and they only work as a pair:
       · `flex-basis: min-content` gives the info column a hypothetical size equal to its LONGEST WORD, so
         flex line-collection wraps `.tx__right` (amount + action slot) onto its own line rather than
         squeezing the name below a word. `min-width: 0` STAYS, so in the pathological case (a single
         unbroken 40-character merchant) it can still shrink and break rather than overflow the card.
       · `overflow-wrap: break-word` replaces the legacy `word-break: break-word`, which is an alias for
         `overflow-wrap: anywhere` — and `anywhere` is precisely what makes min-content one character wide.
         `break-word` breaks a word only as a last resort and leaves the intrinsic size at the longest word,
         which is what the flex-basis above needs to be true.
     `margin-left: auto` keeps the amount right-aligned on whichever line it lands (the row's
     space-between would otherwise park a wrapped right column at the left). Scoped to `.ledger-list` —
     the embedded/peek/arc rows keep today's geometry exactly. */
  .ledger-list .tx__main { flex: 1 1 min-content; }
  .ledger-list .tx__title { word-break: normal; overflow-wrap: break-word; }
  .ledger-list .tx__right { margin-left: auto; }
  /* ── v378 — A LONG PRESS IS A GESTURE, NOT A TEXT SELECTION ────────────────────────────────────────
     The human's v377 phone review: holding a row highlighted its text and the selection "sometimes"
     swallowed the gesture. Both symptoms are one cause — the OS selection engages on the hold, takes the
     pointer, and Chrome fires pointercancel, which cancels the armed long-press timer. So the fix belongs
     here, not in the timer: a ledger row is a control, and a control does not select. `touch-action: pan-y`
     keeps vertical scrolling with the browser (which is also what cancels a press during a real scroll).
     SCOPED to .ledger-list rows — embedded/peek/arc .tx rows elsewhere are untouched — and the expanded
     detail opts BACK IN, because a note is text a person may legitimately want to copy. */
  .ledger-list .tx { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
    -webkit-tap-highlight-color: transparent; touch-action: pan-y; }
  .ledger-list .tx__detail { -webkit-user-select: text; user-select: text; -webkit-touch-callout: default; }
  .tx__meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; margin-top: 3px; }
  .tx__meta .tx__tag { margin: 0; }                    /* tags are inline pills in the flow, not a block strip */
  .tx__meta .tx__note { margin-top: 0; }               /* the note flows inline; block awareness notes below keep their top margin */
  /* the day header — the date lifted out of every row (ratified 2026-07-26). A quiet divider with the day's net. */
  .dayhdr { display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
    padding: 13px 2px 6px; border-top: 1px solid var(--border-subtle); }
  .dayhdr:first-child { border-top: none; }
  .dayhdr + .tx { border-top: none; }                  /* the header already draws the divider — no double line */
  .dayhdr__d { font-size: 12.5px; font-weight: 600; color: var(--text-secondary); }
  .dayhdr__s { font-size: 12px; color: var(--text-tertiary); font-variant-numeric: tabular-nums; }
  /* Slice C — bulk selection: the tick box appears only in select mode (reading is unchanged), selection
     tints the row, and "to here" range-selects within the current filter. */
  .ledger-list .tx { position: relative; }
  /* the tick: a smaller 16px box in a 38px-tall grid cell that MATCHES the category avatar's height, so the
     box centres on the avatar (not floating up beside the merchant line). Human, 2026-07-31: "smaller … and
     centred with the icon". */
  /* ── v387 — THE TICK TAKES THE AVATAR'S SLOT, IT DOES NOT ASK FOR NEW WIDTH ────────────────────────
     Was: a 22px flex item of its own, which with the row's 12px gap slid EVERY row 34px sideways the
     instant selection engaged (the human: "everything shifts a little bit to make space for that
     checkbox"). It is now absolutely positioned inside `.catav`, a box that is already 38×38 on every row
     in both states — so entering selection changes no row's geometry at all. The avatar's own glyph is
     hidden underneath: `visibility` for the icon/emoji ELEMENTS, and `color: transparent` on the wrap for
     the monogram fallback, which is a bare text node with no element to hide. `.tx__tickbox` re-declares
     its own colour, so the check mark is unaffected by that transparent. */
  .tx__tick { position: absolute; inset: 0; display: grid; place-items: center; cursor: pointer; }
  .catav--ticked { position: relative; color: transparent; }
  .catav--ticked > svg, .catav--ticked > .catav__emoji { visibility: hidden; }
  .tx__tickbox { width: 16px; height: 16px; border-radius: 5px; border: 1.5px solid var(--border-strong);
    background: var(--bg-input); display: grid; place-items: center; color: #fff;
    /* v392: nothing may ever paint outside the rounded square again — a clip, not a hope. */
    overflow: hidden; }
  .tx--sel { background: color-mix(in srgb, var(--accent-primary) 10%, transparent); border-radius: 8px; }
  .tx--sel .tx__tickbox { background: var(--accent-primary); border-color: var(--accent-primary); }
  /* ── v392 — THE TICK LEAKED OUT OF ITS BOX, AND IT WAS A CASCADE COLLISION, NOT A FONT ──────────────
     The human saw the tick draw oversized and spill past the rounded square on their phone. It is not a
     font glyph (it has been inline SVG geometry — HUI.icons.check — since the box was built) and it is
     not device-specific: v387 moved the tick INSIDE `.catav`, which already carries the app-wide rule
     `.catav svg { width: 20px; height: 20px }` for a category icon. That rule and `.tx__tickbox svg`
     have IDENTICAL specificity (0,1,1), and `.catav svg` is ~1150 lines LOWER in this file — so source
     order handed it the win and the 11px tick was being painted at 20px inside a 16px box. Every screen
     rendered it that way; the phone is just where it was noticed.
     THE FIX is specificity that cannot be lost to source order (three classes beats one class + one
     element), belt-and-braces `!important`-free, plus the `overflow: hidden` above so a future rule of
     any weight still cannot push ink outside the square. The zero-reflow property v387 bought is
     untouched: the tick is still absolutely positioned inside the 38×38 avatar slot and still claims no
     width of its own. */
  .tx__tickbox > .tx__tickmark { width: 11px; height: 11px; display: block; }
  .tx__tohere { position: absolute; right: 4px; bottom: 4px; font-size: 10.5px; color: var(--accent-primary);
    background: var(--bg-elevated); border: 1px solid var(--accent-primary); border-radius: 999px; padding: 1px 9px; cursor: pointer; }
  .tx__tohere:hover { background: var(--bg-hover); }
  /* the sticky bar is now a COLUMN: a top action row that can GROW an editor drawer in place (no modal) —
     human, 2026-07-31: "make that bottom thing only become slightly bigger … shows all the editing options". */
  .ledger-selbar { position: sticky; bottom: 0; z-index: 7; display: block;
    background: var(--bg-elevated); border: 1px solid var(--accent-primary); border-radius: 12px; padding: 10px 14px; margin-top: 12px;
    box-shadow: 0 -6px 20px rgba(0,0,0,.3); }
  .ledger-selbar__top { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
  .ledger-selbar__n { font-size: 13.5px; font-weight: 600; margin-right: auto; }
  .ledger-selbar__end { border-color: var(--border-strong); color: var(--text-secondary); }
  .ledger-selbar__drawer { margin-top: 10px; border-top: 1px solid var(--border-subtle); padding-top: 10px; }
  .ledger-selbar__drawer:empty { display: none; }
  /* the editor drawer — one row of option chips, and the ONE active control (never every field at once) */
  .bulkbar-chips { display: flex; flex-wrap: wrap; gap: 6px; }
  .bulkchip { background: var(--bg-input); border: 1px solid var(--border-default); border-radius: 999px;
    padding: 5px 12px; font: inherit; font-size: 12.5px; color: var(--text-secondary); cursor: pointer; white-space: nowrap; }
  .bulkchip:hover { background: var(--bg-hover); color: var(--text-primary); }
  .bulkchip.is-on { border-color: var(--accent-primary); color: var(--text-primary);
    background: color-mix(in srgb, var(--accent-primary) 18%, var(--bg-input)); font-weight: 600; }
  /* Remove is destructive → amber (never red; red is only the zero line) */
  .bulkchip--del { color: var(--accent-warm); border-color: color-mix(in srgb, var(--accent-warm) 45%, var(--border-default)); }
  .bulkchip--del:hover { background: color-mix(in srgb, var(--accent-warm) 14%, var(--bg-input)); color: var(--accent-warm); }
  .bulkchip--del.is-on { border-color: var(--accent-warm); color: var(--accent-warm);
    background: color-mix(in srgb, var(--accent-warm) 18%, var(--bg-input)); }
  .bulkbar-ctl { margin-top: 11px; }
  .bulkbar-ctl:empty { display: none; }
  .bulkbar-field { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
  .bulkbar-field > label { font-size: 12px; color: var(--text-secondary); min-width: 56px; }
  .bulkbar-field .catpick, .bulkbar-field .dropdown { flex: 1 1 150px; min-width: 0; }
  .bulkbar-input { flex: 1 1 140px; min-width: 0; background: var(--bg-input); border: 1px solid var(--border-default);
    color: var(--text-primary); border-radius: 8px; padding: 8px 10px; font: inherit; font-size: 13px; }
  .bulkbar-input:focus { outline: 2px solid var(--accent-primary); outline-offset: 1px; }
  .bulkbar-hint { font-size: 11.5px; color: var(--text-tertiary); margin: 8px 0 0; }
  .bulkbar-link { background: none; border: 0; color: var(--accent-primary); font: inherit; font-size: 12px;
    cursor: pointer; padding: 0; text-decoration: underline; }
  .bulkbar-confirm { background: color-mix(in srgb, var(--accent-warm) 9%, var(--bg-input));
    border: 1px solid color-mix(in srgb, var(--accent-warm) 40%, transparent); border-radius: 10px; padding: 11px 12px; }
  .bulkbar-confirm__t { font-size: 13px; color: var(--text-primary); margin: 0 0 3px; }
  .bulkbar-confirm__s { font-size: 12px; color: var(--text-secondary); margin: 0 0 10px; }
  .bulkbar-confirm__btns { display: flex; gap: 8px; justify-content: flex-end; }
  /* the drawer lives in the sticky bar at the VIEWPORT BOTTOM, so its pickers must open UPWARD — a downward
     popup falls off-screen and only its header shows (human-caught 2026-07-31). Scoped to the drawer, so the
     same catpick / dateInput open downward everywhere else, unchanged. */
  .ledger-selbar__drawer .combo__list,
  .ledger-selbar__drawer .catpick__box,
  .ledger-selbar__drawer .datepick__pop { top: auto; bottom: calc(100% + 5px); }
  .tx__right { display: flex; align-items: center; gap: 8px; flex: none; }
  /* the actions slot is ALWAYS rendered at a fixed width (empty when a row has no actions) so the
     amounts sit in ONE aligned column whether or not a row is deletable (share rows aren't). */
  .tx__act { flex: none; width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center; }
  .tx__amt { font-size: 15px; }
  .tx__amt--out { color: var(--accent-out); }   /* outgoing money (lavender trial 2026-06-23; was amber §3) */
  .tx__amt--in { color: var(--accent-teal); }     /* income = teal */
  .tx__amt--reimb { color: var(--accent-gold); }  /* reimbursement = gold — money returned, distinct from income at a glance (§3 amendment, 5d·5 refine) */
  .tx__amt--neutral { color: var(--text-secondary); }  /* transfer/provision = neutral money movement, muted — neither spend nor income (§5.1/§8) */
  .tx__shared { font-size: 12px; color: var(--accent-primary); }   /* a household tx seen in a personal view (share) */
  .tx__fx { font-size: 12px; color: var(--text-tertiary); }        /* the original foreign amount beside the base (§9) */
  .tx__amt--pending { color: var(--text-tertiary); font-style: italic; }  /* a not-yet-converted foreign row */
  .txd__fxnote, .txd__fxattr { font-size: 12px; margin-top: 6px; }
  .settings-fieldnote { font-size: 12px; margin: -4px 0 10px; }  /* the base-currency change note sits under its field */
  /* global toast — a brief background-op failure message, shown app-wide (optimistic-write rollback, §20) */
  .hearth-toast { position: fixed; left: 50%; bottom: 22px; transform: translate(-50%, 16px); z-index: 60;
    max-width: 90%; padding: 11px 16px; border-radius: 12px; font-size: 14px; pointer-events: none;
    background: var(--bg-elevated); color: var(--text-primary); border: 1px solid var(--border-default);
    box-shadow: 0 8px 28px rgba(0,0,0,0.45); opacity: 0; visibility: hidden; transition: opacity .18s, transform .18s; }
  .hearth-toast--show { opacity: 1; visibility: visible; transform: translate(-50%, 0); }
  .hearth-toast--err { border-color: var(--accent-warm); color: var(--accent-warm); }
  /* S3: an actionable toast (e.g. "Comparison cleared — Undo") lays its message + one purple action button in a row
     and becomes pointer-interactive (the ordinary status toast stays pointer-events:none so it never blocks). */
  .hearth-toast { display: inline-flex; align-items: center; gap: 14px; }
  .hearth-toast--action { pointer-events: auto; }
  .hearth-toast__action { appearance: none; background: none; border: none; font-family: inherit; font-size: 14px;
    font-weight: 600; cursor: pointer; color: var(--accent-primary); padding: 2px 4px; border-radius: 8px; flex: none; }
  .hearth-toast__action:hover { text-decoration: underline; }
  .tx__owner { font-size: 12px; color: var(--accent-primary); }    /* whose income (a member's income in the household view) */

  /* ── Dashboard / financial summary (Phase 9a, §4) — width-aware data screen ───────────────────── */
  .dash-block { margin-bottom: 18px; }
  .dash-eranote, .dash-note { margin: 8px 2px; }
  .dash-empty { text-align: center; }
  /* 2b — this month's plans (reuses the .intent-track* row styles from the Intentions Track view) */
  .dash-plans__title { margin: 0 0 6px; font-size: 1rem; }
  .dash-plans__foot { display: flex; justify-content: flex-end; margin-top: 8px; }
  .dash-plans__link { color: var(--accent-primary); text-decoration: none; font-size: 0.92rem; cursor: pointer; }
  .dash-plans__link:hover { text-decoration: underline; }
  .dash-plans__empty { display: inline-block; margin-top: 2px; }
  /* headline tiles: Income · Spending — side by side, stacked on a phone.
     v409: this strip was THREE across (Income · Spending · Kept rate). The kept-rate tile is retired
     (the human's own proposal, 2026-08-26) and its percentage now sits in brackets beside the kept
     figure in the Kept card below, so the strip is a pair. Two 1fr tracks rather than three with a
     hole in them — a grid that still declares a column nobody fills is how a retired tile leaves a
     ghost. Analysis's own strip overrides this wholesale (.ana-tiles, below). */
  .dash-tiles { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gap); margin-bottom: var(--gap); }
  .dash-tile { background: var(--bg-surface); border: 1px solid var(--border-subtle); border-radius: var(--radius); padding: 16px 18px; min-width: 0; }
  .dash-tile__label { font-size: 13px; color: var(--text-secondary); margin-bottom: 6px; }
  /* v379 (the fit rule, big-number round). A grid item defaults to min-width:auto and PUSHES its track wide
     rather than overflowing, so an 8-digit amount used to shove the whole three-across strip off the card
     edge and clip mid-digit. `min-width: 0` above makes the track honour its 1fr; nowrap+hidden here makes
     the value the thing that overflows, which is what HUI.fitValues measures (scrollWidth > clientWidth) and
     steps the type down for. The ellipsis is the floor, not the plan: a figure that still will not fit at
     the smallest step is swapped for its compact form with the exact amount in `title`. tabular-nums stops
     the digits jittering as the type steps. */
  /* ── v417 ④ — A MONEY AMOUNT INSIDE A SENTENCE ────────────────────────────────────────────────────
     Every money ELEMENT below already carries `white-space: nowrap`, which is why a tile has never
     split "-A$6,001.31" across a line. A figure filled into a translated SENTENCE has no element of
     its own — `t(key, {amount})` is one text node — and U+002D HYPHEN-MINUS is a line-break
     opportunity, so the human's screenshot showed a bare "-" hanging at the end of a line with the
     money starting the next. `HUI.tProse` (app.js) gives each money slot this span. It is a plain
     utility on purpose: any future sentence with a figure in it uses the same one. */
  .amt-nb { white-space: nowrap; }
  .dash-tile__value { font-weight: 500; line-height: 1.1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-variant-numeric: tabular-nums; }
  /* v386 — ONE SIZE, TWO PEERS. The Kept card's headline figure and the Income/Spending tile figures are
     the same rank of thing, so they share this declaration (and its phone step-down below) rather than
     carrying two numbers that drifted apart in v385. Turn it here and both move. */
  .dash-tile__value, .dash-surplus__fig { font-size: 26px; }
  .dash-tile__value--muted { color: var(--text-tertiary); }
  /* v382 - THE ONE REVEAL. Any element carrying data-exact holds a compacted or capped figure and will
     say the real one on hover (title) or tap (the anchored note, app.js). The affordance has to be
     DISCOVERABLE on a phone, where there is no hover to find it with, and must not shout on a card that
     may hold several of them: a hairline dotted underline in the existing border token, offset clear of
     the digits. No new colour, no new component. */
  [data-exact] { cursor: help; text-decoration: underline dotted 1px var(--border-default); text-underline-offset: 3px; }
  [data-exact]:hover { text-decoration-color: var(--text-tertiary); }
  .dash-tile__sub { margin-top: 4px; }
  /* ── v409 — THE KEPT FIGURE AND ITS RATE, SIDE BY SIDE (HUI.keptPct) ────────────────────────────
     The rate used to own a whole tile. It is now a bracketed secondary beside the figure it is a rate
     OF, on all three surfaces that show a kept figure (Analysis's Kept tile, the WHAT=Kept subject
     tile, Home's Kept card headline).
     `min-width: 0` on the figure is the load-bearing declaration: it lets the amount shrink to its own
     ellipsis so the rate is never the thing that gets clipped, and it is what keeps HUI.fitValues
     measuring a real, constrained box (the figure is the [data-fit] element; the rate is its sibling,
     because fitOne rewrites textContent and would delete a child span). `baseline` so the small
     percentage sits on the amount's baseline rather than floating mid-digit. */
  /* ⚠ NO `flex-wrap` HERE, and that is a decision this round made and then UNMADE — worth the two lines.
     v410 first added `flex-wrap: wrap` so a big bracket beside a seven-figure amount could drop to its own
     line rather than crush the figure. It reads fine, and it is WRONG: the ratified behaviour is that the
     rate takes its space first and the FIT LADDER steps the amount down around it (see HUI.keptPct), which
     keeps the two on one line at every width. Wrapping quietly replaced that ladder with a line break, and
     kept-clarity's "…sitting BESIDE the kept figure, on its line" claim caught it on the Analysis tile —
     which is exactly the assertion's job. If a future round wants the bracket on its own line, that is a
     design change to ask for, not a CSS convenience. */
  .dash-keptfig { display: flex; align-items: baseline; gap: 7px; min-width: 0; }
  .dash-keptfig > .dash-tile__value, .dash-keptfig > .dash-surplus__fig { min-width: 0; flex: 0 1 auto; }
  /* ── v411 — ROUND BRACKETS · GREY GLYPHS · A NUMBER ONE STEP SMALLER ───────────────────────────
     HISTORY, because this has now reversed four times and the next reader needs the whole arc: v409
     made the bracket a quiet 14px secondary; v410 gave it the figure's own size AND colour (*"let the
     % match font size and colour of kept"*); v411 is the human reading THAT on their own phone and
     splitting the two halves of it apart —
       · SIZE: the percentage is *"one step smaller"* than the amount, a FIXED ~30% ratio. It no longer
         size-matches, and that is the point: a same-size bracket is what squeezed the amount down the
         fit ladder into the compact form they photographed ("A$1.8k" for A$1,763.19). The number
         declared here is the ratio against the figure size on the line above (26 × 0.7 = 18.2); JS
         re-applies the SAME ratio against whatever the figure actually settles at (HUI fitBrackets),
         so a screen with no fitting to do never gets an inline style at all.
       · COLOUR: only the NUMBER wears the figure's teal/red. The BRACKET GLYPHS are chrome, and they
         are `--text-secondary` — deliberately not tertiary: at 18px beside a bold coloured figure,
         tertiary parentheses read as a rendering artefact rather than as punctuation. The stray
         coloured "·" the human saw is gone with the delta that needed it.
     THE COLOUR IS STILL STRUCTURAL: every rule below keys off the figure element sitting immediately
     BEFORE the bracket in `.dash-keptfig`, so the number inside can never be painted differently from
     the figure it is a proportion of. DM Mono for the same reason DM Mono is on the figure. Weight
     capped at 500 — DM Mono ships nothing heavier and 600 would smear. */
  .dash-keptpct { flex: none; font-weight: 500; font-variant-numeric: tabular-nums; white-space: nowrap;
    color: var(--text-secondary); min-width: 0; }
  .dash-tile__value + .dash-keptpct, .dash-surplus__fig + .dash-keptpct { font-size: 18.2px; }
  .dash-keptpct__v { font-weight: 500; }
  .dash-keptpct__b { color: var(--text-secondary); }   /* the ( and the ) — chrome, never a value */
  /* the four ways a kept figure is painted, and the NUMBER inside the bracket beside them. §3: red is
     ONLY the genuine below-zero case, and it is the same red the figure is already wearing. */
  .dash-amt--pos + .dash-keptpct .dash-keptpct__v { color: var(--accent-teal); }
  .dash-amt--neg + .dash-keptpct .dash-keptpct__v { color: var(--accent-red); }
  .ana-tile--kept .dash-tile__value + .dash-keptpct .dash-keptpct__v { color: var(--accent-teal); }
  .ana-tile--neg .dash-tile__value + .dash-keptpct .dash-keptpct__v { color: var(--accent-red); }
  /* …and the belt-and-braces twin of the same statement, for a host that carries neither class. */
  .dash-keptpct--neg .dash-keptpct__v { color: var(--accent-red); }
  /* ── THE DELTA'S OWN LINE (v411) ───────────────────────────────────────────────────────────────
     It used to live INSIDE the bracket, and that is what widened the whole page: a nowrap run holding
     "[-128.5% · ↓119.5 pts vs Feb]" gave the Kept card a ~310px minimum content width, the card became
     the widest thing on a 390px phone, and "Where it went" beneath it was dragged sideways and clipped
     (the human's March screenshot). Out here it is exactly the Income/Spending tiles' delta chip —
     same class, same 12px, same teal / muted / tertiary — one short line under the figure it describes.
     NO `margin-top` of its own: `.dash-surplus__fig` already carries 6px below itself, which lands the
     chip within a pixel of where the tiles' own chips sit under their figures. */
  .dash-keptdelta { margin-top: 0; }
  .dash-tile--in .dash-tile__value { color: var(--accent-teal); }
  .dash-tile--out .dash-tile__value { color: var(--accent-out); }
  /* month picker row */
  .dash-controls { margin-bottom: var(--gap); }
  /* a quiet "vs last month" delta under a tile — teal when favourable, muted otherwise (never alarming) */
  .dash-delta { margin-top: 5px; font-size: 12px; color: var(--text-tertiary); }
  .dash-delta--good { color: var(--accent-teal); }
  .dash-delta--soft { color: var(--text-secondary); }
  /* body: surplus structure | where it went — two columns on a wide screen, stacked on a phone */
  .dash-cols { display: grid; grid-template-columns: 1fr; gap: var(--gap); }
  .dash-card__title { font-size: 15px; margin: 0 0 12px; color: var(--text-primary); }
  /* the surplus (Kept → − Set aside → Free to use) */
  /* v385 — the Kept figure is the card's headline now: its label row and explainer hint retired, so the
     heading above it does the naming and the figure takes the weight the total row used to carry. Weight
     stays at 500 — DM Mono ships nothing heavier and a 600 here would faux-bold into a smear.
     v386 — THE 30px IS VETOED. The human, on their own phone, over the shipped v385: "I don't know why the
     kept number is much larger font… get the font back in line. It's unnecessarily large." The figure is a
     PEER of the Income/Spending tile figures, not a bigger sibling, so it takes their size EXACTLY — the
     declaration below and the phone step-down are both written so the two can only ever move together.
     What v385 actually earned survives untouched: the retired label row, the retired hint, the weight.
     THE SIZE ITSELF IS NOT DECLARED HERE — it rides the shared `.dash-tile__value, .dash-surplus__fig`
     rule beside the tile it must match (and its phone step-down), so there is ONE number to turn. */
  .dash-surplus .copy-head { margin: 0 0 6px; }
  /* v411 — `overflow: hidden` + the ellipsis, exactly as its declared peer `.dash-tile__value` has had
     since v379. Without it a figure that has yielded its width (min-width:0, inside `.dash-keptfig`)
     simply paints over the card's edge — visible on the human's March screenshot as a red figure
     escaping its own card. The ellipsis is the floor and nothing should ever reach it: fitPeers sizes
     this figure with its tile siblings and fitBrackets yields the bracket first. */
  .dash-surplus__fig { font-weight: 500; line-height: 1.15; white-space: nowrap; margin: 0 0 6px;
    overflow: hidden; text-overflow: ellipsis; }
  /* ── v398 — THE WEIGHTED ROWS (mock ②, approved 2026-08-24) ────────────────────────────────────────
     `.dash-surplus__row` is re-tenanted: it named the v385 label/amount rows, which v386 retired, and
     these rules sat rendering nothing for two rounds. Their replacements are the mock's rows —
     dot · label · mono amount — and the OLD `__label` / `__hint` / `__amt` / `__row--total` / `__rule`
     declarations go with the markup they styled. (`__amt` is still referenced by three poc readers
     capturing PRE-v386 state; those are re-pointed in this round, per the v387 "a moved figure's
     snapshot reader follows it in the same round" rule.)
     `min-width: 0` on the label is what lets a long fund name wrap instead of pushing the amount off
     the row (the standing phone rule); the amount is `nowrap` because a split figure is unreadable. */
  .dash-surplus__rows { margin: 10px 0 0; border-top: 1px solid var(--border-subtle); padding-top: 2px; }
  .dash-surplus__row { display: flex; align-items: baseline; gap: 9px; padding: 6px 0; font-size: 15px; }
  /* THE DOT ALIGNS TO THE FIRST LINE, NOT TO THE BLOCK. Found by EYE on this round's own 390px
     screenshot: "Still set aside — Holiday Fund" wraps on a phone, and `align-self: center` then parked
     the dot half-way down a two-line label, pointing at nothing. `flex-start` + half a line of offset
     puts it beside the first word, where a bullet belongs, at every width. */
  .dash-surplus__dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; align-self: flex-start; margin-top: 6px; }
  .dash-surplus__dot--gold { background: var(--accent-gold); }
  .dash-surplus__dot--teal { background: var(--accent-teal); }
  .dash-surplus__rowlbl { flex: 1 1 auto; min-width: 0; color: var(--text-secondary); overflow-wrap: break-word; }
  .dash-surplus__rowfund { color: var(--text-primary); font-weight: 600; }
  /* ── v410 ② — THE HIERARCHY SWAP ON THE NEGATIVE-KEPT STORY (human-directed 2026-08-27) ───────────
     Their words, asked whether the v400 why-line shade was right: *"just switch the colour with the
     words 'Still set aside' so those particular words in the tile get a lower heirarchy and then the
     whole line get a bump up otherwise all is good."*
     TWO declarations, and they are a SWAP — the ink each element wears trades places, and nothing else
     about either changes. No size moves, no figure moves, no element is renamed, and `.dash-overall__why`
     is deliberately untouched ("otherwise all is good" — the v400 shade stands).
     SCOPE. `--still` is set by HUI.keptStory only on the NEGATIVE case's gold row, so the ordinary
     month's "Set aside" label is not touched; the fund NAME inside the label keeps `--text-primary`
     via its own rule below-in-specificity-but-on-the-child, because a fund's name is user content and
     never recedes. Both screens move together because both render the one builder. */
  .dash-surplus__row--still .dash-surplus__rowlbl { color: var(--text-tertiary); }
  .dash-surplus__rowamt { font-size: 15.5px; font-weight: 500; white-space: nowrap; flex: 0 0 auto; }
  .dash-surplus__rowamt--gold { color: var(--accent-gold); }
  .dash-surplus__rowamt--teal { color: var(--accent-teal); }
  .dash-surplus__rowamt--neg { color: var(--accent-red); }   /* §3: a genuine below-zero value, and only that */
  /* the proportion bar. A visible TRACK is not needed here because BOTH halves are filled — the bar is a
     split, not a fill against a target (the `.intent-track__bar` rule is about the other shape). Square
     corners, per the human's standing call on every bar in the app. */
  .dash-surplus__split { display: flex; height: 8px; overflow: hidden; margin: 10px 0 0; border: 1px solid var(--border-subtle); }
  .dash-surplus__split-a { background: var(--accent-gold); }
  .dash-surplus__split-b { background: var(--accent-teal); }
  /* ── v387 — the named-funds strip, restored under the one-line summary (the v386 rider's veto) ──────
     A wrapping flex row rather than a joined string, so a fund LABEL (user content) never sits inside the
     same text node as app chrome (the v383 rule) — a fund genuinely called "Car · Bike" stays legible as
     ONE fund. Each name+amount pair is `nowrap`, so a fund and its own figure can never be split across
     two lines; the STRIP wraps between funds instead.
     THE SEPARATOR IS WHITESPACE, and that was a correction made by EYE on this round's own 390px
     screenshot: the first cut put a real "·" element between funds, and the moment the strip wrapped —
     which at phone width is every time there is more than one fund — the dot was left DANGLING at the end
     of a line with nothing after it. A gap cannot dangle. It is also the one separator that needs no
     `aria-hidden` and can never be confused with a character in a fund's name. */
  .dash-surplus__named { margin: 2px 0 4px; display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 18px; }
  .dash-surplus__fund { display: inline-flex; align-items: baseline; gap: 5px; white-space: nowrap; }
  .dash-surplus__fundname { overflow-wrap: break-word; }
  /* v417 ④ — the one money ELEMENT in the app that had missed the app-wide nowrap rule its siblings
     (.dash-tile__value, .dash-surplus__rowamt, .dash-overall__amt, .dash-keptpct) all carry. */
  .dash-surplus__fundamt { font-weight: 500; white-space: nowrap; }
  .dash-surplus__none { margin: 4px 0 0; }
  /* ── v398 — THE OVERALL BLOCK (the wealth change) ──────────────────────────────────────────────────
     Sits at the FOOT of the Kept card behind its own rule, because it answers a different question from
     everything above it: those rows split what this period's income kept, this one says what ALL your
     money did. It renders only when the two differ (i.e. only when spending was paid from a fund), so
     the rule and the block appear together or not at all — no dead chrome.
     RED is on the FIGURE only, never the sentence: a red paragraph would read as an alarm, and the grey
     why line is the part doing the reassuring (same reasoning as v389's paintNeg scoping). */
  .dash-overall { margin-top: 10px; border-top: 1px solid var(--border-subtle); padding-top: 10px; }
  .dash-overall__head { display: flex; align-items: baseline; gap: 9px; font-size: 15px; }
  .dash-overall__lbl { flex: 1 1 auto; min-width: 0; color: var(--text-secondary); overflow-wrap: break-word; }
  .dash-overall__amt { font-size: 17px; font-weight: 500; white-space: nowrap; flex: 0 0 auto; }
  .dash-overall__amt--pos { color: var(--accent-teal); }
  .dash-overall__amt--neg { color: var(--accent-red); }
  /* ── v400 — THE WHY LINE IS READ, SO IT IS LEGIBLE ────────────────────────────────────────────────
     The human, over their own screenshot: "Good and important information … I'd rather have it
     effectively displayed and easily readable." It shipped in the `.tertiary` register, which measures
     3.54:1 against the card (--text-tertiary #6a6a88 on --bg-surface #13131e) — under the 4.5:1 floor
     for body text, and it is a full sentence people are meant to actually read, not a footnote to skim.
     Moving it to --text-secondary measures 7.27:1 on the same surface. It stays 13px and stays grey:
     the SIZE and the words are unchanged, only the shade, and only on this class — the rest of the
     app's tertiary text is untouched, because most of it genuinely is a footnote. Hierarchy survives on
     type size: the Overall head above it is 15px, this is 13px. */
  .dash-overall__why { margin: 6px 0 0; line-height: 1.5; color: var(--text-secondary); }
  /* §8: release event line + pre-funded notes — gold-tinted calm lines (money returned / previously set aside) */
  .dash-surplus__released { margin: 6px 0 0; color: var(--accent-teal); }   /* released = savings in hand (human-directed 2026-07-15; was gold) */
  /* fund-story note colours (human-directed 2026-07-15, supersedes all-gold): RELEASED notes wear teal (the
     money is savings in hand again).
     v400 — the PAID-FROM-FUND half of this rule is GONE with the elements it painted. `.dash-funded` and
     `.ana-fundnote` were the two lavender lines beside the Spending figure; the human asked for that story
     to live INSIDE the tile, so it is `.dash-tile__srow` below and it wears the same two meanings — gold
     for the fund's share, --accent-out for the rest. Do not resurrect a paid-from-fund NOTE LINE elsewhere;
     amounts/badges (ledger fund badge, Sankey gold bands) keep gold, as they always have. */
  .ana-relnote { color: var(--accent-teal); opacity: 0.9; }
  /* ── v400 — THE SPENDING TILE'S BIFURCATION ────────────────────────────────────────────────────────
     A gold/lavender split bar and two compact rows INSIDE the Spending tile, on Home and on Analysis.
     THE BAR IS THE SAME MECHANISM the Kept card's split uses (HUI.splitBar) — same geometry, same square
     corners, same 1px outline — and only its second half is re-painted here, from teal (free to use) to
     --accent-out (spending). One bar, two meanings, no second implementation.
     THE ROWS ARE NOT the Kept card's weighted rows at a smaller size. Those are a card-level statement;
     these are a caption for the figure directly above them, inside a tile that is one-third of a desktop
     strip and full width on a phone. So: smaller type, tighter padding, no top rule of their own (the bar
     is the separator), and the label allowed to wrap while the amount never does. */
  .dash-tile__split { margin: 10px 0 0; height: 6px; }
  .dash-tile__split .dash-surplus__split-b { background: var(--accent-out); }
  .dash-tile__srows { margin: 7px 0 0; }
  .dash-tile__srow { display: flex; align-items: baseline; gap: 7px; padding: 2px 0; font-size: 12.5px; }
  /* the dot aligns to the FIRST LINE of its label, not to the block — the v398 fix, same reasoning:
     a long fund name wraps at 390px and a centred dot then points at nothing. */
  .dash-tile__sdot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto; align-self: flex-start; margin-top: 4px; }
  .dash-tile__sdot--gold { background: var(--accent-gold); }
  .dash-tile__sdot--out { background: var(--accent-out); }
  .dash-tile__slbl { flex: 1 1 auto; min-width: 0; color: var(--text-secondary); overflow-wrap: break-word; }
  .dash-tile__sfund { color: var(--text-primary); font-weight: 600; }
  /* weight 500 is DM Mono's heaviest real face — 600 here would faux-bold into a smear (the standing cap) */
  .dash-tile__samt { font-size: 12.5px; font-weight: 500; white-space: nowrap; flex: 0 0 auto; font-variant-numeric: tabular-nums; }
  .dash-tile__samt--gold { color: var(--accent-gold); }
  .dash-tile__samt--out { color: var(--accent-out); }
  /* ── v401 — THE KEPT STORY, INSIDE THE KEPT TILE (kept-attach mock, Option A, 2026-08-25) ──────────
     HISTORY, because this is the third placement in three rounds and the next reader needs to know why
     it stopped here. v398 built the weighted card on Home; v399 gave Analysis the SAME builder but in
     its own full-width card (`.ana-keptblock`) under the tile strip; v400 put the Spending split
     INSIDE the Spending tile and the human, looking at the two side by side, chose the same treatment
     for Kept. So `.ana-keptblock` is gone with its card, and the compact register lives here.
     ONE HOOK, `dash-tile--keptstory`, added to the HOST by HUI.keptStory when the caller asks for the
     tile register — no piece is renamed, so every print transcription, test selector and screen reader
     path keeps working, and the two screens still render one builder's output.
     THE NUMBERS ARE THE MOCK'S: 12.5px rows over a 6px bar, the Overall head at 12.5px with its figure
     at 13px, and the why line at 11.5px. Hierarchy still survives on type size (26px figure → 12.5px
     rows → 11.5px prose) inside a tile that is one of four across on a desktop. */
  .dash-tile--keptstory .dash-surplus__split { margin: 10px 0 0; height: 6px; }
  .dash-tile--keptstory .dash-surplus__rows { margin: 7px 0 0; border-top: 0; padding-top: 0; }
  .dash-tile--keptstory .dash-surplus__row { gap: 7px; padding: 2px 0; font-size: 12.5px; }
  .dash-tile--keptstory .dash-surplus__dot { width: 7px; height: 7px; margin-top: 4px; }
  .dash-tile--keptstory .dash-surplus__rowamt { font-size: 12.5px; }
  /* ── v401 — THE BREATHING ROOM (the human's own two taps on the mock: "need a small line space
     above" — once on "Free to use", once on "From this period's income") ───────────────────────────
     The space goes above the SECOND row of an in-tile split, so the gold row and whatever explains it
     read as one group and the row that follows is visibly a different statement. It is deliberately
     scoped to the TILE register: Home's Kept CARD is a different surface with its own rhythm, and the
     human was looking at tiles when they asked. */
  .dash-tile--keptstory .dash-surplus__row--free { margin-top: 5px; }
  .dash-tile__srow--income { margin-top: 5px; }
  /* the per-fund strip, in both tiles: indented under the row it belongs to, and its figures GOLD —
     the fund's share of the money, the same meaning the row above it wears (§3). On Home's Kept CARD
     the strip keeps the tertiary grey it has had since v387; this is the in-tile register only. */
  .dash-tile--keptstory .dash-surplus__named,
  .dash-tile__named { margin: 5px 0 0 14px; gap: 3px 14px; font-size: 11.5px; }
  .dash-tile--keptstory .dash-surplus__fundamt,
  .dash-tile__named .dash-surplus__fundamt { color: var(--accent-gold); }
  .dash-tile--keptstory .dash-overall { margin-top: 9px; padding-top: 8px; }
  .dash-tile--keptstory .dash-overall__head { margin: 0; font-size: 12.5px; gap: 7px; }
  .dash-tile--keptstory .dash-overall__amt { font-size: 13px; }
  .dash-tile--keptstory .dash-overall__why { font-size: 11.5px; margin: 5px 0 0; line-height: 1.45; }
  .dash-tile--keptstory .dash-surplus__line { font-size: 11.5px; line-height: 1.45; }
  /* The sentences wear `dash-note` so the PRINTED page still gathers them (NOTE_SEL), but
     `.dash-note`'s 2px side inset is for free-standing notes in the results region — inside a tile it
     would push each sentence off the left edge its own rows are aligned to. Cancel the inset. */
  .dash-tile--keptstory .dash-note { margin-left: 0; margin-right: 0; }
  /* ── v417 ④ — AND THE IN-TILE RELEASE LINE WEARS THE RELEASE COLOUR ───────────────────────────────
     FOUND BY EYE on this round's own 390px screenshot, not by an assertion. The sentence that used to
     live in this slot (`analysis.keptSplitReleased`) was tertiary GREY, and when it was replaced by the
     release register the grey came with it — leaving the one release line in the app that is not teal,
     beside Home's `.dash-surplus__released` and Analysis's own `.ana-relnote`, which both are.
     §3's 2026-07-16 amendment is explicit: a "released … back in hand" note is TEAL, because the money
     is savings in hand again. A register is its colour as well as its words. Same declaration as
     `.ana-relnote` so the two cannot drift. */
  .ana-keptreleased { color: var(--accent-teal); opacity: 0.9; }
  .dash-tile--keptstory .ana-keptreleased { margin: 8px 0 0; font-size: 11.5px; line-height: 1.45; }
  /* v410 ② — the OTHER half of the swap: the sentence that explains a negative-kept month is the thing
     being read, so it takes the secondary ink the row label just gave up. It arrives wearing
     `.tertiary` (which also carries its 13px, and that stays), so this rule sits AFTER `.tertiary` in
     source order and wins the colour at equal specificity — the size is untouched on purpose. Same
     reasoning as v400's `.dash-overall__why` bump, and the same measurement: --text-secondary is
     7.27:1 on --bg-surface where --text-tertiary is 3.54:1, and this is a full sentence, not a footnote. */
  .dash-surplus__line--neg { color: var(--text-secondary); }
  .dash-amt--pos { color: var(--accent-teal); }
  .dash-amt--neg { color: var(--accent-red); }       /* below the zero line — red is the only zero/below colour (§3) */
  .dash-amt--gold { color: var(--accent-gold); }      /* set aside / reserved (§3 gold) */
  /* where it went — a CALM ranked list (no heavy bars): name · % · amount. List ⇄ Groups toggle. */
  .dash-cats__head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
  .dash-cats__head .dash-card__title { margin: 0; }
  .dash-seg { display: inline-flex; border: 1px solid var(--border-default); border-radius: 999px; padding: 2px; gap: 2px; }
  .dash-seg__btn { appearance: none; background: none; border: none; font-family: inherit; font-size: 12px; cursor: pointer;
    color: var(--text-secondary); padding: 4px 11px; border-radius: 999px; }
  .dash-seg__btn.is-on { background: var(--accent-primary); color: var(--on-accent-primary); }
  /* §4 fix #6: an honestly-disabled tab (e.g. the Table panel's Over-time at 1 month / on a #tag subject) must NOT
     read as a live, clickable control — dimmed + not-allowed cursor; its `title` (set in JS) carries the reason. */
  .dash-seg__btn:disabled { color: var(--text-tertiary); opacity: 0.5; cursor: not-allowed; }
  .dash-seg__btn:disabled:hover { background: none; }
  .dash-cat { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--border-subtle); }
  .dash-cat:last-child { border-bottom: none; }
  .dash-cat--sub { padding-left: 18px; }
  .dash-cat--sub .dash-cat__name { color: var(--text-secondary); }
  .dash-cat--toggle { cursor: pointer; }
  .dash-cat--toggle:hover .dash-cat__name { color: var(--text-primary); }
  .dash-cat__l { display: flex; align-items: baseline; gap: 7px; min-width: 0; }
  /* v450 ④ — the reserved emoji slot. It is on EVERY row (empty on a header, on "Other (N)" and on a
     category with no emoji) so the NAMES share one left edge; a variable-width glyph would otherwise
     stagger the column the row exists to make readable. Fixed width, centred, never shrinks. */
  .dash-cat__emoji { flex: 0 0 18px; width: 18px; text-align: center; font-size: 14px; line-height: 1; }
  .dash-cat__name { color: var(--text-primary); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* v450 ① — the chevron follows the name (it used to lead the row and knocked "Other (N)" out of
     line with the categories above it). `flex: none` so it is never the thing that gives. */
  .dash-cat__chev { color: var(--text-tertiary); font-size: 12px; transition: transform .15s; display: inline-block; flex: none; }
  .dash-cat__chev.is-open { transform: rotate(180deg); }
  .dash-cat__r { display: flex; align-items: baseline; gap: 12px; flex: 1 1 auto; min-width: 0; justify-content: flex-end; }
  .dash-cat__pct { font-size: 12px; min-width: 30px; text-align: right; flex: none; white-space: nowrap; font-variant-numeric: tabular-nums; }
  /* v379: the right group was `flex: none`, so a very large amount could never yield — it took the space
     from the category NAME instead (the human's screenshot: a 7-digit amount beside a 5-digit percentage,
     with the label crushed). `1 1 auto` + min-width:0 lets it shrink to its own ellipsis, and the pct is
     capped in JS (catRow, PCT_CAP) so it can no longer be five digits at all. */
  .dash-cat__amt { color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; font-variant-numeric: tabular-nums; }
  @media (min-width: 760px) {
    .dash-cols { grid-template-columns: 1fr 1fr; align-items: start; }
  }
  @media (max-width: 520px) {
    .dash-tiles { grid-template-columns: 1fr; }
    .dash-tile__value, .dash-surplus__fig { font-size: 23px; }   /* v386 — the pair steps down together */
    /* v411 — …and the bracket beside them, at the SAME ratio (23 × 0.7 = 16.1). One media query, one
       pair of numbers, all three surfaces: a rate that kept its desktop size on a 23px figure would be
       the exact drift the shared declaration exists to prevent. */
    .dash-tile__value + .dash-keptpct, .dash-surplus__fig + .dash-keptpct { font-size: 16.1px; }
  }

  /* ── Simple analysis — the four-tier Sankey (Phase 9a2, §15) ──────────────────────────────────── */
  .ana-tiles { grid-template-columns: repeat(auto-fit, minmax(168px, 1fr)); }   /* 3 or 4 (with the internal-transfers tile) across since v409 retired the rate tile; wraps on narrow */
  /* ── v409 — A KEPT TILE THAT CARRIES THE STORY TAKES THE WHOLE ROW ─────────────────────────────
     The human, on their own phone: *"if it's holding much information, use a full line rather than 1/2
     a column."* Their screenshot showed a hard month's Kept tile at roughly three times its
     neighbours' height — red figure, gold "Still set aside" row, the whole negOver paragraph — beside
     a kept-rate tile holding one number and a void.
     TWO declarations, both needed. The span is obvious. The TEMPLATE swap is not: `.ana-tiles` is
     `auto-fit`, and auto-fit collapses only the tracks that are entirely EMPTY — the moment one item
     spans `1 / -1` every track is occupied, nothing collapses, and a desktop strip would hand Income
     and Spending a sixth of the width each. A bounded two-across keeps the row above whole, and it is
     also exactly the phone shape the ask came from.
     ── v411 — …AND IT IS NO LONGER CONDITIONAL ON THE STORY. Their Analysis screenshots are the whole
     argument: a half-width Kept tile sat ALONE on its row with an empty half beside it, and inside that
     136px it could not hold "A$1,763.19 (37.3%)" at its siblings' size — so the fit ladder squeezed the
     amount into "A$1.8k", which is the compact violation they photographed. A Kept tile now ALWAYS
     carries more than a bare figure (the figure AND its rate, and often the story), which is exactly the
     condition the human's own v409 rule names. Giving it the row is the geometry that simply fits, at
     every width, with no media query guessing where the boundary is. `--keptstory` keeps its span so
     nothing that reads that class has to change; `--keptfig` is the always-on marker. */
  /* ── v417 ③ — THE v411 D1 VETO, EXERCISED (human-directed 2026-08-31) ──────────────────────────────
     v411 flagged "the Kept tile now takes the FULL row ALWAYS" as veto-able, and named the reversal:
     *"one media query reverses"*. The human exercised it. On a LAPTOP an ordinary month's Kept tile —
     a figure and its bracket, nothing else — sat alone across a 1100px row with two empty thirds
     beside it, which is the mirror image of the v409 complaint that started all this.
     SO THE ROW IS DECIDED BY TWO THINGS, and each is one of the two rulings this pendulum has
     produced, neither overruled:
       · v409 STANDS — a Kept tile CARRYING THE STORY takes the whole row, at any width. The gate is
         the story's own mark (`dash-tile--keptstory`, stamped by HUI.keptStory only when it actually
         rendered something), so it cannot drift out of step with what is in the tile.
       · v411's ARITHMETIC STANDS on the phone — a ~136px half-tile cannot hold "A$1,763.19 (37.3%)"
         at its siblings' size, and the ladder falling to "A$1.8k" is the compact violation they
         photographed. So below the 700px breakpoint `--keptfig` keeps the full row unconditionally.
     Above it, an ordinary month's tile is an ordinary member of `.ana-tiles`'s auto-fit row again, and
     takes back the slot the retired kept-rate tile left.
     THE TEMPLATE SWAP RIDES THE SAME CONDITION, and it must: `.ana-tiles` is `auto-fit`, which
     collapses only tracks that are entirely EMPTY — the moment one item spans `1 / -1` nothing
     collapses and a desktop strip would hand Income and Spending a sixth of the width each. So the
     bounded two-across applies exactly where something spans, and nowhere else. */
  .ana-tiles--keptwide:has(.dash-tile--keptstory) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .ana-tiles .dash-tile--keptstory { grid-column: 1 / -1; }
  @media (max-width: 700px) {
    .ana-tiles--keptwide { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .ana-tiles .ana-tile--keptfig { grid-column: 1 / -1; }
  }
  @media (max-width: 380px) { .ana-tiles--keptwide { grid-template-columns: minmax(0, 1fr); } }
  /* internal transfers — listed CATEGORY BY CATEGORY (each its own total), never summed across categories */
  .ana-tile--xfer .dash-tile__label { margin-bottom: 7px; }
  .ana-xfer-list { display: flex; flex-direction: column; gap: 4px; }
  .ana-xfer-row { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; min-width: 0; }
  .ana-xfer-row__name { font-size: 13px; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .ana-xfer-row__amt { font-size: 15px; color: var(--text-secondary); flex: none; }   /* neutral — muted, not income/spend coloured */
  .ana-tile--kept .dash-tile__value { color: var(--accent-teal); }   /* v409: `.ana-tile--rate` left this selector with the tile it dressed */
  .ana-tile--neg .dash-tile__value { color: var(--accent-red); }
  .ana-card { overflow-x: auto; position: relative; }       /* the chart is wide — scroll it on a phone, never the page; relative = the expand button's containing block (S8) */
  /* ── v409 — THE HEADER ROW STOPS SCROLLING WITH THE CHART ───────────────────────────────────────
     The rule above gave `.ana-card` two jobs at once, and they are incompatible on a phone: it is the
     `position: relative` containing block for the ⛶ button (the S8 note) AND the horizontal scroller
     for a chart wider than the screen. An absolutely-positioned child of a scroll container travels
     with the content, and a block heading inside one is laid out at the container's width — so the
     human's screenshot showed a card titled "– Aug 26" (the "Mar" had scrolled away) with the ⛶
     button pinned to a diagram halfway off-screen.
     A card that has been through `wireChartExpands` therefore hands the SCROLLING job to an inner
     wrapper and keeps only the positioning job. `overflow-x: visible` is the whole reset: without it
     the card is still a scroll container and nothing changes. Text-only cards (the era table, the
     "Where it went" list) never get the wrapper and keep the original rule untouched — checked, they
     genuinely rely on it. */
  .ana-card--hdrfixed { overflow-x: visible; }
  .ana-card__scroll { overflow-x: auto; overflow-y: hidden; min-width: 0;
    /* themed thin bar, like every other scroller here (`.combo__list`, `.ana-expand__stage`) — the
       browser default light-grey bar reads as a foreign object on a #08080d chart card. */
    scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
  .ana-card__scroll::-webkit-scrollbar { height: 10px; }
  .ana-card__scroll::-webkit-scrollbar-track { background: transparent; }
  .ana-card__scroll::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 999px;
    border: 2px solid transparent; background-clip: content-box; }
  /* Slice 2 — chart cards go DEEP BLACK (#08080d, human-approved) so the raised/gradient bars pop. Scoped with :has
     to cards that actually CONTAIN a chart svg — text cards (the "Where it went" list, table, era table) keep the surface bg. */
  /* review round 1 · item 1: .ana-edonut was missing from this list — the Everything donut card kept the plain
     surface background while its sibling (the scoped .ana-donut) went deep black. Same family, same tile. */
  .ana-card:has(.ana-chart, .ana-donut, .ana-edonut, .ana-sankey) { background: #08080d; }
  /* ── v383 · THE CHART HEADING: `WHO / WHAT / WHEN`, with the separator in the CHROME register ────────
     A category label is USER CONTENT and may contain a slash of its own ("Rent / Mortgage"). If the app's
     own separators looked identical to that one, the heading would be unreadable — so the three parts wear
     the title's own ink and the separators are quiet tertiary. `white-space: pre` keeps the separator's
     spaces inside the span, which is what lets `textContent` stay the plain composed string that the ⛶
     bar and the printed section heading both read. */
  /* the ⛶ button floats at the card's top-right (34px + 8px inset); the heading is longer now that it
     carries all three facts, so it keeps clear of it rather than running underneath. Only cards that get
     the heading treatment get `.ana-head`, and those are exactly the cards that get the ⛶ button. */
  .ana-head { display: block; padding-right: 44px; }
  .ana-head__p { color: inherit; }
  .ana-head__sep { color: var(--text-tertiary); font-weight: 400; white-space: pre; }
  /* v383 item 2 — ON A PHONE THE CHART CLAIMS THE CARD. At 390px the card's 20px side padding plus the
     chart's own two label rails left the plot at ~215px of a 390px screen, and the human's screenshot
     showed the first column's tilted label sliced against the edge while the card wasted a rail. A chart
     card halves its side padding below the phone breakpoint: everything in it (title, legend, notes) is
     still further inset than the surrounding page chrome, and the chart gains ~20px of real plot. */
  @media (max-width: 640px) {
    .ana-card { padding-left: 10px; padding-right: 10px; }
  }
  /* Slice 2 — raised bars: soft lift + brighten on hover (the drop-shadow lives on the SVG bars group filter). */
  .ana-bar { transition: transform .12s ease, filter .12s ease; }
  .ana-bar--hi { transform: translateY(-3px); filter: brightness(1.18); }
  .ana-arc { transition: filter .12s ease, stroke-width .12s ease; }
  .ana-arc--hi { filter: brightness(1.15); }
  .ana-dlrow { border-radius: 7px; transition: background .12s ease; padding: 2px 4px; }
  .ana-dlrow--hi { background: var(--bg-hover); }
  /* review round 1 · item 3b: the kept slice's own parts, listed under the two headline legend rows while it
     is split open — indented and quieter so the two totals still read as the headline. */
  .ana-dlrow--sub { padding-left: 16px; }
  .ana-dlrow--sub .ana-dl__lbl { font-size: 12px; color: var(--text-secondary); }
  .ana-dlrow--sub .ana-dl__amt { font-size: 12px; color: var(--text-secondary); }
  .ana-sankey__wrap { position: relative; min-width: 720px; }
  .ana-sankey { display: block; width: 100%; height: auto; }
  .ana-sankey__labels { position: absolute; inset: 0; pointer-events: none; }
  .ana-lbl { position: absolute; transform: translateY(-50%); display: flex; flex-direction: column; line-height: 1.15;
    max-width: 40%; width: max-content;
    /* soft dark HALO (not a boxy pill — human-directed Round-6) so a label over a ribbon never dissolves into it */
    text-shadow: 0 1px 3px rgba(10, 10, 18, 0.9), 0 -1px 3px rgba(10, 10, 18, 0.9), 1px 0 3px rgba(10, 10, 18, 0.9), -1px 0 3px rgba(10, 10, 18, 0.9); }
  .ana-lbl__name { font-size: 12px; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .ana-lbl__amt { font-size: 12px; color: var(--text-primary); }
  .ana-lbl--hdr, .ana-lbl--cat, .ana-lbl--goal { flex-direction: row; align-items: baseline; gap: 6px; }
  .ana-lbl__row { display: flex; align-items: baseline; gap: 6px; }                 /* name+amt line of a two-line label */
  .ana-lbl__sub { font-size: 10.5px; color: var(--accent-gold); opacity: .85; white-space: nowrap; }   /* "now holds …" */
  .ana-lbl--hassub { flex-direction: column; }
  .ana-lbl--gold .ana-lbl__name { color: var(--accent-gold); }                      /* Into {fund} / paid-from-funds */
  .ana-lbl--teal .ana-lbl__name { color: var(--accent-teal); }                      /* Free to use */
  /* the two mid TOTALS (Went out · Kept) sit LEFT of the middle bar, mirroring Income (Round-6 label layout) */
  .ana-lbl--mid { flex-direction: row; align-items: baseline; gap: 6px; justify-content: flex-end; text-align: right; transform: translate(-100%, -50%); }
  .ana-lbl--mid .ana-lbl__name { color: var(--text-primary); font-weight: 600; }
  /* the Kept LEGEND CARD (crowded stack → one card instead of leader spaghetti) */
  .ana-lgd { position: absolute; width: 250px; max-width: 46%; background: var(--bg-elevated); border: 1px solid var(--border-default);
    border-radius: 10px; padding: 8px 12px 10px; pointer-events: auto; }
  .ana-lgd__head { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; padding-bottom: 6px; margin-bottom: 4px; border-bottom: 1px solid var(--border-default); }
  .ana-lgd__hname { font-size: 12.5px; font-weight: 600; color: var(--text-primary); }
  .ana-lgd__hamt { font-size: 12.5px; color: var(--text-primary); }
  .ana-lgd__row { display: flex; align-items: baseline; gap: 8px; padding: 2.5px 0; }
  .ana-lgd__sw { width: 9px; height: 9px; border-radius: 2.5px; flex: 0 0 auto; align-self: center; }
  .ana-lgd__name { font-size: 12px; color: var(--text-secondary); flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .ana-lgd__name--teal { color: var(--accent-teal); }
  .ana-lgd__amt { font-size: 12px; color: var(--text-primary); }
  .ana-lgd__sub { font-size: 10.5px; color: var(--accent-gold); opacity: .85; padding-left: 17px; margin-top: -2px; }
  .ana-lbl--hdr { pointer-events: auto; cursor: pointer; }
  .ana-lbl--hdr:hover .ana-lbl__name { color: var(--text-primary); }
  .ana-lbl--hdr .ana-lbl__amt, .ana-lbl--cat .ana-lbl__amt, .ana-lbl--goal .ana-lbl__amt { color: var(--text-secondary); }
  /* review round 1 · item 7: a NOTE is a sentence ("incl. A$400.00 set aside from other funds"), and a
     single-line ellipsis reduced it to three dots on a phone — unreadable, so the information was simply gone.
     It wraps to at most TWO lines instead (the width comes from lbl()'s two-column allowance); the clamp keeps
     a long one from growing without bound into the bands below it. */
  .ana-lbl--note .ana-lbl__name { color: var(--text-tertiary); font-size: 11px; font-style: italic;
    white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
  /* review round 2 · item 2: the "incl. {amount} returned" line under a merchant fan is MONEY RETURNED, which the
     ratified §3 rule colours GOLD. It needs its own (0,3,0) rule — .ana-lbl--note wins over .ana-lbl--gold on
     source order alone, so a plain --gold class would silently read tertiary grey here. */
  .ana-lbl--note.ana-lbl--gold .ana-lbl__name { color: var(--accent-gold); }
  .ana-lbl__chev { color: var(--text-tertiary); font-size: 11px; transition: transform .15s; }
  .ana-lbl__chev.is-open { transform: rotate(180deg); }
  .ana-lbl--warn .ana-lbl__name, .ana-lbl--warn .ana-lbl__amt { color: var(--accent-warm); }
  .ana-hnode { cursor: pointer; }
  .ana-hnode.is-open { stroke: var(--accent-out); stroke-width: 1.5; }
  .ana-mnode { cursor: pointer; }
  .ana-mnode:hover { stroke: var(--accent-teal); stroke-width: 1.5; }
  .ana-cnode { cursor: pointer; }                                /* a category with merchants — tap to drill in */
  .ana-cnode.is-open { stroke: var(--accent-out); stroke-width: 1.25; }
  .ana-lbl[role="button"] { cursor: pointer; }
  .ana-leader { stroke: var(--text-tertiary); stroke-width: 1; opacity: .4; }    /* connects a pushed-off label to its node */
  /* income-stream labels sit to the LEFT of the income bar (Method A), right-aligned toward it */
  .ana-lbl--inc { flex-direction: row; align-items: baseline; gap: 6px; justify-content: flex-end; text-align: right; transform: translate(-100%, -50%); }
  .ana-lbl--inc .ana-lbl__amt { color: var(--text-secondary); }
  .ana-lbl--inc.ana-lbl--indent { font-size: 11px; }
  .ana-lbl--inc.ana-lbl--indent .ana-lbl__name { color: var(--text-tertiary); }
  .ana-lbl--inc.ana-lbl--total .ana-lbl__name { color: var(--text-primary); }    /* the income TOTAL heads its streams */

  /* ── Analysis: the WHO / WHAT / WHEN axis bar — three dropdown menus (9a2 redesign) ─────────── */
  .ana-axisbar { display: flex; align-items: flex-start; flex-wrap: wrap; gap: 10px 14px; margin-bottom: var(--gap); }
  .ana-menu { width: auto; }
  .ana-menu__trigger { width: auto; min-width: 140px; max-width: 340px; gap: 10px; padding: 9px 12px; }
  .ana-menu__lbl { flex: none; font-size: 11px; letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-tertiary); }
  .ana-menu__val { flex: 1 1 auto; min-width: 0; font-size: 14px; color: var(--text-primary); }
  .ana-menu__pop { min-width: 240px; max-width: 360px; padding: 6px; }
  /* v383: the shared pull-up flip (anaMenu measures and adds the class only when there is no room below) */
  .ana-menu--up .ana-menu__pop { top: auto; bottom: calc(100% + 4px); }
  /* WHAT is the biggest list → give it most of the screen height so there's far less scrolling */
  .ana-menu--what .ana-menu__pop { max-height: min(72vh, 640px); }
  /* Slice 5 — the unified WHAT ▾ dropdown (Idea C): mode toggle + search + tabs + list.
     §4 fix #4: the popup was clamped to its trigger's width (300–400px) while the 5 tabs (Overall · Categories ·
     Merchants · Tags · Transfers) need ~390–410px on ONE line — and the strip's scrollbar was hidden (scrollbar-
     width:none + a webkit display:none rule), so an overflowing tab (Transfers) was reachable by NOTHING visible.
     Widened the popup and let the tab strip WRAP to a second line instead of hiding an invisible scroll affordance. */
  .ana-menu--what2 .ana-menu__pop { min-width: 420px; max-width: 460px; max-height: min(74vh, 660px); padding: 8px; }
  .ana-whatmode2 { width: 100%; margin-bottom: 8px; }
  .ana-whatmode2 .dash-seg__btn { flex: 1 1 0; }
  .ana-whatsearch { position: relative; margin-bottom: 8px; }
  .ana-whatsearch__input { width: 100%; box-sizing: border-box; }
  .ana-whatsearch__pop { position: absolute; left: 0; right: 0; top: calc(100% + 2px); z-index: 6; max-height: 240px; }
  .ana-whattabs { display: flex; flex-wrap: wrap; gap: 4px 6px; margin-bottom: 6px; border-bottom: 1px solid var(--border-subtle); padding-bottom: 6px; }
  .ana-whattab { flex: 0 0 auto; appearance: none; background: transparent; border: 0; font-family: inherit; font-size: 13px; color: var(--text-tertiary); cursor: pointer; padding: 6px 10px; border-radius: 8px; white-space: nowrap; }
  .ana-whattab:hover { background: var(--bg-hover); color: var(--text-secondary); }
  .ana-whattab.is-on { background: var(--accent-glow); color: var(--accent-primary); font-weight: 600; }
  .ana-whatlist { max-height: 46vh; overflow-y: auto; }
  .ana-whatempty { padding: 12px 8px; font-size: 12.5px; }
  .ana-whatoverall, .ana-whatmerch { padding-left: 10px; }
  .lcat-cb--drill { visibility: hidden; }   /* alignment spacer where a checkbox would sit (single-mode drill rows) */
  .ana-breadcrumb--slim { margin-top: 4px; }
  .ana-by { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 8px; margin-top: 8px; }
  .ana-by--none { display: none; }
  .ana-by__lbl { text-transform: uppercase; letter-spacing: .04em; font-size: 11px; }
  .ana-by__seg { flex-wrap: wrap; }
  .ana-unit { display: inline-flex; align-items: center; gap: 6px; }
  .ana-unit__lbl { text-transform: uppercase; letter-spacing: .04em; font-size: 11px; }
  .ana-unit__seg { flex-wrap: wrap; }
  .ana-unitcaption { margin-top: 8px; font-size: 13px; }
  /* v353 (Q2): the compare TREND's measure pills — Income · Spending · Kept, one at a time. Same dash-seg
     the "Show" unit control wears, so the two read as siblings; it sits at the top of the trend card
     because that is the chart it changes (the ratified mock's own placement). */
  .ana-measure { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin: 0 0 10px; }
  .ana-measure__seg { flex-wrap: wrap; }
  /* these three are the control a PHONE reader taps to change what the trend is about, so they carry a real
     touch target rather than the 24px a lens-strip seg gets away with beside a row of other chrome. */
  .ana-measure__seg .dash-seg__btn { min-height: 30px; display: inline-flex; align-items: center; }
  .ana-leaf__count { margin: 0 0 10px; font-size: 13px; }
  .ana-leaf__list { display: flex; flex-direction: column; }
  .ana-leaf__more { margin-top: 8px; }
  /* v353: `--tap` no longer dresses a PERSON TOTAL — that affordance was dropped outright (see compSums).
     Its one remaining consumer is the periods strip's "Show all (41)" button below, which is a show-more
     control, not a door into a person. `.ana-backcompare` (the "← back to comparison" breadcrumb) went with
     the affordance it returned from. */
  .ana-compsums__item--tap { cursor: pointer; background: none; border: 1px solid transparent; border-radius: 8px; padding: 2px 6px; font: inherit; color: inherit; }
  .ana-compsums__item--tap:hover { border-color: var(--border-default); background: var(--bg-input); }
  /* R7 item J1: the "Show all (41)" / "Show the last 6" tap that ends the periods sums strip. It carries its
     own border at rest (unlike --tap, whose trigger is the row it sits in) so it reads as tappable on a
     phone, where there is no hover to discover it with. */
  .ana-compsums__more { border-color: var(--border-default); color: var(--text-secondary); }
  .ana-compsums__more:hover { color: var(--text-primary); }
  /* transfers (neutral) view — a calm muted tile + note (transfers are neither spending nor income) */
  .ana-tile--xfer .dash-tile__value { color: var(--text-secondary); }
  .ana-xfernote { margin: -4px 0 10px; }
  /* INCOME & NEUTRAL ENGINES (2026-08-15) — a #tag's two halves. The section captions are quiet dividers, not
     headings: they name which direction the rows below are, so income and spending are never read as one list. */
  .ana-bdsect { margin: 10px 0 4px; font-size: 12px; letter-spacing: .04em; text-transform: uppercase; }
  .ana-bdsect:first-child { margin-top: 0; }
  /* v354 — the four-section tag view. RETURNED is a gold caption (§3: money coming back wears gold), so the
     section reads as a return before a single figure is looked at. MOVED is one muted footnote, deliberately
     the quietest thing on the card: a transfer/provision joins no number here and the line says so. */
  .ana-bdsect--gold { color: var(--accent-gold); }
  .ana-tagmoved { margin: 10px 0 0; font-size: 12.5px; line-height: 1.5; }
  .ana-tile--gold .dash-tile__value { color: var(--accent-gold); }
  /* A compare bucket carrying BOTH directions shows both figures. On a phone the pair is wider than the
     frame, so this variant is allowed to WRAP onto a second line rather than run off the card edge — the
     unwrapped siblings keep their single line, which is why the rule is scoped to this variant. */
  .ana-compsums__item--both { white-space: normal; flex-wrap: wrap; min-width: 0; }
  /* v422 — a PERIOD BEFORE THE LEDGER BEGAN. The chart leaves it undrawn (gapPreHistory); the strip says the
     same thing in words with the em dash the over-time table already uses for a figure it cannot compute, and
     wears the tertiary ink so it reads as absent rather than as a number. A true zero INSIDE history is
     untouched and keeps its full A$0.00 in the primary ink — it is a real observation. */
  .ana-compsums__item--gap, .ana-compsums__item--gap .ana-compsums__amt { color: var(--text-tertiary); }
  /* ── v383 · THE WHAT PICKER STOPS DETACHING ON A PHONE (human-reported defect, 2026-08-20) ───────────
     THE ROOT CAUSE, measured: WHO and WHEN open as `position: absolute` popups 4px under their own trigger
     and travel with the page; WHAT opened as a `position: fixed` bottom SHEET, so it sat at a constant
     viewport position (measured 184px ABOVE its own trigger) and stayed put while the page scrolled behind
     it — the human's *"a little bit awkward"*. It was one media-query rule, written for a real reason (the
     desktop popup's 420px min-width overflowed a 390px phone), but a sheet was the wrong instrument for it:
     the sibling controls solve the same width problem by going full-width and staying attached.
     THE FIX is exactly that. The WHAT menu takes its own line in the (already column-stacked) axis bar, and
     its popup is an ORDINARY attached dropdown, inheriting `.combo__list`'s `top: calc(100% + 4px)` like its
     two siblings. `min-width: 0` still does the work the old comment describes; nothing else about the
     popup's contents, tap targets or overscroll containment changes. */
  @media (max-width: 640px) {
    .ana-menu--what2 .ana-whatmenu-pop { left: 0; right: 0; width: auto; min-width: 0; max-width: none; max-height: 78vh; }
    /* S8: the sheet scrolls internally without the page bleeding through underneath it (overscroll containment) */
    .ana-menu--what2 .ana-whatlist { max-height: 50vh; overscroll-behavior: contain; }
    /* S8: the floating Table (left) / Ledger (right) panel HANDLES sit at a fixed vertical-mid position and can
       occlude the sheet's (now-wrapping) tab strip at some scroll positions. Hide + inert them while the sheet is
       open; :has() reacts live to the popup's own `dropdown--open` class, so closing the sheet restores them with
       no extra JS/state to keep in sync. */
    body:has(.ana-menu--what2.dropdown--open) .ana-lpanel-handle,
    body:has(.ana-menu--what2.dropdown--open) .ana-tpanel-handle { opacity: 0; pointer-events: none; }
    /* S8: comfortable (~44px) tap targets inside the sheet — tabs and rows were sized for a mouse pointer */
    .ana-menu--what2 .ana-whattab { padding: 13px 14px; font-size: 13.5px; }
    .ana-menu--what2 .ana-cbrow, .ana-menu--what2 .lcat-crow, .ana-menu--what2 .lcat-hrow { padding-top: 13px; padding-bottom: 13px; }
  }
  /* an explicit "Clear all" reset row (distinct from the "Total life costs" comparison unit) */
  .ana-clearall { appearance: none; background: none; border: none; font-family: inherit; cursor: pointer;
    display: block; width: 100%; text-align: left; padding: 6px 10px; border-radius: 7px;
    font-size: 12.5px; color: var(--text-tertiary); }
  .ana-clearall:hover { background: var(--bg-hover); color: var(--accent-warm); }
  /* flat checkbox row (WHO entities + the "Total life costs" reset) — same look as the Ledger filter rows */
  .ana-cbrow { display: flex; align-items: center; gap: 10px; padding: 9px 10px; border-radius: 8px; cursor: pointer; font-size: 14px; color: var(--text-secondary); }
  .ana-cbrow:hover { background: var(--bg-hover); }
  .ana-cbrow.lcat-crow--on { color: var(--text-primary); }
  .ana-opt__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .ana-opt--off { color: var(--text-tertiary); opacity: 0.45; cursor: not-allowed; }
  .ana-opt--off:hover { background: transparent; }
  /* the Compare/Add toggle sticks to the popup's bottom edge so it stays reachable in a long category list */
  .ana-menu__foot { position: sticky; bottom: -6px; background: var(--bg-elevated); border-top: 1px solid var(--border-subtle);
    margin: 6px -6px -6px; padding: 8px 8px 8px; }
  .ana-menu__footrow { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 10px; }
  .ana-menu__foot-lbl { font-size: 12px; color: var(--text-tertiary); }
  .ana-menu__footnote { font-size: 11px; margin-top: 6px; line-height: 1.35; }
  /* WHAT comparison buckets (A/B/C) */
  .ana-buckets { padding: 6px 2px 8px; border-bottom: 1px solid var(--border-subtle); margin-bottom: 4px; }
  .ana-buckets__row { display: flex; flex-wrap: wrap; gap: 6px; }
  .ana-bucket { appearance: none; font-family: inherit; font-size: 13px; cursor: pointer; display: inline-flex; align-items: center; gap: 6px;
    padding: 5px 9px; border-radius: 999px; background: var(--bg-input); border: 1px solid var(--border-default); color: var(--text-secondary); max-width: 100%; }
  .ana-bucket.is-active { border-color: var(--accent-primary); box-shadow: 0 0 0 2px var(--accent-glow); color: var(--text-primary); }
  .ana-bucket__dot { width: 11px; height: 11px; border-radius: 3px; flex: none; }
  /* v386 — WRAP, NEVER TRUNCATE. A slot now names every category in it (analysis.js `bucketLabel`), and an
     ellipsis here would put the abbreviation straight back — a chip reading "Groceries, Dining a…" says no
     more than the "+1" the round exists to remove. The pill grows a second line instead; `max-width: 100%`
     above already keeps it inside the popup, and the stadium radius survives a two-line box. */
  .ana-bucket__lbl { overflow-wrap: break-word; min-width: 0; }
  .ana-bucket__x { flex: none; margin-left: 2px; font-size: 14px; line-height: 1; opacity: 0.7; }
  .ana-bucket__x:hover { opacity: 1; }
  .ana-bucket--add { font-size: 16px; line-height: 1; padding: 5px 11px; color: var(--text-tertiary); }
  .ana-buckets__hint { font-size: 11px; margin-top: 6px; }
  /* a category's bucket mark: the bucket COLOUR square with its letter (replaces the plain checkbox in compare mode) */
  .ana-bcb { color: #15151f; font-size: 11px; font-weight: 700; display: flex; align-items: center; justify-content: center; }
  .ana-limitnote { margin: 0 0 12px; }
  /* WHEN: the dropdown holds the custom range; the quick presets sit beside it */
  .ana-whengroup { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 10px; }
  .ana-when-presets { flex-wrap: wrap; }
  .ana-when-pop { overflow: visible; max-height: none; min-width: 280px; padding: 12px; }
  .ana-when-lbl { font-size: 12px; margin-bottom: 8px; }
  .ana-rangerow { display: grid; grid-template-columns: 44px 1fr; align-items: center; gap: 8px; margin-top: 6px; }
  /* v319: the WHEN range is ONE picker with no From/To label — full-width single column */
  .ana-rangerow--single { grid-template-columns: 1fr; }
  /* discrete-period comparison controls (the WHEN "Compare periods" mode) */
  .ana-whenmode { margin-bottom: 10px; }
  .ana-prow { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 8px; }
  .ana-prow__lbl { flex: 0 0 auto; }
  .ana-prow .dash-seg { flex-wrap: wrap; }
  .ana-prow .dropdown { min-width: 130px; }
  .ana-pyears { margin-top: 10px; }
  .ana-pyears > .ana-prow__lbl { display: block; margin-bottom: 4px; }
  .ana-pyears__box { display: flex; flex-wrap: wrap; gap: 2px 14px; max-height: 160px; overflow-y: auto; }
  .ana-pyears__box .ana-cbrow { flex: 0 0 auto; padding: 5px 4px; }
  .ana-customrange { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 6px; }
  .ana-customrange__lbl { font-size: 12px; }
  .ana-shape { margin-left: 2px; }
  /* Slice 3 — the shape ICON STRIP (replaces the breakdown-shape dropdown; hover an icon for its name). */
  .ana-shaperow { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }
  .ana-shapestrip { display: inline-flex; gap: 3px; background: var(--bg-input); border: 1px solid var(--border-default);
    border-radius: 12px; padding: 4px; max-width: 100%; overflow-x: auto; scrollbar-width: none; }
  .ana-shapestrip::-webkit-scrollbar { display: none; }
  .ana-shbtn { position: relative; flex: 0 0 auto; width: 38px; height: 36px; display: inline-flex; align-items: center;
    justify-content: center; background: transparent; border: 0; border-radius: 9px; cursor: pointer; color: var(--text-tertiary); padding: 0; }
  .ana-shbtn:hover { background: var(--bg-hover); color: var(--text-secondary); }
  .ana-shbtn.is-on { background: var(--accent-glow); color: var(--accent-primary); }
  .ana-shbtn svg { width: 20px; height: 20px; }
  .ana-shbtn__tip { position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%); background: #000;
    color: var(--text-primary); font-size: 12px; padding: 4px 9px; border-radius: 6px; white-space: nowrap; opacity: 0;
    pointer-events: none; transition: opacity .12s ease; border: 1px solid var(--border-strong); z-index: 5; }
  .ana-shbtn:hover .ana-shbtn__tip, .ana-shbtn:focus-visible .ana-shbtn__tip, .ana-shbtn--tip .ana-shbtn__tip { opacity: 1; }   /* §4 fix #9: --tip is the touchstart-revealed state (JS-toggled) */
  /* §4 fix #9: the active shape's NAME, always visible beside the icon strip (touch can't hover an icon to learn it) */
  .ana-shaperow__label { font-size: 12.5px; white-space: nowrap; }
  .ana-legend { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-bottom: 10px; }
  /* v386: a legend key may now carry a slot's WHOLE membership ("Groceries, Dining and Transport"), so the
     item is allowed to take a second line rather than push the row into a horizontal scroll. */
  .ana-legend__item { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-secondary); max-width: 100%; min-width: 0; }
  .ana-legend__lbl { overflow-wrap: break-word; min-width: 0; }
  .ana-legend__dot { width: 11px; height: 11px; border-radius: 3px; flex: none; }
  /* ── REVIEW ROUND 3 · part 1: the chart draws at REAL pixels ────────────────────────────────────────────────
     It used to be `width:100%` over a fixed 760x300 viewBox — i.e. the browser scaled the whole drawing, axis
     text included, down to the card. On a 390px phone that made the numbers ~5px. The SVG now carries real
     width/height attributes equal to its viewBox, so nothing is scaled and 12.5px text is 12.5px everywhere;
     `max-width:100%` + `height:auto` stay purely as a safety net for the frame between a resize and its redraw.
     (report-analysis.js deliberately overrides this with width:100% — paper scales the whole page.) */
  .ana-chart__wrap { position: relative; }
  /* v376 — THE CHARTS USE THE ROOM. `margin: 0 auto` was the second half of the 860px cap: with the drawing
     hard-limited to 860, every wider card centred it and hung grey off both sides. The cap is gone (the JS
     now sizes a chart at min(the room it has, n marks x SLOT_CAP + rails)), and the centring stays — it is
     what keeps a deliberately-narrow small-data chart sitting in the middle of a wide card rather than
     stranded against its left edge. Flagged veto-able on the round's card: left-aligned may read better
     once several cards of different mark counts stack on a wide monitor. */
  .ana-chart { display: block; max-width: 100%; height: auto; margin: 0 auto; }
  /* v376 — the fullscreen amount on a mark. QUIET INK, never the series colour (a coloured amount would read
     as a fifth meaning on a screen whose colours already mean four things), and a size under the axis font
     so the numbers sit behind the axis in the reading order rather than competing with it. Deliberately NOT
     given the `.ana-axis` class: every suite that harvests axis text reads that class, and a mark amount is
     not an axis label — sharing the class would have quietly changed what a dozen existing assertions count. */
  .ana-markamt { fill: var(--text-secondary); font-size: 12px; font-family: inherit; pointer-events: none; }
  /* the interactive stage (part 3): the drawn SVG plus the two axis GUTTER strips laid exactly over the
     gutters the chart reserves. touch-action is the whole touch policy — a vertical flick on the plot still
     scrolls the page (pan-y), the strips only stretch their axis (none). */
  .ana-cstage { position: relative; margin: 0 auto; max-width: 100%; touch-action: pan-y; cursor: grab; }
  .ana-cstage--drag { cursor: grabbing; }
  .ana-cax { position: absolute; touch-action: none; -webkit-user-select: none; user-select: none; }
  /* R6 (v343): the money gutter moved to the RIGHT, so this strip did too — the gradient mirrors with it, and
     still fades AWAY from the plot (strongest at the outer edge, invisible where the drawing begins). */
  .ana-cax--y { cursor: ns-resize; background: linear-gradient(270deg, rgba(255,255,255,.03), rgba(255,255,255,0)); }
  .ana-cax--x { cursor: ew-resize; background: linear-gradient(0deg, rgba(255,255,255,.03), rgba(255,255,255,0)); }
  .ana-cax:hover { background: rgba(255,255,255,.06); }
  /* the way back, shown ONLY once the view is off its default (at rest both buttons would be no-ops) */
  .ana-cbar { display: flex; justify-content: flex-end; gap: 6px; margin-top: 6px; flex-wrap: wrap; }
  .ana-cbar__btn { background: var(--bg-input); border: 1px solid var(--border-default); color: var(--text-secondary);
    border-radius: 8px; font: inherit; font-size: 12px; padding: 5px 9px; min-height: 30px; cursor: pointer; }
  .ana-cbar__btn:hover { border-color: var(--accent-primary); color: var(--text-primary); }
  .ana-cbar__btn.is-on { background: var(--accent-glow); border-color: var(--accent-primary); color: var(--text-primary); }
  /* part 2: the "Show by" pills sit with the other trend-lens controls */
  .ana-granwrap { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
  .ana-granwrap__lbl { font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: var(--text-tertiary); }
  .ana-grannote { margin: 0 2px 8px; }
  /* v349 ④: the heading over the scoped WHO-compare's per-person TOTAL bars — it has to separate two charts
     that now live in one card, so it carries air above as well as below. */
  .ana-comptotals__ttl { margin: 10px 2px 4px; }
  /* v369 (SIXTH-REVIEW ROUND D): the compare consistency rule's section captions. The REGISTER is entirely
     `.ana-bdsect` above — the app's existing small-uppercase section caption, which the compare TABLE's own
     `.ana-comptab__sechd` already borrows — so no second small-caps voice entered the app. This modifier adds
     only what two CHARTS inside one card need that a run of list rows does not: real air, and one hairline in
     the same `--border-default` the table's section rule uses. No new colour token, no new hex.
     It sits AFTER `.ana-comptotals__ttl` deliberately: the v349 heading keeps its class (suites select on it)
     and takes this spacing, rather than two rules fighting over the same margin. */
  .ana-bdsect--chart { margin: 20px 2px 8px; padding-top: 14px; border-top: 1px solid var(--border-default); }
  .ana-bdsect--first { margin-top: 4px; padding-top: 0; border-top: 0; }
  .ana-grid { stroke: rgba(255, 255, 255, .06); stroke-width: 1; }   /* Slice 2: softer gridlines on the deep-black chart card */
  /* R6 (v343): 12.5 → 13.5px, the human's "the axis labels need genuinely more readability". THREE PLACES MUST
     AGREE: this rule, its literal duplicate in js/report-analysis.js (the printed page cannot see this file),
     and AXIS_FONT_PX in js/analysis.js (which MEASURES the text to size the money gutter and to decide whether
     an x label fits horizontally — a stale constant there would reserve the wrong width). */
  .ana-axis { fill: var(--ana-axis-ink); font-family: inherit; font-size: 13.5px; }   /* v366 ②: colour only — 13.5px is the measuring contract (AXIS_FONT_PX), do not touch it here */
  .ana-eratable { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 14px; }
  .ana-eratable th { text-align: left; color: var(--text-tertiary); font-weight: 500; font-size: 12px; padding: 6px 10px; border-bottom: 1px solid var(--border-default); }
  .ana-eratable td { padding: 8px 10px; border-bottom: 1px solid var(--border-subtle); color: var(--text-secondary); }
  /* numeric column: right-align the HEADER too (the `.ana-eratable th` rule out-specifies a bare `.ana-eratable__num`,
     so the header was left-aligned while its number was right-aligned → they drifted far apart in a wide column). */
  .ana-eratable th.ana-eratable__num, .ana-eratable td.ana-eratable__num { text-align: right; }
  /* keep the numbers from sprawling to the far edge of a very wide frame — a calmer fixed currency column + a cap */
  .ana-eratable th:first-child, .ana-eratable td:first-child { width: 120px; }
  /* §9 per-era SECTIONS: one labelled block per currency era when a range spans a base-currency change. A clear
     divider + a header (currency name · date span) sets each era apart; sections stack single-column on a phone. */
  .ana-era-section { margin-top: 18px; padding-top: 14px; border-top: 2px solid var(--border-default); }
  .ana-era-section:first-of-type { margin-top: 14px; }
  .ana-era-section__hd { font-family: var(--font-head, inherit); font-size: 17px; font-weight: 600; color: var(--text-primary); margin: 0 2px 10px; }

  /* ── Analysis → Ledger linked panel (floating, filtered ledger view) ───────────────────────────── */
  /* SLIM + GLASS handles (2026-07-16, human-picked "variant C" from the mockup artifact): the icon leads the word
     along ONE vertical line (flex-direction: row in vertical-rl = the inline/vertical axis → ~25px wide, half the
     old tab), and the tab is translucent purple + blur so a covered card title stays readable through it. Hover /
     focus restores full purple — the tap target is never a ghost. (rgba literal of --accent-primary, the
     --accent-glow precedent.) */
  .ana-lpanel-handle {
    position: fixed; top: 50%; right: 0; transform: translateY(-50%);
    z-index: 60; display: flex; flex-direction: row; align-items: center; gap: 5px;
    background: rgba(124, 106, 255, 0.40); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
    color: #fff; border: none; cursor: pointer;
    padding: 10px 5px; border-radius: 12px 0 0 12px; box-shadow: -3px 0 14px rgba(0,0,0,0.35);
    writing-mode: vertical-rl; font-size: 11px; font-weight: 600; letter-spacing: 0.5px;
    transition: opacity 0.18s ease, transform 0.18s ease, background 0.15s ease;
  }
  .ana-lpanel-handle svg { transform: rotate(90deg); }
  .ana-lpanel-handle:hover, .ana-lpanel-handle:focus-visible { background: var(--accent-primary); }
  .ana-lpanel-handle--gone { opacity: 0; transform: translateY(-50%) translateX(110%); pointer-events: none; }

  .ana-lpanel {
    position: fixed; top: 0; right: 0; height: 100vh; width: min(380px, 92vw);
    z-index: 61; display: flex; flex-direction: column;
    background: var(--bg-surface); border-left: 1px solid var(--border-default);
    box-shadow: -6px 0 26px rgba(0,0,0,0.4);
    transform: translateX(100%); transition: transform 0.22s ease, width 0.18s ease;
  }
  .ana-lpanel--open { transform: translateX(0); }
  .ana-lpanel--wide { width: min(620px, 96vw); }
  .ana-lpanel__head { padding: 14px 14px 10px; border-bottom: 1px solid var(--border-subtle); flex: 0 0 auto; }
  .ana-lpanel__titlerow { display: flex; align-items: center; gap: 6px; }
  .ana-lpanel__title { font-family: var(--font-head, inherit); font-size: 16px; font-weight: 600; color: var(--text-primary); }
  .ana-lpanel__spacer { flex: 1 1 auto; }
  .ana-lpanel__expand svg { transition: transform 0.18s ease; }
  .ana-lpanel--wide .ana-lpanel__expand svg { transform: rotate(180deg); }
  .ana-lpanel__meta { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-top: 8px; }
  .ana-lpanel__count { font-size: 13px; }
  .ana-lpanel__net { font-size: 15px; font-weight: 500; }
  .ana-lpanel__sortrow { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
  .ana-lpanel__sortlbl { font-size: 12px; }
  .ana-lpanel__filters { display: flex; flex-wrap: wrap; gap: 8px 12px; margin-top: 10px; }
  .ana-lpanel__filters:empty { display: none; }
  .ana-lpanel__filter { display: flex; flex-direction: column; gap: 3px; flex: 1 1 100%; min-width: 0; }
  .ana-lpanel--wide .ana-lpanel__filter { flex: 1 1 200px; }   /* side-by-side only when there's room (expanded panel) */
  .ana-lpanel__filterhd { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
  .ana-lpanel__filterhd > span { font-size: 11px; }
  .ana-lpanel__clear { font-size: 11px; color: var(--text-secondary); }
  .ana-lpanel__clear:hover { color: var(--accent-primary); }
  .ana-lpanel__filter .dropdown, .ana-lpanel__filter .catpick, .ana-lpanel__filter .combo { width: 100%; min-width: 0; }
  .ana-lpanel__filter .dropdown__trigger, .ana-lpanel__filter .catpick__input, .ana-lpanel__filter .combo input { width: 100%; min-width: 0; box-sizing: border-box; }
  .ana-lpanel__body .tx__detail { width: 100%; margin-top: 4px; }
  .ana-lpanel__body .txd__golink { margin-top: 8px; }
  .ana-lpanel__body { flex: 1 1 auto; overflow-y: auto; padding: 6px 10px 18px; }
  .ana-lpanel__body .tx { padding: 9px 4px; border-bottom: 1px solid var(--border-subtle); }
  .ana-lpanel__empty { padding: 24px 8px; text-align: center; font-size: 14px; }
  .ana-lpanel__more { padding: 12px 4px; font-size: 13px; display: flex; align-items: center; gap: 10px; }
  @media (max-width: 900px) {
    .ana-lpanel { width: 100vw; }
    .ana-lpanel--wide { width: 100vw; }
  }

  /* ── Slice 4: the LEFT-sliding Table panel + its edge handle (mirror of the right ledger panel) ─────────── */
  .ana-tpanel-handle {
    position: fixed; top: 42%; left: 0; transform: translateY(-50%);
    z-index: 60; display: flex; flex-direction: row; align-items: center; gap: 5px;
    background: rgba(124, 106, 255, 0.40); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
    color: #fff; border: none; cursor: pointer;
    padding: 10px 5px; border-radius: 0 12px 12px 0; box-shadow: 3px 0 14px rgba(0,0,0,0.35);
    writing-mode: vertical-rl; font-size: 11px; font-weight: 600; letter-spacing: 0.5px;
    transition: opacity 0.18s ease, transform 0.18s ease, background 0.15s ease;
  }
  .ana-tpanel-handle:hover, .ana-tpanel-handle:focus-visible { background: var(--accent-primary); }
  .ana-tpanel-handle--gone { opacity: 0; transform: translateY(-50%) translateX(-110%); pointer-events: none; }
  /* S7: .ana-tpanel-handle--hidden removed — the panel is ALWAYS-ON (the review-#2 compare/periods hide is gone). */
  .ana-tpanel {
    position: fixed; top: 0; left: 0; height: 100vh; width: min(var(--tpanel-w, min(560px, 94vw)), 94vw);
    z-index: 61; display: flex; flex-direction: column;
    background: var(--bg-surface); border-right: 1px solid var(--border-default);
    box-shadow: 6px 0 26px rgba(0,0,0,0.4);
    transform: translateX(-102%); transition: transform 0.24s cubic-bezier(.2,.8,.2,1), width 0.2s ease;
  }
  .ana-tpanel--open { transform: translateX(0); }
  .ana-tpanel--max { width: min(1400px, 96vw); }                 /* S7 §3.7: maximise → ~full width */
  .ana-tpanel--dragging { transition: none; user-select: none; }  /* S7: no width transition mid-drag */
  /* S7 §3.7: the right-edge drag GRIP (new code — pointer events; data-noswipe so mobile swipe-nav can't hijack it) */
  .ana-tpanel__grip {
    position: absolute; top: 0; right: 0; width: 12px; height: 100%; z-index: 3;
    cursor: col-resize; touch-action: none;
  }
  .ana-tpanel__grip::after {
    content: ''; position: absolute; top: 50%; right: 3px; width: 3px; height: 46px; transform: translateY(-50%);
    border-radius: 3px; background: var(--border-default); transition: background 0.15s ease;
  }
  .ana-tpanel__grip:hover::after, .ana-tpanel--dragging .ana-tpanel__grip::after { background: var(--accent-primary); }
  .ana-tpanel__max { flex: 0 0 auto; }
  .ana-tpanel__max.is-on { color: var(--accent-primary); }
  .ana-tpanel__head { padding: 14px 14px 10px; border-bottom: 1px solid var(--border-subtle); flex: 0 0 auto; }
  .ana-tpanel__titlerow { display: flex; align-items: flex-start; gap: 8px; }
  /* v386 — WRAP, NEVER TRUNCATE, here too. Caught by eye on the round's own screenshot: a compare slot now
     names every category in it, and this title ellipsised it to "…Eating out and Transport vs…". The row's
     other child (the Over time / Totals segment) is `flex: 0 0 auto`, so letting the title take a second
     line costs the panel a few pixels of header and nothing else. Same rule as the slot chip and the legend.
     `align-items` moves to flex-start so the segment stays beside the FIRST line, not vertically centred
     against a two-line title. */
  .ana-tpanel__title { font-family: var(--font-head, inherit); font-size: 16px; font-weight: 600; color: var(--text-primary);
    flex: 1 1 auto; min-width: 0; overflow-wrap: break-word; }
  .ana-tpanel__seg { flex: 0 0 auto; }
  .ana-tpanel__sub { font-size: 12.5px; margin-top: 8px; }
  .ana-tpanel__pctrow { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
  .ana-tpanel__pctrow:empty { display: none; }
  .ana-tpanel__pctlbl { font-size: 12px; }
  .ana-tpanel__body { flex: 1 1 auto; overflow: auto; padding: 8px 6px 24px; }
  .ana-tpanel__empty { padding: 24px 8px; text-align: center; font-size: 14px; }
  .ana-mtab { border-collapse: collapse; width: 100%; font-size: 13px; }
  .ana-mtab th, .ana-mtab td { padding: 8px 10px; text-align: right; white-space: nowrap; }
  .ana-mtab th:first-child, .ana-mtab td.ana-mtab__name, .ana-mtab td:first-child { text-align: left; position: sticky; left: 0; background: var(--bg-surface); }
  .ana-mtab thead th { color: var(--text-tertiary); font-weight: 600; border-bottom: 1px solid var(--border-default); position: sticky; top: 0; background: var(--bg-surface); z-index: 1; }
  .ana-mtab td.mono { font-family: var(--font-mono, inherit); font-weight: 500; }   /* DM Mono has no weight > 500 */
  .ana-mtab tbody tr:hover { background: rgba(255, 255, 255, .03); }
  .ana-mtab tr.ana-mtab__tot td { border-top: 1px solid var(--border-default); color: var(--text-primary); font-weight: 600; }
  .ana-mtab__totcol { border-left: 1px solid var(--border-subtle); color: var(--text-primary); }
  .ana-mtab__dot { display: inline-block; width: 9px; height: 9px; border-radius: 3px; margin-right: 7px; vertical-align: middle; }
  /* v325: the month table's drillable Spending row (Everything) — same chevron affordance as the breakdown
     breakdown peek, sized for a table cell; its category sub-rows indent one step. */
  .ana-mtab__chev { appearance: none; background: none; border: none; cursor: pointer; color: var(--text-tertiary);
    width: 18px; height: 18px; padding: 0; margin-right: 4px; display: inline-flex; align-items: center; justify-content: center; vertical-align: middle; }
  .ana-mtab__chev svg { width: 13px; height: 13px; transition: transform 0.15s ease; }
  .ana-mtab__chev.is-open svg { transform: rotate(180deg); }
  /* v326.1: a RESERVED, empty chevron slot on a non-drilling trio row (income/kept with nothing to expand) so the
     colour dots + labels stay aligned with the drilling rows — the chevron never shoves one label out of column.
     v328: its OWN class (not .ana-mtab__chev + a modifier) so a `.ana-mtab__chev` selector only ever finds a real
     drill button, never this hidden spacer (that collision made the table-drill test grab the wrong element). */
  .ana-mtab__chevpad { display: inline-flex; width: 18px; margin-right: 4px; flex: none; vertical-align: middle; }
  .ana-mtab td.ana-mtab__name--sub { padding-left: 28px; color: var(--text-secondary); }
  .ana-mtab thead th.ana-mtab__num .ana-mtab__dot { margin-right: 5px; }   /* S7: person/bucket column-header identifier dots */
  .ana-mtab__partial { opacity: 0.72; }   /* S7: the in-progress month/period column */
  /* S7 §3.7: phone/tablet — the panel is already full-width, so drag/maximise are hidden (a graceful no-op) */
  @media (max-width: 900px) {
    .ana-tpanel, .ana-tpanel--max { width: 100vw; }
    .ana-tpanel__grip, .ana-tpanel__max { display: none; }
  }
  /* comparison card head + per-series totals (used by WHO/WHAT comparison views) */
  .ana-comphead { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 14px; margin-bottom: 10px; }
  .ana-comphead .dash-card__title { margin: 0; }
  .ana-measure { margin-left: auto; }
  .ana-compsums { display: flex; flex-wrap: wrap; gap: 6px 18px; margin-bottom: 12px; }
  .ana-compsums__item { display: inline-flex; align-items: baseline; gap: 6px; font-size: 13px; color: var(--text-secondary); }
  .ana-compsums__dot { width: 10px; height: 10px; border-radius: 3px; align-self: center; flex: none; }
  .ana-compsums__amt { color: var(--text-primary); white-space: nowrap; font-variant-numeric: tabular-nums; }
  .ana-combined { margin: -4px 0 12px; }
  /* S10 phone gate: the ranked-list-per-unit fallback for a grouped-bars compare that would be too dense at
     narrow widths (analysis.js tooManyBarsForNarrow / drawCategoryCompare) — a stack of per-person mini-lists,
     reusing the existing .ana-bdrow list-row look. */
  /* R12 (SW v342): compare's OWN "Where it went" — one row per unit, one column per person. It reuses the
     .ana-mtab look (identical data shape to the Table panel's units × people grid) but lives in a card in the
     results flow, so it needs its own horizontal scroller: with six compared people the table is genuinely
     wider than a 390px phone, and the PAGE must never scroll sideways — the TABLE does, inside this box. */
  .ana-comptab { padding-top: 6px; padding-bottom: 10px; }
  .ana-comptab .dash-card__title { margin-bottom: 6px; }
  .ana-comptab__scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; max-width: 100%; }
  /* CONTENT-WIDTH, not 100%: .ana-mtab stretches to fill its host, which inside a wide results card pushes the
     second person's column to the far right edge — and reading two people's numbers ON THE SAME LINE is the
     entire reason the human chose one merged table over stacked lists. max-content keeps the columns adjacent
     on a monitor (they already are on a phone, where the content fills the width) and lets the scroller above
     take over when there are enough people to overflow. */
  .ana-comptab__tab { width: max-content; max-width: none; }
  .ana-comptab__tab th.ana-mtab__num, .ana-comptab__tab td.ana-mtab__num { padding-left: 30px; }
  .ana-comptab__goldrow td.ana-mtab__gold { color: var(--accent-gold); }
  /* v353 (Q3): the three labelled sections of the compare-under-Everything table — Where it came from ·
     Where it went · What each kept. A section head is a full-width caption in the same small-uppercase
     register the income engine's own section captions use (.ana-bdsect), so nothing new is learned. */
  .ana-comptab__sec td { border-bottom: 0; border-top: 1px solid var(--border-default); padding-top: 16px; }
  .ana-comptab__sec:first-child td { border-top: 0; padding-top: 2px; }
  .ana-comptab__sechd { font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--text-tertiary); text-align: left; }
  .ana-comptab__pctrow td { border-bottom: 0; }
  /* below zero is the ONE place red belongs (§3) — a negative Kept, and its negative share of own income */
  .ana-mtab__neg { color: var(--accent-red); }
  .ana-comparenarrow__note { margin: 0 0 12px; }
  .ana-comparenarrow__unit { margin-bottom: 18px; }
  .ana-comparenarrow__unit:last-child { margin-bottom: 0; }
  .ana-comparenarrow__hd { display: flex; align-items: center; gap: 8px; padding: 6px 6px 8px; border-bottom: 1px solid var(--border-default); }
  .ana-comparenarrow__name { font-weight: 600; color: var(--text-primary); flex: 1 1 auto; min-width: 0; }
  .ana-comparenarrow__amt { color: var(--text-primary); }
  /* ── R0 redesign: control surface (breadcrumb-drill + verbs) · lens switcher · breakdown rows ───── */
  .ana-controls2 { margin-bottom: var(--gap); }
  /* R4: saved reports strip */
  .ana-presets { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
  .ana-presets__lbl { font-size: 11px; letter-spacing: 0.05em; text-transform: uppercase; margin-right: 2px; }
  /* Slice 6 — the ★ Saved ▾ dropdown (apply · × delete · "Save this view" inside). */
  .ana-menu--saved .ana-menu__lbl { display: none; }   /* no uppercase axis label — just "★ Saved" */
  .ana-menu--saved .ana-menu__trigger { min-width: auto; }
  .ana-menu--saved .ana-menu__val { font-size: 13px; color: var(--text-secondary); }
  .ana-saved-pop { min-width: 240px; max-width: 320px; padding: 6px; }
  .ana-saved__empty { padding: 10px 8px; font-size: 12.5px; }
  .ana-saved__row { display: flex; align-items: center; gap: 4px; }
  .ana-saved__apply { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 8px; appearance: none; background: transparent; border: 0; font-family: inherit; font-size: 13.5px; color: var(--text-secondary); cursor: pointer; text-align: left; padding: 8px; border-radius: 7px; }
  .ana-saved__apply:hover { background: var(--bg-hover); color: var(--text-primary); }
  .ana-saved__star { color: var(--accent-gold); flex: none; }
  .ana-saved__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .ana-saved__x { flex: none; appearance: none; background: transparent; border: 0; color: var(--text-tertiary); font-size: 16px; line-height: 1; cursor: pointer; padding: 6px 8px; border-radius: 6px; }
  .ana-saved__x:hover { color: var(--accent-warm); background: var(--bg-hover); }
  .ana-saved__sep { height: 1px; background: var(--border-subtle); margin: 6px 2px; }
  .ana-saved__saverow { display: flex; align-items: center; gap: 6px; padding: 2px; }
  .ana-saved__nameinput { flex: 1 1 auto; min-width: 0; box-sizing: border-box; }
  .ana-saved__savebtn { flex: none; padding: 8px 12px; font-size: 13px; }
  /* Export slice D — "⎙ Save as PDF", beside ★ Saved. Always at least 40px tall (a real tap target); z-index
     keeps it above the CLOSED ★ Saved popup box.
     v383: `margin-left: auto` DROPPED. It used to push the button to the far end of the strip, which read
     fine while the "SAVED REPORTS" label anchored the left. With that label retired the button was left
     floating in the middle of an otherwise empty row (caught on the desktop screenshot). It now simply
     follows ★ Saved — the same pairing the phone layout ratifies — and the [Explore | Usual ranges] strip's
     own `margin-left: auto` still carries it to the right edge. Flagged veto-able on the round's card. */
  .ana-pdfbtn { margin-left: 0; position: relative; z-index: 1; flex: none; appearance: none; cursor: pointer;
    background: var(--bg-elevated); border: 1px solid var(--border-default); border-radius: 9px;
    color: var(--text-primary); font-family: inherit; font-size: 13px; padding: 8px 12px; min-height: 40px; }
  .ana-pdfbtn:hover { border-color: var(--accent-primary); background: var(--bg-hover); }
  .ana-pdfbtn:focus-visible { outline: 2px solid var(--accent-primary); outline-offset: 2px; }
  /* On a phone the right edge of the frame is where the Ledger panel's handle tab floats, so the far-right
     placement would put the button half under it (caught on the 390px screenshot). Below the breakpoint it
     simply follows ★ Saved instead — same strip, same order, nothing covering it. */
  @media (max-width: 700px) { .ana-pdfbtn { margin-left: 0; } }
  /* ── v383 · THE PHONE HEADER SHUFFLE (human-ratified 2026-08-20) ─────────────────────────────────────
     Three rows became two. The "SAVED REPORTS" heading label is gone from the markup entirely (the ★ Saved
     trigger says what it is), the [Explore | Usual ranges] toggle moves ABOVE, and ★ Saved gives up just
     enough width for the ⎙ button to sit on its RIGHT so the pair shares one line. It is flex `order` on a
     strip that already wraps, so the DESKTOP arrangement is untouched — nothing above the breakpoint moves. */
  @media (max-width: 700px) {
    .ana-presets .ana-subviewstrip { order: 1; width: 100%; margin-left: 0; }
    .ana-presets .ana-menu--saved { order: 2; flex: 1 1 auto; min-width: 0; width: auto; }
    .ana-presets .ana-menu--saved .ana-menu__trigger { width: 100%; max-width: none; }
    .ana-presets .ana-pdfbtn { order: 3; margin-left: 0; }
  }
  /* v350 — icon-only. The name now lives in `title`/`aria-label` (hover tooltip on desktop, long-press note on a
     phone), so the button becomes a square 40px tap target holding just the ⎙ glyph. Same box, same placement. */
  .ana-pdfbtn--icon { display: inline-flex; align-items: center; justify-content: center; min-width: 40px; padding: 0 10px; }
  .ana-pdfbtn__ico { font-size: 17px; line-height: 1; display: block; }
  .ana-preset { appearance: none; display: inline-flex; align-items: center; gap: 6px; font-family: inherit; font-size: 13px;
    cursor: pointer; padding: 6px 11px; border-radius: 999px; border: 1px solid var(--border-default); background: var(--bg-input); color: var(--text-primary); }
  .ana-preset:hover { border-color: var(--border-strong); }
  .ana-preset--starter { background: var(--bg-surface); color: var(--text-secondary); }
  .ana-preset--save { border-style: dashed; color: var(--accent-primary); border-color: var(--accent-primary); background: none; }
  .ana-preset__x { color: var(--text-tertiary); font-size: 15px; line-height: 1; }
  .ana-preset__x:hover { color: var(--accent-warm); }
  .ana-preset__name { width: 180px; max-width: 52vw; padding: 7px 10px; font-size: 13px; }
  .ana-preset__savebtn { padding: 7px 14px; font-size: 13px; }
  .ana-whowrap { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 8px; }
  /* the Single/Add/Compare mode toggle at the top of the WHO dropdown (gates single vs multi select) */
  .ana-whomode { display: flex; margin-bottom: 8px; }
  .ana-whomode .dash-seg__btn { flex: 1 1 0; text-align: center; }
  .ana-menu__hint { font-size: 11px; line-height: 1.35; margin: -2px 2px 8px; }
  /* "one comparison at a time" info panel — anchored to the Compare button the user clicked (2026-07-09 review) */
  .ana-blocknote { position: fixed; z-index: 300; max-width: 260px; display: flex; gap: 8px; align-items: flex-start;
    padding: 10px 12px; border-radius: 10px; background: var(--bg-elevated); border: 1px solid var(--border-default);
    box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45); color: var(--text-primary); font-size: 13px; line-height: 1.4;
    opacity: 0; transform: translateY(-4px); transition: opacity 0.14s ease, transform 0.14s ease; }
  .ana-blocknote--show { opacity: 1; transform: translateY(0); }
  .ana-blocknote__icon { flex: 0 0 auto; color: var(--accent-primary); font-weight: 700; }
  .ana-blocknote__msg { flex: 1 1 auto; white-space: pre-line; }   /* honour explicit line breaks so a bulleted note (drift "What is this?") reads as points; single-line notes unaffected */
  /* the "More in Help →" deep link inside an anchored note (copy pass round 1 — the relocation GATE:
     copy moved into Help must stay reachable from the screen it left). `display:block` puts it on its own
     line under the prose without needing a <br> inside a pre-line text node. */
  .ana-blocknote__link { display: block; margin-top: 8px; background: none; border: none; padding: 0;
    font-family: inherit; font-size: 12px; color: var(--accent-primary); cursor: pointer; text-align: left; }
  .ana-blocknote__link:hover { text-decoration: underline; }
  .ana-blocknote__link:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-glow); border-radius: 6px; }
  /* ── the shared ⓘ affordance (copy pass round 1) ─────────────────────────────────────────────────
     Quiet by default — it must never read as a warning, so no accent fill and no alarm colour. Same
     visual as the earlier hand-rolled `.intent-infobtn`, minus that screen's `margin-right:auto`. */
  .info-dot { background: none; border: 1px solid var(--border-default); color: var(--text-tertiary);
    width: 22px; height: 22px; border-radius: 50%; font-size: 12px; line-height: 1; padding: 0; cursor: pointer;
    display: inline-flex; align-items: center; justify-content: center; flex: none; }
  .info-dot:hover { color: var(--text-primary); border-color: var(--border-strong); }
  .info-dot:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-glow); }
  /* `<h2>` + its ⓘ on one baseline. h2's own `margin: 0 0 4px` moves to the wrapper so vertical rhythm
     is byte-identical to the bare heading it replaces. */
  .copy-head { display: flex; align-items: center; gap: 8px; margin: 0 0 4px; }
  /* v385 — the row now also carries an h3 (Home's Kept card), so the margin reset covers both. */
  .copy-head h2, .copy-head h3 { margin: 0; }
  /* breadcrumb-drill row */
  .ana-breadcrumb { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 6px; margin-top: 12px; }
  .ana-crumb { appearance: none; background: none; border: none; font-family: inherit; font-size: 14px; padding: 4px 6px; border-radius: 8px; }
  .ana-crumb--link { color: var(--text-secondary); cursor: pointer; }
  .ana-crumb--link:hover { color: var(--text-primary); background: var(--bg-hover); }
  .ana-crumb--cur { color: var(--text-primary); font-weight: 600; }
  .ana-crumb__sep { color: var(--text-tertiary); font-size: 13px; }
  /* typeable jump — §4 fix #8: the bare `.ana-jump` wrapper was only the (now-deleted) dead buildJump's own div;
     the `.ana-jump__*` piece classes below stay LIVE (shared by buildWhatSearch, the WHAT popup's search box). */
  .ana-jump__input { width: 150px; max-width: 46vw; padding: 7px 10px; font-size: 13px; }
  .ana-jump__pop { position: absolute; top: calc(100% + 4px); left: 0; z-index: 40; min-width: 240px; max-width: 330px; padding: 4px; }
  .ana-jump__opt { display: flex; align-items: baseline; gap: 8px; padding: 8px 10px; border-radius: 8px; cursor: pointer; }
  .ana-jump__opt:hover, .ana-jump__opt--hi { background: var(--bg-hover); }
  .ana-jump__type { flex: 0 0 64px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; }
  .ana-jump__lbl { color: var(--text-primary); font-size: 14px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* lens switcher strip (under the headline tiles) */
  .ana-lensstrip { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 12px; margin-bottom: 12px; }
  /* S4 (D6): an honestly-DISABLED tab/segment button — greyed with a reason tooltip (title attr), never a
     live-looking dead control and never hidden (stable geography). Wired via class + aria-disabled (a disabled
     attr would swallow the hover tooltip); the :disabled selector is covered too for any future native wiring. */
  .dash-seg__btn:disabled, .dash-seg__btn--off { opacity: 0.38; cursor: default; }
  .dash-seg__btn--off:hover { background: none; color: inherit; }
  .ana-shbtn--disabled { opacity: 0.35; cursor: default; }
  .ana-shbtn--disabled:hover { background: none; }
  /* S3 (§3.3/D1): the "Comparing: A vs B ✕" pill sits in the lens-tab row with TAB-CLASS visual weight (a bordered,
     elevated, semibold pill — not a slim chip a thumb misses). ✕ is the one consistent, undoable compare exit. */
  /* S7.5 #2: the current-subject check on WHAT-popup rows (Single mode) */
  .ana-curmark { margin-left: auto; display: inline-flex; align-items: center; color: var(--accent-primary); flex: none; }
  .ana-curmark svg { width: 14px; height: 14px; }
  .ana-comparepill { display: inline-flex; align-items: center; gap: 6px; max-width: 100%;
    border: 1px solid var(--accent-primary); background: var(--bg-elevated); border-radius: 999px;
    padding: 4px 5px 4px 13px; }
  .ana-comparepill__lbl { font-size: 12px; font-weight: 600; color: var(--text-primary);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .ana-comparepill__x { appearance: none; background: none; border: none; cursor: pointer; flex: none;
    color: var(--text-secondary); font-size: 13px; line-height: 1; width: 22px; height: 22px; padding: 0;
    border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; }
  .ana-comparepill__x:hover { background: var(--bg-hover); color: var(--text-primary); }
  /* the breakdown list — the "Where it went" card (v325's Everything bifurcation, generalised to every subject
     at R11 and, since v367, the ONE host of the breakdown rows: the retired List skin's own card class went
     with the merge and its rule folded into this one). Extra right padding keeps the right-aligned amounts clear of the
     floating Ledger-panel handle (fixed at the right edge, vertically centred). */
  /* v368 ④: `.ana-incomecard` is the "Where it came from" streams card that sits ABOVE it at Everything. It
     wears the SAME rule (identical padding + title rhythm — two stacked cards that read as one pair) but its
     OWN class, because `.ana-bifur` is what a dozen suites select as `querySelector('.ana-bifur')` and a
     second card carrying that name would silently hand them the wrong one. No new colours: the rows are
     `.ana-bdrow`s exactly as in the card below, teal via childColor's income family. */
  .ana-bifur, .ana-incomecard { padding-top: 6px; padding-bottom: 6px; padding-right: 44px; }
  .ana-bifur .dash-card__title, .ana-incomecard .dash-card__title { margin-bottom: 2px; }
  /* R9 (SW v339) item (2) — STRICT COLUMNS. §4 fix #5 made name/amount fixed-basis flex items, which lined the
     bars up but left the row `flex-wrap`: a long name plus a wide amount still reflowed the AMOUNT underneath
     the name (the human's wrapping complaint). The row is a real GRID now — name | bar | % | amount — so a long
     name wraps INSIDE its own cell and every other cell holds its place. Only the TEMPLATE varies per row kind
     (below); the base carries no template of its own, so a variant rule can never be out-specified by the
     phone media query further down. */
  .ana-bdrow { display: grid; align-items: center; gap: 6px 10px; padding: 9px 6px; border-bottom: 1px solid var(--border-subtle); }
  .ana-bdrow--bars { grid-template-columns: minmax(110px, 34%) 1fr 44px minmax(92px, max-content); }
  .ana-bdrow--sw.ana-bdrow--bars { grid-template-columns: 10px minmax(110px, 34%) 1fr 44px minmax(92px, max-content); }
  /* (3) no income in the period ⇒ no ruler ⇒ no bar and no % at all (the greyed-donut honesty family) */
  .ana-bdrow--nobar { grid-template-columns: 1fr minmax(92px, max-content); }
  .ana-bdrow--sw.ana-bdrow--nobar { grid-template-columns: 10px 1fr minmax(92px, max-content); }
  .ana-bdrow:last-child { border-bottom: none; }
  .ana-bdrow--sub { font-size: 13px; }
  .ana-bdrow--sub .ana-bdrow__name { color: var(--text-secondary); }
  /* v326: a PEEKABLE row (its tap expands the row inline — the chevron button is gone). Renamed from the old
     --drill hover; the whole row is the affordance now (pointer + hover). A non-peekable leaf gets neither. */
  /* R5 (SW v334): the row is a TAP SURFACE, so it gets the tap-surface treatment the donuts already carry —
     `touch-action: manipulation` (no 300ms double-tap-zoom wait, which is a real part of "the tap felt slow"),
     no platform tap-highlight rectangle over the bars, and a PRESSED state so a finger gets feedback in the
     same frame rather than only when the merchant aggregate lands. */
  .ana-bdrow--peek { cursor: pointer; border-radius: 8px; touch-action: manipulation;
    -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; }
  .ana-bdrow--peek:hover { background: var(--bg-hover); }
  .ana-bdrow--peek:hover .ana-bdrow__name { color: var(--text-primary); }
  .ana-bdrow--peek:active { background: var(--bg-hover); filter: brightness(1.18); }
  .ana-bdrow--peek:active .ana-bdrow__name { color: var(--text-primary); }
  /* R5: the one calm placeholder child drawn while a just-opened category peek waits for its merchants. */
  .ana-bdrow--peekwait { grid-template-columns: 1fr; cursor: default; }
  .ana-bdrow--sw.ana-bdrow--peekwait { grid-template-columns: 10px 1fr; }
  /* v345: the wait line carries the app's ONE loading visual (three fading dots, .hload__spin) beside the words
     — the human's ask, "that same waiting thing we have built". `.hload` on the same span brings its grace-period
     fade-in, so a warm drill that lands in a few ms never flashes anything. Row scale: smaller dots, tighter gap.
     A FAILED read swaps that line for the shared tap-to-retry (.load-retry) — muted, never red (§3: red is the
     zero line only), and it is a real affordance because the failure was never cached. */
  .ana-bdrow__wait { display: inline-flex; align-items: center; gap: 8px; }
  .ana-bdrow__wait .hload__spin { gap: 4px; }
  .ana-bdrow__wait .hload__spin i { width: 5px; height: 5px; }
  .ana-bdrow__wait .hload__txt { font-size: inherit; }
  .ana-bdrow--peekfail { cursor: default; }
  .ana-bdrow__retry { margin: 0; display: inline-block; }
  /* the results-region + chart-card read-failure line (resultsRetry / retryCard) */
  .ana-loadfail { margin: 0; padding: 6px 2px; }
  /* R9 (4) — THE MERCHANT LINE. A merchant is a leaf: a bar drawn against its own parent could only ever say
     "this is some of that", and against income it would say nothing about the fan it sits in. So it is a quiet
     indented line with the numbers right beside the name in lighter ink — tools/merch-rows-mock.src.html,
     ratified 2026-08-12. Still a GRID (not flex) so a swatched card's reserved gutter lines up cell-for-cell
     with the bar rows above it. */
  .ana-bdrow--mline { grid-template-columns: minmax(0, max-content) max-content; justify-content: start; align-items: baseline; padding: 6px 6px; }
  .ana-bdrow--sw.ana-bdrow--mline { grid-template-columns: 10px minmax(0, max-content) max-content; }
  .ana-bdrow--mline .ana-bdrow__name { color: var(--text-secondary); font-size: 13px; }
  .ana-bdrow--mline .ana-bdrow__amt { color: var(--text-secondary); font-size: 13px; min-width: 0; text-align: left; }
  .ana-bdrow__mnum { min-width: 0; color: var(--text-tertiary); font-size: 12.5px; white-space: nowrap; font-variant-numeric: tabular-nums; }
  .ana-bdrow__mpct { color: var(--text-tertiary); }
  /* R10 (SW v340) — THE REIMBURSEMENT LINE-ITEM. v330's gold SENTENCE ("incl. X returned") is retired from
     every list; the money that came back is now a row like the shops above it, in the same quiet --mline
     shape at the same indent, so the fan reads as one column of numbers that adds up. GOLD is the ratified
     colour for money returned (§3) — deliberately NOT the red of --fill--neg, even though the amount is
     drawn negative: red is reserved for a genuine zero-or-below boundary and money coming back is not one.
     The Sankey keeps the sentence (a ribbon fan cannot draw a negative band). */
  .ana-bdrow--reimb .ana-bdrow__name,
  .ana-bdrow--reimb .ana-bdrow__amt,
  .ana-bdrow--reimb .ana-bdrow__mnum { color: var(--accent-gold); }
  /* v326: the grain toggle ("Categories | Groups"). v367: it rehomed out of the retired List's Spending peek
     onto the "Where it went" card itself, so it is a plain block in the card rather than an indented row. */
  .ana-bifurgrain { padding: 6px 6px 8px 0; }
  .ana-bifurgrain .dash-seg { display: inline-flex; }
  /* R9 (2): the name is a real grid CELL now — it wraps inside itself instead of ellipsing, and nothing it does
     can move the bar, the % or the amount. (The old `flex: 0 0 34%` + nowrap + ellipsis is gone with the flex row.) */
  .ana-bdrow__name { min-width: 0; color: var(--text-primary); overflow-wrap: break-word; }
  /* R9 (1): the hatched remainder TRACK IS GONE — no background, no border, no hatch. The colour fill just ends
     against the card. (The Intentions hatches — .intent-meter__seg--free, .intent-track__seg--left/--past/--over,
     .intent-hist__* — are a separately-ratified surface and are deliberately left byte-identical.) */
  .ana-bdrow__barwrap { height: 14px; min-width: 40px; position: relative; }
  .ana-bdrow__fill { display: block; height: 100%; border-radius: 2px; }
  .ana-bdrow__fill--out { background: var(--accent-out); }
  .ana-bdrow__fill--in { background: var(--accent-teal); }
  .ana-bdrow__fill--gold { background: var(--accent-gold); }
  .ana-bdrow__fill--neg { background: var(--accent-red); }   /* §4 fix #1 — a below-zero row (e.g. negative liquid) */
  /* R9 (5): nesting must read at a glance — a SUB-row's bar is a lighter tint of its own family colour. Derived
     by LIGHTNESS only (the outShade precedent): no new semantic colour, so a child can never mean something the
     palette has not already agreed on. */
  .ana-bdrow__fill--tint { opacity: .55; }
  .ana-bdrow__pct { text-align: right; font-size: 12px; font-variant-numeric: tabular-nums; }
  /* review round 1 · item 3a: the optional leading colour chip (Everything donut, Spending slice split open).
     R9 (6): --space is the SAME cell with no colour — a swatched card's peek children reserve the gutter, so
     every row's bar/%/amount stays at one x whether or not that row carries a chip. */
  .ana-bdrow__sw { width: 10px; height: 10px; border-radius: 2.5px; align-self: center; }
  .ana-bdrow__sw--space { background: none; }
  /* review round 1 · item 4 (the human: "the minus sign lands on its own line"). The ASCII hyphen in
     "-A$2,284.67" is a UAX#14 break opportunity, so with no white-space rule the negative wrapped straight
     after the minus while a positive of the same width merely overflowed — two different wrong answers to the
     same problem, which is why the amount column looked misaligned at 5-7 digits. nowrap forbids the break;
     flex 0 1 auto + a 92px MINIMUM keeps the old alignment for ordinary amounts and lets a genuinely wider one
     take the room it needs (min-width:0 had to go — it is what let the box shrink below its own content).
     R9: the flex basis is gone with the flex row — the 92px minimum now lives in the grid TEMPLATE
     (`minmax(92px, max-content)`), which does the same job and additionally lets a genuinely wider amount take
     the room it needs without ever shrinking the bar below its own min-width. */
  .ana-bdrow__amt { white-space: nowrap; text-align: right; color: var(--text-primary); font-weight: 500; font-variant-numeric: tabular-nums; }
  .ana-leafhint { margin-bottom: 8px; }
  /* R1: breakdown SHAPES — donut + table + the shape dropdown */
  .ana-shapedd .dropdown__trigger { min-width: 110px; }
  .ana-donutwrap { display: flex; align-items: center; flex-wrap: wrap; gap: 16px 28px; }
  .ana-donut { width: 184px; height: 184px; flex: none; }
  /* v328: the Everything donut — a non-square viewBox (360×300) so the "income" mark fits; sized by width,
     height auto (never the .ana-donut fixed square). Slices/segments are tappable (split-in-place).
     R4 (v333): when the month spent past its income the svg keeps its width and GROWS its viewBox upward
     (and, on a very large overspend, downward) to hold the second lap riding outside the ring plus the income
     mark above it — so `height: auto` simply draws a slightly taller card. Nothing here needs to know the
     numbers; the geometry lives in analysis.js everythingDonut. */
  /* review round 1 · item 2 (the human: "a transparent green/grey box flashes over the donut on a longer tap").
     Nothing suppressed the platform tap-highlight / long-press selection + callout, so a slow tap on a slice
     painted the OS highlight rectangle over the artwork and offered a text selection of the SVG. Both donuts in
     the family get the same treatment — they are tap surfaces, not text. */
  .ana-edonut, .ana-donut { -webkit-tap-highlight-color: transparent; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }
  .ana-edonut { width: min(100%, 300px); height: auto; flex: 0 1 300px; touch-action: manipulation; }
  .ana-edonut .face { cursor: pointer; transition: filter .14s; }
  .ana-edonut .face:hover { filter: brightness(1.09); }
  .ana-edonut__open { animation: ana-edpop .24s ease; }
  @keyframes ana-edpop { from { opacity: .4; } to { opacity: 1; } }
  @media (prefers-reduced-motion: reduce) { .ana-edonut .face { transition: none; } .ana-edonut__open { animation: none; } }
  .ana-donut__total { fill: var(--text-primary); font-size: 20px; font-family: var(--font-mono, inherit); font-weight: 500; }
  .ana-donutlegend { flex: 1 1 240px; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
  .ana-dlrow { display: flex; align-items: baseline; gap: 10px; padding: 6px 0; border-bottom: 1px solid var(--border-subtle); }
  .ana-dlrow:last-child { border-bottom: none; }
  .ana-dl__dot { width: 11px; height: 11px; border-radius: 3px; flex: none; align-self: center; }
  /* v376: a legend row whose colour is no longer in the ring (Spending, while the ring is drilled into its
     categories) keeps the row and drops the dot — the label still lines up with its swatched siblings. */
  .ana-dlrow--nodot { padding-left: 21px; }
  .ana-dl__lbl { flex: 1 1 auto; min-width: 0; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .ana-dl__pct { flex: 0 0 40px; text-align: right; font-size: 12px; }
  .ana-dl__amt { flex: 0 1 auto; min-width: 76px; text-align: right; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-variant-numeric: tabular-nums; }
  /* R3: interactive chart tooltip. S8 "tooltip z-index fix": was 80 — a tap/hover on a chart element INSIDE the
     per-chart expand overlay (.ana-expand, z-index 90) still needs its tooltip visible, so this now paints above
     the overlay (96 > 90); nothing else in the app uses 81-95, so the gap is deliberate headroom. */
  .ana-tip { position: fixed; z-index: 96; pointer-events: none; max-width: 260px; padding: 8px 11px;
    background: var(--bg-elevated); border: 1px solid var(--border-strong); border-radius: 10px;
    box-shadow: 0 6px 22px rgba(0,0,0,0.45); font-size: 13px; }
  .ana-tip__lbl { color: var(--text-primary); font-weight: 500; margin-bottom: 2px; }
  .ana-tip__amt { color: var(--text-primary); font-family: var(--font-mono, inherit); }
  .ana-tip__meta { font-size: 11px; margin-top: 3px; }
  /* v349 ④ — THE TOOLTIP CARET (human-ratified 2026-08-13, "wherever I can choose"). A floating card that
     merely appeared NEAR the pointer never said WHICH mark it was about — on a dense grouped bar chart, or a
     donut whose slices meet, the reader had to infer it. A small triangle now points back at the exact bar /
     dot / slice under the pointer. It is drawn in two halves so the 1px border shows: an outer triangle in
     the border colour with an inner one in the card's own background sitting 1px inside it. The caret slides
     along the card's edge to stay over the mark even when the card is clamped against a screen edge, and it
     flips to the top edge when the card has to sit BELOW the mark (no room above).
     v389 — THE GEOMETRY IS NOW `.hui-caret`, ONE BLOCK FOR EVERY CHART TOOLTIP (the drift/usual-ranges tip
     gained a caret this round, and the queued "caret everywhere incl. donut" item becomes call sites rather
     than copies). Each tooltip keeps its OWN identity class beside it (.ana-tip__caret / .drift-tip__caret)
     — that is the hook a test or a future per-surface tweak reads; the triangle itself is drawn once here.
     JS half: HUI.chartHit.placeTip (app.js), whose CARET_HALF must stay equal to the 7px border below. */
  .hui-caret { position: absolute; width: 0; height: 0; border-left: 7px solid transparent; border-right: 7px solid transparent; }
  .hui-caret::after { content: ''; position: absolute; left: -6px; width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; }
  .hui-caret--down { bottom: -7px; border-top: 7px solid var(--border-strong); }
  .hui-caret--down::after { top: -8px; border-top: 6px solid var(--bg-elevated); }
  .hui-caret--up { top: -7px; border-bottom: 7px solid var(--border-strong); }
  .hui-caret--up::after { bottom: -8px; border-bottom: 6px solid var(--bg-elevated); }
  /* ── v392 — A CHART HIT SURFACE WEARS NO PLATFORM TAP-HIGHLIGHT ────────────────────────────────────
     The human, on the forecast day-by-day trace: every tooltip tap flashes the mobile browser's grey
     rectangle. The rectangle is drawn over the element that handled the tap, and on a chart that is the
     whole <svg> — so a tap to read one day flashes the entire chart. The class is applied by
     HUI.chartHit.bind (app.js), i.e. by the definition of "this is a chart hit surface", so no chart can
     be forgotten and no blind global is needed. The property INHERITS, so the marks inside are covered.
     `-webkit-touch-callout` goes with it (a slow tap on an SVG can otherwise raise the platform menu) and
     the selection guard stops a drag from highlighting the axis text — the same three the donut already
     carries at .ana-edonut. Focus is NOT suppressed: a bound <svg> is not focusable, and a chart that
     grows keyboard-reachable marks must keep its ring. */
  .hui-charthit { -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none;
    user-select: none; -webkit-user-select: none; }
  /* ── S8 (§3.7 "Phone user"): per-chart EXPAND overlay — move-the-live-node pattern ─────────────────────────
     A small button on every chart CARD moves that card's live body (chart + legend, exactly as rendered — not
     a re-render) into a full-screen overlay; closing restores it to its original spot. See analysis.js
     wireChartExpands/openChartExpand/closeChartExpand for the JS half (the "close-on-draw" rule lives there). */
  .ana-card__expand { position: absolute; top: 8px; right: 8px; z-index: 2; width: 34px; height: 34px;
    background: rgba(0,0,0,0.35); border: 1px solid var(--border-default); border-radius: 9px; color: var(--text-primary); }
  .ana-card__expand:hover { background: rgba(0,0,0,0.55); }
  .ana-card__expand svg { width: 15px; height: 15px; }
  .ana-card__body { position: relative; }
  /* ── REVIEW ROUND 3 · part 4: the fullscreen is a plain TOP-TO-BOTTOM PAGE (the ratified mock's bottom demo).
     It used to be an absolutely-positioned stage centred with translate(-50%,-50%) and `align-items:center` —
     which is exactly why the legend was cut off in landscape: a centred flex item taller than its box overflows
     EQUALLY off both ends, and the top end (where the legend lives) went under the bar with no way to scroll to
     it. Now: a slim bar, then the stage taking the rest of the height, scrolling from its top. The chart asks
     for the height that is actually left (analysis.js measureH), so portrait gets a TALL chart, not a strip. */
  .ana-expand { position: fixed; inset: 0; z-index: 90; display: none; }
  .ana-expand.is-open { display: flex; flex-direction: column; }
  .ana-expand__backdrop { position: absolute; inset: 0; background: rgba(4,4,10,0.94); z-index: 0; }
  .ana-expand__bar { position: relative; z-index: 1; flex: 0 0 auto; display: flex; align-items: center;
    justify-content: flex-end; gap: 8px; padding: 10px 12px; }
  /* R7 item H: the chart's own name, first in the bar, `margin-right:auto` pushing the buttons to the right
     edge (the ratified chart-scale mock's `.fs__bar .ttl` rule verbatim). min-width:0 so a long title
     ellipses instead of squeezing the close button off a phone. */
  .ana-expand__ttl { margin-right: auto; min-width: 0; font-size: 13.5px; font-weight: 500;
    color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .ana-expand__btn { position: relative; width: 42px; height: 42px; display: flex; align-items: center;
    justify-content: center; background: rgba(255,255,255,0.08); border: 1px solid var(--border-default);
    border-radius: 11px; color: var(--text-primary); cursor: pointer; padding: 0; }
  .ana-expand__btn:hover { background: rgba(255,255,255,0.18); }
  .ana-expand__btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-glow); }
  .ana-expand__btn svg { width: 18px; height: 18px; }
  /* the landscape-rotation aid is a portrait-phone affordance only — pointless on desktop/landscape */
  .ana-expand__rotbtn { display: none; }
  @media (max-width: 640px) and (orientation: portrait) { .ana-expand__rotbtn { display: flex; } }
  .ana-expand__stage { position: relative; z-index: 1; flex: 1 1 auto; min-height: 0; width: 100%;
    display: block; overflow: auto; padding: 10px 12px 16px; box-sizing: border-box;
    /* v409 — PAN ON BOTH AXES. `pan-y pinch-zoom` was the whole of defect ② in the 2026-08-26 phone
       review: the stage's `overflow: auto` was willing to scroll sideways, and the touch policy above
       it forbade the gesture that would have done so — so a diagram wider than the phone simply could
       not be reached ("once I reach the categories… there's no way to scroll further"). A live
       line/bar chart is UNAFFECTED because its own plot declares `touch-action: pan-y` one level down
       (`.ana-cstage`), so a horizontal drag there still belongs to the chart's {i0,i1} window gesture
       and never becomes a stage pan. Only the surfaces that declare nothing — the Sankey, a wide
       legend — gain the sideways pan they always needed. */
    -webkit-overflow-scrolling: touch; touch-action: pan-x pan-y pinch-zoom;
    /* v376 — PAN CHROME IS APP-NATIVE (human-ratified 2026-08-19). This round introduces NO new scroll
       container: chart panning is the existing {i0,i1} drag gesture. But the stage itself can scroll when a
       tall legend rides along, and a browser-default grey bar on a near-black overlay is exactly what the
       ratification was about — so it wears the app's own tokens, thin, like every other themed scroller
       here (`.combo__list`, `.lcat-box`). */
    scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
  .ana-expand__stage::-webkit-scrollbar { width: 10px; height: 10px; }
  .ana-expand__stage::-webkit-scrollbar-track { background: transparent; }
  .ana-expand__stage::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 999px;
    border: 2px solid transparent; background-clip: content-box; }
  .ana-expand__stage::-webkit-scrollbar-thumb:hover { background: var(--text-tertiary); background-clip: content-box; }
  .ana-expand__stage > * { width: 100%; }
  /* v376 — the donut's sizing follows rule 1 the way a donut can: it has ONE mark, so on a card it stays at
     its efficient 300px (that IS "do not stretch pointlessly"), and in the ⛶ stage — where the reader went
     precisely to see it bigger — it takes a generous share of the room it was given. Nothing else about the
     donut changes this round, as ratified. */
  .ana-expand__stage .ana-edonut, .ana-expand__stage .ana-donut { width: min(100%, 560px); flex: 0 1 560px; }
  /* the rotate aid still exists (a portrait phone with a wide chart) — it now steps OUT of the column and sizes
     itself against the viewport minus the bar, whose height rides in --anabar (set by openChartExpand). */
  .ana-expand__stage--rot { position: absolute; left: 50%; top: calc(var(--anabar, 56px) + (100% - var(--anabar, 56px)) / 2);
    width: calc(100vh - var(--anabar, 56px) - 20px); height: calc(100vw - 20px);
    transform: translate(-50%, -50%) rotate(90deg); }
  @media (prefers-reduced-motion: no-preference) { .ana-expand__backdrop { transition: opacity .15s ease; } }
  /* The floating-baseline ZERO line (drawn only when a chart actually dips below zero).
     ── COLOUR IDENTITY ROUND, human-ratified 2026-08-13: it is SOFT GREY, SOLID and THIN, no longer red and
     dashed. The reasoning the human gave: the zero line is a RULER, not an alarm — it is furniture that says
     "here is nothing", and dressing the furniture in the app's one alarm colour spent red on a gridline and
     left it worth less where it means something. Every below-zero VALUE keeps red: negative columns, the kept
     line's below-zero segments, negative amounts, the donut's over-income treatment. Only the ruler changed. */
  .ana-zeroline { stroke: var(--text-tertiary); stroke-width: 1.1; opacity: 0.9; }
  .ana-zeroline__lbl { fill: var(--ana-axis-ink); }   /* v366 ②: the zero tick is an axis label and reads in the same register — same ink. The LINE itself stays --text-tertiary (v347: the ruler is furniture). */
  /* v387 — the % axis caption ("% of spending" / "% of income") over the money rail. It carries `.ana-axis`
     so it takes the axis ink here AND the report's faint print ink for free; only the SIZE steps down, so it
     reads as a caption rather than as another tick. Never drawn in Raw (an "A$" prefix is its own caption). */
  .ana-unitcap { font-size: 11.5px; }

  /* phone: stack the axis menus full-width so the summaries aren't truncated */
  @media (max-width: 560px) {
    .ana-axisbar { flex-direction: column; align-items: stretch; }
    .ana-menu, .ana-whengroup { width: 100%; }
    .ana-menu__trigger { width: 100%; max-width: none; }
    .ana-menu__pop { max-width: none; }
    .ana-whowrap { width: 100%; }
    .ana-whowrap .ana-menu { flex: 1 1 auto; }
    .ana-jump__input { width: 100%; max-width: none; box-sizing: border-box; }
    /* keep name · % · amount on a phone (the % carries the proportion). R9: hiding the bar is no longer enough —
       a grid ROW must lose the bar's COLUMN too, or the % slides into it and the amount into the %'s. Only the
       bar-bearing template changes; --nobar/--mline/--peekwait already have no bar column to drop. */
    .ana-bdrow__barwrap { display: none; }
    /* the % column shrinks to what it actually holds ("100%") and the gap tightens, so the freed width all
       goes to the NAME — with only ~330px of card a long category name is the one thing that needs it */
    .ana-bdrow { column-gap: 8px; }
    .ana-bdrow--bars { grid-template-columns: 1fr 38px minmax(92px, max-content); }
    .ana-bdrow--sw.ana-bdrow--bars { grid-template-columns: 10px 1fr 38px minmax(92px, max-content); }
  }

  /* category avatar — BARE treatment (human-approved 2026-07-30): no chip. An emoji (when the category has
     one) brings the colour + variety; the fallback icon/monogram stays engine-tinted. Slot width is kept so
     the name column aligns across every row. */
  .catav { flex: none; width: 38px; height: 38px; border-radius: 11px; display: flex; align-items: center; justify-content: center; }
  .catav svg { width: 20px; height: 20px; }
  .catav--mono { font-weight: 700; font-size: 17px; }
  .catav__emoji { font-size: 22px; line-height: 1; }
  .catav--life_costs { color: var(--accent-warm); }
  .catav--income { color: var(--accent-teal); }
  .catav--reserves { color: var(--accent-gold); }
  .catav--neutral { color: var(--text-secondary); }
  /* SESSION VEIL (§4.7): a locked private row — calm, muted, tap-to-unlock. Never alarm colours. */
  .catav--veil { color: var(--text-tertiary); }
  .catav--veil svg { width: 16px; height: 16px; }

  /* CATEGORY EMOJI — the tappable emoji button on the Categories screen, + the picker popover. */
  .cat-emojibtn { flex: none; width: 40px; height: 40px; border-radius: 11px; display: flex; align-items: center; justify-content: center;
    background: var(--bg-elevated); border: 1px solid var(--border-default); cursor: pointer; font-size: 22px; line-height: 1; padding: 0;
    color: var(--text-secondary); transition: border-color .12s, background .12s; }
  .cat-emojibtn:hover { border-color: var(--accent-primary); background: var(--bg-hover); }
  .cat-emojibtn--mono { font-weight: 700; font-size: 16px; }
  .cat-emojibtn--none { border-style: dashed; color: var(--text-tertiary); font-size: 20px; }   /* no emoji set yet */
  .cat-row .cat-emojibtn { margin-right: 2px; }
  .cat-row > .slot__name { flex: 1 1 auto; text-align: left; }   /* the emoji made the row 3 flex items — keep the name left, not centred by space-between */
  .cat-add .cat-emojibtn { width: 44px; height: 44px; }

  /* THE EMOJI SHEET (v362 rebuild, from the ratified round-2 mock). Desktop = a compact anchored popover,
     phone = a bottom sheet; ONE structure either way: [box + name + ✕] · grid · [Cancel | Apply]. No search
     box, and no separate type-your-own row — the box in the head is both the preview and the input. */
  .emojipop { position: fixed; z-index: 1200; width: 320px; max-width: calc(100vw - 16px); background: var(--bg-elevated);
    border: 1px solid var(--border-default); border-radius: 13px; box-shadow: 0 12px 34px rgba(0,0,0,.42);
    display: flex; flex-direction: column; overflow: hidden; max-height: min(70vh, 486px); }
  .emojipop__grab { width: 36px; height: 4px; border-radius: 2px; background: var(--border-strong); margin: 8px auto 0; flex: 0 0 auto; }
  .emojipop__head { display: flex; align-items: center; gap: 11px; padding: 11px 12px 11px; flex: 0 0 auto;
    border-bottom: 1px solid var(--border-subtle); }
  /* the ONE box: live preview AND typable input. Empty = the dashed monogram (the placeholder), never a
     phantom emoji. 16px+ font on the phone stops iOS zoom-on-focus. */
  .emojipop__prev { width: 48px; height: 48px; flex: 0 0 auto; text-align: center; padding: 0; border-radius: 11px;
    background: var(--bg-input); border: 1px solid var(--border-default); color: var(--text-primary);
    font: inherit; font-size: 25px; line-height: 1; caret-color: var(--accent-primary); }
  .emojipop__prev:focus { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-glow); }
  .emojipop__prev--none { border-style: dashed; color: var(--text-tertiary); }
  .emojipop__prev::placeholder { color: var(--text-tertiary); font-size: 17px; font-weight: 700; opacity: 1; }
  .emojipop__name { flex: 1 1 auto; min-width: 0; }
  .emojipop__nm { display: block; font-size: 15px; font-weight: 700; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .emojipop__sub { display: block; font-size: 12px; color: var(--text-tertiary); margin-top: 1px; }
  .emojipop__x { flex: 0 0 auto; background: none; border: 0; color: var(--text-tertiary); font-size: 18px; cursor: pointer; padding: 6px; line-height: 1; }
  .emojipop__x:hover { color: var(--text-primary); }
  .emojipop__scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 10px 12px 2px;
    scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; overscroll-behavior: contain; }
  .emojipop__scroll::-webkit-scrollbar { width: 10px; }
  .emojipop__scroll::-webkit-scrollbar-track { background: transparent; }
  .emojipop__scroll::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 999px; border: 2px solid var(--bg-elevated); background-clip: content-box; }
  .emojipop__scroll::-webkit-scrollbar-thumb:hover { background: var(--text-tertiary); background-clip: content-box; }
  /* v388 — the duplicate courtesy: ONE quiet line, never a warning colour, never a disabled Apply. It is
     information the person may act on or ignore, so it wears the ordinary secondary text register. */
  .emojipop__dupe { margin: 0; padding: 0 12px 9px; font-size: 12.5px; color: var(--text-secondary); }
  .emojipop--sheet .emojipop__dupe { padding: 0 16px 10px; }
  .emojipop__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; }
  .emojipop__e { aspect-ratio: 1; border: 1px solid transparent; background: none; border-radius: 8px; font-size: 21px; line-height: 1;
    cursor: pointer; padding: 0; color: var(--text-primary); }
  .emojipop__e:hover { background: var(--bg-hover); }
  .emojipop__e--on { border-color: var(--accent-primary); background: color-mix(in srgb, var(--accent-primary) 18%, transparent); }
  .emojipop__gridfoot { display: flex; justify-content: flex-end; margin: 9px 0 3px; }
  .emojipop__none { font-size: 12.5px; color: var(--text-secondary); background: none; border: 0; cursor: pointer; padding: 2px 0;
    text-decoration: underline; text-underline-offset: 2px; white-space: nowrap; }
  .emojipop__none:hover { color: var(--text-primary); }
  /* Apply is ALWAYS tappable — "leave it as it is" is a legitimate thing to apply. It is never disabled. */
  .emojipop__foot { display: flex; gap: 10px; flex: 0 0 auto; padding: 11px 12px; border-top: 1px solid var(--border-subtle);
    background: var(--bg-elevated); }
  .emojipop__foot .btn { padding: 11px 0; font-size: 14.5px; }
  .emojipop__cancel { flex: 1 1 0; }
  .emojipop__apply { flex: 2 1 0; }
  /* PHONE: a bottom sheet with a light backdrop — room to browse, and it never fights the on-screen keyboard. */
  .emojipop__backdrop { position: fixed; inset: 0; z-index: 1199; background: rgba(6, 6, 10, .45); }
  .emojipop--sheet { position: fixed; left: 0; right: 0; bottom: 0; top: auto; width: 100%; max-width: 100%;
    border-radius: 16px 16px 0 0; border-bottom: 0; max-height: 78vh; }
  .emojipop--sheet .emojipop__head { padding: 10px 16px 12px; }
  .emojipop--sheet .emojipop__scroll { padding: 12px 16px 4px; }
  .emojipop--sheet .emojipop__grid { grid-template-columns: repeat(8, 1fr); gap: 4px; }
  .emojipop--sheet .emojipop__e { font-size: 24px; }
  .emojipop--sheet .emojipop__nm { font-size: 16px; }
  .emojipop--sheet .emojipop__foot { padding: 12px 16px calc(14px + env(safe-area-inset-bottom)); }
  .tx--veiled .tx__title { color: var(--text-secondary); font-style: italic; }
  .tx__veilhint { font-size: 12px; color: var(--text-tertiary); }
  .tx__amt--veiled { color: var(--text-tertiary); letter-spacing: 0.12em; }
  .bulk-le--veiled { cursor: pointer; }
  .bulk-le--veiled .bulk-le__t { color: var(--text-secondary); font-style: italic; }
  .bulk-le__a--veiled { color: var(--text-tertiary); letter-spacing: 0.12em; }
  .ana-veilnote { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; font-size: 13px; margin: 2px 0 10px; }
  .cat-tile--private { display: inline-flex; align-items: center; gap: 7px; }
  .cat-tile--private svg { width: 13px; height: 13px; color: var(--text-tertiary); flex: none; }
  .cat-tile--private.cat-tile--active svg { color: currentColor; }
  .bulk-row--veil { cursor: pointer; }
  .bulk-cn--veil { display: inline-flex; align-items: center; gap: 7px; color: var(--text-secondary); font-style: italic; }
  .bulk-veil__ic svg { width: 13px; height: 13px; color: var(--text-tertiary); display: block; }
  .history-row--veiled .history-row__desc { color: var(--text-secondary); font-style: italic; cursor: pointer;
    display: inline-flex; align-items: baseline; gap: 10px; }
  /* tap-to-detail + inline edit (Phase 5b) */
  .tx__main--tap { cursor: pointer; }
  .tx__detail { flex-basis: 100%; margin-top: 10px; padding-top: 12px; border-top: 1px dashed var(--border-subtle); }
  .txd__grid, .txd__split { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 8px 18px; }
  .txd__split { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border-subtle); }
  .txd__splitt { grid-column: 1 / -1; font-size: 11px; color: var(--text-tertiary); text-transform: uppercase; letter-spacing: 0.05em; }
  .txd__row { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
  .txd__k { font-size: 11px; color: var(--text-tertiary); text-transform: uppercase; letter-spacing: 0.05em; }
  .txd__v { font-size: 14px; color: var(--text-primary); word-break: break-word; }
  .txd__note { margin-top: 10px; font-size: 13px; color: var(--text-secondary); font-style: italic; }
  .txd__actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 14px; }
  .txd__hint { font-size: 13px; }
  .txd__edit { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px 16px; }
  .txd__f { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
  .txd__f > label { font-size: 12px; color: var(--text-secondary); }
  .txd__edit .txd__actions, .txd__edit .toast { grid-column: 1 / -1; }
  .txd__edit .txd__wide { grid-column: 1 / -1; margin: 0; } /* a full-width intro line in an inline form */
  .txd__hh { grid-column: 1 / -1; } /* the household split/who-paid section spans the edit grid */
  /* linked-transaction peek card (5d·5 refine): the OTHER side of a reimbursement link, shown inside
     an open detail so the pair reads side by side even across months/filters */
  .txd__peek { grid-column: 1 / -1; display: flex; align-items: center; gap: 10px; min-width: 0;
    padding: 8px 10px; background: var(--bg-elevated); border: 1px solid var(--border-subtle); border-radius: 10px; }
  .txd__peek__main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
  .txd__peek__amt { flex: 0 0 auto; text-align: right; }
  .txd__peeknet { font-size: 11px; }
  /* "Go to →" jump link on a linked peek card (purple = neutral/brand action, §3) */
  .txd__peekgo { align-self: flex-start; margin-top: 3px; background: none; border: 0; padding: 2px 0;
    color: var(--accent-primary); font: inherit; font-size: 12px; font-weight: 600; cursor: pointer; }
  .txd__peekgo:hover { text-decoration: underline; }
  /* brief highlight when the list jumps to a row (purple glow, fades) — "here it is", not an alert */
  @keyframes txflash { 0% { background: rgba(124, 106, 255, 0.22); } 100% { background: transparent; } }
  .tx--flash { animation: txflash 1.7s ease-out; border-radius: 10px; }

  /* archive (Settings → Archive): amber marks the impending PURGE (destructive), nothing else */
  .arc-warn { border-color: var(--accent-warm); }
  .arc-warn p { margin: 0; color: var(--text-primary); font-size: 14px; }
  .arc-soon { color: var(--accent-warm); }
  .arc-norestore { font-size: 12px; max-width: 170px; text-align: right; }
  /* archive rows get extra breathing room (three info lines per row read cramped at list padding) */
  .tx--arc { padding: 16px 2px; }
  .tx--arc .tx__right { gap: 14px; }
  .tx__settle { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin-top: 8px; }
  .tx__settlehint { font-size: 12px; color: var(--text-tertiary); }
  .tx__settled { margin-top: 6px; font-size: 12px; color: var(--accent-teal); }
  /* month-end settle-up summary: gentle, NOT alarming (teal/info, never red) — Bible §4.3 */
  .settle-summary { border-color: var(--accent-teal); background: rgba(62, 207, 170, 0.06); }
  .settle-summary p { color: var(--text-secondary); font-size: 14px; margin: 0; }
  /* teal button variant — income/settled money (Bible §3); dark ink on light teal */
  .btn--teal { background: var(--accent-teal); color: var(--bg-base); }
  /* gold button variant — reimbursement actions (§3 amendment, 5d·5 refine); dark ink on gold */
  .btn--gold { background: var(--accent-gold); color: var(--bg-base); }
  /* inline remove confirm — a slim full-width strip BELOW the row (no modal; stays in sightline) */
  .tx__confirm { flex-basis: 100%; display: flex; align-items: center; flex-wrap: wrap; gap: 8px 10px;
    margin-top: 8px; padding-top: 8px; border-top: 1px dashed var(--border-subtle); }
  .tx__confirmq { font-size: 13px; color: var(--text-secondary); margin-right: auto; }
  .tx__confirmbtns { display: flex; gap: 8px; flex: none; }   /* keep Cancel + Remove together as a unit */
  .tx__confirmerr { flex-basis: 100%; font-size: 12px; color: var(--accent-warm); }

  /* ── v377 — THE LEDGER SHELL: a docked left column on a laptop, a slide-in over a scrim on a phone ────
     Mock-ratified both rounds. Structure: .ledger-shell [ aside.ledger-side | .ledger-main [ .ledger-strip
     (sticky) + .card.ledger-list ] ], with the edge tab + scrim as fixed overlays outside the flow.
     PHONE is the default here (mobile-first for this block) — the dock is the @media addition below. */
  .ledger-shell { display: block; }
  .ledger-main { min-width: 0; }
  .ledger-side {
    position: fixed; top: 0; left: 0; bottom: 0; z-index: 61; width: min(320px, 86vw);
    display: flex; flex-direction: column;
    background: var(--bg-elevated); border-right: 1px solid var(--border-strong);
    box-shadow: 6px 0 26px rgba(0, 0, 0, 0.45);
    transform: translateX(-103%); transition: transform 0.2s cubic-bezier(.2,.8,.2,1);
  }
  .ledger-shell--open .ledger-side { transform: translateX(0); }
  .ledger-side__head { display: flex; align-items: center; gap: 8px; flex: 0 0 auto;
    padding: 12px 14px; border-bottom: 1px solid var(--border-subtle); }
  .ledger-side__t { font-family: 'Syne', sans-serif; font-size: 14px; font-weight: 700; }
  .ledger-side__x { margin-left: auto; }
  /* v384: the collapse chevron is a LAPTOP affordance — on a phone the ✕ beside it already closes the
     panel and there is nothing to collapse to. Shown in the dock media query below. */
  .ledger-side__col { display: none; }
  .ledger-side__body { flex: 1 1 auto; overflow-y: auto; padding: 12px 14px 24px; }
  /* ── v380 DEFECT (a): EVERY scrollbar inside the panel wears the app theme ─────────────────────────
     The human's laptop screenshot: the panel's own scroll bar, and the category box's inside it, were
     browser-default grey — the one bright chrome-coloured thing on a dark screen. The `*` reach is
     deliberate: the panel HOSTS other components (the category box, the merchant/tag combos, the month
     popover) and each one growing its own themed-scrollbar rule is how a defect like this comes back.
     Tokens only, no new hex; same thin/round shape the combo lists and the emoji picker already use.
     ── v393: `.bulk-ledger` JOINS THE SAME RULE, it does not get a second one. The human found the bulk
     entry screen's side panel still scrolling on the OS default bar — the identical complaint, one screen
     over, which is precisely what the paragraph above predicted would happen. Widening this selector list
     is the fix the comment asks for; a `.bulk-ledger__list { scrollbar-width: … }` block of its own would
     have been the third copy and the next surface would still be unthemed. The `*` reach covers the list,
     the pending strip and anything the panel grows later. The thumb's 2px border is a GUTTER, not an
     outline — it has to be the colour of whatever the bar sits on, and the two panels sit on different
     surfaces (`--bg-elevated` vs `--bg-surface`). That is carried by ONE inherited custom property rather
     than by forking the thumb rule, so the shape still has a single definition. */
  .ledger-side, .ledger-side *, .bulk-ledger, .bulk-ledger * { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
  .ledger-side { --hscroll-gutter: var(--bg-elevated); }
  .bulk-ledger { --hscroll-gutter: var(--bg-surface); }
  .ledger-side::-webkit-scrollbar, .ledger-side *::-webkit-scrollbar,
  .bulk-ledger::-webkit-scrollbar, .bulk-ledger *::-webkit-scrollbar { width: 10px; height: 10px; }
  .ledger-side::-webkit-scrollbar-track, .ledger-side *::-webkit-scrollbar-track,
  .bulk-ledger::-webkit-scrollbar-track, .bulk-ledger *::-webkit-scrollbar-track { background: transparent; }
  .ledger-side::-webkit-scrollbar-thumb, .ledger-side *::-webkit-scrollbar-thumb,
  .bulk-ledger::-webkit-scrollbar-thumb, .bulk-ledger *::-webkit-scrollbar-thumb {
    background: var(--border-strong); border-radius: 999px;
    border: 2px solid var(--hscroll-gutter, var(--bg-elevated)); background-clip: content-box; }
  .ledger-side::-webkit-scrollbar-thumb:hover, .ledger-side *::-webkit-scrollbar-thumb:hover,
  .bulk-ledger::-webkit-scrollbar-thumb:hover, .bulk-ledger *::-webkit-scrollbar-thumb:hover {
    background: var(--text-tertiary); background-clip: content-box; }
  /* ── v380 DEFECT (b): the month picker FITS the panel, so nothing inside it ever scrolls sideways ───
     `.hmonthpop` is a fixed 260px popover — fine under a wide trigger, 40px too wide inside a 250px
     panel. It cut the December column off and raised a phantom horizontal scrollbar that vanished the
     moment you reached for it (touching it closed the popover). Sized to its container instead: the
     grid keeps its three columns and the month buttons give up their minimum width, which is what lets
     three of them fit. The standing rule this now enforces: NO horizontal scroll inside the panel, ever. */
  .ledger-side .hmonthsel { min-width: 0; }
  .ledger-side .hmonthpop { width: 100%; max-width: 100%; left: 0; right: 0; }
  .ledger-side .hmonthpop__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 5px; }
  .ledger-side .hmonthpop__m { min-width: 0; padding-left: 2px; padding-right: 2px; overflow: hidden; text-overflow: ellipsis; }
  .ledger-side .hmonthpop__preb { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
  .ledger-side .hmonthpop__yr { font-size: 14px; }
  .ledger-side__grp { margin: 0 0 10px; }
  .ledger-side__grp .dropdown { width: 100%; }
  .ledger-side__sel { width: 100%; }
  /* the panel hosts the SAME filter block the two cards used to — it just has one column to lay out in */
  .ledger-side .ledger-panel { margin-top: 4px; padding-top: 12px; }
  .ledger-side .ledger-prow { flex-direction: column; gap: 10px; }
  .ledger-side .ledger-field { flex: 1 1 auto; }
  /* the purple edge tab — present at ANY scroll position, which was the whole complaint */
  .ledger-tab {
    position: fixed; top: 46%; left: 0; transform: translateY(-50%); z-index: 60;
    background: var(--accent-primary); color: #fff; border: 0; cursor: pointer;
    border-radius: 0 10px 10px 0; padding: 13px 5px; box-shadow: 2px 0 12px rgba(0, 0, 0, 0.4);
    writing-mode: vertical-rl; text-orientation: mixed; font: inherit; font-size: 10.5px;
    font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase;
  }
  .ledger-tab__lbl { pointer-events: none; }
  /* v378: the gold "a filter is applied" dot is GONE — the pinned strip's ✕-chips say the same thing in
     words, always on screen, and two indicators for one fact left the quieter one unexplained ("I don't
     know why it has a little orange dot"). The tab's ARIA label still carries the count. */
  .ledger-scrim { position: fixed; inset: 0; z-index: 59; background: rgba(6, 6, 10, 0.45);
    opacity: 0; pointer-events: none; transition: opacity 0.18s; }
  .ledger-shell--open ~ .ledger-overlays .ledger-scrim { opacity: 1; pointer-events: auto; }
  /* ── the PINNED STRIP: count · oldest-loaded · nets · the active filter chips with their ✕ ──────────
     position:sticky works because html/body use overflow-x:clip (the standing rule — same as .cal-sticky).
     Full-bleed across the .app gutters so nothing peeks past it as rows scroll under. */
  .ledger-strip { position: sticky; top: 0; z-index: 30; margin: 0 -20px 10px; padding: 7px 20px 6px;
    background: color-mix(in srgb, var(--bg-base) 88%, transparent);
    backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
    border-bottom: 1px solid var(--border-subtle); }
  .ledger-strip .ledger-summary { margin-top: 0; padding-top: 0; border-top: none; }
  .ledger-strip .ledger-chips { margin-top: 5px; }
  .ledger-countline { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; min-width: 0; }
  .ledger-oldest { font-size: 11.5px; }
  .ledger-countline .info-dot { align-self: center; }
  /* A PINNED STRIP EARNS ITS HEIGHT OR IT IS JUST A SMALLER SCREEN. On a phone the default flex row wraps
     into three tall lines and eats ~180px of a 780px viewport — the opposite of the point. Two slim lines:
     the count (with its oldest-loaded footnote and the ⓘ) over the month nets, both a register smaller. */
  @media (max-width: 700px) {
    .ledger-strip { padding: 5px 20px 5px; }
    .ledger-strip .ledger-summary { flex-direction: column; align-items: flex-start; gap: 1px; }
    .ledger-strip .ledger-nets { justify-content: flex-start; gap: 12px; width: 100%; }
    .ledger-strip .ledger-net { font-size: 12.5px; }
    .ledger-strip .ledger-netfig { gap: 4px; }
    .ledger-strip .ledger-netfig__l { font-size: 10.5px; }
    .ledger-countline { font-size: 11px; gap: 5px; }
    .ledger-oldest { font-size: 10.5px; }
    .ledger-countline .info-dot { font-size: 12px; padding: 0 2px; }
  }
  /* ── v380 — ONE MONTH AT A TIME: a full-width band, its DAYS wrapping beneath it (mock round 3) ─────
     Supersedes the v377 months-side-by-side shape, which the human vetoed ("two months in descending
     order at the same time… I can't wrap my head around it"). One timeline top to bottom; the width does
     its work INSIDE a month. The wrap unit is the DAY CARD — never split, never a scroller of its own. */
  .ledger-monthblock { margin: 0 0 6px; }
  .ledger-mband { display: flex; align-items: baseline; gap: 10px; padding: 10px 2px 7px; margin-top: 6px;
    border-bottom: 1px solid var(--border-default); font-size: 12px; letter-spacing: .07em;
    text-transform: uppercase; font-weight: 700; color: var(--text-secondary); }
  /* v392 — the SECOND instance of the same faux-bold defect item 2 fixed on the fund headline, found by
     sweeping every element that carries `mono` for a rule above weight 500. This one is DM Mono too (the
     element is `mono ledger-mband__n`), so 600 smeared it exactly the same way. 500 is DM Mono's real
     heaviest; the band's LABEL beside it is DM Sans and keeps its 700. */
  .ledger-mband__n { margin-left: auto; letter-spacing: 0; text-transform: none; font-size: 13.5px; font-weight: 500; }
  .ledger-cols { display: flex; gap: 14px; align-items: flex-start; padding: 10px 0 4px; }
  .ledger-cols--one { display: block; padding-bottom: 0; }   /* a quiet month keeps one column — width is a ceiling */
  .ledger-col { flex: 1 1 0; min-width: 0; }
  .ledger-dcard { border: 1px solid var(--border-subtle); border-radius: 11px; overflow: hidden;
    margin: 0 0 12px; background: var(--bg-surface); }
  .ledger-dcard .tx { padding-left: 12px; padding-right: 12px; }
  /* ── v397 — THE DAY CARD UNCLIPS ITSELF WHILE A POPUP IS OPEN INSIDE IT ────────────────────────────
     The human opened Edit on a ledger row and found the date calendar cut off mid-grid: the last week or
     two of the month simply were not there, and the days that WERE drawn near the fold could not be
     tapped. Nothing was wrong with the calendar. The day card above owns an `overflow: hidden` that
     exists for ONE reason — to keep its children inside its 11px rounded corners — and every popup in
     the edit form is `position: absolute` INSIDE that card, so the card's bottom edge is where they end.
     Which is why it looked random: on a card with more rows below the form the popup fitted and looked
     perfect, and on a one-row card the same popup lost a third of itself. The clip point was the card,
     never the dates.
     THE RELEASE is the same live `:has()` shape the analysis sheet already uses to react to a popup's own
     open class (see `body:has(.ana-menu--what2.dropdown--open)` above) — no JS, no state to keep in sync,
     and the clip is back the instant the popup closes. Two selectors because there are two families:
       · `.dropdown--open` is the shared open-state class of EVERY HUI popup control (the themed date
         calendar, the category picker, the draw-from dropdown, the multi-picker) — one class covers all
         four, so a future control added to this form is covered on the day it lands. Measured on a
         one-day month (where the form lays out on a single grid row and the card ends right under it):
         14 of 42 day cells and 5 of 8 category rows were unreachable before this rule.
       · the tag autocomplete is NOT a `.dropdown` — it is its own control — so it says it is open the
         only other honest way it can, through the field's `aria-expanded`. 4 of 7 suggestions were
         unreachable there.
     SAFE at the corners, checked rather than assumed: the day header has carried no background of its own
     since v384, ledger rows paint no hover background, and the go-to flash brings its OWN border-radius
     (`.tx--flash`), so nothing draws a square corner while the clip is off. */
  .ledger-dcard:has(.dropdown--open),
  .ledger-dcard:has(.taginput__field[aria-expanded="true"]) { overflow: visible; }
  /* ── v384 — THE LAPTOP DAY HEADER LOSES ITS GREY BAND ──────────────────────────────────────────────
     The human, reading their own ledger on a laptop: "this additional gray band demands a little bit more
     cognition". The PHONE's register is the target and it was already right — the day label is plain text
     and the hairline that separates one day from the next is the only chrome. So the day card's header
     drops its filled background and its own bottom rule; the card's border is the hairline, and
     `.dayhdr + .tx { border-top: none }` (the standing rule) keeps it from doubling. Nothing else about
     the day card changes — this is a paint removal, not a re-layout. */
  .ledger-dcard .dayhdr { padding: 9px 12px 7px; border-top: none; background: none; border-bottom: none; }
  /* ── the DOCK: at ≥1100px the panel is a permanent left column ("we have enough space") ───────────── */
  @media (min-width: 1100px) {
    .ledger-shell { display: flex; align-items: flex-start; gap: 18px; }
    .ledger-side { position: sticky; top: 12px; bottom: auto; transform: none; box-shadow: none; z-index: auto;
      flex: 0 0 250px; width: 250px; max-height: calc(100vh - 32px);
      border: 1px solid var(--border-subtle); border-radius: var(--radius); }
    .ledger-main { flex: 1 1 auto; }
    .ledger-side__x, .ledger-tab, .ledger-scrim { display: none; }
    .ledger-strip { margin-left: 0; margin-right: 0; padding-left: 0; padding-right: 0; }
    /* ── v384 (a) — THE PANEL COLLAPSES, AND THE WAY BACK IS THE TAB IT ALREADY HAS ──────────────────
       Docked-open is still the default; the chevron in the panel head puts the column away and the SAME
       purple edge tab the phone opens the panel with comes back as the rail. Two rules, both scoped
       INSIDE this media query on purpose: at phone width `--collapsed` must mean nothing at all, or a
       laptop user who collapsed once would find no panel on their phone. */
    .ledger-side__col { display: inline-flex; margin-left: auto; }
    .ledger-shell--collapsed .ledger-side { display: none; }
    .ledger-shell--collapsed ~ .ledger-overlays .ledger-tab { display: block; }
    /* ── v384 (b) — THE PANEL'S SCROLLBAR AUTO-HIDES ─────────────────────────────────────────────────
       It was permanently visible on a laptop and the human found it annoying. The v380 themed colours are
       kept exactly; they are simply only PAINTED while the panel is being used (hover, or a focused
       control inside it — which is what "while scrolling" amounts to on a pointer device). The gutter is
       still reserved (`scrollbar-width: thin` is unchanged), so nothing reflows when it appears. */
    .ledger-side, .ledger-side * { scrollbar-color: transparent transparent; }
    .ledger-side:hover, .ledger-side:hover *, .ledger-side:focus-within, .ledger-side:focus-within * {
      scrollbar-color: var(--border-strong) transparent; }
    .ledger-side::-webkit-scrollbar-thumb, .ledger-side *::-webkit-scrollbar-thumb { background: transparent; }
    .ledger-side:hover::-webkit-scrollbar-thumb, .ledger-side:hover *::-webkit-scrollbar-thumb,
    .ledger-side:focus-within::-webkit-scrollbar-thumb, .ledger-side:focus-within *::-webkit-scrollbar-thumb {
      background: var(--border-strong); background-clip: content-box; }
    /* ── v384 (c) — A DENSITY PASS ON THE PANEL'S CONTROLS (laptop only) ─────────────────────────────
       "The shorter side of the rectangle is a little bit too long." The app-wide control height (~45px)
       is sized for a thumb; inside a 250px laptop side panel it makes six controls fill the viewport. Only
       the VERTICAL padding and the type size move — the horizontal padding, the radii, the borders and
       every colour are untouched, so it reads as the same control set, just tighter. The phone keeps its
       thumb-sized controls exactly ("the phone seems all right"), which is why this lives here and not in
       the base `.ledger-side` rules. `:not(.taginput__field)` because the tag input's inner field is a
       borderless child of its own box — padding it like a control puts a step in the middle of one. */
    .ledger-side .dropdown__trigger,
    .ledger-side input:not(.taginput__field) { padding-top: 7px; padding-bottom: 7px; font-size: 13.5px; }
    .ledger-side .taginput { min-height: 34px; padding-top: 3px; padding-bottom: 3px; }
    .ledger-side .btn--sm { padding-top: 6px; padding-bottom: 6px; font-size: 12.5px; }
    .ledger-side .ledger-field > label { font-size: 11.5px; }
    .ledger-side .ledger-field { gap: 4px; }
    .ledger-side__grp { margin-bottom: 8px; }
    .ledger-side .ledger-prow { gap: 10px; }
    .ledger-side .ledger-panel { gap: 10px; }
  }
  /* ledger filter toolbar / panel / groups (Phase 5a) */
  .ledger-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; }
  .ledger-inline { display: inline-flex; align-items: center; gap: 6px; }
  .ledger-inline__l { font-size: 12px; color: var(--text-tertiary); white-space: nowrap; }
  .ledger-inline .dropdown { width: auto; }
  .ledger-inline .dropdown__trigger { padding: 7px 10px; font-size: 13px; }
  .ledger-filterbtn--on { border-color: var(--accent-primary); color: var(--accent-primary); }
  /* the filter panel: two labelled flex-wrap rows (desktop: Date·Type·Merchant·Category, then
     Amount·Search); mid widths wrap naturally and a phone stacks single-column (basis 190px) */
  .ledger-panel { display: flex; flex-direction: column; gap: 12px;
    margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border-subtle); }
  .ledger-prow { display: flex; flex-wrap: wrap; gap: 12px 16px; }
  .ledger-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; flex: 1 1 190px; }
  .ledger-field > label { font-size: 12px; color: var(--text-secondary); }
  .ledger-amt { display: flex; align-items: center; gap: 8px; }
  .ledger-amt input { flex: 1; min-width: 0; }
  .ledger-cat__chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
  /* removable filter chips */
  .ledger-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
  .ledger-chip { display: inline-flex; align-items: center; gap: 4px; padding: 4px 4px 4px 10px;
    border-radius: 14px; background: var(--accent-glow); color: var(--accent-primary); font-size: 12px; }
  .ledger-chip__x { display: inline-flex; align-items: center; justify-content: center; padding: 2px;
    background: none; border: none; cursor: pointer; color: var(--accent-primary); opacity: 0.75; }
  .ledger-chip__x:hover { opacity: 1; }
  .ledger-chip__x svg { width: 12px; height: 12px; }
  /* v384 — "Clear all", in the pinned strip beside the chips it clears. Deliberately NOT chip-shaped: a
     chip means "one filter, ✕ to drop it" and this is the opposite verb, so it reads as a quiet text
     action instead (underline on hover, the app accent, no pill). Tokens only. */
  .ledger-clearall { background: none; border: 0; padding: 4px 2px; margin-left: 2px; cursor: pointer;
    font: inherit; font-size: 12px; color: var(--text-secondary); text-decoration: underline;
    text-underline-offset: 2px; text-decoration-color: var(--border-strong); }
  .ledger-clearall:hover { color: var(--accent-primary); text-decoration-color: var(--accent-primary); }
  /* result summary + group headers */
  .ledger-summary { display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
    margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border-subtle); }
  .ledger-net { font-size: 15px; }
  /* slice 5 (fetch half, §A): the scope net — default = last + this month (two figures), each labelled with
     its month; a filter shows the one filtered-scope net. Wraps cleanly on a phone. */
  .ledger-nets { display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap; justify-content: flex-end; }
  .ledger-netfig { display: inline-flex; align-items: baseline; gap: 6px; }
  .ledger-netfig__l { font-size: 12.5px; }
  /* v392 — the FUND scope figure ("Set aside X · Paid from it Y"), one pair per currency era. It is a
     .ledger-netfig like the others, so it inherits the baseline row, the label size and the "checking…"
     dimming; the only additions are the gap between the two halves and their §3 colours. GOLD for money
     set aside and --accent-out for money spent, matching the fund card's own event list exactly
     (.reserve__evamt--in / --out) — the two surfaces describe the same movements and must not disagree
     about what colour they are. */
  .ledger-fundfig { column-gap: 9px; row-gap: 2px; flex-wrap: wrap; justify-content: flex-end; }
  .ledger-fundfig__half { display: inline-flex; align-items: baseline; gap: 6px; }
  .ledger-fundfig__sep { color: var(--text-tertiary); }
  .ledger-fundfig__aside { color: var(--accent-gold); }
  .ledger-fundfig__paid { color: var(--accent-out); }
  .ledger-checking { font-size: 12px; opacity: .8; }   /* §C "checking earlier…" reconcile hint (deep-scroll list) */
  /* F2: provisional net at complete:false — the figure dims slightly + carries a small "checking…" qualifier so a
     not-yet-reconciled number reads as still-verifying, never as a wrong number that silently corrects. */
  .ledger-netfig--checking .ledger-net { opacity: .55; }
  /* ── the SHARED "checking…" marker (human-ratified 2026-08-04, .claude/plans/flicker-fixes.md) ──────
     ONE look wherever a load-bearing figure is showing its cached value while that read revalidates:
     Home tiles, the Analysis headline tiles, the Calendar month totals, the Forecast card figure, and
     the ledger net (which was the original of the pattern). Quiet + tertiary — the figure dims a touch
     so "provisional" is legible at a glance, and the word carries the meaning. NEVER a spinner and
     never a banner: the number itself is what is provisional, so the note belongs on it. */
  .hui-checking { font-size: 11.5px; opacity: .8; font-style: italic; }
  .is-checking .dash-tile__value, .is-checking .cal-footpart__val, .is-checking .fc-leftnum { opacity: .55; }
  .dash-tile .hui-checking, .cal-footpart .hui-checking { display: block; margin-top: 2px; line-height: 1.3; }
  .cal-footpart .hui-checking { text-align: right; }
  .fc-range .hui-checking { margin-left: 6px; }
  /* ── the TRUNCATION line (v371, P4 ①) ─────────────────────────────────────────
     Sibling of the marker above and deliberately the same quiet register: it says how far back a
     surface could look, which is a fact about the answer, not a fault. NO new colour — it inherits
     .tertiary. It is invisible on every real ledger today and only appears past the server's row
     limit, so it must never rearrange a layout: one block line, its own small breathing room. */
  .cap-note { margin: 6px 0 0; line-height: 1.35; }
  .csv-cap { margin: 8px 0 0; }
  .reimb-finder .cap-note { margin: 2px 0 8px; }
  .ledger-more { height: 1px; }   /* slice 5: the paged-list scroll sentinel (IntersectionObserver target) */
  /* v372 (P2): the foot of a windowed list — what you are looking at, and one tap for the next band.
     Quiet by construction: it only renders when there IS earlier history, and it uses no new colour. */
  .ledger-older { display: flex; flex-direction: column; align-items: center; gap: 6px; margin: 14px 0 4px; }
  .ledger-older__note { margin: 0; font-size: 12px; text-align: center; }
  .ledger-older__btn { min-height: 34px; }
  /* v377: the foot can now carry up to three affordances (older band · newer band · Jump to…). It spans the
     full width under the two columns — a fetch is about the whole list, not about one column. */
  .ledger-older__btns { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 8px; }
  .ledger-newer__btn { min-height: 34px; }
  .ledger-jump { width: auto; min-width: 150px; }
  .ledger-jump .dropdown__trigger { padding: 7px 10px; font-size: 13px; min-height: 34px; }
  /* v372 (P2): "Searching all your history…" — the honest state while a settled query reaches past the
     loaded window. Same tertiary register as "checking…" (tone-and-language.md), never a warning. */
  .ledger-searchall { margin: 0 0 8px; font-size: 12px; }
  .ledger-group { display: flex; align-items: baseline; gap: 10px; padding: 14px 2px 8px;
    border-top: 1px solid var(--border-subtle); }
  .ledger-group:first-child { border-top: none; }
  .ledger-group__l { font-weight: 600; color: var(--text-primary); }
  .ledger-group__c { font-size: 12px; }
  .ledger-group__n { margin-left: auto; font-size: 14px; }
  /* Sort/View popups: let them grow past the (narrow) trigger instead of clipping the option text */
  .ledger-inline .dropdown__list { right: auto; min-width: 160px; }
  .ledger-inline .dropdown__opt { white-space: nowrap; }
  /* grouped, ticked Category multi-select */
  .lcat { position: relative; }
  .lcat-trigger { display: flex; align-items: center; justify-content: space-between; gap: 8px; width: 100%;
    text-align: left; background: var(--bg-input); color: var(--text-primary); border: 1px solid var(--border-default);
    border-radius: 10px; padding: 11px 12px; font-family: inherit; font-size: 15px; cursor: pointer; }
  .lcat-trigger--open { border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-glow); }
  .lcat-trigger__v { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* a floating popup (like every other dropdown in the filter row) — overlays the list instead of
     expanding the card and pushing rows down. Anchored to the trigger (.lcat is position:relative). */
  .lcat-box { position: absolute; z-index: 30; top: calc(100% + 4px); left: 0; right: 0;
    min-width: 220px; max-width: 86vw; max-height: 300px; overflow-y: auto; padding: 4px;
    border: 1px solid var(--border-default); border-radius: 10px; background: var(--bg-elevated);
    box-shadow: 0 10px 28px rgba(0,0,0,0.45); }
  .lcat-empty { padding: 12px; }
  .lcat-hrow, .lcat-crow { display: flex; align-items: center; gap: 10px; padding: 9px 10px; border-radius: 8px; cursor: pointer; }
  .lcat-hrow:hover, .lcat-crow:hover { background: var(--bg-hover); }
  .lcat-hrow { margin-top: 4px; }
  .lcat-hrow:first-child { margin-top: 0; }
  .lcat-hlabel { font-weight: 600; color: var(--text-primary); font-size: 14px; }
  /* §schema tags section in the WHAT menu (Phase 5) — a quiet divider above it; tag rows read as #pills inline */
  .lcat-hrow--tags { margin-top: 10px; padding-top: 12px; border-top: 1px solid var(--border-subtle); cursor: default; }
  .lcat-hrow--tags:hover { background: none; }
  .ana-tagrow { color: var(--text-secondary); }
  .lcat-crow { padding-left: 30px; font-size: 14px; color: var(--text-secondary); }
  .lcat-crow--on { color: var(--text-primary); }
  .lcat-cb { flex: none; width: 18px; height: 18px; border-radius: 5px; border: 1.5px solid var(--border-strong);
    display: inline-flex; align-items: center; justify-content: center; color: var(--on-accent-primary); }
  .lcat-cb--on, .lcat-cb--mix { background: var(--accent-primary); border-color: var(--accent-primary); }
  .lcat-cb__mix { width: 9px; height: 2px; background: var(--on-accent-primary); border-radius: 1px; }

  /* ── Bulk entry (§4d) — category-anchored grid + live month ledger. Two-pane on desktop (the view
     registers wide:true), single-column on phones; mobile hides the Merchant/Note columns. ────────── */
  .bulk-topbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 4px; }
  .bulk-topbar__title { margin: 0; font-size: 20px; font-weight: 700; }
  .bulk-monthsel { position: relative; min-width: 150px; }
  .bulk-monthsel .dropdown__trigger { border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-glow); }
  /* free-form month/year picker popup */
  .bulk-monthpop { position: absolute; z-index: 40; top: calc(100% + 4px); left: 0; width: 260px;
    background: var(--bg-elevated); border: 1px solid var(--border-default); border-radius: 12px;
    box-shadow: 0 12px 30px rgba(0,0,0,.45); padding: 10px; }
  .bulk-monthpop__nav { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
  .bulk-monthpop__arrow { background: none; border: none; color: var(--text-secondary); cursor: pointer; padding: 6px; border-radius: 8px; display: inline-flex; line-height: 0; }
  .bulk-monthpop__arrow:hover:not(:disabled) { background: var(--bg-hover); color: var(--text-primary); }
  .bulk-monthpop__arrow:disabled { opacity: .3; cursor: default; }
  .bulk-monthpop__yr { font-weight: 700; font-size: 15px; color: var(--text-primary); }
  .bulk-monthpop__grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
  .bulk-monthpop__m { background: var(--bg-input); border: 1px solid transparent; color: var(--text-secondary);
    border-radius: 8px; padding: 9px 0; font-family: inherit; font-size: 13px; cursor: pointer; }
  .bulk-monthpop__m:hover:not(:disabled) { border-color: var(--border-strong); color: var(--text-primary); }
  .bulk-monthpop__m--sel { background: var(--accent-primary); color: var(--on-accent-primary); font-weight: 600; }
  .bulk-monthpop__m:disabled { opacity: .3; cursor: default; }
  /* shared month/year picker (HUI.monthPicker) — same look as the bulk one, reusable (e.g. the refund finder) */
  .hmonthsel { position: relative; }
  .hmonthsel .dropdown__trigger { width: 100%; }
  .hmonthpop { position: absolute; z-index: 60; top: calc(100% + 4px); left: 0; width: 260px; max-width: 80vw;
    background: var(--bg-elevated); border: 1px solid var(--border-default); border-radius: 12px;
    box-shadow: 0 12px 30px rgba(0,0,0,.45); padding: 10px; }
  .hmonthpop__any { display: block; width: 100%; text-align: left; background: var(--bg-input); border: 1px solid transparent;
    color: var(--text-secondary); border-radius: 8px; padding: 8px 10px; margin-bottom: 8px; font: inherit; cursor: pointer; }
  .hmonthpop__any:hover { border-color: var(--border-strong); color: var(--text-primary); }
  .hmonthpop__any--sel { background: var(--accent-primary); color: var(--on-accent-primary); font-weight: 600; }
  .hmonthpop__nav { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
  .hmonthpop__arrow { background: none; border: none; color: var(--text-secondary); cursor: pointer; padding: 6px; border-radius: 8px; display: inline-flex; line-height: 0; }
  .hmonthpop__arrow:hover:not(:disabled) { background: var(--bg-hover); color: var(--text-primary); }
  .hmonthpop__arrow:disabled { opacity: .3; cursor: default; }
  .hmonthpop__yr { font-weight: 700; font-size: 15px; color: var(--text-primary); }
  .hmonthpop__grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
  .hmonthpop__m { background: var(--bg-input); border: 1px solid transparent; color: var(--text-secondary);
    border-radius: 8px; padding: 9px 0; font-family: inherit; font-size: 13px; cursor: pointer; }
  .hmonthpop__m:hover:not(:disabled) { border-color: var(--border-strong); color: var(--text-primary); }
  .hmonthpop__m--sel { background: var(--accent-primary); color: var(--on-accent-primary); font-weight: 600; }
  .hmonthpop__m:disabled { opacity: .3; cursor: default; }
  /* Slice B — month-RANGE mode: presets INSIDE the popup that light the grid, and the months BETWEEN endpoints
     tinted + square-edged so a span reads as one continuous block. */
  .hmonthpop__m--in { background: color-mix(in srgb, var(--accent-primary) 22%, transparent); color: var(--text-primary); border-color: transparent; border-radius: 0; }
  .hmonthpop__pre { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 9px; }
  .hmonthpop__preb { background: var(--bg-input); border: 1px solid transparent; color: var(--text-secondary);
    border-radius: 7px; padding: 6px 9px; font: inherit; font-size: 12px; cursor: pointer; }
  .hmonthpop__preb:hover { border-color: var(--border-strong); color: var(--text-primary); }
  .hmonthpop__preb--sel { background: color-mix(in srgb, var(--accent-primary) 20%, var(--bg-input)); border-color: var(--accent-primary); color: var(--text-primary); font-weight: 600; }
  .hmonthpop__or { font-size: 11.5px; color: var(--text-tertiary); display: flex; align-items: center; gap: 8px; margin: 4px 0 8px; }
  .hmonthpop__or::before, .hmonthpop__or::after { content: ""; height: 1px; background: var(--border-subtle); flex: 1; }
  .hmonthpop__hint { font-size: 11.5px; color: var(--text-tertiary); margin-top: 9px; }
  /* v405 — the CONFIRM button (opt-in `confirm` mode only). It is the whole affordance: taps choose, this
     reads. No cancel beside it and no hint line above it — ratified cut ("it's intuitive by itself"). */
  .hmonthpop__go { display: block; width: 100%; margin-top: 10px; }
  .hmonthpop__go:disabled { background: var(--bg-input); color: var(--text-tertiary); border: 1px solid var(--border-subtle); cursor: default; }
  .bulk-topbar__grow { flex: 1; }
  .bulk-topbar__as { font-size: 13px; color: var(--text-tertiary); }
  .bulk-topbar__ctx { font-size: 14px; font-weight: 600; color: var(--accent-primary); border: 1px solid var(--border-default);
    background: var(--bg-input); border-radius: 16px; padding: 4px 12px; }
  .bulk-intro { margin: 8px 0 4px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
  .bulk-intro b { color: var(--text-primary); font-weight: 600; }

  /* Dense grids fill the whole frame (bulk = two category columns + the ledger; CSV = the 3-column triage board).
     LATER in source order than `.app--wide .view`, so they win when a view carries BOTH classes (e.g. the CSV
     board lives inside Settings, which is app--wide AND app--csv). Phones/laptops collapse gracefully (below). */
  /* v380 — the LEDGER joins them (the human's laptop review): with the panel docked as a fixed left column
     the reading area was being squeezed inside the 1100px .app--wide cap, so the panel sat adrift in the
     middle of the page instead of starting at the logo's own column. It is a dense screen — a docked panel
     plus a two-column month band — so it uses the full 1500px .app frame like Bulk and the CSV board.
     Per-VIEW: setWide() strips the class on every navigation, so no other screen widens. */
  .app--bulk .view, .app--csv .view, .app--analysis .view, .app--ledger .view { max-width: 100%; }
  .bulk-panes { display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: 18px; align-items: start; margin-top: 14px; }
  .bulk-entry { background: var(--bg-surface); border: 1px solid var(--border-subtle); border-radius: var(--radius); overflow: hidden; }
  /* each engine is its own section; a stronger divider (border-strong) separates engines, so the eye reads
     SPENDING · INCOME · TRANSFERS · GOALS at a glance (vs the subtle within-engine group dividers). */
  .bulk-engine + .bulk-engine { border-top: 2px solid var(--border-strong); }
  /* the categories area splits into two balanced columns on wide screens (≥1200px), one column otherwise */
  .bulk-cols { display: grid; grid-template-columns: 1fr; }
  .bulk-ehead, .bulk-row { display: grid; grid-template-columns: 1.5fr 52px 1.1fr 1fr 116px; align-items: center; }
  .bulk-ehead { background: var(--bg-elevated); border-bottom: 1px solid var(--border-default); }
  .bulk-h { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-tertiary); font-weight: 600; padding: 10px 12px; }
  .bulk-h.h--amt { text-align: right; } .bulk-h.h--day { text-align: center; }
  .bulk-group { padding: 9px 12px 5px; font-weight: 700; font-size: 12px; letter-spacing: .04em; text-transform: uppercase;
    color: var(--text-tertiary); border-top: 1px solid var(--border-subtle); }
  .bulk-group__note { font-weight: 500; text-transform: none; letter-spacing: 0; color: var(--text-tertiary); opacity: .7; margin-left: 8px; }
  /* engine-coloured group headers (Bible §3 language): income = teal, transfers = muted, goals = gold;
     spending (life_costs) keeps the calm default. The note next to the label takes the same hue, dimmed. */
  .bulk-group--income { color: var(--accent-teal); }
  .bulk-group--neutral { color: var(--text-secondary); }
  .bulk-group--reserves { color: var(--accent-gold); }
  .bulk-group--income .bulk-group__note, .bulk-group--neutral .bulk-group__note, .bulk-group--reserves .bulk-group__note { color: currentColor; opacity: .6; }
  .bulk-row { border-top: 1px solid var(--border-subtle); }
  .bulk-row:hover { background: var(--bg-hover); }
  .bulk-row--focus { background: var(--accent-glow); box-shadow: inset 3px 0 0 var(--accent-primary); }
  /* a composer entry lands on its row → a brief, faint BRAND-purple flash (cohesive, not obtrusive) (Q1) */
  .bulk-row--flash { animation: bulkrowflash 0.9s ease; }
  @keyframes bulkrowflash { 0%, 100% { background: transparent; } 25% { background: var(--accent-glow); } }
  /* §28 pattern double-log nudge under a grid row — calm (purple-tinted), never amber */
  .bulk-dupask { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 8px 12px; margin: 2px 0;
    background: var(--accent-glow); border: 1px solid var(--border-default); border-radius: 8px; }
  .bulk-dupask__q { font-size: 12.5px; color: var(--text-secondary); line-height: 1.5; flex: 1 1 220px; min-width: 0; overflow-wrap: anywhere; }
  .bulk-clabel { padding: 8px 12px; min-width: 0; }
  .bulk-cn { font-size: 14px; font-weight: 600; }
  .bulk-ct2 { font-family: 'DM Mono', monospace; font-size: 12px; color: var(--text-tertiary); }
  .bulk-ct2 b { color: var(--text-secondary); font-weight: 500; }
  .bulk-row--in .bulk-ct2 b { color: var(--accent-teal); }
  .bulk-row[data-engine="income"] .bulk-ct2 b { color: var(--accent-teal); }
  .bulk-row[data-engine="reserves"] .bulk-ct2 b { color: var(--accent-gold); }
  .bulk-row[data-engine="neutral"] .bulk-ct2 b { color: var(--text-secondary); }
  .bulk-cell { min-width: 0; }
  .bulk-i { width: 100%; background: none; border: none; outline: none; color: var(--text-primary); font-family: inherit; font-size: 14px; padding: 11px 10px; min-width: 0; }
  .bulk-i::placeholder { color: var(--text-tertiary); }
  .bulk-i--day { font-family: 'DM Mono', monospace; text-align: center; padding: 11px 4px; }
  /* the sticky-day placeholder is barely-there (you can tell it's there; it stays out of the way) */
  .bulk-i--day::placeholder { color: var(--border-strong); opacity: 1; }
  .bulk-i--amt { font-family: 'DM Mono', monospace; text-align: right; }
  .bulk-i:focus { background: var(--bg-input); box-shadow: inset 0 0 0 2px var(--accent-primary); border-radius: 6px; }

  .bulk-strip { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: rgba(124,106,255,.06);
    border-top: 1px dashed var(--border-default); flex-wrap: wrap; }
  .bulk-strip__lbl { font-size: 12px; color: var(--text-tertiary); }
  .bulk-mk { appearance: none; border: 1px solid var(--border-default); background: var(--bg-surface); color: var(--text-secondary);
    border-radius: 16px; padding: 5px 11px; font-family: inherit; font-size: 12px; cursor: pointer; }
  .bulk-mk:hover { border-color: var(--border-strong); color: var(--text-primary); }
  .bulk-mk--on { border-color: var(--accent-warm); color: var(--accent-warm); background: rgba(232,131,74,.10); font-weight: 600; }
  .bulk-mk--purple.bulk-mk--on { border-color: var(--accent-primary); color: var(--accent-primary); background: rgba(124,106,255,.12); }
  .bulk-mk--teal.bulk-mk--on { border-color: var(--accent-gold); color: var(--accent-gold); background: rgba(232,197,71,.12); }
  .bulk-strip__tip { margin-left: auto; font-size: 11px; color: var(--text-tertiary); }
  /* "Log" commit button — TOUCH-ONLY (a numeric keyboard has no Enter key). Desktop commits with Enter, so the
     button is hidden there (it only cluttered the tag strip). Shown on touch devices (incl. a landscape phone,
     which is wider than the mobile breakpoint but still has no Enter) OR any narrow viewport. */
  .bulk-strip__log { display: none; flex: 0 0 auto; }
  @media (max-width: 820px), (hover: none) and (pointer: coarse) { .bulk-strip__log { display: block; } }
  .bulk-strip__log:disabled { opacity: .45; cursor: default; }
  /* mobile-only merchant field — in portrait the merchant column is hidden, so set it here. Desktop: column is used.
     v412: the field now sits inside its `.mcombo` wrap, so the WRAP carries the show/hide — the input's own
     `display` would otherwise hide the box while the wrap kept its space in the strip. */
  .bulk-strip > .mcombo { display: none; }
  .bulk-strip__merch { display: none; font: inherit; font-size: 14px; padding: 8px 10px; border-radius: 8px;
    border: 1px solid var(--border-default); background: var(--bg-input); color: var(--text-primary); min-width: 0; }
  .bulk-strip__merch:focus { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-glow); }
  /* §schema tags in the bulk strip (Phase 5): a calm reveal + a compact tagInput so it fits the dense strip */
  .bulk-strip__addtags { font-size: 12px; padding: 2px 0; }
  .bulk-strip__tags { flex: 1 1 200px; min-width: 160px; min-height: 36px; padding: 4px 6px; }
  .bulk-strip__tags .taginput__field { min-width: 70px; }

  /* month totals — compact, at the top of the always-visible ledger (normal size, not a big footer) */
  .bulk-metric { display: inline-flex; align-items: baseline; }
  .bulk-k { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-tertiary); }
  .bulk-v { font-family: 'DM Mono', monospace; font-size: 14px; font-weight: 500; margin-left: 6px; }
  .bulk-v--out { color: var(--accent-out); } .bulk-v--in { color: var(--accent-teal); }
  .bulk-foot__note { color: var(--text-tertiary); font-weight: 400; }
  .bulk-foot--settle { color: var(--accent-primary); }

  /* the live ledger STAYS IN VIEW as you scroll the long category list (sticky), and its list is sized to the
     viewport so the whole panel fits on screen — so an entry you just made (top of the list) is always visible. */
  .bulk-ledger { background: var(--bg-surface); border: 1px solid var(--border-subtle); border-radius: var(--radius); position: sticky; top: 16px; overflow: hidden; max-height: calc(100vh - 32px); display: flex; flex-direction: column; }
  .bulk-ledger__head { padding: 12px 16px; border-bottom: 1px solid var(--border-default); background: var(--bg-elevated); }
  .bulk-ledger__row1 { display: flex; align-items: center; gap: 8px; }
  .bulk-ledger__row1 h3 { margin: 0; font-size: 15px; font-weight: 700; flex: 1; }
  .bulk-pend { display: inline-flex; align-items: center; gap: 5px; background: rgba(232,131,74,.12); color: var(--accent-warm);
    border: 1px solid rgba(232,131,74,.4); border-radius: 14px; padding: 3px 9px; font-size: 12px; font-weight: 600; cursor: pointer; }
  .bulk-ledger__money { display: flex; gap: 18px; flex-wrap: wrap; align-items: baseline; margin-top: 8px; }
  .bulk-ledger__ctxline { font-size: 12px; font-weight: 600; margin-top: 6px; }
  .bulk-ledger__ctxline[hidden] { display: none; }
  .bulk-ledger__sub { display: block; font-size: 12px; color: var(--text-tertiary); margin-top: 6px; }
  .bulk-ledger__list { flex: 1; min-height: 120px; overflow: auto; }
  .bulk-empty { padding: 28px 16px; color: var(--text-tertiary); font-size: 13px; text-align: center; }
  .bulk-le { display: flex; align-items: flex-start; gap: 8px; padding: 7px 12px 7px 16px; border-bottom: 1px solid var(--border-subtle); }
  .bulk-le:hover { background: var(--bg-hover); }
  .bulk-le--pending { opacity: .72; }
  .bulk-le__day { font-family: 'DM Mono', monospace; font-size: 12px; color: var(--text-tertiary); width: 42px; flex: none; padding-top: 1px; }
  .bulk-le__main { flex: 1; min-width: 0; line-height: 1.35; }
  .bulk-le__t { font-size: 14px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .bulk-le__m { font-size: 12px; color: var(--text-tertiary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* badges on their own wrapping line under the name → always fully visible, never lost to the title ellipsis */
  .bulk-le__badges { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px; }
  .bulk-badge { display: inline-block; font-size: 10px; font-weight: 600; border-radius: 5px; padding: 1px 5px; vertical-align: middle; }
  .bulk-badge--neutral { background: var(--bg-input); color: var(--text-secondary); }
  .bulk-badge--shared { background: rgba(124,106,255,.16); color: var(--accent-primary); }
  .bulk-badge--mark { background: rgba(232,131,74,.14); color: var(--accent-warm); }
  .bulk-badge--ref { background: rgba(232,197,71,.14); color: var(--accent-gold); }
  .bulk-badge--settled { background: rgba(62,207,170,.14); color: var(--accent-teal); }   /* reflects the Ledger's settled state (teal = resolved) */
  .bulk-badge--session { background: transparent; color: var(--text-tertiary); border: 1px dashed var(--border-default); } /* faint = this-session only, drops on reload */
  .bulk-le__a { font-family: 'DM Mono', monospace; font-size: 14px; white-space: nowrap; padding: 2px 4px; border-radius: 5px; }
  .bulk-le__a--edit { cursor: text; }
  .bulk-le__a--edit:hover { background: var(--bg-input); box-shadow: inset 0 0 0 1px var(--border-default); }
  .bulk-le__a--in { color: var(--accent-teal); } .bulk-le__a--ref { color: var(--accent-gold); } .bulk-le__a--neutral { color: var(--text-tertiary); }
  .bulk-le__aedit { width: 82px; font-family: 'DM Mono', monospace; font-size: 14px; text-align: right; background: var(--bg-input);
    border: none; box-shadow: inset 0 0 0 2px var(--accent-primary); border-radius: 6px; color: var(--text-primary); padding: 3px 5px; outline: none; }
  .bulk-le__rm { background: none; border: none; color: var(--text-tertiary); cursor: pointer; font-size: 16px; flex: none; padding: 2px 6px; border-radius: 5px; line-height: 1; }
  .bulk-le__rm:hover { color: var(--accent-warm); background: var(--bg-input); }
  .bulk-confirm { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 8px 16px; background: var(--bg-elevated); border-bottom: 1px solid var(--border-subtle); }
  .bulk-confirm__q { font-size: 13px; color: var(--text-secondary); }
  .bulk-confirm__btns { display: flex; gap: 6px; margin-left: auto; }

  .bulk-jump { position: fixed; left: 50%; bottom: 26px; transform: translateX(-50%); background: var(--bg-elevated);
    border: 1px solid var(--accent-primary); box-shadow: 0 8px 24px rgba(0,0,0,.5), 0 0 0 3px var(--accent-glow);
    border-radius: 24px; padding: 9px 18px; font-size: 14px; z-index: 50;
    display: flex; align-items: center; flex-wrap: wrap; gap: 6px; max-width: min(560px, calc(100vw - 24px)); }
  .bulk-jump b { color: var(--accent-primary); } .bulk-jump__k { color: var(--text-tertiary); }
  /* the buffer is a REAL focused input (native typing/backspace/caret) styled to read as inline accent text */
  .bulk-jump__in { font: inherit; color: var(--accent-primary); background: transparent; border: none; outline: none;
    padding: 0; margin: 0; min-width: 4ch; caret-color: var(--accent-primary); }
  .bulk-jump__in::placeholder { color: var(--text-tertiary); }
  /* selectable jump pills — one per candidate (category or learned merchant); the highlighted one is purple-ringed */
  .bulk-jump__pills { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 6px; }
  .bulk-jump__pill { font: inherit; cursor: pointer; border-radius: 14px; padding: 3px 10px; line-height: 1.3;
    background: var(--bg-hover); border: 1px solid var(--border-default); color: var(--text-secondary); }
  .bulk-jump__pill:hover { border-color: var(--border-strong); }
  .bulk-jump__pill--on { border-color: var(--accent-primary); background: var(--accent-glow); color: var(--text-primary);
    box-shadow: 0 0 0 1px var(--accent-primary); }
  .bulk-jump__m { color: var(--accent-primary); font-weight: 600; }   /* a merchant name → this pill fills the merchant cell */
  .bulk-jump__c, .bulk-jump__hint { color: var(--text-tertiary); }
  .bulk-jump__c2 { color: var(--text-primary); }
  .bulk-jump__hint { font-size: 12px; }
  /* composer chips — the locked day + category/merchant, shown left of the amount box in the AMOUNT stage */
  .bulk-jump__chips { display: inline-flex; align-items: center; gap: 6px; }
  .bulk-jump__chips:empty { display: none; }
  .bulk-jump__day { font-size: 12px; font-weight: 600; color: var(--accent-gold); background: rgba(232,197,71,.12);
    border-radius: 12px; padding: 3px 9px; white-space: nowrap; }
  .bulk-jump__catchip { font-size: 13px; font-weight: 600; color: var(--text-primary); background: var(--accent-glow);
    border: 1px solid var(--accent-primary); border-radius: 12px; padding: 3px 10px; white-space: nowrap; }
  /* v393 — THE NEWCOMER OFFER. The chips are the SAME chip as the amount stage's locked one, one state
     earlier: an OFFER rather than a decision, so it wears the outline and leaves the fill for the moment
     it is chosen. Thirty filled purple chips would be the loudest thing on the screen and would say
     "thirty decisions", which is the opposite of what the bar is for. The strip wraps and caps its height
     so the bar can never grow past the phone; `data-noswipe` keeps a scroll here from navigating tabs. */
  .bulk-jump__newask { font-size: 13px; color: var(--text-primary); font-weight: 600; }
  .bulk-jump__newchips { display: flex; flex-wrap: wrap; gap: 6px; width: 100%; max-height: 108px; overflow-y: auto;
    padding: 2px 0; scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
  .bulk-jump__newchips::-webkit-scrollbar { width: 8px; }
  .bulk-jump__newchips::-webkit-scrollbar-track { background: transparent; }
  .bulk-jump__newchips::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 999px; }
  .bulk-jump__catchip--pick { font-family: inherit; cursor: pointer; font-weight: 500; color: var(--text-secondary);
    background: var(--bg-hover); border-color: var(--border-default); }
  .bulk-jump__catchip--pick:hover { border-color: var(--accent-primary); background: var(--accent-glow); color: var(--text-primary); }

  /* wide screens → two category columns side by side (the second carries a divider). When stacked (narrow),
     the second column's header is hidden so the two columns read as one continuous list. */
  @media (min-width: 1200px) {
    .bulk-cols { grid-template-columns: 1fr 1fr; }
    .bulk-cols--single { grid-template-columns: 1fr; }   /* a one-group engine (income/transfers/goals) spans full width */
    .bulk-col--second { border-left: 1px solid var(--border-subtle); }
  }
  @media (max-width: 1199px) {
    .bulk-col--second > .bulk-ehead { display: none; }
  }
  @media (max-width: 820px) {
    .bulk-panes { grid-template-columns: 1fr; }
    .bulk-ledger { position: static; max-height: none; }   /* stacked below the categories → grows naturally, no nested scroll */
    .bulk-ledger__list { min-height: 0; }
    .bulk-ehead, .bulk-row { grid-template-columns: 1.4fr 44px 96px; }
    .bulk-h.h--merch, .bulk-h.h--note, .bulk-cell--merch, .bulk-cell--note { display: none; }
    /* the merchant column is gone in portrait → set the merchant in the focus strip instead; stack the strip:
       merchant on top, chips in the middle, a full-width "Log" button at the bottom. The desktop Enter tip is
       meaningless on a phone (no Enter key), so hide it. */
    .bulk-strip > .mcombo { display: block; flex: 1 1 100%; min-width: 0; }
    .bulk-strip__merch { display: block; flex: 1 1 100%; }
    .bulk-strip__log { flex: 1 1 100%; }
    .bulk-strip__tip { display: none; }
  }

  /* §4d "to finish" — a clickable badge/button (bulk) + the gentle row nudge (ledger) + the shared modal */
  .bulk-badge--btn { cursor: pointer; border: none; font: inherit; font-size: 10px; font-weight: 600; }
  .bulk-badge--btn:hover { filter: brightness(1.15); }
  .bulk-le--flash { animation: bulkflash 1.2s ease; }
  @keyframes bulkflash { 0%, 100% { background: transparent; } 30% { background: var(--accent-glow); } }
  .tx__finish { display: inline-block; margin-top: 4px; background: rgba(232,131,74,.10); color: var(--accent-warm);
    border: 1px solid rgba(232,131,74,.35); border-radius: 8px; padding: 3px 9px; font: inherit; font-size: 12px;
    font-weight: 600; cursor: pointer; }
  .tx__finish:hover { background: rgba(232,131,74,.18); }
  .finish-row { display: flex; gap: 12px; flex-wrap: wrap; align-items: flex-end; margin: 6px 0 4px; }
  .finish-row__go { flex: 0 0 auto; margin-left: auto; } /* "Set up plan" sits inline, pushed to the row's end */
  .finish-field { display: flex; flex-direction: column; gap: 5px; }
  .finish-field label { font-size: 12px; color: var(--text-secondary); }
  .finish-field input { width: 120px; background: var(--bg-input); border: 1px solid var(--border-default);
    border-radius: 8px; padding: 9px 10px; color: var(--text-primary); font-family: inherit; font-size: 14px; }
  .finish-field input:focus { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-glow); }
  .finish-preview { min-height: 18px; margin: 2px 0 8px; font-family: 'DM Mono', monospace; }
  .finish-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; flex-wrap: wrap; }
  .finish-untag { margin-top: 12px; text-align: center; border-top: 1px solid var(--border-default); padding-top: 10px; }
  .finish-cands { display: flex; flex-direction: column; gap: 6px; margin: 8px 0; max-height: 320px; overflow: auto; }
  .finish-cand { display: flex; flex-direction: column; gap: 2px; text-align: left; background: var(--bg-input);
    border: 1px solid var(--border-default); border-radius: 10px; padding: 10px 12px; cursor: pointer; font: inherit; }
  .finish-cand:hover { border-color: var(--accent-primary); background: var(--bg-hover); }
  .finish-cand__t { font-size: 14px; font-weight: 600; color: var(--text-primary); }
  .finish-cand__m { font-size: 12px; color: var(--text-tertiary); font-family: 'DM Mono', monospace; }
  /* "N to finish" list (§4d Slice 3): an ACCORDION — collapsed one-liners you expand to finish */
  .finish-list { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }
  .finish-acc { border: 1px solid var(--border-default); border-radius: 12px; background: var(--bg-input); overflow: hidden; }
  .finish-acc--done { opacity: 0.7; }
  .finish-acc__head { display: flex; align-items: center; gap: 10px; width: 100%; padding: 12px 14px;
    background: none; border: none; cursor: pointer; font: inherit; text-align: left; color: var(--text-primary); }
  .finish-acc__head:hover:not(:disabled) { background: var(--bg-hover); }
  .finish-acc__head:disabled { cursor: default; }
  .finish-acc__t { font-weight: 600; min-width: 0; flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .finish-acc__amt { font-family: 'DM Mono', monospace; color: var(--text-secondary); flex: 0 0 auto; }
  .finish-acc__needs { flex: 0 0 auto; font-size: 11px; padding: 2px 8px; border-radius: 999px;
    background: rgba(124,106,255,.12); color: var(--accent-primary); }
  .finish-acc__needs--done { background: transparent; color: var(--accent-teal); font-weight: 600; }
  .finish-acc__chev { flex: 0 0 auto; display: inline-flex; line-height: 0; color: var(--text-tertiary); transition: transform .15s ease; }
  .finish-acc--open .finish-acc__chev { transform: rotate(180deg); }
  .finish-acc__body { padding: 4px 14px 14px; border-top: 1px solid var(--border-subtle); }
  .finish-acc__body .finish-untag { margin-top: 10px; }

  /* ── CSV import (Settings → Import, §11.4) ─────────────────────────────────────────────── */
  /* .csv-lanes removed in copy pass round 2 — the double-logging advice it framed is no longer a standing
     paragraph on the door; it is rendered verbatim by the door's ⓘ (csv.lanes is still the key). */
  .csv-drop { display: flex; flex-direction: column; align-items: center; gap: 4px; text-align: center;
    padding: 26px 18px; margin-top: 14px; border: 1.5px dashed var(--border-subtle); border-radius: 14px;
    cursor: pointer; transition: border-color .15s ease, background .15s ease; }
  .csv-drop:hover, .csv-drop:focus-visible { border-color: var(--accent-primary); background: var(--bg-hover); outline: none; }
  .csv-drop--drag { border-color: var(--accent-primary); border-style: solid; background: var(--bg-hover); }
  /* Start-over escape hatch: a discreet ghost button (pushed to the right) that arms an inline confirm. */
  .csv-reset__btn { margin-left: auto; }
  .csv-reset { margin-left: auto; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
  .csv-reset__msg { font-size: 13px; }
  .csv-drop__icon { font-size: 22px; color: var(--accent-primary); }
  .csv-drop__t { font-weight: 600; }
  .csv-drop__h { font-size: 12px; }
  .csv-file { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
  .csv-err { margin-top: 12px; }

  /* ══ v404 — SEEDING: THE BOOK PAGER ("Past months", the third import door) ═══════════════════════════
     The v402 months × categories GRID is gone (human-redesigned, 2026-08-25 night). ONE MONTH PER PAGE:
     the whole category tree top to bottom, income first, ‹ › and the dots to turn. It is a READING column,
     not a spreadsheet — so it centres at a comfortable measure on a wide screen and needs no extra frame
     width at all (csv.js no longer grants `.app--csv` for this door). The phone layout is now the SAME
     layout, narrower: the only thing the breakpoint changes is the sticky commit dock's gutter. */
  .seed-host { margin-top: 4px; }
  .seed-intro { margin: 0 0 12px; font-size: 13px; }
  .seed-span { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
  .seed-span__lbl { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; }
  .seed-empty { margin: 14px 0; font-size: 13px; }
  /* ── v420 ③ — THE MONTH CONTROL STEPS DOWN INTO THE DROPDOWN REGISTER ─────────────────────────────
     Measured on the shipped v419 door: a 195 × 45px trigger at 15px in the primary text colour, sitting
     alone on the resting screen — the human read it as the screen's big BUTTON rather than as a field to
     open. Nothing about the control changes; it is simply sized and toned like the dropdown it is (14px,
     8px/10px padding), and the EMPTY state — where it is naming an act rather than a chosen span — reads
     in the secondary tone, the way a placeholder does everywhere else in the app. Once a span is chosen
     the trigger names it and steps back up to the primary tone, because then it is a value, not a prompt. */
  .seed-span .dropdown__trigger { width: auto; font-size: 14px; padding: 8px 10px; border-radius: 9px;
    color: var(--text-secondary); }
  .seed-span .dropdown__trigger .dropdown__value { color: inherit; }
  .seed-span--chosen .dropdown__trigger { color: var(--text-primary); }
  /* v410 ⑤ — the door's resting state. It is an invitation, not a warning, so no colour of its own and
     nothing that reads as a status. v420 ②: it is now the SAME paragraph as `.seed-intro` (one line at
     rest, not two); the class stays because it is the structural marker of "no span chosen yet". */
  .seed-pick { margin: 0 0 12px; font-size: 13px; }
  .seed-book { max-width: 520px; margin: 0 auto; }
  /* ── the pager ── */
  .seed-pager { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 0 0 6px; }
  .seed-pg { flex: 0 0 auto; width: 42px; height: 42px; border-radius: 50%; background: var(--bg-elevated);
    border: 1px solid var(--border-default); color: var(--text-secondary); font: inherit; font-size: 19px;
    line-height: 1; cursor: pointer; }
  .seed-pg:hover:not(:disabled) { color: var(--text-primary); border-color: var(--accent-primary); }
  .seed-pg:disabled { opacity: .3; cursor: default; }
  .seed-pg__m { flex: 1 1 auto; min-width: 0; text-align: center; font-size: 18px; font-weight: 600; }
  .seed-pg__c { display: block; font-size: 11.5px; font-weight: 400; margin-top: 2px; }
  /* ── the dots (ratification 3): purple = has figures · gold ring = already holds real entries ── */
  .seed-dots { display: flex; flex-wrap: wrap; justify-content: center; gap: 7px; margin: 4px 0 14px; }
  .seed-dot { width: 9px; height: 9px; padding: 0; flex: 0 0 auto; border-radius: 50%;
    background: transparent; border: 1.5px solid var(--border-default); cursor: pointer; }
  .seed-dot--has { background: var(--accent-primary); border-color: var(--accent-primary); }
  .seed-dot--real { border-color: var(--accent-gold); }
  .seed-dot--now { box-shadow: 0 0 0 2px var(--accent-glow); }
  .seed-dot--cur { outline: 2px solid var(--accent-primary); outline-offset: 2px; }
  /* the calm busy note — gold, the same signal the dot's ring carries, never amber (that would read alarm) */
  .seed-busy { margin: 0 0 12px; padding: 8px 11px; font-size: 12.5px; line-height: 1.45;
    color: var(--text-secondary); border: 1px solid var(--accent-gold); border-radius: 10px; }
  /* ── the tree: sections fold, and a fold is remembered ── */
  .seed-sec { margin: 0 0 2px; }
  .seed-sec__head { display: flex; align-items: center; gap: 8px; padding: 9px 2px 5px; cursor: pointer;
    border-bottom: 1px solid var(--border-subtle); }
  .seed-sec__head:focus-visible { outline: 2px solid var(--accent-primary); outline-offset: 2px; }
  .seed-sec__car { flex: 0 0 auto; font-size: 10px; color: var(--text-tertiary); transition: transform .15s ease; }
  .seed-sec--closed .seed-sec__car { transform: rotate(-90deg); }
  .seed-sec__l { flex: 1 1 auto; min-width: 0; font-size: 12px; text-transform: uppercase;
    letter-spacing: .06em; color: var(--text-tertiary); overflow-wrap: break-word; }
  /* DM Mono ships no weight above 500 — a money figure never asks for one */
  .seed-sec__sum { flex: 0 0 auto; font-size: 12px; font-weight: 500; color: var(--text-secondary);
    font-variant-numeric: tabular-nums; }
  .seed-sec--inc .seed-sec__l, .seed-sec--inc .seed-sec__sum { color: var(--accent-teal); }
  .seed-sec--closed .seed-sec__body { display: none; }
  .seed-crow { display: flex; align-items: center; gap: 10px; padding: 6px 2px; }
  .seed-crow__l { flex: 1 1 auto; min-width: 0; font-size: 14px; color: var(--text-secondary); overflow-wrap: break-word; }
  .seed-in { flex: 0 0 128px; min-width: 0; text-align: right; padding: 7px 9px; font-size: 14px;
    font-variant-numeric: tabular-nums; background: var(--bg-input); border: 1px solid var(--border-subtle);
    border-radius: 8px; color: var(--text-primary); }
  .seed-in:focus { border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-glow); outline: none; }
  /* an income cell reads as income (§3 teal) the moment it is typed, like every other income figure */
  .seed-in--inc { color: var(--accent-teal); }
  /* ── v420 ⑤ — WHAT THIS PASTE PUT HERE, marked ────────────────────────────────────────────────────
     Their words, turning the pages after a paste: *"there is no highlight… okay, these ones are the ones
     that are getting inputted… so now it's hard."* NO NEW SEMANTIC COLOUR: this is the same brand purple
     the page dots already use for "this month holds typed figures", at the glow token's own 18% — it says
     "the app put this here", never anything about the money. The inset bar is what carries it at a glance
     on a phone, where an 18% tint on a dark field is easy to miss; a FOCUS ring still wins over it, because
     the cell you are typing in is the one that matters. The label beside a marked cell steps up one tone so
     the whole ROW reads as marked rather than just the box. */
  .seed-in--new { background: var(--accent-glow); border-color: var(--accent-primary);
    box-shadow: inset 3px 0 0 var(--accent-primary); }
  .seed-in--new:focus { box-shadow: 0 0 0 3px var(--accent-glow), inset 3px 0 0 var(--accent-primary); }
  .seed-crow--new .seed-crow__l { color: var(--text-primary); }
  /* the one quiet line that says what the mark means — a colour without a sentence is a puzzle */
  .seed-note--mark { margin: 3px 0 0; }
  .seed-cleared { margin: 6px 0 0; font-size: 12px; }
  /* ══ v405 — THE ALWAYS-IN-VIEW SUMMARY ═══════════════════════════════════════════════════════════════
     The human's review: *"the live Income / Spending / Kept summary must ALWAYS be in view — like the
     intentions Set screen's side panel"*, and on the phone *"otherwise somebody puts 12 months, the bottom
     pin will take the entire screen"*. So this is the intentions two-column recipe, copied to seed- names:
     ONE element, sticky-bottom bar below the workspace breakpoint and a sticky sidebar card above it. No
     second DOM tree, no JS media query — the only thing the breakpoint changes is CSS. */
  .seed-pin { position: sticky; bottom: 0; z-index: 2; display: flex; flex-direction: column;
    margin-top: 14px; padding: 8px 0 calc(4px + env(safe-area-inset-bottom, 0px));
    background: var(--bg-surface); border-top: 1px solid var(--border-subtle); }
  .seed-pin__t { display: none; }                       /* the two headings are the sidebar's, not the bar's */
  .seed-pin__cur { display: flex; align-items: baseline; flex-wrap: wrap; gap: 2px 10px; padding: 5px 2px 2px; }
  .seed-pin__mn { font-size: 13px; font-weight: 600; color: var(--text-primary); }
  /* the current month's figures keep the v404 `.seed-msum` element and its colours — the same three figures,
     moved into view rather than re-invented. FULL CENTS here, on both screens. */
  .seed-msum { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 14px; flex: 1 1 auto; min-width: 0; }
  .seed-msum__st { white-space: nowrap; font-size: 13px; }
  .seed-msum__k { font-size: 12px; margin-right: 5px; }
  .seed-msum__v { font-weight: 500; font-variant-numeric: tabular-nums; }
  .seed-msum__v--out { color: var(--accent-out); }
  .seed-msum__v--in { color: var(--accent-teal); }
  .seed-msum__none { font-size: 12.5px; }
  /* ── the months-so-far list. ONE grid for the whole list (rows are `display: contents`) so the four
     columns line up down the panel — per-row grids would size their columns independently. ── */
  .seed-pin__list { order: -1; display: grid; grid-template-columns: minmax(0, 1fr) auto auto auto;
    gap: 0 8px; align-items: baseline; padding: 4px 2px 7px; border-bottom: 1px solid var(--border-subtle); }
  .seed-pin--closed .seed-pin__list { display: none; }
  .seed-pin__lh, .seed-pin__lr { display: contents; }
  .seed-pin__lh > span { font-size: 10px; text-transform: uppercase; letter-spacing: .05em;
    color: var(--text-tertiary); padding-bottom: 3px; text-align: right; }
  .seed-pin__lh > span:first-child { text-align: left; }
  /* THE QUICK-LOOK REGISTER IS 11px, and it is a MEASUREMENT, not a taste (poc/_probe405.mjs, re-measured
     in the suite at 5/6/7 digits on both breakpoints). The number columns are `auto`, so at seven digits it
     is the MONTH column — the only flexible one — that gets squeezed to "Jan 20…"; the mock's own
     measurement missed this because it only checked the amount cells. One step down in size buys ~20px of
     slack at 390px with whole dollars and ~21px in the 400px panel with cents. */
  .seed-pin__lm { font-size: 11px; color: var(--text-secondary); padding: 2px 0;
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* DM Mono ships no weight above 500 — a money figure never asks for one */
  .seed-pin__n { font-size: 11px; font-weight: 500; font-variant-numeric: tabular-nums;
    white-space: nowrap; text-align: right; padding: 2px 0; }
  .seed-pin__n--out { color: var(--accent-out); }
  .seed-pin__n--in, .seed-pin__n--kept { color: var(--accent-teal); }
  .seed-pin__lr--cur .seed-pin__lm { color: var(--text-primary); font-weight: 600; }
  .seed-pin__lr--none .seed-pin__n { color: var(--text-tertiary); }
  /* THE QUICK-LOOK IS WHOLE DOLLARS AND THE PANEL IS CENTS, and both are the same node: measured at 390px
     (poc/measure-pin-digits.mjs) a row with cents WRAPS at six digits while whole dollars hold one line
     through seven. `display: none` also keeps the hidden one out of the accessibility tree. */
  .seed-pin__n2 { display: none; }
  .seed-pin__bar { display: flex; align-items: center; gap: 10px; padding: 6px 0 0; }
  /* rider 2: the expand-up does NOT get its own line — it sits beside Save */
  .seed-pin__more { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 5px; background: none;
    border: 0; color: var(--text-tertiary); font: inherit; font-size: 12.5px; padding: 9px 2px; cursor: pointer;
    white-space: nowrap; }
  .seed-pin__more:hover { color: var(--text-secondary); }
  .seed-pin__more:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-glow); border-radius: 8px; }
  .seed-pin__caret { font-size: 9px; }
  /* v408 ⑦: one size step down — the human, twice: the pin's Save is "still a little too big". Scoped to
     THIS button, never the app-wide .btn. */
  .seed-pin .seed-save { flex: 1 1 auto; font-size: 13.5px; padding: 10px 16px; }
  /* v408 ③ (their laptop screenshot): with nothing to save, the disabled button was still PRIMARY PURPLE —
     it read as the main action while refusing to be one. A control that cannot be pressed says so. */
  .seed-pin .seed-save:disabled { background: var(--bg-elevated); color: var(--text-tertiary);
    box-shadow: inset 0 0 0 1px var(--border-subtle); opacity: 1; }
  /* the calm one-line beat after an ask that resolved to nothing at all (v408 ③) */
  .seed-note { margin: 0; padding: 4px 2px 2px; font-size: 12.5px; line-height: 1.4; }
  /* ── v408 ④ — THE ACTIVE WAIT: the bar becomes the saving state, dots and all ── */
  .seed-saving { display: flex; align-items: baseline; gap: 7px; flex: 1 1 auto; padding: 10px 2px;
    font-size: 13.5px; color: var(--text-secondary); }
  .seed-saving__d { display: inline-flex; align-items: baseline; gap: 4px; }
  .seed-saving__dot { width: 5px; height: 5px; border-radius: 50%; background: var(--accent-primary);
    display: inline-block; animation: seedDots 1.05s ease-in-out infinite; }
  .seed-saving__dot:nth-child(2) { animation-delay: .18s; }
  .seed-saving__dot:nth-child(3) { animation-delay: .36s; }
  @keyframes seedDots { 0%, 70%, 100% { opacity: .25; transform: translateY(0); } 35% { opacity: 1; transform: translateY(-3px); } }
  /* motion is a nicety; the words are the message. Someone who has asked for less of it gets the words. */
  @media (prefers-reduced-motion: reduce) {
    .seed-saving__dot { animation: none; opacity: .55; }
  }
  .seed-err { font-size: 12.5px; color: var(--accent-warm); padding: 6px 2px 0; }
  /* v408 ③: `.seed-actions` is RETIRED. v405 kept it because the busy-ask card still had a button row at
     its foot; act-on-answer removed that row ("it will remove those two buttons at the bottom anyway"), so
     the class had no user left. A class kept alive by nothing is how a stylesheet rots. */
  @media (min-width: 900px) {
    /* THE WORKSPACE: book left, picture right. The Import screen already renders inside Settings' wide
       frame, so nothing has to be re-widened — the door simply stops being one narrow column. */
    .seed-layout { display: grid; grid-template-columns: minmax(0, 1fr) 400px; gap: 20px; align-items: start; }
    .seed-layout .seed-book { margin: 0; max-width: 560px; }
    .seed-pin { position: sticky; top: 16px; bottom: auto; z-index: 1; margin-top: 0; padding: 16px;
      border: 1px solid var(--border-subtle); border-top: 1px solid var(--border-subtle); border-radius: 14px;
      max-height: calc(100vh - 32px); overflow: auto; }
    .seed-pin__t { display: block; font-size: 12px; color: var(--text-tertiary); text-transform: uppercase;
      letter-spacing: .06em; margin-bottom: 7px; }
    .seed-pin__t--list { margin-top: 14px; padding-top: 13px; border-top: 1px solid var(--border-subtle); }
    .seed-pin__cur { display: block; padding: 0; }
    .seed-pin__mn { display: block; font-size: 15px; margin-bottom: 7px; }
    .seed-pin .seed-msum { display: flex; flex-direction: column; gap: 3px; }
    .seed-pin .seed-msum__st { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
    .seed-pin .seed-msum__k { margin-right: 0; }
    /* always there on a laptop — nothing to expand, so the toggle goes and the list ignores the fold class */
    .seed-pin__more { display: none; }
    .seed-pin__list, .seed-pin--closed .seed-pin__list { display: grid; }
    /* the panel has room the phone does not, and cents make three long figures — 8px let them read as one
       block (caught by eye, not by an assertion; the 7-digit fit is re-measured in arm ⑦) */
    .seed-pin__list { order: 0; padding: 0; border-bottom: 0; gap: 0 12px; }
    .seed-pin__n0 { display: none; }
    .seed-pin__n2 { display: inline; }             /* rider 4: the laptop shows CENTS always */
    .seed-pin__bar { padding-top: 14px; }
    .seed-pin .seed-save { width: 100%; }
  }
  /* ══ v408 ②③ — THE BUSY-MONTH ASK: IN VIEW, AND IT ACTS ═══════════════════════════════════════════════
     The defect the human hit on Live: *"even on laptop it opens at the bottom, so I do need to scroll; not
     good"* — the card rendered BELOW the book on both form factors, so the one state change that needs an
     answer was the one nothing showed. ONE card, two shapes, decided here the same way the pin's are:
       · PHONE — a SHEET over the screen under a scrim (the emoji sheet's treatment);
       · LAPTOP — the right column of the workspace, where the pin steps aside for exactly as long as it
         stands (`.seed-layout--asking`), so the question sits beside the book with nothing to scroll to.
     Calm, purple (the judgement is theirs), never amber — amber would read as alarm. */
  .seed-askback { position: fixed; inset: 0; z-index: 60; background: rgba(6, 6, 10, .45); }
  .seed-layout--asking .seed-pin { display: none; }
  .seed-ask { margin: 0; border-color: var(--accent-primary);
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 61;
    max-height: 82vh; overflow-y: auto; border-radius: 16px 16px 0 0;
    padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px)); }
  .seed-ask__head { display: flex; align-items: flex-start; gap: 10px; }
  .seed-ask__t { font-size: 15px; margin: 0 0 10px; flex: 1 1 auto; min-width: 0; }
  /* the ONE quiet escape — small, tertiary, never a second primary action */
  .seed-ask__x { flex: 0 0 auto; appearance: none; border: 0; background: none; color: var(--text-tertiary);
    font: inherit; font-size: 15px; line-height: 1; padding: 2px 4px; cursor: pointer; }
  .seed-ask__x:hover { color: var(--text-primary); }
  .seed-ask__x:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-glow); border-radius: 8px; }
  .seed-ask__row { padding: 9px 0; border-top: 1px solid var(--border-subtle); display: flex;
    align-items: center; gap: 12px; flex-wrap: wrap; }
  .seed-ask__q { margin: 0; font-size: 13px; flex: 1 1 200px; min-width: 0; }
  .seed-ask__seg { display: inline-flex; border: 1px solid var(--border-default); border-radius: 9px; overflow: hidden; }
  .seed-ask__b { appearance: none; border: 0; background: transparent; color: var(--text-secondary);
    padding: 8px 13px; font: inherit; font-size: 12.5px; cursor: pointer; }
  .seed-ask__b + .seed-ask__b { border-left: 1px solid var(--border-default); }
  .seed-ask__b:hover { background: var(--accent-glow); color: var(--accent-primary); }
  .seed-ask__b:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--accent-primary); }
  /* an ANSWERED month: the row collapses to one calm line — and the line is itself the way to change it */
  .seed-ask__ans { display: flex; align-items: baseline; gap: 8px; width: 100%; appearance: none; border: 0;
    background: none; text-align: left; font: inherit; font-size: 13px; color: var(--text-secondary);
    padding: 2px 0; cursor: pointer; }
  .seed-ask__tick { flex: 0 0 auto; font-size: 11px; color: var(--accent-teal); }
  .seed-ask__ansl { flex: 1 1 auto; min-width: 0; }
  .seed-ask__chg { flex: 0 0 auto; font-size: 11.5px; }
  .seed-ask__ans:hover .seed-ask__chg { color: var(--text-secondary); }
  .seed-ask__ans:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-glow); border-radius: 8px; }
  @media (min-width: 900px) {
    /* THE LAPTOP SHAPE. The same card, un-sheeted, taking the workspace's right column — auto-placed there
       because the pin (the only other column child) is out of flow while the ask stands. Nothing is covered
       up, so there is no scrim. It must be reachable WITHOUT SCROLLING: that is the whole fix, and the suite
       measures the rendered rect against the viewport rather than trusting this rule. */
    .seed-askback { display: none; }
    .seed-ask { position: static; z-index: auto; max-height: none; overflow-y: visible;
      border-radius: 14px; padding-bottom: 16px; }
  }
  /* ══ v419 ① — THE PASTE-COLLISION ASK ═════════════════════════════════════════════════════════════
     The SAME card as the busy-month ask (`.seed-ask` carries the geometry: a sheet on a phone, the
     workspace's right column on a laptop) with the paste's own rows inside it. Reusing the class rather
     than copying the rules is the point — one register, one place to change it, and the two questions
     cannot drift into looking like two different kinds of question. */
  .seed-pask__sub { margin: 0 0 6px; font-size: 12px; line-height: 1.45; }
  .seed-pask__qw { flex: 1 1 200px; min-width: 0; }
  .seed-pask__figs { margin: 2px 0 0; font-size: 12px; color: var(--text-tertiary);
    font-variant-numeric: tabular-nums; }
  /* the answer-for-all row reads as a heading over the list it governs, not as one more row in it —
     and it stays OUTSIDE the scroller with the title and the escape, so a long list never carries them
     off the top. The list's own cap on a laptop is MEASURED in `drawPasteAsk`, not guessed here. */
  .seed-pask__row--all { border-top: 0; border-bottom: 1px solid var(--border-default); }
  .seed-pask__row--all .seed-pask__q { color: var(--text-tertiary); font-size: 12.5px; }
  .seed-pask__list { overscroll-behavior: contain; }
  /* ── v419 ⑤ — the span we could not read. HONEST, tappable, and it names its months. It is a load
     failure, so it wears the app's own read-failure line rather than an alarm colour of its own. */
  .seed-spanfail { margin: 12px 0 0; padding: 12px 14px; border-radius: 12px;
    border: 1px solid var(--border-default); background: var(--bg-elevated); }
  .seed-spanfail__l { margin: 0; }
  .seed-spanfail__h { margin: 6px 0 0; font-size: 12px; line-height: 1.45; }
  /* the paste's quiet trailing lines: why a month ended with nothing new, and the way back into a
     collision ask that was closed with answers still open. */
  .seed-notes { margin: 4px 0 0; }
  .seed-note--m { margin: 3px 0 0; }
  .seed-note--reopen { margin: 5px 0 0; font-size: 12px; }
  /* the celebration — a milestone, so GOLD (§3), and it is the only gold on this screen */
  .seed-done { margin-top: 16px; text-align: center; border-color: var(--accent-gold); }
  .seed-done__t { font-size: 17px; margin: 0 0 6px; color: var(--accent-gold); }
  .seed-done__b { margin: 0 0 14px; font-size: 13px; color: var(--text-secondary); }
  .seed-done__act { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
  @media (max-width: 700px) {
    /* THE PHONE IS THE SAME SCREEN, NARROWER — one page per month reads identically at 390px, which is the
       whole point of the redesign. The only change is the commit dock, which bleeds to the card's own edges
       so its top rule spans the width instead of floating inside a gutter. */
    .seed-book { max-width: none; }
    .seed-in { flex: 0 0 122px; }
    .seed-pin { background: var(--bg-base); margin: 14px -20px 0;
      padding: 8px 20px calc(10px + env(safe-area-inset-bottom, 0px)); }
  }

  /* ══ v414 — THE PASTE DOOR + THE SHARED PER-NAME MAPPING CARD ═════════════════════════════════════
     One reading column at every width, exactly like the book it feeds: this is a short, sequential
     question-and-answer flow, and a two-column workspace would only stretch the gap between a label and
     the thing it labels. Nothing here is a new colour — purple for the act, teal for what was read,
     gold for what was left out (gold is the "look at this" of §3; amber would read as alarm, and a
     negative cell left out is not an alarm). */
  .seed-pastelink { display: inline-block; margin: 0 0 12px; font-size: 13px; }
  .seed-paste { max-width: 520px; margin: 0 auto; }
  .paste-t { font-size: 16px; margin: 4px 0 4px; }
  .paste-sub { margin: 0 0 10px; font-size: 13px; line-height: 1.5; }
  .paste-tip { margin: 0 0 12px; font-size: 12.5px; line-height: 1.5; }
  /* THE BOX. This is the app's FIRST <textarea>, and the global `select, input` rule does not reach it —
     so it dresses itself from the same tokens rather than inheriting a browser default (the standing
     "a control the browser draws is a control that matches nothing else" rule). DM Mono, because a
     pasted block of cells is a grid and a proportional face makes its columns lie. */
  .paste-box { width: 100%; min-height: 132px; resize: vertical; box-sizing: border-box;
    background: var(--bg-input); color: var(--text-primary);
    border: 1px solid var(--border-default); border-radius: 10px; padding: 10px 12px;
    font-family: 'DM Mono', ui-monospace, monospace; font-size: 12.5px; line-height: 1.5; }
  .paste-box:focus { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-glow); }
  .paste-box::placeholder { color: var(--text-tertiary); }
  .paste-err { margin: 8px 0 0; font-size: 12.5px; color: var(--accent-warm); }
  .paste-acts { display: flex; gap: 9px; flex-wrap: wrap; margin-top: 12px; }
  .paste-calm { margin: 10px 0 0; font-size: 12px; line-height: 1.5; }
  /* the picture redirect — calm, and it POINTS somewhere; a refusal with no way forward is a dead end */
  .paste-pic { margin: 0 0 12px; padding: 11px 13px; border: 1px solid var(--border-default);
    border-radius: 11px; background: var(--bg-elevated); }
  .paste-pic__t { margin: 0; font-size: 13px; line-height: 1.5; color: var(--text-secondary); }
  .paste-pic .paste-acts { margin-top: 10px; }
  /* what it read — teal, because this is the half that arrived */
  .paste-read { margin: 4px 0 10px; padding: 10px 13px; border-radius: 11px;
    border: 1px solid var(--border-subtle); background: var(--bg-elevated); }
  .paste-read__l { margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--accent-teal); }
  .paste-read__l + .paste-read__l { margin-top: 2px; }
  .paste-read__n { margin: 5px 0 0; font-size: 12.5px; line-height: 1.5; }
  /* what was left out — each line NAMES what it left */
  .paste-outs { margin: 0 0 10px; }
  .paste-out { margin: 0 0 5px; padding: 7px 11px; font-size: 12.5px; line-height: 1.45;
    color: var(--text-secondary); border-left: 2px solid var(--accent-gold);
    background: var(--bg-elevated); border-radius: 0 8px 8px 0; overflow-wrap: break-word; }
  .paste-flip { margin: 0 0 4px; padding: 9px 12px; font-size: 13px; line-height: 1.45;
    color: var(--text-secondary); border: 1px solid var(--border-default); border-radius: 11px; }
  .paste-maphost { margin: 4px 0 0; }
  .paste-maphint { margin: 8px 0 0; font-size: 12.5px; }

  /* the shared mapping card (web/js/mapcard.js) — the paste door is its first caller, the migration
     door will be its second, which is why nothing in here is named for pasting. */
  .mapcard { margin: 0; }
  .mapcard__row { padding: 10px 0; border-bottom: 1px solid var(--border-subtle); }
  .mapcard__n { font-size: 14.5px; font-weight: 600; color: var(--text-primary); overflow-wrap: break-word; }
  .mapcard__ok { display: flex; align-items: baseline; gap: 6px; margin-top: 2px;
    font-size: 13px; color: var(--accent-teal); }
  .mapcard__tick { flex: 0 0 auto; font-size: 11px; }
  /* ── v419 ① — ONE ROW PER NAME: the name, the app's own category picker, and the way out ──────────
     The v414 shape printed the WHOLE category tree as chips under EVERY unanswered name. It wrapped
     honestly and it was unusable at ten unmatched names — the answer you wanted was somewhere inside a
     kilometre of pills. A `catPicker` holds the same groups, in the same order, in one line you can
     type into. On a laptop the name sits beside the control; at phone width it stacks above it. */
  .mapcard__pick { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 6px; }
  .mapcard__cat { flex: 1 1 220px; min-width: 0; }
  /* v420 ④ — one size smaller. Their review of the v419 card: *"a little bit big… not the biggest deal"*.
     The picker is a question on a card, not the screen's main field, so it steps down to the register the
     door's own buttons took in v419 — 14px, a shorter box, a narrower chevron lane. Scoped to this card
     ONLY: `catPicker` is shared with the ledger edit form and the CSV board, where it IS the main field. */
  .mapcard__cat .catpick__input { font-size: 14px; padding-top: 8px; padding-bottom: 8px; padding-right: 32px; }
  .mapcard__cat .catpick__chev { width: 30px; }
  /* "Leave this row out" is quiet and it is NEVER inside the picker: a list of destinations must only
     hold destinations, or the escape hatch is the thing somebody scrolls past looking for a category. */
  .mapcard__out { flex: 0 0 auto; appearance: none; background: none; border: 1px solid transparent;
    border-radius: 999px; color: var(--text-tertiary); font: inherit; font-size: 12.5px;
    font-style: italic; padding: 6px 10px; cursor: pointer; }
  .mapcard__out:hover { color: var(--text-secondary); border-color: var(--border-default); }
  .mapcard__out:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-glow); }
  .mapcard__out--on { background: var(--accent-glow); border-color: var(--accent-primary);
    color: var(--accent-primary); font-style: normal; }
  /* ── v419 ② — two names landing in one category. Calm and INFORMATIONAL: this is a fact about what
     will happen, not a warning, so it wears no amber — a summed figure is the correct outcome and the
     only thing wrong with it would be nobody being told. */
  .mapcard__dups { margin: 10px 0 0; }
  .mapcard__dup { padding: 10px 12px; border-radius: 11px; border: 1px solid var(--border-default);
    background: var(--bg-elevated); }
  .mapcard__dup--ok { border-color: var(--accent-teal); }
  .mapcard__dupl { margin: 0 0 6px; font-size: 12.5px; line-height: 1.45; color: var(--text-secondary); }
  .mapcard__dupack { appearance: none; background: none; border: 1px solid var(--border-default);
    border-radius: 999px; color: var(--text-secondary); font: inherit; font-size: 12.5px;
    padding: 6px 13px; cursor: pointer; }
  .mapcard__dupack:hover { color: var(--text-primary); border-color: var(--accent-primary); }
  .mapcard__dupack:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-glow); }
  .mapcard__dupack--on { border-color: var(--accent-teal); color: var(--accent-teal); }
  @media (min-width: 900px) {
    /* left-aligned with the book, not centred — the card heading and the door chooser above it are
       left-aligned, and a centred column under them reads as a different screen. */
    .seed-paste { margin: 0; max-width: 560px; }
  }
  /* ── v419 ④ — THE PASTE PREVIEW, an aligned mini-grid ────────────────────────────────────────────
     A textarea renders a tab at whatever the next tab stop happens to be, so a spreadsheet copy read as
     a ragged staircase and there was no way to see, by eye, whether the block copied was the block
     meant. The door already parses the text exactly, so it shows the cells it found. Tabular numerals
     so the columns line up; the month row and the name column muted so the shape of the sheet is
     legible at a glance; and its OWN horizontal scroller, because a five-year sheet is sixty columns
     wide and the page itself must never scroll sideways. */
  /* the box collapses once it holds something — the grid below is the preview now, and two renderings
     of the same cells (one of them ragged) is exactly the thing being fixed. */
  /* `height` as well as `min-height`: the box's drawn height comes from its `rows` attribute, which
     min-height alone cannot pull DOWN. `resize: vertical` still lets a reader drag it back open. */
  .paste-box--filled { min-height: 64px; height: 64px; }
  .paste-prev { margin: 10px 0 0; }
  .paste-prev__t { margin: 0 0 5px; font-size: 12px; }
  .paste-prev__scroll { overflow-x: auto; max-width: 100%; border: 1px solid var(--border-subtle);
    border-radius: 9px; background: var(--bg-elevated); }
  .paste-prev__tbl { border-collapse: collapse; font-size: 12px; font-variant-numeric: tabular-nums; }
  .paste-prev__c { padding: 4px 9px; white-space: nowrap; color: var(--text-primary);
    border-bottom: 1px solid var(--border-subtle); border-right: 1px solid var(--border-subtle); text-align: right; }
  .paste-prev__c--h { color: var(--text-tertiary); text-align: left; }
  .paste-prev__tbl tr:last-child .paste-prev__c { border-bottom: 0; }
  .paste-prev__more { margin: 5px 0 0; font-size: 11.5px; }
  @media (max-width: 700px) {
    .seed-paste { max-width: none; }
    .paste-box { min-height: 120px; font-size: 12px; }
    .paste-box--filled { min-height: 56px; height: 56px; }
    .paste-acts .btn { flex: 1 1 auto; }
    .paste-prev__c { padding: 4px 7px; }
    /* the name column stacks above its picker — 220px of control and a long category name will not
       share a 390px line without one of them being squeezed to nothing. */
    .mapcard__cat { flex: 1 1 100%; }
  }

  /* ── MODE 5 (§11.4b): screenshot import ── */
  /* the source segmented control (CSV file | Screenshots | Past months) */
  .csv-srcseg { display: inline-flex; margin: 4px 0 14px; border: 1px solid var(--border-default); border-radius: 9px; overflow: hidden; }
  .csv-srcseg__b { appearance: none; border: 0; background: transparent; color: var(--text-tertiary);
    padding: 8px 16px; font: inherit; font-size: 13px; cursor: pointer; transition: background .15s ease, color .15s ease; }
  .csv-srcseg__b + .csv-srcseg__b { border-left: 1px solid var(--border-default); }
  .csv-srcseg__b:hover { color: var(--text-primary); }
  .csv-srcseg__b--on { background: var(--accent-glow); color: var(--accent-primary); font-weight: 600; }
  /* v402 — a THIRD door means three labels on one line; at phone width they wrap rather than run off the
     screen (the page itself must never scroll sideways). The chooser stays one control either way. */
  @media (max-width: 700px) {
    .csv-srcseg { display: flex; flex-wrap: wrap; max-width: 100%; }
    .csv-srcseg__b { flex: 1 1 auto; padding: 8px 12px; }
  }
  /* v415 ③ — "an import is in progress under {name}". INFORMATIONAL, so it wears nothing: no colour, no
     border, no icon. It is not a warning (nothing is wrong), not an action (the only move is to switch
     profile, which is a control they already have), and not an alarm (the work is safe). One quiet line
     above the door, sized so it never competes with the door's own title. */
  .csv-othercx { margin: 0 0 14px; font-size: 13px; line-height: 1.45; }
  /* the privacy note on the door — calm, purple-tinged (a trust/human moment, Bible §3) */
  .shot-privacy { margin-top: 10px; padding: 12px 14px; border-radius: 12px; background: var(--accent-glow);
    border: 1px solid var(--border-subtle); display: flex; flex-direction: column; gap: 6px; }
  .shot-privacy__line { font-size: 13px; line-height: 1.45; }
  .shot-drop__icon, .shot-drop .csv-drop__icon { color: var(--accent-primary); }
  .shot-picked { margin-top: 12px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
  .shot-picked__n { font-size: 13px; font-weight: 600; }
  .shot-letgo { margin-top: 4px; }
  /* per-image OCR progress — the SHARED fading dots (larger, centred) + "reading N of M". Coherent with every
     other loading surface in the app; the dots + reduced-motion behaviour come from .hload__spin. */
  .shot-progress { margin-top: 18px; display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 22px; }
  .shot-progress__dots i { width: 9px; height: 9px; }
  .shot-progress__txt { font-size: 14px; color: var(--text-secondary); }
  /* review-row flags: a pending pill (neutral/amber status) + an "uncertain amount" pill (amber = needs-a-look) */
  .csv-shotpill { display: inline-block; padding: 1px 7px; border-radius: 999px; font-size: 11px; font-weight: 600;
    white-space: nowrap; border: 1px solid transparent; }
  .csv-shotpill--pending { color: var(--text-tertiary); background: var(--bg-hover); border-color: var(--border-default); }
  .csv-shotpill--check { color: var(--accent-warm); background: rgba(232,131,74,.12); border-color: rgba(232,131,74,.35); }
  /* an OPEN QUESTION (which way? / which day?) — amber is the "needs a fix" register, never an alarm */
  .csv-shotpill--ask { color: var(--accent-warm); background: rgba(232,131,74,.12); border-color: rgba(232,131,74,.35); }
  /* the two partial CAUSES read as two calm sentences inside one box, never one merged claim */
  .shot-partials .shot-partial + .shot-partial { margin-top: 6px; }

  .csv-map { display: flex; flex-direction: column; gap: 10px; margin-top: 14px; }
  .csv-map__row { display: flex; align-items: center; gap: 12px; }
  .csv-map__row--ask { background: rgba(232,131,74,.07); border-radius: 10px; padding: 6px 8px; margin: -6px -8px; }
  .csv-map__lbl { flex: 0 0 200px; display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 14px; }
  .csv-map__row .dropdown { flex: 1; min-width: 0; }
  .csv-ask { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em;
    color: var(--accent-amber); background: rgba(232,131,74,.14); border-radius: 999px; padding: 2px 7px; }
  .csv-ok { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em;
    color: var(--accent-teal); background: rgba(58,191,176,.12); border-radius: 999px; padding: 2px 7px; }

  .csv-tablewrap { margin-top: 14px; overflow-x: auto; border: 1px solid var(--border-subtle); border-radius: 12px; }
  .csv-table { width: 100%; border-collapse: collapse; font-size: 13px; }
  .csv-table th { text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .04em;
    color: var(--text-tertiary); padding: 8px 12px; border-bottom: 1px solid var(--border-subtle); white-space: nowrap; }
  .csv-table td { padding: 7px 12px; border-bottom: 1px solid var(--border-subtle); vertical-align: top; }
  .csv-table tr:last-child td { border-bottom: none; }
  .csv-td--desc { color: var(--text-secondary); max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .csv-td--amt { text-align: right; white-space: nowrap; }
  .csv-row--bad td { background: rgba(232,131,74,.08); color: var(--text-tertiary); }
  .csv-more { padding: 8px 12px; font-size: 12px; }

  .csv-warn { margin-top: 12px; font-size: 13px; background: rgba(232,131,74,.09);
    border: 1px solid rgba(232,131,74,.3); border-radius: 10px; padding: 9px 12px; }
  /* sign-MEANING control: a calm, brand-accented notice (not amber/teal — it's neutral, not a spend/income cue) */
  .csv-flip { margin-top: 14px; display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
    background: var(--accent-glow); border: 1px solid var(--border-strong); border-radius: 12px; padding: 12px 14px; }
  .csv-flip__info { flex: 1 1 240px; min-width: 0; }
  .csv-flip__lbl { font-size: 12px; font-weight: 600; letter-spacing: .02em; color: var(--text-secondary);
    text-transform: uppercase; margin-bottom: 4px; }
  .csv-flip__reading { font-size: 14px; color: var(--text-primary); line-height: 1.45; }
  .csv-flip__counts { font-size: 12px; margin-top: 4px; }
  .csv-flip__btn { flex: 0 0 auto; }
  .csv-ccy__combo { flex: 0 0 230px; max-width: 100%; }
  .csv-actions { display: flex; align-items: center; gap: 12px; margin-top: 16px; flex-wrap: wrap; }
  .csv-soon { font-size: 12px; }

  /* ── CSV review (slice b) ── */
  /* top header: counter on the left, an import button on the right so you can commit what's ready without
     scrolling to the footer (it's in both places). Wraps on a phone. */
  .csv-tophead { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin: 6px 0 14px; }
  .csv-tophead__left { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
  .csv-tophead .csv-counter { margin: 0; }
  .csv-counter { display: flex; align-items: baseline; gap: 10px; margin: 6px 0 14px; }
  .csv-counter__big { font-size: 28px; font-weight: 500; color: var(--accent-primary); font-family: 'DM Mono', monospace; } /* DM Mono's real heaviest is 500 (700 faux-bolds = blurry) */
  .csv-counter__lbl { font-size: 13px; }
  .csv-bulkbar { position: sticky; top: 6px; z-index: 4; display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    background: var(--accent-primary); color: #fff; border-radius: 12px; padding: 8px 12px; margin-bottom: 14px; box-shadow: 0 6px 18px rgba(0,0,0,.25); }
  .csv-bulkbar__n { font-weight: 600; margin-right: 4px; }
  .csv-bulkbar .btn { background: rgba(255,255,255,.16); border-color: transparent; color: #fff; }
  .csv-bulkbar .btn--warm { background: var(--accent-amber); }
  /* the import-selected button stands out on the purple bar: solid white pill, purple text (the primary action) */
  .csv-bulkbar .csv-bulkbar__go { background: #fff; color: var(--accent-primary); font-weight: 700; }
  .csv-bulkbar .csv-bulkbar__go:disabled { background: rgba(255,255,255,.4); color: rgba(124,106,255,.6); }

  .csv-sect { margin-top: 16px; }
  .csv-sect__head { display: flex; align-items: center; gap: 10px; padding: 6px 0; border-bottom: 1.5px solid var(--border-subtle); }
  .csv-sect__t { font-weight: 600; }
  .csv-sect__n { font-size: 12px; color: var(--text-tertiary); background: var(--bg-hover); border-radius: 999px; padding: 1px 9px; }
  .csv-sect--transfer .csv-sect__t { color: var(--text-secondary); }
  .csv-sect__empty { padding: 10px 4px; font-size: 13px; }
  .csv-rows { display: flex; flex-direction: column; }

  /* a COMPACT two-line row (line 1 = select·date·raw·amount·skip; line 2 = merchant·category·tags) so a long
     statement stays scannable instead of scrolling forever. */
  .csv-r { display: flex; flex-direction: column; gap: 3px; padding: 6px 6px; border-bottom: 1px solid var(--border-subtle); }
  .csv-r:hover { background: var(--bg-hover); }
  .csv-r--sel { background: rgba(124,106,255,.08); }
  .csv-r--skip { opacity: .45; }
  .csv-r--skip .csv-r__raw, .csv-r--skip .csv-r__desc, .csv-r--skip .csv-r__merch { text-decoration: line-through; }
  .csv-r--sug { box-shadow: inset 3px 0 0 var(--accent-amber); }
  .csv-r__l1 { display: flex; align-items: center; gap: 8px; }
  .csv-r__l2 { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } /* merchant · category · tag icons */
  /* ⚠ THIS IS THE APP'S ONE AND ONLY CHECKBOX. `.hui-check` is the name to use in NEW code; `.csv-check` is
     the original name, kept because four poc/verify scripts select on it. They are the same control — never
     add a third.
     WHY IT IS CUSTOM-DRAWN, and why a native <input type=checkbox> is NOT an option here: a native box is
     drawn by the browser, so its corner radius, its size and its HOVER STATE all come from the UA stylesheet,
     not from our tokens — `accent-color` only tints it, and Chrome then derives its own darker shade on hover.
     The human caught exactly that twice: first as "the bright native white square, too much in the eye", and
     again on 2026-07-30 on the merchant-rename offer ("it changes the purple to a darker purple, which I
     don't see anywhere else in the application"). It is FAINT at rest (a subtle themed outline) with a clear
     purple fill + drawn tick when ticked, so a selection still reads.
     padding:0 + box-sizing:border-box are REQUIRED — the global `select, input { padding: 11px 12px }` rule
     was inflating this appearance:none box to ~26px no matter what width was set (the real "size won't change"
     bug). With padding reset, width finally takes effect: 12px ≈ half of what it had been rendering. */
  .hui-check, .csv-check { appearance: none; -webkit-appearance: none; box-sizing: border-box; width: 12px; height: 12px;
    padding: 0; margin-top: 4px; flex: 0 0 auto; cursor: pointer; border: 1px solid #3f3f5c; border-radius: 3px;
    background: transparent; display: inline-grid; place-content: center;
    transition: background .12s ease, border-color .12s ease; } /* #3f3f5c = a touch more visible than --border-default */
  .hui-check::after, .csv-check::after { content: ""; width: 3px; height: 6px; margin-top: -1px; border: solid #fff;
    border-width: 0 2px 2px 0; transform: rotate(45deg) scale(0); transform-origin: center; transition: transform .12s ease; }
  .hui-check:hover, .csv-check:hover { border-color: var(--text-tertiary); }
  .hui-check:checked, .csv-check:checked { background: var(--accent-primary); border-color: var(--accent-primary); }
  .hui-check:checked::after, .csv-check:checked::after { transform: rotate(45deg) scale(1); }
  .hui-check:focus-visible, .csv-check:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-glow); }
  .csv-r__date { flex: 0 0 auto; font-size: 12px; color: var(--text-tertiary); white-space: nowrap; }
  /* the raw bank line shares line 1 with the date — it takes the slack and ellipsises */
  .csv-r__raw { flex: 1 1 auto; min-width: 0; font-size: 11px; color: var(--text-tertiary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .csv-r__desc { flex: 1 1 auto; min-width: 0; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* editable clean merchant (what gets stored & learned) — looks like text, reveals as an input on hover/focus.
     A FIXED basis (not flex-grow) keeps the category dropdown's left edge aligned down the column, instead of
     drifting right on rows with a short merchant (the "Pay / Uber" misalignment the human caught). */
  .csv-r__merch, .csv-r__desc { flex: 0 0 120px; min-width: 0; }
  .csv-r__merch { font-size: 13px; font-weight: 600; color: var(--text-primary);
    background: transparent; border: 1px solid transparent; border-radius: 8px; padding: 3px 7px; }
  .csv-r__merch:hover { border-color: var(--border-subtle); }
  .csv-r__merch:focus { outline: none; border-color: var(--accent-primary); background: var(--bg-hover); }
  .csv-r__desc { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 3px 0; }
  .csv-r__cat { flex: 1 1 0; display: flex; align-items: center; gap: 6px; min-width: 0; } /* basis 0 so it GROWS into the space left after the FIXED merchant + tags area (never claims content width → no spurious wrap); every dropdown ends up the same width down a column */
  .csv-r__cat .catpick { min-width: 0; width: 100%; }
  .csv-r__owner { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
  .csv-r__ownerlbl { font-size: 12px; }
  /* the tag chips = MONOCHROME icon buttons (currentColor SVG + tooltip). text-secondary at rest so they read
     clearly on the dark surface (the emoji set buried ↻/▦ into the background); purple fill when on. */
  /* a FIXED-width tag area (sized to hold the 4 expense icons on ONE row) so the category to its left is the
     same width on every row — a credit (1 icon) and an expense (4 icons) line up identically (the human's
     "choosers must match"). Icons wrap inside only in the rare 5-tag (2-member household) case. */
  .csv-tags { display: flex; gap: 2px; flex-wrap: wrap; align-items: center; flex: 0 0 100px; }
  .csv-tag { display: inline-flex; align-items: center; justify-content: center; color: var(--text-secondary);
    border: 1px solid var(--border-subtle); background: transparent; border-radius: 8px; padding: 2px; cursor: pointer; }
  .csv-tag svg { width: 16px; height: 16px; display: block; }
  .csv-tag:hover { color: var(--text-primary); border-color: var(--accent-primary); background: var(--bg-hover); }
  /* §schema LABEL tags on a CSV review row (Phase 5): the "＋ tags" reveal sits inline; the expanded chip input
     wraps to its OWN line within the (flex-wrap) l2 so it never disturbs the fixed merchant/category column widths. */
  .csv-r__addtags { flex: 0 0 auto; font-size: 12px; padding: 2px 0; }
  .csv-r__tags { flex: 1 1 100%; min-height: 36px; padding: 4px 6px; }
  .csv-r__tags .taginput__field { min-width: 70px; }
  .csv-tag--on { color: #fff; background: var(--accent-primary); border-color: var(--accent-primary); }
  .csv-r__amt { flex: 0 0 auto; text-align: right; white-space: nowrap; font-size: 13px; }
  .csv-r__amt--in { color: var(--accent-teal); }
  .csv-r__amt--ret { color: var(--accent-gold); } /* a refund = money returned (Bible §3) */
  .csv-r__skip { flex: 0 0 auto; background: transparent; border: none; color: var(--text-tertiary); font-size: 16px; cursor: pointer; padding: 0 4px; border-radius: 8px; line-height: 1; }
  .csv-r__skip:hover { background: var(--bg-hover); color: var(--accent-amber); }

  /* ── 3-column triage board (Sorted · To categorise · Transfers side by side on desktop) ──
     Columns use minmax(MIN, 1fr): they FILL the available width on a roomy screen but never shrink below MIN,
     and the board itself scrolls horizontally (overflow-x:auto = the safety net the human asked for) on a screen
     too small to hold the chosen column count — so a column can never silently run off the page again. It still
     collapses to 2 then 1 column at narrower viewports so scrolling is rarely needed. */
  .csv-cols { display: grid; grid-template-columns: 1fr; gap: 0 14px; align-items: start; }
  @media (min-width: 720px) { .csv-cols { grid-template-columns: repeat(2, minmax(300px, 1fr)); } }
  /* 3-up only when there's room; the board h-scrolls if a small screen can't hold three (the safety net). The
     overflow is scoped here (≥1180) so it never clips a category popup at narrower widths, where the per-column
     scroller below isn't active yet. */
  @media (min-width: 1180px) { .csv-cols { grid-template-columns: repeat(3, minmax(320px, 1fr)); overflow-x: auto; overflow-y: hidden; padding-bottom: 2px; } }
  .csv-cols .csv-sect { margin-top: 14px; min-width: 0; }
  .csv-cols .csv-r { padding: 6px 4px; }
  /* on a desktop board each column scrolls on its own with a sticky label, so all three stay in view at once */
  @media (min-width: 1080px) {
    .csv-cols .csv-sect__head { position: sticky; top: 0; background: var(--bg-surface); z-index: 2; }
    .csv-cols .csv-rows { max-height: calc(100vh - 300px); overflow-y: auto; overflow-x: hidden; }
  }
  /* FAINT, thin column scrollbars: invisible at rest, a faint thin thumb appears only while the cursor is over
     the column (i.e. while you're scrolling it) — never the chunky default bar. (The human: "too much to look
     at … should pretty much be invisible.") The 2px transparent border + content-box clip thins the thumb. */
  .csv-cols .csv-rows { scrollbar-width: thin; scrollbar-color: transparent transparent; }
  .csv-cols .csv-rows:hover { scrollbar-color: var(--border-default) transparent; }
  .csv-cols .csv-rows::-webkit-scrollbar { width: 8px; }
  .csv-cols .csv-rows::-webkit-scrollbar-track { background: transparent; }
  .csv-cols .csv-rows::-webkit-scrollbar-thumb { background: transparent; border-radius: 999px; border: 2px solid transparent; background-clip: content-box; }
  .csv-cols .csv-rows:hover::-webkit-scrollbar-thumb { background: var(--border-default); background-clip: content-box; }
  .csv-cols .csv-rows::-webkit-scrollbar-thumb:hover { background: var(--text-tertiary); background-clip: content-box; }

  /* ── CSV duplicates (slice c) ── */
  .csv-overlap { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; font-size: 13px;
    background: rgba(232,131,74,.10); border: 1px solid rgba(232,131,74,.3); border-radius: 10px; padding: 9px 12px; }
  .csv-rw { border-bottom: 1px solid var(--border-subtle); }
  .csv-rw .csv-r { border-bottom: none; }
  .csv-rw--dup { box-shadow: inset 3px 0 0 var(--accent-amber); background: rgba(232,131,74,.05); }
  .csv-r__descline { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
  .csv-duppill { font-size: 11px; border: 1px solid rgba(232,131,74,.45); background: rgba(232,131,74,.12); color: var(--accent-amber);
    border-radius: 999px; padding: 1px 9px; cursor: pointer; white-space: nowrap; }
  .csv-dup { margin: 0 6px 10px 34px; background: var(--bg-hover); border: 1px solid var(--border-subtle); border-radius: 10px; padding: 10px 12px; }
  .csv-dup__lead { font-size: 12px; color: var(--text-secondary); margin-bottom: 6px; }
  .csv-dup__match { display: flex; align-items: center; gap: 12px; font-size: 13px; padding: 6px 0; flex-wrap: wrap; }
  .csv-dup__desc { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .csv-dup__opts { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 6px; }
  .csv-res--on { outline: 2px solid var(--accent-primary); outline-offset: 1px; }
  .csv-dup__hint { font-size: 11px; margin-top: 8px; }

  /* ── review redesign: section tabs · merchant/category grouping · light category controls ──
     The two levers that kill the old "wall of boxes": FEWER pickers (group by merchant) and LIGHTER pickers
     (ghost "+ categorise" with no border at rest). Section tabs double as a focus control. */
  .csv-tabs { display: inline-flex; background: var(--bg-input); border: 1px solid var(--border-default);
    border-radius: 12px; padding: 3px; gap: 2px; margin: 4px 0 6px; max-width: 100%; overflow-x: auto; }
  .csv-tab { appearance: none; border: 0; background: transparent; color: var(--text-secondary); font: inherit;
    font-weight: 600; font-size: 13.5px; padding: 8px 14px; border-radius: 9px; cursor: pointer; white-space: nowrap;
    display: inline-flex; align-items: center; gap: 7px; }
  .csv-tab__n { font-size: 12px; color: var(--text-tertiary); background: var(--bg-hover); border-radius: 999px; padding: 1px 8px; }
  .csv-tab--on { background: var(--accent-primary); color: #fff; }
  .csv-tab--on .csv-tab__n { color: #fff; background: rgba(255,255,255,.22); }
  .csv-tab--auto.csv-tab--on { background: var(--accent-teal); color: #06231b; }    /* Sorted = teal (positive, done) */
  .csv-tab--auto.csv-tab--on .csv-tab__n { color: #06231b; background: rgba(0,0,0,.16); }

  .csv-toolbar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 16px 0 8px; }
  .csv-toolbar__lbl { font-size: 13px; }
  .csv-toolbar .dropdown { width: auto; min-width: 150px; }
  .csv-toolbar__meta { font-size: 13px; margin-left: 6px; }
  /* credits-only segmented filter — money-in reads teal, so the active segment tints teal */
  .csv-creditseg { display: inline-flex; border: 1px solid var(--border-default); border-radius: 8px; overflow: hidden; }
  .csv-creditseg__b { appearance: none; border: 0; background: transparent; color: var(--text-tertiary);
    font: inherit; font-size: 12px; padding: 6px 12px; cursor: pointer; }
  .csv-creditseg__b + .csv-creditseg__b { border-left: 1px solid var(--border-default); }
  .csv-creditseg__b:hover { color: var(--text-primary); }
  .csv-creditseg__b--on { background: rgba(62,207,170,.14); color: var(--accent-teal); font-weight: 600; }

  /* BOARD — 3-up overview; columns fill the width, collapse to 2 then 1. No per-column scroll (the page scrolls). */
  .csv-board { display: grid; grid-template-columns: 1fr; gap: 6px 26px; align-items: start; margin-top: 14px; }
  @media (min-width: 760px) { .csv-board { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
  @media (min-width: 1100px) { .csv-board { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
  .csv-bcol { min-width: 0; }
  .csv-bcol__head { display: flex; align-items: center; gap: 10px; padding: 4px 2px 8px; border-bottom: 1px solid var(--border-default); }
  .csv-bcol__t { font-family: 'Syne', sans-serif; font-weight: 700; font-size: 16px; }
  .csv-bcol--transfer .csv-bcol__t { color: var(--text-secondary); }
  .csv-focbtn { margin-left: auto; appearance: none; border: 1px solid var(--border-default); background: transparent;
    color: var(--text-tertiary); border-radius: 8px; padding: 4px 10px; font-size: 12px; cursor: pointer; }
  .csv-focbtn:hover { color: var(--text-primary); border-color: var(--border-strong); }

  /* FOCUS — one section full-width but capped for comfortable reading */
  .csv-focus { max-width: 880px; }

  /* the GROUP row — light + airy; one decision per merchant */
  .csv-grpwrap { border-bottom: 1px solid var(--border-subtle); }
  .csv-grp { display: flex; align-items: center; gap: 14px; padding: 15px 4px; }
  .csv-grp--compact { padding: 12px 2px; gap: 10px; }
  /* the group main area is tap-to-expand (mirrors the ledger row); padding+negative margin gives the hover a
     target without shifting the siblings (category control / chevron). */
  .csv-grp__main { display: flex; align-items: center; gap: 11px; min-width: 0; flex: 1; cursor: pointer;
    border-radius: 8px; padding: 4px 6px; margin: -4px -6px; }
  .csv-grp__main:hover { background: var(--bg-hover); }
  .csv-grp__main:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-glow); }
  .csv-grp__name { flex: 1 1 auto; min-width: 0; font-weight: 600; font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .csv-grp--compact .csv-grp__name { font-size: 14px; }
  .csv-chip { flex: none; font-size: 12px; color: var(--text-secondary); background: var(--bg-hover); border-radius: 999px; padding: 2px 9px; font-weight: 500; }
  .csv-grp__range { flex: none; font-size: 13px; }
  /* the board is the at-a-glance OVERVIEW — drop the amount range in its narrow columns so the merchant name
     keeps full width (amounts show in the focused single-column view). */
  .csv-grp--compact .csv-grp__range { display: none; }
  .csv-grp--in .csv-grp__range { color: var(--accent-teal); }            /* a credit group (refund/income in) reads teal */
  .csv-grp__dup { flex: none; font-size: 11px; color: var(--accent-amber); border: 1px solid rgba(232,131,74,.4);
    background: rgba(232,131,74,.1); border-radius: 999px; padding: 1px 8px; white-space: nowrap; }
  /* A GROUP THE READER LEFT OUT ENTIRELY (2026-07-28) — the group-header twin of .csv-r--skip, so a refused
     line reads the same whether it renders as a row or (single-row group) as only a header. MUTED, never
     alarm-coloured: this is "we didn't take it", not "something went wrong" — amber is reserved for
     destructive/feedback, and nothing here is either. */
  /* dim the NAME/COUNT/RANGE area only — opacity makes a stacking context, so dimming the whole header would
     drag the ↺ down with it and a child cannot climb back out. The one way back must stay fully legible. */
  .csv-grpwrap--out .csv-grp__main { opacity: .45; }
  .csv-grpwrap--out .csv-grp__name { text-decoration: line-through; }
  .csv-grp__reason { font-size: 12px; padding: 0 12px 8px; }
  .csv-grp__bringin { flex: none; appearance: none; border: 0; background: transparent; color: var(--text-tertiary);
    font: inherit; font-size: 15px; line-height: 1; cursor: pointer; padding: 6px 9px; border-radius: 8px; }
  .csv-grp__bringin:hover { background: var(--bg-hover); color: var(--accent-primary); }
  /* ghost category control: NO border at rest (the white space), becomes a real picker on engage */
  .csv-ghostcat { flex: none; appearance: none; border: 1px dashed transparent; background: transparent;
    color: var(--accent-primary); font: inherit; font-weight: 600; font-size: 14px; cursor: pointer; padding: 6px 10px; border-radius: 9px; }
  .csv-ghostcat:hover { border-color: var(--border-strong); background: var(--bg-elevated); }
  .csv-catset { flex: none; appearance: none; border: 1px solid var(--border-default); background: var(--bg-elevated);
    color: var(--text-primary); font: inherit; font-weight: 600; font-size: 14px; cursor: pointer; padding: 6px 12px;
    border-radius: 9px; display: inline-flex; align-items: center; gap: 7px; max-width: 220px; }
  .csv-catset__lbl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .csv-catset--various { color: var(--text-tertiary); font-weight: 500; }
  /* PARTIAL consensus — a leaning category with rows still blank; dashed (= not finished) + a purple "N to go"
     count signalling one tap fills the rest (mirrors the dashed ＋categorise ghost's actionable language). */
  .csv-catset--part { border-style: dashed; border-color: var(--border-strong); }
  .csv-catset__more { flex: none; color: var(--accent-primary); font-weight: 600; font-size: 12px; }
  .csv-tick { flex: none; color: var(--accent-teal); font-weight: 800; }
  .csv-grp .catpick { flex: none; width: 240px; max-width: 100%; }      /* the inline picker while choosing */
  .csv-grp--compact .catpick { width: 100%; }
  /* per-group one-click Import — shows once the whole group is ready; compact so a column of them stays calm */
  .csv-grpimport { flex: none; padding: 6px 14px; font-size: 13px; white-space: nowrap; }
  .csv-exp { flex: none; appearance: none; border: 0; background: transparent; color: var(--text-tertiary);
    cursor: pointer; font-size: 15px; padding: 6px; border-radius: 8px; width: 30px; line-height: 1; }
  .csv-exp:hover { background: var(--bg-hover); color: var(--text-primary); }
  /* expanded individual rows under a group (rowEl) — slightly inset + a calm background */
  .csv-sub { padding: 2px 0 10px 10px; }
  .csv-sub .csv-rw { border-bottom: 1px solid var(--border-subtle); }
  .csv-sub .csv-rw:last-child { border-bottom: none; }
  .csv-grphint { font-size: 13px; margin-top: 16px; }

  /* group merchant rename (one-shot, focus view) — the ✎ pencil beside the name + the inline rename box */
  .csv-grp__editbtn { flex: none; appearance: none; border: 0; background: transparent; color: var(--text-tertiary);
    cursor: pointer; font-size: 14px; padding: 6px 8px; border-radius: 8px; line-height: 1; }
  .csv-grp__editbtn:hover { background: var(--bg-hover); color: var(--accent-primary); }
  .csv-grp__rename { flex: 1 1 auto; display: flex; align-items: center; gap: 10px; min-width: 0; }
  .csv-grp__renameinput { flex: 1 1 auto; min-width: 0; appearance: none; font: inherit; font-weight: 600; font-size: 15px;
    color: var(--text-primary); background: var(--bg-elevated); border: 1px solid var(--accent-primary); border-radius: 9px; padding: 7px 11px; }
  .csv-grp__renameinput:focus { outline: none; box-shadow: 0 0 0 3px var(--accent-glow); }
  .csv-grp__renamehint { flex: none; font-size: 12px; white-space: nowrap; }

  /* duplicate review overlay — the "check each" gather-all (every flagged dup in one place) */
  .csv-dupreview__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; font-size: 13px; }
  .csv-dupreview__list { display: flex; flex-direction: column; gap: 12px; }
  .csv-dupreview__empty { font-size: 14px; }
  .csv-dupr { border: 1px solid var(--border-default); border-radius: 12px; padding: 11px 12px; background: rgba(232,131,74,.06); }
  .csv-dupr__row { display: flex; align-items: baseline; gap: 9px; font-size: 13px; }
  .csv-dupr__row--match { margin-top: 4px; opacity: .85; }
  .csv-dupr__tag { flex: none; width: 62px; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; }
  .csv-dupr__date { flex: none; }
  .csv-dupr__desc { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .csv-dupr__amt { flex: none; font-weight: 500; } /* DM Mono ships no 600 → 600 faux-bolds (smeary/"blurry"); 500 is its real heaviest */
  .csv-dupr__opts { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 10px; }
  .csv-overlap__txt { flex: 1 1 auto; }

  /* ── the one-time PENDING ask (§11.4b) — calm amber "status feedback", never an alarm ── */
  .csv-pendask { margin: 12px 0 4px; padding: 12px 14px; border-radius: 12px;
    background: rgba(232, 131, 74, 0.08); border: 1px solid rgba(232, 131, 74, 0.3); }
  .csv-pendask__txt { font-weight: 600; font-size: 14px; }
  .csv-pendask__sub { font-size: 12.5px; margin-top: 4px; }
  .csv-pendask__acts { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }

  /* ── THE OPEN QUESTIONS on a screenshot row (§11.4b, 2026-07-26) — same calm amber register as the
     pending ask: this needs a small fix, it is not an alarm. Rendered by ONE builder (csv.js askStrip)
     on every surface, so the desktop row and the phone row can never disagree. ── */
  .csv-ask2 { margin-top: 9px; display: flex; flex-direction: column; gap: 10px; }
  .csv-ask2__q { padding: 9px 11px; border-radius: 10px;
    background: rgba(232, 131, 74, 0.07); border: 1px solid rgba(232, 131, 74, 0.28); }
  .csv-ask2__t { font-size: 13px; font-weight: 600; }
  .csv-ask2__why { font-size: 12px; margin-top: 3px; line-height: 1.45; }
  /* the direction answer. While UNANSWERED neither side is filled — that is what makes it read as a
     question rather than a decided default. Once chosen, the side adopts its MEANING colour
     (teal = money in · --accent-out = money out); no new hues. */
  .csv-dirseg { display: flex; gap: 0; margin-top: 9px; border: 1px solid var(--border-default);
    border-radius: 10px; overflow: hidden; max-width: 320px; }
  .csv-dirseg__b { flex: 1 1 0; min-width: 0; appearance: none; border: 0; background: var(--bg-surface);
    color: var(--text-secondary); font-family: inherit; font-size: 13px; font-weight: 600;
    padding: 9px 10px; cursor: pointer; }
  .csv-dirseg__b + .csv-dirseg__b { border-left: 1px solid var(--border-default); }
  .csv-dirseg__b:active { background: var(--bg-hover); }
  .csv-dirseg__b--on.csv-dirseg__b--out { background: var(--accent-out); color: #14121a; }
  .csv-dirseg__b--on.csv-dirseg__b--in { background: var(--accent-teal); color: #08201d; }
  .csv-dateask { display: flex; align-items: center; gap: 8px; margin-top: 9px; flex-wrap: wrap; }
  .csv-dateask .datepick { flex: 1 1 150px; min-width: 0; }
  /* the AMOUNT ask (2026-07-27). The standing money-input shape: the currency symbol is a prefix INSIDE the
     box and the box carries the border + focus ring, so it never reads as a bare number field. The input is
     appearance:none, so it must reset the global `input` padding it would otherwise inherit (the documented
     custom-control gotcha) — without padding:0 the field inflates well past its set height. */
  .csv-amtbox { flex: 1 1 150px; min-width: 0; display: flex; align-items: center; gap: 2px;
    border: 1px solid var(--border-default); border-radius: 9px; padding: 0 10px; background: var(--bg-base); }
  .csv-amtbox:focus-within { border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-glow); }
  .csv-amtbox__sym { flex: none; font-size: 13px; }
  .csv-amtbox__in { flex: 1 1 auto; min-width: 0; appearance: none; -webkit-appearance: none; border: 0;
    background: none; padding: 10px 0; margin: 0; box-sizing: border-box; font-family: var(--font-mono);
    font-size: 14px; color: var(--text-primary); }
  .csv-amtbox__in:focus { outline: none; box-shadow: none; }
  /* the remembered-pending line: one quiet row + a link, never a card competing with the real work */
  .csv-pendnote { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
    font-size: 12.5px; padding: 8px 2px 2px; }
  .csv-pendnote__chg { appearance: none; border: 0; background: none; padding: 0; cursor: pointer;
    font-family: inherit; font-size: 12.5px; color: var(--accent-primary); text-decoration: underline; }
  .csv-pendnote__chg:hover { opacity: .85; }
  /* An amount whose DIRECTION is still a question wears neither meaning colour and carries no sign.
     TWO classes deliberately: .csv-ph__amt / .csv-r__amt set their own colour and are defined LATER in this
     file, so a single-class modifier would lose on source order (the standing "a modifier must win the
     cascade" rule — caught by eyeballing the 390px screenshot, not by the class-name assertion). */
  .csv-r__amt.csv-amt--unknown, .csv-ph__amt.csv-amt--unknown { color: var(--text-tertiary); }
  .csv-counter__ask { font-size: 13px; color: var(--accent-warm); }
  .csv-r__reason { font-size: 12px; margin-top: 6px; }
  /* a line the READER left out: why + back in one tap, as a sibling strip under the row */
  /* mirrors .csv-ph__kids (an attached sub-panel under its row), so the two read as one family */
  .csv-ph__outrow { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    padding: 9px 4px 10px 26px; background: rgba(255,255,255,0.014);
    border-bottom: 1px solid var(--border-subtle); }
  .csv-ph__reason { flex: 1 1 180px; min-width: 0; font-size: 12px; line-height: 1.45; }
  .csv-ph__bring { flex: none; }

  /* ── PHONE REVIEW SURFACE (§11.4/§11.4b, 2026-07-25) — two zones over the same state.review ──
     Only ever rendered below the phone/short-landscape breakpoint (the JS gate mirrors this), so the
     desktop board's rules are untouched. */
  .csv-ph { margin-top: 6px; }
  .csv-ph__zone { display: flex; align-items: center; gap: 8px; padding: 16px 0 7px; }
  .csv-ph__zone h3 { margin: 0; font-size: 11px; text-transform: uppercase; letter-spacing: .055em;
    font-weight: 700; color: var(--text-tertiary); }
  .csv-ph__zone--needs h3 { color: var(--accent-primary); }
  .csv-ph__n { font-size: 11px; font-weight: 700; color: var(--text-secondary); background: var(--bg-hover);
    padding: 1.5px 7px; border-radius: 6px; }
  .csv-ph__rule { flex: 1 1 auto; height: 1px; background: var(--border-subtle); }
  .csv-ph__hint { font-size: 12px; padding: 0 0 6px; }
  .csv-ph__empty { padding: 28px 4px; text-align: center; font-size: 13px; }

  .csv-ph__item { padding: 11px 0; border-bottom: 1px solid var(--border-subtle); }
  .csv-ph__l1 { display: flex; align-items: baseline; gap: 8px; }
  .csv-ph__nm { flex: 1 1 auto; min-width: 0; appearance: none; border: 0; background: none; padding: 0;
    font-family: inherit; font-size: 14.5px; font-weight: 600; color: var(--text-primary); text-align: left;
    cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .csv-ph__x { flex: none; font-size: 11px; font-weight: 700; color: var(--text-tertiary);
    background: var(--bg-hover); padding: 2px 7px; border-radius: 6px; }
  /* DM Mono ships no weight above 500 — 600 faux-bolds it (smeary). 500 is its real heaviest. */
  .csv-ph__amt { flex: none; font-size: 14px; font-weight: 500; color: var(--accent-out); }
  .csv-ph__amt--neutral { color: var(--text-secondary); }
  /* MEANING colour on the phone amount (Bible §3): a credit is TEAL, a refund GOLD. Same cascade trap as
     .csv-amt--unknown above — .csv-r__amt--in/--ret are single-class and defined ~230 lines EARLIER, so
     .csv-ph__amt (later, equal specificity) was winning on source order and every credit rendered
     --accent-out. At a glance money-in and money-out were indistinguishable on the review surface. */
  .csv-ph__amt.csv-r__amt--in { color: var(--accent-teal); }
  .csv-ph__amt.csv-r__amt--ret { color: var(--accent-gold); }
  .csv-ph__sub { font-size: 11.5px; margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .csv-ph__l2 { display: flex; align-items: center; gap: 7px; margin-top: 8px; }
  /* the category picker takes the WHOLE row — its popup is left:0/right:0 of this box, so a narrow parent
     is exactly what made it render as a tall narrow strip on a phone. */
  .csv-ph__cat { flex: 1 1 auto; min-width: 0; }
  .csv-ph__cat .catpick { width: 100%; min-width: 0; }
  .csv-ph__ico { flex: none; width: 38px; height: 38px; border-radius: 10px; border: 1px solid var(--border-subtle);
    background: transparent; color: var(--text-tertiary); cursor: pointer; font-size: 15px; line-height: 1;
    padding: 0; font-family: inherit; }
  .csv-ph__ico:active { background: var(--bg-hover); }
  .csv-ph__pills { display: flex; gap: 5px; flex-wrap: wrap; margin-top: 7px; }
  .csv-ph__pill { font-size: 10.5px; font-weight: 600; letter-spacing: .03em; text-transform: uppercase;
    padding: 2.5px 7px; border-radius: 6px; background: var(--bg-hover); color: var(--text-tertiary); }
  .csv-ph__pill--dup { background: rgba(232, 197, 71, 0.14); color: var(--accent-gold); }

  /* the concise ready line — C's shape, made tappable */
  .csv-ph__rdy { display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
    padding: 11px 0; border: 0; border-bottom: 1px solid var(--border-subtle); background: none;
    font-family: inherit; color: inherit; cursor: pointer; }
  .csv-ph__rdy:active { background: var(--bg-hover); }
  .csv-ph__rdy--out { opacity: .45; }
  .csv-ph__tick { flex: none; width: 16px; color: var(--accent-teal); font-size: 14px; }
  .csv-ph__rdy--out .csv-ph__tick { color: var(--text-tertiary); }
  .csv-ph__who { flex: 1 1 auto; min-width: 0; }
  .csv-ph__wtop { display: flex; align-items: baseline; gap: 7px; }
  .csv-ph__wname { font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; min-width: 0; }
  .csv-ph__wcat { font-size: 12.5px; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .csv-ph__chev { flex: none; color: var(--text-tertiary); font-size: 13px; }

  .csv-ph__kids { border-bottom: 1px solid var(--border-subtle); background: rgba(255,255,255,0.014); }
  .csv-ph__kid { display: flex; align-items: center; gap: 9px; padding: 9px 4px 9px 12px;
    border-top: 1px solid var(--border-subtle); font-size: 13px; }
  .csv-ph__kid--out { opacity: .45; }
  .csv-ph__kd { flex: none; font-size: 11px; color: var(--text-tertiary); }
  .csv-ph__kr { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
  .csv-ph__kid .csv-ph__ico { width: 32px; height: 32px; font-size: 13px; }

  /* ── the row EDITOR (inside HUI.modal) ── */
  .csv-ed__meta { font-size: 12.5px; margin: -2px 0 2px; }
  /* WHY a row is left out, in the editor a left-out row opens to. Muted, never alarm-coloured — "we didn't
     take it" is not an error (the same register .csv-r__reason / .csv-grp__reason use on the desktop board). */
  .csv-ed__why { font-size: 12.5px; line-height: 1.45; margin: 6px 0 2px; }
  .csv-ed__lbl { display: block; font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em;
    color: var(--text-tertiary); font-weight: 700; margin: 15px 0 6px; }
  .csv-ed__cat .catpick { width: 100%; min-width: 0; }
  .csv-ed__in { width: 100%; box-sizing: border-box; }
  .csv-ed__all { display: flex; align-items: center; gap: 9px; margin-top: 9px; font-size: 12.5px;
    color: var(--text-secondary); cursor: pointer; }
  .csv-ed__chips { display: flex; gap: 7px; flex-wrap: wrap; }
  .csv-ed__chip { display: inline-flex; align-items: center; gap: 7px; appearance: none; font-family: inherit;
    font-size: 12.5px; font-weight: 600; padding: 8px 12px; border-radius: 9px; cursor: pointer;
    border: 1px solid var(--border-default); background: var(--bg-surface); color: var(--text-secondary); }
  .csv-ed__chip svg { width: 15px; height: 15px; flex: none; }
  .csv-ed__chip--on { background: var(--accent-glow); border-color: var(--accent-primary); color: var(--text-primary); }
  .csv-ed__raw { font-size: 12px; background: var(--bg-surface); border: 1px solid var(--border-subtle);
    border-radius: 9px; padding: 10px 11px; word-break: break-word; }
  .csv-ed__foot { display: flex; gap: 9px; margin-top: 20px; }
  .csv-ed__foot .btn:last-child { flex: 1 1 auto; }

  @media (max-width: 640px) {
    .csv-map__row { flex-direction: column; align-items: stretch; gap: 6px; }
    .csv-map__lbl { flex: none; }
    .csv-dup { margin-left: 6px; }
    /* on a phone, drop line 2's indent so the merchant + category get the full row width (tags wrap beneath) */
    .csv-r__l2 { padding-left: 0; gap: 6px; }
    /* grouped review on a phone: let the group row wrap so the category control drops below the name (the name
       keeps priority), and the inline picker + focus column take the full width. */
    .csv-grp { flex-wrap: wrap; gap: 8px 10px; }
    .csv-grp__main { flex: 1 1 100%; }
    .csv-grp .catpick { width: 100%; }
    .csv-focus { max-width: 100%; }
    .csv-sub { padding-left: 4px; }
    /* preview/mapping table: keep ALL columns visible on a phone — esp. the AMOUNT (it was scrolling off the
       right edge inside the table's own scroller). Tighten padding + font and cap the description so the fixed
       date / type / amount columns always fit; a long raw bank line ellipsises instead of pushing them off. */
    .csv-table { font-size: 12px; }
    .csv-table th, .csv-table td { padding: 7px 8px; }
    .csv-td--desc { max-width: 92px; }
    /* the Import buttons (top header + footer) read outsized against the compact phone rows (human review
       of v310) — same words, one calm step down. */
    .csv-commitbtn { font-size: 13.5px; padding: 9px 14px; }
  }

  /* ── Intentions / Spending Plans (§4.14) — INLINE TREE editor: savings floor + the spending tree, each row a control ── */
  /* v406 — the head is ONE row, not a card: [Set|Track] · ⓘ · "For {name}" hard right. The title card and
     the standing intro line were retired (human-directed 2026-08-26) to give the screen its top back, so
     this row carries the whole head and the first real card starts immediately under it. */
  .intent-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
  .intent__forctx { font-size: 12px; color: var(--text-tertiary); margin: 0 0 0 auto; text-align: right; min-width: 0; overflow-wrap: anywhere; }
  /* a row = name/hint on the left, the [ccy|%] + amount control on the right; rows WRAP so a long name never
     crushes the control (and vice-versa) — the control simply drops onto its own line when there's no room. */
  .intent-srow, .intent-hrow, .intent-crow { display: flex; align-items: center; justify-content: space-between; gap: 10px 12px;
    flex-wrap: wrap; padding: 10px 2px; border-top: 1px solid var(--border-subtle); }
  .intent-goals__hdr { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; margin: 10px 2px 2px; }   /* "Saving toward" sub-heading */
  .intent-goalrow { border-top: 1px solid var(--border-subtle, var(--border-default)); padding-top: 8px; }
  /* "budget from reality" mirror (slice 5) */
  .intent-mirrorcta { display: flex; justify-content: flex-end; margin: 2px 2px 8px; }
  .intent-mirror__hdr { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 4px; }
  .intent-mirror__title { margin: 0; font-size: 1rem; }
  .intent-mirror__intro { font-size: 12px; margin: 0 0 10px; }
  .intent-mirror__kept { font-size: 13px; margin: 0 0 6px; }
  .intent-mirror__kept--ok { color: var(--accent-teal); }
  .intent-mirror__kept--over { color: var(--accent-warm); }
  .intent-mirror__room { font-size: 12px; color: var(--text-secondary); margin: 0 0 12px; }
  .intent-mirror__hrow { display: flex; justify-content: space-between; align-items: baseline; margin: 12px 0 4px; font-weight: 500; }
  .intent-mirror__hamt { font-variant-numeric: tabular-nums; color: var(--text-secondary); }
  .intent-mirror__crow { display: flex; align-items: center; gap: 10px; padding: 5px 2px 5px 12px; cursor: pointer; }
  .intent-mirror__cname { flex: 1 1 auto; }
  .intent-mirror__camt { font-variant-numeric: tabular-nums; color: var(--text-secondary); }
  .intent-mirror__bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 16px; }
  .intent-srow { border-top: none; padding: 4px 2px; }
  .intent-tree__hint { font-size: 12px; margin: 0 0 8px; }
  .intent-rowmain { min-width: 140px; flex: 1 1 auto; display: flex; align-items: center; gap: 8px; }
  .intent-rowmain--btn { background: none; border: none; padding: 6px 4px; margin: -6px -4px; text-align: left; cursor: pointer; color: inherit; border-radius: 8px; }
  .intent-rowmain--btn:hover { background: var(--bg-hover); }
  .intent-hrow .intent-rowname { font-size: 15px; color: var(--text-primary); font-weight: 600; }
  /* categories sit clearly INDENTED under their header (a bullet-style nesting cue, on top of the lighter font) */
  .intent-crow { padding-left: 34px; }
  .intent-crow .intent-rowname { font-size: 14px; color: var(--text-secondary); }
  .intent-rowname { overflow-wrap: anywhere; }                        /* wrap at spaces; break only a too-long single word */
  .intent-rowname--savings { font-size: 15px; color: var(--accent-teal); }
  .intent-rowhint { font-size: 12px; margin: 2px 0 0; }
  .intent-srow .intent-rowmain { flex-direction: column; align-items: flex-start; gap: 1px; }
  .intent-chev { display: inline-flex; color: var(--text-tertiary); transition: transform .15s ease; }
  .intent-chev--collapsed { transform: rotate(-90deg); }
  /* the control — keeps its content size; wraps onto its own line under a long name. max-width caps it so a
     long resolved/warning line WRAPS within the column instead of stretching the row off the card edge. */
  /* max-width holds the $/% toggle + amount + cadence chip on ONE line (resolved/warn/midwin still wrap below,
     they're flex-basis:100%). Widened from 230 so the compact cadence chip sits INLINE, not on its own row. */
  .intent-ctrl { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; flex-wrap: wrap; justify-content: flex-end; max-width: 272px; }
  /* the $/% toggle now lives INSIDE the amount field as a clickable symbol pill (the separate segment was removed
     to free space + a colour). Subtle at rest; purple on hover/focus = a tappable action. */
  .intent-money2 { display: flex; align-items: center; gap: 4px; border: 1px solid var(--border-default);
    border-radius: 9px; padding: 0 7px; background: var(--bg-surface); width: 116px; }
  .intent-money2:focus-within { border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-glow); }
  .intent-affix { flex: none; color: var(--text-tertiary); font-size: 13px; }
  .intent-affix--btn { cursor: pointer; border: none; background: var(--bg-elevated); color: var(--text-secondary);
    padding: 3px 6px; margin: 0; border-radius: 6px; font-size: 12px; font-weight: 500; line-height: 1; font-family: inherit; }
  .intent-affix--btn:hover { background: var(--accent-primary); color: var(--on-accent-primary); }
  .intent-affix--btn:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--accent-glow); }
  .intent-amt { flex: 1; min-width: 0; border: none; background: none; box-shadow: none; outline: none; padding: 9px 0; text-align: right; }
  .intent-amt:focus { border: none; box-shadow: none; outline: none; }
  .intent-resolved { font-size: 11px; color: var(--accent-teal); flex-basis: 100%; max-width: 100%; text-align: right; white-space: normal; overflow-wrap: anywhere; }
  .intent-resolved--needs { color: var(--accent-primary); }           /* calm purple nudge, never alarm */
  /* a HARD-block marker on a row (amber = needs a fix), + the input ring goes amber */
  .intent-warn { font-size: 11px; color: var(--accent-warm); flex-basis: 100%; max-width: 100%; text-align: right; white-space: normal; overflow-wrap: anywhere; }
  .intent-money2--bad { border-color: var(--accent-warm); }
  .intent-money2--bad:focus-within { border-color: var(--accent-warm); box-shadow: 0 0 0 3px rgba(232,168,80,.18); }
  /* PERIOD cadence chip (§4.14, Variant A) — a compact clock + letter; tap opens a small menu. Monthly reads muted,
     a longer cadence tints teal so it's visible at a glance. Sits INLINE with the $/% toggle + amount (no wrap). */
  /* a FIXED-WIDTH slot the cadence chip lives in — reserved on EVERY row (empty on savings/goals) so the amount
     boxes align in one clean column regardless of whether a row shows a chip or how wide the chip is. */
  .intent-cadslot { flex: 0 0 44px; display: flex; align-items: center; justify-content: center; }
  .intent-cad { display: inline-flex; align-items: center; gap: 3px; flex: 0 0 auto; border: 1px solid var(--border-default);
    background: var(--bg-surface); color: var(--text-tertiary); border-radius: 8px; padding: 5px 7px; line-height: 0; }
  .intent-cad svg { width: 15px; height: 15px; }
  .intent-cad__l { font-size: 11px; font-weight: 500; line-height: 1; font-family: 'DM Mono', ui-monospace, monospace; }
  .intent-cad:hover { border-color: var(--border-strong); color: var(--text-secondary); }
  .intent-cad--on { color: var(--accent-primary); border-color: color-mix(in srgb, var(--accent-primary) 50%, var(--border-default)); background: color-mix(in srgb, var(--accent-primary) 10%, var(--bg-surface)); }
  /* monthly = the ASSUMED default: a faint, icon-only clock (no letter) — a quiet "you could set a cadence here"
     affordance that doesn't repeat "Monthly" down every row. Brightens on hover. Non-monthly shows the teal chip. */
  .intent-cad--ghost { border-color: transparent; opacity: .4; }
  .intent-cad--ghost:hover { opacity: 1; border-color: var(--border-default); color: var(--text-secondary); }
  .intent-cad:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-glow); }
  /* the cadence menu (a small themed popover; position set inline by JS) */
  .intent-cadpop { position: fixed; z-index: 120; background: var(--bg-elevated); border: 1px solid var(--border-default);
    border-radius: 10px; box-shadow: 0 12px 32px rgba(0,0,0,.5); padding: 5px; min-width: 148px; }
  .intent-cadpop__opt { display: flex; align-items: center; gap: 6px; width: 100%; text-align: left; background: none;
    border: none; color: var(--text-secondary); font-size: 13px; padding: 8px 10px; border-radius: 7px; }
  .intent-cadpop__opt:hover { background: var(--bg-hover); color: var(--text-primary); }
  .intent-cadpop__opt--sel { color: var(--accent-primary); }
  .intent-cadpop__tick { width: 12px; flex: none; font-size: 12px; }
  /* calm, informational mid-window creation note (not an apology) */
  .intent-midwin { font-size: 11px; color: var(--text-tertiary); flex-basis: 100%; max-width: 100%; text-align: right; white-space: normal; overflow-wrap: anywhere; margin-top: 2px; }
  /* "already committed" line (§11/§28) — quiet info; below-plan variant is a calm purple nudge, NEVER amber
     (it's the user's call, not a mistake to fix) */
  .intent-committed { font-size: 11px; color: var(--text-tertiary); flex-basis: 100%; max-width: 100%; text-align: right; white-space: normal; overflow-wrap: anywhere; margin-top: 2px; }
  .intent-committed--below { color: var(--accent-primary); }
  /* R3 (2026-07-19): the cadence "working" ("($1,000 fortnightly)") is tappable — a calm dotted underline says
     "there's more here" without shouting; tapping opens the anchored per-frequency math panel (.ana-blocknote). */
  .intent-cadinfo { cursor: help; text-decoration: underline dotted; text-decoration-color: var(--border-strong); text-underline-offset: 2px; border-radius: 3px; }
  .intent-cadinfo:hover, .intent-cadinfo:focus-visible { color: var(--text-secondary); text-decoration-color: var(--text-secondary); outline: none; }
  .intent-cadinfo:focus-visible { background: var(--accent-glow); }
  .intent-committedsum { font-size: 12px; color: var(--text-tertiary); margin: 6px 2px 0; }
  /* the window label under a non-monthly Track row's name ("this quarter (Jul–Sep)") + the forecast-annotation seam */
  .intent-track__win { display: block; font-size: 11px; color: var(--text-tertiary); margin-top: 1px; font-weight: 400;
    overflow: hidden; text-overflow: ellipsis; }
  .intent-track__annot { font-size: 11px; color: var(--text-tertiary); margin-top: 4px; }
  /* ── v398 — the Savings row's Overall note (the shared wealth-change sentence) ──────────────────────
     Stacked, not inline: the head is the answer and the why is the explanation, and on a 390px row an
     inline pair would wrap into an unreadable run. The colour is on the HEAD's figure sentence only —
     the why line stays tertiary, because it is doing the reassuring, not the reporting (§3). */
  .intent-track__overall { display: flex; flex-direction: column; gap: 2px; margin-top: 5px; }
  .intent-track__overallhead { font-size: 12px; }
  .intent-track__overallhead--pos { color: var(--accent-teal); }
  .intent-track__overallhead--neg { color: var(--accent-red); }
  .intent-track__overallwhy { font-size: 11px; color: var(--text-tertiary); line-height: 1.45; }
  /* the sticky FOOTER — meter (or income ask) + the over-commit confirm + the save row, always visible */
  .intent-footer { position: sticky; bottom: 0; margin-top: 12px; padding: 10px 12px 12px;
    background: linear-gradient(to top, var(--bg-base) 78%, transparent); }
  .intent-savebar { display: flex; align-items: center; justify-content: flex-end; gap: 12px; margin-top: 8px; flex-wrap: wrap; }
  .intent-applyfrom { display: flex; align-items: center; gap: 8px; margin-right: auto; }
  .intent-applyfrom__lbl { font-size: 12px; color: var(--text-tertiary); }
  /* v406 rider — "a hair smaller" (human-directed 2026-08-26), a step DOWN from `.btn` (15px/12·20) but not
     all the way to `.btn--sm` (13px/8·12). Scoped to this bar so no other screen's primary action moves. */
  .intent-savebar__save { font-size: 14px; padding: 10px 16px; }
  .intent-savebar__count { font-size: 12px; color: var(--accent-primary); }
  .intent-savebar__count--bad { color: var(--accent-warm); }
  .intent-savebar__count--ok { color: var(--accent-teal); }
  /* ── Set-screen two-column workspace (Variant A) — tree left, "Your picture" card right on desktop ── */
  .intent-setmain { min-width: 0; }
  .intent-treehead { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; margin-bottom: 6px; }
  .intent-treehead__ttl { font-size: 15px; font-weight: 600; color: var(--text-primary); margin: 0; }
  /* the relocated tree hint lives behind this quiet ⓘ (Hearth.showAnchoredNote) — margin-right:auto keeps the
     "Set all" toggle hard right while the ⓘ sits with the title it explains. */
  .intent-infobtn { background: none; border: 1px solid var(--border-default); color: var(--text-tertiary);
    width: 22px; height: 22px; border-radius: 50%; font-size: 12px; line-height: 1; padding: 0; cursor: pointer;
    display: inline-flex; align-items: center; justify-content: center; margin-right: auto; flex: none; }
  .intent-infobtn:hover { color: var(--text-primary); border-color: var(--border-strong); }
  .intent-infobtn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-glow); }
  .intent-master { display: flex; align-items: center; gap: 8px; }
  .intent-master__lbl { font-size: 11px; color: var(--text-tertiary); text-transform: uppercase; letter-spacing: .05em; }
  .intent-master__seg { display: inline-flex; border: 1px solid var(--border-default); border-radius: 8px; overflow: hidden; }
  .intent-master__b { background: var(--bg-surface); border: none; color: var(--text-tertiary); font-size: 12px; padding: 6px 11px; line-height: 1; cursor: pointer; min-width: 30px; }
  .intent-master__b + .intent-master__b { border-left: 1px solid var(--border-default); }
  .intent-master__b:hover { color: var(--text-secondary); background: var(--bg-hover); }
  .intent-master__b--on, .intent-master__b--on:hover { background: var(--accent-primary); color: var(--on-accent-primary); }   /* the chosen "set all" side fills with the app accent */
  /* gentle "you'd overwrite N rows that already have a value" confirm, under the master toggle (never blocks) */
  .intent-masterconfirm { display: flex; align-items: center; justify-content: space-between; gap: 10px 14px; flex-wrap: wrap;
    margin: 4px 0 10px; padding: 10px 12px; border: 1px solid var(--border-default); border-radius: 10px; background: var(--bg-elevated); }
  .intent-masterconfirm__msg { font-size: 12.5px; color: var(--text-secondary); flex: 1 1 200px; min-width: 0; }
  .intent-masterconfirm__btns { display: flex; gap: 8px; flex: 0 0 auto; }
  .intent-sidetitle { display: none; }
  @media (min-width: 900px) {
    .intent-setlayout { display: grid; grid-template-columns: minmax(0,1fr) 340px; gap: 20px; align-items: start; }
    /* the footer becomes a sticky sidebar CARD (its phone form stays the bottom-pinned gradient bar below 900px) */
    .intent-footer { position: sticky; top: 16px; bottom: auto; margin-top: 0; padding: 16px; background: var(--bg-surface);
      border: 1px solid var(--border-subtle); border-radius: 14px; max-height: calc(100vh - 32px); overflow: auto; }
    .intent-footer .intent-meter { background: none; border: none; padding: 0; }
    .intent-sidetitle { display: block; font-size: 12px; color: var(--text-tertiary); text-transform: uppercase; letter-spacing: .06em; margin-bottom: 12px; }
    .intent-footer .intent-savebar { flex-direction: column; align-items: stretch; gap: 10px; }
    .intent-footer .intent-applyfrom { margin-right: 0; }
    .intent-footer .intent-savebar .btn { width: 100%; }
  }
  /* live meter — VARIANT C (human-chosen 2026-07-28, from tools/intentions-phone-mock.src.html). ONE thick
     segmented bar under the numbers, read WHOLE: already committed · what you planned · still free, where the
     still-free band is HATCHED so the empty part reads as deliberate space rather than absence ("want that bar
     under the numbers thicker, and also see it fully to see the empty space"). The TRACK carries its own
     background + border so it is visibly distinct from the card behind it — without that a bar simply
     disappears into its card (the defect the Track screen still carries). Widths are set by paintMeter from
     the SAME numbers the line above shows; they always sum to 100% of the track. */
  .intent-meter { padding: 10px 12px; border-radius: 12px; background: var(--bg-surface); border: 1px solid var(--border-subtle); }
  .intent-meter--over { border-color: var(--accent-warm); }
  .intent-meter__line { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; font-size: 13px; }
  .intent-meter__planned { color: var(--text-primary); }
  .intent-meter__left { color: var(--accent-teal); font-variant-numeric: tabular-nums; }
  .intent-meter__left--over { color: var(--accent-warm); }
  .intent-meter__track { height: 22px; border-radius: 8px; background: var(--bg-input); border: 1px solid var(--border-default);
    display: flex; overflow: hidden; margin: 9px 0 0; }
  .intent-meter__seg { display: block; height: 100%; transition: width .18s ease; }
  .intent-meter__seg--comm { background: var(--accent-out); }                 /* outgoing money = lavender (§3 trial) */
  .intent-meter__seg--plan { background: var(--accent-primary); }
  .intent-meter__seg--free { background: repeating-linear-gradient(135deg, transparent, transparent 5px, rgba(160,160,192,.13) 5px, rgba(160,160,192,.13) 10px); }
  .intent-meter--over .intent-meter__seg--plan { background: var(--accent-warm); }
  .intent-meter__legend { display: flex; gap: 14px; margin-top: 9px; flex-wrap: wrap; font-size: 11.5px; color: var(--text-tertiary); }
  .intent-meter__lg { display: flex; gap: 6px; align-items: center; }
  .intent-meter__swatch { width: 9px; height: 9px; border-radius: 3px; display: block; flex: none; }
  .intent-meter__swatch--comm { background: var(--accent-out); }
  .intent-meter__swatch--plan { background: var(--accent-primary); }
  .intent-meter__swatch--free { background: none; border: 1px solid var(--border-strong); }
  .intent-meter--over .intent-meter__swatch--plan { background: var(--accent-warm); }
  .intent-meter__note { font-size: 11px; display: flex; gap: 6px; align-items: center; margin-top: 9px; }
  .intent-meter__editincome { font-size: 11px; }
  /* inline rough-income ask (no income yet) */
  .intent-incomeask { padding: 12px; border-radius: 12px; background: var(--bg-surface); border: 1px solid var(--accent-primary); }
  .intent-incomeask__label { font-size: 14px; color: var(--text-primary); }
  .intent-incomeask__hint { font-size: 12px; margin: 2px 0 8px; }
  .intent-incomeask__row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
  .intent-incomeask__row .intent-money2 { width: 160px; }
  .intent-incomeask__err { font-size: 12px; color: var(--accent-warm); flex-basis: 100%; }
  /* over-commitment courtesy — calm, never alarm (amber would read as a telling-off); purple accent + a soft surface */
  .intent-overcommit { margin-top: 8px; padding: 12px 14px; border-radius: 12px; background: var(--bg-surface);
    border: 1px solid var(--accent-primary); display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
  .intent-overcommit__msg { font-size: 13px; color: var(--text-secondary); margin: 0; flex: 1 1 240px; }
  .intent-overcommit__btns { display: flex; gap: 8px; flex: none; }
  /* Set ⇄ Track toggle (in the header card) */
  /* v406: the toggle lives in the `.intent-head` flex row now — its old 12px top margin (which spaced it
     under the retired title card) would push the whole row off its own baseline. */
  .intent-modeseg { display: inline-flex; flex: 0 0 auto; border: 1px solid var(--border-default); border-radius: 10px; overflow: hidden; }
  .intent-modeseg__b { background: var(--bg-surface); border: none; color: var(--text-tertiary); font-size: 13px; padding: 8px 18px; cursor: pointer; }
  .intent-modeseg__b + .intent-modeseg__b { border-left: 1px solid var(--border-default); }
  .intent-modeseg__b:hover { color: var(--text-secondary); }
  .intent-modeseg__b--on { background: var(--accent-primary); color: var(--on-accent-primary, #fff); }
  /* TRACK view — actual-vs-intention rows (calm; tone by status, never alarm) */
  .intent-trackcard { display: flex; flex-direction: column; }
  .intent-track__hdr { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; }
  .intent-track__sub { font-size: 12px; margin: 0; }
  /* range selector */
  .intent-rangeseg { display: inline-flex; border: 1px solid var(--border-default); border-radius: 9px; overflow: hidden; flex: none; }
  .intent-rangeseg__b { background: var(--bg-surface); border: none; color: var(--text-tertiary); font-size: 12px; padding: 6px 11px; cursor: pointer; white-space: nowrap; }
  .intent-rangeseg__b + .intent-rangeseg__b { border-left: 1px solid var(--border-default); }
  .intent-rangeseg__b:hover { color: var(--text-secondary); }
  .intent-rangeseg__b--on { background: var(--accent-primary); color: var(--on-accent-primary, #fff); }
  .intent-rangeseg__b:disabled { opacity: .5; cursor: default; }
  /* income BASIS switcher (stated-income override) — reuses the .intent-rangeseg segmented control */
  /* ── v406 — THE "YOUR PICTURE" SCROLLBAR, and why it was there ────────────────────────────────────────
     The human's desktop screenshot showed a stock horizontal scrollbar along the bottom of the sidebar
     card. DIAGNOSED, not guessed (poc probe at their own 1820×880): the card is `overflow: auto` so it can
     scroll when tall, and the income-basis segmented control was overflowing it — 348px of content in a
     338px box, the segment itself 9px past the edge. The cause is two rules meeting: `.intent-rangeseg`
     is `flex: none`, so as a flex item it sizes to MAX-CONTENT and can never shrink, and both its buttons
     are `white-space: nowrap` and carry a MONEY FIGURE that grows with the amount. With a stated income
     set, BOTH buttons read a full amount ("Recent months · A$5,220.98" + "Set amount · A$5,220.98") and
     the pair simply does not fit a 340px sidebar. `.intent-incomeseg`'s own flex-wrap could never save it,
     because a `flex: 0 0 auto` item is never squeezed hard enough to wrap.
     THE FIX IS TO LET IT GIVE, not to hide the bar: inside the picture the segment may shrink, and its
     labels may wrap onto a second line. The control keeps its shape and its borders; it just stops being
     wider than the box it lives in, at any income. Scoped to `.intent-footer` — the segment is used
     elsewhere on roomier rows where nowrap reads better. */
  .intent-footer .intent-rangeseg { flex: 0 1 auto; min-width: 0; }
  .intent-footer .intent-rangeseg__b { white-space: normal; min-width: 0; }
  .intent-incomebasis { flex-wrap: wrap; }
  .intent-incomebasis__lbl { font-weight: 500; color: var(--text-secondary); }
  .intent-incomeseg { flex-wrap: wrap; }
  .intent-incomebasis__entry { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; width: 100%; margin-top: 6px; }
  .intent-incomebasis__entry .intent-money2 { width: 150px; }
  @media (max-width: 480px) { .intent-incomeseg .intent-rangeseg__b { padding: 6px 8px; font-size: 11px; } }
  /* actual-vs-planned income awareness note (calm — teal for above/good news, muted for below; never alarm) */
  .intent-incomenote { font-size: 12px; margin: 2px 0 10px; }
  .intent-incomenote--above { color: var(--accent-teal); }
  .intent-incomenote--below { color: var(--text-tertiary); }
  /* TABLE-like: name · Intention · So far (aligned columns + a small header); calm — numbers are NOT coloured.
     Two lines a row: name+numbers on one baseline, then the bar with its status at the end. */
  /* v379 (big-number round): the two money columns were a HARD 96px, so a 9-digit amount simply spilled
     past the card edge — the human's phone screenshot had "−A$987,664,310.20" running off the frame.
     `minmax(96px, auto)` keeps the old width as the FLOOR (nothing narrows) while letting a big figure
     take the room it needs from the name, which already ellipsises and keeps its full text in `title`.
     `auto` rather than `max-content` so the column can still yield when even that is not enough. */
  /* v382 - THE HEADER ROW AND EVERY DATA ROW ARE SEPARATE GRIDS, so an auto track sizes itself to its
     OWN content and the columns stop lining up down the card. v379 moved these to minmax(_, auto) to stop
     a 9-digit figure clipping, and that is exactly the misalignment the human then photographed. With the
     summary rule capping how wide a figure can get (>=1M reads compact), a FIXED track is safe again -
     and a fixed track is the only thing that makes independent grids agree. Widened from the pre-v379
     96px so a 6-digit amount still fits without the fit ladder having to step. */
  .intent-track__cols, .intent-track__grid { display: grid; grid-template-columns: minmax(0, 1fr) 108px 108px; gap: 2px 16px; align-items: baseline; }
  .intent-track__cols { margin: 2px 0; padding: 0 2px; }
  .intent-track__collbl { font-size: 11px; color: var(--text-tertiary); text-align: right; }
  .intent-track { padding: 9px 2px; border-top: 1px solid var(--border-subtle); }
  /* the name TRUNCATES rather than wraps: a wrapped name pushes the row to three or four lines and the number
     columns stop being scannable (the whole point of the fixed-width columns). The full name stays in `title`. */
  .intent-track__name { font-size: 14px; color: var(--text-primary); min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .intent-track__num { font-size: 14px; color: var(--text-secondary); text-align: right; font-variant-numeric: tabular-nums; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .intent-track__num--planned { color: var(--text-tertiary); }   /* the intention is the muted reference; "so far" is the live one */

  /* ── THE SHARED BAR (rebuilt 2026-07-28 from the approved Track mock; same build as the Set screen's
     variant-C meter, so Set and Track read as ONE feature) ────────────────────────────────────────────
     The bug it fixes: the old track was `--bg-surface` on a `--bg-surface` card — invisible — so only the fill
     was ever on screen and there was nothing to read it against; at zero spent that fill was a 3% dot. The
     track now carries its OWN background and border. SQUARE corners (human's call, 28 July): no radius on the
     bar or any segment. The remainder is HATCHED so empty reads as deliberate space rather than absence. */
  /* v406 — EQUAL-LENGTH BARS (human-directed 2026-08-26: "have them all the exact same length forever…
     all 100% ends at one place, so everything as a percentage looks completely correct"). The bar used to
     take whatever the caption left it, so "A$880.00 left" and "Add your income to track this intention"
     produced two different 100% marks on the same card and the rows could not be read against each other.
     The caption now lives in a FIXED-WIDTH right-aligned slot (`--intent-capw`), so every bar in a
     container computes to the identical width and 100% lands at one x. A caption longer than the slot
     WRAPS inside it — it never claws width back off the bar. THE GEOMETRY IS UNTOUCHED: trackBarGeom still
     owns the over-intention story (the track is a fixed 100% frame; the intention shrinks inside it with an
     amber overshoot and a mark), and this changes only how many pixels that frame is drawn across. */
  .intent-track__foot { --intent-capw: 168px; display: flex; align-items: center; gap: 10px; margin-top: 7px; flex-wrap: wrap; }
  .intent-track__bar { flex: 1 1 150px; min-width: 120px; height: 14px; border-radius: 0; background: var(--bg-input);
    border: 1px solid var(--border-default); display: flex; overflow: hidden; position: relative; }
  .intent-track__seg { height: 100%; display: block; border-radius: 0; transition: width .2s ease; }
  .intent-track__seg--done { background: var(--accent-teal); }
  .intent-track__seg--left { background: repeating-linear-gradient(135deg, transparent, transparent 5px, rgba(160,160,192,.15) 5px, rgba(160,160,192,.15) 10px); }
  /* past a FLOOR = the good outcome, so it stays teal (hatched to read as "beyond the mark") */
  .intent-track__seg--past { background: repeating-linear-gradient(135deg, var(--accent-teal), var(--accent-teal) 4px, rgba(62,207,170,.5) 4px, rgba(62,207,170,.5) 8px); }
  /* above a CEILING = amber (money-outgoing/attention), never red — §3 */
  .intent-track__seg--over { background: repeating-linear-gradient(135deg, var(--accent-warm), var(--accent-warm) 4px, rgba(240,168,104,.55) 4px, rgba(240,168,104,.55) 8px); }
  /* where the intention sits — a FLOOR fills toward and past it; a crossed CEILING marks where it was */
  .intent-track__mark { position: absolute; top: -1px; bottom: -1px; width: 2px; background: var(--border-strong); z-index: 2; }
  /* the ONE legitimate red in Hearth (§3): a genuine zero-or-below boundary — nothing was kept at all */
  .intent-track__zero { position: absolute; top: -1px; bottom: -1px; left: 0; width: 2px; background: var(--accent-red); z-index: 3; }
  /* the FIXED slot (v406). `flex: 0 0 <w>` — it never grows and never shrinks, which is the whole
     guarantee: whatever the sentence says, the bar beside it is the same length as every other bar. */
  .intent-track__cap { flex: 0 0 var(--intent-capw); max-width: 100%; text-align: right; overflow-wrap: anywhere;
    font-size: 12px; color: var(--text-tertiary); font-variant-numeric: tabular-nums; line-height: 1.35; }
  /* a phone has no room for the desktop slot AND a readable bar, so the slot steps down (measured: the
     everyday money captions — "A$880.00 left", "A$359.89 to go" — still fit on one line at 390px, and the
     bar keeps ~140px). The bars stay equal to each other, which is what the rule is for. */
  @media (max-width: 560px) { .intent-track__foot { --intent-capw: 132px; } }
  /* CEILING escalates as it nears, because a spending limit is actionable: teal → gold → amber. Never red. */
  .intent-track--within .intent-track__cap { color: var(--accent-teal); }
  .intent-track--nearing .intent-track__seg--done { background: var(--accent-gold); }
  .intent-track--nearing .intent-track__cap { color: var(--accent-gold); }
  .intent-track--over .intent-track__seg--done { background: var(--accent-warm); }
  .intent-track--over .intent-track__cap { color: var(--accent-warm); }
  /* FLOOR does NOT escalate — met/above celebrates in teal, below is MUTED. Escalating colour on a savings
     shortfall reads as a telling-off (tone-and-language.md, "shortfall is never scolded"). */
  .intent-track--met .intent-track__seg--done { background: var(--accent-teal); }
  .intent-track--met .intent-track__cap { color: var(--accent-teal); }
  .intent-track--below .intent-track__cap { color: var(--text-secondary); }
  .intent-track--negative .intent-track__cap { color: var(--accent-red); }
  .intent-track--needsIncome .intent-track__cap { color: var(--accent-primary); }
  .intent-track--needsIncome .intent-track__bar { opacity: .5; }
  /* the legend — said once for the whole card, so no row has to explain itself */
  .intent-track__legend { display: flex; gap: 13px; flex-wrap: wrap; font-size: 11.5px; color: var(--text-tertiary);
    margin-top: 12px; padding-top: 11px; border-top: 1px solid var(--border-subtle); }
  .intent-track__lg { display: flex; gap: 6px; align-items: center; }
  .intent-track__swatch { width: 9px; height: 9px; display: block; flex: 0 0 9px; border-radius: 2px; }
  .intent-track__swatch--done { background: var(--accent-teal); }
  .intent-track__swatch--left { background: repeating-linear-gradient(135deg, transparent, transparent 3px, rgba(160,160,192,.3) 3px, rgba(160,160,192,.3) 6px); border: 1px solid var(--border-strong); }
  .intent-track__swatch--over { background: var(--accent-warm); }
  .intent-track__swatch--mark { width: 2px; flex: 0 0 2px; height: 13px; border-radius: 0; background: var(--border-strong); }
  /* the header's left half keeps the sub-line and its ⓘ together; the range selector stays hard right */
  .intent-track__hdrl { display: flex; align-items: center; gap: 8px; min-width: 0; }
  /* Track has nothing to show until Set does — say so, and offer the one door out */
  .intent-trackempty { text-align: center; padding: 26px 10px 22px; }
  .intent-trackempty__ttl { font-family: 'Syne', sans-serif; font-size: 15px; color: var(--text-primary); margin: 0 0 6px; }
  .intent-trackempty__msg { font-size: 13.5px; color: var(--text-secondary); margin: 0 auto; max-width: 44ch; }
  .intent-trackempty__cta { margin-top: 14px; }
  @media (max-width: 480px) { .intent-track__cols, .intent-track__grid { grid-template-columns: minmax(0, 1fr) 86px 86px; gap: 2px 10px; } }

  /* ── HISTORY: "how you usually do" (2026-07-28; box-is-the-intention redesign 2026-08-01) ───────────────
     A multi-period range is a PATTERN question, so it renders one COLUMN PER PERIOD instead of one summed bar.
     Each row scales to its OWN max. The box IS the intention (neutral hatched, taller/shorter as it moved,
     top edge purple where it changed); the ONLY colour is the deviation from that edge — see the block below.
     NB amber on a SAVINGS shortfall here is deliberate and human-ratified for this pattern view (2026-08-01),
     a documented exception to tone-and-language.md's "a floor short is never amber" (which still holds for the
     live single-month Track view). */
  .intent-hist__lede { background: var(--bg-input); border: 1px solid var(--border-default); border-radius: 11px;
    padding: 11px 13px; font-size: 13px; color: var(--text-secondary); margin: 4px 0 12px; }
  /* the ORDER control (v296) — a small neutral dropdown where the static "sorted by" sentence used to sit.
     The thin-history note rides alongside it, because that invariant holds under every order. */
  .intent-hist__sortline { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    font-size: 11.5px; color: var(--text-tertiary); margin-bottom: 10px; }
  .intent-hist__sortlbl { flex: 0 0 auto; }
  .intent-hist__sortdd { flex: 0 0 auto; min-width: 0; }
  .intent-hist__sortdd .dropdown__trigger { width: auto; padding: 5px 9px; font-size: 12px; border-radius: 8px; gap: 6px; }
  /* the option list must not be squeezed to the trigger's width — at 14px the longest label wrapped to two lines
     (v297). Same shape as .ledger-inline: release the right edge so the list sizes to its content, and hold every
     option on ONE line. BELOW ~360px the override restores normal wrapping — a wrapped label is readable, a label
     pushed off the edge of the screen is lost. */
  .intent-hist__sortdd .dropdown__list { right: auto; min-width: 100%; }
  .intent-hist__sortdd .dropdown__opt { white-space: nowrap; font-size: 12.5px; padding: 7px 10px; }
  @media (max-width: 359px) {
    .intent-hist__sortdd .dropdown__list { right: 0; min-width: 0; }
    .intent-hist__sortdd .dropdown__opt { white-space: normal; }
  }
  .intent-hist__sortnote { flex: 1 1 auto; min-width: 0; }
  .intent-hist { padding: 11px 0 12px; border-bottom: 1px solid var(--border-subtle); }
  .intent-hist:last-of-type { border-bottom: 0; }
  .intent-hist__t { display: flex; align-items: baseline; gap: 8px; margin-bottom: 2px; }
  /* the name + its bracket are ONE left group (v297) so "Savings (20%)" reads as a unit; the group takes the
     free space, the count sits hard right on its own. Inside the group the NAME is the only thing that gives. */
  .intent-hist__ng { flex: 1 1 auto; min-width: 0; display: flex; align-items: baseline; gap: 6px; }
  .intent-hist__n { flex: 0 1 auto; min-width: 0; font-size: 14px; color: var(--text-primary);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* the CURRENT intention in brackets — its OWN span so a long name ellipsises and the number survives */
  .intent-hist__cur { flex: 0 0 auto; font-size: 12px; color: var(--text-tertiary); font-variant-numeric: tabular-nums; }
  .intent-hist__s { flex: 0 0 auto; font-size: 12.5px; color: var(--text-secondary); font-variant-numeric: tabular-nums; }
  .intent-hist--good .intent-hist__s { color: var(--accent-teal); }
  .intent-hist--watch .intent-hist__s { color: var(--accent-warm); }
  /* thin evidence reads MUTED whatever its ratio — three periods is the floor for calling something a habit */
  .intent-hist--thin .intent-hist__s { color: var(--text-tertiary); }
  .intent-hist__sub { font-size: 11.5px; color: var(--text-tertiary); margin-bottom: 8px; min-height: 1em; }
  .intent-hist__mv { color: var(--accent-primary); }
  /* BOX-IS-THE-INTENTION (redesign 2026-08-01, approved mock). The column is a NEUTRAL hatched box drawn to the
     intention amount — taller/shorter as the intention moved, its top edge the intention line (purple where it
     changed). Nothing inside is filled; the ONLY colour is the deviation from that edge: teal on the good side
     (under a ceiling / over a floor), amber on the over side (over a ceiling / short of a floor), red for a
     negative floor. A period with no intention is a PLAIN empty column. Many columns (All) scroll horizontally. */
  .intent-hist__cols { display: flex; gap: 3px; align-items: flex-end; overflow-x: auto; scrollbar-width: thin; padding-bottom: 1px; }
  .intent-hist__m { flex: 1 0 22px; min-width: 22px; display: flex; flex-direction: column; align-items: center; gap: 3px; }
  .intent-hist__track { width: 100%; height: 52px; position: relative; cursor: pointer; -webkit-tap-highlight-color: transparent; }
  .intent-hist__track::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: var(--border-default); }
  .intent-hist__track:focus-visible { outline: 2px solid var(--accent-primary); outline-offset: 2px; }
  .intent-hist__box { position: absolute; left: 0; right: 0; bottom: 0; border: 1px solid var(--border-default);
    border-top: 2px solid var(--text-secondary); background-color: var(--bg-input);
    background-image: repeating-linear-gradient(135deg, transparent, transparent 4px, rgba(160,160,192,.09) 4px, rgba(160,160,192,.09) 8px); overflow: hidden; }
  .intent-hist__box--moved { border-top-color: var(--accent-primary); }         /* you changed it that period */
  .intent-hist__box--open { border-style: dashed; border-top-style: dashed; }   /* still running */
  .intent-hist__track--none .intent-hist__box { border: 1px solid var(--border-subtle); background-color: transparent; background-image: none; }
  /* the coloured DEVIATION from the intention edge — the only colour in the column */
  .intent-hist__under { position: absolute; left: 0; right: 0; }                 /* inside the box, actual → edge */
  .intent-hist__under--good { background: var(--accent-teal); }
  .intent-hist__under--bad { background: var(--accent-warm); }
  .intent-hist__over { position: absolute; left: 0; right: 0; }                  /* above the box edge */
  .intent-hist__over--bad { background: var(--accent-warm); }
  .intent-hist__over--good { background: var(--accent-teal); }
  .intent-hist__prov { opacity: .5; }                                            /* still-running period → provisional */
  /* a period that spent more than it earned: the zero line, §3's one legitimate red */
  .intent-hist__zero { position: absolute; left: 0; right: 0; bottom: 0; height: 2px; background: var(--accent-red); z-index: 3; }
  .intent-hist__lb { font-size: 10px; color: var(--text-tertiary); display: flex; gap: 2px; align-items: center;
    max-width: 100%; overflow: hidden; white-space: nowrap; }
  .intent-hist__pc { color: var(--accent-primary); font-size: 9px; }
  .intent-hist__swatch { width: 10px; height: 10px; display: block; flex: 0 0 10px; border-radius: 2px; }
  .intent-hist__swatch--box { background-color: var(--bg-input); background-image: repeating-linear-gradient(135deg, transparent, transparent 3px, rgba(160,160,192,.16) 3px, rgba(160,160,192,.16) 6px); border: 1px solid var(--border-default); border-top: 2px solid var(--text-secondary); }
  .intent-hist__swatch--in { background: var(--accent-teal); }
  .intent-hist__swatch--over { background: var(--accent-warm); }
  .intent-hist__swatch--neg { background: var(--accent-red); }
  .intent-hist__swatch--none { background: transparent; border: 1px solid var(--border-subtle); }
  .intent-hist__swatch--line { background: var(--bg-input); border: 1px solid var(--border-default); border-top: 2px solid var(--accent-primary); }
  /* the per-column hover/tap CARD */
  .intent-histtip { position: fixed; z-index: 200; background: var(--bg-elevated); border: 1px solid var(--border-strong);
    border-radius: 10px; padding: 9px 11px; font-size: 12px; color: var(--text-secondary); max-width: 220px;
    box-shadow: 0 8px 24px rgba(0,0,0,.45); pointer-events: none; opacity: 0; transform: translateY(3px);
    transition: opacity .12s ease, transform .12s ease; }
  .intent-histtip--show { opacity: 1; transform: translateY(0); }
  .intent-histtip__h { font-weight: 600; color: var(--text-primary); font-size: 12.5px; margin-bottom: 3px; }
  .intent-histtip__r { display: flex; justify-content: space-between; gap: 14px; font-variant-numeric: tabular-nums; }
  .intent-histtip__r span:last-child { color: var(--text-primary); }
  .intent-histtip__v { margin-top: 4px; padding-top: 4px; border-top: 1px solid var(--border-subtle); font-weight: 500; }
  .intent-histtip__v--over, .intent-histtip__v--bad { color: var(--accent-warm); }
  .intent-histtip__v--in { color: var(--accent-teal); }
  .intent-histtip__v--neg { color: var(--accent-red); }
  /* 12 columns on a 390px phone leaves ~26px each — enough for a box, so the labels thin rather than the boxes */
  @media (max-width: 480px) {
    .intent-hist__cols { gap: 2px; }
    .intent-hist__track { height: 46px; }
    .intent-hist__lb { font-size: 9px; }
    .intent-hist__m { flex: 1 0 20px; min-width: 20px; }
  }

  /* ── SET screen on a PHONE: the "picture" collapses to the numbers + the bar (2026-07-28, approved mock) ──
     The bottom-pinned sheet owned roughly a quarter of the phone screen: the income segmented control, the
     committed line and the legend are all reference, not the thing you came to do. They now sit behind ONE
     tap. Desktop is untouched — the toggle is display:none above the breakpoint and the picture stays the
     roomy sidebar card. The toggle flips a class only, never a redraw (the live-recompute rule). */
  .intent-footer__more { display: none; }
  @media (max-width: 899px) {
    /* v406 — the control now RIDES the save row instead of owning a line under it (see buildFooter). So it
       stops being a full-width strip and becomes a quiet inline chip on the row's left; `.intent-applyfrom`'s
       margin-right:auto still pins Save hard right. Keeps its 36px tap target. */
    /* margin-right:auto lives HERE, not on `.intent-applyfrom` — the Apply-from row only appears when there
       are unsaved changes, and while it is hidden its auto margin is hidden with it, which let the control
       drift in from the edge and sit against Save. The ratified row is "income & details left · Save right"
       in BOTH states, so the control owns the left push itself and the Apply-from row gives its up. */
    .intent-footer__more { display: inline-flex; align-items: center; justify-content: flex-start; gap: 6px;
      flex: 0 0 auto; width: auto; margin-right: auto;
      background: none; border: 0; color: var(--text-tertiary); font: inherit; font-size: 12px; cursor: pointer;
      padding: 0 2px; min-height: 36px; }
    .intent-savebar .intent-applyfrom { margin-right: 0; }   /* the control above owns the left push now */
    .intent-footer__more:hover { color: var(--text-secondary); }
    .intent-footer__more:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-glow); border-radius: 8px; }
    /* exactly what the approved mock's `.collapsed` hides: the income basis switcher, the committed line and
       the legend. The numbers + the bar itself always stay — they are what the screen is for. */
    .intent-footer--collapsed .intent-meter__legend,
    .intent-footer--collapsed .intent-meter__note,
    .intent-footer--collapsed .intent-committedsum { display: none; }
  }

  /* ── SET screen on a PHONE: ONE ROW, ONE LINE (2026-07-28, approved mock `tools/intentions-phone-mock.src.html`) ──
     Each category used to take THREE lines — name, then an indented amount box, then the cadence clock stranded
     on its own centred row — so about three categories fit a phone screen. The row becomes a 3-column GRID
     (name · amount · cadence) on one baseline, at ~55px a row → about ten categories on screen. `display:
     contents` on the control wrapper lets its children BE grid items, so the amount + cadence land in their own
     columns while the sub-lines (resolved / warn / mid-window / committed) span the FULL row width instead of
     wrapping inside a narrow control column. Nothing is removed: same controls, same order, fewer lines. */
  @media (max-width: 700px) {
    .intent-srow, .intent-hrow, .intent-crow {
      display: grid; grid-template-columns: minmax(0, 1fr) 106px 34px;
      align-items: center; column-gap: 9px; row-gap: 2px; padding: 7px 2px; }
    .intent-crow { padding-left: 14px; }
    .intent-ctrl { display: contents; }
    .intent-rowmain { min-width: 0; }
    .intent-rowname { min-width: 0; flex: 1 1 auto; }
    /* a long name truncates rather than pushing the amount column around (the numbers stay in ONE column) */
    .intent-hrow .intent-rowname, .intent-crow .intent-rowname {
      display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; overflow-wrap: normal; }
    .intent-money2 { width: 106px; }
    .intent-amt { padding: 8px 0; font-variant-numeric: tabular-nums; }
    /* the cadence chip: a 30px square in its reserved slot — quiet at monthly, lit only when it is not */
    .intent-cadslot { flex: 0 0 34px; width: 34px; }
    .intent-cad { height: 30px; min-width: 30px; padding: 0 3px; gap: 2px; justify-content: center; }
    /* the row's sub-lines span the whole row (they are grid items now, not children of the control column) */
    .intent-resolved, .intent-warn, .intent-midwin, .intent-committed { grid-column: 1 / -1; max-width: 100%; }
    .intent-treehead { margin-bottom: 2px; }
  }

  /* ── entry RULES (§28 amended 2026-07-16): the +Add quick bar · the Settings manager · bulk pill ── */
  .rules-bar { padding: 14px 16px; }
  .rules-bar__in { width: 100%; box-sizing: border-box; }
  .rules-bar__chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
  .rules-bar__chips:empty { display: none; }
  .rules-bar__chip { appearance: none; font-family: inherit; cursor: pointer; display: inline-flex; flex-direction: column;
    align-items: flex-start; gap: 2px; text-align: left; padding: 7px 11px; border-radius: 10px; max-width: 100%;
    background: var(--bg-input); border: 1px solid var(--border-default); color: var(--text-secondary); }
  .rules-bar__chip:hover { background: var(--bg-hover); }
  .rules-bar__chip--on { border-color: var(--accent-primary); box-shadow: 0 0 0 2px var(--accent-glow); color: var(--text-primary); }
  .rules-bar__chipname { font-weight: 600; font-size: 14px; color: var(--text-primary); }
  .rules-bar__chipsum { font-size: 12px; color: var(--text-tertiary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 300px; }
  /* v392 — the "no rules yet" sentence, now a sub-line UNDER the input rather than a paragraph standing
     where the input should be (the two-people-one-household screenshots). Same words, same tertiary
     colour; it just explains the control it sits beneath instead of replacing it. */
  .rules-bar__hint { margin: 7px 2px 0; font-size: 12.5px; }
  .rules-bar__stage { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
  .rules-bar__stage:empty { display: none; }
  .rules-bar__note { font-size: 13px; color: var(--text-secondary); }
  /* ── v411 ② — THE LOG BEAT, INSIDE THE CARD, ABOVE ANY KEYBOARD ─────────────────────────────────
     The human's report: the bar has focus, so the Android keyboard is UP, and the global toast paints
     UNDERNEATH it — a one-tap log *"looked like nothing happened"*. Nothing rendered at the bottom of
     the viewport can fix that, so the sentence lands here, immediately under the input the finger is on.
     Deliberately NOT a toast: no shadow, no float, no dismiss. It is a status line in the card's own
     register — teal because it reports money landing safely (§3), with the Undo beside it as a quiet
     underlined control rather than a second button competing with Log. */
  .rules-bar__done { display: flex; align-items: baseline; flex-wrap: wrap; gap: 10px; margin-top: 10px;
    padding: 8px 10px; border: 1px solid var(--border-subtle); border-radius: 10px;
    background: var(--bg-elevated); font-size: 13px; }
  .rules-bar__donetxt { color: var(--accent-teal); min-width: 0; overflow-wrap: break-word; }
  .rules-bar__undo { background: none; border: 0; padding: 0; font: inherit; cursor: pointer;
    color: var(--text-secondary); text-decoration: underline; text-underline-offset: 3px; flex: 0 0 auto; }
  .rules-bar__undo:hover { color: var(--text-primary); }
  .rules-bar__undo:focus-visible { outline: 2px solid var(--accent-primary); outline-offset: 2px; border-radius: 4px; }
  /* the busy state on that Undo (HUI.btnBusy) — the words stay put, the dots sit after them */
  .rules-bar__undo.btn--busy { text-decoration: none; cursor: default; }
  .rules-bar__amt { width: 130px; }
  .entry-savearule { margin-top: 10px; }
  /* manager rows (Settings → Rules) — a row of the shared v413 table register (see .regrow below);
     only the name's own weight and the two full-width strips live here. `.rulerow__sum` is RETIRED:
     the run-on summary line became the row's quiet sub-line (.regrow__sub) plus real money and cadence
     columns. Kept as a declaration-free note so an old snapshot of the markup still reads sanely. */
  .rulerow__stale { grid-column: 1 / -1; margin-top: 4px; font-size: 12px; color: var(--accent-warm); }
  .rulerow__confirm { grid-column: 1 / -1; display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding-top: 8px; }
  .rulerow__confirmq { font-size: 13px; color: var(--text-secondary); }
  .rulemodal__amtrow { display: flex; gap: 8px; }
  .rulemodal__amtrow input[type="number"] { flex: 1 1 auto; min-width: 0; }
  .rulemodal__amtrow .combo { flex: 0 0 150px; }
  .rulemodal__hint { margin: -4px 0 10px; }
  /* recurring-door framing (intro at top of modal · the accept-hint on a prefilled suggestion) */
  .rulemodal__intro { margin: 0 0 14px; }
  .rulemodal__accepthint { margin: 0 0 14px; color: var(--accent-primary); }
  .rulemodal__tags { margin-bottom: 4px; }
  /* the composer's rule pill — brand-purple ring so the takeover is visible (a rule beats a merchant match) */
  .bulk-jump__pill--rule { border-color: var(--accent-primary); box-shadow: 0 0 0 1px var(--accent-glow); }
  /* v392: `.rules-teaser` (the standalone paragraph that REPLACED the bar) is retired — the sentence moved
     inside the card as .rules-bar__hint above. Kept as a rule only so a stale deep link or an old snapshot
     of the markup still reads sanely; nothing in web/js emits the class any more. */
  .rules-teaser { margin: -6px 2px 14px; }
  /* §28 (2026-07-20) — rule/recurring explanation lines + nameless-cost shortcut offer + shortcut-collision note */
  .reccard__what, .rules-managerwhat { margin: 4px 0 8px; font-size: 12.5px; }
  /* the manager explainer now sits under its own card heading (not loose under the recurring card) */
  .rules-manager .merchant-add { margin-top: 4px; }
  .rulemodal__shortcut { margin-top: 12px; border-top: 1px solid var(--border-default); padding-top: 12px; }
  .rulemodal__shortcutrow { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-top: 8px; }
  .rulemodal__shortcutrow input { flex: 1 1 180px; }
  /* calm (not amber) shortcut-namespace collision — a friendly open-to-edit, never a dead-end error */
  .rulemodal__collide, .rulemodal__collidebox { margin-top: 10px; font-size: 13px; color: var(--text-secondary); line-height: 1.5; }
  .rulemodal__collide .linkbtn { margin-left: 2px; }
  .rulemodal__del { margin-top: 8px; }
  /* "Your regulars" card (§28 consumer) — the honest yearly burden of the rhythms AND what arrives on
     one; outgoing money wears --accent-out (never amber), income teal, amounts in DM Mono capped at
     weight 500 (its real heaviest — 600 faux-bolds and reads hazy) */
  .reccard__top { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
  .reccard__title { font-size: 15px; font-weight: 600; color: var(--text-primary); }
  .reccard__ctx { font-size: 12px; color: var(--text-tertiary); }
  /* THE TWO-FIGURE HEADLINE — costs and income side by side, never one netted number. Each half is a
     money span in its own colour plus a quiet unit suffix; they wrap onto two lines before they crowd. */
  .reccard__hl { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 16px; margin: 10px 0 0; }
  .reccard__hlfig { display: inline-flex; align-items: baseline; gap: 5px; white-space: nowrap; }
  .reccard__hlamt { font-family: 'DM Mono', monospace; font-weight: 500; font-size: 18px;
    font-variant-numeric: tabular-nums; }
  .reccard__total .reccard__hlamt { color: var(--accent-out); }
  .reccard__inctotal .reccard__hlamt { color: var(--accent-teal); }
  .reccard__hlunit { font-size: 12px; color: var(--text-tertiary); }
  /* the join between two currencies is not an addition — it must never wear the money's colour */
  .reccard__hlplus { font-size: 12px; color: var(--text-tertiary); }
  /* v418 ⑧(b) — the ~ belongs WITH the figure it qualifies (unlike the "+" above, which must never read
     as arithmetic): same colour, same mono register, a shade smaller so the digits still lead. */
  /* the negative margin cancels `.reccard__hlfig`'s own 5px gap: FOUND BY EYE on the first desktop
     shot, where the headline read "~ A$11,460.00" with the mark floating free while every row said
     "~A$225.00" tight against its figure. A mark that detaches from its number stops modifying it. */
  .reccard__hlapprox { font-family: 'DM Mono', monospace; font-weight: 500; font-size: 15px; margin-right: -5px; }
  .reccard__total .reccard__hlapprox { color: var(--accent-out); }
  .reccard__inctotal .reccard__hlapprox { color: var(--accent-teal); }
  .reccard__hlnote { margin: 4px 0 0; font-size: 12px; color: var(--text-tertiary); }
  .reccard__amt { font-family: 'DM Mono', monospace; font-weight: 500; color: var(--accent-out); white-space: nowrap; }
  .reccard__incamt { font-family: 'DM Mono', monospace; font-weight: 500; color: var(--accent-teal); white-space: nowrap; }
  /* a recurring row is tap-to-edit — the WHOLE row is the calm target now, so the affordance is a tint
     across it rather than an underline under one phrase */
  .reccard__rule { cursor: pointer; border-radius: 6px; transition: background .12s ease; }
  .reccard__rule:hover { background: var(--bg-hover); }
  .reccard__rule:focus-visible { background: var(--accent-glow); outline: none; box-shadow: inset 0 0 0 1px var(--accent-primary); }
  .reccard__foot { margin-top: 8px; font-size: 12.5px; color: var(--text-tertiary); }
  .reccard__addrow { margin-top: 10px; }

  /* ── THE TABLE REGISTER (v413) — one row shape for all three cards on this screen ───────────────
     Phone: [ identity (category over merchant) | amount | how often ], the identity's two lines
     stacked in their own cell. From 760px `.regrow__id` becomes `display: contents` and those two
     lines take true columns of their own beneath a header row — ONE markup, two shapes, so the phone
     and the laptop can never drift apart.
     ⚠ THE MONEY AND CADENCE TRACKS ARE FIXED WIDTHS, and that is the whole reason the columns line up:
     every row is its OWN grid, and two separate grids never share an `auto` track (the v382 intentions
     lesson — `auto` sizes each row to its own content and the money column then walks left and right
     down the page). Fixed tracks are what make independent grids agree, which is what lets a group
     header sit between two rows and cost nothing. The vars live on `.regrow` itself, not on the table,
     because a suggestion row is a `.regrow` living outside any `.regtable`. */
  .regtable { margin-top: 10px; }
  .regrow { --reg-amt: 88px; --reg-cad: 70px;
    display: grid; grid-template-columns: minmax(0, 1fr) var(--reg-amt) var(--reg-cad);
    gap: 2px 8px; align-items: baseline; padding: 7px 0 6px; border-bottom: 1px solid var(--border-subtle); }
  .regrow__id { display: block; min-width: 0; }
  /* every identity line clamps to ONE line: the money columns must never move, and each of these lines
     LEADS with the words that identify it, so an ellipsis can only ever eat the tail (v388) */
  .regrow__main, .regrow__sub, .rulerow__name {
    display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .regrow__main { font-size: 14px; color: var(--text-primary); }
  .rulerow__name { font-size: 14.5px; font-weight: 600; color: var(--text-primary); }
  .regrow__sub { font-size: 11.5px; color: var(--text-secondary); margin-top: 1px; }
  .regrow__amt { text-align: right; font-size: 13.5px; font-variant-numeric: tabular-nums; min-width: 0; }
  /* "no set amount" is prose, not money — it drops out of the mono register and stays quiet */
  .regrow__vari { font-family: 'DM Sans', sans-serif; font-size: 11px; font-weight: 400;
    color: var(--text-secondary); white-space: normal; }
  /* v418 ⑧(a) — a USUAL amount stays in the money register (it is money, and it is in the totals); the
     `~` in front of it is what makes it an estimate rather than a promise. It must never wrap away from
     its own figure. */
  .regrow__approx { white-space: nowrap; }
  /* the "usually around" label sits ABOVE its figure, so the money column stays one number wide */
  .regrow__ty { display: block; font-family: 'DM Sans', sans-serif; font-size: 10.5px; font-weight: 400;
    color: var(--text-secondary); }
  .regrow__cad { text-align: right; font-size: 11.5px; color: var(--text-secondary); white-space: nowrap; }
  /* the ✎/🗑 pair. On a phone it takes its OWN line, right-aligned, rather than a fourth track: the two
     icon buttons cost ~50px, and a 390px card has only ~318px of content — spending a sixth of that on
     chrome would leave a rule's name about 86px, i.e. ellipsised to nothing (v388). It rejoins the row
     as a real fifth column from 760px, where the space genuinely exists. */
  .regrow__act { grid-column: 1 / -1; display: flex; gap: 2px; justify-content: flex-end;
    margin-top: 2px; align-self: center; }
  .regrow--sug { border-bottom: 0; padding: 0 0 2px; }
  /* the column header is desktop-only — on a phone there are no columns to name */
  .regrow--head { display: none; }
  .regrow--head span { font-size: 10.5px; letter-spacing: .8px; text-transform: uppercase;
    color: var(--text-tertiary); }
  .regrow--head .regrow__hamt, .regrow--head .regrow__hcad { text-align: right; }
  /* group headers carry the user's OWN tree words; income leads, in teal (§4.3) */
  .reggrp__h { display: block; font-size: 11px; letter-spacing: .9px; text-transform: uppercase;
    font-weight: 700; color: var(--text-primary); padding: 12px 0 4px;
    border-bottom: 1px solid var(--border-default); }
  .reggrp--inc .reggrp__h { color: var(--accent-teal); border-bottom-color: rgba(62, 207, 170, .28); }
  /* v418 ⑦ — the two SIDES of the table are named and coloured as a pair: income teal, expenses in the
     outgoing lavender (§3/§24 — the money colours, not new ones). The user's own TREE headers sit under
     "Expenses" and stay their own neutral words; a small indent and a quieter rule are what say they are
     inside it, so two headings in a row cannot read as two peers. */
  .reggrp--out .reggrp__h { color: var(--accent-out); border-bottom-color: rgba(218, 130, 255, .28); }
  .reggrp--tree .reggrp__h { padding-left: 10px; border-bottom-color: var(--border-subtle); }
  .reggrp--out + .reggrp--tree .reggrp__h { padding-top: 6px; }
  /* ⚠ the ROWS are NOT indented, deliberately: a row is its own grid with fixed right-hand tracks, so a
     left padding would pull its money cell 10px in and break the one guarantee this table is built on —
     every money figure on the card sharing ONE right edge (v413, measured). The heading indents; the
     money never moves. */
  @media (min-width: 760px) {
    .regrow { --reg-amt: 116px; --reg-cad: 100px;
      grid-template-columns: minmax(0, 220px) minmax(0, 1fr) var(--reg-amt) var(--reg-cad); gap: 2px 18px; }
    .regrow--rule { grid-template-columns: minmax(0, 220px) minmax(0, 1fr) var(--reg-amt) var(--reg-cad) 62px; }
    .regrow--rule .regrow__act { grid-column: auto; margin-top: 0; }
    .regrow--head { display: grid; padding: 8px 0 4px; border-bottom-color: var(--border-default); }
    .regrow__id { display: contents; }
    .regrow__main, .rulerow__name { font-size: 13.5px; }
    .regrow__sub { font-size: 13px; margin-top: 0; color: var(--text-secondary); }
    .regrow__cad { font-size: 12.5px; }
  }
  /* §28 slice 4 — the personal↔household OVERLAP ASK (save-time strip in the modal · card flag · footnote).
     Calm, never alarm — a purple-tinted awareness panel; the destructive "Retire it" is the only warm button. */
  .rulemodal__overlaps { margin-top: 14px; border-top: 1px solid var(--border-default); padding-top: 12px; }
  .rulemodal__overlapintro { margin: 0 0 8px; }
  .reccard__flags { margin-top: 10px; }
  .overlapask { background: var(--accent-glow); border: 1px solid var(--border-default); border-radius: 10px;
    padding: 10px 12px; margin-top: 8px; display: flex; flex-direction: column; gap: 8px; }
  .overlapask__q { font-size: 13px; color: var(--text-secondary); line-height: 1.5; overflow-wrap: anywhere; }
  .overlapask__btns { display: flex; gap: 8px; flex-wrap: wrap; }
  .overlapask--done { flex-direction: row; align-items: center; }
  .overlapask__donemsg { font-size: 13px; color: var(--text-tertiary); }
  .intent-committed__overlap { color: var(--text-tertiary); }
  /* "Looks recurring" — §28 source-2 suggestions (ask, never assume) */
  .sugblock__title { font-size: 15px; font-weight: 600; color: var(--text-primary); }
  .sugblock__hint { font-size: 12.5px; margin: 2px 0 4px; }
  .sugrow { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding: 10px 0; border-top: 1px solid var(--border-default); }
  .sugrow:first-of-type { border-top: none; }
  .sugrow__main { flex: 1 1 260px; min-width: 0; }
  .sugrow__msg { font-size: 13.5px; color: var(--text-primary); }
  .sugrow__meta { font-size: 12px; margin-top: 2px; }
  .sugrow__btns { display: flex; gap: 8px; flex-wrap: wrap; }
  .sugrow--done { color: var(--text-tertiary); font-size: 13px; gap: 8px; }
  .sugrow__donemsg { font-size: 13px; }
  /* rules bar "variant A" (2026-07-16, human-picked): the list OVERLAYS the form — zero height at any rule count */
  /* v421 ② — the combo is a ROW now: the input, and beside it the quick-Log button that appears the
     moment the bar holds a rule plus an amount. Beside, not underneath, and deliberately so: the
     popup is absolutely positioned at `top: 100%` of this box, so a control living INSIDE the row can
     never be covered by it (the v418 ④ lesson — rendered is not seen). It also keeps the button at the
     top of the screen, above any soft keyboard (the v411 ② lesson), within one-hand reach.
     `position: relative` is unchanged and still what the popup hangs from. */
  .rules-bar__combo { position: relative; display: flex; align-items: center; gap: 8px; }
  .rules-bar__in { flex: 1 1 0; min-width: 0; }
  .rules-bar__go { flex: 0 0 auto; }
  .rules-bar__pop { position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 45;
    background: var(--bg-elevated); border: 1px solid var(--border-strong); border-radius: 12px; padding: 6px;
    box-shadow: 0 10px 30px rgba(0,0,0,.5); max-height: 320px; overflow-y: auto; }
  .rules-bar__pop::-webkit-scrollbar { width: 8px; }
  .rules-bar__pop::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 4px; }
  .rules-bar__opt { appearance: none; font-family: inherit; cursor: pointer; display: flex; width: 100%;
    align-items: baseline; justify-content: space-between; gap: 10px; padding: 9px 10px; border-radius: 8px;
    background: none; border: 0; text-align: left; }
  .rules-bar__opt:hover { background: var(--bg-hover); }
  .rules-bar__opt--hi { background: var(--accent-glow); }
  .rules-bar__optname { font-weight: 600; font-size: 14px; color: var(--text-primary); white-space: nowrap; }
  .rules-bar__optsum { font-size: 12px; color: var(--text-tertiary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .rules-bar__pophint { font-size: 11.5px; color: var(--text-tertiary); padding: 7px 10px 3px; border-top: 1px solid var(--border-subtle); margin-top: 4px; }
  /* §22 sticky-date PIN — quiet until back-dating; purple when holding the date */
  .entry-datepin { appearance: none; flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
    width: 32px; height: 32px; border-radius: 9px; background: none; border: 1px solid var(--border-default);
    color: var(--text-tertiary); cursor: pointer; padding: 0; }
  .entry-datepin:hover { color: var(--text-secondary); background: var(--bg-hover); }
  .entry-datepin--on { color: var(--accent-primary); border-color: var(--accent-primary); box-shadow: 0 0 0 2px var(--accent-glow); }

  /* ── Settings → Help (in-app FAQ): kept-model explainer port + rules/recurring Q&A ─────────────
     Colours map to Hearth's real tokens (teal = income/kept, --accent-out = went out, gold = fund,
     amber = negative/over). No new hues, no blue. DM Mono numbers stay ≤ weight 500 (its heaviest). */
  .help { color: var(--text-primary); }
  /* v423 — `.help-seg` (the ten-pill topic wall that used to sit above every guide) is RETIRED. The
     guide now starts at the top of the screen and the topics live in the section nav: indented under
     Help in the laptop sidebar (`.settings-navsub`), or in the thumb-bar's sheet on a phone (`.snav`).
     Do not reintroduce a wall here — the wall costing 360px before a sentence appeared is the whole
     reason for the round. What replaces it at the FOOT of each guide is the Next link below. */
  .help-next { appearance: none; border: 0; background: none; cursor: pointer; font-family: inherit;
    display: flex; align-items: center; justify-content: flex-end; gap: 7px; width: 100%;
    margin: 14px 0 2px; padding: 10px 2px; font-size: 14px; color: var(--accent-primary); text-align: right; }
  .help-next__arr { font-size: 15px; }
  @media (hover: hover) { .help-next:hover { text-decoration: underline; } }
  .help-next--wrap { color: var(--text-tertiary); font-size: 13px; }
  .help-next:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-glow); border-radius: 9px; }

  .help .mono { font-weight: 500; }   /* DM Mono ships no weight > 500 — never faux-bold a number */
  .help-eyebrow { font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--accent-primary); font-weight: 600; }
  .help-h1 { font-size: 22px; line-height: 1.2; margin: 6px 0 10px; font-weight: 700; }
  .help-lede { color: var(--text-secondary); max-width: 62ch; margin: 0; }
  .help-lede b, .help-intro b { color: var(--text-primary); }

  .help-legend { display: flex; flex-wrap: wrap; gap: 8px 14px; margin-top: 14px; font-size: 13px; color: var(--text-secondary); }
  .help-legend span { display: inline-flex; align-items: center; gap: 6px; }
  .help-swatch { width: 10px; height: 10px; border-radius: 3px; display: inline-block; flex: none; }
  .help-sw--teal { background: var(--accent-teal); }
  .help-sw--out { background: var(--accent-out); }
  .help-sw--gold { background: var(--accent-gold); }

  .help-model h3, .help-gloss h3 { font-size: 15px; margin: 0 0 4px; }
  .help-model ol { margin: 12px 0 4px; padding-left: 22px; color: var(--text-secondary); }
  .help-model li { margin: 8px 0; max-width: 72ch; }
  .help-model li b { color: var(--text-primary); }

  .help-card { background: var(--bg-surface); border: 1px solid var(--border-subtle); border-radius: var(--radius); padding: 20px; margin-bottom: var(--gap); }
  .help-card__head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
  .help-card__n { font-size: 12px; font-weight: 700; color: var(--accent-primary); background: var(--accent-glow); border-radius: 7px; padding: 2px 8px; white-space: nowrap; }
  .help-card__title { font-size: 17px; font-weight: 700; margin: 0; }
  .help-chips { display: flex; gap: 6px; flex-wrap: wrap; margin-left: auto; }
  .help-chip { font-size: 11px; color: var(--text-tertiary); border: 1px solid var(--border-default); border-radius: 999px; padding: 1px 9px; white-space: nowrap; }
  .help-story { color: var(--text-secondary); margin: 10px 0 14px; max-width: 68ch; }
  .help-story b { color: var(--text-primary); }

  .help-tblscroll { overflow-x: auto; }
  .help-facts, .help-ans { border-collapse: collapse; width: 100%; font-size: 13.5px; }
  .help-facts th, .help-ans th { text-align: right; font-weight: 600; font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-tertiary); padding: 5px 10px; border-bottom: 1px solid var(--border-default); white-space: nowrap; }
  .help-facts th:first-child, .help-ans th:first-child { text-align: left; }
  .help-facts td, .help-ans td { text-align: right; padding: 6px 10px; border-bottom: 1px solid var(--border-subtle); white-space: nowrap; }
  .help-facts td:first-child, .help-ans td:first-child { text-align: left; color: var(--text-secondary); }
  .help-facts tr:last-child td, .help-ans tr:last-child td { border-bottom: none; }
  .help td.v-in { color: var(--accent-teal); }
  .help td.v-out { color: var(--accent-out); }
  .help td.v-gold { color: var(--accent-gold); }
  .help td.v-neg { color: var(--accent-warm); }
  .help-focusrow td { background: var(--bg-elevated); }
  .help-relmark { color: var(--text-tertiary); }

  .help-sect { font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-tertiary); font-weight: 700; margin: 18px 0 8px; }
  .help-why { margin: 12px 0 0; padding-left: 20px; color: var(--text-secondary); }
  .help-why li { margin: 7px 0; max-width: 72ch; }
  .help-why b { color: var(--text-primary); }
  .help-q { color: var(--text-primary); font-weight: 600; display: inline; font-size: inherit; margin: 0; }
  .help-windownote { background: var(--bg-elevated); border-radius: 10px; padding: 12px 14px; margin-top: 14px; font-size: 13.5px; color: var(--text-secondary); }
  .help-windownote b { color: var(--text-primary); }
  .help-diagram { margin-top: 16px; overflow-x: auto; }
  .help-diagram svg { display: block; }
  .help-dnote { font-size: 12.5px; color: var(--text-tertiary); margin-top: 4px; font-style: italic; }

  .help-gloss dl { margin: 10px 0 0; }
  .help-gloss dt { font-weight: 700; margin-top: 12px; }
  .help-gloss dd { margin: 3px 0 0; color: var(--text-secondary); max-width: 72ch; }
  .help-t--teal { color: var(--accent-teal); }
  .help-t--out { color: var(--accent-out); }
  .help-t--gold { color: var(--accent-gold); }
  .help-outro { color: var(--text-tertiary); font-size: 13.5px; margin-top: 8px; max-width: 68ch; }

  /* Rules & recurring Q&A */
  .help-qa { padding: 14px 0; border-bottom: 1px solid var(--border-subtle); }
  .help-qa:first-child { padding-top: 0; }
  .help-qa:last-child { border-bottom: none; padding-bottom: 0; }
  .help-faq .help-q { display: block; font-size: 15px; margin-bottom: 6px; }
  .help-a { color: var(--text-secondary); margin: 0; max-width: 72ch; }
  /* item 12: the band mechanism (Help d8) reads as SHORT points — the "larger of two widths, not added" logic
     lands at a glance instead of a dense paragraph. Each figure is computed live from the engine consts. */
  .help-bullets { color: var(--text-secondary); margin: 0; max-width: 72ch; padding-left: 20px; }
  .help-bullets li { margin: 0 0 8px; line-height: 1.5; }
  .help-bullets li:last-child { margin-bottom: 0; }

  @media (max-width: 640px) {
    .help-card { padding: 16px; }
    .help-chips { margin-left: 0; }
    .help-h1 { font-size: 19px; }
  }

  /* Help accordion — each scenario / Q&A is a collapsed, tap-to-expand row (whole header is the target,
     like the ledger/csv group rows). Multiple may be open at once; deep-linked items flash briefly. */
  .help-accgroup { margin-top: 4px; }
  .help-acc { border: 1px solid var(--border-subtle); border-radius: var(--radius); background: var(--bg-surface); margin-bottom: 10px; }
  .help-acc__head { display: flex; align-items: center; gap: 12px; padding: 14px 18px; cursor: pointer; user-select: none; border-radius: var(--radius); }
  .help-acc__head:hover { background: var(--bg-hover); }
  .help-acc__head:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--accent-primary); }
  .help-acc__label { display: flex; align-items: baseline; gap: 10px; flex: 1 1 auto; min-width: 0; flex-wrap: wrap; }
  .help-acc__n { font-size: 12px; font-weight: 700; color: var(--accent-primary); background: var(--accent-glow); border-radius: 7px; padding: 2px 8px; white-space: nowrap; flex: none; }
  .help-acc__title { font-size: 15px; font-weight: 600; color: var(--text-primary); }
  .help-acc__chev { flex: none; color: var(--text-tertiary); display: inline-flex; transition: transform 160ms ease; }
  .help-acc--open > .help-acc__head .help-acc__chev { transform: rotate(180deg); }
  .help-acc--open > .help-acc__head { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
  .help-acc__body { padding: 2px 18px 18px; }
  .help-acc__body > .help-chips { margin: 0 0 12px; }
  .help-acc__body > .help-story:first-child { margin-top: 0; }
  .help-acc__body .help-a { margin: 0; }
  .help-acc--flash { box-shadow: 0 0 0 2px var(--accent-primary), 0 0 18px var(--accent-glow); transition: box-shadow 200ms ease; }
  @media (max-width: 640px) {
    .help-acc__head { padding: 12px 14px; }
    .help-acc__body { padding: 2px 14px 16px; }
    .help-acc__title { font-size: 14px; }
  }

  /* ── Calendar (§31) ────────────────────────────────────────────────────────────────────────── */
  /* ── v417 ⑥ — THE SUB-TAB HOLDS ONE POSITION ACROSS Calendar ⇄ Forecast ────────────────────────────
     The head row was `justify-content: space-between` over a child list that CHANGES with the tab: in
     Forecast it holds two items (title · Forecast|Calendar) and the segment sat at the right edge; in
     Calendar it holds three (title · Forecast|Calendar · Grid|List) and the same segment jumped to the
     MIDDLE. So the control you use to switch tabs moved every time you used it — the human's "chrome
     movement" on a laptop. The row now packs from the LEFT (title · ⓘ · sub-tabs, all always present,
     so all three hold still) and the Grid|List switch — the only conditional member — takes the far
     end via its own `margin-left: auto`. The words "Grid" and "List" stay, as ratified.
     The month controls below already held still (`+ Add` is the conditional one there and it too is
     `margin-left: auto`); what moved them was the explainer line above, now retired. */
  .cal-head .cal-headrow { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
  .cal-headttl { display: flex; align-items: center; gap: 8px; min-width: 0; }
  .cal-head .cal-viewseg { margin-left: auto; }
  .cal-head h2 { margin: 0; }
  /* v417 ⑥: `.cal-intro` is retired — the line is gone from the markup and its ⓘ moved onto the title.
     The rule goes with it; the class no longer exists anywhere. (Its phone `display: none` below is
     retired in the same breath.) */
  .cal-seg, .cal-typeseg, .cal-certseg, .ana-subseg { display: inline-flex; border: 1px solid var(--border-default); border-radius: 999px; padding: 3px; background: var(--bg-elevated); gap: 2px; }
  .cal-seg__btn, .cal-typeseg__btn, .cal-certseg__btn, .ana-subseg__btn { appearance: none; background: none; border: none; color: var(--text-secondary); font: inherit; font-size: 13px; padding: 6px 14px; border-radius: 999px; cursor: pointer; transition: background .12s, color .12s; }
  .cal-seg__btn--on, .cal-typeseg__btn--on, .cal-certseg__btn--on, .ana-subseg__btn--on { background: var(--accent-primary); color: var(--on-accent-primary); }
  /* v417 ⑥ — …AND THE MONTH ROW RESERVES ITS TALLEST MEMBER'S HEIGHT. Measured: the row is 34px in
     Calendar (the ＋Add button's own height) and 30px in Forecast, where that button does not exist —
     so the centred month label sat 2px lower on one tab than the other. Small, and exactly the kind of
     movement this item is about. Same shape as `.intent-cadslot`: reserve the slot on every row rather
     than let the optional control decide the geometry. The number is the `.btn--sm` height; if that
     ever changes, verify-v417-polish ⑥ fails loudly rather than the row quietly drifting again. */
  .cal-monthnav { display: flex; align-items: center; gap: 10px; margin-top: 14px; flex-wrap: wrap; min-height: 34px; }
  .cal-monthlabel { font-size: 17px; font-weight: 600; color: var(--text-primary); min-width: 150px; text-align: center; }
  .cal-addbtn { margin-left: auto; }

  /* grid */
  .cal-gridcard { padding: 12px; }
  .cal-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)) minmax(64px, 0.72fr); gap: 4px; }
  .cal-dow { font-size: 11px; letter-spacing: .04em; text-transform: uppercase; color: var(--text-secondary); text-align: center; padding: 4px 0; }
  .cal-dow--tot { color: var(--text-primary); }
  .cal-day { min-height: 92px; border: 1px solid var(--border-default); border-radius: 10px; padding: 5px 6px; background: var(--bg-elevated); display: flex; flex-direction: column; gap: 3px; cursor: pointer; overflow: hidden; transition: border-color .12s, background .12s; }
  .cal-day:hover { border-color: var(--accent-primary); }
  .cal-day:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--accent-glow); }
  .cal-day--off { visibility: hidden; }
  /* "PAPER ZONES" (mock pick C, ④a 2026-07-22): PAST and FUTURE cells share the same deep-black surface
     (--cal-zone-future). The PAST reads as "settled" via a stronger border + a bright day-number; the FUTURE
     reads as "projected" via a FAINT, WIDE-PERIOD diagonal hatch (~4-5 lines per cell — the forecast's
     "projected = hatched" language) + a dim number. TODAY keeps its surface + 2px purple ring + glow (unchanged);
     EMPTY days recede (transparent, faint border, dim number). Amounts keep their kind colours. */
  .cal-day--past { background: var(--cal-zone-future); border-color: var(--cal-zone-past-border); opacity: 1; }
  .cal-day--past .cal-day__num { color: var(--text-primary); }
  /* ④a eyeball tweak (human-directed 2026-07-22): the future "projected" mark is ONE dim diagonal stroke in the
     top-left corner — as if diagonally underlining the day number — not an all-over hatch. A single non-repeating
     gradient confined to a 34px corner box; still a background-image, so the quiet-day rules below are unchanged. */
  .cal-day--future { background-color: var(--cal-zone-future); border-color: var(--border-subtle); opacity: 1;
    background-image: linear-gradient(-45deg, transparent calc(50% - 0.6px), var(--cal-zone-hatch) calc(50% - 0.6px), var(--cal-zone-hatch) calc(50% + 0.6px), transparent calc(50% + 0.6px));
    background-repeat: no-repeat; background-size: 34px 34px; background-position: 0 0; }
  .cal-day--future .cal-day__num { color: var(--text-tertiary); font-weight: 500; }
  .cal-day--today { background: var(--cal-zone-past); border: 2px solid var(--accent-primary); box-shadow: 0 0 14px rgba(124, 106, 255, .25); opacity: 1; }
  .cal-day--today .cal-day__num { color: var(--accent-primary); }
  /* EMPTY days recede via a TRANSPARENT base — background-color only (NOT the `background` shorthand), so a quiet
     FUTURE day keeps its projected hatch (from --future above) over the recessive base; a quiet past day stays plain. */
  .cal-day--quiet { background-color: transparent; border-color: var(--cal-zone-empty-border); opacity: 1; }
  .cal-day--past.cal-day--quiet { background-image: none; }   /* an empty PAST day is plain (no hatch) */
  .cal-day--quiet .cal-day__num { color: #4a4a60; }
  .cal-day__top { display: flex; align-items: center; justify-content: space-between; min-height: 16px; }
  .cal-day__num { font-size: 13px; font-weight: 600; color: var(--text-primary); }
  .cal-day--today .cal-day__num { color: var(--accent-primary); }
  .cal-day__add { appearance: none; background: none; border: none; color: var(--accent-primary); font-size: 15px; line-height: 1; cursor: pointer; padding: 0 2px; opacity: 0; transition: opacity .12s; }
  .cal-day--addable:hover .cal-day__add, .cal-day__add:focus-visible { opacity: 1; outline: none; }
  @media (hover: none) { .cal-day__add { opacity: .55; } }   /* touch: no hover, so keep the ＋ faintly visible */
  .cal-day__chips { display: flex; flex-direction: column; gap: 1px; min-height: 0; }
  /* the chip TEXT carries the colour by kind — NO borders (declutters a dense month): expense lavender ·
     income teal · provision/fund/reimbursement gold · transfer muted/neutral (internal movement) */
  .cal-chip { display: flex; align-items: baseline; justify-content: space-between; gap: 4px; font-size: 11px; padding: 1px 3px; border-radius: 5px; }
  /* the NAME is normal text (review round 2); only the AMOUNT carries the kind colour */
  .cal-chip__lbl { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 500; color: var(--text-primary); }
  .cal-chip--past .cal-chip__lbl { color: var(--text-secondary); }
  .cal-chip__amt { font-size: 11px; flex: none; }
  .cal-ev--in { color: var(--accent-teal); }
  .cal-ev--out { color: var(--accent-out); }
  .cal-ev--gold { color: var(--accent-gold); }
  .cal-ev--neutral { color: var(--text-secondary); }
  .cal-chip--past { opacity: .9; }
  .cal-ev--soft { opacity: .82; }
  .cal-ev--dashed .cal-chip__lbl { text-decoration: underline dotted; text-underline-offset: 2px; opacity: .82; }
  .cal-more { appearance: none; background: none; border: none; color: var(--text-secondary); font: inherit; font-size: 11px; text-align: left; padding: 0 3px; cursor: pointer; }
  .cal-more:hover { color: var(--accent-primary); }
  .cal-day__net { margin-top: auto; font-family: 'DM Mono', monospace; font-size: 11px; font-weight: 500; text-align: right; }
  .cal-day__net--solo { display: none; }   /* single-item day: the chip's own amount carries the number on desktop */
  .cal-net--pos { color: var(--accent-teal); } .cal-net--neg { color: var(--accent-out); }
  .cal-weektot { min-height: 92px; min-width: 0; border: 1px dashed var(--border-default); border-radius: 10px; padding: 6px; display: flex; flex-direction: column; align-items: flex-end; justify-content: center; gap: 2px; }
  .cal-weektot__lbl { display: none; }   /* shown only when the column collapses to a full-width row on phone */
  .cal-weektot__val { font-family: 'DM Mono', monospace; font-size: 12px; font-weight: 500; text-align: right; max-width: 100%; overflow-wrap: anywhere; }
  .cal-gridscroll { }   /* the grid's own horizontal scroll container (only scrolls when EXPANDED on phone) */
  .cal-monthfoot { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border-default); }
  .cal-monthfoot__row { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; justify-content: flex-end; }
  .cal-footpart { display: flex; align-items: baseline; gap: 8px; }
  .cal-footpart__lbl { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--text-secondary); }
  .cal-footpart__val { font-family: 'DM Mono', monospace; font-size: 15px; font-weight: 500; }

  /* agenda */
  .cal-stripcard { padding: 12px; }
  .cal-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(24px, 1fr)); gap: 4px; }
  .cal-heat { appearance: none; border: 1px solid var(--border-subtle); border-radius: 6px; aspect-ratio: 1; font-size: 10px; color: var(--text-tertiary); background: var(--bg-surface); cursor: pointer; display: flex; align-items: center; justify-content: center; }
  .cal-heat--quiet { opacity: .5; }
  .cal-heat--busy { background: color-mix(in srgb, var(--accent-out) 26%, var(--bg-surface)); color: var(--text-secondary); border-color: var(--accent-out); }
  .cal-heat--in { background: color-mix(in srgb, var(--accent-teal) 24%, var(--bg-surface)); color: var(--text-secondary); border-color: var(--accent-teal); }
  .cal-heat--today { box-shadow: 0 0 0 2px var(--accent-primary); color: var(--text-primary); }
  .cal-agenda { padding: 4px 0; }
  .cal-agenda__group { font-size: 12px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-tertiary); padding: 12px 16px 4px; }
  .cal-agenda__day { padding: 6px 16px; border-top: 1px solid var(--border-subtle); }
  .cal-agenda__day:first-of-type { border-top: none; }
  .cal-agenda__date { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 4px; }
  .cal-agenda__datelbl { min-width: 0; }
  .cal-agenda__evs { display: flex; flex-direction: column; gap: 3px; }
  .cal-arow { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; padding: 3px 0; }
  .cal-arow--editable { cursor: pointer; } .cal-arow--editable:hover .cal-arow__lbl { color: var(--accent-primary); }
  .cal-arow__left { min-width: 0; display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
  .cal-arow__lbl { color: var(--text-primary); font-size: 14px; }
  .cal-arow__cat { font-size: 12px; }
  .cal-arow__amt { font-size: 13px; flex: none; }
  /* a small QUIET ＋ icon at the end of a future day's header line (not a full-width row) */
  .cal-agenda__add { flex: 0 0 auto; appearance: none; background: none; border: none; color: var(--accent-primary); font-size: 16px; line-height: 1; cursor: pointer; padding: 1px 5px; border-radius: 6px; opacity: .68; transition: opacity .12s, background .12s; }
  .cal-agenda__add:hover, .cal-agenda__add:focus-visible { opacity: 1; background: var(--accent-glow); outline: none; }

  /* day detail */
  .cal-detaillist { display: flex; flex-direction: column; gap: 4px; margin-bottom: 12px; }
  .cal-drow { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--border-subtle); }
  .cal-drow__left { min-width: 0; display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
  .cal-drow__lbl { color: var(--text-primary); }
  .cal-drow__meta { font-size: 12px; }
  .cal-drow__amt { font-size: 14px; flex: none; }
  .cal-drow__edit { font-size: 12px; padding: 0; }

  /* paused / stale */
  .cal-paused { margin-top: 12px; }
  .cal-paused__head { appearance: none; background: none; border: none; color: var(--text-secondary); font: inherit; font-size: 14px; cursor: pointer; padding: 4px 0; }
  .cal-paused__head:hover { color: var(--text-primary); }
  .cal-paused__intro { margin: 6px 0 10px; }
  .cal-paused__row { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; padding: 6px 0; border-top: 1px solid var(--border-subtle); flex-wrap: wrap; }
  .cal-paused__lbl { color: var(--text-primary); }
  .cal-paused__why { font-size: 12px; }

  /* add/edit modal bits */
  .cal-suggest { margin-top: 6px; font-size: 12px; color: var(--accent-primary); }
  .cal-certhint { margin: 4px 0 2px; line-height: 1.5; }
  .cal-notledger { margin: 10px 0 4px; padding: 8px 10px; border-radius: 8px; background: var(--accent-glow); color: var(--text-secondary); }
  .cal-removebtn { margin-top: 8px; }
  .cal-removeconfirm { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 8px; color: var(--text-secondary); font-size: 13px; }

  @media (max-width: 700px) {
    .cal-day { min-height: 76px; }
    /* COLLAPSED (default): the week total becomes a slim FULL-WIDTH row under its week — the 8th column can't fit
       beside 7 day cells at ~390px, so it wraps the −/+ lines into one right-aligned strip (never off-screen) */
    .cal-dow--tot { display: none; }
    .cal-grid { grid-template-columns: repeat(7, minmax(0, 1fr)); }
    .cal-weektot { grid-column: 1 / -1; min-height: 0; flex-direction: row; align-items: center; justify-content: flex-end; gap: 12px; border: none; border-top: 1px dashed var(--border-subtle); border-radius: 0; padding: 3px 4px 6px; }
    .cal-weektot__lbl { display: inline; margin-right: auto; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--text-tertiary); }
    .cal-weektot__val { font-size: 11.5px; }

    /* EXPANDED (opt-in): a comfortable desktop-like 8-col grid inside a scroller. The week-totals COLUMN is
       sticky right:0 (stays pinned while panning) and the weekday header is sticky top — both need an opaque bg.
       overflow:auto (both axes) makes the scroller the scroll context for both sticky axes; html/body use
       overflow-x:clip (not hidden) so the standing sticky gotcha doesn't bite. */
    .cal-gridscroll--on { overflow: auto; max-height: 72vh; -webkit-overflow-scrolling: touch; }
    .cal-grid--expand { grid-template-columns: repeat(7, minmax(92px, 1fr)) minmax(76px, 0.72fr); min-width: 760px; }
    .cal-grid--expand .cal-dow { position: sticky; top: 0; z-index: 2; background: var(--bg-surface); }
    .cal-grid--expand .cal-dow--tot { display: block; position: sticky; top: 0; right: 0; z-index: 4; background: var(--bg-surface); }
    .cal-grid--expand .cal-weektot { grid-column: auto; min-height: 76px; flex-direction: column; align-items: flex-end; justify-content: center; gap: 2px; border: 1px dashed var(--border-default); border-radius: 10px; padding: 6px; position: sticky; right: 0; z-index: 3; background: var(--bg-surface); }
    .cal-grid--expand .cal-weektot__lbl { display: none; }
    .cal-grid--expand .cal-weektot__val { font-size: 12px; }
    .cal-grid--expand .cal-day { min-height: 84px; }
  }
  /* landscape phone (item 2b): a narrow week-totals COLUMN must contain 5+ digit amounts — step the font down
     (the base rule already wraps rather than ellipsises) */
  @media (max-height: 520px) and (orientation: landscape) {
    .cal-weektot { padding: 4px 3px; }
    .cal-weektot__val { font-size: 9px; letter-spacing: -0.2px; }   /* fits a typical 5-digit total on one line; still wraps (never ellipsises) if larger */
  }
  @media (max-width: 560px) {
    /* narrow grid: the chip label carries the meaning, the day net carries the number (avoids clipped amounts) —
       which means a SINGLE-item day must show its net here too, or a lone salary date shows no number at all
       while the week total includes it (human-caught 2026-08-02) */
    .cal-chip__amt { display: none; }
    .cal-day__net--solo { display: block; }
    /* a ~48px cell can't hold "−A$2,000.00" on one line — wrap it (never clip), same rule as .cal-weektot__val */
    .cal-day__net { font-size: 10px; letter-spacing: -0.2px; overflow-wrap: anywhere; }
    .cal-chip { padding: 2px 4px; }
    .cal-day { padding: 4px 4px; }
  }

  /* ── Forecast (§29) — the verdict card (F1) + the Home tile (F2) ─────────────────────────────── */
  .cal-topseg { border-color: var(--accent-primary); }
  .fc-card { }
  .fc-title { margin: 0 0 6px; font-size: 15px; }
  .fc-empty__title { margin: 0 0 6px; font-size: 15px; font-weight: 600; }   /* new-user forecast empty state */
  .fc-verdict { font-size: 16px; line-height: 1.5; margin: 6px 0 2px; max-width: 62ch; color: var(--text-primary); }
  .fc-verdict--past { color: var(--text-secondary); }
  /* r2 ADDENDUM — the verdict rewrite. okay/tight are retired. The class now rides on the CARD PARAGRAPH (the
     sentence is one i18n value, so there is no inner word to colour), which means these rules must be scoped or
     a whole sentence gets painted. On the card only the BOUNDARY case is marked — amber for "spending more than
     income", never red, never an alarm — while keep/strong stay plain text, because decorating good news is how
     a calm app starts shouting. The tile's short PHRASE is a single element, so it colours normally.
     No new colour token was introduced. */
  .fc-verdict.fc-v--under { color: var(--accent-warm); }
  b.fc-v--keep, b.fc-v--strong { color: var(--accent-teal); }
  b.fc-v--under { color: var(--accent-warm); }
  /* v389 — the kept range stopped being floored at zero (human-ratified 2026-08-21: "just show the honest
     figure"), so ONE endpoint inside an otherwise ordinary sentence can now be a below-zero value. That is
     literally §3's sanctioned red — a genuine zero-or-below VALUE, not the ruler and not routine spending —
     and it is deliberately scoped to the number: the surrounding sentence keeps its own colour, because
     painting the whole line red would turn "you'll probably keep something, but it could dip" into an
     alarm. On the tile the span sits INSIDE the teal <b class="fc-v--keep">, and that is fine without any
     specificity games: `b.fc-v--keep` matches the <b>, this matches the span, and a rule that matches the
     element itself always beats a colour merely inherited from its parent. */
  /* v417 ④ (the wrap sweep): paintNeg already gives a below-zero endpoint its own span inside the
     verdict sentence — so it is the one place in that sentence that can carry the nowrap rule. */
  .fc-v__neg { color: var(--accent-red); white-space: nowrap; }
  .dash-fc__strong { color: var(--accent-teal); }
  .fc-range { color: var(--text-tertiary); font-size: 13px; margin: 2px 0 12px; }
  /* R5 change 2 — the household scope caveat under the range line. Calm and small: it explains what the figure
     above it covers, so it must recede beside it and never read as a warning (no accent, no border). */
  .fc-hhscope { margin: -6px 0 12px; font-size: 12px; line-height: 1.45; max-width: 62ch; }
  .fc-viewtog { display: inline-flex; border: 1px solid var(--border-default); border-radius: 999px; padding: 3px; background: var(--bg-elevated); gap: 2px; margin: 2px 0 12px; }
  .fc-viewtog__btn { appearance: none; background: none; border: none; color: var(--text-secondary); font: inherit; font-size: 12.5px; padding: 5px 13px; border-radius: 999px; cursor: pointer; }
  .fc-viewtog__btn--on { background: var(--accent-primary); color: var(--on-accent-primary); }
  .fc-barstog { margin: 4px 0 6px; }
  .fc-barsbox { margin: 0 0 2px; }

  .fc-bar { margin: 12px 0 4px; }
  .fc-bar__lab { display: flex; justify-content: space-between; font-size: 12.5px; color: var(--text-secondary); margin-bottom: 5px; gap: 8px; }
  .fc-bar__amt { color: var(--text-primary); font-weight: 500; }
  .fc-bar__pm { color: var(--text-tertiary); }
  .fc-bar__track { display: flex; height: 26px; border-radius: 6px; overflow: hidden; background: var(--bg-elevated); }
  .fc-seg { position: relative; height: 100%; min-width: 2px; }
  .fc-seg + .fc-seg { margin-left: 2px; }
  /* band semantics: SOLID to (centre − spread), HATCHED to (centre + spread) — the hatch IS the ± window. */
  .fc-seg--band { background: linear-gradient(90deg, var(--seg-c) 0 var(--seg-solid), var(--seg-dim) var(--seg-solid) 100%); }
  .fc-seg--band::after { content: ''; position: absolute; top: 0; right: 0; bottom: 0; width: var(--tail, 18%);
    background: repeating-linear-gradient(-52deg, rgba(255,255,255,.72) 0 2px, rgba(255,255,255,0) 2px 5px); }
  .fc-bar__legend { display: flex; gap: 6px 14px; flex-wrap: wrap; margin-top: 7px; font-size: 12px; color: var(--text-tertiary); }
  .fc-bar__legend span { display: inline-flex; gap: 6px; align-items: center; }
  .fc-bar__vary { margin-left: auto; }
  .fc-sw { width: 9px; height: 9px; border-radius: 3px; display: inline-block; flex: 0 0 auto; }
  .fc-sw--hatch { background: repeating-linear-gradient(-52deg, rgba(255,255,255,.85) 0 2px, rgba(120,120,150,.25) 2px 4px); border: 1px solid var(--border-default); }

  .fc-drivers { margin: 14px 0 0; }
  .fc-drivers h4 { margin: 0 0 6px; font-size: 12px; letter-spacing: .07em; text-transform: uppercase; color: var(--text-tertiary); }
  /* v375 — "each amount is the whole month". Sits between the heading and the first row, in the heading's own
     tertiary tone but sentence case: it is a caption, not a second title. No new colour token. */
  .fc-drivers__scope { margin: -2px 0 8px; font-size: 12px; line-height: 1.35; }
  /* wrap, don't clip: a long category name / big amount row folds the pill+amount onto a second line (amount
     keeps right via margin-left:auto) instead of squeezing the name or pushing past the card edge (2026-08-02) */
  .fc-drv { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px; padding: 6px 0; border-bottom: 1px solid var(--border-subtle); font-size: 14px; }
  .fc-drv:last-child { border-bottom: none; }
  .fc-drv--sub { padding-left: 14px; }
  .fc-drv--toggle, .fc-drv--tap { cursor: pointer; }
  .fc-drv--tap:hover .fc-drv__name { color: var(--text-primary); }
  .fc-drv__chev { display: inline-block; width: 14px; text-indent: 0; color: var(--text-tertiary); transition: transform .12s; }   /* text-indent:0 — it inherits the name's -14px hanging indent and would paint outside its box */
  .fc-drv__chev.is-open { transform: rotate(90deg); }
  /* hanging indent: a wrapped name's second line + the note lines align under the LABEL, not under the chevron */
  .fc-drv__name { flex: 1; min-width: 0; padding-left: 14px; text-indent: -14px; overflow-wrap: anywhere; }
  .fc-drv__absorb { display: block; text-indent: 0; font-size: 11.5px; color: var(--text-tertiary); margin-top: 2px; line-height: 1.3; }
  /* R5 change 3 — "from {name}" on a household income row. INLINE (not a block like .fc-drv__absorb): it is a
     short qualifier on the label, not a sentence about it, so it must not cost the row a whole second line.
     Truncates on a narrow phone rather than pushing the amount around; the name column already yields width. */
  .fc-drv__member { display: inline-block; text-indent: 0; margin-left: 6px; max-width: 45%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; font-size: 11.5px; color: var(--text-tertiary); }
  /* the whose-income summary note, under the income rows — a quiet fact, never a warning */
  .fc-drv__whose { margin: 6px 0 0; font-size: 12px; line-height: 1.4; }
  .fc-drv__pill { font-size: 11px; border: 1px solid var(--border-default); border-radius: 999px; padding: 1px 9px; color: var(--text-tertiary); white-space: nowrap; }
  .fc-drv__pill:empty { display: none; }   /* an empty pill must vanish, never render as a tiny hollow chip */
  .fc-drv__pill--known { border-color: var(--border-strong); color: var(--text-secondary); }
  .fc-drv__pill--estimated { border-style: dashed; }
  .fc-drv__pill--income { border-color: var(--accent-teal); color: var(--accent-teal); }
  .fc-drv__pill--tuned { border-color: var(--accent-primary); color: var(--accent-primary); }
  .fc-drv__pill--unpriced { border-style: dashed; border-color: var(--accent-warm); color: var(--accent-warm); }
  .fc-drv__amt { color: var(--accent-out); white-space: nowrap; margin-left: auto; }   /* keeps right when wrapped under a long name */
  .fc-drv__amt--in { color: var(--accent-teal); }
  .fc-drv__var { color: var(--text-tertiary); font-size: 12.5px; min-width: 52px; text-align: right; }
  /* a SPARSE driver shows an asymmetric range ("usually A$0 · up to ~A$X") in prose, not a mono ± — let it size
     to its content and stay on one line; the name column (flex:1, min-width:0) yields the room. */
  .fc-drv__var--sparse { min-width: 0; white-space: nowrap; font-size: 11.5px; }
  /* R2 P4 — a symmetric range now reads as "likely A$460–540" too, so it needs the same room as the sparse
     one (the ± it replaced fit in 52px; two amounts do not). The name column yields it (flex:1, min-width:0). */
  .fc-drv__var--range { min-width: 0; white-space: nowrap; font-size: 11.5px; }
  /* R2 P2/P2b — the thin-history sentence on a driver row. Tertiary and quiet: it states what Hearth knows,
     it is never a warning, so it must not wear --accent-warm or any escalation colour. */
  .fc-drv__thin { font-style: italic; }
  /* R2 P4 — ON A PHONE THE RANGE TAKES ITS OWN LINE. A two-endpoint range is roughly twice the width of the
     "± A$41" it replaced, and in a 390px row it stole that width from the NAME: "Freelance · recent months"
     wrapped onto four lines and the row became unreadable (caught on the 390px screenshot, not by the
     no-overflow assertion — the row did not overflow, it just squeezed). Below 560px the name/pill/amount keep
     line one and the range drops beneath them, right-aligned under the amount it belongs to. The same applies
     to the SPARSE range, which had the identical squeeze before this round and simply nobody had said so. */
  @media (max-width: 560px) {
    .fc-drv { flex-wrap: wrap; }
    .fc-drv__name { flex: 1 1 auto; }
    .fc-drv__var--range, .fc-drv__var--sparse { flex: 1 0 100%; text-align: right; margin-top: 1px; }
  }
  .fc-setaside { margin: 10px 0 0; color: var(--accent-gold); }
  /* ROUND ⑥ fund add-back notes — spending-describing note lines: --accent-out. (They used to be described
     as matching `.dash-funded`/`.ana-fundnote`; those two retired in v400 — the forecast's own note lines
     stay, and the meaning they carry is now also worn by `.dash-tile__samt--out` in the Spending tile.) */
  .fc-fundbox { margin: 6px 0 10px; }
  .fc-fundcovered { margin: 8px 0 0; color: var(--accent-out); opacity: 0.9; }
  .fc-drv__fund { display: block; text-indent: 0; font-size: 11.5px; color: var(--accent-out); opacity: 0.85; margin-top: 2px; line-height: 1.3; }
  /* the shortfall sentence on a driver row — long, so no accent colour; secondary (not tertiary-dim: it matters) */
  .fc-drv__fund--partial { color: var(--text-secondary); opacity: 1; }
  /* fund-link row markers wear GOLD — the ledger fund badge's colour (.tx__note--fund), the human's ask;
     the forecast's summary NOTE LINES stay --accent-out per the §3 note-line rule. */
  .cal-fromfund { font-size: 12px; color: var(--accent-gold); opacity: 0.9; }
  .cal-chip__fund { flex: 0 0 auto; color: var(--accent-gold); font-size: 8px; line-height: 1; align-self: center; }
  .reserve__expected { font-size: 12.5px; color: var(--accent-out); opacity: 0.9; margin-top: 2px; }
  .cal-fundhint { margin: 4px 0 0; }

  /* per-driver explainer + tune */
  .fc-explain { background: var(--bg-elevated); border: 1px solid var(--border-subtle); border-radius: 10px; padding: 10px 12px; margin: 2px 0 8px; }
  .fc-explain__lead { margin: 0 0 6px; font-size: 13.5px; color: var(--text-primary); }
  .fc-explain__note { margin: 3px 0 0; font-size: 12.5px; color: var(--text-secondary); }
  /* the driver mini-chart (real matched history · median line · ±MAD band · ringed outliers) — its own scroller */
  .fc-explain__chartwrap { margin: 6px 0 8px; overflow-x: auto; max-width: 100%; }
  .fc-explain__chartwrap svg { display: block; }
  .fc-explain__hist { display: flex; flex-wrap: wrap; gap: 5px; margin: 4px 0 6px; }
  .fc-explain__chip { font-size: 11.5px; color: var(--text-tertiary); border: 1px solid var(--border-subtle); border-radius: 7px; padding: 1px 7px; }
  .fc-explain__chip b { color: var(--text-secondary); font-weight: 500; }
  .fc-tune { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-top: 8px; padding-top: 8px; border-top: 1px dashed var(--border-subtle); font-size: 13px; }
  .fc-tune__lead { flex: 1 1 100%; }
  .fc-tune__money { width: 130px; }
  .fc-tune__revert { color: var(--accent-primary); }

  .fc-simplelines { margin: 10px 0 4px; }
  .fc-sline { display: flex; justify-content: space-between; padding: 5px 0; font-size: 14px; }
  .fc-sline__lbl { color: var(--text-secondary); }
  .fc-amt--in { color: var(--accent-teal); }
  .fc-amt--neg { color: var(--accent-warm); }

  .fc-affordcard { margin-top: 12px; }
  .fc-card__title { margin: 0 0 8px; font-size: 15px; }
  .fc-affordrow { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }
  .fc-affordmoney { flex: 0 0 auto; width: 140px; }
  .fc-affordout { font-size: 14px; color: var(--text-primary); }
  .fc-affordunder { color: var(--accent-warm); }

  /* F3 — the next-six-months overview card */
  .fc-six__title { margin: 0 0 4px; font-size: 15px; }
  .fc-six__intro { margin: 0 0 8px; max-width: 62ch; }
  .fc-six__scroll { overflow-x: auto; max-width: 100%; }   /* the chart scrolls INSIDE its card on a phone — page never does */
  .fc-six__svg { display: block; margin: 0 auto; }
  .fc-six__legend { display: flex; gap: 8px 16px; flex-wrap: wrap; margin-top: 8px; font-size: 12px; color: var(--text-tertiary); }
  .fc-six__legend span { display: inline-flex; gap: 6px; align-items: center; }
  .fc-six__swline { height: 3px; border-radius: 2px; }

  /* F4 — the day-by-day trace (§29 item 3): an inline expander on the F1 card, closed by default */
  .fc-trace { margin: 0 0 12px; }
  .fc-trace__toggle { display: inline-flex; align-items: center; gap: 5px; font-size: 13px; }
  .fc-trace__chev { display: inline-block; transition: transform .15s ease; }
  .fc-trace__chev.is-open { transform: rotate(90deg); }
  .fc-trace__body { margin-top: 8px; padding: 10px 12px; border: 1px solid var(--border-subtle); border-radius: 10px; background: var(--bg-elevated); }
  .fc-trace__intro { margin: 0 0 6px; max-width: 62ch; }
  /* the chart SCALES with its container (viewBox + width:100%) — the scroller is a belt-and-braces guard so a
     phone can never push the PAGE sideways (the standing "that element scrolls, not the screen" rule) */
  .fc-trace__scroll { overflow-x: auto; max-width: 100%; }
  .fc-trace__svg { display: block; width: 100%; }
  /* v389 (the "hazy" round): the trace's legend and its figure labels are how you read the chart, so they
     take the shared chart-axis ink rather than --text-tertiary — 3.54:1 → 5.89:1 on --bg-surface. Measured,
     not guessed; the FIGURES themselves (--text-primary / teal / lavender) are untouched. */
  .fc-trace__legend { display: flex; gap: 8px 16px; flex-wrap: wrap; margin-top: 6px; font-size: 12px; color: var(--ana-axis-ink); }
  .fc-trace__legend span { display: inline-flex; gap: 6px; align-items: center; }
  .fc-trace__swband { border-radius: 3px; }
  .fc-trace__figs { display: flex; gap: 10px 22px; flex-wrap: wrap; margin-top: 10px; }
  .fc-trace__fig { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
  .fc-trace__figlbl { font-size: 11px; color: var(--ana-axis-ink); }
  .fc-trace__figval { font-size: 14px; font-weight: 500; color: var(--text-primary); }   /* DM Mono ships no weight above 500 */
  .fc-trace__fig--in { color: var(--accent-teal); }
  .fc-trace__fig--out { color: var(--accent-out); }
  .fc-trace__cross { margin: 8px 0 0; }
  @media (max-width: 640px) {
    .fc-trace__body { padding: 8px 9px; }
    .fc-trace__figs { gap: 8px 14px; }
  }

  /* §31.1 calendar light filter */
  .cal-filter { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; padding: 10px 14px; }
  .cal-filterseg { flex-wrap: wrap; }
  .cal-filter .catpick, .cal-filter .multipick { flex: 1 1 200px; min-width: 160px; max-width: 320px; }
  .cal-filter__note { color: var(--accent-primary); }
  .cal-monthfoot__filtered { width: 100%; font-size: 12px; margin-bottom: 4px; color: var(--accent-primary); }

  /* §31 phone NAV FIX — ONE compact sticky control bar (calendar.js stickyBar): ‹ Month Year › centred, icons for
     grid⇄list · Expand · funnel · ＋ add. PINS to the top while scrolling (position:sticky works because html/body
     use overflow-x:clip, the standing rule). Desktop keeps the original layout — the bar is display:none there. */
  .cal-sticky { display: none; }
  /* NB the gate is width OR a short landscape viewport — a LANDSCAPE PHONE is ~844px wide (past any phone width
     breakpoint) but only ~390px tall, which is exactly where controls scroll out of reach worst. Same lesson as the
     bulk commit button: tie a "needs the controls in thumb reach" affordance to the real constraint, not width alone. */
  @media (max-width: 700px), (max-height: 520px) and (orientation: landscape) {
    /* v417 ⑥: the `.cal-intro { display: none }` that stood here is retired with the element — the line
       no longer exists on EITHER form factor, so there is nothing left to hide. */
    .cal-viewseg, .cal-monthnav { display: none; }   /* the sticky bar carries month nav · view toggle · add */
    .cal-sticky { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 2px;
      position: sticky; top: 0; z-index: 40; margin: 10px -20px; padding: 4px 14px;   /* full-bleed across the .app 20px gutters so nothing peeks past the pinned bar */
      background: var(--bg-base); border-bottom: 1px solid var(--border-subtle); }
    .cal-sticky__side { display: flex; gap: 2px; min-width: 0; }
    .cal-sticky__side--r { justify-content: flex-end; }
    .cal-sticky__month { display: flex; align-items: center; justify-content: center; gap: 2px; min-width: 0; }
    .cal-sticky__label { font-size: 15px; font-weight: 600; color: var(--text-primary); white-space: nowrap; }
    .cal-sticky .iconbtn { position: relative; }
    .cal-sticky .iconbtn--on { color: var(--accent-primary); background: var(--accent-glow); }
    .cal-sticky__add { color: var(--accent-primary); }
    /* a filter is APPLIED (even with the card closed) — honest state, a quiet dot on the funnel */
    .cal-sticky__filter--applied::after { content: ''; position: absolute; top: 4px; right: 4px; width: 6px; height: 6px; border-radius: 50%; background: var(--accent-primary); }
    /* the filter card: hidden until the funnel opens it; scroll-margin keeps it clear of the pinned bar */
    .cal-filter { display: none; scroll-margin-top: 56px; }
    .cal-filter.cal-filter--open { display: flex; }
  }

  /* F2 — Home tile */
  .dash-fc { cursor: pointer; }
  .dash-fc__line { font-size: 15px; }
  .dash-fc__go { color: var(--accent-primary); font-size: 13px; text-decoration: none; float: right; margin-left: 10px; }
  .dash-fc__sub { font-size: 12.5px; margin: 3px 0 8px; }
  /* v365 — the SHARED past-month headline (window.HFcPastLine) also renders here now that the month picker
     sits above this card. It keeps its own muted --past colour; only the tile's register is applied, so the
     line matches .dash-fc__line rather than the Forecast screen's larger card type. No new colour. */
  .dash-fc .fc-verdict { font-size: 15px; line-height: 1.4; margin: 0 0 2px; }
  /* R5 change 2 — the household scope caveat, tile-short. Quiet and small; it qualifies the figure above it. */
  .dash-fc__scope { font-size: 11.5px; margin: -4px 0 8px; line-height: 1.35; }
  /* ── v417 ⑧ — THE FORECAST TILE HOLDS THE ROOM IT LAST TOOK WHILE IT LOADS ────────────────────────
     Measured: Home painted with this tile 75px tall and grew it to 122px a second later, dropping the
     whole money block beneath it. The reservation is a `min-height` written by dashboard.js from the
     height this very tile settled at on its last render (see the note at fcRoom) — a measurement, not
     a skeleton, because a skeleton over-reserved the EMPTY state by 100px. Nothing to style but the
     wait line, which centres itself in the room rather than sitting at the top of it. */
  .dash-fc .dash-fc__loading { justify-content: center; }
  .dash-fc__chart { margin-top: 4px; }
  .dash-fc__chart svg { display: block; }
  .dash-fc__chart svg text { font-family: 'DM Sans', system-ui, sans-serif; }
  .dash-fc__chart svg text.mono-t { font-family: 'DM Mono', ui-monospace, monospace; font-variant-numeric: tabular-nums; }

  /* ── §19 in-app NOTIFICATION CENTRE (the bell beside the active-context picker) ─────────────── */
  .notifslot { position: relative; display: flex; align-items: center; }
  .notifbell {
    appearance: none; background: none; border: 1px solid transparent; border-radius: 8px; cursor: pointer;
    color: var(--text-secondary); padding: 6px; line-height: 0; position: relative;
  }
  .notifbell:hover { color: var(--text-primary); background: var(--bg-hover); }
  .notifbell--on { color: var(--accent-primary); }
  .notifbell__badge {
    position: absolute; top: 0; right: 0; min-width: 15px; height: 15px; padding: 0 3px; border-radius: 8px;
    background: var(--accent-primary); color: #fff; font-size: 10px; line-height: 15px; text-align: center;
    font-family: 'DM Sans', system-ui, sans-serif; font-weight: 500;
  }
  /* ── v392 — THE PANEL IS ANCHORED TO THE BELL, AT EVERY WIDTH ───────────────────────────────────────
     It used to resolve against an ANCESTOR: `.notifslot` on desktop, and on a phone (where a 340px card
     right-aligned to a mid-row bell would have hung off the left edge) `.app__header` — whose bottom, on
     a phone, is BELOW THE NAV STRIP. So the panel opened at the top of the content area, three rows away
     from the bell, reading as a card belonging to whatever screen you were on. It is now `position:
     fixed` and placed from the bell's measured rect by notify.js `placePop`, with a caret pointing back
     at the bell — so a phone can give it nearly the full width and it still visibly descends from the
     control that opened it. `left`/`top` are set inline; the values here are only the pre-measure resting
     place, so a first paint can never flash in the corner. */
  .notifpop {
    position: fixed; top: -9999px; left: -9999px; z-index: 60; width: min(340px, calc(100vw - 16px));
    background: var(--bg-elevated); border: 1px solid var(--border-default); border-radius: 12px;
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45); overflow: hidden;
    animation: notifpop-in 130ms ease-out both;
  }
  /* the caret is a SIBLING of the panel, not a child, so the panel keeps `overflow: hidden` (its rounded
     corners depend on it) and the triangle is not clipped away. Geometry is the shared .hui-caret. */
  .notifpop__caret { position: fixed; z-index: 61; border-bottom-color: var(--border-default);
    animation: notifpop-in 130ms ease-out both; }
  .notifpop__caret::after { border-bottom-color: var(--bg-elevated); }
  @keyframes notifpop-in { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: none; } }
  @media (prefers-reduced-motion: reduce) {
    .notifpop, .notifpop__caret { animation: none; }
  }
  .notifpop__head { display: flex; align-items: center; justify-content: space-between; padding: 10px 8px 10px 14px; border-bottom: 1px solid var(--border-subtle); }
  .notifpop__title { font-size: 13px; font-weight: 600; color: var(--text-primary); }
  .notifpop__empty { margin: 0; padding: 16px 14px; font-size: 13px; }
  .notifpop__list { max-height: min(60vh, 420px); overflow-y: auto; }
  .notifpop__list::-webkit-scrollbar { width: 8px; }
  .notifpop__list::-webkit-scrollbar-thumb { background: var(--border-default); border-radius: 4px; }
  .notifitem { display: flex; align-items: flex-start; gap: 4px; border-bottom: 1px solid var(--border-subtle); }
  .notifitem:last-child { border-bottom: 0; }
  .notifitem__main { flex: 1 1 auto; min-width: 0; padding: 11px 4px 11px 14px; cursor: pointer; }
  .notifitem__main:hover { background: var(--bg-hover); }
  /* unread = a small dot before the title (honest state, never a border accent — 2026-07-22) */
  .notifitem__dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--accent-primary); margin-right: 7px; vertical-align: 1px; }
  .notifitem--good .notifitem__dot { background: var(--accent-teal); }
  .notifitem__tag { font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-tertiary); margin-bottom: 2px; }
  .notifitem__title { font-size: 13.5px; color: var(--text-primary); }
  .notifitem--good .notifitem__title { color: var(--accent-teal); }
  .notifitem__body { font-size: 12px; margin-top: 2px; }
  .notifitem__x {
    appearance: none; background: none; border: 0; cursor: pointer; color: var(--text-tertiary);
    font-size: 17px; line-height: 1; padding: 10px 12px 10px 6px;
  }
  .notifitem__x:hover { color: var(--text-primary); }

  /* ── §18 DRIFT — the Home card, the band chart, the "what changed" table ─────────────────────
     Never red (red is the zero line only). Upward drift is the calm brand purple; downward drift wears
     teal, because good news gets EQUAL billing (§18.1). */
  .drift-host:empty { display: none; }
  .drift-card { display: flex; align-items: flex-start; gap: 6px; flex-wrap: wrap; }   /* no accent border — the tag names the feature (2026-07-22) */
  .drift-card__main { flex: 1 1 240px; min-width: 0; cursor: pointer; border-radius: 8px; padding: 2px 4px; margin: -2px -4px; }
  .drift-card__tag { font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-tertiary); margin-bottom: 3px; }
  .drift-card__main:hover { background: var(--bg-hover); }
  .drift-card__line { font-size: 15px; color: var(--text-primary); }
  .drift-card--good .drift-card__line { color: var(--accent-teal); }
  .drift-card__sub { font-size: 12.5px; margin-top: 3px; }
  .drift-card__go { display: inline-block; margin-top: 6px; font-size: 13px; color: var(--accent-primary); }
  .drift-card__x { appearance: none; background: none; border: 0; cursor: pointer; color: var(--text-tertiary); font-size: 18px; line-height: 1; padding: 2px 6px; }
  .drift-card__x:hover { color: var(--text-primary); }
  .drift-card__more { flex: 1 1 100%; text-align: left; font-size: 12.5px; margin-top: 2px; }
  .drift-card__all { flex: 1 1 100%; text-align: left; font-size: 12.5px; margin-top: 2px; color: var(--text-tertiary); }
  .drift-card__all:hover { color: var(--accent-primary); }

  /* the detail modal earns a REAL wide desktop view — the band chart is its point (item 10). The SVG's viewBox
     tracks this width so it renders ≈1:1 (crisp, not a small viewBox upscaled into a blur). Phones/landscape are
     unaffected (the modal already fills the viewport width there, capped by max-width). .modal--wide is a reusable
     hook for any future wide modal. */
  .modal--wide { max-width: 960px; }
  .drift-modal { max-width: 960px; }
  .drift-asof { font-size: 12.5px; margin: 0 0 10px; font-style: italic; }   /* item 9: the quiet "as it stood in {month}" historical marker */
  .drift-lead { margin-bottom: 14px; }
  .drift-lead__head { font-size: 16px; color: var(--text-primary); margin: 0 0 4px; }
  .drift-lead--good .drift-lead__head { color: var(--accent-teal); }
  .drift-lead__sub { font-size: 13.5px; color: var(--text-secondary); margin: 0 0 2px; }
  .drift-lead__noticed, .drift-lead__gatelate, .drift-lead__sofar { margin: 4px 0 0; font-size: 12px; }   /* §18.1 #4 timing + ⑦ fired so-far line — quiet */
  .drift-chart__trim { margin: 4px 0 0; font-size: 12px; }                             /* §18.1 #8 set-aside note */
  .drift-chart__head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
  .drift-chart__title { font-size: 13px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-tertiary); margin: 0 0 8px; font-weight: 500; }
  .drift-why { font-size: 12.5px; }
  .drift-chart__wrap { overflow-x: auto; }             /* the chart scrolls inside its own card, never the page */
  .drift-chart__wrap svg { display: block; min-width: 280px; }
  /* ── v390 — the band chart pans on the app's ONE pan gesture ────────────────────────────────────────
     `touch-action: pan-y` is the same touch policy the Analysis stage has carried since v376: a vertical
     flick still scrolls the modal, a horizontal drag belongs to the chart. It is applied ONLY while the
     chart is genuinely windowed — a chart showing everything must not swallow a horizontal swipe it has
     nothing to do with. The grab cursor is the desktop half of the same affordance (.ana-cstage's pair). */
  .drift-chart__wrap--pannable { touch-action: pan-y; cursor: grab; overscroll-behavior-x: contain; }
  .drift-chart__wrap--pannable.drift-chart__wrap--drag { cursor: grabbing; }
  .drift-chart__wrap--pannable.drift-chart__wrap--drag svg { cursor: grabbing; }
  /* the way-back row, the sibling of Analysis's .ana-cbar — it draws ONLY when the reader has panned off
     the recent end, because a chip that restores what you are already looking at is a dead control. */
  .drift-cbar { display: flex; justify-content: flex-end; gap: 8px; margin: 6px 0 0; }
  .drift-cbar__btn { background: var(--bg-elevated); border: 1px solid var(--border-subtle); color: var(--text-secondary);
    border-radius: 999px; padding: 5px 12px; font-size: 12px; min-height: 28px; cursor: pointer; }
  .drift-cbar__btn:hover { border-color: var(--border-strong); color: var(--text-primary); }
  .drift-chart__wrap svg text { font-family: 'DM Sans', system-ui, sans-serif; }
  .drift-chart__wrap svg text.mono-t { font-family: 'DM Mono', ui-monospace, monospace; font-variant-numeric: tabular-nums; }
  /* §18 round ⑦c — the glyph micro-legend under the band chart; wraps cleanly on a phone, never overflows sideways */
  .drift-legend { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 14px; font-size: 11.5px; margin: 6px 0 0; }
  .drift-legend__item { display: inline-flex; align-items: center; gap: 5px; min-width: 0; }
  .drift-leg__g { flex: 0 0 auto; overflow: visible; }
  /* v389: chart-reading chrome takes the shared chart-axis ink (3.54:1 → 5.89:1 on the card). */
  .drift-legend__lbl { color: var(--ana-axis-ink); }
  .drift-chart__wrap svg { cursor: crosshair; }        /* the chart is hoverable — each month reveals its numbers */
  /* the hover tooltip — themed like the app's popovers; reads the chart's own plotted values (never recomputed) */
  /* v389: the border moves --border-default → --border-strong so the SHARED caret (.hui-caret, whose outer
     triangle is drawn in --border-strong) meets the card's own edge in the same colour. Without this the
     caret reads as a slightly brighter fin stuck onto a dimmer box — the one visual seam the shared
     geometry cannot fix from its side. Same token the Analysis tooltip has always used. */
  .drift-tip { position: fixed; z-index: 3000; pointer-events: none; max-width: 260px;
    background: var(--bg-elevated); border: 1px solid var(--border-strong); border-radius: 8px;
    padding: 8px 10px; box-shadow: 0 6px 20px rgba(0, 0, 0, .22); font-size: 12px; }
  .drift-tip__mon { font-weight: 500; color: var(--text-secondary); margin: 0 0 5px; }
  .drift-tip__rows { display: flex; flex-direction: column; gap: 3px; }
  .drift-tip__row { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; }
  .drift-tip__lbl { color: var(--ana-axis-ink); }   /* v389: 3.30:1 → 5.49:1 on --bg-elevated */
  .drift-tip__val { font-family: 'DM Mono', ui-monospace, monospace; font-variant-numeric: tabular-nums;
    font-weight: 500; color: var(--text-primary); text-align: right; }
  .drift-tip__spent { color: var(--accent-out); }
  .drift-tip__usual { color: var(--accent-primary); }
  .drift-tip__range { color: var(--text-secondary); }
  .drift-tip__prov { color: var(--ana-axis-ink); font-size: 11px; margin: 6px 0 0; line-height: 1.35; }   /* round ⑦c — provenance line (v389 ink) */
  .drift-provisional { font-size: 12.5px; color: var(--accent-warm); margin: 8px 0 0; }
  .drift-callout { font-size: 14px; color: var(--text-primary); margin: 0 0 4px; }
  .drift-callout--spread { color: var(--text-secondary); }
  .drift-table { margin-top: 12px; }
  .drift-row { display: flex; align-items: baseline; gap: 8px; padding: 7px 0; border-bottom: 1px solid var(--border-subtle); }
  .drift-row--head { border-bottom-color: var(--border-default); }
  .drift-row:last-child { border-bottom: 0; }
  .drift-row__name { flex: 1 1 0; min-width: 0; font-size: 13.5px; color: var(--text-primary); display: flex; flex-direction: column; }
  .drift-row--head .drift-row__name { font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.05em; }
  .drift-row__sub { font-size: 11px; }
  .drift-row__n { flex: 0 0 84px; text-align: right; font-size: 13px; font-weight: 400; }
  .drift-row--head .drift-row__n { font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.05em; }
  .drift-d--up { color: var(--accent-out); }
  .drift-d--down { color: var(--accent-teal); }
  .drift-table__note { font-size: 11.5px; margin: 8px 0 0; }
  .drift-helplink { font-size: 12.5px; margin-top: 12px; }

  /* ── §18 the Analysis "Usual ranges" sub-view (drift-view.js) ────────────────────────────────
     The Analysis frame is full-width (.app--analysis); this content reads better narrow, so it caps its
     OWN width (the stable-frame rule — the frame itself never jumps between sub-views). */
  /* review round 1 · item 5: .ana-headrow is GONE with the heading card it belonged to. What survives is the
     [ Explore | Usual ranges ] pill pair + the ⓘ, which now live in the controls' presets row (pushed to the
     right of it) on Explore, and stand alone above the drift view — which returns before buildControls runs.
     .ana-subseg brings its own pill-strip border/background, so it reads fine outside a card. */
  .ana-subviewstrip { display: flex; align-items: center; gap: 8px; margin-left: auto; }
  /* ── v417 ⑤ — ONE LAPTOP POSITION FOR THE [ Explore | Usual ranges ] TOGGLE ────────────────────────
     In Explore the strip lives inside `.ana-presets` and its `margin-left: auto` carries it to the
     RIGHT end of the first row. In Usual ranges there is no presets row, so the standalone copy sat at
     the TOP LEFT — the one control that exists to move you between the two sub-views was in a
     different place depending on which one you were on. It now stands where Explore has it (the
     human's ratified default, veto-able): pushed right, and given the presets row's own bottom margin
     and minimum height so its Y matches too — `.ana-presets` is `align-items: center` around a 40px
     ★ Saved trigger, and a strip that is merely its own height would sit a few px high.
     PHONE IS UNTOUCHED: below 700px the Explore strip is already full-width `order: 1` at the top of
     its row (v383's header shuffle), which is where the standalone one already was. */
  .ana-subviewstrip--alone { margin: 0 0 12px; }
  @media (min-width: 701px) {
    .ana-subviewstrip--alone { margin-left: auto; width: fit-content; min-height: 40px; }
  }
  @media (max-width: 700px) { .ana-subviewstrip { margin-left: 0; } }
  .ana-driftview { max-width: 880px; }
  .driftv-section { margin-top: 16px; }
  .driftv-section__title { font-size: 13px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-tertiary); margin: 0 0 10px; font-weight: 500; }
  .driftv-finding { border: 1px solid var(--border-subtle); border-radius: 12px; padding: 12px 14px; margin-bottom: 8px; }
  .driftv-row { display: flex; align-items: center; gap: 12px; padding: 9px 12px; border: 1px solid var(--border-subtle); border-radius: 12px; margin-bottom: 8px; cursor: pointer; background: var(--bg-elevated); }
  .driftv-row:hover { border-color: var(--border-default); background: var(--bg-hover); }
  .driftv-row__name { flex: 1 1 auto; min-width: 0; }
  .driftv-row__label { font-size: 14px; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .driftv-row__range { font-size: 12px; margin-top: 2px; }
  .driftv-row__spark { flex: 0 0 auto; line-height: 0; }
  /* item 8: occasional/sparse categories are LISTED (never hidden behind a footnote) but read muted — no band,
     just a dot strip — so a real usual range is unmistakably the steady, brighter thing. */
  .driftv-row--occasional { background: transparent; }
  .driftv-row--occasional .driftv-row__label { color: var(--text-secondary); }
  .driftv-row__dots { opacity: 0.85; }
  .driftv-pastlist { display: flex; flex-direction: column; gap: 8px; }
  /* item 9: a past episode is tappable (opens the as-of detail) — give it the same affordance as a range row */
  .driftv-past { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; padding: 8px 12px; border: 1px solid var(--border-subtle); border-radius: 12px; cursor: pointer; background: var(--bg-elevated); }
  .driftv-past:hover { border-color: var(--border-default); background: var(--bg-hover); }
  .driftv-past__when { flex: 0 0 auto; font-size: 12px; font-variant-numeric: tabular-nums; }
  .driftv-past__line { flex: 1 1 240px; min-width: 0; font-size: 13.5px; color: var(--text-secondary); }
  .driftv-past--good .driftv-past__line { color: var(--accent-teal); }
  .driftv-past__go { flex: 0 0 auto; font-size: 12px; color: var(--accent-primary); }
  .driftv-past__meta { flex-basis: 100%; font-size: 11.5px; }                          /* §18.1 #4 noticed/gate-late — its own line */
  .driftv-row__swing { font-size: 12px; margin-top: 2px; }                             /* §18.1 #2 volatility line — muted, not amber */
  .driftv-more { font-size: 12.5px; margin-top: 10px; }

  /* item ⑤: a steady category row expands to the places within it. The catwrap groups the row + its (lazy)
     merchant list; the chevron is a sibling control inside the row (stops the tap from opening the modal). */
  .driftv-catwrap { margin-bottom: 8px; }
  .driftv-catwrap > .driftv-row { margin-bottom: 0; }
  .driftv-chev { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; margin: -4px -6px -4px -2px; border-radius: 8px; color: var(--text-tertiary); cursor: pointer; }
  .driftv-chev:hover { color: var(--text-secondary); background: var(--bg-hover); }
  .driftv-chev svg { transition: transform 0.15s ease; }
  .driftv-chev--open svg { transform: rotate(180deg); }
  .driftv-row--expanded { border-color: var(--border-default); }
  /* the merchant sub-list — indented under the category, quieter, its own left rule so it reads as "within" */
  .driftv-merchlist { margin: 6px 0 2px 14px; padding-left: 12px; border-left: 2px solid var(--border-subtle); display: flex; flex-direction: column; gap: 6px; }
  .driftv-row--merch { margin-bottom: 0; }
  .driftv-row--merch .driftv-row__label { font-size: 13px; }
  .driftv-merchloading, .driftv-merchempty { font-size: 12.5px; margin: 4px 2px; }
  .driftv-retry { cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
  .driftv-retry:focus-visible { outline: 2px solid var(--accent-primary); outline-offset: 2px; border-radius: 4px; }
  @media (prefers-reduced-motion: reduce) { .driftv-chev svg { transition: none; } }

  /* ── shared LOADING + READ-FAILURE affordances (HUI.loadingCard / HUI.loadingDots / HUI.loadRetry) ──
     ONE calm loading indicator — three purple dots that fade in sequence (human-chosen) — beside the words,
     and one honest, tappable read-failure line. Used by EVERY loading/error surface in the app (screen busy
     guards, the screenshot-OCR progress, the CSV sort, analysis drill panels) so they all read as one thing.
     Both respect reduced-motion. */
  /* opacity:0 + a delayed fade-in is a GRACE PERIOD: on a warm SWR cache the load resolves in a few ms and the
     card is replaced BEFORE the spinner ever becomes visible (no flicker); only a genuinely slow load shows it. */
  .hload { display: flex; align-items: center; gap: 12px; padding: 6px 2px; opacity: 0;
    animation: hload-fadein .25s ease .18s forwards; }
  @keyframes hload-fadein { to { opacity: 1; } }
  /* fading dots (human-chosen): three purple dots fading in sequence — fade only, no movement (the calmest). */
  .hload__spin { display: inline-flex; align-items: center; gap: 5px; flex: 0 0 auto; }
  .hload__spin i { width: 7px; height: 7px; border-radius: 50%; background: var(--accent-primary);
    opacity: .25; animation: hload-fade 1.2s ease-in-out infinite; }
  .hload__spin i:nth-child(2) { animation-delay: .2s; }
  .hload__spin i:nth-child(3) { animation-delay: .4s; }
  @keyframes hload-fade { 0%, 100% { opacity: .25; } 45% { opacity: 1; } }
  .hload__txt { color: var(--text-secondary); font-size: 14px; }
  @media (prefers-reduced-motion: reduce) {
    .hload { opacity: 1; animation: none; }
    .hload__spin i { animation: none; opacity: .7; }
  }
  /* ── v411 — THE BUSY BUTTON (HUI.btnBusy) ───────────────────────────────────────────────────────
     THE STANDING RULE: every non-optimistic action wears the active moving-dots state for its round
     trip — never a silent dead button. `disabled` alone greys a control and says nothing, which reads
     exactly like a tap that missed (the human's report on Create rule and on Undo). No new spinner:
     these ARE `.hload__spin`, the app's one loading visual, so they already stand their motion down
     under prefers-reduced-motion while the WORD stays. The button keeps its own size and colour — it
     is the same button, working — and the `.btn:disabled` dimming is lifted, because a control that is
     ACTIVELY doing the thing you asked for is not a control that is unavailable. */
  .btn--busy { cursor: default; display: inline-flex; align-items: center; justify-content: center; gap: 9px; }
  .btn.btn--busy:disabled, button.btn--busy:disabled { opacity: 1; }
  .btn--busy .btn__busydots i { width: 6px; height: 6px; background: currentColor; }
  .load-retry { cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
  .load-retry:hover { color: var(--text-primary); }
  .load-retry:focus-visible { outline: 2px solid var(--accent-primary); outline-offset: 2px; border-radius: 4px; }
  /* v357 — the fail-closed retry lines. NO new colour: every one of these is the shared muted .load-retry;
     these rules only place it where it now has to sit (a header slot, a modal body, a locked group). */
  .ctxslot .load-retry { margin: 0; font-size: 12px; max-width: 46vw; line-height: 1.25; }
  .cat-locked .load-retry, .cat-usagefail, .finish-loadfail, .profiles-pinfail { margin: 0 0 10px; }
  .cat-locked .load-retry { margin-bottom: 8px; }
  .notifpop__loadfail { margin: 0; padding: 12px 14px; font-size: 13px; border-bottom: 1px solid var(--border-subtle); }
  /* v359 — the intentions / goals / plans / forecast / archive / merchant-rules retry lines. Same rule as
     v357 above and it is worth restating: NO new colour is introduced here. Every one of these IS the
     shared muted .load-retry; these declarations only PLACE it (a card's chokepoint, a fund's draw-down
     list, a popover row). A second palette is how an honest line stops reading as the app's own voice. */
  .intent-legfail, .reserve-loadfail, .cal-loadfail, .arc-loadfail, .plans-loadfail, .mrule-loadfail { display: block; margin: 0 0 12px; font-size: 13px; }
  .intent-mirrorfail, .intent-trackunknown { display: block; margin: 8px 0 0; font-size: 13px; }
  .reserve-drawfail { display: block; margin: 0 0 8px; font-size: 12.5px; }
  .sugblock--unknown .sugblock__fail { display: block; margin: 6px 0 0; font-size: 13px; }
  .ana-saved__fail { display: block; padding: 12px 14px; font-size: 13px; }

  @media (max-width: 640px) {
    /* keep the indent shallow on a phone so the nested rows never push a column off-screen */
    .driftv-merchlist { margin-left: 8px; padding-left: 8px; }
    .drift-row__n { flex: 0 0 68px; font-size: 12px; }
    .drift-row__name { flex: 1 1 100%; }               /* the name gets its own line so no column is squeezed off */
    .drift-row { flex-wrap: wrap; }
    /* v392 — THESE TWO PHONE OVERRIDES ARE GONE, and the history is worth keeping because the second one
       is what the human eventually reported. The problem they solved was real: on a phone the bell sits
       mid-row, so a slot-anchored 340px popup would have hung off the LEFT edge (the page is
       overflow-x:clip, so it would have silently clipped rather than scrolled). The answer at the time was
       to drop `.notifslot` out of the positioning chain and let the popup resolve against `.app__header`
       and span it — but the phone header CONTAINS THE NAV, so `top: 100%` of it is the top of the content
       area, and the panel arrived nowhere near the bell.
       The replacement does not choose an ancestor at all: notify.js measures the bell and places a fixed
       panel against it, clamping into the viewport, which is the thing an ancestor could never do. See
       `.notifpop` above. Nothing phone-specific is needed any more — the clamp IS the phone case. */
  }

  /* ══ Settings → Export (Bible §26, slice A) ═══════════════════════════════════════════════════════
     Palette notes: purple is the primary action (Export / Download all); teal marks what is INCLUDED;
     the outgoing-money token is not used here (no amounts are rendered); amber appears nowhere, because
     nothing on this screen is destructive — an export only reads. */
  .exp-title { margin: 0 0 4px; }
  .exp-sub { margin: 0 0 14px; }
  .exp-ctx { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--text-secondary);
    background: var(--bg-elevated); border: 1px solid var(--border-default); border-radius: 10px;
    padding: 9px 12px; margin: 0 0 14px; }
  .exp-ctx__dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent-primary); flex: 0 0 auto; }
  .exp-card { margin-bottom: 14px; }
  .exp-h3 { font-family: 'Syne', sans-serif; font-weight: 700; font-size: 14.5px; margin: 0 0 8px; }

  .exp-inc { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 6px;
    font-size: 13px; color: var(--text-secondary); }
  .exp-inc li { display: flex; gap: 8px; align-items: baseline; min-width: 0; }
  .exp-inc__tick { color: var(--accent-teal); flex: 0 0 14px; }
  .exp-inc__what { min-width: 0; }
  .exp-inc__file { color: var(--text-tertiary); font-size: 11.5px; margin-left: auto;
    white-space: nowrap; flex: 0 0 auto; padding-left: 8px; }

  .exp-seg { display: flex; border: 1px solid var(--border-default); border-radius: 9px;
    overflow: hidden; margin: 12px 0 4px; }
  .exp-seg__b { flex: 1 1 0; min-width: 0; background: var(--bg-input); border: 0; color: var(--text-secondary);
    font: inherit; font-size: 12px; padding: 9px 4px; cursor: pointer; white-space: nowrap; }
  .exp-seg__b + .exp-seg__b { border-left: 1px solid var(--border-default); }
  .exp-seg__b:hover { background: var(--bg-hover); color: var(--text-primary); }
  .exp-seg__b[aria-pressed="true"] { background: var(--accent-primary); color: #fff; }
  .exp-custom { margin: 10px 0 2px; }

  .exp-hint { font-size: 11.5px; margin: 6px 0 0; }
  .exp-tert { font-size: 12px; margin: 9px 0 0; text-align: center; }
  /* one size down from .btn — the export is a deliberate act, not the screen's centrepiece (human, v316);
     v317 extends the same size to the done-stage buttons (zip · start over · open in Drive) on their ask */
  .exp-go { margin-top: 12px; }
  .exp-go, .exp-zip, .exp-again { font-size: 14px; padding: 9px 16px; font-weight: 500; }
  .exp-again { margin-top: 12px; }

  .exp-veil .exp-veil__p { margin: 0 0 7px; font-size: 13px; color: var(--text-secondary); }
  .exp-veil__p--last { margin: 8px 0 0; }
  .exp-veil__row { display: flex; gap: 8px; align-items: baseline; font-size: 13px;
    color: var(--text-secondary); padding: 6px 0; border-top: 1px solid var(--border-subtle); }
  .exp-veil__mask { color: var(--text-tertiary); letter-spacing: 2px; }
  .exp-veil__amt { margin-left: auto; color: var(--text-tertiary); font-size: 12px; }
  .exp-note { background: var(--accent-glow); border: 1px solid rgba(124, 106, 255, .3); border-radius: 12px;
    padding: 11px 13px; font-size: 13px; color: var(--text-secondary); margin: 0 0 14px; }

  .exp-prog { display: flex; align-items: center; gap: 10px; font-size: 13.5px; color: var(--text-secondary); }
  .exp-prog__txt { min-width: 0; }
  /* A real TRACK, visibly distinct from the card behind it — the "a bar needs a track" rule. Square
     corners, no minimum fill: an empty track already says "nothing yet" honestly. */
  .exp-prog__bar { height: 6px; background: var(--bg-input); border: 1px solid var(--border-default);
    margin: 12px 0 4px; overflow: hidden; }
  .exp-prog__bar i { display: block; height: 100%; width: 8%; background: var(--accent-primary);
    transition: width .25s ease; }

  .exp-done__t { display: flex; gap: 8px; align-items: center; color: var(--accent-teal);
    font-size: 14px; margin-bottom: 10px; }
  .exp-done__tick { flex: 0 0 auto; }
  .exp-zip { margin-top: 0; }

  .exp-files { margin: 10px 0 0; padding: 0; list-style: none; font-size: 13px; }
  .exp-files li { display: flex; gap: 8px; align-items: baseline; padding: 8px 0;
    border-top: 1px solid var(--border-subtle); min-width: 0; }
  .exp-files li:first-child { border-top: 0; }
  .exp-files__name { font-size: 12.5px; text-align: left; min-width: 0; overflow-wrap: anywhere; }
  .exp-files__meta { margin-left: auto; color: var(--text-tertiary); font-size: 11.5px;
    font-variant-numeric: tabular-nums; white-space: nowrap; flex: 0 0 auto; padding-left: 8px; }

  /* ══ the REPORTS card (slice C) ═══════════════════════════════════════════════════════════════════
     Three rows on the Export screen; the PAGES they open carry their own inline CSS (they are standalone
     documents on white and cannot see this stylesheet — see report.js). Purple is the action, as it is on
     the export card above; no amount is rendered here, so no money colour appears. */
  .rep-list { margin: 0; padding: 0; list-style: none; }
  .rep-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; min-width: 0;
    padding: 9px 0; border-top: 1px solid var(--border-subtle); }
  .rep-row:first-child { border-top: 0; padding-top: 2px; }
  .rep-row__name { font-size: 13.5px; color: var(--text-primary); min-width: 0; }
  .rep-row__ctl { display: flex; align-items: center; gap: 10px; margin-left: auto; min-width: 0; }
  .rep-row__meta { font-size: 12.5px; color: var(--text-tertiary); white-space: nowrap;
    overflow: hidden; text-overflow: ellipsis; min-width: 0; }
  .rep-open { font-size: 12.5px; white-space: nowrap; flex: 0 0 auto; }
  .rep-open:disabled { opacity: .55; cursor: default; }
  /* the shared month picker, shrunk to sit inline: the trigger is normally a full-width field */
  .rep-mp { min-width: 0; }
  .rep-mp .dropdown__trigger { width: auto; min-width: 0; padding: 7px 10px; font-size: 12.5px; }
  .rep-mp .dropdown__value { overflow: hidden; text-overflow: ellipsis; }
  /* `[hidden]` is a UA rule at element specificity, so a class that sets `display:flex` silently beats it —
     without this line the empty status box draws as a stray dashed rectangle under the hint (caught on the
     390px screenshot, not by any behavioural assertion). Any hidden-by-default flex box needs this pair. */
  .rep-status[hidden] { display: none; }
  .rep-status { display: flex; flex-direction: column; gap: 2px; margin-top: 10px;
    border: 1px dashed var(--border-strong); border-radius: 10px; padding: 10px 12px;
    font-size: 12.5px; color: var(--text-secondary); }
  .rep-status b { color: var(--text-primary); font-weight: 500; }

  .exp-empty, .exp-retry { color: var(--text-secondary); font-size: 13.5px; text-align: center; padding: 16px 6px; }
  .exp-retry { border: 1px dashed var(--border-strong); border-radius: 11px; }
  .exp-empty b, .exp-retry b { display: block; color: var(--text-primary); font-weight: 500; margin-bottom: 4px; }

  @media (max-width: 640px) {
    /* At 390px a four-way segment plus a filename column is the tight spot: let the file name wrap onto its
       own line rather than pushing the size off the edge, and shrink the segment text a touch. */
    .exp-seg__b { font-size: 11.5px; padding: 9px 2px; }
    /* the Export / Download-all buttons read outsized on a phone (human review of v309) — one calm step down */
    .exp-go, .exp-zip { font-size: 13.5px; padding: 9px 14px; }
    /* the filename ALWAYS drops to its own line here, so the list reads as five identical two-line entries
       rather than four wrapped ones and one that happened to fit */
    .exp-inc li { flex-wrap: wrap; }
    .exp-inc__what { flex: 1 1 auto; }              /* stays beside its tick */
    /* always its own line, indented under the label. The indent is PADDING, not margin: a `flex-basis:100%`
       item plus a margin measures wider than its row and the row becomes its own tiny scroller. */
    .exp-inc__file { flex: 0 0 100%; margin-left: 0; padding-left: 22px; }
    .exp-files li { flex-wrap: wrap; }
    .exp-files__meta { margin-left: 0; padding-left: 0; flex-basis: 100%; }
    /* A report row is name + control + "open →" — three things that do not fit one 390px line, so the
       controls drop to their own row under the name rather than squeezing the month out of the picker. */
    .rep-row__ctl { margin-left: 0; flex-basis: 100%; justify-content: space-between; }
    .rep-mp .dropdown__trigger { padding: 7px 9px; font-size: 12px; }
  }

  /* ══ WEEK AHEAD — the week-ahead picture (§16/§19), rebuilt from the ratified mock ════════════════
     `tools/week-ahead-clean.src.html`, human-ratified 2026-09-03: filled drawn icons ("I like the
     filled icons") · no motion at all ("keep it still, no motion required at all") · the NEXT SEVEN
     DAYS with today first · tap a day · the collapsed card on Home · no legend · no other-people's-weeks.

     THE STRIP IS THE WEATHER, and the words beside it are plain — that split is the whole ratified
     decision ②. v426 lays a day out the way a weather app lays out an hour: NO box, NO border, NO card
     around a day. Label, icon, figure, stacked and centred, seven across. Everything that used to draw
     the weather in CSS — the bordered columns, the falling droplets, the rolling mist — is GONE, because
     the icon now IS the weather. It is an inline SVG built in weekahead.js (`IC`), so it draws the same
     picture on every device instead of depending on which emoji font that device happens to ship (a
     platform missing U+1F32B drew an empty tofu box where the fog should be).

     ⚠ NOTHING IN THIS BLOCK ANIMATES, and that is a decision, not an omission. The human's words:
     "keep it still, no motion required at all". So there is no keyframe rule here, no animation
     property and no transition — and, the part worth stating so nobody reads it as a gap, no
     reduced-motion stand-down either, because there is nothing here to stand down. A future round
     that adds motion here must add the reduced-motion block in the same change.
     (The suite asserts all four of those by grepping this block, so the words above deliberately name
     them in prose rather than in code — a comment that quotes a property is a comment that trips it.)

     NO LEGEND. A sun, a cloud, rain and a bright sun over a teal figure need no key — and the tap-a-day
     line under the strip replaces one with something better: the legend told you what a cloud MEANS,
     the line tells you what THIS cloud IS.

     NO position:fixed ANYWHERE in this block — deliberately. A fixed descendant of #view is re-parented
     by the 210ms enter transform (the v417 ⑦ trap) and would have to join the stand-down block; this
     screen has nothing that needs to leave the flow, so it never earns that debt.

     PALETTE: only tokens the app already defines. Outgoing money is --accent-out, income --accent-teal,
     today's label and the card's link --accent-primary, the sun and its rays --accent-gold, and every
     neutral inside an icon (the cloud body, the rain, the fog lines) is --text-secondary — the same
     token the v425 droplets already wore. No new colour, no raw hex, no red. */
  .wk-topbar { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; margin: 0 0 12px; }
  .wk-topbar__back { color: var(--accent-primary); font-size: 14px; flex: 0 0 auto; }
  .wk-topbar__title { margin: 0; flex: 1 1 auto; min-width: 0; }
  .wk-topbar__who { flex: 0 0 auto; }
  .wk { padding: 18px; }
  .wk__range { font-size: 12px; letter-spacing: 1px; text-transform: uppercase; color: var(--text-tertiary); }
  .wk__verdict { font-size: 16.5px; line-height: 1.45; margin: 6px 0 2px; text-wrap: balance; color: var(--text-primary); }
  .wk__rowh { font-size: 11.5px; letter-spacing: 1.1px; text-transform: uppercase; color: var(--text-tertiary); margin: 18px 0 6px; }

  /* ── THE STRIP: seven borderless columns ─────────────────────────────────────────────────────── */
  /* the payday glow needs an ALPHA, and a colour token cannot carry one — so --accent-gold's own value
     at half strength is stated ONCE, here, as a local variable, rather than re-typed at each use */
  .wk-beam { --wk-glow: rgba(232, 197, 71, .5); display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; margin: 16px 0 6px; }
  /* ③ every column is a real <button>, so Enter and Space toggle it for free and a screen reader
     announces the pressed state without any extra wiring */
  .wk-beam__day { appearance: none; background: none; border: none; font-family: inherit; cursor: pointer;
    display: flex; flex-direction: column; align-items: center; gap: 6px; min-width: 0; padding: 6px 0 4px; border-radius: 10px; }
  .wk-beam__day--sel { background: var(--bg-elevated); }
  .wk-beam__day:focus-visible { outline: 2px solid var(--accent-primary); outline-offset: 1px; }
  /* v430 R1 — THE TAP LANDS ON THE DAY, NEVER ON THE CARD (human, on their own phone: *"if I tap any
     of the days, the whole card kind of gets fleetingly highlighted … it's just weird on the eye"*).
     The whole `.wk-card` is a click target (it is the open/close toggle), so a phone paints ITS
     platform tap-highlight rectangle over the entire card the instant a finger lands anywhere inside
     it — including on a day column, whose own tap the card never sees because the column stops the
     click propagating. Suppressing the platform rectangle on the three surfaces the finger can meet,
     and giving the DAY a real pressed state instead, moves the feedback onto the thing that was
     actually tapped. Same treatment as the analysis bars (.ana-bdrow--peek) and .hui-charthit.
     There is deliberately no transition here: the block still animates nothing (the v426 rule). */
  .wk-card, .wk-card__head, .wk-beam__day { -webkit-tap-highlight-color: transparent; touch-action: manipulation; }
  .wk-beam__day:active { background: var(--bg-elevated); }
  /* v427 ① THE DAY LABEL IS READ, SO IT IS LEGIBLE (human, on their own phone: "a little bit too light
     to read right now"). The tertiary ink on the card surface measures about 3.3:1 — under the 4.5:1
     line — and this is the hardest shape to read there: small, uppercase, letter-spaced. The secondary
     ink measures about 6.9:1 on the same ground (both ratios are recomputed from the RESOLVED colours
     in verify-weekahead --only screen, so they cannot quietly go stale). The SIZE does not move — seven
     columns share a 390px phone — and today's label keeps its purple: only the resting colour and the
     weight change. */
  .wk-beam__dl { font-size: 11px; letter-spacing: .4px; text-transform: uppercase; color: var(--text-secondary); font-weight: 600; }
  .wk-beam__day--today .wk-beam__dl { color: var(--accent-primary); font-weight: 700; }
  .wk-beam__ic { width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; }
  .wk-beam__ic svg { width: 30px; height: 30px; display: block; overflow: visible; }
  /* FILLED, the human's pick — at phone size an outline cloud goes thin and loses its rain */
  .wk-beam__ic .wk-ic__cl { fill: var(--text-secondary); stroke: none; }
  .wk-beam__ic .wk-ic__sunb { fill: var(--accent-gold); stroke: none; }
  .wk-beam__ic .wk-ic__ray { stroke: var(--accent-gold); stroke-width: 2.2; stroke-linecap: round; fill: none; }
  .wk-beam__ic .wk-ic__drop { stroke: var(--text-secondary); stroke-width: 2.2; stroke-linecap: round; fill: none; }
  .wk-beam__ic .wk-ic__fogl { stroke: var(--text-secondary); stroke-width: 2.4; stroke-linecap: round; fill: none; }
  /* a cloud sitting IN FRONT of a sun needs a rim in the surface colour, or the two shapes merge into
     one silhouette and the sun-shower reads as a lumpy cloud */
  .wk-beam__ic .wk-ic__cl--front { stroke: var(--bg-surface); stroke-width: 2.2; paint-order: stroke; }
  /* A CLEAR DAY RECEDES. On an ordinary week five of the seven days have nothing due, and at full size
     five bright suns are the loudest thing on the screen while saying the least — they are the
     background state, not the news. Payday is the SAME plain sun at full size, carried by the gold glow
     and the teal figure; the face-sun is deliberately never used. */
  .wk-beam__day--sun .wk-beam__ic { opacity: .55; }
  .wk-beam__day--sun .wk-beam__ic svg { width: 24px; height: 24px; }
  .wk-beam__day--pay .wk-beam__ic { filter: drop-shadow(0 0 6px var(--wk-glow)); }
  .wk-beam__amt { display: block; font-size: 10.5px; font-family: 'DM Mono', ui-monospace, monospace;
    font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--accent-out); min-height: 13px; font-weight: 400; }
  .wk-beam__amt--in { color: var(--accent-teal); }
  /* ⑨(c) a clear day carries NO figure — never "A$0". The empty slot still reserves its line so the
     seven icons sit on one row whatever the week holds. */
  .wk-beam__amt--none { min-height: 13px; }
  /* DM Mono ships no weight above 500 — a 600 here would be FAUX-bolded and read as smeared. 500 is its
     real heaviest, and it is what a payday and a big-bill day get. */
  .wk-beam__day--pay .wk-beam__amt, .wk-beam__day--storm .wk-beam__amt { font-weight: 500; }
  /* on a laptop the seven columns would otherwise stretch to 150px each and read as an empty grid — the
     strip is a phone-shaped thing and stays one */
  @media (min-width: 641px) { .wk-beam { max-width: 720px; } }
  /* ③ the line that replaced the legend */
  .wk-dayline { font-size: 13px; color: var(--text-secondary); min-height: 18px; margin: 0 2px 4px; line-height: 1.4; }
  .wk-dayline__d { color: var(--text-primary); font-weight: 500; }

  /* the two dated lists */
  .wk-it { display: flex; align-items: baseline; gap: 9px; padding: 8px 2px; border-bottom: 1px solid var(--border-subtle); }
  .wk-it:last-child { border-bottom: none; }
  /* v430 R2 — A ROW THAT BELONGS TO THE TAPPED DAY. Their words, with the card open: *"the coming out
     and coming in lines are there anyway … it's just repetition. If the card is expanded and I tap on
     any of the days, highlight the row for that day."* So it is the ROWS that answer, not a line that
     re-reads them. The lift is the SAME elevated surface a selected day column takes — a neutral
     surface, never a money colour: a selection is not a fact about the money. The negative margin +
     matching padding let the highlight breathe past the row's own 2px inset without moving anything. */
  .wk-it--sel { background: var(--bg-elevated); border-radius: 8px; margin: 0 -6px; padding-left: 8px; padding-right: 8px; }
  .wk-it__d { flex: 0 0 52px; font-size: 12px; color: var(--text-tertiary); }
  .wk-it__n { flex: 1 1 auto; min-width: 0; font-size: 14.5px; line-height: 1.3; color: var(--text-primary); }
  .wk-it__hint { display: block; font-size: 12px; color: var(--text-tertiary); margin-top: 1px; }
  .wk-it__a { flex: 0 0 auto; font-size: 14.5px; color: var(--accent-out); font-family: 'DM Mono', ui-monospace, monospace; font-weight: 500; font-variant-numeric: tabular-nums; white-space: nowrap; }
  .wk-it__a--in { color: var(--accent-teal); }
  .wk-it__a--none { font-size: 12.5px; font-family: inherit; }
  /* §19: a payment plan gets a little more prominence than an ordinary bill */
  .wk-it--plan .wk-it__n { font-weight: 600; }
  .wk-it--plan .wk-it__d { color: var(--text-secondary); }
  /* A MONTH-LEVEL LINE IS NOT A DATED ROW AND IS NOT SHAPED LIKE ONE. It has no date column, and its
     “a month” rider is long: forced into a dated row it crushed the name to a five-line wrap. Two lines
     of its own instead — the fact, then the reason it has no day. */
  .wk-monthrow { padding: 8px 2px; border-bottom: 1px solid var(--border-subtle); }
  .wk-monthrow:last-child { border-bottom: none; }
  .wk-monthrow__top { display: flex; align-items: baseline; gap: 9px; }
  .wk-monthrow__nm { flex: 1 1 auto; min-width: 0; font-size: 14.5px; color: var(--text-secondary); }
  .wk-monthrow__a { flex: 0 0 auto; font-size: 13.5px; color: var(--accent-teal); font-family: 'DM Mono', ui-monospace, monospace; font-variant-numeric: tabular-nums; white-space: nowrap; }
  .wk-monthrow__hint { font-size: 12px; color: var(--text-tertiary); line-height: 1.45; margin-top: 2px; }
  /* (f) a thing YOU added is MARKED — never "confirmed", never "matched" */
  .wk-it__badge { display: inline-block; font-size: 10.5px; letter-spacing: .6px; text-transform: uppercase; color: var(--accent-primary); border: 1px solid var(--border-default); border-radius: 12px; padding: 0 6px; margin-left: 5px; vertical-align: 1px; }
  .wk-empty { padding: 10px 2px; line-height: 1.5; margin: 0; }
  /* (g) a rhythm the app has SPOTTED and nobody has confirmed. Tertiary and NOT row-shaped on purpose:
     it has no date column and no amount column, because it is a question rather than a fact and must
     not be able to read as one of the dated rows above it. */
  .wk-maybe { font-size: 13px; line-height: 1.5; margin: 8px 2px 0; }
  .wk-maybe__link { font-size: 13px; color: var(--accent-primary); }

  /* the everyday band, and — ⑨(b) — the lumps it refused to smear, now BENEATH its box rather than
     inside it: an aside about what the number cannot hold is not part of the number's own claim */
  .wk-band { background: var(--bg-elevated); border-radius: 11px; padding: 11px 12px; margin: 14px 0 0; }
  .wk-band__line { font-size: 13.5px; color: var(--text-secondary); line-height: 1.5; margin: 0; }
  .wk-band__line .amt-nb { color: var(--text-primary); }
  .wk-band__prov { color: var(--accent-gold); font-size: 12px; }
  .wk-band__lump { font-size: 13px; color: var(--text-tertiary); line-height: 1.5; margin: 8px 2px 0; }

  /* the intentions strip — ceilings only. A ceiling is the one shape that escalates: teal with room,
     gold as it nears, amber once above. Never red (red is the zero line), and never a floor here. */
  .wk-int { display: flex; align-items: center; gap: 9px; padding: 7px 2px; font-size: 13.5px; }
  .wk-int__nm { flex: 0 0 104px; min-width: 0; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .wk-int__track { flex: 1 1 auto; min-width: 0; height: 6px; border-radius: 3px; background: var(--bg-input); overflow: hidden; }
  .wk-int__fill { height: 100%; border-radius: 3px; background: var(--accent-teal); }
  .wk-int--near .wk-int__fill { background: var(--accent-gold); }
  .wk-int--over .wk-int__fill { background: var(--accent-warm); }
  .wk-int__st { flex: 0 0 auto; font-size: 12.5px; color: var(--accent-teal); font-variant-numeric: tabular-nums; white-space: nowrap; }
  .wk-int--near .wk-int__st { color: var(--accent-gold); }
  .wk-int--over .wk-int__st { color: var(--accent-warm); }

  /* ── ⑥ THE HOME CARD ─────────────────────────────────────────────────────────────────────────────
     Collapsed by default: the strip and one sentence, a glance. Tapping opens it in place to the two
     dated lists, with a link to the full screen; the everyday band and the month's intentions stay
     there. The HOST is what holds the reserved height while the picture loads (weekahead.js `room`),
     so the month picker and the money below it never jump when the read lands. */
  .wk-cardhost { display: block; }
  .wk-card { padding: 12px 14px; cursor: pointer; }
  .wk-card__head { appearance: none; background: none; border: none; padding: 0; width: 100%; text-align: left;
    font-family: inherit; cursor: pointer; display: flex; align-items: center; gap: 8px; }
  .wk-card__t { flex: 1 1 auto; min-width: 0; font-size: 11.5px; letter-spacing: 1.1px; text-transform: uppercase; color: var(--text-tertiary); }
  .wk-card__chev { flex: 0 0 auto; display: flex; color: var(--text-tertiary); }
  /* the chevron TURNS rather than animating — see the no-motion note at the top of this block */
  .wk-card--open .wk-card__chev { transform: rotate(180deg); }
  /* the card's strip keeps the SAME 720px laptop cap the full screen's does (it inherits it — the
     override that removed it is deliberately absent). Caught by the eyeball gate: spread across a
     1100px Home the seven days sat a finger apart with 26px icons adrift between them, and the
     figure under TODAY ended up alone at the far left of the card. */
  .wk-card .wk-beam { margin: 8px 0 4px; }
  .wk-card .wk-beam__ic, .wk-card .wk-beam__ic svg { width: 26px; height: 26px; }
  .wk-card .wk-beam__day--sun .wk-beam__ic svg { width: 21px; height: 21px; }
  .wk-card .wk-dayline { font-size: 12.5px; min-height: 17px; }
  .wk-card .wk-dayline--empty { display: none; }   /* quiet until a day is tapped (main-loop rider) */
  .wk-card__sent { font-size: 14px; line-height: 1.45; margin: 6px 0 0; color: var(--text-primary); }
  .wk-card__more { border-top: 1px solid var(--border-subtle); margin-top: 10px; padding-top: 4px; }
  .wk-card__more .wk__rowh { margin-top: 10px; }
  .wk-card__link { display: inline-block; color: var(--accent-primary); font-size: 13.5px; margin-top: 12px; }

  @media (max-width: 640px) {
    .wk { padding: 14px; }
    .wk__verdict { font-size: 15.5px; }
    .wk-beam { gap: 1px; }
    .wk-beam__day { padding: 5px 0 4px; gap: 5px; }
    .wk-beam__ic, .wk-beam__ic svg { width: 27px; height: 27px; }
    .wk-beam__day--sun .wk-beam__ic svg { width: 22px; height: 22px; }
    /* seven money figures share a 390px row; the label drops a touch so none of them can wrap */
    .wk-beam__amt { font-size: 9.5px; }
    .wk-beam__dl { font-size: 10px; }
    .wk-it__d { flex-basis: 46px; font-size: 11.5px; }
    .wk-it__n { font-size: 14px; }
    .wk-it__a { font-size: 14px; }
    /* the intention name stops taking a third of a 390px row */
    .wk-int__nm { flex-basis: 84px; font-size: 13px; }
    .wk-int__st { font-size: 12px; }
    .wk-card { padding: 11px 12px; }
    .wk-card .wk-beam__ic, .wk-card .wk-beam__ic svg { width: 25px; height: 25px; }
    .wk-card .wk-beam__day--sun .wk-beam__ic svg { width: 20px; height: 20px; }
  }

  /* ── DEMO MODE — the ONE piece of chrome demo mode adds (js/demo-backend.js) ────────────────────────
     A slim line above the header saying what this is and offering a way back to the start. It is the
     whole visible difference between the demo and the app: no screen, control or figure changes, because
     the point of the demo is that you are looking at the real thing. Palette tokens only — the purple is
     the brand/primary family (§24), never a semantic money colour. */
  .demo-bar { display: flex; align-items: center; gap: 10px 12px; flex-wrap: wrap;
    margin: 0 0 16px; padding: 8px 12px; border-radius: 10px;
    background: var(--bg-elevated); border: 1px solid var(--border-default);
    color: var(--text-secondary); font-size: 13px; line-height: 1.35; }
  .demo-bar__msg { flex: 1 1 200px; min-width: 0; }
  .demo-bar__btn { flex: 0 0 auto; cursor: pointer; font: inherit; font-size: 13px;
    padding: 5px 12px; border-radius: 999px; border: 1px solid var(--border-strong);
    background: transparent; color: var(--accent-primary);
    transition: background .12s, color .12s, border-color .12s; }
  .demo-bar__btn:hover { background: var(--bg-hover); color: var(--text-primary); border-color: var(--accent-primary); }
  .demo-bar__btn:focus-visible { outline: 2px solid var(--accent-primary); outline-offset: 2px; }
  /* 390px: the sentence and the button stack rather than squeezing the words to two per line. */
  @media (max-width: 430px) {
    .demo-bar { padding: 8px 10px; font-size: 12.5px; margin-bottom: 14px; }
    .demo-bar__msg { flex-basis: 100%; }
    .demo-bar__btn { font-size: 12.5px; padding: 4px 11px; }
  }

  /* ══ THE WALL (v437, js/wall.js) ═══════════════════════════════════════════════════════════════════
     The paywall. It REPLACES the app rather than sitting over it: `.app` is display:none for as long as
     the layer is up, so nothing underneath is drawn, tappable, tabbable or readable by a screen reader —
     a wall you can still read your figures through is not a wall.

     THE LAYER LIVES ON <body>, not inside #view, and that is load-bearing rather than tidy: `view--enter-*`
     puts a TRANSFORM on #view for 210ms after a swipe, and a transformed element becomes the containing
     block for every position:fixed descendant (conventions-frontend.md, v417 ⑦). z-index 4000 sits above
     every layer the app owns (pinned bars 30/40, header 50, ledger panel 59–61, popovers 100/200/300,
     emoji sheet 1199/1200, drift tip 3000).

     NO NEW COLOURS. Purple is the primary action (§24, brand/primary family), teal is the "it worked"
     tick, gold is the trial's last three days in Settings. Amber and red appear nowhere on this screen:
     a trial ending is not an error and not a below-zero boundary. */
  body.is-walled .app { display: none; }
  body.is-walled { overflow: hidden; }

  .wall-layer { position: fixed; inset: 0; z-index: 4000; overflow-y: auto;
    background: var(--bg-base); color: var(--text-primary);
    display: flex;
    padding: 24px 20px calc(24px + env(safe-area-inset-bottom, 0px)); }
  .wall-layer:focus { outline: none; }
  /* `margin: auto` centres on BOTH axes and — unlike `align-items: center` — degrades correctly when
     the content is taller than the viewport: the margins resolve to 0 and the layer scrolls, instead
     of the top of the screen being clipped above the scroll origin. */
  .wall { width: 100%; max-width: 420px; margin: auto; text-align: left; }

  .wall__mark { width: 38px; height: 38px; border-radius: 11px; margin: 0 0 14px;
    display: grid; place-items: center; font-size: 19px;
    background: var(--accent-glow); border: 1px solid rgba(124, 106, 255, .35); color: var(--accent-primary); }
  .wall__mark--ok { background: rgba(62, 207, 170, .14); border-color: rgba(62, 207, 170, .35); color: var(--accent-teal); }
  .wall__busy { margin: 0 0 14px; min-height: 22px; display: flex; align-items: center; }

  .wall__title { font-family: 'Syne', 'DM Sans', sans-serif; font-size: 22px; font-weight: 700;
    letter-spacing: -.3px; line-height: 1.25; margin: 0 0 9px; color: var(--text-primary); text-wrap: balance; }
  .wall__lead { font-size: 14.5px; line-height: 1.5; color: var(--text-secondary); margin: 0 0 18px; }
  .wall__foot { font-size: 12.5px; line-height: 1.6; color: var(--text-tertiary); margin: 0; }
  .wall__tiny { font-size: 12px; color: var(--text-tertiary); margin: 0 0 10px; }
  .wall__needs { font-size: 11.5px; color: var(--text-tertiary); text-align: center; margin: -4px 0 12px; }
  /* the calm one-liner under the acts: the shop is not open yet, a download that did not work. It is
     STATUS, not alarm — tertiary text, no colour, no icon. */
  .wall__note { font-size: 13px; line-height: 1.5; color: var(--text-secondary); margin: 0 0 14px;
    padding: 10px 12px; border-radius: 10px; background: var(--bg-elevated); border: 1px solid var(--border-subtle); }

  .wall__acts { display: grid; gap: 9px; margin: 0 0 14px; }
  /* The wall's buttons are full-width stacked, so they need no per-button width guessing on a phone.
     `.btn` is already the purple primary and `.btn--ghost` the quiet one; `--second` is the neutral
     bordered middle the mock draws, scoped to the wall so no other screen gains a fourth button style. */
  .wall-act { width: 100%; display: block; text-align: center; min-height: 46px; }
  .wall-act.btn--second { background: var(--bg-input); border-color: var(--border-default); color: var(--text-primary); }
  .wall-act.btn--second:hover { background: var(--bg-hover); border-color: var(--border-strong); }
  .wall-act:disabled { opacity: .4; cursor: not-allowed; }

  .wall-strip { margin: 0 0 16px; padding: 11px 13px; border-radius: 0 10px 10px 0;
    background: var(--bg-elevated); border: 1px solid var(--border-subtle);
    border-left: 3px solid var(--text-tertiary); }
  .wall-strip p { margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--text-secondary); }
  .wall-strip--teal { border-left-color: var(--accent-teal); }

  .wall-fld { margin: 0 0 10px; }
  .wall-fld label { display: block; font-size: 12px; font-weight: 600; color: var(--text-secondary); margin-bottom: 6px; }
  .wall-fld__in { width: 100%; box-sizing: border-box; padding: 12px 13px; border-radius: 10px;
    background: var(--bg-input); border: 1px solid var(--border-default); color: var(--text-primary);
    font-family: 'DM Mono', ui-monospace, monospace; font-size: 14px; }
  .wall-fld__in:focus { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-glow); }
  /* A key that does not fit the shape is not an ERROR anyone made — it is amber only in the sense §3
     reserves amber for feedback, never red, and the sentence beside it describes the shape. */
  .wall-fld__in--bad { border-color: var(--accent-warm); }
  .wall-fld__in--ok { border-color: var(--accent-teal); }
  .wall-msg { display: flex; gap: 7px; align-items: flex-start; font-size: 13px; line-height: 1.5; margin: -2px 0 12px; }
  .wall-msg--bad { color: var(--accent-warm); }
  .wall-msg--ok { color: var(--accent-teal); font-weight: 600; }

  .wall__linkline { text-align: center; margin: 2px 0 0; }
  .wall__link { background: none; border: 0; cursor: pointer; font: inherit; font-size: 12.5px; font-weight: 500;
    color: var(--text-tertiary); padding: 9px 4px; min-height: 40px; border-radius: 8px;
    text-decoration: underline; text-underline-offset: 3px; }
  .wall__link:hover { color: var(--accent-primary); }
  .wall__link:focus-visible { outline: 2px solid var(--accent-primary); outline-offset: 2px; }

  /* 390px: the layer already fills the screen and the column already caps at 420px, so a phone needs
     only the title to stop shouting and the padding to give a little back. */
  @media (max-width: 430px) {
    .wall-layer { padding: 18px 16px calc(18px + env(safe-area-inset-bottom, 0px)); }
    .wall__title { font-size: 20px; }
    .wall__lead { font-size: 14px; }
  }

  /* ── the trial row in Settings (js/settings.js) ────────────────────────────────────────────────── */
  .trial-row__big { font-family: 'Syne', 'DM Sans', sans-serif; font-size: 19px; font-weight: 700;
    color: var(--text-primary); margin: 0 0 6px; }
  /* GOLD in the last three days: the milestone/approaching colour (§3). Never amber, never red — a
     trial running down is not an error and not a below-zero boundary. */
  .trial-row__big--near { color: var(--accent-gold); }
  .trial-row__pay { margin-top: 4px; }
  .trial-row__note { font-size: 12px; margin: 9px 0 0; }

  /* ══ THE SET-UP WIZARD (v438, setup.html + js/setup.js) ══════════════════════════════════════════
   * `setup.html` is a page of its own on this origin, not a screen inside the app — so it does NOT use
   * `.app` / `.view`, and its column is its own (`.setup-page`, capped at 660px, the width the approved
   * mock was read at). Everything else it borrows: the tokens, the fonts, `.btn`, `.brand`, the root
   * scrollbar dressing. Nothing below introduces a raw hex colour and the suite asserts that — the ring
   * is `--accent-primary` and its halo is `--accent-glow`, which is the token that colour already has.
   *
   * ⚠ `.setup-shot__wrap` MUST hug its image. A ring is a percentage box positioned against the WRAPPER,
   * so the moment an `object-fit` or a fixed aspect box comes between the two, every ring slides off the
   * button it is pointing at — silently, and it still looks like a ring. That is a real bug the mock hit
   * once in its sales strip, and poc/verify-wizard.mjs asserts the invariant on every picture on screen.
   * ═══════════════════════════════════════════════════════════════════════════════════════════════ */
  .setup-page { width: min(100%, 660px); margin: 0 auto; padding: 22px 16px calc(40px + env(safe-area-inset-bottom, 0px)); }
  .setup-brand { margin: 0 0 14px; }
  .setup-sitelink { display: inline-block; margin: 0 0 10px; font-size: 14px; color: var(--text-secondary); text-decoration: none; }
  .setup-sitelink:hover { color: var(--text-primary); }
  .demo-bar__site { text-decoration: none; }
  .setup { background: var(--bg-surface); border: 1px solid var(--border-default);
    border-radius: var(--radius); overflow: hidden; }

  /* the head: the ONE choice the buyer makes, made once, plus a counter that can never read "step 0 of 6" */
  .setup__top { position: sticky; top: 0; z-index: 20; background: var(--bg-elevated);
    border-bottom: 1px solid var(--border-default); padding: 10px 14px 0; }
  .setup__toprow { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
  .setup-seg { display: inline-flex; background: var(--bg-input); border: 1px solid var(--border-default);
    border-radius: 9px; padding: 3px; }
  .setup-seg button { appearance: none; border: 0; background: transparent; color: var(--text-secondary);
    font-family: inherit; font-size: 12.5px; font-weight: 600; padding: 8px 11px; border-radius: 7px;
    white-space: nowrap; cursor: pointer; min-height: 36px; transition: background .14s ease, color .14s ease; }
  .setup-seg button.is-on { background: var(--accent-primary); color: var(--on-accent-primary); }
  .setup-seg button:not(.is-on):hover { background: var(--bg-hover); color: var(--text-primary); }
  .setup-seg button:focus-visible { outline: 2px solid var(--accent-primary); outline-offset: 2px; }
  .setup-count { margin-left: auto; font-family: 'Syne', 'DM Sans', sans-serif; font-size: 13px;
    font-weight: 700; color: var(--text-primary); letter-spacing: .2px; white-space: nowrap; }
  .setup-count__of { color: var(--text-tertiary); font-weight: 600; margin-left: 4px; }
  .setup-bar { height: 4px; background: var(--bg-input); border-radius: 3px; margin: 9px 0 10px; overflow: hidden; }
  .setup-bar i { display: block; height: 100%; background: var(--accent-primary); border-radius: 3px;
    transition: width .25s ease; }

  .setup__body { padding: 18px 16px 6px; }
  .setup__nav { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; padding: 6px 16px 20px; }
  .setup-act { min-height: 44px; }
  .setup-act--wide { flex: 1 1 150px; }

  .setup-h2 { font-family: 'Syne', 'DM Sans', sans-serif; font-size: 21px; font-weight: 700;
    letter-spacing: -.2px; margin: 0 0 8px; color: var(--text-primary); text-wrap: balance; }
  .setup-lede { font-size: 15px; color: var(--text-secondary); margin: 0 0 16px; }
  .setup-tiny { font-size: 12.5px; color: var(--text-tertiary); margin: 0 0 12px; }

  /* the numbered lines. The counter is CSS so a translator never has to carry "1." into a string. */
  .setup-do { list-style: none; counter-reset: setupdo; padding: 0; margin: 0 0 16px; }
  .setup-do li { counter-increment: setupdo; position: relative; padding: 0 0 0 32px; margin: 0 0 11px;
    font-size: 15px; color: var(--text-secondary); }
  .setup-do li::before { content: counter(setupdo); position: absolute; left: 0; top: 1px; width: 22px;
    height: 22px; border-radius: 50%; background: var(--bg-input); border: 1px solid var(--border-default);
    color: var(--text-tertiary); font-size: 11.5px; font-weight: 700; display: grid; place-items: center;
    font-family: 'Syne', 'DM Sans', sans-serif; }

  .setup-note { background: var(--bg-elevated); border: 1px solid var(--border-subtle);
    border-left: 3px solid var(--accent-primary); border-radius: 0 10px 10px 0; padding: 11px 13px; margin: 0 0 16px; }
  .setup-note p { margin: 0; font-size: 14px; color: var(--text-secondary); line-height: 1.55; }
  .setup-note--calm { border-left-color: var(--text-tertiary); }
  .setup-note--good { border-left-color: var(--accent-teal); }

  /* ── the pictures and the ring on the exact button ─────────────────────────────────────────────── */
  .setup-shot { margin: 0 0 18px; }
  .setup-shot__wrap { position: relative; display: block; width: 100%; border-radius: 10px; overflow: hidden;
    border: 1px solid var(--border-default); background: var(--bg-input); cursor: zoom-in; padding: 0;
    line-height: 0; appearance: none; }
  .setup-shot__wrap img { display: block; width: 100%; height: auto; }
  /* A PORTRAIT picture is capped by WIDTH on a wide screen and released on a phone. Never by height:
     a max-height with object-fit stops the wrapper hugging the image, and the ring — a percentage box
     against the WRAPPER — would then point at empty space. A 1080x2220 phone shot stretched across a
     628px desktop column is also just a blurry upscale of something 390 points wide. */
  .setup-shot__wrap--tall { max-width: 340px; margin-inline: auto; }
  .setup-shot__wrap:focus-visible { outline: 2px solid var(--accent-primary); outline-offset: 2px; }
  .setup-shot figcaption { font-size: 12.5px; color: var(--text-tertiary); margin-top: 7px; line-height: 1.5; }
  .setup-shot__zoom { font-size: 11.5px; color: var(--text-tertiary); margin: 5px 0 0; }
  .setup-shot figcaption b { color: var(--text-secondary); }
  .setup-ring { position: absolute; border: 3px solid var(--accent-primary); border-radius: 8px;
    pointer-events: none; box-shadow: 0 0 0 3px var(--accent-glow), 0 0 26px 6px var(--accent-glow);
    animation: setupPulse 2.2s ease-in-out infinite; }
  @keyframes setupPulse {
    0%, 100% { box-shadow: 0 0 0 3px var(--accent-glow), 0 0 26px 6px var(--accent-glow); }
    50% { box-shadow: 0 0 0 6px var(--accent-glow), 0 0 36px 12px var(--accent-glow); }
  }
  @media (prefers-reduced-motion: reduce) { .setup-ring { animation: none; } }

  /* ── the two fields (the email, and the address) ───────────────────────────────────────────────── */
  .setup-fld { margin: 0 0 12px; }
  .setup-fld label { display: block; font-size: 12.5px; font-weight: 600; color: var(--text-secondary); margin-bottom: 6px; }
  .setup-fld__in { width: 100%; box-sizing: border-box; padding: 12px 13px; border-radius: 10px;
    background: var(--bg-input); border: 1px solid var(--border-default); color: var(--text-primary);
    font-family: 'DM Mono', ui-monospace, monospace; font-size: 14px; }
  .setup-fld__in:focus { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-glow); }
  /* An address that is not the right one is not a mistake anyone made — amber, which §3 reserves for
     feedback, never red. Red on this page would be the app scolding somebody on their first minute. */
  .setup-fld__in--bad { border-color: var(--accent-warm); }
  .setup-fld__in--ok { border-color: var(--accent-teal); }
  .setup-msg { display: flex; gap: 8px; align-items: flex-start; font-size: 13.5px; line-height: 1.5; margin: -2px 0 12px; }
  .setup-msg--bad { color: var(--accent-warm); }
  .setup-msg--ok { color: var(--accent-teal); font-weight: 600; }

  /* ── step 6's progress list. Each line is one real thing that happened, ticked as it lands. ────── */
  .setup-run { margin: 2px 0 4px; }
  .setup-runrow { display: flex; gap: 9px; align-items: baseline; font-size: 13.5px; margin: 0 0 6px;
    color: var(--text-tertiary); }
  .setup-runrow__m { font-weight: 700; width: 12px; flex: 0 0 12px; text-align: center; }
  .setup-runrow--go { color: var(--text-primary); }
  .setup-runrow--ok { color: var(--accent-teal); }
  .setup-runact { margin-top: 4px; }

  /* ── the hand-off code, and the finished screen ────────────────────────────────────────────────── */
  /* Open Hearth is THE act of this screen, so it takes the full width the nav buttons take rather than
     sitting as a small chip beside a large QR card (read off the 390px screenshot, not asserted). */
  .setup-open { display: block; width: 100%; text-decoration: none; text-align: center; margin: 0 0 18px;
    padding: 14px 20px; min-height: 48px; }
  .setup-qr { background: var(--bg-elevated); border: 1px solid var(--border-default); border-radius: 12px;
    padding: 16px; text-align: center; margin: 0 0 16px; }
  .setup-qr h3 { font-family: 'Syne', 'DM Sans', sans-serif; font-size: 16px; margin: 0 0 4px; color: var(--text-primary); }
  .setup-qr p { font-size: 13.5px; margin: 0 0 12px; color: var(--text-secondary); }
  /* The svg paints its own white field and its own quiet zone, so the frame adds nothing but room. */
  .setup-qr__frame { display: inline-block; line-height: 0; }
  .setup-qr__frame svg { display: block; width: min(260px, 74vw); height: min(260px, 74vw); }
  .setup-qr__cap { font-size: 11.5px; color: var(--text-tertiary); margin: 10px 0 0; overflow-wrap: anywhere; }

  /* ── the lightbox. A picture of a small blue button is useless if you cannot make it bigger. ───── */
  .setup-lb { position: fixed; inset: 0; z-index: 3000; background: var(--bg-base); padding: 16px;
    overflow: auto; cursor: zoom-out; }
  .setup-lb__inner { position: relative; max-width: 1100px; margin: 24px auto; line-height: 0; }
  .setup-lb__inner img { width: 100%; height: auto; border-radius: 8px; display: block; }

  /* 390px: the column already fills the screen, so a phone needs only the head to give a little back
     and the title to stop shouting. The two segment buttons stay on one line at 320px. */
  @media (max-width: 430px) {
    .setup-page { padding: 16px 12px calc(32px + env(safe-area-inset-bottom, 0px)); }
    .setup__body { padding: 16px 13px 6px; }
    .setup__nav { padding: 6px 13px 18px; }
    .setup__top { padding: 9px 11px 0; }
    .setup-h2 { font-size: 19px; }
    .setup-lede { font-size: 14.5px; }
    .setup-seg button { font-size: 12px; padding: 8px 9px; }
    .setup-count { font-size: 12px; }
    .setup-shot__wrap--tall { max-width: 100%; }
  }

  /* ── the Settings "This device" row (js/settings.js) ───────────────────────────────────────────
     The address is a long unbroken token, so it must be allowed to break anywhere or it pushes the
     card sideways on a phone. DM Mono ships no weight above 500 — `.mono` here inherits the base. */
  .device-row__url { font-size: 12.5px; color: var(--text-secondary); overflow-wrap: anywhere;
    background: var(--bg-input); border: 1px solid var(--border-subtle); border-radius: 8px;
    padding: 9px 11px; margin: 8px 0 12px; line-height: 1.5; }
  .device-row__go { display: inline-block; text-decoration: none; }
  .device-row__note { font-size: 12px; margin: 9px 0 0; }

