/* ============================================================================
   ledger.css — single stylesheet (no build step; baked into the image).

   LAYER ORDER (cascade matters — keep new rules in the right band):
     1. Tokens            — :root design tokens (color, type, space, elevation)
     2. Reset / base      — *,html,body,a,button
     3. Layout / shell    — .layout/.sidebar/.main/.appbar/.scrim
     4. Primitives        — .card surface, .grid, .btn, .ledger-table,
                             forms, .page-header, .card-header/section
     5. Feature blocks    — per-feature components, kept in place
     6. Responsive        — ONE consolidated band at the end

   BREAKPOINTS (canonical — do not introduce new ad-hoc values):
     - max-width: 768px   → shell switches to the mobile drawer; tables
                             reflow to cards; multi-col grids stack.
     - max-width: 1024px  → wide multi-col grids relax (optional tier).
   Legacy 680/720/900 blocks are being migrated onto these two; the
   responsive grid primitive (.grid) collapses without a media query.
   ============================================================================ */

/* ── Tokens ───────────────────────────────────────────────────────────────────
   Brand identity (2026 rework): a single warm theme — terracotta accent on
   espresso + warm cream. Display type is Newsreader, body is Inter, numbers
   JetBrains Mono. Honey is a sparing secondary accent. Semantic
   income/expense/warn colours stay distinct from the accent — they're data
   legibility, not branding. Dark mode was retired in this rework (see the
   note where the [data-theme="dark"] block used to be). */
:root {
  --bg:           #faf4ec;   /* warm cream paper */
  --bg-card:      #ffffff;
  --sidebar-bg:           #34271f;   /* espresso */
  --sidebar-border:       #4a3a2e;
  --sidebar-text:         #cdbfae;   /* muted on espresso */
  --sidebar-text-hover:   #ffffff;
  --sidebar-active-bg:    #4a3a2c;
  --sidebar-active-accent:#e07a52;   /* terracotta — active marker on the nav */
  --text-primary: #2a2018;   /* espresso ink */
  --text-secondary:#5a4d42;
  --text-muted:   #8a7d70;
  --border:       #e9ddcf;
  --border-light: #f3ece0;
  --income:       #1f8a5b;   /* money green (semantic) */
  --income-bg:    #e6f5ee;
  --expense:      #c0392b;   /* clear red, distinct from the terracotta accent */
  --expense-bg:   #fbeae8;
  --warn:         #a9711a;
  --warn-bg:      #fcf3e3;
  --accent:       #c75c3a;   /* terracotta (CTA / links / the 'Fi') */
  --accent-soft:  #ad4a2d;   /* hover / pressed terracotta */
  --coral:        #c75c3a;   /* legacy alias → terracotta */
  --coral-pale:   #f7e7df;
  --amber:        #cf9445;   /* honey — sparing secondary accent */
  --amber-pale:   #fbf3e6;
  --radius:       8px;
  --radius-lg:    16px;
  --radius-xl:    24px;
  --shadow:       0 1px 3px rgba(52,39,31,.08),0 1px 2px rgba(52,39,31,.05);
  --font-sans:    'Inter',system-ui,-apple-system,sans-serif;
  --font-serif:   'Newsreader',Georgia,serif;
  --font-mono:    'JetBrains Mono','Fira Code',ui-monospace,monospace;

  /* Type scale — 7 steps consolidating the ~17 ad-hoc px sizes that
     accreted per sprint. Mapping used for the sweep:
       9.5/10/10.5/11        → --fs-xs   (11px)  micro labels, badges
       11.5/12/12.5          → --fs-sm   (12.5px) hints, captions
       13/13.5/14/14.5       → --fs-base (13.5px) body, table, nav
       15                    → --fs-md   (15px)  emphasised body
       18                    → --fs-lg   (18px)  card/section headings
       20/22                 → --fs-xl   (22px)  page titles
       24/26                 → --fs-2xl  (26px)  hero figures */
  --fs-xs:        11px;
  --fs-sm:        12.5px;
  --fs-base:      13.5px;
  --fs-md:        15px;
  --fs-lg:        18px;
  --fs-xl:        22px;
  --fs-2xl:       26px;

  /* Spacing scale — replaces ad-hoc rem/px margins & padding. */
  --sp-1:         .25rem;
  --sp-2:         .5rem;
  --sp-3:         .75rem;
  --sp-4:         1rem;
  /* 1.25rem is the most-used spacing value in the app (70 inline, 66 here), so
     it is a step rather than a rounding target. It sits between 4 and 5 under
     its own name: renumbering would silently move every existing --sp-5..7. */
  --sp-4-5:       1.25rem;
  --sp-5:         1.5rem;
  --sp-6:         2rem;
  --sp-7:         2.5rem;

  /* Elevation scale. --elev-1 is the existing card --shadow (aliased so
     legacy `var(--shadow)` refs keep working); --elev-2/-3 replace the
     hardcoded one-off popover / modal shadows. */
  --elev-1:       0 1px 3px rgba(0,0,0,.06),0 1px 2px rgba(0,0,0,.04);
  --elev-2:       0 8px 24px rgba(0,0,0,.10);
  --elev-3:       0 24px 60px rgba(15,23,42,.32);
  /* Grid surface tints — these were previously hardcoded inside the rule
     definitions, which broke on dark. Promoted to variables so the dark
     palette below can override them without touching the rules. Warm-
     cream-tinted to compose with the new --bg. */
  --table-header-bg:    #f3ece0;
  --table-row-hover-bg: #f7f1e6;
  --row-pending-bg:     #f9eede;   /* faint honey wash on pending rows */
  --ring-cell-hover-bg: #f7f1e6;
}

/* Dark mode was retired in the 2026 brand rework — MoMoFi is now a single
   warm theme. The previous :root[data-theme="dark"] override block lived
   here; it's removed so data-theme="dark" simply falls back to the light
   tokens above (inert, never a stale palette). Re-introduce a
   [data-theme="dark"] block here if a dark variant ever returns. */
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
/* The hidden attribute has to win. Browsers back it with a UA rule of
   [hidden]{display:none}, and any author rule that sets display beats a UA one
   — so el.hidden = true on anything styled here (a .btn is display:inline-flex)
   sets the attribute and changes nothing on screen. Three components had
   already worked around it one at a time; this is the rule they each needed. */
[hidden]{display:none !important}
/* Where a debt term came from. Small, beside its label — the answer only
   matters while you are looking at that field, and a card with five of these
   should not read as five warnings. */
.field-source{
  display:inline-block; margin-left:.4rem; padding:.05rem .4rem;
  border-radius:100px; font-size:.72rem; font-weight:500;
  vertical-align:middle; white-space:nowrap;
}
.field-source--bank{background:var(--income-bg); color:var(--income)}
.field-source--you{background:var(--bg-subtle, #f0ece6); color:var(--text-secondary)}
.field-source--missing{background:var(--warn-bg); color:var(--warn)}
/* The way back from an edit. A link rather than a button in appearance: it
   fills the box, it does not save, and it should not compete with the one
   control on this form that commits anything. */
.field-reset{
  background:none; border:0; padding:0; margin-left:.4rem;
  font-family:inherit; font-size:.72rem; color:var(--accent);
  text-decoration:underline; cursor:pointer; vertical-align:middle;
}
.field-reset:hover{color:var(--accent-soft)}
html{font-size:var(--fs-md)}
body{font-family:var(--font-sans);background:var(--bg);color:var(--text-primary);line-height:1.6;-webkit-font-smoothing:antialiased}
a{color:inherit;text-decoration:none}
button{cursor:pointer;font-family:inherit}

/* ── Layout ─────────────────────────────────────────────────────────────────── */
.layout{display:flex;min-height:100vh}
.sidebar{width:220px;min-height:100vh;background:var(--sidebar-bg);border-right:1px solid var(--sidebar-border);display:flex;flex-direction:column;position:fixed;top:0;left:0;bottom:0;z-index:10}
.main{margin-left:220px;flex:1;min-height:100vh}
.main-inner{max-width:1280px;margin:0 auto;padding:2rem 2.5rem}

/* Mobile app-bar + drawer scrim. Hidden on desktop (the fixed sidebar
   is always visible there); the @media(max-width:768px) band at the end
   of the file flips these on and turns .sidebar into an off-canvas
   drawer. Kept in the Layout layer so the shell lives in one place. */
.appbar{display:none}
.scrim{display:none}

/* ── Sidebar ─────────────────────────────────────────────────────────────────── */
.sidebar-brand{display:flex;align-items:center;gap:10px;padding:1.5rem 1.25rem 1.25rem;border-bottom:1px solid var(--sidebar-border)}
/* Was a text glyph sized in em; now the score ring, so it needs real box
   dimensions. Sized to sit on the wordmark's cap height beside it. */
.brand-mark{width:20px;height:20px;flex-shrink:0;display:block}
.brand-name{font-size:var(--fs-sm);font-weight:600;letter-spacing:.12em;color:var(--sidebar-text-hover)}
/* MoMoFi wordmark spans — "MoMo" carries the brand, "Fi" is the
 * URL/domain anchor (smaller + lower opacity) so visually the brand
 * reads as "MoMo" first. On dark surfaces (sidebar + mobile appbar)
 * the colours flip so the wordmark stays legible. */
.brand-mo{color:var(--text-primary);font-family:var(--font-serif);font-weight:600;letter-spacing:-.01em}
.brand-fi{color:var(--accent);font-family:var(--font-serif);font-weight:600;font-size:.85em;letter-spacing:-.01em}
.sidebar .brand-mo,.appbar .brand-mo{color:#fff}
.sidebar .brand-fi,.appbar .brand-fi{color:var(--sidebar-active-accent);opacity:1}
.sidebar-nav{flex:1;padding:1rem 0}
.nav-section-label{padding:.9rem 1.25rem .25rem;font-size:.65rem;font-weight:600;letter-spacing:.12em;text-transform:uppercase;color:var(--sidebar-text);opacity:.55}
.nav-section-label:first-child{padding-top:.25rem}
.nav-item{display:flex;align-items:center;gap:10px;padding:.6rem 1.25rem;font-size:var(--fs-base);color:var(--sidebar-text);transition:all .12s;border-left:2px solid transparent}
.nav-item:hover{color:var(--sidebar-text-hover);background:rgba(255,255,255,.04)}
.nav-item.active{color:var(--sidebar-text-hover);background:var(--sidebar-active-bg);border-left-color:var(--sidebar-active-accent)}
.nav-icon{width:16px;height:16px;flex-shrink:0;opacity:.75}
.nav-item.active .nav-icon,.nav-item:hover .nav-icon{opacity:1}
.sidebar-foot{padding:1rem 1.25rem;border-top:1px solid var(--sidebar-border);display:flex;flex-direction:column;gap:.35rem}
.sidebar-user-name{font-size:var(--fs-sm);color:var(--sidebar-text-hover);font-weight:500;letter-spacing:.02em;text-decoration:none;display:flex;flex-direction:column;line-height:1.2}
.sidebar-user-name:hover{color:var(--sidebar-text);text-decoration:underline}
.sidebar-user-sub{font-size:.7rem;font-weight:400;opacity:.55;letter-spacing:.04em}
.sidebar-foot-row{display:flex;align-items:center;gap:.5rem}
.sidebar-logout{background:transparent;border:0;color:var(--sidebar-text);font-size:var(--fs-base);cursor:pointer;padding:.4rem 0;text-align:left;font-family:inherit;flex:1}
.sidebar-logout:hover{color:var(--sidebar-text-hover)}
.sidebar-toggle{background:transparent;border:1px solid var(--sidebar-border);color:var(--sidebar-text);cursor:pointer;font-family:inherit;width:30px;height:30px;border-radius:6px;display:inline-flex;align-items:center;justify-content:center;font-size:var(--fs-base);line-height:1;padding:0}
.sidebar-toggle:hover{color:var(--sidebar-text-hover);border-color:var(--sidebar-text-hover)}

/* ── Page header ─────────────────────────────────────────────────────────────── */
.page-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1.75rem}
.page-header__back{display:inline-block;font-size:var(--fs-sm);color:var(--text-muted);text-decoration:none;margin-bottom:.35rem}
.page-header__back:hover{color:var(--text-primary);text-decoration:underline}
.page-title{font-family:var(--font-serif);font-size:var(--fs-xl);font-weight:600;letter-spacing:-.01em;line-height:1.2}
.page-sub{font-size:var(--fs-base);color:var(--text-muted);margin-top:2px}

/* Headings — Newsreader serif for an editorial, calm display voice.
   Body, table, form, and nav text stay in --font-sans (Inter). Numeric
   tables use --font-mono (JetBrains Mono) via .mono / .money. */
h1,h2,h3{font-family:var(--font-serif);font-weight:600;letter-spacing:-.005em}

/* ── Prompt card ─────────────────────────────────────────────────────────────── */
.prompt-card{display:flex;align-items:center;gap:16px;background:var(--sidebar-bg);color:var(--sidebar-text-hover);border-radius:var(--radius-lg);padding:1rem 1.25rem;margin-bottom:1.75rem}
.prompt-icon{font-size:var(--fs-xl);flex-shrink:0;width:32px;text-align:center}
.prompt-body{flex:1;font-size:var(--fs-base);line-height:1.5}
.prompt-body strong{display:block;color:#fff;font-weight:500;margin-bottom:2px}
.prompt-body span{color:var(--sidebar-text)}
.prompt-action{font-size:var(--fs-base);color:var(--sidebar-active-accent);white-space:nowrap;font-weight:500;flex-shrink:0}
.prompt-action:hover{text-decoration:underline}

/* ── Stat grid ─────────────────────────────────────────────────────────────── */
.stat-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:14px;margin-bottom:1.75rem}
.stat-grid--4{grid-template-columns:repeat(4,1fr)}
.stat-grid--3{grid-template-columns:repeat(3,1fr)}
.stat-grid--2{grid-template-columns:repeat(2,1fr)}
.stat-card{background:var(--bg-card);border:1px solid var(--border-light);border-radius:var(--radius);padding:1rem 1.25rem}
.stat-label{font-size:var(--fs-xs);font-weight:500;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted);margin-bottom:6px}
.stat-value{font-size:var(--fs-xl);font-weight:600;letter-spacing:-.02em;font-family:var(--font-mono);line-height:1.2}
.stat-compare{font-size:var(--fs-sm);color:var(--text-muted);margin-top:4px}
.stat-card--positive{border-top:3px solid var(--income)}
.stat-card--negative{border-top:3px solid var(--expense)}

/* ── Cards ─────────────────────────────────────────────────────────────────── */
.card{background:var(--bg-card);border:1px solid var(--border-light);border-radius:var(--radius-lg);overflow:hidden;box-shadow:var(--shadow);margin-bottom:var(--sp-4-5)}
.card--table{padding:0}
.card--empty{padding:2.5rem;text-align:center;color:var(--text-secondary)}
/* Empty-state CTA spacing — the <p> + <a class="btn"> pattern is the
   shape for /goals, /coming-up, kid dashboard, etc. Browser default <p>
   margins vary; the explicit margin-top here guarantees a consistent
   gap between the "you haven't done X yet" copy and the action button.
   Cheap insurance, applies only inside .card--empty so other pages
   are untouched. */
.card--empty .btn{margin-top:.25rem}
/* A card whose own box carries the gutter, rather than leaving it to the form
   or .card-body inside it — for a card that mixes a heading and prose with a
   form, where otherwise only the form would be inset. Same 1.25rem as
   `.card > form`, .card-body and .card-header, so a card's gutter is one value
   however it is applied; the `.card--pad > form` rule further down stops the
   two from stacking. */
.card--pad{padding:var(--sp-4-5)}
.card-header{display:flex;justify-content:space-between;align-items:center;padding:.875rem 1.25rem;border-bottom:1px solid var(--border-light)}
/* Card headers stay in --font-sans regardless of tag. Most are <span>, but a
   few are <h2> (timeline / insights "Pin a moment", budget template preview) —
   without pinning the family those headings inherit the serif h1,h2,h3 rule
   and read differently from every other card header. */
.card-title{font-size:var(--fs-base);font-weight:600;letter-spacing:-.01em;font-family:var(--font-sans)}
.card-link{font-size:var(--fs-sm);color:var(--accent);font-weight:500}
.card-link:hover{text-decoration:underline}
.two-col{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;align-items:start}

/* ── Ledger table ─────────────────────────────────────────────────────────── */
.ledger-table{width:100%;border-collapse:collapse;font-size:var(--fs-base)}
.ledger-table th{font-size:var(--fs-xs);font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted);padding:.6rem 1.25rem;text-align:left;border-bottom:1px solid var(--border-light);background:var(--table-header-bg)}
.ledger-table td{padding:.7rem 1.25rem;border-bottom:1px solid var(--border-light);vertical-align:middle}
.ledger-table tbody tr:last-child td{border-bottom:none}
.ledger-table tbody tr:hover{background:var(--table-row-hover-bg)}
.ledger-table--full .ledger-table th,.ledger-table--full .ledger-table td{padding:.7rem 1rem}
.row-pending{background:var(--row-pending-bg) !important}
.row-reconciled{opacity:.65}
tfoot .total-row td{border-top:2px solid var(--border);border-bottom:none;padding-top:.875rem;background:var(--table-header-bg)}

/* ── Typography helpers ───────────────────────────────────────────────────── */
.mono{font-family:var(--font-mono);font-size:.92em}
.text-right{text-align:right}
.text-center{text-align:center}
.text-muted{color:var(--text-muted)}
.text-income{color:var(--income)}
.text-expense{color:var(--expense)}
.date-cell{color:var(--text-secondary);font-size:var(--fs-sm);white-space:nowrap}

/* ── Money presentation (see docs/MONEY_PRESENTATION.md) ───────────────────
   Flows: magnitude-only, direction by colour + ▲/▼ arrow + sr-only word.
   Nets: signed (the one place a sign appears). Reuses the --income/--expense
   tokens so dark mode is inherited. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.money{font-variant-numeric:tabular-nums;white-space:nowrap}
.money--in{color:var(--income)}
.money--out{color:var(--expense)}
.money__arrow{font-size:.78em;margin-right:.14em;vertical-align:baseline}

/* ── Tooltip engine (CSP-safe: pure CSS, no JS). Wrap the trigger in
   <span class="tip" tabindex="0" aria-label="…"> and add a sibling
   <span class="tip__bubble" aria-hidden="true">…</span>. Reveals on
   hover AND keyboard focus. .tip--help adds a learn-more affordance. */
.tip{position:static;display:inline-flex;align-items:center;gap:.2em}
.tip--help{cursor:help;border-bottom:1px dotted currentColor}
.tip__bubble{position:absolute;bottom:calc(100% + 6px);left:0;
  background:var(--text-primary);color:var(--bg);padding:.42rem .6rem;border-radius:6px;
  font-size:var(--fs-xs);font-weight:500;line-height:1.35;white-space:normal;
  width:max-content;max-width:min(240px,100%);text-align:left;
  opacity:0;visibility:hidden;transition:opacity .12s ease;z-index:60;pointer-events:none}
.tip:hover .tip__bubble,.tip:focus-within .tip__bubble{opacity:1;visibility:visible}
/* The bubble is anchored to the block it sits in, never to the trigger — hence
   .tip being static, which is the whole mechanism and not an oversight. A
   trigger is an inline icon or chip that can land anywhere on its line, so a
   240px box hung off one runs past an edge as soon as the line is long enough:
   left of the card on a short label, right off the viewport on a long one, and
   (because .card is overflow:hidden) either silently cut down to a sliver or,
   in a card that :has(.tip--section) has opened up, scrolling the whole page
   sideways with nothing visible to see.
   Anchored to the block, the bubble starts at that block's content edge and
   min(240px,100%) stops it outgrowing it, so it cannot reach past an edge the
   block itself is inside. An anchor therefore has to be about as wide as a
   bubble is useful; the one below that isn't — a table cell — opts out of the
   width cap and hangs off its right edge instead.
   The :has() lets a section's bubble out of the card it now sits above; scoped
   to cards that carry one, so nothing else loses its corner clipping. */
.card:has(.tip--section){overflow:visible}
.settings-h,.form-section-title,.dash-section-title,
.label-row,.ledger-table td,.account-balance,
.debt-plan-row__needsinfo,.badge-member__row{position:relative}
/* A table cell is the exception to "cap at the block": a column can be
   narrower than the bubble, and min(240px,100%) would shrink a tip in a MODE
   column to the width of the word "auto". Hanging it off the cell's right edge
   keeps the full 240px and still lands it beside the chip, because a cell's
   right edge is inside the table, which is inside the card. */
.ledger-table td .tip__bubble{left:auto;right:0;max-width:240px}
/* A section heading is the first thing in its card, so its bubble opens over
   the card's own top edge rather than over a line of text; it spans the heading
   instead of capping at 240px, which reads better for a whole-section note. */
.tip--section .tip__bubble{width:auto;max-width:100%}
/* label_tip partial: a form label + a focusable info "i" icon (see label_tip.html). */
.label-row{display:flex;align-items:center;gap:.4em;margin-bottom:.35rem}
.label-row .form-label{margin-bottom:0}
.tip__i{display:inline-flex;align-items:center;justify-content:center;width:15px;height:15px;
  border:1px solid var(--text-muted);border-radius:50%;font-size:10px;font-style:italic;
  font-weight:700;line-height:1;color:var(--text-muted);cursor:help}
.tip:hover .tip__i,.tip:focus-within .tip__i{border-color:var(--accent);color:var(--accent)}
/* Live calc estimate line under the date/payment fields. */
.calc-estimate{font-size:var(--fs-sm);color:var(--text-secondary);margin:.15rem 0 0}
.calc-estimate--warn{color:var(--expense)}
/* Beginner explanatory note — a short plain-language callout shown only to
   beginner-level users (gated by .Session.IsBeginner in the templates). */
.beginner-note{border:1px solid var(--border-light);border-left:3px solid var(--accent);
  border-radius:var(--radius);background:var(--bg-card);padding:.6rem .85rem;
  margin:.25rem 0 1rem;font-size:var(--fs-sm);color:var(--text-secondary);line-height:1.5}
.tx-link{color:var(--text-primary)}
.tx-link:hover{color:var(--accent);text-decoration:underline}
.tx-desc{display:block}
.tx-notes{display:block;font-size:var(--fs-sm);color:var(--text-muted)}
.action-cell{white-space:nowrap;text-align:right}
.action-link{font-size:var(--fs-sm);color:var(--text-muted);padding:2px 6px;background:none;border:none;font-family:inherit}
.action-link:hover{color:var(--accent)}
.action-link--danger:hover{color:var(--expense)}
.action-link--muted{color:var(--text-muted)}
.action-link--ghost{font-size:.7rem;opacity:.6;font-style:italic}
.action-link--ghost:hover{opacity:1}
.notif-header-actions{display:flex;gap:.5rem;align-items:center}

/* ── Badges ─────────────────────────────────────────────────────────────── */
.badge,.badge-pending{display:inline-block;font-size:var(--fs-xs);font-weight:500;padding:1px 6px;border-radius:4px;background:var(--warn-bg);color:var(--warn);margin-left:6px;vertical-align:middle}
.badge-count{display:inline-block;font-size:var(--fs-xs);font-weight:600;padding:1px 7px;border-radius:10px;background:var(--border-light);color:var(--text-secondary);margin-left:6px}
.badge-custom{display:inline-block;font-size:var(--fs-xs);font-weight:500;padding:1px 5px;border-radius:4px;background:#eff6ff;color:#1d4ed8;margin-left:5px}
.cat-badge{display:inline-block;font-size:var(--fs-sm);padding:2px 8px;border-radius:4px;font-weight:500}
.cat-badge--income{background:var(--income-bg);color:var(--income)}
.cat-badge--expense{background:var(--expense-bg);color:var(--expense)}
.cat-badge--transfer{background:#f3e8ff;color:#7e22ce}
.cat-badge--payment{background:#fef3c7;color:#b45309}
/* Inline category-edit form on /transactions Category cell. Keeps the
   select compact so the row height matches the read-only badge rows;
   .inline-cat-form--js (added by transactions_inline_cat.js once
   change-listeners are wired) hides the Save button because the
   auto-submit on change makes it redundant. Pure-no-JS browsers still
   see the button and can submit explicitly. */
.inline-cat-form{display:inline-flex;align-items:center;gap:6px;margin:0}
.inline-cat-select{max-width:200px;min-width:140px}
.inline-cat-save{padding:2px 8px;font-size:var(--fs-xs)}
.inline-cat-form--js .inline-cat-save{display:none}
/* Rule-suggestion nudge strip above the /transactions table.
   Mirrors .alert-banner styling but tighter — fits a single line on
   desktop, wraps on mobile. */
.rule-suggest{display:flex;flex-wrap:wrap;align-items:center;gap:12px;background:var(--income-bg);border:1px solid var(--income);border-radius:6px;padding:.75rem 1rem;margin-bottom:.75rem}
.rule-suggest__text{flex:1 1 220px;font-size:var(--fs-sm)}
.rule-suggest__actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.rule-suggest__check{display:inline-flex;align-items:center;gap:6px;font-size:var(--fs-sm);color:var(--text-secondary)}
/* Category reorder (drag-and-drop on /categories) — handle column,
   dragged-row state, drop-target highlight, and budget-group
   separator rows inside the Expense card. The handle is a static
   text glyph; the row is draggable=true so the entire row is the
   drag source, with the handle providing the visual affordance. */
.cat-reorder-row{cursor:grab}
.cat-reorder-row:active{cursor:grabbing}
.cat-reorder-row.is-dragging{opacity:.45}
.cat-reorder-row.is-drop-target{box-shadow:inset 0 2px 0 0 var(--accent);background:var(--table-row-hover-bg)}
.cat-reorder-handle{width:24px;color:var(--text-muted);font-weight:700;letter-spacing:-2px;text-align:center;cursor:grab;user-select:none}
.cat-reorder-sep td{padding-top:.85rem;padding-bottom:.25rem;font-size:var(--fs-xs);font-weight:600;text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--border-light)}
.cat-reorder-sep{cursor:default}
.lines-chip{display:inline-block;font-size:var(--fs-xs);margin-left:6px;padding:1px 6px;border-radius:3px;background:#eef2ff;color:#4338ca;font-weight:500}
/* Same chip, amber: the bank disagrees with a row the household edited. It is a
   thing to notice, not a thing that went wrong, so it borrows the reauth pill's
   colour rather than the error one. position:relative because the chip IS the
   tip trigger here (no .tip wrapper of its own to anchor the bubble). */
.bank-chip{position:relative;display:inline-block;font-size:var(--fs-xs);margin-left:6px;
  padding:1px 6px;border-radius:3px;background:#fef3c7;color:#92400e;font-weight:500;cursor:help}
.verify-banner{display:flex;align-items:center;gap:.6rem;padding:.7rem 1rem;margin:-.5rem 0 1rem;border-radius:8px;background:#fff7ed;color:#9a3412;border:1px solid #fed7aa;font-size:var(--fs-base)}
/* alert-banner — unified top-of-page banner. One component, three
   severity variants (critical/warn/info). Replaces both the old
   .critical-banner (notification-record-driven) and .outcome-alert
   (score-derived) blocks that had drifted into separate visual
   languages. Driven by .NotificationMeta.Critical, which now
   surfaces the newest unread notification at severity ≥ warn.
   Layout: icon · title+body stack · CTA pill · × dismiss. Mobile
   reflow at 640px stacks the cluster vertically. */
.alert-banner{display:flex;align-items:flex-start;gap:.9rem;padding:1rem 1.2rem;margin:0 0 1.2rem;border-radius:var(--radius-lg);border:1px solid;font-size:var(--fs-base);line-height:1.45}
.alert-banner__icon{font-size:1.4rem;line-height:1;flex:0 0 auto;padding-top:.1rem}
.alert-banner__body{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:.2rem}
.alert-banner__title{display:block;font-weight:600;font-size:var(--fs-md)}
.alert-banner__text{margin:0;font-size:var(--fs-sm);opacity:.9}
.alert-banner__action{flex:0 0 auto;align-self:center;font-weight:600;font-size:var(--fs-sm);text-decoration:none;padding:.45rem .9rem;border-radius:999px;border:1px solid currentColor;white-space:nowrap;color:inherit}
.alert-banner__action:hover{background:rgba(0,0,0,.06)}
.alert-banner__dismiss-form{margin:0;flex:0 0 auto;align-self:flex-start}
.alert-banner__dismiss{background:none;border:none;cursor:pointer;font-size:var(--fs-lg);line-height:1;padding:.2rem .4rem;color:inherit;opacity:.55}
.alert-banner__dismiss:hover{opacity:1}
.alert-banner__dismiss:focus-visible{outline:2px solid currentColor;outline-offset:2px;border-radius:.25rem}
.alert-banner--critical{background:#fdf2f2;color:#7f1d1d;border-color:#fca5a5}
.alert-banner--warn{background:#fffbeb;color:#92400e;border-color:#fcd34d}
.alert-banner--info{background:#eff6ff;color:#1e3a8a;border-color:#93c5fd}
@media(max-width:640px){.alert-banner{flex-direction:column;align-items:stretch}.alert-banner__action{align-self:flex-start}.alert-banner__dismiss-form{position:absolute;top:.5rem;right:.5rem}.alert-banner{position:relative}}
.verify-banner__icon{font-size:var(--fs-lg);flex:0 0 auto}
.verify-banner__msg{flex:1 1 auto;line-height:1.4}
.settings-section{margin-bottom:1.5rem;padding:1.25rem 1.5rem}
.settings-h{font-size:var(--fs-md);font-weight:600;margin:0 0 1rem;padding-bottom:.6rem;border-bottom:1px solid var(--border-light)}
/* .settings-back was the per-settings-sub-page back-link style; the four
 * sub-pages that used it (Account/Appearance/Security/Profile) now render
 * via page_header_lead which uses .page-header__back — same visual, one
 * source of truth. Class removed. */

/* Hub tiles — 5 stacked tap targets on the /settings page. Each tile
 * is an <a> sized for touch (≥56px tall). Width capped at 680px so
 * the column reads well on a wide monitor without stretching across
 * the full main-inner area. Phone view inherits the cap and naturally
 * collapses since 680 > 360. */
.settings-tiles{display:flex;flex-direction:column;gap:.5rem;max-width:680px}
.settings-tile{
  display:flex;align-items:center;gap:1rem;
  /* min-height keeps every tile the same size regardless of how
   * many lines the description wraps to. Combined with the icon's
   * fixed square container + the chevron's fixed font-size, the
   * row reads as a uniform grid: icons line up horizontally down
   * the column, chevrons line up on the right, titles line up on
   * the same baseline. */
  min-height:64px;
  padding:1rem 1.25rem;
  background:var(--bg-card);border:1px solid var(--border-light);
  border-radius:var(--radius-lg);
  text-decoration:none;color:inherit;
  transition:border-color .12s,background .12s;
}
.settings-tile:hover{border-color:var(--accent);background:rgba(0,0,0,.02)}
/* Icon container — a 2rem square box centered on the row. Emojis
 * render at different glyph sizes; the fixed square + inline-flex
 * centering keeps the visual anchor point consistent across rows
 * (so 👤 vs 💳 vs 📦 all sit on the same x-coordinate). */
.settings-tile__icon{
  flex:0 0 2rem;width:2rem;height:2rem;
  display:inline-flex;align-items:center;justify-content:center;
  font-size:1.5rem;line-height:1;
}
.settings-tile__body{flex:1;display:flex;flex-direction:column;gap:.15rem;min-width:0}
.settings-tile__title{font-size:var(--fs-base);font-weight:600;color:var(--text-primary);line-height:1.3}
.settings-tile__desc{font-size:var(--fs-sm);color:var(--text-muted);line-height:1.4}
/* Chevron — same fixed box treatment so it lines up on the right
 * edge regardless of icon or body content. */
.settings-tile__chevron{
  flex:0 0 1.25rem;width:1.25rem;height:1.25rem;
  display:inline-flex;align-items:center;justify-content:center;
  font-size:1.4rem;color:var(--text-muted);line-height:1;
}
.settings-form{display:flex;flex-direction:column;gap:.35rem;max-width:480px}
.settings-form .btn{margin-top:.75rem;align-self:flex-start}
.settings-inline-form{display:inline-block;margin-top:.5rem}
.settings-row{display:flex;align-items:center;gap:.6rem;margin:0 0 .75rem}
.settings-label{color:var(--text-secondary);font-size:var(--fs-base)}
.settings-pending{background:#fff7ed;border:1px solid #fed7aa;border-radius:8px;padding:.75rem 1rem;margin-top:.75rem;color:#9a3412;font-size:var(--fs-base)}
.settings-pending p{margin:0 0 .5rem}
.settings-link-strip{margin-top:1rem;padding:.6rem .85rem;border-radius:6px;background:rgba(0,0,0,.02);font-size:var(--fs-sm);color:var(--text-muted);display:flex;flex-wrap:wrap;align-items:center;gap:.4rem;line-height:1.6}
.settings-link-strip__label{font-weight:600;color:var(--text-secondary);margin-right:.25rem}
.settings-link-strip__sep{opacity:.5}
.settings-link-strip__locked{opacity:.65;display:inline-flex;align-items:center;gap:.3rem}
.badge--ok{background:#d1fae5;color:#065f46;padding:1px 7px;border-radius:4px;font-size:var(--fs-xs);font-weight:600}
.badge--warn{background:#fef3c7;color:#92400e;padding:1px 7px;border-radius:4px;font-size:var(--fs-xs);font-weight:600}
.badge--pro{background:#1e40af;color:#fff;padding:1px 7px;border-radius:4px;font-size:var(--fs-xs);font-weight:600;letter-spacing:.04em;margin-left:.4rem}
.badge--pro-locked{background:#e5e7eb;color:#6b7280;padding:1px 7px;border-radius:4px;font-size:var(--fs-xs);font-weight:600;letter-spacing:.04em;margin-left:.4rem}
/* Small neutral chip rendered next to private/loopback IPs in the
   /settings/security activity log so the user can spot "this was me on
   my LAN" at a glance and distinguish it from a real outside sign-in.
   Same shape as .badge--pro-locked (grey, tabular-feeling) so it reads
   as informational metadata, not a warning. */
.badge--lan{background:#e5e7eb;color:#374151;padding:1px 6px;border-radius:4px;font-size:var(--fs-xs);font-weight:600;letter-spacing:.04em;margin-left:.4rem}
/* Muted placeholder for /settings/security Location cells that come
   from sp_log_event SP-side emits (no http.Request context → NULL
   ipAddress → no real geo to show). Visually dimmer + italic so the
   "In-app activity" string reads as descriptive metadata rather than
   a missing field, sitting next to rows that DO have real city names. */
.loc-muted{color:var(--text-muted, #6b7280);font-style:italic;font-size:var(--fs-sm)}
.mfa-enroll{display:flex;gap:1.25rem;align-items:flex-start}
.mfa-qr{flex:0 0 auto;border-radius:6px;background:#fff;padding:6px}
.mfa-enroll-side{flex:1 1 auto}
.mfa-secret{background:var(--surface-2);padding:.6rem .8rem;border-radius:6px;font-size:var(--fs-base);font-family:var(--font-mono);display:inline-block}
.mfa-codes{background:var(--surface-2);padding:.8rem 1rem;border-radius:6px;font-family:var(--font-mono);font-size:var(--fs-base);line-height:1.7;letter-spacing:.05em;white-space:pre;user-select:all}
.mfa-codes-warning{background:#fff7ed;border:1px solid #fed7aa;border-radius:6px;padding:.6rem .8rem;color:#7c2d12}
.mfa-codes-actions{display:flex;align-items:center;gap:.6rem;margin-top:.6rem}
.mfa-confirm-saved{display:flex;align-items:center;gap:.5rem;margin-bottom:.75rem;font-size:var(--fs-base);color:var(--text-secondary)}
.mfa-confirm-saved input{margin:0}
#mfa-confirm-btn:disabled{opacity:.55;cursor:not-allowed}
.recipient-list{list-style:none;padding:0;margin:0 0 1rem}
.recipient-row{display:flex;justify-content:space-between;align-items:center;padding:.5rem .25rem;border-bottom:1px solid var(--border-light)}
.recipient-row:last-child{border-bottom:0}
.balance-label{font-size:var(--fs-xs);font-weight:500;opacity:.7;text-transform:uppercase;letter-spacing:.04em;margin-left:.25rem}

/* ── Reconcile button ─────────────────────────────────────────────────────── */
.reconcile-btn{width:22px;height:22px;border-radius:50%;border:1.5px solid var(--border);background:transparent;font-size:var(--fs-xs);color:var(--text-muted);display:inline-flex;align-items:center;justify-content:center;transition:all .12s;padding:0}
.reconcile-btn:hover{border-color:var(--income);color:var(--income);background:var(--income-bg)}
.reconcile-btn--done{border-color:var(--income);background:var(--income-bg);color:var(--income)}
.reconcile-btn--lg{width:28px;height:28px;font-size:var(--fs-base)}
/* A phone has no hover, so the title never shows: the action is spelled out
   beside the symbol, on a pill big enough to hit with a thumb. */
.reconcile-btn__text{display:none}
@media(max-width:768px){
  .reconcile-btn,.reconcile-btn--lg{width:auto;min-height:32px;padding:0 .7rem;border-radius:999px;gap:.35rem}
  .reconcile-btn__text{display:inline;font-size:var(--fs-sm)}
}

/* ── Month nav ─────────────────────────────────────────────────────────────── */
.month-nav{display:flex;align-items:center;gap:16px;margin-bottom:1.25rem}
.month-nav-label{font-size:var(--fs-base);font-weight:600;min-width:120px;text-align:center}
.month-nav-btn{font-size:var(--fs-sm);color:var(--text-secondary);padding:4px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-card);transition:all .12s}
.month-nav-btn:hover{border-color:var(--accent);color:var(--accent)}

/* ── Filter bar (legacy) ─────────────────────────────────────────────────── */
.filter-bar{display:flex;align-items:center;gap:8px;margin-bottom:1rem;flex-wrap:wrap}

/* ── Filter panel (new compact style) ───────────────────────────────────── */
.filter-panel{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--bg-card);border:1px solid var(--border-light);border-radius:var(--radius);margin-bottom:1rem;flex-wrap:wrap}
.filter-group{display:flex;align-items:center;gap:5px}
.filter-label{font-size:var(--fs-sm);font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em;white-space:nowrap}
.filter-select{width:auto;min-width:90px;max-width:180px}
/* Transactions day-filter: wide enough that the year + the native
   date-picker / clear icons aren't clipped (the old .filter-select
   180px cap overlaid the last year digit). Uses .form-control (no
   select chevron) so nothing sits over the native calendar control. */
.txn-date-filter{width:auto;min-width:13rem;max-width:none}
/* Custom placeholder for the always-type=date day filter. The overlay
   span sits on top of the (text-hidden) empty input; pointer-events:
   none so a single click reaches the input and opens the native
   picker. ledger.js toggles .showing-native (focused or has a value).*/
.txn-date-wrap{position:relative;display:inline-block;vertical-align:middle}
.txn-date-wrap:not(.showing-native) .txn-date-filter{color:transparent}
.txn-date-ph{
  position:absolute;left:.65rem;top:50%;transform:translateY(-50%);
  pointer-events:none;color:var(--text-muted);font-size:var(--fs-sm);
  white-space:nowrap;
}
.txn-date-wrap.showing-native .txn-date-ph{display:none}
.filter-clear{margin-left:auto}

/* ── Direction toggle ────────────────────────────────────────────────────── */
.dir-toggle{display:flex;gap:0;border:1px solid var(--border);border-radius:6px;overflow:hidden;width:fit-content}
.dir-btn{display:flex;align-items:center;gap:6px;padding:.4rem .9rem;cursor:pointer;font-size:var(--fs-base);color:var(--text-secondary);transition:background .1s,color .1s}
.dir-btn:first-child{border-right:1px solid var(--border)}
.dir-btn input[type=radio]{display:none}
.dir-btn:has(input:checked){background:var(--accent);color:#fff}

/* ── Flow badge ──────────────────────────────────────────────────────────── */
.flow-badge{display:inline-block;font-size:var(--fs-xs);font-weight:600;letter-spacing:.05em;text-transform:uppercase;padding:.1em .45em;border-radius:4px;margin-left:5px;vertical-align:middle}
.flow-badge--asset{background:#d1fae5;color:#065f46}
.flow-badge--liability{background:#fee2e2;color:#991b1b}
.flow-badge--general{background:#e0e7ff;color:#3730a3}

/* ── Totals strip ─────────────────────────────────────────────────────────── */
.totals-strip{display:flex;align-items:center;gap:12px;padding:.625rem 1rem;background:var(--bg-card);border:1px solid var(--border-light);border-radius:var(--radius);margin-bottom:1rem;font-size:var(--fs-base)}
.totals-item{display:flex;gap:6px;align-items:center}
.totals-sep{color:var(--border)}

/* ── Budget ─────────────────────────────────────────────────────────────── */
.budget-list{padding:.5rem 1.25rem 1rem}
.budget-row{margin-bottom:1rem}
.budget-row:last-child{margin-bottom:0}
.budget-meta{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:5px}
.budget-cat{font-size:var(--fs-base);font-weight:500}
.budget-amounts{font-size:var(--fs-sm);color:var(--text-muted)}
.progress-track{height:5px;background:var(--border-light);border-radius:3px;overflow:hidden;margin-bottom:3px}
.progress-track--inline{margin-top:4px;margin-bottom:0}
.progress-fill{height:100%;background:var(--income);border-radius:3px;transition:width .3s}
.progress-fill--warn{background:#f59e0b}
.progress-fill--danger{background:var(--expense)}
.budget-pct{font-size:var(--fs-sm);color:var(--text-muted)}
.budget-table-cat{display:flex;flex-direction:column;gap:2px}

/* ── Account cards ─────────────────────────────────────────────────────────── */
.account-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:14px;margin-bottom:1.5rem}
.account-card{background:var(--bg-card);border:1px solid var(--border-light);border-radius:var(--radius-lg);padding:1.25rem;box-shadow:var(--shadow);position:relative}
/* Liability cards visually differentiated from assets: rose-tinted background,
   matching left border, and a small "Liability" tag in the header. Asset cards
   are unchanged so the eye picks out debts at a glance. */
.account-card--liability{background:#fff1f2;border-left:3px solid #f43f5e}
.account-class-tag{font-size:var(--fs-xs);letter-spacing:.06em;text-transform:uppercase;font-weight:600;padding:2px 6px;border-radius:4px;background:#fee2e2;color:#9f1239;margin-left:.4rem}
.account-card-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem;gap:.4rem}
.account-card-top .account-type-badge{margin-right:auto}
.account-card-top .action-link{margin-left:auto}
.account-type-badge{font-size:var(--fs-xs);font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--text-muted)}
.account-name{font-size:var(--fs-md);font-weight:600;margin-bottom:.5rem}

/* The account name links to the account's own page. Inherits rather than taking
   link colour: a heading that turns blue reads as navigation chrome instead of
   as the name of the thing. */
.account-name-link { color: inherit; text-decoration: none; }
.account-name-link:hover,
.account-name-link:focus-visible { text-decoration: underline; }

/* Unadorned list for short informational runs (the "still watching" charges).
   No bullets: they are a continuation of the sentence above them, not a menu. */
.plain-list { list-style: none; margin: 0; padding: 0; }
.plain-list li { padding: .15rem 0; }

/* Onboarding's experience-level question. No fieldset chrome — it reads as one
   more question on the step, not a boxed form within a form. */
.onboarding-level { border: 0; padding: 0; margin: 1rem 0 0; }
.onboarding-level > legend { padding: 0; margin-bottom: .35rem; }

/* Marks bank-linked accounts vs manual ones — inline before the name, in the accent colour. */
.account-linked-icon{
  width:15px;height:15px;flex:0 0 auto;
  color:var(--accent);vertical-align:-2px;margin-right:.35rem;
}
/* Household lens — owner pill under the account name, so an adult sees which
   member owns it. Rendered only when OwnerDisplayName is set (Household view). */
.account-owner{display:inline-flex;align-items:center;gap:.3rem;margin:-.25rem 0 .55rem;padding:.1rem .5rem .1rem .4rem;border-radius:100px;background:var(--amber-pale);color:var(--warn);font-size:var(--fs-sm);font-weight:600;line-height:1.4}
.account-owner svg{flex:0 0 auto;opacity:.85}
/* Which bank a linked account syncs from. Same chip idiom as the owner and
   sharing pills, in the accent colour the sync bolt already uses. */
/* Uncategorised spend on /budget — a reconciliation line, not a budget bar.
   Muted rather than alarming: it is work to do, not an overspend. */
.budget-row--uncategorised .budget-row__name{color:var(--text-muted)}
.budget-row--uncategorised .budget-row__sep{color:var(--text-muted);font-style:italic}
.account-institution{display:inline-flex;align-items:center;gap:.25rem;margin:-.25rem 0 .55rem;padding:.1rem .5rem .1rem .4rem;border-radius:100px;background:var(--coral-pale);color:var(--accent-soft);font-size:var(--fs-sm);font-weight:600;line-height:1.4;cursor:default}
.account-institution .account-linked-icon{width:13px;height:13px;margin-right:0;vertical-align:0}
/* Sharing-status chip (Personal view) — green = shared with household,
   grey = private to you. Shared by /accounts and /goals cards. */
.account-share{display:inline-flex;align-items:center;gap:.3rem;margin:-.25rem 0 .55rem;padding:.1rem .5rem .1rem .4rem;border-radius:100px;background:var(--income-bg);color:var(--income);font-size:var(--fs-sm);font-weight:600;line-height:1.4;cursor:default}
.account-share svg{flex:0 0 auto;opacity:.85}
.account-share--private{background:var(--table-header-bg);color:var(--text-muted)}
.account-balance{font-size:var(--fs-xl);font-weight:600;letter-spacing:-.02em;font-family:var(--font-mono);margin-bottom:.5rem}
.account-meta{font-size:var(--fs-sm);color:var(--text-muted);margin-bottom:.75rem}
/* Provider liability detail line (APR/min/due) on debt cards. */
.liability-detail{display:flex;flex-wrap:wrap;align-items:center;gap:.15rem}
.account-txn-link a{font-size:var(--fs-sm);color:var(--accent);font-weight:500}
.account-txn-link a:hover{text-decoration:underline}

/* ── Info / success / empty ─────────────────────────────────────────────── */
.info-card{background:var(--bg-card);border:1px solid var(--border-light);border-radius:var(--radius);padding:1rem 1.25rem;margin-bottom:1.5rem}
.info-card-steps{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.info-step{display:flex;align-items:center;gap:7px;font-size:var(--fs-sm);color:var(--text-secondary)}
.info-step-num{width:20px;height:20px;border-radius:50%;background:var(--sidebar-bg);color:#fff;font-size:var(--fs-xs);font-weight:600;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0}
.info-step-sep{color:var(--border);font-size:var(--fs-sm)}
.success-card{display:flex;align-items:center;gap:16px;background:var(--income-bg);border:1px solid #b7e8d8;border-radius:var(--radius-lg);padding:1.25rem 1.5rem;margin-bottom:1.5rem;color:var(--income)}
.success-icon{font-size:var(--fs-2xl);font-weight:700}
.success-card strong{display:block;font-weight:600;margin-bottom:2px}
.success-card p{font-size:var(--fs-base);margin:0;opacity:.8}
.empty-state{padding:1.5rem 1.25rem;font-size:var(--fs-base);color:var(--text-muted);text-align:center}
.empty-state a{color:var(--accent)}
.empty-card{text-align:center;padding:3rem 2rem;background:var(--bg-card);border:1px solid var(--border-light);border-radius:var(--radius-lg);color:var(--text-secondary)}
.empty-card p{margin-bottom:1rem}

/* ── Forms ─────────────────────────────────────────────────────────────── */
/* Single-column forms cap at 560px — comfortable scan length for an
 * email/password-style stack. Forms that contain a .form-row--2
 * (multi-column row) widen to 880px so each 2-col cell gets ~420px
 * of usable width instead of cramming dropdowns + selects into
 * ~270px each. `:has()` is supported by all evergreen browsers as of
 * Safari 15.4 / Chrome 105 / Firefox 121; older browsers fall through
 * to the 560px baseline (degraded but functional). Mobile is
 * unaffected — viewport ≪ 880px clamps via the natural sizing. */
.form-wrap{max-width:560px}
.form-wrap:has(.form-row--2){max-width:880px}
.form-group{margin-bottom:1.25rem}
.form-row{display:grid;gap:14px}
.form-row--2{grid-template-columns:1fr 1fr}
.form-label{display:block;font-size:var(--fs-base);font-weight:500;color:var(--text-primary);margin-bottom:6px}
.form-optional{font-size:var(--fs-xs);font-weight:400;color:var(--text-muted);margin-left:4px}
.form-required{font-size:var(--fs-xs);font-weight:500;color:#b91c1c;margin-left:4px;text-transform:uppercase;letter-spacing:.04em}
.form-hint{font-size:var(--fs-sm);color:var(--text-muted);margin-top:5px}
.form-error{font-size:var(--fs-sm);color:var(--expense);margin-top:4px}
/* A .card--table pads nothing, so the hint that stands in for the table when
   there is nothing to list has to state its own inset. Stated AFTER .form-hint
   on purpose: equal specificity, and that rule's margin-top longhand beats this
   shorthand's top value anywhere above it. Opt-in rather than
   `.card--table .form-hint`, which would also have moved the three hints on
   recurring and transaction_search that deliberately carry no inset. */
.card-empty-hint{margin:var(--sp-3) var(--sp-4)}
.form-control,.form-select{display:block;width:100%;padding:.5rem .75rem;font-size:var(--fs-base);font-family:var(--font-sans);color:var(--text-primary);background:var(--bg-card);border:1px solid var(--border);border-radius:6px;transition:border-color .12s,box-shadow .12s;appearance:none;-webkit-appearance:none}
.form-select{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='%238b929e' d='M4.5 6l3.5 4 3.5-4z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right .5rem center;background-size:16px;padding-right:2rem}
.form-control:focus,.form-select:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(26,127,100,.12)}
.form-control::placeholder{color:var(--text-muted)}
.form-control.form-control-sm,.form-select.form-select-sm{padding:.35rem .65rem;font-size:var(--fs-sm)}
/* Restore the arrow's clearance. The shorthand above resets padding-right,
   which the rule two lines up had widened to hold the chevron, so a small
   select rendered its longest option straight over the arrow — the account
   filter showed "Alex Checking" with no visible way to open it. The arrow
   sits 0.5rem in and is 16px wide, so it needs ~1.5rem; 1.75rem leaves a gap. */
.form-select.form-select-sm{padding-right:1.75rem}

/* ── seg-control — segmented-radio "pill group" for small fixed enums (≤4
   options). Native <input type="radio"> under the hood, so a11y is
   automatic (radiogroup + arrow-key nav). The visible chip is a sibling
   span; the input is screen-reader-only via :sr-only-equivalent positioning.
   Use when all options + their meaning matter at a glance (vs a quiet
   <select> for data pickers or long lists). */
.seg-control{display:inline-flex;border:1px solid var(--border);border-radius:8px;background:var(--bg-card);padding:2px;gap:2px;vertical-align:middle}
/* Money-flow month picker. A dropdown rather than a row of chips: the list runs
   to 24 months on a paid plan, which fits no card header at any width. */
.flow-month-picker{display:flex;align-items:center;gap:8px;flex-shrink:0}
/* The insights page-header controls: month picker + CSV, kept on one line so
   the month reads as a page filter rather than as part of any one card. */
.insights-controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.flow-month-picker .form-select{min-width:11rem}
/* The submit button is the no-script path; section_swap.js marks the form once
   the select applies itself, and then the button only repeats what just
   happened. */
/* Two forms side by side in one table cell (the Subscriptions row's
   Track it / Not a subscription pair). Each <form> is a block, so without
   this they stack and the row grows to two lines. */
.sub-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:.45rem}
/* On narrow viewports the long-label seg-controls (e.g. the account
 * classification "Asset (income-side) / Liability (expense-side) /
 * General (both)") can't fit all three nowrap chips in one row.
 * Opt-in modifier lets the group wrap to multiple rows instead of
 * clipping. Used by the /accounts edit form's classification row. */
.seg-control--wrap{display:flex;flex-wrap:wrap}
.seg-control__item{cursor:pointer;display:inline-flex;margin:0}
.seg-control__item input{position:absolute;opacity:0;width:1px;height:1px;pointer-events:none}
.seg-control__chip{padding:.38rem .75rem;font-size:var(--fs-sm);font-weight:500;color:var(--text-secondary);border-radius:6px;line-height:1.2;display:inline-flex;align-items:center;transition:background .12s,color .12s;white-space:nowrap}
.seg-control__item:hover .seg-control__chip{color:var(--text-primary)}
.seg-control__item input:focus-visible + .seg-control__chip{outline:2px solid var(--accent);outline-offset:2px}
.seg-control__item input:checked + .seg-control__chip{background:var(--accent);color:#fff}
.seg-control__item input:checked:focus-visible + .seg-control__chip{outline-color:transparent}
/* Link-based seg-control: same chips, but each option is a GET link rather
   than a radio. Used by the money-flow period picker, where the choice belongs
   in the URL so it can be bookmarked and shared. */
.seg-control__link{text-decoration:none;display:inline-flex;margin:0}
.seg-control__link:hover .seg-control__chip{color:var(--text-primary)}
.seg-control__link.is-active .seg-control__chip{background:var(--accent);color:#fff}
.seg-control__link:focus-visible .seg-control__chip{outline:2px solid var(--accent);outline-offset:2px}

/* Personal/Household view-scope toggle (sidebar) — a button-based segmented
   control (POSTs the lens, no JS). Reuses .seg-control but re-skinned in the
   sidebar palette as one solid green block so nothing reads as a light pill on
   the dark sidebar. The active lens is marked like .nav-item.active (white text
   + amber underline), not a second fill. */
.scope-toggle{margin:.85rem 1.25rem .35rem}
.scope-toggle .seg-control{display:flex;width:100%;background:var(--sidebar-active-bg);border-color:var(--sidebar-border)}
.scope-toggle__btn{flex:1;justify-content:center;border:none;background:transparent;cursor:pointer;font-family:inherit;color:var(--sidebar-text)}
.scope-toggle__btn:not(.is-active):hover{color:var(--sidebar-text-hover)}
.scope-toggle__btn.is-active{color:var(--sidebar-text-hover);font-weight:600;box-shadow:inset 0 -2px 0 var(--sidebar-active-accent)}
.scope-toggle__btn:focus-visible{outline:2px solid var(--sidebar-active-accent);outline-offset:-2px}
.input-prefix{position:relative}
.input-prefix-symbol{position:absolute;left:.75rem;top:50%;transform:translateY(-50%);color:var(--text-muted);font-size:var(--fs-base);pointer-events:none}
.input-prefix .form-control{padding-left:1.75rem}
.form-actions{display:flex;align-items:center;gap:10px;padding-top:.5rem;border-top:1px solid var(--border-light);margin-top:1.5rem}
.delete-form{margin-left:auto}
.divider{border:none;border-top:1px solid var(--border-light);margin:1.25rem 0}

/* ── Buttons ─────────────────────────────────────────────────────────────── */
.btn{display:inline-flex;align-items:center;gap:6px;padding:.5rem 1rem;font-size:var(--fs-base);font-weight:500;border-radius:100px;border:1px solid transparent;transition:transform .2s cubic-bezier(.34,1.56,.64,1),background .14s ease,border-color .14s ease,color .14s ease,box-shadow .2s ease;white-space:nowrap;font-family:var(--font-sans)}
.btn-primary{background:var(--accent);color:#fff;border-color:var(--accent);box-shadow:0 2px 8px rgba(27,58,45,.18)}
.btn-primary:hover{background:var(--accent-soft);border-color:var(--accent-soft);box-shadow:0 6px 16px rgba(27,58,45,.24)}
.btn-outline{background:var(--bg-card);color:var(--text-secondary);border-color:var(--border)}
.btn-outline:hover{border-color:var(--accent);color:var(--accent)}
.btn-ghost{background:transparent;color:var(--text-muted);border-color:transparent}
.btn-ghost:hover{background:var(--border-light);color:var(--text-primary)}
.btn-danger{background:var(--expense-bg);color:var(--expense);border-color:#fca5a5}
.btn-danger:hover{background:#fce8e8}
.btn-sm{padding:.3rem .75rem;font-size:var(--fs-sm)}
.btn-full{width:100%;justify-content:center}
/* Playful hover: every button lifts + grows a touch on a springy curve (the
   ease overshoots, so it bounces in), then presses down with a quick squish on
   click. Skips disabled buttons and honours prefers-reduced-motion. */
.btn:hover:not(:disabled){transform:translateY(-2px) scale(1.02)}
.btn:active:not(:disabled){transform:translateY(0) scale(.97);transition-duration:.08s}
@media (prefers-reduced-motion:reduce){
  .btn{transition:background .14s ease,border-color .14s ease,color .14s ease,box-shadow .14s ease}
  .btn:hover:not(:disabled),.btn:active:not(:disabled){transform:none}
}

/* ── Toast notifications ────────────────────────────────────────────────
   Fixed-position floating message at the bottom-center of the viewport.
   Lives outside the document flow so showing/hiding never reflows page
   content. Auto-dismiss + fade-out run from ledger.js after ~4 seconds;
   the close button (and clicking the toast body) also dismiss early.
   Translucent border on the success/error/warning variants matches the
   existing palette (income / expense / warn). */
/* Sized to be seen from wherever the click was: a toast lands away from the
   pointer, so it has to catch the eye before it can be read. Bigger type, more
   padding and a wider floor, paired with the longer dwell in ledger.js. */
.toast{
  position:fixed; left:50%; top:1.25rem; transform:translateX(-50%);
  display:flex; align-items:center; gap:1rem;
  /* 560 is a reading measure, not the screen: uncapped, a long message renders
     720px wide on a desktop and still wraps to two lines, so the extra width
     buys nothing and costs the eye a longer sweep. */
  min-width:300px; max-width:min(560px, calc(100vw - 2rem));
  padding:1rem 1.25rem 1rem 1.35rem; border-radius:12px;
  font-size:var(--fs-lg); line-height:1.45;
  background:var(--income-bg); color:var(--income); border:1px solid #b7e8d8;
  box-shadow:0 16px 36px rgba(0,0,0,.22), 0 3px 8px rgba(0,0,0,.12);
  z-index:1000;
  animation:toast-in .25s ease-out;
}
.toast-error,.toast-danger{background:var(--expense-bg); color:var(--expense); border-color:#fca5a5}
.toast-warning{background:var(--warn-bg); color:var(--warn); border-color:#fcd34d}
.toast-msg{flex:1}
.toast-close{
  background:transparent; border:0; color:inherit; opacity:.6; cursor:pointer;
  font-size:var(--fs-lg); line-height:1; padding:0 .25rem; font-family:inherit;
}
.toast-close:hover{opacity:1}
/* A view-only member's controls: visible, greyed, and view_only.js says why. */
:is(button, input, select, textarea, a).is-view-only{opacity:.5; cursor:not-allowed}
table.is-view-only .cat-reorder-handle{visibility:hidden}
@keyframes toast-in{
  from{opacity:0; transform:translate(-50%, -12px)}
  to  {opacity:1; transform:translate(-50%, 0)}
}
@media (prefers-reduced-motion: reduce){
  .toast{animation:none}
}
/* On a phone the toast could never use the room it had. Centring with left:50%
   makes the shrink-to-fit width half the viewport, so it sat at its min-width
   and wrapped a long message to four lines while 43px went spare either side.
   Pinning both edges instead lets it span the screen, and its own keyframes
   need replacing because they animate translate(-50%) for the old centring. */
@media (max-width: 480px){
  .toast{
    left:1rem; right:1rem; transform:none;
    min-width:0; max-width:none;
    animation-name:toast-in-edge;
  }
  @keyframes toast-in-edge{
    from{opacity:0; transform:translateY(-12px)}
    to  {opacity:1; transform:translateY(0)}
  }
}

/* ── Responsive ─────────────────────────────────────────────────────────── */
@media(max-width:900px){
  .two-col{grid-template-columns:1fr}
  .stat-grid--3,.stat-grid--4{grid-template-columns:1fr 1fr}
}
/* ── Mobile shell (≤768px) — canonical breakpoint ───────────────────────
   Replaces the old 56px icon-rail. The fixed sidebar becomes an
   off-canvas drawer opened by the app-bar hamburger. Two open
   mechanisms, both supported:
     • JS (ledger.js)  → toggles `.is-open` + manages aria/scroll-lock/Esc
     • No-JS fallback  → the burger is <a href="#sidebar">, so
                          `.sidebar:target` opens it; the scrim is
                          <a href="#"> which clears the hash to close.
   Multi-column grids collapse here too (desktop grid behaviour is left
   untouched above — protects the "desktop identical" constraint). */
@media(max-width:768px){
  /* Day filter: full-width on phones so the date box + native picker
     aren't squished inside the wrapping filter panel. */
  .filter-group--wide{flex:1 1 100%}
  .txn-date-wrap{display:block;width:100%}
  .txn-date-filter{width:100%;min-width:0}

  .appbar{
    display:flex;align-items:center;gap:var(--sp-3);
    /* z-index 50 puts the appbar (and its ☰ burger) above the open
     * drawer (sidebar gets z-index:40 below). Without this lift the
     * drawer covers the burger when open, leaving scrim-tap or Esc
     * as the only ways to close it — the burger's own click handler
     * in ledger.js can close it, but only if the user can see it.
     * The same fix is mirrored under :root[data-effective-layout=
     * "mobile"] in ledger.mobile.css for the explicit mobile toggle. */
    position:sticky;top:0;z-index:50;
    padding:var(--sp-3) var(--sp-4);
    background:var(--sidebar-bg);border-bottom:1px solid var(--sidebar-border);
  }
  .appbar-burger{
    display:inline-flex;align-items:center;justify-content:center;
    width:40px;height:40px;border-radius:var(--radius);
    color:var(--sidebar-text-hover);background:transparent;border:0;
    font-size:var(--fs-lg);line-height:1;
  }
  .appbar-burger:hover{background:rgba(255,255,255,.06)}
  .appbar-title{font-size:var(--fs-sm);font-weight:600;letter-spacing:.12em;color:var(--sidebar-text-hover)}

  .layout{display:block}
  .sidebar{
    width:min(82vw,300px);
    transform:translateX(-100%);
    transition:transform .22s ease,box-shadow .22s ease;
    z-index:40;
  }
  /* Shadow only when open: parked off-screen, its blur still shades the page edge. */
  .sidebar:target,.sidebar.is-open{transform:none;box-shadow:var(--elev-3)}

  .scrim{
    display:block;position:fixed;inset:0;z-index:35;
    background:rgba(2,6,23,.55);
    opacity:0;pointer-events:none;transition:opacity .22s ease;
  }
  .sidebar:target ~ .scrim,
  .sidebar.is-open ~ .scrim,
  body.drawer-open .scrim{opacity:1;pointer-events:auto}

  .main{margin-left:0}
  .main-inner{padding:var(--sp-4)}

  /* Stack the page-header title row + its action buttons. */
  .page-header{flex-direction:column;gap:var(--sp-3);align-items:stretch}
  .page-header-actions{flex-wrap:wrap}

  /* Grid collapse catch-all (desktop column rules untouched above). */
  .two-col,.stat-grid,.stat-grid--2,.stat-grid--3,.stat-grid--4,
  .account-grid,.account-mini-grid,.goal-grid,.badge-grid,
  .template-grid,.quick-log-grid,.income-card-grid,
  .persona-grid,.form-row,.custom-weights{
    grid-template-columns:1fr;
  }

  /* Comfortable tap targets + no iOS zoom-on-focus (inputs ≥16px). */
  .btn,.nav-item,.appbar-burger{min-height:44px}
  input,select,textarea{font-size:16px}

  /* Tables → stacked label/value cards. thead is visually hidden (kept
     for a11y); each row becomes a card; each cell shows its column name
     from data-label. Cells without a data-label (action buttons) just
     render right-aligned with no label. */
  .card--table{padding:var(--sp-3)}
  .ledger-table thead{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
  .ledger-table,.ledger-table tbody,.ledger-table tr,.ledger-table td{display:block;width:auto}
  .ledger-table tr{
    background:var(--bg-card);border:1px solid var(--border-light);
    border-radius:var(--radius-lg);box-shadow:var(--elev-1);
    margin-bottom:var(--sp-3);padding:var(--sp-2) var(--sp-4);
  }
  .ledger-table tbody tr:last-child{margin-bottom:0}
  .ledger-table tbody tr:hover{background:var(--bg-card)}
  .ledger-table td{
    display:flex;justify-content:space-between;align-items:baseline;
    gap:var(--sp-4);text-align:right;
    padding:var(--sp-2) 0;border:0;
    border-bottom:1px solid var(--border-light);
  }
  .ledger-table td:last-child{border-bottom:0}
  .ledger-table td::before{
    content:attr(data-label);
    flex:0 0 auto;text-align:left;
    font-size:var(--fs-xs);font-weight:600;letter-spacing:.04em;
    text-transform:uppercase;color:var(--text-muted);
  }
  /* Action cells carry several buttons and no label. Every other cell is one
     label plus one value, which fits a phone on one line; three buttons do not,
     and as items in a nowrap flex row they ran past the card and off the screen
     rather than wrapping — so Disconnect was unreachable at 375px. They wrap
     and sit right, and the empty ::before is dropped because a zero-width
     pseudo still claims a gap and pushes the first button further over. */
  .ledger-table td[data-label=""],
  .ledger-table td:not([data-label]){
    flex-wrap:wrap;justify-content:flex-end;gap:var(--sp-2);
  }
  .ledger-table td[data-label=""]::before,
  .ledger-table td:not([data-label])::before{content:none}

  /* Overflow safety net — nothing should push the page sideways on a
     phone. Media (incl. SVG charts) caps to its container; the score
     ring + the 2-col scorecard stack. */
  img,svg,canvas,video{max-width:100%;height:auto}
  .scorecard{grid-template-columns:1fr;gap:var(--sp-4);text-align:center}

  /* Calendar stays a 7-col month grid (columns keep aligned via 1fr)
     but tightens so it fits a 375px viewport without overflow. */
  .cal-grid-head,.cal-week{gap:3px}
  .cal-cell{min-height:60px;padding:3px}
  .cal-chip{font-size:var(--fs-xs)}
  .cal-chip__label{max-width:5ch}
}

@media(max-width:768px) and (prefers-reduced-motion:reduce){
  .sidebar,.scrim{transition:none}
}

/* ── Scorecard ──────────────────────────────────────────────────────────── */
/* Block, not a two-column grid: the card now stacks the circle-graph summary,
   the pillar legend and the trend strip, each of which lays itself out. */
.scorecard{background:var(--bg-card);border:1px solid var(--border-light);border-radius:var(--radius);padding:1.25rem 1.5rem;margin-bottom:1.5rem}
.scorecard--compact{padding:1rem 1.25rem;margin-bottom:1rem}
.scorecard-trend{display:flex;align-items:center;gap:1.25rem;flex-wrap:wrap;margin-top:1rem;padding-top:.85rem;border-top:1px solid var(--border-light)}
.scorecard-spark{min-width:180px;flex:1}
.scorecard-tier{display:flex;align-items:center;gap:.5rem;margin:.85rem 0 0}
/* Still used on /health/{pillar}, where a single pillar's own grade sits beside
   its rubric. flex-shrink:0 keeps it round when the flex parent compresses it
   sideways; without it the circle goes egg-shaped at narrow widths. */
.score-circle{width:84px;height:84px;flex-shrink:0;border-radius:50%;display:flex;flex-direction:column;justify-content:center;align-items:center;color:#fff;font-family:var(--font-mono)}
.score-circle.score-good{background:#1a7f64}
.score-circle.score-warn{background:#d97706}
.score-circle.score-bad{background:#b91c1c}
/* A pillar that is not in the weighted mean has no score. Outlined and muted
   rather than one of the three grades, because grey-as-a-grade would still be
   read as "bad" next to the red one. */
.score-circle--na{background:none;border:2px dashed var(--border-light);color:var(--text-muted);text-align:center;padding:0 .4rem}
.score-circle--na .score-label{opacity:1;font-size:var(--fs-xs);line-height:1.2;font-family:var(--font-sans)}
.score-number{font-size:var(--fs-2xl);font-weight:600;line-height:1}
.score-label{font-size:var(--fs-xs);opacity:.75;margin-top:2px}
/* --sm is the compact 64x64 variant used wherever a pillar's own score sits
   beside its rubric. Inner text scales proportionally: 20px score number =
   round(26 * 64/84); 10px label keeps the prior floor. */
.score-circle--sm{width:64px;height:64px;flex-shrink:0}
.score-circle--sm .score-number{font-size:var(--fs-xl)}
.score-circle--sm .score-label{font-size:var(--fs-xs)}

/* ── Rubric (score breakdown) ─────────────────────────────────────────── */
.rubric-list{display:flex;flex-direction:column;gap:.5rem;margin-top:.5rem}
.rubric-row{padding:.75rem .9rem;border-radius:8px;border:1px solid var(--border-light);background:rgba(255,255,255,.02)}
.rubric-row--full{border-color:rgba(26,127,100,.4);background:rgba(26,127,100,.06)}
.rubric-row--partial{border-color:rgba(217,119,6,.35);background:rgba(217,119,6,.04)}
.rubric-row--empty{border-color:rgba(185,28,28,.3);background:rgba(185,28,28,.04)}
.rubric-row-head{display:flex;align-items:baseline;justify-content:space-between;gap:1rem}
.rubric-points{font-size:var(--fs-base);white-space:nowrap}
.rubric-earned{font-size:var(--fs-lg);font-weight:600}
.rubric-detail{margin:.25rem 0;font-size:var(--fs-base);line-height:1.45}
.rubric-actual{margin:.4rem 0 0;font-size:var(--fs-base)}

/* ── Breakdown page (one card per pillar) ─────────────────────────────── */
.breakdown-pillar{padding:1.25rem;margin-bottom:1rem}
.breakdown-pillar-head{display:flex;align-items:center;justify-content:space-between;gap:1rem}
.breakdown-pillar-title{display:flex;align-items:center;gap:.5rem;font-weight:600;font-size:var(--fs-md);flex:1;min-width:0}
/* The separating rule belongs to the expanded state: collapsed, it would be a
   line under a lone heading with nothing beneath it. */
details.breakdown-pillar[open]>.breakdown-pillar-head{padding-bottom:.85rem;border-bottom:1px solid var(--border-light);margin-bottom:.85rem}
.breakdown-recs{margin-top:1rem;padding-top:1rem;border-top:1px solid var(--border-light)}
.breakdown-recs-title{margin:0 0 .5rem;letter-spacing:.04em;text-transform:uppercase}
/* ── Score circle graph + legend ────────────────────────────────────────────
   The graph carries two quantities at once (wedge angle = what a pillar is
   worth, wedge radius = how much of that it earned), so the legend does not
   repeat the picture. It gives the numbers the picture only approximates. */
.score-summary{display:grid;grid-template-columns:auto 1fr;gap:1.5rem;align-items:center}
.score-summary__graph{position:relative;display:block;line-height:0}
.score-summary__graph .score-donut{width:200px;height:200px;max-width:100%}
.scorecard--compact .score-summary__graph .score-donut{width:150px;height:150px}
.score-donut__hub{fill:var(--text-primary);font-family:var(--font-mono)}
/* Hover a wedge: it lifts out of the ring, the rest recede, and the middle of
   the graph swaps the overall score for that pillar's own figure. All of it is
   selector-driven. script-src is 'self' with no inline allowance, and this is
   not worth a JS file when :hover already expresses it.

   An enhancement only, and gated on :has() throughout. Touch has no hover and
   the legend under the graph carries the same numbers; a browser without :has()
   falls back to the native tooltip from each wedge's <title>. Screen readers
   are served by the SVG's aria-label, which already names every pillar's
   contribution, not by those titles: role="img" makes the graph a leaf. */
.score-donut__seg{transition:opacity .15s ease}
/* Each wedge is an <a> to its own pillar page, so it needs the affordance the
   wrapping link used to provide. */
.score-donut__seg[href]{cursor:pointer}
.score-donut__track{fill-opacity:.13}
/* transform-box is stated rather than assumed: its initial value is view-box
   per spec, but getting it wrong scales the wedge about its own bounding box
   and flings it off the ring instead of growing it in place. */
.score-donut__seg path{transform-box:view-box;transform-origin:50% 50%;transition:transform .15s ease}
.score-donut:has(.score-donut__seg:hover) .score-donut__seg{opacity:.25}
/* Repeats the :has() so it outweighs the rule above it. A plain
   `.score-donut__seg:hover` loses on specificity and every wedge stays dimmed. */
.score-donut:has(.score-donut__seg:hover) .score-donut__seg:hover{opacity:1}
.score-donut__seg:hover path{transform:scale(1.05)}
.score-donut__hub-group{transition:opacity .12s ease}
.score-donut:has(.score-donut__seg:hover) .score-donut__hub-group{opacity:0}
/* pointer-events:none matters even while hidden: the panel sits over the hub,
   so without it, moving the pointer through the middle of the graph would
   silently hover whichever wedge's panel happened to be underneath. */
.score-donut__detail{opacity:0;pointer-events:none;transition:opacity .12s ease}
/* Gated on :has() like the rest of the interaction, not on :hover alone. A
   browser without :has() would otherwise reveal the panel while leaving the
   hub visible underneath it, printing two different numbers on top of each
   other. Without the gate the graph is simply static, which is fine. */
.score-donut:has(.score-donut__seg:hover) .score-donut__seg:hover .score-donut__detail{opacity:1}
.score-donut__detail-label{fill:var(--text-secondary);font-family:var(--font-sans)}
.score-donut__detail-pts{fill:var(--text-primary);font-family:var(--font-mono)}
.score-donut__detail-of{fill:var(--text-muted)}
@media(prefers-reduced-motion:reduce){
  .score-donut__seg path{transition:none}
  .score-donut__seg:hover path{transform:none}
}
.score-donut__sub{fill:var(--text-muted);font-family:var(--font-sans)}
/* The tier chip. It briefly lived inside the graph, which broke as soon as a
   household hit "Foundation Builder": the hub is 2 x the inner radius across and
   the label overflowed it. Out here the length is free. */
.score-tier{display:inline-block;padding:.15rem .5rem;border-radius:999px;font-size:var(--fs-xs);font-weight:600;letter-spacing:.04em;text-transform:uppercase}
.score-tier--bronze{background:rgba(180,83,9,.12);color:#92400e}
.score-tier--silver{background:rgba(156,163,175,.18);color:#374151}
.score-tier--gold{background:rgba(251,191,36,.16);color:#92400e}
.score-summary__tier{margin:0 0 .45rem}
.score-summary__sentence{font-size:var(--fs-lg);line-height:1.45;margin:0 0 .5rem;color:var(--text-primary)}
.score-summary__meta{margin:0}

.pillar-legend{list-style:none;margin:1rem -.4rem 0;padding:0;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:.15rem .75rem}
.pillar-legend__item a{display:flex;align-items:center;gap:.5rem;padding:.3rem .4rem;border-radius:6px;text-decoration:none;color:inherit;font-size:var(--fs-sm)}
.pillar-legend__item a:hover{background:var(--ring-cell-hover-bg)}
.pillar-legend__label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pillar-legend__points{font-family:var(--font-mono);font-size:var(--fs-xs);font-variant-numeric:tabular-nums}
.pillar-legend__note{font-size:var(--fs-xs);color:var(--text-muted);font-style:italic}
.pillar-legend__item--na{opacity:.7}
/* Hollow swatch for a pillar that holds no wedge in the graph, so the legend
   cannot imply a slice the reader will then fail to find. */
.pillar-legend__item--na .pillar-legend__dot{background:none;box-shadow:inset 0 0 0 2px currentColor;color:var(--text-muted)}
.pillar-legend__dot{width:10px;height:10px;border-radius:3px;flex-shrink:0}
.pillar-legend__dot--visibility{background:#3e6901}
.pillar-legend__dot--income{background:#2ba06c}
.pillar-legend__dot--expense{background:#f86111}
.pillar-legend__dot--budget{background:#7b5aae}
.pillar-legend__dot--emergency{background:#1690ca}
.pillar-legend__dot--debt{background:#a73c4c}
.pillar-legend__dot--investing{background:#e31bf6}
.pillar-legend__dot--goals{background:#f452a1}
.pillar-legend__dot--outcome{background:#4d1beb}

/* ── Score breakdown (/health/breakdown) ────────────────────────────────────
   Shaped like a till receipt: one line per pillar, a rule, then a total the
   lines actually reach. Prefixed score-breakdown, not receipt: this app already
   has receipts (the photos attached to a transaction, .receipt-thumb and
   friends further down) and two unrelated things under one prefix is how the
   wrong rule gets edited. */
.score-breakdown__lede{margin:0 0 .85rem}
.score-breakdown__lines{list-style:none;margin:0;padding:0}
.score-breakdown__line{display:grid;grid-template-columns:minmax(9rem,1.1fr) minmax(4rem,2fr) auto;align-items:center;gap:.85rem;padding:.45rem 0;border-bottom:1px dotted var(--border-light)}
.score-breakdown__name{display:flex;align-items:center;gap:.5rem;text-decoration:none;color:inherit;min-width:0}
.score-breakdown__name:hover{text-decoration:underline}
.score-breakdown__bar{display:block;height:8px;border-radius:999px;background:var(--border-light);overflow:hidden}
.score-breakdown__bar-fill{display:block;height:100%;border-radius:999px;background:var(--accent)}
.score-breakdown__pts{font-family:var(--font-mono);font-variant-numeric:tabular-nums;text-align:right;white-space:nowrap}
.score-breakdown__earned{font-weight:700}
.score-breakdown__note{font-size:var(--fs-xs);color:var(--text-muted);font-style:italic}
.score-breakdown__line--na{opacity:.72}
/* Hollow swatch, matching the legend: a filled dot promises a wedge in the
   circle graph that this pillar does not have. */
.score-breakdown__line--na .pillar-legend__dot{background:none;box-shadow:inset 0 0 0 2px currentColor;color:var(--text-muted)}
.score-breakdown__total{display:flex;align-items:baseline;justify-content:space-between;gap:1rem;margin:.85rem 0 0;padding-top:.75rem;border-top:2px solid var(--text-primary);font-weight:600}
.score-breakdown__total-pts{font-family:var(--font-mono);font-size:var(--fs-xl);font-variant-numeric:tabular-nums}
.breakdown-pillar-foot{margin:.75rem 0 0}
/* <details> in a card: the summary is the clickable header, so kill the
   default marker and give it the pointer affordance instead. */
details.breakdown-pillar>summary{cursor:pointer;list-style:none}
details.breakdown-pillar>summary::-webkit-details-marker{display:none}
details.breakdown-pillar>summary::after{content:"▾";color:var(--text-muted);transition:transform .15s}
details.breakdown-pillar[open]>summary::after{transform:rotate(180deg);display:inline-block}
.pillar-detail{display:flex;align-items:center;gap:24px}
.pillar-detail-body{flex:1}
.pillar-status{font-size:var(--fs-base);color:var(--text-secondary);margin:0}
.rec-list{display:flex;flex-direction:column;gap:14px}
.rec-row{display:flex;align-items:center;gap:14px;padding:.75rem 0;border-bottom:1px solid var(--border-light)}
.rec-row:last-child{border-bottom:none}
.rec-body{flex:1}
.rec-body strong{display:block;font-size:var(--fs-base);color:var(--text-primary);margin-bottom:2px}
.rec-body p{margin:0;font-size:var(--fs-base);color:var(--text-secondary)}
.prompt-card--rec{border-color:#a7d4c4;background:linear-gradient(0deg,var(--income-bg),var(--bg-card))}
.strategy-presets{display:flex;flex-direction:column;gap:10px;border:none;padding:0;margin:0}
.strategy-option{display:flex;align-items:flex-start;gap:12px;padding:.85rem 1rem;border:1px solid var(--border-light);border-radius:var(--radius);cursor:pointer}
.strategy-option:has(input:checked){border-color:var(--accent);background:rgba(26,127,100,.04)}
.strategy-option-body{display:flex;flex-direction:column;gap:4px;flex:1}
.weight-mini{display:flex;flex-wrap:wrap;gap:6px;margin-top:4px;font-size:var(--fs-xs);color:var(--text-muted);font-family:var(--font-mono)}
.weight-mini span{padding:1px 5px;border:1px solid var(--border-light);border-radius:3px}
.custom-weights{display:grid;grid-template-columns:repeat(2,1fr);gap:10px 16px;margin-top:8px}
.custom-weights label{display:flex;align-items:center;justify-content:space-between;gap:10px;font-size:var(--fs-base);color:var(--text-primary)}
/* Inputs are standard .form-control now (consistent border / radius / focus
   ring / font with every other form); just constrain the width for the grid. */
.custom-weights .form-control{flex:0 0 auto;width:5.5rem;padding:.4rem .6rem;text-align:right;font-variant-numeric:tabular-nums}

@media(max-width:900px){
  /* Circle graph over the sentence, and the legend narrows to two columns so
     pillar names stay narrower. */
  .score-summary{grid-template-columns:1fr;gap:1rem;justify-items:center;text-align:center}
  .pillar-legend{grid-template-columns:repeat(2,minmax(0,1fr));text-align:left}
}
@media(max-width:520px){
  /* One column: two clip every label to an ellipsis at phone width. */
  .pillar-legend{grid-template-columns:1fr}
  .score-breakdown__line{grid-template-columns:1fr auto}
  .score-breakdown__bar{display:none}
}

/* ── Onboarding ─────────────────────────────────────────────────────────── */
.onboarding-shell{max-width:720px;margin:1rem auto}
.onboarding-progress{margin:0 0 1.5rem;text-align:center}
.onboarding-progress__label{margin:0 0 .55rem;font-size:var(--fs-sm);font-weight:600;color:var(--text-secondary);letter-spacing:.01em}
.onboarding-progress__name{color:var(--text-muted);font-weight:400}
.onboarding-progress__name::before{content:"  ";white-space:pre}
.onboarding-progress__pills{display:flex;justify-content:center;gap:8px}
.step-pill{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:50%;background:var(--border-light);color:var(--text-muted);font-size:var(--fs-base);font-weight:600;font-family:var(--font-mono)}
.step-pill--active{background:var(--sidebar-bg);color:#fff}
.step-pill--done{background:#1a7f64;color:#fff}
.form-section-title{font-size:var(--fs-base);font-weight:600;color:var(--text-primary);margin:.5rem 0 .75rem}
.form-help{font-size:var(--fs-sm);color:var(--text-muted);margin:.25rem 0 .75rem}
/* Help placed between a field's label and its input, not under the input. */
.form-help.form-help--above-input{margin:.15rem 0 .5rem}
/* Onboarding's "Nothing to add? Next moves you on" note under each step. */
.form-help.onboarding-skip{margin:.6rem 0 0}
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.form-label{display:block;font-size:var(--fs-sm);font-weight:500;color:var(--text-secondary);margin:.85rem 0 .35rem}
.checkbox-inline{display:flex;align-items:center;gap:6px;font-size:var(--fs-sm);color:var(--text-secondary);cursor:pointer}
/* Centring only reads right while the label is one line. Where it wraps to a
   paragraph — the deferred-interest explainer, the reconcile options — the box
   has to sit with the first line instead. */
.checkbox-inline--top{align-items:flex-start;gap:var(--sp-2)}
.withholding-grid{display:flex;flex-direction:column;gap:6px}
.withholding-grid-header,.withholding-row{display:grid;grid-template-columns:1.4fr 1.2fr 1fr 50px;gap:10px;align-items:center}
.withholding-grid-header{font-size:var(--fs-xs);text-transform:uppercase;letter-spacing:.5px;color:var(--text-muted);padding-bottom:4px;border-bottom:1px solid var(--border-light)}
@media(max-width:680px){
  .form-row,.withholding-row,.withholding-grid-header,.custom-weights{grid-template-columns:1fr}
}

/* ── Income-rule live math strip ──────────────────────────────────────
   Pinned under the Gross/Net inputs on /income/add and /income/{id}/edit.
   Three states keyed by .is-ok | .is-off | .is-empty on the wrapper —
   driven by income_form.js. Tokens follow the rest of the app so light
   and dark variants flow through without per-state hex literals. */
.income-math{
  background:var(--table-header-bg);
  border:1px solid var(--border-light);
  border-radius:var(--radius);
  padding:.75rem 1rem;
  margin-top:.75rem;
  font-size:var(--fs-sm);
  font-family:var(--font-mono);
}
.income-math__row{
  display:flex;justify-content:space-between;align-items:baseline;
  padding:.15rem 0;
}
.income-math__label{color:var(--text-secondary)}
.income-math__val{font-variant-numeric:tabular-nums;color:var(--text-primary)}
.income-math__row--expected{
  border-top:1px dashed var(--border);
  padding-top:.45rem;margin-top:.3rem;
  font-weight:600;
}
.income-math__row--delta .income-math__val{font-weight:600}
.income-math.is-ok  .income-math__row--delta .income-math__val{color:var(--income)}
.income-math.is-off .income-math__row--delta .income-math__val{color:var(--expense)}
.income-math.is-empty .income-math__row--delta .income-math__val{color:var(--text-muted)}
.income-math__hint{
  margin:.6rem 0 0;
  font-family:var(--font-sans);
  font-size:var(--fs-xs);
  color:var(--text-muted);
  line-height:1.4;
}

/* ── Sparkline + goals ──────────────────────────────────────────────────── */
.sparkline{display:block;width:200px;height:40px;color:var(--text-muted);margin-top:6px}
.goal-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px}
/* .goal-card / .goal-header / .goal-name — the original goal-card vocabulary,
   still used by /shopping (list cards) and /goals/{id} (detail header). Left
   intact so those surfaces are unchanged; the /goals grid card was migrated
   to the .account-card shell below and no longer uses these. */
.goal-card{padding:1rem 1.25rem;display:flex;flex-direction:column;gap:8px}
.goal-header{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}
.goal-name{margin:.25rem 0 0;font-size:var(--fs-md);font-weight:600;font-family:var(--font-sans);letter-spacing:normal}
.goal-name a{color:var(--text-primary)}
/* /goals grid card — reuses the .account-card shell (see "Account cards"
   above) so the two money-object surfaces read as one card system: identical
   padding, border, shadow, name + balance type scale, and the same owner pill
   / share chip from /accounts. Only the goal-specific bits are styled here —
   the badge + actions header row, the current/target headline figure, and the
   progress footer. Markup: class="account-card" (no .goal-card). */
.goal-card-top{display:flex;align-items:flex-start;justify-content:space-between;gap:.5rem;margin-bottom:.75rem}
.goal-badges{display:flex;align-items:center;flex-wrap:wrap;gap:.35rem}
.goal-actions{display:flex;gap:.35rem;flex-shrink:0}
.goal-card-name{margin:0 0 .5rem;font-size:var(--fs-md);font-weight:600;font-family:var(--font-sans);letter-spacing:normal}
.goal-card-name a{color:var(--text-primary)}
/* Current / target — the goal's headline figure. Reuses .account-balance
   (mono, --fs-xl) for the saved amount; the " / target" half is muted. */
.goal-target{color:var(--text-muted);font-weight:500}
.goal-footer{display:flex;align-items:center;flex-wrap:wrap;gap:10px;font-size:var(--fs-sm);color:var(--text-muted);margin-top:.25rem}
.progress-fill--success{background:#1a7f64}
.small{font-size:var(--fs-sm)}

/* ── Card body padding ─────────────────────────────────────────────────────
   `.card` is just a chrome container (background + border + radius). When it
   holds a `.card-header`, `.ledger-table`, or `.budget-list`, those inner
   elements supply their own padding and stay edge-flush — that's the
   dashboard pattern. When it holds a form, prose, or one of the wellness
   widgets directly, those need their own body padding or text sits against
   the boundary. Match the 1.25rem horizontal used by .card-header /
   .ledger-table cells so the spacing is visually consistent. */
form.card,
.card > form,
/* .card-body is the general answer to the same problem: a card whose content
   is not itself a form still needs the gutter. Reach for it rather than adding
   another content class to this list — the list is hand-maintained, and the
   Confirm-backfill card went unpadded for exactly as long as nobody noticed its
   <h2> was not on it. */
.card > .card-body,
.card > .pillar-detail,
.card > .rec-list,
.card > .score-breakdown__body,
.card > .strategy-presets {
  padding: 1.25rem;
}
/* Same reason, but the rubric list is also a direct child of the per-pillar
   cards on /health/breakdown, which already carry their own padding. Excluding
   them keeps that one from double-insetting. */
.card:not(.breakdown-pillar) > .rubric-list {
  padding: 1.25rem;
  margin-top: 0;
}
/* The same double-inset, from the other direction: .card--pad already gutters
   the card's own box, so a form inside one must not add a second. Two effects,
   both on the transaction form: the fields sat 2.75rem in, and each of the
   out-of-band POST forms that the Delete buttons target — which hold nothing
   but a hidden input — rendered as an empty 37.5px band, one per receipt.
   Stated here, after `.card > form`, because it matches at the same weight and
   would lose on source order anywhere above it. */
.card--pad > form {
  padding: 0;
}

/* ── Income source extras ───────────────────────────────────────────────── */
.auto-pill{display:inline-block;font-size:var(--fs-xs);font-weight:600;letter-spacing:.04em;text-transform:uppercase;padding:1px 7px;border-radius:10px}
.auto-pill--on{background:var(--income-bg);color:var(--income);border:1px solid #b7e8d8}
.auto-pill--off{background:var(--border-light);color:var(--text-muted);border:1px solid var(--border)}
.checkbox-row{display:flex;align-items:flex-start;gap:10px;padding:.5rem 0;font-size:var(--fs-base);line-height:1.5}
.checkbox-row input[type=checkbox]{margin-top:3px;flex-shrink:0}
.checkbox-row strong{display:block;color:var(--text-primary)}

/* ── Dashboard sections ─────────────────────────────────────────────────── */
.dash-section{margin-bottom:1.75rem}
.dash-section-head{display:flex;justify-content:space-between;align-items:baseline;margin:0 0 .65rem;padding:0 .25rem}
.dash-section-title{font-size:var(--fs-base);font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--text-muted);margin:0}
.dash-section-sub{font-size:var(--fs-sm);color:var(--text-muted)}

/* Debt card — canonical view of every household debt on /accounts.
 * Replaces the prior plan-card-plus-table pair that duplicated queue
 * ordering. Each row carries the full term sheet (balance/APR/min/
 * target/due) plus the Edit/Add terms action. Strategy-specific
 * bits (rank badge, recommended monthly payment, cascade arrow) only
 * render for ranked rows. Excluded + paid-off debts still appear so
 * the user can edit terms or re-enroll, with muted treatment. */
.debt-plan-card{margin:0 0 1rem;padding:1.1rem 1.25rem}
.debt-plan-card__head{margin-bottom:.85rem}
.debt-plan-card__title{font-size:var(--fs-md);font-weight:600;margin:0 0 .25rem;font-family:var(--font-serif);letter-spacing:-.005em}
.debt-plan-card__sub{font-size:var(--fs-sm);color:var(--text-muted);margin:0;line-height:1.5}
.debt-plan-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.65rem}
.debt-plan-row{border:1px solid var(--border-light);border-radius:6px;padding:.75rem .9rem;background:var(--bg-card)}
.debt-plan-row--target{border-color:#fcd34d;background:rgba(254,243,199,.25)}
.debt-plan-row--excluded,.debt-plan-row--paid{opacity:.72}
.debt-plan-row--paid .debt-plan-row__name{text-decoration:line-through;text-decoration-color:rgba(0,0,0,.25)}

.debt-plan-row__head{display:flex;align-items:baseline;gap:.65rem;flex-wrap:wrap}
.debt-plan-row__rank{font-size:var(--fs-xs);font-weight:600;letter-spacing:.02em;padding:2px 8px;border-radius:4px;background:#e5e7eb;color:#6b7280;white-space:nowrap;flex:0 0 auto;text-transform:uppercase}
.debt-plan-row__rank--target{background:#fef3c7;color:#92400e}
.debt-plan-row__rank--muted{background:transparent;color:var(--text-muted);border:1px solid var(--border-light)}
.debt-plan-row__name{font-weight:500;flex:1 1 auto;min-width:0}
.debt-plan-row__amount{font-weight:600;flex:0 0 auto;white-space:nowrap}

.debt-plan-row__promo{margin-top:.45rem;font-size:var(--fs-sm);display:flex;align-items:center;gap:.45rem;flex-wrap:wrap}

/* Terms grid — five label/value pairs (Balance / APR / Min / Target /
 * Due) laid out as a wrapping flex row. Each pair is a tiny stack:
 * muted uppercase label over a mono value. Wraps to multiple lines
 * on narrow viewports without breaking pairs. */
.debt-plan-row__terms{display:flex;flex-wrap:wrap;gap:.85rem 1.4rem;margin:.55rem 0 0;padding:0}
.debt-plan-row__terms > div{display:flex;flex-direction:column;gap:1px;min-width:5rem}
.debt-plan-row__terms dt{font-size:.65rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted);margin:0}
.debt-plan-row__terms dd{margin:0;font-size:var(--fs-sm);color:var(--text-primary)}

.debt-plan-row__split{font-size:var(--fs-sm);margin-top:.45rem;line-height:1.4}
.debt-plan-row__cascade{font-size:var(--fs-sm);color:var(--text-secondary);margin-top:.5rem;padding-top:.5rem;border-top:1px dashed var(--border-light);line-height:1.4}
.debt-plan-row__foot{margin-top:.5rem;padding-top:.45rem;border-top:1px dashed var(--border-light);display:flex;justify-content:flex-end;gap:.85rem;flex-wrap:wrap}
.debt-plan-row__foot .action-link{font-size:var(--fs-sm)}
/* Monthly-budget input row + the "needs info to join the plan" nudge. */
.debt-budget-form__row{display:flex;gap:.5rem;align-items:center;max-width:340px;margin-top:.35rem}
.debt-budget-form__row .input-prefix{flex:1}
.debt-plan-row__needsinfo{display:flex;gap:.6rem;align-items:center;flex-wrap:wrap;font-size:var(--fs-sm);color:var(--text-secondary);margin-top:.4rem}

/* Cash-flow card — income vs expense bars on a shared 100% baseline,
   net position summarised below. Bar width comes from the handler so a
   $0/0 month doesn't divide-by-zero. */
.cashflow-card{padding:1.25rem 1.5rem}
.cashflow-row{display:grid;grid-template-columns:80px 1fr 130px 130px;gap:14px;align-items:center;padding:.4rem 0}
.cashflow-row-label{font-size:var(--fs-base);color:var(--text-secondary);font-weight:500}
.cashflow-bar-track{height:14px;background:var(--border-light);border-radius:7px;overflow:hidden}
.cashflow-bar{display:block;height:100%;border-radius:7px;transition:width .4s ease-out}
.cashflow-bar--income{background:linear-gradient(90deg,#34d399,#047857)}
.cashflow-bar--expense{background:linear-gradient(90deg,#fb923c,#c2410c)}
.cashflow-row-amount{font-size:var(--fs-md);font-weight:600;text-align:right}
.cashflow-row-compare{font-size:var(--fs-sm);text-align:right}
.cashflow-net{display:flex;align-items:baseline;gap:14px;margin-top:.85rem;padding-top:.85rem;border-top:1px solid var(--border-light)}
.cashflow-net-label{font-size:var(--fs-sm);text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted);font-weight:600}
.cashflow-net-value{font-size:var(--fs-xl);font-weight:600;letter-spacing:-.02em}
.cashflow-net-tag{font-size:var(--fs-sm);color:var(--text-muted);margin-left:auto}
.cashflow-net--surplus .cashflow-net-value{color:var(--income)}
.cashflow-net--deficit .cashflow-net-value{color:var(--expense)}
/* Net worth row inside the cashflow card — visually a "by the way,
   here's the lifetime number" anchor under the month-level cash flow.
   Lighter weight than .cashflow-net so the month figure still leads. */
.cashflow-networth{display:flex;align-items:baseline;gap:14px;margin-top:.6rem;padding-top:.6rem;border-top:1px dashed var(--border-light);font-size:var(--fs-sm)}
.cashflow-networth-label{text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted);font-weight:600}
.cashflow-networth-value{font-weight:600;letter-spacing:-.01em}
.cashflow-networth-link{margin-left:auto;text-decoration:none;font-size:var(--fs-sm)}
.cashflow-networth-link:hover{text-decoration:underline}

/* Account mini cards — compact tiles for the dashboard's accounts section.
   Distinct from .account-card on /accounts which is roomier. */
.account-mini-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px}
.account-mini-card{display:flex;flex-direction:column;gap:4px;padding:.85rem 1rem;background:var(--bg-card);border:1px solid var(--border-light);border-radius:var(--radius);text-decoration:none;color:inherit;transition:border-color .12s,transform .12s}
.account-mini-card:hover{border-color:var(--text-muted);transform:translateY(-1px)}
.account-mini-type{font-size:var(--fs-xs);font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted)}
.account-mini-name{font-size:var(--fs-base);font-weight:500;color:var(--text-primary)}
.account-mini-balance{font-size:var(--fs-lg);font-weight:600;letter-spacing:-.01em;margin-top:2px}
.account-mini-card--credit .account-mini-balance,
.account-mini-card--mortgage .account-mini-balance,
.account-mini-card--auto_loan .account-mini-balance,
.account-mini-card--personal_loan .account-mini-balance,
.account-mini-card--heloc .account-mini-balance,
.account-mini-card--student_loan .account-mini-balance,
.account-mini-card--line_of_credit .account-mini-balance{color:var(--expense)}
.account-mini-card--investment .account-mini-balance,
.account-mini-card--property .account-mini-balance{color:var(--accent)}

@media(max-width:680px){
  .cashflow-row{grid-template-columns:80px 1fr 110px;gap:10px}
  .cashflow-row-compare{display:none}
  .cashflow-net{flex-wrap:wrap}
  .cashflow-net-tag{margin-left:0;width:100%}
}

/* Page-header action group — multiple buttons aligned to the right of a title. */
.page-header-actions{display:flex;align-items:center;gap:8px}

/* Live weight tally on /health/settings custom strategy editor. */
.weight-tally{font-size:var(--fs-sm);margin:.65rem 0 0;line-height:1.4}
.weight-tally[data-state="ok"]{color:var(--income)}
.weight-tally[data-state="over"]{color:var(--warn)}

/* Strategy list on /health/settings — radio + per-row edit/delete actions. */
.strategy-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}
.strategy-row{display:flex;align-items:flex-start;gap:12px;padding:.85rem 1rem;border:1px solid var(--border-light);border-radius:var(--radius);background:var(--bg-card);transition:border-color .12s,background .12s}
.strategy-row:hover{border-color:var(--text-muted)}
.strategy-row--active{border-color:var(--accent);background:rgba(26,127,100,.04)}
.strategy-row-main{display:flex;align-items:flex-start;gap:12px;flex:1;cursor:pointer}
.strategy-row-main input[type=radio]{margin-top:5px;flex-shrink:0}
.strategy-row-body{display:flex;flex-direction:column;gap:6px;flex:1;min-width:0}
.strategy-row-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:var(--fs-base)}
.strategy-row-head strong{color:var(--text-primary);font-weight:600}
.strategy-row-actions{display:flex;align-items:center;gap:6px;flex-shrink:0}
.badge-active{background:var(--income-bg);color:var(--income);border:1px solid #b7e8d8;font-weight:600;text-transform:uppercase;letter-spacing:.04em}

/* ── Literacy layer ─────────────────────────────────────────────── */

/* Glossary terms — dotted underline + popover on hover/focus.
   Keyboard-accessible via tabindex="0" on the term + :focus-within. */
.glossary-term{position:relative;border-bottom:1px dotted var(--text-muted);cursor:help;outline:none}
.glossary-term:focus,.glossary-term:hover{border-bottom-color:var(--accent)}
.glossary-pop{position:absolute;left:0;top:calc(100% + 6px);z-index:30;display:none;width:max-content;max-width:280px;padding:.55rem .7rem;border-radius:var(--radius);background:var(--bg-card);border:1px solid var(--border-light);box-shadow:var(--elev-2);color:var(--text-primary);font-size:var(--fs-base);line-height:1.45;font-weight:400;cursor:default}
.glossary-term:hover .glossary-pop,.glossary-term:focus .glossary-pop,.glossary-term:focus-within .glossary-pop{display:block}

/* Persona picker — used on /onboarding/welcome (4 cards) and
   /settings/profile (5 cards including Retired). Same shape as the
   scoring-strategy radio cards already in use. */
.persona-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin:.5rem 0 1rem}
.persona-card{position:relative;display:block;padding:.85rem .95rem;border:1px solid var(--border-light);border-radius:var(--radius);background:var(--bg-card);cursor:pointer;transition:border-color .12s,background .12s}
.persona-card:hover{border-color:var(--text-muted)}
.persona-card input[type=radio]{position:absolute;opacity:0;pointer-events:none}
.persona-card input[type=radio]:checked + .persona-card-body{}
.persona-card:has(input[type=radio]:checked){border-color:var(--accent);background:rgba(26,127,100,.04)}
.persona-card-body{display:block}
.persona-card-label{display:block;font-weight:600;color:var(--text-primary);font-size:var(--fs-base);margin-bottom:.2rem}
.persona-card-tag{display:block;color:var(--text-muted);font-size:var(--fs-sm);line-height:1.4}

/* Pillar-detail "Learn about this pillar" collapsible. Built on the
   <details>/<summary> primitive so it works with no JS and is keyboard-
   accessible by default. */
.pillar-lesson{margin:1rem 0 1.5rem;padding:1rem 1.1rem;border:1px solid var(--border-light);border-radius:var(--radius);background:var(--bg-card)}
.pillar-lesson summary{cursor:pointer;font-weight:600;color:var(--text-primary);list-style:none;padding-right:1rem;position:relative}
.pillar-lesson summary::-webkit-details-marker{display:none}
.pillar-lesson summary::after{content:"▸";position:absolute;right:0;top:0;color:var(--text-muted);transition:transform .15s}
.pillar-lesson[open] summary::after{transform:rotate(90deg)}
.pillar-lesson-body{margin-top:.85rem;color:var(--text-primary);line-height:1.55;font-size:var(--fs-base)}
.pillar-lesson-body p{margin:0 0 .65rem}
.pillar-lesson-good{color:var(--text-muted);font-size:var(--fs-base);line-height:1.5;margin:.5rem 0 0;padding:.55rem .7rem;border-left:3px solid var(--accent);background:rgba(26,127,100,.04)}

/* ── Flexible income wizard ─────────────────────────────────────── */

/* Picker grid — same shape as the persona-picker but tighter so all 6
   cards fit on a row at desktop widths. */
.income-card-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px;margin:.5rem 0 1rem}
.income-card{position:relative;display:flex;align-items:flex-start;gap:.7rem;padding:.85rem .95rem;border:1px solid var(--border-light);border-radius:var(--radius);background:var(--bg-card);cursor:pointer;transition:border-color .12s,background .12s}
.income-card:hover{border-color:var(--text-muted)}
.income-card input[type=checkbox]{position:absolute;opacity:0;pointer-events:none}
.income-card:has(input[type=checkbox]:checked){border-color:var(--accent);background:rgba(26,127,100,.04)}
.income-card-icon{font-size:var(--fs-xl);line-height:1;flex-shrink:0;margin-top:1px}
.income-card-body{display:flex;flex-direction:column;gap:.15rem;min-width:0}
.income-card-label{font-weight:600;color:var(--text-primary);font-size:var(--fs-base)}
.income-card-tag{color:var(--text-muted);font-size:var(--fs-sm);line-height:1.4}

/* Fragments — hidden by default; revealed when their matching picker
   checkbox is :checked. Uses :has() (well-supported in modern
   browsers; older agents see all fragments hidden but the form still
   submits cleanly because picked_X gates parsing). */
.income-fragment{display:none;margin-top:1.25rem;padding-top:1rem;border-top:1px solid var(--border-light)}
.income-wizard-form:has(input[name="picked_paystub"]:checked) .income-fragment[data-for="paystub"]{display:block}
.income-wizard-form:has(input[name="picked_allowance"]:checked) .income-fragment[data-for="allowance"]{display:block}
.income-wizard-form:has(input[name="picked_gig"]:checked) .income-fragment[data-for="gig"]{display:block}
.income-wizard-form:has(input[name="picked_freelance"]:checked) .income-fragment[data-for="freelance"]{display:block}
.income-wizard-form:has(input[name="picked_rental"]:checked) .income-fragment[data-for="rental"]{display:block}
.income-wizard-form:has(input[name="picked_gift"]:checked) .income-fragment[data-for="gift"]{display:block}
.income-fragment-title{font-size:var(--fs-md);font-weight:600;margin:0 0 .65rem;color:var(--text-primary)}

/* Quick log income tiles — dashboard surface for irregular rules. Same
   .account-mini-card-shaped grid for visual consistency, but tinted to
   read as "tap me to log income" (income green hint, plus a + sign). */
.quick-log-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px}
.quick-log-tile{display:flex;flex-direction:column;gap:.25rem;padding:.85rem 1rem;border:1px solid var(--border-light);border-radius:var(--radius);background:var(--bg-card);text-decoration:none;color:inherit;transition:transform .08s,border-color .12s,box-shadow .12s}
.quick-log-tile:hover{border-color:var(--income);box-shadow:0 0 0 1px var(--income-bg);transform:translateY(-1px)}
.quick-log-tile-icon{font-size:var(--fs-xl);line-height:1}
.quick-log-tile-name{font-weight:600;font-size:var(--fs-base);color:var(--text-primary)}
.quick-log-tile-amount{font-size:var(--fs-base);font-weight:600}
.quick-log-tile-acct{font-size:var(--fs-sm)}
/* Past-due variant — amber/warning treatment for recurring charges
 * that slipped past their due date without a logged payment. The
 * extra .quick-log-tile-prompt row sits at the bottom and reads as
 * a soft prompt ("Already paid? Log it →"). Border + left-stripe
 * use the existing amber-warn palette so users recognise it as the
 * same severity as overdue cards in /accounts. */
.quick-log-tile--past-due{border-color:#fcd34d;background:rgba(254,243,199,.28);box-shadow:inset 3px 0 0 var(--amber)}
.quick-log-tile--past-due:hover{border-color:var(--amber);box-shadow:inset 3px 0 0 var(--amber-dark),0 0 0 1px var(--amber-light)}
.quick-log-tile--past-due .quick-log-tile-acct{color:#92400e;font-weight:500}
.quick-log-tile-prompt{font-size:var(--fs-sm);color:var(--amber-dark);font-weight:500;margin-top:.15rem}

/* ─── Budget templates ────────────────────────────────────────────────────
   Three group buckets — Needs / Wants / Savings — each get a tinted card
   on /budget and a coloured pill on the preview table. Palette borrows
   from the pillar gradients: blue for needs (structural / required),
   purple for wants (discretionary), green for savings (forward-looking).
*/
.group-summary{margin-top:0}
.group-card{border-top:3px solid var(--border-light);position:relative}
.group-card--need{border-top-color:#3b82f6}
.group-card--want{border-top-color:#8b5cf6}
.group-card--savings{border-top-color:#10b981}
.group-card .stat-label{display:flex;align-items:center;justify-content:space-between}
.group-target-pct{font-size:var(--fs-xs);color:var(--text-muted);font-weight:500;letter-spacing:.03em}
.group-target{color:var(--text-muted);font-size:var(--fs-sm);font-weight:500}
.progress-fill--need{background:#3b82f6}
.progress-fill--want{background:#8b5cf6}
.progress-fill--savings{background:#10b981}

/* Template picker — three cards in a grid on /budget/templates */
.template-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1rem;margin-top:1rem}
.template-card{background:var(--bg-card);border:1px solid var(--border-light);border-radius:var(--radius);padding:1.25rem;display:flex;flex-direction:column;gap:.75rem;transition:border-color .12s,box-shadow .12s}
.template-card:hover{border-color:var(--text-muted)}
.template-card__head{display:flex;flex-direction:column;gap:.25rem}
.template-card__title{margin:0;font-size:var(--fs-lg);font-weight:600}
.template-card__tag{font-size:var(--fs-sm);color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em}
.template-card__desc{margin:0;font-size:var(--fs-base);color:var(--text-secondary);flex:1;line-height:1.45}

/* Preview rows + group pills */
.preview-row td{vertical-align:middle}
.group-pill{display:inline-block;padding:.15rem .55rem;border-radius:999px;font-size:var(--fs-xs);font-weight:600;letter-spacing:.02em;text-transform:uppercase}
.group-pill--need{background:rgba(59,130,246,.12);color:#1d4ed8}
.group-pill--want{background:rgba(139,92,246,.12);color:#6d28d9}
.group-pill--savings{background:rgba(16,185,129,.12);color:#047857}
.group-pill--none{background:var(--border-light);color:var(--text-muted)}
.input-prefix--compact{max-width:8rem;margin-left:auto}
.checkbox-label{display:flex;align-items:center;gap:.5rem;cursor:pointer}

/* ─── Budget vs actual ────────────────────────────────────────────────────
   Inline SVG bar chart + click-to-edit row list on /budget; Budget Health
   tile on the dashboard. Status colors duplicate the inline fills in
   internal/svg/budgetviz.go so theme overrides work without recompiling.
*/
.page-header-actions{display:flex;gap:.5rem;align-items:center}
.budget-chart-card{padding:0;overflow:hidden}
.budget-chart-card .card-header{padding:1rem 1.25rem;border-bottom:1px solid var(--border-light);display:flex;align-items:center;justify-content:space-between}
.card-hint{color:var(--text-muted);font-size:var(--fs-sm)}
.budget-chart-scroll{padding:1rem 1.25rem;overflow-x:auto}
.budget-chart-scroll svg{display:block;max-width:100%;height:auto}
.budget-chart-empty{padding:2rem 1.25rem;text-align:center}
/* #debt-payoff uses .dash-section-head + a raw SVG rather than the
   .card-header + .budget-chart-scroll pair, so .budget-chart-card's padding:0
   leaves its heading, chart, and hint flush against the border. Pad the whole
   card so it matches the inset of the other insights cards. */
#debt-payoff{padding:1rem 1.25rem}
#debt-payoff svg{display:block;max-width:100%;height:auto}

/* Insights summary "stat strip" — small data-anchored text strip
   sitting between the card-header and the chart on each /insights
   card. Gives the user concrete totals, averages, and peaks so the
   chart's shapes have numbers to map against. Wraps gracefully on
   mobile; tabular-nums keeps the dollar figures aligned. */
.insights-stat-strip{
  display:flex;flex-wrap:wrap;gap:.25rem 1.5rem;
  padding:.6rem 1.25rem;
  background:var(--table-header-bg);
  border-bottom:1px solid var(--border-light);
  font-size:var(--fs-sm);
  font-variant-numeric:tabular-nums;
  color:var(--text-secondary);
}
.insights-stat-strip__item{display:inline-flex;align-items:baseline;gap:.35rem;white-space:nowrap}
.insights-stat-strip__label{color:var(--text-muted);font-size:var(--fs-xs);text-transform:uppercase;letter-spacing:.04em}
.insights-stat-strip__item strong{color:var(--text-primary);font-weight:600}
.insights-stat-strip__pos{color:var(--income)}
.insights-stat-strip__neg{color:var(--expense)}

.budget-row-list{list-style:none;margin:0;padding:0;border-top:1px solid var(--border-light)}

/* Each row contains its own inline bar (no separate SVG chart above
 * the list). Two grid columns: body (the tappable
 * anchor that opens /budget/category/<id> burndown detail) +
 * actions (✎ edit, × delete). The bar lives inside the body so the
 * whole visual is one cohesive surface.
 *
 * Three CSS custom properties on each <li> drive the bar geometry
 * (server-rendered in BudgetList; --pace-pct is pct-of-month-elapsed
 * which the SP already computes per row):
 *   --actual-pct    width of the actual-spend fill, 0-100
 *   --budgeted-pct  where the budget cap (and overshoot split) sits
 *   --pace-pct      "ghost zone" extent within the budget track —
 *                   a darker shade showing where you SHOULD be by
 *                   now. The fill draws on top; under-pace rows
 *                   show the gap between fill and ghost-zone-end. */
.budget-row{
  display:grid;grid-template-columns:1fr auto;gap:.5rem 1rem;
  align-items:center;padding:.75rem 1.25rem;
  border-bottom:1px solid var(--border-light);transition:background .12s;
}
.budget-row:last-child{border-bottom:0}
.budget-row:hover{background:rgba(0,0,0,.02)}

.budget-row__body{
  display:flex;flex-direction:column;gap:.45rem;
  text-decoration:none;color:inherit;
}
.budget-row__body:hover .budget-row__name{text-decoration:underline}
.budget-row__head{
  display:flex;align-items:baseline;gap:.65rem;flex-wrap:wrap;
}
.budget-row__name{font-weight:500}
.budget-row__values{
  font-size:var(--fs-base);color:var(--text-secondary);
  font-variant-numeric:tabular-nums;
  display:flex;gap:.3rem;align-items:baseline;
  margin-left:auto;                /* push to right side of head row */
}
.budget-row__sep{color:var(--text-muted)}
.budget-row__pct{color:var(--text-muted);font-size:var(--fs-sm)}
/* Carryover provenance line. Sits under the values row so the bigger limit
   has a visible reason; muted because it is an explanation, not a number to
   act on. */
.budget-row__carryover{display:block;margin-top:.15rem;color:var(--text-muted);font-size:var(--fs-xs)}

/* Actual-spend amount carries the warning colour:
 *   on_track / untracked → readable default
 *   over_pace            → amber (approaching the cap)
 *   over_budget          → red (at or past the cap)
 * The same status colours light up the percentage in parens so the
 * two read together. */
.budget-row__actual{color:var(--text-primary);font-weight:600}
.budget-row--untracked  .budget-row__actual{color:var(--text-secondary);font-weight:500}
.budget-row--over_pace  .budget-row__actual,
.budget-row--over_pace  .budget-row__pct   {color:#b45309;font-weight:600}
.budget-row--over_budget .budget-row__actual,
.budget-row--over_budget .budget-row__pct  {color:var(--expense);font-weight:700}

/* The bar lives directly beneath the head row. Track + fill + tick
 * + overshoot are sibling spans positioned absolutely inside the
 * bar container. Heights match the SVG's old 16px. */
.budget-row__bar{
  position:relative;height:14px;border-radius:3px;
  background:transparent;
}
/* Track = the grey "budget cap" rectangle. Width is --budgeted-pct
 * so it visually represents how much room the cap occupies in the
 * shared maxVal-relative scale. */
.budget-row__bar-track{
  position:absolute;left:0;top:0;bottom:0;
  width:var(--budgeted-pct);
  background:var(--border-light);border-radius:3px;
}
/* Pace zone = a slightly darker shade INSIDE the track, sized by
 * --pace-pct (pct-of-month-elapsed). Because it's a child of
 * .budget-row__bar-track (which is itself absolutely positioned),
 * `width: var(--pace-pct)` resolves against the track's width —
 * e.g. mid-month with budgeted-pct=60 and pace-pct=50, the pace
 * zone extends to 30% of the bar (= 50% of the way through the
 * 60%-wide track). No calc()-multiplication needed.
 *
 * The .budget-row__bar-fill draws on top in DOM order, so:
 *   - Under-pace rows show the pace zone peeking out past the
 *     fill's right edge (the visible "buffer you have left").
 *   - Over-pace rows have the pace zone fully hidden by the fill;
 *     the fill colour already switched to amber via the
 *     .budget-row--over_pace status class. */
.budget-row__bar-pace{
  position:absolute;left:0;top:0;bottom:0;
  width:var(--pace-pct);
  background:rgba(0,0,0,.07);
  border-radius:3px 0 0 3px;
}
/* Fill = the actual-spend in the row's status colour. Capped at
 * the budget cap for tracked rows (so over-budget rows show solid
 * fill + striped overshoot); untracked rows have no track, so the
 * fill is just a grey actual-spend bar. */
.budget-row__bar-fill{
  position:absolute;left:0;top:0;bottom:0;
  width:min(var(--actual-pct), var(--budgeted-pct));
  border-radius:3px;
  background:#9ca3af; /* default = untracked grey; overridden below */
}
.budget-row--on_track   .budget-row__bar-fill{background:#10b981}
.budget-row--over_pace  .budget-row__bar-fill{background:#f59e0b}
.budget-row--over_budget .budget-row__bar-fill{background:#dc2626}
/* Untracked has no track so the fill should run the full actual
 * width, not be capped at budgeted-pct (which is 0 for untracked). */
.budget-row--untracked .budget-row__bar-fill{
  width:var(--actual-pct);background:#9ca3af;
}
/* Over-budget overshoot — striped continuation from --budgeted-pct
 * to --actual-pct, signalling the cap was blown past. Diagonal
 * red stripes match the old SVG pattern (the visual is well-known
 * to anyone migrating from the chart). */
.budget-row__bar-overshoot{
  position:absolute;top:0;bottom:0;
  left:var(--budgeted-pct);
  width:calc(var(--actual-pct) - var(--budgeted-pct));
  border-radius:3px;
  background:
    repeating-linear-gradient(45deg,
      rgba(220,38,38,.85) 0,
      rgba(220,38,38,.85) 4px,
      rgba(127,29,29,.65) 4px,
      rgba(127,29,29,.65) 8px);
}
.budget-row__delete{margin:0}

/* Dashboard Budget Health tile */
.health-card{padding:1.25rem}
.health-card__empty{display:flex;flex-direction:column;gap:.6rem;align-items:flex-start}
.health-card__empty .empty-state{margin-bottom:.4rem}
.health-card__top{display:flex;align-items:flex-start;justify-content:space-between;gap:1rem;margin-bottom:.85rem}
.health-card__headline{display:flex;flex-direction:column}
.health-card__count{font-size:var(--fs-2xl);font-weight:600;color:var(--text-primary);font-variant-numeric:tabular-nums}
.health-card__count-sep{font-size:var(--fs-sm);color:var(--text-muted);font-weight:400;margin:0 .25rem}
.health-card__count-label{font-size:var(--fs-sm);color:var(--text-muted)}
.health-card__spark{display:flex;flex-direction:column;align-items:flex-end;gap:.15rem}
.health-card__spark svg{display:block}
.health-card__spark-label{font-size:var(--fs-xs);color:var(--text-muted);letter-spacing:.04em;text-transform:uppercase}

.health-card__variance{display:flex;align-items:center;gap:.5rem;padding:.55rem 0;border-top:1px solid var(--border-light);font-size:var(--fs-base)}
.health-card__dot{width:.6rem;height:.6rem;border-radius:50%;display:inline-block;flex-shrink:0}
.health-card__dot--ok{background:#10b981}
.health-card__dot--over{background:#dc2626}
.health-card__sub{color:var(--text-muted);font-size:var(--fs-sm);margin-left:.25rem}

.health-card__top-over{display:flex;align-items:center;gap:.4rem;padding-top:.45rem;border-top:1px dashed var(--border-light);font-size:var(--fs-base)}
.health-card__top-over-label{color:var(--text-muted);font-size:var(--fs-sm)}
.health-card__top-over-cat{font-weight:500}
.health-card__top-over-amt{margin-left:auto;font-weight:600}

.health-card__pace{margin:.4rem 0 0;font-size:var(--fs-sm)}

/* ─── Budget burndown + history ───────────────────────────────────────────
   Drill-down page /budget/category/{pk} + multi-period trend /budget/history.
   Reuses .budget-chart-card, .budget-chart-scroll, .stat-card. */
.budget-row__actions{display:flex;align-items:center;gap:.35rem}
.budget-row__actions .action-link{display:inline-flex;align-items:center;justify-content:center;width:1.7rem;height:1.7rem;border-radius:4px;text-decoration:none;font-size:var(--fs-base);color:var(--text-muted)}
.budget-row__actions .action-link:hover{background:rgba(0,0,0,.05);color:var(--text-primary)}
.budget-row__actions .action-link--danger:hover{background:rgba(220,38,38,.10);color:var(--expense)}

/* Status pills — used by both the burndown stat-card and the history table */
.status-pill{display:inline-block;padding:.18rem .55rem;border-radius:999px;font-size:var(--fs-xs);font-weight:600;letter-spacing:.02em}
/* Cell-pills wrapper — used when a single table cell needs to hold
   MULTIPLE inline elements (two status-pills, badge + text, pill +
   small button, etc.) with consistent spacing + wrap behavior. The
   inline-flex shell gives a visible gap between siblings, flex-wrap
   lets them break to a second line on narrow screens (mobile cell
   layout right-aligns to flex-end so the wrapped row stays tidy),
   and align-items:center keeps the elements visually centered
   regardless of pill padding / line-heights. Single-element cells
   don't need this wrapper. See the cell-content rule block in
   ledger.mobile.css for the broader baseline. */
.cell-pills{display:inline-flex;flex-wrap:wrap;gap:var(--sp-1);justify-content:flex-end;align-items:center}
.status-pill--on_track{background:rgba(16,185,129,.12);color:#047857}
.status-pill--over_pace{background:rgba(245,158,11,.14);color:#b45309}
.status-pill--over_budget{background:rgba(220,38,38,.12);color:#b91c1c}
.status-pill--untracked{background:var(--border-light);color:var(--text-muted)}
/* Bank connection status — reuses the budget pill palette. */
.status-pill--good{background:rgba(16,185,129,.12);color:#047857}
.status-pill--reauth{background:rgba(245,158,11,.14);color:#b45309}
.status-pill--error{background:rgba(220,38,38,.12);color:#b91c1c}
.status-pill--off{background:var(--border-light);color:var(--text-muted)}

/* Periods switcher on /budget/history */
.card-actions{display:flex;gap:.35rem;align-items:center}
.periods-pill{padding:.2rem .6rem;border-radius:999px;border:1px solid var(--border-light);font-size:var(--fs-sm);text-decoration:none;color:var(--text-muted);transition:border-color .12s,background .12s}
.periods-pill:hover{border-color:var(--text-muted);color:var(--text-primary)}
.periods-pill--active{background:var(--text-primary);color:#fff;border-color:var(--text-primary);cursor:default}
.periods-pill--locked{opacity:.4;cursor:not-allowed;background:repeating-linear-gradient(45deg,transparent 0 4px,rgba(0,0,0,.04) 4px 8px)}

/* Key-value list on the drill-down stat panel */
.kv-list{display:grid;grid-template-columns:auto 1fr;gap:.45rem 1rem;margin:0}
.kv-list dt{color:var(--text-muted);font-size:var(--fs-base)}
.kv-list dd{margin:0;font-size:var(--fs-base)}
.text-muted{color:var(--text-muted)}

/* ─── Celebrations ────────────────────────────────────────────────────────
   Score-delta chip on /dashboard, tier label on the scorecard, full-screen
   celebration modal with confetti, and the /health Badges card grid.
   Every animation has a prefers-reduced-motion: reduce escape hatch.
*/

/* Tier label between "Financial health" and the strategy line on dashboard.
   Tone variants reuse the bronze/silver/gold palette from the badge tiles. */

/* Score-delta chip — sits next to the score circle on /dashboard. Floats
   up + fades over 1.6s; JS removes the element after animationend so it
   doesn't outstay its welcome as a focus target. */
.score-delta{position:absolute;top:-.25rem;right:-.5rem;padding:.1rem .45rem;border-radius:999px;font-size:var(--fs-xs);font-weight:600;font-variant-numeric:tabular-nums;animation:score-delta-float 1.6s ease-out forwards}
.score-delta--positive{background:rgba(16,185,129,.16);color:#047857}
.score-delta--negative{background:rgba(220,38,38,.12);color:#b91c1c}

@keyframes score-delta-float{
  0%   {opacity:0; transform:translateY(0)}
  18%  {opacity:1; transform:translateY(-4px)}
  100% {opacity:0; transform:translateY(-14px)}
}

/* The graph link is the delta chip's positioning context.
   the delta chip can absolute-position above it. */
.score-summary__graph{position:relative}

/* ── Modal ────────────────────────────────────────────────────────────── */

.celebration-modal{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(15,23,42,.55);z-index:1100;animation:celebration-fade-in .25s ease-out}
@keyframes celebration-fade-in{from{opacity:0} to{opacity:1}}

.celebration-card{position:relative;background:var(--bg-card);border:1px solid var(--border-light);border-radius:16px;padding:2.25rem 2rem 1.75rem;width:min(420px, 92vw);text-align:center;box-shadow:var(--elev-3);z-index:1102}
.celebration-card--bronze{border-color:#b45309}
.celebration-card--silver{border-color:#9ca3af}
.celebration-card--gold{border-color:#fbbf24;box-shadow:0 24px 60px rgba(251,191,36,.32)}

/* Intentional one-off display size (the celebration glyph) — larger
   than the type scale tops out (--fs-2xl); not a missed token sweep. */
.celebration-icon{font-size:3rem;line-height:1;margin-bottom:.85rem}
.celebration-title{font-size:var(--fs-xl);font-weight:600;margin:0 0 .5rem;color:var(--text-primary)}
.celebration-subtitle{margin:0 0 1.5rem;color:var(--text-secondary);font-size:var(--fs-base);line-height:1.45}
.celebration-form{margin:0}
.celebration-cta{min-width:160px}

/* ── Confetti ─────────────────────────────────────────────────────────── */

.celebration-confetti{position:absolute;inset:0;pointer-events:none;overflow:hidden;z-index:1101}
.confetti-piece{position:absolute;top:-12px;width:8px;height:14px;border-radius:2px;will-change:transform,top,opacity;animation-name:confetti-fall;animation-timing-function:linear;animation-fill-mode:forwards}

@keyframes confetti-fall{
  0%   {transform:translateY(0) rotate(0deg); opacity:1}
  85%  {opacity:1}
  100% {transform:translateY(110vh) rotate(720deg); opacity:0}
}

/* ── Badges card on /health ──────────────────────────────────────────── */

.badge-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:.85rem;padding:1rem 1.25rem 1.25rem}
.badge-tile{display:grid;grid-template-columns:auto 1fr;gap:.85rem;align-items:center;padding:.85rem 1rem;border:1px solid var(--border-light);border-radius:10px;background:var(--bg-card);transition:border-color .12s,box-shadow .12s}
.badge-tile--bronze{border-left:4px solid #b45309}
.badge-tile--silver{border-left:4px solid #9ca3af}
.badge-tile--gold{border-left:4px solid #fbbf24}
.badge-tile--locked{opacity:.55;filter:grayscale(.7)}
.badge-tile__icon{font-size:var(--fs-2xl);line-height:1}
.badge-tile__body{display:flex;flex-direction:column;gap:.15rem}
.badge-tile__name{font-weight:600;font-size:var(--fs-base)}
.badge-tile__desc{font-size:var(--fs-sm);color:var(--text-secondary);line-height:1.35}
.badge-tile__earned{font-size:var(--fs-xs);color:var(--text-muted);font-variant-numeric:tabular-nums;margin-top:.2rem}
.badge-tile__locked-tag{font-size:var(--fs-xs);color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;margin-top:.2rem}
.badge-tile__progress{font-size:var(--fs-xs);color:var(--text-secondary);font-variant-numeric:tabular-nums}
/* Progress toward a locked badge; a native <progress>, so no inline width. */
.badge-progress{appearance:none;width:100%;height:6px;margin-top:.35rem;border:0;border-radius:999px;background:var(--border-light);overflow:hidden}
.badge-progress::-webkit-progress-bar{background:var(--border-light);border-radius:999px}
.badge-progress::-webkit-progress-value{background:var(--accent);border-radius:999px}
.badge-progress::-moz-progress-bar{background:var(--accent);border-radius:999px}
.badge-chip{display:inline-flex;align-items:center;gap:.3rem;padding:.15rem .55rem;border-radius:999px;border:1px solid var(--border-light);font-size:var(--fs-sm)}
.badge-chip--bronze{border-color:#b45309}
.badge-chip--silver{border-color:#9ca3af}
.badge-chip--gold{border-color:#fbbf24}
.kid-badges__row{display:flex;flex-wrap:wrap;gap:var(--sp-4);align-items:center;padding:var(--sp-3) var(--sp-4) var(--sp-4)}
.kid-badges__count{font-size:var(--fs-2xl);font-weight:700}
.kid-badges__next{display:flex;gap:var(--sp-3);align-items:center;flex:1 1 220px}
.kid-badges__icon{font-size:var(--fs-2xl);line-height:1}
.kid-badges__next-body{display:flex;flex-direction:column;gap:.15rem;flex:1}

/* ── prefers-reduced-motion: turn off every animation in this section ── */

@media (prefers-reduced-motion: reduce){
  .score-delta{animation:none; opacity:1; transform:none}
  .celebration-modal{animation:none}
  .confetti-piece{display:none}
}

/* ─── Streaks ─────────────────────────────────────────────────────────────
   Three-state pill (active / grace / broken) + optional comeback card on
   the dashboard. No animations — pills are persistent shell furniture and
   shouldn't draw the eye more than the score circle.
   ───────────────────────────────────────────────────────────────────── */

.streak-row{
  display:flex; flex-wrap:wrap; gap:.6rem;
  margin:.85rem 0 1.1rem;
}

.streak-pill{
  display:inline-flex; align-items:center; gap:.45rem;
  padding:.35rem .75rem;
  border-radius:999px;
  border:1px solid transparent;
  font-size:var(--fs-base);
  line-height:1;
  font-variant-numeric:tabular-nums;
  background:var(--bg-card, #fff);
}
.streak-pill__flame{font-size:var(--fs-md); line-height:1}
.streak-pill__count{font-weight:700; min-width:1ch; text-align:right}
.streak-pill__label{color:var(--text-secondary); font-size:var(--fs-sm)}

.streak-pill--active{
  background:rgba(249,115,22,.08);
  border-color:rgba(249,115,22,.35);
  color:#9a3412;
}
.streak-pill--active .streak-pill__flame{filter:none}

.streak-pill--grace{
  background:rgba(245,158,11,.08);
  border-color:rgba(245,158,11,.30);
  color:#92400e;
}
.streak-pill--grace .streak-pill__flame{filter:saturate(.6) brightness(.95)}

.streak-pill--broken{
  background:rgba(100,116,139,.08);
  border-color:rgba(100,116,139,.30);
  color:var(--text-muted);
}
.streak-pill--broken .streak-pill__flame{filter:grayscale(1) opacity(.6)}

.scorecard-streak{
  display:flex; align-items:center; gap:.7rem;
  margin:.5rem 0 .35rem;
  flex-wrap:wrap;
}

.comeback-card{
  display:flex; align-items:center; gap:1rem;
  padding:.9rem 1rem;
  margin:.5rem 0 1.1rem;
  border-radius:.65rem;
  border:1px solid rgba(249,115,22,.35);
  background:rgba(249,115,22,.05);
}
.comeback-card__icon{font-size:var(--fs-xl); line-height:1; flex:0 0 auto}
.comeback-card__body{flex:1; display:flex; flex-direction:column; gap:.15rem}
.comeback-card__body strong{font-size:var(--fs-md)}
.comeback-card__body span{font-size:var(--fs-sm); color:var(--text-secondary)}
.comeback-card__action{
  flex:0 0 auto;
  padding:.4rem .75rem;
  border-radius:.4rem;
  background:#f97316; color:#fff;
  text-decoration:none; font-size:var(--fs-base); font-weight:600;
  white-space:nowrap;
}
.comeback-card__action:focus-visible{outline:2px solid #c2410c; outline-offset:2px}
.comeback-card__dismiss-form{margin:0}
.comeback-card__dismiss{
  background:none; border:none; cursor:pointer;
  font-size:var(--fs-lg); line-height:1; padding:.2rem .4rem;
  color:var(--text-muted);
}
.comeback-card__dismiss:hover{color:var(--text-primary)}
.comeback-card__dismiss:focus-visible{outline:2px solid #c2410c; outline-offset:2px; border-radius:.25rem}

/* ─── Chores & earned allowance ───────────────────────────────────────────
   Dashboard tile + the 0-chores setup card. Reuses the comeback-card
   layout; green accent (earned money) vs the setup card's neutral tone.
   No animations. */
.chores-card{
  display:flex; align-items:center; gap:1rem;
  padding:.9rem 1rem; margin:.5rem 0 1.1rem;
  border-radius:.65rem;
  border:1px solid rgba(16,185,129,.35);
  background:rgba(16,185,129,.05);
}
.chores-card--setup{
  border-color:var(--border); background:var(--border-light);
}
.chores-card__icon{font-size:var(--fs-xl); line-height:1; flex:0 0 auto}
.chores-card__body{flex:1; display:flex; flex-direction:column; gap:.15rem}
.chores-card__body strong{font-size:var(--fs-md)}
.chores-card__body span{font-size:var(--fs-sm); color:var(--text-secondary)}
.chores-card__action{
  flex:0 0 auto; padding:.4rem .75rem; border-radius:.4rem;
  background:#10b981; color:#fff;
  text-decoration:none; font-size:var(--fs-base); font-weight:600;
  white-space:nowrap;
}
.chores-card--setup .chores-card__action{background:var(--accent)}
.chores-card__action:focus-visible{outline:2px solid #047857; outline-offset:2px}
/* Chore/pending action cluster: keep buttons on one line but
   left-align so the column header sits above the buttons (the rest of
   the chores table is left-aligned). Scoped — does not touch the
   app-wide right-aligned .action-cell. */
.chore-actions{white-space:nowrap;text-align:left}
.chores-decision-col{width:200px}
.inline-form{display:inline}
/* A POST that reads as a link inside a sentence (sign out on the invite page). */
.link-button{background:none;border:0;padding:0;font:inherit;color:var(--accent);text-decoration:underline;cursor:pointer}
/* "Not approved…" opens a reason field under the Approve button; "Add a note"
   opens an optional note inside the Mark done or Approve form. */
.chore-reject,.chore-note{display:inline-block;margin-left:.5rem}
.chore-reject summary,.chore-note summary{display:inline;cursor:pointer;list-style:none}
.chore-reject summary::-webkit-details-marker,.chore-note summary::-webkit-details-marker{display:none}
.chore-reject[open],.chore-note[open]{display:block;margin:.5rem 0 0;white-space:normal}
.chore-reject[open] form,.chore-note[open] .chore-note-field{display:flex;flex-direction:column;gap:.35rem;max-width:280px;margin-top:.4rem}
@media(max-width:768px){
  .chore-decision{text-align:right}
  .chore-decision .chore-note-field,.chore-decision .chore-reject form{margin-left:auto;text-align:left}
}
.status-pill--waiting{background:rgba(245,158,11,.14);color:#b45309}
.status-pill--approved{background:rgba(16,185,129,.12);color:#047857}
.status-pill--declined{background:rgba(220,38,38,.12);color:#b91c1c}

/* ─── Notifications ───────────────────────────────────────────────────────
   Sidebar bell badge, inbox rows with a severity colour stripe, and the
   top-of-dashboard critical banner. No animations — notifications are
   informational furniture, not attention-grabbers.
   ───────────────────────────────────────────────────────────────────── */

/* Notification bell — icon-only, right-aligned on the user-name row in
   the sidebar footer (moved out of the nav list). */
.sidebar-user-row{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:.5rem;
  /* Lives in the footer account cluster now (name+bell above Sign out).
     No own padding/border — .sidebar-foot supplies the inset and the
     border-top that separates the cluster from the nav; the divider is
     back on .sidebar-brand so nav reads cleanly under the brand. */
  padding-bottom:.35rem;
}
.sidebar-bell{
  position:relative;
  flex:0 0 auto;
  display:inline-flex;
  align-items:center;
  justify-content:center;
  width:28px;
  height:28px;
  border-radius:6px;
  color:var(--sidebar-text);
  text-decoration:none;
}
.sidebar-bell svg{width:18px;height:18px}
.sidebar-bell:hover{color:var(--sidebar-text-hover);background:var(--sidebar-active-bg)}
.sidebar-bell.active{color:var(--sidebar-text-hover);background:var(--sidebar-active-bg)}
.sidebar-bell:focus-visible{outline:2px solid var(--sidebar-active-accent);outline-offset:1px}
.sidebar-bell__badge{
  position:absolute;
  top:-2px;
  right:-2px;
  min-width:1rem;
  height:1rem;
  padding:0 .25rem;
  border-radius:999px;
  background:#ef4444;
  color:#fff;
  font-size:var(--fs-xs);
  font-weight:700;
  line-height:1rem;
  text-align:center;
  font-variant-numeric:tabular-nums;
}

.notif-group{margin-bottom:1.4rem}
.notif-group__date{
  font-size:var(--fs-xs);
  text-transform:uppercase;
  letter-spacing:.06em;
  color:var(--text-muted);
  margin:0 0 .5rem;
}

.notif{
  display:flex;
  align-items:flex-start;
  gap:.8rem;
  padding:.85rem 1rem .85rem 0;
  background:var(--bg-card);
  border:1px solid var(--border-light);
  border-radius:.5rem;
  margin-bottom:.5rem;
  overflow:hidden;
}
.notif__stripe{
  flex:0 0 4px;
  align-self:stretch;
  border-radius:4px 0 0 4px;
  background:var(--text-muted);
}
.notif--info    .notif__stripe{background:#3b82f6}
.notif--success .notif__stripe{background:#10b981}
.notif--warn    .notif__stripe{background:#f59e0b}
.notif--critical .notif__stripe{background:#ef4444}
.notif--read{opacity:.6}
/* Dismissed = the user explicitly closed the banner. Page now shows
   dismissed rows (history view); they get a heavier fade + a small
   inline "dismissed" badge in the timestamp line. Layers with
   .notif--read so a read+dismissed row is the most-faded. */
.notif--dismissed{opacity:.45;background:repeating-linear-gradient(45deg,transparent,transparent 12px,rgba(0,0,0,.018) 12px,rgba(0,0,0,.018) 13px)}
.notif--dismissed .notif__stripe{filter:saturate(.3)}
.notif__badge{display:inline-block;font-size:var(--fs-xs);padding:1px 5px;border-radius:3px;background:var(--border-light);color:var(--text-muted);font-weight:500;text-transform:uppercase;letter-spacing:.04em}

/* Archive section on /notifications — native <details> for the
   collapse, styled to match the rest of the page. Closed by default;
   summary acts as a clickable header with count + a "change cutoff"
   link to /settings/profile. */
.notif-archive{margin-top:1.5rem;border-top:1px solid var(--border-light);padding-top:1rem}
.notif-archive__summary{display:flex;align-items:center;gap:.5rem;font-family:var(--font-sans);font-size:var(--fs-md);font-weight:600;color:var(--text-secondary);cursor:pointer;list-style:none;padding:.3rem .15rem;border-radius:6px}
.notif-archive__summary:hover{background:var(--table-row-hover-bg)}
.notif-archive__summary::-webkit-details-marker{display:none}
.notif-archive__chevron{display:inline-block;font-size:var(--fs-base);transition:transform .15s ease-out;color:var(--text-muted);width:1em}
.notif-archive[open] .notif-archive__chevron{transform:rotate(90deg)}
.notif-archive__count{font-weight:400;font-size:var(--fs-sm);color:var(--text-muted)}
.notif-archive__settings{margin-left:auto;font-weight:500;font-size:var(--fs-sm);color:var(--accent);text-decoration:none}
.notif-archive__settings:hover{text-decoration:underline}
.notif-archive__body{margin-top:.75rem}

/* Notification settings rows on /settings/profile — unified
   typography across checkboxes and numeric-input labels. Previously
   `.checkbox-inline` and bare `<label class="form-inline-label">`
   inherited different fonts (the bare label fell back to the browser
   default, which is serif on most browsers, while .checkbox-inline
   used the system sans). This block re-uses the body sans font + fs-sm
   so the whole settings section reads consistently. */
.notif-pref-list{display:flex;flex-direction:column;gap:.55rem;margin:.85rem 0 1.1rem}
.notif-pref-list > .form-hint{margin:.3rem 0 0}
.notif-pref-row{display:flex;align-items:center;gap:.55rem;font-family:var(--font-sans);font-size:var(--fs-sm);color:var(--text-secondary);cursor:pointer;line-height:1.4}
.notif-pref-row input[type="checkbox"]{flex:0 0 auto;margin:0}
.notif-pref-row > span{flex:1 1 auto}
.notif-pref-num{display:flex;align-items:center;justify-content:space-between;gap:.75rem;padding:.25rem 0 .25rem 1.8rem;font-family:var(--font-sans);font-size:var(--fs-sm);color:var(--text-secondary);line-height:1.4}
.notif-pref-num > span{flex:1 1 auto}
.notif-pref-num input[type="number"]{flex:0 0 auto;width:6rem;padding:.3rem .5rem;border:1px solid var(--border);border-radius:5px;font-family:var(--font-mono);font-size:var(--fs-sm);color:var(--text-primary);background:var(--bg-card)}
/* Standalone variant — drops the left-indent that aligns sub-option
   labels under their parent checkbox. Used for top-level numeric
   settings (e.g. Inbox-display "Archive after N days") that aren't
   bound to a sibling checkbox above. Without this the row reads as
   a sub-option of whatever checkbox precedes it. */
.notif-pref-num--standalone{padding-left:0}
@media(max-width:540px){
  .notif-pref-num{flex-direction:column;align-items:flex-start;gap:.25rem;padding-left:1.8rem}
  .notif-pref-num--standalone{padding-left:0}
  .notif-pref-num input[type="number"]{width:100%}
}

/* Settings-section subdividers — break one settings card into clearly-
   distinct subsections (e.g. "Notifications" subscriptions vs "Inbox
   display" layout). The h3 uses uppercase tracking so it reads as a
   sub-label, not a competing heading with the card's h2. */
.settings-divider{border:none;border-top:1px solid var(--border-light);margin:1.25rem 0 .85rem}
.settings-subh{font-size:var(--fs-xs);font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted);margin:0 0 .6rem}
.notif__body{flex:1;display:flex;flex-direction:column;gap:.15rem;padding:.1rem 0}
.notif__title{font-size:var(--fs-base)}
.notif__text{font-size:var(--fs-sm);color:var(--text-secondary);line-height:1.4}
.notif__time{font-size:var(--fs-xs);color:var(--text-muted);margin-top:.1rem}
.notif__actions{
  display:flex;align-items:center;gap:.4rem;
  padding-right:1rem;flex-wrap:wrap;
}

/* .critical-banner replaced by the unified .alert-banner above (line
   ~303). The bank_reauth_banner.html partial still uses the
   .critical-banner classes — it's a derived (not notification-record-
   driven) banner; updated below to use .alert-banner classes too. */

/* ─── Starter journey ─────────────────────────────────────────────────────
   Dashboard hero card + step strip + the /journey detail list. Shares the
   dark prompt-card surface so it reads as the primary CTA. No animations.
   ───────────────────────────────────────────────────────────────────── */

.journey-card{
  background:var(--sidebar-bg);color:#e6edf3;
  border-radius:var(--radius-lg);
  padding:1.1rem 1.25rem;margin-bottom:1.75rem;
}
.journey-card__head{display:flex;align-items:baseline;justify-content:space-between;gap:1rem}
.journey-card__head strong{color:#fff;font-weight:600;font-size:var(--fs-md)}
.journey-card__step{font-size:var(--fs-sm);color:#8b929e;font-variant-numeric:tabular-nums;white-space:nowrap}

.journey-steps{
  list-style:none;display:flex;gap:6px;margin:.85rem 0;padding:0;
}
.journey-step{flex:1}
.journey-step__dot{
  display:flex;align-items:center;justify-content:center;
  height:24px;border-radius:12px;
  background:rgba(255,255,255,.08);color:#8b929e;
  font-size:var(--fs-xs);font-weight:600;font-family:var(--font-mono);
}
.journey-step--done .journey-step__dot{background:#1a7f64;color:#fff}
.journey-step--current .journey-step__dot{background:var(--sidebar-active-accent);color:#06241b}

.journey-card__current{
  border-top:1px solid rgba(255,255,255,.08);
  padding-top:.85rem;font-size:var(--fs-base);line-height:1.5;
}
.journey-card__current strong{display:block;color:#fff;font-weight:600;margin-bottom:2px}
.journey-card__current span{color:#8b929e;display:block;margin-bottom:.7rem}
.journey-card__actions{display:flex;align-items:center;gap:1rem;flex-wrap:wrap}
.journey-card__all{font-size:var(--fs-base);color:var(--sidebar-active-accent);font-weight:500;white-space:nowrap}

.journey-card--done{display:flex;align-items:center;gap:16px}
.journey-card--done .journey-card__badge{font-size:var(--fs-2xl);flex-shrink:0}
.journey-card--done .journey-card__body{flex:1;font-size:var(--fs-base);line-height:1.5}
.journey-card--done .journey-card__body strong{display:block;color:#fff;font-weight:600;margin-bottom:2px}
.journey-card--done .journey-card__body span{color:#8b929e}

/* /journey/{code} detail list — the wrapping .card has no inner
   padding by default (the global .card rule only sets background +
   radius + shadow); without horizontal padding here the marker
   chip and body text hug the card border, which reads as cramped
   especially on the family_foundations journey where body copy
   wraps to two lines. The border-bottom still spans card edge to
   edge per typical list-inside-card pattern. */
.journey-list{list-style:none;margin:0;padding:0}
.journey-list__item{
  display:flex;align-items:flex-start;gap:1rem;
  padding:1rem 1.5rem;border-bottom:1px solid var(--border-light);
}
.journey-list__item:last-child{border-bottom:none}
.journey-list__marker{
  flex:0 0 28px;height:28px;border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  background:var(--border-light);color:var(--text-muted);
  font-size:var(--fs-base);font-weight:600;font-family:var(--font-mono);
}
.journey-list__item.is-done .journey-list__marker{background:#1a7f64;color:#fff}
.journey-list__item.is-current .journey-list__marker{background:var(--sidebar-bg);color:#fff}
.journey-list__body{flex:1;display:flex;flex-direction:column;gap:.2rem}
.journey-list__body strong{font-size:var(--fs-base)}
.journey-list__body span{font-size:var(--fs-base);color:var(--text-secondary);line-height:1.45}
.journey-list__cta{font-size:var(--fs-base);color:var(--accent);font-weight:500;margin-top:.3rem}
.journey-list__status{
  flex:0 0 auto;font-size:var(--fs-xs);text-transform:uppercase;letter-spacing:.05em;
  color:var(--text-muted);padding-top:.3rem;
}
.journey-list__item.is-current .journey-list__status{color:var(--accent)}

/* ─── Calendar (upcoming outlook) ────────────────────────────────────────
   Read-only month grid of projected recurring income/expense. Reuses the
   repeat(7,1fr) grid idiom + existing card/border/color tokens. No
   animations.
   ───────────────────────────────────────────────────────────────────── */

.cal-nav{display:flex;align-items:center;gap:.75rem}
.cal-nav__label{font-weight:600;font-size:var(--fs-base);min-width:9.5rem;text-align:center;font-variant-numeric:tabular-nums}

.cal-summary{
  display:grid;grid-template-columns:repeat(3,1fr);gap:1rem;margin-bottom:1rem;
}
.cal-summary__card{
  display:flex;flex-direction:column;gap:.2rem;
  padding:.7rem .9rem;border:1px solid var(--border-light);
  border-radius:.5rem;background:var(--bg-card);
}
.cal-summary__label{font-size:var(--fs-xs);text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted)}
.cal-summary__val{font-size:var(--fs-md);font-weight:700;font-variant-numeric:tabular-nums}

.cal-card{padding:.75rem}
.cal-grid-head,.cal-week{display:grid;grid-template-columns:repeat(7,1fr);gap:6px}
.cal-grid-head{margin-bottom:6px}
.cal-grid-head span{
  font-size:var(--fs-xs);text-transform:uppercase;letter-spacing:.05em;
  color:var(--text-muted);text-align:center;padding-bottom:2px;
}
.cal-week{margin-bottom:6px}
.cal-week:last-child{margin-bottom:0}
.cal-cell{
  /* Fixed height so every day box in the month grid is identical
     regardless of how many items land on it. Overflow is clipped; the
     per-chip label truncates with an ellipsis (.cal-chip__label). The
     mobile agenda overrides this back to height:auto. */
  height:7.5rem;padding:5px 5px 6px;overflow:hidden;
  border:1px solid var(--border-light);border-radius:.4rem;
  background:var(--bg-card);display:flex;flex-direction:column;gap:3px;
}
.cal-cell--blank{background:transparent;border-color:transparent}
/* Today highlight — gives instant past/future context. Accent ring +
   tint, bolded day number. */
.cal-cell--today{
  border-color:var(--accent);
  box-shadow:0 0 0 1px var(--accent) inset;
  background:rgba(26,127,100,.06);
}
.cal-cell--today .cal-daynum{color:var(--accent);font-weight:700}
/* Full date label — agenda-only (mobile drops the grid; see the 768
   block). Hidden on desktop. */
.cal-cell__date{display:none}
.cal-daynum{font-size:var(--fs-xs);color:var(--text-muted);font-variant-numeric:tabular-nums}
/* The date is a link to that day's filtered transactions — a hover cue
   makes it discoverable without changing the resting look (global
   a{} already gives inherit colour + no underline). */
.cal-daynum,.cal-cell__date{cursor:pointer;align-self:flex-start}
.cal-daynum:hover,.cal-cell__date:hover{color:var(--accent);text-decoration:underline}
.cal-chip{
  display:flex;justify-content:space-between;gap:4px;
  font-size:var(--fs-xs);line-height:1.25;padding:2px 5px;border-radius:4px;
  text-decoration:none;border:1px solid transparent;
}
.cal-chip__label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cal-chip__amt{font-variant-numeric:tabular-nums;flex:0 0 auto}
.cal-chip--income{
  color:var(--income);background:rgba(13,122,95,.10);border-color:rgba(13,122,95,.30);
}
.cal-chip--expense{
  color:var(--expense);background:rgba(192,57,43,.09);border-color:rgba(192,57,43,.28);
}
/* Transfers/payments — neutral; listed but out of the income/expense
   totals (they don't change net worth). */
.cal-chip--transfer{
  color:var(--text-secondary);background:rgba(120,130,145,.10);
  border-color:var(--border-light);
}
/* Projected = upcoming recurring (not yet booked). Dashed border +
   slightly muted distinguishes a forecast from a logged transaction. */
.cal-chip--projected{border-style:dashed;opacity:.92}
.cal-chip:hover{filter:brightness(.97)}
.cal-chip:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
.cal-foot{font-size:var(--fs-sm);margin-top:.9rem}
.cal-foot a{color:var(--accent)}

/* Mobile = agenda, not a squeezed 7-col grid. The grid crushed chip
   text into ~45px columns and overlapped the cells; here each day with
   activity becomes a full-width block led by its date. Empty/blank days
   are dropped (a true agenda). Canonical 768px breakpoint. */
@media (max-width:768px){
  .cal-summary{grid-template-columns:1fr}
  .cal-card{padding:.5rem}
  .cal-grid-head{display:none}
  .cal-week{display:block;margin:0}
  .cal-cell{
    height:auto;min-height:0;margin-bottom:8px;overflow:visible;
    flex-direction:column;gap:5px;padding:.6rem .7rem;
  }
  /* Agenda lists only days with activity — EXCEPT today, which always
     shows (even empty) so the current-day highlight is present on
     mobile for the same past/future context the desktop grid gives. */
  .cal-cell--blank,.cal-cell--empty:not(.cal-cell--today){display:none}
  .cal-cell__date{
    display:block;font-size:var(--fs-sm);font-weight:600;
    color:var(--text-secondary);
  }
  /* Today in the agenda: the base .cal-cell--today accent ring/tint
     still applies; make the date line accent + a touch larger so it
     clearly reads as the highlight, and give an empty today a caption
     so it isn't a bare highlighted box. */
  .cal-cell--today .cal-cell__date{color:var(--accent);font-size:var(--fs-base)}
  .cal-cell--today.cal-cell--empty::after{
    content:"Nothing logged or due today.";
    font-size:var(--fs-sm);color:var(--text-muted);
  }
  .cal-daynum{display:none}
  .cal-chip{font-size:var(--fs-sm);padding:.4rem .6rem}
  .cal-chip__label{overflow:visible;white-space:normal;text-overflow:clip}
}

/* ─── Auth pages (standalone dark) ───────────────────────────────────────
   Consolidated from the per-page inline <style> blocks that were copy-
   pasted verbatim across login / forgot / reset / login_mfa / signup.
   Scoped under body.auth-page so the dark treatment never leaks into the
   app (the bare body{} rule is exactly why it had to be inline before —
   adding the class lets it live here). landing.html stays inline by
   design (standalone marketing page, its own theme, redefines base).
   ───────────────────────────────────────────────────────────────────── */
body.auth-page{min-height:100vh;background:radial-gradient(125% 80% at 50% 0%,#fff4e8 0%,var(--bg) 50%);color:var(--text-primary)}
.auth-page .auth-shell{max-width:460px;margin:0 auto;padding:4rem 1.5rem}
/* MoMoFi wordmark on the auth pages. The mixed-case wordmark wants
   negative tracking to match the landing-page nav-logo treatment.
   Inner .brand-mo / .brand-fi spans (defined elsewhere in this file)
   set the espresso / terracotta two-tone colors. */
.auth-page .auth-brand{text-align:center;font-family:var(--font-serif);font-weight:700;letter-spacing:-.02em;font-size:1.6rem;margin-bottom:2rem;color:var(--accent)}
.auth-page .auth-brand .mark{color:var(--amber);margin-right:.4rem}
.auth-page .auth-card{position:relative;overflow:hidden;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:2.25rem 1.75rem 2rem;box-shadow:0 24px 64px rgba(52,39,31,.10),0 4px 16px rgba(52,39,31,.05)}
/* Signature brand bar — terracotta→honey, echoing landing's dashboard-card. */
.auth-page .auth-card::before{content:"";position:absolute;top:0;left:0;right:0;height:4px;background:linear-gradient(90deg,var(--accent-soft),var(--accent),var(--amber))}
.auth-page .auth-title{font-family:var(--font-serif);font-size:var(--fs-xl);font-weight:600;margin:0 0 .5rem;color:var(--text-primary)}
.auth-page .auth-sub{color:var(--text-secondary);margin:0 0 1.5rem;font-size:var(--fs-base)}
.auth-page .auth-flash{padding:.7rem .9rem;border-radius:8px;margin-bottom:1rem;font-size:var(--fs-base)}
.auth-page .auth-flash.success{background:var(--income-bg);color:var(--income)}
.auth-page .auth-flash.error{background:var(--expense-bg);color:var(--expense)}
.auth-page .auth-flash.info{background:var(--border-light);color:var(--text-primary)}
.auth-page .auth-label{display:block;font-size:var(--fs-sm);font-weight:500;color:var(--text-primary);margin:.85rem 0 .35rem}
.auth-page .auth-input{width:100%;padding:.7rem .85rem;border-radius:8px;border:1px solid var(--border);background:var(--bg-card);color:var(--text-primary);font-size:var(--fs-base);font-family:inherit;box-sizing:border-box;transition:border-color .12s,box-shadow .12s}
.auth-page .auth-input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(199,92,58,.15)}
/* login_mfa OTP field — large, spaced, centred. */
.auth-page .auth-input--otp{font-size:var(--fs-lg);letter-spacing:.1em;text-align:center}
.auth-page .auth-hint{font-size:var(--fs-sm);color:var(--text-muted);margin-top:.3rem}
.auth-page .auth-hint--error{color:var(--expense)}
/* Password strength meter (signup + reset). The container gets a
   .pw-strength--1..4 class from confirm-password.js; width + colour are
   driven from here so the JS only toggles a class. */
.auth-page .pw-strength{margin-top:.45rem}
.auth-page .pw-strength__track{height:5px;border-radius:3px;background:var(--border);overflow:hidden}
.auth-page .pw-strength__bar{height:100%;width:0;border-radius:3px;transition:width .2s ease,background .2s ease}
.auth-page .pw-strength__label{display:inline-block;margin-top:.25rem;font-size:var(--fs-sm);color:var(--text-muted)}
.auth-page .pw-strength--1 .pw-strength__bar{width:25%;background:var(--expense)}
.auth-page .pw-strength--2 .pw-strength__bar{width:50%;background:#e0832f}
.auth-page .pw-strength--3 .pw-strength__bar{width:75%;background:#c9a227}
.auth-page .pw-strength--4 .pw-strength__bar{width:100%;background:var(--income)}
.auth-page .auth-btn{display:block;width:100%;padding:.8rem;margin-top:1.5rem;background:var(--accent);color:#fff;border:0;border-radius:100px;font-weight:600;font-size:var(--fs-base);cursor:pointer;font-family:inherit;box-shadow:0 4px 12px rgba(199,92,58,.28);transition:background .15s,transform .12s,box-shadow .15s}
.auth-page .auth-btn:hover{background:var(--accent-soft);transform:translateY(-1px);box-shadow:0 6px 16px rgba(199,92,58,.34)}
.auth-page .auth-foot{text-align:center;color:var(--text-secondary);margin-top:1.25rem;font-size:var(--fs-base)}
.auth-page .auth-foot a{color:var(--accent);font-weight:500}
.auth-page .auth-back{display:inline-block;margin-top:1.5rem;color:var(--text-muted);font-size:var(--fs-sm);text-align:center;width:100%;text-decoration:none}
.auth-page .trial-banner{background:var(--amber-pale);border:1px solid #e8c87a;color:var(--amber);padding:.7rem .9rem;border-radius:8px;font-size:var(--fs-base);margin-bottom:1.25rem}
.auth-page .trial-banner strong{color:var(--text-primary)}

/* Split-transaction editor (was inline in transaction_form.html). */
.split-lines{display:flex;flex-direction:column;gap:.5rem;margin-bottom:.5rem}
.split-line{display:grid;grid-template-columns:7rem 1fr 1fr 2rem;gap:.5rem;align-items:center}
.split-line__amount{text-align:right}
.split-line__remove{padding:.25rem .5rem;font-size:var(--fs-lg);line-height:1}
.split-controls{display:flex;gap:.5rem;margin-top:.25rem}
.split-sum{margin-top:.5rem;font-variant-numeric:tabular-nums}

/* Wide withholding row + matching header variant (income_form.html
   uses the 6-column layout: label / type / amount / % / target /
   remove). The base .withholding-grid-header,.withholding-row CSS
   still declares a 4-col grid for legacy compatibility, but the
   income form opts into 6 via the --wide modifier on BOTH the header
   and the row so they line up. The remove column is a fixed 28px box
   for the × button so it doesn't steal horizontal space from the
   data columns. ≤768px collapse to 1fr is already handled by the
   mobile band's .withholding-row catch-all. */
.withholding-row--wide,
.withholding-grid-header--wide{grid-template-columns:1.4fr 1.2fr 1fr 50px 1.4fr 28px}

/* Per-row × Remove button. Muted by default; tints expense-red on
   hover so the destructive intent is visible. Focus-visible outline
   keeps it keyboard-reachable. */
.withholding-row__remove{
  background:transparent;border:0;cursor:pointer;
  font-size:18px;line-height:1;padding:2px 6px;
  color:var(--text-muted);border-radius:4px;
  align-self:center;justify-self:end;
}
.withholding-row__remove:hover{background:var(--expense-bg);color:var(--expense)}
.withholding-row__remove:focus-visible{outline:2px solid var(--accent);outline-offset:1px}

.withholding-add-row{display:flex;align-items:center;gap:.75rem;margin-top:.75rem}
.withholding-add-row__hint{font-size:var(--fs-sm);color:var(--text-muted)}

/* ── Billing pricing cards + interval toggle ─────────────────────────────
 * Used on /settings/billing for free-tier users picking a paid plan.
 * Mirrors the landing-page pricing section's structure but in the authed
 * light theme. The monthly/annual toggle JS (in ledger.js) flips
 * .is-annual on .billing-tiers and updates the hidden interval inputs
 * inside each tier's checkout form. CSS does the rest (price labels
 * hide/show; .is-active styles the chosen toggle button).
 */
.billing-interval{
  display:inline-flex;gap:.25rem;
  background:var(--bg);border:1px solid var(--border-light);
  border-radius:999px;padding:.25rem;margin:.25rem 0 1rem;
}
.billing-interval button{
  appearance:none;background:transparent;border:0;cursor:pointer;
  padding:.4rem 1rem;border-radius:999px;
  font:inherit;font-size:var(--fs-sm);font-weight:500;
  color:var(--text-muted);transition:background .12s,color .12s;
}
.billing-interval button:hover{color:var(--text-secondary)}
.billing-interval button.is-active{
  background:var(--bg-card);color:var(--text-primary);
  box-shadow:0 1px 2px rgba(0,0,0,.06);
}
.billing-interval__save{
  display:inline-block;margin-left:.4rem;
  font-size:var(--fs-xs);color:var(--accent);font-weight:600;
}
.billing-tiers{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));
  gap:1rem;margin-top:.5rem;
}
.billing-tier{
  position:relative;display:flex;flex-direction:column;
  background:var(--bg-card);border:1px solid var(--border-light);
  border-radius:var(--radius-lg);padding:1.5rem 1.25rem 1.25rem;
}
.billing-tier--current{background:var(--bg)}
.billing-tier--featured{
  border-color:var(--accent);
  box-shadow:0 0 0 1px var(--accent),0 8px 20px rgba(26,127,100,.08);
}
.billing-tier__ribbon{
  position:absolute;top:-10px;left:50%;transform:translateX(-50%);
  background:var(--accent);color:#fff;
  font-size:var(--fs-xs);letter-spacing:.06em;font-weight:600;
  padding:.2rem .65rem;border-radius:999px;text-transform:uppercase;
}
.billing-tier__head h3{margin:0;font-size:var(--fs-md);font-weight:600;color:var(--text-primary)}
.billing-tier__tag{color:var(--text-muted);font-size:var(--fs-sm);margin:.25rem 0 0;line-height:1.5}
.billing-tier__price{margin:1rem 0 .85rem;line-height:1.2}
.billing-tier__price-num{font-size:1.9rem;font-weight:700;color:var(--text-primary)}
.billing-tier__price-unit{color:var(--text-muted);margin-left:.3rem;font-size:var(--fs-sm)}
.billing-tier__feats{list-style:none;padding:0;margin:0 0 1.25rem;flex-grow:1}
/* The "Everything in X" lead list above the Plus divider — doesn't grow
   (the Plus list below carries flex-grow so the CTA still pins to the
   card bottom) and sits flush against the divider. */
.billing-tier__feats--base{flex-grow:0;margin-bottom:0}
.billing-tier__feats li{
  color:var(--text-secondary);font-size:var(--fs-sm);
  padding:.3rem 0 .3rem 1.3rem;position:relative;line-height:1.45;
}
.billing-tier__feats li::before{
  content:"✓";position:absolute;left:0;color:var(--accent);font-weight:700;
}
/* Plus divider + label — mirrors landing.html's .plan-divider /
   .plan-section-label so the two surfaces group "Everything in X" apart
   from the tier's own additions identically. */
.billing-tier__divider{border:none;border-top:1px solid var(--border-light);margin:1.1rem 0}
.billing-tier__plus{
  font-size:var(--fs-xs);font-weight:700;letter-spacing:.08em;
  text-transform:uppercase;color:var(--text-muted);margin:0 0 .6rem;
}
.billing-tier__form{margin:0}
.billing-tier__cta{display:block;width:100%;text-align:center}
.billing-tier__current-pill{
  display:inline-block;align-self:flex-start;
  background:var(--bg-card);border:1px solid var(--border-light);
  color:var(--text-muted);
  font-size:var(--fs-xs);font-weight:600;letter-spacing:.04em;
  padding:.35rem .75rem;border-radius:999px;text-transform:uppercase;
}
.billing-tier__price-annual{display:none}
.billing-tiers.is-annual .billing-tier__price-monthly{display:none}
.billing-tiers.is-annual .billing-tier__price-annual{display:inline}

/* ── Receipt attachments on the transaction form ────────────────────── */
.receipt-group .form-control[type="file"]{padding:.4rem}
.receipt-existing{
  display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));
  gap:.6rem;margin-bottom:.6rem;
}
.receipt-thumb{
  margin:0;display:flex;flex-direction:column;
  border:1px solid var(--border-light);border-radius:8px;
  overflow:hidden;background:var(--bg);
}
.receipt-thumb a{display:block;line-height:0}
.receipt-thumb img{
  width:100%;aspect-ratio:1/1;object-fit:cover;
  display:block;background:#f3f3f1;
}
.receipt-thumb figcaption{
  display:flex;align-items:center;gap:.3rem;
  padding:.35rem .5rem;background:var(--bg-card);
  font-size:var(--fs-xs);color:var(--text-secondary);
  border-top:1px solid var(--border-light);
}
.receipt-thumb__name{
  flex:1;min-width:0;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.receipt-thumb__delete{margin:0;display:inline-flex}
.btn-icon{
  appearance:none;background:transparent;border:0;cursor:pointer;
  width:1.4rem;height:1.4rem;border-radius:4px;padding:0;
  font:inherit;font-size:1rem;line-height:1;
  color:var(--text-muted);
  display:inline-flex;align-items:center;justify-content:center;
}
.btn-icon:hover{background:var(--bg);color:var(--text-primary)}
.btn-icon--danger:hover{background:var(--expense-bg);color:var(--expense)}
.receipt-cap-hint{color:var(--text-muted)}
.receipt-cap-hint--full{color:var(--warn);font-weight:500}

/* ── In-app tester feedback widget ────────────────────────────────────
   Only rendered when FEEDBACK_ENABLED is set (template gate). The FAB
   trigger is visible on BOTH desktop and mobile — unlike .fab which
   is mobile-only — because feedback during testing is useful from
   any screen size. The modal mirrors .celebration-modal's overlay +
   card pattern. Strict CSP / no inline event handlers — JS in
   web/static/js/feedback.js. */
.feedback-fab{
  position:fixed; right:16px; bottom:16px;
  width:48px; height:48px;
  display:flex; align-items:center; justify-content:center;
  background:var(--accent, #2e8b6f); color:#fff;
  border:none; border-radius:50%;
  box-shadow:0 4px 12px rgba(0,0,0,.18);
  cursor:pointer;
  /* z-index 1099 sits just under the modal (1100) and above every
     other fixed chrome surface: appbar (50), bottom-nav (45),
     sidebar (10/40), tooltips (60). The earlier 45 tied with
     bottom-nav, causing the FAB to render *under* it on kid mobile
     views once a stacking context kicked in (the mobile sidebar's
     transform + the kid dashboard's grid both create one). 1099
     guarantees the button can't be obscured short of an actual
     modal taking over the screen. */
  z-index:1099;
  /* Force the FAB to be its own root stacking context — independent
     of any ancestor's transform / filter / will-change. Belt-and-
     suspenders with the template-side move out of .layout. */
  isolation:isolate;
  transition:transform .15s ease, box-shadow .15s ease;
}
.feedback-fab:hover{ transform:translateY(-1px); box-shadow:0 6px 14px rgba(0,0,0,.22); }
.feedback-fab:focus-visible{ outline:2px solid var(--accent, #2e8b6f); outline-offset:3px; }
.feedback-fab svg{ width:22px; height:22px; }

/* On mobile, the feedback FAB takes over the lower-right slot the
   "+ Add transaction" .fab used to occupy (the .fab is hidden on mobile
   in ledger.mobile.css — see comment there). One floating action is
   better than two stacked: the previous stack put the feedback button
   144px above the bottom edge, where the iOS Safari address bar and
   home-indicator chrome could still occlude it depending on scroll
   state. Sitting exactly one row above the bottom-nav (56 + 16 + safe)
   keeps the bubble in the thumb's natural reach and well clear of the
   nav itself. Same 48×48 accent-color circle as desktop — only the
   position differs between layouts.

   Layout:                           viewport
                                     ┌──────────────┐
                                     │              │
                                     │              │
                                     │              │
                                     │              │
                                     │     [💬]     │  ← feedback (72px up)
                                     ├──────────────┤
                                     │ bottom-nav   │
                                     └──────────────┘
*/
:root[data-effective-layout="mobile"] .feedback-fab{
  right:16px;
  bottom: calc(56px + 16px + env(safe-area-inset-bottom, 0px));
  top:auto;
  /* Same 1099 ceiling as desktop — must dominate bottom-nav (45),
     appbar (50), and the mobile drawer (40). Pre-fix the mobile
     rule was 46, one above bottom-nav, but iOS Safari's scroll-
     induced safe-area fluctuation could still leave the FAB
     visually nested behind the nav after a hard scroll-to-bottom. */
  z-index:1099;
}

.feedback-modal{
  position:fixed; inset:0; z-index:1100;
  display:flex; align-items:center; justify-content:center;
  background:rgba(15,23,42,.55);
  padding:1rem;
}
.feedback-modal[hidden]{ display:none; }
.feedback-card{
  background:var(--surface, #fffcf6);
  border-radius:12px;
  box-shadow:0 12px 32px rgba(0,0,0,.18);
  width:min(480px, 96vw);
  padding:1.5rem;
}
.feedback-title{
  font-size:1.125rem; margin:0 0 .25rem 0; font-weight:600;
  color:var(--text-primary, #1f2937);
}
.feedback-sub{
  font-size:.85rem; color:var(--text-secondary, #6b7280);
  margin:0 0 1rem 0;
}
.feedback-textarea{
  width:100%; padding:.625rem .75rem;
  font:inherit; color:var(--text-primary, #1f2937);
  background:#fff;
  border:1px solid var(--border-color, #d1d5db);
  border-radius:6px;
  resize:vertical; min-height:5rem;
  box-sizing:border-box;
}
.feedback-textarea:focus{
  outline:none; border-color:var(--accent, #2e8b6f);
  box-shadow:0 0 0 3px rgba(46,139,111,.18);
}
.feedback-actions{
  display:flex; gap:.5rem; justify-content:flex-end;
  margin-top:1rem;
}

/* ── Idle lock-screen warning ────────────────────────────────────────
   Revealed by idle_timer.js shortly before the inactivity window ends.
   Mirrors the .feedback-modal overlay + card pattern; starts hidden. */
.idle-modal{
  position:fixed; inset:0; z-index:1100;
  display:flex; align-items:center; justify-content:center;
  background:rgba(15,23,42,.55);
  padding:1rem;
}
.idle-modal[hidden]{ display:none; }
.idle-card{
  background:var(--surface, #fffcf6);
  border-radius:12px;
  box-shadow:0 12px 32px rgba(0,0,0,.18);
  width:min(420px, 96vw);
  padding:1.75rem 1.5rem;
  text-align:center;
}
.idle-title{
  font-size:1.25rem; margin:0 0 .4rem 0; font-weight:600;
  color:var(--text-primary, #1f2937);
}
.idle-sub{
  font-size:.9rem; color:var(--text-secondary, #6b7280);
  margin:0 0 1.25rem 0; line-height:1.5;
}
.idle-actions{
  display:flex; gap:.5rem; justify-content:center; flex-wrap:wrap;
}


/* ── Kid (teen) gamified dashboard ───────────────────────────────────
   Branched into from /dashboard for sessions where Session.IsChild()
   (see dashboard_kid.go). The visual brief is "gamified momentum"
   for a teen audience: bright accents, big numbers, progress framing.
   The .dashboard--kid wrapper scopes these overrides so they don't
   leak into other surfaces (e.g. the .stat-card on the adult
   dashboard stays its neutral self).

   Streak banner: amber-warm card sitting above the tiles. .active
   variant lights up when StreakWeeks > 0. Icon is a unicode flame
   (no SVG to keep this CSS-only). */
.dashboard--kid .kid-streak{
  display:flex; align-items:center; gap:.85rem;
  padding:1rem 1.25rem; margin-bottom:1.25rem;
  border-radius:12px;
  background:linear-gradient(135deg,#fef3c7 0%,#fde68a 100%);
  border:1px solid #f59e0b;
  color:#7c2d12;
}
.dashboard--kid .kid-streak--active{
  background:linear-gradient(135deg,#fed7aa 0%,#fb923c 100%);
  border-color:#c2410c;
  color:#7c2d12;
}
.dashboard--kid .kid-streak__icon{
  font-size:2rem; line-height:1; flex:0 0 auto;
}
.dashboard--kid .kid-streak__body{ flex:1 1 auto; }
.dashboard--kid .kid-streak__big{
  font-size:1.25rem; font-weight:700; line-height:1.2;
}
.dashboard--kid .kid-streak__sub{
  font-size:.875rem; opacity:.85; margin-top:.15rem;
}

/* Big-number momentum tiles — same .stat-card shape as adult, but
   with bright accent borders + larger numbers. */
.dashboard--kid .kid-tile{
  border-left:4px solid var(--accent, #2e8b6f);
  transition:transform .12s ease, box-shadow .12s ease;
}
.dashboard--kid .kid-tile:hover{
  transform:translateY(-2px);
  box-shadow:0 4px 12px rgba(0,0,0,.08);
}
.dashboard--kid .kid-tile .stat-value{
  font-size:1.75rem; font-weight:700; line-height:1.1;
}
.dashboard--kid .kid-tile .stat-sub{
  font-size:.75rem; color:var(--text-muted); margin-top:.25rem;
  letter-spacing:.02em;
}
.dashboard--kid .kid-tile--balance{ border-left-color:#0ea5e9; }
.dashboard--kid .kid-tile--earned { border-left-color:#10b981; }
.dashboard--kid .kid-tile--chores { border-left-color:#8b5cf6; }

/* Goal rows — compact one-per-row listing on the dashboard glance.
   Full progress UI lives on /goals; this is just an at-a-glance reminder. */
/* The goals body pads itself because .card pads nothing, and takes no top
   inset because the .card-header directly above it already separates it.
   That is why it is not .card-body, which would add 1.25rem all round. */
.dashboard--kid .kid-goal-list{ padding:0 var(--sp-4) var(--sp-4); }
.dashboard--kid .kid-goal-row{
  display:flex; justify-content:space-between; align-items:baseline;
  padding:.5rem 0;
  border-bottom:1px solid var(--border-light, #e5e7eb);
}
.dashboard--kid .kid-goal-row:last-child{ border-bottom:none; }
.dashboard--kid .kid-goal-row__name{ font-weight:600; }
.dashboard--kid .kid-goal-row__target{
  font-size:.875rem; color:var(--text-muted);
}

/* ── Subscription-lapsed banner ──────────────────────────────────────
   Shown globally above each page's content when the household
   subscription is in a non-active state (canceled / unpaid / no row).
   Adult variant carries a Reactivate CTA aligned to the right; kid
   variant is information-only (no CTA — they can't fix it themselves).
   Built on the existing .alert-banner shell so it inherits the
   border / padding / icon-slot semantics. */
.lapsed-banner{
  background:#fef3c7;
  border-color:#f59e0b;
  color:#7c2d12;
}
.lapsed-banner__cta{
  flex:0 0 auto; align-self:center;
  white-space:nowrap;
  margin-left:auto;
}

/* ───────────────────────────────────────────────────────────────────
   Shopping list — inline-edit-in-place pattern.

   Each row is a <details> wrapping a <summary class="shopping-row__
   display"> (the always-visible row) and a <form class="shopping-row
   __editor"> (the edit form). When the details is open, :has()
   replaces the display block with the edit form rather than stacking
   the two — no more "see the same item twice" redundancy.

   The summary marker is overridden to a custom pencil icon at the
   right edge (closed) → "✕ Cancel" affordance (open). Browsers handle
   interactive elements inside summary per spec — clicking the
   checkbox or delete button fires that control's handler, not the
   disclosure toggle.
   ─────────────────────────────────────────────────────────────────── */
.shopping-row{
  border-top:1px solid var(--border);
}
.shopping-row:first-child{ border-top:0; }
.shopping-row__details{ display:block; }
.shopping-row__details > summary{
  /* Kill the default ▶/▼ disclosure marker — we draw our own. */
  list-style:none;
}
.shopping-row__details > summary::-webkit-details-marker{ display:none; }

/* Display block (closed state). Flex bar holding the toggle form,
   delete X, and the custom pencil "edit" affordance via ::after. */
.shopping-row__display{
  display:flex;
  align-items:center;
  gap:.6rem;
  padding:.5rem 1rem;
  cursor:pointer;
  position:relative;
}
.shopping-row__display:hover{ background:var(--surface-2, rgba(0,0,0,.025)); }
/* Four aligned columns: [checkbox] [name] [meta] [price]. The name flexes
   (minmax(0,1fr)) while the meta + price tracks stay put, so the trailing
   info lines up in a column down the list instead of hanging off the end of
   each (variable-length) name. */
.shopping-row__toggle{
  display:grid;
  grid-template-columns:auto minmax(0,1fr) auto 5.5rem;
  align-items:center;
  column-gap:.6rem;
  flex:1;
  margin:0;
  cursor:auto;          /* form area shouldn't inherit summary's pointer */
}
.shopping-row__name{
  min-width:0;          /* shrink/wrap within the cell, don't widen the grid */
}
.shopping-row__name--done{
  text-decoration:line-through;
  opacity:.6;
}
/* Contextual middle (log-as-expense controls or purchase metadata). Right-
   aligned so its trailing edge meets the price column — open and purchased
   rows then share the same vertical guides. */
.shopping-row__meta{
  display:flex;
  align-items:center;
  justify-content:flex-end;
  gap:.5rem;
  min-width:0;
}
.shopping-row__amount{ max-width:7rem; }
.shopping-row__account{ max-width:10rem; }
.shopping-row__delete{ margin:0; cursor:auto; }
.shopping-row__price{
  justify-self:end;
  text-align:right;
  white-space:nowrap;
}

/* Custom pencil/edit affordance — sits at the far right of the
   display row. Becomes the cancel affordance when the editor is open
   (handled below). */
.shopping-row__display::after{
  content:"✎";
  font-size:.95em;
  color:var(--text-muted, #6b7280);
  padding:.15rem .45rem;
  border-radius:4px;
  flex:0 0 auto;
  transition:background .12s, color .12s;
}
.shopping-row__display:hover::after{
  background:var(--surface-3, rgba(0,0,0,.05));
  color:var(--text, #111);
}

/* Editor (open state). Replaces the display row's spot in the layout
   via :has() — display block hides, editor takes the same padded
   slot. */
.shopping-row__editor{
  display:none;
  gap:.5rem;
  align-items:flex-end;
  flex-wrap:wrap;
  padding:.65rem 1rem .75rem;
  background:var(--surface-2, rgba(0,0,0,.02));
}
.shopping-row__details[open] > .shopping-row__editor{
  display:flex;
}
/* When the editor is open, hide the display block. :has() is the
   load-bearing selector here; widely supported since 2023. */
.shopping-row:has(.shopping-row__details[open]) > .shopping-row__details > .shopping-row__display{
  /* Keep the summary clickable (it's the toggle back to closed) but
     collapse it visually to a tiny header strip above the editor. */
  padding:.35rem 1rem 0;
  cursor:pointer;
}
.shopping-row:has(.shopping-row__details[open]) .shopping-row__toggle,
.shopping-row:has(.shopping-row__details[open]) .shopping-row__delete{
  display:none;
}
.shopping-row:has(.shopping-row__details[open]) .shopping-row__display::after{
  content:"Editing — click to close";
  font-size:.82em;
  color:var(--text-muted, #6b7280);
  padding:0;
  background:none;
}
.shopping-row:has(.shopping-row__details[open]) .shopping-row__display:hover::after{
  color:var(--text, #111);
  background:none;
}

/* Shopping-list title — click-to-edit. Same <details>/:has() pattern
   as the item rows so a user editing a list title and editing an
   item feel visually identical. Closed → the h1 shows with a small ✎
   marker after it; clicking the title (or the marker) opens an
   inline rename form that replaces the title text. The data-close-
   details handler in ledger.js handles Cancel. */
/* Block so the whole title component falls onto the line BELOW
   .page-header__back (which is inline-block with margin-bottom).
   The inner .list-title-edit__display stays inline-flex so the
   title text + ✎ marker still hug their content rather than
   spanning the row width. */
.list-title-edit{ display:block; }
.list-title-edit > summary{ list-style:none; }
.list-title-edit > summary::-webkit-details-marker{ display:none; }

.list-title-edit__display{
  display:inline-flex;
  align-items:baseline;
  gap:.45rem;
  cursor:pointer;
  padding:.05rem .35rem .05rem 0;
  border-radius:6px;
}
.list-title-edit__display:hover{
  background:var(--surface-2, rgba(0,0,0,.025));
}
.list-title-edit__display::after{
  content:"✎";
  font-size:.7em;
  color:var(--text-muted, #6b7280);
  transition:color .12s;
}
.list-title-edit__display:hover::after{ color:var(--text, #111); }

/* Rename form — shown only when the title's <details> is open.
   Replaces the h1 in place. */
.list-title-edit__form{
  display:none;
  gap:.5rem;
  align-items:center;
  flex-wrap:wrap;
  margin:.15rem 0 .25rem;
}
.list-title-edit[open] > .list-title-edit__form{ display:flex; }
.list-title-edit__form input[name="listName"]{
  font-size:1.4rem;          /* match h1.page-title scale so the form
                                feels like the title transforming, not
                                a generic input dropping in */
  font-weight:600;
  min-width:14rem;
}

/* When the rename form is open, hide the h1 so the input takes its
   visual slot (same in-place swap pattern the item rows use). */
.list-title-edit[open] > .list-title-edit__display{
  display:none;
}

/* ── Life journey (/timeline) ─────────────────────────────────────────── */
/* The global .card rule sets no padding (it's only added to .card > form /
   prose widgets); give every timeline card a consistent inner body so the
   chart, form, and headings don't hug the card border. Matches the 1.25rem
   used by .card-header / .card > form. */
.lj-card-body{ padding:1.25rem; }
.lj-card-body > .card-title{ margin-bottom:.35rem; }
.lj-timeline{ width:100%; height:auto; display:block; }
.lj-legend{
  display:flex; flex-wrap:wrap; gap:1.25rem; margin-top:.9rem;
  font-size:var(--fs-sm); color:var(--text-muted);
}
.lj-legend-item{ display:inline-flex; align-items:center; gap:.45rem; }
.lj-swatch{ width:.85rem; height:.85rem; border-radius:3px; display:inline-block; }
.lj-swatch--networth{ background:#c75c3a; }
.lj-swatch--score{ background:#2e8b6f; }
.lj-swatch--moment{ background:#cf9445; border-radius:50%; }
/* Moments list — rows span the card edge-to-edge (the <ul> sits outside the
   padded body) with their own 1.25rem horizontal padding, the standard
   list-inside-card pattern (cf. .journey-list). */
.lj-moment-list{ list-style:none; margin:0; padding:0; border-top:1px solid var(--border-light); }
.lj-moment-row{
  display:flex; align-items:center; justify-content:space-between;
  gap:1rem; padding:.7rem 1.25rem; border-bottom:1px solid var(--border-light);
}
.lj-moment-row:last-child{ border-bottom:none; }
.lj-moment-main{ display:flex; align-items:baseline; gap:.5rem; flex-wrap:wrap; }
.lj-moment-date{
  font-variant-numeric:tabular-nums; color:var(--text-muted);
  font-size:var(--fs-sm); min-width:6.5rem;
}
/* Collapsed life-journey section on /insights — native <details>, no JS. */
.lj-disclosure{ padding:0; }
.lj-disclosure__summary{
  display:flex; align-items:baseline; gap:.6rem; flex-wrap:wrap;
  cursor:pointer; list-style:none; padding:1rem 1.25rem;
  font-weight:600; color:var(--text-primary);
}
.lj-disclosure__summary::-webkit-details-marker{ display:none; }
.lj-disclosure__summary::after{
  content:"\25B8"; margin-left:auto; color:var(--text-muted); transition:transform .15s;
}
.lj-disclosure[open] .lj-disclosure__summary::after{ transform:rotate(90deg); }
.lj-disclosure[open] .lj-disclosure__summary{ border-bottom:1px solid var(--border-light); }
.lj-disclosure__title{ font-size:var(--fs-base); }
.lj-disclosure__hint{ font-weight:400; font-size:var(--fs-sm); color:var(--text-muted); }
.lj-disclosure__pin{ border-top:1px solid var(--border-light); }

/* Age attestation on the household invite form. Visible by default so a
   browser without :has() still shows the field the server requires; hidden
   once a relationship other than Student is picked. */
.age-attest{display:flex;align-items:flex-start;gap:.5rem;max-width:26rem;font-size:var(--fs-sm);color:var(--text-secondary);cursor:pointer}
.age-attest input{margin-top:.15rem;flex:none}
form:has(input[name="relationship"]:checked:not([value="child"])) .age-attest{display:none}

/* Age + terms confirmation on signup. */
/* Consent sits under the last input with no label of its own, so it needs the
   top margin the labels were providing for every other field. It had none, and
   1rem below on top of the button's own 1.5rem, so it hugged the household-name
   box and floated away from the button. A little more than the label rhythm
   (.85rem) because this is a commitment, not another field. */
.auth-attest{display:flex;align-items:flex-start;gap:.6rem;margin:1.35rem 0 .25rem;font-size:var(--fs-sm);color:var(--text-secondary);cursor:pointer;line-height:1.5}
.auth-attest input{margin-top:.2rem;flex:none}

/* Point value on a suggested next step. The engine ranks on exactly this
   number and used to discard it, so the list was ordered with no stated
   reason for the order. */
.rec-points{flex:none;display:inline-flex;flex-direction:column;align-items:center;justify-content:center;
  min-width:46px;padding:5px 4px;border-radius:7px;background:#fbeae8;color:var(--accent);
  font-weight:700;font-size:var(--fs-base);line-height:1.05;font-variant-numeric:tabular-nums}
.rec-points small{font-size:9px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--text-muted)}

/* "What doesn't apply to you" rows on the scoring settings page. */
.decl-row{display:flex;align-items:flex-start;gap:.65rem;padding:.6rem 0;font-size:var(--fs-sm);
  color:var(--text-secondary);line-height:1.5;cursor:pointer;border-top:1px solid var(--border-light)}
.decl-row input{margin-top:.25rem;flex:none}
.decl-row strong{color:var(--text-primary)}

/* Derived-value input: filled in by the page, not the person. Greyed so the
   difference from an empty required field is visible at a glance, and paired
   with an explicit unlock rather than being a dead end. */
.form-control.is-derived{background:var(--surface-sunken,#f1ece5);color:var(--text-muted);cursor:default}
.btn-link-inline{background:none;border:0;padding:0;margin-top:.35rem;font-size:var(--fs-xs);
  color:var(--accent);text-decoration:underline;cursor:pointer}
.btn-link-inline[hidden]{display:none}

/* Onboarding "already added" list. The steps used to be blank forms on every
   visit, so Back looked like data loss and people entered things twice. */
/* Deliberately loud for a non-error notice. Someone arriving here by pressing
   Back is about to retype what they just saved, so this has to catch the eye
   before the form does: accent rule down the side, a tinted panel rather than
   the page's own background, and a check mark that reads as "done" at a glance.
   The alternative is the duplicate accounts this whole box exists to prevent. */
.onboarding-added{margin:0 0 1.5rem;padding:.9rem 1.1rem;border:1px solid var(--accent);
  border-left:4px solid var(--accent);border-radius:var(--radius-md,8px);
  background:rgba(199,92,58,.07);  /* the terracotta accent, washed out */
  box-shadow:0 1px 3px rgba(0,0,0,.05)}
.onboarding-added__title{display:flex;align-items:center;gap:.4rem;margin:0 0 .5rem;
  font-weight:700;font-size:var(--fs-sm);color:var(--accent)}
.onboarding-added__title::before{content:"✓";font-weight:700}
.onboarding-added__list{list-style:none;margin:0 0 .5rem;padding:0}
.onboarding-added__item{display:flex;justify-content:space-between;gap:1rem;padding:.2rem 0}
.onboarding-added__detail{color:var(--text-muted);font-size:var(--fs-sm)}

/* ── Forecast (dashboard summary + /insights chart) ─────────────────────── */
.forecast-summary__body {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
.forecast-summary__value { font-size: 1.6rem; font-weight: 600; line-height: 1.1; }
.forecast-summary__month { color: var(--text-muted); font-size: .85rem; margin-top: .15rem; }
.forecast-summary__spark { flex: 0 0 auto; }
.forecast-summary__spark svg { display: block; width: 132px; height: 36px; }
.forecast-summary__warn,
.forecast-warn { color: var(--expense); }
.forecast-summary__warn { font-size: .85rem; margin: .6rem 0 0; }

/* .budget-chart-card is padding:0 so its chart can run edge to edge, which
   leaves any note placed AFTER the chart (a direct child, not inside
   .card-header) sitting on the border. The money-flow unlock note and the
   12-month upgrade note are both in that position. */
.budget-chart-card > .form-hint { padding: 0 1.25rem 1rem; margin: .6rem 0 0; }

/* Same position in the monthly review's two money cards: the private-half note
   and the beginner's "only cleared payments" line both sit between the header
   and the table. Aligned with the header's inset rather than the 1rem an inline
   style had been giving one of them. */
.review-money > .form-hint { padding: 0 1.25rem; margin: .6rem 0 .75rem; }

/* .card carries no padding of its own (see .card > form etc.), so every direct
   child a partial emits has to bring its own or it sits on the border. */
.forecast-body { padding: 1rem 1.25rem; }
.forecast-body > .form-hint { margin: .5rem 0 0; }
.forecast-body > .form-hint:first-child { margin: 0 0 .6rem; }
.forecast-body > .empty-state + .form-hint { margin-top: .4rem; }
.forecast-summary { padding: 1.25rem; }

/* dash-section-link is used by the dashboard's section heads (Budget health,
   Recent transactions, the forecast) but was never defined, so those links
   rendered without the small-and-quiet treatment .card-link gets. */
.dash-section-link { font-size: var(--fs-sm); color: var(--accent); font-weight: 500; }
.dash-section-link:hover { text-decoration: underline; }

/* The chart scrolls inside its own box rather than widening the page. */
.forecast-chart-wrap { overflow-x: auto; }
.forecast-chart-wrap svg { display: block; width: 100%; min-width: 460px; height: auto; }

.forecast-numbers { margin-top: .75rem; }
.forecast-numbers > summary {
  cursor: pointer;
  color: var(--accent);
  font-size: .9rem;
  padding: .25rem 0;
}
.forecast-numbers > summary:hover { color: var(--accent-soft); }
.forecast-numbers > table { margin-top: .6rem; }

/* Who makes up a category's budget, when more than one person does. Sits with
   the carryover note: both answer "where did this number come from". */
.budget-row__split {
  display: block;
  font-size: .78rem;
  color: var(--text-muted);
  margin-top: .1rem;
}

/* Legal links at the foot of every signed-in page. Sits inside .main-inner so
   it scrolls with the content and never collides with the fixed bottom-nav. */
.app-footer{display:flex;gap:1.25rem;justify-content:center;margin-top:3rem;
  padding-top:1.25rem;border-top:1px solid var(--border-light)}
.app-footer a{font-size:var(--fs-xs);color:var(--text-muted);text-decoration:none}
.app-footer a:hover{text-decoration:underline}
@media (max-width:768px){.app-footer{margin-bottom:4.5rem}}

/* ── Spacing utilities ───────────────────────────────────────────────────────
   300 inline style="margin…" overrides had grown across the templates, in 93
   distinct values, because a one-off gap had nowhere else to live. These carry
   the scale so it can: use a component's own class where the component owns its
   spacing, and one of these where the gap is genuinely local to one place.
   Only the values the templates actually use are defined; add a step when a
   real call site needs it, not in anticipation. */
.u-m-0{margin:0}
.u-mt-0{margin-top:0}
.u-mt-1{margin-top:var(--sp-1)}
.u-mt-2{margin-top:var(--sp-2)}
.u-mt-3{margin-top:var(--sp-3)}
.u-mt-4{margin-top:var(--sp-4)}
.u-mt-4-5{margin-top:var(--sp-4-5)}
.u-mt-5{margin-top:var(--sp-5)}
.u-mb-2{margin-bottom:var(--sp-2)}
.u-mb-3{margin-bottom:var(--sp-3)}
.u-mb-4{margin-bottom:var(--sp-4)}
.u-mb-4-5{margin-bottom:var(--sp-4-5)}
.u-mb-5{margin-bottom:var(--sp-5)}
.u-ml-2{margin-left:var(--sp-2)}

/* ── Goal detail ─────────────────────────────────────────────────────────────
   This page carried 28 inline style attributes, several of them whole component
   definitions (flex layout, borders, padding) written on the element. Off-scale
   values are snapped to the nearest step as part of the move: each lands within
   1.5px of what it replaced, and the page was diffed to see only those. */
.goal-item-list{list-style:none;margin:0;padding:0}
.goal-item-list--inset{margin:var(--sp-2) 0 0;padding:var(--sp-1) 0 0}

/* One row of a goal's parts or pledges. */
.goal-item-row{display:flex;align-items:center;gap:var(--sp-3);padding:var(--sp-2) var(--sp-4);
  border-top:1px solid var(--border)}
.goal-part-row{display:flex;justify-content:space-between;gap:var(--sp-3);padding:var(--sp-2) var(--sp-4);
  border-top:1px solid var(--border)}
.goal-item-toggle{display:flex;align-items:center;gap:var(--sp-2);flex:1;margin:0}
/* The row's main cell, taking the slack beside its badges and buttons.
   Shared with the pledge list on /insights, which is the same component. */
.goal-item-row__main{flex:1}

/* The note that stands in for an empty list, inset to the card's own gutter. */
.goal-note{padding:var(--sp-2) var(--sp-4)}
.goal-note--open{padding-bottom:0;margin:0}

/* The add-a-part / add-a-pledge row at the foot of a card. */
.card-inline-form{display:flex;gap:var(--sp-2);align-items:flex-end;flex-wrap:wrap;
  border-top:1px solid var(--border)}
/* `.card > form` above supplies a 1.25rem gutter and outweighs a single class,
   which is what the inline style used to beat. Said at matching weight here.
   The --bare variant sits inside a div, so that rule never reaches it.

   Named for the card rather than the goal page: the shopping list's add-item
   form is the same component, declaration for declaration, which is what the
   rename is for. */
.card > form.card-inline-form{padding:var(--sp-3) var(--sp-4)}
.card-inline-form--bare{padding:0;border-top:0;margin:0}

/* Share-by-link block. */
.goal-share{padding:var(--sp-2) var(--sp-4) var(--sp-4);border-top:1px solid var(--border)}
.goal-share-actions{display:flex;gap:var(--sp-4);margin-top:var(--sp-2)}
.goal-share-divider{border-top:1px dashed var(--border);margin-top:var(--sp-4);padding-top:var(--sp-4)}
.goal-hint{margin:var(--sp-1) 0 0}

/* ── Household page ──────────────────────────────────────────────────────────
   family.html carried 20 inline style attributes; .owed-form and its children
   were named classes with no rules, so their whole definition lived on the tag.
   Off-scale values snapped to the nearest step, each within 1.5px. */
.owed-form{border-top:1px solid var(--border)}
/* `.card > form` supplies a 1.25rem gutter and outweighs a single class. */
.card > form.owed-form{padding:var(--sp-3) var(--sp-4)}
.owed-form__list{list-style:none;padding:var(--sp-2) 0;margin:0}
.owed-form__item{display:flex;gap:var(--sp-2);align-items:center;padding:var(--sp-1) 0}
.owed-form__actions{display:flex;gap:var(--sp-2);align-items:center;flex-wrap:wrap;
  margin-top:var(--sp-2)}

/* The graduation offer under a student's accounts. */
.graduate-row{display:flex;align-items:center;gap:var(--sp-2);flex-wrap:wrap;
  padding:var(--sp-2) var(--sp-4);border-top:1px solid var(--border)}

/* Badges, grouped per member. */
.badge-members{padding:var(--sp-2) var(--sp-4) var(--sp-4)}
.badge-member{padding:var(--sp-2) 0;border-top:1px solid var(--border-light)}
.badge-member__row{margin-top:var(--sp-1);display:flex;flex-wrap:wrap;gap:var(--sp-2)}

/* ── Transaction form ────────────────────────────────────────────────────────
   17 inline style attributes, mostly .form-hint margins a hair off the class's
   own 5px default; those now simply take the default. What is left is the one
   hint that has to sit flush under its heading and this expander's paragraph,
   which needs a different rhythm from a hint under a field. */
.quick-breakdown .form-hint{margin:var(--sp-2) 0 var(--sp-3)}

/* ── Account form ────────────────────────────────────────────────────────────
   .form-disclosure was a named class with no rules at all, so the component
   was written on the tag — twice, because the opening-balance explainer has
   one body in add mode and another in edit mode, and each carried its own
   copy. Off-scale values snapped as part of the move, each within 1.5px. */
.form-disclosure{margin-top:calc(var(--sp-1) * -1);margin-bottom:var(--sp-4)}
.form-disclosure > summary{cursor:pointer;font-weight:500;color:var(--text-muted)}
.form-disclosure__body{padding:var(--sp-2) 0 0;font-size:.93em;line-height:1.55}
/* The global reset zeroes every margin, so the body states only its outer
   two: a gap below the summary, and nothing hanging past the last paragraph.
   Written structurally because the two bodies end on different elements. */
.form-disclosure__body > :first-child{margin-top:var(--sp-2)}
.form-disclosure__body > :last-child{margin-bottom:0}
.form-disclosure__list{margin:var(--sp-2) 0;padding-left:var(--sp-4-5)}
.form-disclosure__list li:not(:last-child){margin-bottom:var(--sp-2)}

/* ── Statement import ────────────────────────────────────────────────────────
   The upload half of the picker, divided from the account chooser above it.
   `.card--pad > form` zeroes the gutter, so restoring this one's top padding
   has to outweigh that rule rather than sit beside it.

   Its border used to name `--border-color`, which is defined nowhere. An
   undefined var with no fallback makes the whole shorthand invalid at
   computed-value time, so border-style fell back to `none` and the divider
   drew no line at all. Measured: style `none`, width `0px`. */
.card--pad > form.import-upload{margin-top:var(--sp-5);padding-top:var(--sp-5);
  border-top:1px solid var(--border)}

/* The "matches: <date> · <description>" line under a will-merge badge. */
.import-match-detail{font-size:.85rem;color:var(--text-secondary);margin-top:var(--sp-1)}

/* ── Household settings ──────────────────────────────────────────────────────
   The invite row sits inside a .card--table, so `.card > form` reaches it and
   outweighs a single class. Said at matching weight, like the goal and owed
   forms above. */
.card > form.invite-form{padding:var(--sp-4) var(--sp-4-5);gap:var(--sp-4);align-items:end}

/* The notes under the invite row, indented to the card's own gutter rather
   than the form's. --alone carries the top padding for the case where no
   invite row renders above it. */
.invite-note{padding:0 var(--sp-4-5) var(--sp-4);margin:0;max-width:46rem}
.invite-note--alone{padding:var(--sp-4) var(--sp-4-5)}

/* ── Settings sections ───────────────────────────────────────────────────────
   .settings-section pads its own box, and 36 of them across the settings pages
   hold a form directly. `.card > form` reached those forms and added a second
   gutter on top, so a form's fields sat 19px deeper than the heading directly
   above them, inside the same section. Measured before the change: heading 23px
   from the card edge, form content 42px.

   The selector list mirrors the card gutter list above, one entry per child
   that rule pads. TestSettingsSectionResetsEveryCardGutter fails if the two
   drift apart, because a gutter selector added there and forgotten here comes
   back as the same double inset.

   Stated after `.card > form`, which it matches at the same weight and would
   otherwise lose to on source order. */
.settings-section > form,
.settings-section > .card-body,
.settings-section > .pillar-detail,
.settings-section > .rec-list,
.settings-section > .score-breakdown__body,
.settings-section > .strategy-presets,
.settings-section > .rubric-list{padding:0}

/* The other half of the same problem. `form.card` (0,1,1) outweighs
   .settings-section (0,1,0), so a section that IS a form took 1.25rem all round
   where a section that is a div or a section took 1.25rem 1.5rem, and siblings
   on one page did not line up. Restated at (0,2,1) with the section's own
   padding. */
form.card.settings-section{padding:var(--sp-4-5) var(--sp-5)}

/* ── Card header subtitle ────────────────────────────────────────────────────
   The one-line explanation under a card's title. Seventeen of these across
   five pages each wrote `margin:.2rem 0 0` on the tag — that was EVERY
   .form-hint inside a .card-header, with no counter-example anywhere — so the
   rhythm belongs to the component, not to each page. --sp-1 is the nearest
   step, 0.75px from what all seventeen of them said. */
.card-header .form-hint{margin-top:var(--sp-1)}

/* ── Shopping list ───────────────────────────────────────────────────────────
   .shopping-list was a named class with no rules anywhere, so its own reset
   lived on the tag. The add-item form at the foot of the card is .card-inline-
   form, shared with the goal page rather than copied. */
.shopping-list{list-style:none;margin:0;padding:0}
/* Prose sitting directly on the card, inset to the card's own compact gutter. */
.shopping-empty{padding:var(--sp-2) var(--sp-4)}
.shopping-inbound{padding:var(--sp-3) var(--sp-4)}
/* The inbound address, as a copyable chip. Colour is carried over exactly as
   the tag wrote it: --surface-2 is defined nowhere, but unlike --border-color
   it has a fallback, so the declaration stays valid and the grey renders. */
.inbound-address{display:inline-block;padding:var(--sp-1) var(--sp-2);
  background:var(--surface-2,#f3f4f6);border-radius:6px;user-select:all}

/* ── Accounts ────────────────────────────────────────────────────────────────
   Variants of components that already exist, rather than new components: each
   one was an inline override of the class already on the tag. */
.account-meta--inset{padding:var(--sp-3) var(--sp-4-5)}
.account-meta--banner{margin-top:var(--sp-2);padding:var(--sp-2) var(--sp-3);border-radius:.25rem}
/* Tighter than .empty-state's own 1.5rem, for the one inside a card. */
.empty-state--tight{padding:var(--sp-4) var(--sp-4-5)}
/* The bank-link error keeps `display` inline, because the link JS toggles it. */
.bank-link-error{margin:0 var(--sp-4-5) var(--sp-4)}

/* The gap above a section heading that follows another section. 1.75rem is
   deliberately literal: it is .dash-section's own rhythm (and .page-header's
   and .stat-grid's) but it is NOT a --sp step, and the other 1.75rem values in
   this file are an icon-clearance offset and a font-size, so tokenising the
   number as one thing would be wrong. */
.section-gap{margin-top:1.75rem}
.section-gap__note{margin-top:calc(var(--sp-1) * -1);margin-bottom:var(--sp-4)}

/* ── Account settings ────────────────────────────────────────────────────────
   The passkey button and its live status share a row. .85rem snaps to --sp-3,
   1.5px off what the tag said. */
.passkey-actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-3);
  margin-top:var(--sp-3)}

/* ── Pledge status (public) ──────────────────────────────────────────────────
   The page a pledger reaches from an email, with no account. Everything here
   was written on the tag. .6rem (9px) snaps DOWN to --sp-2, 1.5px off: --sp-3
   is 11.25px and would move it 2.25px, which the computed-box diff caught. The
   .9rem padding snaps up to --sp-4, 1.5px. */
.pledge-status-list{list-style:none;margin:var(--sp-4) 0 0;padding:0;text-align:left}
.pledge-status-row{padding:var(--sp-3) 0;border-top:1px solid var(--border)}
.pledge-status-track{margin:var(--sp-2) 0 var(--sp-1)}
.pledge-status-nudge{margin-top:var(--sp-4-5);padding:var(--sp-4) var(--sp-4);
  border:1px solid var(--border);border-radius:.6rem;text-align:left}
.pledge-status-nudge__note{margin:var(--sp-1) 0 var(--sp-2)}
.pledge-status-optout{margin-top:var(--sp-4);text-align:center}

/* ── Subscriptions ───────────────────────────────────────────────────────────
   Two blocks inside .card--table, which pads nothing, so each states the
   card's own gutter on the side it needs it. */
.subs-empty{padding:var(--sp-4-5) var(--sp-4-5) 0}
.subs-categories{padding:0 var(--sp-4-5) var(--sp-4-5)}
