/* DustyBankers — calm personal-finance design system. Light + dark, mobile-first. */

/* iOS-neutral theme (ported from the Claude Design homepage redesign): system
   grays, white rounded cards, iOS green accent.

   Token NAMES are deliberately unchanged from the previous fintech-green theme.
   Every screen and every chart reads these names, so re-pointing the values
   restyles the whole app at once and keeps test/contrast.test.js — which parses
   these two blocks by name — enforcing the gate. Change a token and that test
   tells you if it broke.

   Two tokens intentionally differ from the source design, both to hold 4.5:1:
   --ink-3 in dark (design #8e8e93 measured 4.27:1 on --surface-2) and --warn
   (design #ffb020 is a fill, never legible as small text on white). The vivid
   values survive as --accent-fill / --warn-fill for bars, arcs and borders,
   which are decoration and never carry text. */
:root {
  --bg: #f2f2f7;
  --surface: #ffffff;
  --surface-2: #f7f7fa;
  --ink: #1d1d1f;
  --ink-2: #5c5c61;
  /* carries almost all small print — row subtitles, notes, metric labels, axes */
  --ink-3: #66666b;
  --hairline: rgba(60, 60, 67, 0.16);
  --hairline-soft: rgba(60, 60, 67, 0.10);
  /* the design's color-mix(in srgb, #34c759 52%, #062611) resolved to a literal
     hex: contrast.test.js only parses #rrggbb, so leaving it as a color-mix
     would silently drop --accent from the light-theme gate */
  --accent: #1e7a36;
  --accent-fill: #34c759;
  --on-accent: #ffffff;
  --accent-soft: rgba(52, 199, 89, 0.14);
  --accent-tint: #e9f7ed;
  --good: #1e7a36;
  --good-soft: rgba(52, 199, 89, 0.11);
  --warn: #8a5a00;
  --warn-fill: #ffb020;
  /* Added for the Plan tab's category-budget strip: a surface tint, not a
     text colour. Both themes, because a token defined in only one is how a
     screen ends up unreadable in the other. */
  --warn-tint: #fff6e4;
  --danger: #d70015;
  --danger-soft: rgba(215, 0, 21, 0.09);
  --chart-surface: #ffffff;
  /* The forecast chart's scenario/ghost stroke. This grey and --accent are a
     VALIDATED pair (contrast-checked 2026-08-26, both themes): the previous
     pairing, --accent against --good, resolves to one green in both themes
     and left the two chart lines told apart by their dashes alone. A fill,
     not a text colour — it is deliberately outside the contrast gate's lists. */
  --chart-ghost: #8a8a8f;

  /* iOS system fills — the neutral chip/track/segmented backgrounds */
  --fill: rgba(118, 118, 128, 0.10);
  --fill-2: rgba(118, 118, 128, 0.16);
  --separator: rgba(60, 60, 67, 0.10);
  --card-border: rgba(0, 0, 0, 0.035);
  --bar-bg: rgba(242, 242, 247, 0.72);
  --scrim: rgba(0, 0, 0, 0.30);

  --shadow: 0 1px 1px rgba(0, 0, 0, 0.03), 0 8px 26px -10px rgba(0, 0, 0, 0.14);
  /* a recessed well — the groove a slider or progress track sits in */
  --well: rgba(118, 118, 128, 0.11);
  --well-inset: inset 0 1px 2.5px rgba(0, 0, 0, 0.09), inset 0 0 0 0.5px rgba(0, 0, 0, 0.04);
  --r-hero: 26px;
  --r-lg: 22px;
  --r-md: 16px;
  --r-sm: 11px;
  --tabbar-h: 62px;
  /* Motion. One strong ease-out for entrances and presses: the built-in
     easings are too weak to read as intentional, and ease-in delays the
     first frames — exactly where the eye is. */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  color-scheme: light;
}

:root[data-theme="dark"] {
  --bg: #000000;
  --surface: #1c1c1e;
  --surface-2: #2c2c2e;
  --ink: #f5f5f7;
  --ink-2: #a1a1a6;
  /* lifted from the design's #8e8e93, which measured 4.27:1 on --surface-2 */
  --ink-3: #98979c;
  --hairline: rgba(84, 84, 88, 0.55);
  --hairline-soft: rgba(84, 84, 88, 0.36);
  --accent: #30d158;
  --accent-fill: #30d158;
  --on-accent: #10240f;
  --accent-soft: rgba(48, 209, 88, 0.14);
  --accent-tint: #17251a;
  --good: #30d158;
  --good-soft: rgba(48, 209, 88, 0.13);
  --warn: #fab219;
  --warn-fill: #ffb020;
  --warn-tint: #372e1e;
  /* lifted from the design's #ff453a, which measured 4.09:1 on --surface-2 */
  --danger: #ff6961;
  --danger-soft: rgba(255, 105, 97, 0.14);
  --chart-surface: #1c1c1e;
  /* the dark half of the validated chart pair — see the light block */
  --chart-ghost: #8e8e93;

  --fill: rgba(118, 118, 128, 0.24);
  --fill-2: rgba(118, 118, 128, 0.36);
  --separator: rgba(84, 84, 88, 0.55);
  --card-border: rgba(255, 255, 255, 0.06);
  --bar-bg: rgba(20, 20, 22, 0.70);
  --scrim: rgba(0, 0, 0, 0.5);

  --shadow: none;
  /* the well reads as depth by shadow in light and by darkness in dark, where
     a light-mode drop shadow would be invisible against a near-black card */
  --well: rgba(0, 0, 0, 0.38);
  --well-inset: inset 0 1px 3px rgba(0, 0, 0, 0.6), inset 0 0 0 0.5px rgba(255, 255, 255, 0.05);
  color-scheme: dark;
}

* { margin: 0; padding: 0; box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }

body {
  font-family: -apple-system, system-ui, "Segoe UI", Roboto, sans-serif;
  font-size: 16px;
  line-height: 1.45;
  background: var(--bg);
  color: var(--ink);
  min-height: 100dvh;
  -webkit-font-smoothing: antialiased;
}

button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
input, select, textarea { font: inherit; color: var(--ink); }

/* Double-tap on a control must not zoom the page — people tap twice when the
   first tap did not appear to land, and on iOS Safari that zooms instead.
   `manipulation` opts each CONTROL out of double-tap-zoom while keeping pans
   and pinch-zoom everywhere, which is why it is used and the viewport-tag
   routes (user-scalable=no / maximum-scale) are not: those would kill
   pinch-zoom for the whole page — a real loss on a screen made of figures —
   and iOS ignores them anyway. App-wide on purpose: every tab is built from
   these same tap targets, not just the screen the defect was found on. */
button, a, input, select, textarea, summary, [role="button"] {
  touch-action: manipulation;
}

/* Keyboard focus.
   `.input` clears the native outline, so without this a keyboard user gets
   nothing but a border tint on inputs and no cue at all on buttons or rows.
   :focus-visible keeps it off for pointer users. */
:focus-visible {
  outline: 2.5px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}
.row:focus-visible, .tab:focus-visible { outline-offset: -2.5px; }
.input:focus-visible { outline-offset: 0; }

/* ---------------------------------------------------------------- layout */

/* The shell is a 100dvh flex column with its own scroll container, rather than
   a scrolling body. The header can then stay pinned and react to scroll depth,
   and the floating tab bar can sit over the content without the iOS Safari
   URL-bar resize jitter a `position: fixed` bar gets. Every view renders into
   .app-scroll > .screen, so no view needed changing for this. */
.app-shell {
  position: relative;
  height: 100dvh;
  display: flex; flex-direction: column;
  overflow: hidden;
  background: var(--bg);
}

.app-header {
  position: relative; z-index: 20; flex-shrink: 0;
  background: var(--bar-bg);
  -webkit-backdrop-filter: saturate(180%) blur(24px);
  backdrop-filter: saturate(180%) blur(24px);
  padding-top: env(safe-area-inset-top, 0px);
}
.app-header-inner {
  max-width: 760px; margin: 0 auto;
  padding: 0 20px; height: 54px;
  display: flex; align-items: center; gap: 8px;
}
/* the app name only appears once the large in-page title has scrolled away,
   so the two never read as a duplicate heading */
.app-header .title {
  flex: 1; min-width: 0;
  font-size: 17px; font-weight: 600; letter-spacing: -0.01em;
  opacity: 0; transform: translateY(6px);
  transition: opacity 0.28s ease, transform 0.28s cubic-bezier(0.32, 0.72, 0, 1);
}
.app-header .hairline {
  height: 1px; background: var(--separator);
  opacity: 0; transition: opacity 0.3s ease;
}
.app-shell.scrolled .app-header .title { opacity: 1; transform: none; }
.app-shell.scrolled .app-header .hairline { opacity: 1; }

.header-btn {
  width: 34px; height: 34px; border-radius: 999px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  color: var(--ink-2); background: var(--fill);
  transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease;
}
.header-btn:hover { color: var(--ink); background: var(--fill-2); }
.header-btn:active { transform: scale(0.92); }
.header-btn svg { width: 17px; height: 17px; }
.header-btn.spinning svg { animation: spin 0.9s linear infinite; }

.app-scroll {
  flex: 1; overflow-y: auto; overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
}

.screen {
  max-width: 760px;
  margin: 0 auto;
  padding: 12px 20px calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px) + 44px);
}

.topbar {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 4px 14px;
}
.topbar h1 { font-size: clamp(30px, 7vw, 36px); font-weight: 700; letter-spacing: -0.032em; line-height: 1.08; flex: 1; }
.topbar .sub { font-size: 13px; color: var(--ink-3); }

/* A circular action sitting beside the page title. Same fill, easing and
   press-scale as .btn so it presses like every other control; sized down to
   40px so it reads as secondary to a 36px title rather than competing. */
.topbar-action {
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; padding: 0;
  border-radius: 999px;
  background: var(--fill); border: 1px solid transparent;
  color: var(--ink);
  transition: background 0.2s ease, transform 0.18s cubic-bezier(0.32, 0.72, 0, 1);
}
.topbar-action:hover { background: var(--fill-2); }
.topbar-action:active { transform: scale(0.96); }
.topbar-action svg { width: 20px; height: 20px; }

/* ---------------------------------------------------------------- tab bar */

/* A floating pill rather than a full-width bar: it reads as a control sitting
   over the content, and the inset keeps it clear of the home indicator.
   Glass, not a flat fill — the gradient and the two inset highlights give the
   track an edge to catch light on, which is what stops a blurred bar reading
   as a grey smear over whatever it happens to be sitting on. */
.tabbar {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 25;
  display: flex; justify-content: center;
  padding: 0 12px 14px;
  padding-bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  pointer-events: none;
}
.tabbar-inner {
  pointer-events: auto; position: relative;
  display: flex; width: 100%; max-width: 520px; padding: 6px;
  border-radius: 30px;
  background: linear-gradient(180deg, rgba(252, 252, 253, 0.86), rgba(240, 240, 244, 0.82));
  -webkit-backdrop-filter: saturate(200%) blur(30px); backdrop-filter: saturate(200%) blur(30px);
  border: 0.5px solid rgba(0, 0, 0, 0.06);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.95),
    inset 0 -0.5px 0 rgba(0, 0, 0, 0.04),
    0 1px 2px rgba(0, 0, 0, 0.05),
    0 10px 22px -8px rgba(0, 0, 0, 0.18),
    0 24px 48px -16px rgba(0, 0, 0, 0.26);
}
:root[data-theme="dark"] .tabbar-inner {
  background: linear-gradient(180deg, rgba(44, 44, 46, 0.86), rgba(28, 28, 30, 0.82));
  border-color: rgba(255, 255, 255, 0.08);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.07),
    inset 0 -0.5px 0 rgba(0, 0, 0, 0.4),
    0 1px 2px rgba(0, 0, 0, 0.5),
    0 10px 22px -8px rgba(0, 0, 0, 0.6),
    0 24px 48px -16px rgba(0, 0, 0, 0.7);
}

/* One pill for six tabs, moved rather than redrawn. Width and offset are
   fractions of the track so it lands correctly at any width. */
.tab-pill {
  position: absolute; top: 6px; bottom: 6px; left: 6px;
  width: calc((100% - 12px) / 6);
  border-radius: 24px; pointer-events: none;
  background: linear-gradient(180deg, #fff, #f7f7fa);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 1),
    inset 0 0 0 0.5px rgba(0, 0, 0, 0.045),
    0 1px 1px rgba(0, 0, 0, 0.04),
    0 4px 10px -2px rgba(0, 0, 0, 0.10),
    0 8px 20px -6px rgba(0, 0, 0, 0.10);
  transition: left 320ms cubic-bezier(0.32, 0.72, 0, 1);
}
:root[data-theme="dark"] .tab-pill {
  background: linear-gradient(180deg, #3a3a3c, #2c2c2e);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.10),
    inset 0 0 0 0.5px rgba(255, 255, 255, 0.05),
    0 1px 1px rgba(0, 0, 0, 0.3),
    0 4px 10px -2px rgba(0, 0, 0, 0.4);
}
/* the stretch is its own one-shot animation: a transition can carry the pill
   or squash it, not both at once */
@keyframes pillStretch {
  0% { transform: scaleX(1); }
  45% { transform: scaleX(1.06); }
  100% { transform: scaleX(1); }
}
.tab-pill.moving { animation: pillStretch 320ms cubic-bezier(0.32, 0.72, 0, 1); }

.tab {
  flex: 1; min-width: 0; position: relative; z-index: 1;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px;
  padding: 9px 0 8px;
  color: var(--ink-3); background: none;
  white-space: nowrap;
  transition: color 150ms ease;
}
.tab-lab { font-size: 10px; font-weight: 520; letter-spacing: 0.004em; }
.tab-ico { display: flex; }
.tab svg { width: 23px; height: 23px; stroke-width: 1.6; transition: stroke-width 150ms ease; }
/* the pill and the ink carry the active state — no second coloured chip */
.tab.active { color: var(--good); }
.tab.active svg { stroke-width: 2.15; }
.tab.active .tab-lab { font-weight: 680; }
.tab .badge {
  position: absolute; top: 5px; left: calc(50% + 7px);
  background: var(--danger); color: #fff; font-size: 10.5px; font-weight: 700;
  min-width: 17px; height: 17px; border-radius: 9px;
  display: flex; align-items: center; justify-content: center; padding: 0 4px;
  box-shadow: 0 0 0 2px rgba(250, 250, 252, 0.95), 0 1px 3px rgba(215, 0, 21, 0.35);
}
:root[data-theme="dark"] .tab .badge { box-shadow: 0 0 0 2px rgba(32, 32, 34, 0.95), 0 1px 3px rgba(0, 0, 0, 0.5); }

@media (prefers-reduced-motion: reduce) {
  .tab-pill { transition: left 100ms linear; }
  .tab-pill.moving { animation: none; }
}

/* ---------------------------------------------------------------- lists & rows */

.group { background: var(--surface); border: 1px solid var(--card-border); border-radius: var(--r-lg); overflow: hidden; box-shadow: var(--shadow); }
.group + .group, .section + .section { margin-top: 16px; }
/* Section headings are now full-size sentence case, not tiny uppercase eyebrows —
   they carry real hierarchy against the large in-page title. */
.group-title {
  font-size: 21px; font-weight: 660; color: var(--ink);
  letter-spacing: -0.025em;
  padding: 0 6px 11px;
  margin-top: 26px;
}
.group-title:first-child { margin-top: 0; }

/* Inside a sheet these are sub-labels within a picker, not page sections —
   at page size they would outweigh the sheet's own title. */
.sheet .group-title {
  font-size: 13px; font-weight: 650; color: var(--ink-3);
  text-transform: uppercase; letter-spacing: 0.05em;
  margin-top: 16px; padding: 0 4px 6px;
}
.sheet .group-title:first-child { margin-top: 2px; }
/* an optional quiet value or control sitting opposite the heading. Wraps
   rather than squeezing, so a segmented control keeps its labels on one line
   instead of breaking "Single charge" across two. */
.section-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px 12px; flex-wrap: wrap;
  padding: 0 6px 11px;
}
.section-head .group-title { padding: 0; margin: 0; }
.section-head .aside { font-size: 13.5px; color: var(--ink-3); text-align: right; margin-left: auto; }
.section-head .seg { flex: 0 0 auto; margin-left: auto; }

.row {
  display: flex; align-items: center; gap: 13px;
  padding: 12px 18px; min-height: 56px;
  border-bottom: 1px solid var(--separator);
  width: 100%; text-align: left;
  transition: background 0.18s ease;
}
.row:last-child { border-bottom: none; }
button.row:hover { background: var(--surface-2); }
button.row:active { background: var(--fill); }
.row-main { flex: 1; min-width: 0; }
.row-title { font-size: 15.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.row-sub { font-size: 12.5px; color: var(--ink-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; display: flex; align-items: center; gap: 6px; }
.row-end { text-align: right; flex-shrink: 0; }

.import-preview-row { gap: 10px; }
.import-check {
  width: 22px; height: 22px; flex-shrink: 0;
  accent-color: var(--accent);
}
.import-row-open {
  display: flex; align-items: center; gap: 12px;
  flex: 1; min-width: 0; text-align: left;
}
.import-row-open:active { opacity: 0.72; }

.amount { font-variant-numeric: tabular-nums; font-weight: 650; font-size: 15.5px; letter-spacing: -0.01em; }
.amount .dec { font-size: 0.78em; opacity: 0.75; }
.amount.in { color: var(--good); }
.amount-sub { font-size: 11.5px; color: var(--ink-3); margin-top: 1px; font-variant-numeric: tabular-nums; }

.day-head {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: 12.5px; font-weight: 600; color: var(--ink-3);
  padding: 16px 6px 6px;
}
.day-head .day-total { font-variant-numeric: tabular-nums; font-weight: 500; }

/* avatar: merchant logo or initials.

   ONE container, four steps. Every merchant renders in the same shape with the
   same optical logo size, whatever the artwork is — the consistency comes from
   the container, not from the marks agreeing with each other. Eight ad-hoc
   sizes (34/34/40/50/54/56/60/66) across seven radii collapsed to:

     sm 36 · (default) 44 · lg 56 · big 68      radius = 0.29 × size, rounded
                                                padding = 0.145 × size
                                                initials = 0.34 × size

   Padding scales with the box, so the *mark* stays the same fraction of the
   container at every step rather than the box growing around a fixed inset.

   The plate is white in BOTH themes and so is the hairline (a literal rgba,
   not a token): a brand mark carrying white or black elements would vanish
   against one theme if the plate followed it. That is also why merchants do
   not each carry a background token. The plate and ring appear only when there
   is an image to frame — a monogram keeps the full brand fill. */
.avatar {
  width: 44px; height: 44px; border-radius: 13px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 15px; font-weight: 700; color: #fff;
  background: var(--av-bg, #8b8a82);
  overflow: hidden;
}
.avatar img, .avatar .mark {
  width: 100%; height: 100%; object-fit: contain; background: #fff;
  padding: 6px; box-shadow: inset 0 0 0 1px rgba(60, 60, 67, 0.12);
}
.avatar.sm { width: 36px; height: 36px; border-radius: 10px; font-size: 12px; }
.avatar.sm img, .avatar.sm .mark { padding: 5px; }
.avatar.lg { width: 56px; height: 56px; border-radius: 16px; font-size: 19px; }
.avatar.lg img, .avatar.lg .mark { padding: 8px; }
.avatar.big { width: 68px; height: 68px; border-radius: 20px; font-size: 23px; }
.avatar.big img, .avatar.big .mark { padding: 10px; }
/* .tile and .id predate the scale and are kept as aliases so the 18 call sites
   need no edit: tile (recurring grid) and id (item detail) both land on lg. */
.avatar.tile, .avatar.id { width: 56px; height: 56px; border-radius: 16px; font-size: 19px; }
.avatar.tile img, .avatar.id img, .avatar.tile .mark, .avatar.id .mark { padding: 8px; }

/* pending dot + chips */
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); display: inline-block; flex-shrink: 0; }
.chip {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 12.5px; font-weight: 560; color: var(--ink-2);
  background: var(--fill); border: 1px solid transparent;
  padding: 3px 10px; border-radius: 999px; white-space: nowrap;
  letter-spacing: -0.01em; font-variant-numeric: tabular-nums;
}
.chip.accent { color: var(--accent); background: var(--accent-soft); border-color: transparent; }
.chip.warn-chip { color: var(--warn); }
.chip.good { color: var(--good); background: var(--good-soft); border-color: transparent; }
.chip.danger-chip { color: var(--danger); background: var(--danger-soft); border-color: transparent; }
.chip.muted { color: var(--ink-3); }

/* A glyph leading a line of text outside a button — chips, row sublines.
   Em-sized so one rule tracks a 12.5px chip and a 15px row alike, and left on
   currentColor so it takes the colour of the text it leads. Its containers are
   flex with their own gap, so it carries no margin of its own. */
.ico-inline {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.15em; height: 1.15em; flex-shrink: 0;
}
.ico-inline svg { width: 100%; height: 100%; }

/* metric strip: 2–4 figures side by side under a hero number. Labels are tiny
   and quiet; the numbers carry the meaning. */
.metrics { display: grid; gap: 1px; background: var(--hairline-soft); border-radius: var(--r-md); overflow: hidden; margin-top: 14px; }
.metrics.c2 { grid-template-columns: 1fr 1fr; }
.metrics.c3 { grid-template-columns: 1fr 1fr 1fr; }
.metric { background: var(--surface); padding: 10px 12px; text-align: center; }
.stat.tinted .metric { background: var(--accent-tint); }
.metric .k { font-size: 10.5px; font-weight: 650; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.04em; white-space: nowrap; }
.metric .v { font-size: 17px; font-weight: 750; letter-spacing: -0.02em; margin-top: 2px; font-variant-numeric: tabular-nums; }
.metric .v.pos { color: var(--good); }
.metric .v.neg { color: var(--danger); }
.metric .s { font-size: 10.5px; color: var(--ink-3); margin-top: 1px; }

/* a segmented progress bar that can show spent vs moved in one track */
.split-bar { height: 8px; border-radius: 4px; background: var(--hairline); overflow: hidden; display: flex; }
.split-bar i { height: 100%; display: block; }

/* dotted projection marker sitting on the bar */
.bar-wrap { position: relative; }
.bar-mark { position: absolute; top: -3px; width: 2px; height: 14px; background: var(--ink-3); border-radius: 1px; }

/* recurring confidence: three dots, filled by level — never color-alone (label sits beside) */
.conf-dots { display: inline-flex; gap: 3px; align-items: center; }
.conf-dots i { width: 5px; height: 5px; border-radius: 50%; background: var(--hairline); }
.conf-dots i.on { background: var(--accent); }

/* ---------------------------------------------------------------- controls */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  font-size: 15px; font-weight: 600;
  min-height: 46px; padding: 10px 20px;
  border-radius: 999px;
  background: var(--fill); border: 1px solid transparent;
  color: var(--ink); letter-spacing: -0.01em;
  transition: background 0.2s ease, transform 0.18s cubic-bezier(0.32, 0.72, 0, 1);
}
.btn:hover { background: var(--fill-2); }
.btn:active { transform: scale(0.96); }
.btn.primary { background: var(--accent); border-color: transparent; color: var(--on-accent); }
.btn.primary:hover { background: var(--accent); filter: brightness(1.06); }
.btn.danger { color: var(--danger); }
.btn.quiet { background: none; border: none; color: var(--accent); }
.btn.quiet:hover { background: none; opacity: 0.65; }
.btn.small { min-height: 38px; padding: 0 15px; font-size: 14px; font-weight: 560; }
/* a count riding inside a pill button, e.g. "Transactions · 1 excluded" */
.btn .btn-count {
  font-size: 11.5px; font-weight: 600; color: var(--ink-3);
  background: var(--surface); border-radius: 999px; padding: 2px 7px;
}
/* SVG glyphs inside buttons — one size token, matching stroke, so buttons stay
   consistent with the tab bar instead of mixing emoji and vector art */
.btn svg { width: 17px; height: 17px; flex-shrink: 0; }
.btn.small svg { width: 15px; height: 15px; }

/* Touch targets.
   Compact controls stay visually compact but get a 44px tap area via an
   invisible overlay — the web equivalent of hitSlop. The overlay is a child of
   the control, so clicks still land on the control itself, and it never
   affects layout. Only applied on coarse pointers, where it matters. */
@media (pointer: coarse) {
  .btn.small, .seg button, .month-nav button, .chart-key, .topbar-action { position: relative; }
  /* the overlay must stay clickable — it is what enlarges the target, and the
     click bubbles to its parent button. Vertical only, so it can never reach
     a neighbour horizontally. */
  .btn.small::after, .seg button::after, .month-nav button::after, .chart-key::after,
  .topbar-action::after {
    content: ''; position: absolute; left: 0; right: 0; top: 50%;
    transform: translateY(-50%); height: 44px;
  }
  /* icon-only controls also need 44px of width */
  .month-nav button, .topbar-action { min-width: 44px; justify-content: center; }
  .chart-key { min-height: 44px; }
}
.btn.block { width: 100%; }
.btn:disabled { opacity: 0.5; pointer-events: none; }

.field { display: block; margin-bottom: 14px; }
.field > span { display: block; font-size: 13px; font-weight: 600; color: var(--ink-2); margin-bottom: 6px; }
.input, select.input, textarea.input {
  width: 100%; font-size: 17px;
  background: var(--surface-2); border: 1px solid var(--hairline);
  border-radius: var(--r-md); padding: 12px 14px;
  outline: none; min-height: 48px;
}
.input:focus { border-color: var(--accent); }
textarea.input { min-height: 80px; resize: vertical; }
.input-row { display: flex; gap: 10px; }
.input-row > * { flex: 1; }

.seg {
  display: flex; background: var(--fill);
  border-radius: 10px; padding: 2px; gap: 2px;
}
.seg button {
  flex: 1; font-size: 13.5px; font-weight: 560; color: var(--ink-2);
  padding: 0 12px; border-radius: 8px; min-height: 30px;
  letter-spacing: -0.01em; white-space: nowrap;
  transition: background 0.25s cubic-bezier(0.32, 0.72, 0, 1), color 0.25s ease, box-shadow 0.25s ease;
}
.seg button.active { background: var(--surface); color: var(--ink); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); }

.searchbar {
  display: flex; align-items: center; gap: 8px;
  background: var(--surface-2); border: 1px solid var(--hairline-soft);
  border-radius: var(--r-md); padding: 0 12px; min-height: 42px;
}
.searchbar svg { width: 17px; height: 17px; color: var(--ink-3); flex-shrink: 0; }
.searchbar input { flex: 1; border: none; background: none; outline: none; font-size: 16px; min-width: 0; }

.month-nav { display: flex; align-items: center; gap: 4px; }
.month-nav .label { font-size: 15px; font-weight: 700; min-width: 92px; text-align: center; }
.month-nav button { color: var(--ink-2); padding: 8px; border-radius: 8px; display: flex; }
.month-nav button:disabled { opacity: 0.3; }
.month-nav svg { width: 18px; height: 18px; }

.toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; flex-wrap: wrap; }
.toolbar .grow { flex: 1; }

/* The in-page title is now display-sized (up to 36px), so a month nav beside it
   no longer fits on a phone — it was being clipped off the right edge at 375px.
   Wrap it onto its own line for anything narrower than a small tablet. */
@media (max-width: 480px) {
  .topbar { flex-wrap: wrap; }
  .topbar .month-nav {
    order: 3; width: 100%; justify-content: space-between;
    border-top: 1px solid var(--hairline-soft); padding-top: 6px; margin-top: 2px;
  }
}
@media (max-width: 359px) {
  .screen { padding-left: 14px; padding-right: 14px; }
}

/* ---------------------------------------------------------------- stats & meters */

.stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.stat {
  background: var(--surface); border: 1px solid var(--card-border);
  border-radius: var(--r-lg); padding: 16px 18px; box-shadow: var(--shadow);
}
.stat .label { font-size: 13px; font-weight: 600; color: var(--ink-2); letter-spacing: -0.01em; }
.stat .value { font-size: 24px; font-weight: 660; letter-spacing: -0.03em; margin-top: 3px; font-variant-numeric: tabular-nums; }
.stat .value .cur { font-size: 12px; font-weight: 600; color: var(--ink-3); margin-right: 4px; }
.stat .delta { font-size: 12.5px; color: var(--ink-3); margin-top: 4px; }
.stat .delta .up { color: var(--danger); }
.stat .delta .down { color: var(--good); }
.stat.hero { grid-column: 1 / -1; }
.stat.hero .value { font-size: 34px; }
/* tinted hero card — the light-green balance/spend card of the fintech look */
.stat.tinted { background: var(--accent-tint); border-color: transparent; }
.stat.tinted .label { color: var(--ink-2); }
.stat .chiprow { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; }

.meter { padding: 2px 0; }
.meter-head { display: flex; justify-content: space-between; align-items: baseline; font-size: 13px; margin-bottom: 6px; }
.meter-head .nm { font-weight: 600; color: var(--ink-2); display: flex; gap: 6px; align-items: center; }
.meter-head .val { color: var(--ink-3); font-variant-numeric: tabular-nums; font-size: 12.5px; }
/* a neutral track, so the fill's own colour is the only thing carrying meaning
   — a green track under a red fill read as two competing signals */
.meter-track { height: 7px; border-radius: 999px; background: var(--fill); overflow: hidden; }
.meter-fill { height: 100%; border-radius: 999px; background: var(--accent-fill); }
.meter.warn .meter-track { background: rgba(250, 178, 25, 0.18); }
.meter.warn .meter-fill { background: var(--warn); }
.meter.over .meter-track { background: var(--danger-soft); }
.meter.over .meter-fill { background: var(--danger); }

/* ---------------------------------------------------------------- sheets */

.sheet-backdrop {
  position: fixed; inset: 0; z-index: 40;
  background: var(--scrim);
  -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
  opacity: 0; transition: opacity 0.22s;
}
.sheet-backdrop.show { opacity: 1; }
.sheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 41;
  max-width: 520px; margin: 0 auto 12px;
  background: var(--surface);
  border-radius: var(--r-hero);
  border: 1px solid var(--card-border);
  box-shadow: 0 -4px 60px -10px rgba(0, 0, 0, 0.4);
  max-height: calc(100dvh - 40px);
  display: flex; flex-direction: column;
  transform: translateY(calc(100% + 12px)); transition: transform 0.42s cubic-bezier(0.32, 0.72, 0, 1);
  margin-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
  width: calc(100% - 24px);
}
.sheet.show { transform: translateY(0); }
.sheet-grip { width: 38px; height: 5px; border-radius: 3px; background: var(--fill-2); margin: 10px auto 0; flex-shrink: 0; }
.sheet-head { display: flex; align-items: center; padding: 10px 16px 8px; gap: 10px; }
.sheet-head h2 { font-size: 17px; font-weight: 700; flex: 1; }
.sheet-body { overflow-y: auto; padding: 4px 16px 16px; }
.sheet-actions { display: flex; gap: 10px; padding: 10px 16px 14px; border-top: 1px solid var(--hairline-soft); }
.sheet-actions .btn { flex: 1; }

/* ---- pushable sheet: one sheet, several screens ----
   The old item detail was one sheet of 13 label/value rows, which answered
   every question at the same volume. This ranks them instead: the first screen
   answers what, how much and when next, and everything else is a push away.
   Depth drives height — one screen sizes to its content, more than one fills
   the sheet — so there is no second "how tall" setting to keep in step. */
.sheet.stacked { padding-bottom: 0; overflow: hidden; transition: transform 0.42s cubic-bezier(0.32, 0.72, 0, 1), height 0.42s cubic-bezier(0.32, 0.72, 0, 1); }
.sheet-stack { position: relative; flex: 1; min-height: 0; }
.sheet-screen {
  position: absolute; inset: 0;
  display: flex; flex-direction: column;
  background: var(--surface); border-radius: 0 0 var(--r-hero) var(--r-hero);
  transition: transform 0.42s cubic-bezier(0.32, 0.72, 0, 1), opacity 0.28s ease;
}
/* grouped-list screens sit on the page canvas, so the white groups read as
   cards on it rather than panels floating on another panel */
.sheet-screen.canvas { background: var(--bg); }
.sheet-screen.offstage { transform: translateX(100%); }
/* the screen behind travels a third of the way, the standard iOS parallax */
.sheet-screen.behind { transform: translateX(-30%); opacity: 0.55; pointer-events: none; }
.sheet-scroll { flex: 1; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 4px 20px 20px; }
.sheet-screen.canvas .sheet-scroll { padding: 0 16px 20px; }
/* the grabber belongs to the sheet, not a screen, but a pushed screen's nav bar
   should sit right under it rather than leaving a gap where the head was */
.sheet.stacked.deep .sheet-grip { margin-bottom: 2px; }

.sheet-nav {
  flex: none; position: relative; z-index: 1;
  display: flex; align-items: center; gap: 10px;
  padding: 4px 14px 10px;
  border-bottom: 1px solid var(--hairline-soft);
  background: var(--surface);
}
.sheet-screen.canvas .sheet-nav { background: var(--bg); }
.nav-title { flex: 1; text-align: center; font-size: 16px; font-weight: 650; letter-spacing: -0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* a spacer the same width as the back button, so a title with only one side
   action still sits optically centred */
.nav-spacer, .nav-back { flex: none; width: 32px; height: 32px; }
.nav-back {
  display: flex; align-items: center; justify-content: center;
  border-radius: 999px; background: var(--fill); color: var(--good); cursor: pointer;
}
.nav-back:active { background: var(--fill-2); }
.nav-back svg { width: 17px; height: 17px; stroke-width: 2.3; }
.nav-act { flex: none; min-width: 32px; font-size: 15.5px; font-weight: 600; color: var(--good); background: none; padding: 4px 2px; cursor: pointer; text-align: right; }
.nav-act.strong { font-weight: 650; }
.nav-act:active { opacity: 0.5; }

@media (prefers-reduced-motion: reduce) {
  .sheet.stacked, .sheet-screen { transition: opacity 0.1s linear; }
  .sheet-screen.offstage { transform: none; opacity: 0; }
  .sheet-screen.behind { transform: none; }
}

/* ---- grouped lists inside a pushed screen ----
   Tighter than the app's page-level .row: these are reference rows read in a
   column, not tap targets competing with a list of transactions. */
.sh-cap {
  font-size: 12.5px; font-weight: 600; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--ink-3); padding: 20px 16px 7px;
}
.sh-note { font-size: 12.5px; color: var(--ink-3); padding: 9px 16px 0; line-height: 1.4; }
.sh-row {
  display: flex; align-items: center; gap: 12px;
  width: 100%; text-align: left; padding: 13px 16px;
  font-size: 16px; font-family: inherit; color: inherit; background: none;
}
button.sh-row { cursor: pointer; }
button.sh-row:active { background: var(--fill); }
/* A row that leaves for another app is an <a>, not a button — see sheetkit's
   note on why the element is the point. It has to be told the two things a
   button already inherits from .sh-row. */
a.sh-row { cursor: pointer; text-decoration: none; }
a.sh-row:active { background: var(--fill); }
/* the divider starts where the label does, so the group reads as one block */
.sh-row + .sh-row { position: relative; }
.sh-row + .sh-row::before {
  content: ''; position: absolute; left: 16px; right: 0; top: 0;
  height: 1px; background: var(--separator);
}
/* The label takes only what it needs and never breaks — "Price change" folding
   onto two lines to make room for its own value read like a bug. The value
   takes the rest and wraps instead, which is the half that can afford to. */
.sh-lab { flex: none; white-space: nowrap; letter-spacing: -0.01em; }
.sh-val { flex: 1; min-width: 0; color: var(--ink-3); font-variant-numeric: tabular-nums; text-align: right; }
.sh-val.ink { color: var(--ink); }
.sh-row .chev { flex: none; display: flex; width: 13px; height: 13px; color: var(--ink-3); opacity: 0.65; }
.sh-row .chev svg { width: 100%; height: 100%; stroke-width: 2.4; }
/* a whole row that is one centred action, not a label/value pair */
.sh-row.act { justify-content: center; font-size: 16px; font-weight: 600; color: var(--good); }
.sh-row.act.danger { color: var(--danger); }

/* iOS switch */
.switch {
  flex: none; width: 50px; height: 30px; border-radius: 999px;
  background: var(--fill-2); padding: 2px; cursor: pointer;
  transition: background 0.24s cubic-bezier(0.32, 0.72, 0, 1);
}
.switch i {
  display: block; width: 26px; height: 26px; border-radius: 50%;
  background: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.28);
  transition: transform 0.24s cubic-bezier(0.32, 0.72, 0, 1);
}
.switch.on { background: var(--accent-fill); }
.switch.on i { transform: translateX(20px); }
@media (prefers-reduced-motion: reduce) {
  .switch, .switch i { transition: none; }
}

@media (min-width: 700px) {
  .sheet { bottom: auto; top: 50%; left: 50%; right: auto; width: 480px; margin: 0;
    transform: translate(-50%, -46%) scale(0.97); opacity: 0; border-radius: 18px; border-bottom: 1px solid var(--hairline-soft); }
  .sheet.show { transform: translate(-50%, -50%) scale(1); opacity: 1; }
  .sheet-grip { display: none; }
}

/* ---------------------------------------------------------------- misc */

.toast {
  position: fixed; bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px) + 42px);
  left: 50%; transform: translateX(-50%) translateY(16px);
  background: var(--ink); color: var(--bg);
  border-radius: 999px; padding: 10px 18px;
  font-size: 14px; font-weight: 600; max-width: 90vw; text-align: center;
  letter-spacing: -0.01em;
  box-shadow: 0 8px 30px -8px rgba(0, 0, 0, 0.35);
  opacity: 0; pointer-events: none; transition: opacity 0.22s, transform 0.22s; z-index: 60;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }

.chart-tip {
  position: absolute; display: none; pointer-events: none; z-index: 5;
  background: var(--ink); color: var(--bg);
  border-radius: 8px; padding: 5px 10px; font-size: 12px; white-space: nowrap;
}

.empty { text-align: center; padding: 48px 24px; color: var(--ink-3); }
/* an icon, not an emoji — a sized box rather than a 40px text node */
.empty .glyph { display: block; width: 40px; height: 40px; margin: 0 auto 12px; color: var(--ink-3); }
.empty .glyph svg { width: 100%; height: 100%; }
.empty h3 { font-size: 16px; font-weight: 700; color: var(--ink-2); margin-bottom: 6px; }
.empty p { font-size: 13.5px; line-height: 1.5; max-width: 320px; margin: 0 auto; }

.spinner {
  width: 22px; height: 22px; margin: 40px auto; display: block;
  border: 2.5px solid var(--hairline); border-top-color: var(--accent);
  border-radius: 50%; animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.note { font-size: 13px; color: var(--ink-3); line-height: 1.5; padding: 10px 4px; }
.note strong { color: var(--ink-2); }
/* the same glyph inside prose, where there is no flex gap to space it: it
   rides the baseline and carries its own trailing margin */
.note-ico {
  display: inline-flex; width: 1.15em; height: 1.15em;
  vertical-align: -0.2em; margin-right: 5px;
}
.note-ico svg { width: 100%; height: 100%; }
.callout {
  background: var(--accent-soft); border-radius: var(--r-md);
  padding: 12px 14px; font-size: 13.5px; line-height: 1.5; color: var(--ink-2);
}
.callout.danger-c { background: var(--danger-soft); }
/* A callout that leads with an icon: the glyph gets its own small left box and
   the text stacks beside it. Opt-in so the callouts that are pure prose keep
   their block layout. */
.callout.with-ico { display: flex; align-items: flex-start; gap: 10px; }
.callout.with-ico > .callout-ico {
  width: 18px; height: 18px; flex-shrink: 0; margin-top: 1px;
  display: flex; color: var(--accent);
}
.callout.with-ico > .callout-ico svg { width: 100%; height: 100%; }
.callout.danger-c > .callout-ico { color: var(--danger); }
.callout.with-ico > .callout-body { flex: 1; min-width: 0; }

/* Category glyph in front of a name — pickers, rows, chips and filter chips.
   Sized in em and inheriting its colour, so one rule serves a 12.5px chip and a
   15px picker row without a scoped override at each site. The homescreen tile
   is not this: it carries its own tint and box. */
.cat-ico {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.25em; height: 1.25em; flex-shrink: 0; color: inherit;
}
.cat-ico svg { width: 100%; height: 100%; }
.mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 13px; word-break: break-all; }

.auth-wrap {
  min-height: 100dvh; display: flex; flex-direction: column;
  align-items: center; justify-content: center; padding: 24px;
}
.auth-card { width: 100%; max-width: 360px; }
.auth-logo { width: 72px; height: 72px; border-radius: 20px; margin: 0 auto 16px; display: block; }
.auth-card h1 { text-align: center; font-size: 26px; font-weight: 800; letter-spacing: -0.02em; margin-bottom: 4px; }
.auth-card .sub { text-align: center; font-size: 14px; color: var(--ink-3); margin-bottom: 24px; }

.fade-in { animation: fadeIn 0.2s ease; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; } }

/* ---------------------------------------------------------------- homescreen */

@keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes growX { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes barUp { from { transform: scaleY(0); } to { transform: scaleY(1); } }
@keyframes drawArc { from { stroke-dashoffset: var(--from, 400); } }
@keyframes popIn { from { opacity: 0; transform: scale(0.92) translateY(-6px); } to { opacity: 1; transform: none; } }

.section { animation: rise 0.55s cubic-bezier(0.16, 1, 0.3, 1) both; }

/* the salary hero: one big card, larger radius than the ordinary cards */
.hero-card {
  background: var(--surface); border: 1px solid var(--card-border);
  border-radius: var(--r-hero); box-shadow: var(--shadow);
  padding: 22px 22px 18px;
  position: relative;
}
.hero-card .headline {
  font-size: clamp(46px, 11vw, 62px); font-weight: 660;
  letter-spacing: -0.04em; line-height: 0.98; font-variant-numeric: tabular-nums;
}
.hero-card .headline-cur { font-size: 15px; font-weight: 600; color: var(--ink-3); letter-spacing: -0.01em; }

/* headline beside the ring, stacking on a phone with the ring centred rather
   than left-aligned under the number */
.hero-split { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; }
.hero-split > :first-child { flex: 1 1 210px; min-width: 0; }
@media (max-width: 480px) {
  .hero-split { justify-content: center; }
  .hero-split > :first-child { flex-basis: 100%; }
}

/* the projection donut — two arcs, spent and projected-additional */
.donut { position: relative; width: 146px; height: 146px; flex-shrink: 0; }
.donut svg { transform: rotate(-90deg); }
.donut .donut-mid {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px;
}
.donut .donut-pct { font-size: 29px; font-weight: 660; letter-spacing: -0.035em; line-height: 1; font-variant-numeric: tabular-nums; }
.donut .donut-cap { font-size: 11.5px; font-weight: 560; color: var(--ink-3); letter-spacing: 0.01em; }

/* ─────────────────────────────────────────────────── the salary cycle card

   A salary cycle is a race between money and time, and the old card never said
   so: a ring showed "% spent", a split bar showed the same spend again, and two
   dashed tiles restated the forecast in words. Three instruments, one fact.

   This is one instrument. A recessed track spans the whole salary; the fill is
   what has left it (spent, then moved, then what the forecast still expects);
   a marker shows how far *time* has come. Fill behind the marker means the
   cycle is running under pace — the comparison that used to need a sentence is
   now spatial, read in one glance and understood without a legend lookup.

   Everything else steps down hard from the headline: one verdict sentence, one
   quiet strip of figures. Nothing competes with the number that matters. */

.cyc { padding: 20px 18px 16px; }

/* Sections, not nested cards. Each group sits on one recessed surface with no
   border and no shadow of its own, so the card still reads as a single object
   with regions rather than a tray of little cards. */
.cyc-panel {
  background: var(--surface-2); border-radius: 18px; padding: 15px 16px; margin-top: 9px;
  animation: rise 360ms var(--ease-out) both;
}
/* sections arrive in reading order, 45ms apart — enough to feel deliberate,
   short enough that the whole card has settled well inside a glance */
.cyc-hero { animation: rise 360ms var(--ease-out) both; }
.cyc-panel:nth-of-type(1) { animation-delay: 60ms; }
.cyc-panel:nth-of-type(2) { animation-delay: 105ms; }
.cyc-panel:nth-of-type(3) { animation-delay: 150ms; }
.cyc-sect {
  font-size: 10.5px; font-weight: 650; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--ink-3); white-space: nowrap;
}
.cyc-sectrow { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 12px; }

/* The hero is not a panel. It sits on the card's own surface while every
   supporting group is recessed into one, so primacy comes from the layer it
   lives on rather than from a slab of colour. That also spends the green where
   it means something — the ring, the spent segment, the kept figure — instead
   of on a background that means nothing. */
.cyc-hero {
  display: flex; align-items: center; gap: 16px;
  padding: 0 2px 20px; margin-bottom: 3px;
  border-bottom: 1px solid var(--hairline-soft);
  animation: rise 360ms var(--ease-out) both;
}
.cyc-hero > .cyc-heroL { flex: 1; min-width: 0; }
.cyc-hero .donut { flex: none; }

/* Two figures sharing one surface, split by a hairline rather than a gap: they
   are one comparison, not two cards. */
.cyc-pair { display: flex; align-items: stretch; }
.cyc-pair > div { flex: 1; min-width: 0; }
.cyc-pair > div + div { border-left: 1px solid var(--hairline-soft); padding-left: 15px; margin-left: 15px; }

/* Type is sized as a set. The eyebrow takes positive tracking because small
   caps read tight; the headline takes negative tracking because large numerals
   read loose — one letter-spacing value for both would be wrong for one. */
.cyc-eyebrow {
  font-size: 11.5px; font-weight: 650; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--ink-3);
}
/* The card's own title needs air under it: two uppercase labels stacked close
   together read as one confused heading rather than a title and a section. */
.cyc-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px; }
.cyc-amount { display: flex; align-items: baseline; gap: 6px; margin-top: 7px; }
.cyc-cur { font-size: 13px; font-weight: 600; color: var(--ink-3); letter-spacing: -0.01em; }
.cyc-big {
  font-size: 44px; font-weight: 650;
  letter-spacing: -0.042em; line-height: 0.94; font-variant-numeric: tabular-nums;
}
.cyc-big.over { color: var(--danger); }
.cyc-sub {
  margin-top: 7px; font-size: 12.5px; color: var(--ink-3);
  letter-spacing: -0.005em; font-variant-numeric: tabular-nums; line-height: 1.35;
}

/* The instrument is pressable and opens the transactions behind it — the
   control sits on the thing it describes rather than beside it. Press feedback
   is deliberately smaller than a button's 0.97: the wider the surface, the less
   scale it needs to read as pressed. */
.cyc-track-btn {
  display: block; width: 100%; margin: 0; padding: 0;
  background: none; border: none; text-align: left;
  transition: transform 160ms var(--ease-out), opacity 160ms var(--ease-out);
}
.cyc-track-btn:active { transform: scale(0.985); }
@media (hover: hover) and (pointer: fine) {
  .cyc-track-btn:hover .cyc-track { box-shadow: var(--well-inset), 0 0 0 3px var(--accent-soft); }
}
.cyc-track {
  position: relative; height: 15px; border-radius: 999px;
  background: var(--well); box-shadow: var(--well-inset);
  display: flex; overflow: hidden;
  transition: box-shadow 200ms var(--ease-out);
}
/* Segments reserve their width in layout and reveal by scaleX, so the browser
   only composites — no width animation, no layout thrash per frame. */
.cyc-seg {
  height: 100%; transform-origin: left center;
  animation: growX 440ms var(--ease-out) both;
}
.cyc-seg.spent { background: var(--accent-fill); }
.cyc-seg.proj {
  background: repeating-linear-gradient(115deg,
    var(--accent-fill) 0 3px, transparent 3px 7px);
  opacity: 0.42; animation-delay: 140ms;
}
.cyc-seg.over-fill { background: var(--danger); }

/* Time. A hairline the full height of the well, bright enough to read against
   both the empty track and a filled segment it may sit on top of. */
.cyc-now {
  position: absolute; top: -3px; bottom: -3px; width: 2px; border-radius: 2px;
  background: var(--ink); box-shadow: 0 0 0 2px var(--surface);
  animation: cycNow 320ms var(--ease-out) 260ms both;
}
@keyframes cycNow { from { opacity: 0; transform: scaleY(0.4); } to { opacity: 1; transform: none; } }
/* Its own row, so the label sits under the marker without colliding with
   anything. The axis needs no numeric scale: "of 1,507 salary" is already
   stated above the track, and repeating it here would be a second answer to a
   question nobody asked twice. */
.cyc-nowrow { position: relative; height: 15px; margin-top: 7px; }
/* The entrance keyframe has to carry the centering translate itself: a
   keyframe ending at `transform: none` would win over a declared transform and
   silently knock the label half its own width off the marker. */
.cyc-nowlab {
  position: absolute; top: 0; transform: translateX(-50%);
  font-size: 11px; font-weight: 600; color: var(--ink-2); white-space: nowrap;
  animation: cycLabIn 320ms var(--ease-out) 300ms both;
}
@keyframes cycLabIn {
  from { opacity: 0; transform: translateX(-50%) translateY(6px); }
  to { opacity: 1; transform: translateX(-50%); }
}

/* One type scale for every figure below the hero, so sections differ by what
   they group rather than by inventing a new size each time. */
.cyc-statv {
  font-size: 25px; font-weight: 660; letter-spacing: -0.03em; line-height: 1.02;
  font-variant-numeric: tabular-nums;
}
.cyc-statv small { font-size: 11.5px; font-weight: 600; color: var(--ink-3); letter-spacing: 0; }
.cyc-statv.good { color: var(--accent); }
.cyc-statv.bad { color: var(--danger); }
.cyc-statk {
  margin-top: 4px; font-size: 12px; color: var(--ink-3);
  letter-spacing: -0.005em; font-variant-numeric: tabular-nums;
}

/* The ring is sized to be read, not to fill a leftover gap. At 96px the track
   leaves ~63px of clear centre, so the figure sits in air rather than against
   the stroke, and the cap below it never crowds the number. */
.donut.ring-lg .donut-mid { gap: 3px; }
.donut.ring-lg .donut-pct { font-size: 25px; font-weight: 660; letter-spacing: -0.032em; }
.donut.ring-lg .donut-pct.bad { color: var(--danger); }
.donut.ring-lg .donut-cap {
  font-size: 9.5px; font-weight: 650; color: var(--ink-3);
  text-transform: uppercase; letter-spacing: 0.07em;
}

/* Spent so far — the second thing asked for, so it leads its own section. */
.cyc-lead { display: flex; align-items: baseline; gap: 6px; }
.cyc-lead .v {
  font-size: 30px; font-weight: 660; letter-spacing: -0.033em; line-height: 1;
  font-variant-numeric: tabular-nums;
}
.cyc-lead .v.bad { color: var(--danger); }
.cyc-lead .u { font-size: 12px; font-weight: 600; color: var(--ink-3); }

/* Legend: every colour is named in text beside it, so a hue is a repeated cue
   next to a label and never the only thing telling two segments apart. */
/* Three columns, never a wrapping line of chips. Stacking the label over its
   value halves the width each item needs, so all three fit a phone at a size
   that is still comfortably readable — the fix is the layout, not a smaller
   font. Columns share the panel evenly and align on their left edge. */
/* Auto-flow columns rather than a fixed three: when a segment is clamped away
   the survivors share the width evenly instead of leaving a hole. */
.cyc-legend { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 10px; margin-top: 15px; }
.cyc-legend > div { min-width: 0; }
.cyc-legend .lbl {
  display: flex; align-items: center; gap: 5px;
  font-size: 11px; font-weight: 600; color: var(--ink-3);
  letter-spacing: 0.01em; white-space: nowrap;
}
.cyc-legend .val {
  margin-top: 3px; font-size: 15.5px; font-weight: 650; color: var(--ink);
  letter-spacing: -0.02em; font-variant-numeric: tabular-nums;
}
.cyc-legend i { width: 8px; height: 8px; border-radius: 2.5px; flex: none; }

/* The forecast, as two figures rather than a sentence.

   The old card narrated it — "On track to keep 743 OMR by 22 Aug" — which buried
   the number inside prose at 16px and made the reader parse a clause to find it.
   Stating the two figures at size, under a rule that says when they land, says
   the same thing without asking anyone to read. The tone on the second figure
   is the verdict: green kept, red short. Show, don't narrate. */
.cyc-sect em { font-style: normal; font-weight: 600; text-transform: none; letter-spacing: 0.01em; }

.cyc-support { padding: 15px 2px 2px; animation: rise 360ms var(--ease-out) 195ms both; }
/* Pace: two readings of the same habit, so they share a row and a surface. */
.cyc-pace {
  display: flex; align-items: baseline; gap: 6px 14px; flex-wrap: wrap;
  font-size: 12.5px; color: var(--ink-2);
  letter-spacing: -0.005em; font-variant-numeric: tabular-nums;
}
.cyc-pace b { font-weight: 640; color: var(--ink); }
.cyc-pace .vs { color: var(--ink-3); }
.cyc-pace .vs b { color: var(--ink-2); }

/* One row that fits the phone by construction rather than by shrinking: the
   primary takes the remaining space, the secondary takes its content, the
   overflow control is a fixed 44px square. Nothing wraps because nothing is
   allowed to grow past its share. All three keep a 44px target. */
.cyc-actions { display: flex; gap: 8px; margin-top: 16px; flex-wrap: nowrap; }
.cyc-actions .btn {
  min-height: 44px; padding: 0 14px; font-size: 14.5px;
  white-space: nowrap; min-width: 0;
}
/* Primary reads as primary by weight of surface, not by colour: green here
   would be a fourth green on the card and would say "confirm" rather than
   "open". A step up in fill is enough to rank three neutral controls. */
.cyc-actions .act-primary { flex: 1 1 auto; background: var(--fill-2); font-weight: 620; }
.cyc-actions .act-secondary { flex: 0 1 auto; }
.cyc-actions .act-more { flex: 0 0 44px; width: 44px; padding: 0; justify-content: center; }
.cyc-actions .btn svg { width: 16px; height: 16px; }
/* On the narrowest phones the labels and the icons cannot both fit. The label
   is what identifies the action, so the icon goes and the word stays whole —
   an ellipsised "Transactio…" beside a glyph reads worse than the plain word. */
@media (max-width: 360px) {
  .cyc-actions .btn { padding: 0 12px; gap: 0; }
  .cyc-actions .act-primary svg, .cyc-actions .act-secondary svg { display: none; }
}

/* Reduced motion keeps the meaning and drops the travel: things still arrive,
   they just do not move to get here. */
@media (prefers-reduced-motion: reduce) {
  .cyc-seg, .cyc-now, .cyc-panel, .cyc-hero {
    animation: cycFade 200ms ease both; animation-delay: 0ms;
  }
  /* keeps its centering translate — a plain fade would drop the transform */
  .cyc-nowlab { animation: cycFade 200ms ease both; animation-delay: 0ms; }
  .cyc-track-btn:active { transform: none; }
}
@keyframes cycFade { from { opacity: 0; } to { opacity: 1; } }

/* accounts carousel */
.acct-strip {
  display: flex; gap: 14px; overflow-x: auto;
  scroll-snap-type: x mandatory;
  padding: 2px 6px 10px; margin: 0 -6px;
  scrollbar-width: none;
}
.acct-strip::-webkit-scrollbar { display: none; }

/* The payment-card face — the unified "split plane" design.

   Everything except the lockups and the two network marks is drawn in code:
   the graphite base is a gradient on the card, the identity plane and its seam
   are clip-path polygons, and every figure on the face is live text. Nothing
   here is a flattened image, so a balance can never be baked into a picture.

   The card is its own query container, so type, padding, lockup heights and
   mark widths all scale from the card's width rather than the viewport. That
   keeps one set of proportions working from a 320px phone carousel up to the
   desktop width, which fixed px sizes could not do. cqw is used on descendants
   only — on the container itself it would resolve against the *parent*
   container. Every clamp() below is (floor, %-of-card-width, cap), and the
   percentages are the design's own figures.

   The three per-bank values arrive as custom properties from accountcard.js:
   --p0/--p58/--p100 for the plane ramp, --seam for the seam, and
   --lk-min/--lk-cq/--lk-max for that bank's lockup height. */
.acct-card {
  position: relative;
  container-type: inline-size;
  width: min(420px, 86vw); flex-shrink: 0;
  scroll-snap-align: start;
  /* ID-1, the real card ratio. Locked: never letterboxed, never cropped. */
  aspect-ratio: 85.6 / 53.98;
  /* 24px at the 420px reference width. Split into a horizontal and a vertical
     percentage because a single % on a non-square box would draw an ellipse;
     these two resolve to the same length and so scale as a circular corner. */
  border-radius: 5.71% / 9.06%;
  overflow: hidden;
  border: none; padding: 0;
  text-align: left;
  color: #f7f5f5;
  background: linear-gradient(160deg, #1b1c20 0%, #101114 100%);
  box-shadow: 0 18px 44px -14px rgba(0, 0, 0, 0.56), 0 2px 6px rgba(0, 0, 0, 0.34);
  cursor: pointer;
  transition: transform 0.18s cubic-bezier(0.32, 0.72, 0, 1);
}
/* subtle on purpose: the card is a large surface, and a big travel on one
   reads as the whole carousel lurching */
.acct-card:active { transform: scale(0.985); }

/* The last card in the carousel: the same box, none of the artwork.

   It deliberately keeps `.acct-card`'s width, aspect ratio and snap alignment
   so the strip's snap points stay even — a smaller tile would make the scroll
   stutter on the last swipe. Everything the real card paints is overridden
   back to flat surface, because there is no bank here to represent. */
.acct-card.add {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 8px;
  background: var(--surface-2);
  border: 1px dashed var(--hairline);
  box-shadow: none;
  color: var(--ink-2);
}
.acct-card.add .acct-add-plus { display: block; width: 26px; height: 26px; }
.acct-card.add .acct-add-t { font-size: 14px; font-weight: 600; }

/* Decorative layer stack, bottom to top. Each layer is inset to the card box
   and clipped by the card's own overflow, so no plane, seam or glow escapes
   the radius. */
.acct-art { position: absolute; inset: 0; pointer-events: none; }
.acct-art > i { position: absolute; inset: 0; display: block; }

/* The identity plane: a quadrilateral with a full-bleed left edge whose right
   side is a single straight seam running from 61% of the width at the top to
   49% at the bottom — a 12%-of-width lean over the full height. It is the one
   shape on the card, and the bank's colour lives nowhere else. */
.acct-art .p-plane {
  background: linear-gradient(168deg, var(--p0) 0%, var(--p58) 58%, var(--p100) 100%);
  clip-path: polygon(0 0, 61% 0, 49% 100%, 0 100%);
}
/* A 0.5%-of-width sliver riding the plane's edge. This is the only place a
   bank's secondary colour appears anywhere on the face, which is what keeps a
   two-colour identity from turning the card into a flag. */
.acct-art .p-seam {
  background: var(--seam);
  clip-path: polygon(61% 0, 61.5% 0, 49.5% 100%, 49% 100%);
}
/* restrained lighting: one soft highlight top-right, one deep corner falloff
   bottom-left, so the face has a direction without looking lacquered */
.acct-art .p-sheen {
  background:
    radial-gradient(90% 80% at 96% 4%, rgba(255, 255, 255, 0.1), transparent 58%),
    radial-gradient(70% 64% at 4% 104%, rgba(0, 0, 0, 0.42), transparent 64%);
}
.acct-card .edge {
  position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
}

/* Live content, in two columns. Padding lives here, not on the container, so
   cqw resolves against the card. */
.acct-body {
  position: relative; height: 100%;
  padding: clamp(15px, 5.2cqw, 30px);
  display: flex; gap: clamp(10px, 3cqw, 18px);
}
/* 46% of the content width: chosen so the column sits wholly inside the
   identity plane at every point along the seam, including its narrowest at
   the bottom edge. Widening it would push text across the seam. */
.acct-ident {
  width: 46%; flex-shrink: 0; min-width: 0;
  display: flex; flex-direction: column; justify-content: space-between;
}
.acct-pay {
  flex: 1; min-width: 0;
  display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between;
}

/* Height-constrained, not width-constrained: the marks differ in optical
   weight, so each bank sets its own height and they are NOT normalised. */
.acct-lockup { display: block; height: clamp(var(--lk-min), var(--lk-cq), var(--lk-max)); width: auto; }
/* the account's name, for a brand with no lockup — set at the weight the
   lockups carry so the identity band reads the same either way */
.acct-name {
  font-size: clamp(11px, 3.1cqw, 17px); font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.acct-kind {
  margin-top: clamp(6px, 1.8cqw, 10px);
  font-size: clamp(8.5px, 2.3cqw, 12px); font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.11em;
  color: rgba(247, 245, 245, 0.92);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.acct-bal-k {
  font-size: clamp(8.5px, 2.3cqw, 12px); font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.16em;
  color: rgba(247, 245, 245, 0.92);
}
.acct-bal { margin-top: clamp(2px, 1cqw, 6px); display: flex; align-items: baseline; gap: clamp(6px, 2cqw, 11px); }
.acct-bal-v {
  font-size: clamp(24px, 8.4cqw, 46px); font-weight: 600;
  letter-spacing: -0.04em; line-height: 0.94;
  font-variant-numeric: tabular-nums;
}
/* The design's type scale assumes a balance of a few thousand — its own seven
   sample cards top out at 5,900. Measured against the seam, a seven-figure
   figure runs PAST it and paints onto the graphite plane: 1,284,000 overhangs
   by 5px and 12,840,000 by 27px. So past seven characters the amount steps
   down one notch and stays inside its own plane.

   This is a deliberate departure from a spec that is otherwise reproduced
   literally, and it is inert for every realistic balance: six figures clear
   the seam by 23px, so nothing below 1,000,000 OMR is affected, and no value
   in the design itself changes size. A smaller number is a fair trade for one
   that crosses onto the payment side.

   Two steps rather than one because a single step still leaves nine figures
   over the line. Neither is reachable by an account this app has ever seen;
   they are here so that no balance can break the face. */
.acct-bal.long .acct-bal-v { font-size: clamp(19px, 6.6cqw, 36px); }
.acct-bal.longer .acct-bal-v { font-size: clamp(16px, 5.5cqw, 30px); }
.acct-bal-cur {
  font-size: clamp(9.5px, 2.6cqw, 13.5px); font-weight: 500;
  letter-spacing: 0.1em; color: rgba(247, 245, 245, 0.9);
}
.acct-pan {
  margin-top: clamp(4px, 1.4cqw, 8px);
  font-size: clamp(9px, 2.5cqw, 13px); letter-spacing: 0.16em;
  font-variant-numeric: tabular-nums; color: rgba(247, 245, 245, 0.86);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Payment furniture, on the graphite plane. */
.acct-tap { flex-shrink: 0; display: flex; color: rgba(255, 255, 255, 0.6); }
.acct-tap svg { width: clamp(16px, 4.4cqw, 26px); height: clamp(16px, 4.4cqw, 26px); }
/* Gold on every bank — there is no silver variant. The three rules are the
   contact plate's divisions, not decoration. */
.acct-chip {
  flex-shrink: 0;
  width: clamp(28px, 8cqw, 42px); height: clamp(21px, 6cqw, 32px);
  border-radius: clamp(4px, 1.2cqw, 6px);
  background: linear-gradient(135deg, #f6e6b0 0%, #d8b355 46%, #8f6f2a 100%);
  box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.3);
  display: flex; flex-direction: column; justify-content: center;
  gap: clamp(2px, 0.8cqw, 4px);
  padding: 0 clamp(4px, 1.2cqw, 7px);
}
.acct-chip i { display: block; height: 1px; background: rgba(0, 0, 0, 0.3); }

.acct-markbox {
  min-width: 0;
  display: flex; flex-direction: column; align-items: flex-end;
  gap: clamp(4px, 1.4cqw, 8px);
}
/* 19% of the card width, which is about 16mm on a real ID-1 card — inside
   Visa's minimum at every width this card is rendered at. */
.acct-scheme { display: block; width: clamp(73px, 19cqw, 112px); height: auto; flex-shrink: 0; }
/* the Mastercard symbol is two circles rather than a wordmark, so it is set
   narrower to carry the same optical weight */
.acct-scheme.mc { width: clamp(58px, 15cqw, 89px); }
/* where the figure came from — small, quiet, but never omitted */
.acct-note {
  max-width: 100%;
  font-size: clamp(8px, 2.2cqw, 11.5px); letter-spacing: 0.02em;
  color: rgba(247, 245, 245, 0.62);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* "where it went" category rows */
.cat-row {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 12px 12px 13px; border-radius: 18px;
  text-align: left; width: 100%;
  background: var(--surface-2);
  transition: background 0.22s ease, transform 0.18s cubic-bezier(0.32, 0.72, 0, 1);
  animation: rise 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.cat-row:hover { background: var(--fill); }
.cat-row:active { transform: scale(0.99); }
.cat-row.on { background: var(--fill); }
.cat-row .tile {
  width: 38px; height: 38px; flex-shrink: 0; border-radius: 12px;
  display: flex; align-items: center; justify-content: center;
}
.cat-row .tile svg { width: 17px; height: 17px; }
.cat-row .body { flex: 1; min-width: 0; }
.cat-row .head { display: flex; align-items: center; gap: 9px; }
.cat-row .nm { flex: 1; min-width: 0; font-size: 15.5px; font-weight: 560; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cat-row .amt { font-size: 15.5px; font-weight: 620; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.cat-row .pct { font-size: 12px; font-weight: 620; font-variant-numeric: tabular-nums; padding: 3px 8px; border-radius: 999px; }
.cat-row .track { display: block; margin-top: 9px; height: 5px; border-radius: 999px; background: var(--fill); overflow: hidden; }
.cat-row .track i { display: block; height: 100%; border-radius: 999px; transform-origin: left; animation: growX 0.95s cubic-bezier(0.16, 1, 0.3, 1) both; }
/* the inline "these need a category" nudge on the Uncategorized row */
.cat-review {
  display: flex; align-items: center; gap: 8px; width: 100%;
  margin-top: 11px; padding: 9px 12px; border-radius: 12px;
  border: 1px solid color-mix(in srgb, var(--warn-fill) 40%, transparent);
  background: color-mix(in srgb, var(--warn-fill) 10%, transparent);
  color: var(--warn); font-size: 13.5px; font-weight: 600; letter-spacing: -0.01em;
  transition: background 0.22s ease;
}
.cat-review:hover { background: color-mix(in srgb, var(--warn-fill) 18%, transparent); }
.cat-review svg { width: 15px; height: 15px; }

/* trends bar chart */
.trend-bars { display: flex; align-items: stretch; height: 214px; }
.trend-bars button { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 9px; }
.trend-bars .v { font-size: 11.5px; text-align: center; letter-spacing: -0.01em; font-variant-numeric: tabular-nums; transition: color 0.25s ease, opacity 0.25s ease; }
.trend-bars .track { position: relative; flex: 1; border-radius: 13px; background: var(--fill); overflow: hidden; transition: background 0.25s ease; }
.trend-bars button:hover .track { background: var(--fill-2); }
.trend-bars .track i {
  position: absolute; left: 0; right: 0; bottom: 0; border-radius: 13px;
  transform-origin: center bottom;
  animation: barUp 0.85s cubic-bezier(0.16, 1, 0.3, 1) both;
  transition: background 0.3s ease, box-shadow 0.3s ease;
}
.trend-bars .l { font-size: 12.5px; text-align: center; letter-spacing: -0.01em; transition: color 0.25s ease; }

/* anchored popover — the period picker on wide screens */
.popover {
  position: absolute; z-index: 40; width: 288px; max-width: calc(100vw - 40px);
  padding: 6px; border-radius: var(--r-md);
  background: var(--surface); border: 1px solid var(--card-border);
  box-shadow: 0 10px 40px -8px rgba(0, 0, 0, 0.28), 0 2px 6px rgba(0, 0, 0, 0.08);
  transform-origin: top left;
  animation: popIn 0.24s cubic-bezier(0.32, 0.72, 0, 1) both;
}
.popover button {
  width: 100%; display: flex; align-items: center; gap: 10px;
  padding: 9px 10px; border-radius: var(--r-sm); text-align: left;
  transition: background 0.16s ease; min-height: 40px;
}
.popover button:hover { background: var(--fill); }
.popover button:disabled { opacity: 0.45; pointer-events: none; }
.popover .tick { width: 18px; display: flex; color: var(--accent); }
.popover .tick svg { width: 16px; height: 16px; }
.popover .lbl { flex: 1; font-size: 15px; letter-spacing: -0.01em; }
.popover .hint { font-size: 13px; color: var(--ink-3); font-variant-numeric: tabular-nums; }

/* "worth knowing" cards */
.insight-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 12px; }
.insight-card {
  background: var(--surface); border: 1px solid var(--card-border);
  border-radius: var(--r-lg); box-shadow: var(--shadow);
  padding: 17px 19px; display: flex; gap: 13px; align-items: flex-start;
}
.insight-card .ico {
  width: 32px; height: 32px; border-radius: var(--r-sm); flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  background: var(--accent-soft); color: var(--accent);
}
.insight-card .ico svg { width: 17px; height: 17px; }
.insight-card .txt { font-size: 15px; line-height: 1.45; color: var(--ink-2); letter-spacing: -0.01em; }
.insight-card .txt strong { color: var(--ink); font-weight: 560; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}

::selection { background: var(--accent-soft); }

/* ------------------------------------------------------------------ Recurring tab
   Append to public/style.css. Every colour is an existing token, so the light
   theme follows automatically and test/contrast.test.js keeps passing. */

/* section header with an optional trailing pill / count */
.sec-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 18px 0 10px; }
.sec-head h2 { font-size: 20px; font-weight: 680; letter-spacing: -0.02em; margin: 0; }
.sec-count { font-size: 13px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.btn.pill { border-radius: 999px; }

/* group title carrying a right-hand subtotal */
.group-title.spread { display: flex; align-items: baseline; justify-content: space-between; }
.group-title .gt-sub { font-weight: 600; color: var(--ink-3); font-variant-numeric: tabular-nums; }

/* ---- hero: what this month is committed to, and how much of it has gone ----
   One figure, then the split. The card used to state the total three times — as
   the headline, as a percentage, and again as "22.000 OMR of 80.800 OMR" — and
   then split paid-vs-upcoming across both a bar and a two-up stat row. Now the
   total is said once and the bar is the only place the split lives. */
.rec-hero { border-radius: var(--r-hero); padding: 17px 18px 16px; }
.rec-hero .hero-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
/* an eyebrow, not a title: the page heading already says "Recurring", so this
   only has to name the month the figures below belong to */
.rec-hero .hero-eyebrow {
  font-size: 11.5px; font-weight: 650; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--ink-3);
}

/* the count doubles as the way into the full list, so it is a real control */
.active-pill {
  flex: none; display: inline-flex; align-items: center; gap: 1px;
  font-size: 13px; font-weight: 620; letter-spacing: -0.01em; font-family: inherit;
  color: var(--good); background: var(--fill); border: none;
  border-radius: 999px; padding: 6px 8px 6px 12px; cursor: pointer;
}
.active-pill:active { background: var(--fill-2); }
.active-pill .ap-chev { display: flex; width: 15px; height: 15px; opacity: 0.8; }
.active-pill .ap-chev svg { width: 100%; height: 100%; stroke-width: 2.4; }

.rec-hero .hero-amount { display: flex; align-items: baseline; gap: 7px; margin-top: 14px; font-size: 44px; font-weight: 660; letter-spacing: -0.042em; line-height: 1; font-variant-numeric: tabular-nums; }
.rec-hero .ha-cur { font-size: 15px; font-weight: 620; letter-spacing: -0.01em; color: var(--ink-3); }
.rec-hero .hero-amount-sub { font-size: 13.5px; color: var(--ink-3); margin-top: 7px; }

/* progress: the bar leads and the two figures label its ends, so paid and
   remaining are read off the thing that already shows the ratio */
.hero-prog { margin-top: 18px; }
.rec-hero .rec-bar { height: 14px; border-radius: 999px; background: var(--well); box-shadow: var(--well-inset); overflow: hidden; }
.rec-hero .rec-bar i { display: block; height: 100%; border-radius: 999px; background: var(--accent-fill); }
.hp-labels { display: flex; justify-content: space-between; gap: 10px; margin-top: 9px; font-size: 12px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.hp-labels b { font-weight: 640; color: var(--ink-2); }

/* next payment — no longer a well of its own. The card is already one thing;
   boxing its last row made it read as two, so a hairline does the separating
   and the row sits in the card's own padding. */
.next-pay {
  display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
  margin-top: 16px; padding: 14px 0 0;
  background: none; border: none; border-top: 1px solid var(--hairline-soft);
  color: inherit; font: inherit; cursor: pointer;
}
/* no surface left to tint, and a transform would shift the row against the
   hairline it sits under — so the press reads as a dim instead */
.next-pay:active { opacity: 0.55; }
/* size comes from .avatar.sm on the element — see views/recurring.js */
.np-main { flex: 1; min-width: 0; }
.np-name { font-size: 15px; font-weight: 620; letter-spacing: -0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.np-when { font-size: 12.5px; color: var(--ink-3); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.np-lead + .np-rel::before { content: ' · '; }
.np-amt { flex: none; font-size: 15.5px; font-weight: 660; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.np-chev { flex: none; display: flex; width: 16px; height: 16px; color: var(--ink-3); opacity: 0.8; }
.np-chev svg { width: 100%; height: 100%; stroke-width: 2.2; }

/* Below ~350px the name and figure alone fill the row. Must sit after the
   .np-* rules above — same specificity, so source order decides. */
@media (max-width: 349px) {
  .next-pay { gap: 8px; }
  .np-amt { font-size: 14.5px; }
  /* "in 3 days" answers the question; the lead-in is the half to lose */
  .np-lead, .np-lead + .np-rel::before { display: none; }
}

/* ---- upcoming: horizontal cards ---- */
.up-strip { display: flex; gap: 10px; overflow-x: auto; scroll-snap-type: x proximity; padding: 2px 16px 4px; margin: 0 -16px; -webkit-overflow-scrolling: touch; }
.up-strip::-webkit-scrollbar { display: none; }
.up-card {
  flex: none; width: 138px; scroll-snap-align: start; text-align: left;
  background: var(--surface); border: 1px solid var(--card-border); border-radius: var(--r-lg);
  box-shadow: var(--shadow); padding: 14px; color: inherit; font: inherit; cursor: pointer;
}
.up-card:active { transform: scale(0.985); }
/* size comes from .avatar.sm on the element — see views/recurring.js */
.up-amount { margin-top: 12px; font-size: 20px; font-weight: 680; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; }
.up-amount span { font-size: 11px; font-weight: 600; color: var(--ink-3); margin-left: 4px; }
.up-days { margin-top: 3px; font-size: 12.5px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.up-days.soon { color: var(--good); font-weight: 600; }
.up-name { margin-top: 10px; font-size: 14.5px; font-weight: 620; letter-spacing: -0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* days-left chip in the expanded upcoming rows */
.days-chip { flex: none; width: 46px; display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 5px 0; border-radius: 12px; background: var(--surface-2); }
.days-chip b { font-size: 17px; font-weight: 700; line-height: 1; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.days-chip i { font-size: 9px; font-weight: 600; letter-spacing: 0.06em; font-style: normal; color: var(--ink-3); }
.days-chip.soon { background: var(--good-soft); }
.days-chip.soon b { color: var(--good); }

.callout.total-c { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 12px; font-variant-numeric: tabular-nums; }

/* ---- all recurring: a two-column tile grid ----
   A full-width row spent most of itself on empty middle, and gave the logo —
   the fastest way to recognise a subscription — the smallest part of the line.
   A tile turns that on its head: the mark leads, the cost is the second thing
   read, and twice as many fit before a scroll. */
.rec-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 11px; }
.rec-tile {
  display: flex; flex-direction: column; align-items: flex-start; text-align: left;
  width: 100%; min-height: 138px;
  background: var(--surface); border: 1px solid var(--card-border); border-radius: var(--r-lg);
  box-shadow: var(--shadow); padding: 13px 13px 14px; color: inherit; font: inherit; cursor: pointer;
}
.rec-tile:active { transform: scale(0.985); }
.rt-top { display: flex; align-items: center; justify-content: space-between; gap: 7px; width: 100%; }
/* The chip rides at tile scale — smaller and upper-case, so it reads as a label
   on the logo rather than a second piece of content competing with the name.
   Block, not the chip's usual inline-flex, so a cadence longer than the column
   ellipses instead of pushing the logo out of the tile.
   9.5px, not the design's 10: beside a 54px logo the column leaves ~81px, and
   "QUARTERLY" and "TOMORROW" both overrun that at 10px. Half a pixel is
   imperceptible; a truncated cadence is not. */
.rec-tile .chip {
  display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis;
  font-size: 9.5px; font-weight: 660; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--ink-3); padding: 4px 7px;
}
/* pushed to the bottom, so tiles line up across the row whatever the logo does */
.rt-body { margin-top: auto; padding-top: 16px; width: 100%; }
.rt-name { font-size: 14.5px; font-weight: 620; letter-spacing: -0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rt-amt { margin-top: 5px; font-size: 19px; font-weight: 680; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; }
.rt-amt span { font-size: 10.5px; font-weight: 600; color: var(--ink-3); margin-left: 4px; }
.rt-meta { margin-top: 3px; font-size: 11.5px; color: var(--ink-3); }
/* paused: dimmed, never hidden — the word "Paused" in the chip is what says so,
   the opacity only ranks it below the live ones */
.rec-tile.paused .avatar { opacity: 0.45; }
.rec-tile.paused .rt-body { opacity: 0.55; }

/* Below the 393px this was drawn at, the chip column loses the few pixels the
   longest labels need — at 375px "QUARTERLY" wants 77px and has 74. Shrinking
   the logo buys them back; dropping to one column would double the scroll to
   fix three pixels. */
@media (max-width: 389px) {
  .rec-tile { padding: 12px 12px 13px; }
  /* two tiles cannot hold a 56px mark and a cadence chip below 390px — drop to
     the default step rather than inventing a ninth size */
  .rec-tile .avatar.tile { width: 44px; height: 44px; border-radius: 13px; font-size: 15px; }
  .rec-tile .avatar.tile img, .rec-tile .avatar.tile .mark { padding: 6px; }
  .rec-tile .chip { padding: 4px 6px; }
}

/* Below ~350px a two-column tile leaves the chip so little room that even
   "MONTHLY" truncates, so the grid goes to one column — which is what this
   list was before the redesign, and no worse. The tile then hugs its content
   instead of holding the 138px height it needs to line up beside a neighbour. */
@media (max-width: 349px) {
  .rec-grid { grid-template-columns: 1fr; }
  .rec-tile { min-height: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .up-card:active, .rec-tile:active { transform: none; }
}

/* ---- the item detail flow (screens 4a–4d) ----
   4a answers the four questions worth interrupting for — what it is, what it
   costs, when it goes next, and whether the price moved — and hands the rest
   to a pushed screen. Everything here is scoped to the sheet, so the page
   keeps its own vocabulary. */
.sh-half { padding: 14px 0 4px; }
.sh-id { display: flex; align-items: center; gap: 14px; }
.sh-id-main { min-width: 0; }
.sh-name { display: flex; align-items: center; gap: 8px; font-size: 22px; font-weight: 680; letter-spacing: -0.027em; }
.sh-paused {
  flex: none; font-size: 11.5px; font-weight: 640; letter-spacing: 0.05em;
  text-transform: uppercase; color: var(--ink-3);
  background: var(--fill); border-radius: 999px; padding: 3px 8px;
}
.sh-sub { margin-top: 3px; font-size: 15px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.sh-sub b { font-weight: 400; color: var(--ink-2); }

/* the one thing most likely to be why the sheet was opened */
.sh-next { display: flex; align-items: center; gap: 13px; margin-top: 20px; padding: 15px 16px; background: var(--good-soft); border-radius: var(--r-lg); }
.sh-next-icon {
  flex: none; width: 38px; height: 38px; border-radius: 12px;
  display: flex; align-items: center; justify-content: center;
  background: var(--accent-soft); color: var(--good);
}
.sh-next-icon svg { width: 20px; height: 20px; }
.sh-next-t { font-size: 15.5px; font-weight: 620; letter-spacing: -0.01em; }
/* --good, not a lightened green: a paler tone measured 2.9:1 on this tint */
.sh-next-s { margin-top: 2px; font-size: 13px; color: var(--good); font-variant-numeric: tabular-nums; }

.sh-ctx { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 16px; font-size: 14.5px; color: var(--ink-3); }
.sh-rise, .sh-badge {
  flex: none; font-size: 12.5px; font-weight: 620; color: var(--danger);
  background: var(--danger-soft); border-radius: 999px; padding: 2px 9px;
  font-variant-numeric: tabular-nums;
}
.sh-first { flex: none; font-size: 12.5px; color: var(--ink-3); }

.sh-more {
  display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;
  margin-top: 18px; padding: 15px 16px;
  background: var(--surface-2); border-radius: var(--r-lg);
  color: inherit; font: inherit; cursor: pointer;
}
.sh-more:active { background: var(--fill); }
.sh-more > div { flex: 1; min-width: 0; }
.sh-more-t { font-size: 15.5px; font-weight: 600; letter-spacing: -0.01em; }
.sh-more-s { margin-top: 2px; font-size: 12.5px; color: var(--ink-3); }
.sh-more .chev { flex: none; display: flex; width: 15px; height: 15px; color: var(--ink-3); opacity: 0.7; }
.sh-more .chev svg { width: 100%; height: 100%; stroke-width: 2.3; }

/* the quietest way out of the sheet, below the two real actions */
.sh-end {
  display: block; width: 100%; margin-top: 4px; padding: 12px;
  font-size: 15px; font-family: inherit; color: var(--danger);
  background: none; cursor: pointer;
}
.sh-end:active { opacity: 0.55; }

/* 4b: what the pushed screen opens with */
.sh-summary { display: flex; align-items: center; gap: 13px; margin-top: 14px; padding: 14px 16px; background: var(--surface); border: 1px solid var(--card-border); border-radius: var(--r-lg); box-shadow: var(--shadow); }
.sh-sum-t { font-size: 17px; font-weight: 650; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.sh-sum-s { margin-top: 2px; font-size: 13.5px; color: var(--ink-3); }

/* 4c's total — what this merchant has taken — is the kit's `md` amount step
   inside a padded card now, so the four rules that drew it here are gone. It
   was the only 32px figure in the app, which is exactly the drift the kit
   exists to absorb. */

/* a payment row: the row opens the transaction, the trailing control flags it
   out of the pattern — two targets, so neither is nested in the other */
.sh-row.pay { padding: 0; gap: 0; }
.sh-row.pay .pay-main {
  flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px;
  padding: 13px 4px 13px 16px; font: inherit; color: inherit; background: none; cursor: pointer; text-align: left;
}
.sh-row.pay .pay-main:active { background: var(--fill); }
/* the date leads, the amount stays on one line at the end, and the badge sits
   between them — the amount wrapping under its own badge looked broken */
.sh-row.pay .sh-val { flex: none; margin-left: auto; white-space: nowrap; }
.sh-row.pay .sh-badge, .sh-row.pay .sh-first { margin-left: 2px; }
/* quiet: eight of these down a column should not outshout the payments they
   sit beside, so it stays ink until pressed */
.pay-x {
  flex: none; align-self: stretch; padding: 0 14px 0 10px;
  font-size: 12.5px; font-weight: 560; font-family: inherit;
  color: var(--ink-3); background: none; cursor: pointer;
}
.pay-x:active { color: var(--good); opacity: 0.6; }
.sh-row.pay.out .pay-main { opacity: 0.5; }
.pay-out { font-style: normal; font-size: 12.5px; color: var(--ink-3); margin-left: 7px; }

/* 4d: a label column beside each control, so the fields read as a list */
.sh-icon-block { display: flex; justify-content: center; padding: 18px 0 16px; }
.fr { display: flex; align-items: center; gap: 12px; padding: 9px 16px; min-height: 52px; }
.fr + .fr, .fr + .sh-row, .sh-row + .fr { position: relative; }
.fr + .fr::before, .fr + .sh-row::before, .sh-row + .fr::before {
  content: ''; position: absolute; left: 16px; right: 0; top: 0;
  height: 1px; background: var(--separator);
}
.fr-lab { flex: none; width: 104px; font-size: 16px; color: var(--ink-3); letter-spacing: -0.01em; }
.fr-val { flex: 1; min-width: 0; display: flex; justify-content: flex-end; }
/* the shared amount/cadence controls bring their own labels, so they take the
   whole row rather than sitting in the value column */
.fr.fr-block { display: block; padding: 12px 16px; }
.fr.fr-block .field:last-child { margin-bottom: 0; }
.fr-input {
  width: 100%; text-align: right; font-family: inherit; font-size: 16px; font-weight: 520;
  color: var(--ink); background: none; border: none; outline: none; padding: 4px 0;
  min-height: 32px;
}
.fr-val select.fr-input { text-align: right; text-align-last: right; }
.fr-input::placeholder { color: var(--ink-3); }
.fr-val .field { margin-bottom: 0; width: 100%; }

/* ---- the sheet kit's own pieces (public/js/sheetkit.js) ----
   Drawn to the transaction-sheet design: an identity block over one large
   figure, then flat groups of rows. The promoted rules above already carry the
   row and label/value vocabulary, so these are only the parts the design adds.

   Everything here takes margin-top and never margin-bottom. stackSheet's
   contentHeight() sums each direct child's height plus BOTH its vertical
   margins, so two adjacent siblings whose margins collapse would be measured
   as the sum of a gap the browser renders as the maximum — and the half detent
   would open taller than its content. */

/* The quick sheet's name sits above a 40px figure that carries the emphasis;
   the recurring sheet's identity is itself the headline and keeps its 22px. */
.sh-id.compact .sh-name { font-size: 19px; font-weight: 620; letter-spacing: -0.027em; }
.sh-name-t { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* the one big figure, and the quiet lines around it */
.sh-amt {
  margin-top: 20px; font-size: 40px; font-weight: 640; letter-spacing: -0.035em;
  font-variant-numeric: tabular-nums; color: var(--ink);
}
.sh-amt.in { color: var(--good); }
.sh-amt-cur { font-size: 16px; font-weight: 600; color: var(--ink-3); letter-spacing: -0.01em; }
.sh-amt-cap { margin-top: 4px; font-size: 14.5px; color: var(--ink-3); font-variant-numeric: tabular-nums; }

/* The second and last size: a total stated inside a card, where 40px would
   crowd the walls. Heavier than the large step rather than lighter, because at
   32px the same weight reads thinner than it measures. */
.sh-amt.md { margin-top: 6px; font-size: 32px; font-weight: 680; line-height: 1; }
.sh-amt.md .sh-amt-cur { font-size: 14px; font-weight: 620; }
.sh-amt.md + .sh-amt-cap { margin-top: 6px; font-size: 12.5px; }
/* the line above the figure — what the number counts, before it is stated */
.sh-amt-eye { font-size: 12.5px; color: var(--ink-3); }

/* A flat group sits on a fill rather than a card, so a sheet reads as one
   surface with a list on it. A caption already supplies the gap above. */
.sh-group { margin-top: 18px; background: var(--surface-2); border-radius: var(--r-md); overflow: hidden; }
/* both of these already end in space of their own — a caption in its padding,
   the edit screen's avatar block in its */
.sh-cap + .sh-group, .sh-icon-block + .sh-group { margin-top: 0; }

/* The raised shape — what a pushed screen uses, where the sheet is a canvas
   and each group has to lift off it. Same rules `.group` carries on a page. */
.sh-group.card {
  background: var(--surface); border: 1px solid var(--card-border);
  border-radius: var(--r-lg); box-shadow: var(--shadow);
}
/* Padding belongs to a row, not to the group holding it — a padded group would
   inset the separators away from the edge. A group holding a figure instead of
   rows has nothing to supply it, so it asks. */
.sh-group.pad { padding: 16px 18px 17px; }

/* An action row: one label leading, a chevron trailing. The label takes the
   width here — the opposite of a label/value row, where the value does. */
.sh-row.nav { gap: 11px; padding: 14px 16px; font-size: 15.5px; letter-spacing: -0.01em; }
.sh-row.nav .sh-lab { flex: 1; min-width: 0; white-space: normal; }
.sh-row.nav.danger .sh-lab { color: var(--danger); }
.sh-row.nav .chev { width: 16px; height: 16px; opacity: 1; }
.sh-row.nav .chev svg { stroke-width: 1.8; }

/* A leading glyph on an action row. Sized in em so one rule serves the 15.5px
   nav row and the 16px label/value row, and it inherits its colour so a danger
   row tints its glyph without a second selector. */
.sh-row-ico {
  flex: none; display: flex; width: 1.15em; height: 1.15em;
  color: var(--ink-3);
}
.sh-row-ico svg { width: 100%; height: 100%; }
.sh-row.danger .sh-row-ico { color: var(--danger); }

/* A kit callout brings its own top margin and never a bottom one, so where a
   stack of them is followed by the app's form fields — the editor — the gap
   below is the field's to take. Scoped to that adjacency: `.field` is app-wide
   and keeps its own spacing everywhere else. */
.sh-callout { margin-top: 14px; }
.sh-callout + .field { margin-top: 14px; }
/* `warn` has no soft tint of its own — --warn-fill is a bar colour, not a
   background for 13.5px text — so it takes the neutral fill and says its piece
   through the glyph. */
.callout.warn-c { background: var(--fill); }
.callout.warn-c > .callout-ico { color: var(--warn); }

/* ── Added by dustybanker-app ──────────────────────────────────────────────
   Everything above this line is DustyBankers/public/style.css verbatim. The
   rules below exist only here, and only for screens the reference does not
   have: Google sign-in, and the four states public/js/views/states.js renders.
   Keeping them at the end rather than merged in is what makes the copy above
   diffable against the reference in one command. */

/* Google's mark keeps its own four colours on both themes — it is a trademark,
   not an icon in our palette — so the button sits on a surface, not on the
   accent, and the mark is not tinted by currentColor. */
.btn.google { background: var(--surface); border-color: var(--hairline); color: var(--ink); }
.btn.google:hover { background: var(--surface-2); }

.note.center { text-align: center; }
.callout.neutral { background: var(--fill); }

.stack { display: flex; flex-direction: column; gap: 10px; }

/* the signed-in identity block: the email is the whole content, so it gets the
   weight rather than a label above it */
.identity {
  background: var(--surface); border: 1px solid var(--card-border);
  border-radius: var(--r-md); box-shadow: var(--shadow);
  padding: 16px; text-align: center; margin-bottom: 18px;
}
.identity .label { font-size: 11.5px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--ink-3); }
.identity .value { font-size: 16px; font-weight: 600; margin-top: 4px; word-break: break-all; }

/* the status glyph above a refusal or an outage — a sized box, not an emoji */
.state-glyph {
  display: block; width: 44px; height: 44px; margin: 0 auto 14px;
  color: var(--ink-3);
}
.state-glyph svg { width: 100%; height: 100%; }
.state-glyph.danger-g { color: var(--danger); }
.state-glyph.warn-g { color: var(--warn); }

/* The sliding tab pill is sized from the tab count, not from a literal 6.

   The rule above is the reference's, and it reads
   `width: calc((100% - 12px) / 6)` — correct there, where TABS has exactly six
   entries and app.js's movePill positions the pill in sixths to match. The two
   sixes are the same number written twice, in two languages, with nothing
   holding them together.

   This app ships four tabs (plan, more and importpdf are not ported), so the
   literal would draw a pill one sixth wide and slide it in quarters: too narrow,
   and further off the tab it names with every step right. app.js sets
   --tab-count on .tabbar-inner from TABS.length, so the width and the travel now
   come from one number. The fallback keeps the reference's value if it is ever
   rendered without the property. */
.tab-pill { width: calc((100% - 12px) / var(--tab-count, 6)); }


/* ══════════════════════════════════════════════════════════════════════════
   THE PLAN TAB
   ══════════════════════════════════════════════════════════════════════════

   Ported from ~/Developer/plan-design/plan-v1.3.html. **The authority is
   `plan-v2.html`**, and has been since the forecast overhaul began on
   2026-09-01 — this comment named v1.3 until 2026-09-05, which is a bad line to
   get wrong precisely because it tells you where to look instead of reading
   here. Class names and structure are still the design's, unchanged, so a
   question about why something is 13px is answered by opening v2.

   Every colour is a token. The two rgba() shadows on the slider thumb are the
   only literals, and they are shadows rather than text or surface colours —
   the same form the tab badge and the segmented control already use in this
   file. Nothing here names a colour that does not follow the theme.
   ══════════════════════════════════════════════════════════════════════════ */

/* ================= the design's SECOND style block =================

   v1.3 carries its plan CSS in two <style> elements and only the first was
   ported. Everything below came from the second, and its absence was visible:
   the overall-budget card rendered as a UA-styled centred-text button, the
   budget tick could never be drawn, the chevron was oversized, and the word
   "suggested" behind the ✦ glyph had nowhere to hide.
   ================================================================= */

/* Every pw- component that toggles with [hidden] also declares its own
   display, and an author display beats the UA's [hidden]{display:none} — so
   without this rule a hidden gain chip renders "+0.000". */
[hidden] { display: none !important; }

/* screen-reader-only text — carries the word "suggested" behind the ✦ glyph */
.vh {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* a whole card as one tap target (the overall-budget card) */
.pw-cardbtn { display: block; width: 100%; text-align: left; font: inherit; color: inherit; cursor: pointer; }
.pw-cardbtn:active { transform: scale(0.99); }
.pw-cardchev { flex: none; display: flex; width: 15px; height: 15px; color: var(--ink-3); opacity: .7; }
.pw-cardchev svg { width: 100%; height: 100%; stroke-width: 2.3; }

/* the budget marker on the overall bar — where the budget ends when the
   expected total runs past it. `.pw-bar { position: relative }` is already
   declared in section 1a below, which is what this absolute box is measured
   against. */
.pw-budtick { position: absolute; top: -3px; bottom: -3px; width: 2px; border-radius: 2px; background: var(--ink); opacity: .8; }

/* the app's coarse-pointer hitSlop rule, extended to this screen's compact
   controls (same ::after overlay technique as the Touch targets block above) */
@media (pointer: coarse) {
  .pw-datebtn, .pw-acct, .pw-plangain, .pw-info { position: relative; }
  .pw-datebtn::after, .pw-acct::after, .pw-plangain::after, .pw-info::after {
    content: ''; position: absolute; left: 0; right: 0; top: 50%;
    transform: translateY(-50%); height: 44px;
  }
}

/* ================= shared ================= */
.pw-card { padding: 16px 18px 15px; }
.pw-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.pw-eyebrow { font-size: 11.5px; font-weight: 650; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-3); }
.pw-state { font-weight: 650; }
.pw-state.good { color: var(--good); }
.pw-state.warn { color: var(--warn); }
.pw-state.over { color: var(--danger); }

/* ================= 1a. the plan (budget ledger) ================= */
.pw-bar { --seg: var(--accent-fill); position: relative; height: 13px; margin-top: 15px; }
.pw-fill { height: 100%; border-radius: 999px; display: flex; overflow: hidden; background: var(--well); box-shadow: var(--well-inset); }
.pw-fill i { height: 100%; display: block; }
.pw-spent { background: var(--seg); }
.pw-due { background: repeating-linear-gradient(115deg, var(--seg-hatch, var(--seg)) 0 3px, transparent 3px 7px); opacity: .9; }
.st-warn .pw-bar { --seg: var(--warn-fill); --seg-hatch: var(--warn); }
.st-over .pw-bar { --seg: var(--danger); }

/* The three numbers, each with a name that cannot be mistaken for another.
   The V1 draft showed 132.900 and 257.900 both labelled "Budget left". */
.pw-three { display: flex; margin-top: 16px; }
.pw-three > div { flex: 1; min-width: 0; }
.pw-three > div + div { border-left: 1px solid var(--hairline-soft); padding-left: 13px; margin-left: 13px; }
.pw-k { font-size: 10.5px; font-weight: 650; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-3); line-height: 1.3; }
.pw-v { margin-top: 4px; font-size: 18px; font-weight: 680; letter-spacing: -0.025em; font-variant-numeric: tabular-nums; }
.pw-v.hero { font-size: 22px; color: var(--accent); }
.pw-v.bad { color: var(--danger); }
.pw-v.unknown { color: var(--ink-3); font-weight: 600; }
.pw-s { margin-top: 3px; font-size: 11px; color: var(--ink-3); font-variant-numeric: tabular-nums; line-height: 1.35; }

.pw-verdict {
  display: block; line-height: 1.45; margin-top: 14px; padding-top: 13px;
  border-top: 1px solid var(--hairline-soft);
  font-size: 13px; color: var(--ink-2); font-variant-numeric: tabular-nums;
}
.pw-verdict b { font-weight: 650; color: var(--ink); }
/* ── The verdict's four states ────────────────────────────────────────────
   Three against the stated buffer, plus the HOLD — the state this build ships
   in, where the everyday estimate is withheld and there is no low point to be
   safe or unsafe about. The hold is deliberately the quietest of the four: it
   is an admission, and a bordered coloured panel would make an absence read as
   an alert. Colour is never the only cue — every state leads with its own
   word ("Safe.", "Too close to call.", "Not safe.", "No low point yet."). */
.pw-vlead { font-weight: 680; color: var(--good); }
.pw-vlead.warn { color: var(--warn); }
.pw-vlead.over { color: var(--danger); }
.pw-verdict.st-hold .pw-vlead { color: var(--ink-2); }
/* ── Withheld: an estimate the app declines to make ──────────────────────
   Italic and quiet, never a gap and never a zero. Used wherever a figure would
   have gone if the engine had an opinion — the cycle card's panel, the budget
   card's reason line, the method sheet's opening paragraph. */
.pw-withheld { color: var(--ink-3); font-style: italic; }
.cyc-hold { margin-top: 10px; font-size: 12.5px; line-height: 1.5; }
.pw-foot { margin-top: 10px; font-size: 11.5px; color: var(--ink-3); line-height: 1.5; }

/* ── Disclosure lines ─────────────────────────────────────────────────────
   What a screen knows and is not showing. Deliberately quiet — no chip, no
   border, no colour: these are admissions, and anything that made them look
   like badges would make an omission read as a feature. They sit directly
   under the figure they qualify, which is the only placement that makes them
   do any work. */
.pw-disclose, .rec-disclose {
  margin: 9px 2px 0; font-size: 11.5px; line-height: 1.5; color: var(--ink-3);
}
.pw-disclose small, .rec-disclose small { display: block; margin-top: 2px; color: var(--ink-3); }
.rec-disclose { margin: 12px 0 0; }
.rec-disclose + .rec-disclose { margin-top: 6px; }

/* ================= 1b. balance forecast ================= */
.pw-acct {
  flex: none; display: inline-flex; align-items: center; gap: 5px;
  font-size: 12.5px; font-weight: 620; letter-spacing: -0.01em; font-family: inherit;
  color: var(--ink-2); background: var(--fill); border: none;
  border-radius: 999px; padding: 6px 11px; cursor: pointer; min-height: 32px;
}
.pw-acct:active { background: var(--fill-2); }
.pw-acct .chev { width: 13px; height: 13px; display: flex; opacity: .7; }

/* provenance: where the anchor came from and when. The forecast is only ever
   drawn forward from a balance the bank actually stated. */
.pw-anchor {
  display: flex; align-items: center; gap: 8px; margin-top: 12px;
  padding: 9px 11px; border-radius: 11px; background: var(--surface-2);
  font-size: 11.5px; color: var(--ink-3); font-variant-numeric: tabular-nums; line-height: 1.4;
}
.pw-anchor b { font-weight: 650; color: var(--ink-2); }
.pw-anchor svg { width: 14px; height: 14px; flex: none; opacity: .8; }

.pw-amount { display: flex; align-items: baseline; gap: 7px; margin-top: 14px; }
.pw-cur { font-size: 13px; font-weight: 600; color: var(--ink-3); }
.pw-big { font-size: 38px; font-weight: 650; letter-spacing: -0.04em; line-height: 1; font-variant-numeric: tabular-nums; }
.pw-big.bad { color: var(--danger); }
.pw-big .dec { font-size: 0.58em; opacity: .8; }
.pw-sub { margin-top: 6px; font-size: 12.5px; color: var(--ink-3); line-height: 1.4; }

/* ---- the date control: the displayed date IS the picker ----
   Tapping it opens a calendar sheet; the slider below stays for scrubbing.
   Two ways in, one selection. */
.pw-datebtn {
  display: inline-flex; align-items: center; gap: 7px;
  margin-top: 5px; min-height: 36px; padding: 5px 12px 5px 10px;
  font-size: 13px; font-weight: 620; font-family: inherit; letter-spacing: -0.01em;
  color: var(--ink-2); background: var(--fill); border: none; border-radius: 999px;
  cursor: pointer; font-variant-numeric: tabular-nums;
}
.pw-datebtn:active { background: var(--fill-2); }
.pw-daterow { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 5px; }
.pw-daterow .pw-datebtn { margin-top: 0; }
.pw-daterow .pw-plangain { margin-left: 0; white-space: nowrap; }
.pw-datebtn svg { width: 14px; height: 14px; opacity: .85; }
.pw-datebtn .chev { display: flex; width: 12px; height: 12px; opacity: .7; }
.pw-datebtn .chev svg { width: 100%; height: 100%; }

/* the one provenance line the card keeps — everything longer lives in the ⓘ */
.pw-from {
  display: flex; align-items: center; gap: 6px;
  margin-top: 9px; font-size: 11.5px; color: var(--ink-3);
  font-variant-numeric: tabular-nums; line-height: 1.4;
}
.pw-from b { font-weight: 650; color: var(--ink-2); }
.pw-info {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; border-radius: 999px; padding: 0;
  color: var(--ink-3); background: var(--fill); border: none; cursor: pointer;
  position: relative;
}
.pw-info::after { content: ''; position: absolute; inset: -10px; }
.pw-info svg { width: 13px; height: 13px; }
.pw-info:active { background: var(--fill-2); }

/* ---- the chart: one continuous line, not 12 bars ----
   An area chart reads "where is this heading" the way bars never did. Drawn as
   inline SVG from the same day model the readouts use, so the picture cannot
   disagree with the numbers. The selected day is a hairline + dot; salary and
   bill days sit as dots ON the line; the dashed second line appears only when
   a manual budget actually bends the future. Scrub by touch, or use the
   slider below — the svg is a pointer surface, the range input keeps the
   keyboard and screen-reader semantics. */
.pw-chart { position: relative; margin-top: 16px; height: 138px; }
/* pan-y, not none: a vertical swipe starting on the chart must still scroll
   the page — the chart only claims horizontal scrubbing */
.pw-chart svg { display: block; width: 100%; height: 100%; touch-action: pan-y; cursor: pointer; }
/* The <svg> is the ONLY thing that hears input. Everything drawn inside it is
   decoration, and any one of them left interactive swallows the scrub the
   moment a finger lands on it — and the selected dot is exactly where a finger
   lands, because it is the thing that looks draggable. A blanket rule rather
   than a per-element audit: a future dot cannot forget it. (Scoped to this
   chart: the donut and bar charts route taps through data-tip children.) */
.pw-chart svg * { pointer-events: none; }
.pw-chart .c-area { fill: var(--accent-soft); }
/* --accent for the line, NOT --accent-fill: the line colour and --chart-ghost
   are a validated pair (see the token), and light-mode --accent-fill is a
   different, unvalidated green. */
.pw-chart .c-exp { fill: none; stroke: var(--accent); stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; }
/* the forecast: the same line gone dashed and lighter — estimated, not fact */
.pw-chart .c-est { fill: none; stroke: var(--accent); stroke-width: 2; stroke-dasharray: 4 3; stroke-linecap: round; opacity: .7; }
/* The validated ghost grey, not a second green: a what-if line should read as
   scenario, not as "more good" — and --accent/--good are ONE green in both
   themes, which is how the two lines once shipped distinguishable by their
   dashes alone. */
.pw-chart .c-plan { fill: none; stroke: var(--chart-ghost); stroke-width: 2; stroke-dasharray: 3 5; stroke-linecap: round; }
.pw-chart .c-grid { stroke: var(--hairline-soft); stroke-width: 1; }
.pw-chart .c-today { stroke: var(--ink-3); stroke-width: 1; opacity: .5; }
.pw-chart .c-sel { stroke: var(--ink); stroke-width: 1.5; opacity: .85; }
.pw-chart .c-seldot { fill: var(--surface); stroke: var(--ink); stroke-width: 2.5; }
.pw-chart .c-ev { fill: var(--ink-3); }
.pw-chart .c-ev.in { fill: var(--accent); }
.pw-chart .c-anchor { fill: var(--surface); stroke: var(--accent); stroke-width: 2; }
.pw-leg {
  display: flex; flex-wrap: wrap; gap: 5px 14px; margin-top: 8px;
  font-size: 11px; font-weight: 600; color: var(--ink-3);
}
.pw-leg span { display: inline-flex; align-items: center; gap: 5px; }
.pw-leg i { width: 14px; height: 0; border-top: 2.5px solid var(--accent); border-radius: 2px; }
.pw-leg .est i {
  border: 0; height: 2px; width: 18px;
  background: repeating-linear-gradient(90deg, var(--accent) 0 4px, transparent 4px 7px);
  opacity: .7;
}
.pw-leg .plan i {
  border: 0; height: 2px; width: 18px;
  background: repeating-linear-gradient(90deg, var(--chart-ghost) 0 3px, transparent 3px 6px);
}
.pw-leg i.dot { width: 8px; height: 8px; border: 0; border-radius: 50%; background: var(--accent); }
.pw-leg i.dot.low { background: var(--warn-fill); }
.pw-leg i.dot.low.bad { background: var(--danger); }

/* the one-line payoff of keeping your budgets, next to the predicted figure */
.pw-plangain {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 12px; font-weight: 640; color: var(--good);
  background: var(--good-soft); border: none; border-radius: 999px;
  padding: 4px 10px; margin-left: 8px; cursor: pointer;
  font-variant-numeric: tabular-nums; font-family: inherit; letter-spacing: -0.01em;
}
.pw-plangain:active { opacity: .7; }

/* ---- drivers: what moves the number between today and the selected date ----
   Five rows, biggest first, each with a magnitude bar so the eye ranks them
   before the numbers are read. Tapping a category row opens its budget sheet. */
.pw-drv { margin-top: 15px; padding-top: 12px; border-top: 1px solid var(--hairline-soft); }
.pw-drvcap {
  font-size: 10.5px; font-weight: 650; text-transform: uppercase;
  letter-spacing: .05em; color: var(--ink-3); margin-bottom: 4px;
  font-variant-numeric: tabular-nums;
}
.pw-drvrow {
  display: flex; align-items: center; gap: 9px; width: 100%;
  min-height: 44px; padding: 4px 0; font-size: 13px; font-family: inherit; color: inherit;
  background: none; border: none; text-align: left;
}
button.pw-drvrow { cursor: pointer; }
button.pw-drvrow:active { opacity: .6; }
/* only the rows that open something get a chevron — a tappable row must not
   look identical to a static one */
.pw-drvrow .chev { flex: none; display: flex; width: 13px; height: 13px; color: var(--ink-3); opacity: .7; }
.pw-drvrow .chev svg { width: 100%; height: 100%; stroke-width: 2.4; }
div.pw-drvrow .chev { visibility: hidden; }
.pw-drvrow + .pw-drvrow { border-top: 1px solid var(--hairline-soft); }
.pw-drvrow .ic { flex: none; display: flex; width: 15px; height: 15px; color: var(--ink-3); }
.pw-drvrow .ic svg { width: 100%; height: 100%; }
.pw-drvrow .nm { flex: 1; min-width: 0; color: var(--ink-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pw-drvrow .nm small { color: var(--ink-3); font-size: 11px; }
.pw-drvrow .mag { flex: none; width: 44px; height: 4px; border-radius: 999px; background: var(--fill); overflow: hidden; }
.pw-drvrow .mag i { display: block; height: 100%; border-radius: 999px; background: var(--ink-3); opacity: .55; }
.pw-drvrow.in .mag i { background: var(--accent-fill); opacity: 1; }
.pw-drvrow .am { flex: none; font-weight: 650; font-variant-numeric: tabular-nums; }
.pw-drvrow.in .am { color: var(--good); }

/* A native range input: real keyboard support (arrows, Home, End), real drag,
   real screen-reader semantics via aria-valuetext — none of which 12 adjacent
   9px buttons could provide. */
.pw-slider { position: relative; margin-top: 2px; height: 44px; display: flex; align-items: center; }
.pw-slider input[type=range] {
  -webkit-appearance: none; appearance: none; width: 100%; height: 44px;
  background: none; margin: 0; cursor: pointer;
}
.pw-slider input[type=range]::-webkit-slider-runnable-track {
  height: 4px; border-radius: 999px; background: var(--fill-2);
}
.pw-slider input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 26px; height: 26px; margin-top: -11px; border-radius: 50%;
  background: var(--surface); border: 2px solid var(--accent);
  box-shadow: 0 1px 4px rgba(0,0,0,.22);
}
.pw-slider input[type=range]::-moz-range-track { height: 4px; border-radius: 999px; background: var(--fill-2); }
.pw-slider input[type=range]::-moz-range-thumb {
  width: 24px; height: 24px; border-radius: 50%;
  background: var(--surface); border: 2px solid var(--accent); box-shadow: 0 1px 4px rgba(0,0,0,.22);
}
.pw-slider input[type=range]:focus-visible { outline: none; }
.pw-slider input[type=range]:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 4px var(--accent-soft); }
.pw-axis { position: relative; display: flex; justify-content: space-between; font-size: 10.5px; font-weight: 600; color: var(--ink-3); font-variant-numeric: tabular-nums; }
/* "today", sitting at today's actual x on the axis — the boundary where the
   solid line turns dashed needs a name. Suppressed near the edges in the view
   so it cannot collide with the endpoint labels. */
.pw-axis .mid { position: absolute; transform: translateX(-50%); white-space: nowrap; }

.pw-jump { display: flex; gap: 8px; margin-top: 10px; }
.pw-jump button {
  flex: 1; min-width: 0; min-height: 44px; padding: 0 6px;
  font-size: 12.5px; font-weight: 600; font-family: inherit; letter-spacing: -0.01em;
  color: var(--ink-2); background: var(--fill); border: none; border-radius: 14px; cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pw-jump button.on { color: var(--on-accent); background: var(--accent); }

.pw-readout { margin-top: 14px; padding-top: 13px; border-top: 1px solid var(--hairline-soft); }
.pw-rrow {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  font-size: 12.5px; padding: 7px 0; font-variant-numeric: tabular-nums;
}
.pw-rrow + .pw-rrow { border-top: 1px solid var(--hairline-soft); }
.pw-rrow .k { color: var(--ink-3); }
.pw-rrow .v { font-weight: 620; color: var(--ink); }
.pw-rrow .v.in { color: var(--good); }
.pw-rrow .v.bad { color: var(--danger); }

.pw-evcap { margin-top: 12px; font-size: 10.5px; font-weight: 650; text-transform: uppercase; letter-spacing: .05em; color: var(--ink-3); }
.pw-ev { display: flex; align-items: center; gap: 9px; padding: 7px 0; font-size: 12.5px; font-variant-numeric: tabular-nums; }
.pw-ev + .pw-ev { border-top: 1px solid var(--hairline-soft); }
.pw-ev .nm { flex: 1; min-width: 0; color: var(--ink-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pw-ev .am { flex: none; font-weight: 640; }
.pw-ev .am.in { color: var(--good); }
.pw-ev .tag {
  flex: none; font-size: 10px; font-weight: 650; letter-spacing: .04em; text-transform: uppercase;
  color: var(--ink-3); background: var(--fill); border-radius: 999px; padding: 2px 7px;
}
.pw-ev .tag.est { color: var(--warn); background: var(--warn-tint); }

/* cash warning is its own verdict, separate from the budget verdict */
.pw-cash {
  display: flex; align-items: flex-start; gap: 9px; width: 100%; text-align: left;
  margin-top: 13px; padding: 11px 12px; border-radius: 12px;
  background: var(--warn-tint); color: var(--warn);
  font-size: 12.5px; font-family: inherit; cursor: pointer; font-variant-numeric: tabular-nums; line-height: 1.45;
}
.pw-cash .grow { flex: 1; min-width: 0; }
.pw-cash b { font-weight: 700; }
.pw-cash svg { width: 15px; height: 15px; flex: none; margin-top: 1px; }
.pw-cash.ok { background: var(--fill); color: var(--ink-3); }
.pw-cash.ok b { color: var(--ink-2); }
.pw-cash.bad { background: var(--danger-soft); color: var(--danger); }

.pw-conf { margin-top: 9px; font-size: 11px; color: var(--ink-3); line-height: 1.45; }

/* ── where the estimates come from ──────────────────────────────────────────
   One quiet line at the foot of the forecast card, and the ONLY way into the
   months sheet — there is no settings screen for this. Quiet on purpose: it is
   provenance, not a control the person is meant to reach for, until the day the
   estimates look wrong and then it is exactly where they will look. */
.pw-histline {
  display: flex; align-items: center; gap: 6px; width: 100%; text-align: left;
  margin-top: 12px; padding: 8px 0 0; border-top: 1px solid var(--hairline-soft);
  font-size: 11.5px; font-family: inherit; color: var(--ink-3);
  line-height: 1.45; cursor: pointer; font-variant-numeric: tabular-nums;
}
.pw-histline .chev { flex: none; display: flex; width: 12px; height: 12px; opacity: .6; margin-left: auto; }
.pw-histline .chev svg { width: 100%; height: 100%; stroke-width: 2.4; }
/* The fail-closed state is a sentence, not a button: there is nothing behind it
   to open, because reading the selection is what failed. */
.pw-histline.plain { cursor: default; }

/* the month list inside the sheet */
.pw-mrow {
  display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
  min-height: 44px; padding: 7px 2px; font-size: 13.5px; font-family: inherit;
  color: var(--ink); background: none; border: none; font-variant-numeric: tabular-nums;
}
button.pw-mrow { cursor: pointer; }
button.pw-mrow:active { opacity: .6; }
.pw-mrow + .pw-mrow { border-top: 1px solid var(--hairline-soft); }
.pw-mrow .tick { flex: none; display: flex; width: 16px; height: 16px; color: var(--accent); }
.pw-mrow .tick svg { width: 100%; height: 100%; stroke-width: 2.6; }
.pw-mrow .nm { flex: 1; min-width: 0; }
.pw-mrow .nm small { display: block; font-size: 11.5px; color: var(--ink-3); margin-top: 1px; }
/* A month the app is only FLAGGING. It is still ticked and still counted —
   the badge is a prompt to look, never a decision — so it is tinted rather
   than dimmed, which would read as excluded. */
.pw-mrow.odd .nm small { color: var(--warn); }
.pw-mrow:not(.on) .nm { color: var(--ink-3); }

/* ================= 2. overall budget ================= */
.pw-budhero { display: flex; align-items: baseline; gap: 7px; margin-top: 13px; }
.pw-budhero .v { font-size: 34px; font-weight: 650; letter-spacing: -0.035em; line-height: 1; font-variant-numeric: tabular-nums; }
.pw-budhero .k { font-size: 12.5px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
/* .pw-budmeta is gone with the card's prose lines (2026-08-26): the three
   figures it carried are the .pw-three row now. */

/* six months of verdicts as marks, not sentences. Colour + the caption both
   say it, so colour is never alone. */
.pw-hist {
  display: flex; align-items: flex-end; gap: 7px;
  margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--hairline-soft);
}
.pw-hist .m { flex: none; display: flex; flex-direction: column; align-items: center; gap: 4px; font-size: 9.5px; font-weight: 600; color: var(--ink-3); }
.pw-hist .m i { width: 26px; height: 7px; border-radius: 4px; background: var(--fill-2); }
/* under = filled, over = OUTLINED — shape carries the state alongside colour */
.pw-hist .m.u i { background: var(--good); opacity: .75; }
.pw-hist .m.o i { background: none; box-shadow: inset 0 0 0 2px var(--danger); opacity: .85; }
.pw-hist .cap { margin-left: auto; font-size: 11px; color: var(--ink-3); align-self: center; font-variant-numeric: tabular-nums; }

/* ================= 3. category budgets ================= */
.pw-cat2 { display: block; width: 100%; text-align: left; padding: 11px 18px 13px; }
.pw-c2head { display: flex; align-items: center; gap: 8px; }
.pw-c2head .cat-ico { color: var(--ink-2); }
.pw-c2head .nm { flex: 1; min-width: 0; font-size: 14.5px; font-weight: 600; letter-spacing: -0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pw-c2amt { flex: none; text-align: right; font-size: 13px; font-variant-numeric: tabular-nums; color: var(--ink-3); }
.pw-c2amt b { font-weight: 650; color: var(--ink); }
/* ✦ marks a suggested figure the user has not overridden — the legend under
   the group names it once. A manual budget is plain ink: yours, no mark. */
.pw-c2amt .sug { color: var(--ink-3); font-weight: 620; }
.pw-c2bar { display: flex; height: 6px; border-radius: 999px; background: var(--fill); overflow: hidden; margin-top: 9px; }
.pw-c2bar .fill { height: 100%; background: var(--accent-fill); }
/* hatch at full opacity in the TEXT-grade colour: the fill-grade greens blend
   to ~1.4:1 against the track at half opacity, and on an on-track row the
   hatch is the only drawing of projected spend */
.pw-c2bar .proj { height: 100%; background: repeating-linear-gradient(115deg, var(--accent) 0 3px, transparent 3px 6px); }
.pw-cat2.warn .pw-c2bar .fill { background: var(--warn-fill); }
.pw-cat2.warn .pw-c2bar .proj { background: repeating-linear-gradient(115deg, var(--warn) 0 3px, transparent 3px 6px); }
.pw-cat2.over .pw-c2bar { background: var(--danger-soft); }
.pw-cat2.over .pw-c2bar .fill { background: var(--danger); }
.pw-c2st { margin-top: 6px; font-size: 11px; font-weight: 650; color: var(--good); }
.pw-cat2.warn .pw-c2st { color: var(--warn); }
.pw-cat2.over .pw-c2st { color: var(--danger); }
.pw-c2st small { font-weight: 600; color: var(--ink-3); font-variant-numeric: tabular-nums; }

/* ================= 4. savings goals (Phase 7) =================
   `.pw-soon`, the "Coming soon" teaser this replaced, is gone with its markup.

   Every colour here is a token. `test/contrast.test.js` scans every `.pw-`
   and `.st-` rule for a literal and fails on one, so a hex typed in here is a
   red suite rather than a theme that only works in light mode. */
.pw-sect { margin-top: 26px; }

.pw-grow { display: block; width: 100%; padding: 13px 0 12px; }
.pw-grow + .pw-grow { border-top: 1px solid var(--hairline-soft); }
.pw-grow:first-child { padding-top: 2px; }

.pw-ghead { display: flex; align-items: baseline; gap: 10px; }
.pw-gname {
  font-size: 14.5px; font-weight: 620; letter-spacing: -.01em; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pw-gamt {
  margin-left: auto; flex: none; font-variant-numeric: tabular-nums;
  font-size: 13.5px; color: var(--ink-2);
}

/* The two halves of the bar. `--seg` is the fill the rest of the Plan tab
   already uses, so a goal's progress reads as the same kind of object as a
   budget's. */
.pw-seg-goal { background: var(--seg); }
.pw-seg-goalrem { background: transparent; }

.pw-gmeta { font-size: 12px; color: var(--ink-3); margin-top: 7px; line-height: 1.45; }
.pw-gmeta b { color: var(--ink-2); font-weight: 620; }

/* The chip carries the COUNT and never a word — see engine/goals.js for why
   the three bands the design started with are not here. Colour grades it in
   thirds, so the tone moves with the evidence rather than with a threshold. */
.pw-gfit {
  display: inline-flex; align-items: center; gap: 5px; font-size: 12.5px;
  font-weight: 620; padding: 3px 9px; border-radius: 999px; margin-top: 9px;
}
.pw-gfit.ok { background: var(--good-soft); color: var(--good); }
.pw-gfit.tight { background: var(--warn-tint); color: var(--warn); }
.pw-gfit.no { background: var(--danger-soft); color: var(--danger); }
.pw-gfit.held { background: var(--fill); color: var(--ink-2); }
.pw-gfit .ic { display: inline-flex; }
.pw-gfit .ic svg { width: 13px; height: 13px; }

.pw-gempty { text-align: left; }
.pw-gempty .h { font-size: 15px; font-weight: 640; letter-spacing: -.012em; }
.pw-gempty .p { font-size: 13px; color: var(--ink-2); line-height: 1.55; margin-top: 7px; }
.pw-est { color: var(--ink); font-variant-numeric: tabular-nums; }

.pw-gadd { margin-top: 13px; width: 100%; justify-content: center; }
.pw-gnote {
  font-size: 12px; color: var(--ink-3); line-height: 1.5; margin-top: 11px;
  padding-top: 11px; border-top: 1px solid var(--hairline-soft);
}
.pw-gnote b { color: var(--ink-2); font-weight: 620; }

.pw-gform { display: flex; flex-direction: column; gap: 13px; }
.pw-gfield { display: flex; flex-direction: column; gap: 6px; }
.pw-gfield > span { font-size: 12.5px; font-weight: 620; color: var(--ink-2); }
.pw-gfield input { width: 100%; }

/* ================= sheets, inside the phone frame =================
   The app's own .sheet vocabulary, scoped absolute so the mock stays inside
   the frame (the real app's sheets are fixed to the real viewport). Closed
   sheets and the idle backdrop must not eat taps. */
.phone .sheet-backdrop, .phone .sheet { position: absolute; }
/* The app stylesheet turns .sheet into a centred desktop modal at ≥700px of
   VIEWPORT — but inside the 390px frame the phone layout must win at any
   viewport, or the sheet renders 480px wide inside a 390px phone. */
@media (min-width: 700px) {
  .phone .sheet {
    bottom: 0; top: auto; left: 0; right: 0;
    width: calc(100% - 24px); max-width: 520px;
    margin: 0 auto calc(12px + env(safe-area-inset-bottom, 0px));
    border-radius: var(--r-hero); border-bottom: none; opacity: 1;
    transform: translateY(calc(100% + 12px));
  }
  .phone .sheet.show { transform: translateY(0); }
  .phone .sheet-grip { display: block; }
}
.phone .sheet-backdrop { pointer-events: none; }
.phone .sheet-backdrop.show { pointer-events: auto; }
.phone .sheet { pointer-events: none; visibility: hidden; transition: transform 0.42s cubic-bezier(0.32, 0.72, 0, 1), visibility 0s 0.42s; }
.phone .sheet.show { pointer-events: auto; visibility: visible; transition: transform 0.42s cubic-bezier(0.32, 0.72, 0, 1); }
.pw-sheet-h2 { font-size: 17px; font-weight: 700; letter-spacing: -0.02em; flex: 1; }
/* the info sheet's values are short prose, not figures — left-aligned, quiet */
#shInfo .sh-row { align-items: flex-start; }
#shInfo .sh-lab { color: var(--ink-3); font-size: 13px; padding-top: 1px; width: 62px; flex: none; }
#shInfo .sh-val { text-align: left; color: var(--ink-2); font-size: 13.5px; line-height: 1.5; }

/* calendar grid — the tap-the-date picker */
.pw-cal { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; padding: 4px 2px 6px; }
.pw-cal .dow { text-align: center; font-size: 10px; font-weight: 650; color: var(--ink-3); text-transform: uppercase; letter-spacing: .04em; padding: 4px 0; }
.pw-cal button {
  aspect-ratio: 1; min-height: 40px; border-radius: 11px;
  font-size: 14.5px; font-family: inherit; font-variant-numeric: tabular-nums;
  color: var(--ink); background: none; border: none; cursor: pointer;
}
.pw-cal button:hover { background: var(--fill); }
.pw-cal button:disabled { opacity: .28; pointer-events: none; }
.pw-cal button.on { background: var(--accent); color: var(--on-accent); font-weight: 700; }
.pw-cal button.today { box-shadow: inset 0 0 0 1.5px var(--accent-fill); }
.pw-cal-cap { font-size: 11.5px; color: var(--ink-3); padding: 2px 2px 10px; }

/* category sheet: six months of history as small bars */
.pw-hist6 { display: flex; align-items: flex-end; gap: 10px; height: 78px; margin-top: 12px; padding: 0 8px; }
.pw-hist6 .b { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px; height: 100%; justify-content: flex-end; font-size: 9.5px; font-weight: 600; color: var(--ink-3); }
.pw-hist6 .b i { display: block; width: 100%; max-width: 26px; border-radius: 6px 6px 3px 3px; background: var(--accent-fill); opacity: .4; height: var(--h); min-height: 4px; }
.pw-hist6 .b.hi i { opacity: .85; }

/* the budget stepper — big targets, one figure */
/* Month vs salary cycle, directly above the card it changes. */
.pw-period { margin: 26px 0 12px; }

.pw-step { display: flex; align-items: center; gap: 12px; margin-top: 6px; }
/* The value IS an input — typed or nudged, one control. Styled as the number
   it replaces, so nothing about the sheet looks different until you tap it. */
.pw-step .v input.amt {
  width: 100%; max-width: 170px; text-align: center; padding: 0;
  background: none; border: none; font-family: inherit; color: var(--ink);
  font-size: 30px; font-weight: 680; letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
}
.pw-step button {
  flex: none; width: 46px; height: 46px; border-radius: 999px;
  background: var(--fill); border: none; cursor: pointer;
  font-size: 22px; font-weight: 600; color: var(--ink); font-family: inherit;
  display: flex; align-items: center; justify-content: center;
}
.pw-step button:active { background: var(--fill-2); }
.pw-step .v { flex: 1; text-align: center; font-size: 30px; font-weight: 680; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; }
.pw-step .v small { font-size: 12px; font-weight: 600; color: var(--ink-3); letter-spacing: 0; }

/* the effect line: what this budget does to the forecast — the sentence that
   connects the two halves of the tab */
.pw-fx {
  margin-top: 14px; padding: 11px 13px; border-radius: 12px;
  background: var(--good-soft); font-size: 12.5px; line-height: 1.45;
  color: var(--ink-2); font-variant-numeric: tabular-nums;
}
.pw-fx b { font-weight: 680; color: var(--good); }
.pw-fx.none { background: var(--fill); }
.pw-fx.none b { color: var(--ink-2); }
/* The budget sheet fills this the moment it opens, so it was never empty and
   the tint was never a problem. The months sheet uses the same element purely
   as a failure channel — nothing to say unless a save fails — and an empty
   tinted block above the Save button reads as a control nobody can operate.
   Found by looking at it signed in, not by a test. */
.pw-fx:empty { display: none; }

@media (prefers-reduced-motion: reduce) {
  .pw-fill i { transition: none !important; animation: none !important; }
}

.cat-ico svg, .ico-inline svg, .tab svg, .month-nav svg, .empty .glyph svg, .btn svg { display: block; }

/* dustybanker-app appends this at style.css:1862 — the shell sets --tab-count
   from TABS.length, so the pill's width and its travel cannot disagree. The
   app has FOUR tabs today; Plan makes five. There is no More tab. */
.tab-pill { width: calc((100% - 12px) / var(--tab-count, 6)); }

/* ══ THE SETTINGS SURFACE ═══════════════════════════════════════════════════
   Appended in one block for the Settings redesign. Everything here is built
   from tokens that already exist — the design was drawn in dark only, but every
   colour in it has a light value already, so both themes follow from using the
   token rather than from a second set of rules.

   NO NEW TOKENS, deliberately. The two tints the design introduces are amber
   and per-category wells, and both are expressed with color-mix over an
   existing token instead of being added to :root. One consequence is worth
   stating rather than discovering: test/contrast.test.js reads the token
   declarations, so a color-mix tint is invisible to it. That is acceptable here
   and only here — these are BACKGROUNDS behind a glyph of the same hue, never
   a text/background pair, so there is no foreground contrast for that test to
   have an opinion about. Anything that does carry text keeps --ink/--ink-2/
   --ink-3 on a real surface token, which the test does see. */

/* The gear, lit while the settings surface is open. It is the only thing on
   screen saying where you are: the pill stays on whichever tab you left,
   because Settings is orthogonal to the five rather than a sixth among them. */
.header-btn.active { background: var(--accent-soft); color: var(--accent); }
.header-btn.active:hover { background: var(--accent-soft); color: var(--accent); }
/* 18px against the other header glyphs' 17px — a cog reads smaller than a solid
   form at the same box size, so this is what makes the pair look equal. */
.header-btn.gear svg { width: 18px; height: 18px; }

/* Touch target. The stylesheet's own convention from the .btn.small block
   above: an invisible overlay that enlarges the tap area without touching
   layout, VERTICAL ONLY. Horizontal would be wrong here specifically — the
   header's buttons are 34px with an 8px gap, so a 44px-wide overlay would
   overlap its neighbour and the two would steal each other's taps. */
@media (pointer: coarse) {
  .header-btn { position: relative; }
  .header-btn::after {
    content: ''; position: absolute; left: 0; right: 0; top: 50%;
    transform: translateY(-50%); height: 44px;
  }
}

/* ── The icon well ────────────────────────────────────────────────────────
   The one new visual pattern. A 30px square behind each settings row's glyph,
   at the tab bar's own 1.6 stroke — which is what makes the list and the tab
   bar read as one family rather than two icon sets that happen to share a
   screen. It replaces sheetkit's bare .sh-row-ico for these rows only; every
   other sheet keeps the plain glyph. */
.sh-well {
  flex: none; display: flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border-radius: 9px;
  background: var(--fill); color: var(--ink-2);
}
.sh-well svg { width: 17px; height: 17px; stroke-width: 1.6; }
/* The taller rows carry a 34px well at an 18px glyph — the category list, where
   the well is the category's own colour and is doing more work than a chrome
   glyph beside a label. Two sizes and no more. */
.sh-row.tall .sh-well { width: 34px; height: 34px; border-radius: 10px; }
.sh-row.tall .sh-well svg { width: 18px; height: 18px; }
/* A row with something to say. The well and the row's value take the same
   colour, so the warning is one signal in two places rather than two. */
.sh-well.warn { background: color-mix(in srgb, var(--warn) 16%, transparent); color: var(--warn); }
.sh-well.danger { background: color-mix(in srgb, var(--danger) 16%, transparent); color: var(--danger); }
.sh-val.warn { color: var(--warn); }
.sh-val.danger { color: var(--danger); font-weight: 600; }
.sh-val.good { color: var(--accent); }
.sh-val.strong { color: var(--ink); font-weight: 600; }

/* ── Settings row geometry ────────────────────────────────────────────────
   `.set` is the navigation row: 52px, the label taking the width and the value
   trailing it. That is the reverse of the base .sh-row, whose label is
   `flex: none` because a reference row's VALUE is the part that can afford to
   wrap. Here the label is the content and the value is a short status. */
.sh-row.set { min-height: 52px; padding: 11px 16px; gap: 12px; font-size: 15.5px; }
.sh-row.set .sh-lab { flex: 1; min-width: 0; white-space: normal; }
.sh-row.set .sh-val { flex: none; }
/* `.lv` is the LABEL-VALUE variant of a settings row — a sub-screen's reference
   row, where the label is short and fixed and the VALUE is the part that can
   afford to wrap. It restores the base .sh-row flex that `.set` inverts, and
   the two exist separately because they answer different questions: a nav row's
   label is its content, and a reference row's value is. Without this the label
   took the width and a long value ("Day the salary lands") wrapped onto three
   lines beside a mostly empty label. */
.sh-row.set.lv .sh-lab { flex: none; white-space: nowrap; }
.sh-row.set.lv .sh-val { flex: 1; text-align: right; }
/* A row carrying two lines, or a 34–36px avatar: merchants, rules, categories. */
.sh-row.tall { min-height: 58px; gap: 13px; }
.sh-row.tall.merch { min-height: 60px; }
.sh-row-main { flex: 1; min-width: 0; }
.sh-row-t { font-size: 15.5px; font-weight: 600; letter-spacing: -0.01em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sh-row-t.plain { font-weight: 400; }
.sh-row-s { margin-top: 2px; font-size: 12.5px; color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sh-row-s.warn { color: var(--warn); }
.sh-row-s.mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 12.5px; }
.sh-row-t.mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 15px; font-weight: 400; }

/* A control living inside a row rather than a screen of its own — the
   appearance picker, the budget period. It takes the right-hand side and stops
   the label from being squeezed by it. */
.sh-row .seg { flex: none; min-width: 168px; }
.sh-row.set:has(.seg) { min-height: 56px; }
.sh-row .switch { margin-left: auto; }

/* ── The profile card ─────────────────────────────────────────────────────
   Its own group above the first caption, because it identifies the screen
   rather than belonging to any of its sections. */
.set-profile { display: flex; align-items: center; gap: 13px; padding: 13px 16px; }
.set-profile .avatar { width: 44px; height: 44px; border-radius: 13px; font-size: 16px; font-weight: 680; }
.set-profile-main { flex: 1; min-width: 0; }
.set-profile-n { font-size: 15.5px; font-weight: 600; letter-spacing: -0.01em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.set-profile-e { margin-top: 2px; font-size: 12.5px; color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── Paste an SMS ─────────────────────────────────────────────────────────
   Monospace because a bank message is a fixed string being checked against
   what the parser made of it, and a proportional font makes two similar
   messages look identical. */
.paste-ta {
  width: 100%; display: block;
  background: var(--surface-2); border: 1px solid var(--separator);
  border-radius: 14px; padding: 12px 14px; min-height: 112px;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 14.5px; line-height: 1.5; color: var(--ink);
  outline: none; resize: vertical;
}
.paste-ta:focus { border-color: var(--accent); }
.paste-ta::placeholder { color: var(--ink-3); }

/* The token, shown once. `user-select: all` so one tap takes the whole thing on
   iOS, where selecting 43 characters by dragging is how a paste goes wrong. */
.token-block {
  background: var(--accent-soft); border-radius: var(--r-md); padding: 12px 14px;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 13px; line-height: 1.5; word-break: break-all; user-select: all;
}

/* The two circular actions beside the Home title. 16px at 1.6 rather than the
   17px the base rule gives them, so they carry the tab bar's weight and the
   pair reads as one set. */
.topbar .btn.small svg { width: 16px; height: 16px; stroke-width: 1.6; }
.btn.small.accent { background: var(--accent-soft); color: var(--accent); }
.btn.small.accent:hover { background: var(--accent-soft); filter: brightness(1.06); }

/* ── Try something: the tools row and the purchase sheet ───────────────────
   Ported from ~/Developer/plan-design/plan-v2.html — the `Try something` section
   at line 1552 and the `shBuy` sheet at 1604.

   Every colour here is a token, and that is load-bearing rather than tidy:
   test/contrast.test.js finds this block by the `pw-`/`st-` prefix and computes
   a WCAG ratio for each token pair, but a literal hex is invisible to that
   machinery — it renders, it can fail 4.5:1, and nothing would ever say so.
   It fails the other way too: a literal does not follow the theme, so it
   survives dark mode looking like a bug rather than failing loudly. */
.pw-tools { display: flex; gap: 10px; }
.pw-tool {
  flex: 1; border: 1px solid var(--card-border); background: var(--surface);
  border-radius: var(--r-lg); box-shadow: var(--shadow); padding: 14px 10px 13px;
  display: flex; flex-direction: column; align-items: center; gap: 7px;
  cursor: pointer; color: var(--ink); font: inherit;
  font-size: 13.5px; font-weight: 620; letter-spacing: -.01em;
  transition: transform .18s var(--ease-out), background .2s ease;
}
.pw-tool:active { transform: scale(.97); background: var(--surface-2); }
.pw-toolico { display: flex; width: 22px; height: 22px; color: var(--accent); }
.pw-toolico svg { width: 100%; height: 100%; }

/* The amount, typed. `6.2ch` on tabular figures holds five digits and the
   caret without the field resizing as it is typed. */
.pw-amtin { display: flex; align-items: baseline; justify-content: center; gap: 8px; padding: 14px 0 4px; }
.pw-amtin input {
  font-size: 40px; font-weight: 660; letter-spacing: -.035em; line-height: 1.1;
  width: 6.2ch; text-align: right; border: none; padding: 0;
  background: var(--surface); color: var(--ink); font-variant-numeric: tabular-nums;
}
.pw-amtin input:focus { outline: none; box-shadow: inset 0 -2px 0 var(--accent); }
.pw-amtin .u { font-size: 15px; color: var(--ink-3); font-weight: 600; }

.pw-quick { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin-top: 8px; }
.pw-quick .chip { min-height: 32px; padding: 0 13px; cursor: pointer; font-size: 13.5px; transition: background .2s ease, color .2s ease; }
.pw-quick .chip[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); }

/* The answer. Larger than the card's own verdict line because it is the whole
   content of the sheet rather than one line inside a card. */
.pw-verdict-lg {
  margin: 16px 0 4px; padding: 14px; border-radius: var(--r-md);
  font-size: 14px; line-height: 1.55;
  background: var(--surface-2); color: var(--ink-2);
}
.pw-verdict-lg b { color: var(--ink); font-weight: 650; }
.pw-verdict-lg.st-good { background: var(--good-soft); }
.pw-verdict-lg.st-warn { background: var(--warn-tint); }
.pw-verdict-lg.st-over { background: var(--danger-soft); }
.pw-verdict-lg.st-hold { background: var(--fill); }
/* The "from when" sentence, set apart from the verdict above it because it is a
   different claim: arithmetic on the careful line, not a judgement carrying the
   verdict's error bar. */
.pw-when { margin-top: 9px; }

@media (prefers-reduced-motion: reduce) {
  .pw-tool { transition: none; }
  .pw-quick .chip { transition: none; }
}
