/* app.css — design tokens + shell layout (Phase 5 scaffold, UI-SPEC §1).
 *
 * Tokens come from the operator's screenshots (D-009: screenshots outrank
 * taste). Sonnet sessions: refine components toward the screenshots; do NOT
 * change token names. Dark theme via [data-theme="dark"] (§8 Display Options).
 */

/* iOS text-autosizing lock: without this, standalone Safari inflates the
 * register/spending row text (payee + amount inherit ~1rem with no explicit
 * size) well past the intended compact density. Pin it to 100% everywhere. */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }

/* ---------- icon system (icons.js): one stroke family, currentColor ----------
 * Every SVG icon inherits the colour of its button/text, so dark/light parity
 * is automatic (CARDINAL rule 14). Default 1.15em box; call sites scale up. */
.ico { width: 1.15em; height: 1.15em; aspect-ratio: 1; display: inline-block; vertical-align: -0.18em; flex: 0 0 auto; }

/* Standard visually-hidden utility: present in the accessibility tree, zero
 * visual footprint. Same technique as the existing bespoke `.sync-copy` /
 * `.sidebar.collapsed .sidebar-footer .sync-copy` pattern, generalised so any
 * accessible-only label keeps no layout footprint. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

:root {
  --canvas: #f2efe6;          /* warm cream app background (mobile/tablet) */
  --canvas-desktop: #ffffff;  /* Win11 content canvas is plain white (§11) */
  --card: #ffffff;
  --ink: #1c1c28;
  --ink-muted: #6f6f80;
  --hairline: rgba(0, 0, 0, 0.08);
  /* Edge for the floating frosted-glass chrome (tab bar, settings gear, privacy
   * eye, scroll-to-top disc). These pills float OVER content and need a slightly
   * stronger rim than an inline divider to read as glass — but they must all use
   * the SAME rim (operator 2026-07-13 congruence pass). One token = one edge,
   * both themes; no more per-selector hardcoded rgba (rule 14). Light = the
   * plain hairline; dark = a touch heavier so the rim survives the blur. */
  --chrome-edge: var(--hairline);
  --chrome-control-fill: color-mix(in srgb, var(--card) 30%, transparent);
  --chrome-control-shadow: 0 2px 10px rgba(0, 0, 0, .18);
  /* BLURPLE (operator 2026-07-15: a bit more blue). The app draws every "blue"
   * surface from this one token: buttons, links, add-flow pickers, payee/category
   * text, geo arrows, charts and Plan accents. Dark mode inherits this too. */
  --accent: #4f6ff2;          /* bluer blurple; was #6366f1 */
  --accent-ink: #ffffff;
  --selected-date-ink: #101018;
  /* Control contract (D-131): every action surface consumes this semantic
   * family, even when a route/theme has its own broader accent vocabulary. */
  --control-accent: var(--accent);
  --control-accent-ink: var(--accent-ink);
  --control-secondary-bg: var(--btn-secondary-bg);
  --control-secondary-bg-hover: var(--btn-secondary-bg-hover);
  --control-secondary-ink: var(--btn-secondary-ink);
  --control-neutral-bg: var(--chip);
  --control-neutral-ink: var(--ink-muted);
  --control-danger: #c62828;
  --control-danger-ink: #ffffff;
  --control-selected-bg: var(--control-accent);
  --control-selected-ink: var(--control-accent-ink);
  --control-switch-off-bg: var(--control-neutral-bg);
  --control-switch-off-ink: var(--control-neutral-ink);
  --control-switch-on-bg: var(--control-accent);
  --control-switch-on-ink: var(--control-accent-ink);
  --clearing-switch-on-bg: var(--pos);
  /* Shared standalone-dismiss face. The transaction-sheet screenshot is the
   * canonical theme: a round card-coloured disc with ordinary ink, never a
   * one-off transparent or outlined X on another surface. */
  --close-control-bg: var(--card);
  --close-control-ink: var(--ink);
  --close-control-shadow: 0 1px 4px rgba(0, 0, 0, .12);
  /* Compact category/reminder lozenges use a deliberately lighter accent edge.
   * Both derive from theme tokens so dark/current stay paired. */
  --status-lozenge-outline: color-mix(in srgb, var(--accent) 60%, var(--accent-ink));
  /* The chip's TEXT is the same blurple as its outline (operator 01/09/2026),
   * but text is held to a contrast floor an edge is not: the outline mix
   * (accent 60% + white) measures 2.26:1 on the light register's white ground
   * at 13.12px, below the 4.5:1 AA body threshold, while reading 7.2:1 in dark.
   * This token is therefore the SAME hue read as ink — the light theme darkens
   * the accent by the smallest amount that clears AA (the bare accent measured
   * 4.30:1, just under), while dark uses the light-blurple mix itself, which
   * reads 7.2:1 against the near-black ground. Each theme lands on the LIGHTEST
   * blurple that still passes, which is what "light blurple text" has to mean
   * on two opposite grounds. */
  --status-lozenge-ink: color-mix(in srgb, var(--accent) 88%, var(--ink));
  /* Touch popup terminals are literal source chrome: a larger white-on-black
   * confirmation disc paired with an equally sized white back disc. Fixed
   * semantic inks keep that contrast identical in Current and dark themes. */
  --touch-popup-terminal-size: var(--ctl-h-lg);
  --touch-popup-confirm-bg: var(--selected-date-ink);
  --touch-popup-confirm-ink: var(--accent-ink);
  --touch-popup-back-bg: var(--accent-ink);
  --touch-popup-back-ink: var(--selected-date-ink);
  --scheduled-fill: color-mix(in srgb, var(--accent) 8%, var(--card));
  --scheduled-fill-strong: color-mix(in srgb, var(--accent) 14%, var(--card));
  /* Enabled-SECONDARY-button semantic state (D-102 part D, 19/08/2026): a pale
   * accent-derived fill, distinct from a disabled control and weaker than a
   * selected/open one. Dedicated token, not --chip — --chip also feeds
   * non-button surfaces (informational chips, pills) that must NOT go blue.
   * Derives from --accent/--card so Current and dark mode cannot drift.
   * all inherit it with no per-theme redeclaration. */
  --btn-secondary-bg: color-mix(in srgb, var(--accent) 12%, var(--card));
  --btn-secondary-bg-hover: color-mix(in srgb, var(--accent) 20%, var(--card));
  --btn-secondary-ink: color-mix(in srgb, var(--accent) 78%, var(--ink));
  --gold-metal: #c8952b;      /* metallic gold companion to the blue mark */
  --pos: #6b8e23;             /* money green family */
  --pos-pill: #dcedc8;
  --neg: #c62828;
  --neg-pill: #fdd8d4;
  /* Plan tab, sampled from the YNAB screenshot (IPP/IPL): the Ready-to-Assign
   * banner and the positive Available pills share one bright lime; group-header
   * bands are a hair warmer than the cream canvas; category rows are pure white;
   * the swipe "Snooze Target" action is periwinkle. */
  --rta-fill: #bae677;        /* bright lime — RTA banner + positive avail pill */
  --rta-ink: #33471c;         /* dark olive-green text on lime */
  --plan-group-bg: #f4f2eb;   /* warm group-header band */
  --plan-row-bg: #ffffff;     /* category rows are white on the cream canvas */
  --snooze: #7276f6;          /* periwinkle "Snooze Target" swipe action */
  --gold: #b8860b;            /* "best value" highlight (unit pricer) */
  --gold-soft: #9a6b00;       /* mixed-unit warning text */
  --amber: #e07b00;           /* foreign-currency code + uncleared-filter tint */
  --amber-ink: #ffffff;       /* text on a solid amber box */
  /* iOS Auto-Assign sheet, sampled from the screenshot: deep-olive button, a
   * very pale-green popover face, light-grey mode pills. */
  --aa-green: #4c6a1e;
  --aa-green-soft: #eef4e0;
  --aa-pill: #d7d7d9;
  --chip: #ececf1;
  --sidebar: #1d2035;         /* desktop dark navy */
  --sidebar-ink: #e8e9f2;
  --sidebar-muted: #9a9db8;
  --tab-inactive: #3a3a44;    /* bottom-tab label, unselected (Part 6) */
  --adjust-teal: #00a3a3;     /* reconcile-adjustment one-off, kept unique (Part 6) */
  /* Semantic paint tokens. Component rules consume these names; theme values
 * live here once so Current and dark mode cannot drift independently. */
  --pos-ink: #ffffff;
  --neg-ink: #ffffff;
  --aa-ink: #ffffff;
  --chart-ink: #ffffff;
  --chart-halo: rgba(0, 0, 0, .28);
  --boarding-field-art: radial-gradient(120% 120% at 50% 38%, #1b2559 0%, #141c46 46%, #0c1230 100%);
  --boarding-brand-ink: #e8c766;
  --boarding-badge-ink: #b9c3e6;
  --sidebar-hover: rgba(255, 255, 255, .08);
  --sidebar-active: rgba(255, 255, 255, .16);
  --surface-hover: rgba(0, 0, 0, .05);
  --surface-hover-soft: rgba(0, 0, 0, .04);
  --sheet-scrim: rgba(20, 20, 30, .45);
  --sheet-scrim-clear: rgba(20, 20, 30, 0);
  --sheet-scrim-soft: rgba(20, 20, 30, .28);
  --modal-scrim: rgba(0, 0, 0, .42);
  --zebra-row: rgba(0, 0, 0, .035);
  --subline-row: rgba(0, 0, 0, .02);
  --rec-warning-fill: color-mix(in srgb, var(--neg) 22%, transparent);
  --options-face: #23233a;
  --options-ink: #f2f2f7;
  --options-rule: rgba(255, 255, 255, .16);
  --options-selected: rgba(255, 255, 255, .18);
  --options-hover: rgba(255, 255, 255, .12);
  --options-hover-strong: rgba(255, 255, 255, .28);
  --options-focus: #9db4ff;
  --cover-close-fill: rgba(0, 0, 0, .28);
  --cover-pill-fill: rgba(0, 0, 0, .34);
  --edge-back-shadow: -12px 0 24px rgba(0, 0, 0, .18);
  --preview-light: #ffffff;
  --preview-dark: #15151d;
  --ca-light: rgb(255 255 255 / var(--ca-strength));
  --flag-red: #c62828;
  --flag-orange: #e07b00;
  --flag-yellow: #e0c000;
  --flag-green: #6b8e23;
  --flag-blue: #2b6cd4;
  --flag-purple: #7c5cd6;
  --swipe-edit: #f59e0b;
  --chart-1: #2a78d6;
  --chart-2: #008300;
  --chart-3: #e87ba4;
  --chart-4: #eda100;
  --chart-5: #1baf7a;
  --chart-6: #eb6834;
  --chart-7: #4a3aa7;
  --chart-8: #e34948;
  --chart-other: #b8b8c4;
  --radius: 24px;
  --radius-s: 12px;
  /* Shared fixed-chrome geometry. Approval's touch dock anchors to the same
   * tab-bar metrics instead of carrying a second magic offset. */
  --tab-bar-height: 63px;
  --tab-bar-bottom: max(env(safe-area-inset-bottom, 0px), clamp(14px, 5.22vw, 21px));
  --approval-dock-gap: 0px;
  --approval-dock-fill: var(--card);
  /* control heights — one scale, no per-screen drift (Part 3) */
  --ctl-h-sm: 34px;   /* desktop-compact: register toolbar, dense filters */
  --ctl-h-md: 44px;   /* touch normal — iOS 44px min; never go below on touch */
  --ctl-h-lg: 54px;   /* primary: Save/Next dock, keypad DONE */
  --ctl-pad-x-sm: .7rem;
  --ctl-pad-x-md: 1rem;
  --ctl-pad-x-lg: 1.2rem;
  /* type scale — collapse the 44-value sprawl to one ladder (Part 4) */
  --fs-micro: .72rem;   /* captions, chip sub-labels, axis labels */
  --fs-label: .82rem;   /* secondary labels, muted row meta */
  --fs-body:  .9rem;    /* default body / list-row primary text */
  --fs-value: 1rem;     /* amounts, row values, primary numbers */
  --fs-h3:    1.1rem;   /* card titles, section headers */
  --fs-h2:    1.3rem;   /* screen sub-headers */
  --fs-h1:    1.6rem;   /* screen titles */
  --fs-display: 2.4rem; /* the big amount readout in the Add sheet */
  /* Track graph labels render at the SAME size as a Track subcategory row
   * (`.plan-row` is --fs-body). The chart is an SVG whose every user unit is
   * multiplied by cardWidth/480, so a fixed font-size rendered 10.26px in a
   * 518px panel and 22px in a 1358px one; the chart now draws 1:1 and reads
   * this token, so the two agree at EVERY width instead of at one fitted
   * monitor size (operator 31/08/2026, reported repeatedly). */
  --fs-track-chart-label: var(--fs-body);
  /* Approval review hero wash sampled from the literal selectapprove frame.
   * The light values are scoped through the component below; dark mode
   * re-derives the same roles from its canvas/ink tokens. */
  --approval-hero-deep: #d0cdc6;
  --approval-hero-light: #e5dfd1;
  --approval-hero-mid: #dbd9ca;
  --approval-hero-low: #cccabd;
  --approval-ref-canvas: #f0ede6;
  --approval-ref-card: #fefefe;
  --approval-ref-chip: #eeece7;
  --approval-ref-accent: #4b43f5;
  /* Per-NODE value labels plotted inside a chart (operator 2026-07-14: the node
   * numbers on Big Picture and Most Frequent Categories read too small). They sit
   * above body text because they are the chart's actual data, competing with the
   * plotted marks around them — not prose. Their own rung, so nudging them can
   * never drag ordinary body text with it.
   * Bumped 1.02 → 1.18rem (operator 2026-07-14: "all such numbers need to be
   * bigger too"). This is the ONE rung every chart's node numbers sit on, so the
   * bump lands on Big Picture, Most Frequent and the trailing report together. */
  --fs-graph-node: 1.18rem;
  /* Chart x-axis tick labels (.bar-xlabel), every chart.
   *
   * MEASURED, NOT ASSUMED (operator 2026-07-17: "on ios the xaxis fontsize is way
   * too small, make much bigger, of all graphs in HOME"). This value is in VIEWBOX
   * UNITS, not screen pixels: every chart draws into a 640-wide viewBox that
   * renders ~252px on a phone, a scale of 0.394. The old flat 10px therefore
   * painted at 10 × 0.394 ≈ 3.9 SCREEN PIXELS — which is the whole bug, and why
   * "it's 10px" looked fine in the stylesheet while being illegible on the device.
   * The same trap is recorded against the value labels above (and cost a previous
   * release a round of "the numbers are too small" reports).
   *
   * So: pick the SCREEN size wanted, then divide by the chart's measured scale.
   *   phone (320px):  viewBox 640 → 252px, scale 0.394 → the touch block sets 33
   *                   units ≈ 13 screen px.
   *   desktop (1440): viewBox 640 → 652px, scale 1.019 → this 10 lands at ~10.2
   *                   screen px, which is already correct and dense, and is why
   *                   only iOS ever looked broken. Both figures measured in-browser
   *                   on the real Home charts, not derived on paper. */
  --fs-graph-axis: 10px;
  /* radius tokens — end the 12/18/24 drift (Part 4, finding 4) */
  --radius-card-desktop: 12px;
  --radius-card-touch: 24px;
  --radius-sheet-inner: 18px;
  /* motion — one vocabulary; all non-essential motion is reduced-motion aware */
  --dur-fast: .12s;    /* taps, press feedback, small colour swaps */
  --dur-base: .2s;     /* default transitions */
  --dur-slow: .32s;    /* sheet / modal entrances */
  /* Add-sheet swipe-away exit. MEASURED from the operator's own reference
   * recording (ynabdismiss.MP4, 60fps, 29/07/2026) rather than chosen by feel:
   * tracking the keypad panel's top edge frame by frame gives a release-to-gone
   * fall of ~180ms, and the normalised travel fits cubic-bezier(.05,0,.55,.95)
   * to an RMSE of 0.004 across every sampled frame.
   *
   * The old value was .72s on --ease-standard, cubic-bezier(.4,0,.2,1) — a
   * DECELERATING curve, i.e. the exact inverse of the reference. That is the
   * operator's report ("hits a wall then resumes ... nonlinearly"): the sheet
   * leapt off the finger, then crawled through a long slow tail four times
   * longer than the real thing. The p1y=0 start below gives the required
   * near-zero initial velocity, and the curve accelerates from there. */
  --dur-sheet-dismiss: .18s;
  --ease-sheet-dismiss: cubic-bezier(.05, 0, .55, .95);
  --dur-bubble-out: .6s;   /* envelope-bubble retract (operator 2026-07-15: 0.6s, scale rebound — NOT a fade) */
  --dur-bubble-in: .5s;    /* envelope-bubble expand, with rebound overshoot at the top */
  --ease-standard: cubic-bezier(.4, 0, .2, 1);      /* enter/exit */
  /* Canonical iOS slideout rebound. Applicable drawers, expanding cards and
   * conditional workflow rows use this token so refactors cannot silently
   * fall back to a flat generic easing curve. */
  --ease-ios-rebound: cubic-bezier(.22, 1.28, .36, 1);
  --ease-spring: var(--ease-ios-rebound);
}

[data-theme="dark"] {
  --canvas: #15151d;
  --canvas-desktop: #15151d;
  --card: #1f1f2b;
  --ink: #ececf4;
  --ink-muted: #9a9aac;
  --hairline: rgba(255, 255, 255, 0.09);
  /* On the near-black ground the pale outline mix IS the legible reading
   * (measured 7.2:1), so chip ink and chip outline are literally one value
   * here — the operator's "same colour as the outline of the lozenge" is
   * exact in dark, and the light theme uses the full accent to clear AA. */
  --status-lozenge-ink: var(--status-lozenge-outline);
  --approval-hero-deep: color-mix(in srgb, var(--canvas) 84%, var(--ink) 16%);
  --approval-hero-light: color-mix(in srgb, var(--canvas) 92%, var(--ink) 8%);
  --approval-hero-mid: color-mix(in srgb, var(--canvas) 86%, var(--ink) 14%);
  --approval-hero-low: color-mix(in srgb, var(--canvas) 78%, var(--ink) 22%);
  --approval-ref-canvas: var(--canvas);
  --approval-ref-card: var(--card);
  --approval-ref-chip: var(--chip);
  --approval-ref-accent: var(--accent);
  --chrome-edge: rgba(255, 255, 255, 0.14);
  --chip: #2b2b3a;
  --scheduled-fill: color-mix(in srgb, var(--accent) 14%, var(--card));
  --scheduled-fill-strong: color-mix(in srgb, var(--accent) 22%, var(--card));
  --pos-pill: #2c3a1e;
  --neg-pill: #45201c;
  /* Money red, lifted for dark (28/07/2026). The light-theme #c62828 scored
   * only 2.9:1 against the dark card and 2.53:1 inside its own --neg-pill —
   * every negative figure on a dark card was the app's dimmest text, in the
   * one colour that must never be missed. #ff6b6b clears 4.5:1 on both. Same
   * treatment --gold and --amber already carry for the same reason. */
  --neg: #ff6b6b;
  --control-danger: #ff6b6b;
  --control-danger-ink: var(--canvas);
  --rta-fill: #4c6f23;        /* muted deep lime for dark cards */
  --rta-ink: #dcf0ac;         /* light lime text on the dark fill */
  --plan-group-bg: rgba(255, 255, 255, 0.045);
  --plan-row-bg: var(--card);
  --snooze: #7c80f0;
  --gold: #e6b34d;            /* lighter goldenrod so "best value" reads on dark cards */
  --gold-soft: #d9a441;
  --amber: #e59436;           /* brighter amber so it reads on dark cards */
  --amber-ink: #1a1206;
  /* Auto-Assign sheet on dark: keep the identifiable green, but a dark popover
   * face + darker pills so it belongs on a dark canvas (dark/light parity). */
  --aa-green: #6b8e23;
  --aa-green-soft: #242c15;
  --aa-pill: #3a3a48;
  --sidebar: #12131f;
  --tab-inactive: #c7c9d6;
  --pos-ink: var(--canvas);
  --neg-ink: var(--canvas);
  --aa-ink: var(--canvas);
  --surface-hover: rgba(255, 255, 255, .05);
  --surface-hover-soft: rgba(255, 255, 255, .04);
  --zebra-row: rgba(255, 255, 255, .05);
  --subline-row: rgba(255, 255, 255, .03);
  --options-face: #2c2c44;
  --chart-1: #3987e5;
  --chart-2: #008300;
  --chart-3: #d55181;
  --chart-4: #c98500;
  --chart-5: #199e70;
  --chart-6: #d95926;
  --chart-7: #9085e9;
  --chart-8: #e66767;
}

* { box-sizing: border-box; }
/* Native-app feel: protect interactive chrome from selection/callout while
 * leaving ordinary explanations, memos and readable values selectable. The
 * no-zoom deviation remains in the viewport contract; touch-action here only
 * prevents accidental double activation on controls. */
* {
  -webkit-tap-highlight-color: transparent;
}
button, a, summary, [role="button"], [role="tab"],
.tab-bar, .sidebar, .fab-txn, .drag-handle, .swipe-row, .reorder-handle,
.gesture-owner {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  touch-action: manipulation;
}
input, textarea, select, [contenteditable="true"], [contenteditable=""] {
  -webkit-user-select: text; user-select: text;
}
.text-selectable {
  -webkit-touch-callout: default;
  -webkit-user-select: text;
  user-select: text;
}
/* Money/number fields opt OUT of selection entirely (operator 2026-07-11:
 * "disable the double-tap cut/copy/paste everywhere"): they are digit-stream
 * fields with the caret pinned to the end, so iOS/Android word-select — the
 * trigger for the cut/copy/paste bar — has nothing to offer and much to break. */
input[inputmode="numeric"], input[inputmode="decimal"] {
  -webkit-user-select: none; user-select: none;
}
/* Replaced elements (charts) must never force their intrinsic width onto a
 * flex/grid parent — that made whole tiles wider than the phone viewport and
 * clipped the right-hand figure (e.g. "$61.26 spent"). */
svg, img, canvas { max-width: 100%; }
/* The hidden attribute must beat every author display rule (#onboarding grid,
 * #app flex, …) — without this, "element.hidden = true" silently does nothing. */
[hidden] { display: none !important; }
/* iOS must never rubber-band horizontally / settle off-centre: html's
 * overflow-x:hidden propagates to the VIEWPORT (CSS root propagation), so the
 * page can't pan sideways; any wide child (nav strip, table) scrolls inside
 * its own box. overscroll-behavior-x stops the horizontal chain.
 *
 * THE DOCUMENT MUST BE THE SCROLLER (punch 21, tap-to-top root cause). The old
 * `html, body { height:100%; overflow-x:hidden }` made BODY a fixed-height
 * inner scroll container (overflow-x:hidden computes overflow-y to auto), so
 * every vertical scroll happened INSIDE the body element and the webview's
 * document scroller had 0 scrollable px. iOS's native status-bar
 * tap-to-scroll-to-top only ever scrolls the DOCUMENT scroller — so it was a
 * guaranteed no-op (v1.1.5's black-translucent change fixed the overlay, not
 * this). body now grows with content (min-height, no overflow clamp) so the
 * document itself scrolls, which also restores same-route scroll preservation
 * (shell.js reads window.scrollY). Verified in Chromium: pre-change the
 * document scroller reported 0 scrollable px, post-change it carries the full
 * page scroll. */
html, body { margin: 0; max-width: 100%; overscroll-behavior-x: none; }
/* background on <html> itself (item 5, 2026-07-15): the inline head script in
 * index.html sets data-theme before this stylesheet even loads, but until it
 * does there is nothing painted on the root element — body's background alone
 * left an unpainted white/black frame for that gap. Token-only, both themes
 * covered because [data-theme="dark"] redefines --canvas on this same html
 * element (the attribute selector below). */
html { height: 100%; overflow-x: hidden; background: var(--canvas); }
body { min-height: 100%; }
/* THE LAUNCH BAND (operator 03/08/2026, "ipad bottom animation.MP4" /
 * "iphone bottom animation.MP4": "the animation doesn't encompass the whole
 * screen when it should, due to this bottom bar thats blocking"). Measured off
 * the 60fps captures, not guessed: as iOS zooms the app card open from the
 * home-screen icon, the register rows are CLIPPED, a pale band sits below the
 * tab bar, and the bar itself floats mid-card instead of on the bottom edge.
 *
 * Root cause is `height: 100%` above, not the tab bar. A percentage height
 * resolves against the containing block, and during the launch zoom that block
 * is the CARD, which starts icon-sized and grows. So the document painted itself
 * short and the shortfall exposed the manifest's `background_color` (#eef1ff —
 * the pale band; app/manifest.json:6). The bar was never "blocking" anything: it
 * is `position: fixed` to that same short viewport, which is exactly why it
 * appeared to float mid-card. No z-index or sheet stacking is involved.
 *
 * `lvh` is the viewport at its LARGEST — the settled frame — so the root paints
 * the full screen from the zoom's first frame and there is no gap to expose. It
 * is the same trap, and the same fix, that `#boarding` already carries at its
 * `100lvh` line. Kept as a `min-height` FLOOR so a taller-than-viewport document
 * still scrolls, and scoped to `standalone` because in a browser TAB `lvh`
 * includes the space behind Safari's collapsible toolbar, where an unconditional
 * floor would reserve a strip of dead scroll under every page.
 *
 * TAP-TO-TOP IS UNAFFECTED (punch 21, documented above). That invariant needs
 * BODY to grow with content and carry no overflow clamp, so the DOCUMENT stays
 * the scroller. Both hold here: `height: auto` merely stops the root being
 * pinned to a percentage of the animating card, `min-height` is a floor rather
 * than a clamp, and no `overflow` is touched. The document scroller keeps the
 * full page scroll. */
@media all and (display-mode: standalone) {
  html { height: auto; min-height: 100%; min-height: 100lvh; }
  body { min-height: 100lvh; }
}
/* THE JUDDER (operator 30/07/2026: "judders occur over regular use... fields
 * like account or category slightly shift location"). Root cause, measured, not
 * guessed: `overflow-x: hidden` above makes the root's `overflow-y` compute to
 * AUTO (it can never stay `visible` once one axis is clipped), so the document
 * scrollbar appears and disappears as the page height crosses the viewport —
 * which happens constantly in normal use: a drawer opens, the approval banner
 * shows, select mode swaps the toolbar, a filter shortens the list.
 *
 * Each toggle changes the viewport width by one scrollbar (15px on the
 * operator's Win11 Firefox), and the register's `fr` tracks redistribute that
 * width. The tracks do NOT move together, which is why it reads as a judder
 * rather than a shift: measured frame-by-frame off "flag and cleared
 * judder.mp4", ACCOUNT and DATE held still (fixed `rem` tracks) while PAYEE
 * moved 18px, CATEGORY 8px, MEMO 1px and the amounts −5px. Reproduced live on
 * the dev register at the exact scrollbar crossing: CATEGORY −6px, MEMO −11px,
 * OUTFLOW −15px.
 *
 * Reserving the track permanently makes the width constant, so nothing reflows.
 * `scrollbar-gutter: stable` is the modern spelling and was MEASURED INERT here
 * — on html, on body, and on both — precisely because this root propagates its
 * overflow to the viewport, and the gutter property does not apply to the
 * element that has been propagated away from. `overflow-y: scroll` is what
 * actually holds. Verified: page-height changes now move nothing, and
 * `document.scrollingElement` is still <html> with the full page scroll, so the
 * iOS tap-to-top document-scroller invariant documented above is untouched.
 *
 * DESKTOP ONLY. Touch surfaces use overlay scrollbars that take no width, so
 * there is no judder to fix there, and forcing a permanent track would paint a
 * dead gutter down the side of a phone. */
@media (hover: hover) and (pointer: fine) {
  html { overflow-y: scroll; }
}

/* ── Boarding screen (operator 2026-07-17) ──────────────────────────────────
 * Covers the unavoidable module-load + IndexedDB-open gap with the app's own
 * flat blue tree instead of an empty themed canvas. Lives high in this file because
 * it is paint-time chrome, like the html/canvas rules above it.
 *
 * Themed by tokens ONLY (--canvas / --text-dim), so the inline theme mirror in
 * index.html has already resolved light/dark before this paints — the boarding
 * screen can never flash the wrong palette (CARDINAL-adjacent: global rule 14,
 * dark/light parity is mandatory).
 *
 * The flat blue is a LOCAL constant, not a theme token: this is the brand
 * mark, deliberately identical on both themes. Only the field behind it changes.
 */
#boarding {
  position: fixed;
  --boarding-edge-extension: env(safe-area-inset-bottom, 0px);
  inset: 0 0 calc(-1 * var(--boarding-edge-extension));
  /* iOS Safari/PWA: `inset:0` alone resolves against the SMALL viewport, so the
   * dynamic browser-chrome area at the bottom (which shrinks to reveal more page
   * as the tree animates) exposed the manifest's beige background_color under
   * the boarding overlay, clipping the bottom motif rows behind it. Fixed
   * inset sizing plus a LARGE-viewport minimum closes that gap without the
   * `100dvh` shrink that exposed the strip on touch webviews. */
  width: auto; height: auto;
  min-height: 100vh;
  min-height: calc(100lvh + var(--boarding-edge-extension));
  z-index: 9999;
  display: grid;
  place-items: center;
  /* Centre the tree+wordmark within the SAFE area, not the raw viewport, so a
   * notch / Dynamic Island / home-indicator on the operator's iPhone fleet
   * (CARDINAL 17, incl. the future-proof clause) can never crop the stage. The
   * padding shrinks only the grid-centred stage; the motif is position:absolute
   * at inset:-24%, so it still paints edge-to-edge under the insets. box-sizing
   * keeps the padding inside the fixed inset:0 box. Zero on non-notched screens
   * (env() resolves to 0), so this is inert everywhere else. */
  box-sizing: border-box;
  padding: env(safe-area-inset-top) env(safe-area-inset-right)
           env(safe-area-inset-bottom) env(safe-area-inset-left);
  /* Deep, calm night field — a dark navy pooling darker toward the edges so the
   * flat blue tree reads clearly against it. The water-caustic effect was removed
   * (operator 2026-07-20: "looks terrible"); this is a flat, quiet backdrop that
   * carries on both themes and never competes with the wordmark. */
  background: var(--boarding-field-art);
  /* iOS installed webviews can expose a home-indicator reserve beyond this
   * fixed layout box. The motif already overscans by 24%; clipping it here was
   * the exact reason that reserve showed only the root's flat colour. Let the
   * animated field paint through the browser-owned edge. */
  overflow: visible;
  /* Opacity-only fade-out. The tree keeps growing THROUGH the fade — a growth
   * that froze at handoff read as the app stalling at the last moment. */
  transition: opacity .42s cubic-bezier(.4, 0, .2, 1);
  pointer-events: none; /* never eats a tap, even mid-fade */
}
/* The stage (tree + wordmark) rides above the backdrop. */
.boarding-stage { position: relative; z-index: 1; }
#boarding.boarding-out { opacity: 0; }
/* Self-dismiss backstop: if a boot dies before main.js can call boardingDone(),
 * the family must never be trapped behind a tree with no way forward. Pure CSS,
 * so it holds even if no JS runs at all. 6s is far past any real boot (the gap
 * this covers is ~0.3-1s) — long enough to never fire on a healthy slow open. */
#boarding { animation: boarding-failsafe 0s linear 6s forwards; }
@keyframes boarding-failsafe { to { opacity: 0; visibility: hidden; } }

.boarding-stage {
  display: grid;
  justify-items: center;
  gap: 18px;
  /* The whole stage lifts a little as it settles, so the growth reads as the
   * tree rising out of the page rather than inflating in place. */
  animation: boarding-rise 1.6s cubic-bezier(.22, .61, .36, 1) both;
}
@keyframes boarding-rise {
  from { transform: translateY(10px); opacity: 0; }
  to   { transform: translateY(0);    opacity: 1; }
}

.boarding-tree {
  width: min(148px, 34vw);
  /* Explicit square, NOT height:auto. The art's own bbox is 526 units wide
   * against a 512 viewBox (it overhangs ~20 units left), and with
   * overflow:visible an auto height resolved the box to 121x110 — a visibly
   * squashed tree. Pinning aspect-ratio to the viewBox keeps it square at every
   * width; measured, not assumed. */
  aspect-ratio: 1 / 1;
  height: auto;
  overflow: visible; /* the sway swings leaves past the viewBox edge */
  /* GROWTH: bigger and taller at once. scaleY runs slightly ahead of scaleX so
   * the tree gains height faster than width — "growing taller" per the brief,
   * not a uniform zoom. transform-origin at the base pins the roots to the
   * ground: a centre origin would sink the trunk as it grew.
   * `both` holds the final frame — the tree must not snap back mid-fade. */
  transform-origin: 50% 100%;
  animation: boarding-grow 2.8s cubic-bezier(.16, .84, .44, 1) both;
}
@keyframes boarding-grow {
  from { transform: scale(.82, .74); }
  to   { transform: scale(1, 1); }
}

/* WIND: canopy leaves and trunk are animated separately and at different
 * strengths, because a real tree does not move as one rigid piece — this
 * split is the entire reason the markup has two <g>s (canopy vs trunk).
 *
 * Within the canopy, EACH of the 17 leaf clusters (.leaf-1 .. .leaf-17) sways
 * on its OWN independent animation — different duration, delay and rotation
 * amplitude per leaf — rather than the whole <g class="tree-canopy"> rotating
 * as one rigid block. A single shared transform on the group reads as the
 * whole silhouette tilting like a stiff cutout; real leaves each catch the
 * gust a beat apart and swing by different amounts. Every leaf still pivots
 * at its own centre (transform-box: fill-box + transform-origin: center) so
 * each rotates about its own stem/base point (bottom of its bbox), not its
 * centre and not the SVG's (0,0) corner — a centre pivot spins the whole leaf
 * blade around its own middle, which reads as tumbling in place rather than
 * flapping from where it's attached to the branch. Durations/delays are
 * spread pseudo-randomly (not a clean arithmetic sequence) so the 17 leaves
 * never fall into a visible shared beat. */
.tree-canopy .leaf {
  transform-box: fill-box;
  transform-origin: 50% 100%;
  animation-name: leaf-sway;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
@keyframes leaf-sway {
  0%, 100% { transform: rotate(var(--leaf-a, -3deg)) translateX(var(--leaf-tx, -1px)) scale(1); }
  50%      { transform: rotate(var(--leaf-b, 3deg))  translateX(calc(var(--leaf-tx, -1px) * -1)) scale(var(--leaf-s, 1)); }
}
/* v1.41.1+ (operator 2026-07-20, third pass — "significantly more animated
 * like in a gale"): the leaf shapes carry no transform attribute of their own
 * (checked against the v1.24.0 fill-box/pre-squash-pivot trap before pushing
 * amplitude further — see project memory — so raising these numbers is safe
 * and stays on-canvas), so rotation/translate/scale go up another ~2x on top
 * of the prior pass, with shorter, more turbulent durations so the canopy
 * reads as caught in a real gust rather than a gentle breeze. */
.leaf-1  { --leaf-a: -17deg; --leaf-b: 15deg;  --leaf-tx: -7px; --leaf-s: 1.14; animation-duration: 1.0s; animation-delay: -.10s; }
.leaf-2  { --leaf-a: -13deg; --leaf-b: 19deg;  --leaf-tx: 5.6px; --leaf-s: 1.18; animation-duration: 1.3s; animation-delay: -1.9s; }
.leaf-3  { --leaf-a: -20deg; --leaf-b: 12deg;  --leaf-tx: -4px;  --leaf-s: 1.12; animation-duration: 1.1s; animation-delay: -.7s; }
.leaf-4  { --leaf-a: -15deg; --leaf-b: 17deg;  --leaf-tx: 7.6px; --leaf-s: 1.16; animation-duration: 1.4s; animation-delay: -2.6s; }
.leaf-5  { --leaf-a: -11deg; --leaf-b: 21deg;  --leaf-tx: -6px;  --leaf-s: 1.20; animation-duration: 1.2s; animation-delay: -1.3s; }
.leaf-6  { --leaf-a: -19deg; --leaf-b: 14deg;  --leaf-tx: 4.6px; --leaf-s: 1.14; animation-duration: 1.0s; animation-delay: -2.2s; }
.leaf-7  { --leaf-a: -14deg; --leaf-b: 18deg;  --leaf-tx: -8px;  --leaf-s: 1.18; animation-duration: 1.5s; animation-delay: -.4s; }
.leaf-8  { --leaf-a: -17deg; --leaf-b: 11deg;  --leaf-tx: 6.6px; --leaf-s: 1.12; animation-duration: 1.1s; animation-delay: -1.6s; }
.leaf-9  { --leaf-a: -12deg; --leaf-b: 20deg;  --leaf-tx: -5px;  --leaf-s: 1.18; animation-duration: 1.4s; animation-delay: -2.9s; }
.leaf-10 { --leaf-a: -21deg; --leaf-b: 16deg;  --leaf-tx: 3.6px; --leaf-s: 1.14; animation-duration: 1.1s; animation-delay: -.9s; }
.leaf-11 { --leaf-a: -13deg; --leaf-b: 13deg;  --leaf-tx: -6.6px; --leaf-s: 1.16; animation-duration: 1.3s; animation-delay: -2.0s; }
.leaf-12 { --leaf-a: -18deg; --leaf-b: 19deg;  --leaf-tx: 8px;   --leaf-s: 1.20; animation-duration: 1.6s; animation-delay: -1.1s; }
.leaf-13 { --leaf-a: -10deg; --leaf-b: 15deg;  --leaf-tx: -4.6px; --leaf-s: 1.12; animation-duration: 1.1s; animation-delay: -2.4s; }
.leaf-14 { --leaf-a: -16deg; --leaf-b: 22deg;  --leaf-tx: 5px;   --leaf-s: 1.18; animation-duration: 1.4s; animation-delay: -.2s; }
.leaf-15 { --leaf-a: -14deg; --leaf-b: 12deg;  --leaf-tx: -7.6px; --leaf-s: 1.16; animation-duration: 1.2s; animation-delay: -1.7s; }
.leaf-16 { --leaf-a: -20deg; --leaf-b: 17deg;  --leaf-tx: 4px;   --leaf-s: 1.14; animation-duration: 0.9s; animation-delay: -.6s; }
.leaf-17 { --leaf-a: -12deg; --leaf-b: 18deg;  --leaf-tx: -5.6px; --leaf-s: 1.18; animation-duration: 1.4s; animation-delay: -2.7s; }
.tree-trunk {
  transform-origin: 50% 100%;
  animation: trunk-sway 4.1s ease-in-out infinite;
}
@keyframes trunk-sway {
  0%, 100% { transform: rotate(-.35deg); }
  50%      { transform: rotate(.35deg); }
}

.boarding-word {
  margin: 0;
  font-size: 1.05rem;
  font-weight: 600;
  letter-spacing: .18em;
  /* Boarding backdrop is a FIXED dark navy gradient regardless of theme (not
   * themed light/dark), so the wordmark needs its own fixed light colour, not
   * a UI text token — --text-dim was never defined anywhere, so it always
   * fell back to inherited (dark) text colour: unreadable on navy. */
  color: var(--boarding-brand-ink);
  text-transform: uppercase;
  animation: boarding-word-in 1.1s ease-out .5s both;
}
.boarding-word-new {
  font-size: .78em;
  font-weight: 400;
  letter-spacing: .12em;
  color: var(--boarding-badge-ink); /* cool pale periwinkle vs the gold "ynab" — reads as a badge prefix */
  margin-right: .35em;
}
@keyframes boarding-word-in {
  from { opacity: 0; }
  to   { opacity: .85; }
}

/* Reduced motion: the boarding screen still EXISTS (it covers a real gap and
 * removing it would restore the blank canvas), but nothing sways, grows or
 * rises — it simply appears, already at full size, and still fades out. The
 * fade is a cross-fade of two static states, which is not vestibular motion. */
@media (prefers-reduced-motion: reduce) {
  .boarding-stage,
  .boarding-tree,
  .tree-canopy .leaf,
  .tree-trunk,
  .boarding-word { animation: none; }
  .boarding-word { opacity: .85; }
}

/* Boarding refresh: a quiet, animated brand scene instead of a fast floating canopy. */
:root { --boarding-field: #eef1ff; --boarding-field-deep: #dce4ff; --boarding-line: rgba(79,111,242,.27); --boarding-word-ink: #26336f; --boarding-new-ink: #6473ab; }
html[data-theme="dark"] { --boarding-field: #111733; --boarding-field-deep: #242e67; --boarding-line: rgba(194,207,255,.24); --boarding-word-ink: #f3e7b7; --boarding-new-ink: #b9c3e6; }
/* Installed touch webviews can expose a strip outside their fixed-layout
 * viewport (home-indicator / dynamic-chrome reserve). Paint the document root
 * with the same boarding field while the loader exists, so that browser-owned
 * edge can never reveal the manifest's old beige canvas. */
html:has(#boarding), body:has(#boarding) { background: var(--boarding-field); }
#boarding { background:radial-gradient(95% 74% at 50% 42%,var(--boarding-field-deep),var(--boarding-field)); transition:opacity .42s cubic-bezier(.32,1.20,.50,1); }
/* DEADZONE (operator 25/07/2026, iPad PWA): a flat, motif-free bar along the
 * bottom edge. Mechanism — `#boarding` is `position:fixed; inset:0`, which on an
 * installed iOS PWA lays out against the SMALL viewport, while the surface the
 * OS actually paints is the LARGE one. The difference (home-indicator reserve)
 * fell outside the overlay entirely, so the rule above painted it flat
 * `--boarding-field` with no motif. That flat fill is exactly the reported bar:
 * v1.61.0 fixed its COLOUR and v1.63.0 fixed the overlay's own clipping, but
 * neither put ANIMATION into a strip the overlay never covered.
 * Headless cannot reproduce this at all — Chromium resolves svh == lvh == dvh,
 * so the strip has zero height there and every past probe read green. The fix
 * therefore must not depend on measuring it: the motif gets its OWN fixed layer
 * sized to the LARGE viewport (`100lvh`), which by definition includes the
 * reserve, so the animation paints across it on any device geometry. On screens
 * where the two viewports are equal this is a no-op. */
/* OVERSCAN ANCHORING (operator 26/07/2026 — the bar was still there on iPhone AND on iPad in
 * BOTH orientations). Percentages here resolved against the SMALL viewport while iOS paints the
 * LARGE one, and `top:-24%` anchored the layer to that small box, so the field's bottom edge
 * landed short of the surface. What showed through underneath was `#boarding`'s own radial
 * gradient at its darkest stop (#0c1230) with no icons on it — a flat, dead, darker band exactly
 * where the screenshots show one. Three previous sessions hunted this as a viewport-unit or
 * home-indicator problem and every probe read green, because Chromium resolves svh == lvh == dvh
 * and the band has zero height there.
 * The fix removes the ambiguity instead of measuring it: anchor to the LARGE viewport in both
 * axes (`lvh`/`lvw`, which by definition include any browser-chrome or home-indicator reserve)
 * and overscan by a fixed 24% on every side. No percentage here resolves against a box whose
 * height depends on which viewport the OS decided to report. */
.boarding-motif {
  --icon-h:clamp(15px,3.1vw,60px);
  position:fixed;
  left:-24%; width:148%;
  top:-24%; height:148%;
  /* Large-viewport anchoring; the pair overrides the percentages above wherever supported. */
  top:calc(-24 * 1lvh); height:calc(148 * 1lvh + var(--boarding-edge-extension));
  left:calc(-24 * 1lvw); width:calc(148 * 1lvw);
  overflow:hidden; transform:rotate(-16deg) scale(1.06);
  color:var(--boarding-line); pointer-events:none;
}
.motif-row { position:absolute; left:-8%; width:116%; display:flex; align-items:center; justify-content:space-between; gap:clamp(2px,.6vw,8px); }
/* Depth cue: size cycles small/medium/large every 3 rows (sawtooth, not
 * triangle — row 4 drops straight back to smallest after row 3's max), and
 * speed rides with size so the biggest (nearest) rows sweep fastest and the
 * smallest (farthest) rows barely creep — a cheap parallax read. Rows sit
 * on an exact 5% grid (21 rows), but `top` alone positions each row's TOP
 * EDGE — since row height tracks icon size (align-items:center, auto
 * height), a small row and a large row on the same grid would have
 * unevenly spaced (and, worst case, overlapping) CONTENT centres. The
 * `- (icon-h * row-scale / 2)` term re-anchors every row to its content
 * centre instead, so consecutive rows are equally spaced regardless of
 * tier. UNIFORM FILL (operator 2026-07-21, Opus pass — "no blank spaces,
 * uniform across the screen"): icons use `justify-content:space-between`,
 * NOT flex-start, so every row spans edge-to-edge with icons evenly
 * distributed — a flex-start row only filled its leading portion, leaving a
 * blank trailing band that the drift animation swept into view. Because icon
 * WIDTH scales with the tier but a fixed count would leave small-tier rows
 * sparse and overflow large-tier rows, each tier carries a DIFFERENT count
 * inversely proportional to its scale (small=58, medium=32, large=21 — set
 * in index.html), giving every tier the same on-screen pitch. Icon size
 * scales with vw across the whole device fleet (width clamp .._84px, height
 * clamp .._60px) so density holds from a 402px phone to a 4K monitor; the
 * height cap is deliberately below the 5% row pitch so wide/landscape screens
 * never overlap rows vertically. Verified: <2% blank on every phone/tablet,
 * <=7px row overlap on desktop, zero blank stripe under drift (Playwright +
 * getBoundingClientRect cell-coverage sweep across the CARDINAL-17 fleet, both
 * drift extremes). The 32" 4K desktop reads airier (same icons, more canvas) —
 * filling it pixel-dense would need ~3x the DOM and is not the complaint.
 * Rows sit on an exact 5% grid (21 rows, operator 2026-07-21 density pass):
 * more, tighter rows shrink every row-to-row gap so none reads as a seam even
 * in the radial gradient's darkest band near the screen edges. */
.motif-row-a { top:calc(0% - (var(--icon-h) * .55 / 2)); --row-scale:.55; animation:motif-drift-a 34s linear infinite; }
.motif-row-b { top:calc(5% - (var(--icon-h) * 1 / 2)); --row-scale:1; animation:motif-drift-b 21s linear infinite; }
.motif-row-c { top:calc(10% - (var(--icon-h) * 1.7 / 2)); --row-scale:1.7; animation:motif-drift-c 13s linear infinite; }
.motif-row-d { top:calc(15% - (var(--icon-h) * .55 / 2)); --row-scale:.55; animation:motif-drift-d 35s linear infinite; }
.motif-row-e { top:calc(20% - (var(--icon-h) * 1 / 2)); --row-scale:1; animation:motif-drift-a 22s linear infinite; }
.motif-row-f { top:calc(25% - (var(--icon-h) * 1.7 / 2)); --row-scale:1.7; animation:motif-drift-b 13s linear infinite; }
.motif-row-g { top:calc(30% - (var(--icon-h) * .55 / 2)); --row-scale:.55; animation:motif-drift-c 36s linear infinite; }
.motif-row-h { top:calc(35% - (var(--icon-h) * 1 / 2)); --row-scale:1; animation:motif-drift-d 23s linear infinite; }
.motif-row-i { top:calc(40% - (var(--icon-h) * 1.7 / 2)); --row-scale:1.7; animation:motif-drift-a 14s linear infinite; }
.motif-row-j { top:calc(45% - (var(--icon-h) * .55 / 2)); --row-scale:.55; animation:motif-drift-b 37s linear infinite; }
.motif-row-k { top:calc(50% - (var(--icon-h) * 1 / 2)); --row-scale:1; animation:motif-drift-c 23s linear infinite; }
.motif-row-l { top:calc(55% - (var(--icon-h) * 1.7 / 2)); --row-scale:1.7; animation:motif-drift-d 9s linear infinite; }
.motif-row-m { top:calc(60% - (var(--icon-h) * .55 / 2)); --row-scale:.55; animation:motif-drift-a 38s linear infinite; }
.motif-row-n { top:calc(65% - (var(--icon-h) * 1 / 2)); --row-scale:1; animation:motif-drift-b 24s linear infinite; }
.motif-row-o { top:calc(70% - (var(--icon-h) * 1.7 / 2)); --row-scale:1.7; animation:motif-drift-c 10s linear infinite; }
.motif-row-p { top:calc(75% - (var(--icon-h) * .55 / 2)); --row-scale:.55; animation:motif-drift-d 40s linear infinite; }
.motif-row-q { top:calc(80% - (var(--icon-h) * 1 / 2)); --row-scale:1; animation:motif-drift-a 25s linear infinite; }
.motif-row-r { top:calc(85% - (var(--icon-h) * 1.7 / 2)); --row-scale:1.7; animation:motif-drift-b 10s linear infinite; }
.motif-row-s { top:calc(90% - (var(--icon-h) * .55 / 2)); --row-scale:.55; animation:motif-drift-c 41s linear infinite; }
.motif-row-t { top:calc(95% - (var(--icon-h) * 1 / 2)); --row-scale:1; animation:motif-drift-d 19s linear infinite; }
.motif-row-u { top:calc(100% - (var(--icon-h) * 1.7 / 2)); --row-scale:1.7; animation:motif-drift-a 11s linear infinite; }
.motif-icon { position:relative; display:block; width:calc(clamp(16px,4.2vw,84px) * var(--row-scale,1)); height:calc(clamp(15px,3.1vw,60px) * var(--row-scale,1)); flex:0 0 auto; background-color:currentColor; -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat; -webkit-mask-position:center; mask-position:center; -webkit-mask-size:contain; mask-size:contain; }
.motif-home { -webkit-mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill=%27none%27 stroke=%27%23000%27 stroke-width=%271.6%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27%3E%3Cpath d=%27M3 11.5 12 4l9 7.5%27/%3E%3Cpath d=%27M5 10v9.5a1 1 0 0 0 1 1h4v-6h4v6h4a1 1 0 0 0 1-1V10%27/%3E%3Cpath d=%27M15 4.5V7l2 1.3V4.5z%27/%3E%3C/g%3E%3C/svg%3E"); mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill=%27none%27 stroke=%27%23000%27 stroke-width=%271.6%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27%3E%3Cpath d=%27M3 11.5 12 4l9 7.5%27/%3E%3Cpath d=%27M5 10v9.5a1 1 0 0 0 1 1h4v-6h4v6h4a1 1 0 0 0 1-1V10%27/%3E%3Cpath d=%27M15 4.5V7l2 1.3V4.5z%27/%3E%3C/g%3E%3C/svg%3E"); }
.motif-coin { -webkit-mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill=%27none%27 stroke=%27%23000%27 stroke-width=%271.6%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27%3E%3Ccircle cx=%2712%27 cy=%2712%27 r=%279%27/%3E%3Ccircle cx=%2712%27 cy=%2712%27 r=%276.4%27/%3E%3Cpath d=%27M12 7.5v9M14.5 9.3c-.4-.9-1.4-1.3-2.5-1.3-1.4 0-2.5.7-2.5 1.9 0 2.6 5 1.2 5 3.8 0 1.2-1.2 1.9-2.6 1.9-1.1 0-2.1-.4-2.5-1.3%27/%3E%3C/g%3E%3C/svg%3E"); mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill=%27none%27 stroke=%27%23000%27 stroke-width=%271.6%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27%3E%3Ccircle cx=%2712%27 cy=%2712%27 r=%279%27/%3E%3Ccircle cx=%2712%27 cy=%2712%27 r=%276.4%27/%3E%3Cpath d=%27M12 7.5v9M14.5 9.3c-.4-.9-1.4-1.3-2.5-1.3-1.4 0-2.5.7-2.5 1.9 0 2.6 5 1.2 5 3.8 0 1.2-1.2 1.9-2.6 1.9-1.1 0-2.1-.4-2.5-1.3%27/%3E%3C/g%3E%3C/svg%3E"); }
.motif-calendar { -webkit-mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill=%27none%27 stroke=%27%23000%27 stroke-width=%271.6%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27%3E%3Crect x=%273.5%27 y=%275%27 width=%2717%27 height=%2715%27 rx=%272%27/%3E%3Cpath d=%27M3.5 9.5h17%27/%3E%3Cpath d=%27M8 3v4M16 3v4%27/%3E%3C/g%3E%3Cg fill=%27%23000%27 stroke=%27none%27%3E%3Ccircle cx=%277.7%27 cy=%2713.2%27 r=%27.9%27/%3E%3Ccircle cx=%2712%27 cy=%2713.2%27 r=%27.9%27/%3E%3Ccircle cx=%2716.3%27 cy=%2713.2%27 r=%27.9%27/%3E%3Ccircle cx=%277.7%27 cy=%2716.6%27 r=%27.9%27/%3E%3Ccircle cx=%2712%27 cy=%2716.6%27 r=%27.9%27/%3E%3C/g%3E%3C/svg%3E"); mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill=%27none%27 stroke=%27%23000%27 stroke-width=%271.6%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27%3E%3Crect x=%273.5%27 y=%275%27 width=%2717%27 height=%2715%27 rx=%272%27/%3E%3Cpath d=%27M3.5 9.5h17%27/%3E%3Cpath d=%27M8 3v4M16 3v4%27/%3E%3C/g%3E%3Cg fill=%27%23000%27 stroke=%27none%27%3E%3Ccircle cx=%277.7%27 cy=%2713.2%27 r=%27.9%27/%3E%3Ccircle cx=%2712%27 cy=%2713.2%27 r=%27.9%27/%3E%3Ccircle cx=%2716.3%27 cy=%2713.2%27 r=%27.9%27/%3E%3Ccircle cx=%277.7%27 cy=%2716.6%27 r=%27.9%27/%3E%3Ccircle cx=%2712%27 cy=%2716.6%27 r=%27.9%27/%3E%3C/g%3E%3C/svg%3E"); }
.motif-leaf { -webkit-mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill=%27none%27 stroke=%27%23000%27 stroke-width=%271.6%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27%3E%3Cpath d=%27M5 19c-1-7 2-13.5 14-14.5 1 10-4 15-14 14.5z%27/%3E%3Cpath d=%27M6 18 17 5.3%27/%3E%3Cpath d=%27M9 14.5c1.4-.6 2.6-1.4 3.6-2.4M7 16.8c2-.5 3.8-1.4 5.2-2.6%27/%3E%3C/g%3E%3C/svg%3E"); mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill=%27none%27 stroke=%27%23000%27 stroke-width=%271.6%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27%3E%3Cpath d=%27M5 19c-1-7 2-13.5 14-14.5 1 10-4 15-14 14.5z%27/%3E%3Cpath d=%27M6 18 17 5.3%27/%3E%3Cpath d=%27M9 14.5c1.4-.6 2.6-1.4 3.6-2.4M7 16.8c2-.5 3.8-1.4 5.2-2.6%27/%3E%3C/g%3E%3C/svg%3E"); }
.motif-bag { -webkit-mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill=%27none%27 stroke=%27%23000%27 stroke-width=%271.6%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27%3E%3Cpath d=%27M6 8h12l1 12.5a1 1 0 0 1-1 1.5H6a1 1 0 0 1-1-1.5L6 8z%27/%3E%3Cpath d=%27M8.5 8V6a3.5 3.5 0 0 1 7 0v2%27/%3E%3Cpath d=%27M6.6 12h10.8%27/%3E%3C/g%3E%3Ccircle cx=%279.3%27 cy=%2714.5%27 r=%27.5%27 fill=%27%23000%27 stroke=%27none%27/%3E%3C/svg%3E"); mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill=%27none%27 stroke=%27%23000%27 stroke-width=%271.6%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27%3E%3Cpath d=%27M6 8h12l1 12.5a1 1 0 0 1-1 1.5H6a1 1 0 0 1-1-1.5L6 8z%27/%3E%3Cpath d=%27M8.5 8V6a3.5 3.5 0 0 1 7 0v2%27/%3E%3Cpath d=%27M6.6 12h10.8%27/%3E%3C/g%3E%3Ccircle cx=%279.3%27 cy=%2714.5%27 r=%27.5%27 fill=%27%23000%27 stroke=%27none%27/%3E%3C/svg%3E"); }
.motif-chart { -webkit-mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill=%27none%27 stroke=%27%23000%27 stroke-width=%271.6%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27%3E%3Cpath d=%27M4 20V4M4 20h16%27/%3E%3Crect x=%276.5%27 y=%2714%27 width=%272.6%27 height=%276%27/%3E%3Crect x=%2711%27 y=%2710%27 width=%272.6%27 height=%2710%27/%3E%3Crect x=%2715.5%27 y=%276.5%27 width=%272.6%27 height=%2713.5%27/%3E%3Cpath d=%27M6.5 9.5 11 6l4.5 3 4-4.5%27/%3E%3C/g%3E%3C/svg%3E"); mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill=%27none%27 stroke=%27%23000%27 stroke-width=%271.6%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27%3E%3Cpath d=%27M4 20V4M4 20h16%27/%3E%3Crect x=%276.5%27 y=%2714%27 width=%272.6%27 height=%276%27/%3E%3Crect x=%2711%27 y=%2710%27 width=%272.6%27 height=%2710%27/%3E%3Crect x=%2715.5%27 y=%276.5%27 width=%272.6%27 height=%2713.5%27/%3E%3Cpath d=%27M6.5 9.5 11 6l4.5 3 4-4.5%27/%3E%3C/g%3E%3C/svg%3E"); }
/* Touch devices (iPhone/iPad/Android PWA, operator 23/07/2026): relative to
 * the previous touch rendering, smallest motifs are 30% larger, medium motifs
 * interpolate at 22.5%, and largest motifs are 15% larger. Rows move from a
 * 4% to 3.5% field pitch and all 21 centres sit inside the visible 15%..85%
 * band, so more rows are on-screen and closer together. Trailing items are
 * hidden per tier because the larger marks would otherwise overlap
 * horizontally; 45/26/18 still fills each row evenly. Mouse surfaces (Win11)
 * are untouched. */
@media (pointer: coarse) {
  /* BLEED (operator 26/07/2026, iPad landscape "tremendous bleed of the motif rows"). The row
   * PITCH is 3.5% of the layer's height, but `--icon-h` was driven by `vw`. Those two units
   * track different things, so the ratio between an icon and the space it has to sit in was
   * never fixed: on a phone (small vw) icons stayed inside their pitch, while on an iPad — and
   * worst of all in landscape, where vw is largest and lvh smallest — a tier-3 icon grew to
   * 105px against a pitch of ~27px and the big rows piled 2-3 deep into blobs.
   * Height is now expressed in the SAME unit as the pitch (lvh), so an icon keeps a constant
   * proportion of its row's slot on every screen and orientation. 2.6lvh x the 1.955 tier-3
   * scale = ~5.1lvh, just inside the 5.18lvh pitch, so the largest rows touch without stacking.
   * Width keeps its own clamp (the marks are wider than they are tall) but is bound to the same
   * base so the aspect ratio no longer drifts with the viewport. */
  .boarding-motif { --icon-h: clamp(14px, 2.6lvh, 46px); }
  .motif-icon { width: calc(var(--icon-h) * 1.4 * var(--row-scale, 1)); height: calc(var(--icon-h) * var(--row-scale, 1)); }
  .motif-row { gap: 1px; }
  .motif-row-a,.motif-row-d,.motif-row-g,.motif-row-j,.motif-row-m,.motif-row-p,.motif-row-s { --row-scale: .715; }
  .motif-row-b,.motif-row-e,.motif-row-h,.motif-row-k,.motif-row-n,.motif-row-q,.motif-row-t { --row-scale: 1.225; }
  .motif-row-c,.motif-row-f,.motif-row-i,.motif-row-l,.motif-row-o,.motif-row-r,.motif-row-u { --row-scale: 1.955; }
  .motif-row-a .motif-icon:nth-child(n+46),.motif-row-d .motif-icon:nth-child(n+46),.motif-row-g .motif-icon:nth-child(n+46),.motif-row-j .motif-icon:nth-child(n+46),.motif-row-m .motif-icon:nth-child(n+46),.motif-row-p .motif-icon:nth-child(n+46),.motif-row-s .motif-icon:nth-child(n+46) { display:none; }
  .motif-row-b .motif-icon:nth-child(n+27),.motif-row-e .motif-icon:nth-child(n+27),.motif-row-h .motif-icon:nth-child(n+27),.motif-row-k .motif-icon:nth-child(n+27),.motif-row-n .motif-icon:nth-child(n+27),.motif-row-q .motif-icon:nth-child(n+27),.motif-row-t .motif-icon:nth-child(n+27) { display:none; }
  .motif-row-c .motif-icon:nth-child(n+19),.motif-row-f .motif-icon:nth-child(n+19),.motif-row-i .motif-icon:nth-child(n+19),.motif-row-l .motif-icon:nth-child(n+19),.motif-row-o .motif-icon:nth-child(n+19),.motif-row-r .motif-icon:nth-child(n+19),.motif-row-u .motif-icon:nth-child(n+19) { display:none; }
  .motif-row-a { top: calc(15% - (var(--icon-h) * var(--row-scale) / 2)); }
  .motif-row-b { top: calc(18.5% - (var(--icon-h) * var(--row-scale) / 2)); }
  .motif-row-c { top: calc(22% - (var(--icon-h) * var(--row-scale) / 2)); }
  .motif-row-d { top: calc(25.5% - (var(--icon-h) * var(--row-scale) / 2)); }
  .motif-row-e { top: calc(29% - (var(--icon-h) * var(--row-scale) / 2)); }
  .motif-row-f { top: calc(32.5% - (var(--icon-h) * var(--row-scale) / 2)); }
  .motif-row-g { top: calc(36% - (var(--icon-h) * var(--row-scale) / 2)); }
  .motif-row-h { top: calc(39.5% - (var(--icon-h) * var(--row-scale) / 2)); }
  .motif-row-i { top: calc(43% - (var(--icon-h) * var(--row-scale) / 2)); }
  .motif-row-j { top: calc(46.5% - (var(--icon-h) * var(--row-scale) / 2)); }
  .motif-row-k { top: calc(50% - (var(--icon-h) * var(--row-scale) / 2)); }
  .motif-row-l { top: calc(53.5% - (var(--icon-h) * var(--row-scale) / 2)); }
  .motif-row-m { top: calc(57% - (var(--icon-h) * var(--row-scale) / 2)); }
  .motif-row-n { top: calc(60.5% - (var(--icon-h) * var(--row-scale) / 2)); }
  .motif-row-o { top: calc(64% - (var(--icon-h) * var(--row-scale) / 2)); }
  .motif-row-p { top: calc(67.5% - (var(--icon-h) * var(--row-scale) / 2)); }
  .motif-row-q { top: calc(71% - (var(--icon-h) * var(--row-scale) / 2)); }
  .motif-row-r { top: calc(74.5% - (var(--icon-h) * var(--row-scale) / 2)); }
  .motif-row-s { top: calc(78% - (var(--icon-h) * var(--row-scale) / 2)); }
  .motif-row-t { top: calc(81.5% - (var(--icon-h) * var(--row-scale) / 2)); }
  .motif-row-u { top: calc(85% - (var(--icon-h) * var(--row-scale) / 2)); }
}
@keyframes motif-drift-a { 0%,100%{transform:translateX(-4%);}50%{transform:translateX(5%);} }@keyframes motif-drift-b { 0%,100%{transform:translateX(5%);}50%{transform:translateX(-5%);} }@keyframes motif-drift-c { 0%,100%{transform:translateX(-2%);}50%{transform:translateX(7%);} }@keyframes motif-drift-d { 0%,100%{transform:translateX(6%);}50%{transform:translateX(-3%);} }
.boarding-stage { gap:clamp(13px,2.4vh,24px); animation:boarding-rise .9s cubic-bezier(.32,1.20,.50,1) both; }
.boarding-tree { width:clamp(150px,min(43vw,39vh),252px); animation:boarding-grow 2.4s cubic-bezier(.32,1.20,.50,1) both; filter:drop-shadow(0 13px 19px rgba(29,32,53,.19)); }
@keyframes boarding-grow { from{transform:scale(.88,.82);} to{transform:scale(1);} }
.tree-canopy { transform-box:fill-box; transform-origin:50% 76.4%; animation:canopy-wind 8.8s ease-in-out infinite; }
@keyframes canopy-wind { 0%,100%{transform:rotate(-.5deg) translateX(-1px);} 23%{transform:rotate(1.1deg) translateX(2px);} 47%{transform:rotate(.25deg) translateX(1px);} 68%{transform:rotate(-1.3deg) translateX(-2px);} 83%{transform:rotate(.35deg) translateX(0);} }
.tree-canopy .leaf { animation-name:leaf-gust; animation-timing-function:ease-in-out; }
@keyframes leaf-gust { 0%,100%{transform:rotate(var(--gust-a,-1.4deg)) translateX(0);} 18%{transform:rotate(var(--gust-b,1.8deg)) translateX(var(--gust-x,2px));} 41%{transform:rotate(var(--gust-c,-.6deg)) translateX(0);} 63%{transform:rotate(var(--gust-d,2.3deg)) translateX(calc(var(--gust-x,2px) * -.7));} 82%{transform:rotate(var(--gust-e,.4deg)) translateX(0);} }
.leaf-1,.leaf-7,.leaf-13 { --gust-a:-2.6deg;--gust-b:2.8deg;--gust-c:-.8deg;--gust-d:3.5deg;--gust-e:.4deg;--gust-x:3px;animation-duration:5.9s; }.leaf-2,.leaf-8,.leaf-14 { --gust-a:1.8deg;--gust-b:-2.4deg;--gust-c:.4deg;--gust-d:-3deg;--gust-e:.2deg;--gust-x:2px;animation-duration:7.1s; }.leaf-3,.leaf-9,.leaf-15 { --gust-a:-1.2deg;--gust-b:2.1deg;--gust-c:-.3deg;--gust-d:2.8deg;--gust-e:.5deg;--gust-x:4px;animation-duration:6.4s; }.leaf-4,.leaf-10,.leaf-16 { --gust-a:2.1deg;--gust-b:-1.6deg;--gust-c:.3deg;--gust-d:-2.6deg;--gust-e:-.4deg;--gust-x:2px;animation-duration:7.8s; }.leaf-5,.leaf-11,.leaf-17 { --gust-a:-2deg;--gust-b:1.4deg;--gust-c:-.2deg;--gust-d:2.2deg;--gust-e:.3deg;--gust-x:3px;animation-duration:6.9s; }.leaf-6,.leaf-12 { --gust-a:1.2deg;--gust-b:-2.2deg;--gust-c:.6deg;--gust-d:-2.7deg;--gust-e:0deg;--gust-x:3px;animation-duration:5.5s; }
.tree-trunk { animation:trunk-sway 8.8s ease-in-out infinite; }@keyframes trunk-sway { 0%,100%{transform:rotate(-.12deg);} 50%{transform:rotate(.16deg);} }
.boarding-word { display:flex; flex-direction:column; align-items:flex-start; margin:0; font-size:clamp(2.52rem,8.4vw,5.04rem); font-weight:800; letter-spacing:.12em; color:var(--boarding-word-ink); line-height:.86; text-shadow:0 2px 0 rgba(255,255,255,.12); animation:boarding-word-in .9s cubic-bezier(.32,1.20,.50,1) .18s both; }.boarding-word-new { margin:0 0 .18em .16em; font-size:.36em; font-weight:500; letter-spacing:.15em; color:var(--boarding-new-ink); }.boarding-word-name { display:block; }.boarding-word-in { from{opacity:0;transform:translateY(8px) scale(.97);}to{opacity:1;transform:translateY(0) scale(1);} }
@media (min-width:760px) and (orientation:landscape) { .boarding-stage { grid-template-columns:auto auto; column-gap:clamp(24px,5vw,74px); align-items:center; }.boarding-tree { width:clamp(170px,min(30vw,58vh),310px); }.boarding-word { font-size:clamp(2.88rem,6vw,5.76rem); } }
@media (prefers-reduced-motion:reduce) { .boarding-motif,.motif-row,.tree-canopy,.tree-canopy .leaf,.tree-trunk { animation:none; } }
body {
  /* iOS system font first (operator 2026-07-10: "explore a font that's iOS
   * system that doesn't look so bold and blocky"). -apple-system / SF Pro Text
   * is the native iOS UI face; grayscale antialiasing renders SF noticeably
   * lighter and cleaner than the default subpixel weight, which is what read as
   * "bold and blocky". A hair of negative tracking matches Apple's own UI. */
  font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, 'Segoe UI', sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  letter-spacing: -0.01em;
  background: var(--canvas);
  color: var(--ink);
  line-height: 1.45;
}
body[data-surface="desktop"] { background: var(--canvas-desktop); }
a { color: var(--accent); text-decoration: none; }
.hidden { display: none !important; }
.muted { color: var(--ink-muted); }
.center { text-align: center; }
.pos { color: var(--pos); }
.neg { color: var(--neg); }
.error { color: var(--neg); }
code { background: var(--chip); padding: .1rem .3rem; border-radius: 6px; }

/* ---------- app frame ---------- */
#app { min-height: 100dvh; display: flex; flex-direction: column; max-width: 100%; overflow-x: hidden; }
/* LAUNCH-ZOOM FLOOR, STANDALONE ONLY. `100dvh` is the DYNAMIC viewport, which
 * during the iOS launch zoom reports the still-growing card — the same trap the
 * root documents. `lvh` (the viewport at its largest) covers the settled screen
 * from the first frame. Deliberately NOT unconditional: in a browser TAB `lvh`
 * includes the space behind Safari's collapsible toolbar, so a blanket floor
 * would reserve a strip of dead scroll under every page. `display-mode:
 * standalone` is exactly the case with no collapsible chrome AND the only one
 * that launches via the home-screen zoom. */
@media all and (display-mode: standalone) {
  #app { min-height: 100lvh; }
}
body[data-surface="desktop"] #app { overflow-x: visible; }
.content { flex: 1; padding: 16px; max-width: 720px; margin: 0 auto; width: 100%; min-width: 0; padding-bottom: 104px; }
/* Touch Accounts motion is a bottom sheet on every touch profile. The shell
 * keeps the prior page as an inert snapshot while the current Accounts panel
 * rises from the bottom; leaving Accounts drives that panel back down. */
.route-motion-viewport {
  position: relative; flex: 1 1 auto; min-width: 0; min-height: 0;
  overflow: hidden;
}
.route-motion-viewport > .route-motion-panel { width: 100%; }
.route-motion-panel.outgoing {
  position: absolute; inset: 0; pointer-events: none; z-index: 0;
}
.route-motion-panel.incoming { position: relative; z-index: 1; }
.accounts-sheet-viewport {
  position: fixed; inset: 0; z-index: 15; display: flex; align-items: flex-end;
  /* Keep the previous route readable in the clamped band above the card; the
   * card itself supplies the opaque surface below it. A heavy full-viewport
   * scrim hid the approval banner that defines the stop point. */
  background: color-mix(in srgb, var(--canvas) 24%, transparent);
  overscroll-behavior: contain;
}
.accounts-sheet-viewport .route-motion-panel.incoming {
  width: 100%; max-height: calc(100dvh - max(56px, env(safe-area-inset-top) + 12px));
  overflow-y: auto; overscroll-behavior: contain;
  border-radius: var(--radius) var(--radius) 0 0;
  background: var(--card); box-shadow: 0 -8px 30px color-mix(in srgb, var(--ink) 15%, transparent);
  transform: translateY(100%);
  transition: transform var(--dur-slow) var(--ease-standard);
}
/* The title and approval strip belong to the previous route's shared top rail;
 * when that strip is present, the incoming Accounts card must stop exactly
 * beneath it instead of painting over the banner. With no banner, leave only
 * the compact title-row band above the card. `:has()` is evaluated after the
 * deferred approval mount, so both entry and list→detail replacements settle
 * to the same clamp without a route-specific pixel constant in JavaScript. */
.accounts-sheet-viewport:has(.route-motion-panel.incoming .approve-topbar) .route-motion-panel.incoming {
  max-height: calc(100dvh - max(118px, env(safe-area-inset-top) + 74px));
}
.accounts-sheet-viewport:not(:has(.route-motion-panel.incoming .approve-topbar)) .route-motion-panel.incoming {
  max-height: calc(100dvh - max(104px, env(safe-area-inset-top) + 60px));
}
.accounts-sheet-viewport .route-motion-panel.outgoing { transition: opacity var(--dur-slow) var(--ease-standard); }
.accounts-sheet-viewport.accounts-sheet-dismiss .route-motion-panel.outgoing {
  transform: translateY(0); border-radius: var(--radius) var(--radius) 0 0;
  background: var(--card); overflow-y: auto;
}
.accounts-sheet-viewport.accounts-sheet-dismiss .route-motion-panel.incoming {
  transform: translateY(0); max-height: none !important; overflow-y: auto; border-radius: 0;
  align-self: stretch; height: 100%;
  background: var(--canvas); box-shadow: none;
}
.accounts-sheet-viewport.running.accounts-sheet-enter .route-motion-panel.incoming { transform: translateY(0); }
.accounts-sheet-viewport.accounts-sheet-static .route-motion-panel.incoming { transition: none; }
.accounts-sheet-viewport.running.accounts-sheet-enter .route-motion-panel.outgoing { opacity: .45; }
.accounts-sheet-viewport.running.accounts-sheet-dismiss .route-motion-panel.outgoing { transform: translateY(100%); }
.accounts-sheet-viewport.running.accounts-sheet-dismiss .route-motion-panel.incoming { opacity: 1; }
.accounts-sheet-viewport.accounts-sheet-enter .route-motion-panel.incoming,
.accounts-sheet-viewport.accounts-sheet-dismiss .route-motion-panel.outgoing {
  will-change: transform;
}
.accounts-sheet-content { position: relative; padding-top: 8px; }
.accounts-sheet-grip { margin: 0 auto 4px; width: 40px; height: 5px; border-radius: 999px; background: color-mix(in srgb, var(--ink-muted) 40%, transparent); }
.accounts-sheet-content.swipe-dragging { transition: none !important; }
.accounts-sheet-content.swipe-dismissing { transition: transform var(--dur-slow) var(--ease-standard) !important; }
@media (prefers-reduced-motion: reduce) {
  .route-motion-viewport .route-motion-panel { transition: none !important; }
}
/* Phone: the top nav bar was removed (§2) but its reserved space wasn't
 * reclaimed — content now starts right under the status bar (safe-area top
 * inset only), and the bottom pad = tab bar height + home-indicator inset so
 * the last row clears the floating bar exactly, no dead gap. */
/* TITLE TO THE TOP (operator 2026-07-14: "look at SPENDING tab and see how far
 * from the top the title is… the title on all tabs must be as close to the top as
 * reasonably possible, never truncated, always in the same location"). The top pad
 * is now just enough to clear the status bar and let the fixed chrome cluster
 * (version/eye) sit level with the title's cap-height — 4px of breathing room
 * instead of 12. Because EVERY view's title is the same `.screen-title` with the
 * same margin, they all land on the identical baseline across tabs. */
body[data-surface="phone"] .content {
  padding-top: calc(4px + env(safe-area-inset-top));
  padding-bottom: calc(140px + env(safe-area-inset-bottom));
}
/* The tablet shares the fixed bottom tab bar and FAB, while its base content
 * rule otherwise inherits the desktop-sized 104px pad. Keep the safe-area
 * reservation explicit on both touch surfaces and teach the document scroller
 * the same clearance so scrollIntoView/end and a finger drag cannot place the
 * final useful control under fixed chrome. */
body[data-surface="tablet"] .content {
  padding-bottom: calc(140px + env(safe-area-inset-bottom));
}
@media (max-width: 1199px) {
  html { scroll-padding-bottom: calc(140px + env(safe-area-inset-bottom)); }
}
/* When the approval banner is showing it ALREADY clears the notch (its own
 * safe-area top padding). The content below must NOT add the inset a second time
 * — that doubled strip was the "incredible vertical wasted space" the operator
 * saw above the banner (operator 2026-07-16). Drop the content's top inset to a
 * plain gap whenever the banner is present, on phone and tablet. */
/* (The banner used to sit ABOVE .content and carry its own safe-area padding, so
 * the content dropped its inset to avoid a doubled strip. Since 26/07/2026 the
 * banner renders INSIDE .content, below the screen title, so the content keeps
 * its normal compact top pad in every state — which is also what keeps the title
 * at one height across tabs whether or not anything is awaiting approval.) */

body[data-surface="desktop"] #app { flex-direction: row; }
body[data-surface="desktop"] .content { max-width: none; margin: 0; padding: 20px 28px; padding-bottom: 20px; }
/* The Pricer is a focused desktop utility, not a full-width ledger.  Keep its
 * cards readable on wide monitors while retaining the normal 28px rails at
 * laptop widths. */
@media (min-width: 1600px) {
  body[data-surface="desktop"] .content:has(.pricer-products) {
    max-width: 1240px;
    margin-inline: auto;
  }
}

/* Zero top margin: the title starts at the very top of the content box, so its
 * position is set purely by .content's padding — identical on every tab (operator
 * 2026-07-14). The bottom margin is the ONLY gap under it. */
.screen-title { font-size: 2rem; font-weight: 600; margin: 0 0 .6rem; letter-spacing: -0.02em; }
/* Reserve room for the fixed version/eye chrome cluster (top-right) so a
 * long title never runs under it on phone/tablet (operator 2026-07-08i). Reserve
 * is 152px — the FULL cluster width incl. the version tag (operator 2026-07-13);
 * the old 104px only cleared the eye, so "Spending Report" / "Migrate from
 * YNAB" ran under the version tag. Row titles reset this to 0 (the row carries
 * its own 152px reserve, just below). */
body[data-surface="phone"] .screen-title,
body[data-surface="tablet"] .screen-title { padding-right: 152px; }
body[data-route="budget"][data-surface="phone"] .screen-title,
body[data-route="budget"][data-surface="tablet"] .screen-title { padding-right: 244px; }
/* The fixed chrome cluster (version + eye) is a 40px-tall row whose top is
 * 8px below the safe-area inset — its optical centre sits 28px down. With the
 * content box now starting 4px below that inset, a min-height of 48px on the title
 * makes the title's own centre land on the cluster's, so the two read as ONE top
 * row on every tab rather than the title floating below the buttons. */
body[data-surface="phone"] .screen-title,
body[data-surface="tablet"] .screen-title {
  min-height: 48px; display: flex; align-items: center;
  /* Operator 2026-07-18: vertical compactness — the title already occupies the chrome row, so
   * the space it hands to the content below shrinks; every screen starts ~6px sooner. */
  margin-bottom: .25rem;
}
/* When the title shares a row with an action pill (e.g. Accounts' Add Account),
 * the cluster-clearing pad belongs on the ROW, not the title — otherwise the
 * pill is pushed under the fixed version/eye/gear cluster and runs half
 * offscreen (operator 2026-07-08j). Move the reserve to the row and let the
 * title sit flush inside it. */
body[data-surface="phone"] .screen-title-row .screen-title,
body[data-surface="tablet"] .screen-title-row .screen-title,
body[data-surface="phone"] .sched-titlerow .screen-title,
body[data-surface="tablet"] .sched-titlerow .screen-title { padding-right: 0; }
/* Reserve must clear the WHOLE fixed cluster: eye (right 58 +40w) + version tag
 * (right 104, ~48px wide for a "vX.Y.Z" string) → its left edge lands ~152px
 * from the right. The old reserve only cleared the eye, so the version tag
 * overlapped the Spending "Select" button
 * (operator 2026-07-13). 152px matches the intent the code comments already
 * claimed and leaves the title/tools stopping just left of the version tag. */
body[data-surface="phone"] .screen-title-row,
body[data-surface="tablet"] .screen-title-row,
body[data-surface="phone"] .sched-titlerow,
body[data-surface="tablet"] .sched-titlerow { padding-right: 152px; }
body[data-surface="desktop"] .screen-title { font-size: var(--fs-h2); }
/* Short landscape: the 2rem title + its 48px row + margin spend ~58px of a
 * 393px screen on one word. Ease it down a step and let it sit on the chrome
 * cluster's own 40px line — still the same weight, family and tokens, still the
 * biggest thing on the page, just not a third of the viewport (operator
 * 2026-07-17). Portrait is untouched. */
@media (orientation: landscape) and (max-height: 500px) {
  body[data-surface="phone"] .screen-title {
    font-size: 1.5rem; min-height: 40px; margin-bottom: .3rem;
  }
  body[data-surface="phone"] .screen-title-row,
  body[data-surface="phone"] .sched-titlerow { min-height: 40px; }
}

/* ---------- cards, rows, buttons (§1) ---------- */
.card {
  background: var(--card);
  border-radius: var(--radius);
  padding: 16px 18px;
  margin-bottom: 14px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
body[data-surface="desktop"] .card { border-radius: var(--radius-s); border: 1px solid var(--hairline); box-shadow: none; }
.card h2 { font-size: var(--fs-value); margin: 0 0 .6rem; }

.btn {
  display: inline-flex; align-items: center; justify-content: center; border: 0; cursor: pointer; font: inherit; font-weight: 600;
  background: var(--control-accent); color: var(--control-accent-ink);
  padding: .55rem 1.2rem; border-radius: 999px; min-height: var(--ctl-h-md);
}
/* Enabled secondary action (D-102 part D, 19/08/2026): pale accent-derived
 * fill, not the old flat --chip grey — a genuinely CLICKABLE secondary button
 * (File Import, Undo, Expand splits, View…) must read as part of the same
 * blurple control family as the primary action beside it, one step down in
 * emphasis. Disabled and selected/open states override this below so neither
 * can be confused with plain "enabled". */
.btn.secondary { background: var(--btn-secondary-bg); color: var(--btn-secondary-ink); }
@media (hover: hover) {
  .btn.secondary:not(:disabled):not([aria-disabled="true"]):hover { background: var(--control-secondary-bg-hover); }
}
/* A disabled secondary control must stay unmistakably muted — never the pale
 * accent fill above, which would read as clickable. */
.btn.secondary:disabled, .btn.secondary[aria-disabled="true"] {
  background: var(--chip); color: var(--ink-muted); opacity: .55; cursor: default;
}
/* Selected/open/toggled secondary controls (e.g. a pressed filter, an expanded
 * "View" menu button) stay STRONGER than a merely-enabled secondary button —
 * the existing `.on`/[aria-pressed="true"]/[aria-expanded="true"] convention
 * used across the app's toggle buttons. */
.btn.secondary.on, .btn.secondary[aria-pressed="true"], .btn.secondary[aria-expanded="true"] {
  background: color-mix(in srgb, var(--accent) 30%, var(--card));
  color: var(--btn-secondary-ink);
}
/* General press feedback for tappable controls that don't already define
 * their own :active (Part 7) — the FAB's .93 and .reg-icon-btn's .88 stay
 * as their own bespoke bounce; this is the default for everything else.
 *
 * 2026-07-25 (operator: "live button feedback"). These rules read as press
 * feedback but every one of them resolved to `transform: none` — i.e. NO
 * visible change on press at all, which is exactly the "tapped it, nothing
 * happened" report. Each control must restyle ITSELF on press (cardinal
 * button-feedback rule): a real scale-down plus a slight darken, fast enough
 * to paint within a tap. `transition` on the base rule carries it back out. */
.btn, .type-pill { transition: transform var(--dur-fast) var(--ease-spring), filter var(--dur-fast) var(--ease-standard); }
.btn:active, .type-pill:active, .key:active { transform: scale(.96); filter: brightness(.92); }
/* Disabled controls must NOT appear to respond — a press that visibly reacts
 * but does nothing is worse than no feedback. */
.btn:disabled:active, .btn[aria-disabled="true"]:active { transform: none; filter: none; }
@media (prefers-reduced-motion: reduce) {
  .btn:active, .type-pill:active, .key:active { transform: none; filter: brightness(.9); }
}
.btn.danger { background: var(--control-danger); color: var(--control-danger-ink); }
/* Buttons that lead with an inline icon: centre the glyph with the label. */
.btn:has(.ico), .fab-txn:has(.ico), .add-account-pill:has(.ico) { display: inline-flex; align-items: center; gap: 6px; }
.btn .ico, .fab-txn .ico, .add-account-pill .ico { width: 1.05em; height: 1.05em; }
.plan-pill-plus { display: inline-flex; align-items: center; justify-content: center; }
.plan-pill-plus .ico { width: 1em; height: 1em; }
.btn-x, .link-btn {
  background: none; border: 1px solid var(--hairline); color: var(--ink-muted);
  border-radius: 8px; cursor: pointer; font: inherit; font-size: var(--fs-body); padding: .15rem .5rem;
}
.link-btn { border: 0; }
/* Touch: .btn-x measured ~30px wide (glyph + thin padding only, v1.29.0
 * tapthru §1). Width floor to match the blanket 44px height floor below. */
body[data-surface="phone"] .btn-x,
body[data-surface="tablet"] .btn-x {
  min-width: var(--ctl-h-md);
  display: inline-flex; align-items: center; justify-content: center;
}

.list-row, .setting-row {
  display: flex; justify-content: space-between; align-items: center; gap: 12px;
  padding: .6rem 0; border-top: 1px solid var(--hairline); color: inherit;
}
.list-row:first-of-type, .setting-row:first-of-type { border-top: 0; }
/* Plan Settings row label with its leading pencil (operator 2026-07-12 punch 16:
 * the pencil relocated here from the Plan header). The glyph sits to the left of
 * the label text, muted, aligned to the first line. */
.ps-label { display: flex; align-items: flex-start; gap: 8px; }
.ps-pencil { flex: 0 0 auto; color: var(--ink-muted); display: inline-flex; position: relative; top: 1px; }
.ps-pencil .ico { width: 1.15rem; height: 1.15rem; }
/* Location grant row (operator 2026-07-12 punch 16): the amber warning triangle
 * sits to the left of the "Allow location access" button while access is not yet
 * granted. Amber is a fixed advisory colour that reads in both themes. */
.loc-grant-row { justify-content: flex-start; gap: 10px; }
.loc-warn { flex: 0 0 auto; display: inline-flex; color: var(--amber); }
.loc-warn .ico { width: 1.25rem; height: 1.25rem; }

/* iOS-style toggle (operator: "switch all selectors to iOS style slider instead
   of checkbox"). Every TRAILING checkbox in a Settings row becomes a sliding
   pill switch — blurple when on, matching the app's control theme. Scoped to DIRECT
   children of .setting-row so leading list checkboxes (e.g. payee-picker bulk
   rows, where the box sits before its label) keep their native box. */
.setting-row > input[type="checkbox"],
input.ios-switch,
.target-switch {
  appearance: none; -webkit-appearance: none;
  flex: 0 0 auto; position: relative; box-sizing: border-box;
  --switch-track-width: 52px;
  --switch-track-height: 32px;
  --switch-knob-size: 28px;
  --switch-track-border: 0px;
  --switch-knob-border: 0px;
  --switch-inset: calc((var(--switch-track-height) - var(--switch-knob-size)) / 2 - var(--switch-track-border));
  --switch-travel: calc(var(--switch-track-width) - var(--switch-track-border) - var(--switch-track-border) - var(--switch-knob-size) - var(--switch-inset) - var(--switch-inset));
  width: var(--switch-track-width); height: var(--switch-track-height); margin: 0; padding: 0; border: var(--switch-track-border) solid transparent; border-radius: 999px;
  background: color-mix(in srgb, var(--ink-muted) 45%, transparent);
  transition: background var(--dur-base) var(--ease-ios-rebound); cursor: pointer;
}
/* Keep the input-owned selector explicit for motion contracts and tooling;
 * geometry remains shared above with target switches. */
input.ios-switch {
  transition: background var(--dur-base) var(--ease-ios-rebound);
}
.setting-row > input[type="checkbox"]::after,
input.ios-switch::after,
.target-switch::after {
  content: ""; position: absolute; top: var(--switch-inset); left: var(--switch-inset);
  width: var(--switch-knob-size); height: var(--switch-knob-size); box-sizing: border-box; border: var(--switch-knob-border) solid transparent; border-radius: 50%; background: var(--card);
  box-shadow: none; transition: transform var(--dur-base) var(--ease-ios-rebound);
}
input.ios-switch::after {
  transition: transform var(--dur-base) var(--ease-ios-rebound);
}
.setting-row > input[type="checkbox"]:checked,
input.ios-switch:checked { background: var(--accent); }
.setting-row > input[type="checkbox"]:checked::after,
input.ios-switch:checked::after,
.target-switch.on::after { transform: translateX(var(--switch-travel)); }
/* WIN11 DESKTOP ONLY, 80% scale (operator 05/08/2026: "the slider button is far
 * too big to be ontheme, win11-wide. make it 20% smaller"). Touch keeps the base
 * 52x32 above — an iOS switch must stay a 44px-class target, so this override is
 * surface-scoped and never reaches phone or tablet.
 *
 * Arithmetic (every figure = base x 0.8):
 *   track  52 x 0.8 = 41.6px wide,  32 x 0.8 = 25.6px tall
 *   knob   28 x 0.8 = 22.4px       (square: 22.4 x 22.4, so 50% stays a circle)
 *   inset  base gap is (32 - 28) / 2 = 2px  ->  (25.6 - 22.4) / 2 = 1.6px
 *   travel = track - knob - inset(left) - inset(right)
 *          = 41.6 - 22.4 - 1.6 - 1.6 = 16px
 *   check: knob right edge when checked = 1.6 + 16 + 22.4 = 40px,
 *          leaving 41.6 - 40 = 1.6px — the SAME inset as the left. Flush. */
body[data-surface="desktop"] .setting-row > input[type="checkbox"],
body[data-surface="desktop"] .target-switch,
body[data-surface="desktop"] input.ios-switch {
  --switch-track-width: 41.6px;
  --switch-track-height: 25.6px;
  --switch-knob-size: 22.4px;
  width: 41.6px; height: 25.6px;
}
body[data-surface="desktop"] .setting-row > input[type="checkbox"]::after,
body[data-surface="desktop"] .target-switch::after,
body[data-surface="desktop"] input.ios-switch::after {
  top: var(--switch-inset); left: var(--switch-inset); width: 22.4px; height: 22.4px;
}
body[data-surface="desktop"] .setting-row > input[type="checkbox"]:checked::after,
body[data-surface="desktop"] .target-switch.on::after,
body[data-surface="desktop"] input.ios-switch:checked::after { transform: translateX(var(--switch-travel)); }
.setting-row > input[type="checkbox"]:disabled,
input.ios-switch:disabled { opacity: .45; cursor: default; }
.setting-row > input[type="checkbox"]:focus-visible,
input.ios-switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.settings-subsection { margin-top: .75rem; padding: .7rem .8rem; border: 1px solid var(--hairline); border-radius: var(--radius-s); }
.settings-subsection > h3 { margin: 0 0 .25rem; }
.envelope-dependent[data-dependent-disabled="true"] { background: var(--chip); color: var(--ink-muted); }
.envelope-dependent[data-dependent-disabled="true"] .muted { color: var(--ink-muted); }
.rta-reset-row[data-dependent-disabled="true"] {
  margin-block: .35rem; padding: .7rem .8rem;
  border-top-color: transparent; border-radius: var(--radius-s);
}
.group-head { display: flex; justify-content: space-between; align-items: baseline; }
.stat-row { display: flex; justify-content: space-between; font-size: var(--fs-h3); }

/* ---------- Home dashboard (§7 OFF-variant) ---------- */
.dash-headline { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.dash-headline-label { font-size: var(--fs-value); color: var(--ink-muted); }
/* The Smoothed-monthly-burden figure. Was --fs-display (2.4rem) — that rung is
 * for the Add sheet's full-width amount readout, and it overpowered this card
 * (operator 2026-07-14: "far too large, make smaller"). */
.dash-headline-amt { font-size: var(--fs-h2); font-weight: 700; }
.dash-sub { margin: .5rem 0 0; font-size: var(--fs-body); }
.dash-sched-main { display: flex; flex-direction: column; min-width: 0; }
.dash-sched-name { font-weight: 600; }
.dash-sched-meta { font-size: var(--fs-body); }
.dash-sched-amt, .dash-bill-amt { white-space: nowrap; }
.dash-per { font-weight: 400; font-size: var(--fs-body); }
.dash-bill { gap: 10px; }
.dash-bill-date { flex: 0 0 auto; font-size: var(--fs-body); min-width: 6.5rem; }
.dash-bill-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.inp {
  font: inherit; padding: .45rem .6rem; border-radius: 8px;
  border: 1px solid var(--hairline); background: var(--card); color: var(--ink);
  min-width: 0;
}
.inp:focus { outline: none; border-color: var(--accent); }
/* One ring, not two (part F, 19/08/2026): on a fine pointer the generic
 * `body :is(...):focus-visible` rule below already paints an external accent
 * outline; the border-color shift above then adds a second, concentric
 * accent stroke on the same box. Neutralise the border on the fine-pointer
 * focus-visible state so the external ring is the only line. Coarse pointers
 * (touch) never reach `:focus-visible` here — CARDINAL rule 20 gates the ring
 * itself behind `pointer: fine` — and keep the plain `:focus` border as
 * their sole affordance. */
@media (pointer: fine) {
  .inp:focus-visible { border-color: var(--hairline); }
}
select.inp {
  appearance: none; -webkit-appearance: none; padding-right: 2.35rem;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--ink-muted) 50%),
    linear-gradient(135deg, var(--ink-muted) 50%, transparent 50%);
  background-position: calc(100% - 16px) 50%, calc(100% - 11px) 50%;
  background-size: 5px 5px, 5px 5px; background-repeat: no-repeat;
}
body :is(button, [role="button"], a, input, select, textarea):focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
/* FREE-TEXT/SEARCH FIELDS KEEP NO RING ON TOUCH (operator 03/08/2026: "an
 * uncalled for outline of the memo field… this is against design theme";
 * widened app-wide 21/08/2026 — the same cascade defeat persisted on every
 * OTHER free-text/search field outside a txn-sheet, e.g. the Spending search
 * pill, Plan/Payees/Accounts rename and search fields: a blue inner rectangle
 * could still paint even where the compound control's OWN
 * `:focus-visible{outline:none}` rule existed, because that rule was scoped
 * `@media (pointer: fine)` — coarse pointers never reached it).
 * iOS/Safari marks a TAPPED text input `:focus-visible`, so the ring below —
 * written for Tab travel — fires on every touch of a text field and draws a
 * 2px accent rectangle round it. A field's own `:focus { outline: none }`
 * LOSES THE CASCADE: `:focus` and `:focus-visible` both match, and
 * `body :is(…):focus-visible` (0,2,1) out-specifies a plain class-level
 * `:focus` (0,1,0) or `:focus-visible` (0,1,1) rule. So this is a cascade
 * defeat, not a missing rule. `input[type=search]`/`input[type=text]` (no
 * explicit type defaults to text) and `textarea` cover every ordinary
 * free-text/search control app-wide without touching specialist inputs
 * (money/date/number/checkbox/radio/select/file, which use other types).
 * Scoped to coarse pointers so Win11 Tab travel, the reason the ring exists
 * at all, is untouched — CARDINAL rule 20: touch gets the caret and the OS
 * keyboard, never a desktop keyboard-focus rail. */
@media (pointer: coarse) {
  body :is(input:not([type]), input[type="text"], input[type="search"], textarea):focus-visible {
    outline: none;
  }
}
body :is(.txn-row, .reg-txn, .plan-add-cat, .plan-activity-link, .plan-cat,
  .sched-row, .sched-txn-row, .sched-occ-toggle):focus-visible {
  outline: 2px solid var(--accent); outline-offset: -2px;
}

/* ---------- banners ---------- */
.banner { padding: .6rem 1rem; font-weight: 600; }
/* Black-translucent status bar (punch 20): an in-flow banner is the topmost
 * element on phone/tablet, so pad it below the clock/Dynamic Island. Desktop
 * overrides .banner to fixed with no inset (no status bar there). */
body[data-surface="phone"] .banner,
body[data-surface="tablet"] .banner { padding-top: calc(.6rem + env(safe-area-inset-top)); }
.banner-integrity { background: var(--neg); color: var(--neg-ink); }
.banner-offline { background: var(--ink-muted); color: var(--accent-ink); }
/* Uncaught code errors are recoverable guidance, not raw red developer text.
 * Integrity/hash failures keep the red banner above; this card uses theme
 * tokens in both modes and stays inside every safe-area/visual viewport. */
.unexpected-error-overlay {
  position: fixed; inset: 0; z-index: 10000; display: grid; place-items: center;
  padding: max(16px, env(safe-area-inset-top)) max(16px, env(safe-area-inset-right))
    max(16px, env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));
  background: color-mix(in srgb, var(--ink) 42%, transparent);
}
.unexpected-error-card {
  width: min(460px, 100%); max-height: min(680px, calc(100dvh - 32px));
  overflow: auto; display: grid; grid-template-columns: auto minmax(0, 1fr);
  gap: 12px 14px; padding: 18px; border: 1px solid var(--hairline);
  border-radius: 18px; background: var(--card); color: var(--ink);
  box-shadow: 0 12px 38px color-mix(in srgb, var(--ink) 28%, transparent);
}
.unexpected-error-mark {
  display: inline-grid; place-items: center; width: 36px; height: 36px;
  border-radius: 50%; background: var(--neg-pill); color: var(--neg);
  font-size: var(--fs-h3); font-weight: 800;
}
.unexpected-error-copy { min-width: 0; }
.unexpected-error-copy h2 { margin: 2px 0 8px; font-size: var(--fs-h2); }
.unexpected-error-copy p { margin: 0 0 8px; line-height: 1.45; }
.unexpected-error-details { margin-top: 10px; }
.unexpected-error-details summary { cursor: pointer; color: var(--ink-muted); font-weight: 600; }
.unexpected-error-details code {
  display: block; margin-top: 8px; padding: 8px; white-space: pre-wrap;
  overflow-wrap: anywhere; color: var(--ink);
}
.unexpected-error-actions {
  grid-column: 1 / -1; display: flex; justify-content: flex-end; gap: 8px;
  padding-top: 4px; border-top: 1px solid var(--hairline);
}
@media (max-width: 420px) {
  .unexpected-error-card { grid-template-columns: 1fr; padding: 16px; }
  .unexpected-error-mark { width: 32px; height: 32px; }
  .unexpected-error-actions { flex-direction: column-reverse; }
  .unexpected-error-actions .btn { width: 100%; min-height: 46px; }
}
/* Reconnecting status (item 1): a small, quiet route-topbar mark that
 * self-clears when sync lands. The text remains available to assistive tech. */
.sync-strip {
  display: inline-flex; align-items: center; justify-content: center;
  box-sizing: border-box; flex: 0 0 2.75rem; width: 2.75rem; min-height: var(--ctl-h-md);
  padding: 0; border: 0; border-radius: 0; background: transparent;
  color: var(--accent); font-size: var(--fs-label); font-weight: 600; vertical-align: middle;
}
/* Touch sync is a passive status marker, not an interactive control. Keep the
 * 44px row height for alignment, but reserve only the dot plus its breathing
 * room so a late-arriving sync state cannot squeeze a route title into
 * arbitrary character fragments. Desktop overrides this footprint below. */
body:is([data-surface="phone"], [data-surface="tablet"]) .sync-strip {
  flex: 0 0 16px; width: 16px; min-width: 16px; min-height: var(--ctl-h-md);
}
/* Desktop alert banners stay overlays; the quiet sync mark stays in the route
 * header's normal flow so it never occludes useful content. */
body[data-surface="desktop"] .banner {
  position: fixed; top: 0; left: 0; right: 0; z-index: 40;
}
body[data-surface="desktop"] .sync-strip {
  position: static; flex: 0 0 auto; white-space: nowrap;
}
.sync-topbar { margin-inline: .35rem; }
.sync-icon {
  display: inline-block; width: 8px; height: 8px; flex: 0 0 8px;
  border-radius: 50%; background: var(--accent); transform-origin: center;
  animation: sync-pulse 2.8s ease-in-out infinite;
}
/* Explicit pull-to-refresh (item 2): the strip can appear before syncState has
 * actually reached 'syncing' (a manual pull's request-accepted phase). Muted,
 * static grey — never the passive-sync blurple pulse — until sync is genuinely
 * active, at which point .sync-strip-muted is removed and the normal pulse
 * takes over on the same element (no separate indicator to invent). */
.sync-strip-muted .sync-icon { background: var(--ink-muted); animation: none; }
/* Touch sync dot 10% larger (8.8px) than the 8px base — touch only, desktop
 * sidebar marker unchanged (D-102 batch, item 2). */
body:is([data-surface="phone"], [data-surface="tablet"]) .sync-icon {
  width: 8.8px; height: 8.8px; flex-basis: 8.8px;
}
.sync-copy {
  /* Keep the status name in the accessibility tree while removing its visual
   * width; role=status still announces the real state. */
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
/* Desktop owns the sync status in the sidebar footer. Expanded sidebars show
 * the canonical copy; the collapsed rail keeps the icon only so it never
 * overflows the 64px utility column. Touch keeps the compact topbar copy
 * hidden through the base rule above. */
body[data-surface="desktop"] .sidebar-footer .sync-strip {
  width: 100%; min-width: 0; flex: 0 1 auto; justify-content: flex-start;
  gap: .5rem; padding: .35rem .6rem; color: var(--sidebar-ink); text-align: left;
}
body[data-surface="desktop"] .sidebar-footer .sync-copy {
  position: static; width: auto; height: auto; padding: 0; margin: 0;
  overflow: visible; clip: auto; white-space: normal; overflow-wrap: anywhere;
  border: 0; line-height: 1.25;
}
body[data-surface="desktop"] .sidebar.collapsed .sidebar-footer .sync-strip {
  justify-content: center; padding-inline: .5rem;
}
body[data-surface="desktop"] .sidebar.collapsed .sidebar-footer .sync-copy {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap;
}
@keyframes sync-pulse { 0%, 100% { opacity: 0; transform: scale(.94); } 50% { opacity: 1; transform: scale(1); } }
/* Desktop Google Drive status is a quiet dot whose pulse is deliberately
 * measurable: the visible radius travels ±15% around its resting size. Touch
 * keeps the compact top-bar pulse above. */
body[data-surface="desktop"] .sync-icon { animation: sync-pulse-desktop 1.8s cubic-bezier(.34, 1.56, .64, 1) infinite; opacity: 1; }
@keyframes sync-pulse-desktop { 0%, 100% { opacity: .72; transform: scale(.85); } 50% { opacity: 1; transform: scale(1.15); } }
@media (prefers-reduced-motion: reduce) {
  .sync-icon { animation: none; opacity: 1; transform: none; }
}
/* In-list approval banner (register.js). Deliberately NOT `.banner`: that class
 * is the app-level alert strip — desktop fixes it to the viewport top, where it
 * covered .approve-topbar and .plan-switcher (v1.29.0 tapthru). font-weight 600
 * kept here because dropping `.banner` dropped its weight. */
.banner-approve {
  background: var(--accent); color: var(--accent-ink); border-radius: var(--radius-s);
  margin-bottom: 12px; padding: .6rem 1rem; font-weight: 600;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
}
/* No desktop rules here any more (D-097). The in-card banner is TOUCH-ONLY now:
 * desktop renders the shell's single `.approve-topbar` instead, so the fixed
 * 54px height and the `visibility:hidden` placeholder that reserved that slot
 * are both gone. Between them they held 54px of the register's vertical budget
 * even when there was nothing to approve. */
.banner-approve .ba-text { min-width: 0; margin-right: auto; }
.banner-approve .ba-approve {
  flex: 0 0 auto; border: 0; background: var(--card); color: var(--accent);
  font: inherit; font-weight: 700; font-size: var(--fs-body); padding: .4rem .9rem;
  border-radius: 999px; cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-spring);
}
.banner-approve .ba-approve:active { transform: scale(.95); filter: brightness(.93); }
.banner-approve .ba-approve:disabled { opacity: .5; }
/* Approve All is the primary action and sits immediately after View. Keep its
 * solid blurple face on every surface so the two adjacent actions cannot be
 * mistaken for duplicate ghost links. */
.banner-approve .ba-approve {
  background: var(--accent); color: var(--accent-ink);
  border: 1px solid color-mix(in srgb, var(--accent-ink) 28%, transparent);
}
/* "View" (§11.2): drops the Is: Unapproved or Needs Category filter into search.
   Ghost variant so the solid "Approve all" stays the primary action. */
.banner-approve .ba-view {
  flex: 0 0 auto; border: 1.5px solid color-mix(in srgb, var(--accent-ink) 80%, transparent); background: transparent;
  color: var(--accent-ink); font: inherit; font-weight: 700; font-size: var(--fs-body); padding: .35rem .9rem;
  border-radius: 999px; cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-spring);
}
.banner-approve .ba-view:active { transform: scale(.95); filter: brightness(.93); }

/* Hidden-by-search note + View All pill under the grid rows (§11.2). */
/* Hidden-by-search note. The reference video CENTRES this under the last row
 * with the note on its own line and the button beneath it, so it reads as the
 * end of the filtered list rather than another row. */
.reg-hidden-note {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  padding: 1rem; font-size: var(--fs-body); text-align: center;
}
/* "View All" is the OTHER way back to the unfiltered register, so it carries the
 * same enlargement as the toolbar Clear (operator 04/08/2026: a larger clear
 * button on every search filter). Was .3rem/.9rem — a pill smaller than the text
 * beside it. */
.reg-hidden-note .reg-viewall {
  flex: 0 0 auto;
  padding: .5rem 1.6rem;
  font-size: var(--fs-body); font-weight: 700;
}

/* "Strong work!" celebration panel (§11.2), shown in the grid when is:unapproved
   has nothing left to approve. */
.reg-strongwork {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: 8px; padding: 2rem 1.5rem; margin: 8px;
}
.reg-strongwork .sw-art { color: var(--accent); }
.reg-strongwork .sw-balloon { width: 56px; height: 56px; }
.reg-strongwork .sw-title { margin: 0; font-size: var(--fs-h2); }
.reg-strongwork .sw-lead { margin: 0; color: var(--ink-muted); }
.reg-strongwork .sw-actions { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; margin-top: 8px; }

/* ---------- desktop sidebar (§11.1) ---------- */
.sidebar {
  width: 260px; flex-shrink: 0; min-height: 100dvh;
  background: var(--sidebar); color: var(--sidebar-ink);
  padding: 14px 10px; display: flex; flex-direction: column; gap: 10px;
  position: sticky; top: 0; align-self: flex-start; max-height: 100dvh; overflow-y: auto;
}
.plan-switcher {
  background: none; border: 0; color: inherit; font: inherit; font-weight: 700;
  text-align: left; padding: .5rem .6rem; border-radius: 8px; cursor: pointer;
  display: flex; justify-content: space-between; width: 100%;
}
@media (hover: hover) { .plan-switcher:hover { background: var(--sidebar-hover); } }
.plan-switcher.on {
  background: var(--sidebar-active);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14);
}
.nav-items { display: flex; flex-direction: column; gap: 2px; }
.nav-item {
  color: var(--sidebar-ink); padding: .45rem .6rem; border-radius: 8px; font-weight: 600;
}
@media (hover: hover) { .nav-item:hover { background: var(--sidebar-hover); } }
.nav-item.active { background: var(--sidebar-active); }
.acct-group-head {
  display: flex; justify-content: space-between; font-size: var(--fs-label); letter-spacing: .08em;
  color: var(--sidebar-muted); padding: .7rem .6rem .2rem;
}
.acct-row {
  display: flex; justify-content: space-between; align-items: center; gap: 8px; color: var(--sidebar-ink);
  padding: .3rem .6rem; border-radius: 8px; font-size: var(--fs-body);
}
@media (hover: hover) { .acct-row:hover { background: var(--sidebar-hover); } }
.acct-row.active { background: var(--sidebar-active); }
/* Name may wrap to two lines (e.g. "Mount Sinai Reconstruction"); min-width:0
 * lets it wrap without starving the balance pill of its space. */
.acct-name { min-width: 0; }
/* The negative balance is a red PILL. It must hug its own single line of text —
 * never stretch to the full height of a two-line account name (that ballooned
 * the red box and floated the figure), never shrink so the minus sign wraps a
 * row above the number, on any platform. */
.acct-bal.neg {
  background: var(--neg); color: var(--neg-ink); border-radius: 999px; padding: 0 .5rem;
  white-space: nowrap; align-self: center; flex-shrink: 0; height: fit-content;
}
.add-account {
  color: var(--sidebar-muted); padding: .5rem .6rem; font-size: var(--fs-body);
  background: none; border: none; text-align: left; cursor: pointer; width: 100%;
  display: flex; align-items: center; gap: 6px;
}
@media (hover: hover) { .add-account:hover { color: var(--sidebar-ink); } }
.add-account .ico { width: 1em; height: 1em; }
/* Plan-switcher caret + inline near-you pin inherit text colour/size. */
.plan-caret .ico { width: .8em; height: .8em; }

/* Signed-in account email under the plan name (§11.1): small grey, single
 * line, ellipsised so a long address never widens the rail. */
.sidebar-email {
  color: var(--sidebar-muted); font-size: var(--fs-label); padding: 0 .6rem;
  margin-top: -6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Nav item icon + label: expanded shows the label only (the sidebar's classic
 * text look); collapsed shows the icon only. */
.nav-item { display: flex; align-items: center; gap: 8px; }
.nav-item .nav-ico { display: none; line-height: 0; }
.nav-item .nav-ico .ico { width: 20px; height: 20px; }

/* Collapsed icon rail (§11.1 footer toggle): narrow, icons only; plan
 * switcher, email and account rows hidden. Clicking an icon navigates as
 * usual; the footer icon expands. */
.sidebar.collapsed { width: 64px; padding: 14px 8px; }
.sidebar.collapsed .plan-switcher,
.sidebar.collapsed .sidebar-email,
.sidebar.collapsed .nav-item .nav-label,
.sidebar.collapsed .sidebar-collapse .nav-label { display: none; }
.sidebar.collapsed .nav-item { justify-content: center; padding: .55rem; }
.sidebar.collapsed .nav-item .nav-ico { display: block; }
.sidebar.collapsed .sidebar-collapse { justify-content: center; }

/* Footer collapse toggle: pinned to the sidebar foot, muted until hover. The
 * bottom margin keeps it clear of the fixed version + eye + gear cluster that
 * floats at the desktop bottom-left (40px pills at bottom:14px). */
.sidebar-collapse {
  margin-top: 0; margin-bottom: 0; background: none; border: 0; cursor: pointer;
  color: var(--sidebar-muted); font: inherit; font-size: var(--fs-body);
  display: flex; align-items: center; gap: 8px; padding: .5rem .6rem;
  border-radius: 8px; text-align: left;
}
@media (hover: hover) { .sidebar-collapse:hover { color: var(--sidebar-ink); background: var(--sidebar-hover); } }
.sidebar-collapse .nav-ico { line-height: 0; }
.sidebar-collapse .nav-ico .ico { width: 18px; height: 18px; }
/* Sync status and collapse are one footer unit. Keeping margin-top:auto on the
 * button itself left a large blank flex gap between the in-flow strip and the
 * control, making a quiet status look like a floating banner. */
.sidebar-footer {
  margin-top: auto; margin-bottom: 56px;
  display: flex; flex-direction: column; gap: 4px; min-width: 0;
}

/* ---------- Accounts screen (§5) ---------- */
.screen-title-row { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
:root { --accounts-value-column: 7.2rem; --accounts-options-column: 44px; }
.accounts-route-root .group-head {
  display: grid; grid-template-columns: minmax(0, 1fr) var(--accounts-value-column) var(--accounts-options-column);
  align-items: center; column-gap: 8px;
}
.accounts-route-root .group-head > strong { grid-column: 2; min-width: 0; text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.acct-list-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 8px;
  transition: background var(--dur-fast) var(--ease-standard);
}
/* The press fill is a shape, not just a colour: an un-rounded rectangle inside
 * a 24px-rounded card read as off-theme against every other fill in the app
 * (operator 31/08/2026). --radius-s matches the iPad-landscape active row
 * below, so one account highlight has one shape on every touch surface. */
body[data-surface="phone"] .acct-list-row.account-row-pressing,
body[data-surface="tablet"] .acct-list-row.account-row-pressing {
  background: color-mix(in srgb, var(--accent) 14%, var(--card));
  border-radius: var(--radius-s);
}
/* align-items: center so a name that wraps to 2 rows (e.g. "Mount Sinai
 * Reconstruction") keeps its balance VERTICALLY CENTRED against the taller name
 * block, not pinned to the first line's baseline (operator 2026-07-12; applied
 * uniformly to the split-view row below too). */
.acct-list-link { display: grid; grid-template-columns: minmax(0, 1fr) var(--accounts-value-column); flex: 1; min-width: 0; gap: 8px; align-items: center; }
/* Let a long account name wrap while the balance stays on ONE line so the minus
 * sign never breaks onto its own row above the digits (operator 2026-07-10:
 * "−" and "$27,496.20" must share a row). Name shrinks/wraps; amount holds. */
.acct-list-link > span:first-child { min-width: 0; }
.acct-list-link > span:last-child { min-width: 0; text-align: right; white-space: nowrap; }
.acct-split-select > span:first-child { min-width: 0; }
.acct-split-select > span:last-child { min-width: 0; text-align: right; white-space: nowrap; }
/* Add Account relocated to a right-aligned button at the bottom of the list
 * (operator 2026-07-10). The bottom margin lifts it clear of the fixed
 * +Transaction FAB (also bottom-right) so the two never overlap when the list
 * is scrolled to the end. */
.acct-add-bottom { display: flex; justify-content: flex-end; margin: 12px 0 56px; }
body[data-surface="tablet"] .acct-split-list .acct-add-bottom { margin: 12px 0 12px; }
.acct-row-ellipsis {
  background: none; border: none; cursor: pointer; font-size: var(--fs-h3); color: var(--muted);
  padding: .2rem .5rem;
}
/* Touch: ⋯ measured 31px wide (v1.29.0 tapthru §1); background-free, so the
 * widening is invisible — pure hit-box growth at the row's right edge. */
body[data-surface="phone"] .acct-row-ellipsis,
body[data-surface="tablet"] .acct-row-ellipsis {
  min-width: var(--ctl-h-md);
  display: inline-flex; align-items: center; justify-content: center;
}
@media (hover: hover) { .acct-row-ellipsis:hover { color: var(--ink); } }
.acct-split-select {
  display: grid; grid-template-columns: minmax(0, 1fr) var(--accounts-value-column); flex: 1; min-width: 0; gap: 8px; align-items: center;
  background: none; border: none; font: inherit; color: inherit; text-align: left; cursor: pointer; padding: 0;
}

/* ---------- iPad-landscape Accounts split view (§5) ----------
 * Scoped by the tablet surface + orientation, NOT a width band: real iPads in
 * landscape are 1080–1366 logical px, so the old `(min-width:768px) and
 * (max-width:1023px)` cap excluded every modern iPad — the split DOM rendered
 * as two unstyled stacked blocks on device. The shell only builds .acct-split
 * on the tablet surface, so body[data-surface="tablet"] is the correct gate. */
@media (orientation: landscape) {
  body[data-surface="tablet"] #app:has(.acct-split) { height: 100dvh; }
  /* column, not row (mission 20/08/2026 item 6): the standalone fixed-chrome
   * header row (shell.js #routeHeaderHost, built when Accounts' split layout
   * owns no direct-child .screen-title) stacks ABOVE the split pane rather
   * than sitting beside it as a flex sibling — .acct-split still fills the
   * remaining height and stays a row internally. */
  body[data-surface="tablet"] .content:has(.acct-split) {
    max-width: none; padding: 0; flex: 1; min-height: 0; display: flex; flex-direction: column;
  }
  /* Match .content's own normal padding exactly (16px sides, 12px + safe-area
   * top — the same box Home's .route-header-row sits inside) so the fixed
   * chrome lands at the IDENTICAL position as every other tablet route,
   * pixel for pixel, not an approximated standalone offset. */
  body[data-surface="tablet"] .content:has(.acct-split) > .route-header-row-standalone {
    flex: 0 0 auto; padding: calc(14px + env(safe-area-inset-top)) 16px 0;
  }
  body[data-surface="tablet"] .acct-split { display: flex; flex: 1; min-height: 0; align-items: stretch; }
  body[data-surface="tablet"] .acct-split-list {
    width: 320px; flex: 0 0 320px; overflow-y: auto; border-right: 1px solid var(--hairline);
    padding: 0 .5rem 1rem;
  }
  body[data-surface="tablet"] .acct-split-detail { flex: 1; min-width: 0; overflow-y: auto; padding: 0 1rem 1rem; }
  body[data-surface="tablet"] .acct-list-row.active { background: var(--surface-hover); border-radius: var(--radius-s); }
}

/* Touch Accounts chrome: the Spending entry is a small all-caps lozenge and
 * account detail titles use the same left-aligned, grey-outline/no-fill face.
 * Both controls remain token-derived in light and dark themes. */
body:is([data-surface="phone"], [data-surface="tablet"]) .spending-title-row .reg-accounts-trigger {
  width: auto; min-width: 34px; padding-inline: 9px; border: 1px solid var(--hairline);
  border-radius: 999px; background: transparent; color: var(--ink-muted);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .spending-title-row .accounts-back {
  width: 34px; min-width: 34px; padding: 0; border: 0; border-radius: var(--radius-s);
  background: transparent; color: var(--ink-muted);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .reg-accounts-all {
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--fs-micro); font-weight: 800; letter-spacing: .08em; line-height: 1;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .account-title-lozenge {
  display: inline-flex; align-items: center; justify-content: flex-start;
  min-width: 0; max-width: 100%; width: fit-content; padding: 4px 10px;
  border: 1px solid var(--hairline); border-radius: var(--radius-s);
  background: transparent; color: var(--ink-muted); text-align: left;
  font-size: var(--fs-value); line-height: 1.2;
}

/* ---------- phone tab bar / tablet pill nav (§2) ---------- */
/* Phone bottom tab bar, docked flush to the bottom edge (2026-07-08, reverted
 * from floating pill to save vertical space). 7 items fill the bar via flex
 * grow; overflow-x: auto is a safety net only, so items scroll instead of
 * clipping if they ever don't fit. Safe-area inset is bottom padding, not
 * an offset, so content clears the iOS home indicator. */
/* Rounded floating tab bar (operator 2026-07-08i): a card-coloured pill inset
 * from the screen edges, horizontally scrollable if the six tabs don't fit.
 * Each tab is an icon above a small label (YNAB-style). A single sliding glass
 * lozenge sits behind the active tab (.tab-lozenge) rather than a per-item chip. */
.tab-bar {
  /* Sits LOWER (operator 2026-07-11, mimic the original): flush against the
   * home indicator — the 6px float gap is gone; the safe-area inset alone
   * keeps it clear of the indicator. */
  /* Sit LOWER (operator 2026-07-12: "needs to be lower… still wasted space
   * below"). The full safe-area inset (~34px on a notched iPhone) floated the bar
   * well clear of the home indicator; pull it down by part of that inset so it
   * hugs the bottom while still clearing the indicator. Floored at 2px so a device
   * with no inset doesn't touch the very edge. */
  --tab-bar-bottom: max(env(safe-area-inset-bottom, 0px), clamp(14px, 5.22vw, 21px));
  position: fixed; left: clamp(16px, 5.22vw, 24px); right: clamp(16px, 5.22vw, 24px);
  bottom: var(--tab-bar-bottom);
  height: var(--tab-bar-height); box-sizing: border-box;
  display: flex; justify-content: space-between; gap: 1px;
  /* Glass toolbar (operator 2026-07-09), transparency ratcheted by operator:
   * 72→36→12→6%, then 3× MORE transparent again (2026-07-11) — 6%→2%. The
   * blur + hairline alone carry the frosted look now. */
  background: color-mix(in srgb, var(--card) 2%, transparent);
  -webkit-backdrop-filter: blur(22px) saturate(1.6);
  backdrop-filter: blur(22px) saturate(1.6);
  border: 1px solid var(--chrome-edge); border-radius: 24px;
  /* Taller bar (operator 2026-07-08j): ~15% more vertical room so the icons and
   * labels can grow and read easier. */
  padding: 4px 6px; z-index: 20; overflow-x: auto;
  box-shadow: 0 4px 20px rgba(0, 0, 0, .12);
  scrollbar-width: none;
}
/* Fixed chrome is positioned against the visual viewport while iOS resizes it
 * for the keyboard. Move only the keyboard-open tab bar back to the layout
 * viewport bottom; at rest the shared reduction is zero and geometry is unchanged. */
html[data-vv-keyboard="true"] body:is([data-surface="phone"], [data-surface="tablet"]) .tab-bar {
  bottom: calc(var(--tab-bar-bottom) - var(--vv-keyboard-reduction, 0px));
}
.tab-bar::-webkit-scrollbar { display: none; }
.tab-item {
  position: relative; z-index: 1;
  /* Five tabs share the bar's width (operator 2026-07-11): the sixth (Pricer)
   * rests out of view and scrolls in on a swipe — the scrolling toolbar the
   * operator asked for. min-width: max-content (punch 21, operator: "every
   * tab's title on every platform is never truncated"): a tab whose label
   * needs more than its 20% share (e.g. "Scheduled" on a narrow phone) widens
   * to fit instead of ellipsising — the bar already scrolls, so extra width
   * costs nothing. */
  /* The five-tab reference allocates 68/66/72/72/68px inside the 346px
   * content strip at 402px.  The shell supplies these weights so the centres
   * remain proportional on other supported viewports while matching the
   * measured 62/129/198/270/340px centres at the primary viewport. */
  flex: var(--tab-weight, 1) 1 0; min-width: max-content;
  display: flex; flex-direction: column; align-items: center; gap: 1px;
  color: var(--ink-muted); font-size: var(--fs-micro); font-weight: 600;
  padding: 3px 2px; border-radius: 26px; white-space: nowrap;
  transition: color var(--dur-base) var(--ease-standard), transform var(--dur-fast) var(--ease-spring);
}
.tab-item .tab-ico { line-height: 0; }
/* Bolder, blacker toolbar (operator 2026-07-09): thicker icon stroke + near-black
 * ink on inactive tabs so the glass bar's icons stand out; active stays accent. */
/* Lighter, less blocky icons + labels (operator 2026-07-10): back off the
 * heavy 2.15 stroke and near-black 700 weight to a softer iOS-style weight. */
/* Larger toolbar icons on touch (operator 2026-07-12): the bar (phone/tablet
 * only — it never renders on desktop) has vertical slack, so the glyph can grow
 * from 1.6em → 2.05em without touching the bar height, label, or any tab width. */
/* Bigger glyphs (operator 2026-07-12: "make icons larger IF POSSIBLE… small
 * relative to the height avail"). The bar has vertical slack, so grow the icon
 * from 2.05em → 2.4em without touching bar height or tab width. */
.tab-item .tab-ico .ico { width: 24px; height: 24px; stroke-width: 1.75; }
.tab-item:not(.active) { color: var(--tab-inactive); font-weight: 500; }
/* Labels never truncate (punch 21): .tab-item's min-width: max-content above
 * guarantees room, so the old overflow/ellipsis clamp is gone. */
.tab-item .tab-label { max-width: none; }
.tab-item:active { transform: scale(.92); }
/* A hair larger where there's width to spare; still fits "Scheduled" at 375px. */
@media (min-width: 390px) { .tab-item { font-size: var(--fs-label); } }
.tab-item.active { color: var(--accent); }
/* Sliding glass lozenge behind the active tab. Animates left/width; the eased
 * transform gives a soft warp as it travels. No colour cycling.
 * CONVEX water-droplet glass (operator 2026-07-11): a top-edge specular
 * highlight, a soft dark ground shadow at the bottom edge and a faint radial
 * sheen make the pill read as a raised droplet, not a flat chip. All overlays
 * are white/black at low alpha over the token base, so both themes read. */
.tab-lozenge {
  position: absolute; top: 4px; bottom: 4px; left: 0; width: 20%;
  /* FROSTED, not opaque (operator 2026-07-12): the fill is a translucent tint over
   * a backdrop blur so the bar/content shows through like real frosted glass,
   * instead of the solid --chip chip that read as fully opaque in dark mode. The
   * specular gradient sits on top for the raised-droplet sheen. */
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  background-image: none;
  -webkit-backdrop-filter: blur(10px) saturate(1.5);
  backdrop-filter: blur(10px) saturate(1.5);
  /* Rounder than the old 16px squircle (operator 2026-07-14: "a bit more circle
   * than square, within reason"). 22px reads noticeably more pill-like at this
   * ~30px tile height without going fully circular. */
  border-radius: 22px; z-index: 0; opacity: 0;
  border: 1px solid color-mix(in srgb, var(--accent) 32%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--card) 34%, transparent),
    inset 0 1px 0 color-mix(in srgb, var(--card) 50%, transparent);
  transition: left var(--dur-slow) var(--ease-spring),
              width .32s cubic-bezier(.34, 1.35, .5, 1), opacity .2s ease;
  pointer-events: none;
}
/* Accounts' reference lozenge is content-hugging and slightly shorter than
 * the Spending/Plan tiles (about 87×50 at 402×874). */
.tab-lozenge.accounts-target { top: 5.5px; bottom: 5.5px; }
/* Left-most tab selected → FLAT frosted glass, not the convex droplet (operator
 * 2026-07-14). Drop the specular radial highlight and the raised inset shadows;
 * keep only the translucent tint + blur and a hairline edge, so it reads as a
 * plain pane of frosted glass sitting flush on the bar. */
.tab-lozenge.at-start {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--card) 34%, transparent);
}
[data-theme="dark"] .tab-lozenge.at-start {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--card) 24%, transparent);
}
[data-theme="dark"] .tab-lozenge {
  /* Dark: a lighter translucent tint over the blur so the tile reads as frosted
   * glass, not a solid block (operator 2026-07-12 — the opaque chip was the bug). */
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  background-image: none;
  border-color: color-mix(in srgb, var(--accent) 36%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--card) 24%, transparent),
    inset 0 1px 0 color-mix(in srgb, var(--card) 22%, transparent);
}

/* (Tablet top pill nav removed 2026-07-07c: tablet now shares the phone bottom
 * tab bar in every orientation — operator wanted one consistent bottom location
 * that never jumps to the top. See .tab-bar rules and shell.js #tabBar.) */

/* ---------- register: phone/tablet list (§3) ---------- */
.txn-list { padding-top: 6px; }
/* Uncleared-only filter view: a soft amber tint fills the whole list so it reads
 * as a distinct, temporary slice of the register (operator 2026-07-12). Tokened
 * off --warn so it tracks dark/light; the "Show all transactions ›" banner
 * reverts it. */
.txn-list-uncleared {
  background: color-mix(in srgb, var(--amber) 12%, var(--card));
  border-radius: 14px; padding-left: 10px; padding-right: 10px;
}
/* The ROWS themselves must be tinted too, not just the section behind them
 * (operator 2026-07-12): each row paints an opaque --card fill on both its swipe
 * wrapper and the row cell, so previously only the gaps/borders showed the amber
 * and every row stayed white. Tint both fills to the SAME amber so the whole row
 * is filled. Same color-mix as the section, so row and surround read as one tint. */
.txn-list-uncleared .swipe-wrap,
.txn-list-uncleared .swipe-wrap .txn-row {
  background: color-mix(in srgb, var(--amber) 12%, var(--card));
}
.date-head { font-weight: 600; font-size: var(--fs-body); padding: .7rem 0 .2rem; color: var(--ink-muted); }
/* Faint on-theme blurple wash on every Spending date-header row, touch only
 * (D-102 batch, item 4). Ordinary transaction date groups, groups inside the
 * Scheduled band, and Current-touch search result date labels (same grouping
 * concept, different markup) all share this tint. Scoped to /spending only —
 * desktop Spending and per-account register date headers (route "account")
 * keep their existing look. */
body[data-route="spending"]:is([data-surface="phone"], [data-surface="tablet"]) :is(.date-head, .reg-search-preview-date) {
  background: var(--scheduled-fill); border-radius: var(--radius-s, 8px); padding-inline: .5rem;
}
.txn-row {
  padding: .5rem 0 .5rem .6rem; border-top: 1px solid var(--hairline);
  cursor: pointer; position: relative; border-radius: 8px;
}
.txn-row:first-of-type { border-top: 0; }
/* Hover tints are MOUSE-ONLY app-wide (operator 2026-07-11): iOS keeps :hover
 * latched on the last-touched row after a scroll-release, leaving a stuck grey
 * box (the "Acres Animal" report). Every row-hover below hides behind
 * (hover: hover); keyboard focus keeps its ring everywhere. */
.txn-row:focus-visible { background: color-mix(in srgb, var(--accent) 5%, transparent); outline: none; }
@media (hover: hover) {
  .txn-row:hover { background: color-mix(in srgb, var(--accent) 5%, transparent); outline: none; }
}
.txn-row.flagged::before {
  content: ''; position: absolute; left: -2px; top: .4rem; bottom: .4rem;
  width: 6px; border-radius: 3px; background: var(--flag, transparent); /* thicker flag (item 11) */
}
/* Fresh-start "match YNAB" correcting transaction: a reserved TEAL used nowhere
   else in the app (#00a3a3) — a teal fill + left stripe so the one adjustment
   entry is unmistakable in the register on both surfaces. */
.txn-row.txn-reconcile, .reg-row.txn-reconcile {
  background: color-mix(in srgb, var(--adjust-teal) 12%, transparent);
  box-shadow: inset 3px 0 0 var(--adjust-teal);
}
/* The row's text block. In ordinary mode it is a plain wrapper and changes
 * nothing; in SELECT mode it is the flex sibling the check circle centres
 * against, which is what keeps the payee line and the sub-line on ONE left
 * rail (register.js — the 38px rail break the operator reported). */
.txn-body { min-width: 0; flex: 1 1 auto; }
/* SELECT mode makes the row itself the flex container so the leading circle is
 * measured against the FULL row height, not the first text line. */
.txn-row.selecting,
/* A reconciled row needs the same row-level flex rail even outside SELECT mode:
 * the lock is a status control, not part of the baseline-aligned payee line. */
.txn-row.reconciled { display: flex; align-items: center; gap: 10px; }
.txn-status-rail {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
}
.txn-main { display: flex; justify-content: space-between; gap: 10px; align-items: baseline; min-width: 0; }
.txn-payee { font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Recurring marker (operator 2026-07-15, punch B6): a small repeat glyph beside
 * the payee on any row posted from a schedule (import_ref starts "sched:").
 * Muted so it reads as metadata, not a call to action; sized off the em so it
 * never grows the row or clips against the payee text either side of it. */
/* aspect-ratio + min-width/min-height keep the disc CIRCULAR (operator
 * 2026-07-17: "the RECURRING icon should be a circle not ovoid"). `flex: 0 0
 * auto` fixes only the MAIN axis; the cross axis was free to stretch against
 * the row's line box, so the round glyph painted as an egg. */
/* 2026-07-17 (tapthru): `vertical-align: middle` aligns to the text BASELINE plus half an
 * x-height, which is NOT the line box's centre — the glyph painted 1.13px BELOW its row's
 * centre in every register/spending list, on every device and both themes. Measured, not
 * eyeballed. `.txn-payee` is an ellipsised nowrap span, so the row itself cannot become a
 * flex container without losing truncation; instead the glyph centres itself the way this
 * codebase's other inline glyph (.xfer-chev) already does — inline-flex + centre — which
 * measures dY = 0.00 and needs no magic offset to maintain. */
/* Glyph + frequency word as ONE compact inline badge, so a scheduled row reads
 * exactly like a normal one save for this small label (operator 2026-07-20,
 * Win11). It stays inline in the nowrap payee cell for constrained rows; the
 * roomy list rail promotes it after the category chip before first paint. The
 * word is muted-accent and a notch smaller than the payee so it registers as
 * metadata, not a second payee. */
.reg-recur {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: 3px;
  margin-left: 5px; cursor: pointer; vertical-align: calc((1cap - 1em) / 2);
}
/* Touch: the badge is a real control (role="button" — it opens the schedule) but
 * it is a SPAN, so app.css's blanket 44px floor (which targets button/.btn/…)
 * never reached it and it measured 16px tall on every iPad and phone profile
 * (v1.74.0 sweep, 24 records).
 *
 * The touch target used to be grown with vertical padding plus a cancelling
 * negative margin. That keeps the badge's INLINE contribution at 16px, but the
 * border box still measures the full 44px, and a border box taller than its row
 * is what the operator sees crossing the hairline (01/09/2026; measured row
 * 36.61px vs badge 51.16px, over by 6.75px above and 7.79px below). The target
 * is therefore grown by a positioned ::after instead — an absolutely positioned
 * child is out of flow, so it can exceed the badge without enlarging any box
 * the row has to contain. Paint and inline metrics are unchanged. */
body[data-surface="phone"] .reg-recur,
body[data-surface="tablet"] .reg-recur {
  position: relative;
  padding-block: 0;
  margin-block: 0;
  background: transparent;
}
body[data-surface="phone"] .reg-recur::after,
body[data-surface="tablet"] .reg-recur::after {
  content: ''; position: absolute; left: 0; right: 0;
  top: 50%; height: var(--ctl-h-md); transform: translateY(-50%);
}
/* Inline transfer editor: the payee cell holds "From → To (select)" so a
 * transfer edits in its own grid row like every other transaction, no giant
 * sheet (operator 2026-07-20, Win11). */
.ir-xfer-ends { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
.ir-xfer-ends .ir-xfer-from { color: var(--ink-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ir-xfer-ends .xfer-chev { flex: 0 0 auto; display: inline-flex; align-items: center; color: var(--accent); }
.ir-xfer-ends .ir-xfer-to { min-width: 0; flex: 1 1 auto; }
.txn-amount { flex: 0 0 auto; font-variant-numeric: tabular-nums; white-space: nowrap; font-weight: 500; }
.txn-amount.pos { color: var(--pos); }
.cleared-badge {
  display: inline-block; background: var(--pos); color: var(--pos-ink); border-radius: 50%;
  font-size: var(--fs-micro); width: 1rem; height: 1rem; text-align: center; line-height: 1rem;
}
/* WRAPS rather than clipping (operator 2026-07-14). The category chip and the
 * account/memo meta share this line, and on a phone a pair like
 * "Inflow: Ready to Assign" + "DBS Checking July salary" simply does not fit —
 * the old meta was ellipsising the memo. A second line costs
 * nothing; a clipped account name is the bug. Same rule as the Scheduled rows. */
.txn-sub {
  display: flex; justify-content: flex-start; align-items: center;
  gap: 10px; font-size: var(--fs-body); margin-top: .15rem; min-width: 0;
  flex-wrap: wrap; row-gap: 2px;
}
.txn-memo { margin-top: .1rem; font-size: var(--fs-body); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chip { min-width: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; background: var(--chip); color: var(--ink); border-radius: 999px; padding: .05rem .55rem; font-size: var(--fs-label); }
/* Category chip leads the sub line and keeps its width; account and memo then
 * own a full-width, left-aligned rail beneath it. */
.txn-sub .chip { flex: 0 1 auto; }
/* No ellipsis: the metadata rail wraps rather than being cut off mid-word.
 * It no longer CLAIMS a whole row of its own: `flex: 1 0 100%` forced a line
 * break before the account lozenge, so "Transfer" and "Savings Nala" always
 * stacked even when both fitted easily side by side (operator 31/08/2026,
 * measured: chip top 506.88, lozenge top 531.5 on a 402px screen). Growing
 * from a zero basis lets the pair share the chip's row and still wrap onto a
 * second line by itself when a long memo genuinely needs one. */
.txn-sub-meta {
  display: inline-flex; align-items: flex-start; justify-content: flex-start;
  /* A zero basis, not `auto` (operator 01/09/2026: "the 'transfer' glyph can be
   * same row as the account"). With `flex-basis: auto` the rail asked for its
   * whole max-content — lozenge 114.5px PLUS memo 172.6px — and `.txn-sub` is
   * wrap, so the browser moved the entire rail to line 2 rather than shrinking
   * it, taking the account lozenge down with the memo. Measured on iPhone 13
   * Pro Max: chip 214.5 + rail 287.1 + 10px gap = 511.6 inside a 346px sub.
   * From zero the rail takes only the room left beside the chip and wraps
   * INTERNALLY, so the lozenge keeps the chip's row and only the memo — which
   * genuinely needs its own line — drops beneath. */
  flex: 1 1 0;
  min-width: 0; gap: .35rem; text-align: left; margin-left: 0; flex-wrap: wrap;
}
.txn-account-lozenge {
  display: inline-flex; align-items: center; max-width: 100%;
  border: 1px solid var(--hairline); background: transparent; color: var(--ink-muted);
  border-radius: 999px; padding: .05rem .55rem; white-space: normal; overflow-wrap: anywhere;
}
.txn-memo-inline { min-width: 0; white-space: normal; overflow-wrap: anywhere; }
.txn-sub:only-child, .txn-sub > .chip:only-child { justify-content: flex-start; }

/* Global floating "+ Transaction" pill (§2/§3; operator 2026-07-08i). YNAB-style:
 * right-aligned above the tab bar and semi-transparent (frosted) so the list
 * shows through, on every tab. iOS press bounce via .fab-txn:active. */
.fab-txn {
  /* Bottom aligned to the scroll-to-top disc (operator 2026-07-12 punch 16) so
   * the FAB and the disc sit on ONE line; the max-width guard below keeps the
   * expanded pill from ever touching the centred disc. */
  position: fixed; bottom: calc(80px + env(safe-area-inset-bottom)); right: 16px;
  /* Width cap that only bites when the disc is actually on screen (operator
   * 2026-07-13: the pill had become far too small — it is a critical, high-
   * frequency button and must be comfortably tappable).
   *
   * MECHANISM: the centred scroll-to-top disc only appears once scrolled
   * (> 240px), and by then the FAB has all but collapsed to a bare + (its label
   * hides as --fab-shrink → 1). So a HARD 50%−40px cap on the EXPANDED pill was
   * guarding a collision that only happens while collapsed — and that cap is
   * exactly what forced the earlier fluid font/padding shrink that shrank the
   * resting pill. Fix: interpolate the cap on --fab-shrink. At rest (shrink 0,
   * disc hidden) the pill is capped only by a roomy content width, so the full
   * word shows at full size on every phone. As it collapses (shrink → 1, disc
   * appearing) the cap tightens to the disc-safe 50%−40px — which keeps the
   * +4px gap during the scroll-UP transient where the disc is still shown while
   * the pill re-expands. Never truncates (label max-width, not this, ellipses),
   * never overlaps the disc. */
  --fab-cap-safe: calc(50% - 48px);      /* disc-safe cap with a clear 12px gap */
  --fab-cap-open: min(100%, 20rem);      /* roomy cap at rest — full word, full size */
  max-width: calc(var(--fab-cap-safe) + (var(--fab-cap-open) - var(--fab-cap-safe)) * (1 - var(--fab-shrink)));
  /* Glassy purple (operator 2026-07-08j): keep the purple identity but make the
   * fill ~80% transparent so the list shows through, with a frosted blur and a
   * bright inner-edge highlight so the rim reads as glass. The text keeps a full
   * accent colour + a subtle white halo so it stays legible over the see-through
   * fill in both themes (rule 14). */
  /* Transparency ratchet (operator): 5%→1.7%→0.85%, then 3× MORE transparent
   * again (2026-07-11) — 0.85%→0.3%; the frosted blur + bright rim carry it. */
  background: color-mix(in srgb, var(--accent) 0.3%, transparent);
  -webkit-backdrop-filter: blur(18px) saturate(1.6);
  backdrop-filter: blur(18px) saturate(1.6);
  color: var(--accent); border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  cursor: pointer;
  /* Comfortable fixed size (operator 2026-07-13: was far too small). The shrink-
   * aware max-width cap above now lets the full-size pill show its full word on
   * every phone, so the fluid downscale is gone — the compact pill keeps the
   * full label without unnecessary width. Horizontal padding lives in the shrink block below (it has to
   * interpolate with --fab-shrink); only the vertical padding lives here. */
  font: inherit; font-weight: 700; padding-top: .7rem; padding-bottom: .7rem;
  font-size: var(--fs-value); border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
  box-shadow: 0 4px 16px color-mix(in srgb, var(--accent) 18%, transparent),
              inset 0 1px 0 rgba(255, 255, 255, .45),
              inset 0 -1px 0 rgba(255, 255, 255, .12);
  text-shadow: 0 1px 2px rgba(255, 255, 255, .35);
  z-index: 15;
  transition: transform var(--dur-fast) var(--ease-spring), box-shadow var(--dur-fast) var(--ease-standard);
}
[data-theme="dark"] .fab-txn {
  color: var(--accent-ink);
  /* Tripled transparency in step with light (operator 2026-07-11): 1.35%→0.45%. */
  background: color-mix(in srgb, var(--accent) 0.45%, transparent);
  text-shadow: 0 1px 2px rgba(0, 0, 0, .35);
  box-shadow: 0 4px 16px rgba(0, 0, 0, .35),
              inset 0 1px 0 rgba(255, 255, 255, .18);
}
.fab-txn .ico { width: 1.15em; height: 1.15em; flex: 0 0 auto; }
.fab-txn:active { transform: scale(.93); box-shadow: 0 4px 14px color-mix(in srgb, var(--accent) 24%, transparent); }
/* Scroll-aware shrink (operator 2026-07-11, original-app-exact): scrolling down
 * collapses the pill to a bare + with a bounce; any scroll up bounces the label
 * back out. The label's max-width/opacity animate on the same springy curve the
 * FAB already uses for its press bounce.
 *
 * Circle-not-lozenge (operator 2026-07-12): the collapsed state must be a TRUE
 * circle around the +, not a wide capsule. Previously only padding-RIGHT shrank
 * (1.15rem→.7rem) while padding-left stayed 1.15rem, so the fully-shrunk pill was
 * a lozenge. Now BOTH horizontal paddings interpolate to the SAME value as the
 * vertical padding (.7rem), and at full shrink the box is pinned to an equal
 * width/height so the + sits dead-centre in a round button. */
.fab-txn {
  --fab-shrink: 0;
  /* Base horizontal padding keeps the label readable without the old oversized
   * face. The shrink term interpolates it toward .7rem (= vertical padding, a square) as the pill
   * collapses to a bare + on scroll. --fab-pad is the live base so the label
   * max-width (below) can key off the same value. */
  --fab-pad: 1.15rem;
  padding-left: calc(var(--fab-pad) - (var(--fab-pad) - .7rem) * var(--fab-shrink));
  padding-right: calc(var(--fab-pad) - (var(--fab-pad) - .7rem) * var(--fab-shrink));
  /* True circle when collapsed (operator 2026-07-14): the comment above claimed
   * the collapsed box was "pinned to equal width/height", but nothing pinned it,
   * so it read as a 47×52 EGG — the icon line box makes it taller than the
   * padding+icon width. Give the button a fixed square height and grow min-width
   * to match, both faded in on --fab-shrink: at rest (shrink 0) width & height
   * are auto/content (the roomy pill is untouched); at full shrink both axes
   * meet at --fab-round, which border-radius:999px renders as a perfect circle.
   * box-sizing:border-box (global) counts the 1px border on both axes. */
  --fab-round: 48px;                               /* collapsed circle diameter */
  min-height: calc(var(--fab-round) * var(--fab-shrink));
  min-width: calc(var(--fab-round) * var(--fab-shrink));
}
/* Collapse the icon↔label gap in step with the label so no stray space is left
 * on the right when the label is gone. Uses the :has(.ico) selector so it wins
 * against the generic `.fab-txn:has(.ico){gap:6px}` rule above (same specificity,
 * later in source) — a plain `.fab-txn{gap}` was silently overridden, leaving a
 * 6px stub that stopped the collapsed pill being a clean circle. */
.fab-txn:has(.ico) { gap: calc(0.4rem * (1 - var(--fab-shrink))); }
.fab-txn .fab-label {
  max-width: calc(8em * (1 - var(--fab-shrink)));
  opacity: calc(1 - var(--fab-shrink));
  /* NO ellipsis (operator 2026-07-14, sole exception): as the pill collapses on
   * scroll, "Transaction" is clipped by the shrinking max-width and faded out —
   * a mid-clip "…" looked broken, so the word simply crops with no ellipsis. */
  overflow: hidden; white-space: nowrap; text-overflow: clip;
}
/* Pricer keeps the same global FAB mounted and functional, but pins it to the
 * existing compact circular + state so it does not cover the comparison flow. */
.fab-txn.fab-txn-compact { --fab-shrink: 1; }

/* ---------- Spending list: search + SELECT (operator 2026-07-08i) ---------- */
/* Two-band header (operator 2026-07-14): the earlier single-row layout crammed
 * the title, search, Select AND the fixed version/eye/gear cluster onto ONE line,
 * so the 2rem title truncated to "Spen…" on a phone. Now the title uses the plain
 * .screen-title on its own row (standard 152px reserve clears the cluster, and
 * "Spending" fits with room to spare), and these tools live on their OWN row
 * below — right-aligned, sitting under the fixed cluster where they have full
 * width. The title position is now identical to every other tab. */
.reg-headtools { display: flex; justify-content: flex-end; align-items: center; margin: 0 0 10px; }
.reg-titletools { display: flex; align-items: center; justify-content: flex-end; gap: 6px; }
.reg-icon-btn {
  width: 38px; height: 38px; border: 0; background: var(--chip); color: var(--ink);
  border-radius: 12px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
  transition: transform var(--dur-fast) var(--ease-spring), background var(--dur-fast) var(--ease-standard);
}
body[data-surface="phone"] .reg-icon-btn,
body[data-surface="tablet"] .reg-icon-btn {
  width: var(--ctl-h-md); height: var(--ctl-h-md); min-width: var(--ctl-h-md); min-height: var(--ctl-h-md);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .reg-accounts-trigger {
  flex: 0 0 var(--ctl-h-md); padding: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .reg-accounts-trigger > span {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
.reg-icon-btn .ico { width: 1.35em; height: 1.35em; }
.reg-icon-btn:active { transform: scale(.88); }
.reg-icon-btn.on { background: var(--accent); color: var(--accent-ink); }
.reg-select-btn {
  border: 0; background: var(--chip); color: var(--accent); font: inherit; font-weight: 700;
  font-size: var(--fs-body); padding: .5rem 1rem; border-radius: 12px; cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-spring), background var(--dur-fast) var(--ease-standard);
}
.reg-select-btn:active { transform: scale(.94); filter: brightness(.93); }
.reg-select-btn.on { background: var(--accent); color: var(--accent-ink); }

.reg-search { margin: 8px 0; animation: views-sheet-in .25s cubic-bezier(.34, 1.4, .5, 1); }
/* Search bar: rounded pill (magnifier + field + clear-×) beside a round dismiss-×
   (iOS YNAB, IMG_4703). */
.reg-search-bar { display: flex; align-items: center; gap: 10px; }
.reg-search-pill {
  flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 8px;
  background: var(--card); border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--hairline)); border-radius: 999px;
  padding: .55rem .85rem; box-shadow: none;
}
.reg-search-pill:focus-within { border-color: var(--accent); outline: none; box-shadow: none; }
.reg-search-mag { flex: 0 0 auto; display: inline-flex; color: var(--ink-muted); }
.reg-search-mag .ico { width: 1.25em; height: 1.25em; }
.reg-search-input {
  flex: 1 1 auto; min-width: 0; border: 0; background: none; color: var(--ink);
  font: inherit; font-size: var(--fs-value); padding: .15rem 0;
}
.reg-search-input:focus { outline: none; }
.reg-search-input::-webkit-search-cancel-button { display: none; } /* use our own × */
.reg-search-clear {
  flex: 0 0 auto; width: 26px; height: 26px; border: 0; border-radius: 50%; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent);
  position: relative; /* anchors the touch overlay below */
}
/* The visible disc stays 26px — it is a small clear affordance beside the
 * field, and growing the box would push the toolbar around. The TAP area is
 * widened instead by a transparent overlay sized independently of layout, so
 * the finger gets the full floor without the disc changing size (the
 * grid-track lesson in the geometry doctrine: widen the target, not the box).
 * Sweep 27/07/2026 measured it 26px wide on every phone and tablet profile in
 * both themes, because the blanket touch floor only lifts min-height. */
body[data-surface="phone"] .reg-search-clear::after,
body[data-surface="tablet"] .reg-search-clear::after {
  content: ''; position: absolute; left: 50%; top: 50%;
  width: var(--ctl-h-md); height: var(--ctl-h-md);
  transform: translate(-50%, -50%);
}
.reg-search-clear .ico { width: .85em; height: .85em; stroke-width: 2.6; }
.reg-search-clear.hidden { display: none; }
.reg-search-dismiss {
  flex: 0 0 auto; width: 44px; height: 44px; border: 0; border-radius: 50%; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--card); border: 1px solid var(--hairline); color: var(--ink);
  transition: transform var(--dur-fast) var(--ease-spring);
}
.reg-search-dismiss:active { transform: scale(.9); }
.reg-search-dismiss .ico { width: 1.15em; height: 1.15em; }
.reg-search-sugg-h { font-weight: 700; font-size: var(--fs-value); margin: 1.1rem 0 .2rem; }
.reg-search-scopes { display: flex; flex-direction: column; }
.reg-scope-row {
  display: flex; align-items: center; gap: 14px; width: 100%; text-align: left;
  border: 0; border-top: 1px solid var(--hairline); background: none; color: var(--ink-muted);
  font: inherit; font-size: var(--fs-value); padding: .85rem .3rem; cursor: pointer;
}
.reg-scope-row:first-child { border-top: 0; }
.reg-scope-row .scope-ico { color: var(--ink); display: inline-flex; flex: 0 0 auto; }
.reg-scope-row .scope-ico .ico { width: 1.5em; height: 1.5em; }
.reg-scope-row .scope-term { color: var(--ink); font-weight: 600; }
.reg-scope-row.on { color: var(--accent); font-weight: 600; }
.reg-scope-row.on .scope-ico, .reg-scope-row.on .scope-term { color: var(--accent); }

/* SELECT-mode bar. Rebuilt 2026-07-14 (operator: "the DOWN arrow beside ACTIONS
 * gets shifted below it and other truncations occur in the text left of that,
 * very poor graphic design").
 *
 * Root cause: three items competed for one flex line, and the count was a single
 * long unbreakable string ("−$60.00 / 1 selected"). Under pressure the text wrapped
 * and the button's own label broke apart, dropping the caret onto a second line.
 *
 * Fix at the mechanism: the count is a two-line STACK (amount over "N selected"),
 * min-width:0 so it — and only it — absorbs any shortfall; both buttons are
 * flex-none and nowrap, so a button can NEVER break internally. */
.reg-selbar-mobile {
  display: flex; align-items: center; gap: 10px;
  background: var(--chip); border-radius: 14px; padding: .5rem .7rem; margin: 8px 0;
}
.rsm-count {
  flex: 1 1 auto; min-width: 0;
  display: flex; flex-direction: column; line-height: 1.15;
}
.rsm-sum {
  font-weight: 800; color: var(--ink); font-variant-numeric: tabular-nums;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rsm-sum.neg { color: var(--neg); }
.rsm-sum.pos { color: var(--pos); }
.rsm-n {
  font-size: var(--fs-label); color: var(--ink-muted); font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rsm-actions, .rsm-done { flex: 0 0 auto; white-space: nowrap; }
.rsm-actions {
  display: inline-flex; align-items: center; gap: 5px;
  padding: .4rem .9rem; font-size: var(--fs-body);
  /* POPS in with a rebound the first time a row is selected (operator
   * 30/07/2026). The pill is absent from the DOM while the selection is empty
   * — see register.js — so this entrance runs exactly once per fresh selection
   * and never as a re-entrance on every count change. */
  animation: rsm-actions-pop .34s cubic-bezier(.34, 1.56, .64, 1) both;
  transform-origin: center;
}
@keyframes rsm-actions-pop {
  from { transform: scale(.6); opacity: 0; }
  to   { transform: scale(1);  opacity: 1; }
}
/* Empty-state copy standing where the pill will appear, so the bar reads as
 * "nothing chosen yet" rather than as a bar missing a button. */
.rsm-hint {
  flex: 0 0 auto; white-space: nowrap;
  font-size: var(--fs-label); color: var(--ink-muted); font-weight: 600;
  padding: .4rem .2rem;
}
@media (prefers-reduced-motion: reduce) {
  .rsm-actions { animation: none; }
}
/* The caret is part of the button's own flex line and never wraps away from the
 * label — the exact failure the operator photographed. */
.rsm-caret { flex: 0 0 auto; font-size: .78em; line-height: 1; opacity: .9; }

/* SELECT-mode row: leading check circle; suppress swipe reveal. */
.swipe-wrap.selecting .swipe-actions { display: none; }
/* The circle is the row's LEADING control, centred on the whole row by
 * `.txn-row.selecting`. Its own margin is gone — the row's flex `gap` owns the
 * spacing now, and keeping both stacked a 22px circle 8px further from the text
 * than the sub-line's own left edge. */
.sel-circle {
  flex: 0 0 auto; width: 22px; height: 22px; border-radius: 50%;
  border: 2px solid var(--hairline); display: inline-flex; align-items: center; justify-content: center;
  position: relative;
  color: var(--accent-ink); transition: background var(--dur-fast) var(--ease-standard), border-color var(--dur-fast) var(--ease-standard), transform var(--dur-fast) var(--ease-spring);
}
/* 44px live zone on the 22px disc without moving the disc itself (the same
 * mechanism as the desktop `.c-sel::before` hit overlay — a real element's
 * ::before, never a replaced input's). */
.sel-circle::before {
  content: ''; position: absolute;
  top: 50%; left: 50%; width: 44px; height: 44px;
  transform: translate(-50%, -50%);
}
.sel-circle.on { background: var(--accent); border-color: var(--accent); transform: scale(1.05); }
.sel-circle .ico { width: 15px; height: 15px; }
.txn-row.row-selected { background: color-mix(in srgb, var(--accent) 8%, transparent); }

/* ---------- register: desktop dense grid (§11.2), single row per txn ---------- */
.bal-equation {
  display: flex; align-items: center; gap: 14px; margin: 0 0 12px; font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.bal-equation small { display: block; font-size: var(--fs-micro); font-weight: 500; color: var(--ink-muted); letter-spacing: .04em; }
.bal-cleared, .bal-uncleared { color: var(--pos); }
/* Future-dated rows are their own term — not money in limbo (see register.js). */
.bal-scheduled { color: var(--ink-muted); }
.bal-working { color: var(--accent); }
.bal-op { color: var(--ink-muted); font-weight: 400; }

.reg-toolbar { display: flex; gap: 8px; margin-bottom: 10px; }
.reg-toolbar .btn { padding: .4rem 1rem; font-size: var(--fs-body); min-height: var(--ctl-h-sm); }
.reg-toolbar .btn.on {
  background: color-mix(in srgb, var(--accent) 14%, var(--card));
  border-color: color-mix(in srgb, var(--accent) 55%, var(--hairline));
  color: var(--accent);
}
body[data-surface="phone"] .reg-toolbar .btn,
body[data-surface="tablet"] .reg-toolbar .btn { min-height: var(--ctl-h-md); }
.reg-undo-group { display: inline-flex; gap: 6px; }
.reg-undo-group .btn[disabled] { opacity: .45; cursor: default; }

.reg-view-menu {
  position: fixed; z-index: 90; width: 270px; padding: 10px;
  border: 1px solid var(--hairline); box-shadow: var(--shadow-lg);
}
.reg-view-title { font-weight: 700; padding: 2px 4px 8px; }
.reg-view-block { padding: 8px 4px; border-block: 1px solid var(--hairline); }
.reg-view-label { display: block; margin-bottom: 6px; font-size: var(--fs-label); color: var(--ink-muted); }
.reg-view-segments { display: grid; grid-template-columns: repeat(3, 1fr); }
.reg-view-segments button {
  min-height: 26px; border: 1px solid var(--hairline); background: var(--card);
  color: var(--ink); font: inherit; font-size: var(--fs-label); cursor: pointer;
}
.reg-view-segments button + button { border-left: 0; }
.reg-view-segments button:first-child { border-radius: 6px 0 0 6px; }
.reg-view-segments button:last-child { border-radius: 0 6px 6px 0; }
.reg-view-segments button.on { background: transparent; border-color: transparent; color: var(--accent-ink); }
.reg-view-check {
  display: flex; align-items: center; gap: 8px; min-height: 30px;
  padding: 3px 4px; font-size: var(--fs-body); cursor: pointer;
}
.reg-view-check input { accent-color: var(--accent); }

.reg-grid { padding: 0; overflow: visible; }
/* Keep the document as the vertical and horizontal scroll owner. A mixed-axis
 * overflow container makes a sticky register header stick to an unscrolling
 * grid box instead of the viewport; desktop grids may still widen the page and
 * use the document's horizontal scrollbar when a narrow width needs it. */
html:has(body[data-surface="desktop"] .reg-grid) { overflow-x: auto; }
.reg-row {
  display: grid; align-items: center; gap: 8px;
  /* Flag column 4px → 7px: thicker colour stripe for visibility (item 11).
   * The five resizable tracks read through --reg-c-* so a drag on the header's
   * grip can retune them (operator 2026-07-29: drag-to-resize column width).
   * Each falls back to the width it has always had, so a device that has never
   * dragged anything renders byte-identically to before. */
  grid-template-columns:
    1.2rem 1.35rem 1.2rem
    var(--reg-c-acct, 8rem)
    var(--reg-c-date, 6.8rem)
    var(--reg-c-payee, 1.25fr)
    var(--reg-c-cat, minmax(var(--reg-min-cat, 0px), 1.5fr))
    var(--reg-c-subcat, minmax(var(--reg-min-subcat, 0px), 1.5fr))
    var(--reg-c-memo, 1fr)
    7.4rem 7.4rem 1.4rem;
  padding: 0 10px; min-height: 28px; border-top: 1px solid var(--hairline);
  font-size: var(--fs-body); white-space: nowrap;
}
/* Outflow/Inflow amounts never wrap their sign onto a second line, whatever the
 * figure length (broader columns above give the room). */
.reg-row .c-out, .reg-row .c-in { white-space: nowrap; }
/* Truncation sweep (operator 2026-07-14): the ROW is `white-space: nowrap`, but
 * only four cells carried `overflow: hidden` — so a long value in any OTHER cell
 * ran out of its track and painted over the next column.
 *
 * Clip the TEXT cells only. The numeric/date/marker cells are deliberately left
 * alone: the register grid is a horizontally SCROLLABLE table (.reg-grid has
 * overflow-x:auto and fixed rem tracks), and clipping those would trade an honest
 * "scroll to see the rest" for a silently truncated figure — strictly worse than
 * the bleed. Money must never be cut off mid-digit. */
.reg-row .c-acct, .reg-row .c-payee, .reg-row .c-cat, .reg-row .c-subcat, .reg-row .c-memo {
  min-width: 0; overflow: hidden; text-overflow: ellipsis;
}
/* Let the single-row register broaden to the full width on tablet LANDSCAPE
 * (the default content column is capped at 720px; the grid wants the room)
 * (item 11). `.reg-grid` is desktop-grid-only (`wideGrid = surface ===
 * 'desktop'` in register.js) — it never renders on tablet, so this selector
 * never matched on tablet at all, leaving Spending's `.content` capped at
 * 720px and CENTRED while Home's own uncap (`.reflect-cardstack`, below) is
 * ALSO landscape-only — so the two routes must share the SAME orientation
 * gate to stay consistent (mission 20/08/2026 item 6's stable-chrome proof
 * measured a mismatch on BOTH orientations before this was orientation-
 * matched: landscape had Spending capped while Home was uncapped; portrait
 * then had Spending uncapped while Home stayed capped, an equally wrong
 * mismatch introduced by an earlier unconditional version of this rule).
 * `.txn-list` is the container renderList() actually builds on touch
 * (register.js), so this now matches for real, on the correct orientation. */
@media (min-width: 768px) and (orientation: landscape) {
  body[data-surface="tablet"] .content:has(.reg-grid),
  body[data-surface="tablet"] .content:has(.txn-list) { max-width: none; }
  /* Pricer (mission 20/08/2026 item 6, third surfaced pre-existing bug): the
   * default content column is capped-and-centred at 720px, which is fine for
   * its own calculator cards but left the fixed version/eye/gear cluster
   * narrower and left-shifted relative to Home once chrome moved into normal
   * document flow inside `.content` — measured on iPad landscape. Uncapping
   * matches Home's own landscape-only treatment; Pricer's own card widths
   * are unaffected (they carry their own sizing, not `.content`'s max-width). */
  body[data-surface="tablet"] .content:has(.pricer-products) { max-width: none; }
}
/* SELECTION BOX HIT AREA (operator 29/07/2026: "the tap zone when clicking the
 * square box to select transaction needs to be bigger").
 *
 * The measured target was the checkbox itself — roughly 13x13px inside a 1.2rem
 * track on a density-sized row. That is a precision mouse target and an impossible touch
 * one.
 *
 * It is grown WITHOUT touching the grid: the track stays 1.2rem, so none of the
 * three `.reg-row` grid templates move (they must stay in lockstep or every
 * column shifts — the v1.85.1 flag-column lesson). Instead the checkbox gets an
 * ::before overlay that spills symmetrically past the cell on both axes and
 * catches the pointer for it. `position: relative` on the cell would clip the
 * overlay to the track, so the ANCHOR is the row and the overlay is positioned
 * off the input's own box.
 *
 * Full row height + ~34px wide is as large as it can be before it starts
 * stealing clicks from the flag column beside it; the flag glyph is its own
 * control (it opens the flag picker), so the two must not overlap. */
.reg-row .c-sel {
  display: flex; align-items: center; justify-content: center;
  position: relative;   /* containing block for the hit overlay below */
  /* The cell itself must not clip the overlay that deliberately spills past it. */
  overflow: visible;
}
.reg-row .c-sel input { margin: 0; cursor: pointer; }
/* The header owns a real label/checkbox target. Its surrounding desktop hit
 * zone remains visual-only so elementFromPoint at the checkbox centre resolves
 * the checkbox itself rather than the forwarding wrapper. Row checkboxes keep
 * the enlarged forwarding overlay below. */
.reg-head .c-sel::before { pointer-events: none; }
.reg-head .c-sel input { width: 18px; height: 18px; }
/* The overlay is generated on the CELL, not on the checkbox.
 *
 * MEASURED: `input[type=checkbox]::before` generates nothing — a checkbox is a
 * REPLACED element, and replaced elements have no generated content in any
 * engine in the operator's fleet. The first cut of this fix put ::before on the
 * input and the enlarged zone silently did not exist: elementFromPoint one
 * pixel off the 13px box already missed. The cell is a plain <span>, so its
 * ::before is real. */
/* `:not(.reg-addrow)` is load-bearing (04/08/2026). The ADD/EDIT row has no
 * select checkbox at all — its `.c-sel` cell holds the COLLAPSE chevron
 * (`.ir-plus`) — and this overlay, generated on the cell, painted straight over
 * that 13px button. Measured on win11-desktop: `elementFromPoint` at the
 * chevron's own centre returned `SPAN.c-sel`, so the click never reached the
 * button and the add row would not fold up. The zone belongs to the checkbox;
 * it has no business in a row that has none. The chevron gets its own,
 * equally generous zone below. */
.reg-row:not(.reg-addrow) .c-sel::before {
  content: '';
  position: absolute;
  /* Symmetric bleed. `top/bottom` reach past the row's padding so the whole
   * row height is live; the left bleed is deliberately larger than the right so
   * the zone grows into the row's own gutter, never into the flag column. */
  top: -8px; bottom: -8px; left: -11px; right: -7px;
  border-radius: 4px;
  cursor: pointer;
}
/* The zone is deliberately INVISIBLE (operator 30/07/2026: "the select button —
 * there is absolutely no need to provide a fill for the hover-over tapzone. it
 * can be invisible"). It used to paint an accent tint on hover so its size was
 * discoverable; in a dense register that tint fires on every row the pointer
 * crosses, which reads as flicker rather than affordance. The hit area is
 * unchanged — only the paint is gone, so the target stays as large as it was
 * and the checkbox itself remains the visible control. */
/* Touch surfaces get the full 44px both ways — there is no cursor to aim with,
 * and the phone/tablet list has row heights that can carry it. */
body[data-surface="phone"] .reg-row:not(.reg-addrow) .c-sel::before,
body[data-surface="tablet"] .reg-row:not(.reg-addrow) .c-sel::before {
  top: 50%; bottom: auto; left: 50%; right: auto;
  width: 44px; height: 44px; transform: translate(-50%, -50%);
}
.reg-txn.selected { background: color-mix(in srgb, var(--accent) 12%, var(--card)); }
@media (hover: hover) { .reg-txn.selected:hover { background: color-mix(in srgb, var(--accent) 16%, var(--card)); } }
.reg-selbar { display: flex; gap: 8px; align-items: center; margin-left: auto; }
.reg-selcount { padding: .35rem .8rem; font-size: var(--fs-body); }
.reg-head {
  border-top: 0; font-size: var(--fs-micro); text-transform: none; letter-spacing: .06em;
  color: var(--ink); font-weight: 700; min-height: 24px; position: sticky; top: 0;
  background: var(--card); z-index: 1;
}
body[data-surface="desktop"] .reg-grid .reg-head {
  position: sticky;
  top: 0;
  z-index: 4;
  background: var(--card);
  isolation: isolate;
}
.reg-txn { cursor: pointer; }
.reg-txn:focus-visible { background: color-mix(in srgb, var(--accent) 6%, var(--card)); outline: none; }
@media (hover: hover) { .reg-txn:hover { background: color-mix(in srgb, var(--accent) 6%, var(--card)); outline: none; } }
.reg-txn.unapproved { background: color-mix(in srgb, var(--accent) 4%, var(--card)); font-style: italic; }
/* FLAG COLUMN (operator 29/07/2026: "each flag by each transaction is also not
 * adhering to the requested pennant symbol", and the glyph was "far, far too
 * small").
 *
 * It used to be a 7px COLOUR STRIPE — a bar of --flag with no glyph at all, so
 * an unflagged row showed nothing and a flagged one showed a stripe, never the
 * flag symbol the reference register shows on every row. The track is now
 * 1.35rem and carries a real outlined swallow-tail flag: faint when the row is
 * unflagged, filled with the row's own colour when it is. */
.reg-row .c-flag {
  display: flex; align-items: center; justify-content: center;
  align-self: center; overflow: visible;
}
/* The flag cell's picker trigger (v1.86.0 made the glyph clickable). It was
 * shipped with NO rules of its own, so it painted with the USER-AGENT button
 * style: a grey `buttonface` fill, a 2px outset border and ~1-6px of padding.
 * That is exactly the operator's report (29/07/2026) — "a grey fill square over
 * it", and the pennant "so small", because the UA padding+border ate the track
 * and squeezed the glyph inside it. The button is a hit target only; the glyph
 * is the entire visual, so strip the chrome and let it fill the cell. */
.reg-row .c-flag .reg-flag-btn {
  appearance: none; -webkit-appearance: none;
  background: none; border: 0; padding: 0; margin: 0; font: inherit; color: inherit;
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%; overflow: visible; cursor: pointer;
}
.reg-row .c-flag .reg-flag-btn:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px; border-radius: 4px;
}
.reg-row-flag {
  width: 1.05rem; height: 1.05rem; flex: none; overflow: visible;
  color: var(--ink-muted); opacity: .3;
}
/* Flagged: the glyph IS the colour — filled, so it reads at a glance down a
 * long register the way the old stripe did, but as a flag. */
.reg-row .c-flag .reg-row-flag.on {
  color: var(--flag, var(--ink-muted));
  fill: var(--flag, none);
  opacity: 1;
}
.reg-info,
.reg-info-head { display: inline-flex; align-items: center; justify-content: center; width: 1.35rem; height: 1.35rem; aspect-ratio: 1; border: 0; border-radius: 50%; background: transparent; color: var(--accent); padding: 0; }
.reg-info :is(.ico, .reg-info-ico), .reg-info-head :is(.ico, .reg-info-ico) { width: 100%; height: 100%; }
.reg-info { margin-inline-end: .2rem; cursor: pointer; }
.reg-info:hover, .reg-info:focus-visible { background: var(--accent); color: var(--accent-ink); }
.reg-info-head { margin-inline-end: .2rem; pointer-events: none; }
.reg-row .c-info { display: flex; align-items: center; justify-content: center; min-width: 0; overflow: visible; }
.reg-row .c-info .reg-info { margin: 0; }
/* Desktop approval review owns a filled information control. Ordinary
 * register rows keep the outlined circled “i” above; this scope includes the
 * scheduled-rule disclosure row as well. */
body[data-surface="desktop"] .reg-info.approval-info,
body[data-surface="desktop"] .reg-info.sched-approval-info {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}
body[data-surface="desktop"] .reg-info.approval-info:hover,
body[data-surface="desktop"] .reg-info.sched-approval-info:hover {
  background: color-mix(in srgb, var(--accent) 84%, var(--card));
  border-color: color-mix(in srgb, var(--accent) 84%, var(--card));
  color: var(--accent-ink);
}
body[data-surface="desktop"] .reg-info.approval-info:focus-visible,
body[data-surface="desktop"] .reg-info.sched-approval-info:focus-visible {
  outline: 2px solid color-mix(in srgb, var(--accent) 70%, var(--card));
  outline-offset: 2px;
}
@media (hover: hover) {
  .reg-row:hover .reg-row-flag { opacity: .55; }
  .reg-row:hover .reg-row-flag.on { opacity: 1; }
}
.reg-row .c-acct, .reg-row .c-payee, .reg-row .c-cat, .reg-row .c-memo {
  overflow: hidden; text-overflow: ellipsis;
}
/* THE TEXT RAIL (operator 04/08/2026). Left-aligned text cells were painting at
 * inkLeft 0 — flush against their grid track — so "Main" in the Account column
 * looked jammed into the column boundary, and clicking the field shifted the
 * same text 6px right because the inline editor's input has its own padding.
 * One token now feeds both, and the header cells take it too or the column
 * labels would no longer sit over their own values.
 *
 * LEFT-ALIGNED CELLS ONLY. `.c-out`/`.c-in` are right-aligned money: their ink
 * sits at the RIGHT edge, so they take the rail on that side instead. Money is
 * never clipped or nudged mid-digit (the rule directly above this one). */
.reg-row :is(.c-acct, .c-date, .c-payee, .c-cat, .c-memo) {
  padding-inline-start: var(--reg-cell-rail, 6px);
}
.reg-row :is(.c-out, .c-in) {
  padding-inline-end: var(--reg-cell-rail, 6px);
}
/* THE EDIT ROW KEEPS THE CELL'S RAIL and its CONTROLS add nothing on top. The
 * ink must land in the same place whether a field is being read or edited, so
 * exactly ONE box in the stack may carry the inset — and making it the CELL
 * means the same rule serves both states.
 *
 * Getting this the other way round is what produced the shift: with the cell AND
 * the input both padded, the Account text jumped 12px on edit; cancelling only
 * the select left 6px. The control audit's "Main account shifted Npx in edit"
 * assertion measures the editor's text against the cell, and it is the SUM that
 * has to match, not each box in isolation. */
/* (The editor controls' own inset is set where they are sized, below — search
 * `.reg-editrow .inp`. Nothing to override here.) */
/* The payee cell carries inline glyphs (recurring loop, transfer chevrons) beside
 * the name. As a plain block cell they sat on the TEXT BASELINE — `vertical-align:
 * middle` aligns to the baseline plus half an x-height, which is not the row's
 * centre, so both glyphs painted low (operator 2026-07-17: "glyphs are not lining
 * up ... not vertically in the vertical center of the row"). A flex line centres
 * them against the row box itself. min-width:0 keeps the name ellipsising, and the
 * name span owns the truncation now that the cell is a flex container. */
/* Scoped to .reg-txn: the header, the inline editor's <input> and the split
 * placeholder all share .c-payee and must keep their block layout. */
.reg-txn .c-payee {
  display: flex; align-items: center; min-width: 0; overflow: hidden;
}
.reg-txn .c-payee > .reg-payee-name,
.reg-txn .c-payee > .xfer-line {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* All transaction text is dark ink — no light-grey category/memo (operator:
 * "no light grey transaction text, all dark grey"). */
.reg-row .c-cat { color: var(--ink); }
.reg-row .c-memo { color: var(--ink); }
.reg-row .c-out, .reg-row .c-in { text-align: right; font-variant-numeric: tabular-nums; }
/* Posted and split transaction amounts share one typographic face. Their
 * semantic colours remain separate (neutral outflow, positive inflow), while
 * size and weight stay identical even when only one side carries a value. */
.reg-txn :is(.c-out, .c-in) { font-size: var(--fs-body); font-weight: 700; }
.reg-txn .c-in { min-width: 0; overflow: visible; text-overflow: clip; font-weight: 700; color: var(--pos); }
/* Tappable clear toggle (item 11): uncleared shows a faint ring so it reads as
 * an actionable control; cleared is a solid green dot. */
.reg-row .c-clr {
  text-align: center; font-size: var(--fs-micro); cursor: pointer; justify-self: center;
  width: 1rem; height: 1rem; line-height: 1rem; border-radius: 50%;
  color: transparent; box-shadow: 0 0 0 1px var(--hairline) inset;
  position: relative;
}
.reg-row .c-clr .clear-sync-ring {
  position: absolute; inset: 0; width: 100%; height: 100%; border-radius: 50%;
  box-sizing: border-box;
  border: 2px solid color-mix(in srgb, var(--accent) 22%, transparent);
  border-top-color: var(--accent); opacity: 0; pointer-events: none;
}
.reg-row .c-clr[aria-busy="true"] .clear-sync-ring {
  opacity: 1; animation: clear-sync-spin .7s linear infinite;
}
.reg-row .c-clr[data-sync-queued="true"] .clear-sync-ring {
  opacity: 1; border-style: dashed; border-color: var(--accent);
}
.reg-row .c-clr.clear-repeat-pulse .clear-sync-ring {
  animation: clear-sync-spin .36s linear infinite, clear-sync-ack .36s ease-out;
}
@keyframes clear-sync-spin { to { transform: rotate(360deg); } }
@keyframes clear-sync-ack { 50% { scale: 1.35; } }
@media (prefers-reduced-motion: reduce) {
  .reg-row .c-clr[aria-busy="true"] .clear-sync-ring,
  .reg-row .c-clr.clear-repeat-pulse .clear-sync-ring { animation: none; }
}
/* The "C" column header is a FILTER TOGGLE, not a label (operator 2026-07-14:
 * clicking it on macOS did nothing — it was a bare <span> with cursor:default,
 * which actively told the user it was dead). It now reads and behaves as a
 * control: pointer cursor, hover tint, and a solid accent fill when the
 * uncleared-only filter is on. Theme tokens only, so dark and light both hold. */
.reg-row.reg-head .c-clr {
  cursor: pointer; box-shadow: none; color: var(--ink-muted);
  width: auto; height: auto; border-radius: 999px;
  background: none; border: 0; font: inherit; font-size: var(--fs-micro);
  padding: .1rem .4rem; letter-spacing: .04em;
  /* The grid's own painted surface sits above its children, so a real button in
   * a grid track receives no clicks without its own stacking context. (A <span>
   * never revealed this — there was nothing to click.) */
  position: relative; z-index: 1;
}
@media (hover: hover) { .reg-row.reg-head .c-clr:hover { color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent); } }
.reg-row.reg-head .c-clr.on {
  color: var(--accent-ink); background: var(--accent); box-shadow: none;
}
@media (hover: hover) { .reg-row .c-clr:hover { box-shadow: 0 0 0 1px var(--pos) inset; } }
.reg-row .c-clr.on {
  color: var(--pos-ink); background: var(--pos); box-shadow: none;
}
/* Sortable column headers (operator 2026-07-20): Date / Payee / Category /
 * Outflow are real buttons that cycle the sort. They must read IDENTICALLY to
 * the inert header labels beside them (same Title Case micro type, same colour,
 * same box) — a header that looks like a button would clutter the row. The only
 * added affordance is a pointer cursor, a hover tint, and a ▲/▼ caret on the
 * active column. z-index:1 for the same grid-paint-eats-clicks reason as .c-clr. */
.reg-row.reg-head .reg-sorthead {
  background: none; border: 0; font: inherit; color: inherit;
  text-transform: none !important; letter-spacing: inherit;
  cursor: pointer; padding: 0; text-align: inherit;
  display: inline-flex; align-items: center; gap: .15em;
  position: relative; z-index: 1; -webkit-tap-highlight-color: transparent;
}
.reg-row.reg-head .reg-sortcell {
  display: flex; align-items: center; min-width: 0; overflow: visible;
}
.reg-row.reg-head .reg-sortcell > .reg-sorthead {
  min-width: 0; max-width: 100%; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap;
}
.reg-row.reg-head .c-out .reg-sorthead {
  width: 100%; justify-content: flex-end;
}
.reg-row.reg-head .reg-sorthead.c-out { justify-content: flex-end; }
@media (hover: hover) { .reg-row.reg-head .reg-sorthead:hover { color: var(--accent); } }
.reg-row.reg-head .reg-sorthead.sorted { color: var(--accent); }
.reg-sort-caret { font-size: .8em; line-height: 1; }
/* COLUMN-HEADER GLYPHS (operator 29/07/2026: "mimic this format including
 * table's column headers (incl. icons and glyphs)"). Each header word gets a
 * small leading marker. Deliberately quiet: the header row is uppercase micro
 * type, so a full-contrast icon beside it would out-shout the label it marks.
 * Sized in em so it tracks the header's own font-size through the density
 * settings and the column-resize widths, and `flex:none` so a narrowed column
 * squeezes the TEXT (which can ellipsis) rather than deforming the glyph. */
.reg-row.reg-head .reg-head-ico {
  width: 1.28em; height: 1.28em; flex: none; opacity: .62;
  margin-right: .35em; overflow: visible;
}
.reg-row.reg-head :is(.c-acct, .c-memo, .reg-sortcell) {
  display: flex; align-items: center; justify-content: flex-start; min-width: 0;
}
.reg-row.reg-head .reg-sortcell > .reg-sorthead { justify-content: flex-start; }
.reg-row.reg-head :is(.c-acct, .c-date, .c-payee, .c-cat, .c-subcat, .c-memo) { text-align: left; }
/* The active/hovered sort column brightens its glyph with its text. */
.reg-row.reg-head .reg-sorthead.sorted .reg-head-ico { opacity: .9; }
@media (hover: hover) {
  .reg-row.reg-head .reg-sorthead:hover .reg-head-ico { opacity: .9; }
}
/* The flag header is a filter trigger, not another sortable column. Keep the
 * glyph centred in its canonical rail and expose the active count without
 * changing the grid track. */
.reg-row.reg-head .reg-flaghead {
  appearance: none; border: 0; background: none; color: inherit;
  overflow: visible; justify-content: center; padding: 0; cursor: pointer;
  position: relative; z-index: 1; -webkit-tap-highlight-color: transparent;
}
.reg-row.reg-head .reg-flaghead.active { color: var(--accent); }
.reg-row.reg-head .reg-flaghead:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px; border-radius: 4px;
}
.reg-flag-filter-count {
  position: absolute; top: 1px; right: 1px;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1em; height: 1em; padding: 0 .18em;
  border-radius: 999px; background: var(--accent); color: var(--accent-ink);
  font-size: .68em; line-height: 1; letter-spacing: 0;
}
.reg-flag-filter-menu {
  z-index: 1005; min-width: 15rem; max-width: min(18rem, calc(100vw - 16px));
  padding: .55rem; box-sizing: border-box;
}
.reg-flag-filter-menu .ctx-menu-title {
  margin: 0 0 .35rem; padding: .2rem .35rem;
  color: var(--ink); font-size: var(--fs-label); font-weight: 700;
}
.reg-flag-filter-list { display: grid; gap: 2px; }
.reg-flag-filter-option {
  display: flex; align-items: center; gap: .55rem; min-height: var(--ctl-h-sm);
  padding: .2rem .35rem; border-radius: var(--radius-s); color: var(--ink);
  cursor: pointer;
}
.reg-flag-filter-option:hover,
.reg-flag-filter-option:focus-within { background: color-mix(in srgb, var(--accent) 10%, var(--card)); }
.reg-flag-filter-option input { margin: 0; accent-color: var(--accent); }
.reg-flag-filter-swatch {
  width: .72rem; height: .72rem; flex: none; border-radius: 50%;
  background: var(--item-color); box-shadow: 0 0 0 1px var(--hairline) inset;
}
.reg-flag-filter-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.reg-flag-filter-foot {
  display: flex; justify-content: space-between; gap: .4rem; margin-top: .45rem;
  padding-top: .45rem; border-top: 1px solid var(--hairline);
}
.reg-flag-filter-foot .btn { min-height: var(--ctl-h-sm); padding: .2rem .55rem; }
/* Sized to match the row glyphs beneath it, not to the header's micro type —
 * an icon scaled off an 11px uppercase label is the "far, far too small"
 * the operator reported (29/07/2026). */
.reg-row.reg-head .reg-flag-ico {
  width: 1.05rem; height: 1.05rem; flex: none; overflow: visible;
}
/* SPLIT-TRANSACTION MARKER (operator 29/07/2026), painted after the word
 * "Split" in the Category cell on EVERY surface. Not a control — the ▸ toggle
 * before the word owns expanding the parts — so it carries no pointer cursor
 * and no hover state. Accent-tinted so it reads as a status marker in the same
 * visual family as the recurring badge. */
.reg-split-ico {
  display: inline-flex; align-items: center; vertical-align: -.14em;
  margin-left: .35em; color: var(--accent); flex: none;
}
.reg-split-ico .ico { width: .95em; height: .95em; overflow: visible; }
/* Inside the touch list's category CHIP the glyph sits on a tinted pill, so it
 * inherits the chip's ink rather than shouting accent against it. */
.txn-cat-inline .reg-split-ico { color: inherit; opacity: .75; margin-left: .3em; }
/* Snooze/defer glyph (operator 20/08/2026): a POSTED transaction's status rail
 * or a scheduled occurrence's cadence badge line, immediately after the
 * recurring badge — same non-interactive status-marker language as
 * .reg-split-ico (no pointer cursor, no hover state; it is a MARKER, not a
 * control). Tokenised colour only — dark/light parity automatic. */
.reg-snooze-ico {
  display: inline-flex; align-items: center; align-self: center; vertical-align: middle; line-height: 0;
  margin-left: .35em; color: var(--accent) !important; flex: none;
}
.reg-snooze-ico .ico { display: block; width: .68em; height: .68em; overflow: visible; vertical-align: 0; }
.reg-reminder-date {
  display: inline-flex; align-items: center; align-self: center; margin-left: .32em;
  padding: .04rem .38rem; border: 1px solid var(--status-lozenge-outline);
  border-radius: 999px; background: transparent; color: var(--status-lozenge-outline);
  font-size: var(--fs-micro); font-weight: 400; line-height: 1.15; white-space: nowrap;
}
body[data-surface="desktop"] .reg-reminder-date {
  padding: .046rem .437rem;
  font-size: calc(var(--fs-micro) * 1.15);
}

/* Confirm Later uses the established card/button/picker families. The state is
 * deliberately compact enough for the desktop drawer while its actions wrap
 * into full-width touch targets on phone/tablet. */
.confirm-later-card {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center; gap: .65rem .8rem; padding: .75rem;
  border: 1px solid var(--hairline); border-radius: var(--radius-s);
  background: var(--card); color: var(--ink);
}
.confirm-later-card.active { border-color: var(--accent); }
.confirm-later-glyph { display: inline-flex; color: var(--accent); }
.confirm-later-glyph .ico { width: 1.2rem; height: 1.2rem; }
.confirm-later-copy { display: flex; flex-direction: column; gap: .15rem; min-width: 0; text-align: left; }
.confirm-later-actions { display: flex; align-items: stretch; justify-content: flex-end; gap: .4rem; flex-wrap: wrap; }
.confirm-later-actions .btn { min-height: var(--ctl-h-md); }
.confirm-later-picker { width: min(24rem, calc(100vw - 2rem)); }
.confirm-later-picker .pick-row { text-transform: none !important; }
.confirm-later-current { margin: .7rem .85rem .2rem; color: var(--ink-muted); text-align: left; }
.cal-cell:disabled {
  background: transparent; border: 0; box-shadow: none;
  color: var(--ink-muted); opacity: .45; cursor: default;
}
body[data-surface="phone"] .confirm-later-card,
body[data-surface="tablet"] .confirm-later-card { grid-template-columns: auto minmax(0, 1fr); }
body[data-surface="phone"] .confirm-later-actions,
body[data-surface="tablet"] .confirm-later-actions { grid-column: 1 / -1; justify-content: stretch; }
body[data-surface="phone"] .confirm-later-actions .btn,
body[data-surface="tablet"] .confirm-later-actions .btn { flex: 1 1 9rem; min-height: var(--ctl-h-md); }
/* Real schedule setting status: a compact word-glyph beside cadence/snooze.
 * It is status text, not a button label; all-caps AUTO is the operator's
 * requested glyph and the accent token preserves day/night parity. */
.sched-autoapprove-ico {
  display: inline-flex; align-items: center; vertical-align: -.14em;
  margin-left: .42em; padding: .12rem .5rem; border-radius: 999px;
  color: var(--accent); flex: none;
  font-size: calc(var(--fs-label) * .9); line-height: 1; font-weight: 400; letter-spacing: .045em;
}
/* Ambiguous-duplicate resolver marker (occurrence-identity/lifecycle mission
 * sub-mission 4/6, task 3): desktop keeps a compact status glyph for its
 * context-menu route; touch upgrades it to the direct action below. Reuses
 * --neg (the same token .sched-problem's warning triangle already uses) —
 * tokenised colour only, dark/light parity automatic. */
.sched-conflict-ico {
  display: inline-flex; align-items: center; vertical-align: -.14em;
  margin-left: .35em; color: var(--neg); flex: none;
}
.sched-conflict-ico .ico { width: .95em; height: .95em; overflow: visible; }
/* Touch duplicate resolver: the warning is a real action, not a decorative
 * glyph that requires an unavailable right-click/long-press gesture. Keep the
 * paint compact while the transparent hit surface honours the 44px floor. */
.sched-conflict-action {
  display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 var(--ctl-h-md); width: var(--ctl-h-md); min-height: var(--ctl-h-md);
  padding: 0; border: 0; border-radius: var(--radius-s); background: transparent;
  color: var(--neg); cursor: pointer;
}
.sched-conflict-action .ico { width: .95em; height: .95em; overflow: visible; }
@media (hover: hover) {
  .sched-conflict-action:hover { background: color-mix(in srgb, var(--neg) 10%, transparent); }
}
.sched-conflict-action:focus-visible { outline: none; background: color-mix(in srgb, var(--neg) 12%, transparent); }
/* Reconcile lock (v5, item 4): grey padlock beside the flag; the whole row reads
 * as locked (muted) until the user unlocks it. */
.rec-lock { border: 0; background: none; cursor: pointer; padding: 0; font-size: var(--fs-label); line-height: 1; filter: grayscale(1); opacity: .6; }
@media (hover: hover) { .rec-lock:hover { opacity: 1; } }
.reg-row.reconciled { opacity: .82; }
.reg-row.reconciled .c-flag { display: flex; align-items: center; justify-content: center; }
/* The lock's paint stays compact, but its row-level status rail remains a full
 * touch target on phone/tablet. This keeps the dense row while preserving the
 * same 44px finger zone as the selection circle. */
body[data-surface="phone"] .txn-status-rail,
body[data-surface="tablet"] .txn-status-rail,
body[data-surface="phone"] .txn-status-rail .rec-lock,
body[data-surface="tablet"] .txn-status-rail .rec-lock {
  width: var(--ctl-h-md); height: var(--ctl-h-md); min-width: var(--ctl-h-md); min-height: var(--ctl-h-md);
  display: inline-flex; align-items: center; justify-content: center;
}
.txn-row.reconciled { opacity: .85; }

/* Inline add row (desktop/wide grid, operator 2026-07-09): type directly into
 * every cell — no sheet, no keypad. Compact inputs sized to their columns. */
.reg-addrow {
  background: color-mix(in srgb, var(--accent) 5%, var(--card));
  min-height: var(--reg-row-h, 32px); height: var(--reg-row-h, 32px);
  box-sizing: border-box; padding-block: 0;
}
/* The chevron is a real COLLAPSE button now (operator 2026-07-16: clicking it
 * must fold the editor back up) — strip the native button chrome so it stays
 * the same blurple glyph in both themes. */
.reg-addrow .ir-plus {
  color: var(--accent); background: none; border: none; padding: 0; margin: 0;
  cursor: pointer; display: inline-flex; align-items: center; min-height: 0;
  position: relative;   /* containing block for its own hit overlay below */
}
/* The chevron's OWN hit zone (04/08/2026). It is a 13px glyph on a 34px row —
 * the same precision-target problem the row-select checkbox had — so it keeps a
 * zone of the same generosity the checkbox's used to give it, now generated on
 * the BUTTON. A <button> is not a replaced element, so unlike a checkbox its
 * ::before is real (the `ynab-checkbox-has-no-pseudo-element` lesson, applied
 * the right way round). The overlay belongs to the button, so a click on it IS
 * a click on the collapse control — no forwarding, no interception. The cell
 * must not clip it. */
.reg-addrow .c-sel { position: relative; overflow: visible; }
.reg-addrow .ir-plus::before {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: 34px; height: 28px;
  transform: translate(-50%, -50%);
  border-radius: 4px;
}
.reg-addrow .ir-plus .ico { width: 1em; height: 1em; }
.reg-addrow .inp {
  width: 100%; min-width: 0; padding: 3px 6px; font: inherit; font-size: var(--fs-body);
  border: 1px solid var(--hairline); border-radius: 6px; background: var(--card); color: var(--ink);
}
.reg-addrow .inp:focus { outline: none; border-color: var(--accent); }
/* --- Editing a row must not move the row ---------------------------------- */
/* Operator 2026-07-29: "clicking a row, any field, ought not to make that row
 * change in height or have any of the fields change location (shift to the
 * right)". The row now follows the active 25/32/41px density ruler in both
 * resting and editing states, with no value sliding between them.
 *
 * Both came from the input chrome, not from the row: `.reg-addrow .inp` is
 * 30px tall (3px padding + 1px border, border-box) and insets its text by 6px
 * padding + 1px border, while the display cell it replaces has neither. The
 * ADD row keeps that roomier treatment — it is a standing composer at the foot
 * of the list, and nothing sits below it to be pushed. An EDIT row is spliced
 * into the middle of the register, so it must occupy exactly the geometry of
 * the row it replaces: pull the input flush to the cell box and let the row's
 * active density ruler govern.
 *
 * The input stays wholly inside its grid cell. Its inset focus ring and
 * hairline paint without changing the row or crossing a column boundary. */
/* `height: 100%` cannot be used here: the row is a grid item sized by
 * min-height, so a percentage height has no definite base to resolve against
 * and the input keeps its own 30px — which is exactly the 3px the row grew by.
 * Pin the control to the row's own density floor instead. */
/* The `body[data-surface="desktop"]` prefix is REQUIRED, not decoration: the
 * desktop control-sizing block sets `.inp { min-height: 30px }` at that same
 * prefix, which outranks a bare `.reg-editrow .inp` and is precisely the 30px
 * that made the row grow. Match its specificity to override it. */
body[data-surface="desktop"] .reg-editrow .inp {
  /* The row owns a 1px top border. A full row-height input therefore made the
   * grid track one pixel taller on edit. Fit inside the content box instead. */
  height: calc(var(--reg-row-h, 32px) - 1px); min-height: 0; line-height: 1;
  padding-block: 0;
  /* THE RAIL MOVES INSIDE THE BOX WHEN A FIELD IS EDITED (operator 04/08/2026:
   * "see how horribly left aligned fields are - touching the left margin of
   * their outline box. this was promised fixed earlier. it was not").
   *
   * The previous arrangement zeroed this inset and let the CELL supply the rail.
   * That did hold the ink still between resting and editing — but this control
   * paints its OWN outline (the inset box-shadow below), and a zero left pad put
   * that outline flush against the first glyph. The text was correctly placed in
   * the column and visibly jammed inside its own box at the same time.
   *
   * So the rail is still exactly ONE 6px inset — it just belongs to whichever
   * box is painting an edge. While editing, the cell cancels its own rail (see
   * the sibling rule below) and the control carries it here, so the ink lands on
   * the same rail as at rest AND the outline clears the glyph. What must stay
   * true is the SUM (cell + control = one rail), never each box in isolation. */
  padding-inline: var(--reg-cell-rail, 6px);
  margin-inline: 0;
  width: 100%;
  border: 0;
  box-shadow: inset 0 0 0 1px var(--hairline);
}
/* Inline editing replaces a display row in-place. Fix the row's own box and
 * stretch its cells so a focused input cannot alter the grid track or move a
 * neighbouring rail. */
body[data-surface="desktop"] .reg-editrow {
  height: var(--reg-row-h, 32px);
  min-height: var(--reg-row-h, 32px);
  box-sizing: border-box;
  align-items: stretch;
}
body[data-surface="desktop"] .reg-editrow > :is(.c-sel, .c-flag, .c-info, .c-acct, .c-date, .c-payee, .c-cat, .c-subcat, .c-memo, .c-out, .c-in, .c-clr) {
  min-width: 0;
  box-sizing: border-box;
}
/* Add and edit rows share the canonical register rails and density height.
 * The row owns the track; each replacing control carries exactly one text rail
 * so switching between display and edit leaves every column stationary. */
body[data-surface="desktop"] :is(.reg-addrow, .reg-editrow) .inp {
  height: calc(var(--reg-row-h, 32px) - 1px); min-height: 0; line-height: 1;
  padding-block: 0; padding-inline: var(--reg-cell-rail, 6px);
  margin-inline: 0; width: 100%; box-sizing: border-box;
  border: 0; box-shadow: inset 0 0 0 1px var(--hairline);
}
body[data-surface="desktop"] :is(.reg-addrow, .reg-editrow) :is(.c-acct, .c-date, .c-payee, .c-cat, .c-memo) {
  padding-inline-start: 0; overflow: hidden;
}
body[data-surface="desktop"] :is(.reg-addrow, .reg-editrow) :is(.c-out, .c-in) {
  padding-inline-end: 0;
}
body[data-surface="desktop"] :is(.reg-addrow, .reg-editrow) .inp:focus {
  box-shadow: inset 0 0 0 1px var(--accent);
}
body[data-surface="desktop"] :is(.reg-addrow, .reg-editrow) select.inp {
  padding-inline: var(--reg-cell-rail, 6px) 18px;
}
/* The other half of the swap above: with the control now carrying the rail, the
 * cell must give its own up, or the two stack and the text jumps 12px on edit —
 * the exact regression the control audit's "Main account shifted Npx in edit"
 * assertion was written to catch. Money cells rail on the RIGHT, so they are
 * untouched here. */
body[data-surface="desktop"] .reg-editrow :is(.c-acct, .c-date, .c-payee, .c-cat, .c-memo) {
  padding-inline-start: 0;
}
/* Money rails on the RIGHT, so the same swap applies to that side: the control
 * already carries 6px of its own, and leaving the cell's end rail in place
 * stacked them to 12px — measured as the amount jumping 6px left the moment the
 * row opened, which is the same defect as the text one, mirrored. */
body[data-surface="desktop"] .reg-editrow :is(.c-out, .c-in) {
  padding-inline-end: 0;
}
body[data-surface="desktop"] .reg-editrow .inp:focus { box-shadow: inset 0 0 0 1px var(--accent); }
/* A select renders its own arrow inside the box; keep its right padding so the
 * arrow does not sit on the text. */
/* The Account select needs NO inset of its own: the cell supplies the shared
 * rail and the select fills that cell's content box, so it already starts on the
 * rail. Its padding is zeroed rather than set to the token — adding the token
 * here as well stacked a second rail and the Account text jumped 12px on edit.
 * (A negative margin to "compensate" instead over-corrected to -6px; the box was
 * always aligned, only the padding was doubled.) The 18px on the end still
 * clears the select's own arrow. */
/* Same swap as the text controls above: the cell has given up its rail while
 * editing, so the select carries it and its own box no longer sits on the "M" of
 * "Main". The 18px on the end still clears the select's own arrow. (Kept ABOVE
 * the selector, not inside the block: v1850-drawer-and-popups.test.js reads the
 * first 220 characters of this rule to prove the select neither overhangs Date
 * nor falls short of its cell, and an inline comment pushes those declarations
 * out of that window.) */
body[data-surface="desktop"] .reg-editrow select.inp {
  padding-inline: var(--reg-cell-rail, 6px) 18px; margin-inline: 0;
  width: 100%;
}
/* Editing text cells retain the display rows' clipping boundary. The inset
 * outline remains visible without opening an overflow route into the next
 * column. */
body[data-surface="desktop"] .reg-editrow :is(.c-acct, .c-payee, .c-cat, .c-memo) { overflow: hidden; }
.reg-addrow .c-out .inp, .reg-addrow .c-in .inp { text-align: right; font-variant-numeric: tabular-nums; }
.reg-addrow .inp.ir-bad { border-color: var(--neg); background: color-mix(in srgb, var(--neg) 8%, var(--card)); }
.reg-addrow .ir-acctname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Inline add/edit rows use the same canonical flag control and glyph as posted
 * rows. The cell stretches with the row rail; no colour stripe pseudo-element
 * or absolute sizing exemption remains. */
.reg-addrow .c-flag { align-self: stretch; overflow: visible; }
.reg-addrow .c-flag .reg-flag-btn {
  appearance: none; -webkit-appearance: none;
  width: 100%; height: 100%; padding: 0; margin: 0;
  display: flex; align-items: center; justify-content: center;
  border: 0; background: none; color: inherit; cursor: pointer;
}
.reg-addrow .c-flag .reg-flag-btn:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px; border-radius: 4px;
}
.reg-addrow .c-flag .reg-row-flag { width: 1.05rem; height: 1.05rem; }
/* Inline FX chip (D-102 desktop FCY parity, 19/08/2026): a small badge button
 * living INSIDE the Memo cell (which already has flexible width — no new grid
 * column, no header realignment) so an ordinary FCY row edits with the same
 * dense rails as a plain SGD one. Off: outline glyph. On: fills with the saved
 * currency code, same accent-derived treatment as any other set toggle in the
 * row (flag, cleared) so it reads as "this row carries extra state" at a
 * glance. */
.reg-addrow .c-memo { display: flex; align-items: center; gap: 4px; }
.reg-addrow .c-memo .ir-memo { flex: 1 1 auto; min-width: 0; }
.reg-addrow .ir-fx {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 1.35rem; height: 1.35rem; padding: 0; border-radius: 999px;
  border: 1px solid var(--hairline); background: var(--card); color: var(--ink-muted);
  cursor: pointer; font-size: 9px; font-weight: 700; letter-spacing: .01em;
}
.reg-addrow .ir-fx svg { width: 14px; height: 14px; }
.reg-addrow .ir-fx:hover { border-color: var(--accent); color: var(--accent); }
/* Active state carries a currency code (text, not a glyph) — content-sized
 * lozenge, not the inactive icon's fixed circle (else "NZD" renders squashed
 * into a 1.35rem circle). */
.reg-addrow .ir-fx.on {
  width: auto; height: 1.35rem; padding: 0 7px;
  background: color-mix(in srgb, var(--accent) 16%, var(--card));
  border-color: color-mix(in srgb, var(--accent) 55%, var(--hairline));
  color: var(--accent);
}
/* The C column keeps acting as the cleared toggle on the add/edit row too
 * (operator, Win11: cleared must be settable inline) — same look as a posted
 * row's .c-clr (reg-txn), just holding a draft value until Save. */
.reg-addrow .c-clr { cursor: pointer; }
/* Cancel + Save action bar under the inline add/edit row (operator screenshot
 * 2026-07-14): a light band spanning the row width with the buttons pushed to
 * the right — Save (accent), Cancel (outline) to its left. Replaces the single
 * tick that used to sit in the last grid column and overlapped the browser's
 * native clear-× on the amount inputs. */
.reg-editbar {
  display: flex; justify-content: flex-end; align-items: center; flex-wrap: wrap; gap: 8px;
  min-height: 22px; padding: 1px 10px;
  background: color-mix(in srgb, var(--accent) 8%, var(--card));
  border-top: 1px solid var(--hairline);
}
.ir-save:disabled { opacity: .5; cursor: default; }
/* UI-1 owns the inline action geometry at the end of this file.  This bar only
 * supplies its layout band; Save/Cancel inherit the active register density. */
.reg-adderr { color: var(--neg); font-size: var(--fs-label); font-weight: 600; padding: 4px 12px; border-top: 1px solid var(--hairline); }

/* Off-budget (tracking) account register: no Category column at all (operator
 * 2026-07-14) — applied grid-wide so the header, every posted row, and the
 * inline editor drop the 6th track together and stay column-aligned. Payee +
 * Memo share the reclaimed width; no blank gap, no false "Uncategorised". */
/* ── Desktop row-expand drawer (operator 2026-07-17) ────────────────────────
 * A rich transaction (split/transfer/reconciled/FX) expands a panel FROM its
 * row, pushing the rows below down, instead of throwing a touch-shaped bottom
 * sheet over the whole register. See views/register-drawer.js for the why.
 *
 * The motion is grid-template-rows 0fr → 1fr, NOT max-height. height:auto is
 * not animatable and this file has no interpolate-size in play, so the usual
 * fallback is a max-height guess — which clips a long split or coasts through
 * dead air on a short one. 0fr→1fr resolves to the content's true height on
 * every frame, at any content length, with no magic number to maintain.
 */
.reg-drawer {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-slow, .34s) var(--ease-ios-rebound);
  /* The drawer is a plain sibling of .reg-row: .reg-grid is a scroll container
   * whose rows are each their OWN grid (each .reg-row declares its own
   * grid-template-columns), so there is no parent track for this to span and
   * nothing to fight over columns. It simply flows full-width under its row. */
  background: color-mix(in srgb, var(--accent) 4%, var(--card));
  border-top: 1px solid var(--hairline);
}
[data-theme="dark"] .reg-drawer { background: color-mix(in srgb, var(--accent) 9%, var(--card)); }
.reg-drawer.open { grid-template-rows: 1fr; }
/* Schedule work needs an unmistakable drawer hand-off. The same overshooting
 * curve applies in both directions, so opening and retracting have a small,
 * deliberate bounce instead of snapping or disappearing under a page repaint. */
.reg-drawer[data-variant="repeat-txn"] {
  transition-timing-function: var(--ease-ios-rebound);
}
.reg-drawer-inner {
  /* Both are load-bearing for the 0fr frame: a grid item's automatic minimum
   * size is its CONTENT, so without min-height:0 the row never actually
   * collapses, and without overflow:hidden the content paints outside the
   * collapsed track (the form would show through a "closed" drawer). */
  min-height: 0;
  overflow: hidden;
}
/* The open row reads as the drawer's handle: it keeps its place in the list and
 * gains the drawer's own tint so the two are visibly one object. */
.reg-row.drawer-open {
  background: color-mix(in srgb, var(--accent) 8%, var(--card));
  box-shadow: inset 3px 0 0 var(--accent);
}
[data-theme="dark"] .reg-row.drawer-open { background: color-mix(in srgb, var(--accent) 14%, var(--card)); }

/* The form host + the sheet re-skinned as an inline panel. Same markup, same
 * code (txn-form.js `mount` option) — only the framing changes: no fixed
 * viewport height, no bottom-sheet rounding, no backdrop, no entry animation
 * (the drawer's expansion IS the motion; a sheet sliding in inside an expanding
 * drawer reads as a stutter). */
.drawer-formhost { display: block; }
/* `.reg-drawer` prefix, not `.txn-sheet-inline` alone: `.txn-sheet` (and its
 * body[data-surface="desktop"] override) are declared LATER in this file, so a
 * bare single-class rule here loses on source order — the drawer rendered as a
 * 480px-wide phone sheet pinned to the left. Measured, not guessed. */
.reg-drawer .txn-sheet-inline {
  /* Undo the bottom-sheet framing: 96dvh would make a drawer taller than the
   * screen, a 480px cap wastes half the register's width, and a top-rounded
   * corner mid-list looks like a torn edge. */
  height: auto;
  max-height: none;
  /* ...and the FLOOR, which the three lines above always missed (operator
   * 30/07/2026: "an astonishing amount of wasted space when SET RECURRING is
   * clicked — should be much much less vertical space wastage esp on win 11").
   *
   * MEASURED on the repeat drawer (Repeat/Never + Delete Transaction, the two
   * cards that path shows): the content is 76px tall, the panel was 320px —
   * 238px of dead space, a 24% fill. The 320px is `min-height` from the desktop
   * CENTRED-DIALOG rule further down this file; a dialog floating over the page
   * needs a minimum body so it doesn't read as a stub, but a drawer is spliced
   * into the register and must be exactly as tall as what it contains.
   * The `height: auto` / `max-height: none` lines above were meant to strip that
   * framing, but this block is declared EARLIER than the dialog rule and so
   * loses to it on source order — measured, the drawer computes the DIALOG's
   * `max-height: 92dvh`, not the `none` written here. That is harmless (92dvh
   * never binds on a drawer this short) and is left alone; the floor is not
   * harmless, so it is excluded at the SOURCE instead, by a
   * `:not(.txn-sheet-inline)` on the dialog rule itself. Anything else added
   * here would lose the same way. */
  width: 100%;
  max-width: none;
  border-radius: 0;
  background: transparent; /* the drawer supplies the tint */
  padding: 4px 2px 2px;
  animation: none;         /* belt-and-braces; JS also sets .entered up front */
  overflow: visible;       /* no inner scroller — the drawer grows instead */
  gap: 0;
  /* Align the form's content with the grid's own 10px row gutter so the amount
   * and the fields sit on the same left edge as the columns above them, rather
   * than flush against the drawer's edge. */
  padding-left: 12px;
  padding-right: 12px;
}
/* Chrome that only makes sense on a phone sheet. The drawer IS the container,
 * so a grab handle (a swipe-dismiss affordance that does nothing on desktop), a
 * floating ✕, and a giant hero amount are all noise — the operator asked for an
 * inline form, not a sheet wearing a drawer as a costume. Escape and the form's
 * own Cancel/Save close it; the drawer needs no ✕ of its own. */
.reg-drawer .sheet-grip,
.reg-drawer .sheet-x { display: none; }
/* The head STAYS a grid — every child (.amount-zone, .calc-expr, .type-pill,
 * .sheet-x) is explicitly placed into its 3x3 tracks, so switching it to flex
 * orphaned those placements and threw the amount off the left edge. Instead
 * re-place the two survivors into ONE left-aligned row: amount, then type pill
 * beside it. Grip and ✕ are gone (above), so rows 1 and 3 are empty anyway. */
.reg-drawer .sheet-head {
  grid-template-columns: auto auto 1fr;
  text-align: left;
  row-gap: 0;
  padding: 2px 0 12px;
  border-bottom: 1px solid var(--hairline);
  margin-bottom: 12px;
}
.reg-drawer .amount-zone { grid-column: 1; grid-row: 1; justify-self: start; }
.reg-drawer .type-pill   { grid-column: 2; grid-row: 1; justify-self: start; margin-left: 14px; }
.reg-drawer .calc-expr   { grid-column: 3; grid-row: 1; justify-self: start; align-self: center; margin-left: 12px; }
/* Desktop is a mouse+keyboard surface: the calculator keypad is a thumb-first
 * affordance and has no place in a drawer. It is already desktop-suppressed for
 * the sheet (body[data-surface="desktop"] rules above); inline it must never
 * reserve its slide-up space either, or the drawer opens with a dead band. */
.reg-drawer .keypad-panel { display: none; }
.reg-drawer .sheet-scroll {
  overflow: visible; /* the drawer is the scroll context, not the form */
  max-height: none;
  padding: 0;
  /* TWO COLUMNS. A phone stacks these rows for the thumb; a 1440px register has
   * room to show the whole transaction at once, which is the entire reason for
   * editing it in place. Overrides the base flex-column (declared later in this
   * file — hence the .reg-drawer prefix). */
  display: grid;
  grid-template-columns: repeat(2, minmax(260px, 1fr));
  gap: 0 36px;
  align-content: start;
}
/* DETERMINISTIC COLUMNS (operator 2026-07-29: "explain why similar transactions
 * can have such bizarrely different layouts").
 *
 * The grid above was AUTO-FLOW, so every card placed itself by document order.
 * The form's child COUNT is not constant — the "Automatically enter this
 * recurring transaction" card only exists when Repeat != never — so one extra
 * card shunted every later card into a different cell. Two transactions that
 * differ only in whether a repeat is set therefore rendered two entirely
 * different shapes: in one, Repeat/Account/Flag/Cleared sat top-right; in the
 * other they were pushed into the LEFT column under the date, with Delete
 * thrown into the right column at card width.
 *
 * Pin each card to its own column and row instead. Now the layout is a property
 * of the FORM, not of how many optional cards happen to be present: the main
 * fields always occupy the left column, the advanced card always the right, and
 * an optional card grows its own column downward without moving anything else. */
.reg-drawer .sheet-scroll > .sheet-fields.card { grid-column: 1; }
.reg-drawer .sheet-scroll > .sheet-adv { grid-column: 2; grid-row: 1 / span 3; align-self: start; }
.reg-drawer .sheet-scroll > .txn-whendue { grid-column: 1; }
.reg-drawer .sheet-scroll > .sheet-dock { grid-column: 1 / -1; }
/* Delete and Show less are DIRECT grid children (they sit beside the advanced
 * card, not inside it), so each claimed a whole grid cell and stretched to fill
 * it — which is how a plain text button came to paint as a tall red slab wider
 * and taller than the fields it sits beside (operator 2026-07-29, "bizarrely
 * noncompliant size of the delete button"). Keep them in the left column at
 * their own intrinsic size. */
.reg-drawer .sheet-scroll > .sheet-del-full,
.reg-drawer .sheet-scroll > .sheet-showmore {
  grid-column: 1; justify-self: start; align-self: start;
  width: auto; height: auto; min-height: var(--ctl-h-sm);
}
/* Desktop field rows: the phone's .85rem stack exists to make a 44px thumb
 * target. A mouse needs none of that, and the drawer's whole value is showing
 * the form WITHOUT burying the register under it — so tighten the rows and drop
 * the chevrons (a chevron says "this pushes a new screen"; here it opens a
 * picker in place). */
.reg-drawer .field-row,
.reg-drawer .field-btn { padding: .32rem 0; gap: 10px; }
/* THE DRAWER IS PART OF THE REGISTER, SO IT READS AT THE REGISTER'S SIZE
 * (operator 05/08/2026, third report: the Giant Supermarket slideout "is
 * completely not ontheme"). Measured on the desktop harness BEFORE this rule:
 * every field row rendered at 16px — the bare document default, because the
 * compaction rule above sets padding and gap but never `font-size` — against
 * the register's own 14.4px directly above it. Same omission, and same visible
 * result, as the scheduled rows fixed in v1.115.0: a panel whose type is a
 * size larger than everything around it does not read as the same app.
 * Row heights were ragged with it (33 / 46.9 / 56.6 / 28.3px); one type size
 * lets the shared padding produce one rhythm. */
body[data-surface="desktop"] .reg-drawer :is(.field-row, .field-btn, .sheet-fields, .sched-section) {
  font-size: var(--fs-body);
}
.reg-drawer .field-chev { display: none; }
/* Further Win11 drawer compaction (operator 2026-07-20: "slideout drawers all
 * can benefit from significant compacting and saving of vertical space… merge
 * rows if needed"). Shrink the head, drop each card's own top/bottom breathing
 * room and section-label margins, and pull the card gap in — the drawer should
 * read as a dense form, not a stack of roomy cards. All token/relative, both
 * themes hold. Merge Starts/Ends onto one row: see .sched-recur below. */
.reg-drawer .sheet-head { padding: 0 0 8px; margin-bottom: 8px; }
.reg-drawer .sheet-fields.card { padding: 4px 14px; }
.reg-drawer .sheet-fields.card + .sheet-fields.card { margin-top: 6px; }
.reg-drawer .field-label.sched-section { margin: 2px 0 0; }
.reg-drawer .sheet-dock { padding-top: 8px; margin-top: 6px; }
/* The advanced card is expanded from the start in a drawer (txn-form.js sets
 * showMore for inline), so its toggle would be a button that does nothing. */
.reg-drawer .sheet-showmore { display: none; }
/* Delete is a full-bleed slab on the phone because it is the last thing in a
 * long scroll and needs a thumb target. In the drawer it lands mid-form, where
 * a wide red block outweighs every field around it — the destructive action
 * should be reachable, not loud. (It still routes through the same designed
 * confirm dialog; only its footprint changes.) */
.reg-drawer .sheet-del-full {
  justify-self: start;
  width: auto;
  padding-inline: 18px;
  background: none;
  color: var(--neg);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--neg) 40%, transparent);
}
.reg-drawer .field-value { font-size: var(--fs-body); }
.reg-drawer .payee-field-context {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: stretch; gap: 10px;
}
.reg-drawer .payee-field-context .field-row { min-width: 0; }
.reg-drawer .payee-context-action {
  align-self: center; min-height: var(--ctl-h-md); padding-inline: 16px;
  white-space: nowrap;
}
.reg-drawer .payee-tools-host {
  min-width: 0; margin: 6px 0 8px; border-block: 1px solid var(--hairline);
}
.reg-drawer .payee-workspace-contextual {
  width: 100%; padding: 10px 0;
}
.reg-drawer .payee-workspace-contextual .payee-workspace-head {
  padding: 0 0 8px; border-bottom: 1px solid var(--hairline);
}
.reg-drawer .payee-workspace-contextual .payees-body { display: block; min-height: 0; }
.reg-drawer .payee-workspace-contextual .payees-right {
  display: grid;
  grid-template-columns: minmax(240px, .8fr) minmax(360px, 1.2fr);
  gap: 10px 24px; overflow: visible;
}
.reg-drawer .payee-workspace-contextual .payee-merge {
  grid-column: 2; grid-row: 1 / span 2;
}
.reg-drawer .payee-workspace-contextual .payee-delete-section {
  grid-column: 1; grid-row: 2;
}
.reg-drawer .payee-workspace-contextual .payees-footer {
  justify-content: flex-end; padding-top: 10px;
  border-top: 1px solid var(--hairline);
}
.reg-drawer .payee-workspace-contextual .payee-done { width: auto; }
/* The Cancel/Save dock spans both columns and closes the form. (The split editor
 * is NOT here: it opens as its own picker modal — `.split-modal` — so there is
 * no inline split block to span. Verified in txn-form.js, not assumed.) */
.reg-drawer .sheet-dock {
  grid-column: 1 / -1;
  flex-direction: row; justify-content: flex-end; align-items: center;
  gap: 10px;
  padding-top: 12px; border-top: 1px solid var(--hairline); margin-top: 8px;
}
/* Width only. Height/padding/radius/font come from the scoped Spending action
 * contract at the end of this file. */
.reg-drawer .sheet-dock .btn {
  width: auto; min-width: 8rem;
}
/* The Repeat editor shares the drawer, but its old footer stretched across the
 * entire register and read as a floating bar over the next transaction. Keep
 * those actions as a visibly bounded part of the drawer itself. */
.reg-drawer .sched-sheet .sheet-footer.drawer-footer {
  align-self: flex-end;
  width: min(100%, 42rem);
  margin: 4px 0 2px;
  padding: 4px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-card-desktop);
  background: color-mix(in srgb, var(--scheduled-fill) 72%, var(--card));
}
/* UI-1 supersedes the former footer-only override.  Drawer actions now use the
 * same density-derived box as the register toolbar and inline editor. */
/* Amount reads as a FIELD on desktop, not a hero display: the sheet's oversized
 * figure exists so a thumb can hit it and the keypad can drive it. Neither
 * applies here. */
.reg-drawer .amount-display { font-size: 1.6rem; }
/* EDITABLE AMOUNT (operator 29/07/2026: "clicking on the expense amount reveals
 * it is uneditable"). On desktop the amount is a real text input, so it must
 * LOOK typeable — a bare figure that happens to accept keystrokes is the same
 * trap in reverse. Underlined at rest (quiet, still reads as the headline
 * figure), fully boxed on focus. It keeps the amount's colour coding, so
 * spending stays red and inflow green while being edited. */
.reg-drawer .amount-input {
  font: inherit; font-size: 1.6rem; font-weight: 700;
  font-variant-numeric: tabular-nums;
  background: none; color: inherit;
  border: 0; border-bottom: 1.5px dashed color-mix(in srgb, currentColor 38%, transparent);
  border-radius: 0; padding: 0 2px 1px; width: 7.5ch; min-width: 0;
  text-align: left;
}
.reg-drawer .amount-input:hover {
  border-bottom-color: color-mix(in srgb, currentColor 65%, transparent);
}
.reg-drawer .amount-input:focus {
  outline: none;
  border-bottom-style: solid;
  border-bottom-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 7%, transparent);
  border-radius: 6px 6px 0 0;
}
/* EMBEDDED SPLIT TABLE (operator 29/07/2026): the split editor rendered in the
 * drawer instead of over it. It is a card in the form, not a floating panel —
 * so it drops the modal's own width cap, centring and scroll cage, and lets the
 * drawer own the height. */
.reg-drawer .split-inline { padding: 8px 14px 10px; }
.reg-drawer .split-embedded {
  display: flex; flex-direction: column; gap: 6px;
  width: 100%; max-width: none; max-height: none;
  padding: 0; background: none; box-shadow: none; border-radius: 0;
}
/* The list scrolls only if a split is genuinely long; short ones (the common
 * case, 2-4 lines) size to their content so no dead band appears under them. */
.reg-drawer .split-embedded .split-list { max-height: 15rem; overflow-y: auto; }
.reg-drawer .split-embedded .split-catsrow {
  padding: 0 0 2px; font-size: var(--fs-label);
  text-transform: uppercase; letter-spacing: .04em; color: var(--ink-muted);
  border-bottom: 1px solid var(--hairline); margin-bottom: 4px;
}
.reg-drawer .split-embedded .split-catsrow .split-remaining {
  text-transform: none; letter-spacing: 0; font-size: var(--fs-body);
}
/* Buttons sit at their own size in a form, never stretched across the card the
 * way a modal's full-width footer buttons do. */
.reg-drawer .split-embedded :is(.split-add, .split-autofix) {
  align-self: start; width: auto; min-height: var(--ctl-h-sm);
  padding-inline: 12px; font-size: var(--fs-label);
}
.reg-drawer .split-embedded .split-autofix { color: var(--accent); }

/* One column when the register is narrow (a smaller laptop, or the account rail
 * eating the width): two 260px tracks + gap need ~550px, and below that the
 * columns would crush the payee/category values into fragments. */
@media (max-width: 1180px) {
  .reg-drawer .sheet-scroll { grid-template-columns: 1fr; }
  .reg-drawer .payee-workspace-contextual .payees-right { grid-template-columns: 1fr; }
  .reg-drawer .payee-workspace-contextual :is(.payee-merge, .payee-delete-section) {
    grid-column: 1; grid-row: auto;
  }
}

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

/* Tracking accounts drop the category column; payee/memo take its room. Same
 * --reg-c-* hooks as the full grid so a drag applies here too. */
.reg-grid-nocat .reg-row {
  grid-template-columns:
    1.2rem 1.35rem 1.2rem
    var(--reg-c-acct, 8rem)
    var(--reg-c-date, 6.8rem)
    var(--reg-c-payee, 1.7fr)
    var(--reg-c-memo, 1.5fr)
    7.4rem 7.4rem 1.4rem;
}
.reg-grid-nocat .reg-row .c-cat,
.reg-grid-nocat .reg-row .c-subcat { display: none; }
/* The desktop category readability rule below has a stronger body-qualified
 * selector. Keep the tracking-account omission authoritative at the same
 * owner boundary, or the hidden tracks reappear in rows while the header has
 * already dropped them. */
body[data-surface="desktop"] .reg-grid-nocat .reg-row :is(.c-cat, .c-subcat) { display: none; }

/* A desktop window can be resized below the normal monitor sweep while the
 * navigation rail and content padding still consume their fixed room. Give
 * the register that right padding back and tighten only its inter-column gap;
 * the live width guard then has enough budget to retain every readable floor
 * without inventing a horizontal scrollbar or clipping a value. Header and
 * transaction rows share this gap, so their tracks remain in lockstep. */
@media (min-width: 1024px) and (max-width: 1180px) {
  body[data-surface="desktop"] .content:has(.reg-grid) { padding-right: 0; }
  body[data-surface="desktop"] .reg-grid .reg-row { column-gap: 6px; }
}

/* --- Drag-to-resize column widths (operator 2026-07-29) -------------------- */
/* A grip on the right edge of each resizable header cell. It is a real button so
 * it is keyboard-reachable (← / → nudge, Home resets); the pointer drag is the
 * mouse path. Sized well beyond its 1px paint so it is grabbable without
 * pixel-hunting, and it overhangs the gutter so the hit area is not stolen by
 * the neighbouring cell. */
.reg-head { position: relative; }
.reg-head > [class*="c-"] { position: relative; }
.reg-colgrip {
  position: absolute; top: 0; right: -4px; width: 18px; height: 100%;
  padding: 0; border: 0; background: none; cursor: col-resize; z-index: 2;
  display: flex; align-items: center; justify-content: center;
}
.reg-colgrip::before {
  content: ''; width: 1px; height: 60%; background: var(--hairline);
  transition: background .12s ease, width .12s ease;
}
@media (hover: hover) {
  .reg-colgrip:hover::before { width: 2px; background: var(--accent); }
}
.reg-colgrip:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.reg-colgrip:focus-visible::before { width: 2px; background: var(--accent); }
/* While dragging, the whole grid takes the resize cursor and stops selecting
 * text under the pointer — otherwise a drag paints a blue selection across
 * every row it passes over. */
body.reg-col-resizing { cursor: col-resize; user-select: none; }
body.reg-col-resizing .reg-colgrip::before { width: 2px; background: var(--accent); }

/* ---- Inline substring typeahead (register-typeahead.js) ------------------- *
 * Replaces the prefix-only <datalist>: matches any word, preselects the top hit,
 * commits on Enter/Tab. Floats over the rows below. Tokens only (dark/light). */
/* FIXED + body-mounted, not absolute inside the row: .reg-grid is overflow-x:auto,
 * which computes overflow-y to auto, so the grid clipped the dropdown vertically
 * and no list ever appeared (operator, 2026-07-16). JS sets top/left from the
 * input's rect — see attachTypeahead's reposition(). Do NOT return this to
 * position:absolute without also removing the grid's scroll container. */
.reg-drop {
  position: fixed; z-index: 70; min-width: 220px; max-width: 340px;
  max-height: 320px; overflow-y: auto;
  background: var(--card); border: 1px solid var(--hairline); border-radius: 10px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.18); padding: 4px;
}
.reg-drop-row { padding: .35rem .55rem; border-radius: 6px; cursor: pointer; white-space: nowrap; color: var(--ink); background: var(--card); }
@media (hover: hover) { .reg-drop-row:hover { background: color-mix(in srgb, var(--accent) 16%, var(--card)); } }
.reg-drop-empty { padding: .4rem .55rem; color: var(--ink-muted); font-size: var(--fs-label); }
/* Zone divider: matches above, the full browsable list below (operator
 * 2026-07-17). Sticky so the label stays readable while the browse zone
 * scrolls; both colours are tokens, so dark mode follows automatically. */
.reg-drop-sep {
  position: sticky; top: -4px; z-index: 1;
  margin: 4px 0 2px; padding: .3rem .55rem .25rem;
  border-top: 1px solid var(--hairline);
  background: var(--card); color: var(--ink-muted);
  font-size: var(--fs-label); font-weight: 600;
  text-transform: uppercase; letter-spacing: .04em; white-space: nowrap;
}
.reg-drop-sep:first-child { margin-top: 0; border-top: 0; }
.reg-drop-foot { display: flex; gap: 6px; border-top: 1px solid var(--hairline); margin-top: 4px; padding-top: 6px; }
.reg-drop-act {
  flex: 1; border: 0; border-radius: 999px; padding: .35rem .5rem; cursor: pointer;
  background: color-mix(in srgb, var(--accent) 14%, var(--card)); color: var(--accent); font: inherit; font-weight: 600; font-size: var(--fs-label);
}
@media (hover: hover) { .reg-drop-act:hover { background: color-mix(in srgb, var(--accent) 24%, var(--card)); } }

/* Inline editor date popover: month grid + repeat row (operator 2026-07-16).
 * Reuses the sheet's .calendar-modal grid, restyled from a centred modal to a
 * dropdown anchored under the date cell. FIXED + body-mounted like .reg-drop
 * above — absolute inside the cell it was clipped by the same .reg-grid
 * scroll container that hid the typeahead (v1.21.0 review); JS sets top/left
 * from the input's rect and flips above when out of room. */
.reg-datepop {
  position: fixed; z-index: 70;
  background: var(--card); color: var(--ink); border: 1px solid var(--hairline); border-radius: 12px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.18);
  max-height: calc(100vh - 8px); overflow-x: hidden; overflow-y: auto;
  animation: recurring-pop-in var(--dur-base) var(--ease-ios-rebound) backwards;
}
.reg-datepop .calendar-modal { position: static; width: 296px; max-width: 86vw; max-height: none; box-shadow: none; border: 0; animation: none; }
/* THE CADENCE LINE IS THE BUTTON (operator 29/07/2026: "make more efficient the
 * 'repeats: never' and deprecate the 'set repeat' button: make clicking on
 * 'repeats:never' the new 'set repeat' button (applies to all device fleet)").
 *
 * It was a static label plus a separate pill beside it — two controls' worth of
 * width for one action, and the half the eye actually reads (the cadence) was
 * the inert half. Now the whole row is one <button>: full-width target, the
 * value stated on the left, a chevron on the right as the only affordance.
 * `.reg-datepop-edit` is retired with the markup that carried it. */
.reg-datepop-repeat {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  width: 100%; padding: 9px 12px 10px; border: 0; border-top: 1px solid var(--hairline);
  background: none; font: inherit; text-align: left; cursor: pointer;
  font-size: var(--fs-label); color: var(--ink); white-space: nowrap;
  /* Fleet rule: this popover is reachable by touch as well, so the row has to
   * clear a finger even though it reads as a dense desktop line. */
  min-height: 44px;
}
.reg-datepop-repeat svg { width: 1em; height: 1em; vertical-align: -2px; color: var(--accent); }
.reg-datepop-repeat .rdr-chev { color: var(--accent); font-size: 1.15em; line-height: 1; opacity: .8; }
@media (hover: hover) {
  .reg-datepop-repeat:hover { background: color-mix(in srgb, var(--accent) 10%, var(--card)); }
}
.reg-datepop-repeat:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* The cadence list scrolls INSIDE the popover, and the popover itself is capped
 * at the viewport and scrolls. A flat 250px list therefore pushed the popover's
 * SCROLL CONTENT below the fold on a short screen (1366x768): the popover box
 * ended on screen, but the last cadence choices sat past the bottom edge and
 * could only be reached by scrolling a container most users never realise is
 * scrollable. `--reg-datepop-list-max` is set from place() to the space actually
 * left beneath the calendar, so the whole list is visible at every desktop
 * height (D-089: a picker must never escape the viewport). */
.reg-datepop-repeat-list {
  display: grid;
  max-height: var(--reg-datepop-list-max, 250px);
  overflow-y: auto;
  border-top: 1px solid var(--hairline); padding: 4px;
}
.reg-datepop-repeat-list[hidden] { display: none; }
.reg-datepop-repeat-list button {
  min-height: 28px; padding: 3px 9px; border: 0; border-radius: 5px;
  background: none; color: var(--ink); font: inherit; font-size: var(--fs-label);
  text-align: left; cursor: pointer;
}
.reg-datepop-repeat-list button.on {
  color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, var(--card));
}
@media (hover: hover) {
  .reg-datepop-repeat-list button:hover { background: color-mix(in srgb, var(--accent) 12%, var(--card)); }
}
.reg-datepop-custom {
  display: grid; gap: 8px; padding: 8px;
  border-top: 1px solid var(--hairline); background: var(--card);
}
.reg-datepop-custom[hidden] { display: none; }
.reg-datepop-custom .cr-count {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  align-items: center; gap: 8px; color: var(--ink); font-size: var(--fs-label);
}
.reg-datepop-custom .cr-n {
  width: 100%; height: 30px; min-height: 30px; padding: 3px 8px;
  text-align: left; font-variant-numeric: tabular-nums;
}
.reg-datepop-custom .cr-units { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px; }
.reg-datepop-custom .cr-unit {
  min-height: 30px; padding: 3px 2px; border-radius: 6px;
  font-size: var(--fs-label);
}
/* The calendar popover's own Save/Cancel footer (operator 04/08/2026: "ensure
 * there is a save button in addition to the cancel button (yes, a save/cancel
 * duplication) in the calendar region popup"). It governs the RECURRENCE panel;
 * picking a day still commits the date on its own. Separated by a hairline so it
 * reads as the popover's commit row rather than another recurrence field. */
.reg-datepop-actions {
  display: flex; justify-content: flex-end; align-items: center; gap: 8px;
  margin-top: 8px; padding-top: 8px;
  border-top: 1px solid var(--hairline);
}
/* Release promotion 05/08/2026 re-attests this exact rendered route without changing behaviour. */
/* The footer keeps only its colour hierarchy; UI-1 supplies one density-derived
 * height, inset, radius and type scale for both buttons. */
.reg-datepop-actions .rdp-cancel {
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  background: color-mix(in srgb, var(--accent) 10%, transparent); color: var(--accent);
  font: inherit; font-size: var(--fs-label);
  cursor: pointer;
}
.reg-datepop-actions :is(.rdp-cancel, .rdp-save) { cursor: pointer; }
@media (hover: hover) {
  .reg-datepop-actions .rdp-cancel:hover {
    background: color-mix(in srgb, var(--accent) 18%, transparent);
    border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  }
}
/* A disabled Save states WHY through its title (set in inline-editor.js); the
 * look has to say "not yet", not "broken". */
.reg-datepop-actions .rdp-save:disabled { opacity: .45; cursor: not-allowed; }
.reg-datepop-custom-actions { display: flex; justify-content: flex-end; gap: 6px; }
.reg-datepop-custom-actions button {
  min-height: 30px; padding: 4px 10px; border-radius: var(--radius-s);
  font: inherit; font-size: var(--fs-label); cursor: pointer;
}
.reg-datepop-custom .cr-cancel {
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  background: color-mix(in srgb, var(--accent) 10%, transparent); color: var(--accent);
}
@media (hover: hover) {
  .reg-datepop-custom .cr-cancel:hover {
    background: color-mix(in srgb, var(--accent) 18%, transparent);
    border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  }
}
.reg-datepop-custom .cr-done { border: 1px solid var(--accent); }
.reg-datepop:has(.reg-recurrence-options:not([hidden])) {
  width: min(520px, calc(100vw - 8px));
}
.reg-datepop:has(.reg-recurrence-options:not([hidden])) .calendar-modal {
  width: 100%; max-width: none;
}
.reg-recurrence-options {
  display: grid; gap: 6px; padding: 7px 10px 9px;
  border-top: 1px solid var(--hairline); background: var(--card);
  font-size: var(--fs-label);
}
.reg-recurrence-options[hidden] { display: none; }
.recurrence-options-head,
.recurrence-policy,
.recurrence-end {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  min-height: 28px;
}
.recurrence-options-head span,
.recurrence-step-end { color: var(--ink-muted); font-size: var(--fs-micro); }
.recurrence-options-head { flex-wrap: wrap; }
.recurrence-pattern, .recurrence-next-occurrence { flex-basis: 100%; }
.recurrence-policy { display: grid; grid-template-columns: auto minmax(0, 1fr); }
.recurrence-policy .rec-reminder-help { grid-column: 1 / -1; margin-top: 2px; }
/* THEMED, not Windows-default (operator 05/08/2026: "Vary amounts" and "+ Add
 * amount step" read as grey OS buttons). A flat 6px box in --chip behind a
 * --hairline is exactly the default-control look; the app's own secondary
 * controls are lozenges in an accent tint. Radius moves to --radius-s to match
 * .btn's family, fill/border/text become accent color-mix so both themes track
 * the token automatically. Heights are UNCHANGED (28px / 27px) — these stay
 * compact. */
.rec-vary,
.rec-add-step,
.rec-end-clear,
.rec-step-delete {
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  color: var(--accent); font: inherit; cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-ios-rebound),
              filter var(--dur-fast) var(--ease-standard),
              background var(--dur-fast) var(--ease-ios-rebound),
              border-color var(--dur-fast) var(--ease-standard);
}
/* Hover deepens the tint only — no size/position change, so the row never jumps. */
.rec-vary:hover,
.rec-add-step:hover,
.rec-end-clear:hover,
.rec-step-delete:not(:disabled):hover {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
}
.rec-vary:active,
.rec-add-step:active,
.rec-end-clear:active,
.rec-step-delete:not(:disabled):active { transform: scale(.95); filter: brightness(.93); }
.rec-step-delete:disabled:active { transform: none; filter: none; }
.rec-vary { min-height: 28px; padding: 3px 9px; }
.rec-vary[aria-pressed="true"] {
  border-color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, var(--card)); color: var(--accent);
}
.rec-auto { display: flex; align-items: center; justify-content: flex-end; gap: 8px; min-width: 0; }
.rec-auto span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rec-remind { display: grid; gap: 5px; padding-top: 5px; border-top: 1px solid var(--hairline); }
.rec-remind-toggle { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.rec-remind-choices { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; }
.rec-remind-choices button,
.txn-remind-mode {
  min-height: 28px; border-radius: var(--radius-s);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  color: var(--accent); font: inherit; cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-ios-rebound),
              background var(--dur-fast) var(--ease-ios-rebound),
              border-color var(--dur-fast) var(--ease-standard),
              color var(--dur-fast) var(--ease-standard);
}
@media (hover: hover) {
  .rec-remind-choices button:not(.on):hover,
  .txn-remind-mode:not(.on):hover {
    background: color-mix(in srgb, var(--accent) 18%, transparent);
    border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  }
}
.rec-remind-choices button:active,
.txn-remind-mode:active { transform: scale(.96); }
/* The chosen reminder has to out-read the new accent-tinted REST state, so it
 * goes solid accent rather than a slightly deeper tint of the same colour. */
.rec-remind-choices button.on,
.txn-remind-mode.on {
  border-color: var(--accent); color: var(--accent-ink);
  background: var(--accent);
}
.rec-remind-choices label { grid-column: 1 / -1; display: flex; align-items: center; gap: 6px; color: var(--ink-muted); }
.rec-remind-choices .inp { width: 64px; min-height: 28px; height: 28px; padding: 2px 6px; }
.recurrence-end { display: flex; align-items: center; gap: 6px; justify-content: flex-start; }
.recurrence-end > span { flex: 0 0 34px; color: var(--ink-muted); }
.rec-end-control { display: flex; align-items: center; gap: 4px; min-width: 0; flex: 1 1 auto; }
.recurrence-end .rec-end-date { height: 28px; min-height: 28px; padding: 2px 7px; flex: 1; min-width: 0; }
.rec-end-calendar-trigger { flex: 0 0 28px; width: 28px; min-height: 28px; padding: 0; }
.rec-end-clear { min-height: 26px; padding: 2px 8px; }
.recurrence-steps { display: grid; gap: 4px; }
.recurrence-step {
  display: grid; grid-template-columns: 22px minmax(0, 1fr) minmax(90px, .55fr) 26px;
  align-items: end; gap: 5px;
}
.rec-step-number { align-self: center; text-align: center; color: var(--ink-muted); }
.recurrence-step label { display: grid; gap: 1px; color: var(--ink-muted); font-size: var(--fs-micro); }
.recurrence-step .inp { height: 28px; min-height: 28px; padding: 2px 6px; font-size: var(--fs-label); }
.rec-step-delete { width: 26px; height: 28px; padding: 0; }
.rec-add-step { justify-self: start; min-height: 27px; padding: 2px 8px; }
.recurrence-step-end { margin: 0; }
.recurrence-group-disclosure {
  display: grid; gap: 4px; padding: 6px 8px;
  border: 1px solid var(--hairline); border-radius: var(--radius-s);
  background: color-mix(in srgb, var(--card) 78%, var(--accent) 4%);
}
.recurrence-group-disclosure > strong { font-size: var(--fs-micro); }
.recurrence-group-list { display: grid; gap: 3px; margin: 0; padding-left: 22px; }
.recurrence-group-list li {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 4px 8px;
  color: var(--ink-muted); font-size: var(--fs-micro);
}
.recurrence-group-list li span:last-child { grid-column: 2; }
.recurrence-group-end { margin: 0; }

.txn-remind-options { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding: 10px 14px 14px; }
.txn-remind-mode { min-height: 44px; padding: 8px; }
.txn-remind-count { grid-column: 1 / -1; display: flex; align-items: center; gap: 8px; color: var(--ink-muted); }
.txn-remind-count .inp { width: 72px; min-height: 44px; text-align: center; }
@media (prefers-reduced-motion: reduce) {
  .rec-remind-choices button,
  .txn-remind-mode { transition: none; }
}

/* Inline split sub-rows + footer (same grid tracks as .reg-row). */
.reg-split-block { background: color-mix(in srgb, var(--accent) 4%, var(--card)); }
.reg-split-row { min-height: 32px; }
.reg-split-row .reg-split-catcell { grid-column: span 2; }
.reg-split-muted { color: var(--ink-muted); }
/* Square box (1.3rem x 1.3rem), so the 50% stays a circle, not an ellipse. */
.reg-split-del {
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  color: var(--accent); border-radius: 50%; width: 1.3rem; height: 1.3rem;
  cursor: pointer; line-height: 1;
}
@media (hover: hover) {
  .reg-split-del:hover {
    background: color-mix(in srgb, var(--accent) 18%, transparent);
    border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  }
}
.reg-split-cat, .reg-split-amt {
  width: 100%; min-width: 0; padding: 3px 6px; font: inherit; font-size: var(--fs-body);
  border: 1px solid var(--hairline); border-radius: 6px; background: var(--card); color: var(--ink);
}
.reg-split-amt { text-align: right; font-variant-numeric: tabular-nums; }
.reg-split-footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: .4rem 12px .55rem; }
.reg-split-add { border: 0; background: none; color: var(--accent); font: inherit; font-weight: 600; cursor: pointer; padding: .2rem 0; }
.reg-split-tally { font-size: var(--fs-body); color: var(--ink); }
.reg-split-remaining { font-weight: 700; font-variant-numeric: tabular-nums; color: var(--neg); }
.reg-split-remaining.ok { color: var(--pos); }

/* M12 loan wizard: keep every staged step inside the visual viewport on touch
 * and desktop.  Long account/category values may wrap, but never push the
 * sheet wider than the available card. */
.loan-form-modal {
  box-sizing: border-box;
  width: min(480px, calc(100vw - 24px));
  max-width: calc(100vw - 24px);
  max-height: calc(100dvh - 24px);
  overflow: auto;
  min-width: 0;
}
.loan-form-modal .ps-field,
.loan-form-modal .inp,
.loan-form-modal .loan-pair-detail { min-width: 0; max-width: 100%; }
.loan-form-modal .sheet-footer { display: flex; flex-wrap: wrap; gap: 8px; }
.loan-form-modal .sheet-footer > .btn { flex: 1 1 8rem; min-width: 0; }
.loan-form-modal .loan-wizard-choice { display: grid; gap: 4px; padding: 12px; border: 1px solid var(--hairline); border-radius: var(--radius-card); background: var(--card); }
.loan-form-modal .loan-pair-choices { display: grid; gap: 8px; margin: 8px 0 12px; }
.loan-form-modal .loan-pair-option { display: flex; align-items: flex-start; gap: 10px; min-width: 0; padding: 11px 12px; border: 1px solid var(--hairline); border-radius: var(--radius-card); background: var(--card); cursor: pointer; }
.loan-form-modal .loan-pair-option:has(input:checked) { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, var(--card)); }
.loan-form-modal .loan-pair-option input { flex: 0 0 auto; margin-top: 3px; accent-color: var(--accent); }
.loan-form-modal .loan-pair-option span { display: grid; gap: 2px; min-width: 0; }
.loan-form-modal .loan-pair-option small { color: var(--ink-muted); overflow-wrap: anywhere; }
.loan-form-modal .loan-loading-state { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 4px 9px; align-items: center; margin: 12px 0; padding: 12px; border: 1px solid var(--hairline); border-radius: var(--radius-card); background: color-mix(in srgb, var(--accent) 8%, var(--card)); }
.loan-form-modal .loan-loading-state p { grid-column: 2; margin: 0; }
.loan-form-modal .loading-spinner { width: 1rem; height: 1rem; border: 2px solid color-mix(in srgb, var(--accent) 28%, transparent); border-top-color: var(--accent); border-radius: 50%; animation: spin .8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.loan-form-modal .loan-wizard-kicker { margin: 0 0 8px; color: var(--accent); font-weight: 700; text-transform: uppercase; letter-spacing: .05em; }
.loan-form-modal .loan-review { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px 14px; margin: 12px 0; }
.loan-form-modal .loan-review div { min-width: 0; }
.loan-form-modal .loan-review dt { color: var(--ink-muted); font-size: var(--fs-label); text-transform: uppercase; }
.loan-form-modal .loan-review dd { margin: 3px 0 0; overflow-wrap: anywhere; }
.loan-form-modal .form-error { color: var(--neg); overflow-wrap: anywhere; }
@media (max-width: 430px) {
  .loan-form-modal .loan-review { grid-template-columns: 1fr; }
}

/* "Something Doesn't Add Up" popup. */
.split-mismatch { max-width: 30rem; }
.split-mismatch-lead { padding: .2rem .2rem .6rem; color: var(--ink); line-height: 1.4; }
.split-mismatch-acts { display: flex; flex-wrap: wrap; gap: 8px; }
.split-mismatch-acts .btn { flex: 1 1 auto; }
.split-mm-discard { width: 100%; margin-top: 10px; }

/* ---------- Add/Edit Transaction sheet (§4) ---------- */
.sheet-overlay {
  position: fixed;
  /* Resting overlays stay anchored to the layout viewport. Only a focused
   * keyboard containment state may move them into a reduced visual band; raw
   * browser-bar/scroll offsets must not shift the whole sheet. */
  inset: 0;
  background: var(--sheet-scrim); z-index: 50;
  display: flex; align-items: flex-end; justify-content: center;
  overflow: hidden; overscroll-behavior: contain;
}
html[data-vv-keyboard="true"] .sheet-overlay {
  inset: var(--vv-top, 0px) 0 var(--vv-bottom, 0px) 0;
}
/* Compact dialogs (for example Add Account) can be taller than a phone's
 * landscape keyboard band. Their overlay owns no document scroll while
 * `.sheet-open` is active, so give every non-transaction dialog a real local
 * scroll owner instead of centring its focused field beyond the visible edge.
 * Transaction sheets already own `.sheet-scroll` and keep their dock fixed. */
html[data-vv-keyboard="true"] body:is([data-surface="phone"], [data-surface="tablet"])
  .sheet-overlay > :not(.txn-sheet) {
  max-height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* Nested transaction pickers are fixed to the layout viewport too. Once their
 * search/rate field owns the software keyboard, constrain the catcher to the
 * same visual band as the parent sheet so top search, bottom-sticky category
 * search and compact conversion fields all remain visible. */
html[data-vv-keyboard="true"] body:is([data-surface="phone"], [data-surface="tablet"]) .sub-backdrop {
  inset: var(--vv-top, 0px) 0 var(--vv-bottom, 0px) 0;
}
html.sheet-open,
body.sheet-open { overflow: hidden; overscroll-behavior: contain; }
body[data-surface="desktop"] .sheet-overlay, body[data-surface="tablet"] .sheet-overlay { align-items: center; }
/* The Assign popup (Plan → Ready to Assign) is a small dialog, not a data-entry
 * sheet — centre it on every surface, including phone, rather than docking it to
 * the bottom edge (operator: it should appear centred). */
.sheet-overlay:has(.assign-modal) { align-items: center; }
/* The sheet is a fixed-height flex column: header + amount stay at the top,
 * the fields region (.sheet-scroll) is the ONLY scroller, and the keypad +
 * Save (.sheet-dock) are glued to the bottom for thumb reach. The sheet itself
 * never scrolls — that kept the numpad in view (was: whole sheet scrolled and
 * pushed the pad off-screen). safe-area padding clears the iOS home indicator. */
.txn-sheet {
  position: relative; /* positioning context for the slide-up keypad panel */
  background: var(--canvas); width: 100%; max-width: 480px;
  height: 96dvh; max-height: 96dvh; overflow: hidden;
  /* No bottom safe-area padding here: .keypad-panel is position:absolute;bottom:0
   * inside this sheet, so it lands on THIS box's padding edge and carries its own
   * safe-area inset (below). A bottom inset here too stacked a second inset UNDER
   * the keypad — the visible beige strip below the numpad on notched iPhones
   * (operator report 2026-07-23). */
  border-radius: 36px 36px 0 0; padding: 10px 14px 0;
  display: flex; flex-direction: column; gap: 8px;
  transition: transform var(--dur-base) var(--ease-ios-rebound); /* non-gesture state changes */
  animation: txn-sheet-in var(--dur-slow) var(--ease-ios-rebound) both;
}
/* Desktop: a centred dialog, never a docked phone sheet (2026-07-21 audit —
 * every non-drawer path that still reaches .txn-sheet on the desktop surface,
 * e.g. a transaction opened from inside the Approval queue, kept the mobile
 * bottom-sheet shape: flat bottom corners and near-full-viewport height).
 * Same treatment .approve-sheet already had; now the whole family. Tablet is
 * deliberately untouched — its sheet height is part of the keypad-clearance
 * geometry (v1.24.0 mid-tap slide bug). */
/* The 320px floor is a CENTRED-DIALOG minimum: a panel floating over the page
 * needs a body under its header or it reads as a stub. The register DRAWER is
 * the same component spliced inline between two rows, where that floor is pure
 * dead space — measured 320px of panel around 76px of content on the repeat
 * path (operator 30/07/2026). Excluded here, at the declaration, because
 * `.reg-drawer .txn-sheet-inline` is declared earlier in this file and any
 * counter-override there loses on source order. */
body[data-surface="desktop"] .txn-sheet:not(.txn-sheet-inline) {
  height: auto; min-height: 320px; max-height: 92dvh; border-radius: 20px;
}
/* The inline drawer deliberately gets NO replacement rule here. Only the FLOOR
 * is being removed: `border-radius: 20px` and `max-height: 92dvh` already reach
 * the drawer from this rule and are LEFT reaching it, unchanged — verified by
 * measuring the drawer against the pre-change stylesheet, where both computed
 * identically (20px, 972px) and only `min-height` moved (320px -> 0). Restating
 * them here would be a no-op at best; adding the drawer's own values would be a
 * silent visual change nobody asked for. */
@keyframes txn-sheet-in {
  from { transform: translateY(8px); opacity: 0; }
  to { transform: translateY(0); opacity: 1; }
}
.sheet-overlay { animation: sheet-overlay-in var(--dur-slow) var(--ease-standard) both; }
@keyframes sheet-overlay-in { from { opacity: 0; } to { opacity: 1; } }

/* SWIPE-DOWN TO DISMISS a pristine sheet (operator 2026-07-14). While the finger
 * is down the sheet must track it exactly, so the release transition is off and
 * the entry animation (which also owns `transform`) is cancelled — otherwise the
 * two fight and the sheet judders. On commit it falls away under the standard
 * transition and the backdrop fades with it. A sheet the user has typed into
 * never enters these states (the gesture refuses to start), so typed work is
 * never at risk from a stray swipe. */
.txn-sheet.swipe-dragging { transition: none; animation: none; }
/* Once the entry animation has finished, drop it for good (txn-form.js adds
 * .entered on animationend). fill:both means a finished txn-sheet-in would
 * otherwise keep OWNING transform/opacity forever — overriding the inline
 * translateY the dismiss paths write — and, worse, removing .swipe-dragging
 * after a cancelled swipe re-attached the animation property, which RESTARTS
 * the whole entry animation (the "sheet judders/replays on release" bug). */
.txn-sheet.entered { animation: none; }
/* Direction splits the two real touch owners before iOS starts the stream.
 * Finger-up inside the fields stays a native, momentum-preserving scroll
 * (`pan-down` in the browser's scroll-direction vocabulary);
 * finger-down is withheld from the browser so the sheet can dismiss from its
 * content at scrollTop 0 (or manually return a previously scrolled form to its
 * top).  The old `none` owner made every upward swipe a sequence of scripted
 * scrollTop writes; the supplied 28/08/2026 recording caught those writes
 * moving briefly and snapping back instead of leaving the transaction
 * scrolled. The parent permits vertical panning so it does not intersect the
 * child's directional permission back down to `none`. */
.txn-sheet { touch-action: pan-y; }
.txn-sheet .sheet-scroll { touch-action: pan-down; }
.txn-sheet .sheet-head { touch-action: none; }
.txn-sheet .sheet-grip { touch-action: none; }
/* Dismiss transition timing only: JS writes the parent sheet's pixel target.
 * The keypad is its child and must not receive another transform; parent and
 * child transforms compound, which made the keypad travel twice on iOS. */
.txn-sheet.swipe-dismissing {
  /* animation:none is load-bearing: while txn-sheet-in is attached (running OR
   * finished-with-fill), the ANIMATION wins the cascade over the inline
   * translateY that dismissTo() writes — the sheet never visibly moved, then
   * vanished at teardown. Detach it so the transition can drive the slide. */
  animation: none;
  transition: transform var(--dur-sheet-dismiss) var(--ease-sheet-dismiss);
}
/* Fade the DIM, not the overlay's opacity: the sheet is the overlay's child, so
 * an opacity fade would ghost the sheet out mid-slide (and the filled entry
 * animation pins opacity anyway). Draining background-color leaves the sheet
 * fully opaque while the backdrop lifts in step with the slide-down. */
.sheet-overlay.fading {
  background: var(--sheet-scrim-clear);
  transition: background var(--dur-sheet-dismiss) var(--ease-sheet-dismiss);
}
@media (prefers-reduced-motion: reduce) {
  .txn-sheet.swipe-dismissing { transition: none; }
  .sheet-overlay.fading { transition: none; }
}
body[data-surface="desktop"] .txn-sheet, body[data-surface="tablet"] .txn-sheet { border-radius: 20px; height: auto; max-height: 90dvh; }
/* Resting touch sheets keep their measured 96dvh/90dvh geometry. A visual
 * viewport reduction is a keyboard constraint only while an editable control
 * owns focus; browser chrome and ordinary scroll must not shrink or recenter
 * the sheet. */
body[data-surface="phone"] .txn-sheet:not(.txn-sheet-inline) {
  /* The reference Add sheet leaves a 48px status-bar/scrim rail above its
   * rounded top edge on the 402x874 iPhone canvas (rather than reaching the
   * browser edge).  Keep the relative rule so future phones scale naturally. */
  height: 94.5dvh;
  max-height: 94.5dvh;
}
body[data-surface="tablet"] .txn-sheet:not(.txn-sheet-inline) {
  max-height: 90dvh;
}
html[data-vv-keyboard="true"] body[data-surface="phone"] .txn-sheet:not(.txn-sheet-inline) {
  height: min(96dvh, var(--vvh, 100dvh));
  max-height: min(96dvh, var(--vvh, 100dvh));
}
html[data-vv-keyboard="true"] body[data-surface="tablet"] .txn-sheet:not(.txn-sheet-inline) {
  max-height: min(90dvh, var(--vvh, 100dvh));
}
/* The sheet is height:auto and the overlay centres it on these surfaces, so the
 * keypad clearance below (.kp-open padding-bottom) was part of its own height:
 * the instant a tap lowered the keypad the sheet shrank by that padding and
 * re-centred, sliding every field row ~146px DOWN between pointerdown and click.
 * The click then landed on whatever had moved under the finger — tapping Payee
 * silently opened the amount keypad instead (v1.24.0 crawl, iPad both
 * orientations; phones are fixed-height and never moved). Holding the open
 * height while the keypad is up keeps the row under the finger. */
body[data-surface="desktop"] .txn-sheet.kp-was-open,
body[data-surface="tablet"] .txn-sheet.kp-was-open { height: var(--kp-open-h); }
body[data-surface="desktop"] .txn-sheet,
body[data-surface="tablet"] .txn-sheet { transition: transform var(--dur-base) var(--ease-ios-rebound), height var(--dur-base) var(--ease-ios-rebound); }
@media (prefers-reduced-motion: reduce) {
  body[data-surface="desktop"] .txn-sheet,
  body[data-surface="tablet"] .txn-sheet { transition: none; }
}
.sheet-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden; display: flex; flex-direction: column; gap: 14px; padding-bottom: 8px; -webkit-overflow-scrolling: touch; overscroll-behavior-y: contain; touch-action: pan-y; }
/* Root-cause fix (2026-07-14): reserve real scroll clearance under the resting
 * keypad so every field row can scroll clear of its rectangle and be tapped
 * directly, not just reached by the "tap lowers pad" gesture. --keypad-h is
 * set from the panel's measured offsetHeight in txn-form.js; 60vh fallback
 * covers the first paint before JS measures it. */
.txn-sheet.kp-open .sheet-scroll { padding-bottom: calc(var(--keypad-h, 60vh) + 12px); }
/* Browser and assistive scroll-to-control paths must reserve the same keypad
 * clearance as a finger scroll. Without scroll-margin, a short landscape
 * viewport considered Category "visible" while the absolute keypad still sat
 * on top of it, so the keypad intercepted the tap. */
.txn-sheet.kp-open :is(.field-row, .field-memo) {
  scroll-margin-bottom: calc(var(--keypad-h, 60vh) + 12px);
}
/* A short phone landscape cannot stack a 5-row calculator beneath the form:
 * the keypad is almost three quarters of the viewport and necessarily covers
 * every field. Use the plentiful horizontal space instead — form on the left,
 * full-size keypad on the right. No touch targets shrink and every field stays
 * directly tappable while the calculator is open. */
@media (orientation: landscape) and (max-height: 500px) {
  body[data-surface="phone"] .txn-sheet {
    width: 100%; max-width: none; height: 100dvh; max-height: 100dvh;
    border-radius: 0; padding-right: calc(44% + 14px);
  }
  body[data-surface="phone"] .txn-sheet.kp-open .sheet-scroll {
    padding-bottom: 8px;
  }
  body[data-surface="phone"] .txn-sheet.kp-open :is(.field-row, .field-memo) {
    scroll-margin-bottom: 8px;
  }
  body[data-surface="phone"] .keypad-panel {
    left: auto; top: 0; right: 0; bottom: 0; width: 44%;
    border-radius: 22px 0 0 22px; padding: 8px;
    display: flex; align-items: stretch;
  }
  body[data-surface="phone"] .keypad { flex: 1; gap: 6px; }
}

.sheet-head { flex: 0 0 auto; position: relative; display: grid; grid-template-columns: 2.4rem 1fr 2.4rem; align-items: center; text-align: center; row-gap: 8px; padding-top: 14px; }
/* Grab strip across the header — the swipe-down-to-dismiss target (IMG_4699). */
.sheet-grip { grid-column: 1 / -1; grid-row: 1; justify-self: center; width: 40px; height: 5px; border-radius: 3px; background: color-mix(in srgb, var(--ink-muted) 40%, transparent); }
/* Round × button top-RIGHT (operator 2026-07-12), on a white bubble. */
.sheet-x {
  grid-column: 1; grid-row: 2; justify-self: start; align-self: start;
  width: 44px; height: 44px; border: 0; border-radius: 50%; cursor: pointer;
  background: var(--card); color: var(--ink); box-shadow: 0 1px 4px rgba(0, 0, 0, .12);
  display: inline-flex; align-items: center; justify-content: center;
  /* The amount zone spans the whole header row (grid-column 1/-1); raise the ×
   * above it so a tap on the × closes the sheet rather than raising the keypad. */
  position: relative; z-index: 2;
}
.sheet-x .ico { width: 1.1em; height: 1.1em; stroke-width: 2.2; }
/* Amount zone: the inline currency chip + the giant amount, centred together. */
/* The NUMBER is centred on screen, EXCLUDING the currency chip (operator
 * 2026-07-12): the amount sits dead-centre; the SGD/amber chip hangs to its left
 * without shifting it off-centre. Technique: the chip is taken out of flow
 * (absolute) and hung off the RIGHT edge of the centred number via its own
 * wrapper — so the number is what's centred, and the chip floats just left of it
 * regardless of how many digits the amount has. */
.amount-zone {
  grid-column: 1 / -1; grid-row: 2;
  display: flex; align-items: center; justify-content: center;
}
/* The number wrapper shrink-wraps the amount and is centred by the zone, so the
 * NUMBER is what sits dead-centre. The chip hangs off its left edge (absolute),
 * excluded from the centred box entirely. */
.amount-numwrap { position: relative; display: inline-flex; align-items: center; }
/* Currency chip: subtle grey "SGD" by default; a boxed amber 3-letter code in
 * foreign mode (operator 2026-07-12). Tap opens the converter. Anchored to the
 * left edge of the centred amount so it never nudges the number off-centre. */
.cur-chip {
  position: absolute; right: 100%; top: 50%; transform: translateY(-50%);
  margin-right: .35em;
  border: 0; background: none; cursor: pointer; font: inherit; font-weight: 700;
  font-size: var(--fs-value); letter-spacing: .03em; color: var(--ink-muted); padding: .1rem .2rem;
  line-height: 1; white-space: nowrap;
}
.cur-chip.foreign {
  color: var(--control-accent-ink); background: var(--control-accent);
  border-radius: 8px; padding: .28rem .5rem;
  box-shadow: 0 1px 3px color-mix(in srgb, var(--control-accent) 55%, transparent);
}
.amount-display {
  font-size: var(--fs-display); font-weight: 700;
  font-variant-numeric: tabular-nums; line-height: 1.1;
  background: none; border: 0; cursor: pointer; color: var(--ink); padding: 0;
  position: relative;
}
/* Direction by COLOUR, not by a +/− sign (operator 2026-07-10): income green,
 * expense red, transfer neutral ink. The magnitude is always shown unsigned. */
.amount-display.inflow { color: var(--pos); }
.amount-display.spending { color: var(--neg); }
/* NO caret on the amount, on any surface (operator 2026-07-28: "remove the
 * flashing cursor permanently from the keypad"). The figure is plain text; the
 * blinking block caret, its cents-digit slot and both blink keyframes are gone
 * for good. Nothing here may animate the amount's opacity again. */
/* Type pill under the amount: box glyph + word + one shared down caret. */
.type-pill {
  grid-column: 1 / -1; grid-row: 3; justify-self: center;
  display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
  background: var(--card); border: 0; border-radius: 999px; padding: .42rem .82rem;
  font: inherit; font-size: var(--fs-value); font-weight: 600; color: var(--ink);
  box-shadow: 0 1px 5px rgba(0, 0, 0, .1);
  min-height: var(--ctl-h-md); max-width: calc(100vw - 48px);
}
.type-pill .ico { width: 1.35em; height: 1.35em; }
.type-pill-ico { display: inline-flex; }
.type-pill-word { min-width: 0; overflow-wrap: anywhere; line-height: 1.12; }
.type-pill-chev { display: inline-flex; color: var(--ink-muted); }
.type-pill-chev .ico { width: 1.1em; height: 1.1em; }
/* Running calculator expression above the big amount (e.g. "0.62 − 1.00"),
 * shown only while an operator is armed so subtraction reads as math. */
.calc-expr {
  grid-column: 1 / -1; grid-row: 2; justify-self: center; align-self: start;
  font-size: var(--fs-value); font-weight: 600; color: var(--ink-muted);
  font-variant-numeric: tabular-nums; min-height: 1.1em; visibility: hidden;
}
.calc-expr.show { visibility: visible; }
.sheet-head.has-fx { row-gap: 10px; }
.sheet-head.has-fx .fx-base-summary {
  grid-row: 3; align-self: center; position: static;
  min-height: 0; padding: .32rem .75rem; border-radius: 999px;
  background: var(--chip); color: var(--ink-muted);
  font-size: var(--fs-label); line-height: 1.25;
}
.sheet-head.has-fx .type-pill { grid-row: 4; margin-top: 2px; }
/* Styled discard confirm (§4) — replaces the browser confirm() dialog. */
.discard-confirm {
  background: var(--card); border-radius: var(--radius-sheet-inner); padding: 20px;
  width: min(340px, calc(100vw - 48px)); text-align: center;
  box-shadow: 0 12px 40px rgba(0,0,0,.28);
  animation: sub-modal-in var(--dur-base) var(--ease-standard) both;
}
@keyframes sub-modal-in {
  from { transform: scale(.98); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}
/* Discard confirm near the thumb on touch (operator 2026-07-12, relocated
 * 2026-07-19): the whole-sheet discard dialog sits at THUMB level — ~35% up
 * from the bottom edge, not flush against it — so Discard/Stay land where a
 * thumb rests mid-swipe, not down at the very edge past comfortable reach.
 * Desktop keeps it top-anchored like every other picker. */
body[data-surface="phone"] .sub-backdrop:has(.discard-confirm),
body[data-surface="tablet"] .sub-backdrop:has(.discard-confirm) {
  align-items: flex-end;
  padding-bottom: calc(24vh + env(safe-area-inset-bottom));
}
body[data-surface="phone"] .discard-confirm,
body[data-surface="tablet"] .discard-confirm { width: min(440px, calc(100vw - 32px)); }
.discard-title { font-weight: 700; font-size: var(--fs-h3); margin-bottom: 4px; }
.discard-msg { color: var(--ink-muted); margin-bottom: 16px; }
.discard-actions { display: flex; flex-direction: column; gap: 8px; }
/* Match the DONE keypad pill's HEIGHT + weight, not its colour (operator
 * 2026-07-12): the old buttons were too short (.55rem padding). Same min-height
 * / font-size / weight / radius as .key.kdone so Discard and Stay read as the
 * same size class as DONE; each keeps its own colour (Discard neg-red, Stay the
 * neutral secondary chip). */
.discard-actions .btn {
  /* Taller than every other sheet/keypad button on purpose (operator
   * 2026-07-19): this popup is the ONLY way left to close a dirty add-txn
   * sheet (swipe-dismiss never fires once dirty, by design), so its two
   * buttons get extra thumb margin as a deliberate one-off exception. */
  width: 100%; min-height: 4.2rem; font-size: var(--fs-h3); font-weight: 700;
  border-radius: 999px; display: inline-flex; align-items: center; justify-content: center;
}
.btn-discard { background: var(--neg); color: var(--neg-ink); }
.app-version { text-align: center; margin-top: 8px; opacity: .7; }

/* Cleared feature disabled (item 6): hide every Cleared visual. Interactive
 * Cleared controls (txn-form row, context-menu action, keyboard C) are guarded
 * in their views; these rules cover the passive display. */
body[data-cleared="off"] .c-clr,
body[data-cleared="off"] .cleared-badge,
body[data-cleared="off"] .rec-lock,
body[data-cleared="off"] .bal-cleared,
body[data-cleared="off"] .bal-uncleared,
body[data-cleared="off"] .bal-scheduled,
body[data-cleared="off"] .bal-equation .bal-op { display: none; }
/* Desktop register grid drops its trailing "C" column so cells stay aligned
 * (same tracks as .reg-row minus the final 1.4rem cleared column; 7px flag). */
body[data-cleared="off"] .reg-head,
body[data-cleared="off"] .reg-row {
  grid-template-columns:
    1.2rem 1.35rem 1.2rem
    var(--reg-c-acct, 8rem)
    var(--reg-c-date, 6.8rem)
    var(--reg-c-payee, 1.25fr)
    var(--reg-c-cat, minmax(var(--reg-min-cat, 0px), 1.5fr))
    var(--reg-c-subcat, minmax(var(--reg-min-subcat, 0px), 1.5fr))
    var(--reg-c-memo, 1fr)
    7.4rem 7.4rem;
}
/* Type menu (opened from the amount-zone pill): compact 3-row list. */
.type-menu .type-row { display: flex; align-items: center; gap: 12px; }
.type-menu .type-row-ico { display: inline-flex; color: var(--ink); }
.type-menu .type-row-ico .ico { width: 1.4em; height: 1.4em; }
.picker-modal.type-menu { background: var(--control-secondary-bg); }
.type-menu .pick-row { background: transparent; }

/* ---- Calculator keypad panel: slides up from the sheet's bottom edge when the
 * amount is tapped; slides back down on "done" or a tap outside the numbers
 * (IMG_4702). Indigo digits/operators on a white face, a solid "done" pill. ---- */
.keypad-panel {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 4;
  background: var(--card); border-radius: 22px 22px 0 0;
  padding: 13px 12px calc(22px + env(safe-area-inset-bottom));
  box-shadow: 0 -6px 24px rgba(0, 0, 0, .16);
  transform: translateY(110%); transition: transform var(--dur-slow) var(--ease-ios-rebound);
  pointer-events: none;
}
.txn-sheet.kp-open .keypad-panel { transform: translateY(0); pointer-events: auto; }
/* First-open of Add: land the keypad in place with NO slide so the first tap
 * registers (operator 2026-07-12 — the animated pad ate the first tap until it
 * settled). Cleared after two frames in txn-form.js so later opens still slide. */
.txn-sheet.kp-instant .keypad-panel { transition: none; }
/* Instant DROP when a field tap lowers the keypad (operator 2026-07-12): the pad
 * must clear the tapped row at once, not sweep over it for .3s and swallow the
 * click. Same zero-transition trick as kp-instant but for the outgoing slide. */
.txn-sheet.kp-drop-instant .keypad-panel { transition: none; }
.keypad {
  display: grid; gap: 10px;
  /* Operators sit in the LEFT two columns so the digits fall under the right
   * thumb (operator: relocate the operands left for right-handed reach).
   * DONE spans the entire bottom row (operator 2026-07-11): always large,
   * always at the very bottom, full width — the "=" grows tall beside 0/⌫.
   * Digit columns are WIDER, operator columns NARROWER (operator 2026-07-12):
   * the two operator tracks take ~0.68fr each, the three digit tracks 1fr, so
   * the numbers field dominates and the operands tuck to the side. */
  /* Operator 2026-07-14: "on mobile further reduce the width of the column housing
   * mathematical operands and expand the width of the number keys." The operator
   * columns carry a single glyph; the digits carry the thumb. */
  /* Match the iOS calculator: digits occupy the three left tracks and the
   * operators sit on the right.  The final row keeps a blank leading track,
   * 0, backspace and a compact done pill. */
  grid-template-columns: .82fr .9fr .96fr .3fr .4fr;
  grid-template-areas:
    "k7 k8 k9 kmul kdiv"
    "k4 k5 k6 kadd ksub"
    "k1 k2 k3 . keq"
    ".  k0 kbk kdone kdone";
}
.keypad:has(.kdot) {
  grid-template-areas:
    "k7 k8 k9 kmul kdiv"
    "k4 k5 k6 kadd ksub"
    "k1 k2 k3 . keq"
    "k0 kdot kbk kdone kdone";
}
.key {
  font: inherit; font-size: var(--fs-h1); font-weight: 500;
  min-height: var(--ctl-h-lg); border: 0; border-radius: 14px;
  background: none; color: var(--accent); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  -webkit-tap-highlight-color: transparent; user-select: none; touch-action: manipulation;
}
.key:active { background: none; }
.key.op { font-weight: 500; }
.key.op.armed { background: var(--accent); color: var(--accent-ink); }
.key.k7 { grid-area: k7; } .key.k8 { grid-area: k8; } .key.k9 { grid-area: k9; }
.key.k4 { grid-area: k4; } .key.k5 { grid-area: k5; } .key.k6 { grid-area: k6; }
.key.k1 { grid-area: k1; } .key.k2 { grid-area: k2; } .key.k3 { grid-area: k3; }
.key.k0 { grid-area: k0; } .key.kdot { grid-area: kdot; } .key.kbk { grid-area: kbk; }
.key.kmul { grid-area: kmul; } .key.kdiv { grid-area: kdiv; }
.key.kadd { grid-area: kadd; } .key.ksub { grid-area: ksub; }
.key.keq { grid-area: keq; }
.key.kdone {
  grid-area: kdone; background: var(--accent); color: var(--accent-ink);
  font-size: var(--fs-value); font-weight: 700; border-radius: 999px;
  width: 90px; min-height: var(--ctl-h-md); justify-self: end; letter-spacing: 0;
}
.key.kdone:active { background: var(--accent); }

/* iOS-style prominent tap flash (touch devices; operator 2026-07-19): every
 * numpad key gets a bright overlay pulse the instant it's tapped, driven by
 * the JS-toggled .key-flash class (txn-form.js flashKey()) rather than :active
 * alone — :active on a fast tap can end before the frame paints, which is why
 * a JS class + animationend is used instead of a pure-CSS :active rule. A
 * pseudo-element overlay is used (not background, which .op.armed/.kdone
 * already own) so the flash reads identically on every key regardless of its
 * own resting colour. currentColor keeps it on-theme + on-brand per key
 * (green keypad flashes green, indigo keypad flashes indigo) without a token. */
.key { position: relative; overflow: hidden; }
.key::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  background: currentColor; opacity: 0; pointer-events: none;
  transform: scale(.92);
}
.key.key-flash-a::after { animation: key-flash-pulse-a .22s var(--ease-standard); }
.key.key-flash-b::after { animation: key-flash-pulse-b .22s var(--ease-standard); }
@keyframes key-flash-pulse-a {
  0%   { opacity: .38; transform: scale(.92); }
  100% { opacity: 0;   transform: scale(1.06); }
}
@keyframes key-flash-pulse-b {
  0%   { opacity: .38; transform: scale(.92); }
  100% { opacity: 0;   transform: scale(1.06); }
}
@media (prefers-reduced-motion: reduce) {
  .key:is(.key-flash-a, .key-flash-b)::after { animation: none; opacity: 0; }
}

/* Inflow keypad: the whole pad reads green (operator: "for inflow change entire
 * numpad to green"). Tokens only (--pos), so it tracks dark/light. Digits +
 * operators go green ink; an armed operator + the done pill fill solid green. */
.keypad.inflow .key { color: var(--pos); }
.keypad.inflow .key:active { background: none; }
.keypad.inflow .key.op.armed { background: var(--pos); color: var(--accent-ink); }
.keypad.inflow .key.kdone { background: var(--pos); color: var(--accent-ink); }
.keypad.inflow .key.kdone:active { background: var(--pos); }

/* Touch-only (phone/tablet) Add-Transaction sizing (operator 2026-07-19): the
 * amount reads 25% bigger with more room around it, paid for by shorter keys
 * and narrower operator columns — the digits (the thumb's actual target) grow
 * wider in trade. Portrait/normal only; the landscape-phone compact keypad
 * above (max-height:500px) already fights a different space budget and is
 * left alone. Desktop's .reg-drawer amount/keypad are untouched (separate,
 * already-small desktop-only rules). */
body[data-surface="phone"] .amount-display,
body[data-surface="tablet"] .amount-display { font-size: calc(var(--fs-display) * 1.25); }
body[data-surface="phone"] .sheet-head,
body[data-surface="tablet"] .sheet-head { row-gap: 14px; padding-bottom: 6px; }
body[data-surface="phone"] .sheet-head,
body[data-surface="tablet"] .sheet-head { padding-top: 4px; }
body[data-surface="phone"] .sheet-grip,
body[data-surface="tablet"] .sheet-grip { display: none; }
body[data-surface="phone"] .amount-zone,
body[data-surface="tablet"] .amount-zone { padding: 10px 0; }
body[data-surface="phone"] .key,
body[data-surface="tablet"] .key { min-height: calc(var(--ctl-h-lg) * .78); }
body[data-surface="phone"] .key.kdone,
body[data-surface="tablet"] .key.kdone { min-height: calc(var(--ctl-h-lg) * .78); }
body[data-surface="phone"] .key,
body[data-surface="tablet"] .key { min-height: 48px; color: var(--control-accent) !important; }
body[data-surface="phone"] .txn-sheet .sheet-x,
body[data-surface="tablet"] .txn-sheet .sheet-x {
  background: var(--card) !important;
  background-color: var(--card) !important;
  color: var(--ink) !important;
}

/* ---- Field rows (IMG_4699): leading icon, label-over-value, trailing chevron. */
.sheet-fields { margin: 0; padding: 4px 16px; }
.sheet-fields.card { border-radius: var(--radius-sheet-inner); }
/* With seeded payees the reference opens directly on the primary field card;
 * retain the same breathing room occupied by the optional geo explanation. */
.sheet-scroll:not(:has(.geo-permission-explain)) > .sheet-fields:first-child {
  margin-top: 28px;
}
.field-row, .field-btn {
  display: flex; align-items: center; gap: 14px;
  width: 100%; padding: .85rem 0; border: 0; border-top: 1px solid var(--hairline);
  background: none; font: inherit; color: inherit; cursor: pointer; text-align: left;
}
.field-row:first-child, .field-btn:first-child { border-top: 0; }
/* KEYBOARD FOCUS (operator 2026-07-29: "ensure the field clearly shows it is
 * selected"). These rows carry `border: 0`, which drops the UA's default focus
 * ring, and nothing replaced it — a field reached by Tab looked identical to
 * every other field, so there was no way to tell where you were. :focus-visible
 * (not :focus) so a mouse click does not paint a ring the mouse user never
 * asked for. Tinted band + accent rail, both from tokens, so it reads in dark
 * and light alike.
 *
 * POINTER-COARSE EXCLUSION (operator 03/08/2026: "weird left vertical bar and
 * an uncalled for outline of the memo field… this is against design theme").
 * `:focus-visible` is NOT keyboard-only: iOS/Safari applies it to a TAPPED text
 * input, so tapping Memo on the phone painted this desktop keyboard affordance —
 * a 3px accent rail down the row plus a tinted band — that a touch user never
 * asked for and cannot dismiss. There is no keyboard to travel with on a coarse
 * pointer, so the affordance has no purpose there. `@media (pointer: fine)`
 * keeps it exactly where it was designed to live (Win11 desktop Tab travel) and
 * removes it everywhere it was never meant to appear. */
@media (pointer: fine) {
  .field-row:focus-visible, .field-btn:focus-visible {
    outline: none;
    background: color-mix(in srgb, var(--accent) 10%, transparent);
    box-shadow: inset 3px 0 0 var(--accent);
    border-radius: 6px;
  }
  /* A field whose own control takes the focus (the memo input, the cleared
   * switch) highlights the ROW, so the travelling highlight never disappears
   * mid-run. :has() is supported on every browser in the fleet. */
  .field-row:has(:focus-visible) {
    background: color-mix(in srgb, var(--accent) 10%, transparent);
    box-shadow: inset 3px 0 0 var(--accent);
    border-radius: 6px;
  }
}
/* On a coarse pointer the row keeps NO ring at all: the caret and the OS
 * keyboard already say where typing goes, which is how every native iOS form
 * behaves. */
@media (pointer: coarse) {
  .field-row:focus-visible, .field-btn:focus-visible { outline: none; }
}
.field-ico { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; color: var(--ink-muted); }
.field-ico .ico { width: 1.5em; height: 1.5em; }
.field-ico.ico-account { color: var(--pos); }
.field-ico.ico-cleared { color: var(--pos); }
.field-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.field-label { color: var(--ink-muted); font-size: var(--fs-body); }
/* Value truncates instead of pushing the row wider / wrapping. */
.field-value { font-weight: 400; font-size: var(--fs-h3); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.field-value.plain { font-size: var(--fs-h3); }
/* `text-overflow: ellipsis` is a NO-OP without `white-space: nowrap` — the text
 * wraps instead of clipping and the chip grows out of its row. */
.field-chip { display: inline-block; background: var(--chip); border-radius: 8px; padding: .15rem .5rem; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; }
.field-chev { flex: 0 0 auto; display: inline-flex; color: var(--ink-muted); }
.field-chev .ico { width: 1.2em; height: 1.2em; }
.field-memo { flex: 1; min-width: 0; border: 0; background: none; font-weight: 400; font-size: var(--fs-h3); padding: 0; }
.field-memo:focus { outline: none; }
.field-toggle-row { cursor: default; }

/* Linked recurrence read-back in touch transaction detail. This is a
 * disclosure card, not a second editor: its only action opens the canonical
 * recurrence editor and its dates remain read-only. */
.txn-recurrence-summary {
  margin: 8px 0 0;
  padding: 12px 16px 14px;
  border: 1px solid color-mix(in srgb, var(--accent) 26%, var(--hairline));
  border-radius: var(--radius-sheet-inner);
  background: color-mix(in srgb, var(--accent) 6%, var(--card));
}
.txn-recurrence-summary-head {
  display: flex; align-items: center; gap: 8px;
  color: var(--ink-muted); font-size: var(--fs-body);
}
.txn-recurrence-summary-head .field-ico { color: var(--accent); }
.txn-recurrence-frequency-row {
  display: flex; align-items: center; gap: 8px;
  margin: 3px 0 8px 2.2rem;
}
.txn-recurrence-frequency {
  min-width: 0; color: var(--ink); font-size: var(--fs-h3); font-weight: 700;
}
.txn-recurrence-edit {
  display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 auto; width: var(--ctl-h-sm); height: var(--ctl-h-sm);
  padding: 0; border: 1px solid color-mix(in srgb, var(--accent) 34%, transparent);
  border-radius: var(--radius-s);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
  color: var(--accent); cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-ios-rebound),
              background var(--dur-fast) var(--ease-standard),
              border-color var(--dur-fast) var(--ease-standard);
}
.txn-recurrence-edit .ico { width: 1.1em; height: 1.1em; }
.txn-recurrence-edit:hover { background: color-mix(in srgb, var(--accent) 16%, transparent); }
.txn-recurrence-edit:active,
.txn-recurrence-edit.is-activated {
  transform: scale(.94);
  background: var(--accent); border-color: var(--accent); color: var(--accent-ink);
}
.txn-recurrence-dates {
  display: grid; gap: 5px; margin: 0 0 0 2.2rem;
}
.txn-recurrence-dates > div {
  display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
  border-top: 1px solid color-mix(in srgb, var(--hairline) 80%, transparent);
  padding-top: 5px;
}
.txn-recurrence-dates dt { color: var(--ink-muted); font-size: var(--fs-body); }
.txn-recurrence-dates dd {
  margin: 0; color: var(--ink); font-size: var(--fs-body); font-weight: 600;
  text-align: right;
}
@media (max-width: 420px) {
  .txn-recurrence-frequency-row,
  .txn-recurrence-dates { margin-left: 0; }
  .txn-recurrence-dates > div { align-items: flex-start; flex-direction: column; gap: 1px; }
}

.cardpay-arrow {
  display: flex; justify-content: center; color: var(--ink-muted);
  margin: -4px 0; min-height: 24px;
}
.cardpay-arrow .ico { width: 28px; height: 28px; stroke-width: 2; }

/* "Show more / Show less" divider — centred label flanked by hairlines. */
.sheet-showmore {
  display: flex; align-items: center; justify-content: center; gap: 14px;
  border: 0; background: none; cursor: pointer; width: 100%;
  color: var(--ink-muted); font: inherit; font-size: var(--fs-value); font-weight: 500; padding: .3rem 0;
}
.sheet-showmore::before, .sheet-showmore::after {
  content: ""; height: 1px; width: 64px; background: var(--hairline);
}

/* Full-width red "Delete Transaction" (inside the expanded advanced block). */
.sheet-del-full {
  width: 100%; background: var(--neg-pill); color: var(--neg);
  padding: .95rem 1rem; border-radius: 16px; font-weight: 700; border: 0;
}

/* ---- Fresh-start "match YNAB" setup wizard (fresh-start.js) ---- */
.fs-steps { display: flex; gap: 8px; margin: 0 0 14px; flex-wrap: wrap; }
.fs-step { font-size: var(--fs-body); font-weight: 600; color: var(--ink-muted); padding: .25rem .7rem; border-radius: 999px; background: var(--chip); }
.fs-step.active { background: var(--accent); color: var(--accent-ink); }
.fs-step.done { color: var(--pos); }
.fs-card { display: flex; flex-direction: column; gap: 12px; }
.fs-card h2 { margin: 0; }
.fs-why-list { margin: 0; padding-left: 1.2rem; display: flex; flex-direction: column; gap: 6px; }
.fs-accts { display: flex; flex-direction: column; gap: 10px; }
.fs-acct { display: flex; flex-direction: column; gap: 8px; padding: 12px; background: color-mix(in srgb, var(--ink-muted) 6%, var(--card)); }
.fs-acct-line { display: flex; gap: 8px; }
.fs-acct-line .fs-a-kind { flex: 0 0 8rem; }
.fs-acct-line .fs-a-bal { flex: 1 1 auto; min-width: 0; text-align: right; }
.fs-a-trackrow { padding: .3rem 0; }
.fs-group { display: flex; flex-direction: column; gap: 4px; margin-top: 4px; }
.fs-group-h { margin: 8px 0 2px; font-size: var(--fs-value); color: var(--ink-muted); text-transform: uppercase; letter-spacing: .04em; }
.fs-cat {
  display: grid; align-items: center; column-gap: 8px; row-gap: 3px;
  grid-template-columns: minmax(0, 1fr) 7rem auto;
  grid-template-areas: "name budget delete" "name-error budget-error delete";
}
.fs-cat .fs-c-name { grid-area: name; min-width: 0; }
.fs-cat .fs-c-bud { grid-area: budget; min-width: 0; text-align: right; }
.fs-cat .fs-c-del { grid-area: delete; color: var(--ink-muted); }
/* Validation is an in-flow sibling of each category field. Explicit grid areas
 * keep the error below its own input instead of stealing horizontal width from
 * the name/budget row; [hidden] remains display:none and does not reserve a row. */
.fs-cat .fs-field-error { min-width: 0; max-width: 100%; overflow-wrap: anywhere; }
.fs-cat .fs-field-error[data-fs-error$="-name"] { grid-area: name-error; }
.fs-cat .fs-field-error[data-fs-error$="-budget"] { grid-area: budget-error; }
.fs-running { text-align: right; font-size: var(--fs-value); }
.fs-tally { display: flex; flex-direction: column; gap: 6px; background: var(--chip); border-radius: 12px; padding: 12px; }
.fs-tally-row { display: flex; justify-content: space-between; }
.fs-tally-total { border-top: 1px solid var(--hairline); padding-top: 6px; font-size: var(--fs-h3); }
.fs-nav { display: flex; gap: 10px; margin-top: 6px; }
.fs-nav .fs-next, .fs-nav .fs-create { flex: 1; }
.fs-status.error { color: var(--neg); }

/* Onboarding chart-favourites step (setup-favourites.js). Theme-token colours
 * only (CARDINAL 14) so it reads in light and dark. */
.setup-fav { gap: 14px; }
.setup-fav-lead { margin: 0; color: var(--ink-muted); }
.setup-fav-preview {
  border: 1px solid var(--hairline);
  border-radius: var(--radius-s);
  padding: 10px 12px;
  background: var(--card-2, var(--card));
  min-height: 120px;
}
.setup-fav-subhead { margin: 4px 0 0; font-size: 1em; }
.setup-fav-picker { max-height: 42vh; overflow-y: auto; }
.setup-fav-actions { margin-top: 4px; }
.setup-fav-actions .btn { flex: 1; }

/* ---- Setup Hub (/start, setup-hub.js, D-050): the permanent home of an
 * incomplete setup. Step list is derived fresh every render, so it can never
 * disagree with reality — see SETUP-GATE-DESIGN.md. Token-only (rule 14). */
.setup-hub-list { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
.setup-hub-row {
  display: flex; align-items: flex-start; gap: 10px; padding: 10px;
  border: 1px solid var(--hairline); border-radius: 12px; background: var(--card);
}
.setup-hub-mark { flex: 0 0 auto; font-size: var(--fs-value); line-height: 1; color: var(--ink-muted); margin-top: 2px; }
.setup-hub-body { flex: 1 1 auto; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.setup-hub-title { font-weight: 700; }
.setup-hub-why { line-height: 1.35; }
.setup-hub-fix { flex: 0 0 auto; align-self: center; }
.setup-hub-resume, .setup-hub-startover, .setup-hub-sched-answer { margin-top: 12px; }

/* ---- Onboarding fork (/start, onboarding-start.js): "do you use YNAB?" ---- */
/* Two stacked choice cards, each a self-contained mini-panel with its own CTA.
 * Token-only so both themes read correctly (CARDINAL rule 14). */
.ob-fork-choice {
  display: flex; flex-direction: column; gap: 10px; margin-top: 4px;
  background: color-mix(in srgb, var(--ink-muted) 6%, var(--card));
  border: 1px solid var(--hairline);
}
.ob-fork-choice h3 { margin: 0; font-size: var(--fs-value); }
.ob-fork-choice p { margin: 0; }
.ob-fork-btn { width: 100%; margin-top: 2px; }

/* Save dock: slides up from the bottom; hidden until the draft is dirty.
 * Bottom safe-area lives HERE, not on .txn-sheet: when the keypad is open it
 * covers the dock and carries its own inset instead (see .keypad-panel), so a
 * second inset on the sheet stacked into a visible beige gap under the pad
 * (operator report 2026-07-23, v1.57.0 fix). */
.sheet-dock { flex: 0 0 auto; display: flex; flex-direction: column; gap: 8px; padding-bottom: env(safe-area-inset-bottom); }
.sheet-dock.hidden { display: none; }
.btn-save {
  flex: 1; padding: .95rem 1.6rem; font-size: var(--fs-h3); border-radius: 16px; min-height: var(--ctl-h-lg);
  display: inline-flex; align-items: center; justify-content: center; gap: .55rem;
}
/* Tick-in-a-circle, reserved for THE Save button (operator 2026-07-12): the
 * circle takes the button's ink colour (its opposite fill), the tick takes the
 * button colour — flips automatically with the theme via the same tokens. */
.btn-save .save-tick {
  flex: 0 0 auto; width: 1.5em; height: 1.5em; border-radius: 50%;
  background: var(--accent-ink); color: var(--accent);
  display: inline-flex; align-items: center; justify-content: center;
}
.btn-save .save-tick .ico { width: .95em; height: .95em; stroke-width: 2.4; }
/* APPROVE (operator 29/07/2026): the approval queue's commit button. Deliberately
 * the SAME button as Save — same accent fill, same tick, same box — because it is
 * the same action plus the approval flag; only the word differs. The label is
 * set in caps, so give it the tracking caps need to stay readable. */
.btn-save.btn-approve { letter-spacing: .04em; }
.btn-save.btn-approve.is-acting { opacity: .72; transform: scale(.96); }

/* "Hidden" marker on a Plan row for a category that is archived but still holds
 * activity or a balance (plan-math.js categoryRows, operator bug hunt
 * 29/07/2026). Such a row is shown so the Plan's own totals still reconcile
 * against the ledger; the tag explains why a hidden category is on screen.
 * Token-only colours so dark/light parity holds (rule 14). */
.plan-hidden-tag {
  display: inline-block; margin-left: .4rem; padding: 0 .4em;
  font-size: var(--fs-label); font-weight: 600; line-height: 1.5;
  border-radius: 999px; vertical-align: middle; white-space: nowrap;
  color: var(--ink-muted);
  background: color-mix(in srgb, var(--ink-muted) 16%, transparent);
}
/* NEXT button (operator 2026-07-12, 3rd pass): the white blocky NEXT is
 * DEPRECATED PERMANENTLY — NEXT must look EXACTLY like the existing SAVE (same
 * accent fill from .btn, same dimensions, same 16px radius). Only the content
 * differs: "Next ›" with an arrow instead of the tick + "Save". So it inherits
 * the identical box as .btn-save; no white/card override, no different corner. */
.btn-next {
  flex: 1; padding: .95rem 1.6rem; font-size: var(--fs-h3); border-radius: 16px;
  display: inline-flex; align-items: center; justify-content: center; gap: .55rem;
}
.btn-next .btn-next-arrow { display: inline-flex; }
.btn-next .btn-next-arrow .ico { width: 1.15em; height: 1.15em; stroke-width: 2.4; }

/* Pinned decision bar at the FOOT of pickers (operator 2026-07-11 thumb-reach):
 * holds "Split Between Categories" on the category picker and the split
 * multi-select's Next. Sits after the scrolling list, never scrolls away. */
.picker-pinned {
  flex: 0 0 auto; padding-block: 6px; border-top: 1px solid var(--hairline);
  display: flex; flex-direction: column;
}
.picker-pinned .btn { width: 100%; padding: .8rem 1.2rem; }
.picker-pinned .picker-pinned-btn {
  border-top: 0; text-align: center; font-size: var(--fs-value); padding: .55rem .4rem;
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
}
.picker-pinned-icon { display: inline-flex; flex: 0 0 24px !important; min-width: 24px !important; width: 24px; height: 24px; color: var(--accent); overflow: visible !important; }
.picker-pinned-icon .ico { width: 24px; height: 24px; }

/* Sub-modal backdrop: a full-screen catcher above the sheet. A tap on the
 * backdrop itself (outside the modal) dismisses ONLY that sub-modal — it never
 * reaches the sheet's own overlay (which would discard the whole transaction)
 * and never lands on a control beneath the tap. */
.sub-backdrop {
  position: fixed; inset: 0; z-index: 60; display: flex;
  /* Anchor pickers to the TOP, not the vertical centre (operator 2026-07-10):
   * a centred modal shrinks toward its middle as its list is filtered, so the
   * results drifted DOWN under the on-screen keyboard. Top-anchored, the search
   * box + results stay put and the list simply shortens from the bottom. */
  align-items: flex-start; justify-content: center;
  padding: max(16px, env(safe-area-inset-top)) 16px 16px;
  background: var(--sheet-scrim-soft);
}
.sub-backdrop .picker-modal {
  position: relative; inset: auto; top: auto; left: auto; transform: none;
}
/* Compact touch dialogs default to the thumb edge. Full-screen search pickers
 * and contextual bubbles own later, more-specific geometry exceptions. */
body:is([data-surface="phone"], [data-surface="tablet"]) .sub-backdrop {
  align-items: flex-end;
  padding: 16px 16px max(12px, env(safe-area-inset-bottom));
}
/* Search-driven pickers deliberately stay above the keyboard. Destructive
 * nuke confirmations deliberately stay centred, away from the thumb edge. */
body:is([data-surface="phone"], [data-surface="tablet"]) .sub-backdrop:has(.picker-search) {
  align-items: flex-start;
  padding: max(16px, env(safe-area-inset-top)) 16px 16px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sub-backdrop.nuke-backdrop {
  align-items: center;
  padding: 16px;
}
/* A six-week calendar plus its destructive End-now action is taller than an
 * iPhone landscape visual viewport. Keep the 44px day/action targets and make
 * the modal itself the scroll owner, so the final action remains reachable
 * instead of painting beyond the fixed backdrop. */
.sub-backdrop .calendar-modal {
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* pickers / popovers / calendar / split — shared modal chrome */
.popover, .picker-modal {
  z-index: 60; background: var(--card); color: var(--ink);
  border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,.28);
}
.popover { animation: recurring-pop-in var(--dur-base) var(--ease-ios-rebound) backwards; }
.picker-modal { animation: recurring-pop-in var(--dur-base) var(--ease-ios-rebound) backwards; }
/* Individual translation is load-bearing here. `.popover` owns centring
 * through transform:translate(-50%,-50%), while anchored pickers own their own
 * transform too. Composite transform animation erased that placement; even an
 * individual scale rescaled the centring matrix and moved the card by 3.72px.
 * A rebound translation keeps both centred and anchored geometry invariant. */
@keyframes recurring-pop-in {
  from { opacity: 0; translate: 0 -4px; }
  to { opacity: 1; translate: 0 0; }
}
.popover {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  min-width: 200px; max-height: calc(100dvh - 24px); overflow-y: auto; padding: 6px;
  overscroll-behavior: contain; opacity: 1; isolation: isolate;
}
.plan-filter-popover {
  background: var(--card); border: 1px solid var(--hairline);
  box-shadow: 0 12px 32px color-mix(in srgb, var(--ink) 22%, transparent);
  /* Menu text must never compete with the rows beneath it during the shared
   * pop-in. Keep this card opaque from its first painted frame. */
  animation: none;
}
/* Month picker popover (operator 2026-07-12): only as wide as its rows need,
 * opened directly below the "Jul 2026" label. Drops the 200px floor. */
.popover.plan-month-pop { min-width: 0; width: max-content; max-width: 60vw; }
.popover.plan-month-pop .pop-row { text-align: center; padding: .5rem 1.1rem; }
.pop-row { display: block; width: 100%; text-align: left; background: none; border: 0; font: inherit; padding: .6rem .8rem; border-radius: 8px; cursor: pointer; }
.pop-row.sel { background: var(--chip); }
@media (hover: hover) { .pop-row:hover { background: var(--chip); } }
/* Recent Moves rows: category · month · signed delta (operator 2026-07-08h). */
.pop-row.recent-move { display: flex; align-items: baseline; gap: .5rem; }
.recent-move .rm-cat { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.recent-move .rm-month { flex: 0 0 auto; }
.recent-move .rm-delta { flex: 0 0 auto; font-variant-numeric: tabular-nums; }

.picker-modal {
  /* Height respects the VISUAL viewport (--vvh, set from visualViewport in
   * main.js): when the software keyboard is up on iPhone OR Pixel the modal —
   * and its pinned bottom buttons — shrink to the space actually visible
   * instead of hiding under the keyboard (family B runs both platforms). */
  width: min(420px, 94vw);
  max-height: min(80dvh, calc(var(--vvh, 100dvh) - 32px));
  display: flex; flex-direction: column;
  padding: 12px; gap: 8px;
}
/* Split editor rides the BOTTOM edge on touch (operator 2026-07-11: it sat just
 * out of thumb reach); its Done never scrolls away. */
@media (pointer: coarse) {
  /* Chart x-axis ticks, touch surfaces. Set on :root so it reaches EVERY chart in
   * one move — Home's Big Picture and trailing bars, Trends' stacked bars,
   * Reflect, net worth and the line chart all paint their ticks through
   * .bar-xlabel.
   *
   * 33 viewBox units. Measured on a 320px phone: the 640-unit viewBox renders at
   * 252px (scale 0.394), so this lands at ~13 SCREEN px — comparable to body text,
   * where the old 10px painted at ~3.9px and was the reported bug. Do not "tidy"
   * this to a screen-px-looking number without re-measuring the scale. */
  :root { --fs-graph-axis: 33px; }

  .sub-backdrop:has(.split-modal) {
    align-items: flex-end;
    padding-bottom: max(12px, env(safe-area-inset-bottom));
  }
  .split-modal .split-done { position: sticky; bottom: 0; }

  /* Thumb-reach dock (operator 2026-07-12, punch 14 rec 1+2): on touch, the
   * currency converter and the calendar/date pickers ride the BOTTOM edge like
   * the split editor, so their action-critical fields (currency / amount / rate /
   * Convert, and the day grid) sit under the thumb instead of top-anchored high on
   * a tall phone. These modals carry no keyboard-driven search list, so the
   * top-anchor that keeps search results clear of the keyboard buys them nothing —
   * bottom-docking is strictly better for reach. Desktop/pointer:fine keeps the
   * top-anchored centred layout. */
  .sub-backdrop:has(.fx-modal),
  .sub-backdrop:has(.calendar-modal) {
    align-items: flex-end;
    padding-bottom: max(12px, env(safe-area-inset-bottom));
  }
  /* The FX footer sticks to the bottom of the modal so Cancel/Convert never
   * scroll away when the (short) body grows. */
  .fx-modal .fx-footer { position: sticky; bottom: 0; background: var(--card); padding-top: 8px; }
}
/* A sticky FX footer can overlap the exchange-rate input when landscape leaves
 * only the keyboard band. While text entry owns that band, let the footer
 * participate in the modal scroll so Cancel never paints over typed digits. */
html[data-vv-keyboard="true"] body:is([data-surface="phone"], [data-surface="tablet"])
  .fx-modal .fx-footer {
  position: static;
}
.picker-head { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: var(--fs-h3); }
.picker-head > span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sheet-footer {
  display: flex; align-items: center; justify-content: flex-end;
  gap: 10px; flex-wrap: nowrap; width: 100%;
}
.sheet-footer > .btn { flex: 0 1 auto; min-width: 0; }
body[data-surface="phone"] .sheet-footer > .btn,
body[data-surface="tablet"] .sheet-footer > .btn { flex: 1 1 0; }
.picker-back { background: none; border: 0; font-size: var(--fs-h2); cursor: pointer; color: var(--accent); flex: 0 0 auto; }
.picker-cancel { background: none; border: 0; font: inherit; font-weight: 600; color: var(--accent); cursor: pointer; flex: 0 0 auto; padding: .2rem .1rem; }
.picker-search { width: 100%; }
/* Force-focused programmatically (forceFocus, txn-form.js advanceToNextField) —
 * iOS/Safari marks a script-focused text input :focus-visible, so the generic
 * button/input focus-visible rule below painted a 2px outline box around it.
 * Every other search input in the app already has this same override.
 * `body` prefix (0,2,1) matches the generic rule's own specificity so this
 * wins on source order — a bare `.picker-search:focus` (0,1,1) LOST that
 * cascade outright, regardless of :focus vs :focus-visible, on every pointer
 * type (operator 21/08/2026 P1-C audit). */
.picker-search:focus { outline: none; border-color: var(--accent); }
body .picker-search:focus-visible { outline: none; border-color: var(--accent); }
/* Custom-interval builder (Repeat → "Every X…"): a count field + a unit toggle
 * row + a confirm button. */
.custom-repeat-body { display: flex; flex-direction: column; gap: 14px; padding: 6px 2px 2px; }
.custom-repeat .cr-count { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.custom-repeat .cr-n { width: 6rem; text-align: center; font-variant-numeric: tabular-nums; }
.cr-units { display: flex; gap: 6px; }
.cr-unit {
  flex: 1; border: 1px solid var(--hairline); background: var(--chip); color: var(--ink);
  border-radius: 12px; padding: .55rem .2rem; font: inherit; font-weight: 600; cursor: pointer;
}
.cr-unit.on { background: transparent; color: var(--accent-ink); border-color: transparent; }
.seg-moving-indicator { pointer-events: none; }
body:is([data-surface="phone"], [data-surface="tablet"]) .custom-repeat .cr-units,
body:is([data-surface="phone"], [data-surface="tablet"]) .reflect-toggle {
  position: relative; gap: 2px; padding: 2px; background: var(--chip);
  border: 1px solid var(--hairline); border-radius: 999px; overflow: hidden;
}
body:is([data-surface="phone"], [data-surface="tablet"]) :is(.custom-repeat .cr-unit, .reflect-toggle .reflect-seg) {
  position: relative; z-index: 1; min-height: var(--ctl-h-md); border: 0;
  background: transparent; color: var(--ink-muted); border-radius: 999px;
  padding: .3rem .7rem; transition: transform var(--dur-fast) var(--ease-standard), color var(--dur-fast) var(--ease-standard);
}
body:is([data-surface="phone"], [data-surface="tablet"]) :is(.custom-repeat .cr-unit.on, .reflect-toggle .reflect-seg.active) {
  background: transparent; border-color: transparent; color: var(--accent);
}
body:is([data-surface="phone"], [data-surface="tablet"]) :is(.custom-repeat .cr-unit:active, .reflect-toggle .reflect-seg:active) {
  transform: scale(.96);
}
body:is([data-surface="phone"], [data-surface="tablet"]) :is(.cr-unit-indicator, .reflect-toggle-indicator) {
  display: block; position: absolute; inset: 2px auto 2px 2px; z-index: 0;
  width: 0; border: 1px solid color-mix(in srgb, var(--accent) 34%, transparent);
  border-radius: 999px; background: color-mix(in srgb, var(--accent) 16%, transparent);
  transition: transform var(--dur-base) var(--ease-ios-rebound), width var(--dur-base) var(--ease-ios-rebound);
}
@media (prefers-reduced-motion: reduce) {
  body:is([data-surface="phone"], [data-surface="tablet"]) :is(.cr-unit-indicator, .reflect-toggle-indicator, .cr-unit, .reflect-seg) {
    transition: none;
  }
}
.cr-done { align-self: stretch; }
.picker-list { overflow-y: auto; display: flex; flex-direction: column; }
/* flex-shrink:0 is LOad-BEARING (operator 29/07/2026: "see what happens when i
 * click categorise on the bottom toolbar").
 *
 * .picker-list is a flex COLUMN with a max-height. A flex item's default
 * flex-shrink is 1, so once the categories overflowed that cap, every row was
 * compressed instead of the list scrolling: measured at 19.7px per row with 67
 * categories, against 42.9px with 7. The rows' own text stayed ~17px, so it
 * spilled past the squashed box and each row's descenders overlapped the next
 * row's cap-height — the half-cut, stacked-on-top-of-each-other picker in the
 * operator's screenshot. overflow:hidden then clipped what was left.
 *
 * Every family has dozens of categories, so this fired on the real budget and
 * never on a small test one. Pinning shrink to 0 makes the list SCROLL (it
 * already has overflow-y:auto) and each row keep its natural height. */
.pick-row {
  text-align: left; background: none; color: var(--ink); border: 0;
  border-top: 1px solid var(--hairline); font: inherit; padding: .6rem .4rem;
  cursor: pointer; min-width: 0; overflow: hidden;
  flex: 0 0 auto;
}
.pick-row:first-child { border-top: 0; }
.pick-row.sel { background: var(--chip); }
:is(.type-menu, .confirm-later-picker) .pick-row { border-top: 0; }
@media (hover: hover) { .pick-row:hover { background: var(--chip); } }
.pick-row.extra { color: var(--accent); font-weight: 600; }
/* The "create the destination here" row inside the re-categorise gate (operator
 * 05/08/2026). Reads as the affirmative next step of a search that found
 * nothing, so it takes the accent, not the neutral row ink. */
.pick-row-new { color: var(--accent); font-weight: 600; }
/* THE FILTERED TRANSACTION LIST behind the re-categorise gate (operator
 * 05/08/2026: deleting a category must show which transactions are affected).
 * Collapsed by default so the replacement picker below it stays reachable with
 * a long ledger. Tokens only — both themes (global rule 14). */
.reassign-affected { margin: .3rem 0 .5rem; }
.reassign-affected > summary {
  cursor: pointer; color: var(--accent); font-weight: 600; padding: .25rem 0;
}
.reassign-affected-list {
  max-height: 12rem; overflow-y: auto; margin-top: .3rem;
  border: 1px solid var(--hairline); border-radius: var(--radius-s);
}
.reassign-affected-row {
  display: grid; grid-template-columns: 6.2rem 1fr auto; gap: 8px;
  align-items: center; padding: .3rem .5rem;
  border-top: 1px solid var(--hairline); font-size: var(--fs-body);
}
.reassign-affected-row:first-child { border-top: 0; }
.reassign-affected-row .ra-payee {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.reassign-affected-row .ra-amt {
  font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--ink-muted);
}
/* Category picker group headers (operator 2026-07-16): a non-clickable
 * section label above each group's categories, replacing the truncating
 * single-line "Group · Name" row. Tokens only for dark/light parity. */
/* flex:0 0 auto for the same reason as .pick-row above — a group header is a
 * flex child of the same capped-height list and was squashed identically. */
.pick-header { padding: 10px 12px 4px; font-size: .72rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--accent); flex: 0 0 auto; }
.pick-header:first-child { padding-top: 6px; }
.pick-row.pick-sub { padding-left: 20px; }
/* Category long-press action sheet (operator 2026-07-14) — one tappable row per
 * action with a leading glyph; identical on every touch device. Tokens only, so
 * dark/light parity holds. */
.cat-actions-list { display: flex; flex-direction: column; margin-top: 6px; }
.cat-action {
  display: flex; align-items: center; gap: 14px;
  text-align: left; background: none; border: 0; border-top: 1px solid var(--hairline);
  font: inherit; font-weight: 600; padding: .85rem .4rem; cursor: pointer; color: var(--ink);
}
.cat-action:first-child { border-top: 0; }
.cat-action .ico { flex: 0 0 auto; width: 22px; height: 22px; }
@media (hover: hover) { .cat-action:hover { background: var(--chip); } }
/* The touch category menu remains near the thumb, but no longer hugs the
 * bottom edge. Translating it by its own measured height gives the operator's
 * requested one-popup-height lift without changing any other compact dialog.
 * The 44dvh cap keeps the doubled geometry inside short landscape viewports;
 * the list becomes its existing local scroll owner when needed. */
body:is([data-surface="phone"], [data-surface="tablet"]) .sheet-overlay:has(.cat-actions) .cat-actions {
  max-height: min(44dvh, calc(var(--vvh, 100dvh) - 32px));
  transform: translateY(-100%);
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* "N overspent categories · Cover" strip (operator capture 2026-07-25) — sits
 * directly under the Ready-to-Assign pill it draws from. Tokens only, so
 * dark/light parity holds. */
.plan-overspent {
  display: flex; align-items: center; gap: 12px;
  background: var(--card); border: 1px solid var(--hairline);
  border-radius: 999px; padding: .5rem .6rem .5rem .5rem; margin: 8px 0 0;
  width: 100%; box-sizing: border-box; color: var(--ink); font: inherit;
  text-align: left; cursor: pointer; appearance: none;
  transition: transform var(--dur-fast) var(--ease-spring), border-color var(--dur-fast) linear;
}
.plan-overspent:active { transform: scale(.99); }
.plan-overspent:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.plan-overspent .ov-count {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  min-width: 26px; height: 26px; padding: 0 7px; border-radius: 999px;
  background: var(--neg); color: var(--neg-ink); font-weight: 800; font-size: .82rem;
}
.plan-overspent .ov-text { flex: 1 1 auto; min-width: 0; font-weight: 600; }
.plan-overspent .ov-text .muted { display: block; font-weight: 400; }
.plan-overspent .ov-action { flex: 0 0 auto; display: inline-flex; align-items: center; gap: .2rem; color: var(--accent); font-weight: 700; white-space: nowrap; }
.plan-overspent .ov-action .ico { width: 1rem; height: 1rem; }
.cover-list { display: flex; flex-direction: column; margin: 8px 0; max-height: 46vh; overflow-y: auto; }
.cover-row, .cover-total {
  display: flex; justify-content: space-between; gap: 12px;
  padding: .55rem .2rem; border-top: 1px solid var(--hairline);
}
.cover-row:first-child { border-top: 0; }
.cover-total { font-weight: 700; border-top-width: 2px; }
.cover-amt { font-variant-numeric: tabular-nums; color: var(--neg); font-weight: 700; }
/* Target editor (D-066, operator reference capture 2026-07-25) — YNAB's own
 * shape: a segmented cadence control over a sentence-style form, Spending
 * Trends beneath, Save/Delete pinned at the bottom. Tokens only, so dark/light
 * parity holds without a second rule (global rule 14). */
.target-editor .picker-head { justify-content: space-between; gap: 10px; }
.target-tabs {
  position: relative; display: flex; gap: 2px; margin: 10px 0 14px; padding: 3px;
  background: var(--chip); border-radius: 999px;
}
.target-tab-indicator {
  position: absolute; z-index: 0; top: 3px; bottom: 3px; left: 3px;
  border-radius: 999px; background: var(--card); box-shadow: 0 1px 3px rgba(0, 0, 0, .12);
  pointer-events: none; transition: left var(--dur-slow) var(--ease-ios-rebound), width var(--dur-slow) var(--ease-ios-rebound);
}
.target-tab {
  position: relative; z-index: 1; flex: 1 1 0; min-width: 0; min-height: 40px; padding: .45rem .3rem;
  border: 0; border-radius: 999px; background: none; cursor: pointer;
  font: inherit; font-weight: 600; color: var(--ink);
  /* The selected tab restyles ITSELF (own background + weight), so the tap has
   * visible per-control feedback, not just a body swap (global rule 26). */
  transition: color var(--dur-fast) ease, transform var(--dur-fast) var(--ease-ios-rebound);
}
.target-tab.on { background: transparent; font-weight: 800; }
.target-tab:active { transform: scale(.94); }
@media (hover: hover) { .target-tab:not(.on):hover { background: var(--surface-hover); } }
@media (prefers-reduced-motion: reduce) {
  .target-tab-indicator, .target-tab { transition: none; }
}
body:is([data-surface="phone"], [data-surface="tablet"]) .target-tab { min-height: 44px; }
.target-form { padding: 4px 14px; }
.target-row {
  display: flex; align-items: center; gap: 14px; width: 100%;
  min-height: 56px; padding: .7rem 0; border: 0; border-top: 1px solid var(--hairline);
  background: none; font: inherit; color: inherit; text-align: left; cursor: pointer;
}
.target-row:first-child { border-top: 0; }
.target-ico { flex: 0 0 auto; width: 24px; height: 24px; color: var(--muted-ink, var(--ink)); opacity: .55; }
.target-ico .ico { width: 24px; height: 24px; }
.target-field { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.target-lbl { font-size: .82rem; color: var(--muted-ink, var(--ink)); opacity: .65; }
.target-inp {
  width: 100%; min-width: 0; border: 0; background: none; padding: 0;
  font: inherit; font-size: 1.06rem; font-weight: 700; color: var(--ink);
}
.target-inp:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
select.target-inp { appearance: none; cursor: pointer; }
.target-chev { flex: 0 0 auto; width: 20px; height: 20px; opacity: .5; }
.target-chev .ico { width: 20px; height: 20px; }
.target-roll-val { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.target-roll-strong { font-weight: 700; }
/* Repeats switch (Custom tab). The knob and track both restyle on toggle. */
.target-switch {
  flex: 0 0 auto; width: var(--switch-track-width); height: var(--switch-track-height); padding: 0; border: 0; cursor: pointer;
  border-radius: 999px; background: var(--chip); position: relative;
  transition: background var(--dur-base) var(--ease-ios-rebound);
}
.target-switch::after {
  content: ''; position: absolute; top: var(--switch-inset); left: var(--switch-inset); width: var(--switch-knob-size); height: var(--switch-knob-size);
  border-radius: 50%; background: var(--card); box-shadow: none;
  transition: transform var(--dur-base) var(--ease-ios-rebound);
}
.target-switch.on { background: var(--accent); }
.target-switch.on::after { transform: translateX(var(--switch-travel)); }
.target-trends-head {
  display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
  margin: 18px 2px 8px; font-weight: 700;
}
.target-trends-card { padding: 2px 14px; }
.target-trend-row {
  display: flex; justify-content: space-between; align-items: center; gap: 12px;
  width: 100%; min-height: 52px; padding: .7rem 0; border: 0; border-top: 1px solid var(--hairline);
  background: none; font: inherit; color: inherit; text-align: left;
}
.target-trend-row:first-child { border-top: 0; }
.target-trend-row.settable { cursor: pointer; }
.target-trend-val { font-variant-numeric: tabular-nums; font-weight: 700; }
.target-trend-row.settable .target-trend-val { color: var(--accent); }
@media (hover: hover) { .target-trend-row.settable:hover { background: var(--surface-hover-soft); } }
/* A tapped trend row confirms itself before the amount above changes. */
.target-trend-row.settable.applied .target-trend-val { color: var(--pos); }
.target-trend-row.settable.applied::after { content: '✓'; color: var(--pos); font-weight: 800; margin-left: 6px; }
.target-actions { display: flex; gap: 10px; margin-top: 18px; }
.target-actions .btn { flex: 1 1 0; min-height: 48px; display: inline-flex; align-items: center; justify-content: center; gap: 8px; }
.target-actions .btn .ico { width: 20px; height: 20px; }
.target-delete { background: var(--neg); color: var(--neg-ink); border-color: var(--neg); }
.target-roll-opt { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; }
.target-roll-opt.on { background: var(--chip); }
/* View Moves list: month on the left, signed delta on the right. */
.cat-moves-list { display: flex; flex-direction: column; margin-top: 6px; max-height: 60vh; overflow-y: auto; }
.cat-move-row { display: flex; justify-content: space-between; gap: 12px; padding: .55rem .4rem; border-top: 1px solid var(--hairline); }
.cat-move-row:first-child { border-top: 0; }
.cat-move-delta { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--pos); }
.cat-move-delta.neg { color: var(--neg); }
/* Confirm sheet (cross-group category move). */
.confirm-modal .confirm-msg { font-weight: 600; margin: 4px 0 14px; }
.confirm-actions { display: flex; gap: 10px; justify-content: flex-end; }
/* Flag picker: colour swatch left of each label so the colour is visible, not
 * memorised. The dot is a fixed flag colour (theme-invariant); its ring uses a
 * token so it reads in both themes. */
.pick-row-swatch { display: flex; align-items: center; gap: 10px; }
.pick-swatch { flex: 0 0 auto; width: 14px; height: 14px; border-radius: 50%; background: var(--item-color, var(--accent)); box-shadow: 0 0 0 1px var(--hairline) inset; }
/* Swatch inline in the txn-form Flag field value (item 5). */
.field-value .pick-swatch { display: inline-block; vertical-align: middle; margin-right: 6px; }
/* The transaction Flag chooser is a set of colour choices, not a ruled data
 * table. Keep its selected fill and separate rows through spacing alone. */
.txn-list-picker[data-picker-title="flag"] .pick-row { border-top: 0; }

/* Currency converter (item: dynamic FX). */
.fx-modal {
  background: var(--card); color: var(--ink); padding: 16px; gap: 12px;
}
.fx-modal .picker-head {
  justify-content: center; text-align: center; font-size: var(--fs-h2); line-height: 1.15; color: var(--ink);
}
.fx-modal .picker-head > span { flex: 0 1 auto; }
.fx-body {
  display: grid; grid-template-columns: minmax(0, 1fr);
  row-gap: 12px; min-width: 0; padding: 2px 8px 4px;
}
.fx-rate-field small { font: inherit; color: var(--ink-muted); }
.fx-field {
  display: flex; flex-direction: column; gap: 6px; min-width: 0; padding-inline: 6px;
  font-size: var(--fs-body); color: var(--ink-muted);
}
.fx-field > span:first-child {
  font-size: var(--fs-value); line-height: 1.2; font-weight: 650; color: var(--ink-muted);
}
.fx-field .inp { font-size: var(--fs-h2); color: var(--ink); }
.fx-modal .fx-cur {
  --fx-cur-fit: var(--fs-h2);
  font-size: var(--fx-cur-fit);
}
.fx-body > .fx-field:first-child,
.fx-rate-note,
.fx-result,
.fx-holiday-rate { grid-column: 1 / -1; }
.fx-modal .fx-code { color: inherit; font-weight: 650; }
.fx-modal .fx-base-code { color: var(--control-secondary-ink); font-weight: 750; }
.fx-rate-sentence {
  display: flex; align-items: baseline; justify-content: center;
  gap: .3em; min-width: 0; min-height: var(--ctl-h-md); padding: 4px 0; box-sizing: border-box;
  border: 0; background: transparent; color: var(--ink); font-size: var(--fs-value);
  font-weight: 500; line-height: 1.15; text-align: center; white-space: nowrap;
}
.fx-rate-equation,
.fx-rate-value {
  display: flex; align-items: baseline; justify-content: center; gap: .28em;
  min-width: 0; font: inherit; line-height: inherit;
}
.fx-rate-sentence .fx-code { color: inherit; font-weight: inherit; }
.fx-rate-sentence .fx-base-unit { font: inherit; }
.fx-rate-sentence .fx-rate {
  display: inline-block; width: auto; max-width: 10ch; min-width: 0; min-height: 0;
  border: 0; padding: 0; background: transparent; color: inherit;
  font: inherit; text-align: center; vertical-align: middle;
}
/* Cancel is the sheet's sole footer action; the conversion equation itself is
 * the primary ENTER SGD VALUE control above it. */
.fx-footer { display: flex; gap: 10px; padding-top: 4px; }
.fx-footer .btn {
  flex: 1 1 0; min-width: 0; min-height: var(--ctl-h-lg);
  justify-content: center; font-size: var(--fs-h3);
}
.fx-result {
  display: flex; flex-direction: column; align-items: stretch; gap: 8px; width: 100%; min-width: 0;
  font-family: inherit; font-size: var(--fs-value); font-weight: 650; color: var(--ink);
  text-align: center; padding: 14px 12px 12px; border: 0;
  border-radius: var(--radius); background: var(--control-secondary-bg); cursor: pointer;
}
.fx-result:disabled { cursor: default; color: var(--ink-muted); }
.fx-result-equation {
  display: block; min-width: 0; font: inherit; font-size: var(--fs-h3); line-height: 1.25;
}
.fx-result-action {
  display: block; color: var(--control-secondary-ink); font-size: var(--fs-label);
  font-weight: 750; line-height: 1.2; letter-spacing: .02em; text-transform: uppercase;
}
.fx-holiday-rate { display: grid; grid-template-columns: minmax(0, 1fr) auto; color: var(--ink); }
.fx-holiday-rate small { grid-column: 1 / -1; }
.fx-modal.fx-home .fx-rate-field,
.fx-modal.fx-home .fx-rate-note,
.fx-modal.fx-home .fx-holiday-rate { display: none; }

body:is([data-surface="phone"], [data-surface="tablet"]) .fx-modal .fx-cur {
  text-align: center; text-align-last: center;
  padding-inline: 2.35rem; font-weight: 650;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .fx-modal .fx-rate-note {
  min-height: 1.2em; margin-top: -4px; text-align: center; font-style: italic; color: var(--ink-muted);
}
.fx-payee-preference {
  animation: fx-preference-in var(--dur-slow) var(--ease-ios-rebound) both;
  grid-template-columns: minmax(0, 1fr) auto;
}
@keyframes fx-preference-in { from { opacity: 0; transform: translateY(-8px) scale(.98); } }
.holiday-banner {
  margin: 0 12px 6px; padding: 5px 10px; min-width: 0; overflow: hidden;
  display: flex; align-items: center; justify-content: center; gap: .55rem;
  border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--hairline));
  border-radius: 999px; background: color-mix(in srgb, var(--accent) 10%, var(--card));
  color: var(--ink); font-size: var(--fs-label); font-weight: 650;
  text-align: center; text-overflow: ellipsis; white-space: nowrap;
}

/* ---------- register context menu (§11.5) ---------- */
.ctx-menu {
  position: fixed; z-index: 70; min-width: 232px; padding: 6px;
  background: var(--card); color: var(--ink); border-radius: 12px;
  box-shadow: 0 10px 34px rgba(0,0,0,.32); border: 1px solid var(--hairline);
}
.reg-accounts-menu { max-width: min(88vw, 320px); max-height: min(60vh, 460px); overflow-y: auto; }
.reg-accounts-menu .ctx-row { min-height: var(--ctl-h-md); }
.reg-accounts-menu .ctx-row.active { color: var(--accent); font-weight: 700; }
.reg-accounts-menu .ctx-empty { display: block; padding: .7rem .6rem; color: var(--ink-muted); }

/* ── MACOS-STYLE EXPAND-FROM-BUTTON (operator 29/07/2026) ───────────────────
 * "all popups i want an macos-style 'stream/expand' out from button animation
 * and vice versa upon collapse. buttery smooth."
 *
 * The menu grows out of the control that opened it, not from its own middle.
 * `transform-origin` is written by mountMenu (register-actions.js) as a pixel
 * point in the menu's own box — the anchor's position — so a menu that flipped
 * ABOVE its button still grows downward toward that button rather than away
 * from it. Nothing here hardcodes a direction; the JS supplies the origin and
 * this supplies the motion.
 *
 * Scale is deliberately not taken to 0: a menu that starts at nothing reads as
 * a pop, not a stream. .92 with a small opacity ramp is the shape that reads as
 * unfurling. The open curve carries a slight overshoot so it SETTLES; the close
 * curve is a plain ease-in and shorter, because a dismissal that lingers feels
 * unresponsive.
 *
 * `will-change` is scoped to the animating states only — leaving it on a
 * long-lived menu would pin a compositor layer for a static element. */
.ctx-menu.menu-in {
  transform: scale(.92);
  opacity: 0;
  will-change: transform, opacity;
  transition:
    transform .19s cubic-bezier(.2, .9, .3, 1.06),
    opacity .13s ease-out;
}
.ctx-menu.menu-in.menu-open { transform: scale(1); opacity: 1; }
.ctx-menu.menu-out {
  transform: scale(.95);
  opacity: 0;
  will-change: transform, opacity;
  transition: transform .15s ease-in, opacity .12s ease-in;
  pointer-events: none;   /* a menu on its way out must not swallow the next click */
}
@media (prefers-reduced-motion: reduce) {
  /* Still fade — an element that blinks in and out with no change at all is
   * harder to follow, not easier — but drop the movement entirely. */
  .ctx-menu.menu-in,
  .ctx-menu.menu-in.menu-open,
  .ctx-menu.menu-out { transform: none; transition: opacity .1s linear; }
  .ctx-menu.menu-in { opacity: 0; }
  .ctx-menu.menu-in.menu-open { opacity: 1; }
}
.ctx-row {
  display: flex; justify-content: space-between; align-items: center; gap: 16px;
  width: 100%; background: none; border: 0; font: inherit; color: inherit;
  padding: .42rem .6rem; border-radius: 8px; cursor: pointer; text-align: left;
}
@media (hover: hover) { .ctx-row:hover:not(.disabled) { background: var(--chip); } }
.ctx-row.disabled { color: var(--ink-muted); opacity: .5; cursor: default; }
.ctx-row.danger { color: var(--neg); }
.ctx-key { font-size: var(--fs-label); color: var(--ink-muted); font-variant-numeric: tabular-nums; }
/* The desktop approval “i” is a compact action popover, not a generic menu:
 * the two labels are the complete workflow and each is a large full-width
 * button. Paint is semantic-token based for Current/dark parity. */
.ctx-menu.approval-info-menu {
  min-width: min(280px, calc(100vw - 16px));
  max-width: min(360px, calc(100vw - 16px));
  padding: 14px;
  border-radius: var(--radius-s);
  box-shadow: var(--chrome-control-shadow);
}
.ctx-menu.approval-info-menu::before {
  content: "";
  position: absolute;
  top: -8px;
  left: var(--approval-caret-x, 50%);
  width: 0;
  height: 0;
  border: 8px solid transparent;
  border-bottom-color: var(--card);
  transform: translateX(-50%);
  pointer-events: none;
}
.ctx-menu.approval-info-menu[data-approval-placement="above"]::before {
  top: auto;
  bottom: -8px;
  border-bottom-color: transparent;
  border-top-color: var(--card);
}
.ctx-menu.approval-info-menu .ctx-row {
  min-height: var(--ctl-h-lg);
  justify-content: center;
  padding: .75rem 1rem;
  border-radius: var(--radius-s);
  font-size: var(--fs-body);
  font-weight: 700;
  text-align: center;
}
.ctx-menu.approval-info-menu .ctx-row + .ctx-row { margin-top: 10px; }
.ctx-menu.approval-info-menu .ctx-key { display: none; }
.ctx-menu.approval-info-menu .approval-info-approve {
  background: var(--accent);
  color: var(--accent-ink);
}
.ctx-menu.approval-info-menu .approval-info-reject {
  background: var(--neg-pill);
  color: var(--neg);
  border: 1px solid color-mix(in srgb, var(--neg) 34%, var(--hairline));
}
@media (hover: hover) {
  .ctx-menu.approval-info-menu .approval-info-approve:hover:not(:disabled) {
    background: color-mix(in srgb, var(--accent) 84%, var(--card));
  }
  .ctx-menu.approval-info-menu .approval-info-reject:hover:not(:disabled) {
    background: color-mix(in srgb, var(--neg-pill) 82%, var(--neg));
  }
}
.ctx-menu.approval-info-menu .ctx-row:focus-visible {
  outline: 2px solid color-mix(in srgb, var(--accent) 70%, var(--card));
  outline-offset: 2px;
}
/* The phone/tablet "Actions" sheet reuses this menu but has no keyboard, so the
 * shortcut column (⇧R, C, Del, …) is meaningless there — hide it on touch. The
 * rows stay full-width and read as plain tappable actions (operator 2026-07-16). */
.ctx-menu.touch .ctx-key { display: none; }
/* Flush with the rows it separates: a 4px side inset left the divider starting
 * 4px right of every row's left edge (v1.24.0 crawl). */
.ctx-sep { height: 1px; background: var(--hairline); margin: 5px 0; }

/* ---------- Win11 cursor-anchored flyouts (operator 2026-07-20) ----------
 * Flag / Categorise / Move-to-Account / Memo / delete-confirm on the desktop
 * Spending + Scheduled surfaces open as a floating list at the cursor, NOT a
 * centred dimming modal (the touch popup shape the operator banned there). They
 * borrow .ctx-menu's fixed positioning + card chrome (mountMenu clamps them into
 * the viewport) and reuse .picker-list/.pick-row for the option rows. No backdrop
 * element exists, so the register stays fully visible behind them. */
.picker-flyout { min-width: 240px; max-height: min(60vh, 460px); display: flex; flex-direction: column; }
.picker-flyout .picker-list { max-height: 42vh; }
.picker-flyout-head {
  font-weight: 700; font-size: var(--fs-label); color: var(--ink-muted);
  text-transform: uppercase; letter-spacing: .05em; padding: 2px 6px 6px;
}
.picker-flyout .picker-search, .memo-flyout .memo-input {
  width: 100%; margin-bottom: 4px;
}
.memo-flyout { min-width: 260px; }
.memo-flyout .memo-input { min-width: 0; }
/* Inline delete confirm: a compact Confirm/Cancel card at the cursor. Enter =
 * Delete, Esc/outside = cancel (wired in confirmDeleteAt). */
.delete-confirm-flyout { min-width: 240px; max-width: 300px; }
.delete-confirm-flyout .dcf-msg { padding: 2px 6px 10px; font-size: var(--fs-body); }
.delete-confirm-flyout .dcf-actions { display: flex; justify-content: flex-end; gap: 8px; }
.delete-confirm-flyout .dcf-actions .btn { padding: .35rem 1rem; font-size: var(--fs-body); min-height: 0; }

/* ---------- Win11 drawer sub-pickers as flyouts (operator 2026-07-20) ----------
 * Inside the desktop drawer, a field-picker (Repeat, Payee, Category, Account,
 * Flag, currency, Schedule options, the split editor, styled confirms) opens as
 * a cursor-anchored flyout instead of a centred dimming sub-modal — the same
 * touch-popup shape the v1.40.0 sheet revamp removed, one layer deeper. The
 * modal node keeps its own .picker-modal / .split-modal / .discard-confirm
 * chrome; .sub-flyout just re-homes it as a floating card (JS sets fixed
 * left/top at the click point) and .sub-flyout-catch is a transparent
 * full-screen outside-tap catcher (no dimming — the register stays visible). */
.sub-flyout-catch { position: fixed; inset: 0; z-index: 78; background: transparent; }
.sub-flyout {
  z-index: 79; box-shadow: 0 10px 34px rgba(0,0,0,.32);
  border: 1px solid var(--hairline); border-radius: 12px;
  /* Anchored, not centred: drop the sheet-era bottom-dock/translate that some
   * picker variants carry, and cap the width so it reads as a menu. */
  margin: 0; transform: none; width: max-content; min-width: 240px; max-width: min(420px, 94vw);
  animation: recurring-pop-in var(--dur-base) var(--ease-ios-rebound) backwards;
}
.sub-flyout.picker-modal {
  /* A long Payee/Category list must shrink its list pane, not paint rows beyond
   * the flyout's measured box. Without min-height:0 on the flex child, a choice
   * could be visible in the DOM yet sit outside the viewport and be unclickable. */
  max-height: min(calc(100dvh - 16px), 460px);
  overflow: hidden;
}
.sub-flyout .picker-list { min-height: 0; overflow-y: auto; }

/* ---------- Win11 compact exemption (operator 2026-07-20) ----------
 * On the desktop surface the register + Scheduled surfaces are permanently
 * exempt from the 44px touch floor in favour of a denser display (the touch
 * floor was already surface-scoped, so this mainly tightens the drawer-hosted
 * forms + Scheduled rows that otherwise inherit --ctl-h-md). Buttons, inputs and
 * rows drop to a compact control height and tighter padding. Scoped to
 * data-surface="desktop" so touch is completely untouched. */
body[data-surface="desktop"] .reg-editbar .btn,
body[data-surface="desktop"] .drawer-formhost .btn,
body[data-surface="desktop"] .drawer-formhost .inp,
body[data-surface="desktop"] .drawer-formhost select.inp {
  min-height: var(--ctl-h-sm);
}
/* Broadened (operator 2026-07-20): "on Win11 waive the button height rules
 * present elsewhere and make buttons shorter and more slimline." Every general
 * .btn, toolbar/picker/context-menu/modal-action button and .inp on the desktop
 * surface drops from the 44px touch floor to a slim 30px, with tighter vertical
 * padding. Touch surfaces keep the 44px floor untouched (data-surface scope).
 * Circular disc buttons opt out via :not — a height cap would egg-shape them
 * (see [[ynab-ovoid-buttons-min-height]]). */
body[data-surface="desktop"] .btn:not(.disc):not(.icon-btn),
body[data-surface="desktop"] .reg-toolbar .btn,
body[data-surface="desktop"] .modal-actions .btn,
body[data-surface="desktop"] .ctx-menu .ctx-row,
body[data-surface="desktop"] .picker-modal .btn,
body[data-surface="desktop"] .picker-modal .pick-row,
body[data-surface="desktop"] .inp:not(textarea),
body[data-surface="desktop"] select.inp {
  min-height: 30px;
}
body[data-surface="desktop"] .btn:not(.disc):not(.icon-btn) {
  padding-top: 4px; padding-bottom: 4px;
}

/* D-058 (raised 2026-07-17, measured and closed 04/09/2026): the desktop block
 * above says "touch surfaces keep the 44px floor untouched" — but no rule ever
 * gave `.inp` that floor on touch. The 44px min came from the BUTTON rule near
 * the foot of this file, which lists `button, .btn, .type-pill, .reg-icon-btn`
 * and never inputs or selects. Measured on iphone13promax-portrait AND
 * ipadmini-portrait: 25 of 38 visible `.inp` rendered 37.3-40.4px, the shortest
 * being Report's period picker at 37.3px. Padding alone cannot reach 44px here
 * because `.inp` is sized by its inherited font plus .45rem of block padding.
 *
 * Scoped to touch surfaces so desktop keeps its 30px density. The excluded
 * selectors are the deliberate compact one-offs that own an explicit 28px
 * height (`.recurrence-step`, `.rec-remind-choices`, `.rec-end-date`): they sit
 * inside stepped rows whose own geometry a blanket floor would break, and each
 * already carries its own documented sizing. */
body:is([data-surface="phone"], [data-surface="tablet"])
  .inp:not(.recurrence-step .inp):not(.rec-remind-choices .inp):not(.rec-end-date) {
  min-height: var(--ctl-h-md);
}
body[data-surface="desktop"] .drawer-formhost .field-row,
body[data-surface="desktop"] .drawer-formhost .txn-field { padding-top: 4px; padding-bottom: 4px; }
/* Scheduled rows: tighten the vertical rhythm on desktop only. */
body[data-surface="desktop"] .sched-txn-row,
body[data-surface="desktop"] .sched-row:not(.reg-row) { padding-top: 6px; padding-bottom: 6px; min-height: 0; }

/* ---------- keyboard shortcuts overlay (§11.5) ---------- */
.shortcuts-modal { max-width: 400px; width: 100%; padding: 20px; display: flex; flex-direction: column; gap: 14px; }
.shortcuts-modal h2 { margin: 0; }
.shortcuts-table { display: grid; grid-template-columns: auto 1fr; gap: 8px 14px; align-items: center; }
.sc-key {
  justify-self: start; background: var(--chip); border-radius: 6px; padding: .12rem .5rem;
  font-size: var(--fs-label); font-family: inherit; border: 1px solid var(--hairline); white-space: nowrap;
}
.sc-desc { font-size: var(--fs-body); }

.calendar-modal .cal-head { display: flex; align-items: center; gap: 6px; font-weight: 700; }
.cal-month { flex: 1; text-align: center; }
.cal-cancel { margin-left: 4px; }
.cal-nav { background: none; border: 0; font-size: var(--fs-h2); cursor: pointer; color: var(--accent); flex: 0 0 auto; }
.cal-dow, .cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); text-align: center; }
.cal-dow span { font-size: var(--fs-label); color: var(--ink-muted); padding: .3rem 0; }
/* color: inherit — a <button> doesn't inherit ink, so dark mode painted the
 * day numbers black on the dark card (found 2026-07-16 wiring the register
 * date popover; the sheet's own calendar had the same latent bug). */
/* SQUIRCLE, NOT AN ELLIPSE (operator 03/08/2026: "see the ellipsoid of the
 * calendar date - also against theme"). `border-radius: 50%` on a cell with no
 * intrinsic size resolves against the box's width and height SEPARATELY, so a
 * grid item in `repeat(7, 1fr)` — wide column, short line-box — painted a
 * stretched OVAL (measured 54x39 on a 375px phone). Nothing else in this app is
 * an ellipse: every chip, pill and card uses the shared radius scale. The cell
 * is now a centred SQUARE (aspect-ratio 1, so the width follows the line height
 * rather than the column) carrying `--radius-s`, the same corner every other
 * small surface uses. The grid keeps its `1fr` columns for even spacing; the
 * cell simply no longer stretches to fill one. */
.cal-cell {
  background: none; border: 0; font: inherit; color: inherit; cursor: pointer;
  padding: 0; margin: 2px auto; aspect-ratio: 1; min-width: 2.15em;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--radius-s);
}
/* The calendar is scrollable on short touch viewports precisely so its real
 * controls can stay finger-sized. Desktop keeps its compact pointer geometry. */
body[data-surface="phone"] :is(.cal-cell, .cal-nav, .cal-cancel, .cal-clear),
body[data-surface="tablet"] :is(.cal-cell, .cal-nav, .cal-cancel, .cal-clear) {
  min-width: var(--ctl-h-md); min-height: var(--ctl-h-md);
}
.cal-cell.empty { cursor: default; }
/* ZOOM-OUT GRIDS (operator 04/08/2026: a means of "zooming out by month/year …
 * to select a faraway date"). Months and years are 3 columns, not 7: a day grid
 * is 7 wide because a week is, and forcing "Sep" or "2027" through a 1/7 column
 * would either clip the label or make the popover wider than the day view it
 * replaces. 12 cells over 3 columns is 4 rows either way, so the popover's
 * height barely moves between levels. */
.cal-grid-month { grid-template-columns: repeat(3, 1fr); }
/* These cells carry TEXT, so they must not inherit the day cell's
 * `aspect-ratio: 1` + `min-width: 2.15em` true-circle geometry
 * above): a square sized to "Sep" would be a tall box with the label adrift in
 * it. They keep the same radius and selected treatment. */
.cal-cell-wide {
  aspect-ratio: auto; min-width: 0; width: 100%;
  padding: .45rem .3rem; margin: 2px 0; border-radius: var(--radius-s);
  color: var(--accent);
}
/* The month/year label is a real button at the day and month levels (it zooms
 * out); at year level there is nothing further out, so it renders as a plain
 * span and this rule simply does not apply. */
.cal-zoom {
  background: none; border: 0; font: inherit; font-weight: 700; color: inherit;
  cursor: pointer; border-radius: var(--radius-s); padding: .2rem .5rem;
}
@media (hover: hover) { .cal-zoom:hover { background: var(--chip); } }
.cal-zoom:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
@media (hover: hover) { .cal-cell:hover { background: var(--chip); } }
/* `--accent-ink`, not a literal `#fff`: the ink token is what every other
 * on-accent surface uses, so a theme change carries here too. */
.cal-cell.sel { background: var(--accent); color: var(--accent-ink); font-weight: 700; }

.split-head { justify-content: space-between; }
.split-remaining { font-size: var(--fs-body); color: var(--neg); font-weight: 700; }
.split-remaining.ok { color: var(--pos); }
.split-list { overflow-y: auto; display: flex; flex-direction: column; gap: 6px; }
.split-line { display: block; }
.split-line-main { display: flex; gap: 6px; align-items: center; }
.split-cat {
  flex: 1; box-sizing: border-box; text-align: left; padding: .5rem .6rem;
  border-radius: var(--radius-s);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  color: var(--accent); font: inherit; cursor: pointer;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@media (hover: hover) {
  .split-cat:hover {
    background: color-mix(in srgb, var(--accent) 18%, transparent);
    border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  }
}
.split-amt { width: 90px; text-align: right; }
.split-line-detail {
  box-sizing: border-box; width: 100%; min-height: var(--ctl-h-md);
  display: grid; grid-template-columns: 28px minmax(0, 1fr) 24px;
  align-items: center; gap: 10px; padding: .55rem .6rem;
  border: 0; border-top: 1px solid var(--hairline); border-radius: 0;
  background: transparent; color: var(--ink); font: inherit; text-align: left;
}
button.split-line-detail { cursor: pointer; }
.split-line-detail-icon, .split-line-detail-chevron {
  display: inline-flex; align-items: center; justify-content: center; color: var(--ink-muted);
}
.split-line-detail-icon .ico { width: 22px; height: 22px; }
.split-line-detail-chevron .ico { width: 18px; height: 18px; }
.split-line-detail-value { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.split-line-memo { grid-template-columns: 28px minmax(0, 1fr); }
.split-line-memo-input {
  box-sizing: border-box; width: 100%; min-width: 0; padding: .35rem 0;
  border: 0; outline: 0; background: transparent; color: var(--ink); font: inherit;
}
.split-line-memo-input::placeholder { color: var(--ink-muted); opacity: 1; }
.split-del { background: none; border: 0; color: var(--ink-muted); cursor: pointer; font-size: var(--fs-value); }
.split-add, .split-done { align-self: stretch; }
.split-done { background: var(--accent); }
/* Split header rows (mirror YNAB iOS): the txn title + signed total on one card,
 * then a Categories / Remaining ledger row. */
.split-txnrow { display: flex; justify-content: space-between; align-items: center; gap: 10px; background: var(--card); border: 1px solid var(--hairline); border-radius: 14px; padding: .7rem .9rem; margin-bottom: 2px; }
.split-txntitle { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.split-txntotal { font-weight: 700; font-variant-numeric: tabular-nums; flex: 0 0 auto; }
.split-catsrow { display: flex; justify-content: space-between; align-items: center; padding: .2rem .3rem .1rem; font-weight: 700; }
.split-catsrow .split-remaining { font-weight: 700; }
/* Per-line ••• options button (replaces the bare ✕). */
.split-menu { background: none; border: 0; color: var(--ink-muted); cursor: pointer; font-size: var(--fs-value); letter-spacing: 1px; padding: 0 .3rem; flex: 0 0 auto; line-height: 1; }
@media (hover: hover) { .split-menu:hover { color: var(--ink); } }
.pick-row.danger { color: var(--neg); }
/* Transfer split line (v6): the destination-account chooser + the Add-a-Transfer
 * button read in the accent so a transfer line is distinct from a category one. */
.split-cat-xfer { color: var(--accent); font-weight: 600; }
.split-add-xfer { color: var(--accent); }

/* Multi-select "Split Between Categories" picker (IMG_4680/4681): check circles
 * fill accent-blue when chosen, header counts the selection, Next confirms. */
.splitpick-head { justify-content: space-between; }
.splitpick-count { font-weight: 700; text-align: center; flex: 1; margin-right: 1.4rem; /* balance the back ‹ so the count stays centred (Next moved to the pinned foot, 2026-07-11) */ }
.splitpick-next { font-weight: 700; }
.splitpick-next:disabled { opacity: .4; cursor: default; }
.splitpick-row { display: flex; align-items: center; gap: 12px; }
.splitpick-check {
  flex: 0 0 24px; width: 24px; height: 24px; min-width: 24px; max-width: 24px;
  aspect-ratio: 1 / 1; padding: 0; overflow: hidden; border-radius: 50%;
  border: 2px solid var(--hairline); display: inline-flex; align-items: center; justify-content: center;
  color: var(--accent-ink); transition: background var(--dur-fast) var(--ease-standard), border-color var(--dur-fast) var(--ease-standard);
}
.splitpick-row.on .splitpick-check { background: var(--accent); border-color: var(--accent); }
.splitpick-check .ico { width: 1em; height: 1em; stroke-width: 2.4; }
.splitpick-name { flex: 1; min-width: 0; }

/* "Something Doesn't Add Up" reconcile dialog (IMG_4682): centred card, one
 * stacked button per resolution. */
.reconcile-modal { max-width: 340px; }
.reconcile-body { display: flex; flex-direction: column; gap: 10px; padding: 4px 2px; }
.reconcile-title { margin: 0; font-size: var(--fs-h3); font-weight: 700; }
.reconcile-msg { margin: 0 0 4px; color: var(--ink-muted); font-size: var(--fs-value); }
.reconcile-opt { align-self: stretch; }

/* ---------- Plan tab (§7 mobile / §11.7 desktop) ---------- */
.plan-header { display: flex; align-items: center; gap: 20px; margin-bottom: 12px; flex-wrap: wrap; }
.plan-header.mobile { justify-content: center; }
.plan-month { display: flex; align-items: center; gap: 4px; }
.plan-mo-btn {
  box-sizing: border-box; padding: 0 .3rem; border-radius: var(--radius-s);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  color: var(--accent); font-size: var(--fs-h2); cursor: pointer;
}
@media (hover: hover) {
  .plan-mo-btn:hover {
    background: color-mix(in srgb, var(--accent) 18%, transparent);
    border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  }
}
.plan-mo-label {
  display: inline-flex; align-items: center; gap: 5px; box-sizing: border-box;
  padding: .2rem .4rem; border-radius: var(--radius-s);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  color: var(--accent); font: inherit; font-weight: 600; font-size: var(--fs-h3);
  cursor: pointer;
}
@media (hover: hover) {
  .plan-mo-label:hover {
    background: color-mix(in srgb, var(--accent) 18%, transparent);
    border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  }
}
/* The ⌄ glyph renders high in its em-box; centre it against the label text and
 * shrink it so it reads as a caret, not a character (operator: align the arrow). */
.plan-mo-caret { display: inline-flex; align-items: center; justify-content: center; color: var(--accent); }
.plan-mo-caret .ico, .plan-mo-btn .ico { width: 1em; height: 1em; }
/* return-to-today (item 10). Pill by design, so the 999px lozenge radius STAYS;
 * only the grey --chip fill becomes the accent tint. */
.plan-mo-today {
  box-sizing: border-box; padding: .18rem .55rem; border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  color: var(--accent); font: inherit; font-size: var(--fs-label); font-weight: 600;
  cursor: pointer; margin-left: 4px;
}
@media (hover: hover) { .plan-mo-today:hover { background: var(--accent); color: var(--accent-ink); } }

/* One green capsule (operator 2026-07-08i): the entire Ready-to-Assign pill is a
 * single tappable button, YNAB-style, with the chevron tucked at the right.
 * iOS press bounce on :active. */
.plan-rta {
  display: flex; align-items: center; gap: 12px; background: var(--rta-fill);
  border: 0; font: inherit; text-align: left; cursor: pointer; width: 100%;
  border-radius: 999px; padding: .8rem 1.4rem; flex: 1; min-width: 260px;
  transition: transform var(--dur-fast) var(--ease-spring), filter var(--dur-fast) var(--ease-standard);
}
/* The pill declares a transform+filter transition above, but :active used to
 * set both to `none`, so the transition animated nothing and the biggest
 * button on the Plan tab gave no press feedback (2026-07-25). */
.plan-rta:active { transform: scale(.985); filter: brightness(.95); }
/* Operator 2026-07-18 fleet check: at 1366w the desktop toolbar squeezed the pill to 289px
 * against 333px of content — the label ran off the lime capsule and under the Undo button.
 * min-width:max-content lets the pill refuse to shrink below its own text; .plan-header
 * already wraps, so the trailing toolbar controls flow to the next line instead. Desktop
 * only: on phone the pill owns a full row and must keep shrinking with the viewport. */
body[data-surface="desktop"] .plan-rta { min-width: max-content; }
/* Amount hugs the left, "Ready to Assign ›" is grouped hard right (screenshot). */
.plan-rta .rta-right { margin-left: auto; display: inline-flex; align-items: center; gap: 6px; }
.plan-rta .rta-caret { color: var(--rta-ink); display: inline-flex; }
.plan-rta .rta-caret .ico { width: 1.1em; height: 1.1em; }
.plan-rta.over .rta-caret { color: var(--neg); }
.plan-rta.zero .rta-caret { color: var(--ink-muted); }
/* Breathing room below the Ready-to-Assign banner so it does not touch the
 * filter toggles beneath it (operator: "green banner is touching the tab
 * toggles below — space it better"). The adjacent-sibling form below never
 * actually matched: on desktop .plan-rta lives INSIDE .plan-header (so its
 * next sibling is .plan-toolbar, not .plan-pills); on mobile .plan-listctl
 * sits between rtaBanner() and filterPills() in the DOM. Kept as documentation
 * of intent, but .plan-pills now carries its own unconditional top margin
 * below so the gap holds regardless of what precedes it (operator still
 * reported them touching — 2026-07-15, punch B5). */
.plan-header.mobile + .plan-rta { margin-top: 8px; }
.plan-rta + .plan-pills { margin-top: 12px; }
/* (Plan title → month gap is tightened directly on .plan-header.mobile below —
 * margin-top there wins on source order, so this sibling combinator is no longer
 * the knob. Kept as documentation of intent.) */
body[data-surface="phone"] .screen-title,
body[data-surface="tablet"] .screen-title { margin-bottom: .5rem; }
/* Small gap below the green Ready-to-Assign capsule so it doesn't touch the
 * plan list / dailies panel directly below it (operator 2026-07-08j). */
.plan-rta + .plan-mobile,
.plan-rta + .dailies-config,
.plan-rta + .card { margin-top: 12px; }
.plan-rta.over { background: var(--neg-pill); }
.plan-rta.zero { background: var(--chip); }
.plan-rta .rta-amt { font-size: var(--fs-h2); font-weight: 700; color: var(--rta-ink); font-variant-numeric: tabular-nums; letter-spacing: -.01em; }
.plan-rta.over .rta-amt { color: var(--neg); }
.plan-rta.zero .rta-amt { color: var(--ink); }
.plan-rta .rta-label { color: var(--rta-ink); font-weight: 600; font-size: var(--fs-value); white-space: nowrap; }
.plan-rta.over .rta-label, .plan-rta.zero .rta-label { color: var(--ink-muted); }
/* Phone: keep the amount + "Ready to Assign" on one line each in the narrow pill. */
body[data-surface="phone"] .plan-rta { padding: .75rem 1.15rem; gap: 8px; }
body[data-surface="phone"] .plan-rta .rta-amt { font-size: var(--fs-h3); }
body[data-surface="phone"] .plan-rta .rta-label { font-size: var(--fs-body); }
.rta-reset { font-size: var(--fs-body); color: var(--ink-muted); padding: 0 .2rem; } /* friendly RTA reset (item 14) */
@media (hover: hover) { .rta-reset:hover { color: var(--accent); } }

.plan-toolbar { display: flex; gap: 8px; margin-bottom: 8px; }
/* When the toolbar lives inside the header row it hugs the right edge (item 10). */
.plan-header > .plan-toolbar { margin-bottom: 0; margin-left: auto; }
.plan-toolbar .link-btn { border: 1px solid var(--hairline); }
.plan-toolbar .link-btn:disabled { opacity: .4; cursor: default; }
/* 2026-07-17 (tapthru §2): these two carry an ICON BESIDE TEXT ("Collapse all", "Reorder")
 * in a button that was not a flex container, so the glyph fell back to vertical-align and
 * painted 1.13px ABOVE the button's centre — measured on every desktop profile, both
 * themes. Centring the button's own line box fixes the icon and the label together, the
 * same way .plan-pill-plus and .xfer-chev already do it. Scoped to these two rather than
 * the shared .link-btn, which is also used for bare text links elsewhere. */
.plan-ctl-collapse, .plan-ctl-reorder {
  display: inline-flex; align-items: center; justify-content: center; gap: .35em;
}
.plan-ctl-collapse .ico, .plan-ctl-reorder .ico { flex: 0 0 auto; }
@media (min-width: 900px) and (max-width: 1300px) {
  body[data-surface="desktop"] .plan-header > .plan-toolbar {
    flex: 1 0 100%; justify-content: flex-end;
  }
  body[data-surface="desktop"] .plan-header > .plan-rta { min-width: 310px; }
}

/* Plan mobile header (operator 2026-07-12 punch 16): the pencil + hamburger pill
 * and the ••• spacer are all gone — only the month remains, dead-centred with its
 * ‹ › steppers. The header is a single tight row: no side elements means nothing
 * shifts the month off-centre, and a small fixed margin below removes the big
 * month→Ready-to-Assign gap that appeared on future months. margin-top clears the
 * fixed version/eye/gear chrome cluster so the month never slides beneath it. */
.plan-header.mobile {
  display: flex; align-items: center; justify-content: center;
  /* The .screen-title now leads Plan on its own row (2026-07-14) and already
   * carries the safe-area reserve, so the month cluster no longer needs to clear
   * the fixed chrome itself — its old calc(10px + safe-area) top margin just
   * stacked below the title as dead space. Pull it up snug under the title's
   * .5rem bottom margin instead. */
  margin-top: 0;
  margin-bottom: 10px;
}
/* Centred month cluster: ‹ September 2026 › */
.plan-mo-center-wrap { flex: 0 1 auto; display: inline-flex; align-items: center; justify-content: center; gap: 6px; max-width: 100%; }
.plan-header.mobile .plan-mo-center { justify-content: center; font-size: var(--fs-h3); font-weight: 700; }
/* Iconlike ‹ › step arrows (operator 2026-07-12): stroke chevrons, vertically
 * centred with the month label, sized to sit as clear tap targets either side. */
.plan-mo-step { display: inline-flex; align-items: center; justify-content: center; }
.plan-mo-step .ico { width: 1.5rem; height: 1.5rem; }
/* ‹ › month step buttons flanking the label (operator 2026-07-12). */
.plan-mo-step { font-size: var(--fs-h1); line-height: 1; color: var(--ink-muted); padding: 0 .25rem; }
/* Touch: ‹ › steppers measured 32px wide (v1.29.0 tapthru §1). Symmetric
 * widening; the month label between them is not a tap target, so no overlap. */
body[data-surface="phone"] .plan-mo-step,
body[data-surface="tablet"] .plan-mo-step { min-width: var(--ctl-h-md); }
body[data-surface="phone"] :is(.plan-mo-btn, .plan-mo-label),
body[data-surface="tablet"] :is(.plan-mo-btn, .plan-mo-label) {
  min-height: var(--ctl-h-md); background: transparent; background-image: none;
  border: 0; border-radius: 0; box-shadow: none; outline: 0;
}
body[data-surface="phone"] :is(.plan-mo-btn, .plan-mo-label):active,
body[data-surface="tablet"] :is(.plan-mo-btn, .plan-mo-label):active { transform: scale(.96); opacity: .78; }
@media (pointer: fine) {
  body[data-surface="phone"] :is(.plan-mo-btn, .plan-mo-label):focus-visible,
  body[data-surface="tablet"] :is(.plan-mo-btn, .plan-mo-label):focus-visible {
    outline: 2px solid var(--accent); outline-offset: 2px;
  }
}
@media (hover: hover) { .plan-mo-step:hover { background: var(--chip); } }
/* Tappable column heading (swaps Assigned⇄Activity in place). */
.mgcol-swap {
  cursor: pointer; text-decoration: none; border: 0; background: none; padding: 0; color: inherit; font: inherit;
  appearance: none; -webkit-appearance: none;
}
/* Touch: this SPAN swaps the Assigned⇄Activity column yet measured 19px tall —
 * a bare text line, outside the blanket button floor (v1.29.0 tapthru §1, the
 * worst target of the sweep). Honest growth: the header row gets taller on
 * phones; no negative-margin hit tricks (D-059's overlap trap). */
body[data-surface="phone"] .mgcol-swap,
body[data-surface="tablet"] .mgcol-swap {
  display: flex; flex-direction: column; align-items: stretch; justify-content: center;
  min-height: var(--ctl-h-md); width: 100%; gap: 1px;
}
/* Operator 2026-07-18 (iPhone screenshot): the touch floor turned this label into an inline-flex
 * box, which silently dropped the surface's text alignment — flex content sits at flex-start, so
 * "Activity ⇅" hung mid-header instead of over its amount column. Restore each surface's
 * alignment with justify-content (phone labels are right-aligned over their totals; tablet keeps
 * the centred base look), and seat the text at the box BOTTOM so it sits directly above its
 * amount like the untouched "Available" label beside it. The 44px box itself stays — honest
 * growth, no hit tricks (D-059). */
body[data-surface="phone"] .mgcol-swap { align-items: stretch; }
body[data-surface="tablet"] .mgcol-swap { align-items: stretch; }

/* Views bounce-in keyframe (kept — .reg-search reuses it). The phone/tablet
 * Views bottom-sheet itself was removed in punch 16 (operator 2026-07-12): the
 * filter pills render inline below Ready-to-Assign now, so .views-sheet/-tools/
 * -row/-check are gone. */
@keyframes views-sheet-in {
  from { transform: translateY(28px); opacity: .4; }
  to { transform: translateY(0); opacity: 1; }
}

/* Unconditional gap above the pill row (operator 2026-07-15, punch B5): the
 * .plan-rta + .plan-pills sibling rule above never matches the real DOM (see
 * comment there), so this reliably spaces the pills off the RTA banner /
 * month-toolbar row on every surface without depending on source order. */
.plan-pills { display: flex; gap: 6px; margin-top: 18px; margin-bottom: 12px; flex-wrap: wrap; }
.plan-pills .btn { min-height: var(--ctl-h-sm); }
body[data-surface="phone"] .plan-pills .btn,
body[data-surface="tablet"] .plan-pills .btn { min-height: var(--ctl-h-md); }
/* Phone/tablet: `.plan-pills` owns one menu trigger, so every saved/built-in
 * view remains reachable without a separate More control or a scrolling row. */
body[data-surface="phone"] .plan-pills,
body[data-surface="tablet"] .plan-pills {
  flex-wrap: nowrap; overflow: visible; min-width: 0;
  margin-left: 0; margin-right: 0; padding: 0;
}
body[data-surface="phone"] .plan-pills::-webkit-scrollbar,
body[data-surface="tablet"] .plan-pills::-webkit-scrollbar { display: none; }
body[data-surface="phone"] .plan-pills .plan-pill,
body[data-surface="tablet"] .plan-pills .plan-pill { flex: 0 0 auto; }
.plan-pillrow-touch { min-width: 0; max-width: 100%; }
.plan-pillrow-touch .plan-pills { flex: 1 1 auto; min-width: 0; overflow: hidden; }
/* Fill tone (Family B, 31/08/2026): the touch filter capsule matches its
 * sibling SPENT/UPCOMING lens on the on-theme LIGHT blurple rather than the
 * solid accent, so the two controls on this row read as one family. */
.plan-pillrow-touch .plan-filter-trigger {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  width: 100%; min-width: 0; min-height: 44px; padding-inline: 2rem;
  appearance: none; -webkit-appearance: none;
  border: 1px solid var(--hairline); border-radius: 999px;
  background: var(--btn-secondary-bg); color: var(--btn-secondary-ink); box-shadow: none;
  white-space: nowrap;
}
.plan-filter-select-wrap {
  position: relative; display: inline-flex; align-items: center; justify-self: start;
  width: max-content; max-width: 100%; min-width: 0;
}
.plan-filter-select {
  display: block; width: max-content; max-width: 100%; min-width: 7rem;
  min-height: var(--ctl-h-md); padding: .4rem 2rem .4rem .85rem;
  border: 1px solid var(--hairline); border-radius: 999px; background: var(--chip);
  color: var(--ink); font: inherit; font-weight: 600; appearance: none; -webkit-appearance: none;
  cursor: pointer;
}
.plan-filter-select-caret {
  position: absolute; inset-inline-end: .7rem; top: 50%; z-index: 1;
  display: inline-flex; align-items: center; justify-content: center;
  width: 1rem; height: 1rem; color: var(--ink-muted); pointer-events: none;
  transform: translateY(-50%);
}
.plan-filter-select-caret .ico { width: 100%; height: 100%; }
.plan-filter-select-label { display: none; }
.plan-filter-trigger-label { display: inline-block; line-height: 1; }
.plan-filter-trigger .ico {
  position: absolute; right: .75rem; width: 1rem; height: 1rem;
}
.plan-filter-overflow-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; text-align: left; }
.plan-filter-overflow-row > span { min-width: 0; white-space: normal; overflow-wrap: anywhere; }
.plan-filter-overflow-actions { border-top: 1px solid var(--hairline); margin-top: 6px; padding-top: 6px; }
/* Dailies configuration panel (item 9). */
.dailies-config { background: color-mix(in srgb, var(--accent) 6%, var(--card)); border: 1px solid var(--hairline); border-radius: 10px; padding: 12px 14px; margin-bottom: 12px; }
.dailies-lead { margin: 0 0 8px; }
.dailies-add { margin-top: 4px; }
/* Mini one-line variant (operator 2026-07-12 punch 16): blurb + small link on a
 * single wrapping row, no big button. */
.dailies-config-mini { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; padding: 8px 12px; }
.dailies-config-mini .dailies-lead { margin: 0; flex: 1 1 12rem; min-width: 0; }
.dailies-config-mini .dailies-add { margin: 0; flex: 0 0 auto; white-space: nowrap; }
/* min-width stops a short label ("All") from reading as a circular disc rather
 * than a pill — symmetric horizontal padding plus a floor wide enough that
 * even 3 letters render as a rounded RECTANGLE, never width==height (operator
 * 2026-07-15, punch B5). */
.plan-pill {
  padding: .3rem .9rem; min-width: 3.2rem; text-align: center;
  border-radius: 999px; background: var(--chip); color: var(--ink); border: 0;
  font: inherit; font-size: var(--fs-body); font-weight: 600; cursor: pointer;
}
.plan-pill.active { background: var(--accent); color: var(--accent-ink); }

/* The filter row's OUTER wrapper: the visible filter/menu control sits on the
 * left and the SPENT/UPCOMING lens remains pinned beside it on the right.
 *
 * `align-items: center` rather than stretch: the pills row can be taller than
 * the lens once it wraps on desktop, and a stretched lens would grow into a
 * tall slab instead of staying a pill. */
.plan-pillrow {
  display: flex; align-items: center; gap: 8px;
  margin-top: 18px; margin-bottom: 12px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-pillrow {
  margin-top: 14px; margin-bottom: 14px;
}
/* The filter container inside the wrapper owns no vertical margin of its own — the
 * wrapper carries the row's spacing now, so the two never double up. */
.plan-pillrow > .plan-pills { margin-top: 0; margin-bottom: 0; flex: 1 1 auto; min-width: 0; }
/* The filter container yields to the lens while retaining a small visual gap. */
body[data-surface="phone"] .plan-pillrow > .plan-pills,
body[data-surface="tablet"] .plan-pillrow > .plan-pills { margin-right: 0; padding-right: 8px; }

/* SPENT / UPCOMING lens (operator 04/08/2026) — the split pill itself.
 *
 * `flex: 0 0 auto` keeps it at its natural width while the filter takes the
 * remaining space rather than squeezing the toggle.
 *
 * `isolation: isolate` gives the sliding fill its own stacking context, so it
 * can sit under the two labels without needing a z-index on the whole row. */
.plan-lens {
  position: relative; isolation: isolate;
  display: inline-grid; grid-template-columns: max-content max-content; align-items: stretch; flex: 0 0 auto;
  padding: 0; border: 0; cursor: pointer;
  border-radius: 999px; background: var(--chip);
  font: inherit; font-size: var(--fs-micro); font-weight: 700;
  letter-spacing: normal; text-transform: none;
  min-height: var(--ctl-h-sm);
  /* A hairline keeps the unlit half legible against a card of the same tone in
   * dark mode, where --chip and --card sit close together. */
  box-shadow: inset 0 0 0 1px var(--hairline);
  /* Press feedback lives here so the control's OWN look changes on tap. Clip
   * the moving fill to the control's exact outer curve; the focus outline is
   * outside this child-paint boundary. */
  overflow: hidden;
  transition: transform var(--dur-fast) var(--ease-spring);
}
.plan-lens:active { transform: scale(.97); }
body[data-surface="phone"] .plan-lens,
body[data-surface="tablet"] .plan-lens { min-height: var(--ctl-h-md); }
/* Equal visible gaps: outer edge -> Spent, Spent -> Upcoming, Upcoming ->
 * outer edge.  Each inner side contributes half a gap; each outside edge owns
 * a full gap.  Desktop keeps its established compact segment padding.
 *
 * MEASURED, ipad mini portrait, 31/08/2026: 13.59 / 13.59 / 13.59 between the
 * three text edges. Making the two halves symmetric instead doubles the middle
 * gap to 27.19, so the whole gap must stay split across the boundary.
 *
 * WHERE the boundary falls is the part Family B reported. The off-state fill
 * is clipped to the SPENT cell, so the cell edge is the lit edge: with the gap
 * split evenly (.425/.425) the pill's left edge sat 13.59px from "Spent" while
 * the fill's right edge sat only 6.80px from it. Giving SPENT the WHOLE inner
 * gap and UPCOMING none moves that boundary to where the ink is centred — the
 * fill now ends 13.59px past "Spent", matching its lead-in, while the three
 * text gaps above are unchanged because the gap still totals .85rem. */
body[data-surface="phone"] .plan-lens-half,
body[data-surface="tablet"] .plan-lens-half { padding-inline: .425rem; }
body[data-surface="phone"] .plan-lens-spent,
body[data-surface="tablet"] .plan-lens-spent { padding-inline: .85rem; }
body[data-surface="phone"] .plan-lens-upcoming,
body[data-surface="tablet"] .plan-lens-upcoming { padding-inline: 0 .85rem; }

/* The moving selection. It is the ONLY animated element: the labels stay put
 * and merely change ink as the fill arrives under them.
 *
 * `left`/`right` rather than `transform: translateX`, because the indicator has
 * to TRAVEL AND STRETCH in one gesture — off, it covers the left half; on, it
 * spans the whole pill. A transform could slide it or scale it, but scaling
 * would squash the rounded ends into ellipses. Animating the two inset edges
 * keeps the corner radius constant at both ends of the motion, and because the
 * left edge is already at 0 in both states, the visible effect is the fill
 * GROWING rightwards over UPCOMING — exactly the slide the operator asked for.
 *
 * --ease-ios-rebound overshoots a touch on settle, which is what makes it read
 * as a physical switch rather than a fade. */
/* Fill tone (Family B, 31/08/2026): the lit half is the on-theme LIGHT
 * blurple, not the solid accent. --btn-secondary-bg is the existing pale
 * accent-over-card token (it already backs every enabled secondary button),
 * so light and dark stay paired with no new colour and no per-theme
 * redeclaration. Its companion --btn-secondary-ink carries the labels: white
 * accent-ink would be unreadable on a 12% fill. */
.plan-lens-fill {
  position: absolute; z-index: 0; top: 0; bottom: 0; left: 0; right: var(--plan-lens-fill-end, 0px);
  border-radius: 999px; background: var(--btn-secondary-bg);
  transition: right var(--dur-base) var(--ease-ios-rebound),
              left var(--dur-base) var(--ease-ios-rebound);
}
.plan-lens.on .plan-lens-fill { left: 0; right: 0; }

/* Both halves must be EXACTLY equal width, or the pill is not "split in half"
 * and the off-state fill stops short of the divider. `flex: 1 1 0` alone did
 * NOT do it: flex distributes free space from each item's CONTENT width, so the
 * longer word ("Upcoming", 97px) still finished wider than "Spent" (69px) and
 * the fill covered 48.2% rather than 50% — measured on pixel8-portrait,
 * 04/08/2026. `flex-basis: 50%` with `min-width: 0` sizes both halves off the
 * PILL instead of off their text, which is the only way to get a true half. */
.plan-lens-half {
  position: relative; z-index: 1;
  min-width: 0;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0 .85rem; white-space: nowrap;
  color: var(--ink);
  transition: color var(--dur-base) var(--ease-standard),
              opacity var(--dur-base) var(--ease-standard);
}
/* SPENT is lit in BOTH states — it is never deselected (the control is
 * "spent" vs "spent AND upcoming", not an either/or). Only UPCOMING changes. */
.plan-lens .plan-lens-spent { color: var(--btn-secondary-ink); }
.plan-lens .plan-lens-upcoming { opacity: .6; }
.plan-lens.on .plan-lens-upcoming { color: var(--btn-secondary-ink); opacity: 1; }

/* Desktop Plan geometry (v126): the month controls and the Recent Moves/lens
 * controls share one honest compact height and painted outline. The touch
 * rules above retain their 44px floor; this block is deliberately desktop-only
 * so phone/tablet hit targets and safe-area spacing do not change. */
body[data-surface="desktop"] .plan-header { margin-bottom: 8px; }
body[data-surface="desktop"] .plan-month { gap: 6px; align-items: center; }
body[data-surface="desktop"] .plan-month > .plan-mo-btn,
body[data-surface="desktop"] .plan-mo-label {
  box-sizing: border-box; min-height: var(--ctl-h-sm);
  border-radius: var(--radius-s); line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
}
body[data-surface="desktop"] .plan-month > .plan-mo-btn {
  min-width: var(--ctl-h-sm); padding: 0 .35rem;
}
body[data-surface="desktop"] .plan-mo-label { padding: 0 .6rem; }
body[data-surface="desktop"] .plan-mo-btn .ico { width: .9rem; height: .9rem; }
body[data-surface="desktop"] .plan-mo-caret { transform: translateY(-.1px); }
body[data-surface="desktop"] .plan-toolbar .link-btn {
  min-height: var(--ctl-h-sm); padding: .2rem .55rem;
  display: inline-flex; align-items: center; justify-content: center;
  line-height: 1; border-radius: var(--radius-s);
}
/* Keep the desktop Ready-to-Assign banner on the same compact control ruler as
 * its sibling Recent moves action.  The touch banner keeps its larger, honest
 * hit target; only the Win11 header chrome is tightened here (operator Image 4,
 * 26/08/2026). */
body[data-surface="desktop"] .plan-rta {
  box-sizing: border-box;
  height: var(--ctl-h-sm);
  min-height: var(--ctl-h-sm);
  max-height: var(--ctl-h-sm);
  padding: .3rem .9rem;
}
body[data-surface="desktop"] .plan-pillrow {
  margin-top: 10px; margin-bottom: 8px;
}
body[data-surface="desktop"] .plan-lens {
  min-height: var(--ctl-h-sm); border: 1px solid var(--hairline);
  border-radius: var(--radius-s); box-shadow: none;
}
body[data-surface="desktop"] .plan-lens-fill {
  border-radius: calc(var(--radius-s) - 3px);
}
body[data-surface="desktop"] .plan-lens-half {
  line-height: 1; padding: 0 .55rem; transform: translateY(-.75px);
}

/* Focus ring is pointer-fine only: iOS applies :focus-visible to a control that
 * was merely TAPPED, which would leave a ring stuck on the pill after every
 * toggle on the phone the operator actually uses. */
@media (pointer: fine) {
  .plan-lens:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
}

/* Reduced motion: same end state, no travel. The fill still moves position
 * instantly, which is the honest non-animated equivalent of a switch. */
@media (prefers-reduced-motion: reduce) {
  .plan-lens, .plan-lens-fill, .plan-lens-half { transition: none; }
}

.plan-layout { display: grid; grid-template-columns: 1fr; gap: 16px; }
body[data-surface="desktop"] .plan-layout { grid-template-columns: minmax(0, 1fr) 320px; align-items: start; }
/* At compact desktop widths the fixed summary rail steals enough room to clip
 * legitimate category names. Stack it below the table until both columns fit;
 * money columns stay single-line and names stay fully readable. */
@media (min-width: 900px) and (max-width: 1366px) {
  body[data-surface="desktop"] .plan-layout { grid-template-columns: minmax(0, 1fr); }
}

.plan-table { padding: 0; overflow-x: auto; }
.plan-row {
  display: grid; grid-template-columns: minmax(10rem, 2fr) 7rem 7rem 8rem;
  align-items: center; gap: 8px; padding: .4rem 14px; border-top: 1px solid var(--hairline);
  font-size: var(--fs-body); white-space: nowrap;
}
/* Same nowrap-without-clip trap as the register — and the same caveat: .plan-table
 * scrolls horizontally, so only the NAME cell (which is meant to ellipsis) is
 * clipped. The number cells keep their full width; a truncated figure is worse
 * than a scrollbar. */
.plan-row .c-cat, .plan-row .plan-cat-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.plan-head { border-top: 0; font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .06em; color: var(--ink-muted); font-weight: 600; position: sticky; top: 0; background: var(--card); z-index: 1; }
/* nowrap so a value NEVER breaks its sign onto its own line (operator 2026-07-15:
 * "win11 ensure no instance where the minus sign appears in one row above the
 * currency value itself"). fmtMoney glues the U+2212 to the digits; nowrap keeps
 * the whole token — sign + amount — on one line even in a tight column. */
.plan-row .c-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Header titles sit over their own columns (operator 04/08/2026: "how misaligned
 * (noncentered) the assigned/activity/available title headers are with the
 * columns below"). The three .c-num titles already inherit text-align:right from
 * the rule above — .plan-head IS a .plan-row, so the descendant selector matches
 * them. What broke the eye-line was the AVAILABLE column: its figure is wrapped
 * in .avail-pill, an inline-block carrying .6rem of its own side padding, so the
 * digits sit .6rem in from the track's right edge while the word "Available" sat
 * flush against it. Pad the header cell by the pill's own inset so the title's
 * last glyph lands on the same vertical as the digits beneath it. Assigned and
 * Activity need no inset — their cells hold bare text (the activity figure's
 * <button> is padding:0), so those two already line up exactly. */
.plan-head .c-num:last-child { padding-right: .6rem; }
.auto-amt, .autoassign-label, .avail-pill { white-space: nowrap; }

/* "+ Add category" row (operator 2026-07-15): a muted, full-width add affordance
 * closing each Plan group, YNAB-style. Token-only for dark/light parity. */
.plan-add-cat {
  cursor: pointer; color: var(--ink-muted); font-weight: 600;
  background: transparent; border: 0; width: 100%; text-align: left; font: inherit;
}
.plan-add-cat:focus-visible { color: var(--accent); }
@media (hover: hover) { .plan-add-cat:hover { color: var(--accent); } }
.plan-add-cat .c-cat, .plan-add-cat { display: flex; align-items: center; gap: 6px; }
/* Operator 2026-07-18 (iPhone screenshot): on the mobile plan this button also matches the
 * `.plan-mobile .plan-mrow` grid rule, whose display:grid won the cascade over the flex above —
 * so the + landed in the name track and "Add category" wrapped inside a 6rem amount column,
 * taking two lines. Higher-specificity flex override keeps it one row on every surface. */
.plan-mobile .plan-mrow.plan-add-cat { display: flex; align-items: center; gap: 6px; }
.plan-add-cat .ico { width: 1em; height: 1em; }
/* Divider between the target/snooze controls and the rename/hide actions. */
.plan-sheet-divider { height: 1px; background: var(--hairline); margin: 10px 0 2px; }

/* Starting-balance −/+ sign toggle (operator 2026-07-15): lets a negative
 * (owed) balance be entered on a decimal keypad that has no minus key. */
.acct-balance-wrap { display: flex; align-items: stretch; gap: 8px; }
.acct-balance-sign {
  flex: 0 0 auto; min-width: 2.4rem; font: inherit; font-weight: 700; cursor: pointer;
  border: 1px solid var(--hairline); border-radius: 10px; background: var(--chip); color: var(--ink);
}
.acct-balance-sign.neg { background: var(--neg); color: var(--neg-ink); border-color: transparent; }
.acct-balance-wrap .acct-balance-input, .acct-balance-wrap .rec-actual { flex: 1; min-width: 0; }
.acct-balance-hint { display: block; font-size: var(--fs-label); margin-top: 4px; }
/* Reconcile step 2 reuses the same sign toggle (Family B 2026-07-16). */
.rec-field .acct-balance-wrap { margin-top: 4px; }
.rec-sign-hint { display: block; margin-top: 4px; }
/* The ACTIVITY figure is a button (it opens that category's activity list), but
 * it must not LOOK like one at rest: the dotted underline it used to carry read
 * as a mis-rendered hyperlink on every row at once (operator 2026-07-29,
 * "bizarre underlining of ACTIVITY fields"). The affordance now appears only on
 * hover and keyboard focus, where it is asked for; at rest the column is plain
 * tabular figures like ASSIGNED beside it. */
.plan-activity-link { appearance: none; border: 0; border-bottom: 1px solid transparent; padding: 0; background: transparent; color: inherit; font: inherit; font-variant-numeric: tabular-nums; cursor: pointer; }
.plan-activity-link:focus-visible { color: var(--accent); border-bottom-color: var(--accent); }
@media (hover: hover) { .plan-activity-link:hover { color: var(--accent); border-bottom-color: var(--accent); } }
.plan-group { font-weight: 600; background: var(--subline-row); }
[data-theme="dark"] .plan-group { background: var(--surface-hover-soft); }
.plan-cat { cursor: pointer; }
.plan-cat:focus-visible { background: color-mix(in srgb, var(--accent) 6%, transparent); }
@media (hover: hover) { .plan-cat:hover { background: color-mix(in srgb, var(--accent) 6%, transparent); } }
.plan-cat .c-cat { padding-left: .8rem; font-weight: 400; } /* subcategories not bold (item 10) */
/* Collapsible group chevron (operator 2026-07-14 — Win11 parity with iOS). The
 * header is clickable; the caret rotates when the section is folded. */
.plan-group { cursor: pointer; user-select: none; }
.plan-group .c-cat { display: inline-flex; align-items: center; gap: 5px; }
.plan-group-caret { flex: 0 0 auto; display: inline-flex; color: var(--ink-muted); transition: transform var(--dur-base) var(--ease-standard); }
.plan-group-caret .ico { width: 1.05em; height: 1.05em; }
.plan-group.collapsed .plan-group-caret { transform: rotate(-90deg); }
.avail-pill { --plan-target-mark-size: 1.4em; display: inline-flex; align-items: baseline; gap: .2em; line-height: normal; border-radius: 999px; padding: .05rem .6rem; font-weight: 600; font-variant-numeric: tabular-nums; position: relative; }
.plan-target-icon {
  /* Legacy rows do not sit inside .avail-pill, so they need the same explicit
   * marker token here instead of falling back to an SVG's 300px intrinsic size. */
  --plan-target-mark-size: 1.4em;
  /* Match the amount's actual line box, not its nominal font size. This keeps
   * the target mark optically level with the Available figure across engines. */
  display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 var(--plan-target-mark-size); width: var(--plan-target-mark-size); height: var(--plan-target-mark-size);
  font-size: 1em; line-height: 1; vertical-align: middle;
  position: absolute; left: .6rem; top: 50%; transform: translateY(-50%);
}
.avail-pill:has(.plan-target-icon) { gap: 0; padding-left: calc(.6rem + var(--plan-target-mark-size) + .2em); }
.plan-target-icon .plan-target-mark { display: block; flex: 0 0 auto; width: 100%; height: 100%; vertical-align: 0; }
.plan-available-amount { display: inline-block; line-height: inherit; }
.avail-pill.pos-pill { background: var(--rta-fill); color: var(--rta-ink); }
.avail-pill.neg-pill { background: var(--neg-pill); color: var(--neg); }
.avail-pill.zero-pill { background: var(--chip); color: var(--ink-muted); }
/* Target-warning yellow/amber (mission 20/08/2026): an active, non-snoozed,
 * unfunded monthly/weekly/by-date target legitimately paints a POSITIVE
 * Available yellow rather than green — the pill tests whether the MONTH'S
 * refill is covered (rollover + assignment), not merely whether the raw
 * figure is non-negative. Token-only per CARDINAL rule 20; --amber already
 * carries the correct light/dark values. */
.avail-pill.warn-pill { background: color-mix(in srgb, var(--amber) 22%, var(--card)); color: var(--amber-ink); }
[data-theme="dark"] .avail-pill.warn-pill { color: var(--amber); }
/* NOW pill (item 7, operator 2026-07-15): the Add-Transaction Date row shows
 * this instead of the date string when draft.date is today. Blurple = the
 * existing --accent token, house pill shape from .avail-pill; sized to the
 * SAME line-height as the plain .field-value text it replaces (font-size:
 * var(--fs-body) at font-weight 700 sits the same baseline as fs-h3/600 in a
 * pill's tighter padding) so the row's vertical rhythm doesn't jump. Readable
 * foreground in both themes via --accent-ink, the token already used for text
 * on solid accent fills app-wide. */
.date-today-pill {
  display: inline-block; border-radius: 999px; padding: .18rem .7rem;
  background: var(--accent); color: var(--accent-ink);
  font-size: var(--fs-body); font-weight: 700; letter-spacing: .03em;
}
/* Balance Style "Differentiate Without Colour" removed entirely (operator
 * 2026-07-12 punch 16) — the data-balance-style outline rules are gone. */
.inline-assign { width: 6.5rem; text-align: right; font-variant-numeric: tabular-nums; padding: .15rem .4rem; }

.activity-modal { width: min(640px, 94vw); max-height: 76dvh; padding: 16px; display: flex; flex-direction: column; gap: 10px; }
.activity-modal .picker-head { justify-content: space-between; font-size: var(--fs-h3); }
.activity-subtitle { margin: 0; color: var(--ink-muted); }
.activity-table { overflow: auto; }
/* minmax(0, …) on every text-bearing track (operator 2026-07-14 truncation sweep).
 * A bare `1fr` track has an implicit `min-width: auto`, so it grows to fit its
 * longest word instead of letting the cell ellipsis — the grid then overflows and
 * the columns bleed into one another. minmax(0, …) is what lets the ellipsis fire. */
.activity-head, .activity-row { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, .9fr) minmax(0, 1.2fr) minmax(0, 1.35fr) minmax(0, .9fr); gap: 10px; align-items: center; padding: .45rem 0; border-top: 1px solid var(--hairline); }
.activity-head { border-top: 0; border-bottom: 1px solid var(--hairline); color: var(--ink-muted); font-size: var(--fs-label); text-transform: uppercase; }
.activity-row { font-size: var(--fs-body); }
.activity-row span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.activity-row span:last-child, .activity-head span:last-child { text-align: right; font-variant-numeric: tabular-nums; }
.activity-empty { margin: 1rem 0; text-align: center; }

.plan-panel { display: flex; flex-direction: column; gap: 12px; }
.panel-card h2 { font-size: var(--fs-value); }
.panel-row { display: flex; justify-content: space-between; gap: 10px; padding: .3rem 0; font-size: var(--fs-body); }
.panel-row + .panel-row { border-top: 1px solid var(--hairline); }

/* Plan -> Target info affordance (mission 20/08/2026 item 2): a small "i"
 * beside the Target heading; negligible resting height, hover/focus/tap
 * reveals a compact popover via the shared popover() chrome — no permanent
 * paragraph. */
.insp-target-head { display: flex; align-items: center; gap: 6px; }
.insp-target-head h2 { margin: 0; }
.target-info-btn {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 1.5em; height: 1.5em; padding: 0; border: 0; border-radius: 999px;
  background: transparent; color: var(--ink-muted); cursor: pointer;
}
.target-info-btn .ico { width: 1.05em; height: 1.05em; }
@media (hover: hover) { .target-info-btn:hover { color: var(--accent); } }
.target-info-btn:focus-visible { color: var(--accent); }
body[data-surface="phone"] .target-info-btn,
body[data-surface="tablet"] .target-info-btn { min-width: var(--ctl-h-md); min-height: var(--ctl-h-md); }
.target-info-pop { max-width: min(320px, 88vw); padding: 10px 12px; }
.target-info-pop p { margin: 0; }

/* Assign dropdown (Auto / Manually) */
.assign-modal .assign-tabs { display: flex; gap: 4px; }
.assign-tab {
  flex: 1; box-sizing: border-box; padding: .5rem; border-radius: var(--radius-s);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  color: var(--accent); font: inherit; font-weight: 600; cursor: pointer;
}
@media (hover: hover) {
  .assign-tab:not(.active):hover {
    background: color-mix(in srgb, var(--accent) 18%, transparent);
    border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  }
}
/* Selected tab stays SOLID accent so it out-reads the tinted rest state. */
.assign-tab.active { background: transparent; border-color: transparent; color: var(--accent-ink); }
.assign-body { margin-top: 4px; }
.auto-list { display: flex; flex-direction: column; }
.auto-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; width: 100%; text-align: left; background: none; border: 0; border-top: 1px solid var(--hairline); font: inherit; padding: .6rem .2rem; cursor: pointer; }
.auto-row:first-child { border-top: 0; }
@media (hover: hover) { .auto-row:hover { background: var(--chip); } }
.auto-amt { font-variant-numeric: tabular-nums; font-weight: 600; }
.auto-row-reset, .auto-row-reset .auto-amt {
  background: var(--card) !important;
  color: var(--neg) !important;
  box-shadow: none !important;
}
@media (hover: hover) { .auto-row-reset:hover { background: var(--chip) !important; } }

/* ---- iOS Auto-Assign mimic (operator 2026-07-12, screenshot-exact) ---- */
/* The mobile Assign menu leads with a full-width dark-green "⚡ Auto-Assign by…"
 * button. Solid deep green with white ink + a lightning glyph, matching YNAB. */
.assign-modal-mobile .autoassign-by {
  width: 100%; background: var(--aa-green); color: var(--aa-ink); font-weight: 700;
  font-size: var(--fs-h3); border-radius: 999px; padding: .95rem 1.2rem;
  display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
  margin-bottom: 12px;
}
.assign-modal-mobile .autoassign-by .ico { width: 1.2em; height: 1.2em; fill: currentColor; stroke: none; }
.assign-modal-mobile .autoassign-by:active { background: var(--aa-green); }
/* The expanded popover: a very light-green rounded card, centred title, and a
 * stack of grey rounded "mode: $amount" pills (screenshot proportions). */
.autoassign-sheet { background: var(--aa-green-soft); border-radius: 26px; padding: 22px 18px; }
.autoassign-title { text-align: center; font-size: var(--fs-h2); font-weight: 700; color: var(--ink); margin-bottom: 18px; }
.autoassign-list { display: flex; flex-direction: column; gap: 14px; }
.autoassign-pill {
  width: 100%; text-align: center; background: var(--aa-pill); color: var(--ink);
  border: 0; border-radius: 999px; padding: 1.1rem 1rem; cursor: pointer;
  font: inherit; font-size: var(--fs-h3); font-weight: 600;
}
.autoassign-pill:active { background: var(--aa-pill); }
.autoassign-label { font-variant-numeric: tabular-nums; }
.manual-form { display: flex; flex-direction: column; gap: 10px; }
.manual-category-list { max-height: min(38dvh, 24rem); border: 1px solid var(--hairline); border-radius: var(--radius-s); }
.manual-category-row { width: 100%; }
.manual-selection { min-height: 1.3em; font-weight: 600; }
.manual-selection.error, .auto-funding-status.error { color: var(--neg); }
.manual-row { display: flex; justify-content: space-between; align-items: center; gap: 10px; padding: .4rem 0; border: 0; border-top: 1px solid var(--hairline); background: none; font: inherit; color: inherit; width: 100%; text-align: left; cursor: pointer; }
.manual-row:first-child { border-top: 0; }
.manual-amt { width: 8rem; text-align: right; }
.manual-to-val.error { color: var(--neg); }
.auto-funding-sheet { max-width: min(520px, 94vw); }
.auto-funding-summary { margin: 0; }
.auto-funding-sources { display: flex; flex-direction: column; gap: 4px; max-height: 38dvh; overflow-y: auto; }
.auto-funding-source { border: 1px solid var(--hairline); }
.auto-funding-source .cover-src-left { font-size: var(--fs-label); }
.auto-funding-add { margin-top: 4px; }
.auto-funding-status { margin: 0; }
.auto-funding-apply:disabled { opacity: .5; cursor: default; }

/* Plan mobile/tablet list (§7) — matches the YNAB Plan screenshot (IPP/IPL).
 * The list is a full-bleed white card sitting on the cream canvas. Each group
 * header and category row shares ONE grid template so the value columns line up
 * perfectly down the screen: a flexible name column, then N fixed value columns
 * (right-aligned). N is 3 on iPad (Assigned · Activity · Available) or 2 on the
 * iPhone (a toggled column + Available). The template is switched by the card's
 * data-cols attribute. */
.plan-mobile { padding: 0; overflow: hidden; background: var(--plan-row-bg); }
/* First two value columns widened a touch (5.4→5.9 / 5.6→5.9rem, operator
 * 2026-07-13): the centred 1.32rem "Assigned"/"Activity" labels (operator's own
 * size choice, 2026-07-12) were ~2px wider than their old columns and bled into
 * the neighbour on iPad. Widening the columns keeps the chosen label size and
 * removes the bleed; the name column (minmax 1fr) still has ample room on tablet. */
.plan-mobile[data-cols="3"] .plan-mgroup,
.plan-mobile[data-cols="3"] .plan-mrow { grid-template-columns: minmax(0, 1fr) 6.2rem 6.2rem 7.2rem; }
.plan-mobile[data-cols="2"] .plan-mgroup,
.plan-mobile[data-cols="2"] .plan-mrow { grid-template-columns: minmax(0, 1fr) 6rem 6.4rem; }
/* Reorder mode prepends a grip handle as the FIRST grid child of every group /
 * category row (mobileGroupHeader / mobileCategoryRow). Without a matching
 * leading track the grip stole the name's 1fr column and shoved the name into a
 * narrow value column — a big blank gap on the left and the name truncated
 * (operator 2026-07-14: "massive blank space to the left of the cat/subcat name
 * and truncates it badly"). Give reorder mode an extra auto grip track up front
 * so the name keeps its full 1fr and everything stays aligned. */
.plan-mobile.plan-reordering[data-cols="3"] .plan-mgroup,
.plan-mobile.plan-reordering[data-cols="3"] .plan-mrow { grid-template-columns: auto minmax(0, 1fr) 6.2rem 6.2rem 7.2rem; }
.plan-mobile.plan-reordering[data-cols="2"] .plan-mgroup,
.plan-mobile.plan-reordering[data-cols="2"] .plan-mrow { grid-template-columns: auto minmax(0, 1fr) 6rem 6.4rem; }
/* Phone reorder mode is about NAMES: the grip column squeezed the value cells
 * until amounts clipped ("−$1,583.36" at 69px) and long group names truncated.
 * The numbers aren't actionable mid-drag — drop them and give names the row. */
body[data-surface="phone"] .plan-mobile.plan-reordering .mrow-cols,
body[data-surface="phone"] .plan-mobile.plan-reordering .mgroup-cols { display: none; }
body[data-surface="phone"] .plan-mobile.plan-reordering .plan-mgroup,
body[data-surface="phone"] .plan-mobile.plan-reordering .plan-mrow { grid-template-columns: auto minmax(0, 1fr); }
/* Category names get every spare pixel (operator 2026-07-14: "display as much
 * category name as possible… a slight font-size reduction here is authorised").
 * The value columns above were trimmed to hand their width to the name column,
 * the row gap tightened, and the name font stepped down one notch — together
 * these show markedly longer names before the ellipsis bites. */
.plan-mobile .plan-mgroup,
.plan-mobile .plan-mrow { gap: 6px; }
/* (name font shrink lives with the phone-surface rules below, where it can win
 * the cascade against body[data-surface="phone"] .mrow-name.) */

/* Group header band: collapsing chevron + name on the left; a stacked
 * label-over-total cell per value column on the right. */
.plan-mgroup {
  display: grid; align-items: end; gap: 8px; padding: .55rem 14px .5rem;
  background: var(--plan-group-bg); cursor: pointer; user-select: none;
}
.plan-mgroup + .plan-swipe .plan-mrow { border-top: 0; }
/* MECHANISM of the "Discretionar…" clip (operator 2026-07-11): text-overflow
 * only works on a block wrapping its own text — on the old inline-FLEX name the
 * ellipsis silently no-oped and the text hard-clipped. The name is now a flex
 * of [caret][.mgroup-txt], and the ellipsis lives on the inner text span. */
.mgroup-name { display: flex; align-items: center; gap: 4px; font-weight: 700; font-size: var(--fs-value); color: var(--ink); min-width: 0; align-self: center; }
.mgroup-txt { min-width: 0; overflow: visible; white-space: normal; line-height: 1.2; }
.mgroup-caret { flex: 0 0 auto; display: inline-flex; color: var(--ink-muted); transition: transform var(--dur-base) var(--ease-standard); }
.mgroup-caret .ico { width: 1.2em; height: 1.2em; }
.plan-mgroup.collapsed .mgroup-caret { transform: rotate(-90deg); }
.mgroup-cols { display: contents; }
/* Column cells stretch so the LABEL can centre over its column while the TOTAL
 * stays right-aligned with the category-row numbers beneath it. */
.mgcol { display: flex; flex-direction: column; align-items: stretch; gap: 1px; min-width: 0; }
/* Column labels doubled in size and centred (operator 2026-07-12: "double
 * existing size and CENTRED"). Was .66rem right-aligned. */
/* These two already bled into the neighbouring column on iPad once (see the note
 * above); the fix then was a wider column, which only moves the cliff. Clip them. */
.mgcol-label { font-size: var(--fs-value); font-weight: 600; color: var(--ink-muted); white-space: nowrap; text-align: center; }
.mgcol-total { font-size: var(--fs-body); font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; white-space: nowrap; text-align: right; }
.mgcol-total.neg { color: var(--ink); }

/* ---- Plan drag-drop reorder (operator 2026-07-14) ---- */
/* While a GROUP is being dragged, every subcategory row hides so only the group
 * headers remain — the operator reorders sections cleanly, then the rows return
 * to their prior collapse state on drop. */
.plan-mobile.plan-drag-collapsed .plan-swipe { display: none; }
/* The lifted element (group header or row) reads as picked-up. Tokens only, so
 * dark/light parity holds. */
.plan-dragging {
  opacity: .92; box-shadow: 0 6px 18px var(--shadow, rgba(0,0,0,.28));
  position: relative; z-index: 3; background: var(--card);
}
/* The group a row would drop INTO when crossing groups. */
.plan-mgroup.drop-target,
.plan-row.plan-group.drop-target { box-shadow: inset 0 0 0 2px var(--accent); }

/* ---- Plan reorder mode (operator 2026-07-14) ---- */
/* Touch Plan controls share the title row in normal document flow. They remain
 * at the top on arrival, then scroll away with the rest of the screen. */
.plan-listctl {
  position: static; height: 44px; display: flex; gap: 0; align-items: center;
}
.plan-touch-toolbar { display: flex; align-items: center; flex: 0 0 auto; }
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-route-title {
  display: flex; align-items: center; gap: 0; width: 100%; margin-bottom: .5rem;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-route-title .screen-title {
  flex: 0 0 auto; min-width: 0; margin: 0; padding-right: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-route-title .plan-title-filter {
  position: absolute; left: 42%; transform: translateX(-50%);
  width: auto; min-width: 34px; margin: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-route-title .plan-title-filter .plan-pills { width: auto; }
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-route-title .plan-title-filter .plan-filter-trigger {
  width: auto; min-width: 34px; min-height: var(--ctl-h-md); padding: 0 9px;
  background: transparent !important; border: 0 !important; color: var(--accent) !important;
  box-shadow: none !important; font-size: var(--fs-micro);
  font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-route-title .plan-title-filter .plan-filter-trigger .ico { display: none; }
.track-replace-notice {
  display: flex; align-items: center; justify-content: space-between; gap: .75rem;
  margin: 0 0 .75rem; padding: .7rem .8rem; border: 1px solid color-mix(in srgb, var(--flag-purple) 32%, var(--hairline));
  border-radius: var(--radius); background: color-mix(in srgb, var(--flag-purple) 8%, var(--card)); color: var(--ink);
}
.track-replace-notice-close {
  flex: 0 0 auto; padding: .35rem .6rem; border: 0; border-radius: 999px;
  background: color-mix(in srgb, var(--flag-purple) 16%, var(--chip)); color: var(--flag-purple); font: inherit; font-weight: 700;
}
/* Plan's title row is flex-start by default (unlike the generic
 * .route-header-row, which is space-between) — the first of
 * .route-extra-actions/.route-header-actions pushes to the right edge so the
 * fixed rail lands in the SAME visual position as every other route. */
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-route-title > .route-extra-actions,
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-route-title > .screen-title + .route-header-actions {
  margin-left: auto;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-touch-toolbar :is(.app-version-tag, .privacy-eye) {
  position: static; inset: auto; flex: 0 0 auto;
}
.plan-ctl-btn {
  width: 44px; height: 44px; flex: 0 0 44px;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--ink-muted);
  background: transparent; background-image: none; border: 0; border-radius: 0;
  padding: 0; cursor: pointer; box-shadow: none; outline: 0;
  transition: transform var(--dur-fast) var(--ease-spring), color var(--dur-fast) var(--ease-standard);
}
/* Plan's static-flow chromatic-aberration controls must contain their own
 * shifted pseudo layers; fixed/absolute hosts keep their existing positions. */
.plan-listctl .plan-ctl-btn.has-ca { position: relative; }
.plan-ctl-btn .ico { width: 20px; height: 20px; }
@media (hover: hover) { .plan-ctl-btn:hover { color: var(--ink); } }
body[data-surface="phone"] .plan-ctl-btn:active,
body[data-surface="tablet"] .plan-ctl-btn:active { transform: scale(.9); }
@media (pointer: fine) {
  .plan-ctl-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
}
/* Reorder toggle lit through accent ink, never a painted disc. */
.plan-ctl-reorder.on {
  background: transparent; border-color: transparent; color: var(--accent);
}
/* The iOS-style grip handle: two faint horizontal bars. Hidden until reorder
 * mode adds it to the row; sits at the very start of the name cell. */
.plan-grip {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 1.5em; margin-right: 2px; color: var(--ink-muted); opacity: .5;
  cursor: grab; touch-action: none;
}
.plan-grip .ico { width: 1.25em; height: 1.25em; }
.plan-grip:active { cursor: grabbing; }
/* While reordering: no text selection, no accidental long-press callouts, and
 * the whole list disables the assign affordance (rows read as tiles, not buttons). */
.plan-reordering { user-select: none; -webkit-user-select: none; }
.plan-reordering .plan-mrow,
.plan-reordering .plan-row.plan-cat,
.plan-reordering .plan-mgroup,
.plan-reordering .plan-row.plan-group { cursor: default; }
/* Desktop drag: a group being dragged flattens its rows away, mirroring mobile. */
.plan-table.plan-drag-collapsed .plan-row.plan-cat { display: none; }

/* Category row: name (flexes + ellipsizes) then the value cells. */
.plan-mrow {
  display: grid; align-items: center; gap: 8px; width: 100%; text-align: left;
  background: var(--plan-row-bg); border: 0; border-top: 1px solid var(--hairline);
  font: inherit; padding: .62rem 14px; cursor: pointer;
}
.mrow-name { min-width: 0; font-weight: 500; font-size: var(--fs-value); color: var(--ink); overflow: visible; white-space: normal; line-height: 1.2; }
.mrow-snz { font-size: var(--fs-label); font-weight: 600; color: var(--snooze); margin-left: .3rem; }
.plan-mrow.snoozed .mrow-name { color: var(--ink-muted); }
.mrow-cols { display: contents; }
.mcell { justify-self: end; font-variant-numeric: tabular-nums; white-space: nowrap; }
.mcell-num { font-size: var(--fs-value); font-weight: 600; color: var(--ink); }
.mcell-avail { display: inline-flex; align-items: center; gap: 4px; }
.plan-mrow .avail-pill { font-size: var(--fs-body); padding: .12rem .62rem; }

/* Snooze Target swipe action — periwinkle, revealed by a left-swipe. Reuses the
 * shared .swipe-wrap / .swiped machinery; the row slides to uncover one action. */
.plan-swipe { background: var(--plan-row-bg); }
/* Snooze button keeps its wider 9rem (the label is long) — the shared 4.6rem
 * width above is for the two-button Spending strip; min-width wins here. Its own
 * padding restores the roomier 1rem the shared rule tightened to .5rem. */
.plan-swipe .swipe-btn.swipe-snooze { background: var(--snooze); width: 9rem; min-width: 9rem; padding: 0 1rem; }
/* Vertical clip fix (operator 2026-07-14: "Snooze Target truncated along the
 * bottom"). The actions strip inset its top/bottom by 4px; on a short plan row
 * that squeezed the pill so its lower edge/text clipped. Let the strip fill the
 * row height and give the button a floor so it always shows in full. */
.plan-swipe .swipe-actions { top: 4px; bottom: 4px; align-items: stretch; }
.plan-swipe .swipe-btn.swipe-snooze { min-height: 0; }
.plan-swipe .plan-mrow { position: relative; z-index: 1; transition: transform var(--dur-base) var(--ease-spring); }
/* Slide far enough to fully clear the snooze button (min 9rem + its 6px right
 * inset) so its leftmost text is never clipped by the wrapper (operator
 * 2026-07-12). Pairs with the right-origin scale on .swipe-btn. */
.plan-swipe.swiped .plan-mrow { transform: translateX(-9.8rem); }

/* (.plan-coltoggle removed 2026-07-11 — the header toggle pill is gone; the
 * column heading itself and the ••• menu carry the Assigned⇄Activity swap.) */
.plan-empty { padding: 1rem 14px; }

/* Phone: BACK TO ONE ROW (operator 2026-07-12, reverses the 2-row stack). The
 * category / group name flexes and ELLIPSISES on the same row as its value cells;
 * the fixed value columns are tightened (4.8rem + 7rem above) so the name keeps
 * as much width as possible. Truncation with "…" is accepted on best-effort —
 * maximising the visible name is the goal, not a full wrap. */
body[data-surface="phone"] .mcell-num { font-size: var(--fs-body); }
body[data-surface="phone"] .plan-mrow .avail-pill { font-size: var(--fs-body); padding: .1rem .5rem; }
body[data-surface="phone"] .mgcol-total { font-size: var(--fs-body); }
/* Column headings: small label sitting over its value column (operator
 * 2026-07-12 — the big 1.24rem heading stole width from the name). */
body[data-surface="phone"] .mgcol-label { font-size: var(--fs-label); text-align: right; }
body[data-surface="phone"] .mgcol { align-items: stretch; }
/* Slight name shrink to fit more category text (operator 2026-07-14: "a slight
 * font-size reduction here is authorised"). Applied on the highest-specificity
 * phone rule so it actually wins the cascade. */
body[data-surface="phone"] .mrow-name { font-size: calc(var(--fs-value) * 0.92); }
body[data-surface="phone"] .mgroup-name { font-size: calc(var(--fs-value) * 0.94); align-self: center; }
body[data-surface="phone"] .plan-mrow { padding: .58rem 14px; }

/* Plan popover overlay */
.plan-pop-overlay { position: fixed; inset: 0; z-index: 60; }

/* ---------- reflect ---------- */
.reflect-tabs { display: flex; gap: 4px; margin-bottom: 12px; flex-wrap: wrap; }
.reflect-tab { padding: .35rem .8rem; border-radius: 999px; background: var(--chip); color: var(--ink); font-size: var(--fs-body); font-weight: 600; }
.reflect-tab.active { background: var(--accent); color: var(--accent-ink); }

/* shared control bar */
.reflect-controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 14px; }
.reflect-pill { padding: .4rem .9rem; border-radius: 999px; border: 1px solid var(--hairline); background: var(--card); color: var(--ink); font: inherit; font-size: var(--fs-body); font-weight: 600; cursor: pointer; }
@media (hover: hover) { .reflect-pill:hover { background: var(--chip); } }
.reflect-pill.active { background: var(--accent); color: var(--accent-ink); border-color: transparent; }

/* ---------- trailing-spend watch (Reflect top + Spending Report) ---------- */
.trailing-report { margin: 0 0 20px; }
.trailing-h { font-size: var(--fs-value); margin: 0 0 10px; }
/* Heading row: title + collapsible-blurb (i) button (operator 2026-07-14). */
.trailing-head { display: inline-flex; align-items: center; gap: 6px; min-width: 0; max-width: 100%; }
.trailing-head .trailing-h {
  flex: 0 1 auto; min-width: 0; margin: 0; overflow: hidden;
  overflow-wrap: anywhere; text-overflow: ellipsis;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
}
body[data-surface="desktop"] .trailing-head .trailing-h {
  display: block; white-space: nowrap; text-overflow: ellipsis;
}
/* .trailing-info (the old touch "i") removed (mission 20/08/2026 item 5B):
 * fully replaced by .trailing-mixer below, no remaining JS reference. */
/* Mixer (mission 20/08/2026 item 5B): replaces the touch "i" — same disc
 * shape/hit area, opens ONE panel with both the explanatory text and every
 * display control, so none of it costs Home page height at rest. */
.trailing-mixer {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 1.7em; height: 1.7em; padding: 0; border: 0; border-radius: 999px;
  background: transparent; color: var(--ink-muted); cursor: pointer;
}
body[data-surface="phone"] .trailing-mixer,
body[data-surface="tablet"] .trailing-mixer { min-width: var(--ctl-h-md); min-height: var(--ctl-h-md); }
.trailing-mixer .ico { width: 1.15em; height: 1.15em; }
.trailing-mixer.on { color: var(--accent); }
@media (hover: hover) { .trailing-mixer:hover { color: var(--accent); } }
.trailing-mixer-panel { display: flex; flex-direction: column; gap: 10px; width: min(420px, 92vw); }
.trailing-mixer-blurb { margin: 0; }
.trailing-mixer-panel .trailing-controls { flex-direction: column; align-items: stretch; gap: 12px; }
/* Mode & average is a peer display toggle, not a primary CTA. Match the
 * compact desktop chart-control ruler; touch restores the 44px hit area below. */
.reflect-pill.trailing-guides-toggle {
  min-height: var(--ctl-h-sm); padding: .3rem .7rem; border: 1px solid var(--hairline);
  border-radius: 999px; background: var(--chip); color: var(--ink-muted);
  font-size: var(--fs-label); font-weight: 600;
}
.reflect-pill.trailing-guides-toggle.active {
  border-color: var(--accent); background: var(--card); color: var(--accent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 20%, transparent);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .reflect-pill.trailing-guides-toggle {
  min-height: var(--ctl-h-md);
}
/* The blurb collapses to nothing on touch until the (i) is tapped; when shown it
 * keeps its normal top margin. Desktop never gets .collapsed, so it's inline. */
.trailing-blurb { margin: 8px 0 0; }
.trailing-blurb.collapsed { display: none; }
.trailing-controls { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; margin-bottom: 12px; }
.trailing-pills { display: inline-flex; gap: 6px; }
.trailing-period-controls { display: inline-flex; align-items: center; gap: 8px; min-width: 0; flex-wrap: wrap; }
.trailing-slider { display: inline-flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1 1 200px; }
.trailing-slider-val { font-size: var(--fs-label); color: var(--ink-muted); font-weight: 600; }
.trailing-range { width: calc(100% - 2px); max-width: calc(100% - 2px); min-width: 0; margin-inline: 1px; box-sizing: border-box; accent-color: var(--accent); cursor: pointer; }
/* minmax(0, 1fr) (not plain 1fr): a bare 1fr track has an implicit
 * min-width:auto, so a wide child (the chart SVG's intrinsic width) forced the
 * card past the viewport and the right-hand figure got clipped. minmax(0,…)
 * lets the track — and the card — shrink to the available width. */
.trailing-list { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; }
body[data-surface="desktop"] .trailing-list { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
/* Home "Spending watch" tile (operator 2026-07-12): its charts pack as many
 * per row as the monitor width comfortably allows, wrapping to fewer as it
 * narrows, instead of a fixed 2×2. auto-fit + minmax(…) fills each row with
 * whole cards and folds the rest to the next row.
 *
 * Mission 20/08/2026 corrective: the old 260px floor let a 4K monitor (the
 * operator's own Dell 32" 4K UHD, CARDINAL rule 17) pack 6+ cards across —
 * the 640-unit SVG viewBox then scaled its "always body-sized" node/guide
 * text down into genuinely miniature labels, the opposite of the visual
 * brief. A 380px floor caps a wide desktop at a responsive 2-3-across grid
 * (never a single huge full-width card, never unbounded packing) so a
 * card's chart stays comfortably readable at its rendered size instead of
 * shrinking to fit more columns. */
body[data-surface="desktop"] .reflect-tile[data-tile-id="watch"] .trailing-list {
  grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
}
/* Bar/Graph + Separate/Combined segmented controls (operator 2026-07-17).
 * Wraps on a phone rather than squeezing; hidden group segment collapses out of
 * the flow entirely ([hidden] beats the flex display). */
.trailing-modes { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
/* ONE segmented rail (mission 20/08/2026 item 5C — supersedes Mission 3A's
 * "fully transparent container, no shared capsule" design, which read as TWO
 * separate floating buttons rather than one control with two options, per
 * the operator's screenshot). The rail itself now carries the shared outer
 * boundary (card face + hairline), the two options sit adjacent with NO gap
 * between them, the selected segment gets a solid flat --accent fill with
 * --accent-ink text, and the unselected segment shows the card/surface face
 * with normal ink text (never literal white — token-derived, so dark mode's
 * unselected face is the theme's own card token). No sliding indicator/ghost
 * pill — each button still paints its own state directly (global rule 26),
 * just against a rail that is visibly ONE control. */
.trailing-seg {
  position: relative; display: inline-flex; gap: 0;
  /* The shared `.segmented-rail` owner below wins the final paint; this
   * historical baseline remains for legacy geometry guards. */
  background: var(--card); border: 1px solid var(--hairline); border-radius: 999px;
  overflow: hidden; /* keeps a selected segment's fill inside the rail's rounded ends */
}
.trailing-seg[hidden] { display: none; }
.trailing-seg-btn {
  position: relative; z-index: 1; border: 0; background: transparent; color: var(--ink); cursor: pointer;
  font: inherit; font-size: var(--fs-label); font-weight: 600;
  padding: .3rem .7rem; white-space: nowrap;
  transition: color var(--dur-fast) ease, background var(--dur-fast) ease, transform var(--dur-fast) var(--ease-ios-rebound);
}
/* Selected = solid flat --accent fill, contrasting ink; controls stay flat everywhere. */
.trailing-seg-btn.sel {
  background: transparent;
  color: var(--accent-ink); box-shadow: none;
}
.trailing-seg-btn:active { transform: scale(.94); }
@media (hover: hover) { .trailing-seg-btn:not(.sel):hover { color: var(--ink); background: var(--surface-hover); } }
@media (prefers-reduced-motion: reduce) {
  .trailing-seg-btn { transition: none; }
}
body:is([data-surface="phone"], [data-surface="tablet"]) .trailing-seg-btn { min-height: 44px; min-width: 44px; }
/* Combined graphs carry up to 3 lines + a legend, so they need the full width
 * even on a wide monitor — override the auto-fit tiling used for bar cards. */
.trailing-list.trailing-collapsed { grid-template-columns: minmax(0, 1fr); }
body[data-surface="desktop"] .trailing-list.trailing-collapsed,
body[data-surface="desktop"] .reflect-tile[data-tile-id="watch"] .trailing-list.trailing-collapsed {
  grid-template-columns: minmax(0, 1fr);
}
.trailing-legend { margin-top: 6px; }
/* Mission 20/08/2026 corrective: the v1.190/v1.191 revamp shipped a card that
 * still read as a faint bordered spreadsheet cell — thin border, no surface
 * separation, header carrying only the watch name. Restrained elevation (a
 * touch more than the base .card shadow) makes the chart command the card
 * without frosted-glass/heavy shadow. */
.trailing-card { padding: 16px 18px; min-width: 0; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06); }
body[data-surface="desktop"] .trailing-card { box-shadow: none; }
[data-theme="dark"] .trailing-card { box-shadow: 0 1px 3px rgba(0, 0, 0, 0.22); }
[data-theme="dark"] body[data-surface="desktop"] .trailing-card { box-shadow: none; border-color: color-mix(in srgb, var(--hairline) 140%, var(--ink) 6%); }
.trailing-card-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; min-width: 0; margin-bottom: 4px; }
.trailing-name { flex: 1 1 auto; max-width: 100%; font-weight: 600; display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding-top: .15rem; }
/* Header right-hand cluster: prominent this-month figure, a quiet "spent"
 * caption immediately beside it, and the active guide summary beneath — the
 * hierarchy restated from the graphCard()/watchCard() header restructure
 * (mission 20/08/2026), so the reference figures live here instead of
 * floating over the plotted nodes. */
.trailing-head-figure { flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; gap: 1px; text-align: right; }
/* "$405.00 spent" on ONE line (mission 20/08/2026 item 5 — corrects v1.192's
 * separate uppercase "SPENT" row beneath the amount). */
.trailing-head-amtrow { display: flex; align-items: baseline; gap: 5px; white-space: nowrap; }
.trailing-thismonth { color: var(--item-color, var(--accent)); font-size: var(--fs-h2); font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; line-height: 1.15; }
.trailing-spent-lbl { font-size: var(--fs-label); text-transform: none; letter-spacing: normal; white-space: nowrap; }
.trailing-guide-summary { white-space: nowrap; margin-top: 1px; }
.trailing-chart { margin: 10px 0 6px; }
.trailing-svg { width: 100%; height: auto; overflow: visible; }
.trailing-avgline { stroke: var(--ink-muted); stroke-width: 1.5; stroke-dasharray: 5 4; opacity: .8; }
.trailing-avglabel { font-size: 10px; fill: var(--ink-muted); }
.mode-line { stroke: var(--accent); stroke-width: 1.5; stroke-dasharray: 2 3; opacity: .75; } /* mode dotted line (item 7) */
.trailing-modelabel { font-size: 10px; fill: var(--accent); } /* mode label (item 7) */
.trailing-track { font-size: var(--fs-body); line-height: 1.4; }
.trailing-track .over, .trailing-track strong.over { color: var(--neg); }
.trailing-track .under, .trailing-track strong.under { color: var(--pos); }
.trailing-proj { font-weight: 600; }
.reflect-export { margin-left: auto; background: var(--accent); color: var(--accent-ink); border-color: transparent; }
@media (hover: hover) { .reflect-export:hover { filter: brightness(1.06); } }

/* layout: donut card + ranked list side by side on wide screens */
.reflect-cols { display: grid; grid-template-columns: 1fr; gap: 14px; }
body[data-surface="desktop"] .reflect-cols { grid-template-columns: minmax(280px, 360px) 1fr; }
.reflect-donut-card { display: flex; flex-direction: column; align-items: center; gap: 10px; }
.reflect-toggle { display: inline-flex; align-self: center; border: 1px solid var(--hairline); border-radius: 999px; overflow: hidden; }
.reflect-seg { padding: .3rem .9rem; background: var(--card); border: 0; font: inherit; font-size: var(--fs-body); font-weight: 600; color: var(--ink-muted); cursor: pointer; }
.reflect-seg.active { background: transparent; color: var(--accent-ink); }

/* donut svg */
.reflect-donut { width: 100%; max-width: 320px; height: auto; overflow: visible; }
.donut-label { font-size: 10px; fill: var(--ink-muted); }
.donut-center-label { font-size: 11px; fill: var(--ink-muted); }
.donut-center-amt { font-size: 18px; font-weight: 700; fill: var(--ink); }

/* ranked list */
.reflect-ranked h2 { margin: 0 0 10px; font-size: var(--fs-value); }
.rank-row { display: grid; grid-template-columns: 12px minmax(0, 1fr) 90px auto; align-items: center; gap: 8px; padding: .32rem 0; border-top: 1px solid var(--hairline); }
.rank-row:first-of-type { border-top: 0; }
.rank-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--item-color, var(--accent)); }
.rank-name { font-size: var(--fs-body); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rank-bar { height: 8px; background: var(--chip); border-radius: 4px; overflow: hidden; }
.rank-bar-fill { display: block; height: 100%; border-radius: 4px; background: var(--item-color, var(--accent)); }
.rank-amt { font-variant-numeric: tabular-nums; font-weight: 600; font-size: var(--fs-body); text-align: right; }

/* stat row */
.reflect-stats { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
body[data-surface="desktop"] .reflect-stats { grid-template-columns: repeat(4, 1fr); }
.stat-cell { display: flex; flex-direction: column; gap: 2px; }
.stat-label { font-size: var(--fs-label); color: var(--ink-muted); text-transform: uppercase; letter-spacing: .02em; }
.stat-value { font-size: var(--fs-value); font-weight: 700; }

/* headline rows (trends / net worth) */
.reflect-headline-row { display: flex; flex-wrap: wrap; gap: 28px; }
.nw-subrow { display: flex; flex-wrap: wrap; gap: 20px; align-items: baseline; width: 100%; margin-top: 4px; }
.nw-stat { font-size: var(--fs-value); font-weight: 700; }
.nw-stat em { font-style: normal; color: var(--ink-muted); font-weight: 600; font-size: var(--fs-body); margin-right: 4px; }
/* Home Net Worth tile (D-043): §6 wants Assets/Debts labels in the chart's own
 * colours (accent bars = assets, red bars = debts) so the legend is implicit. */
.nw-stat .nw-lbl-asset { color: var(--accent); }
.nw-stat .nw-lbl-debt { color: var(--neg); }
.reflect-mcard-networth .mcard-total.neg { color: var(--neg); }

/* svg charts */
.reflect-bars { width: 100%; height: auto; }
/* DESKTOP CHART CAP (operator 2026-07-17: "on win11 both home and reports tend
 * to suffer from oversize charts and graphs — especially report"). Every chart
 * is a 640-unit viewBox at width:100%, and desktop .content is `max-width:none`
 * (app.css:249) — so on a 2560px monitor a single chart stretched to ~2400px,
 * a 3.7× scale. Everything sized in viewBox units (bars, the watch's rotated
 * value labels) inflated with it, while the CSS-px node labels stayed put, so
 * the charts read as cartoonishly large and mismatched. Capping the PAINTED
 * width keeps the scale near 1× where both label systems were designed to sit.
 * Charts stay left-aligned with the card's text rather than centred adrift. */
body[data-surface="desktop"] .reflect-bars,
body[data-surface="desktop"] .trailing-svg,
body[data-surface="desktop"] .tf-chart,
body[data-surface="desktop"] .bigpic-chart {
  max-width: 760px;
}
body[data-surface="desktop"] .reflect-tile[data-tile-id="frequent"] .tf-chart {
  width: 100%; height: auto; overflow: visible;
  max-width: none;
}
/* Chart card treatment (Mission 1, v1.190.0): a calm, restrained flat card —
 * no frosted-glass blur, no inset highlight, no drop shadow. The previous
 * backdrop-filter/box-shadow/gradient-adjacent "glass tile" read as decorative
 * furniture competing with the data rather than quiet supporting structure,
 * and is exactly the kind of faux-3D/bevel treatment the visual revamp
 * retired. A plain token-derived card face + hairline edge is the entire
 * surface language now; the numbers do the work. Applies to the trailing
 * spending-watch bars, the top-frequent line chart and the report bars alike. */
.reflect-bars, .trailing-svg, .tf-chart {
  background: var(--card);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-s);
  padding: 10px 8px; box-sizing: border-box;
}
/* Solid bars with a contemporary radius (from the shared --radius-s scale,
 * not a hardcoded 3-4px nub) — clean flat fill, no translucency-over-glass
 * effect and no 1990s-spreadsheet square corners.
 * .trailing-svg (barsWithAverage, trailing-report.js) is EXCLUDED from the
 * `rx` declaration: it sets a width-derived radius per-rect via the SVG
 * presentation attribute, and `rx` is a CSS geometry property (SVG2) that
 * silently wins the cascade over a presentation attribute — the fixed 6px
 * here was defeating the intended contemporary/width-derived radius on every
 * spending-watch bar (v1.190.0 regression, corrected v1.191.0). All other
 * bar charts (Reports, Most Frequent, Home big-picture) never set rx in JS,
 * so they still need — and keep — this fixed fallback. */
.reflect-bars rect, .trailing-svg rect { fill-opacity: 1; }
.reflect-bars:not(.trailing-svg) rect { rx: 6px; }
/* X-axis category labels, every chart in the app. Sized off a token rather than
 * a literal because touch surfaces need a much larger tick than Win11 does
 * (operator 2026-07-17: "on ios the xaxis fontsize is way too small, make much
 * bigger, of all graphs in HOME"). This is a CSS font-size, so — unlike an SVG
 * font-size attribute — the viewBox scale does NOT shrink it: 10px here painted
 * at ~10 screen px and still read tiny next to body text. The token is set per
 * surface in the touch media block; desktop keeps the dense original. */
.bar-xlabel { font-size: var(--fs-graph-axis); fill: var(--ink-muted); }
/* $ value atop each bar (item 6). Default 9px suits the dense top-frequent /
 * trailing charts; the Home net-worth chart overrides this per-label via an
 * inline style (home.js) so its labels scale up to ~the bar width (operator
 * 2026-07-08i) — inline style wins over this class, an SVG font-size attribute
 * would not. */
/* Node value labels read at BODY size (operator 2026-07-14). 9px was set as an
 * SVG px value INSIDE a scaled viewBox, so it painted at roughly five screen
 * pixels on a phone. Charts that size their labels in CSS (.graph-vlabel) are
 * unaffected by the viewBox scale and override this anyway. */
.bar-vlabel { font-size: var(--fs-graph-node); font-weight: 700; fill: var(--ink); }

/* ---------- APP-WIDE GRAPH-LABEL RULE (operator 2026-07-14) ----------
 * "The numbers in that graph and in Most Frequent — indeed implement as a rule
 *  app-wide for graphs — are too small to be readable and must be the same size
 *  as regular text; ensure as a cardinal rule that they must NEVER bleed over
 *  each other or truncate."
 *
 * Any chart value label carrying .graph-vlabel is rendered at BODY text size, in
 * full ink weight. The no-overlap half of the rule cannot be done in CSS (SVG has
 * no layout engine), so it is enforced where the labels are placed — each chart
 * commits label boxes and pushes colliding ones apart, drawing a leader line to
 * the node when a label ends up displaced (see bigPicChart in home.js). The two
 * halves are deliberately paired: size here, collision there.
 *
 * A CLIPPED node's label (a series truncated by the expense-focused scale) is
 * italic, so an off-scale figure is never mistaken for an on-scale one. */
.graph-vlabel {
  font-size: var(--fs-graph-node);
  font-weight: 700;
  /* A soft halo in the card colour keeps a label legible if it must pass over a
   * gridline or the other series' curve — it can never be *hidden*, only offset. */
  paint-order: stroke; stroke: var(--card); stroke-width: 3px; stroke-linejoin: round;
}
.graph-vlabel.clipped { font-style: italic; }
/* Rotated value label seated INSIDE a bar (operator 2026-07-10): white ink with
 * a soft dark halo so it reads against any bar colour, vertically centred via
 * central baseline (the rotate transform is applied per-label in JS). */
.bar-vlabel-in {
  font-weight: 800; fill: var(--chart-ink); dominant-baseline: central;
  paint-order: stroke; stroke: var(--chart-halo); stroke-width: 2.4px; stroke-linejoin: round;
}
/* Same rotated value, but seated ABOVE a bar too short to hold it inside: ink
 * ink on a background-coloured halo so it stays legible over gridlines. Keeps
 * the whole chart's value labels one orientation (vertical) — never mixed. */
.bar-vlabel-out {
  font-weight: 800; fill: var(--ink); dominant-baseline: central;
  paint-order: stroke; stroke: var(--card); stroke-width: 2.6px; stroke-linejoin: round;
}
.chart-grid {
  stroke: color-mix(in srgb, var(--hairline) 58%, transparent);
  stroke-width: 1;
  stroke-dasharray: 2 7;
  vector-effect: non-scaling-stroke;
}
/* Mission 20/08/2026 corrective: the operator's live screenshot showed a thin
 * Excel-style line that did not read as a "designed" chart against the
 * supplied mock-ups. A confident stroke + slightly larger node geometry +
 * a stronger (but still restrained) area wash reads as an intentional chart
 * at a glance without turning into a solid block. */
.chart-series-line { stroke-width: 3.4; }
/* Single-series area wash (Mission 1, v1.190.0; strengthened 20/08/2026) —
 * the SVG `fill` is already the series' own token colour; opacity here is
 * what keeps it a wash rather than a solid block. Never applied to a
 * multi-series chart (see line-chart.js), so this can never stack into
 * coloured fog. */
/* Mission 20/08/2026 item 5: the flat opacity: .16 constant wash is replaced
 * by a true vertical gradient fill (line-chart.js areaGradientDef) whose own
 * stop-opacity already controls the fade — no CSS opacity layered on top. */
.chart-series-area { pointer-events: none; }
.chart-guide-line { opacity: .72; }
.chart-node-ring {
  fill: var(--card);
  stroke-width: 2.4;
  vector-effect: non-scaling-stroke;
}
.chart-node-core { stroke: none; }
.chart-node-label-leader {
  stroke-width: 1.25;
  stroke-dasharray: 2 3;
  opacity: .72;
  vector-effect: non-scaling-stroke;
  pointer-events: none;
}
.chart-bar-value {
  font-weight: 700;
  dominant-baseline: central;
  paint-order: stroke;
  stroke: var(--card);
  stroke-width: 3px;
  stroke-linejoin: round;
}
.chart-guide-label, .trailing-avglabel, .trailing-modelabel {
  font-size: var(--fs-body);
  font-weight: 700;
  dominant-baseline: central;
  paint-order: stroke;
  stroke: var(--card);
  stroke-width: 3px;
  stroke-linejoin: round;
}
.chart-guide-label, .trailing-avglabel { fill: var(--ink-muted); }
.mode-label, .trailing-modelabel, .chart-guide-combined-label { fill: var(--accent); }
.chart-outlier-marker {
  fill: none; stroke: var(--ink-muted); stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke;
}
.chart-axis-break { pointer-events: none; }
.chart-axis-break-mark { opacity: .82; vector-effect: non-scaling-stroke; stroke-width: 1.8; }
.chart-bar-value-clipped { font-style: italic; }
.plan-filter-popover .pop-row { color: var(--ink); }

/* Current touch owns one neutral filter capsule; the generic active-pill rule
 * is intentionally overridden here because this control opens a menu rather
 * than representing a selected segmented state. */
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .plan-pillrow-touch .plan-filter-trigger {
  background: var(--btn-secondary-bg) !important;
  color: var(--btn-secondary-ink) !important;
  border: 1px solid var(--hairline);
  box-shadow: none;
}
.avg-line { stroke: var(--ink); stroke-width: 1.5; stroke-dasharray: 5 4; opacity: .55; }
.zero-line { stroke: var(--hairline); stroke-width: 1; }
.nw-line { fill: none; stroke: var(--ink); stroke-width: 1.5; opacity: .7; }
.nw-dot { fill: var(--ink); }

/* tables */
.reflect-table { width: 100%; border-collapse: collapse; font-size: var(--fs-body); }
.reflect-table th, .reflect-table td { padding: .4rem .5rem; border-bottom: 1px solid var(--hairline); text-align: left; }
.reflect-table th.num, .reflect-table td.num { text-align: right; font-variant-numeric: tabular-nums; }
.reflect-table thead th { font-size: var(--fs-label); color: var(--ink-muted); text-transform: uppercase; letter-spacing: .02em; }

/* income v expense matrix */
.reflect-matrix-scroll { overflow-x: auto; }
.reflect-matrix { border-collapse: collapse; font-size: var(--fs-body); min-width: 100%; white-space: nowrap; }
.reflect-matrix th, .reflect-matrix td { padding: .35rem .6rem; border-bottom: 1px solid var(--hairline); text-align: left; }
.reflect-matrix th.num, .reflect-matrix td.num { text-align: right; font-variant-numeric: tabular-nums; }
.reflect-matrix thead th { position: sticky; top: 0; background: var(--card); font-size: var(--fs-label); color: var(--ink-muted); }
.reflect-matrix .ie-section td { font-weight: 700; padding-top: .7rem; text-transform: uppercase; font-size: var(--fs-label); color: var(--ink-muted); }
.reflect-matrix .ie-group td:first-child { font-weight: 700; }
.reflect-matrix .ie-name { padding-left: 1.3rem; color: var(--ink-muted); }
.reflect-matrix .ie-total td, .reflect-matrix .ie-grand td { font-weight: 700; border-top: 2px solid var(--hairline); }
.reflect-matrix .ie-grand td { border-bottom: 0; }

/* ---------- mobile/tablet Reflect card stack (§6) ---------- */
/* Tighter tile gap on touch surfaces (operator 2026-07-11: "home screen can
 * afford to slightly reduce gaps"); desktop keeps its 16px grid gap below. */
.reflect-cardstack { display: flex; flex-direction: column; gap: 10px; }
.reflect-card-right { display: flex; flex-direction: column; gap: 14px; }
.reflect-mcard { display: flex; flex-direction: column; gap: 10px; }
.mcard-head { display: flex; justify-content: space-between; align-items: baseline; }
.mcard-month { font-size: var(--fs-label); font-weight: 600; color: var(--ink-muted); text-transform: uppercase; letter-spacing: .03em; }
.mcard-total { font-size: var(--fs-h1); font-weight: 700; }
.mcard-strip { display: flex; width: 100%; height: 10px; border-radius: 6px; overflow: hidden; background: var(--chip); }
.mcard-strip.empty { background: var(--chip); }
.mcard-strip-seg { display: block; height: 100%; background: var(--item-color, var(--accent)); }
.mcard-ranklist { display: flex; flex-direction: column; }
.mcard-list-head { display: flex; justify-content: space-between; font-size: var(--fs-label); color: var(--ink-muted); text-transform: uppercase; letter-spacing: .02em; padding-bottom: 2px; }
.mcard-rank-row { display: grid; grid-template-columns: 12px minmax(0, 1fr) auto var(--ctl-h-md); align-items: center; gap: 8px; padding: .32rem 0; border-top: 1px solid var(--hairline); }
.mcard-rank-row.is-other { background: var(--control-secondary-bg); border-radius: var(--radius-s); padding-inline: .35rem; }
.mcard-rank-row > *:nth-child(2) { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mcard-rank-row:first-of-type { border-top: 0; }
.mcard-sentence { font-size: var(--fs-value); font-weight: 600; margin: 0; }
/* Spending-this-month tile: sort toggle + per-category omit (item 8). */
.mcard-sort {
  position: relative; display: grid; grid-template-columns: max-content max-content;
  gap: 2px; max-width: 100%; background: var(--chip); border-radius: 999px; padding: 2px;
 }
.mcard-sort-indicator {
  position: absolute; inset: 2px auto 2px 2px; width: 0;
  border: 1px solid color-mix(in srgb, var(--accent) 34%, transparent);
  border-radius: 999px; background: color-mix(in srgb, var(--accent) 16%, transparent);
  transition: transform var(--dur-base) var(--ease-ios-rebound), width var(--dur-base) var(--ease-ios-rebound);
  pointer-events: none;
}
.mcard-sort-seg {
  position: relative; z-index: 1; border: 0; background: none; color: var(--ink-muted); font-size: var(--fs-label); font-weight: 600;
  padding: .12rem .5rem; border-radius: 999px; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center; min-height: var(--ctl-h-sm);
}
@media (max-width: 480px) {
  .mcard-sort { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); width: 100%; }
  .mcard-sort-seg { min-width: 0; white-space: normal; overflow-wrap: anywhere; }
}

/* S10 continuation: category/subcategory labels are never ellipsised. Keep
 * account, payee and memo rails unchanged; category text may wrap and grow its
 * row instead of hiding the rest of the name. */
body:is([data-surface="phone"], [data-surface="tablet"]) :is(.txn-cat-inline, .txn-cat-inline-first-line) {
  max-width: 100%; min-width: 0; overflow: visible; text-overflow: clip;
  white-space: normal; overflow-wrap: anywhere;
}
/* The chip must give ground so the account lozenge can sit BESIDE it (operator
 * 01/09/2026: "the 'transfer' glyph can be same row as the account"). v1.214.67
 * removed the `flex: 1 0 100%` that forced the rail onto its own line, yet a
 * long category still pushed the lozenge down: measured 3/12 rows on iPhone 13
 * Pro Max, chip 305.6px + lozenge 114.5px = 420.1px inside a 346px sub-line.
 *
 * `flex-shrink` could not save it. Shrinking only resolves items already sharing
 * a line, and `.txn-sub` is `flex-wrap: wrap` — the browser wraps the overflowing
 * item BEFORE it shrinks anything, so the chip kept its max-content width and
 * the rail moved to line 2. Capping the chip's share of the line is what keeps
 * both on one row: it wraps its own text (white-space is already normal here,
 * S10 forbids ellipsising a category) instead of evicting its neighbour, and a
 * short category is untouched because the cap is a MAXIMUM, not a basis. */
/* The chip may take the whole sub-line MINUS the room the account lozenge needs
 * beside it (operator 04/09/2026: "as far as possible try not to reflow into 2nd
 * row"). This replaces the flat 62% share v1.214.67 used.
 *
 * A PERCENTAGE was the wrong instrument. 62% guaranteed the lozenge kept the
 * chip's row by breaking the CHIP's own text instead: measured on iPhone 13 Pro
 * Max, "Nala GE TotalCare Rider annual 30Jul…" wants 305.6px and was held to
 * 248px of a 400px sub, wrapping to 2.19 lines on 3 of 12 rows. Raising the
 * ratio only moves the failure — the same 305.6px chip needs 78% of a 400px sub
 * but 88% of a 346px one, so no single ratio serves both widths. The constraint
 * is not a share of the line, it is "whatever the neighbour needs", so it is
 * written that way and holds at every width. `--txn-sub-loz-reserve` is the
 * lozenge's typical width plus one gap; a chip longer than the remainder still
 * wraps rather than ellipsising (S10 forbids clipping a category). */
body:is([data-surface="phone"], [data-surface="tablet"]) .txn-sub .txn-cat-inline {
  flex: 0 1 auto; min-width: 0;
  max-width: var(--txn-sub-chip-max, calc(100% - var(--txn-sub-loz-reserve, 4.5rem)));
}
/* A chip with NOTHING beside it must not be capped (operator 04/09/2026: "as
 * far as possible try not to reflow into 2nd row"). The 62% cap above exists
 * only to stop a long category evicting the account lozenge from its row; when
 * the chip is the sub-line's ONLY child there is no neighbour to protect, yet
 * the cap still applied and broke the chip's own text onto a second line —
 * measured on iPhone 13 Pro Max: "Nala GE TotalCare Rider annual…" held to
 * 248px of a 400px sub and wrapping to 2.19 lines. `:only-child` gives those
 * rows the whole width back, so they wrap only when the text genuinely exceeds
 * the sub-line. A chip WITH a metadata rail keeps the cap unchanged. */
body:is([data-surface="phone"], [data-surface="tablet"]) .txn-sub .txn-cat-inline:only-child {
  max-width: 100%;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-row-badgeline {
  overflow: visible; white-space: normal;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-row-meta {
  overflow: visible; text-overflow: clip; white-space: normal; overflow-wrap: anywhere;
}
body[data-surface="desktop"] .reg-row .c-cat {
  overflow: visible; text-overflow: clip; white-space: normal; overflow-wrap: anywhere;
  align-self: stretch; display: flex; align-items: center;
}
body[data-surface="desktop"] .reg-row .c-cat > .reg-cell-text,
body[data-surface="desktop"] .reg-row .c-cat > [title] {
  overflow: visible; text-overflow: clip; white-space: normal; overflow-wrap: anywhere;
  display: block; min-width: 0;
}

body[data-route="spending"]:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-date-group .swipe-wrap:is(.date-group-first, .date-group-middle, .date-group-last, .date-group-only) {
  border-radius: 0;
}
body[data-route="spending"]:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-date-group .swipe-wrap:is(.date-group-last, .date-group-only),
body[data-route="spending"]:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-date-group .swipe-wrap:is(.date-group-last, .date-group-only) .txn-row {
  border-radius: 0 0 var(--radius-s, 8px) var(--radius-s, 8px);
}
body[data-surface="phone"] .mcard-sort-seg,
body[data-surface="tablet"] .mcard-sort-seg { min-height: var(--ctl-h-md); }
.mcard-sort-seg.sel { background: none; color: var(--accent); }
.mcard-sort-seg:active { transform: scale(.96); }
@media (prefers-reduced-motion: reduce) { .mcard-sort-seg { transition: none; } }
.rank-freq { font-size: var(--fs-label); }
/* 2026-07-17 (tapthru §1/§4): this "×" hides a category from the tile, and it measured
 * 12.5px WIDE on every touch profile — `padding: 0 .1rem` was its only horizontal size, so
 * the row's 44px height flattered the vertical axis while the thumb had ~12px to aim at.
 * The project's own token says 44px is the floor on touch and to never go below it. The
 * MARK stays exactly the same size (font-size unchanged); only the hit box grows, and it
 * grows on the touch surfaces alone so the desktop tile keeps its tight rhythm. */
.rank-omit { border: 0; background: none; color: var(--ink-muted); font-size: var(--fs-value); line-height: 1; cursor: pointer; padding: 0 .1rem; opacity: .55; }
.rank-omit-space { display: block; width: var(--ctl-h-md); height: var(--ctl-h-md); }
body[data-surface="phone"] .rank-omit,
body[data-surface="tablet"] .rank-omit {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: var(--ctl-h-md); min-height: var(--ctl-h-md); padding: 0;
}
@media (hover: hover) { .rank-omit:hover { opacity: 1; color: var(--neg); } }
.mcard-omitted { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: .5rem; }
.omit-chip { border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--hairline)); background: var(--control-secondary-bg); color: var(--control-accent); border-radius: 999px; font-size: var(--fs-label); padding: .1rem .5rem; cursor: pointer; }
@media (hover: hover) { .omit-chip:hover { border-color: var(--accent); color: var(--accent); } }

/* ---------- reorderable Reflect tiles (hold-drag; operator 2026-07-07) ---------- */
/* Each tile wraps one report card. user-select:none kills the accidental iOS
 * text-highlight the operator kept triggering on long-press; touch-action keeps
 * vertical scroll working until a drag arms in reorder.js. */
/* min-width:0 so a tile never inherits a wide child's intrinsic width and
 * overflows the phone viewport (see .trailing-list note) — fixes the clipped
 * figures across every Home tile, not just Spending watch. */
.reflect-tile { display: flex; flex-direction: column; min-width: 0; }
.reflect-cardstack > * { min-width: 0; }
/* Wasted-space fix (operator 2026-07-14): each tile's inner card/report kept its
 * own bottom margin (.card 14px, .trailing-report 20px), which STACKED on top of
 * the stack's own flex `gap: 10px` — so Home showed a ~24-30px chasm between
 * "Per schedule", "Spending watch" and "Most frequent". Zero those inner bottom
 * margins inside the stack and let the single 10px gap set the rhythm. */
.reflect-cardstack > .reflect-tile > .card,
.reflect-cardstack > .reflect-tile > .trailing-report,
.reflect-cardstack > .reflect-tile > section { margin-bottom: 0; }
.reorderable {
  -webkit-user-select: none; user-select: none;
  -webkit-touch-callout: none; touch-action: pan-y;
  cursor: grab;
}
.reorderable.dragging {
  cursor: grabbing; opacity: .92;
  box-shadow: 0 12px 30px rgba(0,0,0,.28); border-radius: 16px;
  position: relative; z-index: 5;
}
.reflect-insight-tiles { display: flex; flex-direction: column; gap: 14px; margin-bottom: 16px; }

/* ---------- Top-3 most-frequent categories tile ---------- */
.top-frequent { display: flex; flex-direction: column; gap: 8px; }
.tf-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.tf-head .trailing-h { margin: 0; }
.tf-hide { background: none; border: 0; color: var(--accent); font: inherit; font-size: var(--fs-body); font-weight: 600; cursor: pointer; padding: 0; }
/* Touch: "Hide" measured ~33px wide, zero padding (v1.29.0 tapthru §1).
 * Background-free — the growth is invisible hit box. */
body[data-surface="phone"] .tf-hide,
body[data-surface="tablet"] .tf-hide {
  min-width: var(--ctl-h-md); min-height: var(--ctl-h-md);
  display: inline-flex; align-items: center; justify-content: center;
}
/* Every visible Hide action is a text-only affordance, including confirmation
 * sheets and popovers. The minimum hit target remains on-theme and touch-safe;
 * the control never falls back to a danger/secondary fill. */
.hide-action {
  min-width: var(--ctl-h-md); min-height: var(--ctl-h-md);
  display: inline-flex; align-items: center; justify-content: center;
  padding-inline: .65rem; border: 0 !important; border-radius: var(--radius-s);
  background: transparent !important; box-shadow: none !important;
  color: var(--accent); font: inherit; font-weight: 600; cursor: pointer;
}
.hide-action:disabled { color: var(--ink-muted); cursor: wait; opacity: .7; }
.hide-action:is(:hover, :focus-visible) { background: transparent !important; }
.reflect-pill.hide-action { border-color: transparent !important; }
.reflect-pill.hide-action.on,
.reflect-pill.hide-action[aria-pressed="true"] { background: transparent !important; color: var(--accent); }
.cat-action.hide-action { justify-content: flex-start; width: 100%; text-align: left; }
/* Semantic stamping must never promote a Hide control's legacy `.link` class
 * into a filled secondary face. Keep the whole authored Hide inventory quiet in
 * both themes while retaining the shared press feedback and touch floor. */
:is(button, [role="button"]).tf-hide,
:is(button, [role="button"]).hide-action {
  background: transparent !important;
  border-color: transparent !important;
  box-shadow: none !important;
  transition: transform var(--dur-fast) var(--ease-spring), filter var(--dur-fast) var(--ease-standard);
}
:is(button, [role="button"]).tf-hide:active,
:is(button, [role="button"]).hide-action:active {
  background: transparent !important;
  transform: scale(.96);
  filter: brightness(.92);
}
.tf-sub { margin: 0; }
.tf-chart { width: 100%; height: auto; }
.tf-legend { display: flex; flex-direction: column; gap: 6px; margin-top: 4px; }
/* The NAME takes the flexible track (was `auto`, which sizes to max-content and
 * pushed the meta clean off the card on a long category). */
.tf-legend-row { display: grid; grid-template-columns: 12px minmax(0, 1fr) auto; align-items: center; gap: 8px; font-size: var(--fs-body); }
.tf-legend-dot, .donut-legend-dot { width: 12px; height: 12px; border-radius: 50%; background: var(--item-color, var(--accent)); }
.tf-legend-name { font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tf-legend-meta { justify-self: end; font-size: var(--fs-label); white-space: nowrap; }

/* iPad landscape: 2-column masonry (Spending Breakdown | Income vs Spending +
 * Net Worth stacked — Age of Money card dropped, D-009, so 3 cols → 2). */
@media (min-width: 768px) and (orientation: landscape) {
  body[data-surface="tablet"] .content:has(.reflect-cardstack) { max-width: none; }
  body[data-surface="tablet"] .reflect-cardstack {
    display: grid; grid-template-columns: minmax(280px, 1fr) minmax(320px, 1.2fr); align-items: start;
  }
  /* Plan (§9: "full-width table" in landscape — iPad portrait already shows
   * Activity, this just gives the same table more room). */
  body[data-surface="tablet"] .content:has(.plan-header.mobile) { max-width: none; }
}

/* Desktop (Win11, wide): the Home tile stack becomes a dynamic multi-column
 * grid instead of one tall column — shrinks each tile and packs up to 3 per row
 * to save vertical space. Column count flexes with width (auto-fill, min 300px)
 * and is capped at 3 by a max-column-width ceiling. align-items:start so short
 * tiles don't stretch to the tallest in their row. Hold-drag reorder still works
 * (reorder.js is DOM-order based, grid just reflows). */
body[data-surface="desktop"] .reflect-cardstack {
  display: grid;
  grid-template-columns: 1fr;
  align-items: start;
  gap: 16px;
}
/* 2 across once there's room, 3 across when wide — capped at 3 (never 4+). */
@media (min-width: 900px) {
  body[data-surface="desktop"] .reflect-cardstack { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1300px) {
  body[data-surface="desktop"] .reflect-cardstack { grid-template-columns: repeat(3, 1fr); }
}
/* Spending watch gets a full-width band of its OWN on desktop (operator
 * 2026-07-12: it kept landing offset — a tall tile wedged beside short ones at
 * a mismatched baseline). Spanning every column lets its internal 2-up card
 * grid breathe and stops the grid from tucking a short tile alongside it. */
body[data-surface="desktop"] .reflect-tile[data-tile-id="watch"] { grid-column: 1 / -1; }
/* Most Frequent Categories also spans the full Home main-content width on
 * desktop (Mission 2, v1.190.0): the operator's complaint was an
 * extraordinarily TALL narrow chart — a symptom of being squeezed into one
 * grid column while its three-line legend needed real width. The extra room
 * is specifically to help every node's value label stay clear rather than
 * fighting for a cramped column. */
body[data-surface="desktop"] .reflect-tile[data-tile-id="frequent"] { grid-column: 1 / -1; }

/* filter check-tree + custom range */
.check-tree { width: min(460px, 94vw); }
.check-tree-bar { display: flex; gap: 14px; }
.check-tree-bar .link { background: none; border: 0; color: var(--accent); font: inherit; font-weight: 600; cursor: pointer; padding: 0; }
.check-list { max-height: 46dvh; }
.check-row { display: flex; align-items: center; gap: 8px; padding: .4rem .2rem; font-size: var(--fs-body); cursor: pointer; }
.picker-foot { display: flex; justify-content: flex-end; gap: 8px; padding-top: 6px; border-top: 1px solid var(--hairline); }
.reflect-menu { min-width: 190px; }
.reflect-menu .pick-row.on { font-weight: 700; color: var(--accent); }
.reflect-custom { display: flex; flex-direction: column; gap: 6px; padding: .5rem .4rem; border-top: 1px solid var(--hairline); font-size: var(--fs-body); }
.reflect-custom label { display: flex; justify-content: space-between; gap: 8px; align-items: center; }
.reflect-custom .inp { width: 140px; }

/* Link from the mobile/tablet Reflect card stack into the full Spending Report. */
.reflect-report-link { display: inline-block; margin: -.4rem 0 1rem; font-size: var(--fs-body); font-weight: 600; color: var(--accent); }

/* ---------- Reconcile modal (account statement reconcile) ---------- */
.reconcile-modal { width: min(440px, 94vw); }
.reconcile-modal .rec-lead { margin: .2rem 0 .4rem; font-size: var(--fs-value); }
.reconcile-modal .rec-field { display: flex; flex-direction: column; gap: 4px; margin: .6rem 0 .2rem; font-size: var(--fs-body); font-weight: 600; }
.reconcile-modal .rec-actual { font-size: var(--fs-h3); font-variant-numeric: tabular-nums; padding: .5rem .6rem; }
.reconcile-modal .rec-actual.error { border-color: var(--neg); }
.reconcile-modal .rec-preview { min-height: 1.2em; margin: .2rem 0 0; }
.reconcile-modal .rec-error { margin: .4rem 0 0; }
.reconcile-modal .rec-markall { margin-top: .4rem; }
.reconcile-modal .picker-foot { flex-wrap: wrap; }
.reconcile-account-modal .picker-head .reconcile-title {
  flex: 1; min-width: 0; margin: 0; font: inherit;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Mobile per-account reconcile button sits just under the account title. */
.reg-reconcile { margin: 0 0 .6rem; }

/* ---------- Spending Report (per-category deep dive) ---------- */
.rep-wrap { display: flex; flex-direction: column; gap: 14px; }

.rep-picker { display: flex; flex-direction: column; gap: 10px; }
.rep-tiles { display: flex; flex-wrap: wrap; gap: 8px; }
.rep-tile {
  padding: .4rem .9rem; border-radius: 999px; border: 2px solid var(--tile-color, var(--accent));
  background: var(--card); color: var(--ink); font: inherit; font-size: var(--fs-body); font-weight: 600; cursor: pointer;
}
.rep-tile.on { background: var(--tile-color, var(--accent)); color: var(--chart-ink); border-color: transparent; }
.rep-picker-select {
  align-self: flex-start; max-width: 100%; padding: .45rem .7rem; border-radius: var(--radius-s, 8px);
  border: 1px solid var(--hairline); background: var(--card); color: var(--ink); font: inherit; font-size: var(--fs-body);
}

.rep-headline-card { display: flex; flex-direction: column; align-items: center; gap: 4px; text-align: center; padding: 1.4rem 1rem; }
.rep-headline-card > * { min-width: 0; max-width: 100%; }
.rep-headline-label { max-width: 100%; font-size: var(--fs-value); font-weight: 600; color: var(--ink-muted); overflow-wrap: anywhere; }
.rep-headline-amt {
  display: block; min-width: 0; max-width: 100%;
  color: var(--item-color, var(--accent)); font-size: var(--fs-display); font-weight: 700; line-height: 1.15;
  overflow-wrap: anywhere; word-break: break-word;
}
body[data-surface="desktop"] .rep-headline-amt { font-size: var(--fs-display); }

.rep-controls { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.rep-chart-toggle { margin-left: auto; }

.rep-chart-card { display: flex; flex-direction: column; }
.rep-chart { width: 100%; }
.rep-svg { width: 100%; height: auto; overflow: visible; }

.rep-metrics {
  display: grid;
  /* A 240px floor lets the mobile card stack rather than paint a long
   * currency value into its neighbour; wide cards still fit four cells. */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
  gap: 12px;
}
body[data-surface="desktop"] .rep-metrics { grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); }
.rep-metric-cell { display: flex; flex-direction: column; gap: 2px; min-width: 0; max-width: 100%; }
.rep-metric-label { font-size: var(--fs-label); color: var(--ink-muted); text-transform: uppercase; letter-spacing: .02em; }
.rep-metric-value {
  min-width: 0; max-width: 100%;
  font-size: var(--fs-h3); font-weight: 700; font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere; word-break: break-word;
}

/* ---------- import wizard ---------- */
.import-stats { padding-left: 1.2rem; }
.import-progress { width: 100%; }

/* ---- progress.js (D-052): one progress component, real counts, honest ETA.
 * Never a fake indeterminate spinner on work whose size is known — see
 * SETUP-GATE-DESIGN.md. Token-only (rule 14). */
.progress-panel { display: flex; flex-direction: column; gap: 6px; margin: 10px 0; }
.progress-panel .progress-label { margin: 0; font-weight: 600; }
.progress-panel .progress-bar { width: 100%; }
.progress-panel .progress-note { margin: 0; }
.progress-panel.progress-panel-error .progress-note { color: var(--neg); }
.onboarding-checklist { list-style: none; display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 6px; padding: 0; margin: 0 0 12px; }
.onboarding-checklist li { display: flex; align-items: center; justify-content: center; gap: 5px; min-width: 0; padding: .45rem .35rem; border: 1px solid var(--hairline); border-radius: var(--radius-s); background: var(--card); color: var(--ink-muted); font-size: var(--fs-label); text-align: center; }
.onboarding-checklist li.done { color: var(--pos); border-color: color-mix(in srgb, var(--pos) 40%, var(--hairline)); }
.onboarding-checklist li.active { color: var(--accent); border-color: var(--accent); font-weight: 700; }
.import-backup-step { margin: 12px 0; background: color-mix(in srgb, var(--accent) 6%, var(--card)); }
.import-backup-step h3 { margin-top: 0; }
.onboarding-next { margin-top: 12px; }

/* Migration health check (import-ynab.js verifyPanel) — proves every item from
 * the export landed. Token-only, both themes (CARDINAL rule 14). The pass/gap
 * tint is a colour-mix off the money-green / negative-red tokens so it reads on
 * either background without a hardcoded colour. */
.import-verify,
.import-report { display: flex; flex-direction: column; gap: 8px; min-width: 0; max-width: 100%; overflow-wrap: anywhere; }
.import-verify > *,
.import-report > * { min-width: 0; max-width: 100%; }
.import-verify h3 { margin: 0; }
.import-verify.ok { background: color-mix(in srgb, var(--pos) 12%, var(--card)); border: 1px solid color-mix(in srgb, var(--pos) 40%, transparent); }
.import-verify.gap { background: color-mix(in srgb, var(--neg) 10%, var(--card)); border: 1px solid color-mix(in srgb, var(--neg) 40%, transparent); }
.import-verify .v-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.import-verify .v-list li { display: flex; align-items: baseline; gap: 8px; min-width: 0; overflow-wrap: anywhere; }
.import-verify .v-list li > :last-child { min-width: 0; overflow-wrap: anywhere; }
.import-verify .v-mark { flex: 0 0 auto; font-weight: 700; width: 1.1em; text-align: center; }
.import-verify .v-ok .v-mark { color: var(--pos); }
.import-verify .v-gap .v-mark { color: var(--neg); }
.import-verify .v-gap { color: var(--neg); }
.import-report ul { min-width: 0; max-width: 100%; padding-inline-start: 1.2rem; }
.import-report li { min-width: 0; max-width: 100%; overflow-wrap: anywhere; }
.import-report code { display: inline-block; min-width: 0; max-width: 100%; white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; }
.import-verify-retry { margin-top: 4px; }

/* File Import (§11.9, D-011): column mapping + preview. */
.fi-map-controls { margin: .4rem 0 .8rem; }
.fi-field-rows { margin: .2rem 0; }
.fi-preview { margin: .6rem 0; }
.fi-preview-table { overflow-x: auto; font-size: var(--fs-body); }
.fi-preview-head, .fi-preview-row {
  display: grid; grid-template-columns: .9fr 1.6fr 1.4fr .8fr; gap: 8px;
  padding: .35rem 0; border-top: 1px solid var(--hairline);
}
.fi-preview-head { border-top: 0; color: var(--ink-muted); font-size: var(--fs-label); text-transform: uppercase; }
.fi-preview-row span:last-child { text-align: right; font-variant-numeric: tabular-nums; }
.fi-skip { margin-top: .5rem; font-size: var(--fs-body); }
.fi-skip ul { margin: .3rem 0 0 1rem; color: var(--ink-muted); }

/* Friendly wizard notes (operator 2026-07-08h): a cheery hand-drawn line doodle
 * beside a short, non-scary explainer. The doodle uses currentColor (the accent)
 * so it reads in both themes (rule 14); it shrinks/hides gracefully on a phone
 * where the column is narrow. */
.wizard-note {
  display: flex; align-items: flex-start; gap: 1rem;
  background: color-mix(in srgb, var(--accent) 6%, var(--card));
  border: 1px solid color-mix(in srgb, var(--accent) 20%, transparent);
}
[data-theme="dark"] .wizard-note { background: color-mix(in srgb, var(--accent) 12%, var(--card)); }
.wizard-doodle { flex: 0 0 auto; width: 88px; height: auto; color: var(--accent); margin-top: .2rem; }
.wizard-finish-art { width: 140px; height: auto; display: block; margin: 0 auto 8px; }
.wizard-finish-title { font-size: 1.6rem; text-align: center; margin: 4px 0 10px; }
.wizard-note-body { min-width: 0; }
.wizard-note-body h3 { margin: 0 0 .35rem; font-size: var(--fs-value); }
.wizard-note-body p { margin: 0 0 .5rem; }
.wizard-rta-btn { margin-top: .2rem; }
/* Acknowledged state after Ready-to-Assign is lined up (operator 2026-07-13):
   green-tinted so the user SEES the reset took, not the same "too big?" pitch. */
.wizard-note-done {
  background: color-mix(in srgb, var(--pos) 8%, var(--card));
  border-color: color-mix(in srgb, var(--pos) 30%, transparent);
}
[data-theme="dark"] .wizard-note-done { background: color-mix(in srgb, var(--pos) 15%, var(--card)); }
.wizard-note-done .wizard-doodle,
.wizard-note-done .wizard-note-body h3 { color: var(--pos); }
/* Explicit "you're finished" block — a clear primary exit so leaving onboarding
   reads as completion, not an abort (operator 2026-07-13). */
.wizard-finish { text-align: center; }
.wizard-finish h3 { margin: 0 0 .35rem; font-size: var(--fs-h3); }
.wizard-finish p { margin: 0 0 .6rem; }
.wizard-finish .btn { width: 100%; }
@media (max-width: 520px) { .wizard-doodle { width: 60px; } .wizard-finish-art { width: 110px; } }

/* Scheduled-transactions onboarding review. Safe on every surface: no amount
 * heuristic, nothing preselected, full recurrence vocabulary. */
.sched-review { display: flex; flex-direction: column; gap: 6px; margin: 10px 0 14px; }
/* One-tap bulk tick/untick above the review list (v1.47.0). */
.sched-review-bulk { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0 0; }
.sched-review-bulk .btn { min-height: 44px; flex: 1 1 12rem; }
/* Three zones (operator 2026-07-20): pick | detail | controls. The value +
 * frequency selectors sit BESIDE the detail (in .sched-review-controls), no
 * longer in far-right grid columns whose 10rem select bled off the Win11 screen.
 * The controls cluster wraps UNDER the detail on a narrow screen. */
.sched-review-row {
  display: flex; align-items: flex-start; flex-wrap: wrap; gap: 8px 14px;
  padding: .75rem; border: 1px solid var(--hairline); border-radius: 12px;
  background: var(--card);
}
.sched-review-row.existing { background: var(--chip); }
/* Approved row: tapping the body turns the whole outline bold green (operator
 * 2026-07-14). Token-driven so dark/light parity holds. */
.sched-review-row.approved {
  border-color: var(--pos);
  box-shadow: inset 0 0 0 1px var(--pos);
  background: color-mix(in srgb, var(--pos) 10%, var(--card));
}
.sched-review-row:not(.existing) { cursor: pointer; }
/* A visible, labelled 44px control replaces the tiny bare checkbox. The whole
 * pill is the tap target and remains clear in both themes. */
.sched-review-pick {
  display: inline-grid; grid-template-columns: auto auto; align-items: center; justify-content: center; gap: 1px 7px;
  min-height: 44px; min-width: 7.2rem; padding: .25rem .65rem;
  border: 1px solid var(--hairline); border-radius: 999px;
  background: var(--chip); font-size: var(--fs-label); font-weight: 700;
  cursor: pointer; touch-action: manipulation;
}
.sched-review-leftfreq {
  grid-column: 1 / -1; min-width: 0; min-inline-size: 0; max-width: 100%; text-align: center;
  font-size: var(--fs-micro); font-weight: 600; color: var(--accent);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; overflow-wrap: anywhere;
}
.sched-review-pick input { width: 1.2rem; height: 1.2rem; margin: 0; accent-color: var(--pos); }
.sched-review-row.approved .sched-review-pick {
  border-color: var(--pos);
  background: color-mix(in srgb, var(--pos) 16%, var(--card));
}
.sched-review-row.existing .sched-review-pick { cursor: default; }
/* Detail grows to fill; controls sit right beside it and drop below when the
 * row is too narrow (min-width lets both share a wide row, wrap on a phone). */
.sched-review-main { display: flex; flex-direction: column; min-width: 12rem; flex: 1 1 16rem; }
/* Category + payee + memo must stay fully readable on a narrow iPhone: each
 * wraps rather than truncating (operator 2026-07-14/2026-07-20). */
.sched-review-name { font-weight: 600; overflow-wrap: anywhere; }
.sched-review-cat { font-size: var(--fs-body); font-weight: 600; color: var(--accent); overflow-wrap: anywhere; }
.sched-review-memo { font-size: var(--fs-label); color: var(--ink-muted); overflow-wrap: anywhere; }
.sched-review-meta { font-size: var(--fs-body); color: var(--ink-muted); }
.sched-review-amt { font-variant-numeric: tabular-nums; font-weight: 700; color: var(--neg); white-space: nowrap; }
.sched-review-amt.pos { color: var(--pos); }
/* Right-hand control cluster: cadence select + interval + suggestion note,
 * stacked. flex 0 1 keeps it beside the detail on desktop and lets it wrap
 * under on a phone; max-width caps the select so it can never overrun the row. */
.sched-review-controls {
  display: flex; flex-direction: column; align-items: flex-start; gap: 5px;
  flex: 0 1 14rem; min-width: 10rem; max-width: 100%;
}
.sched-review-cad { min-height: 44px; padding: .45rem .55rem; width: 100%; max-width: 14rem; }
.sched-review-n { display: flex; align-items: center; gap: 4px; color: var(--ink-muted); font-size: var(--fs-label); }
/* Wide enough for a 3-digit interval PLUS the native number-spinner Windows
 * Firefox draws inside the field — 3.7rem clipped even 2 digits, and 5.2rem
 * still hid a lone "0" behind the spinner (operator 2026-07-16, 2026-07-20).
 * min-width guards against flex shrink squeezing the digit out of view. */
.sched-review-n .inp { width: 7rem; min-width: 7rem; min-height: 44px; flex: 0 0 auto; text-align: left; }
.sched-weekdays { display: flex; gap: 7px; flex-wrap: wrap; }
.sched-weekdays label {
  display: inline-flex; align-items: center; justify-content: center; gap: 4px;
  min-width: 44px; min-height: 44px; padding: 0 5px;
  border: 1px solid var(--hairline); border-radius: var(--radius-s);
  font-size: var(--fs-label); cursor: pointer; touch-action: manipulation;
}
.sched-review-flag { font-size: var(--fs-label); font-weight: 700; color: var(--accent); white-space: normal; }
/* Onboarding is a deliberate tap/click review, not the dense desktop register.
 * Its decision controls retain the 44px floor on Win11/macOS too. */
body[data-surface="desktop"] .sched-review-bulk .btn,
body[data-surface="desktop"] .sched-review .sched-review-cad,
body[data-surface="desktop"] .sched-review .sched-review-n .inp {
  min-height: 44px;
}
.rta-review-value { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: .8rem; border: 1px solid var(--hairline); border-radius: var(--radius-s); background: var(--chip); }
.rta-review-value strong { font-size: var(--fs-h3); font-variant-numeric: tabular-nums; }
.rta-review-actions { display: flex; flex-wrap: wrap; gap: 10px; }
@media (max-width: 900px) {
  .onboarding-checklist { display: flex; overflow-x: auto; justify-content: flex-start; }
  .onboarding-checklist li { flex: 0 0 8.5rem; }
  /* The row is flex-wrap: the controls cluster simply drops under the detail on
   * a narrow screen — no grid-column overrides needed (v1.48.0). */
}
@media (max-width: 520px) {
  .sched-review-row { gap: 10px; }
  .sched-review-main { min-width: 10rem; }
  .sched-review-controls { flex: 1 1 100%; min-width: 0; width: 100%; }
  .sched-review-cad { max-width: none; }
  .sched-review-n { flex-wrap: wrap; }
}

/* ---------- pricer (D-016, restyled to app tokens, item 12) ---------- */
/* Pricer intro blurb (item: what/why/how for the non-technical shopper).
 * Matches the muted-chip treatment other screens use for inline help text. */
.pricer-blurb { background: var(--chip); border: 1px solid var(--chrome-edge); border-radius: var(--radius-s); padding: .6rem .9rem; margin-bottom: 14px; }
.pricer-blurb > summary { cursor: pointer; font-weight: 700; list-style: none; padding: .25rem 0; display: flex; align-items: center; gap: .2rem; }
.pricer-info-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.25rem; height: 1.25rem; margin-inline-end: .35rem;
  border: 0; border-radius: 999px;
  color: var(--control-secondary-ink); background: var(--control-secondary-bg);
  font-size: var(--fs-label); font-weight: 800; line-height: 1;
}
.pricer-info-mark .ico { width: 100%; height: 100%; }
.pricer-detail-step.note, .pricer-results .step.note { display: flex; align-items: center; gap: .35rem; }
.pricer-detail-step.note .ico, .pricer-results .step.note .ico { width: 1rem; height: 1rem; flex: 0 0 auto; }
.pricer-blurb > summary::-webkit-details-marker { display: none; }
.pricer-blurb-body { font-size: var(--fs-body); }
.pricer-blurb-body p { margin: .5rem 0; }
.pricer-blurb-body ol { margin: .5rem 0; padding-left: 1.2rem; }
.pricer-blurb-body li { margin: .3rem 0; }

.pricer-products { display: flex; flex-direction: column; gap: 10px; }
/* Product entry card: same radius/shadow language as .card everywhere else,
 * just tighter padding to keep five cards comfortably on one screen. */
.pricer-card { padding: 12px 14px; margin-bottom: 0; }
.pricer-row { display: flex; align-items: center; gap: 6px; margin-top: 8px; flex-wrap: wrap; }
.pricer-row:first-child { margin-top: 0; }
.pricer-num { font-size: var(--fs-label); color: var(--ink-muted); width: 16px; text-align: right; flex-shrink: 0; }
.pricer-card .inp-name { flex: 1; min-width: 6rem; }
.pricer-card .inp-price { width: 74px; text-align: right; font-weight: 700; }
.pricer-card .inp-mult, .pricer-card .inp-bonus-mult { width: 44px; text-align: center; }
.pricer-card .inp-qty, .pricer-card .inp-bonus-qty { width: 64px; }
.pricer-card .inp-density { width: 70px; }
/* Every shopping value shares one visual axis, including native selects. */
.pricer-card .inp { text-align: center; }
.pricer-card select { text-align-last: center; }
/* Toggle row (⊕ density / ⊕ bonus) + the expanded bonus/density rows read as
 * a quieter sub-section, echoing how sub-fields sit under other card rows. */
.pricer-toggles { gap: 12px; }
.r-bonus, .r-density { padding-top: 8px; border-top: 1px solid var(--hairline); }
.pricer-actions { display: flex; gap: 8px; margin: 4px 0 16px; }
.pricer-results { margin-top: 4px; }
.res-heading { font-size: var(--fs-label); font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-muted); padding: .6rem 0 .5rem; }
.group-label { font-size: var(--fs-label); color: var(--ink-muted); text-transform: uppercase; letter-spacing: .1em; margin: .6rem 0 .3rem; }
.warn-mixed { font-size: var(--fs-body); color: var(--gold-soft); background: var(--chip); border-radius: var(--radius-s); padding: .5rem .8rem; margin-bottom: 10px; }
.res-card { padding: 10px 14px; margin-bottom: 8px; }
/* Best-value card reads as a positive highlight, same green language as the
 * Ready-to-Assign banner and available-pill elsewhere in the app. */
.res-card.best { background: var(--pos-pill); box-shadow: none; }
body[data-surface="desktop"] .res-card.best { border-color: var(--pos); }
.res-row { display: flex; align-items: center; gap: 8px; }
.res-rank { width: 18px; text-align: center; color: var(--ink-muted); font-weight: 600; }
.res-card.best .res-rank { color: var(--pos); }
.res-name { flex: 1; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.res-ppu { font-weight: 700; color: var(--accent); font-variant-numeric: tabular-nums; }
.res-card.best .res-ppu { color: var(--pos); }
.res-ppu .unit-label { font-size: var(--fs-label); font-weight: 400; color: var(--ink-muted); }
.saving-tag { color: var(--neg); }
.breakdown { border-top: 1px solid var(--hairline); margin-top: 8px; padding-top: 8px; display: flex; flex-direction: column; gap: 3px; font-size: var(--fs-body); }
.step { padding-left: 8px; border-left: 2px solid var(--hairline); color: var(--ink-muted); }
.step.bonus { color: var(--accent); border-color: var(--accent); }
.step.adjusted { color: var(--accent); border-color: var(--accent); }
.step.final { color: var(--ink); border-color: var(--pos); font-weight: 600; }
.step.note { font-style: italic; border-color: transparent; }

/* Touch Pricer redesign: keep the comparison result above the editable list,
 * use one left rail for the item name and quantity fields, and let each card
 * own its compact unit-price disclosure. All paint is token-derived so the
 * same structure remains legible in Current light and dark themes. */
.pricer-screen-title { display: flex; align-items: center; gap: .45rem; }
body:is([data-surface="phone"], [data-surface="tablet"]) .pricer-info {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  width: var(--ctl-h-md); height: var(--ctl-h-md); min-width: var(--ctl-h-md); min-height: var(--ctl-h-md);
  margin: 0; padding: 0; border: 0; border-radius: 999px;
  background: transparent; color: var(--accent);
  font-family: inherit; font-size: var(--fs-body); font-weight: 700; line-height: 1; cursor: pointer;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .pricer-info .ico { width: 1.15rem; height: 1.15rem; }
body:is([data-surface="phone"], [data-surface="tablet"]) .pricer-info:active { transform: scale(.9); }
@media (hover: hover) { body:is([data-surface="phone"], [data-surface="tablet"]) .pricer-info:hover { color: var(--control-secondary-ink); } }
.pricer-info-modal { width: min(420px, 94vw); }
.pricer-info-modal .picker-head { justify-content: space-between; }
.pricer-info-copy {
  display: flex; flex: 1 1 auto; min-height: 0; flex-direction: column; gap: 10px;
  overflow-y: auto; overscroll-behavior: contain; color: var(--ink);
  padding-bottom: max(4px, env(safe-area-inset-bottom));
}
.pricer-info-modal .picker-head { flex: 0 0 auto; }
.pricer-info-copy p { margin: 0; }
body:is([data-surface="phone"], [data-surface="tablet"]) .pricer-leaderboard {
  display: grid; grid-template-rows: 0fr; overflow: hidden; min-height: 0;
  margin: 0; padding: 0; border: 0; background: transparent;
  transition: grid-template-rows var(--dur-slow, .34s) var(--ease-ios-rebound),
    margin-bottom var(--dur-slow, .34s) var(--ease-ios-rebound),
    padding var(--dur-slow, .34s) var(--ease-ios-rebound),
    border-color var(--dur-slow, .34s) var(--ease-ios-rebound),
    background-color var(--dur-slow, .34s) var(--ease-ios-rebound);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .pricer-leaderboard.is-visible {
  grid-template-rows: 1fr; margin-bottom: 14px; padding: 16px;
  border: 1px solid var(--hairline); background: var(--card);
}
.pricer-leaderboard-inner { min-height: 0; overflow: hidden; }
@media (prefers-reduced-motion: reduce) {
  body:is([data-surface="phone"], [data-surface="tablet"]) .pricer-leaderboard {
    transition: none;
  }
}
body:is([data-surface="phone"], [data-surface="tablet"]) .pricer-leaderboard h2 {
  margin: 2px 0 8px; font-size: var(--fs-h2); letter-spacing: -.01em;
}
.pricer-kicker, .pricer-item-number {
  color: var(--ink-muted); font-size: var(--fs-micro); font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase;
}
.pricer-leaderboard-help { margin: 0 0 10px; color: var(--ink-muted); font-size: var(--fs-label); }
.pricer-leader-row {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center;
  gap: 10px; min-height: var(--ctl-h-md); padding: 8px 10px;
  border: 1px solid var(--hairline); border-radius: var(--radius-s);
  background: var(--scheduled-fill); font-size: var(--fs-body);
}
.pricer-leader-row + .pricer-leader-row { margin-top: 6px; }
.pricer-leader-row.is-leader {
  border: 2px solid var(--pos); background: var(--pos-pill);
}
.pricer-leader-rank { color: var(--ink-muted); font-size: var(--fs-micro); font-weight: 800; }
.pricer-leader-row.is-leader .pricer-leader-rank { color: var(--pos); }
.pricer-leader-name { min-width: 0; overflow-wrap: anywhere; font-weight: 600; }
.pricer-leader-price { color: var(--accent); font-variant-numeric: tabular-nums; white-space: nowrap; }
.pricer-leader-row.is-leader .pricer-leader-price { color: var(--pos); }
.pricer-leader-price small { color: var(--ink-muted); font-size: var(--fs-label); font-weight: 500; }
body:is([data-surface="phone"], [data-surface="tablet"]) .pricer-touch-products { gap: 12px; }
body:is([data-surface="phone"], [data-surface="tablet"]) .pricer-touch-card {
  padding: 14px; border: 1px solid var(--hairline); border-radius: var(--radius-card-touch);
  background: var(--card); transition: border-color var(--dur-fast), background var(--dur-fast);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .pricer-touch-card.is-leader {
  border: 2px solid var(--pos); background: var(--pos-pill);
}
.pricer-card-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.pricer-remove { min-height: var(--ctl-h-md); padding-inline: .8rem; }
.pricer-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.pricer-name-field { margin-top: 12px; }
.pricer-field-label { color: var(--ink-muted); font-size: var(--fs-label); font-weight: 600; }
.pricer-field-grid {
  display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, .8fr) minmax(0, 1fr) minmax(0, .95fr);
  gap: 8px; margin-top: 10px;
}
.pricer-field-grid .inp { width: 100%; min-width: 0; min-height: var(--ctl-h-md); }
.pricer-money-input {
  display: flex; align-items: center; gap: 7px; min-height: var(--ctl-h-md);
  padding-inline: 9px; border: 1px solid var(--hairline); border-radius: var(--radius-s);
  background: var(--card);
}
.pricer-money-input:focus-within { border-color: var(--accent); }
.pricer-money-input .inp { border: 0; padding-inline: 0; min-height: 40px; }
.pricer-currency { color: var(--ink-muted); font-weight: 700; flex: 0 0 auto; }
.pricer-item-summary {
  min-height: 32px; margin-top: 10px; padding-top: 9px; border-top: 1px solid var(--hairline);
  color: var(--accent); font-size: var(--fs-label); font-variant-numeric: tabular-nums;
}
.pricer-item-summary strong { font-weight: 800; }
.pricer-touch-card.is-leader .pricer-item-summary { color: var(--pos); }
.pricer-touch-options { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: stretch; gap: 8px; }
.pricer-touch-options .link-btn { width: 100%; min-height: var(--ctl-h-md); padding-inline: .6rem; }
.pricer-touch-options .adv-density { grid-column: 1 / -1; }
.pricer-touch-options .pricer-details-toggle:disabled,
.pricer-touch-options .pricer-details-toggle[aria-disabled="true"] {
  background: var(--control-neutral-bg); color: var(--control-neutral-ink); opacity: .6; cursor: default;
}
.pricer-detail-panel {
  display: flex; flex-direction: column; gap: 4px; margin-top: 8px; padding-top: 8px;
  border-top: 1px solid var(--hairline); font-size: var(--fs-label);
}
.pricer-detail-meta { color: var(--ink-muted); font-variant-numeric: tabular-nums; }
.pricer-detail-step { padding-left: 8px; border-left: 2px solid var(--hairline); color: var(--ink-muted); }
.pricer-detail-step.bonus, .pricer-detail-step.adjusted { color: var(--accent); border-color: var(--accent); }
.pricer-detail-step.final { color: var(--ink); border-color: var(--pos); font-weight: 700; }
.pricer-advanced-field { display: flex; flex-direction: column; gap: 4px; min-width: 100px; }
.pricer-advanced-field .inp { min-height: var(--ctl-h-md); }
body:is([data-surface="phone"], [data-surface="tablet"]) .pricer-touch-actions { margin-top: 14px; }
body:is([data-surface="phone"], [data-surface="tablet"]) .pricer-touch-actions .btn { flex: 1 1 10rem; min-height: var(--ctl-h-md); }
@media (max-width: 520px) {
  body:is([data-surface="phone"], [data-surface="tablet"]) .pricer-field-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* ---------- Spending repeat band (§11.4) ---------- */
/* A2 (operator 2026-07-15): the tab is now ONE unified list mixing rule rows
 * (.sched-row) and one-off txn rows (.sched-txn-row). They used to live in
 * separate sections with different padding/borders (rule rows: .7rem + a
 * divider; txn rows: .55rem, no divider) — harmonised here to the same
 * padding and the same hairline divider so the merged list reads as ONE
 * coherent list rather than two stitched-together ones. The divider is keyed
 * off :first-child on the LIST rather than each row class, since either row
 * type may be first depending on sort order. */
.sched-list { display: flex; flex-direction: column; }
.sched-list > *:first-child { border-top: 0; }
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-row:not(.reg-row) {
  display: flex; justify-content: space-between; align-items: center; gap: 12px;
  width: 100%; padding: .6rem 0; border-top: 1px solid var(--hairline);
  background: none; font: inherit; color: inherit; text-align: left; cursor: pointer;
}
.sched-row:focus-visible { background: var(--chip); outline: none; }
@media (hover: hover) { .sched-row:hover { background: var(--chip); outline: none; } }
.sched-row.ended { opacity: .55; }
/* flex:1 — the text column must CLAIM the free space, otherwise it stays at its
 * content width and the name is squeezed by the badge while empty space sits to
 * the right of the amount (which is what clipped "Monthly savings top-up"). */
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-row-main {
  display: flex; flex-direction: column; min-width: 0; gap: 1px; flex: 1 1 auto;
}
/* THE NAME OWNS ITS OWN LINE (operator 2026-07-14: names must never be truncated
 * by other things). Measured on an iPhone: the row is 325px, the text column gets
 * 231px, and the cadence badge ate 86 of them — leaving 139px for a name that
 * wanted 183. No amount of flex tuning fixes that; there simply isn't room for
 * both on one line. So the badge drops to the line BELOW, beside the category/
 * transfer meta (which has spare width), and the name gets the full column. */
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-row-name {
  font-weight: 600; min-width: 0;
}
/* WRAPS rather than clips (operator 2026-07-14: text must never be truncated).
 * A rule's meta can be as long as "Transfer → Savings Nala" (154px) and the badge
 * is ~82px — they do not both fit in the 231px text column of a 325px phone row.
 * Ellipsising the meta would be a truncation; so the line WRAPS instead, and the
 * meta simply takes a second line when it needs one. Vertical space is cheap here;
 * a clipped account name is not. */
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-row-badgeline {
  display: flex; align-items: center; gap: 7px; min-width: 0;
  flex-wrap: wrap; row-gap: 3px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-row-badgeline .sched-row-meta { min-width: 0; }
body:is([data-surface="phone"], [data-surface="tablet"]) :is(.sched-row-meta, .sched-row-next) { font-size: var(--fs-body); }
.sched-problem { color: var(--neg); }
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-row-amt {
  white-space: nowrap; font-variant-numeric: tabular-nums;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-amount-reminder {
  color: var(--ink-muted); font-size: var(--fs-micro); font-weight: 600;
}

/* Scheduled transactions grouped by category (operator 2026-07-08h). Category
 * header band, then one row per upcoming transaction. Amount colours use the
 * pos/neg tokens so both themes read (rule 14). */
.sched-txn-row {
  display: flex; justify-content: space-between; align-items: center; gap: 12px;
  width: 100%; padding: .6rem 0; border-top: 1px solid var(--hairline);
  cursor: pointer; text-align: left; background: none; font: inherit; color: inherit;
}
.sched-txn-row:focus-visible { background: var(--chip); outline: none; }
@media (hover: hover) { .sched-txn-row:hover { background: var(--chip); outline: none; } }
.sched-txn-main { display: flex; flex-direction: column; min-width: 0; gap: 2px; flex: 1 1 auto; }
.sched-txn-payee { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
/* Cadence badge + date share the second line, so the payee/account names above get
 * the row's FULL width and can never be clipped by the badge (operator 2026-07-14).
 * The line WRAPS rather than clipping if the date+account still won't fit beside
 * the badge — a second line costs nothing; a truncated account name is the bug. */
.sched-txn-badgeline {
  display: flex; align-items: center; gap: 7px; min-width: 0;
  flex-wrap: wrap; row-gap: 3px;
}
.sched-txn-meta { font-size: var(--fs-body); min-width: 0; }
.sched-txn-amt { white-space: nowrap; font-variant-numeric: tabular-nums; }
.sched-txn-amt.neg { color: var(--neg); }
.sched-txn-amt.pos { color: var(--pos); }
/* Spending repeat-band header row (A2, operator 2026-07-15): the band was rewritten
 * into ONE unified list, so the old two-section title bands above are gone.
 * .screen-title + the Add button now share one flex row at the top; Add keeps
 * the exact look it had under the old band selector, just promoted to a plain
 * class since the band itself no longer exists. Same fixed-chrome reserve
 * treatment as .screen-title-row above (the row carries the 152px pad-right,
 * not the title) — first pass forgot this and pushed Add off the right edge
 * of every phone/tablet width (device-matrix caught it, punch A2). */
.sched-titlerow { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
.sched-titlerow .screen-title { margin: 0; }
.sched-add-btn { flex: 0 0 auto; align-self: center; }

/* Cadence badge (operator 2026-07-08j): a small pill on every scheduled
 * transaction and repeat rule showing its frequency (weekly / monthly / every 2
 * weeks / one-off) so the user reads the cadence at a glance. Tokened → both
 * themes (rule 14). A one-off carries a quieter neutral chip. */
body:is([data-surface="phone"], [data-surface="tablet"]) :is(.sched-txn-payee, .sched-row-name) {
  display: flex; align-items: center; gap: 6px; flex-wrap: nowrap;
  min-width: 0; /* let the TEXT child shrink, not the badge */
}
/* The repeat label is the ONLY thing allowed to give way. Without min-width:0 on
 * a flex child, the text refuses to shrink below its content width and instead
 * squeezes its siblings — which is how the cadence badge came to clip the name
 * (operator 2026-07-14: "ensure they are never truncated by other things").
 * min-width:0 + ellipsis makes the name yield gracefully; the badge is flex:0 0
 * auto and therefore untouchable. */
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-row-nametext {
  min-width: 0; flex: 0 1 auto;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cadence-badge {
  /* NEVER shrinks and never wraps: the badge is the fixed element of the row, so
   * the flexible TEXT ellipsises instead (operator 2026-07-14 — "ensure they are
   * never truncated by other things, as is currently the case with the transfer
   * with savings nala"). The old badge could be squeezed to nothing by a long
   * account name; flex:0 0 auto + the row's min-width:0 text column fixes it at
   * the mechanism. */
  flex: 0 0 auto;
  display: inline-flex; align-items: center; gap: 4px;
  background: color-mix(in srgb, var(--accent) 14%, var(--card));
  color: var(--accent);
  border-radius: 999px; padding: .12rem .5rem .12rem .38rem;
  font-size: var(--fs-label); font-weight: 400; letter-spacing: .01em;
  text-transform: lowercase; white-space: nowrap;
}
.cadence-badge-face { display: inline-flex; align-items: center; gap: 4px; }
.cadence-badge .cad-ico { width: 13px; height: 13px; flex: 0 0 auto; stroke-width: 1.6; }
.cadence-badge .cad-word { white-space: nowrap; }
.schedule-cadence { text-transform: none; }
.schedule-cadence .cad-varying { font-style: normal; }
.schedule-cadence .cadence-frequency { font-style: italic; }
[data-theme="dark"] .cadence-badge { background: color-mix(in srgb, var(--accent) 24%, var(--card)); }
/* Cadence and AUTO are one status family: identical fill and block height. */
:is(.cadence-badge, .sched-autoapprove-ico) {
  box-sizing: border-box;
  min-height: 1.45rem;
  background: var(--control-secondary-bg);
}
.cadence-badge.once { background: var(--chip); color: var(--ink-muted); }
/* The hit box is grown out of flow by the `.reg-recur::after` target defined
 * with the badge's own touch rule above, so the badge's border box never
 * exceeds its row. This rule owns only the touch PAINT. */
body:is([data-surface="phone"], [data-surface="tablet"]) .cadence-badge.reg-recur {
  position: relative; gap: 0;
  background: transparent; color: var(--accent);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .cadence-badge.reg-recur .cadence-badge-face {
  padding: .04rem .4rem .04rem .3rem; border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 14%, var(--card));
}
[data-theme="dark"] body:is([data-surface="phone"], [data-surface="tablet"]) .cadence-badge.reg-recur .cadence-badge-face {
  background: color-mix(in srgb, var(--accent) 24%, var(--card));
}

/* ---- transfer row on Scheduled (operator 2026-07-14) ----
 * FROM ⇢ TO, the direction carried by one blurple chevron between the two
 * account names. The row is a 3-track flex: the names take the flexible space
 * (and ellipsise if they must), while the cadence badge and the amount are both
 * flex-none — so neither can ever be pushed out or clipped by a long name. */
.sched-xfer-row .sched-txn-main { flex: 1 1 auto; min-width: 0; }
.sched-txn-row .sched-txn-amt { flex: 0 0 auto; }
/* Two account names + a chevron. If the pair genuinely cannot fit on one line
 * (long names on a narrow phone) the line WRAPS — the operator's rule is that a
 * name is never truncated, and "Savings Nala" clipped to "Savings Nal…" is exactly
 * the failure they photographed. The chevron stays glued to the names either way. */
.xfer-line {
  display: flex; align-items: center; gap: 7px; min-width: 0;
  font-weight: 600; flex-wrap: wrap; row-gap: 2px;
}
.xfer-acct { min-width: 0; }
/* The chevron: two strokes, the leading one at a light tint and the trailing one
 * at full blurple — an "alternately light and regular tint" mark (operator) that
 * reads as motion in one direction rather than a static arrowhead. */
/* Sizing + alignment (operator 2026-07-18, iPhone screenshot): the glyph was a fixed 17px box
 * whose chevron ink painted ~8px — visibly shorter than the names beside it — and on the PHONE
 * row it rode ~2px high: `.txn-payee` is a nowrap INLINE span (it cannot become flex without
 * losing truncation), so "inline-flex + centre" never applied there; the box's bottom edge sat
 * on the text baseline. Both fixed with font-true units: the ink fills 46% of the viewBox, so a
 * 1.55em box paints the chevrons at ≈ cap height, and the cap-based vertical-align centres the
 * box on the capital letters' midline in inline contexts while staying inert in flex cells
 * (desktop .c-payee still centres by flex). Negative side margins only trim the box's internal
 * whitespace — this is a non-interactive glyph, so no hit-target rule applies. */
.xfer-chev { flex: 0 0 auto; display: inline-flex; color: var(--accent);
  vertical-align: calc((1cap - 1.55em) / 2); margin: 0 -.14em; }
.xfer-chev .ico { width: 1.55em; height: 1.55em; stroke-width: 2.1; }
.xfer-chev .xfer-chev-1 { opacity: .38; }
.xfer-chev .xfer-chev-2 { opacity: 1; }
/* A transfer moves money without spending it — neither red nor green. */
.sched-xfer-row .sched-txn-amt { color: var(--ink); }

/* Double-entry explainer atop the transfer sheet (operator 2026-07-14: "make it
 * clear to the user that this is double entry"). Friendly, plain-English, and
 * carries the same chevron mark the Scheduled row uses, so the one row and the
 * two legs are visibly the same idea. */
.xfer-note {
  display: flex; gap: 10px; align-items: flex-start;
  background: color-mix(in srgb, var(--accent) 9%, var(--card));
  border-radius: 12px; padding: .6rem .7rem; margin-bottom: .2rem;
}
[data-theme="dark"] .xfer-note { background: color-mix(in srgb, var(--accent) 18%, var(--card)); }
.xfer-note-mark { flex: 0 0 auto; display: inline-flex; color: var(--accent); margin-top: 1px; }
.xfer-note-mark .ico { width: 20px; height: 20px; stroke-width: 2.1; }
.xfer-note-mark .xfer-chev-1 { opacity: .38; }
.xfer-note-txt { font-size: var(--fs-label); line-height: 1.35; color: var(--ink-muted); }
.xfer-note-txt b { display: block; color: var(--ink); font-weight: 700; }

/* Upcoming occurrence dates expanded under each repeat rule (operator
 * 2026-07-08j). Originally always visible; operator 2026-07-15: "the dates
 * pills are clogging up the list" — now hidden behind the .sched-occ-toggle
 * chevron below and only shown once a row is expanded. The `Next:` date in
 * .sched-row-next stays visible at all times regardless. */
.sched-occ-list {
  display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
  padding: .3rem 0 .3rem 2px; margin-top: 0;
}
.sched-occ-list[hidden] { display: none; }
.sched-occ {
  display: inline-flex; align-items: center;
  border: 1px solid var(--hairline); background: transparent; color: var(--accent);
  border-radius: 8px; padding: .1rem .5rem;
  font-size: var(--fs-label); font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* Chevron expander (operator 2026-07-15): a small, subtle icon button at the
 * end of the row that toggles .sched-occ-list without triggering the row's
 * own click (which opens the edit sheet) — see the stopPropagation() in
 * scheduled.js. Tokened for dark/light parity (rule 14); rotates 180° when
 * expanded so it always points toward the (now visible/hidden) list. */
.sched-occ-toggle {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; margin: 0; padding: 0; border: 0; border-radius: 8px;
  background: none; color: var(--ink-muted); cursor: pointer;
}
/* Touch: 28px square measured on every iPad profile (v1.29.0 tapthru §1) —
 * grow the box to the 44px floor, glyph stays 18px. Same pattern as .rank-omit. */
body[data-surface="phone"] .sched-occ-toggle,
body[data-surface="tablet"] .sched-occ-toggle { width: var(--ctl-h-md); height: var(--ctl-h-md); }
.sched-occ-toggle .ico { width: 18px; height: 18px; transition: transform var(--dur-base) var(--ease-ios-rebound); }
.sched-occ-toggle.open .ico { transform: rotate(180deg); }
.sched-occ-toggle:focus-visible { background: var(--chip); outline: none; }
@media (hover: hover) { .sched-occ-toggle:hover { background: var(--chip); color: var(--ink); } }

/* schedule add/edit sheet extras (reuses .sheet-*, .field-*, .picker-* chrome) */
.sched-head { display: flex; align-items: center; gap: 10px; }
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .sched-head {
  display: grid; gap: initial;
}
.sheet-title { flex: 1; font-weight: 700; font-size: var(--fs-value); }
/* Live amount READOUT in the sheet header — NOT a control (operator 2026-07-17:
 * "has a red REMINDER (that changes to VARIES) that does nothing when tapped.
 * what does it do?"). It mirrors the Amount field below: $0 reads "Reminder",
 * stepped mode reads "Varies", otherwise the signed figure. It answered no tap
 * because it is a <span>, yet it LOOKED tappable — bold ink in alarm-red, the
 * same treatment the app gives its chips and destructive buttons. The question
 * was the bug report: an element that looks like a button and isn't, is broken
 * whichever way it is fixed.
 *
 * Fixed by making it read as metadata rather than a control: the word states
 * (Reminder / Varies) are muted, lighter and italic — plainly a description of
 * the amount, not a thing to press. A real FIGURE keeps the red/green
 * outflow/inflow semantics, because there the colour carries meaning (money out
 * vs money in) rather than false affordance. */
.sched-amt-preview { font-weight: 700; font-variant-numeric: tabular-nums; color: var(--ink); }
.sched-amt-preview.spending { color: var(--neg); }
.sched-amt-preview.inflow { color: var(--pos); }
.sched-amt-preview.is-word {
  font-weight: 500; font-style: italic; color: var(--ink-muted);
}
.sched-section { padding: .5rem 0 .2rem; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; font-size: var(--fs-label); }
.sched-num-wrap { display: flex; align-items: baseline; gap: 8px; justify-content: flex-end; flex: 1; }
.sched-num-inp { width: 4.5rem; text-align: right; }
.sched-num-hint { font-size: var(--fs-label); text-align: right; }
.sched-weekdays { flex-wrap: wrap; }
.sched-weekday-chips { display: flex; gap: 4px; flex-wrap: wrap; justify-content: flex-end; }
.sched-chip { background: var(--chip); border: 0; border-radius: 999px; font: inherit; font-size: var(--fs-label); padding: .3rem .6rem; cursor: pointer; color: var(--ink); }
.sched-chip.on { background: var(--accent); color: var(--accent-ink); }
.cal-clear { align-self: stretch; margin-top: 6px; }

/* Repeat editor footer parity: Cancel and Save share one responsive action row
 * and derive their geometry from the same control tokens on every surface. */
.sched-sheet .sheet-footer { display: flex; gap: 10px; }
.sched-sheet .sheet-footer .btn {
  flex: 1; padding: .95rem 1.6rem; font-size: var(--fs-h3);
  border-radius: 16px; min-height: var(--ctl-h-lg);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .sheet-footer { flex-wrap: wrap; }
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .sheet-footer .sched-enter-now {
  flex: 1 1 100%; width: 100%; order: -1;
  background: var(--control-secondary-bg); color: var(--control-accent); border-color: transparent;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .sheet-footer .btn:not(.sched-enter-now) { flex: 1 1 0; }
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .sched-head .sched-amt-preview {
  font-size: var(--fs-display); line-height: 1.05;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .sheet-fields.card {
  border-radius: var(--radius-card-touch); border-color: transparent;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .sched-amt-row + .field-row { display: none; }
/* A locked field row (Category on a tracking/transfer schedule): looks like a
 * field row but is inert, muted, and shows no chevron. Token-only (both themes). */
.field-disabled { cursor: default; opacity: .72; }
.field-disabled .field-value { color: var(--ink-muted); }
/* The concrete "You'll be reminded on <date>" promise under the resurface note. */
.sched-review-date { padding: 4px 2px 0; color: var(--ink); }
.sched-review-date strong { font-variant-numeric: tabular-nums; }

/* ---- Repeats as an inline pill sentence (phone / tablet), iOS-Shortcuts style
 * (operator 2026-07-15). Plain text flows with tappable value pills; the pills
 * are the only interactive parts. Token-only for dark/light parity. ---- */
.sched-recur-sentence {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px;
  padding: 6px 2px 2px; line-height: 1.9; font-size: var(--fs-body);
}
.sched-recur-sentence .recur-txt { color: var(--ink-muted); }
.recur-pill {
  font: inherit; font-weight: 700; cursor: pointer; border: 0;
  background: color-mix(in srgb, var(--accent) 16%, var(--card));
  color: var(--accent); border-radius: 999px; padding: .28rem .7rem;
  min-height: var(--ctl-h-sm, 32px); font-variant-numeric: tabular-nums;
  transition: transform var(--dur-fast) var(--ease-ios-rebound), filter var(--dur-fast) var(--ease-standard);
}
.recur-pill:active { transform: scale(.95); filter: brightness(.93); }
.recur-pill.recur-unit, .recur-pill.recur-day, .recur-pill.recur-month, .recur-pill.recur-days {
  /* the "kind"/word pills read as a choice, not a number — a touch softer */
  background: color-mix(in srgb, var(--accent) 16%, var(--card)); color: var(--accent);
}
/* Number wheel: a tall scrollable list, current value centred on open. */
.number-wheel .wheel-list { max-height: 46vh; overflow-y: auto; }
.wheel-row { font-variant-numeric: tabular-nums; }
.wheel-weekdays { justify-content: center; padding: 10px 0; }
.wheel-done { width: 100%; margin-top: 6px; }

/* ---------- stepped (varying-amount) Repeat editor ---------- */
.sched-stepped-toggle { display: flex; align-items: center; justify-content: space-between; }
.sched-stepped-help { margin: 2px 0 6px; }
.sched-segments { display: flex; flex-direction: column; gap: 8px; }
.seg-row {
  display: grid;
  grid-template-columns: 1.4rem 1fr 5.2rem 1.8rem;
  grid-template-areas: "idx amt mon del" "idx range range range";
  align-items: end; gap: 6px 8px;
  padding: 8px; border: 1px solid var(--hairline); border-radius: var(--radius-s);
}
.seg-idx { grid-area: idx; align-self: center; font-size: 1.15em; font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }
.seg-field { grid-area: amt; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.seg-field-mon { grid-area: mon; }
.seg-field .inp { width: 100%; }
.seg-mon { text-align: center; }
.seg-del { grid-area: del; align-self: center; background: none; border: 0; color: var(--ink-muted); font-size: var(--fs-value); cursor: pointer; padding: .3rem; line-height: 1; }
.seg-del:disabled { opacity: .3; cursor: default; }
@media (hover: hover) { .seg-del:not(:disabled):hover { color: var(--neg); } }
.seg-range { grid-area: range; }
/* "+ Add amount step" — breathing room above AND below (operator 2026-07-17:
 * "the add amount step button does not have sufficient spacing with the stuff
 * above and below it (visual inconsistency)"). It sat on margin-top:2px with no
 * bottom margin at all, so it crowded the last step row above it and collided
 * with the "Starts" row below — every other control in this card is separated by
 * the card's own rhythm. 10px matches the .sched-segments gap (8px) plus the
 * card's row padding, so the button reads as its own beat rather than as part of
 * the step list or the row beneath. */
.sched-seg-add { align-self: flex-start; margin: 10px 0 12px; }

/* ---------- onboarding overlay ---------- */
/* Restyled 2026-07-12 (operator: "less Claudey, more YNAB"): the flow lives on
 * a proper app card — logo mark, tight wordmark, full-width accent buttons —
 * instead of loose text on the page canvas. Tokens only; both themes. */
#onboarding {
  min-height: 100dvh; display: grid; place-items: center; padding: 1.5rem;
}
#onboarding main { max-width: 26rem; text-align: center; }
#onboarding main.ob-card {
  background: var(--card); border: 1px solid var(--hairline);
  border-radius: 24px; padding: 2rem 1.6rem;
  box-shadow: 0 10px 40px rgba(0, 0, 0, .10);
}
.ob-logo { display: block; margin: 0 auto .4rem; border-radius: 18px; }
.ob-wordmark { margin: 0 0 1.2rem; font-size: var(--fs-h1); letter-spacing: -.02em; }
#onboarding .ob-card .btn { display: block; width: 100%; padding: .85rem 1.2rem; font-size: var(--fs-value); }
#onboarding .member-setup { text-align: left; }

/* Settings: Family & data actions get real air between them (operator
 * 2026-07-11: guide + migrate buttons were glued together). */
/* "Where your data is kept" (operator 2026-07-14). A tinted, quietly-bordered
 * block so the portability answer reads as a standing fact about the family's
 * data, not one more setting to fiddle with. Theme tokens only — correct in
 * dark and light without a second rule. */
.drive-where {
  margin: .2rem 0 .6rem;
  padding: 12px 14px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-card-desktop);
  background: color-mix(in srgb, var(--accent) 5%, transparent);
}
.drive-where h3 { margin: 0 0 .4rem; font-size: var(--fs-h3); }
.drive-where p { margin: 0 0 .5rem; font-size: var(--fs-body); }
.drive-where p:last-child { margin-bottom: 0; }

.data-actions { display: flex; flex-direction: column; align-items: flex-start; gap: 12px; margin: .4rem 0 .8rem; }
.data-actions .data-action-row { display: flex; align-items: center; }
.device-row .device-remove { flex: 0 0 auto; font-size: var(--fs-body); padding: .4rem .9rem; }

/* Danger zone: nuke icon inherits the row's red; the confirm dialogs share the
 * picker chrome with a red header block. */
.danger-h { color: var(--neg); }
.danger-row { gap: 12px; }
.danger-row .danger-ico { flex: 0 0 auto; display: inline-flex; color: var(--neg); }
.danger-row .danger-ico .ico { width: 2em; height: 2em; }
body[data-surface] .btn.nuke-btn { flex: 0 0 auto; min-width: 6.5rem; }
.nuke-backdrop { z-index: 80; align-items: center; }
.nuke-modal { padding: 18px; gap: 10px; }
.nuke-modal .nuke-head { display: flex; align-items: center; gap: 10px; color: var(--neg); }
.nuke-modal .nuke-head h3 { margin: 0; font-size: var(--fs-h3); }
.nuke-modal .nuke-head .ico { width: 2.2em; height: 2.2em; }
.nuke-modal p { margin: .2rem 0; font-size: var(--fs-body); }

/* Settings select: roomy line box so descenders ("Spending"'s g) never clip. */
.setting-row select.inp { line-height: 1.4; padding-top: .5rem; padding-bottom: .5rem; height: auto; }
/* Horizontal room too (operator 2026-07-12): the row is a flex with the label on
 * the left, and .inp sets min-width:0, so the select was squeezed narrow enough to
 * clip "Spending" at the g. Give it a floor wide enough for the longest option +
 * the native dropdown arrow, and let it keep its intrinsic width, so no option
 * label is ever truncated. */
.setting-row select.inp { min-width: 9.5rem; width: auto; }
/* Settings lozenges own a centred value rail, including the native option
 * popup's selected text; labels and controls stay aligned on touch and desktop. */
.setting-row select.inp,
.ps-field select.inp { text-align: center; text-align-last: center; }
.close-fallback-select { min-width: 10rem; }
body.busy button { opacity: .6; pointer-events: none; }
#status-note { margin-top: 1rem; padding: .6rem .9rem; border-radius: var(--radius-s); background: var(--chip); }
#status-note.error { background: var(--neg-pill); color: var(--neg); }
details { text-align: left; margin-top: 1rem; color: var(--ink-muted); }
summary { cursor: pointer; color: var(--accent); }

/* ---------- guided first-run (D-031 onboarding rebuild) ---------- */
/* Step indicator: 1 sign in · 2 your budget · 3 who's here. Tokens only so it
   reads in both themes; the connector line uses --hairline / --accent. */
.ob-steps {
  list-style: none; display: flex; justify-content: center; gap: 0;
  margin: 0 0 1.4rem; padding: 0;
}
.ob-steps li { display: flex; flex-direction: column; align-items: center; flex: 1; max-width: 6rem; position: relative; }
/* Connector line into each dot (drawn on every li except the first). Accent when
   the flow has reached this step (done or active), else the neutral hairline. */
.ob-steps li + li::before {
  content: ""; position: absolute; top: 0.85rem; right: 50%; left: -50%;
  height: 2px; background: var(--hairline); z-index: 0;
}
.ob-steps li.done::before, .ob-steps li.active::before { background: var(--accent); }
.ob-dot {
  position: relative; z-index: 1; width: 1.7rem; height: 1.7rem; border-radius: 50%;
  display: grid; place-items: center; font-size: var(--fs-body); font-weight: 700;
  background: var(--chip); color: var(--ink-muted); border: 2px solid transparent;
}
.ob-steps li.active .ob-dot { background: var(--accent); color: var(--accent-ink); }
.ob-steps li.done .ob-dot { background: var(--card); color: var(--accent); border-color: var(--accent); }
.ob-step-label { font-size: var(--fs-label); margin-top: .35rem; color: var(--ink-muted); }
.ob-steps li.active .ob-step-label { color: var(--ink); font-weight: 600; }

.ob-lead { margin-bottom: 1.1rem; }
.welcome-once { color: var(--ink-muted); font-size: var(--fs-body); margin-top: .8rem; }
/* "Already set up elsewhere? use the same button" hint (operator 2026-07-15).
   Tinted like an actionable note, not body copy, so a returning user's eye lands
   on it. Token-only for dark/light parity. */
.welcome-returning {
  text-align: left; margin-top: 1rem; padding: .7rem 1rem;
  font-size: var(--fs-body); color: var(--ink);
  background: color-mix(in srgb, var(--accent) 9%, var(--card));
  border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--hairline));
  border-radius: var(--radius-s);
}

/* Always-visible caution / info callout (the "unverified app" heads-up, and the
   "you're joining the family budget" reframe). Left-aligned for readability. */
.ob-callout {
  text-align: left; background: var(--chip); border-radius: var(--radius-s);
  border-left: 3px solid var(--accent); padding: .8rem 1rem; margin: 1.1rem 0;
}
.ob-callout p { margin: .4rem 0 0; font-size: var(--fs-body); }
.ob-callout p:first-child { margin-top: 0; }
.ob-callout-head { font-weight: 700; color: var(--ink); }

/* Helper line under each choice button on the "first or joining?" screen. */
.ob-choice-help { color: var(--ink-muted); font-size: var(--fs-body); margin: .35rem 0 1rem; }
#budget-choice-list { display: grid; gap: 10px; margin-top: 14px; }
#budget-choice-list .btn { width: 100%; }

.budget-recovery-progress {
  position: fixed;
  inset: 0;
  z-index: 9998;
  display: grid;
  place-items: center;
  padding: 24px;
  background: var(--canvas);
}

.budget-recovery-card {
  width: min(520px, 100%);
  display: flex;
  align-items: flex-start;
  gap: 16px;
  padding: 24px;
  color: var(--ink);
  background: var(--card);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-s);
}

.budget-recovery-card h2 { margin: 0 0 8px; }
.budget-recovery-card p { margin: 0; color: var(--ink-muted); }

/* First-sync progress card inside the Setup Hub (second-device join). Reuses the
 * recovery spinner; laid out as a spinner + text row inside the standard card. */
.setup-hub-loading-row { display: flex; align-items: flex-start; gap: 16px; }
.setup-hub-loading-row h2 { margin: 0 0 8px; }
.setup-hub-loading-row p { margin: 0; }

.budget-recovery-spinner {
  width: 24px;
  height: 24px;
  flex: 0 0 auto;
  border: 3px solid var(--hairline);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: budget-recovery-spin .8s linear infinite;
}

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

@media (prefers-reduced-motion: reduce) {
  .budget-recovery-spinner { animation: none; border-top-color: var(--hairline); }
}
.ob-more-link { margin-top: 1rem; }
.ob-privacy summary { font-size: var(--fs-body); }

/* ---------- plan-name menu (§11.6) ---------- */
.plan-menu { min-width: 220px; }

/* ---------- Display Options modal (§8/§11.6) ---------- */
:root { --do-bar: #ececf1; }
[data-theme="dark"] { --do-bar: #3a3a4a; }
.display-options-modal { width: min(620px, 96vw); max-height: 88dvh; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 6px; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 60; }
.display-options-modal .picker-head { justify-content: space-between; }
.do-card-row { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-bottom: 6px; }
.do-card {
  flex: 1; min-width: 0; background: var(--card); border: 2px solid var(--hairline); border-radius: 10px;
  padding: 6px; cursor: pointer; font: inherit; color: inherit; display: flex; flex-direction: column; gap: 6px;
}
.do-card.sel { border-color: var(--accent); }
.do-theme-svg, .do-progress-svg { width: 100%; border-radius: 6px; display: block; }
.do-card-label { display: flex; align-items: center; justify-content: center; gap: 5px; min-height: 2.4em; font-size: var(--fs-label); font-weight: 600; text-align: center; }
/* Balance Style choice-row CSS removed with the feature (operator 2026-07-12 punch 16). */
.do-radio { width: 14px; height: 14px; border-radius: 50%; border: 2px solid var(--hairline); flex-shrink: 0; }
.do-card.sel .do-radio { border-color: var(--accent); background: radial-gradient(var(--accent) 0 40%, transparent 42%); }

/* ---------- Plan Settings modal (§11.6) ---------- */
.plan-settings-modal { width: min(460px, 94vw); padding: 16px; display: flex; flex-direction: column; gap: 12px; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 60; }
.plan-settings-modal .picker-head { justify-content: space-between; }
.ps-field { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; }
.ps-label { font-size: var(--fs-label); color: var(--ink-muted); font-weight: 600; }
.ps-row { display: flex; gap: 12px; }
.plan-settings-section { margin: 8px 0 14px; padding-top: 8px; border-top: 1px solid var(--hairline); }
.plan-settings-section h3 { margin: 0 0 4px; font-size: var(--fs-body); }
.plan-settings-inline { display: flex; flex-direction: column; gap: 10px; }
.ps-inline-grid { display: grid; grid-template-columns: minmax(0, 1.2fr) repeat(2, minmax(0, 1fr)); gap: 10px; }
@media (max-width: 620px) {
  .ps-inline-grid { grid-template-columns: 1fr; }
}

/* ---------- Edit Flags modal (§11.5) ---------- */
.flags-modal { width: min(360px, 94vw); padding: 16px; display: flex; flex-direction: column; gap: 12px; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 60; }
.flags-modal .picker-head { justify-content: space-between; }
.picker-x { background: none; border: 0; font-size: var(--fs-h2); line-height: 1; cursor: pointer; color: var(--ink-muted); }
.flags-list { display: flex; flex-direction: column; gap: 8px; }
.flag-row { display: flex; align-items: center; gap: 10px; }
.flag-label-input { flex: 1; }
.flag-swatch { width: 18px; height: 18px; border-radius: 4px; flex-shrink: 0; }
.flag-red { background: var(--flag-red); }
.flag-orange { background: var(--flag-orange); }
.flag-yellow { background: var(--flag-yellow); }
.flag-green { background: var(--flag-green); }
.flag-blue { background: var(--flag-blue); }
.flag-purple { background: var(--flag-purple); }

/* ---------- Manage Payees modal (§11.5) ---------- */
.payees-modal {
  width: min(760px, 96vw); max-height: 88dvh; padding: 16px; display: flex; flex-direction: column;
  gap: 10px; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 60;
}
.payees-modal .picker-head { justify-content: space-between; }
.payees-body { display: flex; gap: 16px; flex: 1; min-height: 0; }
.payees-left { width: 260px; flex-shrink: 0; display: flex; flex-direction: column; gap: 8px; border-right: 1px solid var(--hairline); padding-right: 12px; }
.payee-add-panel { display: flex; flex-direction: column; gap: 6px; padding: 8px; border: 1px solid color-mix(in srgb, var(--accent) 32%, var(--hairline)); border-radius: var(--radius-s); background: color-mix(in srgb, var(--accent) 6%, var(--card)); }
.payee-add-row { display: flex; gap: 6px; align-items: center; }
.payee-add-input { min-width: 0; flex: 1 1 auto; }
.payee-add-submit { flex: 0 0 auto; white-space: nowrap; }
.payees-count { font-size: var(--fs-label); font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
.payees-list { flex: 1; overflow-y: auto; }
.payees-list .pick-row { display: flex; align-items: center; gap: 8px; }
.payees-list .pick-row.payee-hidden { opacity: .55; }
.payee-select-mark {
  width: 1.2rem; height: 1.2rem; flex: 0 0 1.2rem;
  display: inline-grid; place-items: center;
  border: 1px solid var(--hairline); border-radius: 50%;
  color: var(--accent-ink); background: transparent; font-weight: 800;
}
.payees-list .pick-row.sel .payee-select-mark {
  border-color: var(--accent); background: var(--accent);
}
.payees-right { flex: 1; min-width: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; }
.payees-empty { display: flex; align-items: center; justify-content: center; height: 100%; }
.payee-name-input { font-size: var(--fs-h3); font-weight: 700; width: 100%; }
.payees-section-h { margin: 8px 0 0; font-size: var(--fs-body); text-transform: uppercase; letter-spacing: .04em; color: var(--ink-muted); }
.payee-merge { display: flex; flex-direction: column; gap: 8px; }
.payee-merge-selected {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 8px 10px; border-radius: var(--radius-s);
  background: color-mix(in srgb, var(--accent) 10%, var(--card));
  border: 1px solid color-mix(in srgb, var(--accent) 34%, var(--hairline));
}
.payee-merge-selected strong { color: var(--accent); text-align: right; }
.payee-merge-row {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
}
.payee-match {
  flex: 0 0 auto; padding: .12rem .5rem; border-radius: 999px;
  background: var(--chip); color: var(--accent); font-size: var(--fs-label); font-weight: 700;
}

/* Collapsible Settings section (operator 2026-07-13): tappable header that folds
 * a long section (Favourite categories, Devices) away by default. Header styled
 * like a .payees-section-h so it reads as the same kind of section label, plus a
 * caret and an optional count badge. Full-width tap target, tokens only (rule
 * 14) so both themes read. */
.settings-collapse { margin-top: 8px; }
.settings-collapse-h {
  display: flex; align-items: center; gap: 8px; width: 100%;
  background: none; border: 0; cursor: pointer; padding: .35rem 0;
  color: var(--ink-muted); font: inherit; font-size: var(--fs-body);
  text-transform: none; letter-spacing: normal; text-align: left;
  -webkit-tap-highlight-color: transparent;
}
.settings-collapse-caret { flex: 0 0 auto; width: 1rem; color: var(--accent); display: inline-flex; align-items: center; justify-content: center; transition: transform var(--dur-base) var(--ease-standard); }
.settings-collapse-caret .ico { width: 1rem; height: 1rem; }
.settings-collapse.open .settings-collapse-caret { transform: rotate(90deg); }
.settings-collapse-title { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Count badge — visible while the section is collapsed (e.g. device count). */
.settings-collapse-count {
  flex: 0 0 auto; min-width: 1.4rem; text-align: center;
  background: var(--chip); color: var(--ink); border-radius: 999px;
  padding: .05rem .5rem; font-size: var(--fs-body); font-weight: 700; letter-spacing: 0;
  font-variant-numeric: tabular-nums;
}
.settings-collapse-sub { margin: 0 0 6px; }
.settings-collapse-body { margin-top: 2px; }
.payees-section-h .small { display: block; text-transform: none; font-weight: 400; letter-spacing: 0; margin-top: 2px; }
.small { font-size: var(--fs-body); }

/* Favourite-category chips (Settings hub). Tap to star/unstar. */
.fav-cat-list { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.fav-cat-chip {
  border: 1px solid var(--hairline); background: var(--card); color: var(--ink);
  border-radius: 999px; padding: .35rem .7rem; font: inherit; font-size: var(--fs-body);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.fav-cat-chip.on { background: var(--chip); color: var(--accent); border-color: transparent; font-weight: 600; }

/* Collapsible favourite-category groups (operator: main categories collapse,
 * expand to subcategories). Tokens only — reads in both themes (rule 14). */
.fav-groups { margin-top: 8px; display: flex; flex-direction: column; gap: 2px; }
.fav-group-head { display: flex; align-items: center; gap: 8px; padding: .3rem 0; }
.fav-group-caret {
  background: none; border: 0; cursor: pointer; color: var(--accent);
  font: inherit; width: 1.2rem; line-height: 1; padding: .2rem;
  display: inline-flex; align-items: center; justify-content: center;
  transition: transform var(--dur-base) var(--ease-standard);
  -webkit-tap-highlight-color: transparent;
}
.fav-group-caret .ico { width: 1rem; height: 1rem; }
.fav-group:not(.open) .fav-group-caret { transform: rotate(-90deg); }
.fav-group-star {
  display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0;
  background: none; border: 0; cursor: pointer; font: inherit; color: var(--ink);
  text-align: left; padding: .2rem 0; -webkit-tap-highlight-color: transparent;
}
.fav-star-glyph { color: var(--ink-muted); width: 1.1rem; text-align: center; }
.fav-group-head.starred .fav-star-glyph { color: var(--accent); }
.fav-group-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fav-group-count { flex: 0 0 auto; font-variant-numeric: tabular-nums; }
/* Subcategory chips hide until the group is expanded. */
.fav-sub-list { display: none; flex-wrap: wrap; gap: 6px; padding: 2px 0 8px 1.8rem; }
.fav-group.open .fav-sub-list { display: flex; }
.payee-cat-select { width: 100%; }
.rename-rule-row { display: flex; gap: 8px; align-items: center; }
.rename-rule-text { flex: 1; }
.rename-rule-list { margin: .3rem 0; }
.rename-rule-existing { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: .3rem 0; border-top: 1px solid var(--hairline); font-size: var(--fs-body); }
.rename-rule-existing:first-child { border-top: 0; }
.rename-rule-del { padding: 0 .5rem; line-height: 1.4; }
.payee-delete { align-self: flex-start; }
.payees-footer { justify-content: space-between; align-items: center; }
.undo-redo { display: flex; gap: 8px; }
body[data-surface="phone"] .payees-modal,
body[data-surface="tablet"] .payees-modal {
  width: min(760px, calc(100vw - 16px)); max-height: calc(100dvh - 24px);
  padding: 12px;
}
body[data-surface="phone"] .payees-body {
  flex-direction: column; gap: 10px;
}
body[data-surface="phone"] .payees-left {
  width: 100%; flex: 0 0 34%; min-height: 9rem;
  border-right: 0; border-bottom: 1px solid var(--hairline);
  padding-right: 0; padding-bottom: 10px;
}
body[data-surface="phone"] .payees-right { min-height: 12rem; }
body[data-surface="phone"] .payees-footer { flex-direction: column; align-items: stretch; }
body[data-surface="phone"] .payees-footer .undo-redo { width: 100%; }
body[data-surface="phone"] .payees-footer .undo-redo .btn { flex: 1 1 0; }
body[data-surface="phone"] .payees-footer > .btn { width: 100%; }

/* Locations (N) section in the per-payee editor (§4/§8 geo). */
/* ---------- Staged payee edit / merge workspace (23/07/2026) ------------- */
.payee-workspace-overlay { align-items: stretch !important; background: var(--canvas); }
.payee-workspace { margin: 0; overscroll-behavior: contain; }
.payee-workspace .payees-body,
.payee-workspace .payees-left,
.payee-workspace .payees-right,
.payee-workspace .payees-list { min-height: 0; overscroll-behavior: contain; }
.payee-workspace .payee-select-mark {
  width: 24px; min-width: 24px; max-width: 24px; height: 24px;
  flex: 0 0 24px; aspect-ratio: 1 / 1; border-radius: 50%;
  box-sizing: border-box; align-self: center;
}
.payee-workspace-locked .payees-right { width: 100%; }
.payee-locked-subject {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 10px 12px; border: 1px solid var(--hairline);
  border-radius: var(--radius-s); background: var(--card);
}
.payee-locked-subject strong { min-width: 0; overflow-wrap: anywhere; text-align: right; }
.payee-workspace .payee-list-name { flex: 1 1 auto; min-width: 0; }
.payee-workspace .payee-match { flex: 0 0 auto; }
.payee-edit-section { display: flex; flex-direction: column; gap: 8px; }
.payee-rename-row { display: flex; align-items: stretch; gap: 8px; }
.payee-rename-row .payee-name-input { min-width: 0; flex: 1 1 auto; }
.payee-merge-help { margin: 0; }
.payee-merge-list {
  flex: 0 0 auto; max-height: 15rem;
  /* The first/last 24px selectors need breathing room beyond their row boxes.
   * Without it the scrolling list's clip edge cut the circles into arcs. */
  padding-block: 3px;
}
.payee-workspace .payee-merge-row {
  width: 100%; align-self: stretch;
  min-height: 42px; padding-block: 7px; overflow: visible;
}
.payee-merge-row .payee-select-mark {
  border-color: var(--hairline); background: transparent; color: var(--accent-ink);
}
.payee-merge-row.sel .payee-select-mark { border-color: var(--accent); background: var(--accent); }
body[data-surface="phone"] .payee-workspace .payee-merge-row,
body[data-surface="tablet"] .payee-workspace .payee-merge-row {
  min-height: 44px;
}
.payee-merge-decision:empty { display: none; }
.payee-merge-decision {
  display: flex; flex-direction: column; gap: 10px;
  padding: 10px; border: 1px solid var(--hairline);
  border-radius: var(--radius-s); background: var(--card);
}
.payee-final-choices { display: flex; flex-direction: column; gap: 7px; }
.payee-final-choice {
  width: 100%; display: flex; align-items: center; gap: 10px;
  border: 1px solid var(--hairline); border-radius: 12px;
  padding: .7rem .8rem; background: var(--card); color: var(--ink);
  font: inherit; text-align: left; cursor: pointer;
}
.payee-final-choice.on {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, var(--card));
}
.payee-choice-dot {
  width: 20px; height: 20px; flex: 0 0 20px; border-radius: 50%;
  border: 2px solid var(--hairline); background: transparent;
}
.payee-final-choice.on .payee-choice-dot {
  border-color: var(--accent);
  background: var(--accent);
}
.payee-final-name { width: 100%; }
.payee-merge-preview {
  padding: 10px; border-radius: 10px; line-height: 1.35;
  color: var(--ink); background: color-mix(in srgb, var(--accent) 9%, var(--card));
}
.payee-apply { width: 100%; }
.payee-delete-section { padding-bottom: 8px; border-top: 1px solid var(--hairline); margin-top: 6px; }
.payee-delete-question {
  padding: 10px; border: 1px solid color-mix(in srgb, var(--neg) 35%, var(--hairline));
  border-radius: 12px; background: var(--neg-pill);
}
.payee-delete-question p { margin: 0 0 10px; }
.payee-delete-actions { display: flex; gap: 8px; }
.payee-delete-actions .btn { flex: 1 1 0; }
body[data-surface="phone"] .payee-workspace,
body[data-surface="tablet"] .payee-workspace {
  position: relative; inset: auto; top: auto; left: auto; transform: none;
  width: 100vw; max-width: none; height: 100dvh; min-height: 100dvh; max-height: 100dvh;
  border: 0; border-radius: 0;
  padding: max(10px, env(safe-area-inset-top))
           max(12px, env(safe-area-inset-right))
           max(10px, env(safe-area-inset-bottom))
           max(12px, env(safe-area-inset-left));
  background: var(--canvas); box-shadow: none;
}
body[data-surface="phone"] .payee-workspace .payees-footer,
body[data-surface="tablet"] .payee-workspace .payees-footer {
  flex-direction: row; align-items: stretch; flex: 0 0 auto;
}
body[data-surface="phone"] .payee-workspace :is(.payees-footer .btn, .payee-apply, .payee-delete),
body[data-surface="tablet"] .payee-workspace :is(.payees-footer .btn, .payee-apply, .payee-delete) {
  min-height: 4.2rem; font-size: var(--fs-h3); font-weight: 700;
}
body[data-surface="phone"] .payee-workspace .payees-footer .btn,
body[data-surface="tablet"] .payee-workspace .payees-footer .btn { width: auto; flex: 1 1 0; }
/* Touch Payee workspaces own their scroll in the body/right pane. The footer
 * is therefore a real in-flow dock: it remains visible while long edit/merge
 * content scrolls, paints an opaque face over the pane edge, and carries the
 * device bottom safe area exactly once (not underneath the home indicator). */
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-workspace {
  overflow: hidden;
  padding-bottom: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-workspace .payees-body {
  min-height: 0;
  overflow: hidden;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-workspace .payees-right {
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  padding-bottom: 8px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-workspace .payees-footer {
  position: sticky;
  bottom: 0;
  z-index: 2;
  flex: 0 0 auto;
  padding-top: 8px;
  padding-bottom: max(10px, env(safe-area-inset-bottom));
  border-top: 1px solid var(--hairline);
  background: var(--canvas);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-workspace .payees-footer > .btn {
  box-sizing: border-box;
  min-width: 0;
  min-height: 4.2rem;
  height: 4.2rem;
  flex: 1 1 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-workspace .payee-merge-row {
  width: 100%; min-width: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-workspace .payee-merge-row .payee-list-name {
  min-width: 0;
  overflow-wrap: break-word;
  word-break: normal;
  white-space: normal;
}
/* The final-name chooser is a vertical touch list, not a three-column desktop
 * rail. A full-width selected face keeps the dark-blurple/white contract;
 * unselected peers share the light-blurple rail face and blurple ink. The
 * moving indicator is intentionally absent here: in a stacked list it would
 * span the whole rail and paint a selected balloon around every option. */
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-workspace .segmented-rail.payee-final-choices {
  flex-direction: column;
  gap: 7px;
  width: 100%;
  align-items: stretch;
  border-color: transparent;
  border-radius: var(--radius-s);
  overflow: visible;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-workspace .segmented-rail.payee-final-choices > .seg-moving-indicator {
  /* Keep the canonical indicator mounted for the shared primitive, but make
   * this stacked chooser's full-rail balloon inert and visually absent. The
   * scoped visibility contract avoids changing the global segmented rail. */
  visibility: hidden;
  opacity: 0;
  width: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-workspace .segmented-rail.payee-final-choices > .payee-final-choice {
  box-sizing: border-box;
  width: 100%;
  flex: 0 0 auto;
  min-width: 0;
  min-height: var(--ctl-h-md);
  padding: .7rem .8rem;
  border: 0;
  border-radius: var(--radius-s);
  background: transparent;
  color: var(--control-secondary-ink);
  overflow-wrap: break-word;
  word-break: normal;
  white-space: normal;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-workspace .segmented-rail.payee-final-choices > .payee-final-choice[aria-selected="true"] {
  background: var(--control-selected-bg);
  color: var(--control-selected-ink);
  border-color: transparent;
}
/* Touch Payee Manager owns a safe-area stack; wide tablet landscape keeps a
 * measured master/detail split while narrow tablet and phone collapse safely. */
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager { display:flex; flex-direction:column; gap:0; padding-inline:max(12px,env(safe-area-inset-left)) max(12px,env(safe-area-inset-right)); padding-top:max(10px,env(safe-area-inset-top)); }
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-workspace-head { display:flex; align-items:center; gap:.55rem; min-height:var(--ctl-h-lg); padding-bottom:.55rem; border-bottom:1px solid var(--hairline); }
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-workspace-head > span { flex:1 1 auto; min-width:0; font-size:var(--fs-h3); font-weight:700; }
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-back, body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-add-toggle, body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-head-close { min-width:var(--ctl-h-md); min-height:var(--ctl-h-md); }
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payees-body { display:block; flex:1 1 auto; min-height:0; overflow:hidden; }
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-index, body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-detail { width:100%; height:100%; min-height:0; border:0; padding:.55rem 0; overflow:auto; }
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-row, body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-candidate-row { min-height:var(--ctl-h-lg); padding:.55rem .8rem; }
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-add-row { flex-direction:column; align-items:stretch; }
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-add-row > * { min-height:var(--ctl-h-md); }
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payees-footer { gap:.55rem; padding-bottom:max(10px,env(safe-area-inset-bottom)); }
@media (min-width:56.25rem) and (orientation:landscape) { body[data-surface="tablet"] .payee-touch-manager .payee-touch-index { width:34%; float:left; border-right:1px solid var(--hairline); padding-right:.55rem; } body[data-surface="tablet"] .payee-touch-manager .payee-touch-detail { width:calc(66% - .55rem); float:right; padding-left:.8rem; } }
@media (max-width:56.24rem), (orientation:portrait) { body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-show-index .payee-touch-detail { display:none; } body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-show-detail .payee-touch-index { display:none; } }
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-show-add .payees-list, body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-show-add .payees-count, body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-show-add .payee-list-search { display:none; }
.payee-workspace.swipe-dragging { transition: none; }
.payee-workspace.swipe-dismissing { transition: transform .36s var(--ease-standard); }
.reg-drawer .payee-workspace-inline {
  position: relative; inset: auto; top: auto; left: auto; transform: none;
  width: 100%; max-width: none; max-height: none; border: 0; border-radius: 0;
  padding: 12px; background: transparent; box-shadow: none;
}
.reg-drawer .payee-workspace-head { padding-bottom: 8px; border-bottom: 1px solid var(--hairline); }
.reg-drawer .payee-workspace .payees-body { min-height: 25rem; }
.reg-drawer .payee-workspace .payees-left { width: min(28%, 300px); }
.reg-drawer .payee-workspace .payees-right { overflow: visible; }
.reg-drawer .payee-workspace .payee-merge-list { max-height: 13rem; }
.reg-drawer .payee-workspace .payees-footer { padding-top: 10px; border-top: 1px solid var(--hairline); }
.reg-drawer .payee-workspace :is(.payees-footer .btn, .payee-apply, .payee-delete) {
  min-height: var(--ctl-h-md); font-size: var(--fs-body);
}

/* Standalone desktop Payees route: a real page, not a centred modal. */
body[data-surface="desktop"] .payee-route-header {
  align-items: flex-start; margin-bottom: 18px;
}
.payee-route-header .screen-title { margin-bottom: 4px; }
.payee-route-header .muted { margin: 0; max-width: 58rem; }
body[data-surface="desktop"] .payee-page {
  width: min(100%, 1180px); min-width: 0;
}
body[data-surface="desktop"] .payee-page .payee-workspace-inline {
  position: relative; inset: auto; transform: none; z-index: auto;
  display: flex; flex-direction: column;
  width: 100%; max-width: none; max-height: none; min-height: min(680px, calc(100dvh - 190px));
  margin: 0;
  padding: 18px; background: var(--card); border: 1px solid var(--hairline);
  border-radius: var(--radius-s); box-shadow: var(--shadow-lg);
}
body[data-surface="desktop"] .payee-page .payee-workspace-head {
  padding-bottom: 14px; border-bottom: 1px solid var(--hairline);
}
body[data-surface="desktop"] .payee-page .payees-body { min-height: 32rem; }
body[data-surface="desktop"] .payee-page .payees-footer {
  padding-top: 14px; border-top: 1px solid var(--hairline);
}

/* ---------- WIN11 payee editor: cursor-anchored flyout (operator 04/08/2026) ----------
 * The touch workspace, adapted to the Win11 workrules, REPLACING the retired
 * two-pane Manage-Payees modal. It inherits .sub-flyout's floating-card chrome
 * (the same one every in-drawer sub-picker uses) so there is exactly ONE desktop
 * popover treatment to keep in step; JS sets fixed left/top at the click point.
 * Deliberately NOT a centred dimming modal and NOT a bottom sheet — the Spending
 * no-popup invariant (SWEEP-MANIFEST v1.40.0) binds every layer of this
 * workflow, and this card is the outermost one.
 *
 * Sized against the VIEWPORT, not the content: the payee list plus the merge
 * column plus the extras block will always want more room than a laptop has, so
 * the card caps itself and scrolls internally rather than growing past the
 * bottom edge where an option becomes unclickable (D-089).
 *
 * It deliberately does NOT carry `.sub-flyout`: that class is shaped for a menu
 * (width: max-content, max-width: 420px) and would crush this two-pane editor.
 * Only the transparent dismiss catcher is shared. */
.payee-flyout {
  z-index: 79; /* over .sub-flyout-catch (78), under the sheet layer */
  display: flex; flex-direction: column;
  width: min(62rem, calc(100vw - 32px));
  max-width: calc(100vw - 32px);
  max-height: calc(100dvh - 32px);
  padding: 12px 14px;
  background: var(--card); color: var(--ink);
  border: 1px solid var(--hairline); border-radius: var(--radius);
  box-shadow: 0 12px 32px color-mix(in srgb, var(--ink) 22%, transparent);
  animation: recurring-pop-in var(--dur-base) var(--ease-ios-rebound) backwards;
}
@media (prefers-reduced-motion: reduce) {
  .payee-flyout { animation: none; }
}
/* The card itself is the frame, so the workspace inside drops the modal chrome
 * it carries for the touch overlay (.card/.payees-modal padding + shadow).
 *
 * The reset is EXPLICIT, not just `inset: auto`: the workspace node still
 * carries `.payees-modal`, which is `position: fixed; top: 50%; left: 50%;
 * transform: translate(-50%, -50%)` — a CENTRED MODAL. Left standing it would
 * rip the workspace out of this card and centre it on screen, which is exactly
 * the popup class the Win11 invariant forbids. This selector is (0,2,0) against
 * that rule's (0,1,0) and sits later in the file, so it wins twice over; every
 * property that rule sets is named here so a future reorder cannot revive it. */
.payee-flyout .payee-workspace {
  position: relative;
  top: auto; left: auto; right: auto; bottom: auto;
  transform: none; z-index: auto;
  width: 100%; max-width: none; max-height: none; min-height: 0;
  margin: 0; padding: 0; border: 0; border-radius: 0;
  background: transparent; box-shadow: none;
  display: flex; flex-direction: column; flex: 1 1 auto; gap: 0;
}
.payee-flyout .payee-workspace-head {
  padding: 0 0 8px; border-bottom: 1px solid var(--hairline); flex: none;
}
/* Two panes: the payee list on the left, the editor on the right. Both scroll
 * inside their own track so the card's height stays put while either grows. */
.payee-flyout .payees-body {
  display: flex; gap: 16px; flex: 1 1 auto; min-height: 0; overflow: hidden;
  padding-top: 10px;
}
.payee-flyout .payees-left {
  display: flex; flex-direction: column; gap: 6px;
  width: min(30%, 300px); flex: none; min-height: 0;
}
.payee-flyout .payees-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.payee-flyout .payees-right { flex: 1 1 auto; min-width: 0; min-height: 0; overflow-y: auto; }
.payee-flyout .payee-merge-list { max-height: 13rem; overflow-y: auto; }
.payee-flyout .payees-footer {
  flex: none; justify-content: flex-end;
  padding-top: 10px; border-top: 1px solid var(--hairline);
}
.payee-flyout .payee-done { width: auto; }
/* A locked subject (opened from one transaction's payee) hides the list pane
 * entirely, so the editor takes the full card. */
.payee-flyout .payee-workspace-locked .payees-right { width: 100%; }
/* A field-anchored locked editor has no list pane to justify the full
 * two-pane width. Keep it narrow enough to remain beside the field on a
 * Retina laptop rather than being clamped back to the viewport edge. */
body[data-surface="desktop"] .payee-flyout:has(.payee-workspace-locked) {
  width: min(50rem, calc(100vw - 32px));
}
/* Narrow laptop / the account rail eating the width: the two panes cannot both
 * hold their minimums, so the list pane sheds its fixed width first. */
@media (max-width: 900px) {
  .payee-flyout { width: calc(100vw - 32px); }
  .payee-flyout .payees-body { flex-direction: column; overflow-y: auto; }
  .payee-flyout .payees-left { width: 100%; max-height: 11rem; }
}

/* ---------- per-payee extras, folded in from the retired modal ---------- */
.payee-extras { border-top: 1px solid var(--hairline); padding-top: 8px; }
.payee-extras-h {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 6px 0; border: 0; background: none; color: var(--ink);
  font: inherit; font-weight: 600; text-align: left; cursor: pointer;
}
.payee-extras-caret { color: var(--ink-muted); flex: none; }
.payee-extras-h:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.payee-extras-body { display: flex; flex-direction: column; gap: 4px; padding-bottom: 4px; }
/* Inline delete confirm for a saved location: it replaces what used to be a
 * native window.confirm, which is a system popup and banned on Win11 (and
 * returns undefined inside the installed iOS PWA, so it did nothing there). */
.payee-loc-confirm {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  width: 100%; margin-top: 6px; padding-top: 6px;
  border-top: 1px solid var(--hairline);
}
.payee-loc-confirm .btn { flex: none; }

.payee-loc-list { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
/* flex-wrap so the inline delete confirm (.payee-loc-confirm) drops onto its
 * own line under the row instead of crushing the date/account meta beside it. */
.payee-loc-row { display: flex; align-items: center; justify-content: space-between; gap: 8px;
  flex-wrap: wrap;
  padding: 6px 8px; border: 1px solid var(--hairline); border-radius: 10px; }
.payee-loc-meta { font-size: var(--fs-body); line-height: 1.25; min-width: 0; }
.payee-loc-del { flex: none; }

/* "Near You" section in the Add-Transaction payee picker (§4). */
.near-you { border-bottom: 1px solid var(--hairline); margin-bottom: 4px; padding-bottom: 4px; }
.near-you-head { font-size: var(--fs-label); text-transform: uppercase; letter-spacing: .05em;
  color: var(--ink-muted); padding: .4rem .4rem .2rem; }
/* Rows mirror the reference screenshot (operator 2026-07-14): a plain card list
 * of payee names, each with its DISTANCE right-aligned ("Here", "10 m", "1.2 km").
 * No leading pin — the screenshot has none, and the name is the thing being read.
 * The Payments-and-Transfers section of the reference is deliberately NOT built
 * (operator: "omit entirely"). */
.near-you-row { display: flex; align-items: center; gap: 6px; }
.near-you-row .near-you-pick { flex: 1; min-width: 0; text-align: left; background: none; border: 0;
  font: inherit; padding: .7rem .4rem; cursor: pointer; color: inherit;
  display: flex; align-items: center; gap: .5rem; overflow: hidden; }
/* min-width:0 lets a long payee name ellipsis instead of shoving the distance
 * off the row (the flexbox overflow trap). */
.near-you-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.near-you-dist { flex: 0 0 auto; margin-left: auto; font-size: var(--fs-body);
  color: var(--ink-muted); font-variant-numeric: tabular-nums; }
/* "Here" is the useful one — it earns the accent and the weight. */
.near-you-dist.here { color: var(--accent); font-weight: 700; }
/* The old always-visible location-arrow toggle was REPLACED 2026-07-15 by the
 * swipe-to-reveal "Forget Location" action (.near-you-swipe / .swipe-forget
 * above) mimicking the reference iOS UX. The Payee FIELD-row keeps its own
 * .payee-geo toggle — that is a different control and stays. */

/* Geomemory TOGGLE on the Payee FIELD ROW in the Add sheet (operator 2026-07-12,
 * redefined 2026-07-14): a blurple location-arrow PILL shown for every chosen
 * payee when geo is on. ON by default (auto-remember) → tinted fill and a
 * BOLD-LINED arrow; OFF → muted outline, normal stroke. The × is GONE (operator:
 * "remove the 'x' sign") — the pill itself is the toggle, and a × on an icon read
 * as "close this" rather than "stop remembering". The globe is deprecated. Sits
 * just left of the row's trailing chevron. */
/* SHAPE CORRECTED to the operator's reference icon (29/07/2026). The pill was an
 * OUTLINED capsule — a 2px accent ring around a transparent (ON: faintly tinted)
 * centre. The reference is the opposite: a SOLID FILLED lozenge carrying a
 * knocked-out white arrow, with a small circled-x riding at its right end. So
 * the fill now does the talking (blurple when active, grey when not), the border
 * is gone, and the glyph is painted in the pill's own foreground colour. */
.payee-geo { flex: none; position: relative; margin-right: 6px; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center; gap: 3px;
  min-width: 52px; height: 30px; padding: 0 7px; border-radius: 999px;
  border: 0;
  transition: background var(--dur-fast) var(--ease-standard), opacity var(--dur-fast) var(--ease-standard); }
/* ON — on-theme blurple, solid. OFF — the grey of the screenshot. Both are
 * tokens, so dark/light parity holds (rule 14: never a hardcoded grey). */
.payee-geo.on { background: var(--accent); }
.payee-geo.off { background: color-mix(in srgb, var(--ink-muted) 55%, transparent); }
.payee-geo-arrow { display: inline-flex; align-items: center; justify-content: center; }
/* The arrow is knocked OUT of the filled lozenge in both states — it is the
 * lozenge's fill that carries active/inactive, not the glyph's colour. */
.payee-geo .payee-geo-arrow .ico { width: 1.15em; height: 1.15em; color: var(--accent-ink); }
/* The circled-x at the right end of the reference lozenge. Decorative: the whole
 * pill is still the single toggle (operator 2026-07-14, "remove the 'x' sign"
 * referred to a SEPARATE clickable x, not to this glyph inside the capsule). */
.payee-geo-x { display: inline-flex; align-items: center; justify-content: center; }
.payee-geo .payee-geo-x .ico { width: .95em; height: .95em; color: var(--accent-ink); opacity: .85; }
/* "Bold lined" when enabled (operator 2026-07-14): the whole glyph thickens —
 * the icons are stroked SVGs, so the weight lives in stroke-width, not fill.
 * Thickened further 2.6 → 3.2 (item 6, operator 2026-07-15: "thicker when on").
 * OFF dropped from --ink-muted (still fairly dark) to a genuinely faint
 * color-mix tint — "much fainter grey" — while staying a token so dark/light
 * parity holds (rule 14: never a hardcoded grey hex). Still clearly tappable:
 * it's the control that turns geo back ON, verified in both themes. */
/* The old ON/OFF glyph-recolour + stroke-thickening rules are GONE on purpose.
 * They dated from the outlined-pill design, where the arrow itself had to carry
 * the state. The reference puts the state in the lozenge FILL and keeps the
 * arrow knocked-out white in both, so recolouring the glyph here would undo the
 * very thing being matched. (The stroke-width was inert regardless: nav-arrow is
 * declared fill="currentColor" stroke="none" in icons.js.) */

/* ---------- payee transactions popup (§11.5) ---------- */
.txns-popup {
  width: min(560px, 92vw); max-height: 70dvh; padding: 14px; display: flex; flex-direction: column;
  gap: 10px; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 70;
  background: var(--card); border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,.28);
}
.txns-popup .picker-head { justify-content: space-between; }
.txns-popup-table { overflow-y: auto; font-size: var(--fs-body); }
.txns-popup-head, .txns-popup-row { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, .9fr) minmax(0, 1.2fr) minmax(0, 1.4fr) minmax(0, .8fr); gap: 8px; padding: .4rem 0; border-top: 1px solid var(--hairline); }
.txns-popup-row > * { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.txns-popup-head { border-top: 0; color: var(--ink-muted); font-size: var(--fs-label); text-transform: uppercase; }
.txns-popup-row span:last-child { text-align: right; }

.picker-footer-link { color: var(--accent) !important; font-weight: 600; text-align: center !important; }
.picker-foot-link { color: var(--accent); font-weight: 600; text-align: center; border-top: 1px solid var(--hairline); }

/* ---------- mobile UI + offline batch (2026-07-07c) ---------- */

/* Tab bar now serves phone AND tablet, bottom-fixed in every orientation
 * (operator: iPad-landscape toolbar used to jump to the top). Docked flush
 * full-width (2026-07-08); dark mode gets a lighter hairline so it still
 * reads as separate from the canvas without the old floating-pill shadow. */
/* Give tablet content room above the bottom bar — include the home-indicator
 * safe-area like the phone rule does, so the last rows never slide under the
 * tab-bar/FAB cluster on a home-indicator iPad. */
body[data-surface="tablet"] .content { padding-bottom: calc(84px + env(safe-area-inset-bottom)); }
/* Dashboard documents own a little more scroll clearance than Plan/Spending:
 * the fixed tab bar and the transaction FAB occupy two separate bottom rails.
 * Keep this route-scoped so the compact Plan/Spending geometry is unchanged,
 * while the final Home/Report/Pricer control can always be scrolled into a
 * clear hit target on phone and tablet in either orientation. */
body[data-surface="phone"]:is([data-route="home"], [data-route="report"], [data-route="pricer"]) .content,
body[data-surface="tablet"]:is([data-route="home"], [data-route="report"], [data-route="pricer"]) .content {
  --dashboard-fixed-clearance: 140px;
  padding-bottom: calc(var(--dashboard-fixed-clearance) + env(safe-area-inset-bottom));
  scroll-padding-bottom: calc(var(--dashboard-fixed-clearance) + env(safe-area-inset-bottom));
}
body[data-surface="phone"]:is([data-route="home"], [data-route="report"], [data-route="pricer"]) .content :is(button, input, select, a, [role="button"]) {
  scroll-margin-bottom: calc(var(--dashboard-fixed-clearance) + env(safe-area-inset-bottom));
}
body[data-surface="tablet"]:is([data-route="home"], [data-route="report"], [data-route="pricer"]) .content :is(button, input, select, a, [role="button"]) {
  scroll-margin-bottom: calc(var(--dashboard-fixed-clearance) + env(safe-area-inset-bottom));
}
/* Pricer retains the compact transaction FAB, so its final item/action row also
 * needs a full scroll target above the fixed tab bar, FAB and home-indicator
 * glass. Keep this route-specific so ordinary touch routes retain their
 * measured dashboard clearance. */
body:is([data-surface="phone"], [data-surface="tablet"])[data-route="pricer"] .content {
  --pricer-fixed-clearance: 180px;
  padding-bottom: calc(var(--pricer-fixed-clearance) + env(safe-area-inset-bottom));
  scroll-padding-bottom: calc(var(--pricer-fixed-clearance) + env(safe-area-inset-bottom));
}
body:is([data-surface="phone"], [data-surface="tablet"])[data-route="pricer"] .content :is(button, input, select, a, [role="button"]) {
  scroll-margin-bottom: calc(var(--pricer-fixed-clearance) + env(safe-area-inset-bottom));
}
/* With the black-translucent status bar (punch 20) the page runs under the
 * clock on iPad too, so tablet content needs the same top inset the phone rule
 * above carries. */
body[data-surface="tablet"] .content { padding-top: calc(12px + env(safe-area-inset-top)); }
/* Settings owns a long, fixed-toolbar document in both touch themes. Keep
 * the final rows above the bottom navigation even when the legacy theme is
 * active; the route-specific Current writer carries the same value. */
body:is([data-surface="phone"], [data-surface="tablet"])[data-route="settings"] .content {
  --settings-fixed-clearance: 220px;
  padding-bottom: calc(var(--settings-fixed-clearance) + env(safe-area-inset-bottom));
  scroll-padding-bottom: calc(var(--settings-fixed-clearance) + env(safe-area-inset-bottom));
}
body:is([data-surface="phone"], [data-surface="tablet"])[data-route="settings"] .content :is(button, input, select, textarea, a, [role="button"]) {
  scroll-margin-bottom: calc(var(--settings-fixed-clearance) + env(safe-area-inset-bottom));
}
/* Accounts is itself a scrollable route sheet on touch. Keep its final Add
 * Account action above the fixed tab bar in tablet landscape; that action is
 * the only doorway to the account text-entry form. */
body:is([data-surface="phone"], [data-surface="tablet"])[data-route="accounts"] .content {
  padding-bottom: calc(140px + env(safe-area-inset-bottom));
  scroll-padding-bottom: calc(140px + env(safe-area-inset-bottom));
}
/* The layout viewport keeps its full height while a software keyboard reduces
 * what can be seen. Add the measured reduction to the document's transient
 * scroll tail so a field near the end of Pricer/Settings (or another long
 * touch route) is not stranded at the layout scroll limit. */
html[data-vv-keyboard="true"] body:is([data-surface="phone"], [data-surface="tablet"]) .content {
  padding-bottom: calc(220px + env(safe-area-inset-bottom) + var(--vv-keyboard-reduction, 0px));
}

/* Persistent Settings gear (operator: "where's the settings tab?"). */
.settings-gear {
  position: fixed; top: calc(8px + env(safe-area-inset-top)); right: calc(10px + env(safe-area-inset-right));
  z-index: 30; width: 40px; height: 40px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  /* Frosted glass to match the Add-Transaction FAB (operator 2026-07-12): a
   * near-transparent fill carried by a backdrop blur, so the content shows
   * through like the FAB. Tokened, so both themes read (rule 14). */
  background: var(--chrome-control-fill);
  -webkit-backdrop-filter: blur(16px) saturate(1.6);
  backdrop-filter: blur(16px) saturate(1.6);
  color: var(--ink); border: 1px solid var(--chrome-edge);
  line-height: 1; cursor: pointer;
  box-shadow: var(--chrome-control-shadow);
}
.settings-gear .ico, .privacy-eye .ico { width: 20px; height: 20px; }
.settings-gear.active { color: var(--accent); border-color: var(--accent); }
/* On desktop the gear sits bottom-left, tucked under the sidebar rail. */
body[data-surface="desktop"] .settings-gear { top: auto; bottom: 14px; left: 14px; right: auto; }

/* Privacy eye (D-027, family B): sits in the shared fixed utility rail on
 * touch and next to the settings gear on desktop. Masks all amounts when on.
 * Tokens only — must
 * read correctly in both themes (rule 14). */
.privacy-eye {
  position: fixed; top: calc(8px + env(safe-area-inset-top));
  right: calc(58px + env(safe-area-inset-right));
  z-index: 30; width: 40px; height: 40px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  /* Frosted glass to match the FAB + gear (operator 2026-07-12). */
  background: var(--chrome-control-fill);
  -webkit-backdrop-filter: blur(16px) saturate(1.6);
  backdrop-filter: blur(16px) saturate(1.6);
  color: var(--ink); border: 1px solid var(--chrome-edge);
  line-height: 1; cursor: pointer;
  box-shadow: var(--chrome-control-shadow);
}
.privacy-eye.on { color: var(--accent); border-color: var(--accent); }
/* Desktop: next to the gear at bottom-left. */
body[data-surface="desktop"] .privacy-eye { top: auto; bottom: 14px; left: 62px; right: auto; }

/* Touch chrome is an ink-only control.  The fixed 44px hitbox remains, but the
 * glass disc belongs to transient surfaces, not the settings/privacy icons. */
body:is([data-surface="phone"], [data-surface="tablet"]) :is(.settings-gear, .privacy-eye) {
  width: 44px; height: 44px; min-width: 44px; min-height: 44px;
  padding: 0; border: 0; border-radius: 0; outline: 0;
  background: transparent; box-shadow: none;
  -webkit-backdrop-filter: none; backdrop-filter: none;
  color: var(--ink); transition: transform var(--dur-fast) var(--ease-ios-rebound), opacity var(--dur-fast) ease;
}
body:is([data-surface="phone"], [data-surface="tablet"]) :is(.settings-gear, .privacy-eye).active,
body:is([data-surface="phone"], [data-surface="tablet"]) .privacy-eye.on { color: var(--accent); }
body:is([data-surface="phone"], [data-surface="tablet"]) :is(.settings-gear, .privacy-eye):active { transform: scale(.9); opacity: .72; }
body:is([data-surface="phone"], [data-surface="tablet"]) :is(.settings-gear.has-ca, .privacy-eye.has-ca)::before,
body:is([data-surface="phone"], [data-surface="tablet"]) :is(.settings-gear.has-ca, .privacy-eye.has-ca)::after {
  content: none; display: none;
}
@media (pointer: fine) {
  body:is([data-surface="phone"], [data-surface="tablet"]) :is(.settings-gear, .privacy-eye):focus-visible {
    outline: 2px solid var(--accent); outline-offset: 3px;
  }
}

/* Version tag: inline to the LEFT of the privacy-eye cluster on touch and the
 * eye + gear cluster on desktop
 * (operator couldn't find the version at the foot of Settings). Low-emphasis
 * pill; tap-to-copy flips to a confirmation. Tokens only (rule 14). */
/* Bare text, no pill (operator 2026-07-12): the outlined + filled version pill
 * ate width and crowded the title into "Spendi…". Strip the border and fill to
 * a low-emphasis muted label so the cluster is narrower and the title fits.
 * Vertically centred on the 40px utility discs (their centre is 8+20=28px from
 * the safe-area top); this 40px-tall flex box shares that centre so all three
 * read as one inline row. */
/* Width of the fixed top-right chrome cluster, measured from the screen edge to
 * the LEFT edge of its outermost control. ONE source of truth: the version tag
 * positions itself against it and the approval banner reserves against it, so a
 * route that adds a disc changes this token and both follow. Hardcoding the two
 * independently is what put "v1.75.0" on top of the banner's "Review" on the
 * Plan tab (operator screenshot 26/07/2026).
 *
 * Base = touch eye (58) + spacing + the version text's own width. */
:root { --chrome-cluster-w: 80px; }

.app-version-tag {
  position: fixed; z-index: 30;
  top: calc(8px + env(safe-area-inset-top));
  right: calc(var(--chrome-cluster-w) + env(safe-area-inset-right));
  height: 40px; padding: 0 4px;
  display: flex; align-items: center;
  background: none; color: var(--ink-muted); border: 0;
  font-size: var(--fs-label); font-weight: 600; font-variant-numeric: tabular-nums;
  letter-spacing: .02em; line-height: 1; cursor: pointer;
}
.app-version-tag.copied { color: var(--accent); }
/* PLAN adds two more 40px discs (collapse-all + reorder) to the right cluster,
 * so everything anchored to it shifts left by their combined width. Setting the
 * TOKEN rather than this element's `right` is what keeps the approval banner's
 * reservation in step — see --chrome-cluster-w above. */
body[data-route="budget"][data-surface="phone"],
body[data-route="budget"][data-surface="tablet"] {
  --chrome-cluster-w: 160px;
}
/* With the banner up, the version tag is hidden (see its rule above), so the
 * cluster is only the discs — reserving the tag's width too made the banner wrap
 * to 84px on Plan and pushed that tab's title to y=92 while every other tab sat
 * at 52, breaking the "same position across tabs" half of the rule. Measured
 * per route: 98px covers the touch eye plus spacing, 190px covers Plan's two extra discs. */
/* (The banner-present overrides here are gone: the banner no longer shares the
 * top row, so the cluster width does not change with it. The token still exists
 * for anything that must clear the cluster, and Plan still widens it above.) */
/* Desktop: the eye + gear live bottom-left; sit the version just to their right. */
body[data-surface="desktop"] .app-version-tag {
  top: auto; bottom: 14px; height: 40px; left: 108px; right: auto;
}
/* The desktop marker sits on the sidebar, not the content canvas. Use the
 * sidebar token so light and dark rails retain readable contrast while the
 * copied state keeps its accent feedback. */
body[data-surface="desktop"] .app-version-tag:not(.copied) { color: var(--sidebar-muted); }

/* THE VERSION TAG ALWAYS SHOWS (operator 26/07/2026: "v always must show").
 * v1.76.0 hid it whenever the approval banner was up, because the two competed
 * for the same top row. The banner has since moved BELOW the screen title
 * (shell.js #mountApproval), so nothing contends for that space and the tag
 * stays visible in every state — which is the point of a version marker. */

/* Tap-near-top scroll-to-top is now a document capture listener (#wireTopTap in
 * shell.js), not a fixed hit-strip — the old strip sat under the notch inset and
 * never fired on iPhone/iPad (operator 2026-07-12). No CSS element needed. */

/* Scroll-to-top disc (operator 2026-07-12): the iOS status-bar tap can't reach a
 * PWA webview, so a small frosted disc fades in at the bottom, just LEFT of the
 * +Transaction FAB, only once the page is scrolled down. Same glass treatment as
 * the FAB/gear/eye so it reads as one family; tokens only (rule 14). Sits above
 * the tab bar's clearance, matching the FAB's bottom offset. */
.scroll-top-disc {
  position: fixed; z-index: 29;
  bottom: calc(80px + env(safe-area-inset-bottom));
  left: 50%; transform: translateX(-50%) translateY(8px) scale(.9);
  width: 40px; height: 40px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--card) 30%, transparent);
  -webkit-backdrop-filter: blur(16px) saturate(1.6);
  backdrop-filter: blur(16px) saturate(1.6);
  color: var(--ink); border: 1px solid var(--chrome-edge);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.18);
  cursor: pointer; line-height: 1;
  opacity: 0; pointer-events: none;
  transition: opacity var(--dur-base) var(--ease-standard), transform var(--dur-base) var(--ease-spring);
}
.scroll-top-disc .ico { width: 20px; height: 20px; stroke-width: 2.4; }
.scroll-top-disc.shown {
  opacity: 1; pointer-events: auto;
  transform: translateX(-50%) translateY(0) scale(1);
}
.scroll-top-disc:active { transform: translateX(-50%) translateY(0) scale(1); }

/* Pull-to-refresh indicator (operator): a flow element at the top of .content
 * whose height grows as you drag down; releasing past threshold triggers a
 * Drive resync. Height 0 at rest so it's invisible until pulled. Tokens only. */
.pull-refresh {
  height: 0; overflow: hidden; display: flex; align-items: flex-end;
  justify-content: center; color: var(--ink-muted);
}
.pull-refresh-glyph { line-height: 1; padding-bottom: 6px; transition: transform var(--dur-fast) var(--ease-standard); display: inline-flex; }
.pull-refresh-glyph .ico { width: 1.3rem; height: 1.3rem; }
.pull-refresh.ready .pull-refresh-glyph { transform: rotate(180deg); color: var(--accent); }
.pull-refresh.syncing .pull-refresh-glyph { animation: pull-spin .8s linear infinite; color: var(--accent); }
@keyframes pull-spin { to { transform: rotate(360deg); } }

/* Compact register row: the top line is payee + amount ONLY, so a long payee
 * shows in full; the category chip moved to the sub line (operator 2026-07-14). */
.txn-row.compact { padding-top: .4rem; padding-bottom: .4rem; }
.txn-cat-inline { flex: 0 1 auto; }
.txn-row.compact .txn-main { gap: 8px; }
/* Payee takes the whole top line beside the amount; ellipsizes only when it must. */
.txn-row.compact .txn-payee { flex: 1 1 auto; }
/* Roomy recurring rows promote the badge to a sibling after the category; the
 * flex gap supplies that spacing, so retain the inline margin only in fallback. */
.txn-main > .reg-recur { margin-left: 0; }
/* Explicit compact-row type size (operator: iOS rows read oversized). Payee +
 * amount otherwise inherit the ~1rem body size and iOS inflates them further;
 * pin them to a dense .95rem so the list matches the intended mobile density. */
.txn-row.compact .txn-payee,
.txn-row.compact .txn-amount { font-size: var(--fs-value); }

/* Collapsible "Upcoming" (future-dated) group. */
.upcoming-head {
  display: flex; align-items: center; gap: 8px; width: 100%;
  background: none; border: 0; font: inherit; font-weight: 700; color: var(--ink-muted);
  padding: .6rem 0 .3rem; cursor: pointer; text-align: left;
  /* No grey tap-flash on the Scheduled toggle (operator 2026-07-08j). */
  -webkit-tap-highlight-color: transparent;
}
.upcoming-caret {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.15rem; height: 1.15rem; color: var(--accent);
  transition: transform var(--dur-slow) var(--ease-standard), color var(--dur-fast) var(--ease-standard);
}
.upcoming-caret .ico { width: 1.15rem; height: 1.15rem; stroke-width: 2.2; }
/* iOS disclosure direction: right when closed, down when expanded. The
 * header owns the accessible state; this is its visible, always-synchronised
 * indicator on both the phone list and desktop grid. */
.upcoming-head[aria-expanded="false"] .upcoming-caret { transform: rotate(-90deg); }
.upcoming-head[aria-expanded="true"] .upcoming-caret { transform: rotate(0deg); }
/* iOS-style expand/collapse (operator 2026-07-08j): the band clips to height and
 * both directions use the shared soft rebound curve. Only the
 * .animating state carries the transition so the resting auto/0 heights snap. */
.upcoming-collapse { overflow: hidden; height: 0; }
.upcoming-collapse.animating {
  transition: height var(--dur-slow) var(--ease-standard);
}
.upcoming-count {
  margin-left: auto; background: var(--chip); color: var(--ink-muted);
  border-radius: 999px; font-size: var(--fs-label); font-weight: 600; padding: .05rem .5rem;
}
.upcoming-body { border-left: 2px solid var(--hairline); padding-left: .5rem; margin-bottom: .3rem; }
.upcoming-touch-heading {
  cursor: default;
  padding: .55rem 14px .3rem;
  color: var(--ink-muted);
  font-size: var(--fs-label);
  letter-spacing: .04em;
}
/* Light background fill behind the Scheduled (future-dated) group so it reads as
 * a distinct band from the current posted transactions below (operator
 * 2026-07-08e). Token-driven so it holds up in both light and dark (rule 14):
 * a faint accent wash, plus a soft accent left rail replacing the plain hairline
 * so the scheduled band is unmistakable. */
.upcoming-body.scheduled-fill {
  /* Lighter than --chip (operator 2026-07-08f): a faint accent wash, ~8% over
   * the card, so the scheduled band is distinct but softer than the old solid
   * chip grey. color-mix keeps it token-driven → both themes follow (rule 14). */
  background: var(--scheduled-fill);
  border-left: 3px solid var(--accent);
  border-radius: 0 8px 8px 0;
  padding-top: .2rem; padding-bottom: .2rem;
}
[data-theme="dark"] .upcoming-body.scheduled-fill { background: var(--scheduled-fill); }
/* Full-bleed Spending list re-insets rows; give the scheduled band matching side
 * padding so its rows don't sit flush against the accent rail. */
.card.txn-list .upcoming-body.scheduled-fill > .txn-row,
.card.txn-list .upcoming-body.scheduled-fill > .date-head { padding-left: 16px; }
/* The scheduled fill must span the WHOLE row, not just the band around it
 * (operator 2026-07-08h). Each row is a .swipe-wrap > .txn-row, both painting an
 * opaque var(--card) so the faint wash behind them shows only as a margin — the
 * rows read white. Re-paint the scheduled rows with the SAME accent wash so the
 * tint is continuous across the entire row. The row edge stays opaque (needed to
 * hide the swipe delete strip), just tinted to match the band. Token-driven →
 * both themes follow (rule 14). */
.upcoming-body.scheduled-fill .swipe-wrap,
.upcoming-body.scheduled-fill .swipe-wrap .txn-row {
  background: var(--scheduled-fill);
}
[data-theme="dark"] .upcoming-body.scheduled-fill .swipe-wrap,
[data-theme="dark"] .upcoming-body.scheduled-fill .swipe-wrap .txn-row {
  background: var(--scheduled-fill);
}

/* Desktop grid Scheduled band (operator 2026-07-08h): the collapse/expand that
 * had disappeared on Windows 11. The band gets the same accent wash + left rail
 * as the list surface, and each grid row inside is tinted to match so the fill
 * spans the whole row (not a white row on a coloured band). */
/* box-shadow (not border-left), so the accent rail paints INSIDE the box and
 * doesn't consume layout width — a border-left here pushed every column in
 * the band's grid rows 3px right of the header/other rows (Win11 visual
 * audit, 2026-07-20). */
.reg-grid .reg-sched-head {
  cursor: pointer; display: flex; align-items: center; gap: .4rem;
  font-weight: 600; color: var(--ink);
  background: var(--scheduled-fill);
  box-shadow: inset 3px 0 0 var(--accent);
  /* VERTICALLY CENTRED (operator 2026-07-29: "note how in our project, there is
   * more gap above the 'scheduled' text than below; ensure it's vertically
   * aligned"). It inherits .upcoming-head's `.6rem 0 .3rem` — measured live at
   * 9.6px above against 4.8px below, i.e. exactly double. That padding is
   * deliberate on the PHONE list, where the band header follows a run of rows
   * and needs the extra space above to separate itself from them; in the
   * desktop grid it is a full-width banded row with its own background, so the
   * asymmetry just reads as a misaligned label. Equal padding here only. */
  padding: .45rem 10px;
}
[data-theme="dark"] .reg-grid .reg-sched-head { background: var(--scheduled-fill); }
/* Calendar glyph + full "Scheduled Transactions" wording, matching the operator's
 * reference screenshot of YNAB's own register band. The caret now trails the
 * label (it reads as "open this", not as a bullet), so it must not also claim
 * the leading 1rem the phone list gives it. */
.reg-grid .reg-sched-head .sched-head-ico {
  display: inline-flex; align-items: center; color: var(--accent);
}
/* "Scheduled Transactions" must never truncate (operator 29/07/2026). Pin the
 * label at its natural width and let the trailing whitespace absorb slack. */
.reg-grid .reg-sched-head .sched-head-label {
  flex: none; white-space: nowrap; overflow: visible; text-overflow: clip;
}
/* TRUNCATION FIX (operator 29/07/2026: "our project's icon for scheduled
 * transactions is truncated"). Two causes, both fixed:
 *   1. The calendar path's hanging rings sat at y=3 in a 0..24 viewBox, so at a
 *      1.75 stroke width their outer edge fell ON the boundary and the renderer
 *      clipped them flat. Redrawn inside the box (icons.js).
 *   2. The glyph was sized 1rem inside a line box that is also 1rem, leaving no
 *      room for the stroke's own half-width. Sizing it just under the line box,
 *      with overflow visible, guarantees the whole shape paints at any zoom. */
.reg-grid .reg-sched-head .sched-head-ico .ico {
  width: .95rem; height: .95rem; overflow: visible; flex: none;
}
.reg-grid .reg-sched-head .upcoming-caret { width: auto; }
/* THE ARROW MUST SIT BESIDE THE LABEL IT DISCLOSES (operator 05/08/2026, second
 * report: "the expand/collapse arrow has gone missing"). It was never absent —
 * it was 1,356px away from the words it belongs to. `.upcoming-count` carries
 * `margin-left: auto`, which is right on the narrow PHONE list (it pushes the
 * count to the row's trailing edge) but in the 1,602px-wide desktop grid header
 * that auto margin resolved to 1,320px: measured label right-edge 506px, count
 * at 1,832px and caret at 1,862px — parked in the far corner rather than beside
 * its disclosure label. Cancel the auto margin so count and caret trail the
 * label directly. */
.reg-grid .reg-sched-head .upcoming-count { margin-left: 0; }
.reg-sched-band.scheduled-fill {
  box-shadow: inset 3px 0 0 var(--accent);
  background: var(--scheduled-fill);
}
[data-theme="dark"] .reg-sched-band.scheduled-fill { background: var(--scheduled-fill); }
/* Scheduled occurrences keep the normal transaction rails and typography,
 * but the operator explicitly wants their status visible through one shared,
 * on-theme tint across concrete transactions and repeat rules. */
.reg-sched-band.scheduled-fill .reg-txn { background: var(--scheduled-fill); }
@media (hover: hover) {
  .reg-sched-band.scheduled-fill .reg-txn:hover,
  .reg-sched-band.scheduled-fill .reg-txn:focus-within {
    background: var(--scheduled-fill-strong);
  }
}

/* v1.43.0: the Scheduled band now also carries repeat-RULE rows (.sched-row,
 * folded in from the retired scheduled band). On the desktop grid they sit among
 * the grid txn rows, so give them the grid row's own 10px side padding and let
 * the accent wash show through. The occurrence-pill list + toggle come along. */
.reg-sched-band.scheduled-fill .sched-row { padding-left: 10px; padding-right: 10px; background: var(--scheduled-fill); }
@media (hover: hover) { .reg-sched-band.scheduled-fill .sched-row:hover { background: var(--scheduled-fill-strong); } }
.reg-sched-band.scheduled-fill .sched-occ-list { padding-left: 10px; }
/* A rule the approve banner just pointed at (D-097 fix, 04/08/2026). A DUE
 * occurrence is not a transaction, so the register's search cannot filter to it;
 * the desktop banner expands this band and marks the rules that owe an approval
 * instead of opening the centred sheet the no-popup invariant forbids. Only a
 * stronger wash plus the accent rail — the same accent/card mix every other band
 * state uses, so both themes track it without a hardcoded colour (rule 14). */
.reg-sched-band.scheduled-fill .sched-row.sched-row-due {
  background: color-mix(in srgb, var(--accent) 20%, var(--card));
  box-shadow: inset 3px 0 0 var(--accent);
}
/* Projected rules have their own schedule-local checkbox selection. Reuse the
 * register's token wash so the checked state is visible without placing the
 * row into the transaction options bar. */
.reg-sched-band.scheduled-fill .sched-row.selected {
  background: color-mix(in srgb, var(--accent) 12%, var(--scheduled-fill));
  box-shadow: inset 3px 0 0 var(--accent);
}
@media (hover: hover) {
  .reg-sched-band.scheduled-fill .sched-row.sched-row-due:hover {
    background: color-mix(in srgb, var(--accent) 26%, var(--card));
  }
  .reg-sched-band.scheduled-fill .sched-row.selected:hover {
    background: color-mix(in srgb, var(--accent) 16%, var(--scheduled-fill));
  }
}
/* The Spending register has desktop width to keep a rule on one line. The
 * shared Scheduled-tab row remains multi-line on touch; only this wide band is
 * compacted to the same density as its transaction rows. */
/* A rule without a concrete future entry still represents a scheduled
 * transaction. On the desktop register it therefore uses the same account,
 * plain date, payee/cadence, category, memo and amount grammar as a concrete
 * row. The whole row remains its schedule-editor door. Touch keeps the stacked
 * layout above, where the extra detail is needed for narrow widths. */
body[data-surface="desktop"] .reg-sched-band.scheduled-fill .sched-row.reg-row {
  /* THE GRID IS INHERITED, NOT RE-TYPED (operator 05/08/2026: "why the fields
   * are different sized when the scheduled transaction is selected"). This rule
   * used to restate .reg-row's ten tracks by hand. A hand-copy of a layout is a
   * copy that drifts, and this one already had — twice. It silently missed
   * `white-space: nowrap`, so a long scheduled payee wrapped and grew its row
   * where a posted one clipped; and it inherited the 16px document default
   * against .reg-row's 14.4px until that was patched separately one release
   * ago. Worse, the register has TWELVE grid variants (--reg-grid-balance,
   * -no-flags, -nocat and their combinations, plus data-cleared="off") and every
   * one of them is written for `.reg-row` alone: with its own copy of the
   * DEFAULT tracks, a scheduled row kept the default columns while the posted
   * rows around it moved — the columns visibly disagreeing inside one list.
   * The markup now carries `reg-row` on these rows too, so the tracks, the
   * padding, the type size and all twelve variants arrive from the single
   * definition. Only what is genuinely particular to a scheduled row is set
   * here. */
  height: var(--reg-row-h, 32px);
  /* Kept explicitly even now that `.reg-row` supplies it: the touch rule
   * `.sched-row { font: inherit }` is defined LATER in this file, so it beats
   * `.reg-row`'s own `font-size` on specificity ties and would restore the 16px
   * document default against the register's 14.4px — the exact drift reported
   * one release ago and fixed here. Deleting this line silently re-opens it. */
  /* Same reason: `.sched-row`'s touch rule sets `display`, `padding` and `gap`
   * later in the file and would otherwise win here. The GAP is the one that was
   * actually visible: 12px against the register's 8px, multiplied across the
   * nine gaps of a ten-track grid, took 36px out of the flexible columns, so
   * payee/category/memo on a scheduled row measured 12px, 14px and 10px
   * narrower than the identical columns on the posted row directly below —
   * every scheduled field starting and ending at its own x-position. That is
   * the "different sized fields" in the report, and it is why the ROW must take
   * the register's gap and not merely its tracks. */
}
body[data-surface="desktop"] .reg-sched-band.scheduled-fill .sched-row:focus-visible {
  background: var(--scheduled-fill-strong); outline: none;
}
/* Touch: the header button pairs with the collapsible header on one compact row. */
.reg-sched-headwrap { display: flex; align-items: center; gap: 8px; }
.reg-sched-headwrap .upcoming-head { flex: 1 1 auto; }
.reg-sched-headwrap .reg-sched-suggest .icon { width: 15px; height: 15px; }

/* Swipe-to-delete/edit (operator: swipe to delete and edit). The row slides
 * left over a fixed action strip; buttons use theme tokens so both modes read.
 * Full-bleed: on the phone/tablet Spending list the rows span the whole card
 * width (operator: "each transaction spans full width to use up available
 * space"). The card's side padding is removed for the swipe list (below) so the
 * opaque row edge sits flush with the card edge and the red Delete strip stays
 * fully hidden behind it at rest — no red sliver peeking on the right. */
/* iOS-style swipe actions (operator 2026-07-08i): each action button is a
 * rounded pill that GROWS from its centre axis and jelly-bounces to full size as
 * the swipe latches open (via .swiped). At rest they're scaled to 0 behind the
 * row so no colour peeks. Row slides left to reveal them. Wider reveal (9.4rem)
 * so neither Edit nor Delete is truncated. */
.swipe-wrap {
  position: relative; overflow: hidden; background: var(--card);
}
.swipe-actions {
  position: absolute; top: 4px; right: 6px; bottom: 4px;
  display: flex; align-items: stretch; gap: 6px;
}
.swipe-btn {
  border: 0; font: inherit; font-weight: 700; color: var(--chart-ink); cursor: pointer;
  /* box-sizing:border-box so the declared width IS the rendered width — the
   * reveal distance below is derived from these exact numbers, no guesswork
   * (operator 2026-07-14: amber Edit was clipped on its left because the two
   * buttons' true width exceeded the slide). Each button a fixed 4.6rem. */
  box-sizing: border-box;
  padding: 0 .5rem; width: 4.6rem; min-width: 4.6rem; display: flex; align-items: center; justify-content: center;
  border-radius: 14px; white-space: nowrap;
  /* Grow from the RIGHT edge, not the centre (operator 2026-07-12): the buttons
   * are pinned to the right of the row and the wrapper clips overflow, so a
   * centre-origin scale (and the springy overshoot below) pushed the button's
   * LEFT edge past the revealed area and clipped its leading text ("Snooze
   * Target", the amber "Edit"). Anchoring the right edge keeps the whole label
   * inside the reveal while it bounces. */
  transform: scale(0); transform-origin: right center;
  opacity: 0; transition: transform var(--dur-fast) var(--ease-standard), opacity var(--dur-fast) var(--ease-standard);
  /* A tap must NOT have to wait for the spring to finish (operator 2026-07-14:
   * "annoyingly making user wait until the animation is complete before a button
   * tap registers"). The cause was that the button's own `transform: scale()` IS
   * its hit box — mid-spring the element is geometrically tiny, so a finger
   * landing on where the button LOOKS like it is hits the row underneath instead.
   * Scaling is therefore no longer allowed to gate input: the button is
   * `pointer-events: none` only while it is genuinely hidden, and becomes fully
   * hit-testable at its FINAL size the instant the row latches open (below) —
   * the spring is then pure decoration running on top of a stable target. */
  pointer-events: none;
}
/* Edit action is AMBER (operator 2026-07-09), never the blue accent — so Edit and
 * the destructive red Delete read as two distinct, non-brand action colours. */
/* "Edit Payee" is longer than the old "Edit" label but must stay inside the
 * same fixed 4.6rem button (the reveal-distance translateX below is derived
 * from that exact width — widening the button means re-deriving the slide
 * distance shared with the Scheduled band's identical swipe strip). A smaller
 * font is the whole fix: two words still read clearly at this size. */
.swipe-edit-payee { background: var(--swipe-edit); font-size: .72rem; letter-spacing: -.01em; }
.swipe-del { background: var(--neg); }
.swipe-wrap.swiped .swipe-btn {
  opacity: 1;
  transform: scale(1);
  transition: transform var(--dur-slow) var(--ease-spring), opacity var(--dur-fast) var(--ease-standard);
  /* Hit-testable IMMEDIATELY — no transition, no delay, no waiting on the spring. */
  pointer-events: auto;
}
/* Stagger the two so they pop in sequence (Delete a touch after Edit) — visual
 * only; `transition-delay` must never touch pointer-events or the delay becomes
 * dead time where the button looks tappable and isn't. */
.swipe-wrap.swiped .swipe-del { transition-delay: .04s; }
/* The sliding face of a swipe row. .sched-txn-row is the Spending band's row
 * (operator 2026-07-14: swipe must work there too) — it rides the same rails as
 * the register's .txn-row; without it the buttons would appear but the row would
 * sit still on top of them. */
.swipe-wrap .txn-row,
.swipe-wrap .sched-txn-row {
  position: relative; z-index: 1; background: var(--card);
  border-radius: 0; /* square inside the wrap so no rounded corner reveals the strip */
  transition: transform var(--dur-base) var(--ease-spring);
}
/* Slide = 2×4.6rem buttons + .375rem gap + .375rem right inset = 9.95rem; 10rem
 * gives a hair of slack so the amber Edit's left edge is never clipped. */
.swipe-wrap.swiped .txn-row,
.swipe-wrap.swiped .sched-txn-row { transform: translateX(-10rem); }

/* Payee-picker "Near You" rows swipe left to reveal a single red "Forget
 * Location" action (operator 2026-07-15). One wider button, so its own reveal
 * width and slide distance, and the sliding face is .near-you-row not .txn-row. */
.near-you-swipe { border-radius: 12px; }
.swipe-forget { background: var(--neg); width: 8.4rem; min-width: 8.4rem; }
.near-you-swipe .near-you-row {
  position: relative; z-index: 1; background: var(--card);
  transition: transform var(--dur-base) var(--ease-spring);
}
/* 8.4rem button + .375rem right inset ≈ 8.8rem reveal. */
.near-you-swipe.swiped .near-you-row { transform: translateX(-8.8rem); }

/* Full-bleed Spending list: drop the card's horizontal padding so every row
 * runs edge to edge, and re-inset the row content instead. The date headers and
 * the "Upcoming" toggle keep a matching inset so nothing looks ragged. */
body[data-surface="phone"] .card.txn-list,
body[data-surface="tablet"] .card.txn-list {
  padding-left: 0;
  padding-right: 0;
  /* The list begins with the first date band; inherited .txn-list padding
   * otherwise leaves a narrow unowned gap above TODAY on iPad. */
  padding-top: 0;
}
/* V1 touch disclosure: the Scheduled band is a full-bleed list, not a nested
 * card. Keep the measured header height stable while the body animates its own
 * wrapper so expanding/collapsing never reintroduces card chrome. */
body[data-surface="phone"] .card.txn-list {
  width: calc(100% + 32px); margin-left: -16px; margin-right: -16px;
  border-radius: var(--radius-lg, 16px) var(--radius-lg, 16px) 0 0; box-shadow: none; padding-top: 0;
}
body[data-surface="tablet"] .card.txn-list { border-radius: var(--radius-lg, 16px) var(--radius-lg, 16px) 0 0; }
body[data-surface="phone"] .card.txn-list > .upcoming-head {
  box-sizing: border-box; height: 42px; min-height: 42px; padding: 0 14px;
}
body[data-surface="phone"] .card.txn-list .upcoming-body.scheduled-fill {
  border-left: 0; border-radius: 0; padding-top: 0; padding-bottom: 0;
}
/* Row inset trimmed 18px → 14px (operator 2026-07-14: maximise lateral space on
 * touch devices). Reclaims 8px of row width for the payee + category chip on a
 * narrow phone, still leaving a comfortable edge gutter. Headers match so the
 * left rule stays flush. */
.card.txn-list .txn-row { border-radius: 0; padding-left: 14px; padding-right: 14px; }
.card.txn-list > .date-head,
.card.txn-list .upcoming-body > .date-head { padding-left: 14px; padding-right: 14px; }
.card.txn-list > .upcoming-head,
.card.txn-list > .banner-approve,
.card.txn-list > .muted { padding-left: 14px; padding-right: 14px; }
.card.txn-list .upcoming-body { margin-left: 0; }

/* V1 Phase 04 attempt 3: the frozen phone reference places the Scheduled
 * disclosure about 9px lower than the old clearbar/list seam, leaves a small
 * in-flow breathing gap before its first date/row, and uses a compact 13px
 * chevron around x17. Keep the adjustment touch-only, proportional across
 * future phone widths and lateral-safe-area aware; the existing content top
 * padding already owns the vertical safe-area inset, so it is not doubled. */
@media (max-width: 600px) {
  body[data-surface="phone"] .card.txn-list:has(> .upcoming-head) {
    --v1-upcoming-offset: clamp(8px, 2.3vw, 10px);
    --v1-upcoming-content-gap: clamp(5px, 1.5vw, 8px);
    /* Padding, rather than a top margin, keeps the clearbar/list seam from
     * collapsing the measured offset to only the larger of two margins. */
    padding-block-start: var(--v1-upcoming-offset);
  }
  body[data-surface="phone"] .card.txn-list:has(> .upcoming-head) > .upcoming-head {
    padding-inline-start: max(10px, env(safe-area-inset-left, 0px));
  }
  body[data-surface="phone"] .card.txn-list:has(> .upcoming-head) .upcoming-caret {
    flex: 0 0 13px; width: 13px; height: 13px;
  }
  body[data-surface="phone"] .card.txn-list:has(> .upcoming-head) .upcoming-caret .ico {
    width: 13px; height: 13px; stroke-width: 2;
  }
  body[data-surface="phone"] .card.txn-list:has(> .upcoming-head) .upcoming-body.scheduled-fill {
    padding-top: var(--v1-upcoming-content-gap);
  }
}

/* ---------- cross-platform alignment batch (2026.07.10h) ---------- */

/* Reconcile lock is a stroke icon now (D-029 one icon language, was 🔒). */
.rec-lock .ico { width: .95em; height: .95em; }

/* Category-picker rows carry a right-aligned Available/prorated pill (§4). */
/* Category picker rows (operator 2026-07-14, screenshot: wrapped rows bleeding
 * over their neighbours). A flex child holding text will NOT shrink below its
 * max-content width unless it is given `min-width: 0` — the classic flexbox
 * overflow trap. Without it, a long row ("Insurance Iswen ≈$950 · Iswen Income
 * Insurance annual 18Jan $785…") overflowed the button box and its wrapped lines
 * painted straight over the rows above and below. The name now shrinks and
 * ellipsises; the figure pill never shrinks. */
.pick-row:has(.pick-avail) { display: flex; align-items: center; gap: 8px; }
.pick-row > span:not(.pick-swatch):not(.avail-pill):not(.splitpick-check):not(.payee-select-mark):not(.payee-match):not(.pick-avail) {
  min-width: 0; flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pick-row .pick-avail {
  margin-left: auto; font-size: var(--fs-label); flex: 0 0 auto; white-space: nowrap;
  /* min-width pins the figure to its own text so the NAME ellipsises instead —
   * a shortened name is readable, a truncated amount is misinformation.
   * NOTE the `:not(.pick-avail)` added to the shrink-everything rule above: that
   * selector's `:not()` chain out-specifies `.pick-row .pick-avail` (0,6,1 vs
   * 0,2,0), so without the exclusion it forced `flex:1 1 auto; min-width:0` onto
   * the figure and these declarations never applied — measured 119.5px of text
   * painted into a 71.3px box. Any future non-shrinking picker child must be
   * added to that exclusion list too. */
  min-width: max-content;
}

/* Plan: "hidden by your current view" note + View All (§11.7). */
.plan-hidden-note { padding: .6rem .8rem; font-size: var(--fs-body); }
.plan-hidden-note .plan-viewall { margin-left: 6px; }

/* Plan: category funding-progress track (§11.6 Progress Bars, desktop). */
.plan-progress {
  display: block; height: 3px; margin-top: 3px; border-radius: 2px;
  background: var(--chip); overflow: hidden; max-width: 220px;
}
.plan-progress-fill { display: block; height: 100%; background: var(--accent); border-radius: 2px; }
.plan-progress-fill.met { background: var(--pos); }

/* Plan: category Inspector (D-074 gap 1, 28/07/2026 — desktop only). Selecting
 * a row turns the right-hand panel into that category's own detail. Colours are
 * tokens throughout so light and dark are one rule (global rule 14). */
.plan-cat.plan-cat-selected { background: color-mix(in srgb, var(--accent) 10%, transparent); box-shadow: inset 3px 0 0 var(--accent); }

/* ----------------------------------------------------------------------------
 * CATEGORY NAME EDITOR (operator 04/08/2026, supplied recording). A compact card
 * hanging directly under the clicked category row with a tail pointing up at it:
 * name field, then one split button row — [Hide][Delete] left, [Cancel][OK]
 * right. The recording's card reads roughly 3.7x as wide as it is tall, so the
 * width is pinned near that against its own content height: it is a popover, not
 * a dialog, and letting it grow to the .popover 200px-min default alone made it
 * read as one.
 *
 * Every colour is a token or a color-mix of one, so the card is a single rule in
 * dark and light (global rule 14) — the two tinted pills in particular are
 * accent/--neg mixed toward transparent rather than a second hardcoded pale
 * shade, which would only be pale against a light background. */
/* The row the card hangs off stays lit while it is open, so it is obvious which
 * category is being renamed once the card overlaps the rows below. Same accent
 * wash the Inspector selection uses, so the two never read as different states
 * of the same row. */
.plan-row.plan-row-editing { background: color-mix(in srgb, var(--accent) 10%, transparent); }
.popover.cat-name-pop {
  min-width: 0; width: 310px; max-width: calc(100vw - 24px);
  padding: 10px; display: flex; flex-direction: column; gap: 10px;
  /* Soft ambient drop, no hard border: the recording's card has no outline, and
   * a --hairline border here reads as a table cell against the rows behind. */
  border: 0; box-shadow: 0 6px 24px rgba(0, 0, 0, .22);
  /* The tail is drawn as a child pseudo-element, so it must not be clipped by
   * the .popover overflow-y:auto default. */
  overflow: visible;
}
/* The tail. Pinned near the card's horizontal centre pointing UP at the row the
 * card was anchored beneath. Rotating a square gives a triangle whose two
 * visible edges inherit the card's own surface, so it never needs its own
 * colour: --card in both themes, exactly like the body it grows out of. */
.popover.cat-name-pop::before {
  content: ''; position: absolute; top: -5px; left: 50%;
  width: 10px; height: 10px; background: var(--card);
  transform: translateX(-50%) rotate(45deg);
  /* NOT z-index:-1 — the card paints its own --card background, and a negative
   * index would drop the tail behind it and hide the diamond completely. It
   * sits half outside the card's edge instead, so the protruding half reads as
   * the tail and the buried half is simply covered by the body it matches. */
}
/* A flipped card (no room below) hangs ABOVE the row, so the tail must point
 * DOWN instead — chrome.js sets data-flip when it flips. */
.popover.cat-name-pop[data-flip="up"]::before { top: auto; bottom: -5px; }

.cat-name-pop .cat-name-inp {
  width: 100%; padding: .45rem .6rem; text-align: left;
  border-radius: var(--radius-s);
  border: 1px solid var(--hairline); background: var(--card); color: var(--ink);
}
/* Thin accent ring while focused — the field is focused the moment the card
 * opens, so this is its resting look, not a rare state. */
.cat-name-pop .cat-name-inp:focus,
.cat-name-pop .cat-name-inp:focus-visible {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}

/* One row, two clusters, wide gap between: destructive/secondary on the left,
 * dismiss/commit on the right, exactly as the recording splits them. */
.cat-name-actions { display: flex; justify-content: space-between; gap: 16px; }
.cat-name-cluster { display: flex; gap: 6px; }
.cat-name-btn {
  /* All four share a height and radius; only the fill and ink differ. */
  min-height: 30px; padding: 0 .75rem; border: 0; border-radius: var(--radius-s);
  font: inherit; font-size: var(--fs-body); font-weight: 600; cursor: pointer;
  white-space: nowrap;
}
/* Hide and Delete sit slightly narrower than Cancel/OK (recording). */
.cat-name-cluster:first-child .cat-name-btn { padding: 0 .55rem; }
.cat-name-btn.subdued {
  background: color-mix(in srgb, var(--accent) 12%, transparent); color: var(--accent);
}
.cat-name-btn.destructive {
  background: color-mix(in srgb, var(--neg) 14%, transparent); color: var(--neg);
}
.cat-name-btn.primary { background: var(--accent); color: var(--accent-ink); }
@media (hover: hover) {
  .cat-name-btn.subdued:hover { background: color-mix(in srgb, var(--accent) 20%, transparent); }
  .cat-name-btn.destructive:hover { background: color-mix(in srgb, var(--neg) 22%, transparent); }
  .cat-name-btn.primary:hover { background: color-mix(in srgb, var(--accent) 88%, var(--ink)); }
}
.cat-name-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ============================================================================
 * PLAN MOTION (operator 04/08/2026: "i am interested in animation, flow, and
 * visually matching the video as reference, within reason").
 *
 * The desktop Plan tab already had the video's STRUCTURE — selectable rows, the
 * right-hand inspector, the Available pill — but no motion whatsoever: measured,
 * `.plan-cat`, `.plan-cat-selected`, `.avail-pill` and every `.insp-*` carried
 * no transition, animation or keyframe. Selecting a row snapped, and the
 * inspector blinked in because `rerender()` replaces the whole subtree.
 *
 * That full replacement is why the inspector uses an ENTRY ANIMATION rather than
 * a transition: its node is new on every selection, so there is no previous
 * state for a transition to interpolate from. The row band, by contrast, is a
 * fresh node too — but the ROW's tint is what the eye tracks, and animating it
 * on entry would make every row flash on an unrelated repaint, so it stays a
 * transition and simply covers hover/focus.
 * ==========================================================================*/
.plan-cat {
  transition:
    background-color var(--dur-fast) var(--ease-standard),
    box-shadow var(--dur-fast) var(--ease-standard);
}
/* The pill is the figure the operator watches while assigning, so it gets the
 * spring the rest of the app uses for a value that just changed. */
.avail-pill {
  transition:
    background-color var(--dur-base) var(--ease-standard),
    color var(--dur-base) var(--ease-standard),
    transform var(--dur-base) var(--ease-spring);
}
/* The inspector slides in from the right edge it lives on, so the motion says
 * where the panel came from. Short and small: this fires on every selection. */
@keyframes plan-inspector-in {
  from { opacity: 0; transform: translateX(10px); }
  to   { opacity: 1; transform: none; }
}
.plan-inspector {
  animation: plan-inspector-in var(--dur-base) var(--ease-standard) both;
}
/* Its cards land in sequence rather than as one block — the same staggered
 * settle the reference shows when a category's detail opens. Capped at four:
 * beyond that the last card would arrive late enough to read as lag. */
@keyframes plan-insp-card-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
.plan-inspector .panel-card {
  animation: plan-insp-card-in var(--dur-base) var(--ease-standard) both;
}
.plan-inspector .panel-card:nth-child(1) { animation-delay: 0ms; }
.plan-inspector .panel-card:nth-child(2) { animation-delay: 40ms; }
.plan-inspector .panel-card:nth-child(3) { animation-delay: 80ms; }
.plan-inspector .panel-card:nth-child(n+4) { animation-delay: 120ms; }
/* Rule 14 / the theme contract: every one of these is decorative, so all of it
 * goes when the operator's OS asks for reduced motion. */
@media (prefers-reduced-motion: reduce) {
  .plan-cat, .avail-pill { transition: none; }
  .plan-inspector, .plan-inspector .panel-card { animation: none; }
}
.insp-title { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.insp-title h2 { margin: 0; }
.insp-group { margin: 2px 0 0; }
.insp-close { font-size: var(--fs-value); line-height: 1; padding: 0 .25rem; }
.insp-total { font-weight: 600; }
.insp-target-sentence { margin: .2rem 0 .5rem; font-size: var(--fs-body); }
.insp-progress { max-width: none; margin-top: .6rem; }
.insp-fund, .insp-move-go { width: 100%; margin-top: .7rem; }
.insp-edit-target { margin-top: .6rem; }
.insp-move-amt .insp-move-input { max-width: 130px; text-align: right; font-variant-numeric: tabular-nums; }
.insp-move-to {
  width: 100%; background: none; border: 0; border-top: 1px solid var(--hairline);
  color: var(--ink); font: inherit; text-align: left; cursor: pointer;
}
.insp-move-dest.error { color: var(--neg); }
.insp-links { display: flex; flex-wrap: wrap; gap: 12px; }

/* Register: toolbar search on the wide grid (§11.2). The bare .inp class gave
 * it a border/radius/background, but sitting alone in the toolbar it still
 * read as unstyled/off-theme (operator 2026-07-15, punch B6) — the magnifier
 * icon floated outside any field-like boundary. Wrap icon + input in one
 * bordered, on-theme field so the whole control reads as a single search box,
 * with its own focus ring on the input (tokens only, rule 14). */
.reg-grid-search {
  margin-left: auto; display: inline-flex; align-items: center; gap: 6px;
  padding: 0 .55rem; color: var(--ink-muted);
  background: var(--card); border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--hairline)); border-radius: 10px;
}
.reg-grid-search .ico { width: 1em; height: 1em; flex: 0 0 auto; }
.reg-grid-search-input {
  width: 15rem; max-width: 40vw; padding: .35rem 0; font-size: var(--fs-body);
  border: 0; background: none;
}
/* One ring, not three (part F, 19/08/2026): the wrapper below owns the
 * external accent ring on :focus-within; the child input used to ALSO paint
 * its own box-shadow ring here, and the generic body focus-visible rule
 * added a third outline on top of both. The compound-control contract is
 * "wrapper owns the ring, child owns none". */
.reg-grid-search-input:focus { outline: none; }
.reg-grid-search:focus-within {
  border-color: var(--accent);
  outline: none;
  box-shadow: none;
}
/* This must out-specify the generic `body :is(…):focus-visible` rule above
 * (0,2,1) — a bare `.reg-grid-search-input:focus-visible` is only (0,1,1) and
 * LOSES that cascade, so the desktop grid search field painted a second blue
 * outline directly on the input despite this rule existing (operator
 * 21/08/2026: the wrapper-owns-the-ring contract above was defeated by
 * specificity, not missing). Prefixing `body` matches the generic rule's own
 * specificity; this rule wins on source order (it appears later). */
@media (pointer: fine) {
  body .reg-grid-search-input:focus-visible { outline: none; }
}
/* CLEAR — a LABELLED button, deliberately larger than the 26px × disc the touch
 * search pill uses (operator 04/08/2026: "for all search filters i prefer a
 * larger 'clear' button than the original, to let me return to the 'alll
 * transactions' page"). The desktop grid had NO clear affordance at all, so a
 * filter applied by the approve banner could only be undone by hand-deleting the
 * query. A word on an accent tint, not a grey glyph: this is the way back to the
 * whole register, the most common thing to want after filtering. Hidden while
 * the query is empty, so the resting toolbar is unchanged. */
.reg-grid-search-clear {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  box-sizing: border-box; min-height: var(--ctl-h-sm, 32px); height: var(--ctl-h-sm, 32px);
  border: 0; cursor: pointer; font: inherit; font-weight: 700;
  font-size: var(--fs-label); line-height: 1;
  padding: 0 .8rem; margin-inline-start: .1rem;
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent);
  transition: transform var(--dur-fast) var(--ease-spring), background-color var(--dur-fast) linear;
}
.reg-grid-search-clear:hover { background: color-mix(in srgb, var(--accent) 26%, transparent); }
.reg-grid-search-clear:active { transform: scale(.94); }
.reg-grid-search-clear[hidden] { display: none; }

/* TOOLS ROW (operator 2026-07-14, rebuilt): the uncleared chip + search + Select
 * on ONE row. The chip is content-width (not stretched across the page — a wide
 * grey slab was most of the "so much blank space"), and the icons trail right.
 * The whole row is a compact 8px above the list. */
.reg-clearbar { display: flex; align-items: center; gap: 8px; margin: 0 0 8px; }
.reg-clearbar-spacer { flex: 1 1 auto; }
/* The uncleared chip: ON-THEME, not grey-on-grey (operator: "the text and fill of
 * this and surround icons are nonpleasing grey fill; be more on-theme please").
 * A tinted accent chip with an accent icon and a bold count reads as a live
 * filter you can tap; the ON state inverts to a solid accent fill so the filtered
 * register is unmistakable. Tokened → both themes (rule 14). */
.reg-uncleared-banner {
  flex: 0 1 auto; display: inline-flex; align-items: center; gap: 6px; margin: 0;
  padding: .42rem .8rem; border: 0; border-radius: 999px; cursor: pointer;
  background: color-mix(in srgb, var(--accent) 12%, var(--card));
  color: var(--accent); font: inherit; font-size: var(--fs-body); font-weight: 600;
  min-width: 0; white-space: nowrap;
}
.reg-uncleared-banner b { font-weight: 800; font-variant-numeric: tabular-nums; }
.reg-uncleared-banner span { overflow: hidden; text-overflow: ellipsis; }
.reg-uncleared-banner .unc-ico { width: 15px; height: 15px; flex: 0 0 auto; }
[data-theme="dark"] .reg-uncleared-banner { background: color-mix(in srgb, var(--accent) 22%, var(--card)); }
.reg-uncleared-banner.on { background: var(--accent); color: var(--accent-ink); }
.reg-scheduled-banner {
  flex: 0 1 auto; display: inline-flex; align-items: center; gap: 6px; margin: 0;
  padding: .42rem .8rem; border: 0; border-radius: 999px; cursor: pointer;
  background: color-mix(in srgb, var(--accent) 12%, var(--card));
  color: var(--accent); font: inherit; font-size: var(--fs-body); font-weight: 600;
  min-width: 0; white-space: nowrap;
  transition: background-color var(--dur-fast) ease, transform var(--dur-fast) var(--ease-spring);
}
.reg-scheduled-banner .scheduled-ico { width: 15px; height: 15px; flex: 0 0 auto; }
.reg-scheduled-banner.on { background: var(--accent); color: var(--accent-ink); }
.reg-scheduled-banner:active { transform: scale(.96); }
@media (hover: hover) {
  .reg-uncleared-banner:hover { background: color-mix(in srgb, var(--accent) 20%, var(--card)); }
  .reg-uncleared-banner.on:hover { background: color-mix(in srgb, var(--accent) 88%, var(--canvas)); }
  .reg-scheduled-banner:hover { background: color-mix(in srgb, var(--accent) 20%, var(--card)); }
  .reg-scheduled-banner.on:hover { background: color-mix(in srgb, var(--accent) 88%, var(--canvas)); }
}
/* Select-as-icon: same round icon-button chrome as the search magnifier, so the
 * two read as one control language; both trail the chip, flush right. */
.reg-select-icon { flex: 0 0 auto; }
.reg-select-icon > span { margin-left: 4px; font-size: var(--fs-label); font-weight: 700; }
.reg-search-inline {
  flex: 1 1 12rem; min-width: 8rem; max-width: 22rem; height: var(--ctl-h-md);
  display: inline-flex; align-items: center; gap: 6px; padding: 0 .65rem;
  border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--hairline)); border-radius: 999px;
  background: color-mix(in srgb, var(--card) 92%, var(--accent)); color: var(--ink-muted);
  box-shadow: none;
}
.reg-search-inline:focus-within { border-color: var(--accent); outline: none; box-shadow: none; }
/* Compound control: wrapper above owns the ring, child owns none (part F) —
 * without this the generic body focus-visible rule still painted its own
 * outline on the input itself, doubling the wrapper's ring. */
@media (pointer: fine) {
  .reg-search-inline input:focus-visible { outline: 0; }
}
.reg-search-inline-icon { display: inline-flex; flex: 0 0 auto; }
.reg-search-inline-icon .ico { width: 18px; height: 18px; }
.reg-search-inline input { flex: 1 1 auto; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--ink); font: inherit; font-size: var(--fs-label); }
.reg-search-inline input::-webkit-search-cancel-button { display: none; }
.reg-search-inline-clear { width: 28px; height: 28px; min-width: 28px; border: 0; border-radius: 50%; background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent); display: inline-flex; align-items: center; justify-content: center; cursor: pointer; position: relative; }
.reg-search-inline-clear::after { content: ''; position: absolute; inset: -8px; }
.reg-search-inline-clear .ico { width: 14px; height: 14px; }
.reg-search-inline-clear.hidden { display: none; }
.reg-search-suggestions { display: grid; gap: 8px; margin: 0 0 10px; }
.reg-search-suggestion-group { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.reg-search-suggestion-group h2 { margin: 0 4px 0 0; color: var(--ink-muted); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; }
.reg-search-suggestion { border: 1px solid var(--hairline); border-radius: 999px; background: var(--card); color: var(--ink); padding: 5px 10px; font: inherit; font-size: 12px; cursor: pointer; }
.reg-search-suggestion:hover, .reg-search-suggestion:focus-visible { border-color: var(--accent); color: var(--accent); }

/* Current-touch search previews: bounded, real transaction rows replace the
 * old per-keystroke suggestion-pill repaint. */
.reg-search-preview { display: grid; gap: 12px; margin: 0 0 10px; padding-bottom: calc(170px + env(safe-area-inset-bottom, 0px)); }
.reg-search-preview[hidden] { display: none; }
.reg-search-preview-group,
.reg-search-focused { display: grid; gap: 6px; }
.reg-search-preview-group h2,
.reg-search-focused h2 { margin: 0; color: var(--ink-muted); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; }
body:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-preview-group > h2,
body:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-focused > h2 {
  box-sizing: border-box;
  padding-inline: 16px;
}
.reg-search-date-group {
  display: grid; gap: 0; min-width: 0; overflow: hidden;
  border: 1px solid var(--hairline); border-radius: var(--radius-s);
  background: var(--card);
}
.reg-search-date-rows { min-width: 0; }
.reg-search-date-group .reg-search-preview-date {
  border-radius: var(--radius-s) var(--radius-s) 0 0;
  padding: 4px 8px; background: var(--scheduled-fill);
}
.reg-search-preview-item {
  display: grid; gap: 2px; min-width: 0; overflow: hidden;
  padding: 4px; border: 1px solid var(--hairline); border-radius: var(--radius-s);
  background: var(--card);
}
.reg-search-preview-item .swipe-wrap,
.reg-search-preview-item .txn-row,
.reg-search-preview-item .txn-body,
.reg-search-preview-item .txn-main,
.reg-search-preview-item .txn-sub { min-width: 0; }
.reg-search-preview-item .txn-row { border-top: 0; border-radius: calc(var(--radius-s) - 2px); }
.reg-search-preview-date { color: var(--ink-muted); font-size: var(--fs-micro); padding: 0 4px; }
.reg-search-preview-more,
.reg-search-preview-back { min-height: 44px; width: 100%; border: 1px solid var(--hairline); border-radius: var(--radius-s); background: var(--card); color: var(--accent); font: inherit; font-weight: 700; cursor: pointer; }
.reg-search-preview-more:hover, .reg-search-preview-more:focus-visible,
.reg-search-preview-back:hover, .reg-search-preview-back:focus-visible { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, transparent); }
.reg-search-preview-back { justify-self: start; width: auto; padding: 0 12px; }
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]).search-preview-open .fab-txn { display: none; }
.loan-overview { padding: 18px; }
.loan-owed { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.loan-owed span { color: var(--ink-muted); font-size: var(--fs-label); }
.loan-owed strong { font-size: 1.5rem; }
.loan-facts { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin: 18px 0; }
.loan-facts div { min-width: 0; }
.loan-facts dt { color: var(--ink-muted); font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .06em; }
.loan-facts dd { margin: 4px 0 0; color: var(--ink); font-size: var(--fs-body); }
.loan-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.loan-tabs { display: flex; gap: 4px; margin: 14px 0 8px; border-bottom: 1px solid var(--hairline); }
.loan-tab { border: 0; border-bottom: 2px solid transparent; background: transparent; color: var(--ink-muted); padding: 8px 12px; font: inherit; cursor: pointer; }
.loan-tab.on { color: var(--accent); border-bottom-color: var(--accent); }
.loan-activity { padding: 0 14px; }
.loan-activity-row { display: grid; grid-template-columns: 7rem minmax(0, 1fr) auto auto; gap: 10px; align-items: center; min-height: 42px; border-bottom: 1px solid var(--hairline); font-size: var(--fs-body); }
.loan-activity-row > span:first-child { color: var(--ink-muted); font-size: var(--fs-label); }
.loan-prompt { width: min(440px, calc(100vw - 32px)); padding: 16px; }
.loan-prompt .ps-field { display: grid; gap: 5px; margin: 10px 0; }
@media (max-width: 600px) { .loan-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); } .loan-activity-row { grid-template-columns: 5.5rem minmax(0, 1fr) auto auto; gap: 6px; } }
.txn-cat-inline-first-line { flex: 0 1 auto; max-width: 34%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.date-today-label { color: var(--accent); font-size: .82em; font-weight: 800; letter-spacing: .08em; margin-right: .28rem; }
body:is([data-surface="phone"], [data-surface="tablet"]) .selection-mode .txn-row.selecting { transition: background-color var(--dur-fast) var(--ease-standard), transform var(--dur-fast) var(--ease-spring); }
body:is([data-surface="phone"], [data-surface="tablet"]) .selection-mode .sel-circle { transition: background var(--dur-fast) var(--ease-standard), border-color var(--dur-fast) var(--ease-standard), transform var(--dur-fast) var(--ease-spring); }

/* Register: compact working-balance row on the phone/tablet list (§5). */
.bal-equation.bal-mobile { flex-wrap: wrap; row-gap: 2px; margin-bottom: 10px; }

/* ============================================================================
 * Help / Getting-started screen (/help) + the auto install nudge (2026-07-13).
 * Token-only, both themes (rule 14). The install bar floats OVER content, so it
 * gets the same frosted-glass rim as the rest of the chrome.
 * ==========================================================================*/

.help-intro { color: var(--ink-muted); margin: -.4rem 0 1.2rem; max-width: 46rem; }
/* Numbered part headers ("1 · Put it on your home screen"). */
.help-part {
  font-size: var(--fs-h3); font-weight: 700; color: var(--ink);
  margin: 1.6rem 0 .7rem; letter-spacing: -0.01em;
}
.help-part:first-of-type { margin-top: .4rem; }
.help-card { max-width: 46rem; }
.help-card h2 { font-size: var(--fs-value); margin: 0 0 .5rem; }
.help-card h3 { font-size: var(--fs-value); margin: .9rem 0 .35rem; color: var(--ink); }
.help-block { margin-top: .3rem; }
.help-sub { color: var(--ink-muted); font-weight: 600; margin: .2rem 0 .6rem; }
.help-note { margin-top: .8rem; }

/* Numbered step lists — big tap-friendly rows, generous spacing so a
 * non-technical reader never loses their place. */
.help-steps { margin: .3rem 0 .2rem; padding-left: 1.4rem; }
.help-steps li { margin: .5rem 0; line-height: 1.5; }
.help-list { margin: .3rem 0; padding-left: 1.2rem; }
.help-list li { margin: .55rem 0; line-height: 1.5; }

/* The "important — export is web-only" callout, reusing the ob-callout look. */
.help-callout {
  text-align: left; background: var(--chip); border-radius: var(--radius-s);
  border-left: 3px solid var(--accent); padding: .8rem 1rem; margin: .6rem 0 1rem;
}
.help-callout p { margin: 0; font-size: var(--fs-body); }

/* The other-platform install steps, collapsed by default. */
.help-other summary {
  cursor: pointer; font-weight: 600; color: var(--accent); list-style: none;
}
.help-other summary::-webkit-details-marker { display: none; }
.help-footer { max-width: 46rem; margin-top: 1.4rem; }

/* The Scheduled header carries two buttons; let them wrap below the title on a
 * narrow phone rather than crushing it (the 152px pad-right leaves no room for
 * title + both buttons on one line). The buttons then form ONE right-aligned
 * cluster on the wrapped line: with plain space-between they flew to opposite
 * edges, leaving a chasm between them and no gap at all above the first row.
 * margin-left:auto on the first button pushes the pair right as a unit;
 * row-gap seats them under the title, and the row's own bottom margin
 * re-opens the space the .screen-title margin used to provide. */
.sched-titlerow {
  flex-wrap: wrap;
  justify-content: flex-start;
  row-gap: .5rem;
  margin-bottom: .75rem;
}
.sched-titlerow .screen-title { flex: 1 1 auto; }
/* The two buttons travel as ONE right-aligned cluster: margin-left:auto pushes
 * the pair right while they share a line, and because they wrap TOGETHER (the
 * cluster is a single flex item) the narrowest phones drop both onto line 2 side
 * by side instead of stranding Add alone on a third line. */
.sched-btn-cluster {
  display: flex; align-items: center; gap: .75rem;
  flex: 0 0 auto; margin-left: auto;
}
.sched-suggest-btn, .sched-add-btn { flex: 0 0 auto; }

/* --- One-time "open on Add transaction?" offer (operator 2026-07-17) ------ */
/* Phone-only, so it's sized for a thumb: a bottom sheet with the two choices as
 * full-width stacked buttons in easy reach, never a pair of tiny side-by-side
 * targets. Colours are all tokens → dark mode follows (rule 14). */
.aoo-backdrop {
  position: fixed; inset: 0; z-index: 90;
  display: flex; align-items: flex-end; justify-content: center;
  background: var(--modal-scrim);
  -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
}
.aoo-sheet {
  width: 100%; max-width: 440px;
  background: var(--card); color: var(--ink);
  border: 1px solid var(--chrome-edge);
  border-radius: 20px 20px 0 0;
  padding: 18px 20px calc(18px + env(safe-area-inset-bottom, 0px));
  box-shadow: 0 -8px 40px rgba(0, 0, 0, .28);
  text-align: center;
  animation: aoo-rise .26s cubic-bezier(.22, 1, .36, 1);
}
@keyframes aoo-rise { from { transform: translateY(100%); } to { transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) { .aoo-sheet { animation: none; } }
.aoo-art { width: 96px; height: 76px; display: block; margin: 0 auto 10px; color: var(--accent); }
.aoo-title { margin: 0 0 6px; font-size: var(--fs-h2); font-weight: 700; }
.aoo-body { margin: 0 0 8px; line-height: 1.45; }
.aoo-acts { display: flex; flex-direction: column; gap: 8px; margin-top: 14px; }
.aoo-acts .btn { width: 100%; }

/* --- Auto install nudge ------------------------------------------------- */
/* A slim bar pinned to the bottom, sitting ABOVE the floating tab bar so it
 * never covers navigation. On desktop there's no bottom tab bar, so it rests
 * just above the bottom edge. */
.install-bar {
  position: fixed; left: 12px; right: 12px; z-index: 60;
  bottom: calc(76px + env(safe-area-inset-bottom, 0px));
  display: flex; align-items: center; gap: .6rem;
  padding: .6rem .7rem .6rem .9rem;
  background: color-mix(in srgb, var(--card) 88%, transparent);
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  border: 1px solid var(--chrome-edge); border-radius: 14px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.16);
  animation: install-bar-in .28s ease-out both;
}
body[data-surface="desktop"] .install-bar {
  left: auto; right: 16px; bottom: 16px; max-width: 22rem;
}
@keyframes install-bar-in {
  from { transform: translateY(12px); opacity: 0; }
  to { transform: translateY(0); opacity: 1; }
}
.install-bar-text { flex: 1 1 auto; font-size: var(--fs-body); color: var(--ink); line-height: 1.35; }
.install-bar-go {
  flex: 0 0 auto; border: 0; cursor: pointer; font: inherit; font-weight: 700;
  padding: .45rem .8rem; border-radius: 999px;
  background: var(--accent); color: var(--accent-ink);
}
.install-bar-x {
  flex: 0 0 auto; border: 0; background: transparent; cursor: pointer;
  color: var(--ink-muted); font-size: var(--fs-h2); line-height: 1; padding: 0 .3rem;
}

/* Belt-and-braces: nothing tappable renders under the iOS 44px minimum on
 * touch surfaces, no matter which component forgot to opt in (Part 3).
 *
 * EXCEPTION — the circular chrome discs (settings gear, privacy eye, scroll-to-
 * top disc, the round sheet ×, the FAB) set their OWN equal width & height and a
 * border-radius:50%. A blanket min-height:44px stretched them vertically while
 * width stayed fixed, so a 40×40 circle became a 40×44 EGG (operator 2026-07-14,
 * measured: gear/eye 40×44, disc 36×39.6). They are already ≥36–40px — a
 * comfortable tap target — so exempting them keeps the touch floor honoured
 * while restoring a true circle.
 *
 * EXCEPTION — .key (operator 2026-07-19): the numpad keys are a DELIBERATE
 * shrink, traded for a bigger amount readout above — not a component that
 * forgot to opt in. This floor was silently winning over the shorter .key
 * min-height set earlier in this file (higher specificity, same surface
 * selector), so the keys stayed stuck at 44px until this line was added.
 *
 * EXCEPTION — .discard-actions .btn (operator 2026-07-19): the SAME kind of
 * silent clamp hit the discard-confirm popup's Discard/Stay buttons — this
 * later, more specific rule was overriding their own taller min-height
 * (4.2rem, a deliberate one-off exception for the last-resort dismiss popup)
 * back down to the 44px floor. Excluded so that popup's own rule wins. */
body[data-surface="phone"] :is(button, .btn, .type-pill, .reg-icon-btn):not(.settings-gear, .privacy-eye, .plan-ctl-btn, .scroll-top-disc, .sheet-x, .fab-txn, .key, .discard-actions .btn, .payee-action-tall, .nuke-btn, .reg-search-clear, .reg-search-inline-clear),
body[data-surface="tablet"] :is(button, .btn, .type-pill, .reg-icon-btn):not(.settings-gear, .privacy-eye, .plan-ctl-btn, .scroll-top-disc, .sheet-x, .fab-txn, .key, .discard-actions .btn, .payee-action-tall, .nuke-btn, .reg-search-clear, .reg-search-inline-clear) {
  /* BOTH axes. This guarded min-height only, so any control carrying a small
   * authored WIDTH shipped a target narrower than a finger while passing every
   * height-based check — the search clear button rendered 26x44 on all phone
   * and tablet profiles (sweep 27/07/2026). A one-axis floor is not a floor.
   * `.reg-search-clear` and `.reg-search-inline-clear` are excluded because
   * they keep deliberately small visible discs
   * 26px disc and widens its TAP area with a transparent ::after overlay
   * instead, so growing its box here would undo that. */
  min-height: var(--ctl-h-md);
  min-width: var(--ctl-h-md);
}

/* ---------- Approval queue (app-wide banner + sheet, operator 2026-07-14) ---------- */
/* A single pinned bar at the very top of the chrome whenever anything awaits
 * approval: due schedule occurrences + posted-but-unapproved txns. Tokens only,
 * both themes (rule 14). */
.approve-topbar {
  display: flex; align-items: center; gap: 10px; width: 100%;
  border: 0; text-align: left; cursor: pointer; font: inherit;
  padding: 10px 14px; padding-top: calc(10px + env(safe-area-inset-top));
  background: color-mix(in srgb, var(--accent) 14%, var(--card));
  color: var(--ink); border-bottom: 1px solid var(--chrome-edge);
}
/* Phone/tablet: the version/eye/gear cluster is position:fixed at the top-right
 * (z-30). It used to float OVER the banner's "Review ›" and hide it, and the
 * banner's big safe-area padding left a dead band above it (operator 2026-07-16:
 * "incredible vertical wasted space… yet the banner is hidden behind the version/
 * settings row too"). Fix both: (1) the banner sits at the very top of flow with
 * no extra band, and (2) it reserves right-hand room so its text + Review never
 * slide under the fixed discs — the discs sit ON the banner's right, tappable,
 * exactly like the YNAB reference. The banner also raises the eye/gear/version
 * cluster's own stacking so they stay clickable above it. */
/* PORTRAIT TOO (operator 26/07/2026): "the HOME/SPENDING etc header has been
 * forced down from top… the tab header must always be as high as possible, the
 * header across tabs must always be in the same position." The 56px band was
 * pure empty space whose only job was to drop the banner BELOW the fixed
 * version/eye/gear cluster — and because the banner sits in flow, it pushed
 * every screen title down with it: measured `.screen-title` at y=108 with the
 * banner present against y=52 without it, a 56px shift in the ONE state a new
 * family always hits (a fresh import leaves the approval queue full). The
 * landscape rule below already solved this the right way — step AROUND the
 * cluster sideways rather than below it — so portrait now does the same. The
 * cluster is 148px wide at the top right; 166px of right padding clears it with
 * room for the tap targets, exactly as landscape has done since v1.24.0.
 * Vertical compactness is the invariant here (rule: the title is as high as
 * reasonably possible AND identical across tabs); guarded by
 * tests/screen-title-position.test.js. */
/* BELOW THE TITLE, so it clears the fixed cluster by POSITION, not by padding
 * (operator 26/07/2026: "plan/spending etc title always atop with the 4 buttons.
 * banner can go below, untruncated. v always must show").
 *
 * Every previous attempt tried to share the top row with the version/eye/gear
 * cluster and lost something each time: a 56px band that moved every title, then
 * a reserved width that was measured on the wrong tab and overlapped on Plan,
 * then a truncated label and a hidden version tag. Sitting a row lower costs
 * none of them — it is a card in the content flow like any other, full width,
 * with the title and its four buttons undisturbed above.
 * Guarded by tests/screen-title-position.test.js. */
body[data-surface="phone"] .approve-topbar,
body[data-surface="tablet"] .approve-topbar {
  /* Image 3 contract: the approval banner and Ready-to-Assign card share the
   * same content rail.  Keep the vertical rhythm/padding unchanged; only the
   * inline width/margin are owned here so the painted edges are identical. */
  width: 100%; margin: 0 0 10px; padding: 12px 14px;
  border-radius: 12px; border-bottom: 0;
  min-height: 52px;
}
/* A phone in landscape has ~393px of height and no shortage of width, so that
 * 56px band — pure empty space that exists only to drop below the fixed
 * version/eye/gear cluster — costs 14% of the screen before a single row shows
 * (operator 2026-07-17: compactness within reason on tight landscape). The
 * cluster is 148px wide and 52px tall at the top RIGHT, so here the banner
 * takes the top row and steps around it sideways instead: same tap targets,
 * same theme, one row back. */
@media (orientation: landscape) and (max-height: 500px) {
  /* The banner now sits below the title on every orientation, so it no longer
   * needs to step around the fixed cluster here either — only to stay compact,
   * which matters most on a ~393px-tall landscape phone. */
  body[data-surface="phone"] .approve-topbar {
    margin: 0 0 6px; padding: 8px 12px; min-height: 44px;
  }
}
/* Keep the fixed chrome cluster ABOVE the banner so its discs stay tappable, and
 * drop the banner's own text a hair so it reads as one row with them. */
body:has(.approve-topbar) .settings-gear,
body:has(.approve-topbar) .privacy-eye,
body:has(.approve-topbar) .app-version-tag { z-index: 41; }
/* Desktop #app is a horizontal flex row. Leaving this 100%-wide banner in that
 * row consumed virtually the entire viewport, squeezed .content to zero, and
 * pushed PLAN off-screen whenever an approval was waiting. Pin it above the
 * content rail instead; the offsets are the sidebar's real border-box widths. */
/* SUPERSEDED 26/07/2026: the banner now renders INSIDE .content, below the
 * screen title, so it is already inside the content rail and needs no fixed
 * pinning or sidebar-width offsets — the very things that made it fragile
 * (a collapsed sidebar needed its own override, and the content needed a
 * matching 72px reserve that had to stay in step with the banner's height). */
/* HEIGHT (operator 04/08/2026): "option one except the banner can be slightly
 * taller than the video". The reference strip measures ~32px at this viewport;
 * 38px keeps the operator's margin over it while returning 8px of the 46px it
 * used to take, and the 12px→8px margin returns another 4px. This is now the
 * ONLY approve banner on desktop (D-097), so the in-card one's 54px slot is
 * reclaimed on top of that. */
body[data-surface="desktop"] .approve-topbar {
  margin: 0 0 8px; padding: 6px 14px; border-radius: 10px; border-bottom: 0;
  box-sizing: border-box; min-height: 38px;
}
body[data-surface="desktop"] .approve-topbar-empty { visibility: hidden; }
/* The desktop CTA is a real button face, not a bare "Review ›" link: it now
 * performs the video's action (filter this register) and is the banner's whole
 * point, so it reads as pressable. */
body[data-surface="desktop"] .approve-topbar-go {
  background: var(--accent); color: var(--accent-ink);
  padding: 4px 14px; border-radius: 999px; font-size: var(--fs-label);
}
body[data-surface="desktop"] .approve-topbar-approve {
  flex: 0 0 auto;
  border: 1px solid color-mix(in srgb, var(--accent-ink) 28%, transparent);
  background: var(--accent); color: var(--accent-ink);
  padding: 4px 14px; border-radius: 999px;
  font: inherit; font-size: var(--fs-label); font-weight: 700;
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-spring), filter var(--dur-fast) var(--ease-standard);
}
body[data-surface="desktop"] .approve-topbar-approve:active { transform: scale(.95); filter: brightness(.93); }
body[data-surface="desktop"] .approve-topbar-approve:disabled { opacity: .72; cursor: wait; }
body[data-surface="desktop"] .approve-topbar-approve.is-busy { filter: saturate(.8); }
.approve-topbar-dot {
  flex: 0 0 auto; min-width: 22px; height: 22px; padding: 0 6px; border-radius: 11px;
  background: var(--accent); color: var(--accent-ink);
  display: inline-flex; align-items: center; justify-content: center;
  font-weight: 800; font-size: var(--fs-label); font-variant-numeric: tabular-nums;
}
/* UNTRUNCATED (operator 26/07/2026: "banner can go below, untruncated").
 * The banner now sits BELOW the screen title (shell.js #mountApproval) instead
 * of sharing the top row with the fixed version/eye/gear cluster, so it has the
 * full width and no longer has to choose between truncating its text, wrapping
 * (which made it taller and moved every title), and hiding the version tag.
 * The ellipsis added in v1.76.0 was only ever a symptom of that squeeze. */
.approve-topbar-txt { flex: 1 1 auto; font-weight: 700; font-size: var(--fs-body); min-width: 0; }
.approve-topbar-go { flex: 0 0 auto; color: var(--accent); font-weight: 700; font-size: var(--fs-label); }

/* Approval is a Spending review surface, not a smaller dialog inside the app.
 * It inherits the Add Transaction sheet geometry: docked full-width on phone,
 * centred with the same 480px measure on tablets and desktop. */
body[data-surface="phone"] .sheet-overlay:has(.approve-sheet) { align-items: flex-end; }
.approve-sheet { width: 100%; max-width: 480px; height: 96dvh; max-height: 96dvh; border-radius: 24px 24px 0 0; }
body[data-surface="desktop"] .approve-sheet,
body[data-surface="tablet"] .approve-sheet { height: auto; max-height: 90dvh; border-radius: 20px; }
.approval-suspended { display: none; }
/* The review header reuses the Add Transaction sheet's grip, type scale and
 * close geometry, but has only one sentence-case title row. */
.approve-head { grid-template-columns: 2.4rem 1fr 2.4rem; row-gap: 5px; padding-top: 5px; }
.approve-head .sheet-grip { grid-row: 1; }
.approve-title { grid-column: 2; grid-row: 2; min-width: 0; font-size: var(--fs-value); font-weight: 700; text-align: center; text-transform: none; white-space: nowrap; }
.approve-count { display: inline-flex; align-items: center; justify-content: center; min-width: 1.4rem; height: 1.4rem; margin-left: 4px; padding: 0 .28rem; border-radius: 999px; background: var(--chip); color: var(--ink-muted); font-size: var(--fs-label); font-variant-numeric: tabular-nums; }
.approve-head .sheet-x { position: static; grid-column: 3; grid-row: 2; justify-self: end; align-self: center; width: 38px; height: 38px; min-width: 38px; box-shadow: none; }
/* Touch recording layout: one centred explanatory line, a bold count beneath,
 * and the close disc aligned to the right of that header block. Desktop keeps
 * the compact single-line title above. */
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-head-touch {
  row-gap: 2px; padding-top: 8px; padding-bottom: 6px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-head-touch .sheet-grip { display: none; }
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-head-touch .approve-title {
  grid-column: 1 / 3; grid-row: 1 / 3; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 6px; font-size: var(--fs-body);
  line-height: 1.2; margin-right: 6px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-head-touch .approve-title-label {
  font-size: var(--fs-body); font-weight: 500; white-space: nowrap;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-head-touch .approve-count {
  display: block; min-width: 0; height: auto; margin: 0; padding: 0; border-radius: 0;
  background: transparent; color: var(--ink); font-size: var(--fs-value); font-weight: 800;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-head-touch .sheet-x {
  grid-column: 3; grid-row: 1 / 3; z-index: 1; width: var(--ctl-h-md); height: var(--ctl-h-md);
  min-width: var(--ctl-h-md);
}
.approve-sheet .sheet-scroll { gap: 6px; padding-top: 0; }
.approve-group { padding: 0; }
.approve-group + .approve-group { padding-top: 6px; border-top: 1px solid var(--hairline); }
.approve-group-h { margin: 0; padding: 0 2px; font-size: var(--fs-label); font-weight: 600; text-transform: none; letter-spacing: 0; color: var(--ink-muted); }
.approve-date-heading {
  margin: 0; padding: 10px 12px 8px; border-top: 1px solid var(--hairline);
  background: color-mix(in srgb, var(--card) 88%, var(--canvas)); color: var(--ink);
  font-size: var(--fs-body); font-weight: 800; line-height: 1.2;
}
/* Full-width transaction rows, separated like Spending rows rather than nested
 * cards. The primary name/meta area is the doorway to the real transaction. */
.approve-row {
  display: flex; flex-direction: column; gap: 4px; padding: 10px 2px;
  border: 0; border-top: 1px solid var(--hairline); border-radius: 0;
  transition: transform .22s var(--ease-standard), border-color .15s ease, background-color .15s ease;
  background: var(--canvas); position: relative; z-index: 1;
}
.approve-row:first-of-type { border-top-color: var(--hairline); }
.approve-row-top { display: flex; align-items: center; gap: 10px; }
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-top { gap: 4px; }
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row { padding-inline: 1px; }
.approve-row-main { flex: 1 1 auto; display: flex; flex-direction: column; min-width: 0; }
.approve-row-name-line { display: flex; align-items: center; gap: 7px; min-width: 0; }
.approve-row-flag {
  flex: 0 0 auto; width: .8rem; height: 1.05rem; background: var(--approval-flag, var(--accent));
  clip-path: polygon(0 0, 100% 0, 100% 72%, 52% 60%, 0 72%); opacity: .9;
}
.approve-row-category {
  align-self: flex-start; max-width: 100%; margin-top: 3px; padding: .18rem .55rem;
  border-radius: 999px; background: var(--chip); color: var(--ink); font-size: var(--fs-label);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.approve-row-memo { margin-top: 3px; color: var(--ink); font-size: var(--fs-body); line-height: 1.25; white-space: normal; overflow-wrap: anywhere; }
.approve-row-meta { margin-top: 2px; }
/* The tappable name/meta block opens the transaction — hint it (operator 2026-07-16). */
.approve-row-main[role="button"] { cursor: pointer; }
.approve-select {
  flex: 0 0 auto; width: var(--ctl-h-md); min-width: var(--ctl-h-md); height: var(--ctl-h-md);
  border: 1px solid var(--chrome-edge); border-radius: 50%; background: transparent;
  color: var(--accent-ink); font: inherit; font-weight: 800; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
}
.approve-select.selected { background: var(--accent); border-color: var(--accent); }
/* Desktop: match the slim .approve-row-btn metrics below — the 44px touch disc
 * read as chunky beside it (2026-07-21 audit; same rationale as that rule). */
body[data-surface="desktop"] .approve-select {
  width: var(--ctl-h-sm); min-width: var(--ctl-h-sm); height: var(--ctl-h-sm);
}
/* Defer (snooze) chip strip: a faint label + 1·2·3-day pills, right-aligned so it
 * reads as a secondary control under the primary Approve. */
.approve-row-snooze {
  display: flex; align-items: center; gap: 6px; justify-content: flex-end;
  padding-left: 2px;
}
.approve-row-snooze .approve-snooze-days { display: grid; grid-template-columns: 0fr; gap: 0; overflow: hidden; transition: grid-template-columns .24s var(--ease-spring), gap .24s var(--ease-spring); }
.approve-row-snooze .approve-snooze-days > * { min-width: 0; overflow: hidden; padding: 0; opacity: 0; transition: padding .2s ease, opacity .14s ease; }
.approve-row-snooze.days-open .approve-snooze-days { grid-template-columns: repeat(3, 1fr); gap: 4px; }
.approve-row-snooze.days-open .approve-snooze-days > * { padding: .3rem .55rem; opacity: 1; }
/* Snooze-approval chips inside the txn detail sheet — same pill language as the
 * queue's defer chips (operator 2026-07-16). */
.snooze-row .snooze-chips { flex: 0 0 auto; display: flex; gap: 6px; }
.snooze-chip {
  min-width: 34px; min-height: var(--ctl-h-sm); padding: .2rem .5rem; border-radius: 999px;
  font-size: var(--fs-label); font-weight: 700; font-variant-numeric: tabular-nums;
  border: 1px solid var(--chrome-edge); background: var(--card); color: var(--ink-muted);
}
@media (hover: hover) { .snooze-chip:hover { border-color: var(--accent); color: var(--accent); } }
.approve-row-name { font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.approve-row-meta {
  font-size: var(--fs-label); white-space: normal; overflow-wrap: anywhere;
  line-height: 1.3;
}
.approve-row-amt { flex: 0 0 auto; font-weight: 700; font-variant-numeric: tabular-nums; }
.approve-row-amt.pos { color: var(--pos); }
.approve-row-amt.reminder { color: var(--ink-muted); font-style: normal; font-weight: 600; }
/* Approve tap target (operator, SETUP-GATE-DESIGN.md): a primary decision on a
 * phone must clear the 44px touch floor — the old plain button sized to its
 * padding alone fell short. min-height/min-width use the shared touch token
 * (rule 19: verified with device-matrix-harness, never eyeballed). */
.approve-row-btn {
  flex: 0 0 auto; padding: .4rem .9rem; font-size: var(--fs-label);
  min-height: var(--ctl-h-md); min-width: var(--ctl-h-md);
}
.approve-row-delete {
  flex: 0 0 auto; min-height: var(--ctl-h-md); min-width: var(--ctl-h-md);
  padding: .4rem .72rem; font-size: var(--fs-label);
  color: var(--accent-ink); background: var(--neg); border-color: var(--neg);
}
/* Touch approval rows carry five siblings on a 372px phone measure.  The
 * original button padding made the fixed 44px Enter-now floor plus the two
 * labelled actions total 12px beyond the row; keep the touch floor while
 * letting the labelled controls share the available grouped geometry. */
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-btn { padding-inline: .55rem; }
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-delete { padding-inline: .5rem; }
/* The queue keeps its explicit actions for keyboard/assistive access, but the
 * touch row has five siblings on a narrow phone. Compact the three labelled
 * actions to their own measured widths so long category/memo text cannot push
 * them into the amount rail or overlap the neighbouring control. */
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-btn,
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-enternow,
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-delete {
  min-width: 44px; width: 58px; padding-inline: .25rem; font-size: var(--fs-label);
  line-height: 1.15; white-space: normal;
}
.approve-row-btn.is-acting,
.approve-all.is-acting,
.approve-row-delete.is-acting,
.approve-action-rail button.is-acting {
  opacity: .72; transform: scale(.96);
}
/* Desktop (Win11): the touch floor above is wasted on a mouse — the row read as
 * chunky next to the slim inline-register Cancel/Save pair (operator 2026-07-15).
 * Reuse that pair's exact metrics (--ctl-h-sm + matching padding) so the two
 * button languages match; touch surfaces keep the 44px floor untouched. */
body[data-surface="desktop"] .approve-row-btn {
  min-height: var(--ctl-h-sm); min-width: 0; padding: .3rem 1.1rem;
}
body[data-surface="desktop"] .approve-row-delete {
  min-height: var(--ctl-h-sm); min-width: 0; padding: .3rem .72rem;
}
/* Desktop has no finger reveal. Snooze opens its 1/2/3-day choices inline; the
 * row body remains the Enter amount path. */
.approve-desktop-snooze { display: none; }
body[data-surface="desktop"] .approve-desktop-snooze { display: inline-flex; align-items: center; gap: 5px; }
.approve-snooze-toggle, .approve-snooze-days button {
  min-height: var(--ctl-h-sm); border: 0; border-radius: 999px; background: var(--chip); color: var(--ink); font: inherit; font-size: var(--fs-label); font-weight: 700; cursor: pointer;
}
.approve-snooze-toggle { padding: .3rem .8rem; }
.approve-snooze-days { display: grid; grid-template-columns: 0fr; gap: 0; overflow: hidden; transition: grid-template-columns .24s var(--ease-spring), gap .24s var(--ease-spring); }
.approve-snooze-days > * { min-width: 0; overflow: hidden; padding: 0; opacity: 0; transition: padding .2s ease, opacity .14s ease; }
.approve-desktop-snooze.days-open .approve-snooze-days { grid-template-columns: repeat(3, 1fr); gap: 4px; }
.approve-desktop-snooze.days-open .approve-snooze-days > * { padding: .3rem .55rem; opacity: 1; }
/* Once approved, the WHOLE row (not just the control) takes a green outline —
 * "the entire box containing the transaction" (operator). Token only (--pos),
 * both themes (rule 14). */
.approve-row.approved {
  border-color: var(--pos);
  box-shadow: inset 0 0 0 1px var(--pos);
  background: color-mix(in srgb, var(--pos) 8%, transparent);
}
.approve-swipe {
  position: relative; overflow: hidden; border-radius: 0;
  --approve-swipe-progress: 0;
  touch-action: pan-y;
  transition: max-height .31s var(--ease-standard), margin .31s var(--ease-standard), opacity .2s ease;
}
.approve-swipe::before {
  content: ''; position: absolute; inset: 0 0 auto; height: 1px;
  background: var(--hairline); pointer-events: none; z-index: 2;
}
.approve-swipe-rail {
  position: absolute; inset: 4px 0; display: flex; align-items: center; justify-content: flex-end; padding-right: 18px;
  background: color-mix(in srgb, var(--pos) 72%, var(--canvas)); color: var(--ink);
  font-size: var(--fs-label); font-weight: 800; opacity: var(--approve-swipe-progress);
  transition: opacity .16s ease, background-color .16s ease;
}
.approve-choice-rail { left: auto; width: min(276px, 84%); padding: 0; justify-content: stretch; overflow: hidden; background: transparent; }
.approve-action-rail {
  left: auto; width: min(220px, 68%); padding: 0; gap: 6px;
  max-width: 100%; box-sizing: border-box; overflow: hidden;
  justify-content: stretch; background: transparent;
}
.approve-action-rail button {
  flex: 1 1 0; align-self: stretch; min-width: 0; min-height: var(--ctl-h-md);
  padding-inline: .5rem; box-sizing: border-box; border: 0;
  color: var(--ink); background: var(--chip); font: inherit;
  font-size: var(--fs-label); font-weight: 800; cursor: pointer;
}
.approve-action-rail .approve-choice-delete { background: var(--neg); color: var(--accent-ink); }
.approve-row-delete:active,
.approve-action-rail .approve-choice-delete:active,
.modal-danger:active { transform: scale(.96); }
.approve-row-delete:disabled,
.approve-action-rail .approve-choice-delete[aria-busy="true"],
.modal-danger:disabled { opacity: .72; }
.approve-choice-primary, .approve-choice-days { position: absolute; inset: 0; display: flex; align-items: center; gap: 6px; transition: transform .25s var(--ease-spring), opacity .16s ease; }
.approve-choice-primary { transform: translateX(0); }
.approve-choice-days { transform: translateX(112%); opacity: 0; pointer-events: none; }
.approve-choice-rail button {
  flex: 1 1 0; align-self: stretch; min-height: var(--ctl-h-md); border: 0; border-radius: 999px; color: var(--ink); background: var(--chip);
  font: inherit; font-size: var(--fs-label); font-weight: 800; line-height: 1.15; cursor: pointer;
}
.approve-choice-enter { background: var(--accent) !important; color: var(--accent-ink) !important; }
.approve-swipe-choosing-days .approve-choice-primary { transform: translateX(-112%); opacity: 0; pointer-events: none; }
.approve-swipe-choosing-days .approve-choice-days { transform: translateX(0); opacity: 1; pointer-events: auto; }
.approve-swipe-revealed .approve-choice-rail { opacity: 1; }
.approve-swipe-dragging .approve-row { transition: none; }
.approve-swipe-returning .approve-row { transition: transform .32s var(--ease-spring); }
.approve-swipe-clearing { max-height: 0; margin-top: 0; margin-bottom: -1px; opacity: 0; pointer-events: none; }
.approve-swipe-clearing .approve-row { transform: translateX(-112%) !important; }
/* Swipe is a direct-manipulation affordance on touch hardware. Mouse/keyboard
 * users still receive the same result through the visible buttons and selection
 * bar, avoiding an undiscoverable desktop-only drag gesture. */
body[data-surface="desktop"] .approve-swipe-rail { display: none; }
body[data-surface="desktop"] .approve-swipe { overflow: visible; }
.approve-bulk {
  flex: 0 0 auto; display: flex; align-items: center; gap: 8px;
  padding: 10px 2px 2px; border-top: 1px solid var(--hairline);
  background: var(--canvas);
}
.approve-bulk-count { flex: 1 1 auto; font-size: var(--fs-label); font-weight: 800; }
.approve-bulk-clear { border: 0; background: transparent; color: var(--ink-muted); font: inherit; font-weight: 700; cursor: pointer; padding: .45rem .55rem; }
.approve-bulk-go { flex: 0 0 auto; min-width: 7rem; }
.approve-all { width: 100%; margin: 0 0 8px; }
.approve-empty { padding: 24px 8px; text-align: center; }
@media (prefers-reduced-motion: reduce) {
  .approve-swipe, .approve-row, .approve-swipe-rail { transition: none; }
}

/* ---------- The Big Picture (top Home tile, operator 2026-07-14) ---------- */
.bigpic-card { }
.bigpic-h { margin: 0 0 12px; }
.bigpic-stats {
  display: flex; gap: 4px; margin-bottom: 12px; flex-wrap: wrap;
}
/* Stat strip, restyled (operator 2026-07-14: "Avg income and Avg expense is
 * graphically so misstyled — consider INCOME and EXPENSE and then smaller but
 * still-all-caps 'average' in a different subtler colour below each word").
 * Each block is a 3-line stack: the WORD in its series colour, a small all-caps
 * muted "AVERAGE" beneath it, then the figure. The ratio block leads with the
 * percentage itself (the "Spends" label is gone, per the operator) and carries a
 * plain-English read underneath. */
/* Each stat is a CENTRED stack (operator 2026-07-14: "center their stack of e.g.
 * INCOME, AVERAGE, and the value shown below the text 'average'"). */
.bigpic-stat { flex: 1 1 30%; display: flex; flex-direction: column; align-items: center;
  text-align: center; gap: 1px; min-width: 0; flex-basis: 0; }
.bigpic-word {
  font-style: normal; font-weight: 800; font-size: var(--fs-value);
  letter-spacing: -.01em; line-height: 1.15;
}
.bigpic-word-in { color: var(--pos); }
.bigpic-word-out { color: var(--neg); }
.bigpic-word-ratio { color: var(--ink); font-variant-numeric: tabular-nums; }
/* "AVERAGE" smaller still (operator 2026-07-14: "make the grey 'average' text
 * below both income and expense even smaller"). It is a caption on a caption —
 * it should whisper. */
.bigpic-avg {
  font-style: normal; font-size: .62rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .09em;
  color: var(--ink-muted); opacity: .72; line-height: 1.4;
}
.bigpic-val {
  font-size: var(--fs-h3); font-weight: 800; font-variant-numeric: tabular-nums;
  margin-top: 2px; white-space: nowrap;
}
.bigpic-val.pos { color: var(--pos); }
.bigpic-val.neg { color: var(--neg); }
/* The ratio's under-line is a word, not a figure — size it as a label. */
.bigpic-ratio-note {
  font-size: var(--fs-label); font-weight: 700; color: var(--ink-muted);
}
.bigpic-ratio-note.neg { color: var(--neg); }
/* ---------- factoid strip (operator 2026-07-14) ----------
 * "Display this in a cartoon friendly manner just above smoothed monthly burden."
 * A soft rounded speech-bubble: a big emoji face on the left, the fact in friendly
 * ink, and a quiet ↻ hinting you can tap for another. It has a real tail, drawn
 * with a rotated square, so it reads as a speech bubble rather than another card.
 * Tokened for both themes (rule 14). */
.factoid {
  position: relative; width: 100%; text-align: left;
  display: flex; align-items: center; gap: 10px;
  margin: 0 0 18px; padding: .7rem .8rem;
  border: 0; font: inherit; cursor: pointer;
  border-radius: 16px;
  background: color-mix(in srgb, var(--accent) 10%, var(--card));
  color: var(--ink);
  transition: transform var(--dur-fast) var(--ease-spring), background var(--dur-fast) var(--ease-standard);
}
[data-theme="dark"] .factoid { background: color-mix(in srgb, var(--accent) 20%, var(--card)); }
@media (hover: hover) { .factoid:hover { background: color-mix(in srgb, var(--accent) 16%, var(--card)); } }
.factoid:active { transform: scale(.985); filter: brightness(.96); }
/* The bubble's tail, pointing down at the burden headline it introduces. */
.factoid::after {
  content: ''; position: absolute; left: 26px; bottom: -5px;
  width: 12px; height: 12px; border-radius: 2px;
  background: inherit; transform: rotate(45deg);
}
.factoid-face {
  flex: 0 0 auto; font-size: 1.7rem; line-height: 1;
  /* Emoji, not an icon — deliberately the one place the app is cartoonish. */
  filter: saturate(1.05);
}
.factoid-text {
  flex: 1 1 auto; min-width: 0;
  font-size: var(--fs-body); font-weight: 600; line-height: 1.35;
}
.factoid-more {
  flex: 0 0 auto; color: var(--accent); opacity: .55;
  font-size: 1rem; line-height: 1;
}
@media (prefers-reduced-motion: reduce) { .factoid:active { transform: none; } }

.bigpic-chart { width: 100%; height: auto; margin: 4px 0 8px; }
.bigpic-slider { display: flex; align-items: center; gap: 12px; }
.bigpic-range { flex: 1 1 auto; accent-color: var(--accent); }

/* ---------- This-month / last-month stat block (per-schedule tile) ---------- */
.mstat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 10px; }
.mstat { display: flex; flex-direction: column; gap: 2px; }
.mstat-lbl { font-size: var(--fs-label); color: var(--ink-muted); font-weight: 700; }
.mstat-val { font-size: var(--fs-h3); font-weight: 800; font-variant-numeric: tabular-nums; }
.mstat-val.pos { color: var(--pos); }
.mstat-val.neg { color: var(--neg); }
.mstat-avgtoggle { display: inline-flex; gap: 0; border: 1px solid var(--hairline); border-radius: 999px; overflow: hidden; margin-bottom: 14px; }
.mstat-seg {
  border: 0; background: var(--card); color: var(--ink-muted); cursor: pointer;
  padding: .35rem .85rem; font: inherit; font-size: var(--fs-label); font-weight: 700;
}
.mstat-seg.sel { background: var(--accent); color: var(--accent-ink); }
/* Sub-heading revamp (operator 2026-07-15, punch B7): was full .fs-h3 weight —
 * heavier than the tile's other section labels and inconsistent with the
 * .mstat-lbl sub-head style used just above it in the same card. Match that
 * quieter label language (uppercase-weight, muted colour) instead of inventing
 * a new visual register. */
.persched-sub {
  margin: 14px 0 2px; font-size: var(--fs-label); font-weight: 700;
  text-transform: uppercase; letter-spacing: .02em; color: var(--ink-muted);
}
/* Compact rows: tighter vertical rhythm than the default .list-row (this list
 * can run long), name left / amount right stays via .list-row's own
 * space-between, tabular-nums keeps the figures column-aligned. */
.dash-sched { padding: .4rem 0; }
.dash-sched-amt { font-variant-numeric: tabular-nums; }
.dash-sched-amt.pos { color: var(--pos); }

/* ---------- Repeat editor: reminder hint + auto-enter toggle ---------- */
/* The Amount row is a <label> flex: "Amount" caption on the left, this wrap on
 * the right. The input and the hint stack inside it. */
.sched-amt-row { flex-wrap: wrap; }
.sched-amt-wrap { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; flex: 1 1 auto; min-width: 0; }
/* Hint takes the ROW's full width, not the input's column (operator 2026-07-17:
 * "the 'leave the amount empty' line should avail of the width avail instead of
 * reflowing into 2 rows"). It sat INSIDE .sched-amt-wrap, whose column is only
 * what is left after the "Amount" caption, so it wrapped while width sat unused
 * beside it. It is now a sibling of the caption (see recurrence-editor.js) and takes
 * the whole row via order+100%; the row is flex-wrap:wrap so this lands on its
 * own line instead of squeezing the input.
 *
 * The COPY was shortened in the same breath, because width alone could not fix
 * this and it is worth recording why: measured in-browser at 375px, the old
 * sentence needed 373px on one line while the full row offers only 291px — no
 * layout change fits it, so it would have kept wrapping however much width it
 * was given. "Leave empty and we'll ask when it's due." measures 244px and fits
 * with room to spare. Re-measure before lengthening it again. */
.sched-amt-hint {
  font-size: var(--fs-label); text-align: right;
  order: 3; flex: 1 0 100%; width: 100%; margin-top: 2px;
}
.sched-autoenter-help { margin: 6px 2px 0; }

/* Mandatory reduced-motion guard (Part 7): essential state changes still
 * happen instantly; all decorative motion is killed. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
  :is(.reg-datepop, .popover, .picker-modal, .sub-flyout) { animation: none; }
}

/* ============================================================================
 * TRUNCATION / BLEED SWEEP (operator 2026-07-14)
 * ---------------------------------------------------------------------------
 * One screenshot (the category picker, whose long rows wrapped and painted over
 * their neighbours) turned out to be one instance of a repeating mistake, so it
 * is fixed here as a CLASS rather than a list of one-offs (project rule 6:
 * encode recurring regressions as a static rule instead of re-fixing them).
 *
 * Two mechanisms cause every case:
 *   1. A flex/grid child holding text has no `min-width: 0`. Its automatic
 *      minimum size is max-content, so it REFUSES to shrink — it overflows its
 *      container instead of ellipsising, and the overflow paints over whatever
 *      is next to it.
 *   2. `text-overflow: ellipsis` without BOTH `overflow: hidden` and
 *      `white-space: nowrap` is a silent no-op: the text wraps instead.
 * The corresponding grid-track form of (1) is a bare `1fr`, whose implicit
 * `min-width: auto` widens the track to max-content — hence `minmax(0, 1fr)`
 * throughout the rules above.
 * ========================================================================== */

/* Names that must yield to the figure beside them. */
.settings-collapse-title,
.fav-group-name,
.res-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Names that are allowed to WRAP (multi-line by design) but must not overflow:
 * a single unbroken long word (a payee like "Yochifrozenyoghurtsingapore") would
 * otherwise punch straight out of the card. */
.dash-sched-name,
.payee-loc-meta,
.splitpick-name { min-width: 0; overflow-wrap: anywhere; }

/* Figures that must never be squeezed into an ellipsis of themselves. */
.avail-pill,
.dash-sched-amt,
.mgroup-total { flex: 0 0 auto; white-space: nowrap; }

/* Fixed-size circular badges: a glyph wider than the circle escaped it and
 * painted over the neighbouring column. */
.cleared-badge,
.reg-row .c-clr { overflow: hidden; }
/* The scheduled disclosure is a 28px icon button in the shared trailing rail.
 * Its canonical row track is intentionally compact, so do not clip the button
 * inside the rail; the final row cell has no following column to cover. */
body[data-surface="desktop"] .reg-row.sched-row .c-clr { overflow: visible; }

/* Manage-columns / typeahead / payee-list cells: nowrap in a fixed track with no
 * clip. */
.mcell,
.reg-drop-row { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.payees-list .pick-row { min-width: 0; }

/* Big Picture stat figures: `min-width: 90px` (not 0) means a long currency
 * cannot shrink, so it overflowed the 3-up strip onto the next stat. */
.bigpic-val { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ============================================================================
 * DESIGNED CONFIRM DIALOG (modal.js) — operator 2026-07-14: "the popup
 * confirming it should be aesthetically designed, not system popup/prompt".
 * Replaces native confirm() on the swipe-delete path. Themed, both modes.
 * ========================================================================== */
.modal-backdrop {
  position: fixed; inset: 0; z-index: 90;
  display: flex; align-items: center; justify-content: center; padding: 24px;
  background: var(--modal-scrim);
  animation: modal-fade var(--dur-fast) var(--ease-standard);
}
.modal-backdrop.closing { animation: modal-fade var(--dur-fast) var(--ease-standard) reverse; }
@keyframes modal-fade { from { opacity: 0; } to { opacity: 1; } }
.modal-confirm {
  width: min(340px, 100%); background: var(--card); color: var(--ink);
  border: 1px solid var(--hairline); border-radius: 18px; padding: 20px;
  box-shadow: 0 18px 48px rgba(0, 0, 0, .3); /* style-ok: neutral elevation */
  animation: modal-pop var(--dur-slow) var(--ease-spring);
}
/* Grows from slightly-small with the spring's overshoot — the same "expand and
 * rebound" language as the envelope bubble below, so the app has ONE motion. */
@keyframes modal-pop { from { transform: scale(.88); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.modal-title { font-size: var(--fs-h3); font-weight: 800; margin-bottom: 6px; }
/* pre-line: the stranded-entries review passes a multi-line listing (v1.20.0) */
.modal-msg { font-size: var(--fs-body); color: var(--ink-muted); line-height: 1.45; white-space: pre-line; }
.modal-actions { display: flex; gap: 10px; margin-top: 18px; }
.modal-actions .btn { flex: 1 1 0; min-height: 44px; } /* thumb-reach, both sides */
.modal-danger { background: var(--neg); color: var(--neg-ink); border-color: var(--neg); }
/* E: date-scope chooser stacks its three options (two named + Cancel)
 * vertically — a row would cramp label text at phone widths. */
.modal-actions-col { flex-direction: column; }
.modal-actions-col .btn { flex: 0 0 auto; width: 100%; }

/* ============================================================================
 * ENVELOPE BUBBLE (operator 2026-07-14) — after a transaction that moves a
 * category's remaining budget, a bubble pops up showing that envelope's NEW
 * balance. It expands out of a central dot, overshoots, rebounds, holds ~2s,
 * then plays the same motion in reverse.
 * ========================================================================== */
.env-bubble {
  position: fixed; left: 50%; z-index: 88;
  bottom: calc(env(safe-area-inset-bottom, 0px) + 92px); /* clear of the tab bar */
  transform: translateX(-50%);
  display: flex; align-items: center; gap: 12px;
  max-width: min(92vw, 460px);
  padding: 12px 16px; border-radius: 999px;
  background: var(--card); color: var(--ink);
  border: 1px solid var(--hairline);
  box-shadow: 0 12px 32px rgba(0, 0, 0, .22); /* style-ok: neutral elevation */
  pointer-events: none; /* purely informational — never eats a tap */
}
/* Expand from a central DOT and retract back into one — BOTH directions are pure
 * scale, transform-origin the bubble's own centre. Opacity is held at 1 across
 * almost the whole travel so the motion reads as a physical grow/shrink, NOT a
 * fade (operator 2026-07-15: "certainly not the fade as it retracts").
 *
 * The rebound: the entrance overshoots past scale(1) and settles back; the
 * retract briefly PUSHES OUT past scale(1) again before collapsing to the dot —
 * a spring release from its max-expansion point (operator: "a more rebound curve
 * effect at the max expansion point"). Both keyframes bake the overshoot into the
 * timeline itself with a linear-ish easing, so the rebound is identical every
 * time regardless of the easing token.
 *
 * CRITICAL (operator 2026-07-15, third report): the retract MUST use a DISTINCT
 * keyframe name (env-bubble-out), not `env-bubble-in reverse`. Reusing one
 * animation-name across the .in→.out class swap means the browser sees the name
 * unchanged and does NOT restart the animation — the finished `.in` keyframe
 * stays committed at scale(1) via `both`, so the bubble holds full size and is
 * simply removed at OUT_MS: no visible shrink. A separate name forces a restart. */
.env-bubble.in  { animation: env-bubble-in  var(--dur-bubble-in)  linear both; }
.env-bubble.out { animation: env-bubble-out var(--dur-bubble-out) linear both; }
@keyframes env-bubble-in {
  0%   { transform: translateX(-50%) scale(0);    opacity: 0; }
  55%  { transform: translateX(-50%) scale(1.08); opacity: 1; }
  75%  { transform: translateX(-50%) scale(.96);            }
  90%  { transform: translateX(-50%) scale(1.02);           }
  100% { transform: translateX(-50%) scale(1);     opacity: 1; }
}
@keyframes env-bubble-out {
  0%   { transform: translateX(-50%) scale(1);     opacity: 1; }
  22%  { transform: translateX(-50%) scale(1.09);  opacity: 1; } /* rebound out at max expansion */
  45%  { transform: translateX(-50%) scale(.9);    opacity: 1; }
  80%  { transform: translateX(-50%) scale(.28);   opacity: 1; }
  100% { transform: translateX(-50%) scale(0);     opacity: 0; }
}
.env-bubble-name {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--fs-body); font-weight: 600;
}
/* NO WOBBLE WHILE THE FIGURE TICKS (operator 29/07/2026: "entering a
 * transaction results in weird screen wobble while the 'budget update'
 * animation at bottom plays").
 *
 * MEASURED CAUSE. The bubble is centred with `left:50%` + `translateX(-50%)`,
 * so its horizontal position is a function of its own width. The tick-down
 * (env-bubble.js) rewrites this pill's text on every animation frame — "$1,204"
 * → "$984" → "$1,013" — and each rewrite is a different string width, in a
 * PROPORTIONAL font where a "1" is far narrower than a "4". The whole bubble
 * therefore re-centred sixty times a second for the length of the count. That
 * is the wobble: not an animation glitch, a layout consequence of animating text
 * inside a self-centring box.
 *
 * Two fixes, both needed:
 *   · tabular-nums makes every digit the same advance width, so a figure with
 *     the same digit COUNT never changes width mid-count;
 *   · `--tick-ch` reserves the width of the WIDEST figure in the run (set by
 *     env-bubble.js from the longer of the from/to strings), so crossing a
 *     thousands separator or a digit boundary does not resize it either.
 * The figure is right-aligned in that reserved box so it settles under a fixed
 * right edge rather than drifting as it grows. */
.env-bubble-val {
  flex: 0 0 auto; white-space: nowrap;
  font-variant-numeric: tabular-nums;
  min-width: var(--tick-ch, auto);
  text-align: right;
}
/* Toast variant (item 2, operator 2026-07-15) — reuses the same shell/animation
 * for the "app updated" notice: accent-tinted so it reads as a system message
 * rather than a category figure. Token-only for dark/light parity. */
.env-bubble-toast {
  background: color-mix(in srgb, var(--accent) 14%, var(--card));
  border-color: color-mix(in srgb, var(--accent) 35%, var(--hairline));
  /* Hug the content: a short one-liner ("App updated! v1.10.3") must not sit in a
   * bubble far wider than the text, with the icon+label packed to the left. Shrink
   * to the content width (still capped by the base .env-bubble max-width for long
   * copy) and centre the row so icon + text read as one centred unit. */
  width: max-content;
  max-width: min(92vw, 25rem);
  justify-content: center;
  text-align: center;
}
.env-bubble-toast .env-bubble-name { font-weight: 500; white-space: normal; }
.env-bubble-top {
  top: calc(env(safe-area-inset-top, 0px) + 12px);
  bottom: auto;
}
.env-bubble-dismissible {
  pointer-events: auto;
  cursor: pointer;
}
.env-bubble-update {
  gap: 10px;
  max-width: min(92vw, 25rem);
  padding: 13px 18px;
  background: linear-gradient(
    135deg,
    color-mix(in srgb, var(--accent) 24%, var(--card)),
    color-mix(in srgb, var(--accent) 12%, var(--card))
  );
  border-color: color-mix(in srgb, var(--accent) 46%, var(--hairline));
  box-shadow: 0 14px 34px var(--shadow);
}
/* Status dot only when NO line-drawing icon is supplied (the icon replaces it). */
.env-bubble-update:not(.env-bubble-hasicon)::before {
  content: '';
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 22%, transparent);
  flex: 0 0 auto;
}
.env-bubble-update .env-bubble-name { font-weight: 700; }
/* The line-drawing icon inherits the canonical blurple action accent. */
.env-bubble-icon {
  flex: 0 0 auto; width: 20px; height: 20px;
  color: var(--accent);
}

/* ============================================================================
 * "Show more" — the advanced card ROLLS OPEN (operator 2026-07-14).
 * ---------------------------------------------------------------------------
 * There was never a designed animation here: tapping "Show more" re-rendered the
 * whole sheet, and what the operator saw as "the whole screen quickly fades out
 * and back in again" was simply that teardown. The sheet is no longer rebuilt
 * visually — only the newly-inserted advanced card animates, and it does so on
 * the app's rebound curve, so the reveal has weight and settles rather than
 * blinking.
 *
 * grid-template-rows 0fr → 1fr is the one way to animate to a content height that
 * isn't known in advance (height:auto is not animatable), so nothing has to be
 * measured in JS and no magic pixel height can go stale.
 * ========================================================================== */
.sheet-adv.adv-in {
  animation: adv-open var(--dur-slow) var(--ease-spring) both;
  transform-origin: top center;
}
@keyframes adv-open {
  from { opacity: 0; transform: translateY(-6px) scaleY(.96); }
  to   { opacity: 1; transform: translateY(0) scaleY(1); }
}
@media (prefers-reduced-motion: reduce) {
  .sheet-adv.adv-in { animation: none; }
}

/* Ready to Assign in the category picker (operator 2026-07-14: "as an exception,
 * tint the fill for 'ready to assign' green"). It is not an envelope you spend
 * from — it is money still waiting for a job — so it gets the SAME lime the Plan
 * tab's Ready-to-Assign banner uses. One meaning, one colour, both themes. */
.pick-row.pick-row-rta { background: var(--rta-fill); color: var(--rta-ink); font-weight: 700; }
@media (hover: hover) { .pick-row.pick-row-rta:hover { background: var(--rta-fill); filter: brightness(.97); } }
.pick-row.pick-row-rta.sel { background: var(--rta-fill); }
/* The figure pill would otherwise sit lime-on-lime. */
.pick-row.pick-row-rta .pick-avail { background: var(--card); color: var(--ink); }

/* ============================================================================
 * CHROMATIC ABERRATION — clear glass (operator 25/07/2026)
 *
 * "introduce chromatic aberration of touch device slider and selected tile in
 * the toolbar. ditto for the settings/hide numbers etc buttons that appear on
 * top right — clear glass with chromatic aberration as the background slides
 * below it. ditto add transaction button and the entire toolbar."
 *
 * Real lens fringing, not a tint: a thick glass edge refracts red and blue by
 * different amounts, so a high-contrast edge sliding beneath the glass splits
 * into a warm rim on one side and a cool rim on the other. `backdrop-filter`
 * has no per-channel offset, so the split is built from TWO extra backdrop
 * layers over the element's own: one nudged +x carrying only the red channel,
 * one nudged -x carrying only cyan (green+blue). Recombined with `screen`, an
 * unshifted backdrop is reconstructed EXACTLY — grey stays grey — while any
 * edge picks up the offset the two layers disagree about. That is the physical
 * behaviour, and it means flat regions gain no colour cast.
 *
 * Both halves composite with `screen` (additive only) — see the rule below: a
 * multiply half darkened the host at every intermediate opacity, which the
 * operator saw as the toolbar dimming while the animation played.
 *
 * The offset is largest at the rim and fades toward the middle (a mask), since
 * refraction happens where the glass curves. Both pseudo-elements inherit the
 * host's radius and are `pointer-events:none`, so nothing about hit-testing,
 * focus or scrolling changes.
 *
 * Opt-out: `prefers-reduced-transparency` and `prefers-reduced-motion` drop the
 * fringe back to plain frosted glass (the fringe reads as motion when content
 * scrolls beneath it). Dark and light both carry it — the effect is refraction,
 * not a palette, so it needs no per-theme values (rule 14).
 * ==========================================================================*/

:root {
  /* Lateral split between the red and cyan samples. ~1px at rest reads as a
   * genuine lens edge; beyond ~2px it starts to look like a broken render. */
  --ca-shift: 1.6px;
  /* How far the fringe reaches in from the rim before fading out. Lower = the
   * fringe reaches further toward the centre. */
  --ca-rim: 28%;
  /* Per-channel strength. LOW by design: with `plus-lighter` the layers ADD
   * outright, so this is a gentle lift at the rim, not a colour filter. The old
   * .8 was tuned for a screen/multiply pair and would blow the rim to white. */
  --ca-strength: .16;
}

/* Only an isolated stacking context is needed, so the two blend layers composite
 * against this element's backdrop rather than the page's. `position` is NOT set
 * here: the chrome that carries this class is already `fixed` (tab bar, FAB,
 * gear, eye) or `absolute` (the lozenge), and forcing `relative` re-anchored
 * them to the document — the FAB dropped to y=1791 on a 926px viewport and every
 * touch control fell outside the hit area. `isolation` alone gives the pseudo
 * layers the containing block they need, whatever the host's own positioning. */
.has-ca { isolation: isolate; }

.has-ca::before,
.has-ca::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 0;
  /* NO BACKDROP FILTER HERE — this was the real darkening (operator video,
   * 26/07/2026, measured: the tab bar's mean luminance fell ~19 levels while the
   * page above it stayed flat, so the bar dimmed on its OWN, not from scrolling
   * content).
   *
   * These layers used to re-blur the backdrop with `blur() saturate(1.6)`. But
   * the HOST already blurs it — so the fringe was a SECOND blur stacked on the
   * first. Each blur pass averages in the darker surroundings and a `saturate`
   * on already-blurred pixels deepens them further, so the pair read as a grey
   * wash that appeared exactly when the layers faded in. Switching the blend to
   * `screen` (v1.76.0) could not fix that: the darkening was in the SAMPLE, not
   * in the compositing.
   *
   * The fringe only ever needed to TINT the glass the host already paints, which
   * a plain coloured layer does — the host's own backdrop-filter still supplies
   * the blur underneath, and the mask keeps the tint at the rim where a real
   * lens refracts. */
  /* Strongest at the rim, absent through the middle. */
  -webkit-mask-image: radial-gradient(115% 115% at 50% 50%, transparent var(--ca-rim), #000 100%);
  mask-image: radial-gradient(115% 115% at 50% 50%, transparent var(--ca-rim), #000 100%);
  /* MOTION-ONLY (operator 26/07/2026: "looks half baked — the aberration must
   * 'glow into place' ONLY and ONLY during motion, then 'glow out' in sync as
   * that transition comes to an end").
   *
   * At rest the fringe is INVISIBLE. This is also the physically honest state:
   * refraction splits a high-contrast edge only while that edge TRAVELS beneath
   * the glass; a static rim of red and cyan is just a coloured border, which is
   * exactly why the resting effect read as unfinished.
   *
   * `.ca-move` is added for the duration of a real transition and removed when
   * it ends, so the glow's in and out are driven by the same event the motion
   * is — never by a timer that can drift out of sync with it. The fade-in is
   * quick (the fringe should already be there as the movement registers) and
   * the fade-out matches the host transition's own duration so both land
   * together. */
  opacity: 0;
  /* GLOW OUT — long and eased, so the fringe recedes rather than snaps
   * (operator 26/07/2026: "glowing in and out far, far too quickly and should be
   * more subtle as animation starts and ends"). `--dur-base` (.2s) read as an
   * on/off switch; the fade is now the slowest thing in the interaction, which is
   * what makes it read as light receding rather than a layer being toggled. */
  transition: opacity var(--ca-glow-out, .68s) var(--ease-standard);
}

/* Motion: glow in. Held for as long as the host is actually moving. */
.has-ca.ca-move::before,
.has-ca.ca-move::after {
  /* Full opacity: the strength now lives in the layers' own alpha (see
   * --ca-strength), so this only drives the fade. */
  opacity: 1;
  /* Slower in than out is deliberate: the fringe should still be BUILDING as the
   * movement gets under way, never arrive fully formed on frame one. */
  transition: opacity var(--ca-glow-in, .42s) var(--ease-standard);
}

/* NEVER DARKEN THE HOST (operator 26/07/2026: "dont darken the button/toolbar as
 * the aberration animation plays").
 *
 * The red half used to be `mix-blend-mode: multiply`. Multiply can only ever
 * SUBTRACT light, and it was paired with a `screen` layer that only ever ADDS —
 * the two cancelled exactly at full strength, which is why the resting effect
 * looked clean, but they do NOT cancel at intermediate opacity. So every frame of
 * the fade dimmed the toolbar and it read as the button darkening as the
 * animation played.
 *
 * Both halves are now `screen`, which is additive-only: at any opacity the glass
 * can gain a warm or cool rim but can never lose luminance. The split still
 * reads as refraction because the two layers are displaced in OPPOSITE
 * directions — an edge picks up red on one side and cyan on the other — and a
 * flat region gains next to nothing, since screening a mid-grey with a dim
 * complementary pair is very close to identity. */
/* `plus-lighter`, not `screen` — MEASURED, after `screen` still dimmed the bar.
 *
 * `screen` is only additive in the abstract: these layers composite against the
 * page showing THROUGH the translucent glass, so a saturated red or cyan plate
 * screened over bright glass still resolves darker than the glass alone. Isolated
 * on the tab bar: baseline 237.3, cyan half alone 231.7, red half alone 223.2,
 * both 221.3 — each layer subtracting on its own.
 *
 * `plus-lighter` sums the channels outright, so the result can only ever be
 * brighter than what is under it — the property the operator actually asked for
 * ("dont darken the button/toolbar as the aberration animation plays"). The
 * strength is dropped to match, because addition is far stronger than a screen
 * pass and would otherwise blow the rim out to white. The opposed displacement
 * still carries the refraction: one side gains warmth, the other coolness. */
/* The fringe LIFTS the rim rather than plating colour over it — measured, after
 * three blend modes all failed the same way.
 *
 * A saturated plate darkens no matter how it composites: red is (255,0,0), so
 * over bright glass its two zeroed channels drag the average DOWN. Isolated on
 * the tab bar (mean luminance, rest 237.30): `screen` −3.9, `plus-lighter` −4.1,
 * `lighten` −5.5, `color-dodge` −2.6 — every one negative, and scaling with
 * alpha, which is the tell that the blend was never the mechanism. Proof: with
 * the SAME layers, mask and isolation but a transparent background, the delta is
 * exactly 0.00. The colour itself was the darkening.
 *
 * So each half now carries a near-white tint of its channel — a red-leaning and
 * a cyan-leaning WHITE — under `plus-lighter`. Adding those can only raise
 * luminance, while the opposed displacement still splits a high-contrast edge
 * warm on one side and cool on the other, which is the refraction being drawn. */
.has-ca::before {
  transform: translateX(calc(var(--ca-shift) * -1));
  /* FULLY white-based, with only the opposite channels held back. A tint whose
   * channels all sit below 255 still averages a bright surface DOWN — light mode
   * measured −0.55 at rgb(255,214,214) and only reached a true non-negative once
   * the base was white. The warm/cool split now comes from each half withholding
   * the OTHER's channels slightly, not from darkening any of its own. */
  background: var(--ca-light);
  box-shadow: inset 0 0 0 100px rgb(255 240 240 / calc(var(--ca-strength) * .5));
  mix-blend-mode: plus-lighter;
}

.has-ca::after {
  transform: translateX(var(--ca-shift));
  background: var(--ca-light);
  box-shadow: inset 0 0 0 100px rgb(240 255 255 / calc(var(--ca-strength) * .5));
  mix-blend-mode: plus-lighter;
}

/* The host's own content must ride ABOVE both fringe layers. Chrome elements
 * here are flex/grid parents, so lifting their children is enough and no markup
 * changes.
 *
 * The overlays are EXEMPT (30/07/2026). This rule is later in the file and of
 * equal specificity, so it silently won the `position` on any absolutely-placed
 * child: MEASURED, the accent mask computed `position: relative` despite its own
 * rule saying `absolute`, and a static block in the flex row then collapsed to
 * width 0 — so every clip resolved against a zero-width box. Anything that
 * positions itself against the bar must be listed here. */
.has-ca > *:not(.tab-lozenge) { position: relative; z-index: 1; }

/* --- Per-surface blur, matched to each element's existing glass --- */
.tab-bar { --ca-blur: 22px; }
.tab-lozenge { --ca-blur: 10px; }
.fab-txn { --ca-blur: 18px; }
.settings-gear, .privacy-eye { --ca-blur: 16px; }

/* The sliding lozenge is the "selected tile": its fringe should travel with it,
 * and it carries no children of its own, so it needs no z-index lift. */
.tab-lozenge::before,
.tab-lozenge::after {
  transition: left var(--dur-slow) var(--ease-spring),
              width .32s cubic-bezier(.34, 1.35, .5, 1);
}

/* --- Range sliders (touch): fringe rides the THUMB, which is the glass part.
 * A pseudo-element cannot attach to `::-webkit-slider-thumb`, so the thumb gets
 * the split via a drop-shadow pair instead — same physics, one warm and one cool
 * rim, no extra DOM. --- */
@media (pointer: coarse) {
  .trailing-range::-webkit-slider-thumb,
  .bigpic-range::-webkit-slider-thumb {
    filter:
      drop-shadow(calc(var(--ca-shift) * -1) 0 0 rgba(255, 0, 0, .45))
      drop-shadow(var(--ca-shift) 0 0 rgba(0, 255, 255, .45));
  }
  .trailing-range::-moz-range-thumb,
  .bigpic-range::-moz-range-thumb {
    filter:
      drop-shadow(calc(var(--ca-shift) * -1) 0 0 rgba(255, 0, 0, .45))
      drop-shadow(var(--ca-shift) 0 0 rgba(0, 255, 255, .45));
  }
}

/* Accessibility opt-out: plain frosted glass, no fringe. */
@media (prefers-reduced-transparency: reduce), (prefers-reduced-motion: reduce) {
  .has-ca::before, .has-ca::after { display: none; }
  .trailing-range::-webkit-slider-thumb,
  .bigpic-range::-webkit-slider-thumb,
  .trailing-range::-moz-range-thumb,
  .bigpic-range::-moz-range-thumb { filter: none; }
}

/* ============================================================================
 * DESKTOP DENSITY + READABILITY (Toolkit reference, D-074)
 * ----------------------------------------------------------------------------
 * Every rule below is gated on body[data-surface="desktop"] because D-074 puts
 * the Toolkit explicitly OUT of scope for the touch surfaces. The prefs
 * themselves are written onto <html> by ui/density.js as data-* attributes, so
 * a view re-render can never drop them and there is exactly one writer.
 *
 * Densities are expressed as a min-height/padding pair rather than a transform
 * or a font-size change: shrinking type would fight the app's --fs-* token
 * scale, and a transform would shrink the HIT BOX along with the paint (the
 * scale() hit-box trap already burned this project once). Row height alone
 * changes; type, tokens and hit boxes stay exactly as they are.
 * ==========================================================================*/

/* --- Transaction row height (desktop Spending; +15% v1.122) --------------- */
body[data-surface="desktop"] .reg-row { min-height: var(--reg-row-h, 32px); }
html[data-density-reg="compact"] body[data-surface="desktop"] .reg-row { --reg-row-h: 25px; }
html[data-density-reg="cosy"]    body[data-surface="desktop"] .reg-row { --reg-row-h: 32px; }
html[data-density-reg="roomy"]   body[data-surface="desktop"] .reg-row { --reg-row-h: 41px; }

/* --- Budget row height (desktop cosy is the restrained .3rem contract) ------ */
body[data-surface="desktop"] .plan-row { padding-block: var(--plan-row-pad, .4rem); }
html[data-density-plan="compact"] body[data-surface="desktop"] .plan-row { --plan-row-pad: .15rem; }
html[data-density-plan="cosy"]    body[data-surface="desktop"] .plan-row { --plan-row-pad: .3rem; }
html[data-density-plan="roomy"]   body[data-surface="desktop"] .plan-row { --plan-row-pad: .7rem; }

/* Plan desktop optical contract (v1.126): keep mouse controls content-hugged,
 * share one control height, and remove resting outline noise. Touch retains its
 * larger hit floors and modal geometry above. */
body[data-surface="desktop"] :is(.plan-mo-btn, .plan-mo-label) {
  background: transparent;
  border-color: transparent;
}
@media (hover: hover) {
  body[data-surface="desktop"] :is(.plan-mo-btn, .plan-mo-label):hover {
    background: color-mix(in srgb, var(--accent) 10%, transparent);
    border-color: color-mix(in srgb, var(--accent) 30%, transparent);
  }
}
body[data-surface="desktop"] .plan-pill {
  display: inline-flex; align-items: center; justify-content: center;
  box-sizing: border-box; min-height: var(--ctl-h-sm); line-height: 1;
}
body[data-surface="desktop"] .plan-lens-half { line-height: 1; }

/* --- More accounts visible in the sidebar --------------------------------- */
html[data-density-side="compact"] body[data-surface="desktop"] .acct-row { padding-block: .12rem; }
html[data-density-side="compact"] body[data-surface="desktop"] .acct-group-head { padding-top: .35rem; }

/* --- Fit more tabs across the top without crowding ------------------------ */
html[data-density-tabs="compact"] body[data-surface="desktop"] .tab-bar { padding: 5px 6px; }
html[data-density-tabs="compact"] body[data-surface="desktop"] .tab-item { padding-inline: .45rem; }

/* --- Thinner, less obtrusive scrollbars ----------------------------------- */
/* Firefox is the operator's Win11 browser, so `scrollbar-width` is the primary
 * mechanism and the -webkit- block is the Chromium/Safari companion. Applied to
 * scroll containers only — never `*`, which would also thin the touch surfaces
 * through the shared shell. */
html[data-thin-scrollbars="true"] body[data-surface="desktop"] .reg-grid,
html[data-thin-scrollbars="true"] body[data-surface="desktop"] .plan-table,
html[data-thin-scrollbars="true"] body[data-surface="desktop"] .content,
html[data-thin-scrollbars="true"] body[data-surface="desktop"] .reflect-matrix-scroll,
html[data-thin-scrollbars="true"] body[data-surface="desktop"] .picker-list {
  scrollbar-width: thin; scrollbar-color: var(--hairline) transparent;
}
html[data-thin-scrollbars="true"] body[data-surface="desktop"] .reg-grid::-webkit-scrollbar,
html[data-thin-scrollbars="true"] body[data-surface="desktop"] .plan-table::-webkit-scrollbar,
html[data-thin-scrollbars="true"] body[data-surface="desktop"] .content::-webkit-scrollbar,
html[data-thin-scrollbars="true"] body[data-surface="desktop"] .reflect-matrix-scroll::-webkit-scrollbar,
html[data-thin-scrollbars="true"] body[data-surface="desktop"] .picker-list::-webkit-scrollbar {
  width: 8px; height: 8px;
}
html[data-thin-scrollbars="true"] body[data-surface="desktop"] .reg-grid::-webkit-scrollbar-thumb,
html[data-thin-scrollbars="true"] body[data-surface="desktop"] .plan-table::-webkit-scrollbar-thumb,
html[data-thin-scrollbars="true"] body[data-surface="desktop"] .content::-webkit-scrollbar-thumb,
html[data-thin-scrollbars="true"] body[data-surface="desktop"] .reflect-matrix-scroll::-webkit-scrollbar-thumb,
html[data-thin-scrollbars="true"] body[data-surface="desktop"] .picker-list::-webkit-scrollbar-thumb {
  background: var(--hairline); border-radius: 4px;
}

/* --- Compact income-and-spending report ----------------------------------- */
html[data-density-report="compact"] body[data-surface="desktop"] .reflect-matrix th,
html[data-density-report="compact"] body[data-surface="desktop"] .reflect-matrix td {
  padding: .12rem .45rem;
}
html[data-density-report="compact"] body[data-surface="desktop"] .reflect-matrix .ie-section td {
  padding-top: .35rem;
}

/* --- Zebra striping ------------------------------------------------------- */
/* Struck on the ODD row so the first row keeps the card's own background and
 * the stripe never fights the sticky header. The tint is a neutral ink wash
 * rather than a fixed grey, so it inverts correctly in dark mode (rule 14)
 * instead of turning into a light band on a dark card. */
html[data-zebra-reg="true"] body[data-surface="desktop"] .reg-txn:nth-of-type(odd) {
  background: var(--zebra-row);
}
html[data-zebra-reg="true"][data-theme="dark"] body[data-surface="desktop"] .reg-txn:nth-of-type(odd) {
  background: var(--zebra-row);
}
html[data-zebra-plan="true"] body[data-surface="desktop"] .plan-cat:nth-of-type(odd) {
  background: var(--zebra-row);
}
html[data-zebra-plan="true"][data-theme="dark"] body[data-surface="desktop"] .plan-cat:nth-of-type(odd) {
  background: var(--zebra-row);
}
/* Selection, hover and the unapproved tint must all still WIN over the stripe,
 * or a selected odd row would look unselected. Listed after the stripe with the
 * same specificity, so source order decides in their favour. */
html[data-zebra-reg="true"] body[data-surface="desktop"] .reg-txn.selected,
html[data-zebra-reg="true"] body[data-surface="desktop"] .reg-txn.unapproved {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
@media (hover: hover) {
  html[data-zebra-reg="true"] body[data-surface="desktop"] .reg-txn:hover,
  html[data-zebra-plan="true"] body[data-surface="desktop"] .plan-cat:hover {
    background: color-mix(in srgb, var(--accent) 10%, transparent);
  }
}

/* --- Report row hover (the reports carried no hover rule at all) ----------- */
@media (hover: hover) {
  body[data-surface="desktop"] .reflect-matrix tbody tr:hover td { background: color-mix(in srgb, var(--accent) 6%, transparent); }
  body[data-surface="desktop"] .rank-row:hover { background: color-mix(in srgb, var(--accent) 6%, transparent); }
}

/* --- Emphasis: negative amounts, money in, money out ---------------------- */
/* Weight PLUS the existing colour token, never colour alone — a colour-blind
 * reader gets the weight cue too, and --neg/--pos keep dark mode correct. */
html[data-emph-neg="true"] body[data-surface="desktop"] .neg,
html[data-emph-neg="true"] body[data-surface="desktop"] .amount.neg {
  font-weight: 700;
}
html[data-emph-in="true"] body[data-surface="desktop"] .reg-row .c-in {
  font-weight: 700; color: var(--pos);
}
html[data-emph-out="true"] body[data-surface="desktop"] .reg-row .c-out {
  font-weight: 700; color: var(--neg);
}

/* --- Bigger cleared tick marks -------------------------------------------- */
/* The tick is a 1rem disc whose letter is transparent. Growing it must grow the
 * disc only — the grid track is 1.4rem, so 1.35rem is the ceiling before the
 * column reflows and every row shifts sideways. */
html[data-big-ticks="true"] body[data-surface="desktop"] .reg-row .c-clr {
  width: 1.35rem; height: 1.35rem;
}

/* --- Clearer reconciled marking ------------------------------------------- */
/* Reconciled rows were signalled by opacity .82 alone — nearly invisible beside
 * a merely-cleared row. A left edge in the positive token makes the state
 * readable without relying on a brightness difference. */
body[data-surface="desktop"] .reg-row.reconciled {
  box-shadow: inset 2px 0 0 var(--pos);
}

/* --- Current month stands out clearly ------------------------------------- */
/* The month switcher button gains a heavier weight when the viewed month IS the
 * current one, so "am I looking at this month?" is answerable at a glance
 * instead of by reading the label. Class set by plan.js.
 *
 * The accent underline this used to draw was REMOVED (operator 2026-07-29:
 * "bizarre underlining of ACTIVITY fields & month"). An underline under a word
 * reads as a hyperlink, and the ⊙ Today button already answers the same
 * question positively whenever you have navigated away. Weight alone carries
 * it, and aria-current="date" still carries it for screen readers. */
body[data-surface="desktop"] .plan-mo-label.is-current {
  font-weight: 700;
}

/* --- Coloured band on category group headings ----------------------------- */
/* A left band in the accent token, so groups separate at a glance while the
 * warm --plan-group-bg fill is preserved. */
html[data-group-band="true"] body[data-surface="desktop"] .plan-group {
  box-shadow: inset 3px 0 0 var(--accent);
}

/* --- Small card symbol on the credit-card payment category ---------------- */
body[data-surface="desktop"] .plan-cat .cat-card-glyph {
  display: inline-flex; vertical-align: middle; margin-right: .35rem;
  color: var(--ink-muted);
}
body[data-surface="desktop"] .plan-cat .cat-card-glyph svg { width: 14px; height: 14px; }

/* --- Sidebar marker: accounts holding un-ticked transactions --------------- */
/* A small dot after the account name, not a count badge: the sidebar row is a
 * flex line whose name may already wrap, and a badge would compete with the
 * balance for the same track. */
body[data-surface="desktop"] .acct-uncleared-dot {
  width: 6px; height: 6px; border-radius: 50%; background: var(--amber);
  flex: 0 0 auto; margin-left: .3rem;
}

/* Sub-heading inside a settings card (density group, D-074). */
.settings-subhead {
  margin: 1rem 0 .2rem; font-size: var(--fs-label); text-transform: uppercase;
  letter-spacing: .06em; color: var(--ink-muted);
}
.setting-row .density-select { max-width: 11rem; }

/* Two-part picker row: a name that may be long, and a figure that must stay
 * whole (Move Money / Assign destination pickers, plan.js).
 *
 * ROOT CAUSE this replaces (operator, 26/07/2026: "pick a destination shows a
 * completely unreadable list of destinations due truncation"): those rows set
 * `display:flex` INLINE from JS on a `.pick-row` whose stylesheet rule carries
 * `overflow:hidden`. Neither child was a flex item with `min-width:0`, so both
 * spans hard-CLIPPED instead of ellipsising — measured at the real 420px picker
 * width, the amount painted 145.7px of text into an 83px box, cutting the money
 * figure mid-number with no ellipsis to signal it.
 *
 * This rule supplies ONLY the flex container. The children are already governed
 * by the `.pick-row > span:not(...)` rule further down (added 2026-07-14 for the
 * same class of bug): every span shrinks and ellipsises EXCEPT `.pick-avail`,
 * which is pinned. So the figures here carry `.pick-avail` and the name gives
 * way instead — one convention for picker rows, not two competing ones. A
 * `:last-child` rule here would have LOST to that selector anyway, on both
 * specificity and source order. */
.pick-row-split { display: flex; justify-content: space-between; gap: 10px; align-items: baseline; }

/* Category-group heading inside the desktop typeahead dropdown, and the indent
 * for the subcategories under it (operator, 26/07/2026: the split category list
 * "still shows category>subcategory list instead of the category-subcategory
 * tree of the regular 'select category'. match that latter").
 *
 * Deliberately quieter than .reg-drop-sep, which separates the two ZONES
 * (matches vs browse-everything) and is sticky. A group heading is ordinary
 * structure inside a zone, so it must not compete with the zone divider — it
 * takes the accent colour the touch picker's .pick-header already uses, so the
 * two surfaces read as the same tree. */
.reg-drop-group {
  padding: .3rem .55rem .15rem; color: var(--accent);
  font-size: .72rem; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; white-space: nowrap;
}
.reg-drop-group:first-child { padding-top: .15rem; }
.reg-drop-row.reg-drop-sub { padding-left: 1.15rem; }

/* (The round-up "spare change" figure that used to sit beside the selection
 * count was removed on 29/07/2026 — unlabelled arithmetic on every selection.
 * See register-optionsbar.js for what replaced it.) */
/* Copy button in the Plan activity pop-up: pushed left so Close keeps the
 * trailing (primary) position the sheet footer gives it everywhere else. */
.sheet-footer .activity-copy { margin-right: auto; }
/* The parent payee echoed on each split line stays muted and clipped — it is
 * context, never a second editable payee. */
.reg-split-row .c-payee.reg-split-muted {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* --- Plan category search box (Toolkit wishlist, desktop) ----------------- */
.plan-search { display: flex; align-items: center; gap: 8px; padding: 0 14px 6px; }
.plan-search-input { flex: 1 1 auto; min-width: 0; max-width: 22rem; }
.plan-search-count { flex: 0 0 auto; white-space: nowrap; }

/* --- Categories that have not met their target --------------------------- */
/* A left edge in amber, NOT a background wash: the Plan row already uses
 * background for zebra striping, hover and selection, so a fill here would
 * fight all three. The edge is additive and survives every one of them. */
body[data-surface="desktop"] .plan-cat.plan-cat-unmet {
  box-shadow: inset 3px 0 0 var(--amber);
}

/* The Available column ends at the pill — nothing trails it, matching real
 * YNAB. The former ".plan-sched-note" suffix lived here and overflowed the
 * column; the still-to-come figure is an Inspector row now. */

/* --- Move-money destination picker: search + empty state ----------------- */
.picker-search { margin: 0 0 6px; }
.pick-none { padding: .6rem .4rem; }

/* --- Reconcile helper + card warning ------------------------------------- */
.rec-subset { margin: .2rem 0 .4rem; }
.rec-cardwarn {
  margin: .3rem 0 .5rem; padding: .5rem .6rem; border-radius: 8px;
  background: var(--neg-pill); color: var(--ink);
}
[data-theme="dark"] .rec-cardwarn { background: var(--rec-warning-fill); }

/* ============================================================================
 * PRINT (Toolkit wishlist: "a printer-friendly layout for the budget and
 * transactions"). The app had no print stylesheet at all, so printing produced
 * the full app chrome — sidebar, tab bar, floating buttons — around a clipped
 * grid, and dark mode printed a black page.
 *
 * Three rules do the work: force light ink on white (a themed page wastes toner
 * and reads badly), drop every navigational and interactive element (nothing on
 * paper can be clicked), and unclip the scroll containers so the WHOLE table
 * prints rather than just the visible window.
 * ==========================================================================*/
@media print {
  /* Always print as light-on-white, whatever theme is on screen. */
  :root, [data-theme="dark"] {
    --canvas: #fff; --canvas-desktop: #fff; --card: #fff;
    --ink: #000; --ink-muted: #333; --hairline: #bbb;
  }
  html, body { background: var(--canvas) !important; color: var(--ink) !important; }

  /* Chrome that cannot be used on paper. */
  .sidebar, .tab-bar, .reg-toolbar, .plan-toolbar, .reflect-controls,
  .fab, .scroll-top, .privacy-eye, .settings-gear, .install-prompt,
  .env-bubble, .toast-bubble, .plan-search, .reg-selbar, .reg-selbar-mobile,
  .picker-foot, .sheet-footer, .add-account, .plan-pills, .reg-addrow,
  .plan-mo-btn, .plan-mo-today, .reflect-export, .undo-bar, .reg-undo-group {
    display: none !important;
  }

  /* Unclip every scroll container: printing must not stop at the fold. */
  .content, .reg-grid, .plan-table, .reflect-matrix-scroll, .picker-list,
  #app, .screen {
    overflow: visible !important; max-height: none !important; height: auto !important;
  }
  #app { display: block !important; }
  .content { max-width: none !important; padding: 0 !important; margin: 0 !important; }

  /* Cards flatten: shadows and rounded corners are screen affordances. */
  .card, .panel-card {
    box-shadow: none !important; border: 1px solid var(--hairline) !important;
    border-radius: 0 !important; break-inside: avoid;
  }

  /* A sticky header repeats as a floating band mid-page when printed. */
  .reg-head, .reflect-matrix thead th, .plan-head { position: static !important; }

  /* Never split a row across a page break, and repeat table headers. */
  .reg-row, .plan-row, .rank-row { break-inside: avoid; }
  thead { display: table-header-group; }

  /* Zebra striping and hover tints are toner, not information, on paper. */
  .reg-txn, .plan-cat { background: transparent !important; }

  @page { margin: 12mm; }
}

/* --- Unfolded POSTED split lines in the grid (Toolkit wishlist) ------------ */
/* A read-only child row under its parent. Indented and tinted so it reads as
 * belonging to the row above rather than as another transaction. */
.reg-txn-subline {
  background: var(--subline-row); color: var(--ink-muted); cursor: pointer;
  font-size: var(--fs-label);
}
[data-theme="dark"] .reg-txn-subline { background: var(--subline-row); }
.reg-txn-subline .c-cat { padding-left: 1.1rem; }
.reg-txn-subline:focus-visible {
  outline: 2px solid var(--accent); outline-offset: -2px;
}
/* Disclosure caret on a split's Category cell. */
.reg-split-toggle {
  background: none; border: 0; padding: 0 .3rem 0 0; margin: 0;
  color: var(--ink-muted); cursor: pointer; font: inherit; line-height: 1;
}
@media (hover: hover) { .reg-split-toggle:hover { color: var(--ink); } }
.reg-split-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Sub-lines are context, never printable rows in their own right. */
@media print { .reg-split-toggle { display: none !important; } }

/* --- Desktop bottom options bar (operator 2026-07-29) ----------------------
 * YNAB's own desktop register raises a dark floating pill once rows are
 * selected: "× N Transactions | Categorise | Flag | ⋯ More". The operator asked
 * for that behaviour and that look. See views/register-optionsbar.js for the
 * two-stage click contract that raises it.
 *
 * DELIBERATELY DARK IN BOTH THEMES. Everywhere else in this app a surface
 * follows the theme, but this bar is a transient overlay floating above the
 * register rather than a panel within it — in light mode a light bar would melt
 * into the rows it sits over, and the reference screenshots show the dark pill
 * in a light register. Its own ink token is therefore fixed, not inherited, so
 * dark mode does not invert it into an unreadable light-on-light bar. */
.reg-optionsbar {
  position: fixed;
  left: 50%;
  bottom: 22px;
  transform: translateX(-50%);
  z-index: 60; /* above the grid + drawer, below modals (which start at 100) */
  display: flex;
  align-items: center;
  /* Narrower supported desktop widths (spec §9): wrap logical groups instead
   * of clipping the analytics line or hiding an action. Each group below
   * (`.rob-total`, `.rob-pace`, the action buttons) stays intact — only the
   * gap BETWEEN groups breaks — so nothing wraps mid-figure. */
  flex-wrap: wrap;
  justify-content: center;
  row-gap: 2px;
  /* D-102 part C (19/08/2026): a lightweight action surface, not a giant halo.
   * Height/padding grammar matches the Add Transaction control family
   * (--ctl-h-sm = 34px, .reg-toolbar .btn's .4rem/1rem rhythm) rather than the
   * bar's own former oversized pill, so it reads as a member of the same
   * Spending control set the operator is comparing it against. */
  gap: 3px;
  padding: 3px 6px;
  min-height: var(--ctl-h-sm);
  border-radius: 999px;
  background: var(--options-face);
  color: var(--options-ink);
  box-shadow: 0 8px 28px rgba(0, 0, 0, .32), inset 0 0 0 1px rgba(255, 255, 255, .07);
  font-size: var(--fs-body);
  /* The bar is a viewport-fixed overlay: never let a long selection total push
   * it wider than the screen on a small laptop. No fixed ceiling above that
   * (v1.199.0 P0-C, operator 21/08/2026): a hardcoded 1080px wasted the
   * available width on a wide desktop monitor and let flex-wrap strand
   * Categorise/Flag/More across rows independently of each other, since they
   * were separate flex children with nothing holding them together. */
  max-width: 96vw;
}
/* Categorise/Flag/More (+ Approve/Reject when present) are ONE logical action
 * group (v1.199.0 P0-C): never split across a wrap. The preferred/first wrap
 * point is immediately before the analytics group (.rob-pace), which always
 * follows this span in DOM order. */
.reg-optionsbar .rob-actions {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 3px;
}
[data-theme="dark"] .reg-optionsbar {
  background: var(--options-face);
  box-shadow: 0 8px 28px rgba(0, 0, 0, .5), inset 0 0 0 1px rgba(255, 255, 255, .1);
}
/* Entrance: rise from just below its resting place. Applied for one frame by
 * the module, then removed so the transition runs. Respects reduced motion. */
.reg-optionsbar.entering { transform: translateX(-50%) translateY(14px); opacity: 0; }
/* THE BAR MUST NEVER SIT ON A ROW IT CANNOT BE MOVED OFF. It is a fixed pill at
 * the viewport bottom, so whatever it overlaps is unclickable while a selection
 * is live — and the LAST rows of a register are exactly what an operator reaches
 * for next. Widening it with Approve/Reject (D-097) made that latent overlap
 * bite: the control audit wedged retrying a click on a row the bar now covered.
 *
 * Reserve its height at the end of the scroll content instead, so the last row
 * can always be scrolled clear of it. Scoped to a live selection via :has, so
 * nothing changes when no bar is on screen.
 *
 * MEASURED, not guessed (spec §9): register.js sets --reg-optionsbar-h from
 * the bar's own getBoundingClientRect().height on every repaint (see
 * repaintSelection's optionsbar-clearance sync), so a wrapped two-row bar on a
 * narrow desktop width reserves its OWN actual height plus the same 22px
 * resting gap + 22px breathing room the single-row bar always had — not a
 * fixed guess that would under-reserve once the analytics line forces a wrap.
 * The 96px literal is the fallback for the instant before that first measure
 * lands (e.g. the entrance frame). */
body[data-surface="desktop"]:has(.reg-optionsbar) .reg-grid {
  scroll-padding-bottom: max(96px, calc(var(--reg-optionsbar-h, 52px) + 44px));
}
body[data-surface="desktop"]:has(.reg-optionsbar) .reg-grid::after {
  content: '';
  display: block;
  height: max(96px, calc(var(--reg-optionsbar-h, 52px) + 44px));
  flex: none;
}
.reg-optionsbar { transition: transform .16s ease-out, opacity .16s ease-out; }
@media (prefers-reduced-motion: reduce) {
  .reg-optionsbar { transition: none; }
  .reg-optionsbar.entering { transform: translateX(-50%); opacity: 1; }
}

/* The × + count is one button: the operator's "clicking on 'x 1 transaction'
 * cancels the workflow". Making the count itself the target (not just the ×)
 * matches the reference and gives a forgiving hit area. */
/* Mission 5 (v1.190.0): the selection toolbar's action controls now match
 * Spending's own Add Transaction .btn in the FULL dimension family — height,
 * radius grammar, padding — not just height. They previously kept the same
 * --reg-control-h but a hardcoded 999px lozenge radius while Add Transaction
 * uses --reg-control-radius (a squircle derived from the row height), which
 * is exactly the drift the operator's brief called out. Width stays
 * content-driven; only height/radius/padding/gap are shared.
 * NOTE (21/08/2026): the later "Desktop Spending action geometry (UI-1)"
 * block below (search --spend-action-h) is now the actual authority for this
 * selector's height/radius/padding/gap — it lists `.reg-optionsbar
 * :is(.rob-cancel, .rob-btn)` explicitly and sits later in the cascade at
 * higher specificity. These --reg-control-* values remain live only as the
 * pre-UI-1 fallback (e.g. print/reduced states that don't re-trigger UI-1). */
.reg-optionsbar .rob-cancel {
  display: inline-flex; align-items: center; gap: var(--reg-control-gap, 6px);
  background: none; border: 0; cursor: pointer;
  color: inherit; font: inherit;
  height: var(--reg-control-h); min-height: var(--reg-control-h);
  padding: 0 var(--reg-control-pad-x, 9px); border-radius: var(--reg-control-radius, 999px);
}
.reg-optionsbar .rob-x { display: inline-flex; line-height: 1; opacity: .75; }
.reg-optionsbar .rob-x .ico { width: 1.05em; height: 1.05em; }
.reg-optionsbar .rob-count { white-space: nowrap; }
.reg-optionsbar .rob-total {
  display: inline-flex; align-items: baseline; gap: 8px;
  padding-right: 4px; white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* Selection money (operator 29/07/2026). Each figure carries its own small
 * lower-case label — "out", "in", "net" — because the bar's whole failing was
 * unlabelled arithmetic. The label is deliberately quiet: the NUMBER is what
 * the eye should land on, the word only settles what it means. Amounts are
 * tinted by direction using the same tokens the register rows use, so out/in
 * read identically here and in the grid, in both themes.
 *
 * NO PARENT-LEVEL OPACITY on the directional amount container (v1.184.0):
 * `.rob-total` used to carry `opacity: .82` and each tint was pre-mixed
 * toward `--chart-ink`, which washed the red/green toward neutral on top of
 * an already-translucent parent — the compounded effect read as "barely
 * red/green" in both themes (spec §7). `.rob-out`/`.rob-in` now paint the
 * semantic token directly, INCLUDING their own amount and label text; only
 * `.rob-net` (a genuinely neutral figure — it exists only when both
 * directions are present) and the plain separators keep a quiet opacity. */
.reg-optionsbar .rob-amt {
  display: inline-flex; align-items: baseline; gap: .3em;
}
.reg-optionsbar .rob-amt-lbl {
  font-size: var(--fs-label); font-weight: 400;
  text-transform: lowercase; letter-spacing: .02em;
}
.reg-optionsbar .rob-amt-sep { opacity: .38; margin: 0 2px; }
/* Mission 5 (v1.190.0, operator: "too faint/muddy"): the amount NUMBER now
 * carries explicit bold weight, not just a colour tint at inherited normal
 * weight — a thin coloured numeral reads faint even at full token opacity.
 * The label stays lighter (400) so the eye still lands on the figure first. */
.reg-optionsbar :is(.rob-out, .rob-out .rob-amt-lbl) { color: var(--neg); }
.reg-optionsbar :is(.rob-in, .rob-in .rob-amt-lbl) { color: var(--pos); }
.reg-optionsbar :is(.rob-out, .rob-in) { font-weight: 700; }
/* The label span re-asserts its own lighter weight — .rob-amt-lbl and the
 * rule above share specificity (2 classes each), so without this explicit
 * override cascade order alone would decide it, which is fragile. */
.reg-optionsbar :is(.rob-out, .rob-in) .rob-amt-lbl { font-weight: 400; }
.reg-optionsbar .rob-net { opacity: .9; }
/* Category history summary (operator, 29/08/2026): two compact metric chips —
 * 12-month average and exact month-to-date spend — between the selected total
 * and the action separator. Projected spend is deliberately absent. */
.reg-optionsbar .rob-pace {
  display: inline-flex; align-items: center; gap: 3px;
  padding-right: 2px; white-space: nowrap;
  font-variant-numeric: tabular-nums; font-size: var(--fs-label);
}
.reg-optionsbar .rob-pace-chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 4px 9px; border-radius: 999px;
  background: color-mix(in srgb, var(--options-ink) 8%, transparent);
  opacity: 1;
}
.reg-optionsbar .rob-pace-ico { width: 1em; height: 1em; opacity: .85; flex: none; }
/* Readable at ordinary desktop control/body scale (part C3, 19/08/2026): the
 * previous .82em/opacity-.78 pairing washed both label and value out under a
 * parent-level opacity stack — no parent opacity now, full label contrast. */
.reg-optionsbar .rob-pace-lbl { letter-spacing: .03em; font-size: var(--fs-label); opacity: .9; }
.reg-optionsbar .rob-pace-val { font-weight: 700; font-size: var(--fs-body); }
.reg-optionsbar .rob-sep {
  width: 1px; align-self: stretch; margin: 3px 3px;
  background: var(--options-rule);
}
.reg-optionsbar .rob-btn {
  display: inline-flex; align-items: center; gap: var(--reg-control-gap, 6px);
  background: none; border: 0; cursor: pointer;
  color: inherit; font: inherit;
  height: var(--reg-control-h); min-height: var(--reg-control-h);
  padding: 0 var(--reg-control-pad-x, 11px); border-radius: var(--reg-control-radius, 999px);
  white-space: nowrap;
}
/* Enabled ordinary secondaries (Categorise / Flag / More) get a quiet resting
 * fill (part C4) so they read as pressable against the dark pill, without
 * borrowing the light-surface --btn-secondary-* token (--card-derived — wrong
 * on this bar's deliberately fixed dark face in both themes, see --options-face
 * above). Open/selected (.on) stays the stronger --options-selected fill.
 *
 * Mission 5 (v1.190.0): a flat light-blurple/accent wash replaces the neutral
 * white-alpha resting fill so these read as the bar's PRIMARY actionable
 * controls (matching Spending's Add Transaction accent family) rather than a
 * generic pressable-surface tint. Still token-derived (--accent), still flat
 * (no gradient), and still legible against the bar's fixed dark face in both
 * themes because it is a light tint over dark, not a themed surface colour. */
.reg-optionsbar .rob-btn:not(:disabled):not(.on):not(.rob-approve) {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
}
@media (hover: hover) {
  .reg-optionsbar .rob-btn:not(:disabled):not(.on):not(.rob-approve):hover {
    background: color-mix(in srgb, var(--accent) 34%, transparent);
  }
}
.reg-optionsbar .rob-btn.on {
  background: var(--options-selected);
  box-shadow: inset 0 0 0 1px var(--options-selected);
}
.reg-optionsbar .rob-ico { opacity: .8; line-height: 1; }
.reg-optionsbar .rob-ico.ico { width: 1em; height: 1em; }
/* Approve is the PRIMARY verb while an approve review is on screen (D-097), so
 * it carries a filled face while Reject stays a plain bar button beside it.
 * Mission 5 (v1.190.0): now the same flat --accent family as the rest of the
 * bar's actionable controls (a solid fill, since Approve is the one true
 * primary CTA here) rather than a neutral light-on-dark tint — still flat, no
 * gradient, and legible against the bar's fixed dark face in both themes. */
.reg-optionsbar .rob-approve {
  background: var(--accent);
  color: var(--accent-ink);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-ink) 22%, transparent);
  font-weight: 700;
}
.reg-optionsbar .rob-approve.is-acting {
  opacity: .72;
  transform: scale(.96);
}
@media (hover: hover) {
  .reg-optionsbar .rob-approve:not(:disabled):hover {
    background: color-mix(in srgb, var(--accent) 88%, black 6%);
  }
}
@media (hover: hover) {
  .reg-optionsbar .rob-cancel:hover,
  .reg-optionsbar .rob-btn:not(:disabled):hover { background: var(--options-hover); }
}
.reg-optionsbar .rob-btn:disabled { opacity: .4; cursor: default; }
.ctx-row.is-acting { opacity: .72; transform: scale(.96); }
/* Focus ring must read against the bar's own dark surface, not the page's. */
.reg-optionsbar .rob-cancel:focus-visible,
.reg-optionsbar .rob-btn:focus-visible {
  outline: 2px solid var(--options-focus); outline-offset: 1px;
}
/* The bar's menus (⋯ / Categorise / Flag) open UPWARD. That is handled in
 * mountMenu's flip-above branch, not here — a CSS transform would fight the
 * measured position it computes. `.rob-menu` is kept purely as a hook for the
 * width cap below, so a long category list cannot outgrow the bar it belongs
 * to. */
.ctx-menu.rob-menu { max-width: min(92vw, 420px); }

/* The bar overlays the foot of the register. `.reg-grid` itself never scrolls
 * (no overflow-y — the document/html is the real scroll container on desktop),
 * so `scroll-padding-bottom` on `.reg-grid` was a no-op: `scrollIntoView` looks
 * at the actual scrolling ancestor's scroll-padding, not a static child's.
 * `scroll-margin-bottom` on the row itself works regardless of which ancestor
 * ends up scrolling, so it survives that stack shifting again. MEASURED via
 * --reg-optionsbar-h (spec §9), same value the .reg-grid::after spacer uses,
 * so a wrapped two-row bar reserves its own real height, not a fixed guess. */
body[data-surface="desktop"]:has(.reg-optionsbar) .reg-row {
  scroll-margin-bottom: max(96px, calc(var(--reg-optionsbar-h, 52px) + 44px));
}

/* Optional View-menu columns. These final overrides deliberately sit after the
 * legacy Cleared-off templates so every combination remains aligned. */
.reg-row .c-balance {
  text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap;
}
.reg-grid-no-flags .c-flag { display: none; }
/* Category/Payee track balance (04/08/2026). The desktop register labels rows
 * "Group: Category" like real YNAB, which is materially longer than the bare
 * leaf name the column was sized for — MEASURED: 25 of 25 category cells
 * overflowed at the old 1.2fr and ellipsised away the leaf, the part that is
 * actually read. Category went 1.2fr → 1.5fr and Payee 1.4fr → 1.25fr in the
 * SAME edit so every template's total is unchanged (the lockstep warned about
 * above). Both are still --reg-c-* hooks, so a column drag overrides either. */
.reg-grid-balance .reg-row {
  grid-template-columns:
    1.2rem 1.35rem 1.2rem var(--reg-c-acct, 8rem) var(--reg-c-date, 6.8rem)
    var(--reg-c-payee, 1.25fr) var(--reg-c-cat, minmax(var(--reg-min-cat, 0px), 1.5fr)) var(--reg-c-subcat, minmax(var(--reg-min-subcat, 0px), 1.5fr)) var(--reg-c-memo, 1fr)
    7.4rem 7.4rem 7rem 1.4rem;
}
.reg-grid-no-flags:not(.reg-grid-balance) .reg-row {
  grid-template-columns:
    1.2rem 1.2rem var(--reg-c-acct, 8rem) var(--reg-c-date, 6.8rem)
    var(--reg-c-payee, 1.25fr) var(--reg-c-cat, minmax(var(--reg-min-cat, 0px), 1.5fr)) var(--reg-c-subcat, minmax(var(--reg-min-subcat, 0px), 1.5fr)) var(--reg-c-memo, 1fr)
    7.4rem 7.4rem 1.4rem;
}
.reg-grid-balance.reg-grid-no-flags .reg-row {
  grid-template-columns:
    1.2rem 1.2rem var(--reg-c-acct, 8rem) var(--reg-c-date, 6.8rem)
    var(--reg-c-payee, 1.25fr) var(--reg-c-cat, minmax(var(--reg-min-cat, 0px), 1.5fr)) var(--reg-c-subcat, minmax(var(--reg-min-subcat, 0px), 1.5fr)) var(--reg-c-memo, 1fr)
    7.4rem 7.4rem 7rem 1.4rem;
}
.reg-grid-nocat.reg-grid-balance .reg-row {
  grid-template-columns:
    1.2rem 1.35rem 1.2rem var(--reg-c-acct, 8rem) var(--reg-c-date, 6.8rem)
    var(--reg-c-payee, 1.7fr) var(--reg-c-memo, 1.5fr)
    7.4rem 7.4rem 7rem 1.4rem;
}
.reg-grid-nocat.reg-grid-no-flags:not(.reg-grid-balance) .reg-row {
  grid-template-columns:
    1.2rem 1.2rem var(--reg-c-acct, 8rem) var(--reg-c-date, 6.8rem)
    var(--reg-c-payee, 1.7fr) var(--reg-c-memo, 1.5fr)
    7.4rem 7.4rem 1.4rem;
}
.reg-grid-nocat.reg-grid-balance.reg-grid-no-flags .reg-row {
  grid-template-columns:
    1.2rem 1.2rem var(--reg-c-acct, 8rem) var(--reg-c-date, 6.8rem)
    var(--reg-c-payee, 1.7fr) var(--reg-c-memo, 1.5fr)
    7.4rem 7.4rem 7rem 1.4rem;
}
body[data-cleared="off"] .reg-grid-balance .reg-row {
  grid-template-columns:
    1.2rem 1.35rem 1.2rem var(--reg-c-acct, 8rem) var(--reg-c-date, 6.8rem)
    var(--reg-c-payee, 1.25fr) var(--reg-c-cat, minmax(var(--reg-min-cat, 0px), 1.5fr)) var(--reg-c-subcat, minmax(var(--reg-min-subcat, 0px), 1.5fr)) var(--reg-c-memo, 1fr)
    7.4rem 7.4rem 7rem;
}
body[data-cleared="off"] .reg-grid-no-flags:not(.reg-grid-balance) .reg-row {
  grid-template-columns:
    1.2rem 1.2rem var(--reg-c-acct, 8rem) var(--reg-c-date, 6.8rem)
    var(--reg-c-payee, 1.25fr) var(--reg-c-cat, minmax(var(--reg-min-cat, 0px), 1.5fr)) var(--reg-c-subcat, minmax(var(--reg-min-subcat, 0px), 1.5fr)) var(--reg-c-memo, 1fr)
    7.4rem 7.4rem;
}
body[data-cleared="off"] .reg-grid-balance.reg-grid-no-flags .reg-row {
  grid-template-columns:
    1.2rem 1.2rem var(--reg-c-acct, 8rem) var(--reg-c-date, 6.8rem)
    var(--reg-c-payee, 1.25fr) var(--reg-c-cat, minmax(var(--reg-min-cat, 0px), 1.5fr)) var(--reg-c-subcat, minmax(var(--reg-min-subcat, 0px), 1.5fr)) var(--reg-c-memo, 1fr)
    7.4rem 7.4rem 7rem;
}
body[data-cleared="off"] .reg-grid-nocat.reg-grid-balance .reg-row {
  grid-template-columns:
    1.2rem 1.35rem 1.2rem var(--reg-c-acct, 8rem) var(--reg-c-date, 6.8rem)
    var(--reg-c-payee, 1.7fr) var(--reg-c-memo, 1.5fr)
    7.4rem 7.4rem 7rem;
}
body[data-cleared="off"] .reg-grid-nocat.reg-grid-no-flags:not(.reg-grid-balance) .reg-row {
  grid-template-columns:
    1.2rem 1.2rem var(--reg-c-acct, 8rem) var(--reg-c-date, 6.8rem)
    var(--reg-c-payee, 1.7fr) var(--reg-c-memo, 1.5fr)
    7.4rem 7.4rem;
}
body[data-cleared="off"] .reg-grid-nocat.reg-grid-balance.reg-grid-no-flags .reg-row {
  grid-template-columns:
    1.2rem 1.2rem var(--reg-c-acct, 8rem) var(--reg-c-date, 6.8rem)
    var(--reg-c-payee, 1.7fr) var(--reg-c-memo, 1.5fr)
    7.4rem 7.4rem 7rem;
}
body[data-surface="desktop"] .reg-editbar {
  min-height: 22px; padding-block: 1px; align-items: center;
}
/* Edit actions stay in document flow directly under their row.  Their box is
 * supplied by the scoped Spending action contract, not a one-off pixel value. */
/* Never print a transient selection overlay. */
@media print { .reg-optionsbar { display: none !important; } }

/* --- Win11 drawer reformat (operator 2026-07-29) ---------------------------
 * "radical graphical reformat of the pullout drawer(s) to maximise screen
 * width and remove duplication… drastically reduce the vertical space needed
 * for each transaction… graphically revamp the slideout drawer to match the
 * desktop theme."
 *
 * MEASURED BEFORE CHANGING (1920px viewport, split row): the drawer spanned the
 * full 1588px grid but its content stopped at 776px — the entire right half was
 * empty, because the two-column rule below pinned every card to column 1 and
 * reserved column 2 for an advanced card that a split row never renders. Eight
 * field rows at 52-65px each stacked to ~440px of drawer height.
 *
 * THE FIX, in three parts:
 *   1. THREE columns, not two, and cards flow into them instead of being pinned.
 *      The reserved-column rule that caused the empty half is overridden below.
 *   2. Account / Flag / Cleared are REMOVED from the drawer entirely. Every one
 *      of them is already a column in the register row behind it, editable in
 *      place — the operator's "no need to show it in the drawer anymore".
 *   3. Rows lose the phone's stacked label-above-value box in favour of a
 *      label-beside-value line, which is what takes the row from ~52px to ~34px.
 */
/* THREE columns, explicitly — not `repeat(auto-fit, minmax(280px, 1fr))`, which
 * was measured producing FIVE 290px columns on a 1588px drawer: the cards only
 * filled the first two, so the content stopped at 620px and the reformat spread
 * the drawer WORSE than the two-column layout it replaced. auto-fit sizes the
 * tracks to the container, not to the number of cards there are to put in them.
 * A transaction form has two or three cards; three columns is the honest count,
 * and each one gets a third of a wide register instead of a fifth. */
.reg-drawer .sheet-scroll {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0 28px;
}
/* Below ~1100px of drawer there is no room for three readable columns of
 * label+value pairs; drop to two, then one, rather than letting values wrap. */
@media (max-width: 1400px) {
  .reg-drawer .sheet-scroll { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 900px) {
  .reg-drawer .sheet-scroll { grid-template-columns: minmax(0, 1fr); }
}
/* Undo the deterministic single-column pinning (v1.83.0) that this reformat
 * replaces. That rule existed to stop an OPTIONAL card reshuffling an auto-flow
 * grid; the fix here keeps that guarantee differently — each card spans one
 * column and flows in order, so an extra card lands in the next column instead
 * of displacing the ones before it. */
.reg-drawer .sheet-scroll > .sheet-fields.card,
.reg-drawer .sheet-scroll > .txn-whendue { grid-column: auto; }
.reg-drawer .sheet-scroll > .sheet-adv { grid-column: auto; grid-row: auto; align-self: start; }
/* The inline split table (operator 29/07/2026) is a card like any other and
 * flows into the next free column, beside the fields it belongs to. Measured at
 * 1920px: fields | split | advanced, three filled tracks, 0px unused width. It
 * is deliberately NOT given `span 2` — one track (502px) already fits a category
 * name, its amount and the row menu on one line, and spanning pushed the
 * advanced card onto a second row for no gain. */
.reg-drawer .sheet-scroll > .split-inline { align-self: start; }
/* Delete stays on its own full-width line at the foot, clear of the fields, so
 * a destructive control is never adjacent to an editable value. */
.reg-drawer .sheet-scroll > .sheet-del-full { grid-column: 1 / -1; }

/* DUPLICATION REMOVED. These three are columns in the register row itself:
 * Account (.c-acct), Flag (.c-flag, now also the filter header) and Cleared
 * (.c-clr, a click-to-toggle cell). Showing them again inside the drawer that
 * expands FROM that row is the duplication the operator asked to remove.
 * Desktop only — the phone sheet has no row behind it and still needs all
 * three.
 *
 * Not scoped to `.sheet-fields.card >`: Cleared is built into the ADVANCED card
 * by a separate hardcoded builder, so a child selector on the fields card missed
 * it and left the toggle in the drawer (measured). Match on the field key
 * anywhere inside the drawer instead. */
.reg-drawer [data-f="account"],
.reg-drawer [data-f="flag"],
.reg-drawer [data-f="cleared"] { display: none; }

/* A repeat-only drawer supplements the primary Win11 row; it is not a second
 * transaction editor. Keep only Repeat, due policy, Save and Delete. Rich-row
 * edit drawers retain their split/transfer controls. */
.reg-drawer[data-variant="repeat-txn"] .txn-sheet:not(.sched-sheet) > .sheet-head,
.reg-drawer[data-variant="repeat-txn"] .txn-sheet:not(.sched-sheet) > .sheet-scroll > .sheet-fields.card:not(.sheet-adv):not(.txn-whendue) {
  display: none !important;
}

/* Rich desktop drawers are reserved for split/FX/transfer capabilities that
 * do not fit the row. Keep their shared amount/type controls (the direct FX
 * and type doors) but hide the row-owned text fields and collapse an empty
 * primary card. */
.reg-drawer[data-variant="rich-txn"] [data-f="payee"],
.reg-drawer[data-variant="rich-txn"] [data-f="category"],
.reg-drawer[data-variant="rich-txn"] [data-f="date"],
.reg-drawer[data-variant="rich-txn"] .field-memo,
.reg-drawer[data-variant="rich-txn"] .drawer-payee-context {
  display: none !important;
}
.reg-drawer[data-variant="rich-txn"] .sheet-fields.card:not(.sheet-adv):not(:has(.field-row:not([data-f="payee"]):not([data-f="category"]):not([data-f="date"]):not(.field-memo):not([data-f="account"]):not([data-f="flag"]):not([data-f="cleared"]))) {
  display: none;
}

/* A scheduled rule's wide row is the sole editor for its primary transaction
 * data. The drawer supplements it with only name, cadence, due behaviour and
 * contained completion controls. */

/* DENSITY. The phone stacks label above value to keep a 44px thumb target; a
 * mouse needs neither the stack nor the target, and the drawer's whole purpose
 * is to show the transaction without burying the register under it. Label and
 * value share one line, at a fixed label column so values align down the card. */
/* `:not([data-f=...])` is load-bearing, not decoration. Without it this rule is
 * (0,2,1) — one class more specific than the (0,2,0) hide rule above — so it
 * won the cascade with `display:grid` and put Account, Flag and Cleared back in
 * the drawer AFTER they had been hidden (measured: all three returned at 30-41px
 * tall). Any new non-displayed field must join this chain too. */
.reg-drawer .field-row:not([data-f="account"]):not([data-f="flag"]):not([data-f="cleared"]),
.reg-drawer .field-btn:not([data-f="account"]):not([data-f="flag"]):not([data-f="cleared"]) {
  display: grid;
  grid-template-columns: auto minmax(72px, max-content) minmax(0, 1fr);
  align-items: center;
  gap: 0 10px;
  padding: .2rem 0;
  min-height: 0;
}
.reg-drawer .field-main {
  display: contents; /* let label and value land in the grid's own tracks */
}
.reg-drawer .field-label {
  font-size: var(--fs-label); color: var(--ink-muted);
  margin: 0; white-space: nowrap;
}
.reg-drawer .field-value { font-size: var(--fs-body); min-width: 0; }
/* The toggle row has no .field-main wrapper, so give it the same three tracks
 * with the switch pinned right. Cleared is excluded for the specificity reason
 * above — it is a toggle row AND a hidden field, and this rule would otherwise
 * out-specify the hide. */
.reg-drawer .field-toggle-row:not([data-f="cleared"]) { grid-template-columns: auto 1fr auto; }
/* AUTO-ENTER TOGGLE (operator 29/07/2026: "a bleedover 'automatically enter
 * transaction' toggle").
 *
 * MEASURED CAUSE, not guessed. The row carried only `.field-row`, so the
 * density rule above gave it three tracks — `auto | minmax(72px,max-content) |
 * minmax(0,1fr)`. Its `.field-main` is `display:contents`, so the label and the
 * explanatory line became children 1 and 2 and the `<input class="ios-switch">`
 * landed in the trailing `1fr` track and STRETCHED across it: a switch sitting
 * hard against the card's right edge with the label marooned at the far left,
 * which is what read as the toggle bleeding out of its card. The schedule
 * form's copy has only two children, so it mis-tracked the other way.
 *
 * Both now declare `.field-toggle-row` and are pinned here explicitly: the text
 * takes the free space, the switch takes its own intrinsic width at the end and
 * never stretches. `justify-self:end` is the load-bearing half — a grid item
 * with no justification fills its track regardless of how wide the track is. */
.reg-drawer .field-toggle-row[data-f="autoenter"] {
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: 14px;
}
.reg-drawer .field-toggle-row[data-f="autoenter"] > .field-toggle-leading {
  display: flex; align-items: center; gap: 8px; min-width: 0;
}
.reg-drawer .field-toggle-row[data-f="autoenter"] > .field-toggle-leading .field-main {
  display: flex; flex-direction: column; gap: 1px; min-width: 0;
}
.reg-drawer .field-toggle-row[data-f="autoenter"] .field-label { min-width: 0; white-space: normal; }
.reg-drawer .field-toggle-row[data-f="autoenter"] > .ios-switch {
  justify-self: end; align-self: center; flex: 0 0 auto;
}
/* Icon bubbles are decorative at this density and cost a whole track's width. */
.reg-drawer .field-ico {
  width: 1rem; height: 1rem; background: none; padding: 0;
  color: var(--ink-muted);
}
.reg-drawer .field-ico .ico { width: 1rem; height: 1rem; }

/* MATCH THE DESKTOP THEME. The sheet's cards are phone chrome: heavy radius,
 * their own fill, a drop shadow. On desktop the drawer should read as part of
 * the register — a hairline-separated block, square-ish corners, no shadow. */
.reg-drawer .sheet-fields.card {
  background: none;
  box-shadow: none;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-card-desktop);
  padding: 8px 14px;
}
/* Embedded Schedule Options keeps its recurrence panel inside the generic
 * transaction drawer rather than using the register's fixed body popover.
 * Reuse the canonical option styling without allowing the fixed `.reg-datepop`
 * positioning contract to escape that drawer. */
body[data-surface="desktop"] .reg-drawer .reg-datepop-inline {
  position: relative; z-index: auto; max-width: none; max-height: none;
  overflow: visible; animation: none; box-shadow: none;
  background: none; border: 1px solid var(--hairline); border-radius: var(--radius-card-desktop);
}
body[data-surface="desktop"] .reg-drawer .reg-datepop-inline .reg-recurrence-options {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  align-content: start; gap: 0 18px; min-width: 0;
}
body[data-surface="desktop"] .reg-drawer .reg-datepop-inline .reg-recurrence-options > :is(.sched-section, .sched-stepped-help, .sched-recur-sentence) {
  grid-column: 1 / -1;
}
/* The inline monthly/yearly number rows share the two-column recurrence grid,
 * but the long short-month/leap-year explanation belongs to the full card
 * width.  Keep the first number compact beside its label and let the second
 * row claim both tracks; otherwise its flex value collapses to ~45px and the
 * hint wraps one word per line, inflating only monthly embedded editors. */
body[data-surface="desktop"] .reg-drawer .reg-datepop-inline .reg-recurrence-options > .sched-num {
  grid-template-columns: auto minmax(0, 1fr);
}
body[data-surface="desktop"] .reg-drawer .reg-datepop-inline .reg-recurrence-options > .sched-num + .sched-num {
  grid-column: 1 / -1;
}
body[data-surface="desktop"] .reg-drawer .reg-datepop-inline .reg-recurrence-options > .sched-num .sched-num-wrap {
  min-width: 0;
}
body[data-surface="desktop"] .reg-drawer .reg-schedule-policy {
  background: none; box-shadow: none; border: 1px solid var(--hairline);
  border-radius: var(--radius-card-desktop); padding: 8px 14px;
}
.reg-drawer .sched-recur {
  /* The existing Repeat editor stacked seven mouse-sized rows in one narrow
   * column while the adjacent desktop columns sat mostly empty. Pair the
   * recurrence controls across two columns; touch sheets remain one-column. */
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-content: start;
  gap: 0 18px;
}
.reg-drawer .sched-scroll {
  /* A schedule has four independent cards (basics, links, due behaviour and
   * recurrence). They previously auto-wrapped the fourth card onto a second
   * row, creating the recording's full-width empty band. Desktop has the width
   * to keep all four side by side. */
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0 18px;
}
.reg-drawer .sched-scroll > .sheet-fields.card { align-self: start; }
.reg-drawer .sched-scroll .sched-amt-hint {
  /* The input already says "Leave empty for a reminder". Repeating a longer
   * version beside it wrapped to four lines in Firefox and made the entire
   * drawer taller; the touch sheet keeps the explanatory sentence. */
  display: none;
}
@media (max-width: 1400px) {
  /* Four columns become narrower than the controls at laptop widths. Two
   * columns preserve every value and hint without horizontal clipping. */
  .reg-drawer .sched-scroll { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.reg-drawer .sched-recur > .sched-section,
.reg-drawer .sched-recur > .sched-stepped-help,
.reg-drawer .sched-recur > .sched-recur-sentence {
  grid-column: 1 / -1;
}
.reg-drawer .sched-recur > .sched-num {
  grid-template-columns: auto minmax(0, 1fr);
}
.reg-drawer .sched-recur > .sched-num + .sched-num {
  /* The long "Short months use their last day" explanation needs the card's
   * full width; the short cadence number fits beside the other controls. */
  grid-column: 1 / -1;
}
.reg-drawer .sched-recur > .sched-num .sched-num-wrap { min-width: 0; }
.reg-drawer .sched-recur .sched-num-inp {
  flex: 0 0 4.5rem;
  min-width: 4.5rem;
}
.reg-drawer .sheet-fields.card + .sheet-fields.card { margin-top: 0; }
/* Hairlines between rows inside a card, so a dense stack still parses as rows
 * rather than as one block of text. */
.reg-drawer .sheet-fields.card > .field-row + .field-row,
.reg-drawer .sheet-fields.card > .field-btn + .field-btn,
.reg-drawer .sheet-fields.card > .field-row + .field-btn,
.reg-drawer .sheet-fields.card > .field-btn + .field-row {
  border-top: 1px solid color-mix(in srgb, var(--hairline) 60%, transparent);
}

/* --- Chosen row in a picker list (operator 2026-07-29) ---------------------
 * The category-reassignment dialog marks the chosen replacement with `.on`, but
 * `.pick-row.on` was styled ONLY inside `.reflect-menu` — so the selected row
 * painted identically to every other row and the only feedback that a choice
 * had registered was the status line below the list (measured: identical
 * background, both transparent). A control whose own appearance does not change
 * when you act on it is the recurring "no feedback after tap" defect.
 *
 * Not scoped to the reassign dialog: any picker that marks a row `.on` wants
 * this, and the reflect-menu rule above still adds its own accent weight. */
.pick-row.on {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  font-weight: 600;
  box-shadow: inset 3px 0 0 var(--accent);
}
[data-theme="dark"] .pick-row.on {
  background: color-mix(in srgb, var(--accent) 24%, transparent);
}

/* --- Win11 Spending density contract (D-089, v1.122 row uplift) -----------
 * The row is the visual ruler on the desktop register.  Controls that sit in
 * its header, toolbar, scheduled band and supplemental surfaces must consume
 * that same ruler; otherwise a compact 25px row is surrounded by 30–44px
 * controls and the grid develops a second track or a stretched search field.
 * The register tiers are measured desktop values (25/32/41px, approximately
 * 15% above the previous 22/28/36px baseline). Every rule is surface-scoped so the touch grammar and its 44px floor remain
 * unchanged.  Colours are the theme tokens, never a second light/dark palette.
 */
body[data-surface="desktop"] {
  --reg-row-h: 32px;
  /* TWO control heights, and the split is load-bearing.
   *
   * `--reg-control-h` is the REGISTER GRID's control height and must equal the
   * row height exactly. D-089 pins the toolbar, header and Clear controls to the
   * row's own ruler. The register's density is the operator's measured
   * 25/32/41px setting, not something to inflate underneath it.
   *
   * A popover-local `--reg-control-h` is the POPOVER control height and carries the
   * operator's +10% (04/08/2026, using the "Every N occurrences" segmented
   * button as the reference: "this button height is a bit too short, make it 10%
   * taller"). That button, the calendar's day/nav/cancel cells, the repeat rows
   * and the recurrence panel are what the request pointed at — controls in a
   * floating panel, where nothing has to line up with a register row.
   *
   * General .btn geometry outside Spending remains untouched.  Ordinary
   * Spending actions are deliberately bound to the row by the UI-1 contract
   * below; popover fields keep this separate +10% field scale. */
  --reg-control-h: var(--reg-row-h);
  --reg-control-radius: clamp(5px, calc(var(--reg-control-h) * .28), 9px);
  --reg-control-pad-x: clamp(5px, calc(var(--reg-control-h) * .3), 9px);
  --reg-control-gap: clamp(4px, calc(var(--reg-control-h) * .3), 8px);
  /* THE TEXT RAIL — one number for a cell's resting text and for the input that
   * replaces it while editing (operator 04/08/2026: "the word 'main' in the
   * 'account' field is disproportionally hugging the left boundary … i have
   * already previously asked for this visual check to be done appwide").
   *
   * Measured before: every left-aligned cell painted its text at inkLeft 0,
   * flush against the grid track, while `.reg-editrow .inp` carried
   * padding-inline: 6px — so the same field jumped 6px sideways the moment you
   * clicked it, and at rest looked jammed into the column boundary. Both sides
   * now read this token, which is what stops the two drifting apart again.
   * Guarded by tests/v11130-cell-text-rail.test.js and the rail flow. */
  --reg-cell-rail: 6px;
}
html[data-density-reg="compact"] body[data-surface="desktop"] { --reg-row-h: 25px; }
html[data-density-reg="cosy"] body[data-surface="desktop"] { --reg-row-h: 32px; }
html[data-density-reg="roomy"] body[data-surface="desktop"] { --reg-row-h: 41px; }

/* Rows and their header stay one painted grid track.  Pinning the final Clear
 * control to the final grid line prevents an intrinsic button width from
 * creating an implicit second row at 1366px. */
body[data-surface="desktop"] .reg-row {
  box-sizing: border-box;
  min-height: var(--reg-row-h);
  height: var(--reg-row-h);
}
/* Split allocation lines carry two editable fields and have their own
 * readability floor; they are not the ordinary transaction rails measured by
 * the D-089 geometry contract. */
body[data-surface="desktop"] .reg-row.reg-split-row {
  height: auto;
  min-height: 32px;
}
body[data-surface="desktop"] .reg-row.reg-head {
  min-height: var(--reg-row-h);
  height: var(--reg-row-h);
  align-items: center;
  white-space: nowrap;
}
body[data-surface="desktop"] .reg-row.reg-head > * {
  grid-row: 1;
  min-width: 0;
}
body[data-surface="desktop"] .reg-row.reg-head .c-clr-head {
  /* `-1` is the final GRID LINE, which starts a new implicit track.  That
   * widened the header only (and shifted Outflow/Inflow left of every value
   * row) at laptop widths.  Start on the final explicit track instead. */
  grid-column: -2;
  height: var(--reg-control-h);
  min-height: var(--reg-control-h);
  max-height: var(--reg-control-h);
  padding: 0 var(--reg-control-pad-x);
  border-radius: var(--reg-control-radius);
  line-height: 1;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  overflow: visible;
  padding-inline: 0;
  letter-spacing: 0;
}
/* Header glyphs intentionally paint within otherwise-empty track space, and
 * the final cleared rail has no following column. Do not clip either at the
 * grid-cell edge: the rendered labels remain centred and fully legible. */
body[data-surface="desktop"] .reg-row.reg-head .c-date,
body[data-surface="desktop"] .reg-row .c-clr { overflow: visible; }

/* The toolbar is a single aligned rail.  Explicit height + zero vertical
 * padding removes the generic 30px/UA button contribution that made the
 * search wrapper taller than its compact peers. */
body[data-surface="desktop"] .reg-toolbar {
  align-items: center;
  flex-wrap: nowrap;
}
body[data-surface="desktop"] .reg-toolbar .btn:not(.disc):not(.icon-btn) {
  box-sizing: border-box;
  height: var(--reg-control-h);
  min-height: var(--reg-control-h);
  max-height: var(--reg-control-h);
  padding-block: 0;
  padding-inline: var(--reg-control-pad-x);
  border-radius: var(--reg-control-radius);
  line-height: 1;
  white-space: nowrap;
}
body[data-surface="desktop"] .reg-undo-group {
  align-items: center;
  min-height: var(--reg-control-h);
}
body[data-surface="desktop"] .reg-grid-search {
  box-sizing: border-box;
  align-self: center;
  height: var(--reg-control-h);
  min-height: var(--reg-control-h);
  max-height: var(--reg-control-h);
  padding-inline: var(--reg-control-pad-x);
  border-radius: var(--reg-control-radius);
  flex: 0 0 auto;
}
/* D-089: every toolbar control is measured against the TRANSACTION ROW height,
 * and the search field is measured on the INPUT itself, not its pill wrapper.
 * `height: 100%` resolved against the wrapper's CONTENT box, so the wrapper's
 * 1px borders left the input 2px short of the row (23px against 25px) and the
 * rail read as subtly misaligned. Deriving from --reg-control-h makes the input
 * exactly one row high at every density. */
body[data-surface="desktop"] .reg-grid-search .reg-grid-search-input {
  box-sizing: border-box;
  width: 15rem;
  max-width: 40vw;
  height: var(--reg-control-h);
  min-height: var(--reg-control-h);
  max-height: var(--reg-control-h);
  padding: 0;
  line-height: 1;
}
/* The labelled Clear control is a child of the density-sized search field.
 * Inset its face by 2px on every side: the search wrapper owns a 1px border,
 * and a further 1px breathing line keeps the accent fill from touching that
 * border at compact, cosy, and roomy densities. */
body[data-surface="desktop"] .reg-grid-search-clear {
  height: calc(var(--reg-control-h) - 4px);
  min-height: calc(var(--reg-control-h) - 4px);
  max-height: calc(var(--reg-control-h) - 4px);
  align-self: center;
}
/* At the 1024px desktop floor the sidebar and action cluster leave a finite
 * rail. Let the search wrapper and input shrink together instead of exporting
 * the input's 240px intrinsic width into document overflow; wide desktops keep
 * the original dense 15rem field. */
@media (min-width: 901px) and (max-width: 1199px) {
  body[data-surface="desktop"] .reg-clearbar,
  body[data-surface="desktop"] .reg-toolbar {
    min-width: 0; max-width: 100%;
  }
  body[data-surface="desktop"] .reg-grid-search {
    flex: 0 1 min(279px, 100%); min-width: 0; max-width: 100%;
  }
  body[data-surface="desktop"] .reg-grid-search .reg-grid-search-input {
    width: min(240px, calc(100% - 2.5rem)); max-width: 100%; min-width: 0;
  }
}

/* Scheduled rows are the same register rows, including the band header and
 * its occurrence toggle. */
body[data-surface="desktop"] :is(.reg-grid .reg-sched-head, .reg-sched-band.scheduled-fill) {
  /* The supplied desktop swatch is the same warm greige as Plan group bands.
   * Override only the desktop Scheduled scope; touch date bands keep their
   * existing surface contract, and dark mode follows --plan-group-bg. */
  --scheduled-fill: var(--plan-group-bg);
  --scheduled-fill-strong: color-mix(in srgb, var(--ink) 6%, var(--plan-group-bg));
}
body[data-surface="desktop"] .reg-grid .reg-sched-head {
  box-sizing: border-box;
  height: var(--reg-row-h);
  min-height: var(--reg-row-h);
  padding: 0 10px;
  white-space: nowrap;
}
body[data-surface="desktop"] .reg-sched-band.scheduled-fill .sched-row.reg-row {
  box-sizing: border-box;
  height: var(--reg-row-h);
  min-height: var(--reg-row-h);
  padding-block: 0;
}
body[data-surface="desktop"] .reg-sched-band.scheduled-fill .sched-row.reg-row .c-clr.sched-occ-toggle {
  width: var(--reg-control-h);
  height: var(--reg-control-h);
  min-width: var(--reg-control-h);
  min-height: var(--reg-control-h);
}
/* Scheduled occurrence disclosure owns the schedule row, not the Cleared
 * semantic column.  It lives beside the payee/cadence metadata so the same
 * keyboard/ARIA control remains available when the C track is omitted. */
body[data-surface="desktop"] .reg-sched-band.scheduled-fill .sched-row.reg-row .c-payee {
  display: flex; align-items: center; min-width: 0; gap: var(--reg-control-gap, 4px);
}
body[data-surface="desktop"] .reg-sched-band.scheduled-fill .sched-row.reg-row .c-payee > :is(.reg-payee-name, .xfer-line) {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Transfer rule payees contain two account names, a chevron and cadence/status
 * metadata in one compact register cell. The shared touch transfer primitive
 * allows its account pair to wrap, but a desktop register row is a fixed-height
 * 25px ruler: wrapping here made the `.xfer-line` 68px tall and painted the
 * 20/07 rows over their neighbours. Keep the desktop pair on one ellipsised
 * line; touch retains its deliberate wrapped treatment. */
body[data-surface="desktop"] .reg-sched-band.scheduled-fill .sched-row.reg-row .c-payee > .xfer-line {
  /* Do not let the transfer pair grow into the whole payee rail.  The
   * recurrence/status marks belong immediately after its intrinsic label;
   * flex-shrink still gives the pair the available room and the existing
   * overflow rule keeps a long pair ellipsised on the fixed-height row. */
  flex: 0 1 auto;
  flex-wrap: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* A transfer's source → destination is the useful payee label. On the compact
 * desktop rail the cadence word would consume the room needed for that pair;
 * retain its icon as the schedule cue and let the account pair own the line. */
body[data-surface="desktop"] .reg-sched-band.scheduled-fill .sched-row.reg-row .c-payee > .xfer-line + .reg-recur .cad-word {
  display: none;
}
/* Current desktop parity: projected and posted recurring rows share the same
 * payee rail.  The schedule disclosure lives in this cell, so its old 28px
 * touch-sized box made the projected cell taller than the posted cadence
 * rail; the shared row token keeps both cells on the register's own ruler. */
html[data-desktop-theme="current"] body[data-surface="desktop"] :is(.reg-txn, .reg-sched-band.scheduled-fill .sched-row.reg-row) .c-payee {
  min-height: var(--reg-control-h);
  gap: var(--reg-control-gap);
}
html[data-desktop-theme="current"] body[data-surface="desktop"] .reg-sched-band.scheduled-fill .sched-row.reg-row .sched-occ-slot .sched-occ-toggle {
  width: var(--reg-control-h);
  height: var(--reg-control-h);
  min-width: var(--reg-control-h);
  min-height: var(--reg-control-h);
}
/* The projected rule carries the `reg-txn` contract for typography and text
 * rails. Keep the Scheduled wash authoritative when optional zebra striping is
 * enabled, including its due/selected emphasis in both themes. */
html[data-zebra-reg="true"] body[data-surface="desktop"] .reg-sched-band.scheduled-fill .sched-row.reg-txn {
  background: var(--scheduled-fill);
}
html[data-zebra-reg="true"] body[data-surface="desktop"] .reg-sched-band.scheduled-fill .sched-row.reg-txn.sched-row-due {
  background: color-mix(in srgb, var(--accent) 20%, var(--card));
}
html[data-zebra-reg="true"] body[data-surface="desktop"] .reg-sched-band.scheduled-fill .sched-row.reg-txn.selected {
  background: color-mix(in srgb, var(--accent) 12%, var(--scheduled-fill));
}
/* Keep the native projected checkbox above the register's enlarged c-sel hit
 * overlay. Without a stacking context the overlay becomes the hit owner at
 * the checkbox centre, so a real pointer click never reaches the input. */
body[data-surface="desktop"] .reg-sched-band.scheduled-fill .sched-selrow {
  position: relative;
  z-index: 1;
}
.sched-occ-slot {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  margin-inline-start: auto;
}
.sched-occ-slot .sched-occ-toggle { position: relative; z-index: 1; }

/* Current-theme due reminders use the trailing rail for one compact, explicit
 * action. It replaces the circular cleared glyph only on a reminder row; the
 * Register and touch scheduled list keep their existing composition. */
html[data-desktop-theme="current"] body[data-surface="desktop"] .reg-row.sched-row .c-clr:has(.sched-enter-amount) {
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--reg-control-gap);
  width: auto;
  min-width: 0;
  height: var(--reg-control-h);
  box-shadow: none;
  border-radius: var(--reg-control-radius);
  cursor: default;
}
html[data-desktop-theme="current"] body[data-surface="desktop"] .reg-row.sched-row .c-clr:has(.sched-enter-amount) .sched-occ-toggle {
  display: inline-flex;
}
html[data-desktop-theme="current"] body[data-surface="desktop"] .sched-enter-amount {
  box-sizing: border-box;
  height: var(--reg-control-h);
  min-height: var(--reg-control-h);
  padding: 0 var(--reg-control-pad-x);
  border: 1px solid var(--hairline);
  border-radius: var(--reg-control-radius);
  background: color-mix(in srgb, var(--accent) 8%, var(--card));
  color: var(--accent);
  font: inherit;
  font-size: var(--fs-label);
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
}
html[data-desktop-theme="current"] body[data-surface="desktop"] .sched-due-strip {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  box-sizing: border-box;
  min-height: var(--reg-control-h);
  padding: 0 var(--reg-control-pad-x) var(--reg-control-gap);
  background: var(--scheduled-fill);
}
html[data-desktop-theme="current"] body[data-surface="desktop"] .sched-enter-amount:focus-visible,
html[data-desktop-theme="current"] body[data-surface="desktop"] .sched-enter-amount:hover {
  background: color-mix(in srgb, var(--accent) 16%, var(--card));
  outline: none;
}
html[data-desktop-theme="current"] body[data-surface="desktop"] .sched-due-count {
  color: var(--ink-muted);
  font-size: var(--fs-micro);
}

/* Date/repeat surfaces stay anchored by JS, but their desktop box is field-
 * scale and always contained by the viewport.  The recurrence surface is
 * deliberately narrower than the old 520px panel; collision flipping remains
 * the positioning owner's job and is untouched here. */
/* HEIGHT: fill the room that exists, scroll only when it runs out (operator
 * 04/08/2026: "the popout needs scrolling down when there is still ample screen
 * space below to have a longer popout - do that instead").
 *
 * The old cap was `min(100dvh - 16px, 460px)`. That flat 460px was the real
 * governor on a 1080-tall screen: a popover opened near the top of the register
 * scrolled INTERNALLY while ~600px of empty page sat below it. `--popout-room`
 * is written by the positioner (inline-editor.js `place()`), which already
 * measures the distance from the anchor to the viewport edge on the side it
 * chose to open; the 460px only survives as the FALLBACK for the first paint,
 * before a measurement exists. `overflow-y: auto` stays, so a genuinely tall
 * popover on a genuinely short viewport still scrolls — it just no longer
 * scrolls while there is space to grow into. */
/* THE OPERATOR'S +10%, scoped to the popovers (04/08/2026, using the "Every N
 * occurrences" segmented button as the reference: "this button height is a bit
 * too short, make it 10% taller"). Re-declaring the token HERE lifts every
 * control inside these panels — the calendar's day/nav/cancel cells, the repeat
 * rows, the recurrence panel's segmented pickers and inputs, the flyout's picker
 * rows — without touching a single one of their rules, because they all already
 * size off it.
 *
 * It must NOT reach the register grid. D-089 pins the toolbar, header and Clear
 * controls to the ROW's own ruler, and `win11-d089-fail-first.mjs` measures it:
 * raising the global token above a 25px row puts Clear outside the header
 * rail and reported every toolbar control as drifting. A floating panel has no
 * row to line up with, which is exactly why the extra height belongs here and
 * nowhere else. */
body[data-surface="desktop"] :is(.reg-datepop, .sub-flyout) {
  --reg-control-h: calc(var(--reg-row-h) * 1.1);
  max-width: min(420px, calc(100vw - 16px));
  max-height: min(calc(100dvh - 16px), var(--popout-room, 460px));
  overflow-x: hidden;
  overflow-y: auto;
  border-radius: var(--reg-control-radius);
}
body[data-surface="desktop"] .sub-flyout.picker-modal {
  padding: var(--reg-control-gap);
  gap: var(--reg-control-gap);
}
body[data-surface="desktop"] .sub-flyout :is(.picker-head, .picker-cancel) {
  min-height: var(--reg-control-h);
  line-height: 1;
}
body[data-surface="desktop"] .sub-flyout .picker-search {
  box-sizing: border-box;
  height: var(--reg-control-h);
  min-height: var(--reg-control-h);
  padding-block: 0;
}
body[data-surface="desktop"] .sub-flyout .pick-row {
  box-sizing: border-box;
  height: var(--reg-control-h);
  min-height: var(--reg-control-h);
  max-height: var(--reg-control-h);
  padding: 0 var(--reg-control-pad-x);
  border-radius: var(--reg-control-radius);
  line-height: 1;
}
body[data-surface="desktop"] .reg-datepop .calendar-modal {
  width: 100%;
  max-width: none;
}
body[data-surface="desktop"] .reg-datepop:has(.reg-recurrence-options:not([hidden])) {
  width: min(420px, calc(100vw - 16px));
}
/* The End calendar is deliberately positioned from its trigger and may need
 * to extend past the narrow recurrence row. Its own face owns scrolling; the
 * date popover must not clip the visible layer at the row's right edge. */
body[data-surface="desktop"] .reg-datepop:has(.rec-end-calendar) {
  overflow: visible;
}
body[data-surface="desktop"] .reg-datepop :is(.cal-cell, .cal-nav, .cal-cancel, .cal-clear) {
  box-sizing: border-box;
  min-height: var(--reg-control-h);
}
body[data-surface="desktop"] .reg-datepop .cal-cell {
  height: var(--reg-control-h);
  width: var(--reg-control-h);
  min-width: var(--reg-control-h);
  aspect-ratio: 1;
  padding: 0;
  border-radius: var(--radius-s);
  line-height: 1;
}
/* The month/year zoom cells keep the control HEIGHT but not the day cell's
 * `padding: 0` + square width: they carry a text label ("Sep", "2027"), so they
 * span their column and take horizontal padding. Without this they inherited a
 * zero-padding square and the labels sat hard against the cell edge. */
body[data-surface="desktop"] .reg-datepop .cal-cell-wide {
  width: 100%;
  padding-inline: var(--reg-control-pad-x);
  border-radius: var(--reg-control-radius);
}
body[data-surface="desktop"] .reg-datepop .cal-nav,
body[data-surface="desktop"] .reg-datepop :is(.cal-cancel, .cal-clear) {
  height: var(--reg-control-h);
  padding-block: 0;
  border-radius: var(--reg-control-radius);
  line-height: 1;
}
body[data-surface="desktop"] .reg-datepop-repeat {
  box-sizing: border-box;
  height: var(--reg-control-h);
  min-height: var(--reg-control-h);
  max-height: var(--reg-control-h);
  padding: 0 var(--reg-control-pad-x);
  border-radius: var(--reg-control-radius);
  line-height: 1;
}
body[data-surface="desktop"] .reg-datepop-repeat-list button,
body[data-surface="desktop"] .reg-datepop-custom .cr-n,
body[data-surface="desktop"] .reg-datepop-custom .cr-unit,
body[data-surface="desktop"] .reg-datepop-custom-actions button,
body[data-surface="desktop"] .reg-recurrence-options :is(.rec-vary, .rec-add-step, .rec-end-clear, .rec-step-delete, .rec-remind-choices button, .rec-end-date, .inp) {
  box-sizing: border-box;
  height: var(--reg-control-h);
  min-height: var(--reg-control-h);
  max-height: var(--reg-control-h);
  padding-block: 0;
  border-radius: var(--reg-control-radius);
  line-height: 1;
}
body[data-surface="desktop"] .reg-recurrence-options :is(.recurrence-options-head, .recurrence-policy, .recurrence-end) {
  min-height: var(--reg-control-h);
}

/* ============================================================================
 * POP BURST — the app's value-changed motif (operator 03/08/2026: "note it
 * permanently as one of the visual identity of this app"). D-091.
 *
 * Measured off "overspentflow.MP4" at 60fps, not eyeballed: three short rays
 * fan off the value's top-right corner, stretch outward and fade over ~0.42s,
 * while the digits roll with a slight vertical blur. Driven by pop-burst.js,
 * which adds `.pop-burst` / `.pop-roll` and strips them on animationend.
 *
 * Cost discipline: the rays are ONE pseudo-element painted with three
 * `linear-gradient` strokes, so a burst adds no DOM nodes and no reflow, and
 * animates only `transform`/`opacity` — both compositor-only. That is what
 * makes it safe to use on a slider that fires continuously.
 * ========================================================================= */
:root {
  --pop-dur: .42s;
  --pop-ray-dur: .38s;
  /* Rays inherit the ink of whatever they burst from, so the motif carries into
   * both themes and onto any coloured surface (accent pill, red header, green
   * header) without a per-site override. */
  --pop-ray-ink: currentColor;
}

/* The host only needs a positioning context; it keeps its own box untouched so
 * the motif can be dropped onto an existing pill without restyling it. */
.pop-burst, .pop-roll { position: relative; }

/* THE RAY FAN. One box pinned to the top-right corner, holding three strokes at
 * 20°/45°/70°, scaled out from the corner and faded. `overflow: visible` is
 * implicit — the rays deliberately paint OUTSIDE the host, which is why the host
 * must not clip (see the `.pop-burst` guard on clipping parents below). */
.pop-burst::after {
  content: "";
  position: absolute;
  top: -0.30em; right: -0.62em;
  width: 0.85em; height: 0.85em;
  pointer-events: none;
  transform-origin: bottom left;
  background-repeat: no-repeat;
  /* Three tapered strokes. Each gradient paints one ray; sizes/positions place
   * them in a fan opening up and to the right of the corner. */
  background-image:
    linear-gradient(20deg,  transparent 42%, var(--pop-ray-ink) 42%, var(--pop-ray-ink) 58%, transparent 58%),
    linear-gradient(45deg,  transparent 42%, var(--pop-ray-ink) 42%, var(--pop-ray-ink) 58%, transparent 58%),
    linear-gradient(70deg,  transparent 42%, var(--pop-ray-ink) 42%, var(--pop-ray-ink) 58%, transparent 58%);
  background-size: 0.42em 0.10em, 0.50em 0.10em, 0.42em 0.10em;
  background-position: 0 0.60em, 0.16em 0.26em, 0.52em 0.06em;
  opacity: 0;
  animation: pop-rays var(--pop-ray-dur) var(--ease-standard) both;
}
/* Stretch out from the corner and fade — the reference shows the rays at their
 * longest around 40% of the burst, then thinning rather than shrinking back. */
@keyframes pop-rays {
  0%   { opacity: 0;   transform: scale(.35) translate(-.06em, .06em); }
  35%  { opacity: .95; transform: scale(1)   translate(0, 0); }
  70%  { opacity: .55; transform: scale(1.18) translate(.05em, -.05em); }
  100% { opacity: 0;   transform: scale(1.3)  translate(.09em, -.09em); }
}

/* THE DIGIT ROLL. A brief lift + blur in the direction the value moved, so a
 * rising figure reads as rising. Kept small (0.16em) — the reference is a
 * settle, not a slot-machine spin. */
.pop-roll { animation: pop-roll-up var(--pop-dur) var(--ease-ios-rebound) both; }
.pop-roll[data-pop-dir="down"] { animation-name: pop-roll-down; }
@keyframes pop-roll-up {
  0%   { transform: translateY(.16em); filter: blur(1.1px); opacity: .55; }
  55%  { transform: translateY(-.03em); filter: blur(0); opacity: 1; }
  100% { transform: translateY(0); filter: blur(0); opacity: 1; }
}
@keyframes pop-roll-down {
  0%   { transform: translateY(-.16em); filter: blur(1.1px); opacity: .55; }
  55%  { transform: translateY(.03em); filter: blur(0); opacity: 1; }
  100% { transform: translateY(0); filter: blur(0); opacity: 1; }
}

/* Reduced motion: pop-burst.js already refuses to add the classes, so this is
 * belt-and-braces for a mid-animation preference flip. */
@media (prefers-reduced-motion: reduce) {
  .pop-burst::after { animation: none; opacity: 0; }
  .pop-roll { animation: none; }
}

/* ============================================================================
 * COVER OVERSPENDING — touch flow (operator 03/08/2026, "overspentflow.MP4":
 * "duplicate everything there - the look, size animation, frame rate, workflow,
 * math of this workflow… and colour things on theme").
 *
 * The header IS the state: deep red while money is still owed, morphing to
 * green the moment the shortfall closes. Both come from the app's own money
 * tokens (--neg / --pos), NOT from the reference's hardcoded dark-only reds and
 * greens, so the whole flow reads correctly in light and dark alike.
 * ========================================================================= */
.cover-sheet {
  padding: 0; overflow: hidden; display: flex; flex-direction: column;
  width: 100%; max-width: 480px; max-height: 92dvh;
  /* The motion trace proved the generic picker pop was only a 4px snap plus
   * opacity fade. Cover is a touch bottom-sheet: give the card real travel
   * while keeping the same tokened rebound and reduced-motion endpoint. */
  --cover-entry-duration: .5s;
}
.cover-picker,
.overspent-categories-sheet { --cover-entry-duration: .5s; }
@keyframes cover-sheet-entry {
  from { opacity: 0; translate: 0 100%; }
  82% { opacity: 1; translate: 0 -1.5%; }
  to { opacity: 1; translate: 0 0; }
}
@media (pointer: coarse) {
  body:is([data-surface="phone"], [data-surface="tablet"]) .sheet-overlay:has(.cover-sheet) .cover-sheet,
  body:is([data-surface="phone"], [data-surface="tablet"]) .sheet-overlay:has(.cover-picker) .cover-picker,
  body:is([data-surface="phone"], [data-surface="tablet"]) .sheet-overlay:has(.overspent-categories-sheet) .overspent-categories-sheet {
    animation: cover-sheet-entry var(--cover-entry-duration) var(--ease-ios-rebound) both;
  }
}
.cover-head {
  /* release-owner:plan */
  position: relative; padding: 18px 18px 22px; text-align: center;
  --cover-head-fill: var(--neg);
  background: var(--cover-head-fill); color: var(--accent-ink);
  /* The morph. Matches the reference's slow wash from red to green rather than
   * an instant swap — measured at roughly half a second there. */
  --cover-motion-duration: .5s;
  transition: background var(--cover-motion-duration) var(--ease-standard);
}
.cover-head.is-covered { --cover-head-fill: var(--pos); }
.cover-sheet .cover-head::after {
  content: '';
  position: absolute;
  left: 50%; bottom: -8px;
  width: 16px; height: 16px;
  background: var(--cover-head-fill);
  translate: -50% 0;
  rotate: 45deg;
  transition: background var(--cover-motion-duration) var(--ease-standard);
}
.cover-x {
  position: absolute; top: 14px; left: 14px;
  width: var(--ctl-h-md); height: var(--ctl-h-md);
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: 999px; cursor: pointer;
  background: var(--cover-close-fill); color: var(--accent-ink); font-size: var(--fs-h2); line-height: 1;
}
.cover-title { font-weight: 700; font-size: var(--fs-value); }
.cover-cat { margin-top: 14px; font-size: var(--fs-h3); font-weight: 600; line-height: 1.3; }
/* The live figure. A dark lozenge on the coloured field, exactly as the
 * reference draws it — and the anchor the pop burst fires from. */
.cover-amt-pill {
  display: inline-block; margin-top: 12px; padding: .3rem 1.1rem;
  background: var(--cover-pill-fill); border-radius: 999px;
  font-size: var(--fs-h2); font-weight: 800; font-variant-numeric: tabular-nums;
}
.cover-body { padding: 14px; overflow-y: auto; flex: 1 1 auto; }
.cover-sources { display: flex; flex-direction: column; gap: 4px; }

/* One funding source: who gives, how much, and what it keeps. */
.cover-src { background: var(--card); border-radius: var(--radius-s); padding: 12px 14px; }
.cover-src + .cover-src { margin-top: 8px; }
.cover-src-top { display: grid; grid-template-columns: minmax(0, 1fr) max-content max-content; align-items: center; gap: 10px; }
.cover-src-name { flex: 1 1 auto; min-width: 0; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cover-src-take { flex: 0 0 auto; font-variant-numeric: tabular-nums; color: var(--ink-muted); }
.cover-src-left {
  flex: 0 0 auto; display: inline-block; border-radius: 999px;
  padding: .05rem .6rem; font-weight: 600; font-variant-numeric: tabular-nums;
}
.cover-src-left.pos-pill { background: var(--rta-fill); color: var(--rta-ink); }
.cover-src-left.zero-pill { background: var(--chip); color: var(--ink-muted); }

/* The slider. `--fill` is set from JS so the track shows how much of this
 * source has been committed — the reference fills the bar behind the thumb. */
.cover-range {
  width: 100%; margin-top: 10px; appearance: none; -webkit-appearance: none;
  background: transparent; cursor: pointer; height: var(--ctl-h-md); touch-action: none;
}
.cover-range::-webkit-slider-runnable-track {
  height: 10px; border-radius: 999px;
  background: linear-gradient(to right, var(--accent) var(--fill, 0%), var(--chip) var(--fill, 0%));
}
.cover-range::-moz-range-track {
  height: 10px; border-radius: 999px;
  background: linear-gradient(to right, var(--accent) var(--fill, 0%), var(--chip) var(--fill, 0%));
}
/* Thumb sized for a finger (CARDINAL: never below 44px of touch target — the
 * track is thin but the input itself is --ctl-h-md tall, so the whole strip is
 * grabbable, and the visible thumb sits centred in it). */
.cover-range::-webkit-slider-thumb {
  appearance: none; -webkit-appearance: none;
  width: 28px; height: 28px; margin-top: -9px;
  border-radius: 999px; background: var(--card);
  border: 1px solid var(--hairline); box-shadow: 0 1px 4px rgba(0, 0, 0, .3);
}
.cover-range::-moz-range-thumb {
  width: 28px; height: 28px; border: 1px solid var(--hairline);
  border-radius: 999px; background: var(--card); box-shadow: 0 1px 4px rgba(0, 0, 0, .3);
}

.cover-add {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  width: 100%; min-height: var(--ctl-h-md); margin-top: 10px;
  background: none; border: 0; cursor: pointer;
  color: var(--accent); font: inherit; font-weight: 600;
}
.cover-add-ico {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border-radius: 999px;
  background: var(--accent); color: var(--accent-ink); font-weight: 700; line-height: 1;
}
.cover-add-ico .ico { width: 14px; height: 14px; stroke-width: 2.25; }
.cover-done { margin: 0 14px 14px; align-self: stretch; }
.cover-done:disabled { opacity: .5; cursor: default; }

/* The stacked source picker. */
.cover-picker { max-height: 82dvh; display: flex; flex-direction: column; }
.cover-picker {
  /* A nested source picker is its own opaque layer.  Keep the card bounded and
   * make the results pane the sole scroller so the parent Cover sheet can never
   * bleed through the picker while its rows are painted. */
  min-height: 0;
  overflow: hidden;
  background: var(--card);
}
.cover-sheet.cover-picker-underlay { visibility: hidden; pointer-events: none; }
.cover-picker .picker-list { min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
.cover-pick-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.cover-pick-title { flex: 1 1 auto; font-weight: 700; font-size: var(--fs-value); }
.cover-mo {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: 6px;
  min-height: var(--ctl-h-sm, 34px); padding: .2rem .7rem;
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent); border: 0; border-radius: 999px;
  font: inherit; font-weight: 600; cursor: pointer; position: relative;
}
.cover-mo-label { font-size: var(--fs-micro); color: var(--ink-muted); font-weight: 500; }
.cover-mo-select {
  min-height: var(--ctl-h-sm, 34px); max-width: 9.5rem; padding: .15rem 1.7rem .15rem .25rem;
  border: 0; border-radius: 999px; background: transparent; color: inherit;
  font: inherit; font-weight: 700; cursor: pointer; appearance: none; -webkit-appearance: none;
}
.cover-mo-caret {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1rem; height: 1rem; color: currentColor; pointer-events: none;
}
.cover-mo-caret .ico { width: 100%; height: 100%; }

/* Tapped native selects on touch surfaces are intentionally chrome-free. Keep
 * the real chooser and its accessibility semantics; keyboard users on a
 * pointer-fine surface retain the normal focus affordance below. */
@media (pointer: coarse) {
  body:is([data-surface="phone"], [data-surface="tablet"]) :is(
    .cover-mo-select,
    .plan-filter-select
  ):focus,
  body:is([data-surface="phone"], [data-surface="tablet"]) :is(
    .cover-mo-select,
    .plan-filter-select
  ):focus-visible {
    outline: none !important;
    box-shadow: none !important;
  }
}
@media (pointer: fine) {
  body:is([data-surface="phone"], [data-surface="tablet"]) :is(
    .cover-mo-select,
    .plan-filter-select
  ):focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
  }
}
.cover-picker .pick-row.pick-row-split {
  display: grid; grid-template-columns: minmax(0, 1fr) max-content;
  align-items: center; gap: 12px; padding-inline-start: .4rem;
}
.cover-picker .cover-picker-source-name {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cover-picker .cover-picker-source-amount { justify-self: end; }

@media (prefers-reduced-motion: reduce) {
  .cover-head { transition: none; }
  .sheet-overlay:has(.cover-sheet) .cover-sheet,
  .sheet-overlay:has(.cover-picker) .cover-picker,
  .sheet-overlay:has(.overspent-categories-sheet) .overspent-categories-sheet { animation: none; }
}

/* ============================================================================
 * DESKTOP VERTICAL DENSITY (operator 04/08/2026: "look at the massive waste of
 * space above the 'add transaction' button and below the 'working balance' row.
 * scour all tabs on desktop to weed out such wastage of vertical space").
 *
 * MEASURED on win11-desktop at 1920x1080 before this block: every tab spent its
 * first ~190px on chrome before a single row of content — the screen title
 * (30px + 9.6px), the approve banner (39px + 8px), the balance equation (40px +
 * 12px), the toolbar (24px + 10px) and 20px of `.content` padding on top.
 *
 * The gaps between those blocks were the waste, not the blocks themselves, so
 * this tightens the MARGINS and the page's own top padding rather than shrinking
 * any control — nothing here reduces a hit target, and the +10% control height
 * from the same session's request is untouched.
 *
 * DESKTOP ONLY, and deliberately so. Touch surfaces need their breathing room
 * and their 44px floors; the dense keyboard-and-mouse surface is the one the
 * operator asked to tighten (D-089: "always keyboard and mouse first"). Every
 * selector below is `body[data-surface="desktop"]`-scoped for that reason.
 * ==========================================================================*/
body[data-surface="desktop"] .content {
  /* 20px of dead band above a title that already carries its own margin. */
  padding-top: 10px;
}
body[data-surface="desktop"] .screen-title {
  /* MARGIN ONLY. The desktop title size is already set to --fs-h2 earlier in
   * this file; restating a size here made it BIGGER (measured: the title grew
   * 30px → 37px), which is the opposite of the ask. The gap under it was the
   * waste, not the type. */
  margin-bottom: .35rem;
}
body[data-surface="desktop"] .bal-equation {
  /* The "below the working balance row" gap the operator named. */
  margin-bottom: 6px;
}
body[data-surface="desktop"] .reg-toolbar {
  /* The "above the Add Transaction button" gap the operator named. */
  margin-bottom: 6px;
}
/* The same treatment for the other tabs' own leading blocks, so the tightening
 * is app-wide on desktop rather than a Spending-only fix. */
body[data-surface="desktop"] :is(.reflect-report-link, .reflect-tabs, .reflect-controls) {
  margin-bottom: 8px;
}
body[data-surface="desktop"] .reflect-insight-tiles { margin-bottom: 10px; }
body[data-surface="desktop"] .plan-header { margin-bottom: 8px; }

/* ============================================================================
 * WIN11 SPENDING GLASS + REBOUND LANE (operator 06/08/2026)
 * ----------------------------------------------------------------------------
 * Desktop Spending is a dense pointer surface, so the glass belongs on the
 * controls and floating panels, not on the transaction rails themselves.  The
 * rules are deliberately scoped to `data-surface="desktop"`: touch keeps its
 * existing 44px geometry, sheet layout and opaque-card contrast.  Every fill,
 * edge and shadow is derived from the existing theme tokens, so the same rules
 * read in light and dark without a second colour palette.
 * ========================================================================== */
body[data-surface="desktop"] {
  --spend-glass-fill: color-mix(in srgb, var(--card) 48%, transparent);
  --spend-glass-fill-strong: color-mix(in srgb, var(--card) 68%, transparent);
  /* Foreground editors sit above ledger text. Keep the blur/lens identity but
   * give these panels enough card mix that useful copy cannot ghost through. */
  --spend-glass-fill-panel: color-mix(in srgb, var(--card) 90%, transparent);
  --spend-glass-shadow: color-mix(in srgb, var(--ink) 20%, transparent);
  --spend-glass-highlight: color-mix(in srgb, var(--ink) 10%, transparent);
}

/* The Spending title should hand the balance/toolbar its next row directly;
 * this removes the last visible band under the heading without changing the
 * title's type or any touch surface.  The existing density contract still
 * owns the page's top pad and the balance/toolbar margins. */
body[data-surface="desktop"]:has(.reg-grid) .screen-title { margin-bottom: 2px; }
body[data-surface="desktop"]:has(.reg-grid) .screen-title + :is(.reg-headtools, .approve-topbar, .bal-equation) {
  margin-top: 0;
}
/* An empty approval queue used to leave a hidden 38px desktop placeholder in
 * flow.  That placeholder was the measured 53px title-to-balance band; there
 * is no banner to reserve when it is empty, so remove the slot entirely. */
body[data-surface="desktop"] .approve-topbar-empty { display: none; }

/* Toolbar controls: frosted secondary faces, a translucent accent primary,
 * and a real springy press on the control that was clicked.  The authored
 * heights/padding from D-089 remain untouched, so row rails cannot move. */
body[data-surface="desktop"] :is(
  .reg-toolbar .btn,
  .reg-headtools .reg-icon-btn,
  .reg-headtools .reg-select-btn,
  .reg-headtools .reg-search-dismiss
) {
  border: 1px solid var(--chrome-edge);
  background: var(--spend-glass-fill);
  color: var(--ink);
  -webkit-backdrop-filter: blur(14px) saturate(1.45);
  backdrop-filter: blur(14px) saturate(1.45);
  box-shadow:
    0 4px 12px var(--spend-glass-shadow),
    inset 0 1px 0 var(--spend-glass-highlight);
  transition:
    transform var(--dur-fast) var(--ease-ios-rebound),
    filter var(--dur-fast) var(--ease-standard),
    background var(--dur-base) var(--ease-ios-rebound),
    border-color var(--dur-base) var(--ease-standard),
    box-shadow var(--dur-base) var(--ease-standard);
}
body[data-surface="desktop"] .reg-toolbar .btn:not(.secondary) {
  background: color-mix(in srgb, var(--accent) 82%, transparent);
  border-color: color-mix(in srgb, var(--accent) 58%, var(--chrome-edge));
  color: var(--accent-ink);
}
body[data-surface="desktop"] :is(.reg-icon-btn.on, .reg-select-btn.on) {
  background: color-mix(in srgb, var(--accent) 82%, transparent);
  border-color: color-mix(in srgb, var(--accent) 58%, var(--chrome-edge));
  color: var(--accent-ink);
}
body[data-surface="desktop"] :is(
  .reg-toolbar .btn,
  .reg-headtools .reg-icon-btn,
  .reg-headtools .reg-select-btn,
  .reg-headtools .reg-search-dismiss
):active {
  transform: scale(.95);
  filter: brightness(.93);
}
body[data-surface="desktop"] :is(
  .reg-toolbar .btn:focus-visible,
  .reg-headtools .reg-icon-btn:focus-visible,
  .reg-headtools .reg-select-btn:focus-visible,
  .reg-headtools .reg-search-dismiss:focus-visible
) {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
body[data-surface="desktop"] .reg-grid-search {
  background: var(--spend-glass-fill);
  border-color: color-mix(in srgb, var(--accent) 28%, var(--chrome-edge));
  -webkit-backdrop-filter: blur(12px) saturate(1.35);
  backdrop-filter: blur(12px) saturate(1.35);
  box-shadow: none;
}

/* Switches are the Spending popover/drawer sliders.  Keep the measured 80%
 * desktop dimensions; only the track/knob paint becomes translucent glass. */
body[data-surface="desktop"] :is(.reg-datepop, .reg-drawer) input.ios-switch {
  --switch-track-border: 1px;
  --switch-knob-border: 1px;
  border: 1px solid var(--chrome-edge);
  background: color-mix(in srgb, var(--card) 36%, transparent);
  -webkit-backdrop-filter: blur(8px) saturate(1.35);
  backdrop-filter: blur(8px) saturate(1.35);
  box-shadow: none;
}
body[data-surface="desktop"] :is(.reg-datepop, .reg-drawer) input.ios-switch:checked {
  background: color-mix(in srgb, var(--accent) 76%, transparent);
  border-color: color-mix(in srgb, var(--accent) 58%, var(--chrome-edge));
}
body[data-surface="desktop"] :is(.reg-datepop, .reg-drawer) input.ios-switch::after {
  box-sizing: border-box;
  background: color-mix(in srgb, var(--card) 82%, transparent);
  border: 1px solid var(--chrome-edge);
  box-shadow: none;
}

/* Floating Spending surfaces share the existing pop-in keyframe.  The
 * composable `translate` animation preserves every JS-measured anchor and the
 * rebound token gives the iOS-like settle at the end of the entrance. */
body[data-surface="desktop"] :is(.reg-datepop, .reg-view-menu, .reg-drop),
body[data-surface="desktop"]:has(.reg-grid) :is(
  .sub-flyout,
  .popover,
  .picker-modal:not(.calendar-modal)
) {
  background: var(--spend-glass-fill-strong);
  border: 1px solid var(--chrome-edge);
  border-radius: var(--reg-control-radius, var(--radius-s));
  -webkit-backdrop-filter: blur(18px) saturate(1.45);
  backdrop-filter: blur(18px) saturate(1.45);
  box-shadow:
    0 10px 28px var(--spend-glass-shadow),
    inset 0 1px 0 var(--spend-glass-highlight);
  animation: recurring-pop-in var(--dur-slow) var(--ease-ios-rebound) backwards;
}
body[data-surface="desktop"] .reg-view-menu {
  padding: 10px;
  overflow: hidden;
}
body[data-surface="desktop"] .ctx-menu {
  background: var(--spend-glass-fill-strong);
  border-color: var(--chrome-edge);
  -webkit-backdrop-filter: blur(18px) saturate(1.45);
  backdrop-filter: blur(18px) saturate(1.45);
  box-shadow:
    0 10px 28px var(--spend-glass-shadow),
    inset 0 1px 0 var(--spend-glass-highlight);
}
/* Plan is an anchored utility card on desktop. The generic contextual glass
 * treatment lets register content wash through the menu, so this surface owns
 * an opaque card face in both themes. */
body[data-surface="desktop"] .ctx-menu.plan-menu {
  background: var(--card);
  background-image: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
body[data-surface="desktop"]:has(.reg-grid) .payee-flyout {
  background: var(--spend-glass-fill-panel);
  border-color: var(--chrome-edge);
  -webkit-backdrop-filter: blur(18px) saturate(1.45);
  backdrop-filter: blur(18px) saturate(1.45);
  box-shadow:
    0 10px 28px var(--spend-glass-shadow),
    inset 0 1px 0 var(--spend-glass-highlight);
}
body[data-surface="desktop"]:has(.reg-grid) .picker-modal.reconcile-account-modal {
  /* Reconcile is a picker-modal and otherwise inherits the generic 68% glass
   * rule above; foreground panels need the stronger token in both themes. */
  background: var(--spend-glass-fill-panel);
  border-color: var(--chrome-edge);
  -webkit-backdrop-filter: blur(18px) saturate(1.35);
  backdrop-filter: blur(18px) saturate(1.35);
}
/* FX is an active foreground editing surface, not translucent chrome — the
 * generic Spending glass above (11605) read as grey/inactive on it. */
body[data-surface="desktop"]:has(.reg-grid) .picker-modal.fx-modal {
  background: var(--card);
  border: 1px solid var(--hairline);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  box-shadow: var(--shadow-elevated, 0 10px 28px rgba(0, 0, 0, .18));
}
/* Confirm Later is a readable decision card, not translucent Spending chrome. */
body[data-surface="desktop"]:has(.reg-grid) .picker-modal.confirm-later-picker {
  background: var(--card);
  border-color: var(--hairline);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  box-shadow: var(--shadow-elevated, 0 10px 28px color-mix(in srgb, var(--ink) 18%, transparent));
}
body[data-surface="desktop"] .ctx-menu.menu-in {
  transition:
    transform var(--dur-base) var(--ease-ios-rebound),
    opacity var(--dur-fast) var(--ease-standard);
}
body[data-surface="desktop"] .ctx-menu.menu-out {
  transition:
    transform var(--dur-base) var(--ease-ios-rebound),
    opacity var(--dur-fast) var(--ease-standard);
}
@keyframes spending-surface-out {
  0% { opacity: 1; scale: 1; }
  36% { opacity: 1; scale: 1.018; }
  100% { opacity: 0; scale: .94; }
}
body[data-surface="desktop"] .reg-surface-closing {
  pointer-events: none;
  animation: spending-surface-out var(--dur-base) var(--ease-ios-rebound) both !important;
}

/* The drawer is the register's own expanding glass surface.  Keep its
 * grid-template-rows contract (the measured open/close motion) and add only
 * paint/edge transitions; the inner and footer panes inherit the same lens. */
body[data-surface="desktop"] .reg-drawer {
  background: color-mix(in srgb, var(--accent) 8%, var(--spend-glass-fill-strong));
  border-top-color: var(--chrome-edge);
  -webkit-backdrop-filter: blur(18px) saturate(1.4);
  backdrop-filter: blur(18px) saturate(1.4);
  box-shadow:
    0 8px 24px var(--spend-glass-shadow),
    inset 0 1px 0 var(--spend-glass-highlight);
  transition:
    grid-template-rows var(--dur-slow, .34s) var(--ease-ios-rebound),
    background var(--dur-base) var(--ease-ios-rebound),
    box-shadow var(--dur-base) var(--ease-ios-rebound);
}
body[data-surface="desktop"] .reg-drawer-inner {
  background: color-mix(in srgb, var(--card) 24%, transparent);
  -webkit-backdrop-filter: blur(12px) saturate(1.3);
  backdrop-filter: blur(12px) saturate(1.3);
}
body[data-surface="desktop"] .reg-drawer :is(.sheet-dock, .sheet-footer.drawer-footer) {
  background: color-mix(in srgb, var(--card) 48%, transparent);
  border-color: var(--chrome-edge);
  -webkit-backdrop-filter: blur(12px) saturate(1.35);
  backdrop-filter: blur(12px) saturate(1.35);
  box-shadow: inset 0 1px 0 var(--spend-glass-highlight);
}
body[data-surface="desktop"] .reg-drawer .sheet-footer.drawer-footer {
  background: color-mix(in srgb, var(--scheduled-fill) 62%, transparent);
}
body[data-surface="desktop"] .reg-datepop .reg-recurrence-options {
  position: relative;
  background: color-mix(in srgb, var(--card) 34%, transparent);
  border-top-color: var(--chrome-edge);
  -webkit-backdrop-filter: blur(12px) saturate(1.35);
  backdrop-filter: blur(12px) saturate(1.35);
}
body[data-surface="desktop"] :is(.reg-datepop-repeat, .reg-datepop-repeat-list, .reg-datepop-custom) {
  background-color: color-mix(in srgb, var(--card) 22%, transparent);
}
body[data-surface="desktop"] .reg-datepop :is(.reg-datepop-repeat-list, .reg-datepop-custom) {
  border-top-color: var(--chrome-edge);
}

/* The inline row action rail gets vertical breathing room without changing its
 * density-derived action boxes.  It remains in flow and does not alter any
 * transaction row's grid tracks. */
body[data-surface="desktop"] .reg-editbar {
  min-height: 31px;
  padding: 6px 12px 7px;
  background: var(--spend-glass-fill);
  border-top-color: var(--chrome-edge);
  border-bottom: 1px solid var(--chrome-edge);
  -webkit-backdrop-filter: blur(12px) saturate(1.35);
  backdrop-filter: blur(12px) saturate(1.35);
  box-shadow: inset 0 1px 0 var(--spend-glass-highlight);
}
body[data-surface="desktop"] .reg-editbar :is(.ir-cancel, .ir-save) {
  margin-block: 1px;
  transition:
    transform var(--dur-fast) var(--ease-ios-rebound),
    filter var(--dur-fast) var(--ease-standard),
    background var(--dur-base) var(--ease-ios-rebound),
    box-shadow var(--dur-base) var(--ease-standard);
}
body[data-surface="desktop"] .reg-editbar :is(.ir-cancel, .ir-save):active {
  transform: scale(.95);
  filter: brightness(.93);
}

/* Give the date-popover footer its own inset, rather than letting the buttons
 * sit against the outer card edge when the panel grows with recurrence data. */
body[data-surface="desktop"] .reg-datepop-actions {
  margin: 8px 10px 14px;
  padding: 8px 10px 0;
  border-top-color: var(--chrome-edge);
}
body[data-surface="desktop"] .reg-datepop-actions :is(.rdp-cancel, .rdp-save) {
  transition:
    transform var(--dur-fast) var(--ease-ios-rebound),
    filter var(--dur-fast) var(--ease-standard),
    background var(--dur-base) var(--ease-ios-rebound),
    box-shadow var(--dur-base) var(--ease-standard);
  box-shadow: inset 0 1px 0 var(--spend-glass-highlight);
}
body[data-surface="desktop"] .reg-datepop-actions :is(.rdp-cancel, .rdp-save):active {
  transform: scale(.95);
  filter: brightness(.93);
}

/* The recurrence End control becomes a button that owns a shared calendar.
 * Support either DOM placement used by the editor: as a child of the labelled
 * row or as a direct child of the options panel.  Both placements are bounded
 * by the popover and retain the calendar's month/year zoom cells. */
body[data-surface="desktop"] .reg-recurrence-options .recurrence-end {
  position: relative;
}
body[data-surface="desktop"] .reg-recurrence-options .rec-end-date {
  appearance: none;
  border: 1px solid var(--chrome-edge);
  background: var(--spend-glass-fill);
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: text;
  -webkit-backdrop-filter: blur(8px) saturate(1.3);
  backdrop-filter: blur(8px) saturate(1.3);
  transition:
    transform var(--dur-fast) var(--ease-ios-rebound),
    filter var(--dur-fast) var(--ease-standard),
    background var(--dur-base) var(--ease-ios-rebound),
    border-color var(--dur-base) var(--ease-standard);
}
body[data-surface="desktop"] .reg-recurrence-options .rec-end-calendar-trigger {
  flex: 0 0 var(--reg-control-h);
  width: var(--reg-control-h);
  height: var(--reg-control-h);
  min-height: var(--reg-control-h);
  padding: 0;
  border-radius: var(--radius-s);
}
body[data-surface="desktop"] .reg-recurrence-options .rec-end-date:active {
  transform: scale(.96);
  filter: brightness(.93);
}
body[data-surface="desktop"] .reg-recurrence-options .recurrence-end > .rec-end-calendar {
  position: absolute;
  z-index: 4;
  /* Keep the static fallback within the End row; recurrence-options.js then
   * measures the trigger and clamps the live face to its clipping owner. */
  inset-inline-start: auto;
  inset-inline-end: 0;
  top: calc(100% + 6px);
  width: min(296px, calc(100vw - 32px));
  max-width: calc(100vw - 32px);
}
body[data-surface="desktop"] .reg-recurrence-options > .rec-end-calendar {
  position: absolute;
  z-index: 4;
  inset-inline-start: 8px;
  top: calc(100% + 6px);
  width: min(296px, calc(100% - 16px), calc(100vw - 32px));
  max-width: calc(100vw - 32px);
}
body[data-surface="desktop"] .reg-recurrence-options .recurrence-end > .rec-end-calendar,
body[data-surface="desktop"] .reg-recurrence-options > .rec-end-calendar {
  max-height: min(390px, calc(100dvh - 32px), var(--popout-room, 460px));
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 8px;
  gap: 6px;
  background: var(--spend-glass-fill-strong);
  border: 1px solid var(--chrome-edge);
  border-radius: var(--reg-control-radius, var(--radius-s));
  -webkit-backdrop-filter: blur(18px) saturate(1.45);
  backdrop-filter: blur(18px) saturate(1.45);
  box-shadow:
    0 10px 28px var(--spend-glass-shadow),
    inset 0 1px 0 var(--spend-glass-highlight);
  animation: recurring-pop-in var(--dur-base) var(--ease-ios-rebound) backwards;
}
body[data-surface="desktop"] .reg-recurrence-options .recurrence-end > .rec-end-calendar :is(.cal-cell, .cal-nav, .cal-zoom),
body[data-surface="desktop"] .reg-recurrence-options > .rec-end-calendar :is(.cal-cell, .cal-nav, .cal-zoom) {
  transition:
    transform var(--dur-fast) var(--ease-ios-rebound),
    background var(--dur-fast) var(--ease-standard),
    filter var(--dur-fast) var(--ease-standard);
}
body[data-surface="desktop"] .reg-recurrence-options .recurrence-end > .rec-end-calendar :is(.cal-cell, .cal-nav, .cal-zoom):active,
body[data-surface="desktop"] .reg-recurrence-options > .rec-end-calendar :is(.cal-cell, .cal-nav, .cal-zoom):active {
  transform: scale(.95);
  filter: brightness(.93);
}

/* The selection options bar is an opaque card surface. Its controls retain
 * their own feedback, while ledger rows never wash through the summary. */
body[data-surface="desktop"] .reg-optionsbar {
  background: var(--card);
  color: var(--ink);
  border: 1px solid var(--hairline);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  box-shadow: var(--shadow-elevated, 0 10px 28px color-mix(in srgb, var(--ink) 18%, transparent));
  transition:
    transform var(--dur-base) var(--ease-ios-rebound),
    opacity var(--dur-base) var(--ease-ios-rebound);
}
body[data-surface="desktop"] .reg-optionsbar :is(.rob-cancel, .rob-btn) {
  color: var(--ink);
  transition:
    transform var(--dur-fast) var(--ease-ios-rebound),
    filter var(--dur-fast) var(--ease-standard),
    background var(--dur-fast) var(--ease-standard),
    box-shadow var(--dur-fast) var(--ease-standard);
}
body[data-surface="desktop"] .reg-optionsbar :is(.rob-cancel, .rob-btn):active {
  transform: scale(.95);
  filter: brightness(.93);
}
body[data-surface="desktop"] .reg-optionsbar .rob-cancel:hover {
  background: color-mix(in srgb, var(--ink) 8%, transparent);
}
/* Mission 5 (v1.190.0): Categorise/Flag/More get a flat resting --accent wash
 * (not just on hover/open) so they read as the bar's actionable controls —
 * the same accent family Add Transaction uses — rather than a plain neutral
 * surface that only differentiates itself once pressed.
 *
 * D-102 desktop-CSS defect C follow-up (v1.196.0): Current-theme grammar,
 * but had no `html[data-desktop-theme="current"]` guard. Its three `:not()`
 * exclusions each add class-level specificity, so on plain specificity it
 * out-ranked the scoped `.reg-optionsbar :is(.rob-cancel, .rob-btn)` rule
 * (the secondary-control language) even while the route was active — this
 * alpha-overlay wash leaked in over the route's own solid `--btn-secondary-bg` fill,
 * so Flag/Categorise/More read as an ad-hoc translucent tint instead of the
 * same pre-mixed token the toolbar's own secondary buttons use. Explicit
 * `html[data-desktop-theme="current"]` scoping (the convention every other
 * Current-only rule in this file already uses) is the fix. */
html[data-desktop-theme="current"] body[data-surface="desktop"] .reg-optionsbar .rob-btn:not(:disabled):not(.on):not(.rob-approve) {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
html[data-desktop-theme="current"] body[data-surface="desktop"] .reg-optionsbar .rob-btn:not(:disabled):not(.on):not(.rob-approve):hover {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
}
html[data-desktop-theme="current"] body[data-surface="desktop"] .reg-optionsbar .rob-btn.on,
html[data-desktop-theme="current"] body[data-surface="desktop"] .reg-optionsbar .rob-approve {
  background: color-mix(in srgb, var(--accent) 26%, transparent);
  color: var(--accent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent);
}
html[data-desktop-theme="current"] body[data-surface="desktop"] .reg-optionsbar .rob-btn.on:hover,
html[data-desktop-theme="current"] body[data-surface="desktop"] .reg-optionsbar .rob-approve:not(:disabled):hover {
  background: color-mix(in srgb, var(--accent) 34%, transparent);
}
/* .rob-out/.rob-in paint --neg/--pos directly at the base rule (spec §7) —
 * this glass surface adds no override, so out/in read identically whether
 * the bar is resting on the legacy dark face or this tokenised glass. */
body[data-surface="desktop"] .reg-optionsbar :is(.rob-cancel:focus-visible, .rob-btn:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* Reduced-motion and reduced-transparency preferences are explicit for this
 * late desktop block because it intentionally overrides earlier shared rules. */
@media (prefers-reduced-motion: reduce) {
  body[data-surface="desktop"] :is(
    .reg-toolbar .btn,
    .reg-headtools .reg-icon-btn,
    .reg-headtools .reg-select-btn,
    .reg-headtools .reg-search-dismiss,
    .reg-editbar :is(.ir-cancel, .ir-save),
    .reg-datepop-actions :is(.rdp-cancel, .rdp-save),
    .reg-recurrence-options :is(.rec-end-date, .rec-end-calendar-trigger),
    .reg-optionsbar :is(.rob-cancel, .rob-btn),
    .reg-recurrence-options .rec-end-calendar :is(.cal-cell, .cal-nav, .cal-zoom)
  ) {
    transition: none;
  }
  body[data-surface="desktop"] :is(.reg-datepop, .reg-view-menu, .reg-drop),
  body[data-surface="desktop"]:has(.reg-grid) :is(
    .sub-flyout,
    .popover,
    .picker-modal:not(.calendar-modal)
  ),
  body[data-surface="desktop"] .reg-recurrence-options .rec-end-calendar {
    animation: none;
  }
  body[data-surface="desktop"] .reg-surface-closing {
    animation: none !important;
  }
  body[data-surface="desktop"] .ctx-menu.menu-in,
  body[data-surface="desktop"] .ctx-menu.menu-in.menu-open,
  body[data-surface="desktop"] .ctx-menu.menu-out {
    transform: none;
    transition: none;
  }
  body[data-surface="desktop"] .reg-optionsbar {
    transition: none;
  }
  body[data-surface="desktop"] .reg-drawer {
    transition: none;
  }
  body[data-surface="desktop"] :is(
    .reg-toolbar .btn,
    .reg-headtools .reg-icon-btn,
    .reg-headtools .reg-select-btn,
    .reg-headtools .reg-search-dismiss,
    .reg-editbar :is(.ir-cancel, .ir-save),
    .reg-datepop-actions :is(.rdp-cancel, .rdp-save),
    .reg-recurrence-options :is(.rec-end-date, .rec-end-calendar-trigger),
    .reg-optionsbar :is(.rob-cancel, .rob-btn),
    .reg-recurrence-options .rec-end-calendar :is(.cal-cell, .cal-nav, .cal-zoom)
  ):active {
    transform: none;
  }
}
@media (prefers-reduced-transparency: reduce) {
  body[data-surface="desktop"] :is(
    .reg-toolbar .btn,
    .reg-headtools .reg-icon-btn,
    .reg-headtools .reg-select-btn,
    .reg-headtools .reg-search-dismiss,
    .reg-grid-search,
    .reg-datepop,
    .reg-view-menu,
    .reg-drop,
    .ctx-menu,
    .payee-flyout,
    .reconcile-account-modal,
    .reg-drawer,
    .reg-drawer-inner,
    .reg-drawer :is(.sheet-dock, .sheet-footer.drawer-footer),
    .reg-datepop .reg-recurrence-options,
    .reg-editbar,
    .reg-recurrence-options :is(.rec-end-date, .rec-end-calendar-trigger),
    .reg-recurrence-options .rec-end-calendar,
    .reg-optionsbar
  ) {
    background: var(--card);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  body[data-surface="desktop"]:has(.reg-grid) :is(
    .sub-flyout,
    .popover,
    .picker-modal:not(.calendar-modal)
  ) {
    background: var(--card);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

/* --- Desktop Spending action geometry (UI-1) -----------------------------
 * The register row is the ruler for the entire pointer/keyboard Spending
 * surface.  Earlier local repairs gave the inline bar and drawer footer
 * separate action boxes while the toolbar followed a third value.  That made
 * equivalent Save/Cancel/Add actions look unrelated, and the date
 * popover's deliberate +10% field scale could leak into ordinary actions.
 *
 * Keep this contract below every legacy component rule so one density-aware
 * source wins without changing a non-Spending desktop page or any touch
 * surface.  `--spend-action-h` never inherits the popover's local
 * `--reg-control-h`; field/cell controls may remain 1.1x in that panel, while
 * ordinary text actions always stay on the active register row.
 */
body[data-surface="desktop"]:has(.reg-grid) {
  --spend-action-h: var(--reg-row-h);
  --spend-action-pad-x: clamp(5px, calc(var(--spend-action-h) * .3), 9px);
  --spend-action-radius: clamp(5px, calc(var(--spend-action-h) * .28), 9px);
  --spend-action-border-w: 1px;
  --spend-action-font: var(--fs-body);
  --spend-action-line: 1;
  --spend-action-gap: clamp(4px, calc(var(--spend-action-h) * .3), 8px);
}

/* Ordinary text actions: status is carried by fill/border/ink only.  Height,
 * type, radius and baseline are shared by every action family member.  The
 * block inset is calculated from the density-derived box and the live text
 * line, never from a component-specific pixel height. */
body[data-surface="desktop"]:has(.reg-grid) :is(
  .reg-toolbar .btn,
  .reg-toolbar .reg-select-btn,
  .reg-editbar > .btn,
  .reg-drawer .sheet-dock > .btn,
  .reg-drawer .sheet-footer.drawer-footer > .btn,
  .reg-datepop-actions > .btn,
  .reg-datepop-custom-actions > button,
  .reg-recurrence-options :is(.rec-vary, .rec-add-step, .rec-end-clear, .rec-remind-choices button),
  .custom-repeat .cr-done,
  .reg-optionsbar :is(.rob-cancel, .rob-btn),
  .payee-workspace .payee-action-tall,
  .payee-workspace .payees-footer > .btn,
  .payee-flyout .payee-action-tall,
  .payee-flyout .payees-footer > .btn,
  .account-form-modal .sheet-footer > .btn
) {
  box-sizing: border-box;
  height: var(--spend-action-h);
  min-height: var(--spend-action-h);
  max-height: var(--spend-action-h);
  padding-block: max(0px, calc((var(--spend-action-h) - 1em - (var(--spend-action-border-w) * 2)) * .5));
  padding-inline: var(--spend-action-pad-x);
  border-width: var(--spend-action-border-w);
  border-style: solid;
  border-radius: 999px;
  font-size: var(--spend-action-font);
  font-weight: 600;
  line-height: var(--spend-action-line);
  gap: var(--spend-action-gap);
  white-space: nowrap;
  align-items: center;
  justify-content: center;
}

/* Icon-only controls are a separate square family, but use the same row ruler
 * and squircle so their centres align with the text-action rail. */
body[data-surface="desktop"]:has(.reg-grid) :is(
  .reg-headtools .reg-icon-btn,
  .reg-headtools .reg-search-dismiss,
  .reg-drawer .sheet-x,
  .account-form-modal .picker-x,
  .reg-recurrence-options .rec-step-delete
) {
  box-sizing: border-box;
  width: var(--spend-action-h);
  min-width: var(--spend-action-h);
  max-width: var(--spend-action-h);
  height: var(--spend-action-h);
  min-height: var(--spend-action-h);
  max-height: var(--spend-action-h);
  padding: 0;
  border-width: var(--spend-action-border-w);
  border-style: solid;
  border-color: var(--chrome-edge);
  border-radius: var(--spend-action-radius);
  font-weight: 600;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
body[data-surface="desktop"]:has(.reg-grid) :is(
  .reg-headtools .reg-icon-btn,
  .reg-headtools .reg-search-dismiss,
  .reg-drawer .sheet-x,
  .reg-recurrence-options .rec-step-delete
) {
  font-size: var(--spend-action-font);
}
body[data-surface="desktop"]:has(.reg-grid) :is(
  .reg-headtools .reg-icon-btn,
  .reg-headtools .reg-search-dismiss,
  .reg-drawer .sheet-x,
  .account-form-modal .picker-x
) .ico {
  width: 1.15em;
  height: 1.15em;
}

/* Picker/menu rows and calendar cells keep their own row contracts.  This
 * explicit family marker prevents a future action rule from flattening them
 * into the ordinary button rail. */
body[data-surface="desktop"]:has(.reg-grid) :is(
  .reg-view-segments button,
  .reg-view-check,
  .reg-datepop-repeat-list button,
  .sub-flyout .pick-row,
  .reg-datepop .cal-cell,
  .reg-datepop .cal-nav,
  .reg-datepop .cal-cancel,
  .reg-datepop .cal-clear
) {
  /* no geometry override: these controls are measured as menu/calendar
   * families by the Spending audit below. */
}

/* --- R4 desktop toolbar parity (04-G) -------------------------------------
 * Plan filter pills are the literal authority for this desktop action family:
 * one token-sized lozenge, no elevation, and no glass edge.  The earlier
 * Spending glass contract deliberately used a 25px row ruler plus a drop
 * shadow, so Add/File Import/Undo/Redo and their sibling actions read as a
 * different component.  Scope the parity to the desktop register only;
 * touch hit floors and all non-toolbar drawers retain their existing grammar.
 *
 * D-102 desktop-CSS defect C follow-up (v1.196.0): this whole block is
 * Current-theme grammar (the pill-shaped toolbar), but had no
 * `html[data-desktop-theme="current"]` guard. Its `:not(.disc):not(.icon-btn)`
 * exclusions each add their own class-level specificity, so on plain
 * specificity it out-ranked the scoped `.reg-toolbar .btn` rules below
 * even while the route was active — the 999px pill/34px height leaked into
 * the flat/4px-radius toolbar. Explicit
 * `html[data-desktop-theme="current"]` scoping (the convention every other
 * Current-only rule in this file already uses) is the fix, not lowering this
 * rule's specificity, so this family stays authoritative for Current without
 * relying on source order against a theme it was never meant to touch.
 *
 * D-102 part D (21/08/2026): the selection options bar's OWN buttons
 * (`.reg-optionsbar :is(.rob-cancel, .rob-btn)`) were never added to this
 * list, so they fell through to the generic "Desktop Spending action
 * geometry (UI-1)" fallback (search --spend-action-h) instead of picking up
 * this Current-theme 34px/999px pill family the toolbar's OWN Add
 * Transaction button gets — the exact reason three prior releases
 * (v1.186.0, v1.190.0, v1.196.0) each restyled a DIFFERENT facet of the bar
 * (accent parity, colour, one-row wrap) while the actual undersized
 * mismatched-radius buttons underneath survived every one: none of those
 * passes had measured the button's own computed height/radius against the
 * toolbar's, only the bar's outer shell. Added here rather than as a
 * separate rule so both control families are provably ONE declaration, not
 * two that can drift again. */
html[data-desktop-theme="current"] body[data-surface="desktop"]:has(.reg-grid) :is(.reg-toolbar .btn:not(.disc):not(.icon-btn), .reg-optionsbar .rob-cancel, .reg-optionsbar .rob-btn) {
  box-sizing: border-box;
  height: var(--ctl-h-sm);
  min-height: var(--ctl-h-sm);
  max-height: var(--ctl-h-sm);
  padding: .3rem .9rem;
  border: 0;
  border-radius: 999px;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  line-height: 1;
  gap: .35rem;
}
/* D-102 part D (19/08/2026): File Import / Undo / Expand splits / View, etc.
 * are ENABLED secondary actions, not inert grey chips — same pale accent-
 * derived fill as the base .btn.secondary rule (app.css §"cards, rows,
 * buttons"), so this toolbar-specific override no longer flattens them back
 * to --chip. Disabled (Redo when nothing to redo) and .on/[aria-pressed]/
 * [aria-expanded] states are handled by the base rule's own overrides, which
 * this block does not shadow. */
html[data-desktop-theme="current"] body[data-surface="desktop"]:has(.reg-grid) .reg-toolbar .btn.secondary {
  background: var(--btn-secondary-bg);
  border-color: transparent;
  color: var(--btn-secondary-ink);
}
html[data-desktop-theme="current"] body[data-surface="desktop"]:has(.reg-grid) .reg-toolbar .btn.secondary:disabled,
html[data-desktop-theme="current"] body[data-surface="desktop"]:has(.reg-grid) .reg-toolbar .btn.secondary[aria-disabled="true"] {
  background: var(--chip);
  color: var(--ink-muted);
  opacity: .55;
}
html[data-desktop-theme="current"] body[data-surface="desktop"]:has(.reg-grid) .reg-toolbar .btn:not(.secondary) {
  background: var(--accent);
  border-color: transparent;
  color: var(--accent-ink);
}
html[data-desktop-theme="current"] body[data-surface="desktop"]:has(.reg-grid) .reg-toolbar .btn.on {
  background: color-mix(in srgb, var(--accent) 30%, var(--card));
  border-color: transparent;
  color: var(--accent);
}

/* The split lens is a Plan toolbar sibling, not a register child.  Give its
 * outer box the same height/radius/no-shadow family, then use the same inline
 * padding on both halves so each label receives equal visible side space. */
body[data-surface="desktop"] .plan-lens {
  min-height: var(--ctl-h-sm);
  border: 0;
  border-radius: 999px;
  box-shadow: none;
  color: var(--ink);
}
body[data-surface="desktop"] .plan-lens-fill { border-radius: 999px; }
body[data-surface="desktop"] .plan-lens-half {
  padding-inline: .9rem;
  line-height: 1;
  transform: none;
}

/* Current-touch HIG contract (phone/tablet only). V2 and desktop keep their
 * existing visual owners; these rules are double-scoped so a shared component
 * cannot accidentally inherit the Current treatment. */
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) {
  --touch-switch-track: 52px;
  --touch-switch-knob: 28px;
}

html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"])
  :is(.field-toggle-row, .setting-row:has(> input[type="checkbox"]), .sched-autoenter-toggle, .sched-remind-toggle, .sched-stepped-toggle) {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--touch-switch-track);
  align-items: center;
  column-gap: 12px;
  min-height: var(--ctl-h-md);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"])
  :is(.field-toggle-row, .setting-row:has(> input[type="checkbox"])) > :first-child {
  min-width: 0;
  overflow-wrap: anywhere;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"])
  :is(.field-toggle-row, .setting-row:has(> input[type="checkbox"])) > input.ios-switch,
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"])
  :is(.field-toggle-row, .setting-row:has(> input[type="checkbox"])) > input[type="checkbox"] {
  justify-self: end;
  min-width: var(--touch-switch-track);
}

/* Device-local Current readability modes. Default inherits the existing scale. */
html[data-touch-theme="current"][data-touch-text-size="large"] body:is([data-surface="phone"], [data-surface="tablet"]) {
  --fs-body: 1.125rem;
  --fs-label: 1rem;
  --fs-value: 1.125rem;
  --fs-h3: 1.25rem;
  --fs-h2: 1.45rem;
  --fs-h1: 1.75rem;
  --fs-display: 2.7rem;
}
html[data-touch-theme="current"][data-touch-text-size="extra-large"] body:is([data-surface="phone"], [data-surface="tablet"]) {
  --fs-body: 1.45rem;
  --fs-label: 1.25rem;
  --fs-value: 1.45rem;
  --fs-h3: 1.6rem;
  --fs-h2: 1.85rem;
  --fs-h1: 2.1rem;
  --fs-display: 3rem;
}
html[data-touch-theme="current"][data-touch-text-size="large"] body:is([data-surface="phone"], [data-surface="tablet"]),
html[data-touch-theme="current"][data-touch-text-size="extra-large"] body:is([data-surface="phone"], [data-surface="tablet"]) {
  overflow-x: hidden;
}
html[data-touch-theme="current"][data-touch-text-size="large"] body:is([data-surface="phone"], [data-surface="tablet"])
  :is(.screen-title, .field-label, .field-value, .muted, .setting-row, .tab-label, .sched-recur-sentence, .sched-autoenter-help) {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  overflow-wrap: anywhere;
  line-height: 1.25;
}
html[data-touch-theme="current"][data-touch-text-size="extra-large"] body:is([data-surface="phone"], [data-surface="tablet"])
  :is(.screen-title, .field-label, .field-value, .muted, .setting-row, .tab-label, .sched-recur-sentence, .sched-autoenter-help) {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  overflow-wrap: anywhere;
  line-height: 1.25;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"])
  :is(.field-row, .setting-row, .sheet-footer > .btn, .tab-item, .touch-text-size-select) {
  min-height: 44px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .field-main,
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .field-row > :first-child {
  min-width: 0;
}

/* The stepped editor's secondary action is a link-like control, not a pill. */
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .stepped-step-add.touch-link {
  min-height: 44px;
  align-self: flex-start;
  justify-self: start;
  padding: .5rem 0;
  background: transparent;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  color: var(--accent);
  text-align: left;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .stepped-step-add.touch-link:active {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}

/* Persistent recurrence actions and keyboard-safe sheet geometry. */
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet {
  display: flex;
  flex-direction: column;
  min-height: 0;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet > .sheet-scroll {
  min-height: 0;
  padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 16px);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet > .sheet-footer {
  flex: 0 0 auto;
  padding: 8px 0 max(10px, env(safe-area-inset-bottom, 0px));
  background: var(--canvas);
}
html[data-touch-theme="current"][data-vv-keyboard="true"] body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet {
  max-height: min(90dvh, var(--vvh, 100dvh));
}

/* A short explanation card is readable and selectable, while its controls keep
 * the normal touch target contract. */
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .geo-permission-explain {
  display: grid;
  gap: 8px;
  padding: 12px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-s);
  background: var(--card);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .geo-permission-actions {
  display: flex;
  gap: 8px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .geo-permission-actions > .btn {
  flex: 1 1 0;
  min-height: 44px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .chart-data-toggle {
  min-height: 44px;
  padding: 8px 0;
  gap: 6px;
  border: 0;
  border-radius: 0;
  background: transparent;
  font-weight: 650;
  color: var(--accent);
  text-align: left;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .chart-data-toggle:hover,
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .chart-data-toggle:focus-visible,
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .chart-data-toggle.is-open {
  background: transparent;
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.chart-data-caret {
  display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 1em; width: 1em; height: 1em; line-height: 1;
}
.chart-data-caret-icon { width: 1em; height: 1em; }
.chart-data-equivalent { overflow-x: auto; }
.chart-accessible { display: contents; }
.chart-data-table { width: 100%; border-collapse: collapse; font-size: var(--fs-label); }
.chart-data-table th, .chart-data-table td { padding: 6px 8px; border-bottom: 1px solid var(--hairline); text-align: right; }
.chart-data-table th:first-child, .chart-data-table td:first-child { text-align: left; }
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .home-reorder {
  width: 44px; min-width: 44px; height: 44px; min-height: 44px;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0; border: 0; background: transparent; color: var(--ink-muted); cursor: pointer;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .home-reorder .home-reorder-icon {
  display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .home-reorder .ico {
  width: 22px; height: 22px;
}
/* Current Spending keeps both labelled pills honest at the registered narrow
 * touch width. Reclaim only decorative gap/padding; preserve 44px icon targets
 * and the established readable label size in both themes. */
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-clearbar {
  gap: 4px; flex-wrap: wrap;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-clearbar-spacer { display: none; }
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-inline {
  order: 3; flex: 1 1 100%; max-width: none; min-width: 0;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-select-icon { order: 2; }
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) :is(.reg-uncleared-banner, .reg-scheduled-banner) {
  flex: 0 0 auto;
  gap: 4px;
  padding-inline: .5rem;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) :is(.reg-uncleared-banner > span, .reg-scheduled-banner > span) {
  flex: 0 0 auto;
  overflow: visible;
  text-overflow: clip;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-select-icon .ico { display: none; }
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-select-icon > span {
  margin-left: 0; font-size: var(--fs-micro); letter-spacing: .01em;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reflect-cardstack :is(.reflect-bars, .trailing-svg, .tf-chart, .bigpic-chart) {
  background: transparent;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  padding: 4px 0;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reflect-cardstack .trailing-controls {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 10px 12px;
  align-items: center;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reflect-cardstack .trailing-modes {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reflect-cardstack .trailing-pills {
  min-height: 44px;
  display: inline-flex; flex-wrap: wrap; max-width: 100%; min-width: 0;
  align-items: center;
  gap: 6px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reflect-cardstack .trailing-period-controls {
  display: flex; align-items: center; gap: 8px; min-width: 0; max-width: 100%; flex-wrap: wrap;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reflect-cardstack :is(.trailing-modelabel, .trailing-avglabel, .mode-label, .average-label, .chart-guide-combined-label)[data-guide-label] {
  font-size: calc(var(--fs-body) * 2);
  font-weight: 700;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reflect-cardstack .trailing-slider {
  grid-column: 1 / -1;
  min-width: 0;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reflect-cardstack .trailing-guides-toggle {
  min-height: 44px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .bigpic-stats {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
  align-items: start;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .bigpic-word {
  font-size: var(--fs-label);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .bigpic-val {
  max-width: 100%;
  font-size: var(--fs-body);
  line-height: 1.15;
  white-space: normal;
  overflow-wrap: anywhere;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .geo-permission-explain p {
  margin: 0;
}

/* Touch route chrome follows its title in normal flow. It must not remain
 * fixed/sticky while a long Home, Report or Plan surface scrolls. */
body:is([data-surface="phone"], [data-surface="tablet"]) .route-header-row {
  display: flex; align-items: center; justify-content: flex-start; gap: 8px;
  width: 100%; min-height: 48px; margin: 0 0 .25rem; padding-right: 0;
}
/* Tablet routes may own a centred 720px content column (Settings) while
 * dashboards/registers uncap in landscape. Keep the fixed toolbar rail on the
 * viewport edge in both cases so route changes cannot move sync/version/eye. */
body[data-surface="tablet"] .content > .route-header-row {
  width: 100vw; margin-left: calc(50% - 50vw); padding-inline: 16px; box-sizing: border-box;
}
body[data-surface="tablet"] .content > :is(.plan-route-title, .reg-route-head, .plan-header, .sched-titlerow) {
  width: 100vw; margin-left: calc(50% - 50vw); padding-inline: 16px; box-sizing: border-box;
}
/* The trailing cluster (route-specific extra controls, if any, immediately
 * followed by the fixed version/eye rail) is pushed to the right edge as
 * ONE adjacent group — not distributed via space-between — so
 * .route-header-actions sits in the same place whether or not a
 * .screen-title/.route-extra-actions precedes it (mission 20/08/2026 item
 * 6). Unconditional on .route-extra-actions and on .route-header-actions
 * itself (not sibling-gated) so a STANDALONE row with no title at all — a
 * route with its own internal layout, e.g. Accounts' iPad-landscape split
 * view — still right-aligns the fixed chrome correctly. */
body:is([data-surface="phone"], [data-surface="tablet"]) .route-header-row > .route-extra-actions,
body:is([data-surface="phone"], [data-surface="tablet"]) .route-header-row > .route-header-actions {
  margin-left: auto;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .route-header-row > .screen-title {
  flex: 1 1 auto; min-width: 0; min-height: 48px; margin: 0; padding-right: 0;
  position: static; top: auto; z-index: auto; overflow-wrap: normal; word-break: normal;
  text-transform: none;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .screen-title.accounts-route-title {
  text-transform: none;
}

/* One account's register presents as a PAGE, like Spending (Family B,
 * 31/08/2026). Three parts, all token-derived so light and dark stay paired:
 *
 * 1. The heading is a two-line stack — the H1 says "Spending" (what kind of
 *    screen this is) and the account it is scoped to sits directly beneath in
 *    blurple. The stack is a child of the header row, so it re-applies the
 *    row's own 152px chrome reserve that `> .screen-title` would otherwise
 *    have granted the H1 directly; without it the title runs under the fixed
 *    version/eye cluster.
 * 2. The Back control is a full labelled pill ABOVE the heading rather than a
 *    bare grey chevron beside it — this page is no longer a swipe-dismissable
 *    card, so its only way out has to be unmissable. It uses the enabled
 *    secondary family (pale accent fill, accent ink), the same face the
 *    filter/lens controls now wear.
 * 3. Nothing here is scoped to a sheet: the route renders as ordinary content,
 *    full height, with the shared Add Transaction FAB. */
body:is([data-surface="phone"], [data-surface="tablet"]) .account-page-back {
  display: inline-flex; align-items: center; gap: .25rem;
  min-height: var(--ctl-h-md); margin: 4px 0 10px; padding: .35rem .95rem .35rem .6rem;
  border: 1px solid color-mix(in srgb, var(--accent) 26%, transparent);
  border-radius: 999px;
  background: var(--btn-secondary-bg); color: var(--btn-secondary-ink);
  font: inherit; font-size: var(--fs-body); font-weight: 700; cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-spring),
              background var(--dur-fast) var(--ease-standard);
}
/* The control's OWN look must change on tap, not merely navigate. */
body:is([data-surface="phone"], [data-surface="tablet"]) .account-page-back:active {
  transform: scale(.97); background: var(--btn-secondary-bg-hover);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .account-page-back .ico {
  width: 1.1rem; height: 1.1rem; flex: 0 0 auto;
}
/* The heading owns the FULL content width. The version/eye rail is a flex
 * SIBLING in this row, so it already reserves its own width; the extra 152px
 * pad reserved that space a SECOND time and squeezed the name into a third of
 * the screen — "Savings Nala" broke mid-word into "Savin / gs / Nala"
 * (operator 31/08/2026, measured: 87.81px of text column on a 402px screen).
 * The rail only occupies the first line, which the H1 shares with it, so the
 * name on the second line needs no reserve at all. */
body:is([data-surface="phone"], [data-surface="tablet"]) .account-page-heading {
  flex: 1 1 auto; min-width: 0; padding-right: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .account-page-heading > .screen-title {
  min-height: 0; margin: 0; padding-right: 0; text-transform: none;
}
/* The account name: blurple, directly under the title, and allowed to wrap
 * rather than be clipped — Family B's account names run long
 * ("Mount Sinai Reconstruction"). Sized just under the 2rem title (operator
 * 31/08/2026: "only slightly smaller than the title", was half of it at
 * --fs-value/16px) and given the whole row width so a two-word name never
 * breaks mid-word. `overflow-wrap: anywhere` stays as the last-resort guard
 * for a single unbreakable long word; `balance` keeps a genuine wrap even. */
/* The gap above and below the name must READ equal (operator 31/08/2026). It
 * did not, because the two gaps are made of different things: above it is the
 * grid row-gap PLUS the title's half-leading (a 32px title in a 46.4px line box
 * leaves 4.39px of empty space under its ink), while below it is only the row's
 * own margin. Measured 14.39px above against 10.69px below. Trimming the title's
 * line box to its ink and stating one gap on each side makes both sides the same
 * measured distance from the name's ink. */
body:is([data-surface="phone"], [data-surface="tablet"]) .account-page-account {
  margin: 0; color: var(--accent);
  font-size: 1.65rem; font-weight: 700; line-height: 1.2;
  letter-spacing: -0.02em;
  overflow-wrap: anywhere; text-wrap: balance;
}
/* The rail occupies the FIRST line only, so the name beneath it should use the
 * row's whole width. A two-column grid states that directly: the H1 and the
 * rail share row 1 (rail in column 2, sized to itself), and the heading's
 * second line spans both columns. No negative margins, so no box overlaps the
 * rail's tap target — an earlier margin-based attempt overlapped it by 122px,
 * which the device harness caught. */
/* One token drives BOTH sides of the account name, so they cannot drift apart
 * again: `row-gap` is the space above it and the row's `margin-bottom` is the
 * space below it. */
body:is([data-surface="phone"], [data-surface="tablet"]) .account-page-title-row {
  --account-name-gap: 10px;
  /* Box gaps are not ink gaps: the title's ink overrun its trimmed line box by
   * 0.81px while the name's ink stops 0.69px above its box floor, so equal box
   * gaps still measured 9.19px above against 10.69px below. This 1.5px offset
   * is that difference, applied above, so the INK gaps match — which is what
   * the eye actually reads. */
  --account-name-gap-top: calc(var(--account-name-gap) + 1.5px);
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: start; column-gap: 8px;
  row-gap: var(--account-name-gap-top);
  margin-bottom: var(--account-name-gap);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .account-page-title-row > .account-page-heading {
  display: contents;
}
/* Row 1 also carries the 44px version/eye rail, so it is 44px tall while the
 * title's own box is only ~35px — that 8.81px of leftover fell entirely BELOW
 * the title and padded the gap above the name (measured 18px above against
 * 10.69px below). Stretching the title to fill its row and bottom-aligning its
 * ink puts the title's text on the row's floor, so the row-gap alone separates
 * it from the name and both sides of the name measure the same. ONE rule for
 * this selector: a second block for the same selector reads as an empty first
 * match to the stylesheet's own declaration guards. */
body:is([data-surface="phone"], [data-surface="tablet"]) .account-page-title-row > .account-page-heading > .screen-title {
  grid-column: 1; grid-row: 1;
  min-height: 0; margin-bottom: 0; align-self: stretch;
  display: flex; align-items: flex-end; line-height: 1.1;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .account-page-title-row > :is(.route-extra-actions, .route-header-actions) {
  grid-column: 2; grid-row: 1; margin-left: 0;
}
/* The name spans both columns on row 2 — the full screen width. */
body:is([data-surface="phone"], [data-surface="tablet"]) .account-page-title-row > .account-page-heading > .account-page-account {
  grid-column: 1 / -1; grid-row: 2;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .spending-title-row .reg-accounts-trigger,
body:is([data-surface="phone"], [data-surface="tablet"]) .accounts-title-row .accounts-back {
  flex: 0 0 auto; width: 34px; min-width: 34px; height: 34px; min-height: 34px;
  padding: 0; border: 0; border-radius: var(--radius-s); background: transparent;
  color: var(--ink-muted); display: inline-flex; align-items: center; justify-content: center;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .spending-title-row .reg-accounts-trigger .ico {
  width: 20px; height: 20px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .spending-title-row .reg-accounts-label {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .accounts-title-row .accounts-back {
  width: auto; min-width: 34px; padding-inline: 7px; gap: 4px; font-size: var(--fs-label);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .accounts-title-row .accounts-back > span {
  white-space: nowrap;
}
body:is([data-surface="phone"], [data-surface="tablet"]) :is(.screen-title-row, .sched-titlerow) {
  padding-right: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .route-header-row > :is(
  .privacy-eye, .app-version-tag, .home-reorder
) {
  position: static; inset: auto; z-index: auto; flex: 0 0 auto;
}
/* Current generic routes use the shell-created action wrapper. Keep the
 * wrapper as the single right rail so the title column does not compete with
 * sync/version/privacy when sync appears after its grace period. */
body:is([data-surface="phone"], [data-surface="tablet"]) .route-header-actions {
  display: inline-flex; align-items: center; justify-content: flex-end;
  gap: 8px; min-width: 0; flex: 0 0 auto; min-height: 44px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .route-header-actions > :is(
  .privacy-eye, .app-version-tag, .sync-strip, .home-reorder
) {
  position: static; inset: auto; z-index: auto; flex: 0 0 auto;
}
/* The shared child rule above normalises all controls to their own sizing
 * rules; reassert the passive sync footprint for the one non-control child. */
body:is([data-surface="phone"], [data-surface="tablet"]) .route-header-actions > .sync-strip {
  flex: 0 0 16px; width: 16px; min-width: 16px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) :is(
  .privacy-eye, .app-version-tag
) {
  position: static; inset: auto; z-index: auto;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .app-version-tag {
  height: 44px; min-height: 44px; padding-inline: 4px;
}
/* Stable cross-tab chrome (mission 25/08/2026 item 4): route-specific controls
 * (Plan's list toolbar, Home's reorder button) sit in this SEPARATE slot,
 * always immediately BEFORE .route-header-actions and never inside it — so
 * the fixed version/eye rail never grows or reorders per route. Empty
 * when a route has no extra control (removed from the DOM entirely by
 * shell.js in that case, not just hidden, so it never reserves layout width). */
body:is([data-surface="phone"], [data-surface="tablet"]) .route-extra-actions {
  display: inline-flex; align-items: center; gap: 8px; flex: 0 0 auto; min-height: 44px;
}
/* The Plan toolbar node itself is left in the DOM (mobile.js owns its
 * lifecycle) but empty once its children move into .route-extra-actions —
 * collapse it so it reserves no width/gap in the title row. */
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-touch-toolbar:empty {
  display: none;
}
.field-toggle-leading {
  display: flex; align-items: center; gap: 10px; min-width: 0; overflow-wrap: anywhere;
}
.field-toggle-leading .field-main { min-width: 0; }
.field-toggle-leading .field-value {
  overflow: visible; text-overflow: clip; white-space: normal;
}
/* Scheduled auto-approval card: support the protected lane's icon + text +
 * switch markup, while accepting the normalised two-child wrapper when it is
 * integrated. */
.txn-autoenter-toggle {
  display: grid; grid-template-columns: auto minmax(0, 1fr) var(--touch-switch-track, 52px);
  align-items: center; column-gap: 10px; min-height: var(--ctl-h-md);
}
.txn-autoenter-toggle > .field-toggle-leading {
  display: flex; align-items: center; gap: 10px; min-width: 0;
}
.txn-autoenter-toggle .txn-autoenter-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; border-radius: var(--radius-s);
  background: color-mix(in srgb, var(--accent) 14%, transparent); color: var(--accent);
}
.txn-autoenter-toggle > .field-toggle-leading .field-main { min-width: 0; }
.txn-autoenter-toggle > .field-toggle-leading .muted { white-space: normal; overflow-wrap: anywhere; }
.txn-autoenter-toggle:has(> .field-toggle-leading) { grid-template-columns: minmax(0, 1fr) var(--touch-switch-track, 52px); }
.txn-autoenter-toggle:has(> .field-toggle-leading) > .field-toggle-leading { min-width: 0; }
.home-reorder-controls { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin: 0 0 10px; }
.home-reorder-controls[hidden] { display: none; }
.home-reorder-controls .home-reorder-help { flex: 1 1 100%; margin: 0; }
.home-reorder-controls .home-reorder-done[hidden],
.home-reorder-controls .home-reorder-help[hidden],
.home-reorder-controls .home-reorder[hidden] { display: none; }
.home-tile-reorder-tools {
  display: flex; align-items: center; justify-content: flex-end; gap: 8px;
  margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--hairline);
}
.home-tile-reorder-tools[hidden] { display: none; }
.home-tile-reorder-tools > button {
  flex: 0 0 auto; min-width: 44px; min-height: 44px;
  display: inline-flex; align-items: center; justify-content: center;
}
.home-tile-reorder-tools .home-tile-drag-handle {
  touch-action: none; cursor: grab; color: var(--ink-muted);
}
.home-tile-reorder-tools .home-tile-drag-handle:active { cursor: grabbing; }
.home-reorder-status {
  pointer-events: none;
}

/* --------------------------------------------------------------------------
 * Current touch Spending selection reference (R1–R4, 16/08/2026).
 *
 * The ordinary register toolbar stays compact and three-control: Scheduled,
 * search and Select. Selection replaces that chrome with a centred title,
 * persistent context, full-row checks and a floating action dock. Everything
 * below is tokened so the reference geometry survives both light and dark
 * themes without inventing a second colour system.
 * ------------------------------------------------------------------------ */
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-mode {
  position: relative;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-mode > .route-header-row {
  position: relative;
  justify-content: center;
  min-height: 48px;
  margin-bottom: 4px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-mode > .route-header-row > .screen-title {
  flex: 1 1 auto;
  min-width: 0;
  padding-right: 0 !important;
  text-align: center;
}
/* Current SELECT keeps Spending independently centred while Cancel owns the
 * right edge. The title is a full-width flex row, so adding the action cannot
 * move its centre as the selection state changes. */
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-mode > .reg-selection-header > .reg-selection-title {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  padding-right: 0 !important;
  text-align: center;
  justify-content: center;
  margin: 0;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-mode .route-header-actions {
  display: none !important;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-mode > .route-header-row > :is(.app-version-tag, .privacy-eye, .settings-gear, .sync-strip, .home-reorder) {
  display: none !important;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-mode > .reg-selection-header > .reg-selection-close {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  right: 12px;
  z-index: 2;
  width: auto;
  height: 44px;
  min-width: 68px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 16px;
  border: 1px solid color-mix(in srgb, var(--accent) 46%, var(--hairline));
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 12%, var(--card));
  color: var(--accent);
  font: inherit;
  font-weight: 750;
  box-shadow: 0 4px 16px color-mix(in srgb, var(--ink) 12%, transparent);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-selection-context {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 10px;
  min-height: 44px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-touch-toolbar {
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr) 44px;
  align-items: center;
  gap: 8px;
  width: 100%;
  margin: 0 0 10px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-touch-toolbar.search-active {
  /* Keep the Select affordance on the same rail, but place × in the middle
   * track so it sits directly beside the search field rather than wrapping to
   * a second row. */
  grid-template-columns: minmax(0, 1fr) 44px 44px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-touch-toolbar.search-active .reg-search-inline {
  grid-column: 1;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-dismiss-inline {
  grid-column: 2;
  grid-row: 1;
  width: 44px;
  min-width: 44px;
  height: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid var(--hairline);
  border-radius: 50%;
  background: var(--card);
  color: var(--ink-muted);
  cursor: pointer;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-touch-toolbar.search-active .reg-select-icon {
  grid-column: 3;
  grid-row: 1;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-dismiss-inline:active {
  border-color: color-mix(in srgb, var(--accent) 46%, var(--hairline));
  background: color-mix(in srgb, var(--accent) 14%, var(--card));
  color: var(--accent);
  transform: scale(.96);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-dismiss-inline .ico {
  width: 20px;
  height: 20px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-touch-toolbar .reg-calendar-icon,
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-touch-toolbar .reg-select-icon {
  width: 44px;
  min-width: 44px;
  height: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid var(--hairline);
  border-radius: 50%;
  background: var(--card);
  color: var(--accent);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-touch-toolbar :is(.reg-calendar-icon, .reg-select-icon).on,
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-touch-toolbar :is(.reg-calendar-icon, .reg-select-icon):active {
  border-color: color-mix(in srgb, var(--accent) 46%, var(--hairline));
  background: color-mix(in srgb, var(--accent) 14%, var(--card));
  color: var(--accent);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-touch-toolbar .reg-calendar-icon.on {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--accent-ink);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-touch-toolbar :is(.reg-calendar-icon, .reg-select-icon) .ico {
  width: 22px;
  height: 22px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-touch-toolbar .reg-search-inline {
  order: initial;
  grid-column: 2;
  width: 100%;
  max-width: none;
  min-width: 0;
  height: 44px;
  min-height: 44px;
  margin: 0;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-touch-toolbar .reg-search-inline-clear {
  width: 22px;
  min-width: 22px;
  height: 22px;
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-touch-toolbar .reg-search-inline-clear::after {
  inset: -11px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-touch-toolbar .reg-search-inline-clear .ico {
  width: 12px;
  height: 12px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-inline.token-active {
  gap: 6px;
  padding-inline: 6px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-token-chip {
  min-width: 0;
  max-width: calc(100% - 34px);
  min-height: 30px;
  display: inline-flex;
  align-items: center;
  padding: 0 12px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: var(--fs-label);
  font-weight: 750;
  line-height: 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-inline.token-active > .reg-search-inline-icon {
  display: none;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-inline.token-active input {
  position: absolute;
  width: 1px;
  min-width: 1px;
  height: 1px;
  padding: 0;
  opacity: 0;
  pointer-events: none;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-suggestions {
  display: grid;
  gap: 2px;
  max-height: min(58dvh, 520px);
  margin: 0 0 12px;
  padding: 4px 0 8px;
  overflow-y: auto;
  overscroll-behavior: contain;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-suggestions[hidden] {
  display: none;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-suggestions h2 {
  margin: 0 4px;
  color: var(--ink-muted);
  font-size: var(--fs-micro);
  font-weight: 750;
  letter-spacing: .04em;
  text-transform: uppercase;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-suggestion-list {
  display: grid;
  gap: 0;
  padding: 0;
  border: 0;
  background: transparent;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-suggestion {
  position: relative;
  width: 100%;
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 8px;
  padding: 6px 4px;
  border: 0;
  border-radius: var(--radius-s);
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-body);
  text-align: left;
  cursor: pointer;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-suggestion + .reg-search-suggestion::before {
  content: "";
  position: absolute;
  left: 32px;
  right: 0;
  top: 0;
  height: 1px;
  background: var(--hairline);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-suggestion-icon {
  width: 20px;
  height: 20px;
  display: inline-grid;
  place-items: center;
  color: var(--ink);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-suggestion-icon .ico {
  width: 20px;
  height: 20px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-suggestion:hover,
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-suggestion:focus-visible,
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-suggestion:active {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--accent);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-inline:focus-within {
  border-color: var(--accent);
  outline: 0;
  box-shadow: none;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-touch-toolbar .reg-search-inline {
  background: var(--card);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-touch-toolbar .reg-search-inline input:focus,
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-touch-toolbar .reg-search-inline input:focus-visible {
  outline: none;
  box-shadow: none;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-touch-toolbar .reg-select-icon > span {
  display: none;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-touch-toolbar .reg-select-icon {
  order: initial;
  grid-column: 3;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-touch-toolbar .reg-select-icon .ico {
  display: inline-flex;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]):has(.selection-mode) .fab-txn,
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]):has(.selection-mode) .tab-bar {
  display: none !important;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-selbar-mobile {
  position: fixed;
  left: 12px;
  right: 12px;
  bottom: calc(12px + env(safe-area-inset-bottom, 0px));
  z-index: 40;
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 64px;
  margin: 0;
  padding: 8px 10px;
  border: 1px solid var(--hairline);
  border-radius: 22px;
  background: color-mix(in srgb, var(--card) 90%, transparent);
  -webkit-backdrop-filter: blur(18px) saturate(1.08);
  backdrop-filter: blur(18px) saturate(1.08);
  box-shadow: 0 10px 28px color-mix(in srgb, var(--ink) 18%, transparent);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]):has(.reg-selbar-mobile) .content {
  scroll-padding-bottom: 154px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-selbar-mobile .rsm-count {
  flex: 1 1 auto;
  min-width: 0;
  line-height: 1.12;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-selbar-mobile .rsm-sum {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--fs-body);
  font-weight: 800;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-selbar-mobile .rsm-n {
  display: block;
  overflow: visible;
  text-overflow: clip;
  white-space: nowrap;
  font-size: calc(var(--fs-micro) * .86);
  font-weight: 650;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-selbar-mobile .rsm-actions {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  flex: 0 0 auto;
  padding: 0;
  font-size: var(--fs-micro);
  animation: none;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-selbar-mobile .rsm-action {
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 4px 7px;
  border: 0;
  border-radius: 14px;
  background: transparent;
  color: var(--accent);
  font: inherit;
  font-weight: 750;
  white-space: nowrap;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-selbar-mobile .rsm-action:disabled {
  opacity: .38;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-selbar-mobile .rsm-action:not(:disabled):active {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  transform: scale(.96);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-selbar-mobile .rsm-cleared-disc {
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 2px solid currentColor;
  border-radius: 50%;
  font-size: var(--fs-micro);
  line-height: 1;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-selbar-mobile .rsm-cleared[aria-pressed="true"] {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-selbar-mobile .rsm-more .ico {
  width: 20px;
  height: 20px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu {
  min-width: 252px;
  max-width: calc(100vw - 24px);
  padding: 8px;
  border-radius: 20px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu .ctx-row {
  min-height: 52px;
  align-items: center;
  justify-content: flex-start;
  gap: 12px;
  padding: 8px 10px;
  border-radius: 14px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu .ctx-icon {
  width: 24px;
  height: 24px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 24px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu .ctx-icon .ico {
  width: 22px;
  height: 22px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu .ctx-label {
  flex: 1 1 auto;
  min-width: 0;
  text-align: left;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu .ctx-key {
  display: none;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu .ctx-trailing {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu .ctx-trailing .ico {
  width: 18px;
  height: 18px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu .ctx-row.danger {
  color: var(--neg);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu .ctx-row.disabled {
  color: var(--ink-muted);
  opacity: .5;
}
.pick-sep {
  height: 1px;
  margin: 5px 0;
  background: var(--hairline);
}
.touch-flag-head-icon,
.touch-flag-head-caret {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  color: var(--ink);
}
.touch-flag-head-icon .ico { width: 22px; height: 22px; }
.touch-flag-head-caret .ico { width: 18px; height: 18px; }
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .picker-modal:has(.pick-sep) {
  border-radius: 22px;
}
@media (max-width: 430px) {
  html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-selbar-mobile {
    left: 8px;
    right: 8px;
    gap: 4px;
    padding-inline: 7px;
  }
  html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-selbar-mobile .rsm-action {
    padding-inline: 4px;
    font-size: calc(var(--fs-micro) * .95);
  }
}
@media (prefers-reduced-motion: reduce) {
  html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-mode :is(.sel-circle, .reg-selection-header, .reg-selection-close, .reg-reference-review, .rsm-action) {
    transition: none !important;
    animation: none !important;
  }
}

/* R1 corrective pass: the reference keeps the approval, uncleared and
 * Scheduled surfaces as full-width disclosures above the transaction list. */
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-reference-context {
  display: flex;
  flex-direction: column;
  flex-wrap: nowrap;
  gap: 10px;
  margin: 0 0 12px;
}
/* The gap above Scheduled is the reference measure. Remove the extra context
 * and card-top padding below it so the lower gap matches without touching the
 * Scheduled control's own height or its transaction rows. */
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-mode .reg-reference-context {
  margin-bottom: 0;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-mode .txn-list {
  padding-top: 0;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]):has(.selection-mode) .approve-topbar,
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]):has(.selection-mode) .banner-approve {
  /* Keep the established top spacing while removing the ordinary approval CTA
   * from the selection workflow; the reference supplies its own compact row. */
  visibility: hidden !important;
  pointer-events: none !important;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-reference-review {
  width: 100%;
  box-sizing: border-box;
  min-height: 64px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 10px 18px;
  border: 1px solid color-mix(in srgb, var(--hairline) 62%, transparent);
  border-radius: 36px;
  background: var(--card);
  box-shadow: 0 6px 18px color-mix(in srgb, var(--ink) 8%, transparent);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-reference-review-label {
  min-width: 0;
  font-size: var(--fs-body);
  font-weight: 650;
  white-space: nowrap;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-reference-review-label b {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 32px;
  min-height: 32px;
  margin-right: 8px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--accent) 18%, var(--card));
  color: var(--ink);
  font-size: var(--fs-body);
  font-weight: 800;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-reference-review-btn {
  min-width: 116px;
  min-height: 44px;
  padding: 0 24px;
  border: 0;
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 16%, var(--card));
  color: var(--accent);
  font: inherit;
  font-weight: 750;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-reference-uncleared {
  width: 100%;
  min-height: 64px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 10px 18px;
  border: 1px solid color-mix(in srgb, var(--hairline) 52%, transparent);
  border-radius: 34px;
  background: color-mix(in srgb, var(--surface-2, var(--card)) 90%, var(--ink) 10%);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-body);
  font-weight: 600;
  text-align: left;
  text-transform: none !important;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-reference-uncleared.on {
  border-color: color-mix(in srgb, var(--accent) 42%, var(--hairline));
  background: color-mix(in srgb, var(--accent) 16%, var(--card));
}

/* Step 01-R1: the transaction detail in the supplied uncleared-clearing
 * recording is a narrower, native-style card stack. These rules are scoped to
 * the opt-in sheet class so ordinary add/edit forms do not inherit the
 * reference's field order, neutral amount face or compact save dock. */
.txn-sheet-clearing {
  background: var(--canvas);
  animation: txn-sheet-clearing-in .22s var(--ease-standard) both;
}
@keyframes txn-sheet-clearing-in {
  from { transform: translateY(100%); }
  to { transform: translateY(0); }
}
/* The supplied phone capture leaves the status-bar/scrim band visible above
 * the detail sheet.  The shared editor's 96dvh floor reaches that band, so
 * this workflow uses the measured ~93dvh reference height only. */
body[data-surface="phone"] .txn-sheet.txn-sheet-clearing {
  height: 93dvh;
  max-height: 93dvh;
}
.txn-sheet-clearing .sheet-x {
  background: var(--card) !important;
  color: var(--ink) !important;
  box-shadow: none !important;
  border: 0 !important;
}
.txn-sheet-clearing .amount-display.spending {
  color: var(--ink) !important;
}
.txn-sheet-clearing .type-pill {
  background: var(--card);
  color: var(--ink);
  box-shadow: none;
}
.txn-sheet-clearing .sheet-scroll {
  gap: 10px;
}
/* At the reference's full-field scroll checkpoint, the bottom gesture leaves
 * a measured safe-area tail beneath Show less.  Reserving that tail keeps the
 * Account/Date card just under the sticky title instead of leaving a blank
 * header-sized void above it. */
.txn-sheet-clearing .sheet-scroll:has(.sheet-adv) {
  padding-bottom: 116px;
}
.txn-sheet-clearing .sheet-fields.card:not(.sheet-adv) + .sheet-adv {
  margin-top: 42px;
}
.txn-sheet-clearing .sheet-adv .field-row {
  padding-block: 1rem;
}
.txn-sheet-clearing .sheet-adv .field-toggle-row {
  padding-block: 1rem;
}
.txn-sheet-clearing .sheet-fields.card {
  padding: 4px 16px;
}
.txn-sheet-clearing .sheet-showmore {
  text-transform: none !important;
}
.txn-sheet-clearing .sheet-del-full {
  min-height: var(--ctl-h-md);
  padding: .7rem 1rem;
  border-radius: 999px;
  background: var(--neg-pill);
  color: var(--neg);
  text-transform: none !important;
}
.txn-sheet-clearing .field-toggle-row .ico-cleared {
  color: var(--pos);
}
.txn-sheet-clearing input.ios-switch:checked {
  background: var(--clearing-switch-on-bg);
}
.txn-sheet-clearing .sheet-dock {
  flex-direction: row;
  align-items: center;
  justify-content: flex-end;
  padding: 4px 4px calc(8px + env(safe-area-inset-bottom));
}
.txn-sheet-clearing .sheet-dock .btn-save,
.txn-sheet-clearing .sheet-dock .btn-next {
  flex: 0 0 auto;
  width: auto;
  min-width: 7.5rem;
  border-radius: 999px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-reference-uncleared b {
  font-weight: 850;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-reference-row-caret {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  color: var(--ink-muted);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-reference-row-caret .ico {
  width: 22px;
  height: 22px;
}
/* The reference Flag control is a lower-right floating picker, not the old
 * centred touch modal. It expands upward from the overflow origin, keeps the
 * ledger visible, and uses flag-shaped swatches rather than round dots. */
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-flag-menu {
  width: 252px;
  min-width: 252px;
  max-width: calc(100vw - 24px);
  max-height: min(calc(100dvh - 24px), 620px);
  padding: 8px;
  border-radius: 20px;
  overflow: hidden;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-flag-menu .picker-flyout-head {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 40px;
  padding: 4px 8px 8px;
  color: var(--ink);
  font-size: var(--fs-body);
  font-weight: 750;
  letter-spacing: 0;
  text-transform: none;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-flag-menu .picker-flyout-icon,
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-flag-menu .picker-flyout-caret {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-flag-menu .picker-flyout-icon .ico {
  width: 22px;
  height: 22px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-flag-menu .picker-flyout-caret {
  margin-left: auto;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-flag-menu .picker-flyout-caret .ico {
  width: 18px;
  height: 18px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-flag-menu .pick-row {
  width: 100%;
  min-height: 50px;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 10px;
  border-top: 0;
  border-radius: 12px;
  font-size: var(--fs-body);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-flag-menu .pick-swatch {
  width: 30px;
  height: 22px;
  flex: 0 0 30px;
  border-radius: 0;
  background: var(--item-color);
  clip-path: polygon(0 0, 100% 0, 82% 50%, 100% 100%, 0 100%);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-flag-menu .pick-sep {
  height: 1px;
  margin: 4px 8px;
  background: var(--hairline);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-flag-menu .picker-list {
  max-height: none;
  overflow: visible;
}

/* Actual enter/exit motion for the Current selection shell. Keeping the shell
 * mounted during exit lets the same geometry be measured at first, middle and
 * settled frames instead of reporting an instant DOM swap. */
@keyframes reference-selection-enter {
  from { opacity: 0; transform: translateY(14px) scale(.985); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes reference-selection-exit {
  from { opacity: 1; transform: translateY(0) scale(1); }
  to { opacity: 0; transform: translateY(12px) scale(.985); }
}
@keyframes reference-review-enter {
  from { opacity: 0; transform: translateY(8px) scale(.985); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-mode > :is(.reg-selection-header, .reg-reference-context, .reg-touch-toolbar, .reg-selbar-mobile) {
  animation: reference-selection-enter .42s var(--ease-ios-rebound) both;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-mode.selection-exit > :is(.reg-selection-header, .reg-reference-context, .reg-touch-toolbar, .reg-selbar-mobile) {
  animation: reference-selection-exit .28s var(--ease-standard) both;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-reference-review.is-entering {
  animation: reference-review-enter .28s var(--ease-ios-rebound) both;
}

/* The amount/count summary and the action controls are separate floating
 * surfaces in the supplied selection workflow. */
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-selbar-mobile {
  flex-direction: column;
  align-items: center;
  gap: 8px;
  min-height: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-selbar-mobile .rsm-count {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-width: 176px;
  min-height: 48px;
  padding: 7px 18px;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  background: color-mix(in srgb, var(--card) 94%, transparent);
  box-shadow: 0 8px 22px color-mix(in srgb, var(--ink) 14%, transparent);
  text-align: center;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-selbar-mobile .rsm-sum {
  display: block;
  font-size: var(--fs-body);
  line-height: 1.06;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-selbar-mobile .rsm-n {
  display: block;
  font-size: var(--fs-micro);
  line-height: 1.08;
  font-weight: 650;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-selbar-mobile .rsm-actions {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  flex: 0 0 auto;
  padding: 0;
  background: transparent;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-selbar-mobile .rsm-action {
  min-height: 44px;
  min-width: 44px;
  padding: 4px 13px;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  background: var(--card);
  box-shadow: 0 6px 18px color-mix(in srgb, var(--ink) 12%, transparent);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-selbar-mobile .rsm-cleared {
  width: 44px;
  height: 44px;
  padding: 0;
  border-radius: 50%;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-selbar-mobile .rsm-more {
  width: 44px;
  height: 44px;
  padding: 0;
  border-radius: 50%;
}

/* Canonical mutually-exclusive selector rail (S3).  Every applicable
 * radiogroup/tablist owns one continuous, accent-derived rail and one solid
 * moving droplet.  The option nodes are transparent semantic hit targets;
 * they never paint their own rectangles, dividers or selected blocks. */
.segmented-rail {
  position: relative;
  isolation: isolate;
  display: inline-flex;
  align-items: stretch;
  gap: 0;
  padding: 2px;
  background: color-mix(in srgb, var(--accent) 12%, var(--card));
  border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--hairline));
  border-radius: 999px;
  overflow: hidden;
}
.segmented-rail[hidden] { display: none; }
.segmented-rail > .seg-moving-indicator {
  display: block;
  position: absolute;
  z-index: 0;
  inset: 0 auto 0 0;
  width: 0;
  border: 0;
  border-radius: 999px;
  background: var(--accent);
  box-shadow: none;
  pointer-events: none;
  transform: translate3d(0, 0, 0);
  /* Keep legacy left/width writers animated while newer rails may use
   * transform; target cadence remains a shared indicator, not a jump. */
  transition: left var(--dur-base) var(--ease-ios-rebound),
    transform var(--dur-base) var(--ease-ios-rebound),
    width var(--dur-base) var(--ease-ios-rebound);
}
.segmented-rail > :is(button, [role="radio"], [role="tab"]) {
  position: relative;
  z-index: 1;
  flex: 1 1 auto;
  min-width: 0;
  min-height: var(--ctl-h-sm);
  box-sizing: border-box;
  padding: .3rem .7rem;
  border: 0;
  border-radius: 999px;
  background: transparent;
  box-shadow: none;
  color: var(--ink);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
  transition: color var(--dur-fast) var(--ease-standard),
    transform var(--dur-fast) var(--ease-ios-rebound);
}
.segmented-rail > :is(button, [role="radio"], [role="tab"]):is(.on, .active, .sel, [aria-selected="true"], [aria-checked="true"]) {
  background: transparent;
  color: var(--accent-ink);
}
.segmented-rail > :is(button, [role="radio"], [role="tab"]):active { transform: scale(.96); }
@media (hover: hover) {
  .segmented-rail > :is(button, [role="radio"], [role="tab"]):not(:is(.on, .active, .sel, [aria-selected="true"], [aria-checked="true"])):hover {
    background: color-mix(in srgb, var(--accent) 8%, transparent);
  }
}
@media (pointer: fine) {
  .segmented-rail > :is(button, [role="radio"], [role="tab"]):focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
    z-index: 2;
  }
}
body:is([data-surface="phone"], [data-surface="tablet"]) .segmented-rail > :is(button, [role="radio"], [role="tab"]) {
  min-height: var(--ctl-h-md);
  min-width: 44px;
}
@media (prefers-reduced-motion: reduce) {
  .segmented-rail > .seg-moving-indicator,
  .segmented-rail > :is(button, [role="radio"], [role="tab"]) { transition: none; }
}

/* Existing surfaces keep their semantic class names and density contracts;
 * these selectors only opt them into the shared rail geometry. */
.segmented-rail.trailing-seg { height: auto; }
.segmented-rail.trailing-pills { width: max-content; }
.segmented-rail.reg-view-segments { width: 100%; display: inline-flex; }
.segmented-rail.target-tabs { margin: 10px 0 14px; }
.segmented-rail.target-tabs > .target-tab { flex: 1 1 0; }
.segmented-rail.mcard-sort { display: inline-flex; width: max-content; }
.segmented-rail.assign-tabs { width: 100%; }
.segmented-rail.reflect-toggle { width: max-content; }
.segmented-rail.payee-final-choices { width: 100%; flex-direction: row; gap: 0; }
.segmented-rail.payee-final-choices > .payee-final-choice {
  width: auto;
  flex: 1 1 0;
  min-width: 0;
  min-height: var(--ctl-h-md);
  justify-content: flex-start;
  overflow-wrap: anywhere;
  white-space: normal;
}
.segmented-rail.plan-lens {
  padding: 0;
  background: color-mix(in srgb, var(--accent) 12%, var(--card));
  border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--hairline));
}
.segmented-rail.plan-lens > .plan-lens-fill {
  inset: 0 var(--plan-lens-fill-end, 0px) 0 0;
  width: auto;
  transform: none;
  background: var(--accent);
  border: 0;
  transition: right var(--dur-base) var(--ease-ios-rebound),
    left var(--dur-base) var(--ease-ios-rebound);
}
.segmented-rail.plan-lens > .plan-lens-half { min-height: var(--ctl-h-sm); }
body:is([data-surface="phone"], [data-surface="tablet"]) .segmented-rail.plan-lens > .plan-lens-half { min-height: var(--ctl-h-md); }
@media (max-width: 480px) {
  .segmented-rail.mcard-sort { width: 100%; }
  .segmented-rail.mcard-sort > :is(button, [role="tab"]) {
    flex: 1 1 0;
    white-space: normal;
    overflow-wrap: anywhere;
  }
}

/* Final cascade for date-group clipping after the generic swipe-face rules. */
body:is([data-surface="phone"], [data-surface="tablet"]) :is(.txn-cat-inline, .txn-cat-inline-first-line) {
  max-width: 100%; min-width: 0; overflow: visible; text-overflow: clip;
  white-space: normal; overflow-wrap: anywhere;
}
body:is([data-surface="phone"], [data-surface="tablet"]) :is(.sched-row-badgeline, .sched-row-meta) {
  overflow: visible; text-overflow: clip; white-space: normal; overflow-wrap: anywhere;
}
body[data-surface="desktop"] .reg-row .c-cat,
body[data-surface="desktop"] .reg-row .c-cat > :is(.reg-cell-text, [title]) {
  /* Keep the desktop register one physical rail per transaction.  The prior
   * `white-space: normal` rule let a long category grow its grid track to
   * multiple lines while density still fixed the row to 25/32/41px; the
   * resulting cell spilled into the next posted row (and intercepted its
   * checkbox at narrow desktop widths).  Desktop text follows the same
   * single-line, ellipsis contract as the other canonical rails; touch keeps
   * its own wrapped category treatment above. */
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; overflow-wrap: normal;
}
body[data-surface="desktop"] .reg-row .c-subcat,
body[data-surface="desktop"] .reg-row .c-subcat > :is(.reg-cell-text, [title]) {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; overflow-wrap: normal;
}
body[data-route="spending"]:is([data-surface="phone"], [data-surface="tablet"]) .date-group-head {
  border-radius: var(--radius-s, 8px) var(--radius-s, 8px) 0 0;
}
body[data-route="spending"]:is([data-surface="phone"], [data-surface="tablet"]) .swipe-wrap:is(.date-group-first, .date-group-middle, .date-group-only) {
  border-radius: 0;
}
body[data-route="spending"]:is([data-surface="phone"], [data-surface="tablet"]) .swipe-wrap:is(.date-group-last, .date-group-only) {
  border-radius: 0 0 var(--radius-s, 8px) var(--radius-s, 8px);
}
body[data-route="spending"]:is([data-surface="phone"], [data-surface="tablet"]) .swipe-wrap:is(.date-group-last, .date-group-only) .txn-row {
  border-radius: 0 0 var(--radius-s, 8px) var(--radius-s, 8px);
}

/* ---------- App-wide control contract (D-131) -----------------------------
 * This is the final paint owner. Route blocks may keep layout, hit-area and
 * motion details, but they must not reintroduce a competing action colour or
 * segmented option face. Semantic classes are stamped by shell/dom.js for
 * generated labels such as Cancel, Close, Back and Delete. */
.btn {
  background: var(--control-accent);
  color: var(--control-accent-ink);
}
.btn.secondary {
  background: var(--btn-secondary-bg);
  color: var(--btn-secondary-ink);
}
.btn.secondary:disabled,
.btn.secondary[aria-disabled="true"] {
  background: var(--control-neutral-bg);
  color: var(--control-neutral-ink);
  opacity: .6;
  cursor: default;
}
.btn.danger,
.btn.control-danger,
.control-danger {
  background: var(--control-danger);
  color: var(--control-danger-ink);
  border-color: transparent;
  box-shadow: none;
}
.btn.control-neutral,
.control-neutral {
  background: var(--control-neutral-bg);
  color: var(--control-neutral-ink);
  border-color: transparent;
  box-shadow: none;
}
.control-neutral:is(.picker-x, .sheet-x, .install-bar-x, .reg-search-clear,
  .reg-search-inline-clear, .reg-search-dismiss) {
  background: transparent;
}
.btn.control-neutral:disabled,
.control-neutral[disabled],
.control-neutral[aria-disabled="true"] {
  opacity: .6;
  cursor: default;
}

/* The rail owns the shared light blurple face; option nodes are transparent
 * hit targets and the indicator is the sole selected fill. */
body[data-surface] .segmented-rail.segmented-rail {
  background: color-mix(in srgb, var(--control-accent) 12%, var(--card));
  border-color: color-mix(in srgb, var(--control-accent) 30%, var(--hairline));
}
body[data-surface] .segmented-rail.segmented-rail > .seg-moving-indicator {
  background: var(--control-accent);
}
body[data-surface] .segmented-rail.segmented-rail > :is(button, [role="radio"], [role="tab"]) {
  background: transparent;
  color: var(--control-secondary-ink);
}
body[data-surface] .segmented-rail.segmented-rail > :is(button, [role="radio"], [role="tab"]):is(
  .on, .active, .sel, [aria-selected="true"], [aria-checked="true"]
) {
  background: transparent;
  color: var(--control-accent-ink);
}
body[data-surface] .segmented-rail.segmented-rail > :is(.trailing-seg-btn, .mcard-sort-seg,
  .reflect-seg, .assign-tab, .target-tab, .payee-final-choice) {
  border: 0;
  box-shadow: none;
}

/* Standalone true toggles use the same selected/secondary semantics as a
 * rail, without changing their established hitbox or layout. */
body[data-surface] .reflect-pill {
  background: var(--control-secondary-bg);
  color: var(--control-secondary-ink);
  border-color: transparent;
}
body[data-surface] .reflect-pill:is(.active, .on, [aria-pressed="true"], [aria-expanded="true"]) {
  background: var(--control-accent);
  color: var(--control-accent-ink);
  border-color: transparent;
  box-shadow: none;
}

/* Current theme writers historically out-ranked the shared
 * primitive; these exact scopes make the contract win without !important. */
html[data-desktop-theme="current"] body[data-surface="desktop"]:has(.reg-grid) .reg-toolbar .btn:not(.disc):not(.icon-btn),
html[data-desktop-theme="current"] body[data-surface="desktop"]:has(.reg-grid) .reg-optionsbar :is(.rob-cancel, .rob-btn) {
  background: var(--control-accent);
  color: var(--control-accent-ink);
}
html[data-desktop-theme="current"] body[data-surface="desktop"]:has(.reg-grid) .reg-toolbar .btn.secondary,
html[data-desktop-theme="current"] body[data-surface="desktop"]:has(.reg-grid) .reg-optionsbar .rob-cancel {
  background: var(--control-secondary-bg);
  color: var(--control-secondary-ink);
}
html[data-desktop-theme="current"] body[data-surface="desktop"]:has(.reg-grid) .reg-optionsbar .rob-cancel.control-neutral {
  background: var(--control-neutral-bg);
  color: var(--control-neutral-ink);
}

/* -------------------------------------------------------------------------
 * D-131 semantic control owner (runtime-stamped)
 *
 * The route blocks above remain responsible for layout and touch geometry. A
 * control's paint, however, is selected by the role stamped in shell/dom.js;
 * this late owner therefore wins against Current toolbar, popup and sheet
 * specificity without a route-by-route colour fork.  `surface` is intentionally
 * quiet for row/menu buttons that carry their own structural paint.
 */
:is(button, [role="button"], [role="tab"], [role="radio"]).control-primary,
:is(button, [role="button"], [role="tab"], [role="radio"])[data-control-kind="primary"] {
  background: var(--control-accent);
  color: var(--control-accent-ink);
  border-color: transparent;
  box-shadow: none;
}
:is(button, [role="button"], [role="tab"], [role="radio"]).control-secondary,
:is(button, [role="button"], [role="tab"], [role="radio"])[data-control-kind="secondary"] {
  background: var(--control-secondary-bg);
  color: var(--control-secondary-ink);
  border-color: transparent;
  box-shadow: none;
}
:is(button, [role="button"], [role="tab"], [role="radio"]).control-accent-quiet,
:is(button, [role="button"], [role="tab"], [role="radio"])[data-control-kind="accent-quiet"] {
  background: var(--control-secondary-bg);
  color: var(--control-accent);
  border-color: transparent;
  box-shadow: none;
}
:is(button, [role="button"], [role="tab"], [role="radio"]).control-selected,
:is(button, [role="button"], [role="tab"], [role="radio"])[data-control-kind="selected"] {
  background: var(--control-selected-bg);
  color: var(--control-selected-ink);
  border-color: transparent;
  box-shadow: none;
}
:is(button, [role="button"], [role="tab"], [role="radio"]).control-neutral,
:is(button, [role="button"], [role="tab"], [role="radio"])[data-control-kind="neutral"] {
  background: var(--control-neutral-bg);
  color: var(--control-neutral-ink);
  border-color: transparent;
  box-shadow: none;
}
:is(button, [role="button"], [role="tab"], [role="radio"]).control-danger,
:is(button, [role="button"], [role="tab"], [role="radio"])[data-control-kind="danger"] {
  background: var(--control-danger);
  color: var(--control-danger-ink);
  border-color: transparent;
  box-shadow: none;
}
:is(button, [role="button"], [role="tab"], [role="radio"]).control-switch-off,
:is(button, [role="button"], [role="tab"], [role="radio"])[data-control-kind="switch-off"] {
  background: var(--control-switch-off-bg);
  color: var(--control-switch-off-ink);
  border-color: transparent;
  box-shadow: none;
}
:is(button, [role="button"], [role="tab"], [role="radio"]).control-switch-on,
:is(button, [role="button"], [role="tab"], [role="radio"])[data-control-kind="switch-on"] {
  background: var(--control-switch-on-bg);
  color: var(--control-switch-on-ink);
  border-color: transparent;
  box-shadow: none;
}

/* Current route selectors carry two theme/surface attributes and often
 * two primitive classes.  Keep the same semantic owner specificity so a
 * late route rule cannot turn a stamped secondary or neutral control back
 * into a primary blue face. */
html[data-desktop-theme] body[data-surface] :is(button, [role="button"], [role="tab"], [role="radio"]).control-primary[data-control-kind="primary"],
html[data-touch-theme] body[data-surface] :is(button, [role="button"], [role="tab"], [role="radio"]).control-primary[data-control-kind="primary"] {
  background: var(--control-accent);
  color: var(--control-accent-ink);
  border-color: transparent;
  box-shadow: none;
}
html[data-desktop-theme] body[data-surface] :is(button, [role="button"], [role="tab"], [role="radio"]).control-secondary[data-control-kind="secondary"],
html[data-touch-theme] body[data-surface] :is(button, [role="button"], [role="tab"], [role="radio"]).control-secondary[data-control-kind="secondary"] {
  background: var(--control-secondary-bg);
  color: var(--control-secondary-ink);
  border-color: transparent;
  box-shadow: none;
}
html[data-desktop-theme] body[data-surface] :is(button, [role="button"], [role="tab"], [role="radio"]).control-selected[data-control-kind="selected"],
html[data-touch-theme] body[data-surface] :is(button, [role="button"], [role="tab"], [role="radio"]).control-selected[data-control-kind="selected"] {
  background: var(--control-selected-bg);
  color: var(--control-selected-ink);
  border-color: transparent;
  box-shadow: none;
}
html[data-desktop-theme] body[data-surface] :is(button, [role="button"], [role="tab"], [role="radio"]).control-neutral[data-control-kind="neutral"],
html[data-touch-theme] body[data-surface] :is(button, [role="button"], [role="tab"], [role="radio"]).control-neutral[data-control-kind="neutral"] {
  background: var(--control-neutral-bg);
  color: var(--control-neutral-ink);
  border-color: transparent;
  box-shadow: none;
}
/* Disabled controls are neutral by definition.  Register toolbars and Current
 * surfaces carry more-specific legacy disabled selectors, so the semantic
 * owner must keep its grey token with an explicit important override. */
html[data-desktop-theme] body[data-surface] :is(button, [role="button"], [role="tab"], [role="radio"]).control-neutral[data-control-kind="neutral"]:disabled,
html[data-desktop-theme] body[data-surface] :is(button, [role="button"], [role="tab"], [role="radio"]).control-neutral[data-control-kind="neutral"][aria-disabled="true"],
html[data-touch-theme] body[data-surface] :is(button, [role="button"], [role="tab"], [role="radio"]).control-neutral[data-control-kind="neutral"]:disabled,
html[data-touch-theme] body[data-surface] :is(button, [role="button"], [role="tab"], [role="radio"]).control-neutral[data-control-kind="neutral"][aria-disabled="true"] {
  background: var(--control-neutral-bg) !important;
  color: var(--control-neutral-ink) !important;
  border-color: transparent !important;
}
html[data-desktop-theme] body[data-surface] :is(button, [role="button"], [role="tab"], [role="radio"]).control-danger[data-control-kind="danger"],
html[data-touch-theme] body[data-surface] :is(button, [role="button"], [role="tab"], [role="radio"]).control-danger[data-control-kind="danger"] {
  background: var(--control-danger);
  color: var(--control-danger-ink);
  border-color: transparent;
  box-shadow: none;
}
html[data-desktop-theme] body[data-surface] :is(button, [role="button"], [role="tab"], [role="radio"]).control-switch-off[data-control-kind="switch-off"],
html[data-touch-theme] body[data-surface] :is(button, [role="button"], [role="tab"], [role="radio"]).control-switch-off[data-control-kind="switch-off"] {
  background: var(--control-switch-off-bg);
  color: var(--control-switch-off-ink);
  border-color: transparent;
  box-shadow: none;
}
html[data-desktop-theme] body[data-surface] :is(button, [role="button"], [role="tab"], [role="radio"]).control-switch-on[data-control-kind="switch-on"],
html[data-touch-theme] body[data-surface] :is(button, [role="button"], [role="tab"], [role="radio"]).control-switch-on[data-control-kind="switch-on"] {
  background: var(--control-switch-on-bg);
  color: var(--control-switch-on-ink);
  border-color: transparent;
  box-shadow: none;
}

/* The Current touch Plan filter is an older menu primitive with an
 * `!important` neutral face.  Once it reports an active/selected state it is
 * an enabled selected control and must rejoin the canonical solid/white face. */
html[data-touch-theme] body:is([data-surface="phone"], [data-surface="tablet"]) .plan-filter-trigger.control-selected[data-control-kind="selected"] {
  background: var(--control-selected-bg) !important;
  color: var(--control-selected-ink) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}

/* A segmented rail owns one light-blurple face and one indicator.  The rail
 * edge is deliberately invisible: option outlines were the recurring visual
 * drift between Current writers and made the group read as separate pills. */
body[data-surface] .segmented-rail.segmented-rail {
  background: var(--control-secondary-bg);
  border-color: transparent;
  padding: 0;
  border-width: 0;
}
body[data-surface] .segmented-rail.segmented-rail > .seg-moving-indicator {
  background: var(--control-selected-bg);
  border-width: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"])
  .segmented-rail.segmented-rail > :is(.cr-unit-indicator, .reflect-toggle-indicator) {
  inset: 0 auto 0 0;
}
body[data-surface] .segmented-rail.segmented-rail > :is(button, [role="radio"], [role="tab"]) {
  background: transparent;
  color: var(--control-secondary-ink);
  border-color: transparent;
  box-shadow: none;
}
body[data-surface] .segmented-rail.segmented-rail > :is(button, [role="radio"], [role="tab"]).control-selected,
body[data-surface] .segmented-rail.segmented-rail > :is(button, [role="radio"], [role="tab"]):is(.on, .active, .sel, [aria-selected="true"], [aria-checked="true"]) {
  background: transparent;
  color: var(--control-selected-ink);
}

/* The register row is a display ruler, not an action ruler.  Keep controls in
 * the compact desktop surface at the shared 34px tier so Delete repeat,
 * Cancel, Save and every sibling in the same rail cannot collapse to 25px. */
body[data-surface="desktop"]:has(.reg-grid) {
  --spend-action-h: max(var(--reg-row-h), var(--ctl-h-sm));
}

/* Generated close/remove buttons have no text node to guide a human reviewer;
 * the data role makes their final paint inspectable in computed-style audits. */
:is(button, [role="button"]).control-surface[data-control-kind="surface"] {
  color: inherit;
}
/* A semantic Hide action is still an authored text affordance after the
 * observer stamps it as a generic surface. Keep that late classifier from
 * replacing the theme's control-accent ink in either theme. */
:is(button, [role="button"]).control-surface:is(.tf-hide, .hide-action) {
  color: var(--control-accent) !important;
}

/* Native <summary> disclosures (Pricer/help and future details panels) are
 * controls too, but are not buttons and therefore need their own semantic
 * selector.  The observer stamps them with the same data role as buttons. */
summary.control-primary,
summary[data-control-kind="primary"] {
  background: var(--control-accent);
  color: var(--control-accent-ink);
  border-color: transparent;
}
summary.control-secondary,
summary[data-control-kind="secondary"] {
  background: var(--control-secondary-bg);
  color: var(--control-secondary-ink);
  border-color: transparent;
}
summary.control-selected,
summary[data-control-kind="selected"] {
  background: var(--control-selected-bg);
  color: var(--control-selected-ink);
  border-color: transparent;
}
summary.control-neutral,
summary[data-control-kind="neutral"] {
  background: var(--control-neutral-bg);
  color: var(--control-neutral-ink);
  border-color: transparent;
}
summary.control-danger,
summary[data-control-kind="danger"] {
  background: var(--control-danger);
  color: var(--control-danger-ink);
  border-color: transparent;
}
body:is([data-surface="phone"], [data-surface="tablet"]) summary {
  min-height: var(--ctl-h-md);
  display: flex;
  align-items: center;
}

/* ARIA menu/list controls are often divs rather than buttons.  Give them the
 * same role-owned paint while preserving their row-specific spacing. */
:is([role="menuitem"], [role="option"], [role="checkbox"]).control-primary,
:is([role="menuitem"], [role="option"], [role="checkbox"])[data-control-kind="primary"] {
  background: var(--control-accent);
  color: var(--control-accent-ink);
  border-color: transparent;
}
:is([role="menuitem"], [role="option"], [role="checkbox"]).control-secondary,
:is([role="menuitem"], [role="option"], [role="checkbox"])[data-control-kind="secondary"] {
  background: var(--control-secondary-bg);
  color: var(--control-secondary-ink);
  border-color: transparent;
}
:is([role="menuitem"], [role="option"], [role="checkbox"]).control-selected,
:is([role="menuitem"], [role="option"], [role="checkbox"])[data-control-kind="selected"] {
  background: var(--control-selected-bg);
  color: var(--control-selected-ink);
  border-color: transparent;
}
:is([role="menuitem"], [role="option"], [role="checkbox"]).control-neutral,
:is([role="menuitem"], [role="option"], [role="checkbox"])[data-control-kind="neutral"] {
  background: var(--control-neutral-bg);
  color: var(--control-neutral-ink);
  border-color: transparent;
}
:is([role="menuitem"], [role="option"], [role="checkbox"]).control-danger,
:is([role="menuitem"], [role="option"], [role="checkbox"])[data-control-kind="danger"] {
  background: var(--control-danger);
  color: var(--control-danger-ink);
  border-color: transparent;
}

/* ARIA switches are not always buttons (settings and guided setup use plain
 * elements), so keep their off/on/disabled faces explicit as well. */
[role="switch"].control-switch-off,
[role="switch"][data-control-kind="switch-off"] {
  background: var(--control-switch-off-bg);
  color: var(--control-switch-off-ink);
  border-color: transparent;
}
[role="switch"].control-switch-on,
[role="switch"][data-control-kind="switch-on"] {
  background: var(--control-switch-on-bg);
  color: var(--control-switch-on-ink);
  border-color: transparent;
}
[role="switch"].control-neutral,
[role="switch"][data-control-kind="neutral"] {
  background: var(--control-neutral-bg);
  color: var(--control-neutral-ink);
  border-color: transparent;
}
[role="switch"].control-neutral:disabled,
[role="switch"].control-neutral[aria-disabled="true"] {
  background: var(--control-neutral-bg) !important;
  color: var(--control-neutral-ink) !important;
  border-color: transparent !important;
}

/* Current desktop route blocks predate D-131 and can carry one extra class or
 * pseudo-class of specificity (for example `.reg-toolbar .btn:not(...)`).
 * Keep the semantic contract permanent for that grammar too; the segmented
 * rail reset immediately below remains the one intentional transparent child. */
html[data-desktop-theme] body[data-surface]
  :is(button, summary, [role="button"], [role="tab"], [role="radio"], [role="menuitem"], [role="option"], [role="checkbox"], [role="switch"])[data-control-kind="primary"] {
  background: var(--control-accent) !important;
  color: var(--control-accent-ink) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}
html[data-desktop-theme] body[data-surface]
  :is(button, summary, [role="button"], [role="tab"], [role="radio"], [role="menuitem"], [role="option"], [role="checkbox"], [role="switch"])[data-control-kind="secondary"] {
  background: var(--control-secondary-bg) !important;
  color: var(--control-secondary-ink) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}
html[data-desktop-theme] body[data-surface]
  :is(button, summary, [role="button"], [role="tab"], [role="radio"], [role="menuitem"], [role="option"], [role="checkbox"], [role="switch"])[data-control-kind="accent-quiet"] {
  background: var(--control-secondary-bg) !important;
  color: var(--control-accent) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}
html[data-desktop-theme] body[data-surface]
  :is(button, summary, [role="button"], [role="tab"], [role="radio"], [role="menuitem"], [role="option"], [role="checkbox"], [role="switch"])[data-control-kind="selected"] {
  background: var(--control-selected-bg) !important;
  color: var(--control-selected-ink) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}
html[data-desktop-theme] body[data-surface]
  :is(button, summary, [role="button"], [role="tab"], [role="radio"], [role="menuitem"], [role="option"], [role="checkbox"], [role="switch"])[data-control-kind="neutral"] {
  background: var(--control-neutral-bg) !important;
  color: var(--control-neutral-ink) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}
html[data-desktop-theme] body[data-surface]
  :is(button, summary, [role="button"], [role="tab"], [role="radio"], [role="menuitem"], [role="option"], [role="checkbox"], [role="switch"])[data-control-kind="danger"] {
  background: var(--control-danger) !important;
  color: var(--control-danger-ink) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}
/* Context-menu Delete keeps the menu's neutral surface and uses danger ink
 * only. The semantic danger stamp remains useful for accessibility, but its
 * generic filled face must not turn this text action into a red block. */
html[data-desktop-theme] body[data-surface="desktop"] .ctx-menu .ctx-row.danger.control-danger[data-control-kind="danger"] {
  background: transparent !important;
  color: var(--neg) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}
/* S02 menu paint: destructive choices in the supplied Confirm Later and
 * Actions screenshots are red text on their owner's neutral face.  Keep the
 * classifier's `danger` role for semantics, but remove its generic filled
 * control face without touching other destructive controls. */
html[data-desktop-theme] body[data-surface] .confirm-later-picker .picker-head,
html[data-touch-theme] body[data-surface] .confirm-later-picker .picker-head {
  background: transparent !important;
  background-color: transparent !important;
}
html[data-desktop-theme] body[data-surface] .confirm-later-picker .picker-back.control-neutral[data-control-kind="neutral"],
html[data-touch-theme] body[data-surface] .confirm-later-picker .picker-back.control-neutral[data-control-kind="neutral"] {
  background: transparent !important;
  background-color: transparent !important;
  box-shadow: none !important;
}
html[data-desktop-theme] body[data-surface] .confirm-later-picker .confirm-later-remove.control-danger[data-control-kind="danger"],
html[data-touch-theme] body[data-surface] .confirm-later-picker .confirm-later-remove.control-danger[data-control-kind="danger"] {
  background: transparent !important;
  background-color: transparent !important;
  color: var(--neg) !important;
  border-top-color: var(--hairline) !important;
  box-shadow: none !important;
}
html[data-desktop-theme] body[data-surface] .ctx-menu .ctx-row.danger.control-danger[data-control-kind="danger"],
html[data-touch-theme] body[data-surface] .ctx-menu .ctx-row.danger.control-danger[data-control-kind="danger"] {
  background: transparent !important;
  background-color: transparent !important;
  color: var(--neg) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}
html[data-desktop-theme] body[data-surface]
  :is(button, summary, [role="button"], [role="tab"], [role="radio"], [role="menuitem"], [role="option"], [role="checkbox"], [role="switch"])[data-control-kind="switch-off"] {
  background: var(--control-switch-off-bg) !important;
  color: var(--control-switch-off-ink) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}
html[data-desktop-theme] body[data-surface]
  :is(button, summary, [role="button"], [role="tab"], [role="radio"], [role="menuitem"], [role="option"], [role="checkbox"], [role="switch"])[data-control-kind="switch-on"] {
  background: var(--control-switch-on-bg) !important;
  color: var(--control-switch-on-ink) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}
html[data-desktop-theme] body[data-surface] .segmented-rail.segmented-rail {
  background: var(--control-secondary-bg) !important;
  border-color: transparent !important;
}
html[data-desktop-theme] body[data-surface] .segmented-rail.segmented-rail > .seg-moving-indicator {
  background: var(--control-selected-bg) !important;
}
html[data-desktop-theme] body[data-surface] .segmented-rail.segmented-rail
  > :is(button, [role="radio"], [role="tab"])[data-control-kind] {
  background: transparent !important;
  color: var(--control-secondary-ink) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}
html[data-desktop-theme] body[data-surface] .segmented-rail.segmented-rail
  > :is(button, [role="radio"], [role="tab"])[data-control-kind="selected"] {
  color: var(--control-selected-ink) !important;
}

/* Payee final-name choices have no indicator child on desktop's contextual
 * workspace and therefore need the selected face on the option itself. Keep
 * the same rule on touch (where the indicator is deliberately hidden) so a
 * late semantic stamp cannot leave white text on the shared rail face. */
html[data-desktop-theme] body[data-surface] .segmented-rail.payee-final-choices
  > :is(button, [role="radio"])[aria-selected="true"],
html[data-touch-theme] body[data-surface] .segmented-rail.payee-final-choices
  > :is(button, [role="radio"])[aria-selected="true"] {
  background: var(--control-selected-bg) !important;
  color: var(--control-selected-ink) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}
html[data-desktop-theme] body[data-surface] .segmented-rail.payee-final-choices
  > :is(button, [role="radio"]):not([aria-selected="true"]),
html[data-touch-theme] body[data-surface] .segmented-rail.payee-final-choices
  > :is(button, [role="radio"]):not([aria-selected="true"]) {
  background: transparent !important;
  color: var(--control-secondary-ink) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}

/* The Current touch privacy eye is a transparent ink-only chrome control, not
 * a painted switch rail.  D-131's late switch owner must not reintroduce the
 * old glass disc when the observer stamps its off/on semantic state. */
html[data-touch-theme] body:is([data-surface="phone"], [data-surface="tablet"])
  :is(.settings-gear, .privacy-eye)[data-control-kind="switch-off"],
html[data-touch-theme] body:is([data-surface="phone"], [data-surface="tablet"])
  :is(.settings-gear, .privacy-eye)[data-control-kind="switch-on"] {
  background: transparent !important;
  border-color: transparent !important;
  box-shadow: none !important;
  -webkit-backdrop-filter: none !important;
  backdrop-filter: none !important;
}
html[data-touch-theme] body:is([data-surface="phone"], [data-surface="tablet"])
  .privacy-eye.on[data-control-kind="switch-on"] {
  color: var(--accent) !important;
}

/* Desktop utility-disc parity (operator 26/08/2026). The semantic observer
 * correctly classifies the privacy eye as switch-off/on, while Settings is a
 * navigation surface. Without this late owner the two buttons share their
 * base glass token yet resolve to visibly different final fills. Give the
 * inactive gear the eye's neutral disc tokens; preserve the selected face
 * while Settings itself is open. Geometry remains owned by the 40px rules. */
html[data-desktop-theme] body[data-surface="desktop"]
  .settings-gear[data-control-kind="surface"]:not(.active) {
  background: var(--control-switch-off-bg) !important;
  color: var(--control-switch-off-ink) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}
html[data-desktop-theme] body[data-surface="desktop"]
  .settings-gear.active[data-control-kind="surface"] {
  background: var(--control-selected-bg) !important;
  color: var(--control-selected-ink) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}

/* Plan/Home reorder controls share the same passive icon grammar.  The late
 * semantic owner classifies the text-bearing Plan link as secondary, but that
 * must not turn it darker than Home's equivalent muted control. */
html[data-touch-theme] body:is([data-surface="phone"], [data-surface="tablet"])
  .plan-ctl-btn {
  background: transparent !important;
  border-color: transparent !important;
  box-shadow: none !important;
  color: var(--ink-muted) !important;
}
html[data-touch-theme] body:is([data-surface="phone"], [data-surface="tablet"])
  .plan-ctl-reorder.on {
  color: var(--accent) !important;
  transition: transform var(--dur-fast) var(--ease-spring) !important;
}

/* Sentence-case control contract: visible action labels never inherit a
 * heading's uppercase transform.  Source labels own their intended casing;
 * user-entered names remain untouched because this only resets presentation
 * on interactive controls. */
:is(button, [role="button"], [role="tab"], [role="menuitem"], a.btn) {
  text-transform: none !important;
}

/* The two Plan bulk reset calculations are red-text utility actions, not
 * destructive commands. Their authored neutral classification deliberately
 * bypasses the generic Reset label heuristic; this final owner preserves the
 * required card face after the semantic paint layer stamps the live node. */
html[data-desktop-theme] body[data-surface] button.auto-row-reset[data-control-kind="neutral"],
html[data-touch-theme] body[data-surface] button.auto-row-reset[data-control-kind="neutral"] {
  background: var(--card) !important;
  color: var(--neg) !important;
  box-shadow: none !important;
}

/* ---------- Current visual polish lane (23/08/2026) -----------------------
 * This is intentionally a final, Current-only visual owner.  It does not
 * create a second theme or change markup: every face and state below consumes
 * the shared light/dark tokens defined at the top of this file. */

/* Fields and search controls share one curved, border-box geometry contract.
 * Border colour may change on focus, but border width and box sizing never do,
 * so focus/click cannot move a row or resize a neighbouring control. */
:is(
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]):not([type="hidden"]),
  select, textarea, .inp, .reg-search-pill, .reg-grid-search,
  .reg-search-inline, .picker-search, .plan-search-input,
  .payee-list-search, .memo-input
) {
  box-sizing: border-box;
  border-radius: 999px;
}
:is(.inp, .reg-search-pill, .reg-grid-search, .reg-search-inline, .picker-search,
  .plan-search-input, .payee-list-search, .memo-input):not(:disabled) {
  border-width: 1px;
}
@media (pointer: fine) {
  :is(
    input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]):not([type="hidden"]),
    select, textarea, .inp, .reg-search-pill, .reg-grid-search,
    .reg-search-inline, .picker-search, .plan-search-input,
    .payee-list-search, .memo-input
  ):focus-visible {
    outline: 2px solid color-mix(in srgb, var(--accent) 72%, transparent);
    outline-offset: 2px;
  }
  /* Compound search inputs keep focus on their existing border owner. This
   * higher-specificity reset follows the app-wide field rule deliberately so
   * a real click cannot paint a second boundary or alter perceived geometry. */
  body :is(.reg-grid-search-input, .picker-search, .reg-search-inline input):focus-visible {
    outline: none !important;
    outline-offset: 0;
    box-shadow: none;
  }
}

/* Labels are authored in accessible sentence case; the visible control face
 * presents Title Case.  Capitalise only controls, never user-entered content,
 * and leave icon/SVG text alone so symbols and acronyms remain intact. */
:is(button, [role="button"], [role="tab"], [role="menuitem"], a.btn) {
  text-transform: capitalize !important;
  box-sizing: border-box;
}
:is(button, [role="button"], [role="tab"], [role="menuitem"], a.btn)
  :is(.ico, svg, .icon, .sr-only) {
  text-transform: none !important;
}

/* Sidebar amounts use one right-hand rail.  The old negative-only pill added
 * horizontal padding to just negative figures, which made Mount Sinai's
 * amount appear further indented than its neighbours. */
.acct-row .acct-bal {
  min-inline-size: 7ch;
  padding-inline: .5rem;
  text-align: right;
  color: color-mix(in srgb, var(--pos) 82%, var(--ink));
  font-weight: 700;
}
.acct-row .acct-bal.neg { color: var(--neg-ink); }

/* Plan category names and their header labels start on the same physical rail
 * as the register's text cells; numeric tracks retain their right rail. */
body[data-surface="desktop"] .plan-table .plan-row :is(.c-cat, .plan-cat-name) {
  padding-inline-start: var(--reg-cell-rail, 6px);
  text-align: left;
}
body[data-surface="desktop"] .plan-table .plan-head .c-cat { text-align: left; }
body[data-surface="desktop"] .plan-header .link-btn {
  box-sizing: border-box;
  min-height: var(--ctl-h-sm);
  padding: .3rem .9rem;
  border: 1px solid var(--chrome-edge);
  border-radius: 999px;
}
.link-btn { border-radius: 999px; }

/* The FAB's ordinary drop shadow and chromatic-aberration pseudo layers were
 * mistaken for scroll/expand feedback.  Shrink/expand remains geometry-only. */
.fab-txn,
.fab-txn:hover,
.fab-txn:active,
.fab-txn.has-ca,
.fab-txn.has-ca::before,
.fab-txn.has-ca::after {
  box-shadow: none !important;
}
.fab-txn.has-ca::before,
.fab-txn.has-ca::after {
  opacity: 0 !important;
  display: none !important;
}

/* Add-sheet X and Pricer X are light-blurple neutral actions, never danger
 * red even when semantic labelling sees “Discard” or “Remove product”. */
.txn-sheet .sheet-x,
.pricer-card .btn-x {
  background: var(--control-secondary-bg) !important;
  color: var(--control-accent) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}
.btn-x { border-radius: 999px; }
.pricer-card .btn-x { border-radius: 999px; }

/* Cancel is a quiet continuation action, not a neutral grey reset. The
 * explicit semantic stamp also prevents the shell observer's exact-label
 * Cancel heuristic from repainting it after a DOM refresh. */
body[data-surface="desktop"] .reg-editbar .ir-cancel {
  background: var(--control-secondary-bg) !important;
  color: var(--control-secondary-ink) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}

/* Keep the running expression in its own track above the amount.  The close
 * button remains in the header's second row; FX summaries occupy the same
 * expression track, with the type pill moved below the amount. */
.sheet-head:not(.has-fx) { row-gap: 12px; }
.sheet-head:not(.has-fx) .calc-expr { grid-row: 2; }
.sheet-head:not(.has-fx) .amount-zone { grid-row: 3; padding-block: 4px; }
.sheet-head:not(.has-fx) .type-pill { grid-row: 4; }
.sheet-head.has-fx .calc-expr { grid-row: 2; }
.sheet-head.has-fx .fx-base-summary { grid-row: 3; }
.sheet-head.has-fx .amount-zone { grid-row: 4; padding-block: 4px; }
.sheet-head.has-fx .type-pill { grid-row: 5; }

/* A short landscape phone has a side-by-side keypad, so keep the four header
 * tracks compact enough that Category remains fully tappable above the fold.
 * The portrait spacing stays unchanged. */
@media (orientation: landscape) and (max-height: 500px) {
  body[data-surface="phone"] .sheet-head { row-gap: 4px; padding-top: 4px; }
  body[data-surface="phone"] .sheet-head :is(.amount-zone, .type-pill) { padding-block: 0; margin-block: 0; }
}

/* Touch sync indicator: the requested two-times cycle speed, while muted and
 * reduced-motion modes retain their explicit animation-off contract. */
.sync-icon { animation-duration: 1.4s !important; }

/* Desktop date/category editors are clipped to the density row rather than
 * exporting a taller button/input into adjacent transaction rows. */
@media (min-width: 801px) {
  body[data-surface="desktop"] .reg-row :is(.c-date, .c-cat) {
    min-height: 0;
    overflow: hidden;
  }
  body[data-surface="desktop"] .reg-row :is(.c-date, .c-cat)
    > :is(button, input, select, .inp) {
    box-sizing: border-box;
    height: calc(var(--reg-row-h, 32px) - 2px);
    min-height: 0;
    max-height: calc(var(--reg-row-h, 32px) - 2px);
    overflow: hidden;
  }
}

/* Calendar paint: selected dates are black/ink on blurple; month titles are
 * blurple ink on an unfilled face.  All secondary calendar panes are opaque so
 * the register behind them cannot bleed through. */
.calendar-modal .cal-month,
.calendar-modal .cal-zoom {
  background: transparent !important;
  color: var(--accent) !important;
}
html body[data-surface] .calendar-modal .cal-zoom.cal-zoom[data-control-kind] {
  background: transparent !important;
  color: var(--accent) !important;
}
html body[data-surface] .confirm-later-calendar.confirm-later-calendar .cal-cell.cal-cell.control-neutral:disabled[data-control-kind],
html body[data-surface] .calendar-modal.calendar-modal .cal-cell.cal-cell.control-neutral:disabled[data-control-kind] {
  background: transparent !important;
  color: var(--ink-muted) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}
.calendar-modal :is(.cal-cell.sel, .cal-cell.selected, .cal-cell[aria-selected="true"]) {
  background: var(--accent) !important;
  color: var(--selected-date-ink) !important;
  border-color: transparent !important;
}
.calendar-modal .cal-grid:not(.cal-grid-month)
  :is(.cal-cell.sel, .cal-cell.selected, .cal-cell[aria-selected="true"]) {
  border-radius: 50%;
  aspect-ratio: 1;
}
:is(.calendar-modal, .reg-datepop, .reg-datepop .calendar-modal,
  .reg-datepop .reg-recurrence-options, .reg-datepop-repeat-list,
  .reg-datepop-custom) {
  background: var(--card) !important;
  opacity: 1;
}

/* Current scheduled/future rows use the same light-blurple wash on every
 * surface.  Scheduled transfer text is ordinary-weight, not an accidental
 * bold emphasis.  Search-preview scheduled rows also lose the generic rule
 * border, including the Spotify result path. */
:is(.reg-row[data-scheduled="true"], .txn-row[data-scheduled="true"],
  .reg-search-preview-scheduled .sched-row,
  .reg-search-preview-scheduled-group .sched-row) {
  background: var(--scheduled-fill);
}
:is(.reg-search-preview-scheduled .sched-row,
  .reg-search-preview-scheduled-group .sched-row) {
  border-top: 0 !important;
}
:is(.sched-row, .reg-row) .xfer-line { font-weight: inherit !important; }
:is(.reg-sched-band.scheduled-fill .c-date,
  .upcoming-body.scheduled-fill > .date-head,
  .reg-search-preview-scheduled .c-date,
  .reg-search-preview-scheduled .date-head) {
  color: var(--accent) !important;
}

/* Plan Settings autosaves each field; its X still uses the shared close face. */
.plan-settings-modal .picker-x {
  background: var(--close-control-bg) !important;
  color: var(--close-control-ink) !important;
  border-color: transparent !important;
  box-shadow: var(--close-control-shadow) !important;
}

/* Payee creation/final-target actions share the same large, contrast-safe
 * geometry.  A selected final target is intentionally a large green success
 * face, while the disclosure remains a readable secondary control. */
:is(.payee-add-toggle, .payee-add-submit, .payee-final-choice, .payee-extras-h) {
  box-sizing: border-box;
  min-height: var(--ctl-h-md);
  border-radius: 999px;
}
.payee-final-choice {
  border-color: var(--hairline);
  background: var(--control-secondary-bg);
  color: var(--control-secondary-ink);
  font-weight: 600;
}
.payee-final-choice.payee-final-target-success,
.payee-final-choice[data-final-target="true"] {
  min-height: var(--ctl-h-lg) !important;
  background: var(--aa-green) !important;
  color: var(--aa-ink) !important;
  border-color: transparent !important;
  font-weight: 700;
}
.payee-extras-h {
  display: flex;
  align-items: center;
  gap: .5rem;
  width: 100%;
  padding-inline: .9rem;
  border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--hairline));
  background: var(--control-secondary-bg);
  color: var(--control-secondary-ink);
  font: inherit;
  font-weight: 700;
  text-align: left;
}
.payee-extras-h[aria-expanded="true"] {
  background: var(--control-accent);
  color: var(--control-accent-ink);
  border-color: transparent;
}

/* Every visible action owns a small on-theme press change.  Controls with
 * bespoke spring geometry (FAB, keypad and swipe faces) keep their own motion. */
:is(button, [role="button"], [role="tab"], [role="menuitem"], a.btn):not(
  .fab-txn, .key, .swipe-btn, .seg-moving-indicator
) {
  transition-property: background-color, border-color, color, filter, box-shadow;
  transition-duration: var(--dur-fast);
  transition-timing-function: var(--ease-standard);
}
:is(button, [role="button"], [role="tab"], [role="menuitem"], a.btn):not(
  :disabled, [aria-disabled="true"], .fab-txn, .key, .swipe-btn,
  .seg-moving-indicator
):active {
  filter: brightness(.94);
}

/* Touch swipe faces use full row-height targets and theme tokens; the reveal
 * distance is derived from these dimensions instead of clipping the labels. */
body:is([data-surface="phone"], [data-surface="tablet"]) .swipe-wrap {
  --swipe-edit-w: 6.5rem;
  --swipe-delete-w: 5.5rem;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .swipe-actions {
  top: 4px;
  bottom: 4px;
  right: 6px;
  gap: 6px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .swipe-btn {
  height: auto;
  min-height: 0;
  max-height: none;
  border-radius: var(--radius-s);
  box-shadow: none;
  font-size: var(--fs-label);
  letter-spacing: normal;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .swipe-edit-payee {
  width: var(--swipe-edit-w);
  min-width: var(--swipe-edit-w);
  background: var(--amber);
  color: var(--amber-ink);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .swipe-del {
  width: var(--swipe-delete-w);
  min-width: var(--swipe-delete-w);
  background: var(--control-danger);
  color: var(--control-danger-ink);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .swipe-wrap.swiped .txn-row,
body:is([data-surface="phone"], [data-surface="tablet"]) .swipe-wrap.swiped .sched-txn-row {
  transform: translateX(calc(-1 * (var(--swipe-edit-w) + var(--swipe-delete-w) + 12px)));
}

/* Selection controls are readable faces, not glowing decorations. */
:is(.reg-selbar-mobile, .reg-selection-close, .rsm-count, .rsm-action,
  .reg-optionsbar, .reg-optionsbar .rob-cancel, .reg-optionsbar .rob-btn) {
  box-shadow: none !important;
}

@media (prefers-reduced-motion: reduce) {
  :is(button, [role="button"], [role="tab"], [role="menuitem"], a.btn):not(.fab-txn, .key, .swipe-btn) {
    transition: none !important;
  }
}

/* v1.211.0 — Payee, search and popup geometry corrections.
 * These final owners sit after semantic control stamping so the visible
 * component, rather than a generic button role, owns its paint and sizing. */

/* Desktop Manage Payees uses the available report canvas. Long identities
 * wrap inside both the list and comparison rows instead of being ellipsised. */
body[data-surface="desktop"] .payee-page {
  width: 100%;
  max-width: none;
  min-width: 0;
}
body[data-surface="desktop"] .payee-workspace .payee-list-name {
  white-space: normal !important;
  overflow-wrap: anywhere !important;
  word-break: normal;
  overflow: visible;
  text-overflow: clip;
}
body[data-surface="desktop"] .payee-page .payees-left {
  width: min(38%, 420px);
  min-width: 300px;
}
body[data-surface="desktop"] .payee-workspace :is(.pick-row, .payee-merge-row) {
  height: auto;
  min-height: var(--ctl-h-md);
  max-height: none;
  white-space: normal !important;
  overflow: visible;
}
.payee-duplicate-option {
  display: grid;
  gap: .45rem;
  padding: .3rem;
}
.payee-workspace :is(.payee-add-row, .payee-rename-row, .payee-delete-actions,
  .payees-footer) {
  gap: .65rem;
}
.payee-workspace :is(.payee-add-toggle, .payee-add-submit,
  .payee-duplicates-toggle, .payee-duplicates-back) {
  align-self: flex-start;
  margin-inline: .2rem;
}

/* A search field's wrapper may retain its quiet focus-within border, but the
 * browser's separate outer blue ring is removed from every free-text editor. */
body :is(input:not([type]), input[type="text"], input[type="search"], input[type="email"],
  input[type="password"], input[type="url"], input[type="tel"],
  input[type="number"], textarea, [contenteditable="true"], [contenteditable=""]):focus-visible {
  outline: none !important;
  outline-offset: 0 !important;
}
/* The converter opens with Currency focused. Touch must not turn that scripted
 * focus into a second blue frame; the field keeps one quiet token border. */
body .fx-modal .fx-cur:is(:focus, :focus-visible) {
  outline: none !important;
  outline-offset: 0 !important;
  border-color: var(--hairline);
  box-shadow: none;
}

/* Search-result cards use the same full-width rail as ordinary touch
 * transactions. Only width changes; their vertical rhythm remains intact. */
body:is([data-surface="phone"], [data-surface="tablet"]).search-preview-open .reg-search-preview {
  box-sizing: border-box;
  width: calc(100% + 32px);
  margin-inline: -16px;
}
.reg-search-preview-date {
  font-size: var(--fs-body);
  line-height: 1.35;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .reg-search-preview-date {
  font-size: var(--fs-value) !important;
}
.reg-search-preview-scheduled,
.reg-search-preview-scheduled-group {
  background: var(--scheduled-fill);
}

/* Keep the desktop search control's measured dimensions while restoring a
 * normal text line box so the caret reaches its full height. */
body[data-surface="desktop"] .reg-grid-search .reg-grid-search-input {
  line-height: normal;
}

/* Reports and shell chrome: quiet disclosure, readable sidebar figures and a
 * deterministic way home from the full desktop report browser. */
.chart-data-disclosure {
  display: inline-flex; align-items: center; gap: 6px; min-height: var(--ctl-h-sm);
  vertical-align: middle;
}
.chart-data-toggle {
  display: inline-flex; align-items: center; justify-content: flex-start;
  gap: 6px; line-height: 1.2; vertical-align: middle;
}
.chart-data-toggle {
  background: transparent !important;
  color: var(--accent) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}
body[data-surface] .chart-data-toggle.chart-data-toggle.chart-data-toggle[data-control-kind] {
  background: transparent !important;
  color: var(--accent) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}
.acct-row .acct-bal {
  color: var(--sidebar-ink);
}
.acct-row .acct-bal.neg { color: var(--neg-ink); }
.reflect-route-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: .75rem;
}
.reflect-route-head .screen-title { margin: 0; }
.reflect-back-home {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  min-height: var(--ctl-h-md);
  padding: .45rem .8rem;
  border-radius: 999px;
  color: var(--accent);
  background: var(--control-secondary-bg);
  font-weight: 700;
  text-decoration: none;
}

/* Transaction and repeat editors establish the canonical round close face.
 * Their footer actions share the canonical large-control height. */
:is(.txn-sheet, .sched-sheet) .sheet-x {
  background: var(--close-control-bg) !important;
  color: var(--close-control-ink) !important;
  border-color: transparent !important;
  box-shadow: var(--close-control-shadow) !important;
}
body[data-surface] :is(.txn-sheet, .sched-sheet) .sheet-x.sheet-x[data-control-kind] {
  background: var(--close-control-bg) !important;
  color: var(--close-control-ink) !important;
  border-color: transparent !important;
  box-shadow: var(--close-control-shadow) !important;
}
.sched-sheet .sheet-footer .btn {
  box-sizing: border-box;
  height: var(--ctl-h-lg);
  min-height: var(--ctl-h-lg);
  max-height: var(--ctl-h-lg);
  padding-block: 0;
  font-size: var(--fs-value);
  border-radius: var(--radius-s);
}
body[data-surface="desktop"] .sched-sheet .sched-head {
  display: grid;
  grid-template-columns: 2.4rem minmax(0, 1fr) auto 2.4rem;
  align-items: center;
  gap: .5rem;
}
body[data-surface="desktop"] .sched-sheet .sched-head .sheet-grip {
  grid-column: 1 / -1;
  grid-row: 1;
}
body[data-surface="desktop"] .sched-sheet .sched-head .sheet-x {
  grid-column: 4;
  grid-row: 2;
}
body[data-surface="desktop"] .sched-sheet .sched-head .sheet-title {
  grid-column: 2;
  grid-row: 2;
}
body[data-surface="desktop"] .sched-sheet .sched-head .sched-amt-preview {
  grid-column: 3;
  grid-row: 2;
}
body[data-surface="desktop"] .reg-datepop:has(.rec-end-calendar) {
  overflow-x: hidden;
  overflow-y: auto;
}
.sched-sheet .stepped-step-add.touch-link {
  min-height: var(--ctl-h-md);
  padding: .55rem 0;
  background: transparent !important;
  color: var(--accent) !important;
  border: 0 !important;
  border-radius: 0;
  box-shadow: none !important;
}
body[data-surface] .sched-sheet .stepped-step-add.touch-link[data-control-kind] {
  background: transparent !important;
  color: var(--accent) !important;
  border: 0 !important;
  box-shadow: none !important;
}

/* Keypad forward actions always use the contrast-safe ink authored for their
 * accent face; generic surface classification must not inherit black text. */
.key.kdone {
  color: var(--accent-ink) !important;
}
.key.kdone.inflow { color: var(--accent-ink) !important; }
:is(.key.op.armed, .sched-chip.on, .rec-remind-choices button.on,
  .txn-remind-mode.on, .reg-icon-btn.on, .reg-select-btn.on,
  .reg-uncleared-banner.on, .reg-scheduled-banner.on) {
  color: var(--accent-ink) !important;
}
.env-bubble[data-variant="update"] .env-bubble-icon {
  color: var(--control-accent) !important;
}

/* ---------- Approval parity correction (Phase 01, reference touch surface) ----------
 * The reference queue is a full-width iOS review sheet. The pre-correction
 * generic queue inset the sheet, painted a canvas-coloured flex column, and
 * reserved an entire action rail in every settled row. Keep the real controls
 * in the DOM for keyboard/assistive and existing mechanical paths, but remove
 * their paint from the settled touch frame; the swipe rail and the approval
 * detail dock remain the visible action paths. */
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-topbar {
  box-sizing: border-box;
  /* Image 3: the banner uses the same content rail as Ready-to-Assign. Keep
   * its height, padding and vertical margins; only the inline edge ownership
   * changes from the old inset card. */
  width: 100%;
  margin: 10px 0 12px;
  min-height: 50px;
  padding: 8px 16px;
  border: 0;
  border-radius: 999px;
  background: var(--card);
  color: var(--ink);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-topbar-dot {
  min-width: 24px;
  height: 24px;
  padding: 0 7px;
  border-radius: 999px;
  background: var(--control-secondary-bg);
  color: var(--ink);
  font-size: var(--fs-value);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-topbar-txt {
  font-size: var(--fs-h3);
  font-weight: 500;
  white-space: nowrap;
  text-transform: none !important;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-topbar-go {
  padding: 7px 19px;
  border-radius: 999px;
  background: var(--control-secondary-bg);
  color: var(--control-accent);
  font-size: var(--fs-h3);
  font-weight: 700;
}

body[data-surface="phone"] .approve-sheet {
  box-sizing: border-box;
  width: 100%;
  max-width: 480px;
  height: calc(100dvh - max(env(safe-area-inset-top), 4dvh)) !important;
  max-height: calc(100dvh - max(env(safe-area-inset-top), 4dvh)) !important;
  padding: 10px 0 0;
  border-radius: 24px 24px 0 0;
}
body[data-surface="tablet"] .approve-sheet {
  box-sizing: border-box;
  width: 100%;
  max-width: 480px;
  height: auto !important;
  max-height: 90dvh !important;
  padding: 10px 0 0;
  border-radius: 20px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-head-touch {
  box-sizing: border-box;
  width: 100%;
  height: 92px;
  padding: 14px 14px 18px;
  row-gap: 7px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-head-touch .approve-title {
  grid-column: 1 / 4;
  grid-row: 1 / 3;
  gap: 14px;
  margin-right: 0;
  font-size: var(--fs-body);
  line-height: 1.2;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-head-touch .approve-title-label {
  font-size: var(--fs-body);
  font-weight: 500;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-head-touch .approve-count {
  font-size: 1.35rem;
  font-weight: 800;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-head-touch .sheet-x {
  position: absolute;
  top: 22px;
  right: 14px;
  width: var(--ctl-h-md);
  min-width: var(--ctl-h-md);
  height: var(--ctl-h-md);
  border: 0;
  border-radius: 50%;
  background: var(--card) !important;
  background-color: var(--card) !important;
  color: var(--ink) !important;
  box-shadow: 0 1px 5px color-mix(in srgb, var(--ink) 16%, transparent);
  font: 300 2rem/1 -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
}
body[data-surface] .approve-sheet .approve-head-touch .sheet-x.sheet-x {
  background: var(--card) !important;
  background-color: var(--card) !important;
  color: var(--ink) !important;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-sheet .sheet-scroll {
  gap: 0;
  padding: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-sheet .approve-group {
  width: 100%;
  padding: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-sheet .approve-group + .approve-group {
  padding-top: 0;
  border-top: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-sheet .approve-group-h,
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-sheet .approve-all {
  display: none;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-date-heading {
  box-sizing: border-box;
  width: 100%;
  margin: 0;
  padding: 5px 16px;
  border-top: 0;
  border-bottom: 1px solid var(--hairline);
  background: var(--canvas);
  font-size: var(--fs-h3);
  font-weight: 800;
  line-height: 1.2;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-date-heading:not(:first-of-type) {
  padding-top: 11px;
  padding-bottom: 11px;
}
/* The first date follows the two-line sheet header and keeps the shorter
 * leading cadence; later dates carry the native inter-row breathing room. */
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-date-heading:nth-of-type(2) {
  padding-top: 5px;
  padding-bottom: 5px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-swipe {
  width: 100%;
  border-radius: 0;
  overflow-x: hidden;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row {
  box-sizing: border-box;
  width: 100%;
  min-height: 107px;
  margin: 0;
  padding: 13px 16px 15px 8px;
  border: 0;
  border-left: 4px solid var(--accent);
  border-bottom: 1px solid var(--hairline);
  border-radius: 0;
  background: var(--card);
  display: block;
  overflow-x: hidden;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-top {
  display: grid;
  grid-template-columns: 28px minmax(0, 1fr) max-content;
  grid-template-rows: auto auto auto;
  align-items: center;
  column-gap: 0;
  row-gap: 2px;
  width: 100%;
  min-height: 74px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-main {
  display: contents;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-name-line {
  grid-column: 2;
  grid-row: 1;
  min-width: 0;
  gap: 7px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-name {
  min-width: 0;
  font-size: var(--fs-h3);
  line-height: 1.2;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-flag {
  width: .8rem;
  height: 1.05rem;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-category {
  grid-column: 2;
  grid-row: 2;
  justify-self: start;
  max-width: 100%;
  margin-top: 0;
  padding: .15rem .55rem;
  font-size: calc(var(--fs-label) * .95);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-memo {
  grid-column: 2 / 4;
  grid-row: 3;
  min-width: 0;
  margin-top: 0;
  padding-right: 8px;
  font-size: var(--fs-label);
  line-height: 1.25;
  letter-spacing: normal;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-meta {
  grid-column: 3;
  grid-row: 2;
  justify-self: end;
  align-self: center;
  margin: 0;
  font-size: var(--fs-body);
  line-height: 1.2;
  white-space: nowrap;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-amt {
  grid-column: 3;
  grid-row: 1;
  justify-self: end;
  align-self: center;
  font-size: var(--fs-h3);
  line-height: 1.2;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-amountline {
  grid-column: 3;
  grid-row: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 7px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-amountline .approve-row-amt {
  grid-column: auto;
  grid-row: auto;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-cleared {
  box-sizing: border-box;
  flex: 0 0 16px;
  width: 16px;
  height: 16px;
  padding: 2px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--pos);
  color: var(--card);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-cleared .ico {
  width: 13px;
  height: 13px;
  stroke-width: 2.4;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-select {
  grid-column: 1;
  grid-row: 1 / span 3;
  width: var(--ctl-h-md);
  min-width: var(--ctl-h-md);
  height: var(--ctl-h-md);
  padding: 0;
  border: 0;
  background: transparent;
  color: transparent;
  font-size: 0;
  position: relative;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-select::before {
  content: '';
  position: absolute;
  inset: auto;
  left: 50%;
  top: 50%;
  width: 18px;
  height: 18px;
  box-sizing: border-box;
  border: 2px solid var(--ink-muted);
  border-radius: 50%;
  background: transparent;
  transform: translate(-50%, -50%);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-select.selected::before {
  border: 0;
  background: var(--accent);
  box-shadow: none;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-select.selected::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 10px;
  height: 6px;
  box-sizing: border-box;
  border-left: 2px solid var(--accent-ink);
  border-bottom: 2px solid var(--accent-ink);
  transform: translate(-50%, -50%) rotate(-45deg);
  transform-origin: center;
}
/* Keep mechanical controls measurable/clickable for existing flow contracts,
 * but do not paint them in the settled reference row. */
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row :is(.approve-row-btn, .approve-row-enter, .approve-row-enternow, .approve-row-delete) {
  position: absolute;
  right: 0;
  top: 0;
  width: var(--ctl-h-md);
  min-width: var(--ctl-h-md);
  height: var(--ctl-h-md);
  min-height: var(--ctl-h-md);
  padding: 0;
  opacity: 0;
  pointer-events: none;
  visibility: hidden;
  font-size: 0;
  overflow: hidden;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row .approve-row-btn { right: 112px; }
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row .approve-row-enter { right: 112px; }
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row .approve-row-enternow { right: 64px; }
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row .approve-row-delete { right: 16px; }
/* The approval queue's primary decision must remain visible without a hidden
 * swipe affordance: every directly-approvable pending card shows its own
 * token-sized Approve button in the row's action rail.  Destructive/delete
 * remains gesture/overflow-owned so the primary action stays front-and-centre
 * on narrow touch widths. */
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row .approve-row-btn {
  position: static; right: auto; top: auto;
  grid-column: 2 / 4; grid-row: 4; justify-self: stretch;
  width: 100%; min-width: 0;
  height: var(--ctl-h-md); min-height: var(--ctl-h-md); padding: .35rem .55rem;
  opacity: 1; pointer-events: auto; visibility: visible; font-size: var(--fs-label);
  overflow: visible; flex: 0 0 auto;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-snooze {
  position: absolute;
  right: 16px;
  bottom: 0;
  width: var(--ctl-h-md);
  height: var(--ctl-h-md);
  opacity: 0;
  pointer-events: none;
  visibility: hidden;
  overflow: hidden;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-snooze .approve-snooze-toggle {
  position: absolute;
  inset: 0;
  width: 44px;
  min-width: 44px;
  height: 44px;
  padding: 0;
  font-size: 0;
  overflow: hidden;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-snooze .approve-snooze-days {
  position: absolute;
  inset: 0;
  width: 44px;
  height: 44px;
  display: block !important;
  grid-template-columns: none !important;
  gap: 0 !important;
  overflow: hidden;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-snooze .approve-snooze-days > button {
  position: absolute;
  inset: 0;
  width: 44px;
  min-width: 44px;
  height: 44px;
  min-height: 44px;
  padding: 0;
  font-size: 0;
  overflow: hidden;
}

/* The native multi-select cadence: four touch-sized controls float over the
 * queue's lower edge.  The dock is deliberately absolute so selecting rows
 * never reflows the list or changes the reference scroll position. */
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-selection-dock {
  position: fixed;
  left: 50%;
  /* The reference phone reserves the lower gesture/tab strip and floats the
   * compact action dock over the transfer row, leaving the final canvas band
   * visible beneath it. */
  bottom: calc(var(--tab-bar-bottom) + var(--tab-bar-height) + var(--approval-dock-gap));
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  transform: translateX(-50%);
  pointer-events: none;
  animation: approve-selection-dock-in 220ms cubic-bezier(.2, .9, .25, 1.08) both;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-sheet:has(.approve-selection-dock) .sheet-scroll {
  /* Reserve the fixed dock's footprint so the final row can scroll fully
   * above it instead of ending underneath the actions. */
  padding-bottom: calc(var(--ctl-h-md) + var(--tab-bar-height) + var(--tab-bar-bottom) + var(--approval-dock-gap));
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-selection-dock button {
  box-sizing: border-box;
  min-height: var(--ctl-h-md);
  height: var(--ctl-h-md);
  border: 1px solid var(--hairline);
  border-radius: 999px;
  background: var(--approval-dock-fill);
  color: var(--ink);
  box-shadow: none;
  font: inherit;
  font-size: var(--fs-body);
  font-weight: 700;
  pointer-events: auto;
  touch-action: manipulation;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-selection-dock button:active {
  transform: translateY(1px) scale(.98);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-selection-dock button:disabled {
  opacity: .45;
  pointer-events: none;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-selection-clear,
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-selection-more {
  width: var(--ctl-h-md);
  min-width: var(--ctl-h-md);
  padding: 0;
  font-size: 1.7rem;
  font-weight: 400;
  line-height: 1;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-selection-more .ico {
  width: 20px;
  height: 20px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-selection-approve,
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-selection-categorize {
  padding: 0 15px;
  color: var(--accent);
}
@keyframes approve-selection-dock-in {
  from { opacity: 0; transform: translate(-50%, 12px) scale(.96); }
  to { opacity: 1; transform: translate(-50%, 0) scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  body:is([data-surface="phone"], [data-surface="tablet"]) .approve-selection-dock { animation: none; }
}
body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu {
  min-width: 252px;
  max-width: calc(100vw - 24px);
  padding: 8px;
  border-radius: 20px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu .ctx-row {
  min-height: 52px;
  align-items: center;
  justify-content: flex-start;
  gap: 12px;
  padding: 8px 10px;
  border-radius: 14px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu .ctx-icon {
  width: 24px;
  height: 24px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 24px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu .ctx-icon .ico {
  width: 22px;
  height: 22px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu .ctx-label {
  flex: 1 1 auto;
  min-width: 0;
  text-align: left;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu .ctx-key { display: none; }
body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu .ctx-trailing {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu .ctx-trailing .ico {
  width: 18px;
  height: 18px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu .ctx-row.danger {
  color: var(--neg);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu .ctx-row.disabled {
  color: var(--ink-muted);
  opacity: .5;
}

/* Approval-launched transaction detail: the recording uses the compact review
 * card (Account on page one, Memo card, floating Approve pill), while
 * ordinary transaction entry keeps its existing form geometry. */
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail {
  height: 99.5dvh !important;
  max-height: 99.5dvh !important;
  padding: 8px 16px 0;
  isolation: isolate;
}
/* The approval review uses the native warm hero wash: a shallow, curved
 * backdrop behind the amount/type controls that fades back into the canvas
 * before the first review card.  Keep it as a non-interactive layer so it can
 * never become an invisible hit surface. */
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail::before {
  content: '';
  position: absolute;
  inset: 0 0 auto;
  height: 160px;
  z-index: -1;
  pointer-events: none;
  border-radius: 0 0 50% 50% / 0 0 18% 18%;
  background: linear-gradient(to bottom,
    var(--approval-hero-deep) 0%,
    var(--approval-hero-deep) 3%,
    var(--approval-hero-light) 12%,
    var(--approval-hero-light) 22%,
    var(--approval-hero-mid) 60%,
    var(--approval-hero-low) 90%,
    var(--canvas) 100%);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail > :is(.sheet-head, .sheet-scroll, .sheet-dock) {
  position: relative;
  z-index: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .sheet-head {
  row-gap: 6px;
  height: 219px;
  box-sizing: border-box;
  padding-top: 5px;
  padding-bottom: 2px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .sheet-grip {
  display: none;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .sheet-x {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 3;
  width: var(--ctl-h-md);
  min-width: var(--ctl-h-md);
  height: var(--ctl-h-md);
  grid-column: auto;
  grid-row: auto;
  justify-self: auto;
  align-self: auto;
  background: var(--card) !important;
  background-color: var(--card) !important;
  color: var(--ink) !important;
  font-size: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .sheet-x .ico {
  width: 26px;
  height: 26px;
  stroke-width: 2.2;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .amount-display {
  font-size: calc(var(--fs-display) * 1.2);
  transform: translateY(-24px);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .cur-chip {
  display: none;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .amount-zone {
  padding: 4px 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .type-pill {
  gap: 4px;
  padding: .42rem .4rem;
  font-size: var(--fs-h3);
  transform: translateY(-23px);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .sheet-scroll {
  gap: 24px;
  padding-bottom: 12px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .sheet-fields {
  padding: 4px 12px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .field-row {
  position: relative;
  gap: 22px;
  padding: .64rem 0;
  border-top: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .field-row:not(:first-child)::before {
  content: '';
  position: absolute;
  top: 0;
  left: 44px;
  right: 0;
  border-top: 1px solid var(--hairline);
  pointer-events: none;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .field-label {
  font-size: var(--fs-body);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .field-ico:not(.ico-account) {
  color: color-mix(in srgb, var(--ink-muted) 60%, var(--canvas) 40%);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .field-ico.ico-account {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--pos-pill);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .field-value,
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .field-memo {
  font-size: var(--fs-h3);
  font-weight: 400;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .approval-memo-card {
  margin-top: 0;
}
/* The compact review card presents Memo as a single value line; the generic
 * transaction form's visible label must not consume a second line here. */
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .approval-memo-card .field-row[data-f="memo"] .field-label {
  display: none;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .approval-memo-card .field-row[data-f="memo"] {
  min-height: 67px !important;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .approval-memo-card .field-row[data-f="memo"] .field-memo {
  border: 0;
  outline: 0;
  background: transparent;
  box-shadow: none;
  padding: 0;
  text-transform: none !important;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .sheet-showmore {
  text-transform: none !important;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .sheet-dock {
  position: absolute;
  right: 17px;
  bottom: 108px;
  z-index: 4;
  align-items: flex-end;
  padding: 0;
  pointer-events: none;
}

/* Approval recordings retain a soft dim outside the rounded sheet.  The
 * generic transaction overlay uses the stronger edit-sheet scrim, which
 * darkens the exposed corner pixels beyond the supplied review capture. */
body:is([data-surface="phone"], [data-surface="tablet"]) .sheet-overlay:has(.approve-sheet, .approval-detail) {
  background: var(--sheet-scrim-soft);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .sheet-dock .btn-save.btn-approve {
  width: 158px;
  min-height: 47px;
  padding: .5rem 1.1rem;
  border-radius: 999px;
  font-size: var(--fs-h3);
  text-transform: none;
  pointer-events: auto;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .keypad-panel {
  display: none !important;
  pointer-events: none !important;
}

/* Phase 01 R2 recording parity.  The literal queue uses a restrained iOS
 * text scale and sentence-case transaction content; the earlier R1 touch
 * layer inherited the generic role=button capitaliser and painted every
 * heading one step too large.  These owners keep the measured row cadence
 * while correcting the visible glyph metrics in both themes. */
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-head-touch {
  height: 95px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-head-touch .approve-title,
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-head-touch .approve-title-label {
  font-size: var(--fs-label);
  font-weight: 400;
  text-transform: none !important;
  transform: translateY(-6px);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-head-touch .approve-title {
  transform: none;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-head-touch .approve-count {
  font-size: var(--fs-h3);
  font-weight: 700;
  line-height: 1.2;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-head-touch .sheet-x {
  font-size: 1.55rem;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-date-heading {
  font-size: var(--fs-body);
  font-weight: 700;
  line-height: 1.2;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-main,
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-main * {
  text-transform: none !important;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-name {
  font-size: var(--fs-body);
  font-weight: 700;
  line-height: 1.2;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-top {
  row-gap: 9px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row {
  padding-top: 18px;
  padding-bottom: 10px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-flag {
  width: .5rem;
  height: .9rem;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-amt {
  font-size: var(--fs-body);
  font-weight: 700;
  line-height: 1.2;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-category {
  font-size: var(--fs-label);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-category-income {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  background: var(--pos-pill);
  color: var(--ink);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-category-income .ico {
  width: 16px;
  height: 16px;
  flex: 0 0 16px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-memo {
  font-size: var(--fs-label);
  line-height: 1.25;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-transfer-names {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-transfer-name {
  min-width: 0;
  white-space: nowrap;
  line-height: 1.2;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-transfer-name .approve-row-name {
  white-space: nowrap;
}

/* The reference More affordance is a compact lower-right popover.  The touch
 * hit floor remains 44px on the dock, while the menu's painted rows are
 * shorter and Delete is red text on the same neutral face as its siblings. */
body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu {
  min-width: 252px;
  padding: 5px;
  border-radius: 20px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu .ctx-row {
  min-height: 32px;
  padding: 5px 9px;
  border-radius: 14px;
  gap: 10px;
  font-size: var(--fs-body);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu .ctx-icon {
  width: 22px;
  height: 22px;
  flex-basis: 22px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu .ctx-icon .ico {
  width: 20px;
  height: 20px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu .ctx-row.danger {
  background: transparent;
  color: var(--neg);
}
/* The current-touch semantic layer stamps menu buttons with control-danger;
 * this scoped owner must outrank that generic destructive face. */
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu {
  min-width: 252px !important;
  padding: 5px !important;
  background: color-mix(in srgb, var(--card) 88%, transparent) !important;
  -webkit-backdrop-filter: blur(18px) saturate(1.08);
  backdrop-filter: blur(18px) saturate(1.08);
  box-shadow: 0 10px 28px color-mix(in srgb, var(--ink) 18%, transparent);
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu .ctx-row {
  min-height: 32px !important;
  padding: 5px 9px !important;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .selection-overflow-menu .ctx-row.danger.control-danger {
  background: transparent !important;
  color: var(--neg) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}
/* Final precedence for Step 01-R1. The shared control classifier runs after
 * the form is built and paints semantic danger/neutral faces with !important;
 * these double-scoped rules keep the supplied uncleared sheet's own paint. */
.txn-sheet-clearing .sheet-x.sheet-x {
  grid-column: 1 !important;
  justify-self: start !important;
  background: var(--card) !important;
  color: var(--ink) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}
.txn-sheet-clearing .sheet-grip {
  display: none;
}
.txn-sheet-clearing .cur-chip {
  display: none;
}
.txn-sheet-clearing .sheet-del-full.sheet-del-full {
  background: var(--neg-pill) !important;
  color: var(--neg) !important;
}
.txn-sheet-clearing .field-row {
  position: relative;
  border-top: 0;
}
.txn-sheet-clearing .field-row + .field-row::before {
  content: "";
  position: absolute;
  left: 2.5rem;
  right: 0;
  top: 0;
  height: 1px;
  background: var(--hairline);
}
.txn-sheet-clearing.clearing-scrolled .sheet-head {
  position: relative;
  z-index: 4;
  background: transparent;
  pointer-events: none;
  -webkit-backdrop-filter: blur(1px);
  backdrop-filter: blur(1px);
}
.txn-sheet-clearing.clearing-scrolled .sheet-head .sheet-x {
  pointer-events: auto;
}
.txn-sheet-clearing.clearing-scrolled .sheet-scroll {
  position: absolute;
  inset: 0 14px 0;
  width: auto;
  clip-path: inset(24px 0 0 0);
}
.txn-sheet-clearing.clearing-scrolled .sheet-dock {
  position: absolute;
  left: 14px;
  right: 14px;
  bottom: 40px;
  z-index: 6;
}
.txn-sheet-clearing.clearing-scrolled .sheet-head > :not(.sheet-x) {
  visibility: hidden;
}
.txn-sheet-clearing.clearing-scrolled .sheet-x {
  position: absolute;
  left: .5rem;
  top: .35rem;
  width: 2.5rem;
  height: 2.5rem;
  transform: translateY(-1.7rem);
}
.txn-sheet-clearing.clearing-scrolled .sheet-head::after {
  content: "Spending";
  position: absolute;
  inset: 0;
  display: grid;
  place-items: start center;
  padding-top: 20px;
  box-sizing: border-box;
  color: var(--ink);
  font-size: var(--fs-h3);
  font-weight: 700;
}
body[data-surface] .txn-sheet-clearing .sheet-x.sheet-x[data-control-kind] {
  background: var(--card) !important;
  color: var(--ink) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}

/* Due-occurrence rows keep the legacy static hook for the recording matrix,
 * but their selection box is now a real touch control. */
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-select-static {
  pointer-events: auto;
}

/* Direct visual gate, 25/08/2026. These are the sampled application-owned
 * paints and type metrics from the operator's native 1206×2622 references.
 * They stay scoped to approval so the ordinary register and transaction form
 * retain the project's Current visual system. */
body:is([data-surface="phone"], [data-surface="tablet"]) :is(.approve-sheet, .approval-detail) {
  background: var(--approval-ref-canvas);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-date-heading {
  background: var(--approval-ref-canvas);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row {
  background: var(--approval-ref-card);
  border-left-color: var(--approval-ref-accent);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-category {
  background: var(--approval-ref-chip);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-select.selected::before {
  background: var(--approval-ref-accent);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-cleared {
  background: var(--pos);
  color: var(--approval-ref-card);
  font-size: 11px;
  font-weight: 800;
  line-height: 1;
  text-transform: lowercase;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-cleared .approval-cleared-letter {
  display: block;
  transform: translateY(-.25px);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-selection-dock button {
  background: var(--approval-dock-fill);
  color: var(--ink);
  box-shadow: none;
}
body:is([data-surface="phone"], [data-surface="tablet"]) :is(.approve-selection-approve, .approve-selection-categorize) {
  color: var(--ink);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-selection-dock :is(.approve-selection-approve, .approve-selection-categorize) {
  padding-inline: 11px;
  font-size: var(--fs-label);
  font-weight: 400;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-selection-categorize:not(:disabled) {
  background: var(--approval-dock-fill);
  color: var(--ink);
}

/* S11 touch geometry: reserve a real selection rail before the content track.
 * The old 28px track let the 18px check disc overhang into the category chip;
 * the chip/flag then touched the tick at narrow widths.  A full control-height
 * rail plus a token gap keeps every row's content clear in both themes while
 * preserving the flexible amount column. */
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-top {
  grid-template-columns: var(--ctl-h-md) minmax(0, 1fr) max-content;
  column-gap: var(--ctl-pad-x-sm);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-name-line {
  gap: var(--ctl-pad-x-sm);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approve-row-category {
  max-width: 100%;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .amount-display {
  font-size: var(--fs-display);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .type-pill {
  font-weight: 400;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .sheet-x .ico {
  width: 22px;
  height: 22px;
  stroke-width: 1.9;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail :is(.field-value, .field-memo) {
  font-size: var(--fs-value);
  line-height: 1.2;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .approval-detail .sheet-dock .btn-save.btn-approve {
  background: var(--approval-ref-accent);
  color: var(--accent-ink);
  font-weight: 400;
}
/* Reference initial register checkpoint: the approval pill is compact, the
 * uncleared disclosure is warmer and slightly taller, and both use the same
 * narrow page rails as the supplied phone capture. */
html[data-touch-theme] body:is([data-surface="phone"], [data-surface="tablet"]):has(.clearing-reference) .reg-reference-context {
  margin-inline: -8px;
  gap: 10px;
}
html[data-touch-theme] body:is([data-surface="phone"], [data-surface="tablet"]):has(.clearing-reference) .reg-reference-review {
  min-height: 50px;
  padding: 6px 14px;
  border-radius: 28px;
}
html[data-touch-theme] body:is([data-surface="phone"], [data-surface="tablet"]):has(.clearing-reference) .reg-reference-review-label b {
  min-width: 26px;
  min-height: 26px;
  margin-right: 6px;
}
html[data-touch-theme] body:is([data-surface="phone"], [data-surface="tablet"]):has(.clearing-reference) .reg-reference-review-btn {
  min-width: 92px;
  min-height: 38px;
  padding-inline: 16px;
}
html[data-touch-theme] body:is([data-surface="phone"], [data-surface="tablet"]):has(.clearing-reference) .reg-reference-uncleared {
  min-height: 50px;
  padding: 8px 14px;
  background: color-mix(in srgb, var(--canvas) 92%, var(--amber) 8%);
}
html[data-touch-theme] body:is([data-surface="phone"], [data-surface="tablet"]):has(.clearing-reference) .reg-reference-uncleared b {
  display: inline-flex;
  align-items: center;
  min-height: 22px;
  padding-inline: 4px;
  margin-inline: 4px;
  border-radius: 4px;
  background: color-mix(in srgb, var(--ink-muted) 18%, var(--canvas));
  font-weight: 850;
}
html[data-touch-theme] body[data-surface] .txn-sheet-clearing .sheet-del-full.sheet-del-full[data-control-kind="danger"] {
  background: var(--neg-pill) !important;
  color: var(--neg) !important;
  border-color: transparent !important;
}

/* No sharp authored button in the Add/Split touch workflow. Transparent
 * field buttons still own a rounded hit face; visible circles/pills retain
 * their stronger shapes in the component rules above. */
body:is([data-surface="phone"], [data-surface="tablet"]) .txn-sheet :is(.cur-chip, .amount-display, .field-row, .field-btn, .sheet-showmore) {
  border-radius: var(--radius-s);
}
/* Three-letter currency codes are one sibling group: fixed theme dimensions
 * prevent wide glyphs such as NZD from producing an off-scale lozenge. */
body:is([data-surface="phone"], [data-surface="tablet"]) .txn-sheet .cur-chip.foreign {
  box-sizing: border-box;
  display: inline-flex; align-items: center; justify-content: center;
  width: calc(var(--ctl-h-md) + .5rem); min-width: calc(var(--ctl-h-md) + .5rem);
  height: var(--ctl-h-md); padding: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .picker-modal :is(.picker-back, .picker-cancel, .pick-row, .picker-pinned-btn) {
  border-radius: var(--radius-s);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .splitpick-modal .picker-back {
  width: var(--ctl-h-md); height: var(--ctl-h-md); aspect-ratio: 1;
  border-radius: 50%;
}

/* Add Transaction touch parity (Step 01-A): the recording uses an ink close
 * disc and white text on the compact lower action pill.  These terminal rules
 * intentionally outrank the app-wide control stamping selectors above. */
body:is([data-surface="phone"], [data-surface="tablet"]) .txn-sheet .sheet-x.sheet-x {
  background: var(--card) !important;
  background-color: var(--card) !important;
  color: var(--ink) !important;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .txn-sheet .key.kdone.kdone {
  color: var(--accent-ink) !important;
}
/* A touch Add sheet owns the complete canvas below the status-bar rail.  The
 * route remains mounted for dismissal/repaint, but its desktop register header
 * must not show through the 48px scrim above the sheet (source Add/Split
 * recordings show only the native touch canvas). */
body.sheet-open:is([data-surface="phone"], [data-surface="tablet"]) .content > :not(.txn-sheet) {
  visibility: hidden;
}
html[data-touch-theme] body:is([data-surface="phone"], [data-surface="tablet"]) .txn-sheet .sheet-x.sheet-x[data-control-kind="neutral"] {
  background: var(--card) !important;
  background-color: var(--card) !important;
  color: var(--ink) !important;
}

/* Split Transaction is a second touch sheet in the supplied workflow, not a
 * small centred picker. Keep the editor's arithmetic/rows intact while giving
 * the chrome and each action the recorded full-height geometry. */
body:is([data-surface="phone"], [data-surface="tablet"]) .sub-backdrop:has(.split-modal) {
  align-items: stretch;
  justify-content: stretch;
  padding: max(48px, env(safe-area-inset-top)) 0 0;
  background: var(--canvas);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sub-backdrop:has(.splitpick-modal) {
  align-items: stretch;
  justify-content: stretch;
  padding: max(48px, env(safe-area-inset-top)) 0 0;
  background: var(--canvas);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .split-modal {
  box-sizing: border-box;
  width: 100%; max-width: none; height: 100%; max-height: none;
  overflow-y: auto; overflow-x: hidden;
  padding: 18px 16px calc(18px + env(safe-area-inset-bottom));
  border-radius: 28px 28px 0 0;
  background: var(--canvas); box-shadow: none;
  gap: 12px;
}
.split-headbar {
  display: grid; grid-template-columns: var(--touch-popup-terminal-size) 1fr var(--touch-popup-terminal-size);
  align-items: center; gap: 8px; min-height: var(--touch-popup-terminal-size);
  text-align: center; font-size: var(--fs-h3); font-weight: 700;
}
.split-headbar > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.split-headbar button {
  width: var(--touch-popup-terminal-size); height: var(--touch-popup-terminal-size); min-width: var(--touch-popup-terminal-size);
  padding: 0; border: 0; border-radius: 50%; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--card); color: var(--ink);
}
.split-headbar .split-check {
  background: var(--accent); color: var(--accent-ink);
}
.split-headbar button:active { transform: scale(.94); }
body:is([data-surface="phone"], [data-surface="tablet"]) .split-txnrow,
body:is([data-surface="phone"], [data-surface="tablet"]) .split-line {
  background: var(--card); border: 0; border-radius: var(--radius); padding: 14px 16px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .split-txnrow {
  margin-top: 36px; font-size: var(--fs-body);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .split-catsrow {
  font-size: var(--fs-body); padding-inline: 32px; font-weight: 700;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .split-catsrow .split-remaining {
  color: var(--ink); font-weight: 500;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .split-catsrow .split-remaining.ok { color: var(--pos); }
body:is([data-surface="phone"], [data-surface="tablet"]) .split-line { margin: 0; }
body:is([data-surface="phone"], [data-surface="tablet"]) .split-line-main { gap: 10px; }
body:is([data-surface="phone"], [data-surface="tablet"]) .split-line.has-details { padding-bottom: 0; }
body:is([data-surface="phone"], [data-surface="tablet"]) .split-line.has-details .split-line-detail {
  margin-inline: -16px; width: calc(100% + 32px); padding-inline: 16px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .split-line.has-details .split-line-detail:first-of-type {
  margin-top: 14px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .split-line.has-details .split-line-detail:last-child {
  padding-bottom: 14px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .split-line-payee .split-line-detail-icon .ico {
  width: 20px; height: 20px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .split-list { gap: 16px; }
body:is([data-surface="phone"], [data-surface="tablet"]) .split-cat {
  border: 0; background: transparent; color: var(--ink); padding: 0;
  font-size: var(--fs-body); font-weight: 700; min-height: var(--ctl-h-md);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .split-amt {
  width: 7rem; min-height: var(--ctl-h-md); border: 0;
  border-radius: 999px; background: var(--chip); padding-inline: .8rem;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .split-menu {
  min-width: var(--ctl-h-md); min-height: var(--ctl-h-md); border-radius: 50%;
  color: var(--ink-muted); background: transparent;
}
body:is([data-surface="phone"], [data-surface="tablet"]) :is(.split-add, .split-menu, .split-headbar button, .splitpick-next, .split-linemenu .pick-row).is-activated {
  background: var(--accent) !important;
  color: var(--accent-ink) !important;
  border-color: var(--accent) !important;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .split-add {
  width: 100%; min-height: var(--ctl-h-lg); border: 0;
  border-radius: 999px; background: color-mix(in srgb, var(--canvas) 88%, var(--ink) 12%) !important;
  color: var(--accent) !important; font-weight: 700; text-transform: none !important;
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
}
.split-add-plus {
  width: 20px; height: 20px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--accent); color: var(--accent-ink); font-weight: 700; line-height: 1;
}
html[data-touch-theme] body:is([data-surface="phone"], [data-surface="tablet"]) .split-add.split-add {
  background: color-mix(in srgb, var(--canvas) 88%, var(--ink) 12%) !important;
  color: var(--accent) !important;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .splitpick-modal {
  box-sizing: border-box; width: 100%; max-width: none;
  height: 100%; max-height: none; overflow: hidden;
  padding: 18px 16px calc(18px + env(safe-area-inset-bottom));
  border-radius: 28px 28px 0 0; background: var(--canvas); box-shadow: none;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .splitpick-modal > :is(.splitpick-head, .picker-search) {
  flex: 0 0 auto;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .splitpick-modal > .picker-list {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  touch-action: pan-y;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .splitpick-head {
  display: grid; grid-template-columns: var(--touch-popup-terminal-size) minmax(0, 1fr) auto;
  min-height: var(--touch-popup-terminal-size); gap: 8px; align-items: center;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .splitpick-head .picker-back {
  width: var(--ctl-h-md); height: var(--ctl-h-md); padding: 0;
  border-radius: 50%; background: var(--card); color: var(--ink);
  display: inline-flex; align-items: center; justify-content: center;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .splitpick-count {
  margin: 0; text-align: center; font-size: var(--fs-h2);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .splitpick-next {
  min-width: 90px; min-height: var(--ctl-h-md); padding-inline: 1rem;
  border: 0; border-radius: 999px; background: var(--card); color: var(--ink);
}
/* Split line options are an in-context circular bubble in the supplied touch
 * recording, not a second full-width bottom sheet. Keep the action labels and
 * their own pressed feedback, but give the bubble its recorded round surface. */
body:is([data-surface="phone"], [data-surface="tablet"]) .sub-backdrop:has(.split-linemenu-touch) {
  display: block; padding: 0;
  background: transparent;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .split-linemenu-touch {
  position: fixed; inset: auto; left: var(--split-menu-left, 150px); top: var(--split-menu-top, 252px);
  box-sizing: border-box; width: min(244px, calc(100vw - 32px)); height: auto;
  max-height: none; padding: 8px; gap: 0; border-radius: var(--radius);
  border: 1px solid var(--hairline); background: var(--card);
  backdrop-filter: none; -webkit-backdrop-filter: none;
  box-shadow: 0 14px 38px color-mix(in srgb, var(--ink) 20%, transparent);
  justify-content: flex-start; overflow: hidden;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .split-linemenu-touch .picker-head { display: none; }
body:is([data-surface="phone"], [data-surface="tablet"]) .split-linemenu-touch .picker-list {
  display: grid; grid-template-columns: 1fr; grid-auto-rows: minmax(44px, auto);
  gap: 0; width: 100%; height: auto; overflow: visible;
  filter: none; transform: none;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .split-linemenu-touch .picker-list::-webkit-scrollbar { display: none; }
body:is([data-surface="phone"], [data-surface="tablet"]) .split-linemenu-touch .pick-row {
  display: grid; grid-template-columns: 28px minmax(0, 1fr); align-items: center; gap: 10px;
  min-height: 44px; padding: 3px 8px; border: 0; border-radius: var(--radius-s);
  background: transparent; color: var(--ink); font-size: var(--fs-body);
  line-height: 1.05; white-space: nowrap; text-align: left; text-transform: none;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .split-linemenu-touch .split-line-action-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; color: currentColor;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .split-linemenu-touch .split-line-action-icon .ico {
  width: 22px; height: 22px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .split-linemenu-touch .pick-row.danger {
  color: var(--neg); background: transparent;
}
/* The shared semantic classifier stamps destructive buttons as solid red.
 * This reference menu deliberately uses a quiet destructive row: clear face,
 * red label and an ordinary-ink minus disc. Keep that local presentation while
 * retaining the button's danger semantics. */
html[data-touch-theme] body:is([data-surface="phone"], [data-surface="tablet"])
  .split-linemenu-touch .pick-row.danger.control-danger[data-control-kind="danger"] {
  background: transparent !important;
  background-color: transparent !important;
  color: var(--neg) !important;
  border-color: transparent !important;
  box-shadow: none !important;
}
html[data-touch-theme] body:is([data-surface="phone"], [data-surface="tablet"])
  .split-linemenu-touch .pick-row.danger .split-line-action-icon {
  color: var(--ink) !important;
}

/* Add-transaction list pickers are native-style touch pages, not centred
 * desktop dialogs. The app owns the canvas above the OS keyboard; the keyboard
 * itself remains the device's native keyboard on iPhone/Android. */
body:is([data-surface="phone"], [data-surface="tablet"]) .sub-backdrop:has(.txn-list-picker) {
  align-items: stretch; justify-content: stretch;
  padding: max(48px, env(safe-area-inset-top)) 0 0;
  background: var(--canvas);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .txn-list-picker {
  box-sizing: border-box; width: 100%; max-width: none;
  height: 100%; max-height: none; padding: 30px 16px 16px;
  border-radius: 28px 28px 0 0; background: var(--canvas); box-shadow: none;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .txn-list-picker .picker-head {
  display: grid; grid-template-columns: var(--touch-popup-terminal-size) minmax(0, 1fr) var(--touch-popup-terminal-size);
  align-items: center; min-height: var(--touch-popup-terminal-size); text-align: center;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .txn-list-picker .picker-head > span {
  font-size: var(--fs-h3); color: var(--ink);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .txn-list-picker .picker-head .picker-back {
  width: var(--ctl-h-md); height: var(--ctl-h-md); padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%; background: var(--card); color: var(--ink);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .txn-list-picker .picker-head .picker-cancel { visibility: hidden; }
.picker-search-wrap {
  position: relative; display: grid;
  grid-template-columns: minmax(0, 1fr) var(--ctl-h-md); gap: 8px; align-items: center;
}
.picker-search-wrap .picker-search { min-width: 0; margin: 0; }
.picker-search-clear {
  width: var(--ctl-h-md); height: var(--ctl-h-md); min-width: var(--ctl-h-md); padding: 0;
  border: 0; border-radius: 50%; background: var(--card); color: var(--ink);
  font: inherit; font-size: var(--fs-h2); cursor: pointer;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .txn-list-picker .picker-list {
  flex: 1 1 auto; max-height: none; padding-top: 8px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .txn-list-picker .picker-search-wrap {
  margin-right: -12px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .txn-list-picker .picker-search-wrap::before {
  content: ''; position: absolute; z-index: 1; left: 14px; top: 50%;
  width: 11px; height: 11px; border: 1.8px solid var(--ink); border-radius: 50%;
  transform: translateY(-58%); pointer-events: none;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .txn-list-picker .picker-search-wrap::after {
  content: ''; position: absolute; z-index: 1; left: 25px; top: 55%;
  width: 7px; height: 1.8px; border-radius: 999px; background: var(--ink);
  transform: rotate(48deg); transform-origin: left center; pointer-events: none;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .txn-list-picker .picker-search {
  min-height: var(--ctl-h-md); padding-left: 36px; border-color: transparent;
  border-radius: 999px; background: var(--card);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .txn-list-picker .picker-search:focus,
body:is([data-surface="phone"], [data-surface="tablet"]) .txn-list-picker .picker-search:focus-visible {
  border-color: transparent; box-shadow: none;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .txn-list-picker[data-picker-title="payee"] .pick-header {
  margin: 14px 16px 8px; color: var(--ink); font-size: var(--fs-body);
  font-weight: 700; letter-spacing: 0; text-transform: none;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .txn-list-picker[data-picker-title="payee"] .pick-row {
  min-height: 50px; margin: 0 0 4px; padding-inline: 16px;
  border: 0; outline: none; border-radius: 0; background: transparent;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .txn-list-picker[data-picker-title="category"] .picker-search-wrap {
  order: 20; position: sticky; bottom: 0; margin-right: 0; padding-top: 6px; background: var(--canvas);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .splitpick-row {
  min-height: 64px; margin: 0 0 2px; padding: 12px 16px;
  border: 0; border-radius: var(--radius); background: transparent;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .splitpick-row.on {
  background: transparent; box-shadow: none;
}
/* The latest supplied source explicitly replaces the former no-fill arrow:
 * every touch popup back arrow is now the same large terminal size as its
 * confirmation peer, with a literal white circular face. */
html[data-touch-theme] body:is([data-surface="phone"], [data-surface="tablet"])
  :is(.picker-modal, .splitpick-modal, .txn-list-picker, .split-modal)
  :is(.picker-back, .split-back) {
  width: var(--touch-popup-terminal-size) !important;
  height: var(--touch-popup-terminal-size) !important;
  min-width: var(--touch-popup-terminal-size) !important;
  min-height: var(--touch-popup-terminal-size) !important;
  padding: 0 !important; aspect-ratio: 1; border-radius: 50% !important;
  background: var(--touch-popup-back-bg) !important;
  background-color: var(--touch-popup-back-bg) !important;
  color: var(--touch-popup-back-ink) !important;
  border-color: transparent !important; box-shadow: none !important;
  display: inline-flex; align-items: center; justify-content: center;
}
html[data-touch-theme] body:is([data-surface="phone"], [data-surface="tablet"])
  :is(.picker-modal, .splitpick-modal, .txn-list-picker, .split-modal)
  .touch-popup-confirm {
  width: var(--touch-popup-terminal-size) !important;
  height: var(--touch-popup-terminal-size) !important;
  min-width: var(--touch-popup-terminal-size) !important;
  min-height: var(--touch-popup-terminal-size) !important;
  padding: 0 !important; aspect-ratio: 1; border-radius: 50% !important;
  background: var(--touch-popup-confirm-bg) !important;
  background-color: var(--touch-popup-confirm-bg) !important;
  color: var(--touch-popup-confirm-ink) !important;
  border-color: transparent !important; box-shadow: none !important;
  display: inline-flex; align-items: center; justify-content: center;
}
html[data-touch-theme] body:is([data-surface="phone"], [data-surface="tablet"])
  .touch-popup-confirm .ico {
  width: 30px; height: 30px; stroke-width: 2.2;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .splitpick-check {
  width: 30px; height: 30px; min-width: 30px; max-width: 30px;
  border-width: 2px;
}
/* Track mode: a read-only spending surface with stable, token-based geometry. */
.plan-owned-root[data-mode="track"] .plan-table[data-mode="track"] .plan-row {
  grid-template-columns: minmax(10rem, 1fr) 6.5rem 2rem 7.5rem 7.5rem;
}
.plan-owned-root[data-mode="track"] .plan-layout { grid-template-columns: minmax(0, 1fr) minmax(360px, 760px); }
body[data-surface="desktop"] .plan-owned-root[data-mode="track"] .plan-layout {
  grid-template-columns: minmax(42rem, 52rem) minmax(360px, 1fr);
}
@media (max-width: 93.75rem) {
  .plan-owned-root[data-mode="track"] .plan-layout { grid-template-columns: minmax(0, 1fr); }
  body[data-surface="desktop"] .plan-owned-root[data-mode="track"] .plan-layout { grid-template-columns: minmax(0, 1fr); }
}
/* iPad landscape Track deliberately keeps the desktop table + Inspector
 * composition, even below the ordinary desktop breakpoint. Other tabs and
 * portrait touch layouts retain their compact single-column treatment. */
@media (orientation: landscape) {
  body[data-surface="tablet"]:has(.plan-owned-root[data-mode="track"]) .content {
    max-width: none;
    margin: 0;
    padding: 20px 28px;
    padding-bottom: 20px;
  }
  body[data-surface="tablet"] .plan-owned-root[data-mode="track"] .plan-layout {
    grid-template-columns: minmax(0, 1fr) minmax(360px, 760px);
  }
}
.track-history-heading { align-self: center; color: var(--ink-muted); font-weight: 700; text-align: center; }
.track-pace-heading { min-width: 0; }
.track-history-cell { min-width: 0; align-self: stretch; display: flex; align-items: center; }
.track-history-chart { width: 100%; height: auto; max-height: 5.5rem; overflow: visible; color: var(--chart-1); }
.track-history-chart.track-history-up { color: var(--neg); }
.track-history-chart.track-history-down { color: var(--pos); }
.track-line-baseline { fill: none; stroke: var(--hairline); stroke-width: 1; vector-effect: non-scaling-stroke; }
.track-line-series { fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.track-line-area { stroke: none; pointer-events: none; }
.track-line-value-leader { stroke: var(--accent); stroke-width: 1.5; stroke-dasharray: 2.5 3; vector-effect: non-scaling-stroke; }
.track-line-node circle { fill: var(--card); stroke: currentColor; stroke-width: 2; vector-effect: non-scaling-stroke; }
.track-line-value { fill: var(--ink); stroke: var(--card); font-size: var(--fs-body); }
.track-line-month { fill: var(--ink-muted); font-size: var(--fs-body); }
/* A chart drawn 1:1 (its viewBox width equals its painted width, marked by
 * data-scale="1") has NO coordinate scaling left, so the token is the rendered
 * size: these labels come out at exactly the Track subcategory row's size on
 * every monitor. The previous `* .66` / `* .54` corrections fitted two specific
 * widths on a curve that moves with the panel, which is why the mismatch kept
 * coming back (operator 31/08/2026). */
.track-history-chart[data-scale="1"] :is(.track-line-value, .track-line-month) {
  font-size: var(--fs-track-chart-label);
}
.track-average-value { color: var(--accent); font-weight: 500; }
.track-activity-value { font-weight: 700; }
.track-row-metrics { display: inline-flex; align-items: center; gap: .45rem; margin-inline-start: .55rem; color: var(--ink); font-size: var(--fs-micro); font-weight: 500; }
.track-row-metrics small { white-space: nowrap; }
.track-group-average { display: inline-block; margin-inline-start: .5rem; color: var(--ink); font-size: var(--fs-micro); font-weight: 500; white-space: nowrap; }
.track-average-stack { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; min-width: 0; line-height: 1.1; white-space: nowrap; }
.track-average-label { color: var(--ink-muted); font-size: var(--fs-micro); font-weight: 700; letter-spacing: .08em; line-height: 1; text-transform: uppercase; }
.track-average-value { color: var(--accent); font-size: var(--fs-micro); font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1.1; white-space: nowrap; }
.track-spark {
  display: inline-flex;
  align-items: flex-end;
  gap: 2px;
  min-width: 3.8rem;
  height: 1.15rem;
  padding-block-end: 1px;
  box-sizing: border-box;
  vertical-align: middle;
  border-bottom: 1px solid var(--hairline);
  color: var(--chart-1);
}
.track-spark-bar { flex: 1 1 0; min-width: 2px; height: var(--spark-height, 8%); background: currentColor; border-radius: 2px 2px 0 0; }
.track-spark-up { color: var(--neg); }
.track-spark-down { color: var(--pos); }
.track-spark-flat { color: var(--chart-1); }
.track-pace { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; width: 1.5rem; height: 1.5rem; }
.track-pace .ico { width: 1.35rem; height: 1.35rem; }
.track-pace-up { color: var(--neg); }
.track-pace-flat { color: var(--flag-purple); }
.track-pace-down { color: var(--pos); }
.track-pace-flat-line { display: block; width: 1rem; height: 2px; border-radius: 2px; background: currentColor; }
.track-swipe .plan-mrow { cursor: pointer; }
.plan-track-panel .track-projected strong { color: var(--accent); }
.track-trend-copy { margin: .55rem 0 0; font-style: italic; }
.track-sparkline { display: flex; align-items: end; gap: 4px; height: 54px; margin-top: .75rem; padding: .35rem .2rem 0; border-bottom: 1px solid var(--hairline); }
.track-sparkline span { flex: 1 1 0; min-height: 4px; background: var(--chart-1); border-radius: var(--radius-s) var(--radius-s) 0 0; }
.track-sparkline.track-spark-up span { background: var(--neg); }
.track-sparkline.track-spark-down span { background: var(--pos); }
.plan-owned-root[data-mode="track"] .plan-table[data-mode="track"] .plan-row > :not(.c-cat) {
  align-self: center; justify-self: center; text-align: center;
}
.plan-owned-root[data-mode="track"] .plan-table[data-mode="track"] .track-head > span { font-weight: 700; }
.track-inspector-chart { margin-top: .75rem; }
.track-inspector-chart .track-history-chart { display: block; width: 100%; max-height: none; }
.track-metrics > .panel-row,
.track-metrics > .track-trend-copy { width: 100%; max-width: 25rem; margin-inline: 0; box-sizing: border-box; }
.track-metrics > .panel-row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  justify-content: stretch;
  column-gap: 1.5rem;
  text-align: left;
}
.track-metrics > .panel-row > :is(span, strong),
.track-metrics > .track-trend-copy { text-align: left; }
.track-history-list { width: 100%; max-width: 25rem; margin: .35rem 0 0; }
.track-history-list .panel-row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: 1.5rem;
  justify-content: stretch;
  text-align: left;
}
.track-history-list .panel-row > :is(span, strong) { text-align: left; }
.track-history-list .panel-row strong { font-variant-numeric: tabular-nums; }
/* Desktop Track panels wrap from one column to two according to their own
 * available width. The earlier 150rem viewport switch missed the operator's
 * 4K Firefox window at its active Windows/browser scale: the panel was 1140px
 * wide (ample for 42rem + 20rem + the gap) although the CSS viewport was only
 * 2304px. Intrinsic flex bases make the supplied monitor two-column without
 * squeezing the narrower 1920px panel or binding the result to DPR/zoom. */
body[data-surface="desktop"] :is(.track-category-panel, .track-overview-panel) {
  flex-flow: row wrap;
  align-items: flex-start;
  gap: 12px;
}
body[data-surface="desktop"] .track-category-panel > .insp-head { flex: 1 0 100%; }
/* "Track this month" and "Spending history" are a PAIR that shares one row.
 * The old 42rem/20rem intrinsic bases only fitted side by side on the
 * operator's 4K panel; at 1920px the panel is 756px wide, so the two cards
 * stacked and the whole right column read as one narrow strip. Percentage
 * bases keep them side by side at every panel width the two-column layout is
 * used at, and the chart below spans the full row (operator 31/08/2026). */
body[data-surface="desktop"] :is(.track-category-panel, .track-overview-panel) > .track-metrics {
  flex: 1 1 calc(60% - 6px); min-width: 0;
}
body[data-surface="desktop"] :is(.track-category-panel, .track-overview-panel) > .track-history {
  flex: 1 1 calc(40% - 6px); min-width: 0;
}
body[data-surface="desktop"] :is(.track-category-panel, .track-overview-panel) > .track-chart-card {
  flex: 1 0 100%; min-width: 0;
}
/* The 150rem width-fitted label correction is retired: a 1:1 chart needs no
 * per-width compensation at all (see .track-history-chart[data-scale="1"]). */
.track-note-field { display: grid; gap: .45rem; margin-top: .8rem; color: var(--ink-muted); font-size: var(--fs-label); font-weight: 700; }
.track-note-field textarea {
  min-height: 5rem; height: 5rem; max-height: 10rem; overflow-y: hidden; resize: none;
  border-radius: var(--radius-s); color: var(--ink); font-weight: 400; text-align: left;
}
.track-note-field .btn { justify-self: end; min-height: var(--ctl-h-md); }
.track-note-trigger { position: relative; display: inline-flex; align-items: center; justify-content: center; margin-inline-start: .45rem; color: var(--accent); line-height: 1; vertical-align: calc((1cap - 1.25em) / 2); }
.track-note-trigger > .ico { width: 1.25em; height: 1.25em; vertical-align: 0; }
.track-note-popover {
  position: absolute; z-index: 12; left: 50%; bottom: calc(100% + .55rem); width: max-content; max-width: 20rem;
  padding: .65rem .75rem; border: 1px solid color-mix(in srgb, var(--flag-purple) 34%, var(--chrome-edge)); border-radius: var(--radius-s);
  background: color-mix(in srgb, var(--card) 96%, var(--flag-purple)); color: var(--ink); box-shadow: 0 10px 28px color-mix(in srgb, var(--ink) 18%, transparent); font-size: var(--fs-label);
  font-weight: 500; line-height: 1.35; white-space: normal; overflow-wrap: anywhere;
  opacity: 0; visibility: hidden; transform: translate(-50%, .25rem); pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease-standard), transform var(--dur-fast) var(--ease-standard), visibility var(--dur-fast);
}
.track-window input { accent-color: var(--accent); }
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .mrow-name {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; grid-template-rows: auto auto;
  align-items: center; column-gap: .45rem; row-gap: 2px; min-width: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .mrow-label {
  grid-column: 1; grid-row: 1; min-width: 0; overflow: visible; text-overflow: clip;
  white-space: normal; overflow-wrap: anywhere;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .track-average-inline {
  grid-column: 1; grid-row: 2; justify-self: start; align-self: start; color: var(--accent);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .track-pace {
  grid-column: 2; grid-row: 1 / span 2; align-self: center; width: 1.7rem; height: 100%; min-height: 2rem;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .track-pace .ico { width: 1.7rem; height: 1.7rem; }
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .plan-mobile .plan-mgroup,
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .plan-mobile .plan-mrow {
  grid-template-columns: minmax(0, 1fr) 5.8rem 5.8rem;
  gap: 4px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] :is(.track-history-col, .track-history-cell) {
  display: none;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .plan-mobile .mgcol-label,
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .plan-mobile .mgcol-total {
  text-align: center;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .plan-mobile .mcell {
  justify-self: center;
  text-align: center;
}
/* Track group headers carry a second line for the annual average.  Keep that
 * label inside the flexible name column on touch; the previous inline flex
 * let it paint over the fixed Spent/Upcoming columns on narrow phones. */
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .plan-mobile .mgroup-name {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: auto auto;
  column-gap: 4px;
  row-gap: 2px;
  min-width: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .plan-mobile .mgroup-caret {
  grid-column: 1;
  grid-row: 1;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .plan-mobile .mgroup-txt {
  grid-column: 2;
  grid-row: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .plan-mobile .track-group-average {
  grid-column: 2;
  grid-row: 2;
  min-width: 0;
  margin-inline-start: 0;
  align-items: flex-start;
  overflow: visible;
}

/* S13 Track touch repair: the two derived value columns yield a little width
 * to the category rail, then a genuinely long title gets its own full row.
 * The latter is a deliberate fallback before truncation; ordinary rows retain
 * one shared value grid so every number stays on the same right-hand axes. */
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .plan-mobile .plan-mgroup,
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .plan-mobile .plan-mrow {
  grid-template-columns: minmax(0, 1fr) 4.75rem 4.75rem;
  gap: 1px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .plan-mobile .mrow-label {
  overflow: visible; text-overflow: clip; white-space: normal; overflow-wrap: anywhere;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .plan-mobile .track-long-title {
  grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .plan-mobile .track-long-title .mrow-name {
  grid-column: 1; grid-row: 1; width: 100%;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .plan-mobile .track-long-title .mrow-cols {
  display: grid; grid-column: 1; grid-row: 2;
  grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 2px; width: 100%;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .plan-mobile .track-long-title .mrow-label {
  overflow: visible; text-overflow: clip; white-space: normal;
}

/* Track touch Activity revamp: Category | Average | pace | Activity. The
 * fixed numeric rails keep every group total and category amount aligned. */
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .plan-mobile .plan-mgroup,
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .plan-mobile .plan-mrow {
  grid-template-columns: minmax(0, 1fr) 4.75rem 2rem 4.75rem;
  gap: 1px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .mrow-name {
  display: block; min-width: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .plan-mobile :is(.plan-mrow, .plan-mgroup) {
  align-items: center;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .plan-mobile :is(.mrow-name, .mrow-cols, .mcell, .track-pace, .mgroup-name, .mgcol) {
  align-self: center;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .plan-mobile .plan-mrow:not(.track-long-title) :is(.mrow-name, .mcell) {
  display: flex; align-items: center; height: 1.7rem;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .plan-mobile :is(.mrow-cols, .mgcol) {
  align-items: center;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .track-pace { height: auto; }
body:is([data-surface="phone"], [data-surface="tablet"]) .plan-owned-root[data-mode="track"] .track-pace {
  grid-column: 3; align-self: center; justify-self: center; width: 1.7rem; height: 1.7rem; min-height: 0;
}
.track-average-cell,
.track-average-col { grid-column: 2; color: var(--accent); font-weight: 400; }
.track-activity-cell,
.track-activity-col { grid-column: 4; }
.track-average-col .mgcol-total,
.track-average-cell.mcell-num { color: var(--accent); font-weight: 400; }
.track-average-col .mgcol-label { color: var(--accent); font-weight: 400; }
.track-activity-col .mgcol-label { text-transform: none; }
@media (orientation: landscape) and (min-width: 50rem) {
  body[data-surface="tablet"] .plan-owned-root[data-mode="track"] .plan-mobile .plan-mgroup,
  body[data-surface="tablet"] .plan-owned-root[data-mode="track"] .plan-mobile .plan-mrow {
    grid-template-columns: minmax(8.5rem, 1fr) minmax(24rem, 1.8fr) 5.25rem 2rem 5.25rem;
    gap: 4px;
  }
  body[data-surface="tablet"] .plan-owned-root[data-mode="track"] :is(.track-history-col, .track-history-cell) {
    display: flex; grid-column: 2; min-width: 0;
  }
  body[data-surface="tablet"] .plan-owned-root[data-mode="track"] .track-history-col {
    align-items: center; justify-content: center;
  }
  body[data-surface="tablet"] .plan-owned-root[data-mode="track"] .track-average-col,
  body[data-surface="tablet"] .plan-owned-root[data-mode="track"] .track-average-cell { grid-column: 3; }
  body[data-surface="tablet"] .plan-owned-root[data-mode="track"] .track-pace { grid-column: 4; }
  body[data-surface="tablet"] .plan-owned-root[data-mode="track"] .track-activity-col,
  body[data-surface="tablet"] .plan-owned-root[data-mode="track"] .track-activity-cell { grid-column: 5; }
  body[data-surface="tablet"] .plan-owned-root[data-mode="track"] .track-history-chart { max-height: 5rem; }
  /* The 80px SVG rail leaves a 2px inline-line descent in Chromium's grid
   * track. Offset the text/chart cells by that measured descent so their
   * visual centres match the row and the independently centred pace glyph. */
  body[data-surface="tablet"] .plan-owned-root[data-mode="track"] .plan-mrow :is(.mrow-name, .track-history-cell, .mcell) {
    transform: translateY(2px);
  }
}
body:is([data-surface="phone"], [data-surface="tablet"]) .track-swipe-actions {
  top: 4px; bottom: 4px; right: 6px; gap: 4px; align-items: center;
}
.track-swipe-actions .swipe-btn {
  width: 5.5rem; min-height: var(--ctl-h-md); padding: 0; border-radius: var(--radius-s);
  background: transparent; color: inherit;
}
.track-swipe-actions .swipe-btn > span {
  display: flex; align-items: center; justify-content: center;
  width: 100%; min-height: 2.25rem; border-radius: var(--radius-s);
  transition: transform var(--dur-fast) var(--ease-spring), filter var(--dur-fast) var(--ease-standard);
}
.track-swipe .plan-mrow { min-height: calc(var(--ctl-h-md) + 12px); }
.track-swipe-actions .swipe-rename-category > span {
  background: var(--control-accent); color: var(--control-accent-ink);
}
.track-swipe-actions .swipe-hide-category > span {
  background: var(--control-secondary-bg); color: var(--control-secondary-ink);
}
.track-swipe.swiped .plan-mrow { transform: translateX(calc(-11rem - 10px)); }
.track-swipe-actions .swipe-btn:active { transform: none; filter: none; }
.track-swipe-actions .swipe-btn:active > span { transform: scale(.96); filter: brightness(.92); }

.track-screen-title { display: flex; align-items: center; gap: .2rem; }
body:is([data-surface="phone"], [data-surface="tablet"]) .track-info {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--ctl-h-md); height: var(--ctl-h-md); min-width: var(--ctl-h-md);
  margin: 0; padding: 0; border: 0; border-radius: 999px;
  background: transparent; color: var(--accent); cursor: pointer;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .track-info .ico { width: 1.15rem; height: 1.15rem; }
body:is([data-surface="phone"], [data-surface="tablet"]) .track-info[aria-expanded="true"] {
  background: transparent; color: var(--accent);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .track-info:active { transform: scale(.9); }
.track-info-modal { width: min(420px, 94vw); }
.track-info-modal .picker-head { justify-content: space-between; }
.track-info-copy { display: flex; flex-direction: column; gap: 10px; color: var(--ink); }
.track-info-copy p { margin: 0; }

/* Explanatory touch controls share one calm, centred treatment.  The opener
 * remains a plain information glyph while expanded; the card itself is led by
 * a friendly themed icon and any non-interactive tap dismisses it. */
body:is([data-surface="phone"], [data-surface="tablet"]) :is(.sheet-overlay, .sub-backdrop):has(.touch-info-modal) {
  align-items: center; justify-content: center;
  padding: max(18px, env(safe-area-inset-top)) max(18px, env(safe-area-inset-right)) max(18px, env(safe-area-inset-bottom)) max(18px, env(safe-area-inset-left));
}
body:is([data-surface="phone"], [data-surface="tablet"]) .touch-info-trigger[aria-expanded="true"],
body:is([data-surface="phone"], [data-surface="tablet"]) .touch-info-trigger.on {
  background: transparent; background-image: none; color: var(--accent); box-shadow: none;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .touch-info-modal {
  width: min(34rem, calc(100vw - 36px)); max-height: min(82dvh, calc(var(--vvh, 100dvh) - 36px));
  border: 1px solid var(--hairline); border-radius: var(--radius); padding: 16px;
}
.touch-info-head { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 10px; }
.touch-info-head > span:nth-child(2) { min-width: 0; }
.touch-info-head small { display: block; margin-top: 2px; color: var(--ink-muted); font-size: var(--fs-label); font-weight: 500; }
.touch-info-badge {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--ctl-h-md); height: var(--ctl-h-md); aspect-ratio: 1;
  border-radius: 50%; background: var(--control-secondary-bg); color: var(--control-secondary-ink);
}
.touch-info-badge .ico { width: 1.35rem; height: 1.35rem; }
body:is([data-surface="phone"], [data-surface="tablet"]) .touch-info-modal .picker-x {
  width: var(--ctl-h-md); height: var(--ctl-h-md); padding: 0; border: 0;
  background: var(--close-control-bg); color: var(--close-control-ink);
  box-shadow: var(--close-control-shadow);
}
/* Track insight close consumes the same round close face as every other X. */
html[data-desktop-theme] body[data-surface] .track-insights-modal .picker-x[data-control-kind="neutral"],
html[data-touch-theme] body[data-surface] .track-insights-modal .picker-x[data-control-kind="neutral"] {
  background: var(--close-control-bg) !important; background-image: none !important;
  color: var(--close-control-ink) !important;
  border: 0 !important; border-color: transparent !important;
  box-shadow: var(--close-control-shadow) !important;
}
body:is([data-surface="phone"], [data-surface="tablet"]) :is(.touch-info-trigger, .touch-info-modal .picker-x):focus,
body:is([data-surface="phone"], [data-surface="tablet"]) :is(.touch-info-trigger, .touch-info-modal .picker-x):focus-visible {
  outline: none; box-shadow: none;
}
.track-insights-modal { overflow-y: auto; overscroll-behavior: contain; }
.track-insight-lead { margin: 4px 0; padding: .65rem .75rem; border-radius: var(--radius-s); background: var(--control-secondary-bg); color: var(--control-secondary-ink); font-weight: 700; }
.track-insight-chart { padding: .25rem .2rem; border-bottom: 1px solid var(--hairline); }
.track-insight-chart .track-history-chart { max-height: none; }
.track-insight-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.track-insight-grid > div { display: flex; flex-direction: column; gap: 2px; min-width: 0; padding: .55rem .65rem; border-radius: var(--radius-s); background: var(--chip); }
.track-insight-grid span { color: var(--ink-muted); font-size: var(--fs-label); }
.track-insight-grid strong { overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
.track-insight-grid .track-insight-primary { background: var(--control-secondary-bg); color: var(--control-secondary-ink); }
.track-insight-grid .track-insight-primary span { color: currentColor; }
.track-insight-note {
  margin-top: .75rem; padding: .65rem .75rem; border: 1px solid var(--hairline);
  border-radius: var(--radius-s); background: var(--chip); color: var(--ink);
}
.track-insight-note-label { display: block; color: var(--ink-muted); font-size: var(--fs-label); font-weight: 700; }
.track-insight-note p { margin: .25rem 0 0; white-space: pre-wrap; overflow-wrap: anywhere; }

/* S01 spending follow-up (26/08/2026): the semantic control stamp is applied
 * after view CSS, so approval actions need their own late, scoped face.  Keep
 * the banner and scheduled disclosure on the same register control ruler. */
html[data-desktop-theme="current"] body[data-surface="desktop"] .approve-topbar {
  border-radius: 999px;
}
html[data-desktop-theme="current"] body[data-surface="desktop"] .approve-topbar
  :is(.approve-topbar-go, .approve-topbar-approve)[data-control-kind="surface"] {
  appearance: none;
  background: var(--accent) !important;
  color: var(--accent-ink) !important;
  border: 0 !important;
  border-color: transparent !important;
  border-radius: 999px;
}
html[data-desktop-theme="current"] body[data-surface="desktop"]
  .reg-sched-band.scheduled-fill .sched-row.reg-row .sched-occ-slot {
  margin-inline-start: 0;
}
/* Expanded date lozenges share the scheduled row's blue rail. The original
 * list carried all of its vertical padding below the chips, leaving their
 * measured centre 4.8px above the list centre. Split that padding evenly so
 * every expanded lozenge is centred in its own rail without changing row
 * height or the collapsed disclosure geometry. */
body[data-surface="desktop"] .reg-sched-band.scheduled-fill .sched-occ-list {
  align-items: center;
  padding-block: .3rem;
  margin-top: 0;
}

/* Screenshot corrections (28/08/2026): the repeat sheet owns this geometry.
 * Tokens keep the same hierarchy in Current light and dark themes. */
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .sheet-footer .btn {
  border-radius: var(--radius-card-touch);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .sched-amt-row {
  display: grid;
  grid-template-columns: var(--ctl-h-md) minmax(0, 1fr) var(--ctl-h-md);
  align-items: center;
  gap: 8px;
  padding-block: .75rem;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .sched-amt-row > .field-ico {
  grid-column: 1;
  justify-self: center;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .sched-amt-row > .field-main {
  grid-column: 2;
  display: grid;
  justify-items: stretch;
  gap: 6px;
  min-width: 0;
  text-align: center;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .sched-amt-wrap {
  width: 100%; align-items: stretch;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .sched-amt-row .field-amt {
  width: 100%; text-align: center;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .sched-amt-hint {
  grid-column: 1 / -1;
  width: auto; margin: 0; text-align: center;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .sched-recur {
  padding-block: 12px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .sched-recur > .sched-section {
  padding-block: .3rem; text-align: center;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .sched-recur-sentence {
  justify-content: center;
  gap: 4px;
  padding-block: 10px;
  text-align: center;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .recur-pill {
  min-height: var(--ctl-h-md);
  padding-inline: .35rem;
  border: 0 !important;
  border-radius: 0;
  background: transparent !important;
  color: var(--accent) !important;
  box-shadow: none !important;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .sched-recur > :is(.field-row, .field-btn) {
  border-top: 0;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"])
  .sched-sheet .field-row.sched-stepped-toggle {
  display: grid;
  grid-template-columns: max-content var(--touch-switch-track);
  justify-content: end;
  column-gap: 24px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .seg-row {
  border: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .stepped-step-field {
  text-align: center;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .stepped-step .inp {
  border: 0;
  outline: none;
  box-shadow: none;
  text-align: center;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sub-backdrop:has(> .recurrence-choice-modal) {
  align-items: center;
  padding-block: max(16px, env(safe-area-inset-top)) max(16px, env(safe-area-inset-bottom));
}
body:is([data-surface="phone"], [data-surface="tablet"]) .recurrence-choice-modal {
  background: var(--control-secondary-bg);
  border: 1px solid var(--chrome-edge);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .recurrence-choice-modal .pick-row {
  border-top: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .recurrence-choice-modal .pick-row:has(> .pick-row-check) {
  display: grid;
  grid-template-columns: 1.15em minmax(0, 1fr);
  align-items: center;
  column-gap: .25rem;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .recurrence-choice-modal .pick-row-check {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--accent);
  font-weight: 700;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .recurrence-choice-modal .pick-row-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: normal;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .sched-whendue .sched-num {
  display: grid;
  grid-template-columns: auto auto auto;
  justify-content: center;
  align-items: center;
  column-gap: 8px;
  border-top: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .sched-whendue .sched-num > .field-label {
  grid-column: 1;
  justify-self: start;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .sched-whendue .sched-num .sched-num-wrap {
  display: contents;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .sched-whendue .sched-num .sched-num-inp {
  grid-column: 2;
  justify-self: center;
  text-align: center;
  border: 0;
  outline: none;
  box-shadow: none;
  color: var(--accent);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .sched-sheet .sched-whendue .sched-num .sched-num-hint {
  grid-column: 3;
  justify-self: start;
  text-align: left;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .day-grid-picker {
  width: min(22rem, calc(100vw - 32px));
}
body:is([data-surface="phone"], [data-surface="tablet"]) .day-grid-picker .wheel-list {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 6px;
  max-height: none;
  overflow: visible;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .day-grid-picker .wheel-row {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--ctl-h-md);
  width: 100%;
  min-height: var(--ctl-h-md);
  padding: 0;
  border-radius: var(--radius-s);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .day-grid-picker .wheel-row.sel {
  background: var(--control-selected-bg);
  color: var(--control-selected-ink);
}
body[data-surface="desktop"] .track-note-field textarea.track-note-input {
  border-radius: var(--radius-s) !important;
}
/* D-131's semantic observer classifies text containing “reject” as danger.
 * This focused popover deliberately uses the softer pale-red danger surface,
 * so its explicit action paint wins after runtime stamping while disabled
 * controls still fall back to the shared neutral state. */
body[data-surface="desktop"] .ctx-menu.approval-info-menu .ctx-row {
  min-height: var(--ctl-h-lg);
}
body[data-surface="desktop"] .ctx-menu.approval-info-menu .approval-info-approve {
  background: var(--accent) !important;
  color: var(--accent-ink) !important;
}
body[data-surface="desktop"] .ctx-menu.approval-info-menu .approval-info-reject:not(:disabled) {
  background: var(--neg-pill) !important;
  color: var(--neg) !important;
  border-color: color-mix(in srgb, var(--neg) 34%, var(--hairline)) !important;
}

/* S04 Spending follow-up (26/08/2026): touch register cleanup.  Keep every
 * change token-derived and scoped to the Current touch surfaces so the desktop
 * register and both colour themes retain their established geometry. */
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-reference-context {
  gap: 8px;
  margin-bottom: 8px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]):has(.clearing-reference) .reg-reference-context {
  gap: 8px;
  margin-bottom: 8px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .reg-touch-toolbar {
  margin-bottom: 8px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .swipe-actions {
  top: 6px;
  bottom: 6px;
}
html[data-touch-theme="current"] body:is([data-surface="phone"], [data-surface="tablet"]) .swipe-btn {
  min-height: var(--ctl-h-md);
  border-radius: 999px;
}
/* A swipe action keeps a 44px tap target but never stretches its painted face
 * through a multi-line Spending row. The stationary wrapper owns the separator
 * so translating the row cannot pull the line out from under Edit/Delete. */
html[data-touch-theme="current"] body[data-route="spending"]:is([data-surface="phone"], [data-surface="tablet"]) .swipe-wrap:has(> .txn-row.compact) {
  border-top: 1px solid var(--hairline);
}
html[data-touch-theme="current"] body[data-route="spending"]:is([data-surface="phone"], [data-surface="tablet"]) .swipe-wrap:has(> .txn-row.compact) > .txn-row {
  border-top: 0;
}
html[data-touch-theme="current"] body[data-route="spending"]:is([data-surface="phone"], [data-surface="tablet"]) .swipe-wrap:has(> .txn-row.compact) > .swipe-actions {
  align-items: center;
}
html[data-touch-theme="current"] body[data-route="spending"]:is([data-surface="phone"], [data-surface="tablet"]) .swipe-wrap:has(> .txn-row.compact) > .swipe-actions > .swipe-btn {
  height: min(var(--ctl-h-md), 100%);
  min-height: 0;
  max-height: 100%;
}
/* Reset is destructive but should read as a quiet warning in Settings rather
 * than a saturated primary action. Both themes resolve through semantic red
 * tokens and a faint token-mixed outline. */
body[data-surface] .rta-reset-btn.quiet-danger {
  background: var(--neg-pill) !important;
  color: var(--neg) !important;
  border: 1px solid color-mix(in srgb, var(--neg) 34%, var(--hairline)) !important;
  box-shadow: none !important;
}
/* The touch status rail has a more-specific 44px display rule than the
 * app-wide cleared-off suppression. Remove both the icon and its reserved
 * rail when the family-wide feature is disabled. */
body[data-cleared="off"] :is(.txn-status-rail, .txn-status-rail .rec-lock) {
  display: none !important;
}

/* S13 sheet owns the generic .sheet-scroll hook only for gesture arbitration;
 * restore the Accounts page's compact flow so a title/list card is not treated
 * like a transaction form. */
body:is([data-surface="phone"], [data-surface="tablet"]) .accounts-sheet-content {
  display: block; flex: 0 1 auto; min-height: 0; gap: 0; padding: 8px 16px 104px;
  overflow-y: auto; overflow-x: hidden; touch-action: pan-y;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .accounts-sheet-content .accounts-sheet-grip {
  display: block; margin: 0 auto 4px;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .route-header-row > .account-title-lozenge {
  flex: 0 1 auto; width: fit-content; max-width: min(100%, 24rem); min-height: 0;
  text-align: left; justify-content: flex-start;
}

/* v1.214.44 — touch Edit Payee visual reset. The data/writer workspace remains
 * shared with desktop; touch presents a quiet stack of progressive cards. */
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-workspace-head {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-areas: "back title close";
  gap: .5rem; padding: .25rem 0 .7rem;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-workspace-head > span {
  grid-area: title;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-align: center; font-size: var(--fs-h2);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-workspace-head .payee-touch-back {
  min-width: 0; min-height: var(--ctl-h-md); padding-inline: .8rem;
  white-space: nowrap; margin: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-workspace-head .payee-touch-back { grid-area: back; }
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-workspace-head .payee-head-close { grid-area: close; }
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager.payee-touch-show-index .payee-workspace-head {
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "title close";
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-workspace-head .payee-head-close {
  box-sizing: border-box; width: var(--ctl-h-md); min-width: var(--ctl-h-md);
  height: var(--ctl-h-md); min-height: var(--ctl-h-md); padding: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-subject-source {
  display: none;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-detail {
  display: flex; flex-direction: column; gap: .75rem;
  padding: .75rem 0 calc(var(--ctl-h-lg) + max(10px, env(safe-area-inset-bottom)) + 1rem);
  scroll-padding-block: .75rem calc(var(--ctl-h-lg) + max(10px, env(safe-area-inset-bottom)) + 1rem);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-primary-card,
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-option-card {
  box-sizing: border-box; width: 100%; margin: 0; padding: .9rem; gap: .65rem;
  border: 1px solid var(--hairline); border-radius: var(--radius); background: var(--card);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-card-intro {
  display: grid; grid-template-columns: var(--ctl-h-lg) minmax(0, 1fr);
  align-items: center; gap: .75rem; padding-bottom: .25rem;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-card-intro > span:last-child {
  display: grid; gap: .15rem; min-width: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-card-intro small,
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-disclosure small {
  color: var(--ink-muted); line-height: 1.3;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-avatar {
  display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box;
  width: var(--ctl-h-lg); height: var(--ctl-h-lg); aspect-ratio: 1; border-radius: 50%;
  background: var(--control-secondary-bg); color: var(--control-secondary-ink);
  font-size: var(--fs-h3); font-weight: 800;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-primary-card > .payees-section-h {
  margin: .15rem 0 0; font-size: var(--fs-label);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-rename-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .55rem;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-rename-row > * {
  min-height: var(--ctl-h-md);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-rename-help { margin: 0; }
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-disclosure {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center;
  gap: .75rem; width: 100%; min-height: var(--ctl-h-lg); padding: 0;
  border: 0; background: transparent; color: var(--ink); font: inherit; text-align: left;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-disclosure > span:first-child {
  display: grid; gap: .2rem; min-width: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-disclosure-caret {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.5rem; height: 1.5rem; color: var(--accent); font-size: var(--fs-h2); rotate: 0deg;
  transition: rotate var(--dur-fast) var(--ease-standard);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-option-card.open .payee-touch-disclosure-caret { rotate: 90deg; }
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-option-body {
  display: flex; flex-direction: column; gap: .65rem; padding-top: .8rem;
  margin-top: .15rem; border-top: 1px solid var(--hairline);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-touch-option-body[hidden] { display: none; }
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-extras-h {
  box-sizing: border-box; min-height: var(--ctl-h-lg); padding: 0; border-radius: var(--radius-s);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-extras-body {
  padding-top: .8rem; border-top: 1px solid var(--hairline);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager :is(.payee-action-tall, .payee-apply, .payee-delete) {
  min-height: var(--ctl-h-md); height: auto; font-size: var(--fs-body);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payees-footer > .btn {
  min-height: var(--ctl-h-lg); height: var(--ctl-h-lg); font-size: var(--fs-body);
}
body:is([data-surface="phone"], [data-surface="tablet"]) .payee-touch-manager .payee-delete-section {
  border-top: 1px solid var(--hairline); padding-bottom: .9rem;
}
@media (min-width:56.25rem) and (orientation:landscape) {
  body[data-surface="tablet"] .payee-touch-manager.payee-workspace-locked .payee-touch-detail {
    width: 100%; float: none; padding-left: 0;
  }
}

/* Settings' compact information rows and read-more actions replace long walls
 * of Family/data prose while keeping the full explanation one tap away. */
.settings-brief-row { display: flex; align-items: center; justify-content: space-between; gap: .75rem; padding-block: .25rem; }
.settings-drive-summary > p { margin-block: .25rem; }
.settings-drive-summary .settings-backup-name { display: flex; flex-direction: column; gap: .1rem; margin-top: .45rem; }
.settings-drive-summary .settings-backup-name strong { overflow-wrap: anywhere; }
.settings-drive-summary .settings-backup-type { margin-top: .1rem; }
.settings-read-actions { display: flex; flex-wrap: wrap; gap: .25rem 1rem; margin-top: .35rem; }
.settings-read-more,
.settings-read-actions .link-btn {
  min-height: var(--ctl-h-md);
  padding-inline: 0;
  background: transparent !important;
  border: 0 !important;
  color: var(--accent) !important;
  text-align: left;
}
.settings-collapse[data-settings-section="data-tools"] { margin-top: .35rem; border-top: 1px solid var(--hairline); padding-top: .2rem; }
.settings-collapse[data-settings-section="data-tools"] .settings-collapse-h { min-height: var(--ctl-h-md); }
.settings-collapse[data-settings-section="data-tools"] .settings-collapse-sub { margin-block: .1rem .25rem; }
.settings-collapse[data-settings-section="data-tools"] .settings-collapse-body { margin-top: .2rem; }
.settings-collapse[data-settings-section="data-tools"] .data-actions { margin-block: .2rem .35rem; gap: .5rem; }
.holiday-end { text-align: left; color: var(--ink); }

/* v1.214.45 screenshot follow-up. These final owners intentionally sit after
 * semantic control stamping: they are explicit quiet-control requirements,
 * not new control roles. */

/* release-owner:register — the compact ALL control was present in the DOM but
 * the generic visually-hidden child rule reduced its only visible child to a
 * 1px square. Restore the literal account entry point without a hardcoded
 * viewport or colour. */
body:is([data-surface="phone"], [data-surface="tablet"])
  .spending-title-row .reg-accounts-trigger > .reg-accounts-all {
  position: static; width: auto; height: auto; padding: 0; margin: 0;
  overflow: visible; clip: auto; white-space: nowrap; border: 0;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--accent);
}
body:is([data-surface="phone"], [data-surface="tablet"])
  .spending-title-row .reg-accounts-trigger {
  height: var(--ctl-h-md); min-height: var(--ctl-h-md); align-self: center;
}
/* Track's title-level All is the same control face as Spending's All: clear
 * blurple text on a quiet 44px target.  This final owner intentionally wins over the
 * generic selected-control fill because the active label itself is the state. */
html[data-touch-theme] body:is([data-surface="phone"], [data-surface="tablet"])
  .plan-route-title .plan-title-filter .plan-filter-trigger.control-selected[data-control-kind="selected"] {
  width: var(--ctl-h-md); min-width: var(--ctl-h-md); height: var(--ctl-h-md); min-height: var(--ctl-h-md);
  padding: 0; border: 0 !important; border-radius: var(--radius-s);
  background: transparent !important; color: var(--accent) !important;
  box-shadow: none !important;
}

/* Category chips are one cross-surface format: clear face, blurple outline, and
 * blurple text (operator 01/09/2026: "new look for the spending>transactions>
 * subcategory glyph: light blurple text (same colour as the outline of the
 * lozenge)"). The ink is its own token rather than a second reference to the
 * outline, because a 1px edge and 13px text do not share a contrast floor: see
 * --status-lozenge-ink, which is the outline value itself in dark and the full
 * accent in light, where the outline mix measures only 2.26:1 on white.
 * Register rows and transaction detail fields use different markup but must
 * paint identically whenever the compact category-lozenge format appears. */
:is(.txn-cat-inline, .txn-cat-inline-first-line, .field-chip) {
  box-sizing: border-box;
  background: transparent;
  border: 1px solid var(--status-lozenge-outline);
  color: var(--status-lozenge-ink);
}

/* release-owner:shared-button — every standalone X uses the same round face. */
:is(.picker-x, .sheet-x, .install-bar-x, .picker-search-clear,
  .reg-search-clear, .reg-search-inline-clear, .reg-search-dismiss,
  .reg-search-dismiss-inline, .approve-selection-clear, .insp-close,
  .rename-rule-del, .cover-x, .fs-c-del, .rank-omit, .rec-step-delete) {
  box-sizing: border-box;
  aspect-ratio: 1;
  border-radius: 50% !important;
  background: var(--close-control-bg) !important;
  color: var(--close-control-ink) !important;
  border: 0 !important;
  outline: none !important;
  box-shadow: var(--close-control-shadow) !important;
}
@media (pointer: fine) {
  :is(.picker-x, .sheet-x, .install-bar-x, .picker-search-clear,
    .reg-search-clear, .reg-search-inline-clear, .reg-search-dismiss,
    .reg-search-dismiss-inline, .approve-selection-clear, .insp-close,
    .rename-rule-del, .cover-x, .fs-c-del, .rank-omit, .rec-step-delete):focus-visible { color: var(--accent) !important; }
}

/* release-owner:settings — touch Payee chrome and low-emphasis/destructive
 * text controls keep their literal no-fill/no-outline presentation in every
 * state, including semantic expanded/danger stamping. */
body:is([data-surface="phone"], [data-surface="tablet"])
  .payee-touch-manager .payee-workspace-head .payee-touch-back {
  background: transparent !important; border: 0 !important; box-shadow: none !important;
}
body:is([data-surface="phone"], [data-surface="tablet"])
  .payee-touch-manager .payee-workspace-head .payee-head-close {
  background: var(--close-control-bg) !important; color: var(--close-control-ink) !important;
  border: 0 !important; box-shadow: var(--close-control-shadow) !important;
}
body:is([data-surface="phone"], [data-surface="tablet"])
  .payee-touch-manager .rename-rule-row {
  display: grid; grid-template-columns: minmax(0, 1fr) var(--ctl-h-md);
  gap: .55rem;
}
body:is([data-surface="phone"], [data-surface="tablet"])
  .payee-touch-manager .rename-rule-kind { grid-column: 1 / -1; width: 100%; }
body:is([data-surface="phone"], [data-surface="tablet"])
  .payee-touch-manager .rename-rule-text { min-width: 0; width: 100%; }
body:is([data-surface="phone"], [data-surface="tablet"])
  .payee-touch-manager .rename-rule-add { width: var(--ctl-h-md); min-width: var(--ctl-h-md); padding: 0; }
body:is([data-surface="phone"], [data-surface="tablet"])
  .payee-touch-manager .payee-extras-h {
  background: transparent !important; border: 0 !important; box-shadow: none !important;
}
body:is([data-surface="phone"], [data-surface="tablet"])
  .payee-touch-manager .payee-extras-h[aria-expanded="true"] {
  background: transparent !important; border: 0 !important; box-shadow: none !important;
  color: var(--ink);
}
body:is([data-surface="phone"], [data-surface="tablet"])
  .payee-touch-manager .payee-delete {
  background: transparent !important; color: var(--neg) !important;
  border: 0 !important; box-shadow: none !important;
}
body:is([data-surface="phone"], [data-surface="tablet"])
  .payee-touch-manager .payee-delete-section > .payee-touch-disclosure {
  background: transparent !important; color: var(--neg) !important;
  border: 0 !important; box-shadow: none !important;
}

/* release-owner:settings — every requested Holiday field owns a complete row
 * where needed, so native select chrome never truncates its value. */
.holiday-enable-label em { display: block; font-style: italic; color: var(--ink-muted); }
.holiday-label-row,
.holiday-currency-row { display: grid; grid-template-columns: minmax(0, 1fr); align-items: stretch; }
.holiday-label-row .holiday-label,
.holiday-currency-row .holiday-currency { grid-column: 1 / -1; width: 100%; max-width: none; }
.holiday-label-help { grid-column: 1 / -1; line-height: 1.3; }

/* v1.214.52 — the Payees touch sheet used to inherit three superseded
 * geometry owners: a header divider, h3-sized dock labels and cramped Add
 * spacing. Keep the correction on the touch composition, not the shared
 * desktop workspace. */
body:is([data-surface="phone"], [data-surface="tablet"])
  .payee-touch-manager .payee-workspace-head {
  border-bottom: 0;
}
body:is([data-surface="phone"], [data-surface="tablet"])
  .payee-touch-manager > .payee-add-toggle,
body:is([data-surface="phone"], [data-surface="tablet"])
  .payee-touch-manager .payee-touch-index > .payee-add-toggle {
  margin: .35rem .2rem .7rem;
  color: var(--accent);
}
body:is([data-surface="phone"], [data-surface="tablet"])
  .payee-touch-manager .payee-add-panel {
  margin-bottom: .7rem;
}
body:is([data-surface="phone"], [data-surface="tablet"])
  .payee-touch-manager .payees-footer > .btn {
  font-size: var(--fs-body);
  line-height: 1.2;
}

/* Transaction history is a dense data table. Its old generic row padding
 * consumed vertical room without helping any column, so the table owns the
 * compact rhythm while preserving the configured type scale. */
.txns-popup-head,
.txns-popup-row {
  gap: .4rem;
  padding-block: .22rem;
}

/* The semantic control stamper runs after the legacy Settings component
 * rules. These two read-only explainers are literal text actions, so their
 * final owner must outrank that stamped secondary fill. */
html[data-desktop-theme] body[data-surface]
  .settings-read-actions .link-btn {
  background: transparent !important;
  border-color: transparent !important;
  color: var(--accent) !important;
  box-shadow: none !important;
}
