/* ---------------------------------------------------------------------------
   Design tokens — the single source of truth for color, spacing, radius and
   type. Components below reference these via var(); change a value here and it
   propagates everywhere, which is what keeps the UI consistent.
   --------------------------------------------------------------------------- */
:root,
[data-theme="flow"] {
  /* ====================================================================
     THEME TOKENS — the default values below ARE the "Flow" theme: the
     Hyptic brand — the mark's deep teal for actions, its mint as the
     accent, ink from the wordmark, white surfaces and the capsule radii
     of the logo's two pillars. The nav is light so the full-colour mark
     sits on it exactly as the logo is drawn.
     Legibility: every text token reaches 4.5:1 on every surface it sits on
     (e2e/a11y.spec.js), and every token that draws a line or a control
     edge — --primary-soft, --success, --warning, --border-strong, the
     chart slots — reaches 3:1 on white (WCAG 1.4.11). The light mints of
     the logo stay in the logo and in fills behind text.
     Every [data-theme="…"] block at the end of this file overrides this
     set. Structural tokens (spacing, radius, type *scale*) are not themed.
     The [data-theme="flow"] alias keeps elements tagged with it
     (the picker's preview swatch) on these tokens even when <html>
     carries another theme; the later theme blocks still win on <html>
     itself (equal specificity, later in the file).
     ==================================================================== */

  /* Color — surfaces & text */
  --bg:            #f4f7f6;
  --surface:       #ffffff;
  --surface-2:     #f7faf9;
  --surface-3:     #e9f0ed;
  --text:          #1b3331;
  --text-strong:   #122524;
  --text-2:        #2c4542;
  --muted:         #55676a;
  --border:        #e2eae6;
  --border-subtle: #edf2f0;
  --border-strong: #768b85;   /* control outlines: fields, ghost buttons, checkboxes — >=3:1 on every surface */

  /* Color — intent */
  --primary:       #0e5043;
  --primary-hover: #0c3430;
  --primary-soft:  #2a9a71;
  --primary-text:  #ffffff;   /* text/icon on a filled --primary surface */
  --danger:        #b23a2f;
  --danger-hover:  #a53426;
  --danger-bg:     #fbe7e4;
  --danger-text:   #a53426;
  --success:       #1f8a63;
  --neutral:       #55676a;  /* transfers / net-worth-neutral */
  /* Text tokens — for words and figures, separate from the fills above (a bar,
     a border, a dot). A fill only has to be seen; text has to be read, so each
     *-text / --link / --muted must reach 4.5:1 (WCAG AA) on every surface it
     sits on, in every theme. e2e/a11y.spec.js holds each theme to it. */
  --success-text:  #15785b;
  --link:          #15785b;
  /* Derived once here; the color-mix re-resolves per theme because it references
     the theme's own --success/--surface/--primary (custom props substitute lazily). */
  --success-bg:    color-mix(in srgb, var(--success) 14%, var(--surface));
  --focus-ring:    color-mix(in srgb, var(--primary) 22%, transparent);

  /* Color — nav */
  --nav-bg:          #ffffff;
  --nav-border:      #e2eae6;
  --nav-link:        #55676a;
  --nav-active:      #dff7e8;
  --nav-text:        #122524;   /* brand + plain nav text */
  --nav-active-text: #0e5043;   /* text on the active nav pill */

  /* Color — category accents (uncategorized / needs-attention) */
  --warning:            #a8790c;
  --warning-bg:         #fbf0d8;
  --warning-bg-hover:   #f6e5bd;
  --warning-text:       #7d5410;
  --warning-text-hover: #5e400c;
  --uncat-text:         #7d5410;
  --brand-mint:         #8deeaf;   /* decorative only: never a line or text on a light surface */   /* the mark's light pillar — soft accents, the loading bar */

  /* Chart / pie slice palette — 10 hues (see slice-colors in views.cljs); every slot
     >=3:1 on white, CVD-separated, >=15 dE between neighbours (dataviz validator) */
  --chart-1:  #0f7f58; --chart-2:  #b8860b; --chart-3:  #3f6fb3; --chart-4:  #c75b3a;
  --chart-5:  #1f9a94; --chart-6:  #8a4f9e; --chart-7:  #7fa02c; --chart-8:  #d26a86;
  --chart-9:  #2f88c2; --chart-10: #a8702a;

  /* Type families (themed) — display for headings, num for money figures */
  --font-sans:    "Plus Jakarta Sans", "Inter", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
  --font-display: var(--font-sans);   /* one family; headings carry the weight */
  --font-num:     var(--font-sans);   /* one face for words and figures; digits go tabular below */

  /* Spacing scale */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;

  /* Radius scale */
  --radius-sm:   10px;
  --radius-md:   16px;
  --radius-pill: 999px;

  /* Type scale */
  --fs-xs:   0.75rem;
  --fs-sm:   0.85rem;
  --fs-base: 0.95rem;
  --fs-md:   1rem;
  --fs-lg:   1.25rem;
  --fs-xl:   1.5rem;

  /* Native widgets (select dropdown lists, date pickers, scrollbars) render
     from this, not the tokens above — the dark themes override it to dark. */
  color-scheme: light;
}

* { box-sizing: border-box; }

/* Respect users who prefer reduced motion: near-instant animations/transitions. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

html, body {
  margin: 0;
  padding: 0;
  /* We set our own mobile type scale (the --fs-* overrides in the 640px media
     query below); stop iOS Safari from also applying its heuristic text
     inflation on top of it. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  font-family: var(--font-sans);
  color: var(--text);
  background: var(--bg);
  accent-color: var(--primary);   /* checkboxes/radios in the theme's accent */
}

a { color: var(--link); text-decoration: none; }
a:hover { text-decoration: underline; }
/* A link inside a sentence is underlined — color alone doesn't set it apart
   from the words around it (WCAG 1.4.1). Links styled as controls carry a
   class and keep their own look. */
:where(p, li, dd, .muted, .muted-small, .modal-body) a:not([class]) {
  text-decoration: underline; text-underline-offset: 2px;
}

.app { min-height: 100vh; }

main {
  max-width: 1100px;
  margin: 0 auto;
  padding: var(--space-5) var(--space-4) var(--space-6);
}

h1 { margin-top: 0; }
h2 { margin: 0 0 var(--space-2); }
h3 { font-size: var(--fs-md); }
/* A page's sections are h2s under its h1, drawn as the h3s they used to be. */
h2.section-title { font-size: var(--fs-md); margin: 1em 0; }
.muted { color: var(--muted); font-size: var(--fs-sm); margin-top: 0; }
/* Text for assistive tech only — e.g. the header of an actions column. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Global loading bar — driven by the re-frame :loading? counter */
.loading-bar {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 3px;
  background: linear-gradient(90deg, var(--primary), var(--primary-soft), var(--primary));
  background-size: 200% 100%;
  animation: loading-slide 1s linear infinite;
  z-index: 50;
}
@keyframes loading-slide {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

/* Pull-to-refresh (iOS shell) — the ↻ chip that rides a drag down from the
   top of the page. .armed = past the release-to-refresh trigger; feedback
   after release is the .loading-bar above. */
.ptr {
  position: fixed;
  top: env(safe-area-inset-top, 0px); left: 50%;
  width: 38px; height: 38px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface); color: var(--muted);
  border: 1px solid var(--border);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
  font-size: 20px;
  pointer-events: none;
  z-index: 60;
}
.ptr.armed { color: var(--link); border-color: var(--primary-soft); }

/* Full-screen boot loader: the "Loading…" shown before the shell mounts (the
   static index.html placeholder) and while auth resolves on a cold start.
   Pinned to the viewport centre so it never lands in a corner under the
   loading-bar / nav — both index.html and the (not checked?) app state use it. */
.app-loading {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
  text-align: center;
}
.app-loading p { margin: 0; }
.app-loading .logo-mark, .app-loading .boot-mark { margin: 0 auto var(--space-3); }

/* Content-area loading state: shown in <main> while a page waits on data it
   can't render without. The nav/shell stays put, so switching is instant. */
.loading-panel {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  min-height: 40vh;
}
.spinner {
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  border: 3px solid var(--border);
  border-top-color: var(--primary);
  animation: spinner-rotate 0.7s linear infinite;
}
@keyframes spinner-rotate { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .spinner { animation-duration: 2s; }
}

/* Nav — sticky: it's the app's primary switcher and pages get long, so it
   stays reachable. Above row-menu dropdowns (30), below modals/loading (50).
   sticky still acts as the containing block for the compact ☰ dropdown. */
.top-nav {
  position: sticky;
  top: 0;
  z-index: 45;
  display: flex;
  align-items: center;
  gap: var(--space-5);
  padding: var(--space-3) var(--space-5);
  background: var(--nav-bg);
  color: var(--nav-text);
  border-bottom: 1px solid var(--nav-border);
}
.top-nav .brand { display: inline-flex; align-items: center; gap: 0.5rem; color: var(--nav-text); line-height: 1; }
.top-nav .brand:hover { text-decoration: none; }
.logo-mark { display: block; flex: 0 0 auto; }
/* The wordmark is an inline SVG filled with currentColor; the wrapper's height
   sets its size and the width follows the logo's 3.16:1. */
.wordmark { display: inline-flex; align-items: center; }
.wordmark svg { display: block; height: 100%; width: auto; overflow: visible; }
.help-brand { display: inline-flex; align-items: center; gap: 0.5rem; }
.help-brand-suffix { font-weight: 800; font-size: 1.05rem; letter-spacing: -0.01em; margin-left: 0.15rem; }
.top-nav .links { display: flex; gap: var(--space-3); white-space: nowrap; overflow: hidden; }
.top-nav .nav-link {
  color: var(--nav-link);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
}
.top-nav .nav-link.active { background: var(--nav-active); color: var(--nav-active-text); }
.top-nav .nav-link:hover  { background: var(--nav-active); text-decoration: none; }
.nav-toggle {
  display: none;            /* hidden until .compact (tabs would wrap) reveals it */
  background: none;
  border: none;
  color: var(--nav-link);
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
  padding: 0 var(--space-2);
}
.nav-toggle:hover { color: var(--nav-text); background: none; }
.nav-mid { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; }
/* Hidden inline copy of the tabs; its scrollWidth vs .nav-mid's width tells JS
   the instant the real tabs would wrap. width/height 0 + overflow hidden so it
   never adds a scrollbar, yet scrollWidth still reports the natural tab width. */
.nav-measure {
  position: absolute; left: 0; top: 0; width: 0; height: 0;
  overflow: hidden; visibility: hidden; pointer-events: none;
  display: flex; gap: var(--space-3);
}
.nav-measure .nav-link { flex: 0 0 auto; }
/* .compact is set by JS the moment the tabs no longer fit — collapse to ☰. */
.top-nav.compact .nav-toggle { display: block; }
.top-nav.compact .nav-mid .links {
  position: absolute; top: 100%; left: 0; right: 0;
  flex-direction: column; align-items: flex-start; gap: var(--space-1);
  background: var(--nav-bg); border-bottom: 1px solid var(--nav-border);
  padding: var(--space-2) var(--space-5); overflow: visible;
  display: none; z-index: 40;
}
.top-nav.compact.open .nav-mid .links { display: flex; }
/* In compact the ☰ sits top-left; roomy mode hides it, so order is moot. */
.top-nav.compact .nav-toggle { padding-left: 0; }

/* Drawer-only items — account + site links live in the ☰ drawer alongside
   the tabs, so everything is reachable from one place on a phone. Hidden
   entirely in roomy mode (the top tabs + avatar menu cover it). */
.top-nav .drawer-extra { display: none; }
.top-nav.compact .drawer-extra {
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-1);
  width: 100%;
  border-top: 1px solid var(--nav-border);
  margin-top: var(--space-2); padding-top: var(--space-2);
}
.drawer-logout {
  background: none; border: none; cursor: pointer;
  font: inherit; text-align: left;
}
.drawer-site {
  display: flex; gap: var(--space-4);
  padding: var(--space-1) var(--space-2) var(--space-1);
  font-size: var(--fs-sm);
}
.drawer-site a, .drawer-site button.link { color: var(--nav-link); }
.drawer-site a:hover, .drawer-site button.link:hover { color: var(--nav-text); }
.drawer-site button.link { font-size: inherit; text-decoration: none; }

/* iOS-style bottom tab bar — compact mode's primary nav (the ☰ drawer keeps
   the long tail). Fixed to the viewport; body.has-bottom-tabs (toggled by the
   same measurement that sets .compact) pads main so content scrolls clear. */
.bottom-tabs {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 45;
  display: flex;
  background: var(--nav-bg);
  border-top: 1px solid var(--nav-border);
  padding: var(--space-1) env(safe-area-inset-right, 0px)
           env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px);
}
.bottom-tabs .tab {
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: var(--space-1) 0;
  color: var(--nav-link); font-size: 0.66rem; line-height: 1;
}
.bottom-tabs .tab:hover { text-decoration: none; }
.bottom-tabs .tab.active { color: var(--nav-active-text); }

/* Safe-area (iOS notch / Dynamic Island / home-bar) — additive and a no-op on
   browsers without the insets, since env(..., 0px) falls back to 0. The nav is
   the sticky element pinned to the top edge, so it absorbs the top inset; body
   background paints under the status bar (viewport-fit=cover, set in the iOS
   index.html) so the tint runs to the physical edge. main and the compact ☰
   dropdown pick up the horizontal + bottom insets for landscape + home-bar. */
.top-nav {
  padding-top: calc(var(--space-3) + env(safe-area-inset-top, 0px));
  padding-left: calc(var(--space-5) + env(safe-area-inset-left, 0px));
  padding-right: calc(var(--space-5) + env(safe-area-inset-right, 0px));
}
.top-nav.compact .nav-mid .links {
  padding-left: calc(var(--space-5) + env(safe-area-inset-left, 0px));
  padding-right: calc(var(--space-5) + env(safe-area-inset-right, 0px));
}
main {
  padding-left: calc(var(--space-4) + env(safe-area-inset-left, 0px));
  padding-right: calc(var(--space-4) + env(safe-area-inset-right, 0px));
  padding-bottom: calc(var(--space-6) + env(safe-area-inset-bottom, 0px));
}
body.has-bottom-tabs main {
  padding-bottom: calc(var(--space-6) + 58px + env(safe-area-inset-bottom, 0px));
}

/* Error banner */
.error {
  background: var(--danger-bg);
  color: var(--danger-text);
  padding: var(--space-2) var(--space-4);
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.error button {
  background: transparent;
  border: none;
  color: var(--danger-text);
  cursor: pointer;
  font-size: 1.1rem;
}

/* Neutral, dismissible notice (e.g. "already added") — informational, not an
   error, so it uses the primary accent rather than the danger palette. */
.notice {
  background: var(--surface-2);
  color: var(--text-strong);
  border-left: 3px solid var(--primary);
  padding: var(--space-2) var(--space-4);
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.notice button {
  background: transparent;
  border: none;
  color: var(--muted);
  cursor: pointer;
  font-size: 1.1rem;
}

/* Email-verification nudge — shown until the signup address is confirmed. */
.verify-banner {
  background: var(--warning-bg);
  color: var(--warning-text);
  border-left: 3px solid var(--warning);
  padding: var(--space-2) var(--space-4);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}
.verify-banner button {
  background: transparent;
  border: none;
  color: var(--warning-text);
  cursor: pointer;
  text-decoration: underline;
  padding: 0;
  font-size: inherit;
  white-space: nowrap;
}
.verify-banner button:hover { background: transparent; color: var(--warning-text-hover); }

/* Tables */
table {
  width: 100%;
  border-collapse: collapse;
  background: var(--surface);
  border: 1px solid var(--border);
  margin-bottom: var(--space-5);
}
th, td {
  text-align: left;
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--border-subtle);
}
th { background: var(--surface-2); font-size: var(--fs-sm); font-weight: 600; }
td.right, th.right { text-align: right; font-variant-numeric: tabular-nums; }

/* Empty state */
.empty {
  color: var(--muted);
  background: var(--surface);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-md);
  padding: var(--space-4);
  text-align: center;
}

/* Buttons. Like the form controls below, buttons don't inherit the page font
   by default — without font-family they render in the UA face (Arial). */
button {
  background: var(--primary);
  color: var(--primary-text);
  border: none;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-family: inherit;
  font-size: var(--fs-sm);
}
/* This fill outranks a single-class rule, so any button restyled as ghost,
   card or chip must set its own :hover background or turn primary on hover. */
button:hover { background: var(--primary-hover); }
button:disabled { opacity: 0.6; cursor: not-allowed; }
button.danger { background: var(--danger); }
button.danger:hover { background: var(--danger-hover); }
button.link {
  background: transparent;
  color: var(--link);
  padding: 0;
  text-decoration: underline;
  font-size: var(--fs-sm);
}
button.link:hover { background: transparent; }

/* ---------------------------------------------------------------------------
   Form controls — the themed baseline every input/select/textarea starts from.
   Form controls do NOT inherit the page font, and unstyled ones render with
   browser chrome (UA font, white fields on the dark themes, native select
   look) — off-theme the moment a control sits outside a styled container.
   :where() keeps these at element specificity so any single-class rule below
   (.type-select, .money-input, .inline-edit, …) still overrides cleanly.
   --------------------------------------------------------------------------- */
input:where(:not([type="checkbox"], [type="radio"], [type="file"], [type="range"], [type="color"])),
select,
textarea {
  font: inherit;
  font-size: var(--fs-base);
  color: var(--text);
  background-color: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  padding: var(--space-2);
}
input:where(:not([type="checkbox"], [type="radio"], [type="file"], [type="range"], [type="color"])):focus,
select:focus,
textarea:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--focus-ring);
}
::placeholder { color: var(--muted); opacity: 1; }

/* Keyboard focus is always visible and always themed: every control reached
   by Tab shows a ring in the theme's accent (:where keeps it weak, so a
   component's own ring still wins). The dark nav gets a light one. <main>
   takes focus from the skip link without a ring around the whole page. */
:where(button, a, summary, [role="menuitem"], [role="button"], [tabindex]):focus-visible {
  outline: 2px solid var(--primary-soft); outline-offset: 2px; border-radius: 4px;
}
.top-nav :focus-visible, .bottom-tabs :focus-visible { outline-color: currentColor; }
main:focus { outline: none; }

/* Skip link: off-screen until a keyboard reaches it first thing on the page. */
.skip-link {
  position: absolute; left: var(--space-3); top: -100px; z-index: 100;
  padding: var(--space-2) var(--space-4); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text-strong); font-weight: 600;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.skip-link:focus { top: var(--space-2); }

/* Page titles are the page's h1, sized as the section headings they were. */
h1.page-title { font-size: 1.5em; margin: 0 0 var(--space-2); }
.section-head h1.page-title { margin: 0; }
/* Kill Safari's rounded native search chrome; the baseline above restyles it. */
input:where([type="search"]) { -webkit-appearance: none; appearance: none; }
/* Selects drop the native chrome for the baseline look; the arrow is redrawn
   as a two-triangle chevron in --muted so it re-themes with everything else. */
select {
  appearance: none;
  padding-right: calc(var(--space-2) * 2 + 0.6em);
  background-image:
    linear-gradient(45deg, transparent 50%, var(--muted) 50%),
    linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position:
    right calc(var(--space-2) + 0.3em) top 50%,
    right var(--space-2) top 50%;
  background-size: 0.3em 0.3em;
  background-repeat: no-repeat;
}

/* Forms */
form.create {
  background: var(--surface);
  border: 1px solid var(--border);
  padding: var(--space-4);
  border-radius: var(--radius-md);
  display: grid;
  gap: var(--space-3);
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  align-items: end;
}
form.create h3, form.create .section-title {
  grid-column: 1 / -1;
  margin: 0;
  font-size: var(--fs-md);
}
form.create label {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  font-size: var(--fs-xs);
  color: var(--text-2);
}
form.create button { justify-self: start; }

/* Reusable dollar-amount field (money-input): a currency affix fused to a text
   field sized per site, with in-field arithmetic. The wrapper owns the border so
   affix + field read as one control; the field's width is set inline per site. */
.money-field {
  display: inline-flex;
  align-items: stretch;
  max-width: 100%;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  overflow: hidden;
}
.money-field.affix-right { flex-direction: row-reverse; }
.money-field:focus-within { border-color: var(--primary); box-shadow: 0 0 0 3px var(--focus-ring); }
.money-affix {
  display: inline-flex;
  align-items: center;
  padding: 0 var(--space-2);
  background: var(--surface-2);
  color: var(--muted);
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
  user-select: none;
}
.money-field .money-input {
  border: none;
  background: transparent;
  padding: var(--space-2);
  font-size: var(--fs-base);
  font-variant-numeric: tabular-nums;
  min-width: 0;
}
/* The wrapper owns the focus treatment — the inner field stays bare. */
.money-field .money-input:focus { outline: none; box-shadow: none; }

/* KPI block */
.kpi {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--space-4);
  margin-top: var(--space-4);
}
.kpi > div {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
}
.kpi > div div { font-size: var(--fs-xl); font-variant-numeric: tabular-nums; }

/* Bulk-action strip above transactions */
.bulk-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin: var(--space-2) 0 var(--space-4);
}
.bulk-actions .uncat-count {
  color: var(--uncat-text);
  font-weight: 600;
  font-size: var(--fs-sm);
}
.bulk-actions.warn .uncat-count { color: var(--warning-text); }
.bulk-actions .over-note { font-weight: 500; color: var(--warning-text); }

/* Inline account-type editor in the accounts table: compact + quiet-bordered.
   Longhands only — a padding/background shorthand here would wipe the chevron
   and its right-side room from the select baseline. */
.type-select {
  padding-top: var(--space-1);
  padding-bottom: var(--space-1);
  font-size: var(--fs-sm);
  border-color: var(--border);
}

/* Category cell — three visual states for category-source. */
td.cat-cell { padding: var(--space-1) var(--space-2); }

.cat {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-pill);
  cursor: pointer;
  font-size: var(--fs-sm);
  line-height: 1.2;
}
.cat:hover { background: var(--surface-3); }

/* Categorized: confident, plain (user- or auto-assigned; not distinguished). */
.cat-user { color: var(--text-strong); }

/* Uncategorized: the attention state. Distinct shape (rectangle, not pill),
   warning color, animated pulse so it's hard to overlook. */
.cat-none {
  background: var(--warning-bg);
  color: var(--warning-text);
  border: 1px dashed var(--warning);
  border-radius: var(--radius-sm);
  padding: var(--space-1) var(--space-2);
  font-size: var(--fs-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  cursor: pointer;
  animation: cat-none-pulse 2.4s ease-in-out infinite;
}
.cat-none:hover {
  background: var(--warning-bg-hover);
  color: var(--warning-text-hover);
}
@keyframes cat-none-pulse {
  /* halo derives from the theme's uncategorized colour, not a fixed orange */
  0%, 100% { box-shadow: 0 0 0 0 transparent; }
  50%      { box-shadow: 0 0 0 4px color-mix(in srgb, var(--uncat-text) 22%, transparent); }
}

/* Category picker — a full overlay (category-picker-modal). The transaction
   summary is pinned at the top; every category, grouped under its root and
   filterable, scrolls below for easy browsing. */
.modal.cat-modal {
  width: min(560px, calc(100vw - 2rem));
  max-height: min(80vh, 640px, 100%);
  display: flex;
  flex-direction: column;
  padding: 0;                 /* the sections below own their padding */
}
.cat-modal-head {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-4) var(--space-3);
  border-bottom: 1px solid var(--border);
}
.cat-modal-tx { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.cat-modal-amt { font-weight: 700; font-size: var(--fs-lg); font-variant-numeric: tabular-nums; color: var(--text-strong); }
.cat-modal-route { color: var(--text-2); font-size: var(--fs-sm); }
.cat-modal-desc { color: var(--muted); font-size: var(--fs-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cat-modal-date { color: var(--muted); font-size: var(--fs-xs); }
.cat-modal-close {
  margin-left: auto; background: none; border: none; color: var(--muted);
  font-size: 1.5rem; line-height: 1; cursor: pointer; padding: 0 var(--space-1);
}
.cat-modal-close:hover { color: var(--text-strong); background: none; }

/* A form in its own sheet (Add a transaction): the form's own card chrome goes,
   the sheet is the card. */
.modal.sheet { position: relative; width: min(560px, calc(100vw - 2rem)); overflow-y: auto; }
.modal.sheet form.create { border: none; padding: 0; background: none; }
.sheet-close { position: absolute; top: var(--space-3); right: var(--space-3); }
.cat-modal-search {
  margin: var(--space-3) var(--space-4) 0;
  padding: var(--space-2) var(--space-3);
}
.cat-modal-body { overflow-y: auto; padding: var(--space-3) var(--space-4) var(--space-4); }
.cat-group { margin-top: var(--space-4); }
.cat-group:first-of-type { margin-top: var(--space-3); }
.cat-group-root {
  display: inline-block; background: none; border: none;
  padding: 0 0 var(--space-2); margin: 0;
  font-size: var(--fs-sm); font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--text-strong); cursor: pointer;
}
.cat-group-root:hover { background: none; color: var(--link); text-decoration: underline; }
.cat-group-root.sel { color: var(--link); }
.cat-children { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.cat-opt {
  background: var(--surface-2); color: var(--text-2);
  border: 1px solid var(--border); border-radius: var(--radius-pill);
  padding: var(--space-1) var(--space-3); font-size: var(--fs-sm); cursor: pointer;
}
.cat-opt:hover { background: var(--surface-3); border-color: var(--border-strong); color: var(--text-strong); }
.cat-opt.sel, .cat-opt.sel:hover { background: var(--primary); border-color: var(--primary); color: var(--primary-text); }
.cat-opt-clear { display: block; margin-bottom: var(--space-2); }

/* Money-flow picker (movement-picker) — sits in the categorization popups
   between the pinned transaction summary and whatever scrolls below. */
.mv-picker {
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
}
.mv-picker-label {
  display: flex; align-items: center; gap: var(--space-2);
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--muted);
  margin-bottom: var(--space-2);
}
.mv-picker-opts { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.mv-help {
  margin: 0 0 var(--space-3);
  color: var(--text-2); font-size: var(--fs-sm); line-height: 1.5;
}
.cat-modal-note {
  margin: var(--space-3) var(--space-4) var(--space-4);
  color: var(--muted); font-size: var(--fs-sm);
}

/* In-app legal viewer (legal-doc-modal) — Terms / Privacy readable without
   leaving the app, on web and iOS alike (Apple guideline 5.1.1). */
.modal.legal-modal {
  width: min(720px, calc(100vw - 2rem));
  max-height: min(85vh, 760px, 100%);
  display: flex;
  flex-direction: column;
  padding: 0;
}
.legal-modal-head {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-4) var(--space-4) var(--space-3);
  border-bottom: 1px solid var(--border);
}
.legal-modal-title { font-weight: 600; color: var(--text-strong); }
.legal-modal-body { overflow-y: auto; padding: var(--space-3) var(--space-4) var(--space-4); }
.legal-doc, .help-doc { color: var(--text-2); font-size: var(--fs-sm); line-height: 1.6; }
.legal-doc h1, .help-doc h1 { font-size: var(--fs-lg); margin: 0 0 var(--space-2); color: var(--text-strong); }
.legal-doc h2, .help-doc h2 { font-size: var(--fs-md); margin: var(--space-5) 0 var(--space-2); color: var(--text-strong); }
.legal-doc h3, .help-doc h3 { font-size: var(--fs-sm); margin: var(--space-4) 0 var(--space-2); color: var(--text-strong); }
.legal-doc p, .help-doc p { margin: 0 0 var(--space-3); }
.legal-doc ul, .help-doc ul,
.legal-doc ol, .help-doc ol { margin: 0 0 var(--space-3); padding-left: 1.25rem; }
.legal-doc li, .help-doc li { margin-bottom: var(--space-1); }
.legal-doc .updated { color: var(--muted); font-size: var(--fs-xs); }

/* Income attribution (money-in's category mirror). The unattributed pill is
   deliberately quiet — no warning colour, no pulse — because unmatched money-in
   is usually a one-off (a gift), not something needing the user's attention. */
.inc-none {
  background: transparent;
  color: var(--muted);
  border: 1px dashed var(--border);
  border-radius: var(--radius-sm);
  padding: var(--space-1) var(--space-2);
  font-size: var(--fs-xs);
  cursor: pointer;
}
.inc-none:hover { color: var(--text-strong); border-color: var(--border-strong); background: transparent; }
.inc-muted {
  background: transparent; border: none; color: var(--muted);
  padding: var(--space-1) var(--space-2); cursor: pointer;
}
.inc-muted:hover { color: var(--text-strong); background: transparent; }

/* Money-neutral's category-cell stand-in — quiet like .inc-muted (a transfer
   between own accounts rarely needs attention); opens the category picker so
   its money-flow control is one click away. */
.mv-neutral {
  background: transparent; border: none; color: var(--muted);
  padding: var(--space-1) var(--space-2); cursor: pointer;
  font-size: var(--fs-xs);
}
.mv-neutral:hover { color: var(--text-strong); background: transparent; }
.inc-opt-amt { margin-left: var(--space-2); font-size: var(--fs-xs); opacity: 0.75; font-variant-numeric: tabular-nums; }
.income-picker-opts .cat-opt { display: block; width: 100%; text-align: left; margin-bottom: var(--space-2); }

/* Bulk "apply this category to recurring matches" offer */
.bulk-cat-modal { max-width: 32rem; }
.bulk-cat-list {
  max-height: 50vh; overflow-y: auto;
  margin: var(--space-3) 0; border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.bulk-cat-row {
  display: flex; align-items: baseline; gap: var(--space-3);
  padding: var(--space-2) var(--space-3); cursor: pointer;
  border-bottom: 1px solid var(--border);
}
.bulk-cat-row:last-child { border-bottom: none; }
.bulk-cat-row:hover { background: var(--surface-2); }
.bulk-cat-row input { flex: none; align-self: center; cursor: pointer; }
.bulk-cat-amt { font-weight: 700; font-variant-numeric: tabular-nums; color: var(--text-strong); }
.bulk-cat-route { color: var(--text-2); font-size: var(--fs-sm); }
.bulk-cat-desc {
  color: var(--muted); font-size: var(--fs-sm); margin-left: auto;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 40%;
}
.bulk-cat-date { color: var(--muted); font-size: var(--fs-xs); flex: none; }

/* Plaid section */
.plaid {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-4);
  margin: 0 0 var(--space-5);
}
.plaid .section-title { margin: 0 0 var(--space-1); font-size: var(--fs-md); }
.plaid .plaid-actions {
  margin: var(--space-2) 0 var(--space-3);
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
}
.plaid table { margin: var(--space-3) 0 var(--space-2); }

/* Nav account menu (right side) */
.top-nav .nav-user { margin-left: auto; display: flex; align-items: center; gap: var(--space-3); }

/* The support launcher — the one always-reachable support entry ("Send
   feedback" today; Help joins it later). A viewport-fixed pill above every
   modal layer (backdrops 50, feedback's own layer 65), so the same tap works
   in every UI state, dialog open or not. Labelled, not icon-only — the word
   is the affordance; a bare bubble icon reads as decoration. Otherwise quiet:
   surface + border like the .ptr chip, not a primary blob. */
.support-launcher {
  position: fixed;
  right:  calc(var(--space-4) + env(safe-area-inset-right, 0px));
  bottom: calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
  z-index: 70;
  display: inline-flex; align-items: center; gap: 7px;
  height: 44px; padding: 0 1rem 0 0.85rem; border-radius: 999px;
  background: var(--surface); color: var(--text-2);
  border: 1px solid var(--border-strong);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
  font-size: var(--fs-sm); font-weight: 500; white-space: nowrap;
  cursor: pointer;
}
/* quiet buttons must re-assert background on :hover — the global
   button:hover (type+pseudo) outspecifies a lone class's background,
   and would flood the pill with --primary-hover (cf. button.link:hover) */
.support-launcher:hover {
  background: var(--surface-3);
  color: var(--text-strong);
  border-color: var(--primary-soft);
}
/* clear the fixed bottom tab bar in compact mode (58px — the main-padding
   allowance for the bar, kept in sync with body.has-bottom-tabs main) */
body.has-bottom-tabs .support-launcher {
  bottom: calc(58px + var(--space-3) + env(safe-area-inset-bottom, 0px));
}
/* On a phone, once the user has found the pill (opened its menu, or put its
   intro away), the word has done its job: it shrinks to the icon, a 44px
   circle, and gives the content back its corner. The label stays for screen
   readers. Desktop keeps the word — it has the room. */
body.has-bottom-tabs .support-launcher.used { width: 44px; padding: 0; justify-content: center; }
body.has-bottom-tabs .support-launcher.used .support-launcher-label {
  position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}
/* Reading down a page on a phone, the pill steps aside like the setup bar
   (body.scrolling-down, set by scroll-direction-watcher): parked bottom-right,
   it sits on the amount column and the row menus. Any scroll up, or the top of
   the page, brings it back, and it never goes while its menu is open. Its
   callout hides with it (by visibility: its entrance animation owns opacity). */
body.has-bottom-tabs .support-launcher { transition: transform 0.25s ease, opacity 0.25s ease; }
body.has-bottom-tabs.scrolling-down .support-launcher[aria-expanded="false"] {
  transform: translateY(var(--space-4)); opacity: 0; pointer-events: none;
}
body.has-bottom-tabs.scrolling-down .support-intro { visibility: hidden; }

/* The launcher's one-time intro — a callout bubble caret-ing down at the pill
   until the user uses or dismisses it once (per-user, localStorage). Sits
   *below* modal backdrops (49 vs 50): education can wait while a dialog is
   up, but the pill itself never hides. Entrance mirrors .setup-continue. */
.support-intro {
  position: fixed; z-index: 49;
  right:  calc(var(--space-4) + env(safe-area-inset-right, 0px));
  bottom: calc(var(--space-4) + 44px + 12px + env(safe-area-inset-bottom, 0px));
  max-width: min(19rem, calc(100vw - 2 * var(--space-4)));
  background: var(--surface);
  border: 1px solid var(--primary-soft); border-left: 3px solid var(--primary);
  border-radius: var(--radius-md);
  box-shadow: 0 12px 32px -12px rgba(0, 0, 0, 0.45);
  padding: var(--space-3) var(--space-5) var(--space-3) var(--space-4);
  animation: support-intro-in 1.5s cubic-bezier(0.2, 0, 0, 1) both;
}
.support-intro::after {
  content: ""; position: absolute; bottom: -6px; right: 3rem;
  width: 10px; height: 10px; transform: rotate(45deg);
  background: var(--surface);
  border-right: 1px solid var(--primary-soft); border-bottom: 1px solid var(--primary-soft);
}
@keyframes support-intro-in {
  0%   { opacity: 0; transform: translateY(12px); }
  30%  { opacity: 1; transform: translateY(0); }
  46%  { transform: translateY(4px); }
  62%  { transform: translateY(0); }
  78%  { transform: translateY(2px); }
  100% { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) { .support-intro { animation: none; } }
.support-intro p {
  margin: 0; font-size: var(--fs-sm); line-height: 1.4; color: var(--text-2);
}
.support-intro strong { color: var(--text-strong); }
/* The intro is one line pointing at the pill — small enough not to sit over
   what the user is reading (the week-in ask keeps the fuller callout). */
.support-intro.brief { max-width: min(21rem, calc(100vw - 2 * var(--space-4)));
  padding: var(--space-2) var(--space-6) var(--space-2) var(--space-3); }
.support-intro.brief p { font-size: var(--fs-xs); }
.support-intro-dismiss {
  position: absolute; top: 2px; right: 2px;
  background: none; border: none; cursor: pointer; color: var(--muted);
  font-size: 1rem; line-height: 1; padding: var(--space-2);
}
.support-intro-dismiss:hover { color: var(--text); background: none; }
/* the week-in ask's CTA row (the intro bubble has no buttons) */
.support-intro-actions {
  display: flex; justify-content: flex-end; margin-top: var(--space-3);
}
body.has-bottom-tabs .support-intro {
  bottom: calc(58px + var(--space-3) + 44px + 12px + env(safe-area-inset-bottom, 0px));
}

/* The launcher's menu — Help / Send feedback, opening upward from the pill.
   Same layer as the launcher (70) so it too rides above any open dialog. */
.support-menu {
  position: fixed; z-index: 70;
  right:  calc(var(--space-4) + env(safe-area-inset-right, 0px));
  bottom: calc(var(--space-4) + 44px + 10px + env(safe-area-inset-bottom, 0px));
  min-width: 11rem;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  box-shadow: 0 12px 32px -12px rgba(0, 0, 0, 0.45);
  padding: var(--space-1);
  display: flex; flex-direction: column;
}
body.has-bottom-tabs .support-menu {
  bottom: calc(58px + var(--space-3) + 44px + 10px + env(safe-area-inset-bottom, 0px));
}
.support-item {
  display: block; width: 100%; text-align: left;
  background: none; border: none; cursor: pointer;
  font: inherit; font-size: var(--fs-sm); color: var(--text-strong);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  text-decoration: none;
}
.support-item:hover { background: var(--surface-3); text-decoration: none; }

/* Help mode — the click-anything-for-help layer. The banner rides above
   every layer in the app (backdrops 50, feedback 65, launcher 70) because
   help mode must work with any dialog open; the ring floats just beneath it
   and never intercepts (pointer-events: none). body.help-mode carries the
   cursor so the whole page says "you're choosing something to learn about". */
body.help-mode, body.help-mode * { cursor: help !important; }
.help-banner {
  position: fixed; z-index: 80;
  /* below the nav (--nav-h, published by top-nav's measure), so nav items
     stay clickable targets for help too */
  top: calc(var(--nav-h, 56px) + var(--space-3));
  left: 50%; transform: translateX(-50%);
  max-width: min(34rem, calc(100vw - 2rem));
  display: flex; align-items: center; gap: var(--space-3);
  background: var(--surface);
  border: 1px solid var(--primary-soft); border-left: 3px solid var(--primary);
  border-radius: var(--radius-md);
  box-shadow: 0 12px 32px -12px rgba(0, 0, 0, 0.45);
  padding: var(--space-2) var(--space-3);
  font-size: var(--fs-sm); color: var(--text-2);
}
.help-banner button { cursor: pointer !important; }
.help-banner .link {
  background: none; border: none; padding: 0; font: inherit;
  color: var(--link); text-decoration: underline;
}
.help-banner-close {
  background: none; border: none; color: var(--muted);
  font-size: 1rem; line-height: 1; padding: var(--space-1);
}
.help-banner-close:hover { color: var(--text); background: none; }
.help-ring {
  position: fixed; z-index: 78;
  pointer-events: none;
  border: 2px solid var(--primary);
  border-radius: var(--radius-sm);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--primary) 25%, transparent);
}

/* ---------------------------------------------------------------------------
   Help site (#/help…) — its own chrome: slim sticky header, TOC left,
   doc right. Deliberately NOT the app shell (see views/help-view).
   --------------------------------------------------------------------------- */
.help-head {
  position: sticky; top: 0; z-index: 45;
  display: flex; align-items: center; gap: var(--space-4);
  background: var(--nav-bg);
  border-bottom: 1px solid var(--nav-border);
  padding: calc(var(--space-3) + env(safe-area-inset-top, 0px)) var(--space-5) var(--space-3);
}
.help-brand { font-weight: 700; color: var(--nav-active-text); text-decoration: none; }
.help-brand:hover { text-decoration: none; }
.help-open-app { margin-left: auto; font-size: var(--fs-sm); color: var(--nav-link); white-space: nowrap; }
.help-toc-toggle { display: none; }
.help-body {
  display: grid; grid-template-columns: 16rem 1fr;
  gap: var(--space-6);
  max-width: 64rem; margin: 0 auto;
  padding: var(--space-5) var(--space-4) var(--space-7);
}
.help-toc { align-self: start; position: sticky; top: 4.5rem;
            max-height: calc(100vh - 6rem); overflow-y: auto; }
.help-toc-section { margin-bottom: var(--space-4); }
.help-toc-title {
  font-size: var(--fs-xs); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--muted); margin-bottom: var(--space-1);
}
.help-toc ul { list-style: none; margin: 0; padding: 0; }
.help-toc li { margin: 0; }
.help-toc-link {
  display: block; padding: 0.3rem var(--space-2);
  border-radius: var(--radius-sm);
  color: var(--text-2); font-size: var(--fs-sm); text-decoration: none;
}
.help-toc-link:hover { background: var(--surface-3); text-decoration: none; }
.help-toc-link.active { background: var(--surface-3); color: var(--text-strong); font-weight: 600; }
.help-toc-topics { padding-left: var(--space-3) !important; margin: 2px 0 var(--space-2) !important; }
.help-toc-topics a {
  display: block; padding: 0.2rem var(--space-2);
  color: var(--muted); font-size: var(--fs-xs); text-decoration: none;
  border-left: 1px solid var(--border);
}
.help-toc-topics a:hover { color: var(--text-strong); text-decoration: none; }
.help-content { min-width: 0; }
/* topic deep-links land clear of the sticky header — with room for the
   .help-target block's top edge above the heading */
.help-doc h2, .help-doc h3 { scroll-margin-top: 5rem; }
/* The ?t= target section: the whole topic — heading through its last
   block — is marked, so where to start AND stop reading is visible. The
   tint re-resolves per theme (the --success-bg color-mix pattern). The
   wrapper also carries the Overview's reward-reveal classes
   (.reward-reveal.reward-card, below): the same homing halo + lift plays
   as the section appears — those animations end on the natural resting
   state, so they hand off to this persistent tint with no cleanup, and
   the reduced-motion block already silences them. */
.help-target {
  display: block;
  margin: var(--space-4) calc(-1 * var(--space-3));
  padding: var(--space-3) var(--space-3) var(--space-2);
  border-left: 3px solid var(--primary);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  background: color-mix(in srgb, var(--primary) 7%, var(--surface));
}
.help-target > h2:first-child,
.help-target > h3:first-child { margin-top: 0; }
.help-target > :last-child { margin-bottom: 0; }
.help-index-group h2 { border-bottom: 1px solid var(--border); padding-bottom: 2px; }
.help-index ul { list-style: none; padding-left: 0; }

@media (max-width: 640px) {
  .help-body { display: block; padding: var(--space-4) var(--space-4) var(--space-6); }
  .help-toc { display: none; position: static; max-height: none; }
  .help-body.toc-open .help-toc {
    display: block; margin-bottom: var(--space-4);
    border: 1px solid var(--border); border-radius: var(--radius-md);
    padding: var(--space-3); background: var(--surface);
  }
  .help-toc-toggle {
    display: inline-block; background: none; cursor: pointer;
    border: 1px solid var(--nav-border); border-radius: 999px;
    color: var(--nav-link); font-size: var(--fs-sm);
    padding: 0.25rem 0.7rem;
  }
  .help-toc-toggle:hover { background: none; color: var(--nav-active-text); }
}

.feedback-modal textarea {
  width: 100%; min-height: 140px; resize: vertical;
  margin-top: var(--space-2);
  font: inherit; color: var(--text-strong);
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); padding: var(--space-2);
}
.feedback-error { color: var(--danger-text); font-size: var(--fs-sm); margin-top: var(--space-2); }

/* Profile / user-account menu: avatar trigger + dropdown (email, Profile, Log out). */
.account-menu { position: relative; display: inline-flex; }
.avatar {
  width: 34px; height: 34px; border-radius: 50%;
  background: var(--nav-active); color: var(--nav-text);
  border: none; padding: 0; cursor: pointer; overflow: hidden;
  font-weight: 600; font-size: var(--fs-sm);
  display: inline-flex; align-items: center; justify-content: center;
}
.avatar:hover, .avatar[aria-expanded="true"] { background: var(--primary); }
.avatar-img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Profile photo controls */
.avatar-lg { width: 72px; height: 72px; font-size: 1.6rem; cursor: default; flex: 0 0 auto; }
.avatar-lg:hover { background: var(--nav-active); }   /* not a button — neutralize the trigger hover */
.avatar-row { display: flex; align-items: center; gap: var(--space-4); }
.avatar-controls { display: flex; flex-direction: column; gap: var(--space-2); }
.avatar-buttons { display: flex; align-items: center; gap: var(--space-3); }
.btn-file {
  display: inline-block; cursor: pointer;
  background: var(--surface); color: var(--text-strong);
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3); font-size: var(--fs-sm);
}
.btn-file:hover { border-color: var(--text-strong); }
.account-dropdown {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 30;
  min-width: 200px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: 0 8px 24px rgba(17,24,39,0.10), 0 2px 6px rgba(17,24,39,0.06);
  padding: 0.25rem 0;
}
.account-email {
  padding: 0.5rem 0.85rem; font-size: var(--fs-xs); color: var(--muted);
  border-bottom: 1px solid var(--border); margin-bottom: 0.25rem;
  word-break: break-all;
}
.account-item {
  display: block; width: 100%; text-align: left;
  padding: 0.5rem 0.85rem; font-size: var(--fs-sm); color: var(--text);
  background: none; border: none; cursor: pointer; text-decoration: none;
}
.account-item:hover { background: var(--surface-3); text-decoration: none; }

/* Settings page. */
.settings { max-width: 640px; }
.settings-section {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-md); padding: var(--space-4); margin-bottom: var(--space-4);
}
.settings-section h3 { margin: 0 0 var(--space-2); }
/* Settings groups (Profile, Security, …) and the index that jumps to them. */
.settings-index { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: var(--space-3) 0 var(--space-5); }
.settings-group { scroll-margin-top: calc(var(--nav-h, 56px) + var(--space-4)); margin-bottom: var(--space-5); }
.settings-group-title {
  font-size: var(--fs-sm); font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--muted); margin: 0 0 var(--space-2);
}
.settings-email { font-size: var(--fs-md); color: var(--text-strong); margin: 0; }
.email-verified { color: var(--success-text); margin: var(--space-1) 0 0; }
.email-unverified { margin-top: var(--space-1); }
.email-unverified p { margin: 0; color: var(--warning-text); }
.password-form { display: flex; flex-direction: column; gap: var(--space-3); margin-top: var(--space-3); max-width: 22rem; }
.password-form label { display: flex; flex-direction: column; gap: var(--space-1); font-size: var(--fs-sm); }
.password-form button { align-self: flex-start; }
.google-btn.reauth { margin-top: var(--space-2); }
.profile-ok { color: var(--success-text); font-size: var(--fs-sm); margin-top: var(--space-2); }
.password-reset { margin-top: var(--space-4); padding-top: var(--space-3); border-top: 1px solid var(--border); }
.password-reset h4 { margin: 0 0 var(--space-1); font-size: var(--fs-sm); }
/* Settings → Subscription plans. The catalog is informational until the final
   release: pre-release the grid greys out under the note (billing-section). */
.plan-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 11rem), 1fr));
  gap: var(--space-3); margin: var(--space-3) 0;
}
.plan-card {
  border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--surface);
  padding: var(--space-3);
  display: flex; flex-direction: column; gap: var(--space-1);
}
.plan-card.current { border-color: var(--primary); box-shadow: 0 0 0 1px var(--primary); }
.plan-card .plan-name {
  font-weight: 600; color: var(--text-strong);
  display: flex; gap: var(--space-2); align-items: baseline; flex-wrap: wrap;
}
.plan-card .plan-tag {
  font-size: var(--fs-xs); font-weight: 400; color: var(--muted);
  border: 1px solid var(--border-strong); border-radius: 999px; padding: 0 var(--space-2);
}
.plan-card .plan-price { font-size: var(--fs-md); color: var(--text-strong); }
.plan-card .plan-desc { margin: 0; font-size: var(--fs-xs); color: var(--muted); }
.plan-card .plan-current { margin-top: auto; font-size: var(--fs-xs); font-weight: 600; color: var(--success-text); }
/* The choose button sits on the card's floor, so cards of differing text
   length still line their actions up. */
.plan-card .plan-choose { margin-top: auto; align-self: flex-start; }
.plan-card .plan-current + .plan-choose { margin-top: var(--space-2); }
.plan-cards.unavailable .plan-card {
  opacity: 0.5; filter: grayscale(1);
  cursor: not-allowed; user-select: none;
}
/* Leads the plan section when checkout isn't set up: the cards below are
   greyed out, and this has to read as the reason, not as fine print. */
.plans-unavailable {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-3);
  margin: var(--space-3) 0;
  padding: var(--space-3) var(--space-4);
  background: var(--warning-bg); color: var(--warning-text);
  border: 1px solid var(--warning); border-left-width: 4px;
  border-radius: var(--radius-md);
  text-align: left;
}
.plans-unavailable-icon { font-size: var(--fs-xl); line-height: 1; }
.plans-unavailable-body { flex: 1 1 10rem; }
.plans-unavailable-body p { margin: 0; font-size: var(--fs-base); }
.plans-unavailable-body .plans-unavailable-title {
  font-size: var(--fs-lg); font-weight: 700; margin-bottom: var(--space-1);
}
/* The subscribe dialog: what they're agreeing to, then Stripe's Payment
   Element (an iframe themed from these same tokens by balance.client.stripe). */
.modal.subscribe-modal { width: min(480px, calc(100vw - 2rem)); }
.subscribe-summary {
  border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--bg);
  padding: var(--space-3); margin-bottom: var(--space-4);
}
.subscribe-line {
  display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-2);
  font-weight: 600; color: var(--text-strong); margin-bottom: var(--space-1);
}
.subscribe-price { font-family: var(--font-num); font-size: var(--fs-md); }
.subscribe-element { min-height: 1px; }
.subscribe-loading { margin: var(--space-3) 0; }
.subscribe-promo-toggle { margin-top: var(--space-3); }
.subscribe-promo { display: block; margin-top: var(--space-3); }
/* A subscriber's billing: card, cancel/resume, receipts — label | value | action. */
.billing-account { margin-top: var(--space-3); }
.billing-row {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2) var(--space-3);
  padding: var(--space-3) 0; border-top: 1px solid var(--border-subtle);
}
.billing-row-label { flex: 0 0 9rem; font-size: var(--fs-sm); font-weight: 600; color: var(--text-strong); }
.billing-row-value { flex: 1 1 12rem; font-size: var(--fs-sm); }
.billing-warning { color: var(--danger-text); font-size: var(--fs-sm); }
.billing-receipts { border-top: 1px solid var(--border-subtle); padding-top: var(--space-3); }
.billing-receipts h4 { margin: 0 0 var(--space-2); font-size: var(--fs-sm); }
.billing-receipts ul { list-style: none; margin: 0; padding: 0; }
.billing-receipts li {
  display: grid; grid-template-columns: 7rem 5.5rem 1fr auto; gap: var(--space-2);
  align-items: baseline; font-size: var(--fs-sm); padding: var(--space-1) 0;
}
.billing-amount { font-family: var(--font-num); text-align: right; }
.subscribe-secure { margin-top: var(--space-3) !important; font-size: var(--fs-xs); color: var(--muted); }
.plan-guarantee { margin: 0; font-size: var(--fs-sm); color: var(--text); }
/* The pay-or-stop gate needs room for the plan cards — wider than the sign-in
   card it otherwise looks like. */
.auth-card.gate-card { max-width: 46rem; }
.btn-link { align-self: flex-start; }
.pre-release-note { color: var(--muted); font-size: var(--fs-sm); margin: var(--space-2) 0 0; }

.danger-zone { border-left: 3px solid var(--danger-text); }
.close-account-form label {
  display: flex; flex-direction: column; gap: var(--space-1);
  font-size: var(--fs-sm); margin-top: var(--space-3);
}
.settings-links {
  display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4);
  margin-top: var(--space-5); font-size: var(--fs-sm);
}
.settings-links a, .settings-links button.link { color: var(--text-muted); }
.settings-links a:hover, .settings-links button.link:hover { color: var(--text-strong); }
.settings-links button.link { text-decoration: none; }
.btn-danger {
  background: none; color: var(--danger-text);
  border: 1px solid var(--danger-text); border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3); cursor: pointer; font-size: var(--fs-sm);
}
.btn-danger:hover { background: var(--danger-bg); }

/* Profile → Notifications: one preference per line, each threshold indented
   under the toggle it qualifies. */
.alert-prefs { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-2); }
.alert-prefs .checkbox { display: inline-flex; align-items: center; gap: var(--space-1); }
.alert-prefs .alert-threshold {
  display: inline-flex; align-items: center; gap: var(--space-2);
  margin-left: var(--space-4); font-size: var(--fs-sm); color: var(--muted);
}
.alert-prefs .alert-threshold input { width: 6.5rem; }

/* Auth (sign in / register) */
.auth {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
}
.auth-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-6);
  width: 100%;
  max-width: 360px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.auth-card h1 { margin: 0 0 var(--space-1); }
/* The sign-in card opens on the lockup: mark over wordmark, centred. */
.auth-brand { display: flex; justify-content: center; margin: var(--space-2) 0 var(--space-3); }
.auth-card h1.auth-title { display: flex; justify-content: center; color: var(--text-strong); margin: 0 0 var(--space-4); }
.auth-card h1.auth-title + .muted { text-align: center; }
.auth-card form { display: flex; flex-direction: column; gap: var(--space-3); margin-top: var(--space-4); }
.auth-card label { display: flex; flex-direction: column; gap: var(--space-1); font-size: var(--fs-xs); color: var(--text-2); }
.auth-card button[type="submit"] { margin-top: var(--space-2); }
.name-fields { display: grid; grid-template-columns: 1fr; gap: var(--space-3); }
.field-hint { font-size: var(--fs-xs); color: var(--muted); margin-top: 2px; line-height: 1.4; }
.auth-error {
  color: var(--danger-text);
  background: var(--danger-bg);
  border-radius: var(--radius-sm);
  padding: var(--space-2);
  font-size: var(--fs-sm);
  margin: 0;
}
.auth-switch { margin-top: var(--space-4); }
.auth-pending {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--muted);
  font-size: var(--fs-sm);
  margin: 0;
}
.auth-pending::before {
  content: "";
  width: 1em;
  height: 1em;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: auth-spin 0.7s linear infinite;
}
@keyframes auth-spin { to { transform: rotate(360deg); } }
.auth-notice {
  color: var(--success-text);
  background: var(--success-bg);
  border-radius: var(--radius-sm);
  padding: var(--space-2);
  font-size: var(--fs-sm);
  margin: 0 0 var(--space-3);
}

/* Google sign-in button — follows Google's brand guidance: white surface,
   Roboto/system font, 1px gray border, the colored "G" mark on the left. */
.google-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  margin-top: var(--space-4);
  width: 100%;
  padding: 0.55rem 0.85rem;
  background: #fff;
  color: #3c4043;
  border: 1px solid #dadce0;
  border-radius: var(--radius-sm);
  font-weight: 500;
  font-size: var(--fs-base);
  text-decoration: none;
  transition: background 120ms ease, box-shadow 120ms ease;
}
.google-btn:hover { background: #f8faff; box-shadow: 0 1px 2px rgba(60,64,67,0.18); text-decoration: none; }
.google-btn:active { background: #f1f3f4; }
.google-btn svg { flex: 0 0 auto; }

/* Sign in with Apple — Apple's guideline is a solid black button. */
.apple-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
  width: 100%;
  padding: 0.55rem 0.85rem;
  background: #000;
  color: #fff;
  border: 1px solid #000;
  border-radius: var(--radius-sm);
  font-weight: 500;
  font-size: var(--fs-base);
  text-decoration: none;
  transition: background 120ms ease, box-shadow 120ms ease;
}
.apple-btn:hover { background: #1a1a1a; box-shadow: 0 1px 2px rgba(0,0,0,0.28); text-decoration: none; color: #fff; }
.apple-btn:active { background: #333; }
.apple-btn svg { flex: 0 0 auto; }

/* "or" divider between Google and email/password form */
.auth-divider {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-4) 0 0;
  color: var(--text-2);
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.auth-divider::before,
.auth-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

/* Responsive: let wide tables scroll instead of overflowing on small screens */
@media (max-width: 640px) {
  /* Phone type scale: the desktop tokens read fine at arm's length but are too
     small in the hand, so bump the small end of the scale (spacing tokens stay
     put — horizontal room is the scarce resource here). --fs-base must be
     ≥ 16px: inputs render at it, and iOS Safari force-zooms the viewport when
     a focused input is any smaller. */
  :root {
    --fs-xs:   0.8125rem;   /* 13px */
    --fs-sm:   0.9375rem;   /* 15px */
    --fs-base: 1rem;        /* 16px */
    --fs-md:   1.0625rem;   /* 17px */
  }

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

  /* --- Tables become stacked cards; each cell shows its column via data-label --- */
  thead { display: none; }
  table, tbody, tr, td { display: block; }
  tbody tr {
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
    padding: var(--space-2) var(--space-3);
    margin-bottom: var(--space-3);
  }
  td {
    display: flex; flex-wrap: wrap; justify-content: space-between;
    gap: var(--space-1) var(--space-3);
    border: none !important; padding: var(--space-1) 0;
    text-align: left !important; white-space: normal;
    /* Plaid merchant strings can be one long unbreakable token; without this
       a single such cell widens the whole page past the viewport. */
    overflow-wrap: anywhere;
  }
  td::before { content: attr(data-label); font-weight: 600; color: var(--muted); font-family: var(--font-sans); }
  td.arrow { display: none; }                 /* the "→" cell is noise in a card */
  td.actions { justify-content: flex-end; }
  td.actions::before { content: none; }

  .headline-num { font-size: 2rem; }
  .tx-filters label { width: 100%; }
  .tx-filters label > select { width: 100%; }
}

/* Seed-defaults call-to-action shown to users without the default tree.
   Quiet — primary content is the user's own categories, not us nagging. */
.seed-banner {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--primary);
  border-radius: var(--radius-sm);
  padding: 0.9rem 1rem;
  margin: var(--space-3) 0;
}
.seed-banner p { margin: 0.15rem 0 0; }
.muted-small { font-size: var(--fs-xs); }

/* ---------------------------------------------------------------------------
   Row actions: ⋮ kebab + dropdown menu + confirm modal
   ---------------------------------------------------------------------------
   Don Norman / NN/g shaped this: destructive actions are rare and shouldn't
   visually compete with the primary data. The kebab is a quiet signifier
   ("more here if you want it") and the dropdown carries the destructive
   wording. The follow-up modal restates *what* is being deleted, satisfying
   the "error prevention" and "visibility of system status" heuristics.
   --------------------------------------------------------------------------- */

td.actions {
  text-align: right;
  white-space: nowrap;
}
td.actions > * + * { margin-left: var(--space-2); }

.row-menu { position: relative; display: inline-block; }

.kebab {
  background: transparent;
  color: var(--muted);
  border: none;
  border-radius: var(--radius-sm);
  padding: 0.25rem 0.45rem;
  font-size: 1.05rem;
  line-height: 1;
  cursor: pointer;
  /* The dots should sit centered and unanimated. */
  font-weight: 700;
  letter-spacing: 0.02em;
}
.kebab:hover, .kebab[aria-expanded="true"] {
  background: var(--surface-3);
  color: var(--text-strong);
}
.kebab:focus-visible {
  outline: 2px solid var(--primary-soft);
  outline-offset: 1px;
}

.row-menu-list {
  position: absolute;
  right: 0;
  top: calc(100% + 2px);
  z-index: 30;
  min-width: 180px;
  list-style: none;
  margin: 0;
  padding: 0.25rem 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: 0 8px 24px rgba(17,24,39,0.10), 0 2px 6px rgba(17,24,39,0.06);
}
.row-menu-list li {
  padding: 0.5rem 0.85rem;
  cursor: pointer;
  font-size: var(--fs-sm);
  color: var(--text);
  user-select: none;
}
.row-menu-list li:hover { background: var(--surface-3); }
.row-menu-list li.danger { color: var(--danger-text); }
.row-menu-list li.danger:hover { background: var(--danger-bg); }

/* Inline rename (categories): the name cell becomes an editable field. */
.inline-edit {
  padding: 0.25rem 0.4rem;
  width: 100%;
  max-width: 18rem;
}
.inline-edit-actions { white-space: nowrap; }

/* Subtle lineage hint on a renamed default category. */
.cat-origin {
  margin-left: var(--space-2);
  color: var(--muted);
  font-size: var(--fs-xs);
  font-style: italic;
}

/* ---------------------------------------------------------------------------
   Manual (user-maintained) indicators
   --------------------------------------------------------------------------- */

/* A small amber "Manual" pill on accounts/transactions the user added by hand
   (not synced from a bank) — they must maintain these themselves. */
.manual-tag {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  background: var(--warning-bg);
  color: var(--warning-text);
  padding: 0.05rem 0.45rem;
  border-radius: var(--radius-pill);
  font-size: var(--fs-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
  vertical-align: middle;
}
.manual-line { margin-top: var(--space-1); }
.tx-desc .manual-line { margin: 0 0 0 var(--space-2); }

/* The transactions list: merchant first, the date said once per day. */
.tx-name { font-weight: 600; color: var(--text-strong); }
.upcoming-table tr.group-head th,
.tx-table tr.day-head th {
  background: var(--bg); color: var(--muted);
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: 0.02em;
  text-align: left; padding: var(--space-2) var(--space-3);
}
@media (max-width: 640px) {
  /* Two lines per transaction, not a card of five labeled lines:
       Merchant ............ ▼$28.09  ⋮
       Meal Delivery · Rewards Visa                                     */
  .tx-table tbody tr.tx-row {
    display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto;
    grid-template-areas: "name name amt act" "cat acct acct act";
    align-items: center; column-gap: var(--space-2);
    border: none; border-bottom: 1px solid var(--border); border-radius: 0;
    margin: 0; padding: var(--space-2) var(--space-1);
  }
  .tx-table tbody tr.tx-row:has(> td.cat-cell:empty),
  .tx-table tbody tr.tx-row:not(:has(> td.cat-cell)) {
    grid-template-areas: "name name amt act" "acct acct acct act";
  }
  .tx-table tr.tx-row > td { padding: 0; }
  .tx-table tr.tx-row > td::before { content: none; }
  .tx-table td.tx-desc { grid-area: name; }
  .tx-table td.tx-amt  { grid-area: amt; justify-content: flex-end; }
  .tx-table td.actions { grid-area: act; }
  .tx-table td.cat-cell { grid-area: cat; font-size: var(--fs-xs); justify-content: flex-start; }
  .tx-table td[data-label="Account"] { grid-area: acct; font-size: var(--fs-xs); color: var(--muted);
    justify-content: flex-start; flex-wrap: nowrap; gap: 0; }
  .tx-table td.cat-cell:not(:empty) + td[data-label="Account"]::before { content: "·"; margin-right: var(--space-1); }
  /* Upcoming (Projections): what + how much, then when · kind · account and
     the status badge. */
  .upcoming-table tbody tr:not(.group-head) {
    display: grid; grid-template-columns: auto auto minmax(0, 1fr) auto auto;
    grid-template-areas: "desc desc desc amt act" "when kind acct status act";
    align-items: center; gap: 2px var(--space-2);
    border: none; border-bottom: 1px solid var(--border); border-radius: 0;
    margin: 0; padding: var(--space-2) var(--space-1);
  }
  .upcoming-table tr > td { padding: 0; }
  .upcoming-table tr > td::before { content: none; }
  .upcoming-table td.up-desc { grid-area: desc; font-weight: 600; }
  .upcoming-table td.up-amt { grid-area: amt; justify-content: flex-end; }
  .upcoming-table td.actions { grid-area: act; }
  .upcoming-table td.up-when,
  .upcoming-table td.up-kind,
  .upcoming-table td[data-label="Account"] { font-size: var(--fs-xs); color: var(--muted); flex-wrap: nowrap; }
  .upcoming-table td.up-when { grid-area: when; }
  .upcoming-table td.up-kind { grid-area: kind; }
  .upcoming-table td.up-kind::before,
  .upcoming-table td[data-label="Account"]::before { content: "·"; margin-right: var(--space-1); }
  .upcoming-table td[data-label="Account"] { grid-area: acct; justify-content: flex-start; min-width: 0; }
  .upcoming-table td[data-label="Account"] > span {
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; overflow-wrap: normal;
  }
  .upcoming-table td.up-status { grid-area: status; justify-content: flex-end; }

  /* Insights: the category and what it spent, then a quiet second line —
     planned · the difference · the change from last month. */
  .insights-table tbody tr {
    display: grid; grid-template-columns: auto auto minmax(0, 1fr); align-items: baseline;
    gap: 2px var(--space-2);
    border: none; border-bottom: 1px solid var(--border); border-radius: 0;
    margin: 0; padding: var(--space-2) var(--space-1);
  }
  .insights-table tr > td { padding: 0; gap: var(--space-1); }
  .insights-table tr > td::before { content: none; }
  .insights-table tr > td:nth-child(1) { grid-column: 1 / 3; grid-row: 1; font-weight: 600; }
  .insights-table tr > td:nth-child(3) { grid-column: 3; grid-row: 1; justify-content: flex-end; }
  .insights-table tr > td:nth-child(2),
  .insights-table tr > td:nth-child(4),
  .insights-table tr > td:nth-child(5) { grid-row: 2; font-size: var(--fs-xs); color: var(--muted); }
  .insights-table tr > td:nth-child(2) { grid-column: 1; font-family: var(--font-sans); }
  .insights-table tr > td:nth-child(2)::before { content: "Plan"; font-weight: 400; }
  .insights-table tr > td:nth-child(4) { grid-column: 2; }
  .insights-table tr > td:nth-child(4):not(:empty)::before { content: "·"; font-weight: 400; }
  .insights-table tr > td:nth-child(5) { grid-column: 3; justify-content: flex-end; font-family: var(--font-sans); }
  .insights-table tr > td:nth-child(5)::before { content: "vs last month"; font-weight: 400; }
  .insights-table tr.insights-child > td:first-child { font-weight: 400; padding-left: var(--space-4); }
  .insights-table tr.insights-child > td:first-child::before { content: none; }

  /* Budgets: category and this month's spend, then the budget and its bar. */
  .budgets-table tbody tr {
    display: grid; grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "name spent" "budget act";
    align-items: center; gap: 2px var(--space-2);
    border: none; border-bottom: 1px solid var(--border); border-radius: 0;
    margin: 0; padding: var(--space-2) var(--space-1);
  }
  .budgets-table tr > td { padding: 0; }
  .budgets-table tr > td::before { content: none; }
  .budgets-table tr > td:nth-child(1) { grid-area: name; font-weight: 600; }
  .budgets-table tr > td:nth-child(2) { grid-area: spent; justify-content: flex-end; }
  .budgets-table tr > td:nth-child(3) { grid-area: budget; font-size: var(--fs-xs); }
  .budgets-table tr > td.actions { grid-area: act; }
  .budgets-table tr.budget-child > td:nth-child(1) { font-weight: 400; padding-left: var(--space-3); }
  /* the needs-a-budget tint belongs to the whole row, not each cell's box */
  .budgets-table tr.needs-row { background: var(--warning-bg); }
  .budgets-table tr.needs-row td { background: none; }

  .upcoming-table tr.group-head,
  .tx-table tr.day-head { display: block; margin: 0; border: none; padding: 0; background: none; }
  .upcoming-table tr.group-head th,
  .tx-table tr.day-head th { display: block; padding: var(--space-3) var(--space-1) var(--space-1); background: none; }
  .tx-table tbody { display: block; }
}

/* A calm blue chip on plans that were auto-detected as recurring (vs hand-made). */
.detected-badge {
  display: inline-flex;
  align-items: center;
  background: var(--surface-3);
  color: var(--link);
  border: 1px solid var(--primary-soft);
  padding: 0.05rem 0.45rem;
  border-radius: var(--radius-pill);
  font-size: var(--fs-xs);
  font-weight: 600;
  white-space: nowrap;
  margin-right: var(--space-1);
  vertical-align: middle;
}

/* Recurring reconciliation: an upcoming item's status, plus the nudge shown
   when a recurring payment was due but no matching transaction has posted.
   Uses the warning palette (same as the uncategorized pill). */
.recon-badge {
  display: inline-flex;
  align-items: center;
  padding: 0.05rem 0.45rem;
  border-radius: var(--radius-pill);
  font-size: var(--fs-xs);
  font-weight: 600;
  white-space: nowrap;
  vertical-align: middle;
}
.recon-badge.expected { background: var(--surface-3); color: var(--muted); }
.recon-badge.realized {
  background: var(--surface-3);
  color: var(--success-text);
  border: 1px solid var(--success);
}
.recon-badge.missed {
  background: var(--warning-bg);
  color: var(--warning-text);
  border: 1px solid var(--warning);
}
.recon-badge.missed.clickable {
  font-family: inherit;
  cursor: pointer;
  transition: background 0.12s ease, box-shadow 0.12s ease;
}
.recon-badge.missed.clickable:hover {
  background: var(--warning);
  color: var(--surface);
}
.recon-badge.missed.clickable:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--warning);
}
tr.row-missed { background: var(--warning-bg); }
/* A detected series nobody has kept yet, missing a payment: say it, quietly —
   it's a guess that didn't pan out, not a bill that went unpaid. */
.recon-badge.tentative {
  background: var(--surface-3); color: var(--muted);
  border: 1px dashed var(--border-strong);
  font-family: inherit; cursor: pointer;
}
.recon-badge.tentative:hover { background: var(--surface-2); color: var(--text-2); }
.keep-recurring { font-size: var(--fs-xs); margin-left: var(--space-1); }
.recon-nudge {
  background: var(--warning-bg);
  color: var(--warning-text);
  border-left: 3px solid var(--warning);
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-4);
  margin: var(--space-3) 0;
}
.recon-nudge-title { font-weight: 700; }
.recon-nudge-detail .link { color: var(--warning-text); font-weight: 600; }

/* CSV import modal */
.import-modal { width: min(560px, calc(100vw - 2rem)); }
.import-csv-file { display: block; margin-top: var(--space-2); font-size: var(--fs-sm); }
.import-csv-text {
  width: 100%; margin-top: var(--space-2);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-xs);
  resize: vertical;
}
.import-result { margin-top: var(--space-3); }
.import-summary { font-weight: 600; margin: 0; }
.import-errors { margin-top: var(--space-1); color: var(--muted); font-size: var(--fs-sm); }
.import-errors ul { margin: var(--space-1) 0 0; padding-left: 1.1rem; }
.import-error { color: var(--danger-text); font-weight: 600; }

/* Bulk (multi-account) import */
.bulk-table { width: 100%; margin-top: var(--space-3); }
.bulk-table th { text-align: left; font-size: var(--fs-sm); color: var(--muted); }
.bulk-table td { vertical-align: middle; }
.bulk-table input { width: 100%; }
.bulk-results { margin: var(--space-2) 0 0; padding-left: 1.1rem; }
.bulk-results li { margin-bottom: var(--space-1); }

/* Section header with an action on the right */
.section-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-3); flex-wrap: wrap;
}
.section-head h2 { margin: 0; }

/* Empty-state call-to-action (e.g. import onboarding) */
.empty-cta { margin: var(--space-2) 0; }
.empty-cta p { color: var(--muted); max-width: 46rem; }

/* Period-over-period spending trend (Insights "vs prev"): up = more spending
   (a warning for expenses), down = less (good). */
.trend-up   { color: var(--warning-text); font-weight: 600; white-space: nowrap; }
.trend-down { color: var(--success-text); font-weight: 600; white-space: nowrap; }
/* Insights' difference column: over the plan draws the eye, under stays quiet.
   It reads as words ("$12 over", "not in the plan"), so the text face, not
   the column figure face. */
.insights-table td.diff { font-family: var(--font-sans); font-variant-numeric: tabular-nums; }
.insights-table .over  { color: var(--warning-text); font-weight: 600; white-space: nowrap; }
.insights-table .under { color: var(--muted); white-space: nowrap; }
/* "on plan" takes the row's size (the page-wide .muted sets its own, which
   read larger than its neighbours in the phone rows) */
.insights-table .on-plan { color: var(--muted); }

/* Dashboard "Coming up" list */
.coming-up { list-style: none; margin: var(--space-2) 0 0; padding: 0; }
.coming-up li {
  display: flex; align-items: baseline; gap: var(--space-3);
  padding: var(--space-2) 0; border-bottom: 1px solid var(--border-subtle);
}
.coming-up li:last-child { border-bottom: none; }
.coming-up .cu-date { color: var(--muted); min-width: 4.5rem; font-variant-numeric: tabular-nums; }
.coming-up .cu-desc { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.coming-up .cu-amt  { font-weight: 600; font-variant-numeric: tabular-nums; }
.coming-up .cu-amt.neg { color: var(--danger-text); }

/* Trusted-device list (Profile) */
.device-list { list-style: none; margin: var(--space-2) 0 0; padding: 0; }
.device-list li {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
  padding: var(--space-2) 0; border-bottom: 1px solid var(--border-subtle);
}
.device-list li:last-child { border-bottom: none; }
.device-info { display: flex; flex-direction: column; min-width: 0; }
.device-ua { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 28rem; }
.device-meta { color: var(--muted); font-size: var(--fs-sm); }

/* Projections low-balance watch: red if an account is projected below $0,
   yellow if within the (adjustable) near-zero threshold. */
.balance-watch { margin: var(--space-2) 0 var(--space-3); }
/* a link inside running text needs more than its colour to stand out (WCAG 1.4.1) */
.threshold-note a { text-decoration: underline; }
.balance-alert {
  padding: var(--space-2) var(--space-4); border-radius: var(--radius-sm);
  margin-bottom: var(--space-2);
}
.balance-alert.danger { background: var(--danger-bg); color: var(--danger-text); border-left: 3px solid var(--danger); }
.balance-alert.warn   { background: var(--warning-bg); color: var(--warning-text); border-left: 3px solid var(--warning); }

/* Future expenses: lumpy one-off / yearly costs with a monthly set-aside and
   a readiness badge per expense. */
.future-expenses { margin-top: var(--space-4); }
.fe-summary { display: flex; gap: var(--space-5); flex-wrap: wrap; margin: var(--space-2) 0; }
.fe-kpi .num { display: block; font-weight: 700; font-size: 1.15rem; font-variant-numeric: tabular-nums; }
.fe-kpi .lbl { color: var(--muted); font-size: var(--fs-sm); }
.fe-list { list-style: none; margin: var(--space-2) 0 0; padding: 0; }
.fe-list li {
  display: grid;
  grid-template-columns: 5.5rem minmax(min(100%, 10rem), 1fr) auto auto auto;
  gap: var(--space-1) var(--space-3);
  align-items: baseline;
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--border);
}
.fe-list li:last-child { border-bottom: none; }
.fe-date { color: var(--muted); font-variant-numeric: tabular-nums; }
.fe-desc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fe-amt  { font-weight: 600; font-variant-numeric: tabular-nums; text-align: right; }
.fe-save { color: var(--muted); font-size: var(--fs-sm); }
.fe-badge { font-size: var(--fs-xs); padding: 2px 8px; border-radius: 999px; white-space: nowrap; justify-self: end; }
.fe-badge.ok     { background: color-mix(in srgb, var(--success) 14%, var(--surface)); color: var(--success-text); }
.fe-badge.warn   { background: var(--warning-bg); color: var(--warning-text); }
.fe-badge.danger { background: var(--danger-bg); color: var(--danger-text); }
@media (max-width: 640px) {
  .fe-list li { grid-template-columns: auto 1fr; }
  .fe-desc { white-space: normal; }
  .fe-amt { text-align: left; }
  .fe-badge { justify-self: start; }
}

/* "Mark as paid" picker: a scrollable list of candidate transactions inside
   the modal, each a full-width quiet button. */
.reconcile-candidates { list-style: none; margin: var(--space-2) 0 0; padding: 0; max-height: 16rem; overflow-y: auto; }
.reconcile-candidates li { margin: 0; }
.reconcile-candidates .link {
  display: flex; gap: var(--space-2); align-items: baseline;
  width: 100%; text-align: left;
  padding: var(--space-2); border-radius: var(--radius-sm);
}
.reconcile-candidates .link:hover { background: var(--surface-3); }
.reconcile-candidates .rc-date { color: var(--muted); min-width: 4.5rem; }
.reconcile-candidates .rc-amt { font-weight: 600; min-width: 5.5rem; text-align: right; }
.reconcile-candidates .rc-desc { color: var(--text-2); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.reconcile-cancel {
  display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-2);
  align-items: baseline; justify-content: space-between;
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--border);
}
.reconcile-cancel-note { color: var(--muted); font-size: var(--fs-sm); }
.reconcile-cancel-btn { color: var(--danger-text); font-weight: 600; }

/* Projection plots (Projections tab): one balance line chart per account / set. */
.plots { display: grid; gap: var(--space-3); margin: var(--space-3) 0; }
.plot-card {
  border: 1px solid var(--border);
  border-radius: 0.5rem;
  padding: var(--space-2) var(--space-3);
  background: var(--surface);
}
.plot-head { display: flex; justify-content: space-between; align-items: baseline; }
.plot-label { font-weight: 600; }
.plot-remove {
  background: none; border: none; cursor: pointer;
  color: var(--muted); font-size: 1.1rem; line-height: 1; padding: 0 var(--space-1);
}
.plot-remove:hover { color: var(--danger-text); background: none; }
.plot-figures { margin: var(--space-1) 0 var(--space-2); }
.projection-chart { width: 100%; height: auto; display: block; overflow: visible; }
.projection-chart .proj-line { fill: none; stroke: var(--primary); stroke-width: 2; }
.projection-chart .chart-grid { stroke: var(--border); stroke-width: 1; opacity: 0.6; }
.projection-chart .chart-axis { stroke: var(--border); stroke-width: 1; }
.projection-chart .chart-zero  { stroke: var(--muted); stroke-width: 1.25; }
.projection-chart .chart-start { stroke: var(--primary-soft); stroke-width: 1.25; stroke-dasharray: 5 3; }
.projection-chart .chart-ylabel,
.projection-chart .chart-xlabel { fill: var(--muted); font-size: 11px; }
.projection-chart .chart-ylabel-r { fill: var(--muted); font-size: 11px; }
.projection-chart .chart-startlabel { fill: var(--muted); font-size: 10px; font-style: italic; }

/* Pace Engine chart: expected (with tolerance band) vs actual, with a hover readout. */
.pace-chart { width: 100%; height: auto; display: block; overflow: visible; }
.pace-chart .chart-grid { stroke: var(--border); stroke-width: 1; opacity: 0.6; }
.pace-chart .chart-axis { stroke: var(--border); stroke-width: 1; }
.pace-chart .chart-ylabel, .pace-chart .chart-xlabel { fill: var(--muted); font-size: 11px; }
.pace-chart .pace-band { fill: var(--primary-soft); opacity: 0.16; stroke: none; }
.pace-chart .pace-expected { fill: none; stroke: var(--primary-soft); stroke-width: 1.75; stroke-dasharray: 5 3; }
.pace-chart .pace-actual { fill: none; stroke: var(--primary); stroke-width: 2.25; }
.pace-chart .pace-today { stroke: var(--muted); stroke-width: 1; stroke-dasharray: 3 3; }
.pace-chart .pace-todaylabel { fill: var(--muted); font-size: 10px; }
.pace-chart .pace-hoverline { stroke: var(--muted); stroke-width: 1; opacity: 0.7; }
.pace-chart .pace-dot-exp { fill: var(--primary-soft); }
.pace-chart .pace-dot-act { fill: var(--primary); }
.pace-chart .pace-tipbox { fill: var(--surface); stroke: var(--border); }
.pace-chart .pace-tipdate { fill: var(--text); font-size: 11px; font-weight: 600; }
.pace-chart .pace-tipline { fill: var(--muted); font-size: 11px; }

/* Pace Engine card: chart + the four-part coach conversation + daily guidance. */
.pace-card {
  border: 1px solid var(--border); border-radius: 0.5rem;
  padding: var(--space-3); background: var(--surface);
}
.pace-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-2); flex-wrap: wrap; }
.pace-head .section-title { margin: 0; }
.pace-status { font-size: 1.15rem; font-weight: 650; margin: var(--space-2) 0; }
.pace-status.on    { color: var(--success-text); }
.pace-status.under { color: var(--success-text); }
.pace-status.over  { color: var(--danger-text); }
.pace-unbudgeted {
  display: block; margin: var(--space-2) 0;
  padding: var(--space-2) var(--space-3);
  background: var(--warning-bg); color: var(--warning-text);
  border: 1px dashed var(--warning); border-radius: var(--radius-sm);
  font-size: var(--fs-sm); font-weight: 600; text-decoration: none;
}
.pace-unbudgeted:hover { background: var(--warning-bg-hover); color: var(--warning-text-hover); }
.tx-unbudgeted {
  margin-left: var(--space-2); font-size: var(--fs-xs); font-weight: 600;
  color: var(--warning-text); text-decoration: none; white-space: nowrap;
}
.tx-unbudgeted:hover { color: var(--warning-text-hover); text-decoration: underline; }
.pace-convo { position: relative; display: grid; gap: var(--space-2); margin: var(--space-2) 0; padding-right: 1.5rem; }
.pace-convo .pace-line { color: var(--text); display: flex; align-items: baseline; gap: var(--space-2); }
.pace-convo .pace-label {
  flex: none; align-self: flex-start;
  display: inline-block; min-width: 4.75rem; text-align: center;
  padding: 0.1rem 0.5rem;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  color: var(--muted); font-size: var(--fs-xs); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.03em;
}
.pace-convo .pace-text { flex: 1; }
.pace-convo > .info-tip { position: absolute; top: 0; right: 0; }
.pace-daily {
  display: flex; gap: var(--space-4); flex-wrap: wrap;
  margin-top: var(--space-2); padding-top: var(--space-2); border-top: 1px solid var(--border);
}
.pace-daily .pace-kpi { display: flex; flex-direction: column; }
.pace-daily .pace-kpi .num { font-size: 1.25rem; font-weight: 650; }
.pace-daily .pace-kpi .lbl { color: var(--muted); font-size: 0.85rem; }
.pace-daily .pace-kpi.over .num { color: var(--danger-text); }
.pace-legend { display: flex; gap: var(--space-3); font-size: 0.8rem; color: var(--muted); }
.pace-legend .swatch { display: inline-block; width: 14px; height: 0; border-top-width: 3px; border-top-style: solid; vertical-align: middle; margin-right: 4px; }
.pace-legend .swatch.expected { border-top-style: dashed; border-top-color: var(--primary-soft); }
.pace-legend .swatch.actual { border-top-color: var(--primary); }
.pace-empty {
  border: 1px dashed var(--border); border-radius: 0.5rem;
  padding: var(--space-3); background: var(--surface); text-align: center;
}
.pace-empty .section-title { margin-top: 0; }
.pace-empty .btn-primary { margin-top: var(--space-2); }

/* Budgets — the category table + the per-category editor modal */
/* Subcategory rows sit under their parent with a static left "grouping rail" +
   indentation — a non-interactive nesting cue (like indent guides / nested
   lists). Deliberately NOT a chevron/triangle, which reads as an expand toggle
   and wrongly suggests the row can be opened. Consecutive child rows' rails
   abut into one continuous line bracketing the group. */
.budgets-table .budget-child td:first-child,
.categories-table .cat-child td:first-child,
.insights-table .insights-child td:first-child {
  padding-left: var(--space-6);
  color: var(--muted);
  position: relative;
}
.budgets-table .budget-child td:first-child::before,
.categories-table .cat-child td:first-child::before,
.insights-table .insights-child td:first-child::before {
  content: "";
  position: absolute;
  left: var(--space-3);
  top: 0;
  bottom: 0;
  border-left: 2px solid var(--border-strong);
}
.budgets-table tr.needs-row td { background: var(--warning-bg); }
.budget-edit { text-align: left; }
.budget-set { color: var(--muted); }
/* The needs-a-budget flag: the same attention language as uncategorized. */
.needs-budget {
  background: var(--warning-bg);
  color: var(--warning-text);
  border: 1px dashed var(--warning);
  border-radius: var(--radius-sm);
  padding: var(--space-1) var(--space-2);
  font-size: var(--fs-xs);
  font-weight: 600;
  cursor: pointer;
}
.needs-budget:hover { background: var(--warning-bg-hover); color: var(--warning-text-hover); }

/* A settable cell that pairs an action with a small trailing note (the
   uncategorized tolerance row's "$X/mo"). */
.budget-set-wrap { display: inline-flex; align-items: center; flex-wrap: wrap; gap: var(--space-2); }
/* This month's spending against a budget: a thin bar and "$X left" / "$X over".
   Neutral until spending passes the budget's tolerance — then, and only then,
   the warning color. */
.budget-progress { display: inline-flex; align-items: center; gap: var(--space-2);
  font-size: var(--fs-xs); color: var(--muted); font-variant-numeric: tabular-nums; }
.budget-bar { position: relative; width: 72px; height: 6px; border-radius: 3px;
  background: var(--border); overflow: hidden; }
.budget-fill { position: absolute; inset: 0 auto 0 0; border-radius: 3px; background: var(--text-2); opacity: .45; }
.budget-progress.over { color: var(--warning-text); font-weight: 600; }
.budget-progress.over .budget-fill { background: var(--warning); opacity: 1; }
/* Where a pre-filled amount came from, so "just Save" is obviously safe. */
.budget-suggest-note { margin: 0; color: var(--muted); font-size: var(--fs-xs); }

/* Deep-linked focus: when the user clicks "⚠ unbudgeted" on a transaction, the
   matching category row is scrolled into view and highlighted so it's an
   unmistakable attention grabber (a brass tint + left accent bar, plus a short
   pulse). The class is removed after the pulse; under reduced-motion the global
   rule drops the animation but the static tint + bar remain. */
.budgets-table tr.budget-focus td {
  background: color-mix(in srgb, var(--primary) 16%, var(--surface));
  animation: budget-focus-pulse 0.9s ease-out 2;
}
.budgets-table tr.budget-focus td:first-child {
  box-shadow: inset 3px 0 0 0 var(--primary);
}
@keyframes budget-focus-pulse {
  0%   { background: color-mix(in srgb, var(--primary) 42%, var(--surface)); }
  100% { background: color-mix(in srgb, var(--primary) 16%, var(--surface)); }
}

/* Budget-fit headline: monthly income vs. planned spending + what's left, and
   the income sources that feed it. */
.budget-summary {
  display: flex; flex-wrap: wrap; gap: var(--space-4) var(--space-6);
  justify-content: space-between; align-items: flex-start;
  border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--surface-2);
  padding: var(--space-3) var(--space-4);
  margin: var(--space-3) 0;
}
.fit-figures { display: flex; flex-wrap: wrap; gap: var(--space-5) var(--space-6); }
.fit-stat { display: flex; flex-direction: column; gap: 2px; }
.fit-stat .lbl { color: var(--muted); font-size: var(--fs-sm); }
.fit-stat .num { font-size: var(--fs-xl); font-weight: 650; font-variant-numeric: tabular-nums; }
.fit-sub { color: var(--muted); font-size: var(--fs-xs); }
.fit-leftover.ok .num  { color: var(--success-text); }
.fit-leftover.over .num { color: var(--danger-text); }
.fit-leftover.warn .num { color: var(--warning-text); }

.income-block { display: flex; flex-direction: column; gap: var(--space-1); min-width: 14rem; }
.income-head { display: flex; align-items: baseline; gap: var(--space-3); }
.income-head .lbl { color: var(--muted); font-size: var(--fs-sm); font-weight: 600; }
.income-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.income-item { display: flex; align-items: baseline; gap: var(--space-2); font-size: var(--fs-sm); }
.income-item .income-name { font-weight: 600; }
.income-item .income-amt { font-variant-numeric: tabular-nums; }
.income-item .income-actions { margin-left: auto; display: flex; gap: var(--space-2); }
/* Detected recurring deposits offered as income sources — quieter than the list above. */
.income-suggest { margin-top: var(--space-2); display: flex; flex-direction: column; gap: var(--space-1); }
.income-suggest > .lbl { color: var(--muted); font-size: var(--fs-xs); font-weight: 600; }
.income-suggest-item { display: flex; align-items: baseline; gap: var(--space-2); font-size: var(--fs-sm); }
.income-suggest-item .income-name { color: var(--muted); }
.income-suggest-item .income-amt { font-variant-numeric: tabular-nums; }
.income-suggest-item .income-actions { margin-left: auto; }

/* Completeness banner: what still needs a budget, or an all-covered confirmation. */
.budget-status {
  border-radius: var(--radius-md); padding: var(--space-3) var(--space-4);
  margin: var(--space-3) 0; font-size: var(--fs-sm);
}
.budget-status.warn { background: var(--warning-bg); color: var(--warning-text); }
.budget-status.ok   { background: var(--success-bg); color: var(--success-text); }
.budget-status-msg { margin: 0 0 var(--space-2); }
.budget-status-cats { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.status-chip {
  background: var(--surface); color: var(--warning-text);
  border: 1px dashed var(--warning); border-radius: var(--radius-pill);
  padding: var(--space-1) var(--space-3); font-size: var(--fs-xs); font-weight: 600;
  cursor: pointer;
}
.status-chip:hover { background: var(--warning-bg-hover); color: var(--warning-text-hover); }
.status-chip .chip-amt { color: var(--muted); font-weight: 500; }

/* The category table's status filter: quiet pills, the chosen one filled. */
.budget-filters { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: var(--space-3) 0 var(--space-2); }
.filter-chip {
  background: var(--surface); color: var(--text-2);
  border: 1px solid var(--border-strong, var(--border)); border-radius: var(--radius-pill);
  padding: var(--space-1) var(--space-3); font-size: var(--fs-xs); font-weight: 600; cursor: pointer;
}
.filter-chip:hover { background: var(--bg); color: var(--text-2); }
.filter-chip.active, .filter-chip.active:hover { background: var(--text-2); border-color: var(--text-2); color: var(--surface); }
.filter-chip .chip-amt { opacity: .7; font-weight: 500; }

/* Savings (Budgets page): goals with derived balances. Tags ride the goal name;
   the meter shows progress toward an optional target; the surplus line carries a
   rolling goal's "move it along" suggestion; the backing note is the quiet
   arithmetic tying goal balances to real account money. */
.goal-tag {
  display: inline-flex; align-items: center;
  background: var(--surface-2); color: var(--muted);
  border: 1px solid var(--border); border-radius: var(--radius-pill);
  padding: 0.05rem 0.45rem; margin-left: var(--space-2);
  font-size: var(--fs-xs); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.04em; white-space: nowrap;
  vertical-align: middle;
}
.goal-meter {
  height: 4px; margin-top: var(--space-1);
  background: var(--surface-3); border-radius: var(--radius-pill); overflow: hidden;
}
.goal-meter-fill { height: 100%; background: var(--success); border-radius: var(--radius-pill); }
.goal-surplus { margin-top: var(--space-1); font-size: var(--fs-xs); }
.goal-match { margin-top: var(--space-1); font-size: var(--fs-xs); display: flex; align-items: baseline; gap: var(--space-2); flex-wrap: wrap; }
.goal-ledger { border-top: 1px solid var(--border); padding-top: var(--space-2); font-size: var(--fs-sm); }
.goal-ledger .lbl { color: var(--muted); font-size: var(--fs-xs); font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; }
.goal-ledger-list { list-style: none; margin: var(--space-1) 0 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.savings-backing, .savings-backing-warn { margin-top: var(--space-2); }
.savings-backing-warn { color: var(--warning-text); }
/* "Cover from savings" alternative inside the budget editor: an aside, set off
   like the drill control, offering the one-time allocation instead of a budget. */
.cover-savings {
  display: flex; flex-direction: column; gap: var(--space-1);
  padding: var(--space-2) 0; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
}
.cover-savings-opt { align-self: flex-start; font-weight: 600; }

/* Uncategorized review nudge (Overview): a calm presence indicator that escalates
   to a warning once the review queue exceeds tolerance. */
.review-nudge {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3);
  border-radius: var(--radius-md); padding: var(--space-2) var(--space-3);
  margin: var(--space-3) 0; font-size: var(--fs-sm);
}
.review-nudge.info { background: var(--surface-2); color: var(--muted); border: 1px solid var(--border); }
.review-nudge.warn { background: var(--warning-bg); color: var(--warning-text); border: 1px solid var(--warning); }
.review-nudge .review-msg { flex: 1; }
.review-nudge .review-link { margin-left: auto; font-weight: 600; white-space: nowrap; }

.budget-editor { display: flex; flex-direction: column; gap: var(--space-3); min-width: 18rem; }
.budget-editor input[type=number], .budget-editor select { width: 100%; }
/* Drill control: retarget the budget to the parent category (or back). A quiet,
   self-contained row above the fields — an aside, not a primary field. */
.budget-scope {
  display: flex; flex-direction: column; gap: 2px;
  padding-bottom: var(--space-2); border-bottom: 1px solid var(--border);
}
.budget-scope-link {
  align-self: flex-start; font-size: var(--fs-sm); font-weight: 600;
}
.budget-field { display: flex; flex-direction: column; gap: 2px; font-size: 0.8rem; color: var(--muted); }
.budget-when { display: flex; gap: var(--space-2); align-items: center; }
.budget-when select { flex: 1; }
/* Must outweigh `.budget-editor input[type=number] { width: 100% }` above, or
   the count swallows the row and the months/years select collapses invisible. */
.budget-editor input.budget-day, .budget-day { width: 5rem; flex: none; }
.add-plot {
  margin-top: var(--space-3); padding: var(--space-2) var(--space-3);
  border: 1px dashed var(--border); border-radius: 0.5rem;
}
.add-plot h3 { margin: 0 0 var(--space-2); font-size: 1em; font-family: var(--font-sans); }
.add-plot-accounts {
  display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); margin-bottom: var(--space-2);
}
.add-plot-accounts .checkbox {
  display: inline-flex; align-items: center; gap: var(--space-1); font-weight: normal;
}
.add-plot input[type=text] { margin-right: var(--space-2); }

/* New-user setup: the Overview "Get started" hero + progress checklist.

   Every setup surface — hero, checklist card, collapsed chip, setup bar —
   wears the same primary-tinted band with the 3px left accent, so they read
   as one object wherever it appears. They also share a view-transition-name:
   navigating between the Overview and a step's page morphs one surface into
   the other (router.cljs decides which navigations animate), making the
   guide→bar continuity literal. At most one of them is ever rendered at a
   time, which is what keeps the shared name valid. */
.setup-hero, .setup-card, .setup-chip, .setup-bar { view-transition-name: setup-guide; }
/* Slow enough to actually read as "the card became the bar" — the morph is
   the message. Duration goes on the whole named subtree (the box movement
   AND its content crossfade); the rest of the page keeps the default quick
   swap, which leaves the eye on the moving box. */
::view-transition-group(setup-guide),
::view-transition-image-pair(setup-guide),
::view-transition-old(setup-guide),
::view-transition-new(setup-guide) { animation-duration: 0.5s; }
::view-transition-group(setup-guide) {
  animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-image-pair(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}

.setup-hero {
  background: color-mix(in srgb, var(--primary) 10%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--primary) 30%, var(--surface));
  border-left: 3px solid var(--primary);
  border-radius: 0.5rem;
  padding: var(--space-4) var(--space-5);
  margin: var(--space-3) 0 var(--space-5);
}
.setup-hero .section-title { margin: 0 0 var(--space-2); font-size: 1.3rem; }
.setup-lede { margin: 0 0 var(--space-3); color: var(--text-2); max-width: 42rem; }
.setup-hero-cta { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.setup-secondary { font-size: var(--fs-sm); color: var(--muted); text-decoration: none; }
.setup-secondary:hover { color: var(--text); text-decoration: underline; }
.setup-trust { margin: var(--space-3) 0 0; font-size: var(--fs-xs); color: var(--muted); }

.setup-card {
  background: color-mix(in srgb, var(--primary) 10%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--primary) 30%, var(--surface));
  border-left: 3px solid var(--primary);
  border-radius: 0.5rem;
  padding: var(--space-3) var(--space-4);
  margin: var(--space-3) 0 var(--space-5);
}
/* everything done: the success shift the bar's .done state uses */
.setup-card.setup-complete {
  background: color-mix(in srgb, var(--success) 12%, var(--surface));
  border-color: color-mix(in srgb, var(--success) 30%, var(--surface));
  border-left-color: var(--success);
}
.setup-card-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-2); }
.setup-card-head .section-title { margin: 0; }
/* first-run orientation under the heading — points at the tether banner */
.setup-card-sub { margin: var(--space-2) 0 0; font-size: var(--fs-sm); color: var(--text-2); max-width: 44rem; }
.setup-collapse, .setup-dismiss {
  background: none; border: none; cursor: pointer;
  color: var(--muted); font-size: var(--fs-sm); padding: var(--space-1) var(--space-2);
}
.setup-collapse:hover, .setup-dismiss:hover { color: var(--text); text-decoration: underline; background: none; }

.setup-meter { display: flex; align-items: center; gap: var(--space-2); margin: var(--space-2) 0 var(--space-3); }
.setup-meter-track { flex: 1; height: 6px; background: var(--surface-2); border-radius: var(--radius-pill); overflow: hidden; }
.setup-meter-fill { height: 100%; background: var(--success); border-radius: var(--radius-pill); transition: width 0.4s ease; }
.setup-meter-label { flex: none; font-size: var(--fs-xs); color: var(--muted); font-weight: 600; white-space: nowrap; }

.setup-steps { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-1); }
.setup-step { display: flex; align-items: flex-start; gap: var(--space-3); padding: var(--space-2); border-radius: var(--radius-sm); }
/* deeper wash than the card's own tint, so the emphasized row still reads */
.setup-step.next { background: color-mix(in srgb, var(--primary) 16%, var(--surface)); }
/* the step being worked right now (its bar is live on the other pages):
   returning to the Overview mid-step, this row echoes the bar's left accent
   so the eye lands where the user left off */
.setup-step.active {
  background: color-mix(in srgb, var(--primary) 16%, var(--surface));
  box-shadow: inset 3px 0 0 var(--primary);
}
/* the whole incomplete row taps through to its page — hover deepens past the
   .next/.active resting tint (higher specificity), so the affordance reads */
.setup-step.clickable { cursor: pointer; }
.setup-step.clickable:hover { background: color-mix(in srgb, var(--primary) 20%, var(--surface)); }
.setup-mark {
  flex: none; width: 1.4rem; height: 1.4rem; margin-top: 0.1rem;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--radius-pill); font-size: var(--fs-xs); font-weight: 700;
  border: 2px solid var(--border-strong); color: transparent;
}
.setup-step.done .setup-mark { background: var(--success); border-color: var(--success); color: var(--surface); }
/* A step completing while the card is on screen: the ✓ pops once. .just-done
   is applied only to steps that flipped during this mount (views), so nothing
   pops just for logging in or navigating back. */
.setup-step.just-done .setup-mark { animation: setup-mark-pop 0.35s cubic-bezier(0.2, 0, 0, 1.4); }
@keyframes setup-mark-pop {
  0%   { transform: scale(0.4); }
  60%  { transform: scale(1.25); }
  100% { transform: scale(1); }
}
.setup-step-body { flex: 1; min-width: 0; }
.setup-step-title { font-weight: 600; display: flex; align-items: baseline; gap: var(--space-2); flex-wrap: wrap; }
.setup-step.done .setup-step-title { color: var(--muted); font-weight: 500; }
.setup-count {
  font-size: var(--fs-xs); font-weight: 700; color: var(--link);
  /* a tint of --primary, not --primary-soft: soft is a stroke tone and
     doesn't contrast with primary text in the darker themes */
  background: color-mix(in srgb, var(--primary) 24%, var(--surface));
  border-radius: var(--radius-pill);
  padding: 0.05rem 0.45rem;
}
.setup-step-blurb { font-size: var(--fs-sm); color: var(--muted); margin-top: 0.1rem; }
.setup-substat { font-size: var(--fs-sm); color: var(--muted); margin-top: 0.1rem; }
.setup-substat a { font-weight: 600; }
/* Inline waiting spinner for the "your bank is preparing transactions" states
   (setup card + the banks table). Em-sized so it sits in running text. */
.setup-spin {
  display: inline-block;
  width: 0.8em; height: 0.8em;
  margin-right: 0.4em;
  vertical-align: -0.05em;
  border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--primary) 30%, transparent);
  border-top-color: var(--primary);
  animation: setup-spin 0.8s linear infinite;
}
@keyframes setup-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .setup-spin { animation: none; }
  .setup-step.just-done .setup-mark { animation: none; }
}
/* In a connect button the ring takes the label's color — a primary ring on a
   primary fill would vanish. */
button .setup-spin {
  border-color: color-mix(in srgb, currentColor 30%, transparent);
  border-top-color: currentColor;
}
/* The hand-off from Plaid's dialog (views/plaid-connect-dialog): a step list
   while the bank links, then the accounts it brought in. */
.connect-steps { list-style: none; margin: 0 0 var(--space-3); padding: 0; }
.connect-step {
  display: flex; align-items: center; gap: 0.6em;
  padding: 0.35rem 0; color: var(--muted);
}
.connect-step.done { color: var(--text-2); }
.connect-step.active { color: var(--text-strong); font-weight: 600; }
.connect-mark {
  flex: 0 0 1.2em; display: inline-flex; justify-content: center;
  font-weight: 700;
}
.connect-step.done .connect-mark { color: var(--success-text); }
.connect-mark .setup-spin { width: 1em; height: 1em; margin-right: 0; }
.connect-done-mark { color: var(--success-text); margin-right: 0.4em; }
.connect-wait { color: var(--muted); }
.connect-accounts { list-style: none; margin: 0 0 var(--space-3); padding: 0; }
.connect-accounts li {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: var(--space-3);
  padding: 0.45rem 0; border-bottom: 1px solid var(--border);
}
.connect-accounts li:last-child { border-bottom: 0; }
.connect-account-name { display: flex; flex-direction: column; min-width: 0; color: var(--text-strong); }
/* The CTA-slot placeholder while the first transactions load — a bigger
   spinner where "Categorize →" will land, so the slot never reads as blank. */
.setup-wait {
  display: inline-flex; align-items: center; gap: 0.5em;
  font-size: var(--fs-sm); color: var(--muted); white-space: nowrap;
}
.setup-wait .setup-spin { width: 1.3em; height: 1.3em; margin-right: 0; }
/* "Next" label on the upcoming checklist row — the row tint alone doesn't
   survive a glance. Solid primary, unlike the tinted count pill beside it. */
.setup-next-tag {
  margin-left: 0.5em;
  font-size: 0.7rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--primary-text); background: var(--primary);
  border-radius: var(--radius-pill);
  padding: 0.1rem 0.5rem;
  vertical-align: 0.1em;
}
/* The pinned mini-guide: shown only while the setup card is scrolled out of
   view (setup-sticky). Same band vocabulary as the card/bar, docked under the
   sticky nav (inline top offset = measured nav height); z below the nav (45)
   so menus stay on top. No view-transition-name here on purpose. */
.setup-pin {
  position: fixed; left: 0; right: 0; margin: 0 auto;
  width: calc(100% - 2 * var(--space-4) - env(safe-area-inset-left, 0px) - env(safe-area-inset-right, 0px));
  max-width: calc(1100px - 2 * var(--space-4));
  z-index: 44;
  display: flex; align-items: center; gap: 0.6em;
  padding: var(--space-2) var(--space-4);
  font: inherit; font-size: var(--fs-sm); font-weight: 600;
  color: inherit; text-align: left; cursor: pointer;
  background: color-mix(in srgb, var(--primary) 10%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--primary) 30%, var(--surface));
  border-left: 3px solid var(--primary);
  border-radius: 0 0 0.5rem 0.5rem;
  box-shadow: 0 6px 16px -8px rgba(0, 0, 0, 0.35);
  animation: setup-pin-in 0.2s ease-out;
}
.setup-pin:hover { background: color-mix(in srgb, var(--primary) 16%, var(--surface)); }
@keyframes setup-pin-in { from { transform: translateY(-40%); opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .setup-pin { animation: none; } }
.setup-pin-count {
  font-size: 0.75rem; font-weight: 700;
  background: color-mix(in srgb, var(--primary) 24%, var(--surface));
  border-radius: var(--radius-pill);
  padding: 0.05rem 0.45rem;
}
.setup-pin-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.setup-pin-up { margin-left: auto; color: var(--link); }
.setup-step-go { flex: none; align-self: center; }
.setup-go {
  display: inline-block; white-space: nowrap;
  font-size: var(--fs-sm); font-weight: 600; text-decoration: none;
  color: var(--link); background: var(--surface); font-family: inherit; cursor: pointer;
  border: 1px solid var(--primary-soft); border-radius: var(--radius-sm);
  padding: var(--space-1) var(--space-3);
}
.setup-go:hover { background: color-mix(in srgb, var(--primary) 22%, var(--surface)); }
.setup-go:disabled { opacity: 0.6; cursor: default; }
.setup-card-foot { margin-top: var(--space-2); text-align: right; }
.setup-complete .setup-step-title { color: var(--success-text); }

.setup-chip {
  display: inline-flex; align-items: center; gap: var(--space-1);
  margin: var(--space-2) 0 var(--space-4);
  background: color-mix(in srgb, var(--primary) 10%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--primary) 30%, var(--surface));
  border-radius: var(--radius-pill); cursor: pointer;
  padding: var(--space-1) var(--space-3);
  font-size: var(--fs-sm); font-weight: 600; color: var(--text-2);
}
.setup-chip:hover { border-color: var(--primary); color: var(--text); background: color-mix(in srgb, var(--primary) 10%, var(--surface)); }

/* The setup bar: tethers a non-Overview page back to the onboarding flow while
   a checklist step is active. Not a full-bleed banner: a strip sized and
   aligned to main's content column — the same width, tint, and left accent as
   the checklist card, because it IS the card, condensed (the view transition
   above morphs one into the other). Shifts to success green when the step's
   work is done and the CTA becomes the reward link. */
.setup-bar {
  display: flex; align-items: center; gap: var(--space-2) var(--space-4); flex-wrap: wrap;
  width: calc(100% - 2 * var(--space-4) - env(safe-area-inset-left, 0px) - env(safe-area-inset-right, 0px));
  max-width: calc(1100px - 2 * var(--space-4));
  margin: var(--space-3) auto 0;
  padding: var(--space-2) var(--space-4);
  background: color-mix(in srgb, var(--primary) 10%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--primary) 30%, var(--surface));
  border-left: 3px solid var(--primary);
  border-radius: 0.5rem;
  font-size: var(--fs-sm);
  /* stays pinned under the nav while the user works down a long page — the
     live count/meter and "Done for now" are the point of the bar (--nav-h is
     published by the nav's own ResizeObserver) */
  position: sticky; top: var(--nav-h, 56px);
  /* the band's tint is translucent-over-surface; opaque enough to pin over
     content. Transition makes the done-flip a felt moment, and (unlike a
     keyframe) fires only on live change — remounts stay quiet. */
  transition: background-color 0.3s ease, border-color 0.3s ease,
              transform 0.25s ease, opacity 0.25s ease;
}
/* Reading down a page, the pinned setup bar / pin slide up under the nav out
   of the way; scrolling back up returns them (scroll-direction-watcher). */
body.scrolling-down .setup-bar,
body.scrolling-down .setup-pin {
  transform: translateY(calc(-100% - var(--space-3))); opacity: 0; pointer-events: none;
}
.setup-pin { transition: transform 0.25s ease, opacity 0.25s ease; }
.setup-bar.done {
  background: color-mix(in srgb, var(--success) 12%, var(--surface));
  border-color: color-mix(in srgb, var(--success) 30%, var(--surface));
  border-left-color: var(--success);
}
.setup-bar-step { font-weight: 700; white-space: nowrap; }
.setup-bar-line { color: var(--text-2); }
.setup-bar-line.ok { color: var(--success-text); font-weight: 600; }
/* the checklist card's meter, carried into the bar: same object, tighter fit */
.setup-bar .setup-meter { margin: 0; flex: none; }
.setup-bar .setup-meter-track { width: 7rem; flex: none; }
.setup-bar-actions { margin-left: auto; display: flex; align-items: center; gap: var(--space-3); }
.setup-bar.done .setup-go { color: var(--success-text); border-color: var(--success); }
.setup-bar.done .setup-go:hover { background: color-mix(in srgb, var(--success) 22%, var(--surface)); }
.setup-bar-close { background: none; border: none; cursor: pointer; color: var(--muted); padding: 0 var(--space-1); }
.setup-bar-close:hover { color: var(--text); background: none; }
.setup-next { display: none; }
/* A phone gets the bar in one line: the step and its way on, with the meter a
   hairline along the bar's foot (its "1 of 5" kept for screen readers) and the
   "Setting up" kicker let go — the tint and accent already say what this is.
   The offer's step is its own link there, and the whole strip is its target. */
@media (max-width: 640px) {
  .setup-bar {
    gap: var(--space-1) var(--space-3);
    padding: var(--space-2) var(--space-3) calc(var(--space-2) + 3px);
  }
  .setup-bar-prefix { display: none; }
  .setup-bar .setup-meter {
    position: absolute; left: var(--space-3); right: var(--space-3); bottom: 4px;
    pointer-events: none;
  }
  .setup-bar .setup-meter-track { width: auto; flex: 1; height: 3px; }
  .setup-bar .setup-meter-label {
    position: absolute; width: 1px; height: 1px; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
  }
  .setup-bar-actions { gap: var(--space-2); }
  .setup-bar.offer .setup-bar-step,
  .setup-bar.offer .setup-bar-actions > .setup-go { display: none; }
  .setup-next { display: block; flex: 1; min-width: 0; }
  .setup-next .setup-go {
    display: block; overflow: hidden; text-overflow: ellipsis;
    border: 0; padding: var(--space-1) 0; background: none;
  }
  .setup-next .setup-go::after { content: ""; position: absolute; inset: 0; }
  .setup-next .setup-go:hover { background: none; }
  .setup-bar-close { position: relative; z-index: 1; }
}

/* First-arrival coach: a popover anchored under the bar's actions, its caret
   pointing up at "Done for now →". Below modals (50), above page content.
   view-transition-name makes the bar a stacking context, so it needs its own
   z-index for the coach to sit over main's positioned content. */
.setup-bar { z-index: 40; }  /* sticky (above) is the positioning context */
/* Sections the setup CTAs scroll to must clear the sticky nav + setup bar. */
.setup-scroll-target { scroll-margin-top: calc(var(--nav-h, 56px) + 3.5rem); }
.setup-coach {
  position: absolute; top: calc(100% + 10px); right: var(--space-5);
  z-index: 39; width: 22rem; max-width: calc(100vw - 2rem);
  background: var(--surface); border: 1px solid var(--primary-soft);
  border-radius: var(--radius-md); padding: var(--space-3) var(--space-4);
  box-shadow: 0 8px 24px rgba(17, 24, 39, 0.18);
  font-size: var(--fs-sm);
}
.setup-coach::before {
  content: ""; position: absolute; top: -6px; right: 3.5rem;
  width: 10px; height: 10px; transform: rotate(45deg);
  background: var(--surface);
  border-left: 1px solid var(--primary-soft); border-top: 1px solid var(--primary-soft);
}
.setup-coach-what { margin: 0 0 var(--space-2); }
.setup-coach-hint { margin: 0 0 var(--space-2); color: var(--muted); }
.setup-coach-got {
  display: block; margin-left: auto;
  background: var(--surface); color: var(--link);
  border: 1px solid var(--primary-soft); border-radius: var(--radius-sm);
  padding: var(--space-1) var(--space-3);
  font-size: var(--fs-sm); font-weight: 600; cursor: pointer;
}
.setup-coach-got:hover { background: color-mix(in srgb, var(--primary) 12%, var(--surface)); }

/* The active step's promised click-target. While the setup bar tethers a page,
   the exact control its CTA promised ("＋ Add income", "＋ New savings goal",
   the needs-a-budget banner, the Accounts page's add/import controls) carries
   a pulsing primary ring — the guide's words say what to do, this says where
   to click, without requiring the user to read the page. setup-target-props
   (views.cljs) removes it the moment the step completes or exits. */
@keyframes setup-target-pulse {
  0%, 100% { box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 50%, transparent); }
  50%      { box-shadow: 0 0 0 7px color-mix(in srgb, var(--primary) 14%, transparent); }
}
/* A steady pulse alone is too easy to miss on arrival, so the target first
   lands with one impactful beat — a converging glow + a small pop, the same
   voice as the unlocked-feature halo — then settles into the pulse. Kept to a
   single element's box-shadow/transform (no ::after) since the target is an
   arbitrary button/link/banner that may own its own pseudo-elements. `land`
   ends exactly on the pulse's resting ring and the pulse is delayed to start
   there, so the handoff is seamless; the later-listed pulse wins the shared
   box-shadow once it begins. */
@keyframes setup-target-land {
  0%   { transform: scale(1.07);
         box-shadow: 0 0 0 6px  color-mix(in srgb, var(--primary) 46%, transparent),
                     0 0 26px 12px color-mix(in srgb, var(--primary) 34%, transparent); }
  55%  { transform: scale(1); }
  100% { transform: scale(1);
         box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 50%, transparent),
                     0 0 0 0 transparent; }
}
.setup-target {
  animation:
    setup-target-land  1.1s cubic-bezier(0.2, 0, 0, 1)   both,
    setup-target-pulse 1.8s 1.1s ease-in-out infinite;
}
/* Bare text links get corners for the ring; container targets (the banner)
   keep their own radius. */
button.setup-target, a.setup-target { border-radius: var(--radius-sm); }
@media (prefers-reduced-motion: reduce) {
  .setup-target {
    animation: none;
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 50%, transparent);
  }
}

/* One-shot reward reveal when the user lands back on the Overview from a
   completed setup step. celebrate-ref runs the clock (reveal → hold → point);
   these rules run the motion. Every animation ends on the tile's natural
   resting state, so nothing jumps when celebrate-props' classes are pulled. */
.reward-reveal { position: relative; }

/* The homing halo — a glow that starts large and diffuse and converges onto
   the tile, then fades as the tile's own reveal takes the eye: big and strong
   enough to pull attention, gone before it competes with the content. Drawn on
   ::after (no reward tile needs an extra element, clips overflow, or owns an
   ::after of its own) as a pure box-shadow glow, so it never covers content. */
.reward-reveal::after {
  content: ""; position: absolute; inset: -14px; z-index: 4;
  border-radius: 0.85rem; pointer-events: none;
  animation: reward-halo 1.5s ease-out both;
}
@keyframes reward-halo {
  0%   { opacity: 0; transform: scale(1.4);
         box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 42%, transparent),
                     0 0 40px 14px color-mix(in srgb, var(--primary) 30%, transparent); }
  25%  { opacity: 1; }
  100% { opacity: 0; transform: scale(1);
         box-shadow: 0 0 0 0 transparent, 0 0 0 0 transparent; }
}
/* the finale homes in the success voice */
.reward-reveal.reward-done::after { animation-name: reward-halo-done; }
@keyframes reward-halo-done {
  0%   { opacity: 0; transform: scale(1.4);
         box-shadow: 0 0 0 3px color-mix(in srgb, var(--success) 46%, transparent),
                     0 0 40px 14px color-mix(in srgb, var(--success) 32%, transparent); }
  25%  { opacity: 1; }
  100% { opacity: 0; transform: scale(1);
         box-shadow: 0 0 0 0 transparent, 0 0 0 0 transparent; }
}

/* :pie — the spending pie draws with a clockwise sweep from 12 o'clock, the
   order the wedges are laid down. A conic mask whose angle is animated through
   a registered custom property (plain custom props don't interpolate). The
   360deg initial-value means an unsupported browser, or the moment the class is
   pulled, shows the full pie — never a blank one. */
@property --reward-sweep {
  syntax: "<angle>"; inherits: false; initial-value: 360deg;
}
.reward-reveal.reward-pie .pie {
  animation: reward-pie-sweep 1.4s ease-out both;
  /* conic 0deg is 12 o'clock, clockwise — the pie's own first-wedge start */
  -webkit-mask: conic-gradient(#000 0 var(--reward-sweep), transparent var(--reward-sweep));
          mask: conic-gradient(#000 0 var(--reward-sweep), transparent var(--reward-sweep));
}
@keyframes reward-pie-sweep {
  from { --reward-sweep: 0deg; }
  to   { --reward-sweep: 360deg; }
}
/* the legend rises in just behind the wedges */
.reward-reveal.reward-pie .pie-legend { animation: reward-rise 0.8s 0.45s ease-out both; }

/* :num — the unlocked figure (net worth) rises and settles into place */
.reward-reveal.reward-num .headline-num {
  animation: reward-num-rise 1.1s cubic-bezier(0.2, 0, 0, 1.1) both;
}
@keyframes reward-num-rise {
  0%   { opacity: 0; transform: translateY(0.3em) scale(0.96); }
  55%  { opacity: 1; }
  100% { opacity: 1; transform: none; }
}

/* :card — the tile lifts into place (the pace card, the budget-fit glance).
   The finale card doesn't lift: it carries the guide↔all-set view-transition
   morph plus its own meter sweep, and a transform here would fight them — the
   green halo alone marks it. */
.reward-reveal.reward-card:not(.reward-done) { animation: reward-rise 0.9s ease-out both; }
@keyframes reward-rise {
  from { opacity: 0.4; transform: translateY(10px); }
  to   { opacity: 1;   transform: none; }
}

/* :pace — the spending-pace chart is drawn left to right: a vertical reveal
   edge sweeps across so the planned and actual curves (and their tolerance
   band and 'today' marker) appear the way you'd draw them, in time order. A
   clip-path wipe rather than a stroke dash-offset, because the planned line is
   already stroke-dasharray'd — a pen-trace would fight its dashes; a wipe keeps
   every element cohesive and needs no per-line path measurement. The card
   itself eases in around it (opacity only — a transform would slide the SVG
   mid-wipe). Both settle on the natural resting state, so nothing snaps when
   the class is pulled. */
.reward-reveal.reward-pace { animation: reward-fade-in 0.7s ease-out both; }
@keyframes reward-fade-in {
  from { opacity: 0.55; }
  to   { opacity: 1; }
}
.reward-reveal.reward-pace .pace-chart {
  animation: reward-pace-wipe 1.35s cubic-bezier(0.35, 0, 0.15, 1) both;
}
@keyframes reward-pace-wipe {
  from { clip-path: inset(0 100% 0 0); }
  to   { clip-path: inset(0 0 0 0); }
}

@media (prefers-reduced-motion: reduce) {
  .reward-reveal::after,
  .reward-reveal.reward-pie .pie,
  .reward-reveal.reward-pie .pie-legend,
  .reward-reveal.reward-num .headline-num,
  .reward-reveal.reward-pace,
  .reward-reveal.reward-pace .pace-chart,
  .reward-reveal.reward-card:not(.reward-done) { animation: none; }
  .reward-reveal.reward-pie .pie { -webkit-mask: none; mask: none; }
  .reward-reveal.reward-pace .pace-chart { clip-path: none; }
}

/* The all-set meter sweeps the last stretch to full — the bar the user filled
   all flow finally reads 100%, visibly. */
.setup-complete .setup-meter-fill { animation: setup-meter-finish 0.7s ease-out; }
@keyframes setup-meter-finish { from { width: 80%; } }

/* Beat 3: the callout that points back up to the setup banner once a reward has
   revealed and settled (setup-continue-bubble). Fixed just under the pinned
   mini-guide so the reward the user scrolled to stays put; fades in with a
   short pulse, then holds — static and dismissable — while the user dwells on
   the new feature. Its caret points up at the pin/guide. */
.setup-continue {
  position: fixed; z-index: 46;
  top: calc(var(--nav-h, 56px) + 3rem);
  left: 0; right: 0; margin: 0 auto;
  width: calc(100% - 2 * var(--space-4) - env(safe-area-inset-left, 0px) - env(safe-area-inset-right, 0px));
  max-width: 27rem;
  display: flex; align-items: stretch;
  background: var(--surface);
  border: 1px solid var(--primary-soft); border-left: 3px solid var(--primary);
  border-radius: var(--radius-md);
  box-shadow: 0 12px 32px -12px rgba(0, 0, 0, 0.45);
  animation: setup-continue-in 1.5s cubic-bezier(0.2, 0, 0, 1) both;
}
.setup-continue::before {
  content: ""; position: absolute; top: -6px; left: 1.4rem;
  width: 10px; height: 10px; transform: rotate(45deg);
  background: var(--surface);
  border-left: 1px solid var(--primary-soft); border-top: 1px solid var(--primary-soft);
}
@keyframes setup-continue-in {
  0%   { opacity: 0; transform: translateY(-12px); }
  30%  { opacity: 1; transform: translateY(0); }
  46%  { transform: translateY(-4px); }
  62%  { transform: translateY(0); }
  78%  { transform: translateY(-2px); }
  100% { opacity: 1; transform: translateY(0); }
}
.setup-continue-body {
  flex: 1; min-width: 0; display: flex; align-items: center; gap: 0.7rem;
  background: none; border: none; cursor: pointer; text-align: left;
  font: inherit; color: inherit;
  padding: var(--space-3) var(--space-3) var(--space-3) var(--space-4);
  border-radius: var(--radius-md) 0 0 var(--radius-md);
}
.setup-continue-body:hover { background: color-mix(in srgb, var(--primary) 8%, var(--surface)); }
.setup-continue-arrow {
  flex: none; color: var(--link); font-weight: 700; font-size: 1.25rem; line-height: 1;
}
.setup-continue-text { font-size: var(--fs-sm); line-height: 1.35; color: var(--text-2); }
.setup-continue-next { color: var(--link); font-weight: 600; }
.setup-continue-dismiss {
  flex: none; align-self: flex-start;
  background: none; border: none; cursor: pointer; color: var(--muted);
  font-size: var(--fs-sm); padding: var(--space-2) var(--space-3);
}
.setup-continue-dismiss:hover { color: var(--text); background: none; }
@media (prefers-reduced-motion: reduce) { .setup-continue { animation: none; } }
/* While the nudge shows there's no data behind the tiles below it — render
   them as a dimmed, non-interactive preview so nothing competes with it. */
/* The preview behind the first-run hero is an example household — said
   plainly, above the dimmed page, so nobody takes its numbers for theirs. */
.example-note {
  margin: var(--space-4) 0 var(--space-2); padding: var(--space-2) var(--space-3);
  border: 1px dashed var(--border-strong); border-radius: var(--radius-sm);
  color: var(--text-2); font-size: var(--fs-sm); background: var(--surface);
}
.dash-preview {
  opacity: 0.35;
  filter: grayscale(1);
  pointer-events: none;
  user-select: none;
}

/* The ⓘ that explains, in a tooltip, what "manual" means. */
.info-tip {
  cursor: help;
  color: var(--muted);
  font-weight: 700;
  font-style: normal;
}
.info-tip:focus-visible {
  outline: 2px solid var(--primary-soft);
  outline-offset: 1px;
  border-radius: var(--radius-pill);
}
/* The ⓘ as a button (movement-picker's clickable variant) — same look as the
   span, so it needs its chrome stripped. */
button.info-tip {
  background: none; border: none; padding: 0;
  font: inherit; font-weight: 700; line-height: 1;
  cursor: help;
  color: var(--muted);
}
button.info-tip:hover { color: var(--text-strong); background: none; }
.manual-tag .info-tip { color: inherit; }

/* A quieter, grey pill for a Plaid account whose bank was disconnected — kept
   as a historical snapshot, no longer syncing. */
.disconnected-tag {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  background: var(--surface-3);
  color: var(--muted);
  padding: 0.05rem 0.45rem;
  border-radius: var(--radius-pill);
  font-size: var(--fs-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
  vertical-align: middle;
}
.disconnected-tag .info-tip { color: inherit; }

/* A connected bank (and its accounts) waiting on the user to sign in again —
   amber like the verify banner, since only they can fix it. */
.reconnect-tag {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  background: var(--warning-bg);
  color: var(--warning-text);
  padding: 0.05rem 0.45rem;
  border-radius: var(--radius-pill);
  font-size: var(--fs-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
  vertical-align: middle;
}
.reconnect-tag .info-tip { color: inherit; }

/* Manual-account creation is tucked behind a link, not shown by default. */
.manual-account-cta { margin-top: var(--space-5); }
.manual-account-cta .info-tip { margin-left: var(--space-2); }
.manual-account { margin-top: var(--space-5); }
.manual-account .muted-small { margin-top: var(--space-2); }

/* ---------------------------------------------------------------------------
   Confirm modal
   --------------------------------------------------------------------------- */

/* The backdrop is the dialog's bounding box: its padding is the space a
   dialog must never sit under. Breathing room top and bottom everywhere; in
   compact mode also the bottom tab bar + the support launcher's band (the
   58px bar, the 44px pill, their gaps, the home-indicator inset — kept in
   sync with body.has-bottom-tabs .support-launcher). Without it a tall form
   on a phone ends exactly in the corner the pill owns, and the pill (z 70,
   above every dialog by design) covers Cancel/Save. */
.modal-backdrop {
  position: fixed;
  inset: 0;
  padding: var(--space-4) 0;
  background: rgba(17, 24, 39, 0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 50;
  outline: none;
  animation: backdrop-in 120ms ease-out;
}
body.has-bottom-tabs .modal-backdrop {
  padding-bottom: calc(58px + var(--space-3) + 44px + var(--space-3) + env(safe-area-inset-bottom, 0px));
}
@keyframes backdrop-in { from { opacity: 0; } to { opacity: 1; } }

/* Pickers and entry sheets (category / income / payment pickers, bulk apply,
   Add a transaction) rise from the bottom on a phone, where the thumb is,
   instead of floating mid-screen. The backdrop (z 50) covers the tab bar
   (45), so the sheet runs to the screen's edge; the help pill still floats
   above every dialog (70), so the sheet keeps the pill's band as its own
   bottom padding — the last option or button always clears it. */
@media (max-width: 640px) {
  .modal-backdrop:has(> .modal.cat-modal),
  .modal-backdrop:has(> .modal.bulk-cat-modal),
  .modal-backdrop:has(> .modal.sheet) {
    align-items: flex-end; padding: var(--space-5) 0 0;
  }
  .modal.cat-modal, .modal.bulk-cat-modal, .modal.sheet {
    width: 100%; max-height: 88vh;
    border-radius: 16px 16px 0 0;
    padding-bottom: calc(44px + 2 * var(--space-3) + env(safe-area-inset-bottom, 0px));
    animation: sheet-up 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
  }
  /* a grab handle says "this is a sheet" */
  .modal.cat-modal::before, .modal.bulk-cat-modal::before, .modal.sheet::before {
    content: ""; align-self: center; flex: none;
    width: 36px; height: 4px; border-radius: 2px; margin: -0.5rem 0 var(--space-2);
    background: var(--border-strong);
  }
  .modal.cat-modal::before { margin: var(--space-2) 0 0; }   /* no top padding there */
}
@keyframes sheet-up { from { transform: translateY(100%); } to { transform: none; } }
@media (prefers-reduced-motion: reduce) {
  .modal.cat-modal, .modal.bulk-cat-modal, .modal.sheet { animation: none; }
}

/* Feedback's layer: the support launcher opens it from any state, so its
   backdrop stacks above every other dialog (50) and below the launcher (70). */
.modal-backdrop.top-layer { z-index: 65; }

/* Bounded to the backdrop's padding box, with the title and actions pinned
   and only the body scrolling — so Save is always on screen, and a form
   taller than a short viewport (Safari with its toolbar up) no longer
   centers off both ends with the title unreachable. */
.modal {
  width: min(440px, calc(100vw - 2rem));
  max-height: 100%;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border-radius: var(--radius-md, 8px);
  box-shadow: 0 20px 50px rgba(17,24,39,0.25), 0 6px 16px rgba(17,24,39,0.12);
  padding: 1.25rem 1.25rem 1rem;
  animation: modal-in 160ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
@keyframes modal-in {
  from { opacity: 0; transform: translateY(8px) scale(0.98); }
  to   { opacity: 1; transform: none; }
}
.modal-title {
  margin: 0 0 var(--space-3);
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--text-strong);
}
.modal-body {
  color: var(--text-2); font-size: var(--fs-sm);
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
}
.modal-body p { margin: 0 0 0.5rem; }
.modal-body p:last-child { margin-bottom: 0; }
.modal-actions {
  flex: 0 0 auto;
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  margin-top: var(--space-4);
}
.consent-required { color: var(--danger-text); font-size: var(--fs-sm); margin-top: var(--space-2); }
.consent-check { display: flex; gap: var(--space-2); align-items: flex-start; font-size: var(--fs-sm); margin-top: var(--space-3); }
.consent-check input { flex: 0 0 auto; margin-top: 3px; }

/* Buttons inside the modal: Cancel is the safe default, Delete carries the
   destructive signal via *text colour* rather than a screaming red fill — the
   button is a deliberate choice, not the row's main affordance. */
.btn-secondary {
  background: var(--surface);
  color: var(--text-strong);
  border: 1px solid var(--border-strong);
  padding: 0.45rem 0.95rem;
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  font-weight: 500;
  cursor: pointer;
}
.btn-secondary:hover { background: var(--surface-3); }

.btn-danger {
  background: var(--surface);
  color: var(--danger-text);
  border: 1px solid var(--border-strong);
  padding: 0.45rem 0.95rem;
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  font-weight: 600;
  cursor: pointer;
}
.btn-danger:hover {
  background: var(--danger-bg);
  border-color: var(--danger);
  color: var(--danger-text);
}
/* Primary (recommended) modal action — filled, unlike the outline-style
   secondary/danger buttons. Used for constructive confirms like "Restore". */
.btn-primary {
  background: var(--primary);
  color: var(--primary-text);
  border: 1px solid var(--primary);
  padding: 0.45rem 0.95rem;
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  font-weight: 600;
  cursor: pointer;
}
.btn-primary:hover { background: var(--primary-hover); border-color: var(--primary-hover); }
.btn-secondary:focus-visible,
.btn-danger:focus-visible,
.btn-primary:focus-visible {
  outline: 2px solid var(--primary-soft);
  outline-offset: 2px;
}

/* ---------------------------------------------------------------------------
   Account rows open a detail sheet on click; the disconnect lives there behind
   a two-step typed confirm so it can't fire by accident.
   --------------------------------------------------------------------------- */

.account-row { cursor: pointer; }
.account-row:hover { background: var(--surface-3); }
/* The account name is the keyboard-focusable trigger — a real button styled to
   read as the plain name until you hover or focus it. */
.account-name {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: var(--text-strong);
  cursor: pointer;
  text-align: left;
}
.account-name:hover { text-decoration: underline; background: none; }
.account-name:focus-visible {
  outline: 2px solid var(--primary-soft);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
.row-open-hint {
  color: var(--muted);
  font-size: var(--fs-lg);
  line-height: 1;
}

.synced-tag {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  background: var(--success-bg);
  color: var(--success-text);
  padding: 0.05rem 0.45rem;
  border-radius: var(--radius-pill);
  font-size: var(--fs-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
  vertical-align: middle;
}

.modal.account-detail { width: min(460px, calc(100vw - 2rem)); }
/* The account's recent life: a balance line and its latest transactions. */
.account-activity { margin-top: var(--space-4); border-top: 1px solid var(--border); padding-top: var(--space-3); }
.account-spark-head { display: flex; justify-content: space-between; gap: var(--space-2); }
.balance-spark { display: block; width: 100%; height: 56px; margin: var(--space-1) 0 var(--space-3); }
.balance-spark polyline { fill: none; stroke: var(--primary); stroke-width: 2; vector-effect: non-scaling-stroke; }
.balance-spark .spark-zero { stroke: var(--border-strong); stroke-dasharray: 3 3; vector-effect: non-scaling-stroke; }
.account-activity-title { font-size: var(--fs-sm); margin: 0 0 var(--space-2); }
.account-recent { list-style: none; margin: 0; padding: 0; }
.account-recent li { display: grid; grid-template-columns: 4.5em minmax(0, 1fr) auto; gap: var(--space-2);
  align-items: baseline; padding: var(--space-1) 0; border-bottom: 1px solid var(--border-subtle); font-size: var(--fs-sm); }
.account-recent-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-strong); }
.detail-list {
  margin: 0;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-2) var(--space-4);
  align-items: baseline;
}
.detail-list dt { color: var(--muted); }
.detail-list dd { margin: 0; color: var(--text-strong); text-align: right; }
.detail-action { margin-top: var(--space-4); }
.detail-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-4);
}
/* Quiet red-text link that reveals the disconnect controls — text only, never
   a filled button (the deliberate-choice pattern). */
.danger-link {
  color: var(--danger-text);
  font-weight: 500;
  text-decoration: none;
}
.danger-link:hover { color: var(--danger-text); text-decoration: underline; }
.detail-danger {
  margin-top: var(--space-4);
  padding-left: var(--space-3);
}
.detail-danger p { margin: 0 0 var(--space-2); }
.detail-danger label {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  font-size: var(--fs-sm);
}

/* ---------------------------------------------------------------------------
   Overview dashboard
   --------------------------------------------------------------------------- */

.dashboard .section-title { margin: var(--space-5) 0 var(--space-2); }

/* Net-worth headline — the single big "where do I stand" number. */
.headline { text-align: center; padding: var(--space-4) 0 var(--space-2); }
.headline-label {
  color: var(--muted);
  font-size: var(--fs-sm);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.headline-num {
  font-size: 2.6rem;
  font-weight: 700;
  line-height: 1.1;
  color: var(--success-text);
  /* a display figure, not a column: the proportional face with tabular digits,
     so a monospace --font-num doesn't give the comma and point a full digit cell */
  font-family: var(--font-sans);
  font-variant-numeric: tabular-nums;
}
.headline-num.neg { color: var(--danger-text); }
.headline .muted-small { color: var(--muted); font-size: var(--fs-sm); }

/* Money sign convention — see docs/sign-convention.md.
   Direction (▲ richer / ▼ poorer / ⇄ transfer) and below-zero levels carry a
   shape glyph or the minus sign; color only reinforces, so the meaning
   survives without color. Money-out is ordinary (text color, the ▼ says it);
   red is for problems — a level below zero, a net loss. */
.nw, .lvl { font-variant-numeric: tabular-nums; white-space: nowrap; }
.nw-glyph { font-size: 0.82em; margin-right: 0.22em; }
.m-pos  { color: var(--success-text); }
.m-out  { color: var(--text); }
.m-neg  { color: var(--danger-text); }
.m-flat { color: var(--neutral); }
/* Separator in a transfer's "Checking → Savings" account cell. */
.tx-arrow { color: var(--muted); }

/* Two responsive chart cards side by side; they stack on narrow screens. */
.chart-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
  align-items: start;   /* each card its own height: no half-empty card beside the pie */
  gap: var(--space-4);
  margin: var(--space-3) 0;
}
.chart-card {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  padding: var(--space-4);
}
.chart-card .section-title { margin: 0 0 var(--space-1); }
.chart-card .muted-small { color: var(--muted); font-size: var(--fs-sm); margin: 0 0 var(--space-3); }
.chart-card svg { max-width: 100%; height: auto; }

/* Spending-by-category pie + legend. */
.pie-wrap { display: flex; gap: var(--space-4); align-items: center; flex-wrap: wrap; }
.pie { flex: 0 0 auto; }
.pie-legend {
  list-style: none;
  margin: 0;
  padding: 0;
  flex: 1;
  min-width: 150px;
  font-size: var(--fs-sm);
}
.pie-legend li { display: flex; align-items: center; gap: var(--space-2); padding: 2px 0; }
.pie-legend .swatch { width: 12px; height: 12px; border-radius: 3px; flex: 0 0 auto; }
.pie-legend .lg-label { flex: 1; color: var(--text-strong); }
.pie-legend .lg-val { color: var(--muted); font-variant-numeric: tabular-nums; }
.pie-legend .lg-pct { color: var(--muted); margin-left: var(--space-2); min-width: 2.6em; text-align: right; }

/* Assets-vs-liabilities bar labels (SVG <text>). */
.bar-val { font-size: var(--fs-sm); font-weight: 600; fill: var(--text-strong); }
.bar-lbl { font-size: var(--fs-sm); fill: var(--muted); }

.dash-section { margin-top: var(--space-4); }
.dash-counts { margin-top: var(--space-4); }

/* Transactions: the always-visible primary search row — a broad text search
   (spanning date / amount / account / category / description), the Show-all
   escape hatch, a summary of what's applied, and the Advanced-search toggle.
   The structured, per-field filters live in the collapsible .tx-filters panel. */
.tx-search-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin: var(--space-3) 0;
}
.tx-search {
  display: flex;
  align-items: stretch;
  flex: 1 1 24rem;
  min-width: min(100%, 16rem);
  max-width: 36rem;
}
.tx-search input[type="search"] {
  flex: 1 1 auto;
  min-width: 0;
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}
.tx-search-go {
  flex: 0 0 auto;
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
  padding-left: var(--space-3);
  padding-right: var(--space-3);
}
.tx-show-all { white-space: nowrap; }
.tx-filter-summary { color: var(--muted); font-size: var(--fs-sm); }
.tx-advanced-toggle { margin-left: auto; white-space: nowrap; }
.tx-short { display: none; }
/* On a phone the controls above the list take two short lines, not five: the
   uncategorized actions in one (a compact button, "Show them"), the search box
   with a "Filters" toggle beside it, and what's showing with "Show all" as a
   plain link after it. */
@media (max-width: 640px) {
  .tx-long { display: none; }
  .tx-short { display: inline; }
  .bulk-actions { gap: var(--space-2); margin-bottom: var(--space-2); }
  .bulk-actions > button:not(.link) { padding: var(--space-1) var(--space-2); }
  .tx-search { flex: 1 1 12rem; min-width: 0; }
  .tx-advanced-toggle { order: 2; margin-left: 0; }
  .tx-filter-summary { order: 3; }
  .btn-secondary.tx-show-all,
  .btn-secondary.tx-show-all:hover {
    order: 4; background: transparent; border: 0; padding: 0;
    color: var(--link); text-decoration: underline; font-weight: 400;
  }
}

/* Transaction filter bar */
.tx-filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: flex-end;
  margin: var(--space-3) 0;
}
.tx-filters label {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  font-size: var(--fs-sm);
  color: var(--muted);
}
.tx-filters .link { align-self: flex-end; padding-bottom: var(--space-1); }
.tx-filters .check { flex-direction: row; align-items: center; gap: var(--space-1); color: var(--text-strong); }
.range-custom { display: inline-flex; gap: var(--space-1); align-items: center; }
.range-custom input { width: 4rem; }

/* Pending / Completed groups in the merged Transactions tab. */
.txn-group { margin-bottom: var(--space-5); }
.txn-group .section-title { margin: var(--space-4) 0 var(--space-2); }

.equity-section { margin-top: var(--space-5); }

/* ===========================================================================
   THEMES
   Pick a look by setting data-theme on <html> (see index.html default +
   balance.client.events :apply-theme). Each block overrides the token set
   declared in :root (the default "Flow" theme). Body font flows from
   --font-sans (html,body); the two rules below route the display + number
   faces to headings and money figures.
   =========================================================================== */

h1, h2, h3, .top-nav .brand, .auth-card h1 { font-family: var(--font-display); }

.kpi > div div, td.right, th.right, .nw, .lvl,
.cat-modal-amt, .coming-up .cu-amt, .coming-up .cu-date,
.pie-legend .lg-val, .pie-legend .lg-pct {
  font-family: var(--font-num);
}

/* ---- Instrument — navigator's navy & brass, IBM Plex + Space Grotesk (the pre-rebrand default) ---- */
[data-theme="instrument"] {
  --bg:#f4f5f2; --surface:#ffffff; --surface-2:#f8f8f5; --surface-3:#eeefe9;
  --text:#1c2839; --text-strong:#13243b; --text-2:#2a3a52; --muted:#5d6b7d;
  --border:#e2e4e0; --border-subtle:#eef0ec; --border-strong:#cfd2cd;
  --primary:#9a5e22; --primary-hover:#80501d; --primary-soft:#c0843a; --primary-text:#ffffff;
  --danger:#dc2626; --danger-hover:#b91c1c; --danger-bg:#fde8e8; --danger-text:#b91c1c;
  --success:#16a34a; --neutral:#5d6b7d;
  --nav-bg:#13243b; --nav-border:#0d1a2c; --nav-link:#aeb9c8; --nav-active:#24364f;
  --nav-text:#ffffff; --nav-active-text:#ffffff;
  --warning:#c0843a; --warning-bg:#fff3e6; --warning-bg-hover:#ffe9cf;
  --warning-text:#965b21; --warning-text-hover:#80501d; --uncat-text:#965b21;
  --chart-1:#e08a1e; --chart-2:#0fa3a3; --chart-3:#2f6fd6; --chart-4:#36b34a;
  --chart-5:#ef4d54; --chart-6:#8b5cf6; --chart-7:#f0a92e; --chart-8:#18b5c9;
  --chart-9:#d4546e; --chart-10:#6b8f3a;
  --font-sans:"IBM Plex Sans", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
  --font-display:"Space Grotesk", var(--font-sans);
  --font-num:"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  --radius-sm:4px; --radius-md:6px;
  /* text tokens, AA on every surface */
  --success-text:#107837; --link:#965b21;
}

/* ---- Clarity — daylight & indigo, Sora + Inter ---- */
[data-theme="clarity"] {
  --radius-sm:4px; --radius-md:6px;
  --bg:#f7f8fa; --surface:#ffffff; --surface-2:#fafbfc; --surface-3:#eef2ff;
  --text:#1e293b; --text-strong:#0f172a; --text-2:#334155; --muted:#64748b;
  --border:#e8ebef; --border-subtle:#f1f3f6; --border-strong:#d6dbe2;
  --primary:#4f46e5; --primary-hover:#4338ca; --primary-soft:#6366f1; --primary-text:#ffffff;
  --danger:#dc2626; --danger-hover:#b91c1c; --danger-bg:#fee2e2; --danger-text:#b91c1c;
  --success:#16a34a; --neutral:#64748b;
  --nav-bg:#ffffff; --nav-border:#e8ebef; --nav-link:#64748b; --nav-active:#eef2ff;
  --nav-text:#0f172a; --nav-active-text:#3730a3;
  --warning:#f43f5e; --warning-bg:#fff1f2; --warning-bg-hover:#ffe4e6;
  --warning-text:#be123c; --warning-text-hover:#9f1239; --uncat-text:#be123c;
  --chart-1:#4f46e5; --chart-2:#06b6d4; --chart-3:#10b981; --chart-4:#f59e0b;
  --chart-5:#f43f5e; --chart-6:#8b5cf6; --chart-7:#3b82f6; --chart-8:#fb923c;
  --chart-9:#ec4899; --chart-10:#14b8a6;
  --font-sans:"Inter", system-ui, -apple-system, sans-serif;
  --font-display:"Sora", var(--font-sans);
  --font-num:"Inter", var(--font-sans);
  /* text tokens, AA on every surface */
  --muted:#5d6b81; --success-text:#107837; --link:#4f46e5;
}

/* ---- Bold — blackout & voltage, Archivo Black + Spline Mono (dark) ---- */
[data-theme="bold"] {
  --radius-sm:4px; --radius-md:6px;
  color-scheme: dark;
  --bg:#0a0a0b; --surface:#141417; --surface-2:#1c1c21; --surface-3:#26262c;
  --text:#d4d4d8; --text-strong:#f5f5f4; --text-2:#b4b4bc; --muted:#8b8b94;
  --border:#26262c; --border-subtle:#1c1c21; --border-strong:#34343c;
  --primary:#d4ff3f; --primary-hover:#e4ff6e; --primary-soft:#b9e636; --primary-text:#0a0a0b;
  --danger:#ff5d4d; --danger-hover:#ff7a6e; --danger-bg:#3a1714; --danger-text:#ff8a7e;
  --success:#3ee089; --neutral:#8b8b94;
  --nav-bg:#0a0a0b; --nav-border:#26262c; --nav-link:#8b8b94; --nav-active:#1c1c21;
  --nav-text:#f5f5f4; --nav-active-text:#d4ff3f;
  --warning:#ff5d4d; --warning-bg:#2a1512; --warning-bg-hover:#3a1714;
  --warning-text:#ff8a7e; --warning-text-hover:#ffa99e; --uncat-text:#ff8a7e;
  --chart-1:#d4ff3f; --chart-2:#3ee089; --chart-3:#36d6e0; --chart-4:#7c8bff;
  --chart-5:#c77dff; --chart-6:#ff5d8f; --chart-7:#ff9f43; --chart-8:#ffe14d;
  --chart-9:#5ad1a0; --chart-10:#9b8cff;
  --font-sans:"Archivo", system-ui, -apple-system, sans-serif;
  --font-display:"Archivo Black", var(--font-sans);
  --font-num:"Spline Sans Mono", ui-monospace, monospace;
  /* text tokens, AA on every surface */
  --muted:#94949d; --success-text:#3ee089; --link:#d4ff3f;
}

/* ---- Zen — paper, ink & sage, Fraunces + Inter ---- */
[data-theme="zen"] {
  --radius-sm:4px; --radius-md:6px;
  --bg:#fafaf7; --surface:#ffffff; --surface-2:#f6f6f1; --surface-3:#efefe8;
  --text:#3d3d39; --text-strong:#232320; --text-2:#55554e; --muted:#8f8f87;
  --border:#ecece6; --border-subtle:#f4f4ef; --border-strong:#deded6;
  --primary:#5f6f57; --primary-hover:#4c5a46; --primary-soft:#8a9a82; --primary-text:#ffffff;
  --danger:#cf4436; --danger-hover:#b23a2e; --danger-bg:#f7e7e4; --danger-text:#b23a2e;
  --success:#2f9460; --neutral:#8f8f87;
  --nav-bg:#fafaf7; --nav-border:#ecece6; --nav-link:#8f8f87; --nav-active:#efefe8;
  --nav-text:#232320; --nav-active-text:#5f6f57;
  --warning:#b08a3a; --warning-bg:#f5efe2; --warning-bg-hover:#efe6d2;
  --warning-text:#8a6a22; --warning-text-hover:#6f5519; --uncat-text:#9a8f7a;
  --chart-1:#7a9a86; --chart-2:#5fa8a0; --chart-3:#7e93c0; --chart-4:#9fb46a;
  --chart-5:#b5786c; --chart-6:#a07fb0; --chart-7:#d0a85a; --chart-8:#6fb0a2;
  --chart-9:#c98fa0; --chart-10:#c79a5e;
  --font-sans:"Inter", system-ui, -apple-system, sans-serif;
  --font-display:"Fraunces", Georgia, serif;
  --font-num:"Inter", var(--font-sans);
  /* text tokens, AA on every surface */
  --muted:#6a6a63; --nav-link:#6a6a63; --success-text:#26774d; --link:#5e6e56;
  --warning-text:#846520; --uncat-text:#716856;
}

/* ---- Inviting — cream, coral & teal, Plus Jakarta Sans (rounder) ---- */
[data-theme="inviting"] {
  --bg:#fbf6f0; --surface:#ffffff; --surface-2:#fff6ee; --surface-3:#ffeede;
  --text:#4a4456; --text-strong:#2e2a39; --text-2:#5c5468; --muted:#897f93;
  --border:#f0e6da; --border-subtle:#f7efe6; --border-strong:#e6d8c8;
  --primary:#ff8a5c; --primary-hover:#f4683a; --primary-soft:#ffb695; --primary-text:#ffffff;
  --danger:#e8553f; --danger-hover:#d23e2a; --danger-bg:#fde7e2; --danger-text:#c43a28;
  --success:#2bb3a3; --neutral:#897f93;
  --nav-bg:#ffffff; --nav-border:#f0e6da; --nav-link:#897f93; --nav-active:#ffe9dd;
  --nav-text:#2e2a39; --nav-active-text:#f4683a;
  --warning:#ff8a5c; --warning-bg:#ffe9dd; --warning-bg-hover:#ffdcc9;
  --warning-text:#f4683a; --warning-text-hover:#d2542c; --uncat-text:#f4683a;
  --chart-1:#ff8a5c; --chart-2:#2bb3a3; --chart-3:#ffc15e; --chart-4:#7c9eff;
  --chart-5:#e8736b; --chart-6:#b48ce0; --chart-7:#5ec9b4; --chart-8:#ffac7a;
  --chart-9:#f49ac0; --chart-10:#8fd17a;
  --font-sans:"Plus Jakarta Sans", system-ui, -apple-system, sans-serif;
  --font-display:"Plus Jakarta Sans", var(--font-sans);
  --font-num:"Plus Jakarta Sans", var(--font-sans);
  --radius-sm:10px; --radius-md:14px;
  /* text tokens, AA on every surface */
  --muted:#6f6679; --nav-link:#6f6679; --nav-active-text:#c2380b;
  --success-text:#1c756b; --link:#c23700; --danger-text:#c03927;
  --warning-text:#c2380b; --warning-text-hover:#a02e09; --uncat-text:#c2380b;
  --primary-text:#2e2a39;   /* coral is too light for white text */
}

/* ---- Beautiful — pine, ivory & antique gold, Playfair + Inter ---- */
[data-theme="beautiful"] {
  --radius-sm:4px; --radius-md:6px;
  --bg:#f6f2e9; --surface:#fffdf8; --surface-2:#fbf8f0; --surface-3:#f1e8d2;
  --text:#3a473f; --text-strong:#1c2b25; --text-2:#2c3a32; --muted:#7c857c;
  --border:#e6dfd0; --border-subtle:#f0ebdd; --border-strong:#d8cfba;
  --primary:#18352c; --primary-hover:#1f4438; --primary-soft:#b08d3f; --primary-text:#cba85a;
  --danger:#bf3b30; --danger-hover:#a8332a; --danger-bg:#f6e4e1; --danger-text:#a8332a;
  --success:#2f7d57; --neutral:#7c857c;
  --nav-bg:#f6f2e9; --nav-border:#e6dfd0; --nav-link:#7c857c; --nav-active:#f1e8d2;
  --nav-text:#1c2b25; --nav-active-text:#18352c;
  --warning:#b08d3f; --warning-bg:#f1e8d2; --warning-bg-hover:#e9dcc0;
  --warning-text:#8a6a22; --warning-text-hover:#6f5519; --uncat-text:#b08d3f;
  --chart-1:#18352c; --chart-2:#2f7d57; --chart-3:#b08d3f; --chart-4:#7a6a93;
  --chart-5:#a8453a; --chart-6:#3a6b80; --chart-7:#c08a4a; --chart-8:#6b8f6e;
  --chart-9:#9a7a2f; --chart-10:#4f7d6a;
  --font-sans:"Inter", system-ui, -apple-system, sans-serif;
  --font-display:"Playfair Display", Georgia, serif;
  --font-num:"Inter", var(--font-sans);
  /* text tokens, AA on every surface */
  --muted:#626962; --nav-link:#626962; --success-text:#2c7451; --link:#18352c;
  --warning-text:#80621f; --uncat-text:#7d642d;
}
/* the signature net-worth figure earns the display serif; tables stay aligned in Inter */
[data-theme="beautiful"] .headline-num { font-family: var(--font-display); font-weight: 600; }

/* ---- Accessible — colour-vision-deficiency safe, blue/orange instead of green/red ----
   Tuned for the most common CVD (red–green: deutan/protan, ~8% of men) and
   tritan. Money direction uses blue (in) vs vermillion-orange (out) — a pairing
   that stays distinct under every common CVD where green vs red collapses. The
   minus sign / ▲▼⇄ glyphs still carry the meaning without colour (see
   docs/sign-convention.md); colour only reinforces. Chart palette is the
   Okabe–Ito qualitative set, designed to be CVD-distinguishable. Inter
   throughout for legibility. */
[data-theme="accessible"] {
  --radius-sm:4px; --radius-md:6px;
  --bg:#f6f7f9; --surface:#ffffff; --surface-2:#fafbfc; --surface-3:#e8f1f8;
  --text:#1d2733; --text-strong:#0c121b; --text-2:#33414f; --muted:#5b6b7b;
  --border:#e4e8ed; --border-subtle:#eef1f5; --border-strong:#d2d9e1;
  --primary:#0072b2; --primary-hover:#005d92; --primary-soft:#56b4e9; --primary-text:#ffffff;
  --danger:#d55e00; --danger-hover:#b34e00; --danger-bg:#fcebdd; --danger-text:#a84800;
  --success:#0072b2; --neutral:#5b6b7b;
  --nav-bg:#ffffff; --nav-border:#e4e8ed; --nav-link:#5b6b7b; --nav-active:#e8f1f8;
  --nav-text:#0c121b; --nav-active-text:#005d92;
  --warning:#b8860b; --warning-bg:#fbf3da; --warning-bg-hover:#f5e9c4;
  --warning-text:#8a6500; --warning-text-hover:#6f5200; --uncat-text:#b34e00;
  --chart-1:#0072b2; --chart-2:#e69f00; --chart-3:#009e73; --chart-4:#cc79a7;
  --chart-5:#56b4e9; --chart-6:#d55e00; --chart-7:#c9a227; --chart-8:#8c5800;
  --chart-9:#004d7a; --chart-10:#6b7785;
  --font-sans:"Inter", system-ui, -apple-system, sans-serif;
  --font-display:"Inter", var(--font-sans);
  --font-num:"Inter", var(--font-sans);
  /* text tokens, AA on every surface */
  --muted:#596878; --success-text:#006ba8; --link:#006ba8;
  --warning-text:#856100; --uncat-text:#ab4b00;
}

/* ---- Nocturne — midnight navy & starlight, a calm dark mode, Sora + Inter ----
   The restful counterpart to Bold: a navigator's night sky, not a neon panel.
   Base is deep navy (#0f1722, not black — distinct from Bold's blackout), with
   layered slate surfaces for depth. Money direction stays legible on dark via
   aurora-teal (in) vs warm amber-coral (out); the periwinkle primary is the
   north star you steer by. Numbers in a mono "instrument readout". */
[data-theme="nocturne"] {
  --radius-sm:4px; --radius-md:6px;
  color-scheme: dark;
  --bg:#0f1722; --surface:#16202e; --surface-2:#1b2738; --surface-3:#223046;
  --text:#c3cedb; --text-strong:#eef3f9; --text-2:#aab8c8; --muted:#7689a0;
  --border:#243246; --border-subtle:#1b2738; --border-strong:#324562;
  --primary:#7aa2e3; --primary-hover:#93b4ec; --primary-soft:#5b82c4; --primary-text:#0f1722;
  --danger:#f0876a; --danger-hover:#f59f86; --danger-bg:#3a2018; --danger-text:#f0a78f;
  --success:#4fc9a8; --neutral:#7689a0;
  --nav-bg:#0c121b; --nav-border:#243246; --nav-link:#7689a0; --nav-active:#1b2738;
  --nav-text:#eef3f9; --nav-active-text:#7aa2e3;
  --warning:#e0b15a; --warning-bg:#2e2616; --warning-bg-hover:#3a3019;
  --warning-text:#e8c06a; --warning-text-hover:#f0d088; --uncat-text:#e8c06a;
  --chart-1:#7aa2e3; --chart-2:#4fc9a8; --chart-3:#e0b15a; --chart-4:#c78ce0;
  --chart-5:#f0876a; --chart-6:#5bb3d4; --chart-7:#9bd17a; --chart-8:#e08ab0;
  --chart-9:#6f8fd4; --chart-10:#d4b86a;
  --font-sans:"Inter", system-ui, -apple-system, sans-serif;
  --font-display:"Sora", var(--font-sans);
  --font-num:"Spline Sans Mono", ui-monospace, monospace;
  /* text tokens, AA on every surface */
  --muted:#909fb2; --success-text:#4fc9a8; --link:#7aa2e3;
}
/* the one accessory: the net-worth headline glows like a star read off the dark
   — display face + a faint halo in its own colour (teal when positive). */
[data-theme="nocturne"] .headline-num {
  font-family: var(--font-display); font-weight: 600; letter-spacing: -0.01em;
  text-shadow: 0 0 26px rgba(79,201,168,0.40);
}
[data-theme="nocturne"] .headline-num.neg { text-shadow: 0 0 26px rgba(240,135,106,0.40); }

/* ---- Flow — the Hyptic brand theme (its tokens are :root, at the top of this
   file). The radii are the mark's: 10/16 instead of the instrument themes' 4/6
   (each of those blocks pins its own). Headings carry the wordmark's weight in
   the one family; the net-worth headline heavier still; digits tabular so
   money lines up. ---- */
[data-theme="flow"] :is(h1, h2, h3, .top-nav .brand),
[data-theme="deep"] :is(h1, h2, h3, .top-nav .brand) { font-weight: 800; letter-spacing: -0.01em; }
[data-theme="flow"] .headline-num,
[data-theme="deep"] .headline-num { font-weight: 800; letter-spacing: -0.02em; }
[data-theme="flow"] :is(td.right, th.right, .nw, .lvl, .cat-modal-amt, .cu-amt, .cu-date, .lg-val, .lg-pct),
[data-theme="deep"] :is(td.right, th.right, .nw, .lvl, .cat-modal-amt, .cu-amt, .cu-date, .lg-val, .lg-pct) {
  font-variant-numeric: tabular-nums;
}
/* Buttons become capsules, like the pillars. Fields keep the card radius. */
[data-theme="flow"] button:not(.link):not(.row-menu-btn):not(.cat-toggle):not(.nav-toggle),
[data-theme="deep"] button:not(.link):not(.row-menu-btn):not(.cat-toggle):not(.nav-toggle) { border-radius: 999px; padding-left: var(--space-4); padding-right: var(--space-4); }
[data-theme="flow"] .top-nav .nav-link, [data-theme="deep"] .top-nav .nav-link { border-radius: 999px; padding-left: var(--space-3); padding-right: var(--space-3); }
/* The light nav needs its own hover: the shared rule paints the active fill, whose
   text colour is the deep teal. */
[data-theme="flow"] .top-nav .nav-link:hover { color: var(--nav-active-text); }
[data-theme="flow"] .loading-bar, [data-theme="deep"] .loading-bar { background: linear-gradient(90deg, #0e5043, #2a9a71, #49c78d, #0e5043); background-size: 200% 100%; }

/* ---- Deep — Flow after dark: the wordmark's ink as the surface family, the mark's
   mint as the action colour. The dark default (OS dark mode lands here). ---- */
[data-theme="deep"] {
  color-scheme: dark;
  --bg:#0c1b1a; --surface:#122524; --surface-2:#15302d; --surface-3:#1a3a36;
  --text:#d8e4e0; --text-strong:#f1f7f4; --text-2:#b9c9c4; --muted:#9bb0aa;
  --border:#1f3f3a; --border-subtle:#15302d; --border-strong:#5e8b82;
  --primary:#49c78d; --primary-hover:#5ada95; --primary-soft:#2a9a71; --primary-text:#0c1b1a;
  --danger:#ef8b76; --danger-hover:#f4a391; --danger-bg:#4a241c; --danger-text:#f0a090;
  --success:#5ada95; --neutral:#9bb0aa;
  --nav-bg:#0a1716; --nav-border:#1f3f3a; --nav-link:#93a8a2; --nav-active:#1a3a36;
  --nav-text:#f1f7f4; --nav-active-text:#8deeaf;
  --warning:#e2b84a; --warning-bg:#3a3218; --warning-bg-hover:#4a4020;
  --warning-text:#e8c46a; --warning-text-hover:#f3d78a; --uncat-text:#e8c46a;
  --brand-mint:#8deeaf;
  --chart-1:#2f9a66; --chart-2:#b98d28; --chart-3:#5a86cf; --chart-4:#d5714c;
  --chart-5:#2ea7a1; --chart-6:#9a62b8; --chart-7:#7a9a2a; --chart-8:#d26c8a;
  --chart-9:#4b9bcc; --chart-10:#b58042;
  --font-sans:"Plus Jakarta Sans", "Inter", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
  --font-display:var(--font-sans);
  --font-num:var(--font-sans);
  /* text tokens, AA on every surface */
  --success-text:#77e5a8; --link:#77e5a8;
}

/* ---- Theme picker (Profile › Appearance) ---- */
.theme-options {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(116px, 1fr));
  gap: var(--space-3);
  margin-top: var(--space-3);
}
.theme-option {
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  padding: var(--space-2);
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  color: var(--text);
  text-align: left;
}
.theme-option:hover { border-color: var(--primary); background: var(--surface); }
.theme-option.active {
  border-color: var(--primary);
  box-shadow: 0 0 0 2px var(--primary);
}
.theme-swatch {
  display: block;
  border-radius: var(--radius-sm);
  overflow: hidden;
  border: 1px solid var(--border);
  background: var(--bg);
}
.theme-swatch .tsw-bar { display: block; height: 16px; background: var(--nav-bg); }
.theme-swatch .tsw-dots { display: flex; gap: 6px; padding: 8px; }
.theme-swatch .tsw-dots i { width: 15px; height: 15px; border-radius: 50%; display: inline-block; }
.theme-swatch .tsw-dots .d1 { background: var(--primary); }
.theme-swatch .tsw-dots .d2 { background: var(--success); }
.theme-swatch .tsw-dots .d3 { background: var(--danger); }
.theme-name { font-size: var(--fs-sm); font-weight: 600; }
.theme-check { color: var(--link); }

/* Zen leans on light weights even for headings — part of its calm. */
[data-theme="zen"] h1, [data-theme="zen"] h2, [data-theme="zen"] h3 { font-weight: 400; }

/* The category tree: a root's ▸ / ▾ opens its subcategories beneath it. */
.categories-table .cat-root td:first-child { font-weight: 600; }
.cat-toggle {
  background: none; border: none; padding: 0 var(--space-1) 0 0; margin: 0;
  color: var(--text-2); font-size: 1rem; line-height: 1; cursor: pointer; width: 1.6em; text-align: left;
}
.categories-table .cat-root .muted-small { font-weight: 400; color: var(--muted); }
.cat-toggle:hover { background: none; color: var(--text-strong); }
@media (max-width: 640px) {
  /* one line per category: name, count, ⋮ */
  .categories-table tbody tr {
    display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center;
    gap: var(--space-2); border: none; border-bottom: 1px solid var(--border); border-radius: 0;
    margin: 0; padding: var(--space-2) var(--space-1);
  }
  .categories-table tr > td { padding: 0; }
  .categories-table tr > td::before { content: none; }
  .categories-table tr > td:first-child { justify-content: flex-start; align-items: baseline; gap: 0; }
  .categories-table tr.cat-child > td:first-child { padding-left: var(--space-5); }
}
