/* =====================================================================
   MoneyTracker — design system  (MOBILE-FIRST)

   Structure:
     1. tokens (light + dark)
     2. reset
     3. shell          <- phone: off-canvas drawer + bottom tab bar + FAB
     4. controls
     5. cards / stats / indicators / charts
     6. tables         <- phone: stacked cards;  >=900px: real table
     7. forms
     8. misc
     9. breakpoints    <- min-width only: 600 / 900 / 1200
    10. auth

   Every rule below is written for the smallest screen first and only
   widened with min-width queries. Change a palette by editing the
   --primary / *-soft / --side-* tokens only.
   ===================================================================== */

:root {
  /* Native form controls must follow the theme. Without this the browser paints
     every native internal — the date field's calendar picker icon and its
     day/month/year segments, dropdown popups, scrollbars — using LIGHT-mode
     colours even in dark mode. Measured: the calendar picker icon rendered
     near-black (luminance 0) on a near-black field (luminance 27) and was
     effectively invisible. This is the switch that turns that off. */
  color-scheme: light;

  /* palette ---------------------------------------------------------- */
  --primary:        #0162e8;
  --primary-ink:    #ffffff;
  --primary-soft:   #e8f1fe;
  --primary-deep:   #014ec0;

  --ok:             #157f3d;
  --ok-soft:        #e6f5ec;
  --warn:           #b45309;
  --warn-soft:      #fdf3e2;
  --err:            #c0264a;
  --err-soft:       #fdeff3;
  --info:           #0e7490;
  --info-soft:      #e6f4f7;

  /* neutrals (light) ------------------------------------------------- */
  --bg:             #f4f6fa;
  --surface:        #ffffff;
  --surface-2:      #f8fafc;
  --surface-3:      #eef2f7;
  --border:         #e3e8ef;
  --border-strong:  #cdd5e0;
  --text:           #0f172a;
  --text-2:         #46536b;
  --muted:          #7b8798;
  --faint:          #9aa5b4;

  --side-bg:        #ffffff;
  --side-ink:       #46536b;
  --side-ink-hover: #0f172a;
  --side-active-bg: var(--primary-soft);
  --side-active-ink: var(--primary);
  --side-border:    var(--border);

  --grid-line:      #e8ecf3;
  --shadow-1:       0 1px 2px rgba(16, 24, 40, .05), 0 1px 3px rgba(16, 24, 40, .04);
  --shadow-2:       0 6px 18px rgba(16, 24, 40, .07);
  --shadow-fab:     0 4px 14px color-mix(in srgb, var(--primary) 42%, transparent);
  --overlay:        rgba(15, 23, 42, .45);

  /* geometry --------------------------------------------------------- */
  --r-1: 8px;    /* controls */
  --r-2: 12px;   /* cards, panels */
  --r-3: 16px;   /* large surfaces */
  --r-pill: 999px;
  --side-w: 236px;
  --content-max: 1180px;   /* wide-desktop cap for the content column */
  --topbar-h: 56px;
  --tabbar-h: 58px;
  --gut: 16px;             /* page gutter, widens with the viewport */
  --tap: 44px;             /* minimum touch target */
  --ctrl-h: 46px;          /* ONE height for inputs AND buttons, so a paired
                              input+button share an exact centre line (they
                              used to be 46 vs 44 — a visible 2px step) */

  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
  --font-num: ui-monospace, "SF Mono", "Roboto Mono", Menlo, Consolas, monospace;
}

html[data-theme="dark"] {
  /* see the note on :root — keeps native control internals dark-themed */
  color-scheme: dark;

  --primary:        #4c93ff;
  --primary-ink:    #06122a;
  --primary-soft:   #14243f;
  --primary-deep:   #79abff;

  --ok:             #56c98a;
  --ok-soft:        #12261b;
  --warn:           #e2a34c;
  --warn-soft:      #2a2113;
  --err:            #f2798f;
  --err-soft:       #2c1620;
  --info:           #57bcd4;
  --info-soft:      #12262c;

  --bg:             #0e1116;
  --surface:        #171b22;
  --surface-2:      #1c2128;
  --surface-3:      #232830;
  --border:         #272d37;
  --border-strong:  #39414d;
  --text:           #e9edf3;
  --text-2:         #aab4c3;
  --muted:          #7f8a9b;
  --faint:          #6b7686;

  --side-bg:        #12161c;
  --side-ink:       #aab4c3;
  --side-ink-hover: #e9edf3;
  --side-active-bg: #1d2a3f;
  --side-active-ink: var(--primary);
  --side-border:    var(--border);

  --grid-line:      #232830;
  --shadow-1:       0 1px 2px rgba(0, 0, 0, .35);
  --shadow-2:       0 8px 22px rgba(0, 0, 0, .42);
  --overlay:        rgba(0, 0, 0, .6);
}

/* ============================== reset ============================== */
*, *::before, *::after { box-sizing: border-box; }
html {
  -webkit-text-size-adjust: 100%;   /* stop iOS inflating text in landscape */
  scroll-behavior: smooth;
  /* ---- zoom lock (see also the viewport meta + app.js) ----------------
     pan-x/pan-y keeps scrolling (including .table-wrap's horizontal scroll)
     but does NOT include pinch-zoom, and leaving double-tap out of the list
     also disables double-tap-to-zoom. This is the layer that works in all
     modern browsers; the meta tag covers older Android and app.js covers the
     iOS paths that ignore both (see assets/app.js). */
  touch-action: pan-x pan-y;
}
html, body { min-height: 100%; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 15px;                  /* phone base */
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;               /* a rogue wide child must never pan the page */
  overscroll-behavior-y: none;      /* no rubber-band behind the fixed bars */
}
h1, h2, h3, h4 { margin: 0; font-weight: 650; letter-spacing: -.011em; color: var(--text); }
p { margin: 0 0 10px; color: var(--text-2); }
a { color: var(--primary); text-decoration: none; }
img, svg { max-width: 100%; vertical-align: middle; }
button, input, select, textarea { font: inherit; color: inherit; }
ul { margin: 0; padding: 0; list-style: none; }
:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: 4px; }

.skip {
  position: absolute; left: -9999px; top: 0; background: var(--surface); color: var(--text);
  padding: 10px 14px; border-radius: var(--r-1); z-index: 100;
}
.skip:focus { left: 12px; top: 12px; }

/* scrollbars — guarded so Chromium keeps honouring the pseudo-elements */
@supports not selector(::-webkit-scrollbar) {
  * { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
}
*::-webkit-scrollbar { width: 9px; height: 9px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: var(--r-pill); border: 2px solid var(--bg); }
*::-webkit-scrollbar-thumb:hover { background: var(--faint); }

/* ============================== shell ==============================
   Phone: the sidebar is an off-canvas drawer. Navigation is the fixed
   bottom tab bar, with "Add" as the raised centre action.          */
.app { display: flex; min-height: 100dvh; align-items: stretch; }

.side {
  position: fixed; inset: 0 auto 0 0;
  width: min(280px, 84vw);
  background: var(--side-bg);
  border-right: 1px solid var(--side-border);
  display: flex; flex-direction: column;
  padding: 14px 12px calc(12px + env(safe-area-inset-bottom));
  height: 100dvh; overflow-y: auto;
  transform: translateX(-100%);
  transition: transform .2s ease;
  z-index: 50;
}
body.nav-open .side { transform: none; box-shadow: var(--shadow-2); }
body.nav-open { overflow: hidden; }              /* lock the page behind the drawer */
body.nav-open::after {
  content: ''; position: fixed; inset: 0; background: var(--overlay); z-index: 45;
}

.brand { display: flex; align-items: center; gap: 10px; padding: 4px 6px 16px; }
.brand-mark {
  width: 32px; height: 32px; flex: 0 0 32px; border-radius: 9px;
  background: var(--primary); color: var(--primary-ink);
  display: grid; place-items: center;
}
html[data-theme="dark"] .brand-mark { color: #06122a; }
.brand-text { font-weight: 680; font-size: 15px; letter-spacing: -.015em; }

.nav { display: flex; flex-direction: column; gap: 2px; }
.nav-item {
  display: flex; align-items: center; gap: 10px;
  min-height: var(--tap); padding: 0 10px; border-radius: var(--r-1);
  color: var(--side-ink); font-weight: 550; font-size: 14px;
  text-decoration: none; transition: background .12s, color .12s;
}
.nav-item svg { flex: 0 0 auto; opacity: .85; }
.nav-item:active { background: var(--surface-3); }
.nav-item.is-active {
  background: var(--side-active-bg); color: var(--side-active-ink); font-weight: 620;
}
.nav-item.is-active svg { opacity: 1; }

.side-foot { margin-top: auto; padding-top: 14px; border-top: 1px solid var(--border); }
.who { display: flex; align-items: center; gap: 9px; padding: 2px 6px 10px; }
.who-avatar {
  width: 30px; height: 30px; flex: 0 0 30px; border-radius: var(--r-pill);
  background: var(--primary-soft); color: var(--primary);
  display: grid; place-items: center; font-weight: 680; font-size: 12.5px;
}
.who-name { font-weight: 580; font-size: 13.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* .side-actions was removed with the theme toggle — the footer now holds a single
   full-width Log out button, so the icon-pair flex row it existed for is gone. */

.main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }

.topbar {
  min-height: var(--topbar-h);
  display: flex; align-items: center; gap: 10px;
  padding: 0 var(--gut);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: saturate(1.4) blur(8px);
  border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: 30;
}
.page-title { font-size: 16.5px; font-weight: 660; flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.topbar-actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
/* The theme switch sits directly beside the Add button, so it is sized to match
   that button's height at both breakpoints (--ctrl-h = 46px phone / 38px desktop).
   The .ghost default (44px / 32px) would leave the adjacent pair visibly uneven. */
.topbar-actions .ghost { width: var(--ctrl-h); height: var(--ctrl-h); }
.net-chip {
  font-size: 12.5px; color: var(--text-2);
  background: var(--surface); border: 1px solid var(--border);
  padding: 5px 11px; border-radius: var(--r-pill);
  white-space: nowrap;
}
.net-chip strong { color: var(--text); font-variant-numeric: tabular-nums; }

/* room to scroll past the fixed bar, plus the notch at the bottom */
.content {
  padding: 14px var(--gut) calc(var(--tabbar-h) + 22px + env(safe-area-inset-bottom));
  width: 100%;
}

.menu-btn { display: inline-grid; }
.only-desktop { display: none; }
.topbar-add { display: none; }        /* the FAB owns "add" on phones */

/* ========================== confirm modal ===============================
   Replaces window.confirm(). Same reasoning as the rest of the app: a native
   dialog is browser chrome — unthemeable, a different shape on every platform,
   and it cannot name the action it is guarding.

   Mobile-first: on a phone this is a bottom sheet, so both buttons sit under
   the thumb. At 900px it becomes a centred dialog. The markup is identical. */
.modal { position: fixed; inset: 0; z-index: 60; display: flex; }
/* 60 sits above the drawer (50) and the tab bar (40), below toasts (100). */
.modal[hidden] { display: none; }
.modal-scrim { position: absolute; inset: 0; background: var(--overlay); }

.modal-card {
  position: relative;
  margin-top: auto; width: 100%;
  display: flex; flex-direction: column; gap: 11px;
  background: var(--surface);
  border-radius: var(--r-3) var(--r-3) 0 0;
  padding: 20px 18px calc(18px + env(safe-area-inset-bottom));
  box-shadow: var(--shadow-2);
  animation: modal-sheet-in .19s cubic-bezier(.2, .7, .3, 1);
}
@keyframes modal-sheet-in {
  from { transform: translateY(16px); opacity: 0; }
  to   { transform: none; opacity: 1; }
}

.modal-head { display: flex; align-items: flex-start; gap: 11px; }
.modal-ico {
  flex: 0 0 auto; width: 38px; height: 38px; border-radius: 11px;
  display: grid; place-items: center;
}
/* The icon tile is tinted from --err rather than using --err-soft directly.
   --err-soft (#2c1620) sits at almost exactly the same LUMINANCE as the dark
   card (#171b24) — measured 27 vs 27 — so in dark mode the tile differed from
   its background by hue alone and read as flat. Blending the ink into the
   surface keeps a real luminance step in both themes (light ~215 on 255,
   dark ~54 on 27) and needs no extra tokens. */
.modal-ico--danger { background: color-mix(in srgb, var(--err) 22%, var(--surface)); color: var(--err); }
.modal-ico--warn   { background: color-mix(in srgb, var(--warn) 22%, var(--surface)); color: var(--warn); }
/* Only the active variant shows; all three ship together so the icon can swap
   without innerHTML. See the markup comment in layout.php. */
.modal-ico [data-ico] { display: none; }
.modal-ico[data-active="alert"] [data-ico="alert"],
.modal-ico[data-active="trash"] [data-ico="trash"],
.modal-ico[data-active="lock"]  [data-ico="lock"] { display: block; }

.modal-title { font-size: 16.5px; font-weight: 640; line-height: 1.3; letter-spacing: -.01em; margin: 5px 0 0; }
.modal-msg { margin: 0; color: var(--text-2); font-size: 14px; line-height: 1.5; }
.modal-msg[hidden] { display: none; }
.modal-acts { display: flex; gap: 9px; margin-top: 5px; }
/* Cancel and the destructive action split the row evenly, so neither becomes a
   small target marooned beside dead space. */
.modal-acts .btn { flex: 1 1 0; min-width: 0; }
body.modal-open { overflow: hidden; }   /* lock the page behind the modal */

@media (min-width: 900px) {
  .modal { align-items: center; justify-content: center; padding: 24px; }
  .modal-card {
    margin-top: 0; width: min(432px, 100%);
    border-radius: var(--r-3);
    padding: 22px 22px 20px; gap: 12px;
    animation: modal-dialog-in .17s cubic-bezier(.2, .7, .3, 1);
  }
  @keyframes modal-dialog-in {
    from { transform: scale(.97); opacity: 0; }
    to   { transform: none; opacity: 1; }
  }
}

/* ---------------------------- bottom tab bar ---------------------------- */
.tabbar {
  display: flex; align-items: stretch; justify-content: space-around;
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  background: color-mix(in srgb, var(--surface) 96%, transparent);
  backdrop-filter: saturate(1.4) blur(10px);
  border-top: 1px solid var(--border);
  padding: 4px 2px calc(4px + env(safe-area-inset-bottom));
}
.tab {
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  min-height: 48px; padding: 4px 2px; border-radius: var(--r-1);
  color: var(--muted); font-size: 10.5px; font-weight: 580; text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}
.tab span { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tab.is-active { color: var(--primary); }
.tab.is-active svg { stroke-width: 2.1; }

/* centre action — raised, always thumb-reachable */
.tab--add { flex: 0 0 auto; padding: 0 10px; }
.tab--add .fab {
  width: 50px; height: 50px; margin-top: -20px;
  border-radius: var(--r-pill);
  background: var(--primary); color: var(--primary-ink);
  display: grid; place-items: center;
  box-shadow: var(--shadow-fab);
  border: 4px solid var(--bg);
}
html[data-theme="dark"] .tab--add .fab { color: #06122a; }
.tab--add span { display: none; }

/* ============================ controls ============================ */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  min-height: var(--ctrl-h); padding: 0 16px;      /* matches inputs exactly */
  border-radius: var(--r-1); border: 1px solid transparent;
  background: var(--surface); color: var(--text);
  font-size: 14px; font-weight: 580; cursor: pointer; white-space: nowrap;
  text-decoration: none; transition: background .12s, border-color .12s, color .12s, box-shadow .12s;
  -webkit-tap-highlight-color: transparent;
}
.btn--primary { background: var(--primary); color: var(--primary-ink); }
.btn--outline { background: var(--surface); border-color: var(--border-strong); color: var(--text); }
.btn--ghost { background: transparent; color: var(--text-2); border-color: transparent; }
.btn--danger { background: var(--err-soft); color: var(--err); border-color: transparent; }
/* Solid danger button, for the modal's primary action. --err flips from a deep
   red (light theme) to a light red (dark theme), so the ink must flip with it or
   dark mode would render white-on-pink. --bg is near-white in light and
   near-black in dark, which gives the right polarity both ways with no new
   tokens. Used only inside the confirm modal. */
.btn--danger-solid { background: var(--err); color: var(--bg); border-color: transparent; }
.btn--sm { min-height: var(--tap); padding: 0 12px; font-size: 13px; }   /* keep the 44px thumb target; only the desktop block shrinks it */
.btn--block { width: 100%; }
.btn[disabled], .btn[aria-disabled="true"] { opacity: .5; pointer-events: none; }

.icon-btn {
  display: inline-grid; place-items: center;
  width: var(--tap); height: var(--tap); border-radius: var(--r-1);
  background: transparent; border: 1px solid transparent; color: var(--text-2); cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.icon-btn:active { background: var(--surface-3); color: var(--text); }

/* small icon-only action used inside rows */
.ghost {
  display: inline-grid; place-items: center;
  width: var(--tap); height: var(--tap); border-radius: var(--r-1);
  background: transparent; border: 1px solid transparent; color: var(--muted); cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.ghost:active { background: var(--surface-3); color: var(--text); }

.only-light { display: inline; }
.only-dark { display: none; }
html[data-theme="dark"] .only-light { display: none; }
html[data-theme="dark"] .only-dark { display: inline; }

/* inputs — one family. 16px keeps iOS from zooming on focus. */
.field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.field > label, .lbl {
  font-size: 12.5px; font-weight: 600; color: var(--text-2); letter-spacing: .002em;
}
.hint { font-size: 12px; color: var(--muted); }
input[type="text"], input[type="password"], input[type="number"], input[type="date"],
input[type="month"], input[type="search"], input[type="email"], select, textarea {
  width: 100%; min-height: var(--ctrl-h); padding: 0 11px;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border-strong); border-radius: var(--r-1);
  font-size: 16px;                                  /* must stay >=16px on phones */
  transition: border-color .12s, box-shadow .12s;
}
textarea { min-height: 88px; padding: 10px 11px; resize: vertical; }
select { padding-right: 30px; appearance: none;
  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%237b8798' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 9px center;
}
input:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent);
}
input[type="date"], input[type="month"], input[type="number"] { font-variant-numeric: tabular-nums; }
/* WebKit derives the colour of a date field's internal segments from the UA /
   `color-scheme` rather than honouring the input's own `color`, so on iOS a
   date field can render dark-on-dark and look empty in EITHER theme. Pinning
   every segment (and -webkit-text-fill-color, which wins over `color` in the
   UA shadow tree) makes the date text visible in every engine and both themes.
   `font: inherit` keeps the segments on the app's type scale instead of the
   UA's default. */
input[type="date"]::-webkit-datetime-edit,
input[type="date"]::-webkit-datetime-edit-fields-wrapper,
input[type="date"]::-webkit-datetime-edit-text,
input[type="date"]::-webkit-datetime-edit-month-field,
input[type="date"]::-webkit-datetime-edit-day-field,
input[type="date"]::-webkit-datetime-edit-year-field {
  color: var(--text);
  -webkit-text-fill-color: var(--text);
  font: inherit;
}
/* The picker glyph itself is left to the UA: `color-scheme` above already makes
   Chrome/Safari draw a light glyph on the dark field. An earlier `invert(1)`
   here double-corrected it back to black and hid it again (measured luminance
   delta 20 against the field — i.e. invisible). Do not add a filter:
   `color-scheme` is the single source of truth. */
input[type="date"]::-webkit-calendar-picker-indicator { cursor: pointer; }
::placeholder { color: var(--faint); }
input[type="checkbox"], input[type="radio"] { accent-color: var(--primary); width: 24px; height: 24px; flex: 0 0 auto; }

.checkline { display: flex; align-items: center; gap: 10px; font-size: 14px; color: var(--text-2); min-height: var(--tap); cursor: pointer; }

/* colour swatch picker (accounts & categories) */
.swatches { display: flex; flex-wrap: wrap; gap: 8px; }
/* padding brings the tappable label up to the 44px thumb minimum
   without inflating the visible swatch itself */
.swatch { position: relative; cursor: pointer; padding: 5px; }
.swatch input { position: absolute; opacity: 0; width: 34px; height: 34px; margin: 0; cursor: pointer; }
.swatch span {
  display: block; width: 34px; height: 34px; border-radius: var(--r-1);
  border: 2px solid transparent; box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .08);
  transition: transform .1s;
}
.swatch input:checked + span { border-color: var(--text); }
.swatch input:focus-visible + span { outline: 2px solid var(--primary); outline-offset: 2px; }

/* icon picker */
.iconpick { display: flex; flex-wrap: wrap; gap: 6px; }
.iconpick label { position: relative; cursor: pointer; }
.iconpick input { position: absolute; opacity: 0; }
.iconpick span {
  display: grid; place-items: center; width: 44px; height: 44px;
  border-radius: var(--r-1); border: 1px solid var(--border-strong);
  color: var(--text-2); background: var(--surface);
}
.iconpick input:checked + span { border-color: var(--primary); background: var(--primary-soft); color: var(--primary); }
.iconpick input:focus-visible + span { outline: 2px solid var(--primary); outline-offset: 2px; }

/* amount input with a currency prefix */
.money-input { position: relative; }
.money-input .cur {
  position: absolute; left: 11px; top: 50%; transform: translateY(-50%);
  font-size: 13px; color: var(--muted); font-weight: 600; pointer-events: none;
}
.money-input input { padding-left: 48px; font-variant-numeric: tabular-nums; font-weight: 600; }

/* segmented control — MOBILE-FIRST: every option stays visible (a sideways
   scroll hides options); the desktop block goes back to a single row.
   A 2-column GRID, not flex-wrap. flex-wrap sized each item from its own
   text, so rows came out with mismatched item widths and a ragged tail:
   reports wrapped 3+2 with ~118px items above ~178px ones, and the
   transaction type picker wrapped 3+1 leaving "Card payment" orphaned.
   Equal 1fr columns make every item identical, and an odd final item spans
   the row so no row is ever left half empty. */
.seg {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  padding: 3px; gap: 4px; background: var(--surface-3);
  border-radius: var(--r-1); max-width: 100%;
}
.seg::-webkit-scrollbar { display: none; }
/* scoped to :not(.seg--nav) — the stepper is a 3-column grid and must keep
   its arrow / label / arrow ordering rather than spanning */
.seg:not(.seg--nav) > *:last-child:nth-child(odd) { grid-column: 1 / -1; }
.seg a, .seg button {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  min-height: var(--tap); padding: 0 13px; border: 0; border-radius: 6px;
  background: transparent; color: var(--text-2); font-size: 13.5px; font-weight: 570;
  cursor: pointer; text-decoration: none; white-space: nowrap;
  min-width: 0; overflow: hidden; text-overflow: ellipsis;
}
.seg .is-active { background: var(--surface); color: var(--primary); box-shadow: var(--shadow-1); }

/* Month stepper: arrows at the edges with the cycle label between them,
   filling the row. Geometry only — the label keeps its .pill colouring. */
.seg--nav { grid-template-columns: auto minmax(0, 1fr) auto; }
.seg--nav > .pill { display: inline-flex; align-items: center; justify-content: center; border-radius: 6px; padding: 0 12px; }
/* mirrors the forward arrow when there is nowhere to go, so the cycle label
   stays optically centred instead of drifting right */
.seg-off { display: grid; place-items: center; padding: 0 4px; color: var(--faint); opacity: .5; }

/* ============================== cards ============================== */
.card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-2); box-shadow: var(--shadow-1);
  min-width: 0;                     /* never let a wide row widen the page */
}
.card + .card { margin-top: 14px; }
.card-head {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 13px 14px; border-bottom: 1px solid var(--border);
}
.card-head h2, .card-head h3 { font-size: 14.5px; font-weight: 640; }
.card-head .spacer { margin-left: auto; }
.card-body { padding: 14px; min-width: 0; }
.card-body--flush { padding: 0; }
.card-foot { padding: 12px 14px; border-top: 1px solid var(--border); background: var(--surface-2); border-radius: 0 0 var(--r-2) var(--r-2); }

.section-title {
  font-size: 11.5px; font-weight: 680; text-transform: uppercase; letter-spacing: .07em;
  color: var(--muted); margin: 20px 0 10px;
}
.section-title:first-child { margin-top: 0; }

/* ============================== stats ==============================
   Two per row on a phone keeps the numbers glanceable; widens later.  */
.stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.stat {
  display: block; padding: 12px 13px; min-width: 0;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-2); box-shadow: var(--shadow-1);
  color: inherit; text-decoration: none;
}
a.stat:active { border-color: var(--border-strong); }
.stat-top { display: flex; align-items: center; gap: 7px; margin-bottom: 7px; min-width: 0; }
.stat-ic {
  width: 24px; height: 24px; flex: 0 0 24px; border-radius: 7px;
  background: var(--surface-3); color: var(--text-2); display: grid; place-items: center;
}
.stat-label {
  font-size: 11.5px; font-weight: 580; color: var(--muted);
  /* MOBILE-FIRST: allow a second line rather than truncating the label.
     The desktop block restores nowrap + ellipsis where there is room. */
  overflow-wrap: anywhere;
}
.stat-value { font-size: 19px; font-weight: 680; letter-spacing: -.022em; font-variant-numeric: tabular-nums; }
.stat-sub { font-size: 11.5px; color: var(--muted); margin-top: 2px; }
.stat--ok   .stat-ic { background: var(--ok-soft);   color: var(--ok); }
.stat--warn .stat-ic { background: var(--warn-soft); color: var(--warn); }
.stat--err  .stat-ic { background: var(--err-soft);  color: var(--err); }
.stat--primary .stat-ic { background: var(--primary-soft); color: var(--primary); }
.stat-value.is-neg { color: var(--err); }

/* ============================== bars ==============================
   Phone: name + value on one line, the track spans beneath.        */
.bars { display: flex; flex-direction: column; gap: 12px; }
.bars-row { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 6px 12px; }
.bars-name { display: flex; align-items: center; gap: 7px; font-size: 13.5px; color: var(--text-2); min-width: 0; }
.bars-name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bars-track { grid-column: 1 / -1; height: 8px; background: var(--surface-3); border-radius: var(--r-pill); overflow: hidden; }
.bars-fill { display: block; height: 100%; border-radius: var(--r-pill); }
.bars-val { text-align: right; font-size: 13.5px; font-weight: 620; font-variant-numeric: tabular-nums; }
.bars-budget { grid-column: 1 / -1; text-align: right; font-size: 11.5px; color: var(--muted); }

/* progress row used by budgets */
.bud { display: grid; grid-template-columns: 1fr auto; gap: 6px 12px; padding: 11px 0; border-bottom: 1px solid var(--border); }
.bud:last-child { border-bottom: 0; padding-bottom: 0; }
.bud-name { display: flex; align-items: center; gap: 8px; font-size: 13.5px; font-weight: 570; min-width: 0; }
.bud-num { font-size: 13px; font-variant-numeric: tabular-nums; color: var(--text-2); white-space: nowrap; }
.bud-num strong { color: var(--text); }
.bud-track { grid-column: 1 / -1; height: 7px; background: var(--surface-3); border-radius: var(--r-pill); overflow: hidden; }
.bud-fill { display: block; height: 100%; border-radius: var(--r-pill); }
.bud-fill.is-over { background: var(--err) !important; }

/* ============================== charts ============================== */
.chart { width: 100%; height: auto; display: block; }
.chart .grid { stroke: var(--grid-line); stroke-width: 1; }
.chart .axis { stroke: var(--border-strong); stroke-width: 1; }
.chart .ax { fill: var(--muted); font-family: var(--font); }
.chart .bar-exp { fill: var(--err); opacity: .82; }
.chart .bar-inc { fill: var(--ok); opacity: .72; }
.chart--spark { height: 58px; }
.spark-line { fill: none; stroke: var(--primary); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.spark-area { fill: var(--primary); opacity: .10; }
.chart--donut { max-width: 152px; margin: 0 auto; }
.legend { display: flex; flex-direction: column; gap: 9px; }
.legend-row { display: grid; grid-template-columns: 10px 1fr auto; align-items: center; gap: 9px; font-size: 13px; }
.legend-dot { width: 10px; height: 10px; border-radius: 3px; }
.legend-name { color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.legend-val { font-weight: 620; font-variant-numeric: tabular-nums; }
.chart-legend { display: flex; gap: 14px; font-size: 12.5px; color: var(--muted); flex-wrap: wrap; }
.chart-legend i { display: inline-block; width: 9px; height: 9px; border-radius: 3px; margin-right: 6px; }

/* ---- chart axis text ------------------------------------------------
   The column chart is an SVG with a fixed 760x240 viewBox stretched to its
   container, so a font-size declared here is in USER UNITS and is multiplied
   by the render scale = (container width / 760).

   TWO charts use it and they are NOT the same width:
     Reports   -> full-width card              (container up to ~1102px)
     Dashboard -> inside .grid-2 = half-width
                  from 600px viewport up       (container up to ~528px)

   A VIEWPORT-keyed ladder cannot serve both. Measured at a 1280px viewport:
   Reports rendered 11.4px but the Dashboard only 5.4px, because its
   container is 460px rather than 966px. So the ladder below is keyed to the
   CONTAINER width (.chartwrap), which is the real constraint.

   font-size = 8360 / containerWidth  (=> ~11px rendered). Each band uses the
   value for its midpoint; bands are ~1.18x apart so rendering stays ~10-12px
   in BOTH charts. Re-derive if the chart's container ever changes shape. */
.chartwrap { container-type: inline-size; }

.chart--cols .xlab-alt { display: none; }

/* Fallback ladder, keyed to the viewport. Only applies in browsers without
   container query support (pre-2023); the @container rules below win where
   supported, so these values just need to be sane, not perfect. */
.chart--cols .ax,
.chart--cols .ax-x { font-size: 22px; }
@media (min-width: 640px) {
  .chart--cols .ax,
  .chart--cols .ax-x { font-size: 12px; }
}

/* --- container-keyed ladder (the real fix) ---------------------------
   Values are 8360 / containerWidth, CAPPED AT 28px. Two reasons for the cap:
   font size here is in user units, and on the narrowest containers
   (Dashboard ~242px) an uncapped 34px made the y-labels too wide for padL and
   too tall for padT, so they clipped off the left and top edges. 28 is the
   largest value that fits the chart's padding (see column_chart()). The cap
   costs ~1.5px of rendered size on the narrowest cards and nothing elsewhere. */
@container (min-width: 0px)     { .chart--cols .ax, .chart--cols .ax-x { font-size: 28px; } }
@container (min-width: 308px)   { .chart--cols .ax, .chart--cols .ax-x { font-size: 25px; } }
@container (min-width: 354px)   { .chart--cols .ax, .chart--cols .ax-x { font-size: 22px; } }
@container (min-width: 407px)   { .chart--cols .ax, .chart--cols .ax-x { font-size: 19px; } }
@container (min-width: 468px)   { .chart--cols .ax, .chart--cols .ax-x { font-size: 16.7px; } }
@container (min-width: 538px)   { .chart--cols .ax, .chart--cols .ax-x { font-size: 14.5px; } }
@container (min-width: 619px)   { .chart--cols .ax, .chart--cols .ax-x { font-size: 12.5px; } }
@container (min-width: 712px)   { .chart--cols .ax, .chart--cols .ax-x { font-size: 11px; } }
@container (min-width: 819px)   { .chart--cols .ax, .chart--cols .ax-x { font-size: 9.5px; } }
@container (min-width: 942px)   { .chart--cols .ax, .chart--cols .ax-x { font-size: 8.3px; } }
@container (min-width: 1083px)  { .chart--cols .ax, .chart--cols .ax-x { font-size: 7.6px; } }

/* ============================== tables ==============================
   MOBILE-FIRST: a table is a stack of labelled rows on a phone.
   At >=900px the same markup becomes a real table again.

   Every stacking rule is scoped with :not(.tbl--grid) so that the
   opt-out grid below is never caught by them. Without that guard the
   (0,2,2) selectors here — td[data-label=""], td:not([data-label]) —
   outrank the (0,1,2) grid rules and silently win.
   ------------------------------------------------------------------- */
.table-wrap { overflow-x: visible; }
table.tbl { width: 100%; font-size: 14px; }

table.tbl:not(.tbl--grid) { display: block; }
table.tbl:not(.tbl--grid) thead { display: none; }
table.tbl:not(.tbl--grid) tbody { display: flex; flex-direction: column; }
table.tbl:not(.tbl--grid) tr {
  display: flex; flex-direction: column;
  padding: 12px 14px; border-bottom: 1px solid var(--border);
}
table.tbl:not(.tbl--grid) tr:last-child { border-bottom: 0; }
table.tbl:not(.tbl--grid) td {
  display: flex; align-items: baseline; justify-content: space-between; gap: 14px;
  padding: 3px 0; border: 0; text-align: right; min-height: 22px;
}
table.tbl:not(.tbl--grid) td::before {
  content: attr(data-label);
  flex: 0 0 auto; text-align: left;
  font-size: 11px; font-weight: 680; text-transform: uppercase; letter-spacing: .05em;
  color: var(--muted);
}
/* the unlabelled cell becomes the row heading, and leads the stack */
table.tbl:not(.tbl--grid) td[data-label=""],
table.tbl:not(.tbl--grid) td:not([data-label]) {
  display: block; text-align: left; order: -3;
  font-weight: 620; font-size: 15px; padding-bottom: 5px;
}
table.tbl:not(.tbl--grid) td:not([data-label])::before,
table.tbl:not(.tbl--grid) td[data-label=""]::before { content: none; }
/* date sits directly under the heading */
table.tbl:not(.tbl--grid) td.tbl-date { order: -2; }
/* the figure is the point of the row — make it read first among the rest */
table.tbl:not(.tbl--grid) td.num { font-weight: 640; font-size: 15px; }
/* action buttons go last, as a right-aligned cluster */
table.tbl:not(.tbl--grid) td.actions { order: 9; justify-content: flex-end; padding-top: 8px; }
table.tbl:not(.tbl--grid) td.actions::before { content: none; }
/* a progress cell needs the bar on its own full-width line */
table.tbl:not(.tbl--grid) td[data-label="Progress"] { flex-wrap: wrap; }
table.tbl:not(.tbl--grid) td[data-label="Progress"] .bud-track { flex: 1 1 100%; margin-top: 3px; }
table.tbl:not(.tbl--grid) td[data-label="Progress"] .hint { flex: 0 0 auto; }

.tbl .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.tbl .neg { color: var(--err); }
.tbl .pos { color: var(--ok); }

/* --- opt-out: keep a real grid and scroll it sideways -----------------
   Used where rows are positional rather than labelled — an arbitrary CSV
   preview, or the backup list. Stacking those would lose the column
   meaning, so they stay tabular at every width.                        */
.table-wrap--scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table.tbl--grid { display: table; min-width: 520px; border-collapse: collapse; }
table.tbl--grid thead { display: table-header-group; }
table.tbl--grid tbody { display: table-row-group; }
table.tbl--grid tr { display: table-row; padding: 0; border-bottom: 0; }
table.tbl--grid th,
table.tbl--grid td {
  display: table-cell; padding: 10px 12px; text-align: left;
  border-bottom: 1px solid var(--border); vertical-align: middle;
  white-space: nowrap; min-height: 0;
}
table.tbl--grid td::before { content: none; }
table.tbl--grid th {
  font-size: 11.5px; font-weight: 680; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted); background: var(--surface-2);
}
table.tbl--grid tbody tr:last-child td { border-bottom: 0; }
table.tbl--grid .num { text-align: right; }
table.tbl--grid .actions { text-align: right; }

.tbl .actions { text-align: right; white-space: nowrap; justify-content: flex-end; }
.tbl .actions::before { margin-right: auto; }
.tbl-date { color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.tbl-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.tbl-main strong { font-weight: 590; }
.tbl-main small { color: var(--muted); font-size: 12px; }

/* pills / chips / badges */
.pill {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 22px; padding: 1px 9px; border-radius: var(--r-pill);
  font-size: 11.5px; font-weight: 620; letter-spacing: .01em;
  background: var(--surface-3); color: var(--text-2); white-space: nowrap;
}
.pill--ok   { background: var(--ok-soft);   color: var(--ok); }
.pill--warn { background: var(--warn-soft); color: var(--warn); }
.pill--err  { background: var(--err-soft);  color: var(--err); }
.pill--info { background: var(--info-soft); color: var(--info); }
.pill--primary { background: var(--primary-soft); color: var(--primary); }
.pill .dot { width: 7px; height: 7px; border-radius: var(--r-pill); background: currentColor; }

.cat-chip { display: inline-flex; align-items: center; gap: 6px; font-size: 13.5px; color: var(--text-2); }
.cat-dot { width: 9px; height: 9px; border-radius: 3px; flex: 0 0 9px; }

/* "added by" attribution — the ledger is shared, so every entry names its author */
.by-chip { display: inline-flex; align-items: center; gap: 5px; font-size: 12.5px; color: var(--text-2); }
.by-dot  { width: 7px; height: 7px; border-radius: var(--r-pill); flex: 0 0 7px; }

/* family members list (Settings → Family) */
.members { display: grid; gap: 10px; }
.member {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  min-width: 0; padding: 12px;
  border: 1px solid var(--border); border-radius: var(--r-2); background: var(--surface);
}
.member-avatar {
  width: 42px; height: 42px; flex: 0 0 42px; border-radius: var(--r-pill);
  display: grid; place-items: center;
  color: #fff; font-weight: 650; font-size: 17px; line-height: 1;
}
.member-avatar--sm { width: 32px; height: 32px; flex-basis: 32px; font-size: 14px; }
.member-body { flex: 1 1 160px; min-width: 0; }
.member-name { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-weight: 620; }
.member-sub { font-size: 13px; color: var(--text-2); overflow-wrap: anywhere; }
.member-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-left: auto; }
.member--self { border-color: var(--primary); }
.role-badge {
  display: inline-flex; align-items: center;
  font-size: 11px; font-weight: 650; letter-spacing: .01em;
  padding: 2px 8px; border-radius: var(--r-pill);
  background: var(--surface-3); color: var(--text-2);
}
.role-badge--owner { background: var(--primary-soft); color: var(--primary); }
.role-note { font-size: 12.5px; color: var(--muted); }

/* ============================== forms ============================== */
.form-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; }
.form-grid .col-span-2 { grid-column: 1 / -1; }
.form-actions { display: flex; align-items: center; gap: 10px; margin-top: 16px; flex-wrap: wrap; }
.form-actions .spacer { margin-left: auto; }

.toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 14px; }
.toolbar .grow { flex: 1 1 100%; min-width: 0; }
.search-wrap { position: relative; }
.search-wrap svg { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); color: var(--muted); pointer-events: none; }
.search-wrap input { padding-left: 34px; }

/* Filter bar: a real 2-column grid so every field is exactly half the content
   width and the left/right edges line up row to row. The fields used to carry
   hardcoded inline widths (150/170/140px), so the browser wrapped them by
   accident and each row ended at a different x — measured tails of
   14/66/26/76px, with 274px of dead space stranded beside Apply. */
.filter-grid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px; min-width: 0; flex: 1 1 100%;
}
.filter-grid > .span-2 { grid-column: 1 / -1; }
/* Actions own the row beneath the fields and split it evenly, so Apply is
   never a small button marooned at the left of an empty row. */
.filter-actions { display: flex; gap: 10px; flex: 1 1 100%; }
.filter-actions .btn { flex: 1 1 0; min-width: 0; }
/* On a phone each toolbar control owns a full-width row: a half-width month
   stepper sitting beside a half-width chip left 236px of the screen unused. */
.toolbar > .seg, .toolbar > .pill, .toolbar > .btn { flex: 1 1 100%; }
.toolbar > .pill { justify-content: center; }

/* --- collapsible filter panel (phones) ---------------------------------
   The Transactions filter bar is 9 controls deep; on a phone that buries the
   list under ~420px of fields. A single "Filters" button collapses the lot.

   `.filters-body` is the panel. On a phone it is a plain block that owns full
   rows; on desktop it becomes `display: contents` (see the 900px block) so its
   children keep flowing into the single inline toolbar row unchanged. */
.filters-toggle { flex: 1 1 100%; justify-content: center; gap: 8px; }
/* Count of active filters. Without it, collapsing the panel while filters are
   still applied would hide the reason the list is narrowed. */
.filters-count {
  display: inline-grid; place-items: center; min-width: 20px; height: 20px;
  padding: 0 6px; border-radius: 999px;
  background: var(--primary); color: var(--primary-ink);
  font-size: 11.5px; font-weight: 700; font-variant-numeric: tabular-nums;
}
.filters-body { flex: 1 1 100%; min-width: 0; }

/* Collapsed state. Scoped to `.js` so with JS unavailable nothing is ever
   hidden behind a toggle that could not reopen it — the panel then simply
   behaves as it always did. */
/* This is the one deliberate max-width query in the file. The desktop rule
   below turns .filters-body into `display: contents`, and a min-width-first
   hide rule would out-specify it and blank the entire desktop filter bar. */
@media (max-width: 899px) {
  .js .toolbar[data-filters="closed"] .filters-body { display: none; }

  /* The panel must supply the row gap itself. .filter-grid and .filter-actions
     used to be DIRECT children of .toolbar, where the toolbar's own 10px gap
     separated the fields from the buttons. Wrapping them in .filters-body — a
     plain block — silently removed that gap, so Apply sat FLUSH against the
     last filter row (measured 0px vs the app's 10px). A column flex with the
     same 10px restores the original rhythm exactly.
     The children are pinned to `flex: 0 0 auto` because their inherited
     `flex: 1 1 100%` was written for a wrapping ROW container; in a column
     container that basis means "100% of the container's height". */
  .filters-body { display: flex; flex-direction: column; gap: 10px; }
  .filters-body > .filter-grid,
  .filters-body > .filter-actions { flex: 0 0 auto; }
}

/* The two date fields are wrapped in a real <label> so they stay identifiable
   when empty — an empty native date field draws as a blank box in some engines.
   Only the phone layout shows that label; desktop hides it to keep its single
   38px row (see the 900px block). */
.field--date { gap: 5px; }
.field-label { font-size: 12px; font-weight: 600; color: var(--text-2); }

/* --- desktop: the filter bar collapses back onto ONE row ---------------
   On a phone every field owns a full-width row because nothing else fits.
   On desktop that wastes the width — the filters and the buttons stack into
   separate full-width rows and the bar is 260px tall for no reason. So on
   desktop the fields and the action buttons sit inline side by side.

   `display: contents` on the mobile wrapper drops its box, which promotes
   its children to direct flex items of .toolbar. They then flow in one row,
   size to their content, and wrap as a single group only if the row truly
   runs out of space — instead of each control claiming a whole line. */
@media (min-width: 900px) {
  /* 9 controls have to share one row, and the tightest case is a 1280 viewport
     where only 996px of content exists (minus a 236px sidebar and the gutters).
     So this bar runs denser than the 14px app default — 5px gaps and 13px
     fields — which is normal for a dense desktop filter strip and is what lets
     all nine controls plus both buttons stay on a single line. Everything here
     is scoped to .toolbar--inline; no other control in the app is affected. */
  .toolbar--inline { gap: 5px; }
  /* The toggle is a phone affordance only: there is room for every control on
     desktop, so the button is removed and the panel is permanently expanded.
     `display: contents` drops the panel's box so .filter-grid and
     .filter-actions become direct flex items of .toolbar again — the single
     inline row this bar had before the panel existed, unchanged. */
  .filters-toggle { display: none; }
  .toolbar--inline > .filters-body { display: contents; }
  /* Labels are a phone affordance too — on desktop the date fields sit in a
     38px row and a stacked label would double its height. */
  .toolbar--inline .field--date > .field-label { display: none; }
  .toolbar--inline .filter-grid > .field--date { flex: 0 0 auto; width: auto; }
  /* Reports puts its custom date fields immediately after the range tabs, so the
     tab strip must keep its NATURAL width — the global
     `.toolbar > .seg { flex: 1 1 100% }` otherwise gives it a whole row and pushes
     the date fields onto the line below. */
  .toolbar--inline > .seg { flex: 0 0 auto; }
  /* Everything except an atomic group (the two below) is unwrapped, so its
     controls become direct flex items of .toolbar and flow inline. Transactions
     has no atomic group, so its behaviour is unchanged. */
  .toolbar--inline .filter-grid:not(.filter-grid--dates):not(.filter-grid--scope) { display: contents; }
  /* The search absorbs whatever is left; the rest keep their natural width.
     Its flex-BASIS (not its grown size) decides whether the line wraps, so a
     small basis lets the row stay on one line in the filtered state too, when
     the Clear button joins it. It grows straight back out when there is room. */
  .toolbar--inline .filter-grid > .search-wrap { flex: 1 1 96px; min-width: 88px; }
  .toolbar--inline .filter-grid > select {
    flex: 0 0 auto; width: auto; min-width: 0;
    font-size: 13px; max-width: 115px; padding-left: 8px; padding-right: 6px;
  }
  /* A native date input is left at its INTRINSIC width (width:auto) rather than
     a hardcoded one: Chrome renders "mm/dd/yyyy" plus the calendar button from
     the UA shadow DOM, so a fixed width silently clips the year (118px cut
     "09/16/202…"). Intrinsic sizing adapts to font and locale, and the earlier
     `scrollWidth` check could NOT see the clipping — only a rendered look did.
     A slightly smaller font shrinks that intrinsic width and buys the row back
     ~20px without ever clipping the text. */
  /* Descendant, not child: the transactions date fields now sit inside a
     .field--date label wrapper. Matches the reports ones (still direct children
     of .filter-grid--dates) exactly as before. */
  .toolbar--inline .filter-grid input[type="date"] {
    flex: 0 0 auto; width: auto; font-size: 12.5px; padding-left: 5px; padding-right: 5px;
  }
  .toolbar--inline .filter-actions { flex: 0 0 auto; }
  .toolbar--inline .filter-actions .btn { flex: 0 0 auto; font-size: 12.5px; padding: 0 9px; }

  /* --- Reports: dates + Apply sit BESIDE the Custom tab ------------------
     Deliberately NOT `display: contents` here. That promoted from/to/Apply to
     separate flex items, so Apply could wrap alone to the next line — away from
     the fields it submits — which is exactly the "orphan button beside dead
     space" defect. Three `auto` columns keep the group atomic and sized to its
     own content, which is what lets it fit beside the tab strip.

     Measured before this change (mode=custom, 1280 viewport): bar 180px tall,
     4 rows, Apply alone in a row with 493px of dead space beside it. */
  .toolbar--inline .filter-grid--dates {
    display: grid; grid-template-columns: auto auto auto;
    flex: 0 0 auto; gap: 5px;
  }
  /* The account + Export pair is atomic for the same reason: unwrapped, Export
     wrapped ALONE onto a second row while "All accounts" stayed on the first
     (measured at 1152/1280 — Export stranded by itself). Kept as one box it
     either fits beside the range controls or the pair moves down together. */
  .toolbar--inline .filter-grid--scope {
    display: grid; grid-template-columns: auto auto;
    flex: 0 0 auto; gap: 5px;
  }
  /* Apply and Export are direct .filter-grid children on this bar (the
     transactions bar wraps its buttons in .filter-actions instead), so they
     need the same no-grow, tighter treatment or they inherit the wider
     full-width button padding and cost the row its fit. */
  .toolbar--inline .filter-grid > .btn {
    flex: 0 0 auto; width: auto; font-size: 12.5px; padding: 0 9px;
  }
}

/* ============================== misc ============================== */
.flashes { display: flex; flex-direction: column; gap: 8px; margin-bottom: 14px; }
.flash {
  display: flex; align-items: flex-start; gap: 9px;
  padding: 11px 13px; border-radius: var(--r-1); font-size: 13.5px; font-weight: 540;
  border: 1px solid transparent;
}
.flash svg { flex: 0 0 auto; margin-top: 2px; }
.flash--ok   { background: var(--ok-soft);   color: var(--ok);   border-color: color-mix(in srgb, var(--ok) 22%, transparent); }
.flash--warn { background: var(--warn-soft); color: var(--warn); border-color: color-mix(in srgb, var(--warn) 22%, transparent); }
.flash--err  { background: var(--err-soft);  color: var(--err);  border-color: color-mix(in srgb, var(--err) 22%, transparent); }

.empty {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: 8px; padding: 34px 18px;
}
.empty-ic {
  width: 44px; height: 44px; border-radius: var(--r-pill);
  background: var(--surface-3); color: var(--muted); display: grid; place-items: center;
}
.empty h3 { font-size: 14.5px; }
.empty p { font-size: 13px; color: var(--muted); max-width: 380px; margin: 0 0 6px; }

/* NOTE: minmax(0, 1fr) not 1fr — a bare `1fr` track is minmax(auto, 1fr)
   and its `auto` floor is min-content, so a card holding a wide row would
   refuse to shrink below it and push the whole page sideways on a phone.
   The `> * { min-width: 0 }` guard stops that min-content propagating. */
/* A grid block always separates itself from whatever sits above it.
   This rhythm used to come from a per-call-site inline style="margin-top:16px",
   and FOUR call sites forgot it (accounts, import, installments, settings) — so
   the grid sat flush against the card above with a genuine 0px gap. Putting it
   on the class means a new call site physically cannot lose the gap.
   The 16px collapses with a preceding card's own 16px bottom margin, so grids
   that already spaced correctly render exactly as before. */
.grid-2, .grid-3 {
  display: grid; grid-template-columns: minmax(0, 1fr);
  gap: 14px; margin-top: 16px; align-items: start;
}
.grid-2 > *, .grid-3 > * { min-width: 0; }
.grid-2 > .card + .card, .grid-3 > .card + .card { margin-top: 0; }
.split { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; align-items: center; }
.split > * { min-width: 0; }

.list-row {
  display: flex; align-items: center; gap: 11px; padding: 13px 14px;
  border-bottom: 1px solid var(--border);
  min-width: 0; flex-wrap: wrap;    /* a long label may wrap rather than widen the page */
}
.list-row:last-child { border-bottom: 0; }
.list-row .grow { flex: 1 1 auto; min-width: 0; }
.list-row .amt { font-weight: 640; font-variant-numeric: tabular-nums; white-space: nowrap; }

.acc-ic {
  width: 36px; height: 36px; flex: 0 0 36px; border-radius: 9px;
  display: grid; place-items: center; background: var(--surface-3); color: var(--text-2);
}

/* ============================== installments ==============================
   The "settled" marker on the Due-this-month rows. A tick tile, sized to match
   .acc-ic so the row rhythm is identical to every other list in the app.
   Colours come from --ok so it follows the theme in both light and dark. */
.inst-check {
  width: 36px; height: 36px; flex: 0 0 36px; border-radius: 9px;
  display: grid; place-items: center;
  background: var(--surface-3); color: var(--muted);
}
.inst-check.is-paid {
  background: color-mix(in srgb, var(--ok) 16%, var(--surface));
  color: var(--ok);
}

.inst-help { margin: 0; padding-left: 18px; font-size: 13.5px; color: var(--text-2); }
.inst-help li { margin-bottom: 9px; }
.inst-help li:last-child { margin-bottom: 0; }
.inst-help strong { color: var(--text); font-weight: 600; }

/* Compact metric strip used by the dashboard Installments card.
   Mobile-first: two per row, widening to four once there is room. */
.inst-metrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px 12px; }
.inst-metric { display: grid; gap: 2px; min-width: 0; }
.inst-metric strong {
  font-size: 17px; font-weight: 670; letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}
@media (min-width: 600px) {
  .inst-metrics { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

.kv { display: grid; grid-template-columns: auto 1fr; gap: 8px 16px; font-size: 13.5px; }
.kv dt { color: var(--muted); }
.kv dd { margin: 0; font-weight: 570; text-align: right; font-variant-numeric: tabular-nums; }

.pager { display: flex; align-items: center; gap: 10px; padding: 12px 14px; border-top: 1px solid var(--border); flex-wrap: wrap; }
.pager .spacer { margin-left: auto; }
.pager-info { font-size: 12.5px; color: var(--muted); }

/* danger zone */
.danger-card { border-color: color-mix(in srgb, var(--err) 34%, var(--border)); }
.danger-card .card-head { border-bottom-color: color-mix(in srgb, var(--err) 22%, var(--border)); }
.danger-card .card-head h2 { color: var(--err); }

/* ======================= breakpoints (min-width) ======================= */

/* --- large phones / small tablets -------------------------------- 600px */
@media (min-width: 600px) {
  :root { --gut: 20px; }

  .form-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px 16px; }
  .stats { grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 12px; }
  /* desktop has room: one scrollable row, single-line labels, tighter targets */
  .seg { display: inline-flex; flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; }
  .seg a, .seg button { flex: 0 0 auto; min-height: 38px; }
  .stat-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .stat-value { font-size: 21px; }
  .grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
  .split { grid-template-columns: 180px minmax(0, 1fr); gap: 18px; }
  .toolbar .grow { flex: 1 1 220px; min-width: 180px; }
  .bars-row { grid-template-columns: 150px 1fr 104px; }
  .chart--donut { max-width: 168px; }
  .content { padding-top: 18px; }
}

/* --- tablet / desktop: real sidebar, no bottom bar ---------------- 900px */
@media (min-width: 900px) {
  :root { --gut: 24px; }

  /* the sidebar becomes part of the layout again */
  .side {
    position: sticky; top: 0; transform: none;
    width: var(--side-w); flex: 0 0 var(--side-w);
    height: 100dvh; padding: 14px 12px 12px;
    box-shadow: none;
  }
  body.nav-open .side { box-shadow: none; }
  body.nav-open::after { content: none; }
  body.nav-open { overflow: auto; }

  .menu-btn { display: none; }
  .only-desktop { display: inline; }
  .topbar-add { display: inline-flex; }
  .tabbar { display: none; }
  .content { padding-bottom: 28px; }
  .topbar { min-height: 60px; }

  .card + .card { margin-top: 16px; }
  .grid-2, .grid-3 { gap: 16px; }
  .grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }

  /* ---- tables revert to a real table ----
     Scoped with :not(.tbl--grid) to match the specificity of the mobile
     stack rules above; without it the (0,3,2) mobile selectors outrank
     these (0,1,2) ones and the stack leaks into desktop. */
  .table-wrap { overflow-x: auto; }
  table.tbl:not(.tbl--grid) { display: table; width: 100%; border-collapse: collapse; font-size: 13.5px; }
  table.tbl:not(.tbl--grid) thead { display: table-header-group; }
  table.tbl:not(.tbl--grid) tbody { display: table-row-group; }
  table.tbl:not(.tbl--grid) tr {
    display: table-row; padding: 0; border-bottom: 0;
  }
  table.tbl:not(.tbl--grid) tr:last-child { border-bottom: 0; }
  table.tbl:not(.tbl--grid) th {
    display: table-cell; text-align: left; white-space: nowrap;
    font-size: 11.5px; font-weight: 680; text-transform: uppercase; letter-spacing: .06em;
    color: var(--muted); padding: 11px 14px; background: var(--surface-2);
    border-bottom: 1px solid var(--border); position: sticky; top: 0; z-index: 2;
  }
  table.tbl:not(.tbl--grid) td {
    display: table-cell; padding: 11px 14px; border-bottom: 1px solid var(--border);
    vertical-align: middle; text-align: left; min-height: 0;
  }
  table.tbl:not(.tbl--grid) td::before { content: none; }
  table.tbl:not(.tbl--grid) td[data-label=""],
  table.tbl:not(.tbl--grid) td:not([data-label]) {
    display: table-cell; order: 0; font-weight: inherit; font-size: inherit; padding-bottom: 11px;
  }
  table.tbl:not(.tbl--grid) td.num { font-weight: inherit; font-size: inherit; }
  table.tbl:not(.tbl--grid) td.actions { order: 0; padding-top: 11px; justify-content: normal; }
  table.tbl:not(.tbl--grid) td[data-label="Progress"] .bud-track { margin-top: 0; }
  table.tbl:not(.tbl--grid) tbody tr:hover td { background: var(--surface-2); }
  table.tbl:not(.tbl--grid) tbody tr:last-child td { border-bottom: 0; }
  .tbl .num { text-align: right; }
  .tbl .actions .ghost { width: 32px; height: 32px; }
  .tbl-date { color: var(--muted); }

  /* SVG charts: show every month label again once there is room.
     (font-size is owned by the measured ladder near the top of the file) */
  .chart--cols .xlab-alt { display: block; }

  /* pointers exist again — restore hover affordances */
  .nav-item:hover { background: var(--surface-3); color: var(--side-ink-hover); }
  html[data-theme="dark"] .nav-item:hover { background: #1b2129; }
  .nav-item.is-active:hover { background: var(--side-active-bg); color: var(--side-active-ink); }
  .btn--primary:hover { background: var(--primary-deep); color: var(--primary-ink); }
  html[data-theme="dark"] .btn--primary:hover { background: var(--primary-deep); color: #06122a; }
  .btn--outline:hover { background: var(--surface-3); }
  .btn--ghost:hover { background: var(--surface-3); color: var(--text); }
  .btn--danger:hover { background: var(--err); color: #fff; }
  .icon-btn:hover, .ghost:hover { background: var(--surface-3); color: var(--text); }
  html[data-theme="dark"] .ghost:hover { background: #1f252d; }
  a.stat:hover { border-color: var(--border-strong); box-shadow: var(--shadow-2); text-decoration: none; }
  a:hover { text-decoration: underline; }
  .tab:hover { text-decoration: none; }
  .swatch:hover span { transform: scale(1.07); }
  .iconpick label:hover span { border-color: var(--primary); color: var(--text); }
  .seg a:hover, .seg button:hover { background: color-mix(in srgb, var(--surface) 70%, transparent); color: var(--text); text-decoration: none; }
}

/* --- wide desktop -------------------------------------------------- 1200px */
@media (min-width: 1200px) {
  /* Cap the width AND centre it. The cap on its own pinned the column to the
     left edge of .main, so on a wide monitor all the slack collected as one
     big empty gutter down the right-hand side instead of being split evenly. */
  .content { max-width: var(--content-max); margin-inline: auto; }
  /* The app bar keeps its full-bleed background and border, but its contents
     are padded to that same centred column so the page title and the Net chip
     line up with the cards beneath them instead of drifting to the far left. */
  .topbar { padding-inline: max(var(--gut), calc((100% - var(--content-max)) / 2 + var(--gut))); }
  .app--full .content { max-width: none; }
  .app--full .topbar { padding-inline: var(--gut); }
  .stat-value { font-size: 23px; }
}

/* inputs relax their 16px phone size only where the pointer is a real mouse.
   Deliberately NOT relaxed on width alone: iPad landscape / iPad Pro are
   >=900px but still iOS, and iOS auto-zooms any focused control under 16px.
   That auto-zoom is NOT suppressed by user-scalable=no, so relaxing here
   would zoom the page in with pinch-zoom locked -- the user could not get
   back out. Keep the 16px floor for anything that might be touch. */
@media (min-width: 900px) {
  :root { --ctrl-h: 38px; }
  input[type="text"], input[type="password"], input[type="number"], input[type="date"],
  input[type="month"], input[type="search"], input[type="email"], select, textarea {
    min-height: var(--ctrl-h); font-size: 16px;
  }
  textarea { min-height: 84px; }
  .btn { min-height: var(--ctrl-h); padding: 0 14px; font-size: 13.5px; }
  .btn--sm { min-height: 30px; padding: 0 10px; font-size: 12.5px; }
  .ghost { width: 32px; height: 32px; }
  .icon-btn { width: 36px; height: 36px; }
  .seg a, .seg button { min-height: 30px; padding: 0 12px; font-size: 13px; }
  body { font-size: 14px; }
  .swatch input { width: 26px; height: 26px; }
  .swatch span { width: 26px; height: 26px; }
  .swatch { padding: 0; }
  .iconpick span { width: 32px; height: 32px; }
  input[type="checkbox"], input[type="radio"] { width: 16px; height: 16px; }
  .checkline { font-size: 13px; }
  table.tbl { font-size: 13.5px; }
}

/* Only a real mouse gets the tighter 14px controls. Touch-only devices
   >=900px (iPad, touch laptops in tablet mode) keep 16px so iOS never
   auto-zooms a focused field while pinch-zoom is locked. */
@media (min-width: 900px) and (pointer: fine) {
  input[type="text"], input[type="password"], input[type="number"], input[type="date"],
  input[type="month"], input[type="search"], input[type="email"], select, textarea {
    font-size: 14px;
  }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
  html { scroll-behavior: auto; }
}

/* ============================== auth pages ============================== */
.auth-wrap {
  min-height: 100dvh; display: grid; place-items: center;
  padding: 20px 16px calc(20px + env(safe-area-inset-bottom));
  background: var(--bg);
}
.auth-card {
  width: 100%; max-width: 396px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-3); box-shadow: var(--shadow-2); padding: 24px 20px 20px;
}
.auth-brand { display: flex; align-items: center; gap: 10px; margin-bottom: 4px; }
.auth-title { font-size: 19px; font-weight: 680; letter-spacing: -.02em; margin-bottom: 4px; }
.auth-sub { font-size: 13.5px; color: var(--muted); margin-bottom: 20px; }
.auth-card .field + .field { margin-top: 14px; }
.auth-foot { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--border); font-size: 12.5px; color: var(--muted); }

@media (min-width: 600px) {
  .auth-card { padding: 28px 26px 24px; }
}
