/* ============================================================
   Moneylight — Global stylesheet (YNAB-inspired)

   Ported from the budget-tracker prototype's design system. The
   look is intentionally identical; only the delivery differs
   (Django staticfiles + {% static %} instead of FastAPI's hardcoded
   /static path). Token-based: edit the §1 custom properties to
   retheme everything in one place.

   Architecture:
     1. TOKENS      — CSS custom properties. Edit these to retheme.
     2. RESET       — element baselines.
     3. HEADINGS    — single source of truth for forecast section
                      labels and table column headers.
     4. APP LAYOUT  — sidebar + main grid.
     5. SIDEBAR     — left nav, logo.
     6. MAIN/PAGE   — page containers, headers.
     7. PANEL       — card / panel container.
     8. FORMS       — form rows, fields, errors.
     9. BUTTONS     — .btn variants.
    10. TABLES      — .list-table layout (heading typography in §3).
    11. TOAST       — flash messages.
    12. FORECAST    — weekly grid columns + section banding.
    13. UTIL        — minimal, single-purpose utility classes.
   ============================================================ */

/* 0. FONTS ==================================================
   Figtree self-hosted (not hotlinked from Google Fonts) so no
   visitor IP is transmitted to Google without consent (GDPR).
   Variable font: one woff2 per unicode subset covers every weight;
   unicode-range gating means an English visitor only downloads the
   latin file. The src paths are relative to this stylesheet
   (/static/app.css), so the "fonts/" prefix below resolves under
   /static/. font-src stays 'self'.

   NOTE: never write a CSS url reference inside a comment anywhere in
   this file. WhiteNoise's ManifestStaticFilesStorage rewrites every
   such reference it finds during collectstatic without stripping
   comments first, so one written here (even as documentation) is
   treated as a real file and fails the build with MissingFileError. */
@font-face {
  font-family: "Figtree";
  font-style: normal;
  font-weight: 300 900;
  font-display: swap;
  src: url("fonts/figtree-latin.202cfb54d4e6.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Figtree";
  font-style: normal;
  font-weight: 300 900;
  font-display: swap;
  src: url("fonts/figtree-latin-ext.eca8d8b337ff.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* 1. TOKENS ==================================================
   v2 UI refresh (2026-07): the palette, type scale, spacing, radii
   and shadows were retuned for a calmer, more premium feel (see
   docs/design/Moneylight_UI_Refresh_Design_Brief_v2.md). Brand purple
   and the YNAB-idiom green money banners are preserved; borders are
   softer, whitespace is larger, and typography does more of the work. */
:root {
  /* Surfaces — cleaner, cooler neutrals; softer rules */
  --bg: #f6f6f9;
  --panel: #ffffff;
  --ink: #1e2030;
  --muted: #6b6f83;
  --rule: #ececf1;
  --rule-strong: #dcdce5;
  --rule-soft: #f2f2f6;   /* even lighter row dividers inside data tables (feedback §Tables) */
  --surface-subtle: #f7f8fb;

  /* Sidebar */
  --sidebar-bg: #1c1f58;
  --sidebar-ink: #ffffff;
  --sidebar-hover: rgba(255, 255, 255, 0.07);
  --sidebar-active: #4c45c7;
  --sidebar-rule: rgba(255, 255, 255, 0.09);

  /* Accent (primary purple/indigo) */
  --accent: #3b3b97;
  --accent-hover: #2c2c80;
  --accent-soft: #ecebf9;
  --accent-soft-ink: #3b3b97;
  --accent-tint: #f6f5fd;

  /* Status colors */
  --good: #2f6f1f;
  --good-bg: #c5e98a;   /* YNAB Ready-to-Assign money-green (original; Ken reverted the softened tint 2026-07-17) */
  --good-bg-ink: #1f3d0c;
  --warn: #b45309;
  --warn-soft: #fdf1e3;
  --bad: #b91c1c;
  --bad-soft: #fdeaec;
  --bad-soft-ink: #b91c1c;

  /* Forecast section banding (YNAB-style group rows) */
  --section-band: #f7f8fb;

  /* Week-head bar: the dark slate cap over each week's column (original; Ken
     reverted the light-strip experiment 2026-07-17). Light text sits on it. */
  --week-head-bg: #54586b;

  /* Heading typography — controls every forecast section label
     AND every table column header. Edit these tokens to restyle all
     headings in one place. The rule that applies them lives in §3.
     v2: muted uppercase micro-labels (Stripe/Linear table headers). */
  --heading-size: var(--font-size-sm);
  --heading-color: #55596c;
  --heading-transform: uppercase;
  --heading-weight: 600;
  --heading-letter-spacing: .05em;

  /* Typography — larger, airier scale (brief §Typography) */
  --font-family: "Figtree", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
  --font-size-xs: 12.5px;
  --font-size-sm: 13.5px;
  --font-size-base: 15px;
  --font-size-md: 16px;
  --font-size-lg: 18px;
  --font-size-xl: 22px;
  --font-size-2xl: 30px;
  --line-height: 1.55;

  /* Spacing — 8-pt scale, extended for the wider vertical rhythm */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;

  /* Borders, radius, shadow — softer, flatter (brief §Cards, §Shadows) */
  --radius-sm: 8px;
  --radius-md: 10px;
  --radius-lg: 14px;   /* cards a touch rounder / more premium (feedback §Cards) */
  --radius-xl: 16px;
  --radius-pill: 999px;
  /* Softer but slightly deeper card shadow — more lift off the page without hard edges
     (feedback §Cards, §Forecast — "slightly stronger but softer card shadows"). */
  --shadow-card: 0 1px 2px rgba(20, 22, 60, .05), 0 4px 14px rgba(20, 22, 60, .06);
  --shadow-pop: 0 8px 28px rgba(20, 22, 60, .12);

  --sidebar-width: 272px;
}

/* 2. RESET =================================================== */
*,
*::before,
*::after {
  box-sizing: border-box;
}

body,
input,
select,
textarea,
button {
  font-family: var(--font-family);
}

body {
  font-size: var(--font-size-base);
  line-height: var(--line-height);
  background: var(--bg);
  color: var(--ink);
  margin: 0;
  padding: 0;
}

::placeholder {
  font-family: inherit;
  color: var(--muted);
  opacity: 1;
}

input[type="date"]:not(:focus)::-webkit-datetime-edit {
  color: var(--muted);
}

select:has(option[value=""]:checked) {
  color: var(--muted);
}

select option {
  color: var(--ink);
}

h1 {
  font-size: var(--font-size-2xl);
  /* A little more air under the page title before the first card (feedback §Typography).
     The forecast header overrides this to 0 and owns its own spacing. */
  margin: 0 0 var(--space-3);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.2;
}

h2 {
  font-size: var(--font-size-xs);
  margin: 0 0 var(--space-2);
  color: var(--muted);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .08em;
}

a {
  color: var(--accent);
}

/* 3. HEADINGS ================================================ */
/* SINGLE SOURCE OF TRUTH for the forecast section labels and table
   column headers. Edit the --heading-* tokens in §1 to change them
   all at once.

   The Necessities, Savings and CC Balance headers are ALSO `.line` rows, and `.line`
   (§ further down) sets its own font-size/color. Same specificity + later in the file =
   it wins, so those headers must be qualified `.line.x` here to stay the same size and
   color as the other section labels (Necessities rendered a size larger and CC Balance
   muted until 2026-07-23). */
.section-label,
.line.headline .name,
.line.necessities-summary,
.line.savings-summary,
.line.pending-summary,
.bills-total-row,
.col-label,
.list-table th {
  font-size: var(--heading-size);
  font-weight: var(--heading-weight);
  text-transform: var(--heading-transform);
  letter-spacing: var(--heading-letter-spacing);
}

.section-label,
.line.necessities-summary,
.line.savings-summary,
.line.pending-summary,
.bills-total-row,
.col-label,
.list-table th {
  color: var(--heading-color);
}

/* 4. APP LAYOUT ============================================== */
.app-layout {
  display: grid;
  grid-template-columns: var(--sidebar-width) 1fr;
  min-height: 100vh;
}

.main {
  padding: var(--space-6);
  min-width: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

/* 5. SIDEBAR ================================================= */
.sidebar {
  background: var(--sidebar-bg);
  color: var(--sidebar-ink);
  padding: var(--space-5) 0 var(--space-4);
  position: sticky;
  top: 0;
  align-self: start;
  height: 100vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.sidebar-brand {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-5) var(--space-4);
  border-bottom: 1px solid var(--sidebar-rule);
}

.sidebar-brand .logo {
  height: 46px;
  width: auto;
  flex: 0 0 auto;
}

.sidebar-nav {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 0 var(--space-2);
  /* Fill the sidebar's full height so .nav-account's margin-top: auto has
     free space to push against (otherwise the column is only as tall as its
     content and Account just sits after Settings). */
  flex: 1;
}

/* Inactive links sit at a muted white so the nav recedes and the current page
   (full-white + accent fill) reads as the one thing that's "on" — the design
   feedback's "reduce the visual weight of the left navigation." */
.sidebar-nav a {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 9px var(--space-3);
  border-radius: var(--radius-md);
  color: rgba(255, 255, 255, 0.68);
  text-decoration: none;
  font-size: var(--font-size-base);
  font-weight: 500;
  transition: background .15s ease, color .15s ease;
}

.sidebar-nav a:hover {
  background: var(--sidebar-hover);
  color: #ffffff;
}

.sidebar-nav a.active {
  background: var(--sidebar-active);
  color: #ffffff;
}

.sidebar-nav a .ico {
  width: 20px;
  height: 20px;
  flex: 0 0 20px;
  color: currentColor;
  opacity: .9;
}

.sidebar-nav a.active .ico {
  opacity: 1;
}

/* Progress pill on the "Finish setup" nav link (e.g. "1/4"), pushed to the row's
   trailing edge. Translucent so it reads on both the dark sidebar and the accent
   active state. */
.nav-badge {
  margin-left: auto;
  padding: 1px 8px;
  border-radius: 999px;
  font-size: var(--font-size-xs);
  font-weight: 600;
  line-height: 1.5;
  background: rgba(255, 255, 255, 0.16);
  color: #ffffff;
}

/* Quiet section label that groups the nav into Forecast / Your Money / Setup.
   Not a link; uppercase muted caption sitting above its group. */
.nav-section {
  margin: var(--space-5) 0 var(--space-2);
  padding: 0 var(--space-3);
  font-size: var(--font-size-xs);
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--sidebar-ink);
  opacity: .62;   /* a touch more contrast so the group labels read clearly (feedback §Sidebar) */
}

/* The bottom cluster — Lifetime Access (conditional), Log Out, and Account — is pinned
   to the base of the full-height sidebar on desktop (margin-top: auto eats the free space
   in the flex column) and separated from the Setup group by a top border. Wrapping the
   three in one box keeps a single pin + divider whether or not the Lifetime link renders,
   replacing the old per-link sibling-cancel juggling. Reset to normal flow in the mobile
   drawer (see the max-width: 768px block). */
.nav-bottom {
  margin-top: auto;
  padding-top: var(--space-3);
  border-top: 1px solid var(--sidebar-rule);
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* Log Out now lives in the primary nav (feedback §Sidebar — one-click logout), moved out
   of the Account page. It POSTs to accounts:logout, so it is a form button rather than an
   <a>; these rules give it the same look as the sidebar links. app.js's submit guard
   disables it on submit (a real navigation) and clears it on pageshow, like every other
   navigating form. */
.sidebar-nav .nav-logout-form {
  margin: 0;
  display: flex;
}

.sidebar-nav .nav-logout {
  flex: 1;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 9px var(--space-3);
  border-radius: var(--radius-md);
  color: rgba(255, 255, 255, 0.68);
  background: none;
  border: 0;
  cursor: pointer;
  font: inherit;
  font-size: var(--font-size-base);
  font-weight: 500;
  text-align: left;
  transition: background .15s ease, color .15s ease;
}

.sidebar-nav .nav-logout:hover {
  background: var(--sidebar-hover);
  color: #ffffff;
}

.sidebar-nav .nav-logout .ico {
  width: 20px;
  height: 20px;
  flex: 0 0 20px;
  opacity: .9;
}

/* 6. MAIN / PAGE ============================================= */
.header {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin-bottom: var(--space-5);
  flex-wrap: wrap;
}

.header h1 {
  margin: 0;
}

.header .meta {
  color: var(--muted);
  font-size: var(--font-size-sm);
}

/* Width constraint for the management / settings pages (the forecast grid is
   deliberately full-width; everything else reads better capped). */
.page-narrow {
  max-width: 1040px;
}

.page-setup {
  max-width: 540px;
  margin: 80px auto;
  /* Offset the 80px top+bottom margins so the footer still pins to the bottom
     of the viewport without forcing a scrollbar. */
  min-height: calc(100vh - 160px);
}

/* Landing hero: the connect page has no sidebar, so this carries the brand.
   Deep-indigo card (matches the sidebar) with the wallet logo + wordmark, so
   the setup screen reads as part of the same app. */
.setup-hero {
  background: var(--sidebar-bg);
  color: var(--sidebar-ink);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  padding: var(--space-5) var(--space-4);
  text-align: center;
  margin-bottom: var(--space-4);
}

.setup-hero .logo {
  height: 52px;
  width: auto;
  max-width: 100%;
}

.setup-hero h1 {
  margin: var(--space-4) 0 var(--space-1);
  /* xl (22px), not the base h1's 2xl, + semibold not 700: the logo wordmark is the heavy
     element in the hero, so a smaller, lighter headline keeps it from competing (Ken, 2026-07-17). */
  font-size: var(--font-size-xl);
  font-weight: 600;
  line-height: 1.15;
  /* Keep the headline off a single-word last line. */
  text-wrap: balance;
}

.setup-tagline {
  color: rgba(255, 255, 255, 0.78);
  margin: 0;
  font-size: var(--font-size-md);
  /* No orphan: never let the last word wrap onto a line by itself. */
  text-wrap: pretty;
}

/* Value-prop list on the connect card. Each row leads with a line icon (chart,
   calendar, lock) for visual texture; brand green ties them to the logo accent. */
.setup-points {
  list-style: none;
  padding: 0;
  margin: var(--space-3) 0 var(--space-4);
  display: grid;
  gap: var(--space-3);
}

.setup-points li {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  text-wrap: pretty;
}

.setup-icon {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  margin-top: 1px;
  fill: none;
  stroke: var(--good);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.btn-block {
  display: flex;
  width: 100%;
  /* Matches the trial pill's height so the CTA and pill read as a pair (border-box,
     so this is the exact outer height on both despite the button's 1px border). */
  min-height: 44px;
}

/* Trial offer as a prominent visual badge just below the CTA, instead of a sentence.
   Brand green (the logo's accent) reads as a positive "free" benefit; sized up and
   bold so it's the second thing the eye lands on after the button. */
.trial-badge-row {
  margin: var(--space-3) 0 0;
  text-align: center;
}

.trial-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 44px;
  background: var(--good-bg);
  color: var(--good-bg-ink);
  border-radius: var(--radius-pill);
  padding: var(--space-2) var(--space-4);
  font-size: var(--font-size-md);
  /* Lighter overall, with just the trial length bolded (mockup treatment). */
  font-weight: 500;
}

.trial-badge strong {
  font-weight: 700;
}

/* Filled green check circle at the pill's leading edge (mockup): the money-green mark
   reads as a positive "included" cue. */
.trial-check {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  color: var(--good);
}

/* One small muted line under the CTA: the YNAB sign-in expectation + price. */
.connect-fineprint {
  margin: var(--space-3) 0 0;
  text-align: center;
  color: var(--muted);
  font-size: var(--font-size-xs);
  line-height: 1.5;
  text-wrap: pretty;
}

.connect-fineprint .lock {
  vertical-align: -1px;
  margin-right: 2px;
  opacity: 0.7;
}

/* Referral door on the connect page: deliberately quiet (muted, below the fineprint)
   so it never competes with the Connect CTA. */
.referral-door {
  margin-top: var(--space-4);
  border-top: 1px solid var(--rule);
  padding-top: var(--space-3);
  font-size: var(--font-size-sm);
}

.referral-details summary {
  color: var(--muted);
  cursor: pointer;
  text-align: center;
}

.referral-details form {
  margin-top: var(--space-3);
}

.referral-confirmed {
  margin: 0;
  text-align: center;
  color: var(--good-bg-ink);
  font-weight: 600;
}

.footer {
  color: var(--muted);
  font-size: var(--font-size-xs);
  /* Push to the bottom of the flex-column .main so it pins to the page bottom
     instead of floating after short content. */
  margin-top: auto;
  /* Breathing room ABOVE the hairline. This is padding (not the pin margin), so it
     survives on a tall page where margin-top:auto collapses to 0 and would otherwise
     let the divider hug the last element (brief §Whitespace). */
  padding-top: var(--space-6);
  text-align: center;
  /* No orphan: keep the disclaimer's last word off a line by itself. text-wrap is
     inherited, so this covers the nested paragraphs. */
  text-wrap: pretty;
}

/* The hairline separating the footer from page content (calmer hierarchy, Stripe/Linear
   style), drawn as a child rather than a top border so there's guaranteed space on BOTH
   sides of the line regardless of page height. */
.footer::before {
  content: "";
  display: block;
  border-top: 1px solid var(--rule);
  margin-bottom: var(--space-6);
}

.footer-brand {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}

.footer-logo {
  height: 22px;
  width: auto;
}

.footer-disclaimer {
  /* Wide enough that the required (unabridged) YNAB disclaimer wraps to ~3 rows
     instead of stacking tall, keeping the footer short. */
  max-width: 94ch;
  margin: 0 auto;
  line-height: 1.45;
}

.footer-copy {
  margin: var(--space-2) 0 0;
}

/* Version stamp: secondary to the copyright, monospace digits read as a build id.
   A link since 2026-07: it opens the public changelog on the marketing site. */
.footer-version {
  margin-left: var(--space-2);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.footer-version:hover {
  color: var(--ink);
}

.footer-links {
  margin: var(--space-2) 0 0;
}

.footer-links a {
  color: var(--muted);
}

.footer-links a:hover {
  color: var(--ink);
}

/* 7. PANEL =================================================== */
.panel {
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  padding: var(--space-5);
  /* More breathing room between stacked sections (feedback §Account, §Settings). Inner
     card padding is unchanged (feedback §Cards — "maintain current spacing"). */
  margin-bottom: var(--space-6);
}

.panel h2 {
  color: var(--ink);
  font-size: var(--font-size-lg);
  font-weight: 600;
  text-transform: none;
  letter-spacing: -0.01em;
  margin-bottom: var(--space-3);
}

/* Settings page: a labeled group of related panels, so the page reads as a few
   sections ("Accounts", "Forecast Rules") instead of one long stack of cards
   (design feedback). The title overrides the base uppercase-eyebrow h2. */
.settings-group {
  margin-bottom: var(--space-6);
}

.settings-group-title {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--font-size-md);
  font-weight: 700;
  text-transform: none;
  letter-spacing: -0.01em;
  color: var(--ink);
  margin: 0 0 var(--space-4);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--rule-strong);
}

/* Subtle section icon on a settings group header (feedback §Settings — "simple section
   icons are acceptable if they remain subtle"). Accent-tinted, quiet, non-decorative:
   one per configuration area (Accounts / Credit Cards / Thresholds / Autopay). */
.settings-ico {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  color: var(--accent);
  opacity: .75;
}

/* A panel carrying an offer rather than a setting (the Lifetime Access deal). It should
   read as the one to look at without turning into a banner: a faint accent tint, a
   stronger accent border, and a slightly deeper shadow lift it off the neutral panels
   around it (brief §Account Page — "feel premium"). */
.panel-featured {
  background: var(--accent-tint);
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent), 0 6px 20px rgba(59, 59, 151, .10);
  padding: var(--space-6);
}

/* The offer card is the sidebar's "Lifetime Access" jump target. Keep it clear of the
   mobile sticky topbar, which would otherwise cover the card's heading on arrival. */
#lifetime {
  scroll-margin-top: 64px;
}

/* Lifetime Access benefits, rendered as a short scannable list rather than a wall of
   prose (feedback §Account — "convert Lifetime Access copy into a short benefit list").
   Each item keeps Ken's exact wording; only the layout changed. */
.benefit-list {
  list-style: none;
  margin: 0 0 var(--space-3);
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.benefit-list li {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  color: var(--ink);
  font-size: var(--font-size-base);
}

.benefit-ico {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  margin-top: 3px;
  color: var(--good);
}

/* Plans page: each YNAB plan as its own card instead of one long table row
   (feedback §Plans). The active plan carries the accent tint + a small badge so the
   current selection reads at a glance. */
.plan-cards {
  list-style: none;
  margin: 0 0 var(--space-5);
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.plan-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-4);
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  padding: var(--space-4) var(--space-5);
}

.plan-card.is-active {
  border-color: var(--accent);
  background: var(--accent-tint);
}

.plan-card-main {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  min-width: 0;
}

.plan-card-name {
  font-weight: 600;
  font-size: var(--font-size-md);
}

.plan-card-badge {
  font-size: var(--font-size-xs);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--accent);
  background: var(--accent-soft);
  padding: 2px 8px;
  border-radius: var(--radius-pill);
}

.plan-card-action {
  flex: 0 0 auto;
}

/* 8. FORMS =================================================== */
.form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4);
  margin-bottom: var(--space-4);
}

.form-row-1 {
  grid-template-columns: 1fr;
}

.form-row label {
  display: block;
}

/* Management add/edit forms render a series of labeled fields via
   manage/_form_fields.html, wrapped in .form-edit-grid for a 2-column layout. */
.form-edit-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-4);
  /* Top-align cells, not bottom: the cadence <select> grows a "More
     frequencies…" toggle beneath it, so bottom-aligning would float the paired
     field (e.g. Next date) down to the toggle's baseline. Labels are uniform
     height, so top-aligning keeps every input on the same line. */
  align-items: start;
}

.form-edit-grid>label {
  display: block;
}

.form-edit-grid .full-span {
  grid-column: 1 / -1;
}

.form-actions {
  text-align: right;
  margin-top: var(--space-3);
}

/* Wraps a Django CheckboxSelectMultiple. Django renders an outer <div> of
   per-option <div><label><input>…</label></div>; grid that inner wrapper into 2
   columns. The wrapper class goes on a div the template adds around the field. */
.checkbox-list>div {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 6px var(--space-4);
  margin: var(--space-2) 0;
}

.checkbox-list label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) 0;
  font-size: var(--font-size-base);
}

.checkbox-inline {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 6px 0;
}

/* Credit-card picker: each card's checkbox with its "How do you pay this card?"
   radios nested beneath. Its own wrapper (not .checkbox-list, whose >div grid
   would sit the radios beside the checkbox instead of under it). */
.card-mode-list {
  margin: var(--space-2) 0;
}

.card-mode-item {
  padding: var(--space-1) 0;
}

.card-mode-item>label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--font-size-base);
}

.card-mode {
  border: 0;
  margin: 2px 0 var(--space-1);
  /* Indent under the card's checkbox. */
  padding: 0 0 0 26px;
}

.card-mode-legend {
  font-size: var(--font-size-sm);
  color: var(--muted);
  padding: 0;
  margin-bottom: 2px;
}

.card-mode-choice {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  margin-right: var(--space-4);
  font-size: var(--font-size-sm);
}

/* Bottom-align a checkbox cell with the input next to it in a .form-row grid. */
.align-end {
  align-self: end;
}

.crud-section-note {
  color: var(--muted);
  font-size: var(--font-size-sm);
  margin-top: 0;
}

.field-label {
  font-size: var(--font-size-sm);
  color: var(--muted);
  margin-bottom: var(--space-1);
  display: block;
  font-weight: 500;
}

.cadence-more-toggle {
  display: inline-block;
  margin-top: var(--space-1);
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent);
  font: inherit;
  font-size: var(--font-size-sm);
  cursor: pointer;
}

.cadence-more-toggle:hover {
  text-decoration: underline;
}

input[type=text],
input[type=number],
input[type=date],
input[type=password],
select {
  width: 100%;
  padding: 10px var(--space-3);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-md);
  font: inherit;
  background: var(--panel);
}

input:focus,
select:focus {
  outline: 3px solid var(--accent-soft);
  outline-offset: 0;
  border-color: var(--accent);
}

/* Manage list pages: compact action bar (the "+ Add …" toggle + optional Import
   link) sitting above the collapsible add form, so the page opens focused on the
   existing rows. */
.manage-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;   /* keep the Add button + "Import from YNAB" link on one baseline (feedback §Buttons) */
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

/* The leading "+" rotates into an "×" while the form is open — a quiet cue that the
   button now collapses what it opened. Driven off aria-expanded (set by app.js). */
.add-toggle-ico {
  width: 18px;
  height: 18px;
  transition: transform .18s ease;
}

[data-add-toggle][aria-expanded="true"] .add-toggle-ico {
  transform: rotate(45deg);
}

/* Collapsible add-form region. Animates height via the grid-template-rows 0fr↔1fr
   technique (no JS height measurement). It ships expanded and becomes collapsible
   only once app.js adds .is-collapsible, so the form stays usable with JS disabled.
   visibility:hidden when closed drops the collapsed fields from the tab order and
   from screen readers; it transitions discretely, so it holds "visible" through the
   whole close animation and flips only at the end. */
.add-region.is-collapsible {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows .18s ease;
}

.add-region.is-collapsible.is-open {
  grid-template-rows: 1fr;
}

.add-region.is-collapsible > .add-region-inner {
  min-height: 0;
  overflow: hidden;
  visibility: hidden;
  transition: visibility .18s;
}

.add-region.is-collapsible.is-open > .add-region-inner {
  visibility: visible;
}

@media (prefers-reduced-motion: reduce) {
  .add-region.is-collapsible,
  .add-region.is-collapsible > .add-region-inner,
  .add-toggle-ico { transition: none; }
}

.add-block {
  background: var(--surface-subtle);
  padding: var(--space-4);
  border-radius: var(--radius-md);
  border: 1px solid var(--rule);
  margin-bottom: var(--space-4);
}

.add-block h3 {
  margin: 0 0 var(--space-2);
  font-size: var(--font-size-xs);
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .08em;
  font-weight: 700;
}

.field-error {
  color: var(--bad);
  font-size: var(--font-size-sm);
  display: block;
  margin-top: var(--space-1);
}

/* Form-field help text (ModelForm help_text), rendered under the input. */
.field-help {
  color: var(--muted);
  font-size: var(--font-size-sm);
  display: block;
  margin-top: var(--space-1);
}

/* 9. BUTTONS =================================================
   Four-tier hierarchy (brief §Buttons):
     .btn          → secondary, soft lavender fill (the default; Ken reverted the
                     clean-outline experiment 2026-07-17)
     .btn-primary  → filled brand purple (one primary action per view)
     .btn-danger   → soft red fill (destructive; reverted to original 2026-07-17)
     .btn-ghost    → tertiary, text only (Cancel / Back)
   "Avoid making every button purple" — purple is reserved for the primary. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 44px;   /* slightly larger touch target (feedback §Buttons, §Tables) */
  padding: 0 var(--space-4);
  border-radius: var(--radius-md);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  border: 1px solid transparent;
  background: var(--accent-soft);
  color: var(--accent-soft-ink);
  text-decoration: none;
  line-height: 1.2;
  box-shadow: 0 1px 1px rgba(20, 22, 60, .03);
  transition: background .15s ease, border-color .15s ease, box-shadow .15s ease, transform .15s ease;
}

/* Secondary hover: deepen the lavender fill, plus a gentle lift + soft shadow rather
   than a heavy dark border — "feel less like Bootstrap." */
.btn:hover {
  background: #dad8f0;
  box-shadow: 0 2px 8px rgba(20, 22, 60, .08);
  transform: translateY(-1px);
}

.btn:active {
  transform: translateY(0);
  box-shadow: 0 1px 1px rgba(20, 22, 60, .03);
}

/* A disabled control (the import picker's Continue before anything is ticked; a
   submit mid-flight via app.js). pointer-events also mutes the :hover rules. */
.btn:disabled {
  opacity: 0.55;
  pointer-events: none;
}

.btn-primary {
  background: var(--accent);
  color: #ffffff;
  border-color: var(--accent);
  box-shadow: 0 1px 2px rgba(59, 59, 151, .18);
}

/* Richer primary hover: a deeper, wider accent shadow so the main action feels tactile
   (feedback §Buttons — "richer hover state"). */
.btn-primary:hover {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
  box-shadow: 0 6px 16px rgba(59, 59, 151, .30);
  transform: translateY(-1px);
}

.btn-primary:active {
  transform: translateY(0);
  box-shadow: 0 1px 2px rgba(59, 59, 151, .18);
}

/* Tertiary: text-only, no chrome until hover. For Cancel / Back / low-stakes exits. */
.btn-ghost {
  background: none;
  border-color: transparent;
  color: var(--accent);
}

.btn-ghost:hover {
  background: var(--accent-soft);
  border-color: transparent;
}

.btn-sm {
  min-height: 34px;
  padding: 0 var(--space-3);
  font-size: var(--font-size-sm);
}

/* Destructive: a soft red fill at rest (original; Ken reverted the neutral-border
   experiment 2026-07-17), deepening on hover. */
.btn-danger {
  color: var(--bad);
  background: var(--bad-soft);
  border-color: transparent;
}

.btn-danger:hover {
  background: #f8cdd1;
  border-color: transparent;
  color: var(--bad);
}

/* A borderless text button — used for inline row actions (Edit, etc.). */
.btn-link {
  background: none;
  border: 0;
  color: var(--accent);
  cursor: pointer;
  font: inherit;
  font-weight: 600;
  padding: 0;
  text-decoration: none;
}

.btn-link:hover {
  text-decoration: underline;
}

.btn-link.danger {
  color: var(--bad);
}

form.inline {
  display: inline;
}

/* In-flight feedback + double-submit protection ============= */
/* htmx sets .htmx-request on the element that fired a request, for its whole
   duration. Block re-clicks on it (notably ↻ Refresh, which runs several sequential
   YNAB calls) so a slow request can't be double-fired, and dim it for feedback. */
form.htmx-request,
button.htmx-request,
.htmx-request.btn {
  pointer-events: none;
}

button.htmx-request,
.htmx-request.btn {
  opacity: 0.75;
}

/* An indicator (spinner) stays hidden until its controlling element is mid-request,
   then shows. Standard htmx pattern — htmx adds .htmx-request to the requesting
   element (and any hx-indicator target). */
.htmx-indicator {
  display: none;
}

.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
  display: inline-block;
}

/* Real (non-htmx) form navigations — Stripe checkout/portal, bill import — get their
   submit control disabled on first submit by app.js, so a second click can't create a
   duplicate Stripe session or duplicate bills. Show it as busy. */
.btn.is-submitting {
  opacity: 0.75;
  cursor: progress;
}

/* Spinner: a small ring in the current text color, so it inherits button ink.
   Standalone inside the ↻ Refresh button (wrapped in .htmx-indicator), and injected
   via ::before on a submitting button. Both live in an inline-flex .btn, so spacing
   comes from the button's own gap. */
.spinner,
.btn.is-submitting::before {
  width: 0.85em;
  height: 0.85em;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: ml-spin 0.6s linear infinite;
}

.btn.is-submitting::before {
  content: "";
  display: inline-block;
}

@keyframes ml-spin {
  to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .spinner,
  .btn.is-submitting::before { animation: none; }
  /* Drop the hover lifts + fades for users who ask for reduced motion; the color
     and shadow cues still convey state. */
  .btn,
  .btn:hover,
  .btn-primary:hover,
  .fc-metric,
  .list-table tbody tr td { transition: none; }
  .btn:hover,
  .btn-primary:hover { transform: none; }
}

/* 10. TABLES (list pages) ==================================== */
.list-table {
  width: 100%;
  border-collapse: collapse;
}

.list-table th,
.list-table td {
  /* Comfortable rows with generous horizontal padding so the tables read as product
     surfaces, not dense admin grids (feedback §Tables). Vertical padding trimmed to
     --space-3 (Ken, 2026-07-17). */
  padding: var(--space-3) var(--space-5);
  text-align: left;
  font-size: var(--font-size-base);
}

/* Center cell content vertically now that rows are tall — keeps the Edit/Deactivate
   actions and amounts aligned to the row's middle. */
.list-table td {
  vertical-align: middle;
}

/* Column headers sit tight to their rows, and read as calm sentence-case labels rather
   than shouty uppercase micro-caps (feedback §Typography — "Sentence Case table headers").
   Overrides the shared uppercase §3 HEADINGS treatment for list tables only. */
.list-table th {
  padding-top: var(--space-2);
  padding-bottom: var(--space-2);
  text-transform: none;
  letter-spacing: normal;
  color: var(--muted);
}

/* Subtle hover across a data row (brief §Tables — "product interfaces, not admin
   grids"), fading in so the interface feels responsive. Skips the full-width inline
   edit row, which has no per-cell hover meaning. */
.list-table tbody tr td {
  transition: background .12s ease;
}

.list-table tbody tr:hover td {
  background: var(--surface-subtle);
}

/* Amounts read stronger than the surrounding text. */
.list-table td.num {
  font-weight: 600;
  color: var(--ink);
}

/* When the manage Edit link jumps to #row-<id>, keep the row clear of the mobile
   sticky topbar (and off the very top edge on desktop). */
.list-table tr[id] {
  scroll-margin-top: 64px;
}

.list-table th {
  /* Typography is set by §3 HEADINGS so all column headers match. */
  border-bottom: 1px solid var(--rule);
}

.list-table th.num,
.list-table td.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.list-table th.center,
.list-table td.center {
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.list-table td.actions {
  text-align: right;
  white-space: nowrap;
}

.list-table tr+tr td {
  border-top: 1px solid var(--rule-soft);
}

.list-table tr.inactive td {
  color: var(--muted);
}

/* Budgets list: equal-width action buttons (right-aligned via td.actions) so the
   picker reads as a tidy column with aligned right edges, instead of buttons of
   different widths floating mid-cell. */
.budget-action {
  min-width: 8.5em;
}

.list-header {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
  min-height: 1px;
}

.list-empty {
  color: var(--muted);
  text-align: center;
  padding: var(--space-5);
}

/* YNAB import: full-width, grouped + collapsible category picker.
   Full width (no max cap) so the category name field has room — the name column is
   the only `1fr`, so it absorbs all leftover width and shows the full YNAB category
   name. Each group is one grid so the header row aligns with the inputs, and every
   group grid is the same width -> columns line up across groups. */
.page-wide {
  max-width: none;
}

.import-group {
  border: 1px solid var(--rule);
  border-radius: var(--radius-md);
  margin-bottom: var(--space-3);
  overflow: hidden;
}

.import-group>summary {
  cursor: pointer;
  font-weight: 600;
  padding: var(--space-3) var(--space-4);
  background: var(--surface-subtle);
  user-select: none;
}

.import-group-count {
  color: var(--muted);
  font-weight: 400;
  font-size: 0.85em;
  margin-left: var(--space-2);
}

/* JS-injected "Collapse All / Expand All" bar above the import groups (progressive
   enhancement; absent without JS, and only when there's more than one group). */
.import-groups-toolbar {
  display: flex;
  justify-content: flex-end;
  margin-bottom: var(--space-2);
}

.import-groups-toggle {
  background: none;
  border: none;
  padding: 0;
  color: var(--accent);
  font: inherit;
  font-size: 0.85em;
  font-weight: 600;
  cursor: pointer;
}

.import-groups-toggle:hover {
  text-decoration: underline;
}

.import-dupe-note {
  display: block;
  margin-top: var(--space-1);
  color: var(--muted);
  font-size: 0.8em;
}

/* Step-1 checkbox picker: a light vertical list of categories inside each master
   group (the details step keeps the wider .import-grid). */
.import-picker-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-3) var(--space-4);
}

.import-picker-row {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  flex-wrap: wrap;
  cursor: pointer;
  /* Each option is a tappable row: padding + radius let the selected state read as a
     highlighted pill. Negative margin extends the tint into the list's own padding
     while keeping the checkbox aligned with the rows above/below. */
  padding: var(--space-2) var(--space-3);
  margin: 0 calc(-1 * var(--space-3));
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  transition: background .15s ease, border-color .15s ease;
}

.import-picker-row:hover {
  background: var(--surface-subtle);
}

/* Selected categories get a clear accent tint + border so ticked items stand out from
   the list (brief §Import Pages — "clearly highlight selected items"; guided review,
   not a spreadsheet). Source-ordered after :hover so a checked row stays accent on hover. */
.import-picker-row:has(input:checked) {
  background: var(--accent-soft);
  border-color: var(--accent);
}

.import-picker-row .import-dupe-note {
  /* Inline beside the name here, not a full-width block under a grid cell. */
  display: inline;
  margin-top: 0;
}

/* JS-injected "Select All" / "Clear" bar (progressive enhancement; absent without JS). */
.import-select-all {
  display: flex;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}

.import-select-all button {
  background: none;
  border: none;
  padding: 0;
  color: var(--accent);
  font-size: 0.85em;
  cursor: pointer;
}

.import-select-all button:hover {
  text-decoration: underline;
}

/* JS-injected "select at least one" hint above the picker's Continue button;
   hidden the moment any category is ticked. */
.import-picker-hint {
  color: var(--muted);
  font-size: 0.85em;
  margin: var(--space-2) 0 0;
}

/* Payee typeahead (forms.PayeeSearchWidget + payee-picker.js): a hidden id input,
   a search box, and an absolutely-positioned suggestion list under it. */
.payee-picker {
  position: relative;
  display: block;
}

.payee-results {
  position: absolute;
  z-index: 30;
  left: 0;
  right: 0;
  top: calc(100% + 2px);
  margin: 0;
  padding: var(--space-1) 0;
  list-style: none;
  background: #ffffff;
  border: 1px solid var(--rule);
  border-radius: var(--radius-md);
  box-shadow: 0 6px 18px rgba(20, 20, 43, 0.12);
  max-height: 260px;
  overflow-y: auto;
}

.payee-results li {
  padding: 6px var(--space-3);
  cursor: pointer;
}

.payee-results li:hover,
.payee-results li.is-active {
  background: var(--accent-soft);
}

.payee-results li.payee-msg {
  color: var(--muted);
  cursor: default;
  background: none;
}

.import-grid {
  display: grid;
  width: 100%;
  gap: var(--space-2) var(--space-3);
  /* Align cells to the top, not center: a Name cell can grow a second line (the
     "N already from this category" dupe-note or a field error) and centering would
     drag the checkbox + other inputs down off the field's first line. */
  align-items: start;
  padding: var(--space-3) var(--space-4);
}

.import-grid.bills {
  grid-template-columns: 36px minmax(160px, 1fr) 120px 190px 72px 170px;
}

.import-grid.necessities {
  /* No trailing 1fr: the name column is capped so the amount + period fields sit right
     next to the name instead of being pushed to the far right. Extra row width stays
     empty on the right. */
  grid-template-columns: 36px minmax(260px, 560px) 120px 160px;
}

/* A logical row whose cells join the group grid, so header + inputs share columns. */
.import-rowc {
  display: contents;
}

.import-head-cell {
  font-weight: 600;
  font-size: 0.85em;
  color: var(--muted);
}

.import-check {
  display: flex;
  justify-content: center;
  align-items: center;
  /* Match the input's outer height (14.5px line × 1.45 + 7px×2 padding + 1px×2
     border ≈ 37px) so the checkbox centers on the field's first line even though
     the grid now top-aligns cells. */
  min-height: 37px;
}

.import-grid input[type="text"],
.import-grid input[type="date"],
.import-grid select {
  width: 100%;
}

/* Batch dupe-confirm banner shown on the import screen when selected rows duplicate
   an existing category (the import is held until "Import anyway"). */
.import-confirm {
  background: var(--surface-subtle);
  border: 1px solid var(--rule-strong);
  border-left: 3px solid var(--warn);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  margin-bottom: var(--space-4);
}

.import-confirm h3 {
  margin: 0 0 var(--space-2);
  font-size: var(--font-size-xs);
  color: var(--warn);
  text-transform: uppercase;
  letter-spacing: .08em;
  font-weight: 700;
}

.import-confirm-list {
  margin: var(--space-2) 0 0;
  padding-left: var(--space-4);
}

/* Manage-page duplicate prompt (bills / necessities add forms). Reuses the
   .add-block shell but takes the import screen's amber left-accent + heading so
   "possible duplicate" reads consistently everywhere it appears. */
.dupe-confirm {
  border-left: 3px solid var(--warn);
}

.dupe-confirm h3 {
  color: var(--warn);
}

/* 11. TOAST ================================================== */
.toast {
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  margin-bottom: var(--space-3);
  font-size: var(--font-size-base);
  background: var(--good-bg);
  color: var(--good-bg-ink);
}

.toast-error {
  background: var(--bad-soft);
  color: var(--bad-soft-ink);
}

.toasts {
  list-style: none;
  padding: 0;
  margin: 0 0 var(--space-3);
}

/* Trial-ending banner (base.html): shown site-wide in the last days of a cardless
   trial, nudging the user into the Customer Portal to add a card before day 14.
   Amber left-accent matches the .import-confirm / .dupe-confirm warn pattern. */
.trial-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  background: var(--surface-subtle);
  border: 1px solid var(--rule-strong);
  border-left: 3px solid var(--warn);
  border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-3);
  margin-bottom: var(--space-3);
}

.trial-banner p {
  margin: 0;
  text-wrap: pretty;
}

/* 12. FORECAST GRID + WEEK CARDS ============================= */

/* Summary chart card (_chart.html). Geometry comes pre-computed from
   forecast/chart.py; everything visual lives here. Chrome matches the .week
   cards (panel/rule/radius/shadow). Full width of the main column — long
   horizons need the horizontal room (the old 560px cap crowded the line and
   axis once horizons grew). The plot height is fixed in CSS (.fc-svg), NOT
   proportional to width. */
/* Dashboard metric cards above the chart (brief §Forecast — Dashboard Metrics).
   Four at-a-glance numbers: Current Balance, Lowest Balance, Bills Due Before
   Lowest Balance, Next Paycheck. Values come from the same chart context the plot
   uses, so they swap with the grid on every HTMX edit. */
/* .fc-metrics is the query CONTAINER and holds no layout of its own; .fc-metrics-row
   is the grid. The split is required — a container query can style a container's
   descendants but never the container itself.

   This is a CONTAINER query, not a viewport media query, ON PURPOSE. These cards sit
   in the content column, which is 336px narrower than the viewport (272px sidebar +
   64px .main padding), so a viewport breakpoint fires ~336px late: the old
   `max-width: 768px` rule left 4-up in force down to a 433px column, which cut
   "$3,940.00" off under the neighbouring card from 769-885px and crowded it to 1051px
   (measured, TODO.md). Sizing on the row's own width removes the mismatch entirely —
   there is no viewport number here to keep in sync with the shell's dimensions. */
.fc-metrics {
  container-type: inline-size;
  /* More air below the KPI row before the chart (feedback §Forecast — "more whitespace
     between forecast cards"). */
  margin-bottom: var(--space-5);
}

.fc-metrics-row {
  display: grid;
  /* 2-up is the floor; 4-up only once the row can actually seat four cards. 840px is
     the width at which the widest realistic value ("$123,456.78" = 147px at 25px/700)
     clears the card's 50px of padding+border in a quarter track: 4 x (147+50) + 3 gaps.
     Below it the cards go 2x2 at full size rather than shrinking the numbers. */
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-3);
}

@container (min-width: 840px) {
  .fc-metrics-row {
    grid-template-columns: repeat(4, 1fr);
    gap: var(--space-4);
  }
}

.fc-metric {
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  padding: var(--space-4) var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
  transition: box-shadow .15s ease, border-color .15s ease;
}

/* A quiet lift on hover so the dashboard cards feel alive (design feedback). Shadow
   only, no transform — these aren't clickable, so they shouldn't imply it. */
.fc-metric:hover {
  box-shadow: 0 4px 16px rgba(20, 22, 60, .08);
  border-color: var(--rule-strong);
}

.fc-metric-label {
  font-size: var(--font-size-xs);
  font-weight: 600;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--muted);
}

.fc-metric-value {
  /* ~15% larger than the base xl so the headline KPIs carry the dashboard
     (feedback §Typography, §Forecast — "increase primary KPI values by ~15%"). */
  font-size: 25px;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  line-height: 1.15;
}

.fc-metric-value.warn {
  color: var(--warn);
}

.fc-metric-value.bad {
  color: var(--bad);
}

.fc-metric-sub {
  font-size: var(--font-size-sm);
  color: var(--muted);
}

.fc-card {
  --fc-line: var(--sidebar-bg);
  --fc-accent: var(--good-bg);
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  padding: var(--space-5);
  margin-bottom: var(--space-5);
}

.fc-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-3);
}

.fc-head-end {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
}

/* Collapse / expand. The expanded card gets a labeled pill button; the
   collapsed state IS one big button (the whole strip toggles), with the same
   pill rendered (decoratively) at its end so the affordance reads clearly. */
.fc-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  height: 28px;
  padding: 0 var(--space-2);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-pill);
  background: var(--surface-subtle);
  color: var(--ink);
  font-size: var(--font-size-xs);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  cursor: pointer;
}

.fc-toggle:hover {
  background: var(--sidebar-bg);
  border-color: var(--sidebar-bg);
  color: #fff;
}

.fc-toggle-label {
  line-height: 1;
}

.fc-chevron {
  font-size: 10px;
  line-height: 1;
}

.fc-strip {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  width: 100%;
  padding: var(--space-3) var(--space-4);
  font: inherit;
  text-align: left;
  color: var(--ink);
  cursor: pointer;
}

.fc-strip:hover {
  background: var(--surface-subtle);
}

.fc-strip .fc-toggle-hint {
  margin-left: auto;
}

/* The hint pill is decorative (the strip itself is the button), so light it up
   when the whole strip is hovered, not only when the cursor is over the pill. */
.fc-strip:hover .fc-toggle-hint {
  background: var(--sidebar-bg);
  border-color: var(--sidebar-bg);
  color: #fff;
}

.fc-strip-stats {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-2);
  font-size: var(--font-size-sm);
  color: var(--muted);
}

.fc-strip-stats strong {
  color: var(--ink);
  font-weight: 700;
}

.fc-strip-stats strong.warn {
  color: var(--warn);
}

.fc-strip-stats strong.bad {
  color: var(--bad);
}

.fc-strip-sep {
  color: var(--rule-strong);
}

.fc-title {
  font-size: var(--font-size-xs);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 600;
}

.fc-bal {
  font-weight: 700;
  font-size: var(--font-size-lg);
  color: var(--ink);
}

/* The pill anchors to the plot box, not the card, so header/footer height
   can't skew its inline left/top percentages. */
.fc-plot {
  position: relative;
}

/* Fixed height: the SVG viewBox is a coordinate space, not an aspect ratio
   (height:auto would make the full-width card ~330px tall again). The
   resulting non-uniform stretch is safe because strokes use
   vector-effect="non-scaling-stroke" and everything round (low marker, pill)
   is an HTML overlay — never put circles or fixed-stroke shapes in this SVG. */
.fc-svg {
  width: 100%;
  height: 240px;
  display: block;
}

.fc-baseline {
  stroke: var(--rule-strong);
  stroke-width: 1;
}

.fc-line {
  fill: none;
  stroke: var(--fc-line);
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Gradient stops: stop-color as a CSS *property* accepts var(), the SVG
   attribute doesn't — so the stops are classed and colored here. */
.fc-stop-a {
  stop-color: var(--fc-accent);
  stop-opacity: .35;
}

.fc-stop-b {
  stop-color: var(--fc-accent);
  stop-opacity: 0;
}

/* Low-point marker: an HTML overlay (dot + ::after ring) anchored at the same
   --fc-flag-* percentages as the pill (set inline on .fc-plot). It lives
   outside the SVG so the fixed-height stretch can't squash it into an ellipse. */
.fc-low {
  position: absolute;
  left: var(--fc-flag-left);
  top: var(--fc-flag-top);
  width: 9px;
  height: 9px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: var(--fc-line);
  pointer-events: none;
}

.fc-low::after {
  content: "";
  position: absolute;
  inset: -6px;
  border: 2px solid var(--fc-accent);
  border-radius: 50%;
}

.fc-low.warn::after {
  border-color: var(--warn);
}

.fc-low.bad::after {
  border-color: var(--bad);
}

/* The pill anchors at the low point's raw percentages (inline custom props
   from chart.py) but is clamped in CSS, where its rendered width is known:
   translate(-50%) centers it, so the left bound must leave half a pill of
   room or it overflows the card on narrow plots (caught on mobile). The
   bounds are ~half the pill's width / its full height in rem. */
.fc-flag {
  position: absolute;
  left: clamp(5.5rem, var(--fc-flag-left), calc(100% - 5.5rem));
  top: clamp(2.4rem, var(--fc-flag-top), 100%);
  transform: translate(-50%, -135%);
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  background: var(--fc-line);
  color: #fff;
  font-size: var(--font-size-xs);
  font-weight: 600;
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-card);
  white-space: nowrap;
  pointer-events: none;
}

.fc-flag.warn {
  background: var(--warn);
}

.fc-flag.bad {
  background: var(--bad);
}

.fc-fdot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--fc-accent);
}

/* Per-week axis under the plot: date over rounded amount, one tick per kept
   point (chart.py pre-thins long horizons). Ticks center on their point's x;
   the first/last edge-align instead so they can't overflow the card. Type is
   deliberately below --font-size-xs — these are chart micro-labels, not body
   copy; exact amounts live in the week grid. */
.fc-axis {
  position: relative;
  height: 28px;
  margin-top: var(--space-2);
}

.fc-tick {
  position: absolute;
  top: 0;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
}

.fc-tick:first-child {
  transform: none;
  align-items: flex-start;
}

.fc-tick:last-child {
  transform: translateX(-100%);
  align-items: flex-end;
}

.fc-tick-d {
  font-size: 10px;
  color: var(--muted);
  white-space: nowrap;
}

.fc-tick-v {
  font-size: 10.5px;
  font-weight: 600;
  color: var(--ink);
  opacity: .72;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.fc-tick-v.warn {
  color: var(--warn);
  opacity: 1;
}

.fc-tick-v.bad {
  color: var(--bad);
  opacity: 1;
}

/* Phone widths fit roughly every other tick, so the .minor ones (marked in
   chart.py, which always keeps Today, the low week, and the horizon end)
   drop out. 480px, not the 768px layout breakpoint: at tablet widths the
   full-width card still has room for the complete axis. */
@media (max-width: 480px) {
  .fc-tick.minor {
    display: none;
  }
}

.forecast-grid {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(420px, 1fr);
  gap: var(--space-5);   /* more space between week columns (feedback §Forecast) */
  overflow-x: auto;
  padding-bottom: var(--space-2);

  /* Per-section min-heights to align rows across week columns. The
     --max-* vars are set inline on .forecast-grid from the view. */
  --row-h: 40px;
  --section-label-h: 40px;
}

/* With a single week the 1fr column would stretch to the full main width, which
   looks oversized; cap it at a normal card and left-align instead of filling. */
.forecast-grid.single-week {
  grid-auto-columns: minmax(420px, 480px);
  justify-content: start;
}

.week {
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  display: flex;
  flex-direction: column;
  min-height: 100%;
  overflow: hidden;
}

/* The dark slate cap over each week's column (original; Ken reverted the light-strip
   experiment 2026-07-17). Its white text reads as a distinct "cap" that tells the weeks
   apart, and it stays darker than the green Starting-funds banner directly below so the
   eye still lands on the money. */
.week-head {
  padding: var(--space-4) var(--space-4);
  border-bottom: 1px solid var(--rule-strong);
  background: var(--week-head-bg);
  color: var(--sidebar-ink);
}

.week-head strong {
  font-weight: 700;
  font-size: var(--font-size-md);
  letter-spacing: -0.01em;
}

/* Secondary lines: white on the slate bar (the range/pay dates under the week title). */
.week-head .range {
  font-size: var(--font-size-sm);
  color: var(--sidebar-ink);
  margin-top: 3px;
}

.week-head .pay {
  font-size: var(--font-size-sm);
  margin-top: 3px;
  color: var(--sidebar-ink);
}

.week-body {
  padding: 0;
  flex: 1;
  display: flex;
  flex-direction: column;
}

.section {
  margin: 0;
  border-bottom: 1px solid var(--rule);
}

.section:last-child {
  border-bottom: 0;
}

.section-paychecks {
  min-height: calc(var(--section-label-h) + max(var(--max-paychecks), 1) * var(--row-h));
}

.section-bills {
  min-height: calc(var(--section-label-h) + max(var(--max-bills), 1) * var(--row-h));
}

.section-necessities {
  min-height: 38px;
}

/* Section labels — YNAB-style banded header row. Typography from §3. */
.section-label {
  background: var(--section-band);
  padding: 8px var(--space-3);
  border-bottom: 1px solid var(--rule);
}

/* Collapsible section headers. Every one is a real <button> posting to
   forecast:toggle_section, never a <details>/<summary>: #fc-grid is swapped whole on
   every edit, so client-side toggle state would be wiped on the next swap. A button
   also takes hx-disabled-elt (a <summary> can't be disabled, so it invites the
   double-submit the rest of the grid guards against) and carries aria-expanded on a
   natively focusable control.

   Applied ON TOP of the header's own class (.section-label, .necessities-summary,
   .savings-summary, .pending-summary), which keeps supplying the band, padding and
   typography — this rule only undoes the UA button styling and lays the row out. Three
   traps, all from this rule sitting LATER in the file than the classes it decorates, so
   any property it "resets" beats §3 and the header's own class:
   - reset `font-family` only, never the `font` shorthand — the shorthand also clobbers
     font-size/weight, which §3 sets on .section-label/.necessities-summary up at the
     top, so they would lose to a rule that only meant "not Arial";
   - set no background — every header class supplies its own band, and a reset here
     would paint over all four;
   - set no color — §3 gives every header --heading-color, and a `color: inherit`
     here silently repainted the .section-label ones in --ink (shipped that way
     with the collapse feature; caught by measuring them in a browser, 2026-07-23). */
.section-toggle {
  width: 100%;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  appearance: none;
  border: 0;
  border-bottom: 1px solid var(--rule);
  border-radius: 0;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  user-select: none;
}

/* The label takes the slack so the amount (when shown) sits hard right. */
.section-toggle-label {
  flex: 1;
}

/* CC Balance's "(incl. $X pending)" aside — prose, so it opts out of the uppercase
   heading treatment the header inherits from §3. */
.section-toggle-note {
  text-transform: none;
  letter-spacing: normal;
  font-weight: 400;
  color: var(--muted);
}

/* The caret is DRAWN, not a font glyph (it was &#9656;). A glyph's ink sits low in its
   em box and its size follows the header's font-size, so the six carets rendered at
   different sizes and all of them hung below the text's optical center. A bordered box
   is the same 8px triangle everywhere and centers on the flex cross-axis exactly. */
.section-toggle .caret {
  flex: none;
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 4px 0 4px 7px;
  border-color: transparent transparent transparent var(--muted);
  transition: transform .15s ease;
}

.section-toggle[aria-expanded="true"] .caret {
  transform: rotate(90deg);
}

/* Collapsed amounts are the section total, so they keep the grid's tabular figures
   and opt out of the uppercase heading treatment the header inherits from §3. */
.section-toggle .money {
  font-size: var(--font-size-sm);
  text-transform: none;
}

/* .section-paychecks/.section-bills reserve room for the tallest column's rows so
   the cards stay row-aligned side by side. A collapsed section has no rows to align,
   and the collapse is global across columns, so that floor must lift or the section
   would keep its full height with nothing in it. */
.section-collapsed {
  min-height: 0;
}

/* A collapsed section is just its header, and .section already draws a bottom rule —
   so the header's own border-bottom doubles it into a 2px seam (3px above Necessities,
   which used to add a border-top of its own). Drop it while collapsed so every seam in
   the card is one hairline, open or shut. */
.section-collapsed > .section-toggle {
  border-bottom: 0;
}

/* Starting funds: YNAB Ready-to-Assign green banner. */
.line.headline {
  background: var(--good-bg);
  color: var(--good-bg-ink);
  padding: 10px var(--space-3);
  font-weight: 700;
  border-bottom: 1px solid var(--rule);
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: var(--font-size-md);
}

.line.headline .money {
  font-size: var(--font-size-md);
}

/* Generic row inside a section. */
.line {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-2);
  padding: 7px var(--space-3);
  font-variant-numeric: tabular-nums;
  font-size: var(--font-size-base);
  border-bottom: 1px solid var(--rule);
}

.line:last-child {
  border-bottom: 0;
}

.line.dim {
  color: var(--muted);
}

.line.bill.paid .name,
.line.paycheck.paid .name,
.line.override-row.paid .name,
.line.planned-card-payment.paid .name {
  text-decoration: line-through;
  color: var(--muted);
}

.line.bill.paid .money,
.line.paycheck.paid .money,
.line.override-row.paid .money,
.line.planned-card-payment.paid .money {
  color: var(--muted);
}

.line.bill.paid .paid-toggle,
.line.override-row.paid .paid-toggle,
.line.planned-card-payment.paid .paid-toggle {
  color: var(--good);
}

.line.bill.autopay:not(.paid) .name {
  color: var(--ink);
  opacity: 0.85;
}

/* Click-to-edit values in the grid are <button type=button hx-get> styled to read as
   inline text with a dotted-underline affordance (they were <a> without href before —
   not keyboard-reachable; now real buttons). Strip the native button chrome so they
   sit in the row text, keep the pointer cursor, and let each rule below re-add its own
   dotted border-bottom + color (source order keeps those from being reset here). */
.paycheck-date,
.bill-date-edit,
.bill-amount-edit,
.necessity-amount-edit,
.planned-payment-amount-edit {
  cursor: pointer;
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
}

.paycheck-date {
  color: var(--accent);
  text-decoration: none;
  border-bottom: 1px dotted var(--accent);
}

.paycheck-date:hover {
  color: var(--accent);
  border-bottom-style: solid;
}

.line.paycheck.paid .paycheck-date {
  color: var(--muted);
  border-bottom-color: var(--muted);
}

.paycheck-edit-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
}

.paycheck-edit-field {
  display: flex;
  flex-direction: column;
  flex: 1 1 0;
  min-width: 0;
}

.paycheck-edit-actions {
  display: flex;
  gap: var(--space-1);
  align-items: center;
}

.paycheck-edit-row input[type=date],
.paycheck-edit-row input[type=text] {
  font-size: var(--font-size-sm);
  padding: var(--space-1) 6px;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-sm);
  width: 100%;
  box-sizing: border-box;
}

.paycheck-edit-reset {
  margin: var(--space-1) var(--space-3) var(--space-2);
}

/* Inline bill amount-override editor */
.bill-edit-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-2) var(--space-3);
}

.bill-edit-label {
  flex: 1 1 auto;
  min-width: 0;
}

.bill-edit-row input[type=text],
.bill-edit-row input[type=date] {
  font-size: var(--font-size-sm);
  padding: var(--space-1) 6px;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-sm);
  width: 6em;
  text-align: right;
}

.bill-edit-row input[type=date] {
  width: auto;
  text-align: left;
}

.bill-edit-reset {
  margin: var(--space-1) var(--space-3) var(--space-2);
}

/* Clickable amount on a bill row */
.bill-amount-edit,
.planned-payment-amount-edit {
  text-decoration: none;
  border-bottom: 1px dotted var(--bad);
}

.bill-amount-edit:hover,
.planned-payment-amount-edit:hover {
  border-bottom-style: solid;
}

.line.bill.paid .bill-amount-edit,
.line.planned-card-payment.paid .planned-payment-amount-edit {
  border-bottom-color: var(--muted);
}

.bill-date-edit {
  color: var(--accent);
  text-decoration: none;
  border-bottom: 1px dotted var(--accent);
}

.bill-date-edit:hover {
  border-bottom-style: solid;
}

/* Credit-card / autopay indicators */
.bill-cc-badge {
  font-size: 0.85em;
  vertical-align: -0.05em;
  margin-left: 2px;
  opacity: 0.85;
  cursor: help;
}

.bill-autopay-badge {
  cursor: help;
}

/* Clickable amount on a necessity row (per-week override) */
.necessity-amount-edit {
  text-decoration: none;
  border-bottom: 1px dotted var(--muted);
  color: inherit;
}

.necessity-amount-edit:hover {
  border-bottom-style: solid;
  color: var(--accent);
}

.necessity-edit-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-2) var(--space-3);
}

.necessity-edit-label {
  flex: 1 1 auto;
  min-width: 0;
}

.necessity-edit-row input[type=text] {
  font-size: var(--font-size-sm);
  padding: var(--space-1) 6px;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-sm);
  width: 6em;
  text-align: right;
}

.necessity-edit-reset {
  margin: var(--space-1) var(--space-3) var(--space-2);
}

.bills-total-row {
  /* Layout only. Heading typography comes from §3 HEADINGS. */
  padding: 10px var(--space-3);
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  background: var(--section-band);
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.money {
  font-variant-numeric: tabular-nums;
}

.money.neg {
  color: var(--bad);
}

.money.pos {
  color: var(--good);
}

.week-foot {
  border-top: 1px solid var(--rule);
  background: var(--surface-subtle);
}

/* CC owed row sits between Bills Total and Ending Balance. */
.cc-owed-row {
  padding: 8px var(--space-3);
  border-bottom: 1px solid var(--rule);
}

/* Carried-card debt line (informational; not part of the ending balance) and
   its "No payment scheduled in YNAB." fallback sentence. */
.card-debt-row {
  padding: 8px var(--space-3);
  border-bottom: 1px solid var(--rule);
}

/* The fallback note belongs to the debt row above it — no rule between the two. */
.card-debt-row:has(+ .card-debt-note) {
  border-bottom: 0;
}

.card-debt-note {
  margin: 0;
  padding: 0 var(--space-3) 8px;
  color: var(--muted);
  font-size: var(--font-size-sm);
  border-bottom: 1px solid var(--rule);
}

.card-payment-date {
  color: var(--muted);
  margin-right: var(--space-1);
  font-variant-numeric: tabular-nums;
}

/* Collapsible pending-charge breakdown under CC Balance. The header is a
   .section-toggle button (see §4); .cc-owed-row supplies its padding, and the header's
   own .section-toggle-label takes the slack so the amount stays hard right. */
.pending-summary {
  background: var(--surface-subtle);
}

.pending-breakdown {
  background: var(--surface-subtle);
  border-bottom: 1px solid var(--rule);
}

.pending-group {
  background: var(--panel);
}

.pending-group+.pending-group {
  border-top: 1px dashed var(--rule);
}

/* Top-align so the charge amount stays with the first line of a long,
   wrapping category name (the name+badge can span several lines). */
.pending-group-head {
  align-items: flex-start;
}

/* Name + tag flow as normal inline content so the pill wraps below a long
   name instead of floating vertically-centered beside the wrapped text. */
.pending-group-head .name {
  font-weight: 600;
}

/* Spacing lives on the name (not margin-left on the tag) so the pill wraps
   flush-left under the category name instead of indented by its own margin. */
.pending-group-head .name .pending-cat-name {
  margin-right: var(--space-2);
}

.pending-group-meta {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
}

/* Tag pills. The neutral palette (default + -cc) uses the page rule color since
   Moneylight has no dedicated "available zero" token. */
.pending-tag {
  display: inline-block;
  padding: 1px 8px;
  font-size: var(--font-size-xs);
  border-radius: var(--radius-pill);
  letter-spacing: .02em;
  background: var(--rule);
  color: var(--muted);
}

.pending-tag-covered {
  background: var(--good-bg);
  color: var(--good-bg-ink);
}

.pending-tag-cc {
  background: var(--rule);
  color: var(--muted);
}

.pending-tag-partial {
  background: var(--accent-soft);
  color: var(--accent-soft-ink);
}

.pending-tag-payment {
  background: var(--good-bg);
  color: var(--good-bg-ink);
}

.pending-item {
  padding-left: var(--space-5);
  font-size: var(--font-size-sm);
}

.pending-item-date {
  color: var(--muted);
  margin-right: var(--space-2);
  font-variant-numeric: tabular-nums;
}

/* Which card a pending charge is on. Only rendered for multi-card users; a
   small muted chip matching the badge idiom, cursor:help for its title. */
.pending-item-card {
  display: inline-block;
  padding: 0 6px;
  font-size: var(--font-size-xs);
  border-radius: var(--radius-pill);
  background: var(--rule);
  color: var(--muted);
  white-space: nowrap;
  cursor: help;
}

/* Per-card owed-balance strip at the top of the expanded breakdown. Only
   rendered for multi-card users (one card == the CC Balance total itself).
   Rows are each card's YNAB `balance` (working: cleared + uncleared) and do
   NOT include pending scheduled charges, so they sum to LESS than the CC
   Balance header when pending is nonzero -- which is what the caption's
   tooltip exists to explain. Don't call these "cleared" balances: that means
   something narrower in YNAB's own vocabulary. */
.card-balance-strip {
  padding: var(--space-2) var(--space-3);
  background: var(--panel);
  border-bottom: 2px solid var(--rule-strong);
}

.card-balance-intro {
  margin: 0 0 var(--space-1);
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--ink);
}

/* Only the pending-gap explainer variant carries a tooltip. */
.card-balance-intro[title] {
  cursor: help;
}

.card-balance-row {
  font-size: var(--font-size-sm);
}

.card-balance-row .name {
  font-weight: 600;
}

/* Caption at the top of the expanded breakdown. (The tag legend it used to carry now
   lives once, in the grid's "What the symbols mean" panel.) */
.pending-notes {
  padding: var(--space-3);
  background: var(--panel);
  border-bottom: 2px solid var(--rule-strong);
  font-size: var(--font-size-sm);
  color: var(--muted);
  line-height: 1.4;
}

.pending-notes-intro {
  margin: 0;
  color: var(--ink);
  font-weight: 600;
}

/* Reconciliation footer: Total scheduled = in Necessities + in CC Balance. */
.pending-reconcile {
  padding: 8px var(--space-3);
  font-size: var(--font-size-xs);
  color: var(--muted);
  background: var(--panel);
  border-top: 1px solid var(--rule);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
  justify-content: flex-start;
}

.pending-reconcile-part {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  font-variant-numeric: tabular-nums;
}

.pending-reconcile-part .money {
  color: var(--ink);
  font-weight: 600;
}

.pending-reconcile-sep {
  color: var(--muted);
  font-weight: 400;
}

/* Ending Balance row reuses the green Starting Funds banner styling
   (.line.headline). The `ending` modifier adds the threshold tint. */
.ending-row {
  border-bottom: 0;
}

.ending {
  font-weight: 700;
}

.ending.bad {
  color: var(--bad);
}

.ending.warn {
  color: var(--warn);
}

.badge {
  display: inline-block;
  padding: 1px var(--space-3);
  font-size: var(--font-size-xs);
  border-radius: var(--radius-pill);
  background: var(--bad);
  color: white;
  margin-left: 6px;
  vertical-align: middle;
  text-transform: capitalize;
}

/* The amber "getting low" week badge (the red default above stays "low"). Both
   states carry a text badge so neither is color-only. --warn is dark enough for
   white text. */
.badge-warn {
  background: var(--warn);
}

/* Paid toggle (☐/✓) on bills */
.paid-toggle {
  background: transparent;
  border: 0;
  cursor: pointer;
  padding: 0 2px;
  font-size: var(--font-size-md);
  line-height: 1;
  color: var(--muted);
}

.paid-toggle:hover {
  color: var(--good);
}

/* Collapsible necessities and savings. The header is a .section-toggle button (see §4);
   this supplies the band/rules and keeps the total hard right. Unlike Bills neither has
   a separate total row, so the amount shows in both states. */
.necessities-summary,
.savings-summary {
  /* Layout only. Heading typography comes from §3 HEADINGS. */
  background: var(--section-band);
  padding: 8px var(--space-3);
}

.necessities-breakdown,
.savings-breakdown {
  padding: 0;
  background: var(--panel);
}

.necessities-breakdown .line,
.savings-breakdown .line {
  padding-left: var(--space-5);
}

/* Ad-hoc override rows */
.override-row,
.line.planned-card-payment {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}

.override-row form,
.line.planned-card-payment form {
  display: inline;
}

.override-delete,
.override-edit {
  background: transparent;
  border: 0;
  color: var(--muted);
  cursor: pointer;
  padding: 0 2px;
  font-size: var(--font-size-lg);
  line-height: 1;
  text-decoration: none;
}

.override-edit:hover {
  color: var(--accent);
}

.override-delete:hover {
  color: var(--bad);
}

.override-edit-form,
.planned-payment-edit-form {
  margin: 0;
  padding: var(--space-2) var(--space-3);
  background: var(--surface-subtle);
  border-bottom: 1px solid var(--rule);
}

.override-edit-form .form-row,
.planned-payment-edit-form .form-row {
  grid-template-columns: 1fr;
  gap: 6px;
  margin: 0;
}

.override-edit-form input,
.override-edit-form select,
.planned-payment-edit-form input,
.planned-payment-edit-form select {
  font-size: var(--font-size-sm);
  padding: var(--space-1) 6px;
}

.add-override,
.add-planned-payment {
  padding: var(--space-2) var(--space-3);
  background: var(--surface-subtle);
  border-bottom: 1px solid var(--rule);
  border-top: 1px solid var(--rule);
  font-size: var(--font-size-sm);
}

.add-override summary,
.add-planned-payment summary {
  cursor: pointer;
  color: var(--accent);
  font-weight: 600;
  list-style: none;
}

.add-override summary::-webkit-details-marker,
.add-planned-payment summary::-webkit-details-marker {
  display: none;
}

/* The planned-payment summary carries a `title` explainer, so it uses the help
   cursor like the grid's other title tooltips (.bill-cc-badge, etc.). */
.add-planned-payment summary {
  cursor: help;
}

.add-override .form-row,
.add-planned-payment .form-row {
  grid-template-columns: 1fr;
  gap: 6px;
  margin: var(--space-2) 0;
}

.add-override input,
.add-override select,
.add-planned-payment input,
.add-planned-payment select {
  font-size: var(--font-size-sm);
  padding: var(--space-1) 6px;
}

/* Header controls cluster (horizon form + Refresh from YNAB). One wrapping flex row so
   the two controls read as a pair: Refresh sits beside Update and drops to its own line
   only when the row can't fit them. Wrapping, not a breakpoint — these live in the
   content column (336px narrower than the viewport), so any viewport media query would
   fire at the wrong width; see .fc-metrics above for the same reasoning. */
.header-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

/* Header sub-form (horizon + refresh) */
.header-form {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--font-size-base);
}

.header-form input[type=number] {
  width: 60px;
  padding: var(--space-1) 6px;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-sm);
  font: inherit;
}

/* Balance-chain footnote below the forecast grid (only with a credit card). Muted,
   small, and constrained so it reads as a table footnote, not a banner. */
.grid-note {
  margin: var(--space-3) 0 0;
  max-width: 60ch;
  color: var(--muted);
  font-size: var(--font-size-sm);
  line-height: 1.5;
  text-wrap: pretty;
}

.grid-note strong {
  color: var(--ink);
  font-weight: 600;
}

/* Collapsible symbol legend below the grid — the touch-reachable version of the
   grid's hover-only tooltips. Same visual family as the grid-note footnote. */
.grid-legend {
  margin-top: var(--space-3);
  max-width: 64ch;
  color: var(--muted);
  font-size: var(--font-size-sm);
}

.grid-legend>summary {
  cursor: pointer;
  user-select: none;
  width: max-content;
}

.grid-legend-list {
  margin: var(--space-2) 0 0;
  display: grid;
  grid-template-columns: max-content 1fr;
  column-gap: var(--space-3);
  row-gap: 6px;
  align-items: baseline;
}

.grid-legend-list dt {
  margin: 0;
  font-weight: normal;
  color: var(--ink);
}

.grid-legend-list dd {
  margin: 0;
  line-height: 1.5;
  text-wrap: pretty;
}

/* Group headings inside the legend. They're h2s for heading order, so they opt out of the
   base h2's uppercase eyebrow treatment — that reads as a section header, louder than this
   quiet collapsed panel wants. */
.grid-legend-heading {
  margin: var(--space-3) 0 0;
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--ink);
  text-transform: none;
  letter-spacing: normal;
}

/* A heading owns the list under it, so close the list's own top gap. */
.grid-legend-heading+.grid-legend-list {
  margin-top: 6px;
}

/* Rule between groups: any heading that follows a list, so it lands between every
   pair of groups and never above the first one. */
.grid-legend-list+.grid-legend-heading {
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: 1px solid var(--rule);
}

/* Inline grid error banner (HTMX edit failures). */
.grid-error {
  background: var(--bad-soft);
  color: var(--bad-soft-ink);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  margin-bottom: var(--space-3);
}

/* First-run setup nudge on the forecast. Deliberately NOT .grid-error styling:
   this is an invitation, not an error, so it reads in the accent family. The
   thicker left border gives it a quiet "do this next" spine. */
.setup-cta {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  background: var(--accent-soft);
  border: 1px solid var(--accent);
  border-left-width: 4px;
  border-radius: var(--radius-lg);
  padding: var(--space-4) var(--space-5);
  margin-bottom: var(--space-4);
}

.setup-cta-body {
  flex: 1;
}

.setup-cta-title {
  margin: 0 0 var(--space-1);
  font-size: var(--font-size-lg);
  color: var(--ink);
  /* Opt out of the app's uppercase/tracked h2 eyebrow style (§2) — a first-run
     welcome reads warmer in sentence case with the friendlier h1 tracking. */
  text-transform: none;
  letter-spacing: -0.01em;
}

.setup-cta p {
  margin: 0;
  color: var(--muted);
  text-wrap: pretty;
}

.setup-cta .btn {
  flex-shrink: 0;
}

@media (max-width: 560px) {
  .setup-cta {
    flex-direction: column;
    align-items: flex-start;
  }
}

/* First-run setup checklist (/setup, forecast/setup.html). A hub of derived-state
   steps that link into the existing Settings / Paychecks / import pages. */
/* The setup-page intro reuses the shared .crud-section-note; this scoped override
   adds orphan control without touching the shared class on the manage pages. */
.setup-intro {
  text-wrap: pretty;
}

.setup-progress {
  color: var(--muted);
  font-size: var(--font-size-sm);
  font-weight: 600;
  margin: 0 0 var(--space-4);
}

.setup-done {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
  background: var(--accent-soft);
  border: 1px solid var(--accent);
  border-left-width: 4px;
  border-radius: var(--radius-lg);
  padding: var(--space-4) var(--space-5);
  margin-bottom: var(--space-4);
}

.setup-done-msg {
  flex: 1;
  margin: 0;
  font-weight: 600;
  color: var(--ink);
  text-wrap: pretty;
}

.setup-checklist {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.setup-step {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  padding: var(--space-4) var(--space-5);
}

.setup-step.is-done {
  background: var(--surface-subtle);
}

/* Status marker: a hollow "to do" ring, or a filled accent check when done. */
.setup-step-marker {
  flex-shrink: 0;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 2px solid var(--rule-strong);
}

.setup-step-marker.is-done {
  display: flex;
  align-items: center;
  justify-content: center;
  border-color: var(--accent);
  background: var(--accent);
  color: #fff;
}

.setup-step-marker.is-done svg {
  width: 16px;
  height: 16px;
}

.setup-step-body {
  flex: 1;
  min-width: 0;
}

.setup-step-title {
  margin: 0 0 var(--space-1);
  font-weight: 600;
  color: var(--ink);
  text-wrap: pretty;
}

.setup-step-desc {
  margin: 0;
  color: var(--muted);
  font-size: var(--font-size-sm);
  text-wrap: pretty;
}

/* Inline "Required" / "Optional" pill on a step title. */
.setup-step-tag {
  display: inline-block;
  margin-left: var(--space-1);
  padding: 1px 8px;
  border-radius: 999px;
  font-size: var(--font-size-xs);
  font-weight: 600;
  vertical-align: middle;
  color: var(--muted);
  background: var(--surface-subtle);
  border: 1px solid var(--rule-strong);
}

.setup-step-tag.req {
  color: var(--accent);
  background: var(--accent-soft);
  border-color: var(--accent);
}

.setup-step .btn {
  flex-shrink: 0;
}

.setup-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-5);
}

@media (max-width: 560px) {
  .setup-step {
    flex-wrap: wrap;
  }
  /* Keep the marker + text on one row; drop the action button to a full-width line. */
  .setup-step-body {
    flex-basis: 0;
  }
  .setup-step .btn {
    flex-basis: 100%;
  }
}

/* Slim "finish setup" nudge on the forecast page (forecast.html): shown once accounts
   are picked but paychecks/bills are still missing. Accent family (an invitation),
   distinct from the amber .trial-banner (a billing deadline). */
.setup-nudge {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  background: var(--accent-soft);
  border: 1px solid var(--accent);
  border-left-width: 3px;
  border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-3);
  margin-bottom: var(--space-3);
}

.setup-nudge-body {
  min-width: 0;
}

.setup-nudge-title {
  margin: 0;
  font-weight: 600;
  color: var(--ink);
  text-wrap: pretty;
}

.setup-nudge-meta {
  margin: 0;
  color: var(--muted);
  font-size: var(--font-size-sm);
  text-wrap: pretty;
}

.setup-nudge-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-shrink: 0;
}

/* 13. UTIL =================================================== */
.text-muted {
  color: var(--muted);
}

.text-right {
  text-align: right;
}

.text-xs {
  font-size: var(--font-size-xs);
}

.text-inline {
  display: inline;
}

.ml-auto {
  margin-left: auto;
}

.my-2 {
  margin: var(--space-2) 0 var(--space-4);
}

.grow {
  flex: 1;
}

/* Mobile-only chrome (hamburger top bar + drawer scrim). Hidden by default; the
   §14 RESPONSIVE media query reveals them under the breakpoint. */
.mobile-topbar,
.sidebar-overlay {
  display: none;
}

/* Confirmation modal (native <dialog>): styled replacement for window.confirm() on
   destructive HTMX actions. Centered by the browser; ::backdrop dims the page. */
.modal {
  border: 0;
  padding: 0;
  margin: auto;
  border-radius: var(--radius-lg);
  background: var(--panel);
  color: var(--ink);
  box-shadow: 0 10px 40px rgba(0, 0, 0, .25);
  max-width: 360px;
  width: calc(100vw - var(--space-5) * 2);
}

.modal::backdrop {
  background: rgba(0, 0, 0, .45);
}

.modal-card {
  margin: 0;
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.modal-msg {
  margin: 0;
  line-height: 1.45;
}

.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
}

/* 14. RESPONSIVE ============================================= */
/* Desktop-first: everything above is the ≥769px layout. Below the breakpoint the
   fixed 300px sidebar would swallow a phone viewport, so it becomes an off-canvas
   drawer (toggled by the hamburger in .mobile-topbar) and the 2-column form/grid
   layouts collapse to a single column. The forecast grid keeps its horizontal
   scroll by design — a week is wider than a phone — but its min column shrinks so
   one week fills the screen with the next peeking in to signal scrollability. */
@media (max-width: 768px) {

  /* Layout: drop the sidebar grid column; main goes full width. */
  .app-layout {
    grid-template-columns: 1fr;
  }

  .main {
    padding: var(--space-4);
  }

  /* Sidebar → off-canvas drawer, slid out until .open is toggled on. */
  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    height: 100vh;
    width: min(82vw, var(--sidebar-width));
    z-index: 50;
    transform: translateX(-100%);
    transition: transform .25s ease;
  }

  .sidebar.open {
    transform: translateX(0);
    box-shadow: 0 0 40px rgba(0, 0, 0, .35);
  }

  /* Scrim: present in the DOM but invisible until .visible, so it can transition. */
  .sidebar-overlay {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 40;
    background: rgba(0, 0, 0, .45);
    opacity: 0;
    visibility: hidden;
    transition: opacity .25s ease, visibility .25s ease;
  }

  .sidebar-overlay.visible {
    opacity: 1;
    visibility: visible;
  }

  /* Top bar carrying the hamburger. Negative side margins bleed it to the edges of
     the padded .main (margins must match .main's mobile padding above). */
  .mobile-topbar {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    position: sticky;
    top: 0;
    z-index: 30;
    margin: calc(-1 * var(--space-4)) calc(-1 * var(--space-4)) var(--space-4);
    padding: var(--space-3) var(--space-4);
    background: var(--sidebar-bg);
    color: var(--sidebar-ink);
  }

  .mobile-menu-btn {
    display: inline-flex;
    align-items: center;
    padding: var(--space-1);
    border: 0;
    background: transparent;
    color: var(--sidebar-ink);
    cursor: pointer;
  }

  .mobile-menu-btn svg {
    width: 26px;
    height: 26px;
  }

  .mobile-brand {
    height: 28px;
    width: auto;
  }

  /* In the drawer, the bottom cluster follows the last link in normal flow instead of
     pinning to the bottom (the desktop convention reads as a broken gap on a content-sized
     overlay). Its top-border separator still groups it. */
  .nav-bottom {
    margin-top: var(--space-2);
  }

  /* Forecast header: the desktop row uses ml-auto to push the controls right,
     which reads as broken once it wraps. Stack it instead. */
  .header {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-2);
  }

  .header .ml-auto {
    margin-left: 0;
  }

  /* Forms: every 2-column grid collapses to one column so fields aren't crushed. */
  .form-row,
  .form-edit-grid,
  .checkbox-list>div {
    grid-template-columns: 1fr;
  }

  /* Forecast grid: each week is *just under* full width so the next week's card
     edge (border + shadow + rounded corner) peeks in at the right, signalling the
     grid scrolls horizontally — native touch scrollbars are hidden at rest, so the
     peek is the only passive "swipe for more weeks" affordance on a phone. The
     ~2.5rem reduction leaves a clear sliver after the inter-column gap. */
  .forecast-grid {
    grid-auto-columns: calc(100% - 2.5rem);
    /* On desktop the per-section min-heights and stretched cards align the
       Paychecks/Bills bands across side-by-side columns. On a phone only one
       week is on screen at a time (cards are ~full width, swiped horizontally),
       so that cross-column alignment has nothing to align against and just pads
       every card to the busiest week's height — dead vertical space the user has
       to scroll past. Let each week be its natural height here. */
    align-items: start;
  }

  .week {
    min-height: 0;
  }

  .section-paychecks,
  .section-bills,
  .section-necessities {
    min-height: 0;
  }

  /* Chart card: a 168px plot is oversized next to phone-width week cards, and
     the "Forecast ·" title prefix is redundant under the page's own heading —
     dropping it keeps the head and strip titles on one line. The strip wraps
     to two tidy rows: title + chevron, then the stats. */
  .fc-svg {
    height: 150px;
  }

  /* The 2-up phone rule that lived here is now the DEFAULT (.fc-metrics-row), promoted
     out of this viewport block when the row moved to a container query — phones simply
     never reach the 840px container that turns on 4-up. Nothing to override here. */

  /* Tighter side padding buys ~16px of text width in a half-width card — enough to
     keep "Bills Due Before Lowest Balance" at two lines down to ~360px (a common
     Android width) instead of breaking one word per line. */
  .fc-metric {
    padding: var(--space-4);
  }

  .fc-title-prefix {
    display: none;
  }

  .fc-strip {
    flex-wrap: wrap;
    row-gap: var(--space-1);
  }

  .fc-strip .fc-toggle-hint {
    order: 1;
  }

  .fc-strip-stats {
    order: 2;
    flex-basis: 100%;
  }

  /* List tables → stacked cards. Six columns don't fit a phone, so each row
     becomes a bordered card and every data cell shows its column name (from the
     td's data-label) beside the value. The header row is hidden; the inline edit
     form (a single colspan cell, no data-label) stays a normal full-width block.
     .list-table-plain opts out (see budgets.html) — a 2-column table reads better
     left as a table on a phone. */
  .list-table:not(.list-table-plain),
  .list-table:not(.list-table-plain) tbody {
    display: block;
  }

  .list-table:not(.list-table-plain) thead {
    display: none;
  }

  .list-table:not(.list-table-plain) tr {
    display: block;
    border: 1px solid var(--rule);
    border-radius: var(--radius-md);
    padding: var(--space-1) var(--space-3);
    margin-bottom: var(--space-3);
  }

  .list-table:not(.list-table-plain) td {
    display: block;
  }

  .list-table:not(.list-table-plain) tr+tr td {
    border-top: 0;
    /* cancel the desktop row separators */
  }

  .list-table:not(.list-table-plain) td[data-label] {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--space-3);
    padding: var(--space-1) 0;
    text-align: right;
  }

  .list-table:not(.list-table-plain) td[data-label]::before {
    content: attr(data-label);
    font-weight: 600;
    color: var(--muted);
    text-align: left;
    white-space: nowrap;
  }

  .list-table:not(.list-table-plain) td.actions {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-2);
    margin-top: var(--space-1);
    padding: var(--space-2) 0 var(--space-1);
    border-top: 1px solid var(--rule);
    white-space: normal;
  }

  /* Import pages: the fixed multi-column grid is wider than a phone, so each
     category row becomes a stacked card with per-field labels (from data-label);
     the grid header row is hidden. */
  .import-grid {
    display: block;
    padding: var(--space-3);
  }

  .import-head-cell {
    display: none;
  }

  .import-rowc {
    display: block;
    border: 1px solid var(--rule);
    border-radius: var(--radius-md);
    padding: var(--space-3);
    margin-bottom: var(--space-3);
  }

  .import-rowc .import-cell {
    display: block;
    margin-bottom: var(--space-2);
  }

  .import-rowc .import-cell:last-child {
    margin-bottom: 0;
  }

  .import-rowc .import-check {
    min-height: 0;
    /* drop the desktop 37px centering box */
  }

  .import-rowc [data-label]::before {
    content: attr(data-label);
    display: block;
    font-size: var(--font-size-sm);
    font-weight: 500;
    color: var(--muted);
    margin-bottom: var(--space-1);
  }

  /* The leading "include this category" checkbox: label inline beside the box,
     set off from the fields below with a divider. */
  .import-rowc>label.import-check {
    justify-content: flex-start;
    align-items: center;
    gap: var(--space-2);
    margin-bottom: var(--space-2);
    padding-bottom: var(--space-2);
    border-bottom: 1px solid var(--rule);
    font-weight: 600;
  }

  .import-rowc>label.import-check[data-label]::before {
    display: inline;
    margin-bottom: 0;
    color: var(--ink);
  }

  /* Touch targets: the grid's icon buttons (paid/done toggle, one-off edit and
     remove) render at text size (~15px), well under a usable fingertip target.
     Padding enlarges the hit area and the matching negative margin cancels it in
     layout, so rows don't shift. Overlap with a neighboring row's expanded area
     just splits the slack between them. */
  .paid-toggle,
  .override-edit,
  .override-delete {
    padding: 10px 12px;
    margin: -10px -10px;
  }
}
