/*
 * Theme system layered on Bootstrap 4.1 — v2 (design-system review).
 *
 * What changed from v1:
 *   - Type: adopts Hanken Grotesk (UI/figures) + Newsreader (editorial accents).
 *   - Brand palette warmed & deepened for a more premium green.
 *   - Default theme is now "Evergreen" (the brand green), keyed "finance" for
 *     cookie back-compat. Set the base.html default to 'finance' (see patch note).
 *   - All four themes kept, rebuilt on one shared, green-tinted neutral set.
 *   - Bigger radii, one soft shadow token, mint focus ring, tabular figures.
 *
 * Load the fonts once in <head> of base.html AND public_base.html:
 *   <link rel="preconnect" href="https://fonts.googleapis.com">
 *   <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
 *   <link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700;800&family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;1,6..72,400;1,6..72,500&display=swap" rel="stylesheet">
 */

:root {
    /* Type scale. Every font-size below reads one of these; --fs-2xs (12px)
       is the floor, so nothing in the app is set smaller than it. The one
       designed exception is .display-total, the headline figure. Adding a
       size means adding a step here, not a literal in a rule --
       TypeScaleTests in config/test_ui_conventions.py holds that line. */
    --fs-2xs: 0.75rem;
    --fs-xs: 0.85rem;
    --fs-sm: 0.95rem;
    --fs-base: 1rem;
    --fs-lg: 1.15rem;
    --fs-xl: 1.4rem;
    --fs-2xl: 1.9rem;
    --fs-3xl: 2.6rem;

    /* Font families */
    --font-ui: 'Hanken Grotesk', system-ui, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif;
    --font-display: 'Newsreader', Georgia, 'Times New Roman', serif;

    /* Spacing scale, named by its size in pixels. Every margin, padding
       and gap in this file reads one of these (or is 0, auto, a % or a
       font-relative em). Reach for 4/8/12/16/24/32 first; 2, 6, 10 and 20
       are for the insides of chips, pills and small controls, and 40-64
       for page-level breathing room. Bootstrap spacing utilities (mb-3, px-2 ...)
       sit on 4/8/16/24/48, so they land on the same grid.
       SpacingScaleTests in config/test_ui_conventions.py holds the line. */
    --sp-2: 0.125rem;
    --sp-4: 0.25rem;
    --sp-6: 0.375rem;
    --sp-8: 0.5rem;
    --sp-10: 0.625rem;
    --sp-12: 0.75rem;
    --sp-16: 1rem;
    --sp-20: 1.25rem;
    --sp-24: 1.5rem;
    --sp-32: 2rem;
    --sp-40: 2.5rem;
    --sp-48: 3rem;
    --sp-64: 4rem;

    /* Shape & elevation tokens (replace ad-hoc 8px radii / flat borders) */
    --radius-sm: 8px;
    --radius: 12px;
    --radius-lg: 16px;
    --shadow-sm: 0 1px 3px rgba(20, 32, 26, 0.05);
    --shadow: 0 8px 28px rgba(20, 60, 44, 0.10);
    --shadow-lg: 0 18px 44px rgba(15, 47, 34, 0.16);

    /* Layout. The signed-in shell is a fixed 230px rail plus a reading
       column; --sidebar-rail is the rail *and* its gutter, i.e. where the
       column's left edge sits, and --content-width is how wide that column
       is allowed to get. Both are read in the lg+ sidebar block at the
       bottom of this file, and --content-width is also the cap main and the
       footer share at every width. */
    --sidebar-rail: 250px;
    --content-width: 900px;

    /* Brand palette — logged-out marketing pages (fixed, theme-independent).
       Warmer, deeper, more premium than v1. */
    --brand-dark: #0b241a;
    --brand-deep: #0f2f22;
    --brand-primary: #1f5e45;
    --brand-primary-hover: #174a35;
    --brand-accent: #4cc38a;
    --brand-accent-hover: #6ed3a2;
    --brand-light: #a7e8c8;
    --brand-tint: #cfe6da;
    --brand-muted: #7fa792;
    --brand-success-bg: #dff0e8;
    --brand-surface: #eef5f1;
    --brand-surface-alt: #f6f8f7;
    /* Reading text on the public pages. Green-tinted rather than
       Bootstrap's #495057 / #343a40 greys, which each page used to carry
       in its own <style> block. 8.1:1 and 12.4:1 on white. */
    --brand-text: #45524b;
    --brand-text-strong: #2b3631;
}

/* `hidden` means hidden, whatever display utility the element carries.
   Bootstrap's reboot has [hidden] { display: none !important }, but its
   .d-flex / .d-block / ... utilities are also !important, at the same
   weight and later in bootstrap.min.css -- so an element with d-flex that a
   script marks `hidden` stayed on screen. This file loads after Bootstrap,
   so the same declaration here wins. (The household rename heading hit the
   inline-style version of this trap; see households/_name.html.) */
[hidden] { display: none !important; }

/* Global type: give the brand a voice instead of the OS default. */
body {
    font-family: var(--font-ui);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

/* Keyboard focus, app-wide. Components that draw their own ring (inputs'
   box-shadow, the tab strip, disclosure summaries, card actions...) keep
   it: their rules are two classes deep and win over this one. Everything
   else -- sidebar links, in-text links, plain buttons -- used to fall back
   to the browser's default, a 1px amber ring that matched nothing.
   :is() of bare element names lifts this to (0,1,1), so it beats
   Bootstrap's `button:focus` (which sits before this file) while staying
   under any .class:focus rule -- an attribute selector in the list would
   make it (0,2,0) and override .form-control:focus, so [tabindex] gets
   its own zero-weight :where(). The dark shells (sidebar, public nav and footer) take the light
   nav-link colour instead, since the primary green disappears on them. */
:is(a, button, summary, select, input, textarea):focus-visible,
:where([tabindex]):focus-visible {
    outline: 2px solid var(--theme-primary);
    outline-offset: 2px;
}
.navbar.themed :is(a, button, select, input):focus-visible {
    outline-color: var(--theme-navbar-link);
}
.public-nav :is(a, button):focus-visible,
.public-footer a:focus-visible {
    outline-color: var(--brand-light);
}

/* All figures line up: tabular numerals on money/stat text. */
.display-total,
.stat-value,
.account-section-count,
.change-badge,
.table td,
.table th,
.mock-worth,
.form-control,
input,
select,
textarea,
.tnum {
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1;
}

/* ============================ THEMES ============================ */

/* Name ↔ data-theme key map (keys are frozen for cookie back-compat):
     Evergreen  → "finance"  (DEFAULT)
     Slate Teal → "flatly"
     Midnight   → "dark"
     Azure      → "default"
   The label and the key differ on purpose — edit by NAME, match by KEY. */

/* "Evergreen" — the brand green, now the DEFAULT. Keyed "finance" so existing
   theme cookies keep working. Carries the bare :root so it applies whenever no
   data-theme is resolved. Keep FIRST among theme blocks. */
:root,
[data-theme="finance"] {
    --theme-body-bg: #f6f8f7;
    --theme-body-color: #14201a;
    --theme-muted: #5d6760;
    --theme-primary: #1f5e45;
    --theme-primary-hover: #174a35;
    --theme-on-primary: #ffffff;
    --theme-navbar-bg: #0f2f22;
    --theme-navbar-color: #ffffff;
    --theme-navbar-link: #a7e8c8;
    --theme-navbar-muted: #7fa792;
    --theme-card-bg: #ffffff;
    --theme-border: #e2e9e5;
    --theme-input-bg: #ffffff;
    --theme-input-color: #14201a;
    --theme-success-bg: #dff0e8;
    --theme-success-color: #1a6e49;
    --theme-tile-bg: #eef3f0;
    --theme-month-bg: #e3ede8;
    --theme-danger-color: #ba372a;
    --theme-danger-bg: rgba(186, 55, 42, 0.10);
    --theme-warning-color: #7e631c;
    --theme-warning-bg: rgba(126, 99, 28, 0.10);
    --theme-accent: #4cc38a;
    --theme-focus-ring: rgba(76, 195, 138, 0.30);
    --theme-info: #1d63c4;
    --theme-info-bg: rgba(29, 99, 196, 0.10);
    /* Text printed on a progress fill (the percentage in a goal bar):
       each paired with its fill for 4.5:1 -- see .progress-bar below. */
    --theme-on-success: #ffffff;
    --theme-on-accent: #0b241a;
    --theme-on-warning: #ffffff;
    --theme-on-muted: #ffffff;
}

/* "Slate Teal" — the old Flatly default, refreshed onto the shared neutrals.
   Kept for anyone who prefers the cool teal. Key stays "flatly". */
[data-theme="flatly"] {
    --theme-body-bg: #f7faf9;
    --theme-body-color: #1a2529;
    --theme-muted: #5a6a74;
    --theme-primary: #0f6f5c;
    --theme-primary-hover: #0b5849;
    --theme-on-primary: #ffffff;
    --theme-navbar-bg: #243642;
    --theme-navbar-color: #ffffff;
    --theme-navbar-link: #2bb9a0;
    --theme-navbar-muted: #8aa0aa;
    --theme-card-bg: #ffffff;
    --theme-border: #dde6e6;
    --theme-input-bg: #ffffff;
    --theme-input-color: #1a2529;
    --theme-success-bg: #d3f1e9;
    --theme-success-color: #0e6655;
    --theme-tile-bg: #eef3f3;
    --theme-month-bg: #e2eeec;
    --theme-danger-color: #ba372a;
    --theme-danger-bg: rgba(186, 55, 42, 0.10);
    --theme-warning-color: #7e631c;
    --theme-warning-bg: rgba(126, 99, 28, 0.10);
    --theme-accent: #2bb9a0;
    --theme-focus-ring: rgba(43, 185, 160, 0.28);
    --theme-info: #1d63c4;
    --theme-info-bg: rgba(29, 99, 196, 0.10);
    /* Text printed on a progress fill (the percentage in a goal bar):
       each paired with its fill for 4.5:1 -- see .progress-bar below. */
    --theme-on-success: #ffffff;
    --theme-on-accent: #0b241a;
    --theme-on-warning: #ffffff;
    --theme-on-muted: #ffffff;
}

/* "Midnight" — the dark theme, warmed toward the brand's green family. Key stays "dark". */
[data-theme="dark"] {
    --theme-body-bg: #1e2429;
    --theme-body-color: #eef2f0;
    --theme-muted: #9aa8a1;
    --theme-primary: #4cc38a;
    --theme-primary-hover: #6ed3a2;
    --theme-on-primary: #0b241a;
    --theme-navbar-bg: #161b1f;
    --theme-navbar-color: #f4f7f5;
    --theme-navbar-link: #5fd6a0;
    --theme-navbar-muted: #8a9a92;
    --theme-card-bg: #262d33;
    --theme-border: #384249;
    --theme-input-bg: #262d33;
    --theme-input-color: #eef2f0;
    --theme-success-bg: #1a3a2c;
    --theme-success-color: #8fe6bd;
    --theme-tile-bg: #262d33;
    --theme-month-bg: #2c3a34;
    --theme-danger-color: #ef9a92;
    --theme-danger-bg: rgba(239, 154, 146, 0.14);
    --theme-warning-color: #ffda6a;
    --theme-warning-bg: rgba(255, 218, 106, 0.12);
    --theme-accent: #5fd6a0;
    --theme-focus-ring: rgba(95, 214, 160, 0.35);
    --theme-info: #7db3ff;
    --theme-info-bg: rgba(125, 179, 255, 0.16);
    /* Text printed on a progress fill (the percentage in a goal bar):
       each paired with its fill for 4.5:1 -- see .progress-bar below. */
    --theme-on-success: #0b241a;
    --theme-on-accent: #0b241a;
    --theme-on-warning: #0b241a;
    --theme-on-muted: #0b241a;
}

/* "Azure" — the blue-on-white alternative, tightened. Key stays "default". */
[data-theme="default"] {
    --theme-body-bg: #ffffff;
    --theme-body-color: #16202b;
    --theme-muted: #5d6777;
    --theme-primary: #1d63c4;
    --theme-primary-hover: #17539f;
    --theme-on-primary: #ffffff;
    --theme-navbar-bg: #f2f5f8;
    --theme-navbar-color: #20303f;
    --theme-navbar-link: #1d63c4;
    --theme-navbar-muted: #5f6b79;
    --theme-card-bg: #ffffff;
    --theme-border: #e3e8ee;
    --theme-input-bg: #ffffff;
    --theme-input-color: #16202b;
    --theme-success-bg: #e2edfb;
    --theme-success-color: #155fb8;
    --theme-tile-bg: #f2f5f8;
    --theme-month-bg: #e8eff9;
    --theme-danger-color: #ba372a;
    --theme-danger-bg: rgba(186, 55, 42, 0.10);
    --theme-warning-color: #8a6516;
    --theme-warning-bg: rgba(138, 101, 22, 0.10);
    --theme-accent: #2a70c9;
    --theme-focus-ring: rgba(42, 112, 201, 0.28);
    --theme-info: #1d63c4;
    --theme-info-bg: rgba(29, 99, 196, 0.10);
    /* Text printed on a progress fill (the percentage in a goal bar):
       each paired with its fill for 4.5:1 -- see .progress-bar below. */
    --theme-on-success: #ffffff;
    --theme-on-accent: #ffffff;
    --theme-on-warning: #ffffff;
    --theme-on-muted: #ffffff;
}

/* ---- Type scale utilities ---- */

.fs-2xs { font-size: var(--fs-2xs); }
.fs-xs { font-size: var(--fs-xs); }
.fs-sm { font-size: var(--fs-sm); }
.fs-base { font-size: var(--fs-base); }
.fs-lg { font-size: var(--fs-lg); }
.fs-xl { font-size: var(--fs-xl); }
.fs-2xl { font-size: var(--fs-2xl); }
.fs-3xl { font-size: var(--fs-3xl); }

/* ---- Status text ---- */

.text-status-success { color: var(--theme-success-color); }
.text-status-warning { color: var(--theme-warning-color); }
.text-status-danger { color: var(--theme-danger-color); }

/* ---- Public pages (home, privacy policy, sign in / sign up) ---- */

body.public-page {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}

body.public-page .public-main {
    flex: 1 0 auto;
    display: flex;
    flex-direction: column;
}

/* A checkbox in a form row of labelled inputs (_checkbox_field.html): drop
   it by one label's height plus half the difference between an input and a
   line of text, so the box sits on the inputs' centre line rather than at
   the foot of the row beside their help text. */
.form-check-cell { padding-top: calc(var(--sp-24) + var(--sp-6)); }

/* The public reading pages -- About, Security, Pricing, Terms, Privacy,
   How the projections work: one column of prose at a readable measure.
   Each page used to set this column inline and repeat the same <style>
   block; the rules live here now so the six can't drift apart. */
.pp-content {
    max-width: 720px;
    padding-top: var(--sp-48);
    padding-bottom: var(--sp-64);
}
.pp-content .section-heading {
    font-size: var(--fs-2xl);
    font-weight: 700;
    color: var(--brand-dark);
}
.pp-content h2 {
    font-size: var(--fs-lg);
    font-weight: 600;
    margin-top: var(--sp-32);
    margin-bottom: var(--sp-8);
    color: var(--brand-dark);
}
.pp-content h3 {
    font-size: var(--fs-base);
    font-weight: 600;
    margin-top: var(--sp-20);
    margin-bottom: var(--sp-6);
    color: var(--brand-dark);
}
.pp-content p,
.pp-content li { color: var(--brand-text); line-height: 1.7; }
.pp-lead { font-size: var(--fs-lg); color: var(--brand-text-strong); line-height: 1.6; }
.effective-date { font-size: var(--fs-xs); color: var(--theme-muted); }
.pp-callout {
    border-left: 3px solid var(--theme-warning-color);
    /* A green-tinted surface a step off the page's own #f6f8f7. */
    background: var(--brand-surface);
    padding: var(--sp-16);
    border-radius: var(--radius-sm);
    margin: var(--sp-24) 0;
}
.pp-callout p:last-child { margin-bottom: 0; }

/* The comparison pages (/compare/): a three-column table of topic, them, us,
   and the sign-up button. In here rather than a <style> block for the same
   reason as .pp-content. The table scrolls sideways inside its wrapper at
   phone width rather than pushing the page wider. */
.pp-compare-wrap { overflow-x: auto; margin: var(--sp-24) 0; }
.pp-compare {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--fs-sm);
    color: var(--brand-text);
}
.pp-compare th,
.pp-compare td {
    text-align: left;
    vertical-align: top;
    padding: var(--sp-8) var(--sp-12);
    border-bottom: 1px solid var(--brand-tint);
    line-height: 1.5;
}
.pp-compare thead th {
    color: var(--brand-dark);
    border-bottom: 2px solid var(--brand-primary);
}
.pp-compare tbody th { color: var(--brand-dark); font-weight: 600; }
/* Our own column, tinted so the eye can follow it down. */
.pp-compare .pp-us { background: var(--brand-surface); }
.pp-compare-checked { font-size: var(--fs-xs); color: var(--brand-text); }
.pp-cta {
    background-color: var(--brand-primary);
    border-color: var(--brand-primary);
    color: #fff;
    font-weight: 600;
    padding: var(--sp-12) var(--sp-32);
    border-radius: var(--radius-sm);
}
.pp-cta:hover,
.pp-cta:focus {
    background-color: var(--brand-primary-hover);
    border-color: var(--brand-primary-hover);
    color: #fff;
}

.public-footer {
    background: var(--brand-dark);
    color: var(--brand-muted);
    font-size: var(--fs-xs);
    padding: var(--sp-24) 0;
    text-align: center;
}

.public-footer a { color: var(--brand-light); text-decoration: none; }
.public-footer a:hover { text-decoration: underline; }

.public-nav { background: var(--brand-deep); }

.public-nav .navbar-brand {
    color: var(--brand-tint);
    font-weight: 700;
}

.public-nav .btn-nav-login {
    color: var(--brand-tint);
    border: 1px solid var(--brand-accent);
    border-radius: var(--radius-sm);
    padding: var(--sp-6) var(--sp-16);
}

.public-nav .btn-nav-login:hover {
    background-color: rgba(255,255,255,0.1);
    color: #fff;
}

.public-nav .btn-nav-signup {
    background: var(--brand-accent);
    color: var(--brand-deep);
    font-weight: 700;
    border-radius: var(--radius-sm);
    padding: var(--sp-6) var(--sp-16);
}

.public-nav .btn-nav-signup:hover {
    background: var(--brand-accent-hover);
}

.public-nav .nav-link { color: var(--brand-light); }
.public-nav .nav-link:hover { color: #fff; }

/* ---- Entrance screens (sign in, sign up, one-time code) ---- */

.entrance {
    flex: 1 0 auto;
    background: var(--brand-surface-alt);
    padding: var(--sp-64) var(--sp-16) var(--sp-64);
}

.entrance-card {
    max-width: 26rem;
    margin: 0 auto;
    background: #fff;
    border: 1px solid var(--brand-surface);
    border-radius: var(--radius-lg);
    padding: var(--sp-40) var(--sp-32);
    box-shadow: var(--shadow);
}

.entrance-card h1 {
    font-family: var(--font-display);
    font-size: var(--fs-xl);
    font-weight: 500;
    color: var(--brand-dark);
    margin-bottom: var(--sp-12);
}

.entrance-card p { font-size: var(--fs-sm); color: var(--theme-muted); }

.entrance-card a { color: var(--brand-primary); font-weight: 600; }
.entrance-card a:hover { color: var(--brand-deep); }

/* Abandoning a half-finished sign-in clears session state, so "start over"
   has to be a POST -- a <button> that has to read as one of the links above
   it rather than as Bootstrap's blue btn-link. */
.entrance-card .btn-inline-link {
    padding: 0;
    border: 0;
    background: none;
    vertical-align: baseline;
    color: var(--brand-primary);
    font-weight: 600;
}
.entrance-card .btn-inline-link:hover {
    color: var(--brand-deep);
    text-decoration: underline;
}

.entrance-card hr {
    border-top-color: var(--brand-surface);
    margin: var(--sp-24) 0;
}

.entrance-card .btn-primary {
    background-color: var(--brand-primary);
    border-color: var(--brand-primary);
    color: #fff;
    font-weight: 600;
    padding: var(--sp-10) var(--sp-16);
    border-radius: var(--radius-sm);
}

.entrance-card .btn-primary:hover {
    background-color: var(--brand-primary-hover);
    border-color: var(--brand-primary-hover);
}

.entrance-card .btn-outline-primary {
    color: var(--brand-primary);
    border-color: var(--brand-accent);
    font-weight: 600;
    padding: var(--sp-10) var(--sp-16);
    border-radius: var(--radius-sm);
}

.entrance-card .btn-outline-primary:hover {
    background-color: var(--brand-surface);
    color: var(--brand-deep);
    border-color: var(--brand-accent);
}

.entrance-card .btn:focus,
.entrance-card input:focus {
    outline: none;
    box-shadow: 0 0 0 0.2rem var(--brand-success-bg);
}

.entrance-card form p { margin-bottom: var(--sp-16); }

.entrance-card form label {
    display: block;
    margin-bottom: var(--sp-6);
    font-size: var(--fs-xs);
    font-weight: 600;
    color: var(--brand-dark);
}

.entrance-card form input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]) {
    display: block;
    width: 100%;
    padding: var(--sp-8) var(--sp-12);
    font-size: var(--fs-base);
    line-height: 1.5;
    color: var(--brand-dark);
    background-color: #fff;
    border: 1px solid #cdd9d2;
    border-radius: var(--radius-sm);
}

/* Signup's Terms checkbox. It opts out of `.entrance-card form label` above,
   which is built for field labels: block, bold, small-caps-ish. This label is
   a sentence with links in it, so it sits beside the box at body weight and
   stays readable. */
.consent-row {
    display: flex;
    align-items: flex-start;
    gap: var(--sp-10);
    margin-bottom: var(--sp-16);
}

.consent-row input[type="checkbox"] {
    flex: none;
    /* Nudged down onto the first line's baseline rather than its box top. */
    margin-top: var(--sp-4);
    width: 1rem;
    height: 1rem;
}

.entrance-card .consent-row label {
    display: inline;
    margin-bottom: 0;
    font-size: var(--fs-xs);
    font-weight: 400;
    line-height: 1.5;
    color: var(--theme-muted);
}

.entrance-card .errorlist {
    color: var(--theme-danger-color);
    list-style: none;
    padding-left: 0;
    font-size: var(--fs-xs);
    margin-bottom: var(--sp-4);
}

.entrance-card .helptext {
    display: block;
    color: var(--theme-muted);
    font-size: var(--fs-2xs);
    margin-top: var(--sp-4);
}

.entrance-note {
    max-width: 26rem;
    margin: var(--sp-20) auto 0;
    text-align: center;
    font-size: var(--fs-xs);
    color: var(--theme-muted);
}

/* ---- Mappings onto Bootstrap ---- */

body {
    background-color: var(--theme-body-bg);
    color: var(--theme-body-color);
}

.text-muted { color: var(--theme-muted) !important; }

.navbar.themed { background-color: var(--theme-navbar-bg); }
.navbar.themed .navbar-brand { color: var(--theme-navbar-color); }
.navbar.themed .nav-link { color: var(--theme-navbar-link); }
.navbar.themed .navbar-text { color: var(--theme-navbar-muted); }

.btn-primary {
    background-color: var(--theme-primary);
    border-color: var(--theme-primary);
    color: var(--theme-on-primary);
    font-weight: 600;
    border-radius: var(--radius-sm);
}

.btn-primary:hover,
.btn-primary:focus-visible,
.btn-primary:not(:disabled):active {
    background-color: var(--theme-primary-hover);
    border-color: var(--theme-primary-hover);
    color: var(--theme-on-primary);
}

/*
 * The outline variants, which Bootstrap paints from its own palette and not the
 * theme: #007bff for primary, #dc3545 for danger. Left alone, the Edit button on
 * every account card, the CTA at the foot of all 32 Learn lessons and every
 * Delete in the app rendered stock Bootstrap blue and red on an otherwise green
 * screen -- and the blue label measured 3.98:1 on the three light themes and
 * 3.51:1 on Midnight, under AA either way.
 *
 * `:not(:disabled):active` rather than plain `:active`, to match .btn-primary
 * above: a disabled button must not paint itself as pressed.
 *
 * .entrance-card keeps its own .btn-outline-primary rule further up. Those
 * screens draw from the fixed --brand-* palette on purpose, so a visitor's
 * theme cookie can't repaint the page that introduces the product -- these
 * rules are less specific and don't reach them.
 */
.btn-outline-primary {
    color: var(--theme-primary);
    border-color: var(--theme-primary);
    border-radius: var(--radius-sm);
}

.btn-outline-primary:hover,
.btn-outline-primary:focus-visible,
.btn-outline-primary:not(:disabled):active {
    background-color: var(--theme-primary);
    border-color: var(--theme-primary);
    color: var(--theme-on-primary);
}

.btn-outline-danger {
    color: var(--theme-danger-color);
    border-color: var(--theme-danger-color);
    border-radius: var(--radius-sm);
}

.btn-outline-danger:hover,
.btn-outline-danger:focus-visible,
.btn-outline-danger:not(:disabled):active {
    background-color: var(--theme-danger-color);
    border-color: var(--theme-danger-color);
    /* Not --theme-on-primary: that is white on the light themes but #0b241a on
       Midnight, which would be near-black text on the danger fill. The fill is
       dark enough for white in all four. */
    color: #fff;
}

/*
 * The disabled states, which Bootstrap paints from its palette at a higher
 * specificity (`.btn-outline-primary.disabled` is two classes) than the rules
 * above, so a disabled Edit came back #007bff blue on a green screen -- the
 * one state the outline mapping had missed, and the one every button on a
 * read-only household's screens is in. Same tokens, no fill, and Bootstrap's
 * own .65 opacity does the fading.
 */
.btn-primary.disabled,
.btn-primary:disabled {
    background-color: var(--theme-primary);
    border-color: var(--theme-primary);
    color: var(--theme-on-primary);
}

.btn-outline-primary.disabled,
.btn-outline-primary:disabled {
    color: var(--theme-primary);
    border-color: var(--theme-primary);
    background-color: transparent;
}

.btn-outline-danger.disabled,
.btn-outline-danger:disabled {
    color: var(--theme-danger-color);
    border-color: var(--theme-danger-color);
    background-color: transparent;
}

.btn-danger {
    background-color: var(--theme-danger-color);
    border-color: var(--theme-danger-color);
    color: #fff;
    font-weight: 600;
    border-radius: var(--radius-sm);
}

.btn-danger:hover,
.btn-danger:focus-visible,
.btn-danger:not(:disabled):active {
    background-color: var(--theme-danger-color);
    border-color: var(--theme-danger-color);
    color: #fff;
    filter: brightness(0.92);
}

/* Bootstrap's .btn-link is its fixed #007bff and gets an underline on hover;
   the app's own links are --theme-primary. Two of these exist -- "Back to
   settings" on the passkey list and the household rename pencil -- and both
   should read as the links they are. */
.btn-link {
    color: var(--theme-primary);
    font-weight: 600;
}

.btn-link:hover,
.btn-link:focus {
    color: var(--theme-primary-hover);
}

/* The hover fill above used to be on :focus too, so after a mouse click a
   button kept its hover look for as long as it held focus -- an outline
   Delete or Edit stayed solid red or green until you clicked elsewhere,
   reading as pressed or selected. It is on :focus-visible now: a keyboard
   user still gets the fill (plus the ring below), a click doesn't. */

/* Button focus. Bootstrap gives every variant its own focus glow from its
   own palette -- .btn-primary and .btn-outline-primary a 50% #007bff, so
   the app's green buttons glowed blue on focus in all four themes -- and
   sets `outline: 0` at two-class weight, above the app-wide :focus-visible
   baseline. None of that was ever remapped here. Buttons now show no glow
   on a mouse click and, on keyboard focus, the same 2px ring as every other
   control. The :not() chains match Bootstrap's pressed-and-focused rules
   at equal weight; being later, these win. The dark shells' rules and
   .entrance-card's own are heavier and keep their colours. */
.btn:focus,
.btn.focus,
.btn:not(:disabled):not(.disabled):active:focus,
.btn:not(:disabled):not(.disabled).active:focus,
.show > .btn.dropdown-toggle:focus {
    box-shadow: none;
}
.btn:focus-visible {
    outline: 2px solid var(--theme-primary);
    outline-offset: 2px;
}

.btn-outline-secondary {
    color: var(--theme-body-color);
    border-color: var(--theme-border);
    border-radius: var(--radius-sm);
}

.btn-outline-secondary:hover {
    background-color: var(--theme-border);
    color: var(--theme-body-color);
}

.navbar.themed .btn-outline-secondary {
    color: var(--theme-navbar-color);
    border-color: var(--theme-navbar-muted);
}

.navbar.themed .btn-outline-secondary:hover {
    background-color: var(--theme-navbar-muted);
    color: var(--theme-navbar-bg);
    border-color: var(--theme-navbar-muted);
}

a { color: var(--theme-primary); }
a:hover { color: var(--theme-primary-hover); }

/* A figure in a table that opens its entry for editing. Colour alone
   didn't say so: on Budget > Income the links and the positive changes
   beside them were two near-identical greens, and the non-link Total was
   the same weight as the linked figures. A dotted underline marks it
   without shouting across a column of numbers; hover makes it solid. */
a.figure-link {
    text-decoration: underline dotted;
    text-decoration-color: var(--theme-muted);
    text-underline-offset: 0.2em;
}
a.figure-link:hover,
a.figure-link:focus-visible {
    text-decoration-style: solid;
    text-decoration-color: currentColor;
}

/* -----------------------------------------------------------------------
   Tab strip -- secondary nav inside a section (Balances, Summary).

   A segmented control rather than underlined tabs: "one of these is
   current" is a shape this app already draws, on the dashboard's range
   and basis filters, so borrowing it keeps the section switcher looking
   native. Underlined tabs need a rule running the full width of the
   content to sit on, and with two or three short labels under a short
   heading that rule reads as an orphaned divider.

   Every tab is a link to its own URL, so the current one is marked with
   aria-current in the template and .active here is only the paint.
   -------------------------------------------------------------------- */
.tab-strip {
    display: flex;
    /* Shrink-wrapped: the track is the control, so it should end where
       the tabs end rather than stretching across the content column. */
    width: fit-content;
    max-width: 100%;
    gap: var(--sp-2);
    padding: var(--sp-4);
    margin-bottom: var(--sp-20);
    background-color: var(--theme-tile-bg);
    border: 1px solid var(--theme-border);
    border-radius: var(--radius);
    overflow-x: auto;
    scrollbar-width: none;
}

.tab-strip::-webkit-scrollbar { display: none; }

.tab-strip-item {
    flex: 0 0 auto;
    padding: var(--sp-6) var(--sp-16);
    border-radius: var(--radius-sm);
    font-size: var(--fs-sm);
    font-weight: 600;
    color: var(--theme-muted);
    white-space: nowrap;
}

/* A hover fill light enough to stay clearly below the active pill. Filling
   it with card-bg instead reads as a second raised segment, so on a
   three-tab strip two of them looked selected at once. */
.tab-strip-item:hover,
.tab-strip-item:focus {
    color: var(--theme-body-color);
    background-color: var(--theme-border);
    text-decoration: none;
}

.tab-strip-item.active,
.tab-strip-item.active:hover,
.tab-strip-item.active:focus {
    background-color: var(--theme-primary);
    color: var(--theme-on-primary);
    box-shadow: var(--shadow-sm);
}

/* The active tab is a filled pill, so an outline drawn inside it would be
   invisible; ring it the way the hidden-accounts switch rings itself. */
.tab-strip-item:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--theme-primary-hover);
}

/* Desktop hands the strip a mouse; on a phone it's a thumb target, and
   34px is short for one. There is room to spare -- the widest strip is
   three tabs at 245px of a 375px viewport -- so spend it on height. */
@media (max-width: 767.98px) {
    .tab-strip-item { padding: var(--sp-12) var(--sp-16); }
}

/* Puts a screen's own control (the accounts hidden-toggle) on the same
   band as the section switcher instead of stranding it on a row of its
   own. Wraps on narrow screens, where the two won't fit side by side. */
.tab-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-12);
    margin-bottom: var(--sp-20);
}

.tab-row .tab-strip { margin-bottom: 0; }

.table { color: var(--theme-body-color); }
.table th, .table td { border-color: var(--theme-border); }

/* Bootstrap draws the rule between two <tbody>s at a fixed #dee2e6. It stayed
   dormant while every table had a single body; the budget's section tables
   open one per category, so without this a near-white 2px line runs across
   the card on Midnight. The extra weight is kept -- between two categories is
   exactly where a heavier rule belongs -- only the colour is themed. */
.table tbody + tbody { border-top-color: var(--theme-border); }
.table thead th {
    border-bottom-color: var(--theme-border);
    font-size: var(--fs-2xs);
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--theme-muted);
}

.card {
    background-color: var(--theme-card-bg);
    border-color: var(--theme-border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
}

.card-header {
    background-color: var(--theme-tile-bg);
    color: var(--theme-body-color);
    border-bottom-color: var(--theme-border);
    border-top-left-radius: var(--radius);
    border-top-right-radius: var(--radius);
}

.list-group-item {
    background-color: var(--theme-card-bg);
    color: var(--theme-body-color);
    border-color: var(--theme-border);
}

.badge-secondary {
    background-color: var(--theme-border);
    color: var(--theme-body-color);
}

.badge-primary {
    background-color: var(--theme-primary);
    color: var(--theme-on-primary);
}

/*
 * Warning text on the warning tint measures 4.32:1 in Evergreen and Slate Teal
 * -- under AA for badge type, which is small and bold rather than large. The
 * tint carries the warning; the body colour carries the contrast, which is the
 * same split .badge-secondary above already uses.
 */
.badge-warning {
    background-color: var(--theme-warning-bg);
    color: var(--theme-body-color);
}

/*
 * Progress bars, which Bootstrap paints from its own palette and not the theme:
 * the track #e9ecef, the fills $green / $cyan / $yellow. Left alone, a goal bar
 * rendered stock cyan on an otherwise entirely green screen, and the grey track
 * stayed light under Midnight. The .bg-* utilities ship !important, so these
 * have to carry it too.
 *
 * The three fills mean reached / on track / behind (see goal_list.html and
 * protection/overview.html), so they need to stay tellable apart in every
 * theme -- hence success, accent and warning rather than three weights of one
 * hue, which collapses to a single colour under Midnight.
 */
.progress { background-color: var(--theme-border); }
/* No bg-* class means goal_progress couldn't judge pace yet (on_track is
   None) -- a neutral, not a colour that reads as one of the three verdicts. */
/* Each fill carries its own text colour. Bootstrap prints the bar's label
   ("65%") in white on all of them, which on the light mint "on track" fill
   read 2.22:1 on Evergreen and 1.81:1 on Midnight -- and on Midnight the
   success and warning fills are light too. --theme-on-* pairs each fill with
   ink or white for 4.5:1 in every theme (ProgressLabelContrastTests). */
.progress-bar { background-color: var(--theme-muted); color: var(--theme-on-muted); }
.progress-bar.bg-success { background-color: var(--theme-success-color) !important; color: var(--theme-on-success); }
.progress-bar.bg-info { background-color: var(--theme-accent) !important; color: var(--theme-on-accent); }
.progress-bar.bg-warning { background-color: var(--theme-warning-color) !important; color: var(--theme-on-warning); }
/* A goal's projection, drawn after today's fill: the same verdict hue, hatched
   over the track, so where the money is heading reads as a continuation of the
   bar but never as more money you already have. It carries no text (its
   number is in the "Projected" line), which keeps it out of the label-contrast
   pairs above. .progress-key-projected is the same hatch as an inline swatch
   beside that line. Two classes and !important to outrank the .bg-* fills. */
.progress-bar.progress-bar-projected,
.progress-key-projected {
    --projected-fill: var(--theme-muted);
    background-color: transparent !important;
    background-image: repeating-linear-gradient(
        -45deg,
        var(--projected-fill) 0 2px,
        transparent 2px 5px
    );
    box-shadow: inset 1px 0 0 var(--theme-card-bg);
}
.progress-bar-projected.bg-info { --projected-fill: var(--theme-accent); }
.progress-bar-projected.bg-warning { --projected-fill: var(--theme-warning-color); }
.progress-key-projected.progress-bar-projected {
    display: inline-block;
    width: 1.25em;
    height: 0.75em;
    vertical-align: -0.05em;
    margin-right: 0.25em;
    border-radius: 2px;
    background-color: var(--theme-border) !important;
    box-shadow: none;
}

.form-control,
.custom-select {
    background-color: var(--theme-input-bg);
    color: var(--theme-input-color);
    border-color: var(--theme-border);
    border-radius: var(--radius-sm);
}

.form-control:focus,
.custom-select:focus {
    background-color: var(--theme-input-bg);
    color: var(--theme-input-color);
    border-color: var(--theme-primary);
    box-shadow: 0 0 0 0.2rem var(--theme-focus-ring);
}

/*
 * The dropdown arrow, drawn rather than fetched.
 *
 * Bootstrap ships it as an inline `url("data:image/svg+xml,...")`, which the
 * CSP refuses to load -- img-src is 'self', and a data: URI is not 'self'.
 * Every <select> in the app therefore rendered with no arrow at all: the
 * status control on Protect, the basis control on Goals, the account type on
 * the account form, the fiscal year end on Summary.
 *
 * Two gradients make the same chevron out of two half-filled squares. There
 * is no URL involved, so the fix costs nothing at the policy end -- and the
 * arrow takes its colour from the theme, which the original could not:
 * Bootstrap's is a fixed #343a40, so on Midnight's #262d33 input it would
 * have been dark grey on dark grey even if the browser had been allowed to
 * load it.
 *
 * Don't "restore" the Bootstrap image here. It is blocked, and loosening
 * img-src to data: to bring it back would trade a real policy for an arrow
 * that is then invisible in one of the four themes.
 */
.custom-select {
    background-image:
        linear-gradient(45deg, transparent 50%, var(--theme-muted) 50%),
        linear-gradient(135deg, var(--theme-muted) 50%, transparent 50%);
    background-position:
        calc(100% - 15px) calc(50% - 2px),
        calc(100% - 10px) calc(50% - 2px);
    background-size: 5px 5px, 5px 5px;
    background-repeat: no-repeat, no-repeat;
}

/*
 * Bootstrap's validation icon is the app's other data: image, and the same
 * policy blocks it. Nothing is lost by dropping it: the red border and the
 * .invalid-feedback message beside the field both still render, and an icon
 * inside the box is not how the rest of these forms report an error.
 */
.form-control.is-invalid,
.form-control.is-valid {
    background-image: none;
    padding-right: var(--sp-12);
}

/* ---- Compact form rows ---- */

/*
 * A field cell is label / control / help text stacked in that order, and the
 * help text runs to different lengths from field to field -- so the cells in
 * one row are different heights. Bottom-aligning the row (what these forms
 * used to do) therefore lines up the *help text* and staggers the controls
 * into a staircase. Top-align instead, and reserve one line for the label,
 * and the controls sit on a straight line however much hangs below them.
 */
.field {
    display: flex;
    flex-direction: column;
}

.field > label { min-height: 1.15rem; }

/*
 * The submit button of an add-form shares the row with the fields but has no
 * label of its own, so it needs the same reserved line to land on the control
 * row rather than above it. Only from `md` up: narrower than that the row
 * stacks and the button is a full-width row of its own.
 */
@media (min-width: 768px) {
    .field-action::before {
        content: "";
        display: block;
        min-height: 1.15rem;
        margin-bottom: var(--sp-4);
    }
}

.alert-success {
    background-color: var(--theme-success-bg);
    border-color: var(--theme-success-bg);
    color: var(--theme-success-color);
    border-radius: var(--radius);
}

.alert-warning {
    background-color: var(--theme-warning-bg);
    border-color: var(--theme-warning-bg);
    color: var(--theme-warning-color);
    border-radius: var(--radius);
}

.alert-danger,
.alert-error {
    background-color: var(--theme-danger-bg);
    border-color: var(--theme-danger-bg);
    color: var(--theme-danger-color);
    border-radius: var(--radius);
}

.alert-info {
    background-color: var(--theme-tile-bg);
    border-color: var(--theme-border);
    color: var(--theme-body-color);
    border-radius: var(--radius);
}

/* Bootstrap's own .alert-link rule hardcodes a darkened shade of its default
   $danger/$warning/etc at build time, not a theme variable -- on Midnight
   that lands as near-black text on the alert's already-dark background
   (rgb(73, 18, 23) on the translucent danger fill), unreadable. Tie each
   link back to its alert's own (theme-aware) text color instead. */
.alert-success .alert-link { color: var(--theme-success-color); }
.alert-warning .alert-link { color: var(--theme-warning-color); }
.alert-danger .alert-link,
.alert-error .alert-link { color: var(--theme-danger-color); }
.alert-info .alert-link { color: var(--theme-body-color); }

pre {
    color: var(--theme-body-color);
    background-color: var(--theme-card-bg);
    border: 1px solid var(--theme-border);
    padding: var(--sp-12);
    border-radius: var(--radius-sm);
}

hr { border-top-color: var(--theme-border); }

.display-total {
    font-size: 2.1rem;
    font-weight: 700;
    letter-spacing: -0.02em;
}

@media (max-width: 576px) {
    .display-total { font-size: 1.6rem; }
}

/* A row of small labelled figures inside a card -- the dashboard's Budget
   card: the overview's four tiles without the tiles. Label above figure,
   four across, two across on a phone. */
.mini-stats {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--sp-8) var(--sp-16);
    margin: 0;
}

/* Labels may wrap ("Saved & invested" in a 100px column); the figures
   stay on one baseline by sitting at the bottom of each cell. */
.mini-stats > div {
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
}

.mini-stats dt {
    font-size: var(--fs-2xs);
    font-weight: 500;
    line-height: 1.25;
    color: var(--theme-muted);
}

.mini-stats dd {
    margin: 0;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

/* A small table inside a card -- the dashboard's Plan card. No outer
   rules, muted micro-cap headers, tight rows. */
.mini-table { font-size: var(--fs-sm); }
.mini-table th {
    font-size: var(--fs-2xs);
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--theme-muted);
    border-top: 0;
    border-bottom: 1px solid var(--theme-border);
    padding-left: 0;
}
.mini-table td { padding-left: 0; }
.mini-table tr > :first-child { padding-left: 0; }

/* Stat tiles */
.tile-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    gap: var(--sp-12);
}

/* The budget's four summary tiles: auto-fit rather than auto-fill, so the
   four stretch across the row instead of sitting in four of five 164px
   tracks with the fifth empty -- at 164px "Saved & invested / mo" wrapped
   and made the row taller. The dashboard's type tiles keep auto-fill so a
   short liabilities row stays on the same grid as the assets above it. */
.tile-grid-fit { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
/* Label over figure, and a label may wrap ("Saved & invested / mo" at
   phone width): the figure sits at the foot of its tile, so a row of
   tiles keeps its figures on one line -- the .mini-stats rule, for tiles. */
.tile-grid-fit > .stat-tile {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
}

.stat-tile {
    display: block;
    background-color: var(--theme-card-bg);
    border: 1px solid var(--theme-border);
    border-radius: var(--radius);
    padding: var(--sp-12) var(--sp-16);
}

/* The dashboard tiles are links into the matching Balances section, but they
   should read as cards, not as body links -- the hover lift is the only
   affordance they need. */
a.stat-tile,
a.stat-tile:hover,
a.stat-tile:focus {
    color: inherit;
    text-decoration: none;
}

a.stat-tile:hover,
a.stat-tile:focus-visible {
    border-color: var(--theme-primary);
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
}

/* The next milestone on the dashboard: outlined rather than filled, so it
   reads as the one still ahead beside the solid tiles already earned. */
.stat-tile-next { border: 2px dotted var(--theme-muted); padding: calc(var(--sp-12) - 1px) calc(var(--sp-16) - 1px); }

/* The dashboard's first-run card: three numbered steps, the number on a
   tile-tinted disc, the finished one ticked in the primary colour. */
.get-started-steps {
    list-style: none;
    padding: 0;
    margin: 0;
    display: grid;
    gap: var(--sp-16);
}

.get-started-steps li {
    display: flex;
    gap: var(--sp-12);
    align-items: flex-start;
}

.get-started-step {
    flex: 0 0 auto;
    width: 1.6rem;
    height: 1.6rem;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: var(--fs-xs);
    font-weight: 700;
    background: var(--theme-tile-bg);
    border: 1px solid var(--theme-border);
    color: var(--theme-body-color);
}

.get-started-steps li.is-done .get-started-step {
    background: var(--theme-primary);
    border-color: var(--theme-primary);
    color: #fff;
}

.get-started-steps li.is-done strong { color: var(--theme-muted); }

@media (min-width: 768px) {
    .get-started-steps { grid-template-columns: repeat(3, 1fr); gap: var(--sp-20); }
}

/* Which side of the ledger a tile is on: a small swatch before the label,
   the chart legend's own grammar, rather than the 3px coloured left edge
   the tiles carried until 2026-09-18 -- a bar on every one of thirteen
   cards was the heaviest mark on the dashboard, saying what the section
   header above them and the minus sign already said. */
.stat-tile-asset .stat-label::before,
.stat-tile-liability .stat-label::before {
    content: "";
    display: inline-block;
    width: 0.5em;
    height: 0.5em;
    border-radius: 50%;
    margin-right: 0.45em;
    vertical-align: 0.05em;
}

.stat-tile-asset .stat-label::before { background-color: var(--theme-accent); }
.stat-tile-liability .stat-label::before { background-color: var(--theme-danger-color); }

/* Arriving from a dashboard tile: don't butt the heading against the
   viewport edge, and mark which section was asked for. */
.account-section { scroll-margin-top: var(--sp-16); }

.account-section:target .account-section-header { color: var(--theme-primary); }

.stat-label { font-size: var(--fs-xs); color: var(--theme-muted); }

.stat-value {
    font-size: var(--fs-lg);
    font-weight: 700;
    color: var(--theme-body-color);
}

/* The assistant's answer: the one block in the app built from a model's
   markdown rather than from a template (assistant/render.py). It used to be a
   single <p class="mb-0">, so these rules have to put back what that class did
   -- no trailing margin inside the card -- now that the answer can be several
   elements. */
.assistant-answer > *:last-child { margin-bottom: 0; }

.assistant-answer p { margin-bottom: var(--sp-10); }

.assistant-answer ul,
.assistant-answer ol {
    margin-bottom: var(--sp-10);
    padding-left: var(--sp-20);
}

.assistant-answer li { margin-bottom: var(--sp-4); }

/* Pages an answer cited from a web search (assistant/_sources.html). Same
   indent as the answer's own lists so the two read as one card. */
.assistant-sources { padding-left: var(--sp-20); }

.assistant-sources li { margin-bottom: var(--sp-4); overflow-wrap: anywhere; }

/* Ask's empty state: questions to try, as chips that fill the box. Hidden
   the moment an exchange lands above them (the Ask form prepends into
   #transcript), so the first answer takes their place. */
.ask-starters { margin-bottom: var(--sp-16); }
#transcript .card ~ .ask-starters { display: none; }

.ask-starters-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-8);
    margin-top: var(--sp-4);
}

/* A failed htmx request (csp_handlers.js): pinned above the bottom edge so
   it is seen from anywhere on a long page, above the add-to-home banner. */
.request-error {
    position: fixed;
    left: var(--sp-12);
    right: var(--sp-12);
    bottom: var(--sp-12);
    max-width: 32rem;
    margin: 0 auto;
    z-index: 1090;
    display: flex;
    align-items: flex-start;
    gap: var(--sp-12);
    box-shadow: var(--shadow);
    /* .alert-danger's background is a 10% tint, which over page content
       shows the content through; the same tint laid on the card colour. */
    background: linear-gradient(var(--theme-danger-bg), var(--theme-danger-bg)), var(--theme-card-bg);
}
.request-error .close { margin-left: auto; line-height: 1; }

/* Household name + rename pencil (households/_name.html). A plain rule, not
   .d-flex: csp_handlers.js hides this with an inline display:none while the
   rename form is open, and .d-flex's !important used to win. */
.household-name-view { display: flex; align-items: center; }

/* "Working it out…" beside the Ask button while a question is in flight
   (chat.html). htmx's own stylesheet fades .htmx-indicator in and out. */
.ask-pending {
    margin-left: var(--sp-8);
    font-size: var(--fs-sm);
    color: var(--theme-muted);
}

.ask-starter {
    font-size: var(--fs-xs);
    line-height: 1.4;
    padding: var(--sp-4) var(--sp-12);
    border-radius: 999px;
    border: 1px solid var(--theme-border);
    background: var(--theme-card-bg);
    color: var(--theme-body-color);
    cursor: pointer;
    text-align: left;
}

.ask-starter:hover,
.ask-starter:focus-visible {
    border-color: var(--theme-primary);
    color: var(--theme-primary);
}

.ask-starter:focus-visible {
    outline: 2px solid var(--theme-primary);
    outline-offset: 1px;
}

/* Tokens rather than the usual hardcoded pale grey: under Midnight the card is
   #262d33, and a #f5f5f5 chip on it is a light box in a dark card. */
.assistant-answer code {
    padding: var(--sp-2) var(--sp-4);
    border: 1px solid var(--theme-border);
    border-radius: 3px;
    background-color: var(--theme-body-bg);
    color: var(--theme-body-color);
    font-size: 0.9em;
}

.account-section-header {
    display: flex;
    align-items: center;
    gap: var(--sp-12);
    margin: var(--sp-8) 0 var(--sp-16);
    font-size: var(--fs-2xs);
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--theme-muted);
}

.account-section-header::after {
    content: "";
    flex: 1;
    height: 1px;
    background: var(--theme-border);
}

/* No opacity here, deliberately. This span carries the section's total --
   "$14,700/mo · 64.1% of gross" on Budget -- at 11.5px, and --theme-muted is
   already picked to sit just clear of AA on its background. Fading it a
   further 0.75 measured 2.99:1 on Evergreen, 3.20 on Slate Teal, 3.07 on
   Default and 4.21 on Midnight: the most important number in the band was the
   only text on the screen failing contrast. The weight and letter-spacing
   below are what separate the count from the label; they cost no contrast. */
.account-section-header .account-section-count {
    letter-spacing: 0;
    font-weight: 600;
}

/* The group's subtotal on the Accounts screen sits at the far end of the
   rule, ledger-style, so the eye reads "Cash 2 ........ $113,581" -- the
   ::after line is the flex filler, so ordering the total after it is what
   puts the figure on the right. */
.account-section-header .account-section-total { order: 1; }

.balance-group-header {
    display: flex;
    align-items: center;
    gap: var(--sp-12);
    padding-top: var(--sp-12);
    padding-bottom: var(--sp-6);
    font-size: var(--fs-2xs);
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--theme-muted);
    background: var(--theme-tile-bg);
    border-top: 1px solid var(--theme-border);
}

.balance-group-header::after {
    content: "";
    flex: 1;
    height: 1px;
    background: var(--theme-border);
}

/* The flex band above is for the <div> form of the header (the month card on
   Balances). The budget's tables put the same class on the <th> cells of a
   group row, and there flex is actively wrong: it overrides display:
   table-cell, so the browser wraps the cells in one anonymous cell, every
   colspan stops applying, and the three cells stack vertically inside the
   first column -- which put the group subtotal under "Line" instead of under
   "Per month", the column it totals. Give the cells their display back and
   drop the trailing hairline, which only has a rule to fill in flex. */
tr > .balance-group-header {
    display: table-cell;
    vertical-align: middle;
}

tr > .balance-group-header::after { content: none; }

/* The subtotal beside a category subheading is a figure, not a label, and the
   uppercase --fs-2xs label treatment made it the smallest type in the table
   while it summed the largest column. Body ink at 0.95rem, unshouted, in the
   same column as the rows it adds up. The band, the weight and the rule above
   it still come from .balance-group-header. */
tr > .balance-group-total {
    font-size: var(--fs-sm);
    letter-spacing: 0;
    text-transform: none;
    color: var(--theme-body-color);
}

/* ---------------------------- Budget line tables -------------------------
 *
 * Three sections, three <table>s, one grid. Auto layout gave each its own
 * column widths, so "Per month" moved horizontally as you scrolled down the
 * page and the eye had to re-find it in every section. The widths below are
 * fixed and the columns come from budget/_line_columns.html, which all three
 * include.
 *
 * Only from 48em up. There the container is 690px or wider, which leaves the
 * name column 350px after the numbers have taken theirs. Narrower than that,
 * fixed widths would crush the names instead of scrolling, so the tables keep
 * Bootstrap's auto layout and .table-responsive's sideways scroll.
 */
/* On a phone the six columns left the name a third of 375px -- "Mortgage -
   First Bank (plus insurance)" on four lines -- with Pause off the right
   edge behind a sideways scroll. "% gross" and "Day" are the two the
   overview can do without there: the percentage is on the section strip
   and the total row, the day on Budget > Schedule. The <col> widths above
   48em are untouched; below it the table is auto layout anyway. */
@media (max-width: 35.99em) {
    .budget-table .budget-cell-pct,
    .budget-table .budget-cell-day { display: none; }
    /* The dashboard's history table: five columns at 375px put the
       asset-backed change -- the headline's own -- behind a sideways
       scroll. The cash-based pair goes; the headline basis stays. */
    .history-cell-cash { display: none; }
    /* Balances > Events: the net-worth column goes on a phone, leaving
       month-and-event, change and Delete; the level is on the dashboard. */
    .events-cell-worth { display: none; }
    /* Summary: income, the cash pair and investable growth go on a
       phone, leaving year, net worth, change and the headline rate. */
    .summary-cell-phone-drop { display: none; }
    /* Budget > Income: the earners keep who, gross / yr, take-home / mo
       and the row actions; the history keeps year, total and change, with
       the earner columns folded under the total. Both ran off the right
       edge -- the history by 356px. */
    .income-cell-phone-drop { display: none; }

    /* The dashboard's type tiles: thirteen of them two-up were 1,100px
       of the first phone screens, before the chart. One per row as a
       label-and-figure line, ~40px each, reads as the list it is. The
       milestones grid keeps its tiles -- they carry three lines each. */
    /* The dashboard's Budget card: its four figures go two across. */
    .mini-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .tile-grid-types { grid-template-columns: 1fr; gap: var(--sp-6); }
    .tile-grid-types .stat-tile {
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        gap: var(--sp-12);
        padding: var(--sp-8) var(--sp-12);
    }
    .tile-grid-types .stat-label { margin-bottom: 0 !important; }
    .tile-grid-types .stat-value { font-size: var(--fs-base); white-space: nowrap; }
    /* Milestone tiles are ~140px wide here; "$1,000,000" at full size broke
       mid-figure ("$1,000,00 / 0"). Keep a figure on one line. */
    .tile-grid .stat-value { white-space: nowrap; font-size: var(--fs-base); }
    .tile-grid .stat-tile { padding-left: var(--sp-12); padding-right: var(--sp-12); }
}

@media (min-width: 48em) {
    .budget-table { table-layout: fixed; }
    .budget-table .budget-col-billed { width: 8.5rem; }
    .budget-table .budget-col-money { width: 6rem; }
    .budget-table .budget-col-pct { width: 4.5rem; }
    .budget-table .budget-col-day { width: 3.5rem; }
    .budget-table .budget-col-actions { width: 7rem; }

    /* A sticky header needs a scrollport that actually scrolls vertically, and
       .table-responsive is one: overflow-x: auto computes overflow-y to auto
       too, so `top: 0` would pin to the top of a box that never moves and the
       header would never stick to anything. Above 48em the fixed grid above
       guarantees the table fits, so the wrapper has nothing to scroll and can
       go back to overflowing visibly -- which makes the page the scrollport
       and the header sticky against the viewport. */
    .budget-lines .table-responsive { overflow: visible; }

    /* The Expenses table is 1,368px tall. Two thirds of the way down, a row
       reads "$125 · 0.5% · 26" with nothing on screen saying which is which.
       The background is not optional: rows scroll *under* this, and a
       transparent sticky header shows them through itself. Nor is the shadow
       -- with border-collapse the bottom border belongs to the table grid and
       scrolls away with it, so the rule has to be painted on the cell. */
    .budget-table thead th {
        position: sticky;
        top: 0;
        z-index: 2;
        background-color: var(--theme-card-bg);
        box-shadow: inset 0 -2px 0 var(--theme-border);
    }
}

/* The section total, in the column it totals. Body size against the thead's
   0.72rem and the strip's 11.5px, because it is the answer and they are
   labels. */
.budget-total-row th,
.budget-total-row td {
    border-top: 2px solid var(--theme-border);
    font-weight: 600;
}

/* The label is a label; the figure beside it is the point. */
.budget-total-row th { color: var(--theme-muted); }

.balance-group-header .account-section-count {
    letter-spacing: 0;
    font-weight: 600;
}

.card-body .balance-group-header:first-child { border-top: 0; }

.accounts-container.hide-inactive .account-inactive,
#accounts-container.hide-inactive .account-inactive { display: none; }

/* Enter-balances wraps its switch in a GET form so it works without
   scripting; the form must not become a block in the .tab-row flex line. */
.hidden-toggle-form {
    display: flex;
    align-items: center;
    margin-bottom: 0;
}

.hidden-toggle {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-8);
    font-size: var(--fs-xs);
    color: var(--theme-body-color);
    cursor: pointer;
    user-select: none;
}

.switch {
    position: relative;
    display: inline-block;
    width: 34px;
    height: 18px;
    flex: none;
}

.switch input {
    position: absolute;
    opacity: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    cursor: pointer;
    z-index: 1;
}

.switch-slider {
    position: absolute;
    inset: 0;
    background: var(--theme-border);
    border-radius: 999px;
    transition: background 0.15s ease;
}

.switch-slider::before {
    content: "";
    position: absolute;
    top: 2px;
    left: 2px;
    width: 14px;
    height: 14px;
    background: #fff;
    border-radius: 50%;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
    transition: transform 0.15s ease;
}

.switch input:checked + .switch-slider { background: var(--theme-primary); }
.switch input:checked + .switch-slider::before { transform: translateX(16px); }
.switch input:focus-visible + .switch-slider { box-shadow: 0 0 0 2px var(--theme-primary-hover); }

.hidden-toggle-count {
    font-size: var(--fs-2xs);
    padding: var(--sp-2) var(--sp-6);
    border-radius: 999px;
    border: 1px solid var(--theme-border);
    color: var(--theme-muted);
}

.font-weight-500 { font-weight: 500; }

.change-badge {
    font-size: var(--fs-2xs);
    font-weight: 600;
    border-radius: 999px;
    padding: 0.25em 0.7em;
    vertical-align: middle;
}

/* The dashboard's change badges are also buttons when there is a list of
   movers to open -- the Summary screen's gesture, brought to the headline.
   The two movers sentences used to sit under the figure in full; folded
   behind their badges they cost nothing until asked for. A <button> keeps
   the badge's shape and gets a chevron that turns on aria-expanded. */
button.change-badge {
    border: 0;
    cursor: pointer;
    font-family: inherit;
    line-height: inherit;
}

button.change-badge .disclosure-chevron {
    display: inline-block;
    vertical-align: middle;
    margin-left: 0.25em;
}

button.change-badge:hover { filter: brightness(0.94); }

button.change-badge:focus-visible {
    outline: 2px solid var(--theme-primary);
    outline-offset: 2px;
}

button.change-badge[aria-expanded="true"] .disclosure-chevron { transform: rotate(0); }

/* One movers list, opened from its badge: the window it covers as a
   muted lead, then the names wrapping as on the Summary screen. */
.headline-movers {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2) var(--sp-10);
    margin-top: var(--sp-6);
}

/* The figure's context in one line -- cash-based, the rest, the as-of
   month -- as facts spaced apart rather than a sentence with separators,
   so a phone gets one per line and nothing leads with a stray dot. */
.headline-meta {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2) var(--sp-20);
    margin-top: var(--sp-6);
}

/* A task's state on Protect, as a pill. Colour is spent on what the state
   asks of you: done is green, due is amber, in progress is the info tint,
   and "not started" is neutral -- it's most of a fresh checklist, and in
   red it left the colour nothing to say when a figure was actually short. */
.state-pill {
    display: inline-block;
    font-size: var(--fs-2xs);
    font-weight: 600;
    line-height: 1.5;
    border-radius: 999px;
    padding: 0.1em 0.7em;
    border: 1px solid transparent;
}

.state-pill-done { background-color: var(--theme-success-bg); color: var(--theme-success-color); }
.state-pill-due { background-color: var(--theme-warning-bg); color: var(--theme-warning-color); }
.state-pill-progress { background-color: var(--theme-info-bg); color: var(--theme-body-color); }
.state-pill-quiet { background-color: var(--theme-card-bg); border-color: var(--theme-border); color: var(--theme-muted); }

/* Protect's count line as a bar: one block per task, painted like the
   state pills. Gapped rather than a continuous fill so it reads as
   twelve things, not a percentage. */
.checklist-bar {
    display: flex;
    gap: var(--sp-4);
    max-width: 24rem;
    height: 6px;
}

.checklist-bar-sm { height: 4px; max-width: 16rem; }

.checklist-seg {
    flex: 1 1 0;
    border-radius: 2px;
    background-color: var(--theme-border);
}

.checklist-seg-done { background-color: var(--theme-success-color); }
.checklist-seg-due { background-color: var(--theme-warning-color); }
.checklist-seg-progress { background-color: var(--theme-info); }

/* Both directions filled & symmetrical — easier to scan than v1's outlined down-state. */
.change-up {
    background-color: var(--theme-success-bg);
    color: var(--theme-success-color);
}

.change-down {
    background-color: var(--theme-danger-bg);
    color: var(--theme-danger-color);
}

/* A change that rounds to nothing: no direction, so neither colour. The
   change_class filter picks it, so a flat month stops reading as a gain. */
.change-flat {
    background-color: var(--theme-tile-bg);
    color: var(--theme-muted);
}

/* The standing caveat under a page that shows modelled or projected numbers:
   Learn lessons, the planning screens, goals. Quiet, but on the page rather
   than buried in the Terms -- it has to be readable at the moment someone is
   looking at the number it qualifies. Lived in learn.css until planning
   needed it too. */
.disclaimer {
    margin-top: var(--sp-32);
    font-size: var(--fs-xs);
    color: var(--theme-muted);
    border-top: 1px solid var(--theme-border);
    padding-top: var(--sp-16);
}

/* Monte Carlo success meter on scenario cards. */
.likelihood-meter {
    height: 6px;
    border-radius: 999px;
    background-color: var(--theme-border);
    overflow: hidden;
}

.likelihood-meter > span {
    display: block;
    height: 100%;
    border-radius: 999px;
}

.likelihood-success > span { background-color: var(--theme-success-color); }
.likelihood-warning > span { background-color: var(--theme-warning-color); }
.likelihood-danger > span { background-color: var(--theme-danger-color); }

/* The Household screen: a settings list. Each section keeps its .card
   markup (two of them come from partials shared with other screens) and
   loses the box -- heading in a left column, content beside it, a
   hairline between sections. Stacks on a phone. */
.settings-list .card {
    display: grid;
    grid-template-columns: 11rem minmax(0, 1fr);
    column-gap: var(--sp-32);
    align-items: start;
    background: transparent;
    border: 0;
    border-top: 1px solid var(--theme-border);
    border-radius: 0;
    box-shadow: none;
    margin-bottom: 0 !important;
    padding: var(--sp-24) 0;
}

.settings-list .card:first-child { border-top: 0; padding-top: var(--sp-8); }

.settings-list .card-header {
    background: transparent;
    border: 0;
    border-radius: 0;
    padding: 0;
    font-weight: 600;
}

.settings-list .card-body { padding: 0; }
.settings-list .card-body > p:last-child { margin-bottom: 0; }

/* A member or invite list: flush, and its items lose their side padding
   so the text lines up with the paragraphs in the other sections. */
.settings-list .list-group-item {
    background: transparent;
    padding-left: 0;
    padding-right: 0;
    border-left: 0;
    border-right: 0;
}
.settings-list .list-group-item:first-child { border-top: 0; padding-top: 0; }
.settings-list .list-group-item:last-child { border-bottom: 0; padding-bottom: 0; }

@media (max-width: 47.99em) {
    .settings-list .card { grid-template-columns: minmax(0, 1fr); row-gap: var(--sp-8); }
}

/* Account cards */
.card.account-hidden { border-style: dashed; }

/* Balances on a phone: one card to a row, the name and its balance on the
   first line and everything else beneath at full width. Two-up at 158px,
   names like "Schwab Long Term Investments" wrapped to three lines and the
   institution truncated to "Charles Schwab (fm…"; a full-width row gives the
   name room and puts the figure where the eye lands, like the dashboard's
   type tiles at the same width. d-flex and mb-2 are Bootstrap utilities
   with !important, hence the !important here. */
@media (max-width: 575.98px) {
    .account-card-body {
        display: grid !important;
        grid-template-columns: minmax(0, 1fr) auto;
        column-gap: var(--sp-12);
        align-items: baseline;
    }
    /* One card per row costs height, so the lines under the name sit
       closer than the two-up card's mb-2 (a !important utility). */
    .account-card-body > * { grid-column: 1 / -1; margin-bottom: var(--sp-2) !important; }
    .account-card-body > .account-card-meta { margin-bottom: var(--sp-6) !important; }
    .account-card-body > .account-card-name { grid-column: 1; grid-row: 1; }
    .account-card-body > .account-card-value {
        grid-column: 2;
        grid-row: 1;
        text-align: right;
        white-space: nowrap;
    }
}

/* Whose account it is, at the far end of the card's footer. A chip rather
   than a line of the body: small, on the tile tint, and never wrapping. */
.owner-chip {
    flex-shrink: 1;
    min-width: 0;
    align-self: center;
    font-size: var(--fs-2xs);
    font-weight: 600;
    line-height: 1.6;
    padding: 0 0.55em;
    border-radius: 999px;
    background-color: var(--theme-tile-bg);
    border: 1px solid var(--theme-border);
    color: var(--theme-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* A card's controls: a hairline footer of text buttons, not a row of
   outlined buttons. Edit is the least-used thing on a card; it should be
   findable, not prominent. Account cards and goal cards share it. */
.card-actions {
    display: flex;
    align-items: baseline;
    gap: var(--sp-20);
    padding: var(--sp-8) var(--sp-16);
    border-top: 1px solid var(--theme-border);
}

.card-action,
.row-action {
    font-size: var(--fs-xs);
    font-weight: 600;
    color: var(--theme-muted);
    background: none;
    border: 0;
    padding: 0;
    line-height: 1.5;
}

.card-action:hover,
.card-action:focus,
.row-action:hover,
.row-action:focus {
    color: var(--theme-primary);
    text-decoration: none;
}

/* Delete reads as delete even in the quiet footer: the most dangerous
   control on the card must not look like the tamest
   (config/test_ui_conventions.py). */
.card-action-danger,
.row-action-danger { color: var(--theme-danger-color); }

.card-action-danger:hover,
.card-action-danger:focus,
.row-action-danger:hover,
.row-action-danger:focus { color: var(--theme-danger-color); filter: brightness(0.85); }

/* The same text button in a table row (budget lines, income, salaries),
   where the row is the readout and the controls are at its end. Padded a
   little vertically so the target keeps the 24px floor a btn-sm had. */
.row-action { padding: var(--sp-2) 0; vertical-align: baseline; }

/* A row action that only shows for the row you are on. For a long table
   of rarely used controls -- an account's balance history is 200 months,
   each with a Delete -- repeating the red verb on every row makes the one
   thing nobody does the loudest thing on the page. Hidden with opacity
   rather than display so it stays in the tab order and the accessibility
   tree, and revealed by hovering the row, by focus anywhere in the row
   (tabbing into the amount box brings its Delete with it), and by focus
   on the control itself. Only on devices that can hover: a phone has no
   hover, so there it stays visible like any other row action. */
@media (hover: hover) and (pointer: fine) {
    .row-action-reveal { opacity: 0; transition: opacity 0.12s; }
    tr:hover .row-action-reveal,
    tr:focus-within .row-action-reveal,
    .row-action-reveal:focus-visible { opacity: 1; }
}

.card-action:disabled,
.row-action:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

/* Looks unavailable, but still answers a press with the reason (the card's
   Hide on an account that holds money -- _account_row.html). */
.card-action[aria-disabled="true"] { opacity: 0.45; }
/* Opacity dims the focus ring with the text; full strength while focused. */
.card-action[aria-disabled="true"]:focus-visible { opacity: 1; }

/* Two-up on a phone the card is ~155px wide: tighter gaps so "Joint" still
   fits at the end of the footer instead of clipping to "Jo…". */
@media (max-width: 35.99em) {
    .account-col .card-actions { gap: var(--sp-12); padding-left: var(--sp-12); padding-right: var(--sp-12); }
}

.card.account-hidden .font-weight-500,
.card.account-hidden .stat-value {
    color: var(--theme-muted);
    font-weight: 400;
}

.card.account-hidden .stat-label { opacity: 0.8; }

/* The month header on the enter-balances screen. Inside each card the account
   group headers (Cash, Investments, ...) are quiet uppercase micro-caps on the
   tile tint, and the plain .card-header sat on that same tint -- so the two
   read as one texture and the month stopped announcing itself. The month now
   takes a level louder on three axes the group headers deliberately don't use:
   the display face at sentence case, a warmer primary-tinted band, and an
   accent bar beside the name. The bar is a pseudo-element rather than a
   border-left so it can't collide with the card's rounded top corners. */
.month-card-header {
    background-color: var(--theme-month-bg);
    border-bottom-color: var(--theme-border);
}

.month-card-title {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-8);
    font-family: var(--font-display);
    font-size: var(--fs-lg);
    font-weight: 500;
    line-height: 1.3;
    color: var(--theme-body-color);
}

.month-card-title::before {
    content: "";
    width: 3px;
    align-self: stretch;
    min-height: 1.1em;
    border-radius: 2px;
    background-color: var(--theme-primary);
}

/* The month name doubles as the card's collapse toggle, so it has to lose
   the button chrome and keep the title's typography exactly -- the header
   should read as a heading that happens to be clickable, not as a control
   the header grew. */
.month-card-toggle {
    padding: 0;
    background: none;
    border: 0;
    text-align: left;
    cursor: pointer;
}

.month-card-toggle:focus-visible {
    outline: 2px solid var(--theme-primary);
    outline-offset: 3px;
    border-radius: 2px;
}

/* Drawn from borders rather than a background-image, for the same reason
   the <select> chevron above is: img-src is 'self' and a data: URI is not. */
.month-card-chevron {
    width: 0;
    height: 0;
    border-left: 4px solid transparent;
    border-right: 4px solid transparent;
    /* currentColor, so it reads as part of the month name it discloses --
       and so it can never be the one part of the toggle that fails 1.4.11
       in a theme nobody checked it in. */
    border-top: 5px solid currentColor;
    transform: rotate(-90deg);
    transition: transform 0.15s ease;
}

.month-card-toggle[aria-expanded="true"] .month-card-chevron {
    transform: rotate(0);
}

@media (prefers-reduced-motion: reduce) {
    .month-card-chevron { transition: none; }
}

/* A card that opens on click -- the budget's "Add a line" form. The summary
   is the card-header, so a closed card reads as a one-line heading with a
   chevron and an open one as the ordinary card it always was. The marker
   is drawn from borders like the month-card chevron above (img-src is
   'self'; no data: URIs), and rotates on [open] rather than on aria state
   because <details> keeps its own. */
.disclosure-summary {
    display: flex;
    align-items: center;
    gap: var(--sp-8);
    cursor: pointer;
    list-style: none;
}

.disclosure-summary::-webkit-details-marker { display: none; }

.disclosure-summary:focus-visible {
    outline: 2px solid var(--theme-primary);
    outline-offset: -2px;
}

.disclosure-card:not([open]) .disclosure-summary {
    border-bottom: 0;
    border-radius: var(--radius);
}

.disclosure-chevron {
    width: 0;
    height: 0;
    border-left: 4px solid transparent;
    border-right: 4px solid transparent;
    border-top: 5px solid currentColor;
    transform: rotate(-90deg);
    transition: transform 0.15s ease;
}

.disclosure-card[open] .disclosure-chevron { transform: rotate(0); }

/* In a wrapping summary (Protect's task cards) the title claims the line
   beside the chevron and wraps within it; without a basis its natural
   width was the whole title, so on a phone it dropped under the chevron
   and left the arrow alone on a line of its own. The status wraps under
   the title when the two don't fit, which was the intent of flex-wrap. */
.disclosure-summary .disclosure-title {
    flex: 1 1 60%;
    min-width: 0;
}

/* Summary > "what moved": the accounts behind a year's change, in a row of
   their own under the year rather than inside the Change cell. A <details>
   in the cell widened the column when it opened -- account names are wider
   than figures -- and every other column moved to make room: the header
   re-wrapped, Year narrowed, the last column went behind the sideways
   scroll. A row spanning the table adds one line below the year and moves
   nothing else. The toggle is the change badge itself, a bare button with
   a chevron after it -- not a "what moved" line under the figure, which
   doubled every row -- with the chevron turning on aria-expanded since a
   <button> has no [open]. */
.summary-movers-toggle {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-6);
    padding: 0;
    background: none;
    border: 0;
    color: var(--theme-muted);
    font-weight: normal;
    cursor: pointer;
}

.summary-movers-toggle:hover .change-up,
.summary-movers-toggle:hover .change-down,
.summary-movers-toggle:hover .change-flat { filter: brightness(0.94); }

.summary-movers-toggle:focus-visible {
    outline: 2px solid var(--theme-primary);
    outline-offset: 2px;
    border-radius: 2px;
}

.summary-movers-toggle[aria-expanded="true"] .disclosure-chevron { transform: rotate(0); }

/* No rule between the year and its breakdown: they are one entry. */
.table .summary-movers-row td {
    border-top: 0;
    padding-top: 0;
    padding-bottom: var(--sp-8);
}

/* Names wrap as a whole, never mid-name, so a phone gets one account per
   line and a desktop gets them all on one. */
.summary-movers-list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2) var(--sp-16);
}

@media (prefers-reduced-motion: reduce) {
    .disclosure-chevron { transition: none; }
}

/* How much of the month is filled in, next to its name. On a collapsed card
   this is the whole readout, so it says "9 of 14" rather than only flagging
   a gap: a count you have to notice the absence of is not a count. */
.month-card-count {
    /* --theme-muted is only 4.29:1 on the month band in Evergreen, which is
       under AA for text this small, so the count is mixed from the body
       colour instead -- measured 5.7:1 to 6.3:1 across the four themes. The
       muted line is the fallback for anything without color-mix(). */
    color: var(--theme-muted);
    color: color-mix(in srgb, var(--theme-body-color) 72%, var(--theme-month-bg));
    white-space: nowrap;
}

/* "0 of 22 entered · net worth $1,560,218" is 6px wider than a 375px phone;
   let it wrap there rather than push the page sideways. */
@media (max-width: 35.99em) {
    .month-card-count { white-space: normal; }
}

/* The infinite-scroll trigger under the month cards. The zone is a 1px-wide
   strip anchored to the bottom of the "Loading earlier months" line and
   reaching two screens up over the cards above it, so htmx's `intersect`
   fires while the reader is still that far from the end. It extends upward
   only, so it adds no scroll height, and takes no clicks. Two screens is
   relative to the device: on a phone it's ~1,600px, on a desktop ~2,000px. */
.month-scroll-sentinel {
    position: relative;
}

.month-scroll-zone {
    position: absolute;
    bottom: 0;
    left: 0;
    width: 1px;
    height: 200vh;
    pointer-events: none;
}

/* The jump control above the month cards. */
.month-jump {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-8);
}

.month-jump-label {
    color: var(--theme-muted);
    font-size: var(--fs-sm);
}

.month-jump-select { width: auto; }

.month-jump-back { font-size: var(--fs-sm); }

/* Balance entry rows */
.balance-row + .balance-row { border-top: 1px solid var(--theme-border); }

/* "Hide account" under a zeroed account on the enter screen: a quiet
   text button like every other row action, with its eye-slash glyph. */
.balance-hide-btn {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-4);
    margin-top: var(--sp-4);
}

.balance-hide-btn svg { margin-top: calc(-1 * var(--sp-2)); }

.balance-input { width: 140px; }

/* The previous reading, in a column of its own before the box: last figure,
   this month's box, the move. It used to wrap in two lines under the 140px
   input, which made every row on the screen twice the height of its box. */
.balance-prev {
    min-width: 8.5rem;
    margin-right: var(--sp-12);
    text-align: right;
}

/* "was $42,185.00" beside an empty balance box -- the previous reading, and a
   button that copies it in (balance_prefill.js). Styled as quiet text rather
   than a control: it sits beside every unfilled box on the screen, so button
   chrome twenty times over would read as a form to work through. It still
   has to look pressable on hover and be visible on focus, which is what the
   underline and the ring below are for. */
.balance-prefill {
    display: block;
    /* Full width of its column, and a little vertical padding, so there is
       something to hit on a phone -- the text alone was about 15px tall.
       Not a full 44px target: that is for a primary control, and
       twenty-two of those per month card would add a screen of height to
       a page that is already too long to scroll. The box beside it stays
       the real target; this is the shortcut. */
    width: 100%;
    padding: var(--sp-4) 0;
    border: 0;
    background: none;
    text-align: right;
    font-size: var(--fs-xs);
    line-height: 1.3;
    color: var(--theme-muted);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    cursor: pointer;
}

/* The month a reading is from, when it isn't simply the month before. */
.balance-prev-month {
    display: block;
    font-size: var(--fs-2xs);
}

.balance-prefill:hover {
    color: var(--theme-primary);
    text-decoration: underline;
}

.balance-prefill:focus-visible {
    outline: 2px solid var(--theme-primary);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}

/* The month-over-month column beside a filled-in box. Sized whether or not
   it has anything to say, so the boxes down a card stay in one line and a
   new account's blank cell reads as "nothing to compare to" rather than a
   row laid out differently. */
.balance-change {
    min-width: 5.5rem;
    margin-left: var(--sp-12);
    font-size: var(--fs-xs);
    font-weight: 600;
    line-height: 1.3;
    font-variant-numeric: tabular-nums;
}

.balance-change-since {
    display: block;
    font-size: var(--fs-2xs);
    font-weight: 400;
    color: var(--theme-muted);
}

@media (max-width: 400px) {
    /* The box takes the full width on its own line; the previous reading
       and the move share the line under it, left and right. */
    .balance-input { order: 1; width: 100%; margin-top: var(--sp-6); }
    .balance-prev { order: 2; min-width: 0; margin-right: auto; text-align: left; }
    .balance-prev .balance-prefill { text-align: left; }
    .balance-prev-month { display: inline; margin-left: 0.3em; }
    .balance-change { order: 3; margin-left: auto; margin-top: var(--sp-4); }
}

/* Edited-in-place fields: they sit flat against the page until hovered or
   focused, so a row reads as text rather than a form to fill in, and the
   textareas among them grow with their content (inline_edit.js) instead of
   hiding the tail of a long label. Used by the events screen and by the
   month-event boxes in the balance-entry card headers. */
.flat-input {
    height: auto;
    background-color: transparent;
    border-color: transparent;
    /* A textarea that sizes itself: no scrollbar, no drag handle. */
    resize: none;
    overflow: hidden;
}

.flat-input::placeholder { color: var(--theme-muted); opacity: 0.75; }

.flat-input:hover {
    background-color: var(--theme-input-bg);
    border-color: var(--theme-border);
}

.flat-input:focus {
    background-color: var(--theme-input-bg);
    border-color: var(--theme-primary);
}

/* A heading that reads as a caption: the dashboard's <h1> sits above the net
   worth figure it introduces, so it takes body size rather than heading size. */
.page-eyebrow {
    font-size: var(--fs-base);
    font-weight: 400;
    line-height: 1.5;
}

/* One row of the events screen: month, then the label filling the rest.

   The month shows as "June 2026" with the native date picker laid over it,
   invisible but still the control you click and tab to. A date input renders
   its value as 06/01/2026 with no way to restyle it, and type="month" -- which
   would read correctly -- has no support in desktop Safari (accounts/forms.py
   explains why the whole app avoids it). Covering the input keeps the picker
   and fixes the reading. */
.event-month {
    position: relative;
    display: inline-block;
    flex: 0 0 auto;
}

.event-month-text {
    display: block;
    min-width: 8.5rem;
    padding: var(--sp-2) var(--sp-6);
    border: 1px solid transparent;
    border-radius: 0.2rem;
    font-size: var(--fs-xs);
    line-height: 1.4;
    white-space: nowrap;
}

/* Matches .flat-input: quiet until you reach for it. :focus-within, because
   the focus lands on the input while the styling belongs to the text. */
.event-month:hover .event-month-text {
    background-color: var(--theme-input-bg);
    border-color: var(--theme-border);
}

.event-month:focus-within .event-month-text {
    background-color: var(--theme-input-bg);
    border-color: var(--theme-primary);
}

.event-month-input {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    border: 0;
    background: transparent;
    /* Invisible, not hidden: opacity keeps it focusable and clickable, so the
       picker still opens and the field stays in the tab order. */
    opacity: 0;
    cursor: pointer;
}

.event-label-input {
    flex: 1 1 12rem;
    min-width: 0;
    padding: var(--sp-2) var(--sp-6);
    line-height: 1.4;
}

.event-status:empty { display: none; }

/* Inline month events in the balance-entry card headers. The inputs sit flat
   against the header until hovered or focused so an empty month reads as a
   quiet "+ Add event" rather than a form to fill in. */
.month-events {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: var(--sp-4);
    margin-left: auto;
    /* Line the box up with the amount inputs below by clearing the same
       gutter each balance row's save status occupies (.ml-2 + 3.75rem). */
    margin-right: calc(var(--sp-8) + 3.75rem);
}

.month-event-status:empty { display: none; }

.month-event-input {
    width: 260px;
    max-width: 100%;
    padding: var(--sp-2) var(--sp-6);
    font-size: var(--fs-xs);
    line-height: 1.4;
    /* Right-aligned so the label itself sits against the column edge -- the
       box is invisible until touched, and left-aligned text would leave it
       floating mid-header with a wide empty gap to its right. */
    text-align: right;
}

/* Ragged-left is hard to read, so a label long enough to wrap goes left. */
.month-event-input.is-multiline { text-align: left; }

@media (max-width: 575px) {
    /* Stacked under the month name, there is no column to line up with. */
    .month-events {
        width: 100%;
        justify-content: flex-start;
        margin-top: var(--sp-6);
        margin-left: 0;
        margin-right: 0;
    }

    .month-event-input { width: 100%; text-align: left; }
}

.input-group-text {
    background-color: var(--theme-tile-bg);
    color: var(--theme-muted);
    border-color: var(--theme-border);
}

/* Mobile navbar */
.navbar-toggler.themed-toggler {
    color: var(--theme-navbar-link);
    border: 1px solid var(--theme-navbar-muted);
    font-size: 1.1rem;
    line-height: 1;
    padding: var(--sp-4) var(--sp-10);
}

@media (max-width: 991.98px) {
    .navbar.themed .navbar-collapse { padding-top: var(--sp-8); }
    .navbar.themed .nav-tools {
        border-top: 1px solid var(--theme-navbar-muted);
        margin-top: var(--sp-8);
        padding-top: var(--sp-8);
    }
}

/* Theme picker in the navbar */
.theme-picker {
    background-color: transparent;
    color: var(--theme-navbar-muted);
    border: 1px solid var(--theme-navbar-muted);
    border-radius: var(--radius-sm);
    padding: var(--sp-2) var(--sp-6);
    font-size: var(--fs-xs);
}

.theme-picker option {
    color: var(--theme-body-color);
    background-color: var(--theme-card-bg);
}

/* The Household link sits among controls (theme picker, log out), so it wears
   the same outline instead of reading as bare text next to them. */
.nav-tools .nav-link-outline {
    border: 1px solid var(--theme-navbar-muted);
    border-radius: var(--radius-sm);
    padding: var(--sp-4) var(--sp-10);
}

/* The skip link (base.html). Off-canvas rather than display:none or
   visibility:hidden, both of which take it out of the tab order -- which is the
   one thing it exists to be in. z-index clears the fixed sidebar (1030). */
.skip-link {
    position: absolute;
    top: 0;
    left: -9999px;
    z-index: 1100;
    padding: var(--sp-12) var(--sp-20);
    background-color: var(--theme-primary);
    color: var(--theme-on-primary);
    font-weight: 600;
    border-bottom-right-radius: var(--radius-sm);
}

.skip-link:focus {
    left: 0;
    color: var(--theme-on-primary);
    text-decoration: none;
    outline: 2px solid var(--theme-accent);
    outline-offset: -2px;
}

/* Current page.

   This lived inside the (then md+) sidebar block, so below it -- where the nav is
   a collapsed top navbar rather than a sidebar -- nothing marked the page you
   were on: the active link computed identically to every other one, same
   colour, same weight, no fill. The sidebar owns the *shape* of a nav item;
   "this is where you are" is true at every width, so it belongs out here.

   .navbar.themed rather than .app-sidebar for the same reason -- the class
   that means "this app's nav", not the one that means "laid out as a rail". */
.navbar.themed .navbar-nav .nav-link.active,
.navbar.themed .nav-tools .nav-link.active {
    background-color: var(--theme-primary);
    color: var(--theme-on-primary);
    font-weight: 600;
    border-radius: var(--radius-sm);
}

/* Stacked in the collapsed menu the items are a list to thumb down, not a row
   to read across, so they get a target to match. The sidebar sets its own
   padding inside the lg+ block below. */
@media (max-width: 991.98px) {
    .navbar.themed .navbar-nav .nav-link {
        padding: var(--sp-10) var(--sp-12);
        border-radius: var(--radius-sm);
    }
}

/* -----------------------------------------------------------------------
   The content column.

   <main> and the footer are one column: the same width, at every width.
   This lived in an inline style="max-width: 900px" on <main> alone, so the
   footer kept Bootstrap's stepped .container max-width -- 1140px at xl --
   and, once the md+ block below pushed it right by the rail, computed
   250px + 1140px = 1390px against a 1280px viewport. Every signed-in page
   scrolled sideways by 110px. One declaration, both elements, so the two
   can't drift apart again.
   -------------------------------------------------------------------- */
main.container,
footer.container {
    max-width: var(--content-width);
}

/* -----------------------------------------------------------------------
   Desktop left sidebar (lg+).

   From 992px, not 768px (changed 2026-09-23). At 768 a fixed 230px rail
   took a third of a tablet's width and left the content ~518px -- tiles
   three-up, forms in 290px columns, the Household buttons too narrow for
   their labels. Below 992 the nav is the collapsed top bar, and the page
   gets the full width. base.html's navbar-expand-lg moves with this.
   -------------------------------------------------------------------- */
@media (min-width: 992px) {
    /* Offset past the rail, then centred in the whole viewport once the
       viewport is wide enough that centring puts the column clear of it.

       width:auto is load-bearing, not tidying: Bootstrap's .container is
       width:100%, measured against the viewport rather than against what is
       left beside the rail, so between 992px and 1150px the column is wider
       than its own space and hangs off the right edge even with the
       max-width above. auto fills what is actually free; the max-width caps
       it once there is room to spare. */
    main.container,
    footer.container {
        width: auto;
        margin-left: max(var(--sidebar-rail), calc((100vw - var(--content-width)) / 2));
        margin-right: auto;
    }

    /* On mobile the navbar's own mb-4 pushes the content down. Here the
       sidebar is position:fixed with margin:0, so that gap disappears with it
       and every signed-in page started flush against the top of the viewport
       -- the <h1> measured y=0. Put the breathing room back explicitly. */
    main.container { padding-top: var(--sp-32); }

    .navbar.themed.app-sidebar {
        position: fixed;
        top: 0;
        left: 0;
        bottom: 0;
        width: 230px;
        margin: 0;
        padding: var(--sp-16) var(--sp-12);
        overflow-y: auto;
        align-items: stretch;
        flex-direction: column;
        border-right: 1px solid var(--theme-navbar-muted);
        z-index: 1030;
    }

    .app-sidebar > .container {
        display: flex;
        flex: 1 1 auto;
        flex-direction: column;
        align-items: stretch;
        max-width: none;
        width: 100%;
        padding: 0;
    }

    .app-sidebar .navbar-brand {
        margin: 0 0 var(--sp-20) 0;
        padding: 0 var(--sp-8);
        font-size: var(--fs-lg);
        white-space: normal;
        line-height: 1.25;
    }

    .app-sidebar .navbar-collapse {
        flex: 1 1 auto;
        flex-direction: column;
        align-items: stretch;
    }

    .app-sidebar .navbar-nav {
        flex-direction: column;
        width: 100%;
    }

    .app-sidebar .navbar-nav .nav-link,
    .app-sidebar .nav-tools .nav-link {
        padding: var(--sp-8) var(--sp-12);
        border-radius: var(--radius-sm);
    }

    /* :not(.active) because the active paint now lives outside this block
       (see "Current page" above) at the same specificity and earlier in the
       file -- without this, hovering the current item would grey it out. */
    .app-sidebar .navbar-nav .nav-link:hover:not(.active),
    .app-sidebar .navbar-nav .nav-link:focus:not(.active),
    .app-sidebar .nav-tools .nav-link:hover:not(.active),
    .app-sidebar .nav-tools .nav-link:focus:not(.active) {
        background-color: var(--theme-navbar-muted);
        color: var(--theme-navbar-bg);
    }

    .app-sidebar .nav-tools .nav-link-outline.active {
        border-color: var(--theme-primary);
    }

    .app-sidebar .nav-tools {
        margin-top: auto;
        flex-direction: column;
        align-items: stretch !important;
        border-top: 1px solid var(--theme-navbar-muted);
        padding-top: var(--sp-12);
    }

    .app-sidebar .nav-tools > * {
        margin: var(--sp-4) 0 !important;
        width: 100%;
    }

    .app-sidebar .nav-tools .form-inline { display: block; }
    .app-sidebar .nav-tools .btn { width: 100%; }
}

/* ---- Info icons ---- */

/*
 * The (i) beside a label, column header or figure, and the note it opens --
 * `{% info %}` in accounts/templatetags/formatting.py, opened by
 * csp_handlers.js. Blue on every theme, from its own token: --theme-accent
 * is green on three of the four, and the icon has to read as "explanation"
 * next to green-means-good and amber-means-behind figures. #1d63c4 is 5.6:1
 * on white; #7db3ff is 7.4:1 on Midnight's card.
 *
 * The button is 1.35rem square for a usable touch target, with negative
 * vertical margins so it doesn't push the line it sits on -- most of them
 * sit in an --fs-2xs uppercase header or an --fs-xs label. The note resets
 * every inherited text style for the same reason: without that, a note in a
 * table header would come out uppercase and letter-spaced.
 */
.info {
    display: inline-block;
    line-height: 1;
    vertical-align: middle;
}

.info-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.35rem;
    height: 1.35rem;
    margin: calc(-1 * var(--sp-6)) 0 calc(-1 * var(--sp-6)) var(--sp-2);
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: none;
    color: var(--theme-info);
    cursor: pointer;
    vertical-align: middle;
}

.info-toggle svg { display: block; }

.info-toggle:hover,
.info-toggle[aria-expanded="true"] { background: var(--theme-info-bg); }

.info-toggle:focus-visible {
    outline: 2px solid var(--theme-info);
    outline-offset: 1px;
}

.info-pop {
    position: fixed;
    z-index: 1030;
    width: max-content;
    max-width: min(24rem, calc(100vw - 1.5rem));
    padding: var(--sp-10) var(--sp-12);
    background: var(--theme-card-bg);
    color: var(--theme-body-color);
    border: 1px solid var(--theme-border);
    border-left: 3px solid var(--theme-info);
    border-radius: var(--radius);
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.14);
    font-size: var(--fs-xs);
    font-weight: 400;
    line-height: 1.45;
    letter-spacing: 0;
    text-transform: none;
    text-align: left;
    white-space: normal;
}

.info-pop p:last-child { margin-bottom: 0; }

/* A field whose help has a second paragraph puts the icon beside the label.
   A button can't live inside a <label> (it would be labelled by it), so the
   two share a row that reserves the same line the bare label does -- see
   "Compact form rows" above. */
.field > .field-label {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    min-height: 1.15rem;
    margin-bottom: var(--sp-4);
}

.field > .field-label > label { margin-bottom: 0; }

/* By owner (Balances): on phones the five columns scrolled sideways and hid
   Liabilities and Net. Drop the retirement subtotal (secondary, and still in
   the account cards below) and tighten the rest so Assets/Liabilities/Net fit. */
@media (max-width: 35.99em) {
    .owner-table .owner-retirement { display: none; }
    .owner-table th,
    .owner-table td { padding-left: var(--sp-4); padding-right: var(--sp-4); font-size: var(--fs-xs); }
}

/* A link-styled button in a card footer ("Show all 14 months, back to
   September 2025") is longer than a phone is wide, and .btn never wraps, so
   it pushed the whole dashboard 11px sideways at 375px. */
.card-footer .btn-link { white-space: normal; text-align: left; }

/* Accounts group headers on a phone: "$310,000 +$37,200 past 12 months" wrapped
   to two lines beside the group name. The span is already stated once in the
   totals line above the groups, so the header keeps just the two figures. */
@media (max-width: 35.99em) {
    .account-section-total { white-space: nowrap; }
    .account-section-total .section-span { display: none; }
}

/* Half-width fields on a phone: a date input ("mm/dd/yyyy" + calendar icon)
   and a select ("Take-home pay") were clipped to "mm/dd/yyy" / "Take-home pa"
   in a 6-of-12 column. Tighter side padding and a slightly smaller face is
   enough to fit the whole value without stacking the field full width. */
@media (max-width: 35.99em) {
    .form-row > .col-6 input[type="date"],
    .form-row > .col-6 select.form-control,
    .form-row > .col-6 select.custom-select {
        padding-left: var(--sp-8);
        padding-right: var(--sp-8);
        font-size: var(--fs-sm);
        min-width: 0;
    }
    .form-row > .col-6 select.custom-select { padding-right: var(--sp-24); background-position: right var(--sp-6) center; }
}

/* Summary on a phone: the four columns that stay (year, net worth, change,
   vs. income) were still ~60px wider than the card, so the headline rate sat
   off-screen behind a sideways scroll nobody knew was there. Tighter cell
   padding and header type bring it inside. */
@media (max-width: 35.99em) {
    .summary-table th,
    .summary-table td { padding-left: var(--sp-4); padding-right: var(--sp-4); }
    .summary-table th { font-size: var(--fs-2xs); letter-spacing: 0; }
    .summary-table td .text-nowrap { white-space: normal !important; }
}

/* Summary at 320px (small phones): the 375px rules still left the table ~41px
   over its card. A smaller figure size and 2px cell padding close the gap. */
@media (max-width: 22em) {
    .summary-table { font-size: var(--fs-2xs); }
    .summary-table th,
    .summary-table td { padding-left: var(--sp-2); padding-right: var(--sp-2); }
}

/* At 320px "as of September 2026" wrapped to three lines in the net worth
   cell; the month alone still says which reading it is, in two. */
@media (max-width: 22em) {
    .summary-table .summary-asof { display: none; }
}

/* ---- Print ----

   A household prints the Annual summary or the dashboard to hand to a
   partner or an adviser. Until 2026-09-23 there were no print rules of the
   app's own, and Bootstrap 4.1's were working against it: @page A3, and
   body/.container at min-width 992px, so a Letter or A4 page got a 992px
   layout scaled down to fit; a dark theme printed dark cards with light
   text; and the chart range buttons, tab strips, Edit/Hide actions and info
   icons all went onto paper. Bootstrap's own `.navbar { display: none }`
   already takes the sidebar away.

   Paper always gets Evergreen's light palette, whatever the screen theme:
   the tokens are redefined here at [data-theme] weight, later in the file,
   so they win over every theme block. */
@media print {
    @page { size: auto; margin: 12mm; }

    body,
    .container { min-width: 0 !important; }

    :root,
    [data-theme] {
        --theme-body-bg: #ffffff;
        --theme-body-color: #14201a;
        --theme-muted: #5d6760;
        --theme-primary: #1f5e45;
        --theme-on-primary: #ffffff;
        --theme-card-bg: #ffffff;
        --theme-border: #c9d3ce;
        --theme-tile-bg: #ffffff;
        --theme-month-bg: #eef3f0;
        --theme-success-bg: #dff0e8;
        --theme-success-color: #1a6e49;
        --theme-danger-color: #ba372a;
        --theme-danger-bg: rgba(186, 55, 42, 0.10);
        --theme-warning-color: #7e631c;
        --theme-warning-bg: rgba(126, 99, 28, 0.10);
        --theme-accent: #4cc38a;
        --theme-info: #1d63c4;
        --theme-info-bg: rgba(29, 99, 196, 0.10);
        --theme-on-success: #ffffff;
        --theme-on-accent: #0b241a;
        --theme-on-warning: #ffffff;
        --theme-on-muted: #ffffff;
        --theme-input-bg: #ffffff;
        --theme-input-color: #14201a;
    }

    body { background: #ffffff; }

    /* The page is the content: no rail offset, no centring margin. */
    main.container {
        margin: 0 !important;
        padding-top: 0 !important;
        max-width: none;
        width: auto;
    }

    /* Controls are for the screen. Named one by one rather than every
       <button>: the dashboard's +$1,529 change badges and the enter-balances
       month titles are buttons too, and they are content. A screen-only
       form (the summary's "Year ends in") takes Bootstrap's d-print-none
       in its template instead. */
    .skip-link,
    .tab-strip,
    .btn,
    .navbar-toggler,
    .card-actions,
    .card-action,
    .row-action,
    .info,
    .ask-starter,
    .htmx-indicator,
    .a2hs-banner,
    .request-error,
    .month-jump,
    .hidden-toggle-form,
    body > footer { display: none !important; }

    /* A card or a table row split across two sheets reads as two halves of
       nothing. Wide tables print whole rather than behind a scroll. */
    .card,
    .stat-tile { break-inside: avoid; }
    .table-responsive { overflow: visible !important; }

    /* A chart canvas carries the pixel size it was laid out at on screen;
       the printed column is narrower, so let it shrink with its aspect. */
    canvas { max-width: 100% !important; height: auto !important; }

    /* Links are navigation, and on paper there is nowhere to go. */
    a:not(.btn) { text-decoration: none; color: inherit; }
}
