@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@500;600;700;800&display=swap');

/* ============================================================================
   Design system — "Quiet Analytical"
   ----------------------------------------------------------------------------
   The product's features are sophisticated; the visual layer should match that
   rather than decorate it. The rules:
     - DM Sans for interface, JetBrains Mono for EVERY financial figure
     - one forest-green accent for positive/on-track; a dark brick only for
       over-budget; everything else is near-black or grey. No blue anywhere.
     - one 4px spatial grid, one hairline weight, three radii (8/6/4)
     - boundaries come from alignment and whitespace first, a hairline second,
       a fill third, and a heavy surface never
     - colour communicates a three-step scale (good -> watch -> bad), never
       a rainbow of equal-weight states, and never at full volume
   ============================================================================ */
:root {
  /* --- surfaces --- */
  --bg: #FFFFFF;
  --bg-app: #F6F6F5;
  --bg-sunken: #F4F4F3;
  /* Half a step off white — enough to read as "not yet" across a block of calendar
     cells, quiet enough that a single cell does not look selected. */
  --bg-future: #F8F8F7;
  --bg-hover: #F7F7F6;
  --bg-inset: #EBEBE9;
  /* Progress tracks read against white, and --bg-inset was too close to it to show
     where a bar ENDS — the empty half of a bar is data too. */
  --track: #E3E3DF;
  /* Chart neutral: heavier than a track so a bar reads as a mark rather than as the
     empty part of one. It was a hex literal in two places in analytics.js. */
  --neutral-fill: #C9C9C4;
  /* The mark used by every "how big is this amount" bar in Analytics — Merchants and
     Trends both answer that question and now draw it identically. Deliberately NOT the
     category's own hue: length already encodes the quantity, so colouring the bar by
     category made a nine-hue rainbow that encoded nothing the label beside it did not
     already say, in a palette whose stated rule is a three-step good/watch/bad scale. */
  --bar-fill: rgba(20, 22, 28, .45);
  --border: #E4E4E1;
  --border-strong: #D3D3CF;

  /* --- type ---
     Measured against white, not eyeballed. WCAG AA for body text is 4.5:1, and the
     old muted grey (#8B8E95) sat at 3.28 — it failed, and it was carrying transfer
     amounts, day headings and every explanatory note in the product.
       --text            18.1:1
       --text-secondary   9.5:1
       --text-muted       6.6:1  (6.0:1 on the sunken fills)
     All three clear AA (4.5:1) with room to spare, because passing the standard and
     being comfortable to read are different bars — at 11-12px, minimum-contrast grey
     is legible on a good monitor and gone in sunlight. Three steps far enough apart
     to still read as a hierarchy, none of them decorative. */
  --text: #14161C;
  --text-secondary: #43464D;
  --text-muted: #5A5D64;

  /* --- semantic ---
     Money-coded green, and a red held in reserve. Nothing competes with them. */
  --accent: #14532D;
  --accent-hover: #0E3D21;
  --accent-wash: #EAF1EC;
  --income: #14532D;
  --income-wash: #EAF1EC;
  /* Calm authority: over-budget is information, not an emergency. A dark brick
     reads as serious at a glance without spiking anyone's pulse. */
  --negative: #8A3D31;     /* over budget / overdrawn ONLY */
  --negative-wash: #F7F2F1;
  --warn: #14161C;         /* "watch this" is weight, not a third hue */
  --warn-wash: #F4F4F3;
  /* A transfer is subdued by WEIGHT, not by being too pale to read: it is a real
     figure in a financial ledger and has to meet the same bar as the rest. */
  --transfer: #5A5D64;

  /* --- geometry ---
     Three radii, no exceptions: 8 for surfaces that float above the page, 6 for
     anything you can click, 4 for the small square things. Pills are for tags. */
  --radius-lg: 8px;        /* modals, drawers, menus, popovers */
  --radius: 6px;           /* buttons, inputs, chips, glyphs */
  --radius-sm: 4px;        /* swatches, tooltips, micro-controls */
  --radius-bar: 2px;
  --radius-pill: 9999px;   /* tags only */

  /* One hairline. There is no second border weight anywhere in this product; the
     only 2px stroke is a selected-state indicator, which is not a boundary. */
  --hairline: 1px solid var(--border);

  /* --- 4px spatial grid ---
     Every padding, margin and gap in this file resolves to a multiple of 4. The
     exceptions are 1px hairlines and 2px optical nudges on bar markers. */
  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
  --s5: 20px; --s6: 24px; --s8: 32px; --s10: 40px; --s12: 48px;
  --shadow-overlay: 0 10px 30px rgba(15, 17, 23, .12), 0 1px 2px rgba(15, 17, 23, .08);

  --font-sans: 'DM Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  /* A finance product lives and dies on how its numbers look. Every figure in the
     product is set in mono so digits align in any column, at any size. */
  --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --font-hero: var(--font-mono);

  --sidebar-w: 214px;
  --row-h: 44px;
}

.eyebrow, .metric-label, .section-head h2, .stat-label, .section-sub-head, .field label, th {
  font-size: 11px;
  font-weight: 500;
  text-transform: none;
  letter-spacing: 0;
  color: var(--text-secondary);
}

svg.icn * { stroke-width: 1.6; }

* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  font-family: var(--font-sans);
  background: var(--bg-app);
  color: var(--text);
  font-size: 13px;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
/* Every figure in the product uses tabular figures so decimal points stack
   vertically in any column. This is not optional in a financial interface. */
.num, .tnum, td.num, th.num, .amount, input[type="number"],
.metric-value, .stat-value, .row-amount, .budget-figs, .upcoming-amount,
.stack-value, .day-total, .cal-amount, .cal-day-num,
.kpi-value, .kpi-line-value, .merch-total, .section-total,
.comp-value, .comp-share, .comp-total,
.cf-item-amt, .cf-balance,
.waterfall-legend-item b {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}
/* ---------- private mode ----------
   One rule, because the work of deciding WHAT is a figure is done in JS (see
   hideMoneyIn() in main.js) rather than by a selector list here. A list would
   have to be extended every time a screen gained a number, and the failure mode
   of forgetting is a figure that stays legible in the one mode that exists to
   hide it.

   The figure is not smeared, it is emptied: the glyphs go transparent and the
   span's own box fills with the track grey that every progress bar in the
   product is already drawn in. A blur was the first attempt and it read as a
   rendering fault at the Dashboard's 56px, because a gaussian scaled to the
   type turns a hero figure into a smudge. A filled slot reads as deliberate at
   every size, and it borrows a colour the app is already made of rather than
   introducing an effect it uses nowhere else.

   The box-shadow is the padding: a background alone stops at the glyph box and
   leaves the pill looking pinched against the digits. In em so it holds its
   proportion from an 11px footnote up to the hero. */
.private .money-hidden {
  color: transparent;
  background: var(--track);
  border-radius: 999px;
  box-shadow: 0 0 0 0.14em var(--track);
  box-decoration-break: clone; -webkit-box-decoration-break: clone;
  user-select: none; -webkit-user-select: none;
  /* The pill takes its height from the inline box, and at the Dashboard's 56px
     that box is half again taller than the digits — the slot came out as a
     lozenge the size of a loading skeleton. Pinned to the glyphs instead, which
     is invisible at 11px and is the whole difference at 56px. */
  display: inline-block;
  line-height: 0.82;
  vertical-align: baseline;
}
/* An SVG chart label takes the class on the <text> element itself — a <span> in
   the SVG namespace does not paint. A pill cannot be drawn there without faking
   a rect behind it, so the label simply goes: the gridlines still carry the
   shape of the series, and an axis with no numbers on it is honestly what this
   mode is asking for. */
.private text.money-hidden { fill: transparent; }
/* Transitioned rather than snapped: the toggle is a full re-render, and figures
   appearing sharp-then-filled reads as a rendering fault. */
.money-hidden { transition: background-color .12s ease, color .12s ease; }
@media (prefers-reduced-motion: reduce) { .money-hidden { transition: none; } }

/* Figures that are set in the interface face, not the mono one — table cells, dates,
   counts, relative-day labels. They still have to hold their columns, so they get the
   tabular figures without being forced into mono beside the proper nouns they sit with. */
td, th, .section-meta, .summary-meta, .row-meta-label, .upcoming-when,
.cal-date-label, .budget-flag, .stack-detail, .comp-foot, .kpi-label, .stat-label {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}
h1, h2, h3 { margin: 0; font-weight: 600; letter-spacing: -0.011em; }
button { font-family: inherit; }

.app { display: flex; align-items: stretch; min-height: 100vh; background: var(--bg-app); }

/* ---------- sidebar: text labels, not a row of guessable glyphs ---------- */
.sidebar {
  width: var(--sidebar-w);
  flex-shrink: 0;
  background: var(--bg-app);
  border-right: 1px solid var(--border);
  padding: var(--s6) 0 var(--s4);
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  height: 100vh;
  z-index: 20;
}
.brand {
  display: flex; align-items: center; gap: var(--s2);
  padding: 0 var(--s4) var(--s6);
  color: var(--text);
}
.brand-mark {
  width: 24px; height: 24px; border-radius: var(--radius);
  background: var(--accent); color: #fff;
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.brand-name { font-size: 13px; font-weight: 600; letter-spacing: -0.01em; }

nav.tabs { display: flex; flex-direction: column; gap: 1px; padding: 0; }
nav.tabs button {
  display: flex; align-items: center; gap: var(--s3);
  width: 100%; height: 36px; padding: 0 var(--s4);
  background: none; border: none; border-left: 2px solid transparent; border-radius: 0;
  color: var(--text-secondary); font-size: 13px; font-weight: 500;
  cursor: pointer; text-align: left;
  transition: color .12s ease, border-color .12s ease;
}
nav.tabs button svg { stroke: currentColor; flex-shrink: 0; opacity: .8; }
nav.tabs button:hover:not(.active) { color: var(--text); }
nav.tabs button.active { border-left-color: var(--accent); color: var(--text); font-weight: 600; background: none; }
nav.tabs button.active svg { opacity: 1; }

.sidebar-spacer { flex: 1; }
.sidebar-action { padding: 0 var(--s4) var(--s3); }
.sidebar-note {
  padding: var(--s3) var(--s4) 0; margin: 0 var(--s4);
  border-top: 1px solid var(--border);
  font-size: 11px; line-height: 1.5; color: var(--text-muted);
}

.main { flex: 1; min-width: 0; background: var(--bg); }
/* Generous air at the top of every page and between major blocks, so the dense
   ledgers below have something to breathe against. */
/* Trailing air, kept deliberately small: at 64px the dashboard cleared the viewport
   by exactly 0px, and a page that fits only on a perfect-sized window doesn't fit. */
.main-inner { padding: var(--s10) 48px 48px; }
/* Content still holds a comfortable measure inside the panel, so ledger rows and
   charts don't stretch across an ultrawide display. */
.main-inner > .view { max-width: 1080px; margin: 0 auto; }
/* Charts keep their own, narrower measure: stretched across a full-width column, a
   six-bar series becomes six islands in a field. */
.chart-measure { max-width: 780px; }

section.view { display: none; }
section.view.active { display: block; }
.subview { display: none; }
.subview.active { display: block; }

/* Placed under the active subview's page head by placeSubtabs() in main.js, so every
   screen in the product puts its title at the same height. 24px above (the page head's
   own margin) and 24px below. */
nav.subtabs {
  display: flex; gap: var(--s5); margin-bottom: var(--s6);
  border-bottom: var(--hairline);
}
nav.subtabs button {
  background: none; border: none; border-bottom: 2px solid transparent;
  padding: 0 0 var(--s2); font-size: 12px; font-weight: 500;
  color: var(--text-secondary); cursor: pointer; white-space: nowrap;
  transition: color .12s ease, border-color .12s ease;
}
nav.subtabs button.active { color: var(--text); font-weight: 600; border-bottom-color: var(--text); }
nav.subtabs button:hover:not(.active) { color: var(--text); }

/* ---------- page furniture ---------- */
.page-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--s4); margin-bottom: var(--s6);
}
.page-head-actions { display: flex; align-items: center; gap: var(--s2); flex-shrink: 0; position: relative; }
.page-title { font-size: 20px; font-weight: 600; letter-spacing: -0.02em; }
.page-sub { font-size: 12px; color: var(--text-secondary); margin-top: var(--s1); }

/* A section is a heading + a rule + content. No box, no shadow, no padding halo.
   ONE gap between major sections, everywhere in the product: 48px. A divided section
   splits the same 48 into 24 above its rule and 24 below, so a rule never buys itself
   extra air — the rhythm down the page is identical whether a section carries one or not. */
.section { margin-top: var(--s12); }
.section:first-child { margin-top: 0; }
/* Section heads lean on whitespace and a heavier label rather than a rule. */
.section-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s3); padding-bottom: var(--s3);
}
/* 15px/700 against a 13px/500 row: a step you can see at a glance, so the eye knows
   where a group starts without reading. */
.section-head h2 { color: var(--text); font-size: 15px; font-weight: 700; letter-spacing: -0.012em; }
/* The rule SEPARATES sections; it does not underline a heading. Equal air above and
   below it, and then the heading sits tight to the rows it owns. */
.section.divided { border-top: var(--hairline); margin-top: var(--s6); padding-top: var(--s6); }
.section-head .section-meta { font-size: 11px; color: var(--text-secondary); }
/* A group's explanation belongs under its name, not welded to its total by a
   separator that reads as a minus sign in front of a currency figure. */
/* 8px row padding + 24px action column + 12px gap: the offset every row in the
   product reserves on its right, so a header total can sit on the same rail. */
.section-head.row-aligned > :last-child { margin-right: 44px; }
.section-head .section-sub { font-size: 12px; color: var(--text-muted); margin-top: 2px; font-weight: 400; }
/* A group total that matches its heading reads as a summary; one that matches the row
   beneath it reads as the same number printed twice. */
.section-head .section-total { font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--text); letter-spacing: -0.012em; }
.section-head .section-meta b { color: var(--text); font-weight: 600; }
.section-sub-head {
  padding-bottom: var(--s2); border-bottom: var(--hairline);
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
}

.cols { display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); gap: 64px; align-items: start; }
.dash-cols > .section { margin-top: 0; }
.dash-cols .section .section { margin-top: var(--s12); }
.cols-even { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 32px; align-items: start; }

/* ---------- metric lockup: a number with its label beneath it ---------- */
.metric-lockup .metric-value {
  font-family: var(--font-hero);
  font-size: 40px; font-weight: 600; letter-spacing: -0.04em; line-height: 1;
}
.metric-lockup .metric-label { margin-top: var(--s2); font-size: 12px; }
.metric-lockup .metric-detail { font-size: 12px; color: var(--text-secondary); margin-top: var(--s2); }
/* A problem with the headline, stated on the headline's own label line. It is the
   one item in that dot-separated list that is not neutral, so it carries the only
   colour there — weight would fight the number above it, and a badge would announce
   itself as a component rather than reading as part of the sentence. */
.metric-flag { color: var(--negative); cursor: help; }

/* The single most important number in the product earns its hierarchy from scale,
   not from a black slab behind it. A hairline below it separates the headline from
   the caption tier — that is the entire container. */
.hero-panel {
  padding: var(--s1) 0 0;
  /* The same 48px that separates every other major block on every other page. It
     collapses against the following section's own 48, so the hero closes at exactly
     one section gap rather than at 40 here and 64 on Accounts. */
  margin-bottom: var(--s12);
}
.hero-panel .metric-value { font-size: 56px; letter-spacing: -0.045em; }
.hero-panel .metric-value.neg { color: var(--negative); }
.hero-panel .metric-label { color: var(--text-secondary); margin-top: var(--s3); font-size: 13px; font-weight: 500; }
.hero-panel .metric-detail { color: var(--text-secondary); font-size: 13px; }
/* The balance and the bar that explains it are one unit, held together by a single
   strict 24px. Alerts used to live in this gap; they live behind the bell now, and
   nothing is allowed back in. */
.hero-panel .waterfall, .hero-panel .pace { margin-top: var(--s6); }

/* KPIs beneath the hero: deliberately a caption tier, not a headline tier, and always
   the same 32px below whatever part of the hero it captions — the dashboard used to set
   that inline at 32 while Accounts set it at 40 on .kpi-spread. */
.kpi-row { display: flex; gap: 40px; flex-wrap: wrap; margin-top: var(--s8); }
.kpi .kpi-value { font-family: var(--font-mono); font-size: 16px; font-weight: 600; letter-spacing: -0.02em; }
.kpi .kpi-label { font-size: 11px; color: var(--text-secondary); margin-top: var(--s1); }

/* A headline with two thirds of the page empty beside it isn't restraint, it's an
   unfinished composition. The hero claims the full measure: the number and its
   caption tier on the left, the two terms that number is made of on the right. */
.hero-split {
  display: grid;
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
  gap: 56px;
  align-items: start;
}
/* Even distribution across the left column, so the caption tier ends where the
   headline above it ends instead of trailing off mid-measure. grid-auto-flow rather
   than a fixed count: the row stays evenly spread whatever it is asked to carry. */
.kpi-spread {
  display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr);
  gap: var(--s4);
}
/* Budget's hero leans further left than Accounts': the bar under the number needs
   length to be readable, and three stacked figures need far less than a chart does. */
.hero-split.split-55 { grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); }
/* Three figures as a ledger rather than a row: stacked, they align on one right rail
   and read top-to-bottom in the order you'd ask the questions. */
.kpi-stack { display: grid; gap: var(--s1); padding-top: var(--s2); }
.kpi-line {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s4); padding: var(--s2) 0; border-bottom: var(--hairline);
}
.kpi-line:last-child { border-bottom: none; }
.kpi-line-label { font-size: 12px; color: var(--text-secondary); }
.kpi-line-value {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size: 15px; font-weight: 600; letter-spacing: -0.02em; color: var(--text);
}

/* ---------- hero composition: the two terms behind a net-worth figure ---------- */
.comp-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s3); margin-bottom: var(--s2); }
.comp-head-owed { margin-top: var(--s6); }
.comp-title { font-size: 11px; font-weight: 500; color: var(--text-secondary); }
.comp-total { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 13px; font-weight: 600; letter-spacing: -0.012em; color: var(--text); }
/* Both bars are drawn against one shared scale, so each needs a visible remainder —
   without it a short bar reads as a small total rather than as a small share. */
.waterfall.comp-track { background: var(--bg-inset); }
.comp-legend { display: grid; gap: var(--s2); margin-top: var(--s3); }
.comp-item { display: grid; grid-template-columns: 8px minmax(0, 1fr) auto 34px; align-items: center; gap: var(--s2); font-size: 11px; color: var(--text-secondary); }
.comp-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.comp-value { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 11px; font-weight: 600; color: var(--text); }
.comp-share { text-align: right; color: var(--text-muted); font-variant-numeric: tabular-nums; }
/* Debt in brick would read as an emergency, and almost every household carries some.
   A hatch separates it from the asset tiers by TEXTURE, which costs the palette nothing. */
.comp-debt { background: repeating-linear-gradient(-45deg, rgba(20, 22, 28, .55) 0 3px, rgba(20, 22, 28, .3) 3px 6px); }
.comp-foot { font-size: 11px; color: var(--text-muted); margin: var(--s3) 0 0; line-height: 1.5; }
.comp-foot b { font-family: var(--font-mono); font-weight: 600; color: var(--text-secondary); }

/* ---------- hint: a caveat that waits to be asked ----------
   A paragraph of explanation parked between the summary and the detail interrupts
   the one moment the eye is actually travelling. Behind a dot, the same sentence
   costs nothing until someone wants it. */
.hint-wrap { position: relative; display: inline-flex; align-items: center; gap: var(--s1); }
.hint-dot {
  width: 14px; height: 14px; padding: 0; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  color: var(--text-muted); font-size: 9px; font-weight: 600; line-height: 1;
  cursor: help; transition: color .12s ease, border-color .12s ease;
}
.hint-wrap:hover .hint-dot, .hint-dot:focus-visible { color: var(--text); border-color: var(--text-secondary); }
.hint-bubble {
  position: absolute; left: 0; top: calc(100% + var(--s2)); z-index: 20;
  width: 264px; padding: var(--s3);
  background: var(--text); color: var(--bg); border-radius: var(--radius-sm);
  font-size: 11px; font-weight: 400; line-height: 1.5; letter-spacing: 0;
  box-shadow: var(--shadow-overlay); pointer-events: none;
  opacity: 0; visibility: hidden; transform: translateY(-2px);
  transition: opacity .12s ease, transform .12s ease, visibility .12s;
}
.hint-wrap:hover .hint-bubble, .hint-dot:focus-visible ~ .hint-bubble {
  opacity: 1; visibility: visible; transform: none;
}

.metric-value.neg { color: var(--negative); }
.metric-value.pos { color: var(--income); }

.stat-row { display: flex; gap: 40px; flex-wrap: wrap; }
.stat .stat-value { font-size: 16px; font-weight: 600; letter-spacing: -0.02em; }
.stat .stat-label { margin-top: var(--s1); }
.stat .stat-value.neg { color: var(--negative); }
/* These are the first numbers anyone looks at on opening a view, and a low-contrast
   grey band formatted them like a spreadsheet status line. The fill is gone: they sit
   on the page at full contrast, with a hairline below to close the strip and real
   vertical air so they do not fuse with whatever the view puts underneath. */
.summary-bar {
  display: flex; gap: var(--s2) var(--s10); flex-wrap: wrap; align-items: baseline;
  background: none; border: none; border-bottom: var(--hairline);
  border-radius: 0; padding: 0 0 var(--s4); margin-bottom: var(--s6);
}
.summary-bar .stat-value { font-size: 20px; letter-spacing: -0.025em; }
/* Not every summary slot holds a figure — Merchants puts two NAMES in this strip, and
   mono at 20px is a typeface for digits being asked to set a proper noun. */
.summary-bar .stat-value.is-text { font-family: var(--font-sans); font-size: 15px; letter-spacing: -0.011em; }
.summary-bar .stat-label { margin-top: var(--s1); }
.summary-bar .spacer { margin-left: auto; text-align: right; }
/* Counts are not money. Giving a row tally the same 20px mono as a total made the
   strip read as four headlines, which is the same as none. */
.summary-meta { font-size: 11px; color: var(--text-muted); }

/* Transactions carries two totals, not four, and a one-word label stacked under a
   20px figure read as two separate objects rather than one amount. Inline, the pair
   behaves the way it is actually read — "$15,646.23 out" — with the label small and
   muted so the figure keeps the weight.
   A variant rather than a change to .stat: Calendar, Trends and Merchants put three
   and four slots in this same strip, where stacked labels are still the right call. */
.summary-inline { gap: var(--s2) var(--s8); align-items: center; }
/* Merchants mixes a count, two proper nouns and a total in one strip, so flex sized
   every slot differently and the block read as four things that happened to be near
   each other. Equal columns plant them. grid-auto-flow rather than a fixed count
   because two of the four are conditional on there being any merchants at all. */
.summary-grid { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: var(--s6); align-items: start; }
.summary-grid .stat { min-width: 0; }
.summary-grid .stat-value, .summary-grid .stat-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* baseline INSIDE the pair so the label sits on the figure's baseline rather than the
   middle of its cap height; centred BETWEEN pairs and the meta, per the block-level
   alignment this strip needs. */
.stat-pair { display: flex; align-items: baseline; gap: var(--s2); }
.stat-unit { font-size: 12px; color: var(--text-muted); }

/* ---------- dense ledger row ----------
   No row rules. A date heading, consistent left alignment and a right-aligned
   money column already group these; a line under every row is a third statement
   of the same fact and it is what makes a ledger look assembled. */
.ledger { }
/* The one uppercase in the product, and a deliberate exception to the house rule.
   A date heading and a merchant name are different KINDS of thing, and at 11px vs
   13px they were only different sizes — you had to read the content to know which
   you were looking at. Caps plus tracking plus a lighter weight settles it before
   the words are read, which is the entire job of a group header. Tracking is not
   optional here: caps at 11px without it set as a solid block. */
.ledger-day {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--s3);
  /* 16 above, 8 below. The heading belongs to the rows under it, so it sits closer to
     them than to the group before it — but 32px above was buying separation the caps,
     the tracking and the muted grey had already bought. On a ledger where most days
     hold one transaction, that padding was 30% of the cost of showing the transaction. */
  padding: var(--s4) 44px var(--s2) var(--s2);
  font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: .06em;
  color: var(--text-muted);
  position: sticky; top: 0; background: var(--bg); z-index: 4;
}
.ledger-day:first-child { padding-top: var(--s2); }
.ledger-day .day-total { font-variant-numeric: tabular-nums; font-weight: 500; font-size: 11px; text-transform: none; letter-spacing: 0; }

/* Five tracks, not six: check, glyph, description, amount, actions. The account used
   to hold the fourth and put 148px of nothing between the merchant and its cost. */
.row {
  display: grid;
  /* select · glyph · text · cleared · amount · menu. The cleared column sits
     before the amount because that is where the eye already is when checking a
     figure against a statement, and because putting it after would push the
     amounts off the right rail every other list on the page aligns to. */
  grid-template-columns: 24px 24px minmax(0, 1fr) 18px 116px 24px;
  align-items: center; gap: var(--s3);
  min-height: var(--row-h);
  padding: var(--s1) var(--s2);
  cursor: pointer;
  transition: background-color .1s ease;
}
.row:hover { background: var(--bg-hover); }
.row.selected { background: var(--accent-wash); }
.row-primary { font-size: 13px; font-weight: 500; color: var(--text); display: flex; align-items: center; gap: var(--s2); min-width: 0; }
.row-primary > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row-secondary { font-size: 12px; color: var(--text-secondary); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row-account { font-size: 12px; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row-amount { font-size: 13px; font-weight: 600; text-align: right; font-variant-numeric: tabular-nums; }
/* Semantics: income reads green, an ordinary expense reads as body text, and a
   transfer is deliberately subdued so nobody reads a card payment as a $1,000 loss. */
/* A second, quieter fact under a balance: what the bank has confirmed of it.
   Regular weight and muted, so the working balance stays the number the column
   is scanned for and this one is only read when it is being looked for. */
.row-amount-sub {
  font-size: 11px; font-weight: 400; color: var(--text-muted);
  margin-top: 2px; white-space: nowrap;
}
.row-amount.income { color: var(--income); }
.row-amount.expense { color: var(--text); }
.row-amount.transfer { color: var(--transfer); font-weight: 500; }
.row.is-transfer .row-primary { color: var(--text-secondary); font-weight: 500; }

/* ---------- repeating: the standing orders behind the ledger ----------
   Its own grid rather than .row's. There is nothing to select and nothing to
   reconcile on a template — it is not an entry that happened, it is the rule
   that writes them — so .row's checkbox and cleared tracks would sit empty and
   hold the name 42px off the left rail every other list on the page aligns to.
   In their place, the one column a list of bills is actually read for: when the
   next one lands. */
.repeat-row {
  display: grid;
  grid-template-columns: 24px minmax(0, 1fr) 92px 116px 24px;
  align-items: center; gap: var(--s3);
  min-height: var(--row-h);
  padding: var(--s1) var(--s2);
  cursor: pointer;
  transition: background-color .1s ease;
}
.repeat-row:hover { background: var(--bg-hover); }
/* .row-more is invisible until its row is hovered, and that rule is scoped to
   .row — without this the menu button here would never appear at all. Focus is
   included so the keyboard can reach it; the mouse-only version leaves a
   focused button invisible. */
.repeat-row:hover .row-more, .repeat-row:focus-within .row-more { opacity: 1; }
/* The same 48px every other group of rows in the product puts between itself and
   the one before it. Without it a heading sits 8px under the last row of the
   previous group and reads as a label ON that row. */
.repeat-group { margin-top: var(--s12); }
.repeat-group:first-child { margin-top: 0; }
/* The group total and its "/mo" are one figure. Squeezed, the unit was breaking
   onto its own line under the number and reading as a stray fraction. */
.repeat-group .section-total { white-space: nowrap; }
/* "of £1,450 billed" under a group total, and "£15.50 yours" under a shared
   bill's amount. Right-aligned onto the same rail as the figure it qualifies —
   left-aligned it reads as a caption for the whole header rather than a second
   fact about that one number. */
.repeat-group .section-total .row-amount-sub { text-align: right; font-weight: 400; letter-spacing: 0; }
/* The recovered-share sentence sits under the summary bar, not in it: it is a
   second thought about the figure above, and inside the bar it competed with
   the verdict for the same rail. */
.repeat-recovered { margin: calc(-1 * var(--s3)) 0 var(--s8); }
.repeat-next {
  font-size: 12px; color: var(--text-secondary); text-align: right;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* Due inside the week. Not a warning colour — a bill landing on time is not a
   problem — just enough weight to separate "this week" from "some time next
   month" at a glance. */
.repeat-next.is-soon { color: var(--text); font-weight: 600; }
/* A repeating plan that does not cover itself is the one thing on this screen
   worth colouring. */
.summary-meta.is-negative { color: var(--negative); }
.summary-meta.is-negative b { color: var(--negative); }

/* ---------- account groups: titled sections, not bold text in a list ----------
   These are section headers and nothing else, so they are built the way every other
   section header in the product is: a heading, its note, and a total on the row rail,
   separated from what came before by the same 48px. The filled band that used to sit
   here was the only place in the product where a section header was a SURFACE, and it
   made Accounts read as a different application from Budget — which states the same
   relationship with whitespace and a weight step.
   The rows no longer indent either: with the band gone the indent was orphaning the
   account names 12px off the left rail every other list in the product sits on. */
.acct-group { margin-top: var(--s12); }
.acct-group:first-child { margin-top: 0; }

/* Identity -> movement -> balance. The middle column is what stops the row from being
   a name and a number with a canyon between them, and 30-day direction is the thing
   a balance on its own genuinely cannot tell you. */
/* The name column keeps a hard floor: it carries the only two things that make a row
   identifiable, and a track that can flex to zero will happily flex to zero. */
.acct-row { grid-template-columns: 10px 24px minmax(180px, 1fr) minmax(96px, 220px) 124px 116px 24px; }

/* ---------- drag to reorder ----------
   The grip is an affordance, not a control: the whole row is draggable, so making
   the grip the only handle would just add a small target to miss. It stays
   invisible until the row is hovered, because five accounts each wearing a set of
   dots is a texture, and this page is meant to be quiet. */
.drag-grip {
  width: 10px; height: 16px; align-self: center;
  opacity: 0; transition: opacity .1s ease;
  background-image: radial-gradient(var(--text-muted) 1px, transparent 1px);
  background-size: 4px 4px; background-position: 0 2px;
}
.acct-row:hover .drag-grip, .sortable-row:hover .drag-grip { opacity: .55; }
.acct-row.is-dragging, .sortable-row.is-dragging { opacity: .4; }
/* Inset rather than a border, so the line does not change the row's height and
   shove every row below it down by 2px as the pointer moves. */
.acct-row.drop-before, .sortable-row.drop-before { box-shadow: inset 0 2px 0 var(--accent); }
.acct-row.drop-after, .sortable-row.drop-after { box-shadow: inset 0 -2px 0 var(--accent); }
@media (prefers-reduced-motion: reduce) { .drag-grip { transition: none; } }
.row-meta { text-align: right; min-width: 0; }
.row-meta-value {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size: 12px; font-weight: 500; color: var(--text-secondary);
  white-space: nowrap;
}
.row-meta-arrow { font-family: var(--font-sans); margin-right: 2px; color: var(--text-muted); }
.row-meta-flat { font-family: var(--font-sans); color: var(--text-muted); }
.row-meta-label { font-size: 11px; color: var(--text-muted); margin-top: 2px; white-space: nowrap; }
/* preserveAspectRatio="none" stretches the 100-unit viewBox to whatever the column
   gets; non-scaling-stroke is what stops that stretch from smearing the line. */
.spark-cell { min-width: 0; opacity: .55; transition: opacity .12s ease; }
.row:hover .spark-cell { opacity: 1; }
.spark { display: block; width: 100%; height: 24px; overflow: visible; }
.row.is-scheduled { color: var(--text-secondary); }
.row.is-scheduled .row-amount { color: var(--text-secondary); }

.row-compact { grid-template-columns: 24px minmax(0, 1fr) 116px; }

/* ---- reconciliation toggle ----
   Quiet until it means something. An unticked circle is a hairline that reads as
   part of the row's furniture; a ticked one is the only green mark in the ledger,
   which is the whole signal. Full opacity on hover so the column announces itself
   the moment you go looking for it. */
.clear-cell { display: flex; align-items: center; justify-content: center; }
.clear-dot {
  width: 16px; height: 16px; border-radius: 50%;
  border: 1.5px solid var(--border-strong);
  background: transparent; padding: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 10px; line-height: 1; color: transparent;
  cursor: pointer; opacity: .45;
  transition: opacity .12s ease, border-color .12s ease, background-color .12s ease;
}
.row:hover .clear-dot { opacity: 1; }
.clear-dot:hover { border-color: var(--accent); }
.clear-dot.is-cleared {
  opacity: 1;
  border-color: var(--accent); background: var(--accent); color: #fff;
}
.clear-dot:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Not a control. A scheduled entry has no cleared state to toggle. */
.clear-dot.is-locked { border-style: dashed; border-color: var(--border); opacity: .5; cursor: default; }
.row:hover .clear-dot.is-locked { opacity: .5; }
.row-check { display: flex; align-items: center; opacity: 0; transition: opacity .12s ease; }
.row:hover .row-check, .ledger.selecting .row-check { opacity: 1; }
.row-check input { width: 16px; height: 16px; accent-color: var(--accent); cursor: pointer; margin: 0; }
.row-more { opacity: 0; transition: opacity .12s ease; }
.row:hover .row-more { opacity: 1; }

.type-glyph {
  width: 24px; height: 24px; border-radius: var(--radius);
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}

/* ---------- tags ----------
   A tag is metadata about the money, never the money. One shape, one weight, no
   fills: a hairline outline and muted text, so a row scans as merchant → amount
   with the status available to anyone who looks for it. Only the two labels that
   change a decision — over budget, money back — are allowed a colour. */
.tag {
  display: inline-flex; align-items: center; gap: var(--s1);
  font-size: 11px; font-weight: 500; letter-spacing: 0;
  padding: 1px var(--s2); border-radius: var(--radius-pill); white-space: nowrap;
  background: none; border: 1px solid var(--border); color: var(--text-muted);
}
.tag.neutral, .tag.transfer, .tag.scheduled, .tag.excluded, .tag.warn { }
.tag.income, .tag.ok { color: var(--income); }
.tag.over { color: var(--negative); }
.tag.paid-check { background: none; border: none; color: var(--accent); padding: 0; font-size: 13px; }

/* ---------- pacing bar: the marker is the product ---------- */
.pace {
  position: relative; height: 6px; background: var(--track);
  border-radius: var(--radius-bar); overflow: visible;
}
.pace-fill { height: 100%; border-radius: var(--radius-bar); background: var(--accent); transition: width .25s ease; }
.pace-fill.over { background: var(--negative); }
/* "watch this" sits between on-track and over: a muted neutral, never a black bar
   that out-shouts the brick it is supposed to rank below. */
.pace-fill.fast { background: var(--text-secondary); }
.pace-fill.flat { background: var(--border-strong); }
/* "where you should be today" — if the fill has passed this line, you're spending too fast */
.pace-marker {
  position: absolute; top: -2px; bottom: -2px; width: 2px;
  border-radius: var(--radius-bar); background: var(--text); opacity: .32;
}

/* The hero bar and the number above it are one lockup. 24px is the same strict gap
   the dashboard uses between its balance and its waterfall — the two pages state the
   headline the same way. */
/* .pace-today is absolutely positioned, so it contributes no height. Reserving its
   line here is what lets anything follow the bar without landing on the day count. */
.hero-pace { margin-top: var(--s6); padding-bottom: calc(var(--s2) + 16px); }
.pace-hero { height: 8px; }
.pace-hero .pace-marker { top: -3px; bottom: -3px; opacity: .5; }
/* The label rides the tick. It is positioned by the same percentage the marker is, so
   it cannot drift out of agreement with what it is labelling. */
.pace-today {
  position: absolute; top: calc(100% + var(--s2));
  font-size: 11px; color: var(--text-muted); white-space: nowrap;
}
/* A settled bill reads as complete at a glance; the figures column no longer has to
   carry a word among its numbers to say so. */
.pace-paid { background: var(--accent-wash); }
.pace-check {
  position: absolute; right: -14px; top: 50%; transform: translateY(-50%);
  font-size: 11px; line-height: 1; color: var(--accent);
}

.budget-row {
  display: grid; grid-template-columns: minmax(132px, 232px) minmax(80px, 1fr) 188px 132px 24px;
  align-items: center; gap: var(--s3);
  padding: var(--s3) var(--s2);
  cursor: pointer; transition: background-color .1s ease;
}
/* The Budget screen's own row. The name track is fixed rather than flexible: the bar
   is data and earns the slack, but a name track that absorbed it would just move the
   canyon from the right of the bar to the left of it. 220px holds every category name
   in the product without the flags ever pushing at it.
   (The base .budget-row above still serves Goals on the Accounts page, which has five
   tracks and no flag lane — hence a separate class rather than an edit in place.) */
.cat-row { grid-template-columns: minmax(150px, 180px) 64px minmax(120px, 1fr) 176px 116px 24px; }
/* Right-aligned so the flags stack flush against the bar they qualify: a lane of their
   own to scan down, with no floating gap on either side of it. */
.budget-flags { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; min-width: 0; }
/* Quiet by construction: a hairline pill per row was competing with the category name
   it was meant to annotate. */
.budget-flag {
  font-size: 11px; font-weight: 500; color: var(--text-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;
}
.budget-row:hover { background: var(--bg-hover); }

/* Goals and People are reorderable, so their rows carry a grip. The extra track goes
   on the SECTION rather than on the sortable rows alone: the Unallocated summary row
   sits in the same list and is not draggable, and giving it no grip column would
   indent every goal 10px past the total that is supposed to line up under them. */
.section-sortable .budget-row {
  grid-template-columns: 10px minmax(132px, 232px) minmax(80px, 1fr) 188px 132px 24px;
}

/* ---- move money ----
   A source list, not a dropdown. Which category to raid is a comparison between
   three or four numbers, and a <select> hides every option but one at the exact
   moment the user is trying to weigh them against each other. */
.move-sources { display: flex; flex-direction: column; gap: 2px; }
.move-source {
  display: grid; grid-template-columns: 16px 20px minmax(0, 1fr) auto;
  align-items: center; gap: var(--s2);
  padding: var(--s2); border-radius: var(--radius);
  cursor: pointer; transition: background-color .1s ease;
}
.move-source:hover { background: var(--bg-hover); }
.move-source-name { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.move-source-left { font-size: 12px; color: var(--text-secondary); font-variant-numeric: tabular-nums; }
.move-source:has(input:checked) { background: var(--accent-wash); }

.move-log-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center; gap: var(--s3);
  padding: var(--s2) 0; font-size: 12px;
  border-bottom: 1px solid var(--border);
}
.move-log-row b { font-variant-numeric: tabular-nums; }
/* One line per transaction: enough to recognise that a purchase landed, which is all
   this list is for. The ledger's second line is for reconciling, and reconciling
   happens on the Transactions tab. */
.dash-recent-row {
  /* Three tracks, and the money is the last one — which is what puts it on the same
     vertical line as the upcoming list above, since both lists end their grid at the
     same content edge. The old 4th track was a phantom that existed only to mirror an
     action rail that no longer sits outboard of the money. */
  display: grid; grid-template-columns: 24px minmax(0, 1fr) 112px;
  gap: var(--s3); align-items: center;
  /* Same floor as .row and .upcoming-row. A transaction row is a transaction row
     whichever page it is printed on, and without this one the dashboard's list ran
     4px tighter than the identical list on Transactions. */
  min-height: var(--row-h);
  padding: var(--s1) var(--s2); cursor: pointer;
  transition: background-color .1s ease;
}
.dash-recent-row:hover { background: var(--bg-hover); }
.dash-recent-name { display: flex; align-items: baseline; gap: var(--s2); min-width: 0; font-size: 13px; }
.dash-recent-name > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dash-recent-cat { font-size: 11px; color: var(--text-muted); white-space: nowrap; flex-shrink: 0; }

/* The dashboard's pacing rows live in a ~400px column, so they stack rather than
   using the Budget screen's five tracks. Same bar, same status wording, less width. */
.dash-pace-row { padding: var(--s2) 0; cursor: pointer; }
.dash-pace-row:first-child { padding-top: 0; }
.dash-pace-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s3); margin-bottom: var(--s2);
}
.dash-pace-name { font-size: 13px; font-weight: 500; display: flex; align-items: center; gap: var(--s2); min-width: 0; }
.dash-pace-name > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Bar and status share a line: the status is no taller than the bar, so giving it a
   line of its own bought nothing and cost 22px per category. */
.dash-pace-track { display: grid; grid-template-columns: minmax(0, 1fr) 112px; gap: var(--s3); align-items: center; }
.dash-pace-status { text-align: right; font-size: 11px; white-space: nowrap; }

/* the goals row has to survive the narrower measure this page uses */
.budget-name { font-size: 13px; font-weight: 500; display: flex; align-items: center; gap: var(--s2); min-width: 0; }
/* A goal's name and where the money actually sits, stacked in the one name track.
   The second line is a footnote to the first, not a field: it takes the muted weight
   the row's other secondary text already uses, and both lines truncate rather than
   push the bar, because an account name is user-typed and can be any length. */
.goal-name-stack { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.goal-name-text, .goal-where { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.goal-where { font-size: 11px; font-weight: 400; color: var(--text-muted); }
.budget-figs { font-size: 12px; text-align: right; font-variant-numeric: tabular-nums; color: var(--text-secondary); }
.budget-figs b { color: var(--text); font-weight: 600; }

/* An empty state is a footnote to the list above it, never a peer section. Same
   tracks so the columns still line up, two steps down in weight so the eye files it
   as leftovers rather than as more of the same. */
.section-quiet .section-head h2 { font-size: 12px; font-weight: 600; color: var(--text-secondary); }
.section-quiet .budget-name { color: var(--text-secondary); font-weight: 400; }
.section-quiet .budget-row:hover .link-btn { color: var(--text); }

/* ---------- safe-to-spend waterfall ---------- */
.waterfall { display: flex; height: 8px; background: transparent; border-radius: var(--radius-bar); overflow: hidden; }
.waterfall-seg { height: 100%; position: relative; }
.waterfall-legend { display: flex; flex-wrap: wrap; gap: var(--s2) var(--s6); margin-top: var(--s3); }
.waterfall-legend-item { display: flex; align-items: center; gap: var(--s2); font-size: 11px; color: var(--text-secondary); }
.waterfall-legend-item b { color: var(--text); font-weight: 600; }
.swatch { width: 8px; height: 8px; border-radius: var(--radius-bar); flex-shrink: 0; }

.stack-line {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--s4);
  padding: var(--s2) 0; font-size: 12px;
}
.stack-line .stack-label { color: var(--text); }
.stack-line .stack-detail { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
.stack-line .stack-value { font-variant-numeric: tabular-nums; font-weight: 600; flex-shrink: 0; }
.stack-line.total { border-top: 1px solid var(--border-strong); padding-top: var(--s3); margin-top: var(--s1); }

/* ---------- upcoming ---------- */
/* The amount is the LAST track, so it ends on the column's own right edge. It used to
   sit inboard of a 96px action rail that only one row in four ever filled, which left
   every figure in both dashboard lists floating ~110px short of the column it lived in.
   The action now sits between the description and the money, where it is still always
   visible and costs the composition nothing. */
.upcoming-row {
  display: grid; grid-template-columns: 72px 24px minmax(0, 1fr) 96px 112px;
  align-items: center; gap: var(--s3);
  padding: var(--s1) var(--s2); min-height: var(--row-h);
  font-size: 12px;
}
.upcoming-row:hover { background: var(--bg-hover); }
.upcoming-when { font-size: 11px; color: var(--text-secondary); font-variant-numeric: tabular-nums; }
.upcoming-when.imminent { color: var(--warn); font-weight: 600; }
.upcoming-amount { text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; }
.upcoming-amount.income { color: var(--income); }
.upcoming-tail { display: flex; gap: var(--s2); align-items: center; justify-content: flex-end; }

/* ---------- notifications ----------
   An alert is an offer, not an interruption. It waits behind a bell in the page
   header, where it can be read on the user's schedule and can never push the
   number it is commenting on down the page. */
.notif { display: flex; }
.notif-bell {
  width: 32px; height: 32px; padding: 0; position: relative;
  border: none; background: none; border-radius: var(--radius);
  color: var(--text-muted); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: background-color .12s ease, color .12s ease;
}
.notif-bell:hover { background: var(--bg-hover); color: var(--text); }
.notif-bell.has-unread { color: var(--text-secondary); }
/* A dot, never a count. "There is something here" is an invitation; "4" is a debt.
   The ring keeps it legible when the bell sits on its hover wash. */
.notif-dot {
  position: absolute; top: var(--s1); right: var(--s1);
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--accent); box-shadow: 0 0 0 2px var(--bg);
}
.notif-panel {
  position: absolute; top: calc(100% + var(--s2)); right: 0;
  width: 344px; max-width: calc(100vw - var(--s8));
  background: var(--bg); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-overlay);
  padding: var(--s1); z-index: 60;
}
/* Context, message, action — one grid, so every alert scans identically. */
.notif-item {
  display: grid; grid-template-columns: 24px minmax(0, 1fr) 16px;
  gap: var(--s3); align-items: start;
  padding: var(--s3); border-radius: var(--radius);
}
.notif-item:hover { background: var(--bg-hover); }
.notif-glyph { background: var(--bg-inset); color: var(--text-secondary); margin-top: 1px; }
.notif-text { font-size: 13px; line-height: 1.45; color: var(--text); }
/* Only the variable data is bolded: the figure that moved is what the eye needs. */
.notif-text b { font-weight: 600; }
.notif-action { margin-top: var(--s1); padding: 0; }
/* Dismissal is available but never on display — it appears on the row you're on. */
.notif-dismiss {
  background: none; border: none; color: var(--text-muted); cursor: pointer;
  font-size: 15px; line-height: 1; padding: 0; opacity: 0;
  transition: opacity .12s ease;
}
.notif-item:hover .notif-dismiss, .notif-dismiss:focus-visible { opacity: 1; }
.notif-dismiss:hover { color: var(--text); }
.notif-empty {
  margin: 0; padding: var(--s8) var(--s4);
  text-align: center; font-size: 13px; color: var(--text-muted);
}

/* ---------- controls ---------- */
.btn {
  height: 32px; padding: 0 var(--s3); border-radius: var(--radius);
  border: 1px solid var(--border-strong); background: var(--bg); color: var(--text);
  font-size: 12px; font-weight: 500; cursor: pointer;
  transition: background-color .12s ease, border-color .12s ease;
}
.btn:hover { background: var(--bg-hover); }
.btn.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
.btn.primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.btn.danger { color: var(--negative); border-color: var(--border-strong); }
.btn.danger:hover { background: var(--negative-wash); border-color: var(--border-strong); }
.btn.block { width: 100%; }
.btn.sm { height: 28px; padding: 0 var(--s2); font-size: 12px; }

.ghost {
  background: none; border: none; color: var(--text-secondary);
  font-size: 12px; font-weight: 500; padding: var(--s1) var(--s2); border-radius: var(--radius);
  cursor: pointer; transition: background-color .12s ease, color .12s ease;
}
.ghost:hover { background: var(--bg-hover); color: var(--text); }
.ghost.strong { color: var(--accent); font-weight: 600; }
/* A ghost in a page header shares a row with 32px steppers and buttons, so it takes
   their height there. Everywhere else it stays a text button sized by its own type. */
.page-head .ghost, .period-nav .ghost { height: 32px; display: inline-flex; align-items: center; }

.link-btn {
  background: none; border: none; color: var(--text-secondary); cursor: pointer;
  font-size: 12px; padding: 2px var(--s1); border-radius: var(--radius-sm); text-decoration: underline;
  text-underline-offset: 2px; text-decoration-color: var(--border-strong);
}
.link-btn:hover { color: var(--text); }

.period-nav { display: flex; align-items: center; gap: 2px; }
.period-nav .period-label { font-size: 13px; font-weight: 600; padding: 0 var(--s2); white-space: nowrap; }
.step-btn {
  width: 32px; height: 32px; border: 1px solid var(--border); background: var(--bg);
  border-radius: var(--radius); color: var(--text-secondary); cursor: pointer;
  display: flex; align-items: center; justify-content: center; font-size: 13px; line-height: 1;
}
.step-btn:hover { background: var(--bg-hover); color: var(--text); }

/* 32px, the same as .btn and every input — a chip shares a toolbar with both, and
   at 28px it sat 4px short of everything beside it. The 28px tier is .btn.sm and the
   period stepper, which never appear in that row. */
.chip {
  height: 32px; padding: 0 var(--s3); border-radius: var(--radius); border: 1px solid var(--border-strong);
  background: var(--bg); color: var(--text-secondary); font-size: 11px; font-weight: 500;
  cursor: pointer; transition: all .12s ease;
}
.chip:hover { color: var(--text); background: var(--bg-hover); }
.chip.active { background: var(--bg-inset); color: var(--text); border-color: var(--border-strong); font-weight: 600; }
.chip-row { display: flex; gap: var(--s2); flex-wrap: wrap; align-items: center; }

.field { display: flex; flex-direction: column; gap: 4px; }
.field label { color: var(--text-secondary); }
.field input, .field select, .field textarea {
  height: 32px; border: 1px solid var(--border-strong); border-radius: var(--radius);
  padding: 0 var(--s2); font-size: 12px; background: var(--bg); color: var(--text);
  font-family: inherit; width: 100%;
}
.field input:focus, .field select:focus {
  outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-wash);
}
.field-row { display: flex; gap: var(--s3); flex-wrap: wrap; }
.field-row .field { flex: 1 1 150px; min-width: 0; }
.check-row { display: flex; align-items: center; gap: var(--s2); font-size: 12px; color: var(--text-secondary); }
.check-row input[type="checkbox"] { width: auto; height: auto; accent-color: var(--accent); margin: 0; }

.toolbar { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; padding-bottom: var(--s3); }
.toolbar input[type="text"], .toolbar input[type="number"], .toolbar input[type="date"], .toolbar select {
  height: 32px; border: 1px solid var(--border-strong); border-radius: var(--radius);
  padding: 0 var(--s2); font-size: 12px; background: var(--bg); color: var(--text); font-family: inherit;
}
.toolbar input[type="text"] { flex: 1; min-width: 180px; }
.toolbar input:focus, .toolbar select:focus { outline: none; border-color: var(--accent); }

/* ---------- tables ---------- */
table { width: 100%; border-collapse: collapse; font-size: 12px; }
/* The header rule is the only one a table needs: columns are established by
   alignment, and a line under every row turns data into graph paper. */
th { text-align: left; padding: var(--s2) var(--s3); border-bottom: var(--hairline); white-space: nowrap; }
td { padding: var(--s3); vertical-align: middle; }
/* The outer cells carry no horizontal padding, so a table's first column starts on the
   same left edge as the page title and the summary strip above it, and its last column
   ends on the same right edge. Inner padding still separates the columns from each other. */
th:first-child, td:first-child { padding-left: 0; }
th:last-child, td:last-child { padding-right: 0; }
tbody tr:hover { background: var(--bg-hover); }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
/* A column of exact figures answers "how much" precisely and "which is biggest" not
   at all. A bar beside each total restores the second reading without displacing the
   first — neutral, because a big number here is spending, not an achievement.
   The scale is LINEAR and stays that way. This data is heavily skewed — one merchant
   at $8,700 and the rest under $1,500 — so most bars are slivers. That is the finding,
   not a defect: a sqrt scale would even them out by making length stop meaning money.
   The track is what keeps a sliver locatable, and min-width keeps any real spend from
   rounding away to nothing. */
.merch-total { display: flex; align-items: center; justify-content: flex-end; gap: var(--s2); }
.merch-bar { width: 56px; height: 6px; background: var(--track); border-radius: var(--radius-bar); overflow: hidden; flex-shrink: 0; cursor: help; }
.merch-bar-fill { display: block; height: 100%; min-width: 1px; background: var(--bar-fill); border-radius: var(--radius-bar); }
.empty-row td, .empty { color: var(--text-muted); text-align: center; padding: var(--s6); font-size: 12px; }
.empty-row:hover { background: transparent; }
.sortable { cursor: pointer; user-select: none; }
.sortable:hover { color: var(--text-secondary); }
.sortable .arrow { margin-left: var(--s1); font-size: 9px; }
.sortable.sorted .arrow { color: var(--accent); }

.note { font-size: 12px; color: var(--text-muted); margin: var(--s2) 0 0; line-height: 1.55; }
.error-note { font-size: 12px; color: var(--negative); margin: var(--s2) 0 0; }

/* ---------- overlays: the only place shadows are allowed ---------- */
.row-menu-wrap { position: relative; }
.row-menu-btn {
  background: none; border: none; color: var(--text-muted); font-size: 13px;
  letter-spacing: .5px; cursor: pointer; padding: 2px var(--s1); border-radius: var(--radius-sm); line-height: 1;
}
.row-menu-btn:hover { background: var(--bg-inset); color: var(--text); }
.row-menu {
  position: absolute; right: 0; top: calc(100% + var(--s1)); background: var(--bg);
  border: none; border-radius: var(--radius-lg);
  box-shadow: var(--shadow-overlay); padding: var(--s1); min-width: 176px; z-index: 60;
}
.row-menu button {
  display: block; width: 100%; text-align: left; background: none; border: none;
  padding: var(--s2) var(--s3); font-size: 13px; color: var(--text); border-radius: var(--radius); cursor: pointer;
}
.row-menu button:hover { background: var(--bg-hover); }
.row-menu button.danger-item { color: var(--negative); }

.scrim { position: fixed; inset: 0; background: rgba(28, 27, 25, .30); z-index: 80; }
.drawer {
  position: fixed; top: 0; right: 0; height: 100vh; width: 432px; max-width: 94vw;
  background: var(--bg); border-left: 1px solid var(--border-strong);
  box-shadow: var(--shadow-overlay); z-index: 81; display: flex; flex-direction: column;
  animation: slide-in .16s ease;
}
@keyframes slide-in { from { transform: translateX(16px); opacity: .7; } to { transform: none; opacity: 1; } }
.drawer-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
  padding: var(--s5) var(--s6) var(--s4); border-bottom: var(--hairline);
}
.drawer-head h3 { font-size: 16px; font-weight: 600; }
.drawer-head .drawer-sub { font-size: 12px; color: var(--text-secondary); margin-top: var(--s1); }
.drawer-close {
  background: none; border: none; color: var(--text-muted); font-size: 16px; line-height: 1;
  cursor: pointer; padding: var(--s1); border-radius: var(--radius-sm); flex-shrink: 0;
}
.drawer-close:hover { background: var(--bg-hover); color: var(--text); }
.drawer-body { padding: var(--s4) var(--s6) var(--s8); overflow-y: auto; flex: 1; }
.drawer-section { margin-top: var(--s5); }
.drawer-section:first-child { margin-top: 0; }

.modal-wrap { position: fixed; inset: 0; z-index: 81; display: flex; align-items: center; justify-content: center; padding: var(--s6); }
.modal {
  background: var(--bg); border: none; border-radius: var(--radius-lg);
  box-shadow: var(--shadow-overlay); width: 440px; max-width: 100%; max-height: 86vh; overflow-y: auto;
}
.modal-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s3); padding: var(--s5) var(--s6) var(--s4); border-bottom: var(--hairline); }
.modal-body { padding: var(--s4) var(--s6) var(--s6); }

.bulk-bar {
  display: flex; align-items: center; gap: var(--s3); background: var(--bg-sunken);
  border: none; border-radius: var(--radius);
  padding: var(--s2) var(--s3); margin-bottom: var(--s3);
}
.bulk-bar .count { font-size: 12px; font-weight: 600; color: var(--text); }
.bulk-bar select { height: 28px; font-size: 12px; border: 1px solid var(--border-strong); border-radius: var(--radius); padding: 0 var(--s2); font-family: inherit; }

/* ---------- charts ---------- */
.chart-wrap { position: relative; }
.chart-tooltip {
  position: absolute; pointer-events: none; background: var(--text); color: var(--bg);
  font-size: 11px; padding: var(--s1) var(--s2); border-radius: var(--radius-sm); white-space: nowrap;
  font-variant-numeric: tabular-nums; z-index: 10; opacity: 0; transition: opacity .1s;
}
.bar-track { height: 8px; background: var(--track); border-radius: var(--radius-bar); overflow: hidden; }
.bar-track .bar-fill { height: 100%; border-radius: var(--radius-bar); background: var(--bar-fill); }
/* The tick is 1px and nobody can hover a 1px target. The cell carries the tooltip so
   the explanation is wherever the pointer already is. */
.bar-cell { position: relative; cursor: help; }
.avg-marker { position: absolute; top: -2px; bottom: -2px; width: 1px; background: var(--text-secondary); }
.th-hint { color: var(--text-muted); font-weight: 400; cursor: help; }

/* ---------- money in and out: a plot area, not a row of divs ----------
   The bars were a bare flexbox with no axis to hang labels on, so a $2,020 bar and a
   $20,200 bar were the same picture. The 44px gutter is the axis; the gridlines span
   the plot and carry their own label into that gutter. */
.flow-plot {
  position: relative; height: 132px; margin-top: var(--s2);
  padding-left: 44px; border-bottom: 1px solid var(--border);
}
.flow-gridline {
  position: absolute; left: 44px; right: 0; height: 0;
  border-top: 1px solid var(--border); pointer-events: none;
}
/* The zero line is the axis itself, already drawn by the plot's own bottom border. */
.flow-gridline:first-of-type { border-top-color: transparent; }
.flow-gridline span {
  position: absolute; right: calc(100% + var(--s2)); top: -0.6em;
  font-size: 9.5px; color: var(--text-muted); font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.flow-bars { position: absolute; inset: 0 0 0 44px; display: flex; align-items: flex-end; gap: var(--s3); }
.flow-slot { flex: 1; min-width: 0; display: flex; align-items: flex-end; justify-content: center; gap: var(--s1); height: 100%; }
.flow-bar { width: 40%; max-width: 22px; border-radius: 2px 2px 0 0; }
.flow-labels { display: flex; gap: var(--s3); padding: var(--s2) 0 0 44px; }
.flow-labels span { flex: 1; min-width: 0; text-align: center; font-size: 11px; letter-spacing: 0; color: var(--text-muted); }

/* ---------- calendar: a cash-flow instrument, not a month grid ---------- */
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.cal-weekday { text-align: center; font-size: 11px; font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--text-muted); padding: var(--s2) 0; background: var(--bg); }
/* Fixed, not min-height. A grid whose rows buckle around one busy day is no longer a
   grid, and every cell is capped at two figures upstream to guarantee this holds.
   80px is a day number and that pair with the padding — no more, so the empty days
   are not 20px of nothing each. */
.cal-cell {
  background: var(--bg); height: 80px; padding: var(--s2);
  display: flex; flex-direction: column; cursor: pointer; position: relative;
}
/* A hover is a 3% wash, never an outline snapping on and off under the cursor. */
.cal-cell:hover { background: var(--bg-hover); }
.cal-cell.dim { opacity: .4; }
/* What has not happened yet is a different kind of fact from what has. The tint says
   so across the whole block of remaining days at once — the triangle says it per
   figure, and neither alone was enough to tell the two halves of the month apart. */
.cal-cell.future { background: var(--bg-future); }
.cal-cell.future:hover { background: var(--bg-hover); }
.cal-cell.selected { outline: 1px solid var(--accent); outline-offset: -1px; z-index: 2; }
.cal-day-num { font-size: 11px; color: var(--text-secondary); font-variant-numeric: tabular-nums; }
.cal-date-label { display: none; }
.cal-cell.today .cal-day-num { color: var(--bg); background: var(--text); border-radius: var(--radius-sm); padding: 0 var(--s1); font-weight: 600; width: fit-content; }
.cal-body { position: relative; display: flex; flex-direction: column; gap: var(--s1); flex: 1; min-height: 0; overflow: hidden; }
.cal-fig {
  font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums;
  font-family: var(--font-mono); color: var(--text);
  display: flex; align-items: center; gap: var(--s1); white-space: nowrap;
}
.cal-fig.is-future { color: var(--text-secondary); font-weight: 500; }
.cal-fig.is-income { color: var(--income); }
.cal-fig.is-transfer { color: var(--transfer); font-weight: 500; font-family: var(--font-sans); }

.cal-legend {
  display: flex; flex-wrap: wrap; gap: var(--s2) var(--s6);
  margin: var(--s3) 0 0; font-size: 11px; color: var(--text-muted);
}
.cal-legend-item { display: flex; align-items: center; gap: var(--s2); }

/* The day panel is the calendar's detail pane, not a section that happens to follow
   it. A rule closes the grid, the heading sticks while the list scrolls, and the
   accent rule on it is the same accent outlining the cell it belongs to. */
.cal-selected { padding-top: var(--s4); }
.cal-selected-head {
  position: sticky; top: 0; z-index: 4; background: var(--bg);
  /* 16px from .cal-selected + 8px here = the 24px every other divided section puts
     between its rule and its heading. The 8px lives on the head rather than the
     section so the sticky heading keeps a background pad when rows scroll under it. */
  padding: var(--s2) 0 var(--s3) var(--s3);
  border-left: 2px solid var(--accent);
}

/* ---------- icon picker ---------- */
.icon-picker-trigger {
  display: flex; align-items: center; gap: var(--s2); height: 32px; padding: 0 var(--s2);
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  background: var(--bg); cursor: pointer; font-size: 12px; position: relative;
}
.icon-picker-pop {
  position: absolute; top: calc(100% + var(--s1)); left: 0; background: var(--bg);
  border: none; border-radius: var(--radius-lg);
  box-shadow: var(--shadow-overlay); padding: var(--s2); width: 224px; z-index: 60;
}
.icon-picker-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--s1); }
.icon-picker-btn { background: var(--bg-sunken); border: 1px solid transparent; border-radius: var(--radius); padding: var(--s1); cursor: pointer; display: flex; align-items: center; justify-content: center; }
.icon-picker-btn:hover { background: var(--bg-inset); }
.icon-picker-btn.active { border-color: var(--accent); background: var(--accent-wash); }

/* ---------- keyboard ----------
   Command-driven workflows only work if the interface admits where the keyboard
   is. One ring, drawn outside the element so it never nudges the 4px grid. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
.row:focus-visible, .budget-row:focus-visible, .upcoming-row:focus-visible {
  outline-offset: -2px;
}

kbd {
  font-family: var(--font-mono);
  font-size: 11px; font-weight: 500; line-height: 1;
  padding: var(--s1) var(--s2); border-radius: var(--radius-sm);
  background: var(--bg-inset); color: var(--text-secondary);
  white-space: nowrap;
}
.shortcut-list { display: flex; flex-direction: column; gap: 2px; }
.shortcut-row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s6);
  padding: var(--s2) 0; font-size: 13px;
}
.shortcut-row .shortcut-keys { display: flex; gap: var(--s1); flex-shrink: 0; }
.sidebar-note kbd { background: none; padding: 0; color: var(--text-secondary); font-size: 11px; }
/* Links inside the note are part of the sentence, not controls beside it. */
.sidebar-note .link-btn { font-size: 11px; padding: 0; }
.sidebar-note > div + div { margin-top: var(--s1); }

::-webkit-scrollbar { width: 9px; height: 9px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 999px; }

/* ---------- mobile: different interaction model, not a squeezed desktop ---------- */
/* `nav.tabs button` is (0,1,2), so a bare .mobile-only class would lose to it —
   the nav-scoped rule is what actually hides the mobile Add button on desktop. */
.mobile-only { display: none; }
nav.tabs button.mobile-only { display: none; }
/* Ordered by what a row can least afford to lose: the shape goes first, then the
   trend figure, and the name and balance survive to the narrowest screen. */
/* ============================================================================
   Quick add — the fast capture surface behind N.
   ----------------------------------------------------------------------------
   Anchored near the top of the viewport rather than centred: it opens under the
   cursor's expectation, and a panel that stays open for a second entry must not
   move between them. The amount is the only thing in it typeset at hero size,
   because the amount is the only thing here that is a figure.
   ============================================================================ */
.quick-add {
  position: fixed; top: 12vh; left: 50%; transform: translateX(-50%);
  width: 420px; max-width: calc(100vw - var(--s6));
  background: var(--bg); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-overlay); z-index: 81;
  display: flex; flex-direction: column;
  animation: qa-in .14s ease;
}
@keyframes qa-in { from { transform: translate(-50%, -6px); opacity: .6; } to { transform: translate(-50%, 0); opacity: 1; } }

.qa-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
  padding: var(--s3) var(--s4) 0;
}
.qa-body { padding: var(--s3) var(--s5) var(--s4); }

/* The one figure on the panel, so it gets the mono face every other figure in the
   product gets. The $ is furniture, not data — it sits at label weight. */
.qa-amount-row { display: flex; align-items: baseline; gap: var(--s2); padding: var(--s2) 0 var(--s3); }
.qa-currency { font-family: var(--font-mono); font-size: 22px; color: var(--text-muted); }
.qa-amount {
  flex: 1; min-width: 0; border: none; background: none; padding: 0;
  font-family: var(--font-mono); font-size: 34px; font-weight: 600; color: var(--text);
  letter-spacing: -.02em;
}
.qa-amount:focus { outline: none; }
.qa-amount::placeholder { color: var(--border-strong); }
/* the spinner is 30px of chrome on a field being typed into with a number pad */
.qa-amount::-webkit-outer-spin-button, .qa-amount::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.qa-amount { -moz-appearance: textfield; }

.qa-merchant {
  width: 100%; height: 38px; padding: 0 var(--s3);
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  font-family: inherit; font-size: 14px; color: var(--text); background: var(--bg);
}
.qa-merchant:focus { outline: none; border-color: var(--accent); }

.qa-row { display: flex; gap: var(--s2); margin-top: var(--s2); }
.qa-row select {
  flex: 1; min-width: 0; height: 34px; padding: 0 var(--s2);
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  font-family: inherit; font-size: 13px; color: var(--text); background: var(--bg);
}

/* Reserved height, so the panel does not jump a line taller the first time the
   rules engine has something to say. */
.qa-note { min-height: 17px; margin-top: var(--s2); font-size: 12px; color: var(--text-muted); line-height: 1.4; }
.qa-note.is-auto { color: var(--accent); }
/* Same signal on the full form: a field the app filled in says so in the colour
   the product uses for its own doing, not in the muted grey of a hint. Muted, it
   read as instructions nobody had followed yet. */
.field-note.is-auto { color: var(--accent); }
.qa-remember { margin-top: var(--s2); }
/* .check-row sets display:flex, which outranks the UA's [hidden]{display:none} —
   without this the "always file as" offer sits on screen permanently. */
.qa-remember[hidden] { display: none; }

.qa-foot {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3) var(--s5); border-top: var(--hairline);
}
.qa-foot input[type="date"] {
  height: 30px; padding: 0 var(--s2); border: 1px solid var(--border); border-radius: var(--radius);
  font-family: inherit; font-size: 12px; color: var(--text-secondary); background: var(--bg);
}
.qa-foot .btn { margin-left: auto; }

.qa-receipt {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s2) var(--s5); border-top: var(--hairline);
  background: var(--accent-wash); color: var(--text-secondary);
  font-size: 12px; border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}
.qa-receipt .link-btn { margin-left: auto; }
.qa-receipt-hint { background: var(--bg-sunken); color: var(--text-muted); }
.qa-empty { text-align: center; padding: var(--s5) 0 var(--s3); }
.qa-empty .btn { margin-top: var(--s3); }

/* ============================================================================
   Rules sheet.
   ============================================================================ */
.modal.modal-wide { width: 560px; }
.rule-list { display: flex; flex-direction: column; gap: var(--s1); margin: var(--s3) 0; }
.rule-head, .rule-row {
  display: grid; grid-template-columns: minmax(0, 1.4fr) 96px 14px minmax(0, 1.1fr) 40px 24px;
  gap: var(--s2); align-items: center;
}
.rule-head {
  font-size: 10px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--text-muted); padding-bottom: var(--s1);
}
.rule-row input, .rule-row select {
  height: 30px; min-width: 0; padding: 0 var(--s2);
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  font-family: inherit; font-size: 12px; color: var(--text); background: var(--bg);
}
/* A rule filing into a deleted category does nothing at all, which looks exactly
   like a rule that was never written. It gets said in colour. */
.rule-row.is-broken select[data-rule-cat] { border-color: var(--negative); color: var(--negative); }
.rule-arrow { color: var(--text-muted); font-size: 12px; text-align: center; }
.rule-count { font-family: var(--font-mono); font-size: 11px; color: var(--text-muted); text-align: right; }

.rule-add {
  display: grid; grid-template-columns: minmax(0, 1.4fr) 96px 14px minmax(0, 1.1fr) auto;
  gap: var(--s2); align-items: center;
  margin-top: var(--s4); padding-top: var(--s4); border-top: var(--hairline);
}
.rule-add input, .rule-add select {
  height: 30px; min-width: 0; padding: 0 var(--s2);
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  font-family: inherit; font-size: 12px; color: var(--text); background: var(--bg);
}
.rule-foot {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
  margin-top: var(--s4); padding-top: var(--s3); border-top: var(--hairline);
  font-size: 12px; color: var(--text-secondary);
}
.rule-result { margin-top: var(--s2); font-size: 12px; color: var(--accent); }

@media (max-width: 1120px) {
  .acct-row { grid-template-columns: 10px 24px minmax(0, 1fr) 124px 116px 24px; }
  .acct-row .spark-cell { display: none; }
}
@media (max-width: 1000px) {
  /* Four equal columns of ~120px truncate all four values. Two rows of two hold the
     longest of them — a merchant name — intact. */
  .summary-grid { grid-auto-flow: row; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s4) var(--s6); }
}

/* Merchant, Visits, Total. Average and Last seen are analysis, and this table ran off
   the right edge well before the phone breakpoint even without them. */
@media (max-width: 940px) {
  .col-optional { display: none; }
}

@media (max-width: 860px) {
  .cols, .cols-even { grid-template-columns: 1fr; gap: 24px; }
  .row { grid-template-columns: 24px 24px minmax(0, 1fr) 104px 24px; }
  /* the compact row has three children, not five — without this it inherits the
     full row's tracks and the merchant name gets squeezed into a 24px column */
  .row.row-compact { grid-template-columns: 24px minmax(0, 1fr) 104px; }
  /* the due-date column goes before the amount does: "£15.99" is why the row is
     scanned, "in 3 days" is why it is scanned TODAY, and the frequency line
     under the name still carries the rhythm */
  .repeat-row { grid-template-columns: 24px minmax(0, 1fr) 104px 24px; }
  .repeat-row .repeat-next { display: none; }
  /* the trend column is the first thing to go on an account row — the name and the
     balance are the two things nobody can read the page without */
  .acct-row { grid-template-columns: 10px 24px minmax(0, 1fr) 104px 24px; }
  .acct-row .row-meta { display: none; }
  .hero-split, .hero-split.split-55 { grid-template-columns: 1fr; gap: var(--s8); }
  .row-account { display: none; }
  .budget-row, .section-sortable .budget-row { grid-template-columns: minmax(0, 1fr) 96px 24px; }
  .budget-row .budget-pace-cell { display: none; }
  /* HTML5 drag does not fire from touch and the grip only appears on hover, so at
     this width it is a 10px column of nothing. Move up / Move down in the row menu
     is the reorder path here, which is why it was built as well as the drag. */
  .section-sortable .drag-grip { display: none; }
  /* six tracks do not survive this measure; the name, the figures and the one status
     figure do, and the flags fall back to the row menu that can already toggle them */
  .cat-row { grid-template-columns: minmax(0, 1fr) 156px 112px 24px; }
  .cat-row .budget-flags { display: none; }
  .upcoming-row { grid-template-columns: 64px 24px minmax(0, 1fr) 0 88px; }
  .upcoming-row .upcoming-tail { display: none; }
  .metric-lockup .metric-value { font-size: 32px; }
}
/* The calendar becomes a timeline here rather than at the phone breakpoint. Seven
   columns of to-the-cent currency need roughly 110px each; between 720 and 860 they
   were getting ~82px and the last column was clipping its own numbers. A calendar
   that truncates a figure is worse than one that gives up the grid. */
@media (max-width: 860px) {
  .cal-grid { display: block; background: none; border: none; border-radius: 0; }
  .cal-weekday { display: none; }
  .cal-cell.is-empty, .cal-cell.dim { display: none; }
  .cal-cell {
    height: auto; min-height: 0; padding: var(--s3) var(--s2);
    border-radius: var(--radius);
  }
  .cal-legend { display: none; }
  .cal-cell:not(:last-child) { border-bottom: var(--hairline); }
  .cal-cell.selected { outline: none; background: var(--accent-wash); }
  .cal-body { flex-direction: row; align-items: center; flex-wrap: wrap; gap: var(--s1) var(--s3); }
  .cal-day-num { display: none; }
  .cal-cell.today .cal-day-num { display: none; }
  .cal-date-label {
    display: block; flex: 0 0 76px;
    font-size: 12px; color: var(--text-secondary); font-variant-numeric: tabular-nums;
  }
  .cal-cell.today .cal-date-label { color: var(--text); font-weight: 600; }
  .cal-fig { font-size: 12px; }
}

@media (max-width: 720px) {
  .mobile-only { display: block; }
  .app { flex-direction: column; }
  .main { border-right: none; }
  /* the desktop measure and its 56px of top air are a wasted first screen on a phone */
  .main-inner { padding: var(--s4) var(--s4) 104px; max-width: 100%; }
  .desktop-only { display: none; }
  /* stacked lockups with desktop gaps push everything below the fold */
  .stat-row, .kpi-row { gap: var(--s4) var(--s6); }
  .stat, .kpi { flex: 0 0 auto; min-width: 42%; }
  /* grid-auto-columns keeps three evenly across even at 320px; the flex rule above
     governs the other stat rows, not this one */
  .kpi-spread { gap: var(--s3); }
  .hint-bubble { width: min(264px, 74vw); }
  .stat .stat-value, .kpi .kpi-value { font-size: 16px; }
  .metric-lockup .metric-value { font-size: 36px; }
  .hero-panel { padding: 0; margin-bottom: var(--s6); }
  .hero-panel .metric-value { font-size: 40px; }
  /* the sidebar's active accent is a LEFT rule on desktop; in a bottom bar it has to
     become a top rule, or it hangs off the edge of the first tab */
  nav.tabs button { border-left: none !important; }
  nav.tabs button.active { border-left-color: transparent; }
  /* bottom tab bar with a permanent Add in the middle: logging a purchase on the go
     is the single most common mobile action, so it never hides in a menu */
  .sidebar {
    position: fixed; bottom: 0; top: auto; left: 0; right: 0;
    width: 100%; height: 56px; flex-direction: row; padding: 0;
    border-right: none; border-top: 1px solid var(--border-strong); z-index: 70;
    background: var(--bg);
  }
  .brand, .sidebar-note, .sidebar-spacer, .sidebar-action { display: none; }
  nav.tabs { flex-direction: row; width: 100%; padding: 0; gap: 0; }
  nav.tabs button {
    flex: 1; flex-direction: column; gap: 2px; height: 56px; justify-content: center;
    border-radius: 0; font-size: 10px; padding: 0;
  }
  nav.tabs button.active { background: none; }
  nav.tabs button.active::before { content: ""; position: absolute; top: 0; left: 20%; right: 20%; height: 2px; background: var(--accent); }
  nav.tabs button { position: relative; }
  nav.tabs button.nav-add, nav.tabs button.mobile-only { display: flex; flex: 0 0 62px; }
  nav.tabs button.nav-add .nav-add-mark {
    width: 32px; height: 32px; border-radius: 50%; background: var(--accent); color: #fff;
    display: flex; align-items: center; justify-content: center;
  }
  nav.tabs button.nav-add .nav-label { display: none; }
  /* dropdowns and edit panels become bottom sheets, dismissible downward */
  .drawer {
    top: auto; bottom: 0; height: auto; max-height: 86vh; width: 100%; max-width: 100%;
    border-left: none; border-top: 1px solid var(--border-strong);
    border-radius: var(--radius) var(--radius) 0 0; animation: sheet-up .18s ease;
  }
  @keyframes sheet-up { from { transform: translateY(20px); opacity: .8; } to { transform: none; opacity: 1; } }
  .drawer::before {
    content: ""; display: block; width: 32px; height: 4px; border-radius: var(--radius-bar);
    background: var(--border-strong); margin: var(--s2) auto 0;
  }
  .modal-wrap { align-items: flex-end; padding: 0; }
  .modal { width: 100%; border-radius: var(--radius) var(--radius) 0 0; max-height: 88vh; }
  /* A month grid on a phone is 42 boxes too small to read. The same instrument as a
     vertical timeline — only the days that actually had money move — is legible, and
     it is the order people think in anyway: down the month, not across a week. */


  .ledger-day { top: 0; }
  table { font-size: 12px; }
  th, td { padding: var(--s2); }
  /* Quick add becomes the same bottom sheet every other overlay does — it has to
     sit above the thumb and above the bottom tab bar, not under a keyboard. */
  .quick-add {
    top: auto; bottom: 0; left: 0; right: 0; transform: none;
    width: 100%; max-width: 100%;
    border-radius: var(--radius) var(--radius) 0 0;
    animation: sheet-up .18s ease;
  }
  .qa-receipt { border-radius: 0; }
  /* five columns of controls do not fit a phone; the rule reads down instead */
  .rule-head { display: none; }
  .rule-row, .rule-add {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 24px;
    row-gap: var(--s1); padding: var(--s2) 0; border-bottom: var(--hairline);
  }
  .rule-row .rule-arrow, .rule-add .rule-arrow { display: none; }
  .rule-row .rule-count { text-align: left; }
  .rule-add .btn { grid-column: 1 / -1; }
}

/* ============================================================================
   This month — review card and spending drill-down
   ============================================================================ */

/* The nudge that appears for the first few days of a month, pointing at the one
   that just ended. A line, not a panel: it is a signpost, and it disappears on
   the 6th. */
.review-nudge {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3) var(--s2); margin-bottom: var(--s5);
  border-left: 2px solid var(--accent);
  background: var(--accent-wash);
  font-size: 13px;
}

.review-changes { display: flex; flex-direction: column; }
.review-change {
  display: grid;
  grid-template-columns: 16px 24px minmax(0, 1fr) auto 132px;
  align-items: center; gap: var(--s3);
  padding: var(--s3) var(--s2);
  border-top: 1px solid var(--border);
  cursor: pointer; transition: background-color .1s ease;
}
.review-change:hover { background: var(--bg-hover); }
.review-change-dir { font-size: 13px; text-align: center; color: var(--text-muted); }
.review-change-name { font-size: 13px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.review-change-figs { text-align: right; font-family: var(--font-mono, inherit); font-variant-numeric: tabular-nums; font-size: 13px; }
/* What "usual" actually was, printed beside every change. A card that says
   "+$200 above usual" without saying what usual was is asking to be believed
   rather than checked. */
.review-change-usual { display: block; font-size: 11px; color: var(--text-muted); margin-top: 2px; }
.review-change-delta {
  text-align: right; font-size: 12px; font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}
/* Colour only on an increase. Brick means over-budget in this system, and
   spending less than usual is not an alarm to be raised. */
.review-change-delta.up { color: var(--negative); font-weight: 600; }

/* ---- drill-down ----
   Three depths, one row shape. Depth two indents rather than re-scaling, and its
   bar is a share of its own CATEGORY, not of the month — scaling both against the
   month total would render every merchant bar as a sliver. */
.drill-row {
  display: grid;
  grid-template-columns: 14px 24px minmax(0, 1fr) 160px 128px 132px;
  align-items: center; gap: var(--s3);
  padding: var(--s3) var(--s2);
  border-top: 1px solid var(--border);
  cursor: pointer; transition: background-color .1s ease;
}
.drill-row:hover { background: var(--bg-hover); }
.drill-row.is-open { background: var(--bg-sunken); }
.drill-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* Depth two has no icon of its own — the category above it already said which
   glyph this belongs to, and repeating it down the column is noise. */
.drill-merchant { grid-template-columns: 14px minmax(0, 1fr) 160px 128px 132px; }

/* Drawn rather than typed. The ▸/▾ glyphs render as indistinguishable dots at
   the size this column wants, and a triangle that has to be squinted at is not
   telling anyone a row can be opened. */
.drill-caret {
  justify-self: center;
  width: 0; height: 0;
  border-left: 5px solid var(--text-muted);
  border-top: 3.5px solid transparent;
  border-bottom: 3.5px solid transparent;
  transition: transform .12s ease;
}
.drill-row.is-open > .drill-caret { transform: rotate(90deg); }
.drill-name { font-size: 13px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.drill-merchant .drill-name { font-weight: 400; }

.drill-bar { display: block; height: 6px; border-radius: 3px; background: var(--track); overflow: hidden; }
.drill-bar-fill { display: block; height: 100%; background: var(--bar-fill); border-radius: 3px; }
.drill-bar-fill.sub { background: var(--neutral-fill); }

.drill-figs { text-align: right; font-variant-numeric: tabular-nums; font-size: 13px; }
.drill-share { display: block; font-size: 11px; color: var(--text-muted); font-weight: 400; margin-top: 2px; }

.drill-change { text-align: right; font-size: 12px; font-variant-numeric: tabular-nums; color: var(--text-muted); }
.drill-change.up { color: var(--negative); font-weight: 600; }
.drill-change.new { font-style: italic; }

/* The indent is the only thing carrying depth, so it has to be unmistakable at a
   glance — a rule down the left edge rather than padding alone. */
.drill-children {
  margin-left: var(--s5);
  border-left: 1px solid var(--border);
  padding-left: var(--s3);
}
.drill-txns { padding: var(--s1) 0 var(--s2) var(--s3); }
.drill-txns .row { border-top: none; }

@media (max-width: 900px) {
  .drill-row { grid-template-columns: 14px 24px minmax(0, 1fr) 108px; }
  .drill-merchant { grid-template-columns: 14px minmax(0, 1fr) 108px; }
  .drill-bar, .drill-change, .review-change-delta { display: none; }
  .review-change { grid-template-columns: 16px 24px minmax(0, 1fr) auto; }
}

/* ============================================================================
   Money ahead — projected cash-flow timeline
   ============================================================================ */

/* The finding, stated before the evidence. The rows below exist so this sentence
   can be checked; leading with the rows instead makes the reader do the work the
   projection was built to do for them. */
.cf-lead {
  font-size: 14px; color: var(--text);
  padding: var(--s3) 0 var(--s4);
}
.cf-lead b { font-weight: 600; }
.cf-lead.neg b { color: var(--negative); }

/* The horizon chips stay on the right, where every section action in the product
   sits — but on the rail the section actually has. They used to carry
   .row-aligned's 44px inset, which reserves the width of a row's action column so
   a header total can sit above the figures; the timeline rows have no action
   column, so that offset aligned the chips to nothing and left them 44px adrift
   of everything else on the right. var(--s2) is the row's own horizontal padding,
   so the chips now sit directly over the running-balance column: one rail down
   the right-hand side instead of three near-misses. */
.cf-head > .page-head-actions { margin-right: var(--s2); }

.cf-timeline { display: flex; flex-direction: column; }

.cf-row {
  display: grid;
  grid-template-columns: 20px 104px minmax(0, 1fr) 116px;
  align-items: start; gap: var(--s3);
  padding: var(--s3) var(--s2);
}
.cf-row.is-today { color: var(--text-secondary); }

/* One continuous rule down the column, drawn by the cells rather than by a
   separate element, so it cannot fall out of alignment with the nodes on it. */
.cf-rail {
  position: relative; align-self: stretch;
  display: flex; justify-content: center;
  min-height: 24px;
}
.cf-rail::before {
  content: ""; position: absolute; top: 0; bottom: -100%;
  width: 1px; background: var(--border);
}
.cf-row:last-child .cf-rail::before { bottom: 50%; }
.cf-node {
  position: relative; z-index: 1;
  width: 7px; height: 7px; margin-top: 7px; border-radius: 50%;
  background: var(--text-muted); box-shadow: 0 0 0 3px var(--bg);
}
.cf-node.today { background: var(--bg); border: 1.5px solid var(--text-muted); }
.cf-row.is-negative .cf-node { background: var(--negative); }
.cf-row.is-low .cf-node { background: var(--accent); }

.cf-when { font-size: 12px; line-height: 1.5; }
.cf-when b { display: block; font-size: 13px; font-weight: 500; color: var(--text); }
.cf-when span { color: var(--text-muted); font-size: 11px; }

.cf-items { display: flex; flex-direction: column; gap: var(--s1); min-width: 0; }
/* A grid, not a flex row with the amount floated right by margin-left:auto.
   The "Mark paid" button is present on a real ledger entry and absent on a
   projection, and under flex that difference moved the amount column by one gap
   — so a decimal point landed 8px off whenever a projected row sat above a
   scheduled one. Under a grid the amount is placed in the last track by name,
   so it lands in the same place whether the button is there or not. */
.cf-item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto 92px;
  align-items: center; gap: var(--s2);
  font-size: 13px; min-width: 0;
}
.cf-item-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: flex; align-items: center; gap: var(--s2); }
.cf-item-amt {
  grid-column: 4; text-align: right;
  font-variant-numeric: tabular-nums; color: var(--text-secondary); font-size: 12px;
}
.cf-item-amt.in { color: var(--income); }

.cf-balance {
  text-align: right; font-size: 13px; font-weight: 600;
  font-variant-numeric: tabular-nums; padding-top: 1px;
  /* Aligned with the DATE, not with the first entry, because it is the day's
     closing figure and the date is the other thing on this row that describes
     the day rather than a line in it.
     Not one balance per entry, tempting as that is: entries within a day are
     ordered by size, not by time, so a running figure between them would state
     an order the data does not have — on a payday-plus-rent day it would claim
     to know which landed first. One figure, at the day's close. */
  align-self: start;
}
/* Ledger convention, and the reason the two money columns don't blur into one:
   the entries are what happened, the balance is what it left you with. */
.cf-balance { color: var(--text); }
.cf-item-amt { font-weight: 400; }
.cf-row.is-negative .cf-balance { color: var(--negative); }
.cf-low-tag {
  display: block; font-size: 10px; font-weight: 500; letter-spacing: .04em;
  text-transform: uppercase; color: var(--accent); margin-top: 2px;
}

/* Quiet days are collapsed, not dropped — the running balance has to stay
   continuous to be worth trusting, but thirty rows of "nothing happened" is a
   list nobody reads to the bottom of. */
.cf-gap {
  display: grid; grid-template-columns: 20px minmax(0, 1fr);
  gap: var(--s3); align-items: center;
  padding: var(--s1) var(--s2);
  font-size: 11px; color: var(--text-muted);
}
.cf-gap .cf-rail { min-height: 18px; }
.cf-gap-end .cf-rail::before { bottom: 0; }

@media (max-width: 900px) {
  .cf-row { grid-template-columns: 20px 84px minmax(0, 1fr) 92px; }
  /* Dropping the amount has to drop its track too, or every row keeps a 92px
     hole where the column used to be. */
  .cf-item { grid-template-columns: auto minmax(0, 1fr) auto; }
  .cf-item-amt { display: none; }
}

/* The timeline item carries what the old "Next N days" list carried: what the
   payment is for, which account it leaves, and the option to pull it forward. */
.cf-item { display: grid; grid-template-columns: 20px minmax(0, 1fr) auto auto; align-items: center; gap: var(--s2); }
.cf-item-text { min-width: 0; }
.cf-item-name { display: flex; align-items: center; gap: var(--s2); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cf-item-sub { display: block; font-size: 11px; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 1px; }
.cf-item .btn.sm { opacity: 0; transition: opacity .12s ease; }
.cf-row:hover .cf-item .btn.sm { opacity: 1; }
.cf-item .btn.sm:focus-visible { opacity: 1; }

/* ============================================================================
   Statement import
   ============================================================================ */

/* The review step lists three groups of rows side by side with their matches —
   the standard drawer is too narrow to show a bank row and its counterpart on
   one line, and wrapping them makes the pairing unreadable. */
.drawer-wide { width: min(720px, 100vw); }

.import-drop {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--s2); text-align: center;
  padding: var(--s8) var(--s4);
  border: 1.5px dashed var(--border-strong); border-radius: var(--radius);
  background: var(--bg-sunken);
  cursor: pointer; transition: border-color .12s ease, background-color .12s ease;
}
.import-drop:hover, .import-drop.is-over { border-color: var(--accent); background: var(--accent-wash); }
.import-drop-mark { color: var(--text-muted); }
.import-drop-sub { font-size: 12px; color: var(--text-secondary); }

.import-issues { display: flex; flex-direction: column; gap: var(--s2); margin-bottom: var(--s4); }
.import-issue {
  display: flex; align-items: flex-start; gap: var(--s2);
  font-size: 12px; color: var(--text-secondary);
  padding: var(--s2) var(--s3);
  background: var(--bg-sunken); border-left: 2px solid var(--neutral-fill);
}

/* Three rows as the parser understood them. The only place a date-order mistake
   is catchable before it poisons every match in the plan. */
.import-sample { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.import-sample-row {
  display: grid; grid-template-columns: 190px minmax(0, 1fr) 100px;
  gap: var(--s3); align-items: center;
  padding: var(--s2) var(--s3); font-size: 12px;
  border-bottom: 1px solid var(--border);
}
.import-sample-row:last-child { border-bottom: none; }
.import-sample-desc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-secondary); }
.import-sample-amt { text-align: right; font-variant-numeric: tabular-nums; }
.import-sample-amt.in { color: var(--income); }

.import-dateswitch { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; margin-top: var(--s3); font-size: 12px; color: var(--text-secondary); }
.import-dateswitch-note { color: var(--text-muted); font-size: 11px; }

.import-summary { display: flex; gap: var(--s6); padding: var(--s4) 0; border-bottom: 1px solid var(--border); }
.import-summary-cell b { display: block; font-size: 20px; font-weight: 600; font-variant-numeric: tabular-nums; }
.import-summary-cell span { font-size: 11px; color: var(--text-muted); }

.import-row {
  display: grid; grid-template-columns: 16px minmax(0, 1fr) 92px minmax(0, 1fr);
  align-items: center; gap: var(--s3);
  padding: var(--s2) var(--s2);
  border-top: 1px solid var(--border);
  font-size: 12px; cursor: pointer;
}
.import-row:hover { background: var(--bg-hover); }
.import-row.is-head { grid-template-columns: minmax(0, 1fr) 92px; cursor: default; padding-left: 0; border-top: none; }
.import-row-main { min-width: 0; }
.import-row-desc { display: block; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.import-row-meta { display: block; font-size: 11px; color: var(--text-muted); margin-top: 1px; }
.import-row-amt { text-align: right; font-variant-numeric: tabular-nums; font-size: 13px; }
.import-row-amt.in { color: var(--income); }
/* The ledger side of the pairing, set apart so the two halves read as a match
   rather than as one long row. */
.import-row-match {
  min-width: 0; font-size: 12px; color: var(--text-secondary);
  padding-left: var(--s3); border-left: 1px solid var(--border);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.import-dup { padding: var(--s3) 0; border-top: 1px solid var(--border); }
.import-dup-options { display: flex; flex-direction: column; gap: 2px; margin-top: var(--s2); padding-left: var(--s3); }
.import-dup-option {
  display: grid; grid-template-columns: 16px minmax(0, 1fr) auto;
  align-items: center; gap: var(--s2);
  padding: var(--s2); border-radius: var(--radius); cursor: pointer; font-size: 12px;
}
.import-dup-option:hover { background: var(--bg-hover); }
.import-dup-option:has(input:checked) { background: var(--accent-wash); }
.import-dup-name { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.import-dup-meta { color: var(--text-muted); font-size: 11px; text-align: right; }

.import-actions { display: flex; gap: var(--s2); margin-top: var(--s6); }
.import-done-lead { font-size: 15px; margin-bottom: var(--s4); }

@media (max-width: 760px) {
  .import-row { grid-template-columns: 16px minmax(0, 1fr) 84px; }
  .import-row-match { display: none; }
  .import-sample-row { grid-template-columns: minmax(0, 1fr) 84px; }
  .import-sample-desc { display: none; }
}

/* ---------------------------------------------------------------------------
   Split rows and the inline settle form.

   The action lives on the person's row, so the form that confirms it opens
   directly beneath that row rather than in a panel further down the drawer. It
   is inset and washed so the fields read as belonging to the row above them,
   not as a new section that happens to follow one.
   --------------------------------------------------------------------------- */
.split-row + .split-row { border-top: 1px solid var(--border); }
.split-row.is-settling { background: var(--bg-inset); border-radius: var(--radius); }
.split-row.is-settling > div:first-child { padding-left: var(--s2); padding-right: var(--s2); }

.split-row input[type="text"],
.split-row input[type="number"],
[data-add-split] input {
  height: 28px; font-size: 12px; padding: 0 var(--s2); min-width: 0;
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  font-family: inherit;
}
.split-row input[type="number"],
[data-add-split] input[type="number"] { text-align: right; }

.settle-inline { padding: 0 var(--s2) var(--s3); }
.settle-fields { display: grid; grid-template-columns: 84px 1fr 1fr; gap: var(--s2); }
.settle-fields label {
  display: flex; flex-direction: column; gap: 2px;
  font-size: 11px; color: var(--text-muted);
}
.settle-fields input, .settle-fields select {
  height: 30px; font-size: 12px; padding: 0 var(--s2);
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  font-family: inherit; background: var(--bg); color: var(--text); min-width: 0;
}
.settle-fields input[type="number"] { text-align: right; }
.settle-actions { display: flex; gap: var(--s2); margin-top: var(--s3); }

@media (max-width: 760px) {
  .settle-fields { grid-template-columns: 1fr 1fr; }
  .settle-fields label:last-child { grid-column: 1 / -1; }
}

/* ---------------------------------------------------------------------------
   Plan next month — zero-based allocation drawer.

   The meter is pinned between the drawer head and the scrolling body rather
   than sitting in the flow with the rows. It is the one number the screen is
   about, and a figure that scrolls away while you edit the fields that change
   it is a figure you cannot use.
   --------------------------------------------------------------------------- */
.plan-meter {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s4);
  padding: var(--s4) var(--s6);
  border-bottom: 1px solid var(--border);
  background: var(--bg-subtle);
  flex-shrink: 0;
}
.plan-meter .metric-value { font-size: 26px; }
.plan-meter .metric-label { color: var(--text-secondary); }
/* Balanced is the goal, so it is the only state that earns the accent. Over is
   the reserved red. Under is deliberately neutral — money not yet allocated is
   the normal condition of a plan you are part way through, not a fault. */
.plan-meter-balanced { background: var(--accent-wash); }
.plan-meter-balanced .metric-value { color: var(--accent); }
.plan-meter-over { background: var(--negative-wash); }

.plan-row {
  display: grid; grid-template-columns: 28px 1fr 120px;
  align-items: center; gap: var(--s3);
  padding: var(--s3) 0;
  border-bottom: 1px solid var(--border);
}
.plan-row:last-child { border-bottom: 0; }
.plan-row-icon { display: flex; align-items: center; justify-content: center; }
.plan-row-main { min-width: 0; }
.plan-row-name {
  font-size: 13px; font-weight: 500; color: var(--text);
  display: flex; align-items: center; gap: var(--s2);
}
.plan-row-basis {
  font-size: 11px; color: var(--text-muted); margin-top: 2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.plan-tag {
  font-size: 10px; font-weight: 500; letter-spacing: .02em;
  color: var(--text-muted); background: var(--bg-subtle);
  border: 1px solid var(--border); border-radius: 999px;
  padding: 1px var(--s2);
}
.plan-tag-new { color: var(--accent); background: var(--accent-wash); border-color: var(--accent-wash); }

.plan-input {
  width: 100%; height: 32px; text-align: right;
  font-family: inherit; font-size: 13px; font-variant-numeric: tabular-nums;
  padding: 0 var(--s2);
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  background: var(--bg); color: var(--text);
}
.plan-input:focus { outline: 2px solid var(--accent); outline-offset: -1px; }

.field-note { font-size: 11px; color: var(--text-muted); margin-top: var(--s1); line-height: 1.5; }

@media (max-width: 760px) {
  .plan-meter { flex-direction: column; align-items: flex-start; gap: var(--s3); }
  .plan-row { grid-template-columns: 24px 1fr 92px; }
}

/* ---------- People ----------
   The row is the Goals row (which is the Budget row): "what is outstanding with this
   person" is the same shape of question as "how far along is this goal". The pace bar
   is the one part deliberately not reused — a debt has no target to be a fraction of,
   and a bar filling up as a friend repays you would imply the debt was the goal. */
.person-row { cursor: pointer; }
.person-detail {
  display: flex; flex-direction: column; gap: 2px;
  padding: var(--s2) var(--s2) var(--s3) 40px;   /* 40px lines up under the row's glyph */
}
/* A sortable section indents every row by the grip track, so the detail beneath one
   has to move with it or it stops lining up under the glyph it belongs to. */
.section-sortable .person-detail { padding-left: calc(40px + 10px + var(--s3)); }

/* The Unallocated row became a disclosure, so it says so: a pointer that turns, and
   the same hover the rows above it have. It is the one row in this list that opens,
   which is why the affordance is on the row rather than assumed. */
.unalloc-row { cursor: pointer; }
.disclosure {
  display: inline-block; width: 12px; margin-right: 2px;
  color: var(--text-muted); transition: transform .12s ease;
}
.disclosure.is-open { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) { .disclosure { transition: none; } }

/* ---------- "More details" ----------
   Repeats, sharing and off-budget are the settings that matter enormously on the
   transactions that need them and are noise on every other one. Behind a link,
   the form is six fields and a button; the power is one click away rather than
   permanently in the way.

   The open/close animates on grid-template-rows rather than max-height, so the
   panel travels exactly its own height — no guessed pixel value to be wrong the
   day a field is added, and no snap at the end when the guess was too large. */
.more-details { margin: var(--s2) 0 var(--s4); }
.more-details-toggle {
  display: flex; align-items: center; gap: var(--s1);
  background: none; border: 0; padding: var(--s2) 0; margin: 0;
  font: inherit; font-size: 12px; color: var(--text-secondary); cursor: pointer;
}
.more-details-toggle:hover { color: var(--text); }
.more-details-sum {
  margin-left: var(--s2); color: var(--text-muted); font-variant-numeric: tabular-nums;
}
.more-details-body {
  display: grid; grid-template-rows: 0fr;
  transition: grid-template-rows .18s ease, opacity .18s ease;
  opacity: 0;
}
.more-details.is-open .more-details-body { grid-template-rows: 1fr; opacity: 1; }
/* min-height:0 is what lets a grid row actually collapse to nothing, and the
   overflow is what stops the contents spilling out of it while it does. */
.more-details-inner { overflow: hidden; min-height: 0; }
.more-details.is-open .more-details-inner { padding-top: var(--s2); }
@media (prefers-reduced-motion: reduce) { .more-details-body { transition: none; } }
.person-item {
  display: grid; grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center; gap: var(--s3);
  padding: var(--s2) 0;
  font-size: 12px; color: var(--text-secondary);
  border-bottom: var(--hairline);
}
.person-item:last-child { border-bottom: 0; }
.person-item-what { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* What KIND of claim this is, which the amount alone cannot say — a shared purchase
   settles on its own transaction, a loan settles here. */
.person-item-kind {
  margin-left: var(--s2); font-size: 10px; color: var(--text-muted);
  text-transform: uppercase; letter-spacing: .04em;
}
.person-item-amt { font-variant-numeric: tabular-nums; color: var(--text); white-space: nowrap; }

/* The reality check under the Dashboard headline. Renders only when it has something
   to say — a permanent "your cash covers it" would be furniture, and furniture is
   what people stop reading. */
.hero-cover {
  margin-top: var(--s3);
  font-size: 12px; line-height: 1.5;
  color: var(--text-secondary);
  display: flex; flex-direction: column; gap: 2px;
  max-width: 62ch;
}

/* The "how?" modal now answers two questions, so it needs a divider that names them. */
.modal-section-head {
  padding: var(--s4) var(--s5) 0;
  font-size: 11px; font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-muted);
}
.modal-section-head + .modal-body { padding-top: var(--s2); }
