/* DuitKita, "Lilac Ghibli" (the mockup, §13.3): cream paper, wisteria accent,
   a sky-and-hills scene at the top of each page. Every colour is a token here;
   dark mode redefines the tokens, not the rules (static/dark.css, loaded by
   base.html per the person's light/dark/system choice).

   Categories are never told apart by colour (a dozen categories, no palette
   survives that): bars are one hue, not-spending groups a lighter lilac,
   uncategorised gray, each at least 3:1 against the track in both modes. Bar
   lengths come from data-pct via app.js, because the Content-Security-Policy
   forbids inline style attributes.

   Shared pieces other stylesheets can use (class names are stable):
     .page .scene .deco .hero-top   page body; sky-and-hills header (a rounded
                                    card on desktop, a full-width band on phones)
     .card .card-head .pill .badge  surfaces and labels
     .seg                           segmented control (links or buttons)
     .btn .btn.quiet .btn.sm        primary, secondary, compact (36px) buttons
     .field-label .banner .note     form label, message strip, soft info line
     .bar-track/.bar-fill           one-hue bars (.not-spending, .uncat)
     .lba .lba-label .lba-bar .lba-amount   one label/bar/amount row; lines
                                    under it indent by var(--lba-indent)
     .empty (.is-done)              empty state; render with _macros empty()
     .is-pending                    the button waiting on a request (+ aria-busy)
     .banner.error                  a failed action, top of its card; render
                                    with _macros error_banner() (icon, retry)
     :disabled                      .btn, inputs, selects at var(--dim)
   Tokens (:root): type --fs-sm..--fs-hero and --fs-control (form controls,
   16px on phones); space --s-1..--s-8; radius --radius-xxs/-xs/-sm, --radius,
   --radius-pill; layout --sidebar-w --tabbar-space --corner --row-label
   --row-amount --lba-indent; motion --ease-out --ease-in-out --dur-1..--dur-4;
   --dim for disabled. No raw px font-size or radius outside :root
   (tests/test_css_tokens.py). */

@font-face {
  font-family: "Nunito";
  font-weight: 400 800;
  font-display: swap;
  src: url("fonts/nunito-latin.woff2") format("woff2");
}
@font-face {
  font-family: "Zen Maru Gothic";
  font-weight: 700;
  font-display: swap;
  src: url("fonts/zen-maru-gothic-700-latin.woff2") format("woff2");
}

:root {
  color-scheme: light;
  --bg: #f6eddb;
  --paper: #fdf8ee;
  --paper-2: #ede3cc;
  --ink: #3f3340;
  --ink-2: #5d5060;
  --muted: #6f6272;
  --accent: #6f57ad;
  --accent-hover: #634c9e;
  --on-accent: #ffffff;
  --lilac: #b7a3e3;
  --lilac-bar: #8a72c4;          /* not-spending bars: 3.1:1 on --paper-2 */
  --gray: #837a8a;               /* uncategorised: 3.2:1 on --paper-2 */
  --sunny: #f6d493;              /* the review count badge */
  --on-sunny: #3f3340;
  --glass: rgba(255, 255, 255, 0.72);
  --ok: #2f4a36;
  --bad: #9a3b3b;
  --line: rgba(111, 87, 173, 0.3);
  --shadow-sm: 0 2px 8px rgba(111, 87, 173, 0.15);
  --shadow: 0 4px 16px rgba(111, 87, 173, 0.2);
  --shadow-lg: 0 8px 32px rgba(111, 87, 173, 0.25);
  --sky-top: #a9cbe8;
  --sky-mid: #cfe0f0;
  --sky-low: #f3ecdf;
  --sky-end: #fbeedd;
  --sun-core: #fdf3d2;
  --sun: #f6d493;
  --cloud: rgba(255, 255, 255, 0.85);
  --hill-far: #c9b8ea;
  --hill-mid: #a9d1ad;
  --hill-near: #8fbd95;
  --stars: none;
  /* Type: the only font sizes. Card and section heads are --fs-h3; page
     titles --fs-h1 (24px on phones); the month's total --fs-hero. */
  --fs-sm: 12px; --fs-md: 13px; --fs-base: 14px; --fs-lg: 16px;
  --fs-h3: 18px; --fs-h2: 22px; --fs-h1: 28px; --fs-display: 34px; --fs-hero: 42px;
  /* Form controls: 16px on phones, where anything smaller makes iOS Safari
     zoom the page on tap. */
  --fs-control: var(--fs-md);
  /* Space: every gap, padding and margin. Only 0 and 1-3px hairlines are raw. */
  --s-1: 4px; --s-2: 8px; --s-3: 12px; --s-4: 16px; --s-5: 20px; --s-6: 24px; --s-7: 32px;
  --s-8: 48px;
  --radius-xxs: 6px;             /* chart column tops, the AI mark */
  --radius-xs: 12px;             /* controls, table rows, panels */
  --radius-sm: 16px;             /* cards, buttons */
  --radius: 24px;                /* scenes, hero cards */
  --radius-pill: 999px;
  /* Layout */
  --sidebar-w: 232px;
  --tabbar-space: 104px;         /* room under a phone page for the tab bar */
  --corner: 56px;                /* a phone scene's top strip: the avatar's corner */
  --row-label: 128px;            /* .lba: the label column's least width */
  --row-amount: 96px;            /* .lba: the amount column's least width */
  --lba-indent: calc(max(var(--row-label), 30%) + var(--s-3));
  /* Motion (no spring, no overshoot; all of it off under reduced motion). */
  --ease-out: cubic-bezier(.2, .7, .2, 1);       /* things arriving */
  --ease-in-out: cubic-bezier(.45, 0, .2, 1);    /* things moving across */
  --dur-1: 120ms;                /* press */
  --dur-2: 200ms;                /* hover, colour */
  --dur-3: 360ms;                /* bars, collapse, page */
  --dur-4: 520ms;                /* section entrance */
  --dim: 0.45;                   /* disabled controls */
  --display: "Zen Maru Gothic", "Hiragino Maru Gothic ProN", "Nunito", sans-serif;
}

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: var(--fs-base)/1.45 "Nunito", "Avenir Next", "Segoe UI", system-ui, sans-serif;
}
a {
  color: var(--accent); text-underline-offset: 3px; text-decoration-thickness: 1px;
  text-decoration-color: color-mix(in srgb, var(--accent) 40%, transparent);
}
a:hover { color: var(--accent-hover); text-decoration-color: currentColor; }
b, strong { font-weight: 800; }  /* Nunito stops at 800: bold inside 800 text would ask for 900 */
h1, h2, h3, .display { font-family: var(--display); font-weight: 700; margin: 0; }
h1 { font-size: var(--fs-h1); line-height: 1.1; }
h2 { font-size: var(--fs-h3); line-height: 1.25; }  /* card heads; --fs-h2 is the wordmark */
h3 { font-size: var(--fs-base); }
.muted { color: var(--muted); }
.small { font-size: var(--fs-sm); }
.amount, .num { font-variant-numeric: tabular-nums; white-space: nowrap; }
.sr-only {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}
:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }

/* --- shell: sidebar on wide screens, tab bar on phones --------------------- */

/* The gradient paints the sidebar's column past the fold, under the sticky sidebar. */
.app {
  min-height: 100vh; display: grid; grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  background: linear-gradient(to right, var(--paper-2) calc(var(--sidebar-w) - 1px),
    var(--line) calc(var(--sidebar-w) - 1px) var(--sidebar-w), transparent var(--sidebar-w));
}
.sidebar {
  position: sticky; top: 0; height: 100vh;
  display: flex; flex-direction: column; gap: var(--s-6);
  padding: var(--s-7) var(--s-5) var(--s-5); background: var(--paper-2); border-right: 1px solid var(--line);
}
.lockup { display: flex; align-items: center; gap: var(--s-2); padding: 0 var(--s-2); text-decoration: none; color: var(--ink); }
.lockup .display { font-size: var(--fs-h2); }
/* 23.3%: the logo SVG's own corner (28 of 120), at any size. */
.lockup .logo { flex: none; border-radius: 23.3%; box-shadow: var(--shadow-sm); }
/* The logo's shapes take the scene's tokens, so it turns to night in dark
   mode with the sky around it (_macros logo(); its fills are the fallback). */
.logo .logo-sky-top { stop-color: var(--sky-top); }
.logo .logo-sky-end { stop-color: var(--sky-end); }
.logo .logo-sun { fill: var(--sun); }
.logo .logo-ring { stroke: color-mix(in srgb, var(--sun) 80%, var(--ink)); }
.logo .logo-cloud { fill: var(--cloud); }
.logo .logo-hill-far { fill: var(--hill-far); }
.logo .logo-hill-mid { fill: var(--hill-mid); }
.logo .logo-hill-near { fill: var(--hill-near); }
.sidenav { display: flex; flex-direction: column; gap: var(--s-1); }
.sidenav a, .tabbar a {
  display: flex; align-items: center; text-decoration: none; color: var(--ink-2);
}
.sidenav a {
  gap: var(--s-3); padding: var(--s-3) var(--s-4); border-radius: var(--radius-sm); font-weight: 700;
}
.sidenav a:hover { background: color-mix(in srgb, var(--paper) 55%, transparent); color: var(--ink); }
.sidenav a[aria-current="page"] {
  background: var(--accent); color: var(--on-accent); font-weight: 800; box-shadow: var(--shadow);
}
.sidenav svg { flex: none; }
.badge {
  margin-left: auto; min-width: 22px; height: 22px; padding: 0 var(--s-2); border-radius: var(--radius-pill);
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--sunny); color: var(--on-sunny); font-size: var(--fs-sm); font-weight: 800;
}
.side-foot {
  margin-top: auto; display: flex; flex-direction: column; gap: var(--s-2);
  padding: var(--s-3) var(--s-4); border-radius: var(--radius-sm); background: var(--paper);
  border: 1px solid var(--line); font-size: var(--fs-sm); color: var(--ink-2);
}
.side-foot .who { display: flex; align-items: center; gap: var(--s-2); font-size: var(--fs-md); font-weight: 800; color: var(--ink); }
.side-links { display: flex; align-items: center; justify-content: space-between; gap: var(--s-2); }
.side-links a { display: inline-flex; align-items: center; gap: var(--s-1); color: var(--ink-2); font-weight: 700; text-decoration: none; }
.side-links a:hover, .side-links a[aria-current="page"] { color: var(--accent); }
.side-links form { margin: 0; }
.side-links .link-btn { color: var(--ink-2); font-weight: 700; }
.side-links .link-btn:hover { color: var(--accent); }
.avatar {
  width: 28px; height: 28px; border-radius: 50%; display: inline-flex; align-items: center;
  justify-content: center; flex: none; font-size: var(--fs-sm); font-weight: 800;
  background: var(--paper-2); color: var(--ink); border: 1px solid var(--line);
}
.main { min-width: 0; padding-bottom: var(--s-8); }
/* Past 1180px the content stays that wide, centred right of the sidebar. */
.page { padding: 0 var(--s-7); max-width: 1180px; margin-inline: auto; }
.tabbar { display: none; }
.side-links .link-btn svg { display: none; }  /* the rail's; words are enough here */

/* 900-1199px: the sidebar folds to a rail of icons. Every name stays in its
   link as visually hidden text (the accessible name) and shows as a tooltip
   on hover and keyboard focus, from data-tip. */
@media (min-width: 900px) and (max-width: 1199px) {
  :root { --sidebar-w: 72px; }
  .sidebar { z-index: 5; align-items: center; gap: var(--s-5); padding: var(--s-5) var(--s-2) var(--s-4); }
  .lockup { padding: 0; }
  .sidebar .nav-text, .lockup .display {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); white-space: nowrap;
  }
  .sidenav a { position: relative; justify-content: center; width: 48px; height: 48px; padding: 0; }
  .sidenav .badge {
    position: absolute; top: 1px; right: 1px; margin: 0;
    min-width: 18px; height: 18px; padding: 0 var(--s-1); font-size: var(--fs-sm);
  }
  .side-foot { align-items: center; gap: var(--s-3); padding: var(--s-3) 0; width: 100%; }
  .side-links { flex-direction: column; gap: var(--s-2); }
  .side-links a, .side-links .link-btn {
    position: relative; display: inline-flex; align-items: center; justify-content: center;
    width: 36px; height: 36px; border-radius: 50%;
  }
  .side-links .link-btn svg { display: block; }
  [data-tip]::after {
    content: attr(data-tip); position: absolute; left: calc(100% + var(--s-3)); top: 50%;
    transform: translateY(-50%); padding: var(--s-1) var(--s-2); border-radius: var(--radius-xxs);
    background: var(--ink); color: var(--bg); font-size: var(--fs-sm); font-weight: 700;
    white-space: nowrap; box-shadow: var(--shadow-sm);
    opacity: 0; pointer-events: none; transition: opacity var(--dur-2) var(--ease-out);
  }
  [data-tip]:hover::after, [data-tip]:focus-visible::after { opacity: 1; }
  .side-links [data-tip]::after { left: calc(100% + var(--s-4)); }
}

@media (max-width: 899px) {
  .app { display: block; background: none; }
  .sidebar { display: none; }
  .main { padding-bottom: calc(var(--tabbar-space) + env(safe-area-inset-bottom)); }
  :root { --fs-h1: 24px; --fs-control: var(--fs-lg); }
  .page { padding: 0 var(--s-4); }
  /* Five tabs: Check-in is the month's ritual, so it gets a tab rather than
     hiding behind the avatar menu; five is the most a phone bar holds well. */
  .tabbar {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 10;
    display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); align-items: start;
    padding: var(--s-2) var(--s-1) calc(var(--s-3) + env(safe-area-inset-bottom));
    background: var(--paper); border-top: 1px solid var(--line);
    box-shadow: 0 -4px 16px color-mix(in srgb, var(--accent) 8%, transparent);
  }
  .tabbar a {
    position: relative; flex-direction: column; justify-content: center; gap: 3px;
    min-height: 52px; padding: 2px 0; font-size: var(--fs-sm); font-weight: 700; color: var(--muted);
    text-align: center; line-height: 1.2;
  }
  .tabbar svg { width: 44px; height: 28px; padding: 3px var(--s-3); border-radius: var(--radius-pill); }
  .tabbar a[aria-current="page"] { color: var(--accent); font-weight: 800; }
  .tabbar a[aria-current="page"] svg { background: color-mix(in srgb, var(--accent) 16%, transparent); }
  .tabbar .badge {
    position: absolute; top: -4px; left: calc(50% + 8px); margin: 0;
    min-width: 18px; height: 18px; padding: 0 var(--s-1); font-size: var(--fs-sm);
  }
}

/* --- the scene: sky, sun (a dim moon in the dark), a cloud, three hills ----
   The bottom padding is taller than the hills rise, so text always ends on
   sky, never on a hill. */

.scene {
  --hills: 44px;
  position: relative; overflow: hidden; isolation: isolate;
  margin: var(--s-6) max(var(--s-7), (100% - 1180px) / 2 + var(--s-7)) 0;
  padding: var(--s-7) var(--s-7) calc(var(--hills) + var(--s-3));
  border-radius: var(--radius); border: 1px solid var(--line); box-shadow: var(--shadow-lg);
  background: linear-gradient(to bottom, var(--sky-top), var(--sky-mid) 55%, var(--sky-low) 85%, var(--sky-end));
}
.scene > * { position: relative; }
.scene > .deco { position: absolute; inset: 0; z-index: -1; pointer-events: none; }
.scene .seg { background: var(--glass); }
.deco span { position: absolute; display: block; border-radius: 50%; }
.deco::before { content: ""; position: absolute; inset: 0 0 auto; height: 14%; background: var(--stars); }
.deco .sun {
  right: 30%; top: 16px; width: 60px; height: 60px;
  background: radial-gradient(circle at 40% 40%, var(--sun-core), var(--sun) 58%, transparent 72%);
}
.deco .cloud { right: 40%; top: 30px; width: 120px; height: 34px; border-radius: var(--radius-pill); background: var(--cloud); }
.deco .cloud::before, .deco .cloud::after {
  content: ""; position: absolute; border-radius: 50%; background: var(--cloud);
}
.deco .cloud::before { left: 22px; top: -22px; width: 46px; height: 46px; }
.deco .cloud::after { left: 62px; top: -16px; width: 34px; height: 34px; }
/* Each hill is an ellipse whose top rises exactly --hills above the bottom. */
.deco .hill-far { right: 8%; bottom: calc(var(--hills) - 120px); width: 44%; height: 120px; background: var(--hill-far); }
.deco .hill-mid { right: -8%; bottom: calc(var(--hills) - 8px - 130px); width: 38%; height: 130px; background: var(--hill-mid); }
.deco .hill-near { right: -22%; bottom: calc(var(--hills) - 20px - 130px); width: 44%; height: 130px; background: var(--hill-near); }

@media (max-width: 899px) {
  /* Phones: a full-width band. The sun sits in its own strip of sky above
     the text, so it never crosses a word. */
  .scene {
    --hills: 36px;
    margin: 0; max-width: none; border: 0; border-radius: 0; box-shadow: none;
    padding: calc(var(--corner) + env(safe-area-inset-top)) var(--s-4) calc(var(--hills) + var(--s-3));
  }
  /* The avatar sits at the right edge, just under --corner: the sun keeps
     clear of that column, so the two never touch. */
  .deco .sun { right: calc(var(--corner) + var(--s-4)); top: calc(6px + env(safe-area-inset-top)); width: 46px; height: 46px; }
  .deco .cloud { right: auto; left: 28%; top: calc(24px + env(safe-area-inset-top)); width: 84px; height: 24px; }
  .deco .cloud::before { left: 14px; top: -16px; width: 32px; height: 32px; }
  .deco .cloud::after { left: 42px; top: -10px; width: 24px; height: 24px; }
  .deco .hill-far { right: auto; left: 12%; width: 90%; }
  .deco .hill-mid { right: auto; left: -24%; width: 80%; }
  .deco .hill-near { right: -30%; width: 80%; }
}

/* --- common pieces --------------------------------------------------------- */

.card {
  background: var(--paper); border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: var(--s-5) var(--s-6); box-shadow: var(--shadow);
}
a.card { display: block; color: var(--ink); text-decoration: none; }
.card-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-2); margin-bottom: var(--s-4); }
.card-head > .small, .card-head > .muted { font-weight: 700; }
.pill {
  display: inline-flex; align-items: center; gap: var(--s-1); padding: 3px var(--s-3); border-radius: var(--radius-pill);
  background: var(--paper-2); border: 1px solid var(--line); color: var(--ink);
  font-size: var(--fs-sm); font-weight: 800;
}
.pill.pending { background: transparent; border-style: dashed; color: var(--muted); }
/* A category the LLM picked: named in text, like every other marker. */
.ai-mark {
  display: inline-block; padding: 0 var(--s-1); border-radius: var(--radius-xxs); border: 1px solid var(--line);
  font-size: var(--fs-sm); font-weight: 800; letter-spacing: 0.04em; color: var(--ink-2);
}
/* Items share the width equally, so "Kita | Saya" sits as evenly as "Us | Me". */
.seg {
  display: inline-grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  padding: var(--s-1); gap: var(--s-1); border-radius: var(--radius-pill); background: var(--paper-2);
}
.seg a, .seg button {
  display: inline-flex; align-items: center; justify-content: center; height: 34px; padding: 0 var(--s-4); border: 0;
  border-radius: var(--radius-pill); background: transparent; color: var(--ink-2); font: inherit;
  font-weight: 700; font-size: var(--fs-md); text-decoration: none; cursor: pointer;
}
.seg a:hover, .seg button:hover { color: var(--ink); }
.seg [aria-current="true"], .seg [aria-pressed="true"] {
  background: var(--accent); color: var(--on-accent); font-weight: 800; box-shadow: var(--shadow-sm);
}
.seg [aria-current="true"]:hover, .seg [aria-pressed="true"]:hover { color: var(--on-accent); }
.seg form { display: contents; }

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
  height: 44px; padding: 0 var(--s-5); border-radius: var(--radius-sm); border: 1px solid transparent;
  background: var(--accent); color: var(--on-accent); font: inherit; font-weight: 800;
  cursor: pointer; text-decoration: none; box-shadow: var(--shadow);
}
.btn:hover { background: var(--accent-hover); color: var(--on-accent); }
.btn.quiet { background: var(--paper); color: var(--ink); border-color: var(--line); box-shadow: none; }
.btn.quiet:hover { background: var(--paper-2); color: var(--ink); }
.btn.sm { height: 36px; padding: 0 var(--s-4); font-size: var(--fs-md); }
.btn:disabled:not(.is-pending), .btn[aria-disabled="true"]:not(.is-pending) {
  opacity: var(--dim); box-shadow: none; cursor: not-allowed;
}
.link-btn { background: none; border: 0; padding: 0; font: inherit; color: var(--accent); cursor: pointer; }

/* Form controls: one height, one radius; selects draw their chevron from two
   gradients so it follows the tokens (no image, nothing inline). */
select, textarea,
input[type="password"], input[type="text"], input[type="email"], input[type="number"],
input[type="search"], input[type="date"], input[type="month"] {
  font: inherit; height: 40px; padding: 0 var(--s-3); border-radius: var(--radius-xs);
  border: 1px solid var(--line); background-color: var(--paper); color: var(--ink);
  transition: border-color 0.15s, box-shadow 0.15s;
}
textarea { height: auto; min-height: 96px; padding: var(--s-3) var(--s-3); line-height: 1.45; }
select {
  appearance: none; -webkit-appearance: none; padding-right: var(--s-7); font-weight: 700;
  font-size: var(--fs-control); cursor: pointer;
  --chev: transparent calc(50% - 1px), var(--ink-2) calc(50% - 1px), var(--ink-2) calc(50% + 1px), transparent calc(50% + 1px);
  background-image: linear-gradient(45deg, var(--chev)), linear-gradient(135deg, var(--chev));
  background-position: calc(100% - 20px) 55%, calc(100% - 14px) 55%;
  background-size: 6px 6px; background-repeat: no-repeat;
}
select:hover, textarea:hover, input:hover { border-color: color-mix(in srgb, var(--accent) 55%, transparent); }
select:disabled, textarea:disabled, input:disabled {
  opacity: var(--dim); cursor: not-allowed; border-color: var(--line); background-color: var(--paper-2);
}
select:focus-visible, textarea:focus-visible, input:focus-visible {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 30%, transparent);
}
input[type="checkbox"], input[type="radio"] {
  width: 18px; height: 18px; margin: 0; accent-color: var(--accent); vertical-align: -3px; cursor: pointer;
}
input[type="file"] { font: inherit; font-size: var(--fs-md); color: var(--ink-2); max-width: 100%; }
input[type="file"]::file-selector-button {
  height: 40px; margin-right: var(--s-3); padding: 0 var(--s-4); border-radius: var(--radius-sm);
  border: 1px solid var(--line); background: var(--paper); color: var(--ink);
  font: inherit; font-weight: 800; cursor: pointer;
}
input[type="file"]::file-selector-button:hover { background: var(--paper-2); }
@media (max-width: 899px) {
  input[type="password"], input[type="text"], input[type="email"], input[type="number"],
  input[type="search"], textarea { font-size: var(--fs-lg); }
}
.field-label { display: block; font-size: var(--fs-sm); font-weight: 700; color: var(--ink-2); }

form.inline, span.inline { display: inline-flex; flex-wrap: wrap; gap: var(--s-1); align-items: center; margin: 0; max-width: 100%; }
form.inline select { flex: 1 1 160px; min-width: 0; max-width: 100%; }
.banner {
  padding: var(--s-3) var(--s-4); border-radius: var(--radius-sm); margin: var(--s-4) 0;
  background: var(--paper); border: 1px solid var(--line); box-shadow: var(--shadow-sm);
}
.banner.error {
  display: flex; align-items: flex-start; gap: var(--s-2);
  border-color: var(--bad); border-width: 2px;
}
.banner.error > svg { flex: none; margin-top: 1px; color: var(--bad); }
.banner.error .banner-text { flex: 1 1 auto; min-width: 0; }
.banner.error .retry { flex: none; font-weight: 700; }
.card > .banner.error:first-child { margin: 0 0 var(--s-4); }
.note { display: flex; gap: var(--s-1); align-items: flex-start; color: var(--ink-2); font-size: var(--fs-sm); margin-top: var(--s-1); }
.note svg { flex: none; margin-top: 2px; color: var(--accent); }

.bar-track {
  display: block; height: 12px; border-radius: var(--radius-pill); background: var(--paper-2); overflow: hidden;
}
.bar-fill {
  display: block; height: 100%; width: calc(var(--pct, 0) * 1%); min-width: 4px;
  border-radius: var(--radius-pill); background: var(--accent);
}
.not-spending .bar-fill { background: var(--lilac-bar); }
.uncat .bar-fill { background: var(--gray); }

.spaced { margin-top: var(--s-5); }
.scene + .page > :first-child { margin-top: var(--s-5); }
.phone-me { display: none; text-decoration: none; }
.phone-me .avatar { width: 44px; height: 44px; font-size: var(--fs-md); background: var(--paper); }
@media (max-width: 899px) { .phone-me { display: inline-flex; } }

/* --- .lba: a label, a bar and an amount on one row ------------------------
   One grid for every such row (categories, groups, whose it was, the
   check-in's lists, what changed), so columns line up across sections and
   pages. Children: .lba-label, .lba-bar (optional), .lba-amount; anything
   else can take grid-area: bar. Wide: three columns, the label at least
   --row-label and at most 30%, wrapping to two lines with the bar beside the
   first. Phones: label and amount on one line, the bar full width below.
   A line under the row that belongs to the bar (a split, a note) indents by
   --lba-indent. */

.lba {
  display: grid; align-items: start; gap: var(--s-1) var(--s-3); line-height: 1.3;
  grid-template-columns: minmax(var(--row-label), 30%) minmax(0, 1fr) minmax(var(--row-amount), max-content);
  grid-template-areas: "label bar amount";
}
.lba-label { grid-area: label; min-width: 0; overflow-wrap: anywhere; }
.lba:not(:has(> .lba-bar)) > .lba-label { grid-column: label-start / bar-end; }
.lba-bar { grid-area: bar; align-self: start; margin-top: calc((1lh - 12px) / 2); }
.lba-amount { grid-area: amount; justify-self: end; text-align: right; }
@media (max-width: 899px) {
  .lba {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "label amount" "bar bar";
  }
  .lba:not(:has(> .lba-bar)) > .lba-label { grid-column: label; }
  .lba-bar { margin-top: 0; }
}

/* --- empty states (the empty() macro) -------------------------------------
   A small scene, one line, one button. .empty.is-done swaps the sun for a
   tick: nothing left to do. */

.empty {
  display: flex; flex-direction: column; align-items: center; gap: var(--s-3);
  padding: var(--s-6) var(--s-4); text-align: center;
}
.empty-art {
  position: relative; overflow: hidden; width: 120px; height: 72px; border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: linear-gradient(to bottom, var(--sky-top), var(--sky-mid) 60%, var(--sky-low));
}
.empty-art .sun {
  position: absolute; right: 22px; top: 12px; width: 26px; height: 26px; border-radius: 50%;
  background: radial-gradient(circle at 40% 40%, var(--sun-core), var(--sun) 58%, transparent 72%);
}
.empty-art .hill {
  position: absolute; left: -15%; right: -15%; bottom: -44px; height: 72px; border-radius: 50%;
  background: var(--hill-mid);
}
.empty-art .tick {
  position: absolute; right: 18px; top: 8px; width: 32px; height: 32px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--paper); color: var(--ok); box-shadow: var(--shadow-sm);
}
.empty-title { margin: 0; font-weight: 700; color: var(--ink); }
.empty-body { margin: 0; max-width: 42ch; font-size: var(--fs-md); color: var(--ink-2); }
.empty .btn { margin-top: var(--s-1); }

/* --- pending: the control that started a request ---------------------------
   app.js adds .is-pending and aria-busy="true" to the button it is waiting
   on and removes both when the response comes. The label dims to 60%, a
   spinner (a rotating border, no image) sits beside it, and taps stop. */

.is-pending { pointer-events: none; cursor: progress; }
.btn.is-pending { color: color-mix(in srgb, var(--on-accent) 60%, transparent); }
.btn.quiet.is-pending, .link-btn.is-pending, .seg .is-pending {
  color: color-mix(in srgb, var(--ink) 60%, transparent);
}
.is-pending::after {
  content: ""; display: inline-block; flex: none; width: 1em; height: 1em;
  margin-left: var(--s-1); vertical-align: -0.15em; border-radius: 50%;
  border: 2px solid currentColor; border-right-color: transparent;
  animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* --- skip link: the first thing Tab reaches on every page (A1) ------------- */

.skip-link {
  position: absolute; left: var(--s-3); top: var(--s-3); z-index: 20;
  padding: var(--s-2) var(--s-4); border-radius: var(--radius-sm);
  background: var(--accent); color: var(--on-accent); font-weight: 800; text-decoration: none;
  transform: translateY(calc(-100% - var(--s-4)));
}
.skip-link:focus, .skip-link:focus-visible { transform: none; color: var(--on-accent); }
.main:focus { outline: none; }  /* where the skip link lands; focus isn't an action there */

/* --- .seg with radios: one choice of a few, posted with its form (C6) -------
   Markup: .seg > .choice > input[type=radio] + label. The radio stays in the
   form and the tab order, out of sight; its label is the segment. */

.seg .choice { position: relative; display: inline-flex; }
.seg .choice input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; pointer-events: none; }
.seg .choice label {
  display: inline-flex; align-items: center; min-height: 34px; padding: 0 var(--s-4);
  border-radius: var(--radius-pill); color: var(--ink-2); font-weight: 700; font-size: var(--fs-md);
  cursor: pointer; transition: background-color var(--dur-2), color var(--dur-2);
}
.seg .choice label:hover { color: var(--ink); }
.seg .choice input:checked + label {
  background: var(--accent); color: var(--on-accent); font-weight: 800; box-shadow: var(--shadow-sm);
}
.seg .choice input:focus-visible + label { outline: 3px solid var(--accent); outline-offset: 2px; }

/* A picker's Save shows once a different option is picked: until then the
   selected option is still the one the page came with (review, transactions).
   No :has(), no harm: it shows. */
.pick:has(option[selected]:checked) .save { display: none; }

/* --- touch and motion (M2-M5, M7-M11; D2: smooth, eased, no overshoot) ------
   Everything that changes on screen moves there instead of jumping, briefly
   and on the tokens above. Presses shrink a little at --dur-1; hover and
   colour changes ease at --dur-2. motion.js adds .motion-in on a fresh load
   (never on Back or Forward), which plays the sections' entrance and the
   bars' fill once, then swaps it for .motion-ready so later changes to a bar
   (a fetch updating it) transition instead. The reduced-motion block at the
   end switches all of it off. */

/* M2/M3: what you can tap eases its colours, border and shadow, and its
   pressed state. The browser's own grey tap flash would double up. */
.btn, .seg a, .seg button, .month-nav a, .back, a.card, .cat-row, .tabbar a, .tabbar svg,
.sidenav a, .pill, .people label, .seg .choice label, .switch {
  -webkit-tap-highlight-color: transparent;
  transition: transform var(--dur-2) var(--ease-out), background-color var(--dur-2) var(--ease-out),
    color var(--dur-2) var(--ease-out), border-color var(--dur-2) var(--ease-out),
    box-shadow var(--dur-2) var(--ease-out);
}
input[type="file"]::file-selector-button {
  transition: transform var(--dur-2) var(--ease-out), background-color var(--dur-2) var(--ease-out);
}
/* Pressing is quicker than letting go: the :active rule's own duration
   applies on the way in. */
.btn:active, .seg a:active, .seg button:active, .month-nav a:active, .back:active, a.card:active,
.cat-row:active, .tabbar a:active, .sidenav a:active, .people label:active, .seg .choice label:active,
.switch:active, input[type="file"]::file-selector-button:active {
  transform: scale(.97); transition-duration: var(--dur-1);
}

/* M3: the tab bar's hover, for a trackpad or mouse on a narrow window. */
@media (hover: hover) {
  .tabbar a:hover { color: var(--ink); }
  .tabbar a:hover svg { background: color-mix(in srgb, var(--accent) 8%, transparent); }
  .tabbar a[aria-current="page"]:hover { color: var(--accent); }
  .tabbar a[aria-current="page"]:hover svg { background: color-mix(in srgb, var(--accent) 16%, transparent); }
}

/* M11: link cards and category rows lift under a pointer (not on touch,
   where hover sticks after a tap). A row has no surface of its own, so it
   takes a faint one, widened by a ring of the same colour rather than
   padding, which would shift its columns. */
@media (hover: hover) {
  a.card:hover { transform: translateY(-2px); box-shadow: var(--shadow-lg); }
  .cat-row { border-radius: var(--radius-xxs); }
  .cat-row:hover {
    --lift-bg: color-mix(in srgb, var(--paper-2) 55%, transparent);
    transform: translateY(-2px); background: var(--lift-bg);
    box-shadow: 0 0 0 var(--s-1) var(--lift-bg), var(--shadow-sm);
  }
}

/* M4: bars fill once, row after row (motion.js sets --fill-delay: 20ms a
   row, at most 200ms). The keyframes give only the start; the end is the
   bar's own length. */
.motion-in .bar-fill { animation: fill-width var(--dur-3) var(--ease-out) var(--fill-delay, 0ms) backwards; }
.motion-in .col-fill { animation: fill-height var(--dur-3) var(--ease-out) var(--fill-delay, 0ms) backwards; }
@keyframes fill-width { from { width: 0; } }
@keyframes fill-height { from { height: 0; } }
.motion-ready .bar-fill { transition: width var(--dur-3) var(--ease-out); }
.motion-ready .col-fill { transition: height var(--dur-3) var(--ease-out); }

/* M10: the first six sections ease in, 40ms apart (--enter-delay). They
   start readable, a little low and at 0.6, so a slow device or a screenshot
   never catches a blank page. */
.motion-in .enter { animation: enter var(--dur-4) var(--ease-out) var(--enter-delay, 0ms) backwards; }
@keyframes enter { from { opacity: 0.6; transform: translateY(8px); } }

/* M5: pages cross-fade. The sidebar, the tab bar and the page's sky are
   named, so they hold still while the content between them changes. */
@view-transition { navigation: auto; }
.sidebar { view-transition-name: sidebar; }
.tabbar { view-transition-name: tabbar; }
.scene { view-transition-name: scene; }
::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) {
  animation-duration: var(--dur-3); animation-timing-function: var(--ease-in-out);
}

@media (max-width: 899px) {
  /* The dashboard's and categories' sky is .dash-sky on a phone (on a wide
     screen it has no box of its own). */
  .dash-sky { view-transition-name: scene; }

  /* M8: 44px to tap. Text links keep their look and get a taller hit area. */
  .seg a, .seg button { height: 44px; }
  .seg .choice label { height: 44px; }
  .btn.sm, select, input[type="password"], input[type="text"], input[type="email"],
  input[type="number"], input[type="search"], input[type="date"], input[type="month"] { height: 44px; }
  .card-head a, .hero-pill a, .multiple figcaption a { position: relative; display: inline-block; min-width: 44px; }
  .cat-row { position: relative; }
  .card-head a::after, .hero-pill a::after, .multiple figcaption a::after, .cat-row::after, .switch::after {
    content: ""; position: absolute; left: 0; right: 0;
    top: min(0px, calc(50% - 23px)); bottom: min(0px, calc(50% - 23px));  /* 46: rounding-safe */
  }

  /* M9: the stars keep to the sky above the words, clear of the avatar's
     corner. On .dash-sky that is its top padding. */
  .deco::before { right: var(--corner); }
  .dash-sky .deco::before { height: var(--s-5); }
}

/* M7: reduced motion. Nothing moves: no transitions, no animations, no
   press or lift, no page cross-fade. The pending spinner stays as a still
   ring; aria-busy says the rest. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after, input[type="file"]::file-selector-button {
    transition: none !important; animation: none !important; scroll-behavior: auto !important;
  }
  .btn:active, .seg a:active, .seg button:active, .month-nav a:active, .back:active, a.card:active,
  .cat-row:active, .tabbar a:active, .sidenav a:active, .people label:active, .seg .choice label:active,
  .switch:active, input[type="file"]::file-selector-button:active, a.card:hover, .cat-row:hover {
    transform: none;
  }
  @view-transition { navigation: none; }
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) {
    animation: none !important;
  }
}

/* --- in place: what app.js does instead of a reload (I1-I8, M12, M13) ------
   html.js is set by app.js. A control marked data-js-hide is only for the
   no-JS path (Save beside a select that now sends itself); it comes back
   while pending, which is how Upload shows "Reading the statement…". After a
   swap, a number that changed cross-fades (.settle), a bar eases to its new
   length (.settle-bar), and a changed transactions row flashes once. */

.js [data-js-hide]:not(.is-pending) { display: none; }
@media (prefers-reduced-motion: no-preference) {
  .bar-fill.settle-bar { transition: width var(--dur-3) var(--ease-in-out); }
  .col-fill.settle-bar { transition: height var(--dur-3) var(--ease-in-out); }
  .settle { animation: settle var(--dur-2) var(--ease-out); }
  tr.flash > td { animation: flash var(--dur-4) var(--ease-out); }
}
@keyframes settle { from { opacity: 0.35; } }
@keyframes flash { from { background-color: color-mix(in srgb, var(--accent) 10%, transparent); } }
