/* Hallmark · macrostructure: ledger-strip app shell
   (anchor figure → ruled account ledger → month sentence → transaction stream)
   · theme: custom light dual-tone — pine + clay
   · knobs: rows-not-cards, hairline-ruled, right-aligned tabular figures,
            keypad-first amount entry
   · nav: bottom tab bar (app chrome, not marketing N1a) · footer: none (app)
   · pre-emit critique: P5 H5 E4 S5 R4 V5
   · contrast: pass (40–41) · slop: pass (42–45) · honest: pass (46)
   · chrome: pass (47) · tokens: pass (48) · responsive: pass (49)
   · mobile: pass (34, 49, 50–57) · icons: pass (30)

   PHILOSOPHY — the position this UI takes:
   Spending is not an emergency. Most finance apps paint every expense red, which
   turns opening the app into being told off. Here expenses are neutral ink; the
   accent only appears when you cross a limit YOU set. Colour is reserved for the
   two things that are genuinely worth your attention: money coming in, and a
   budget you chose being passed.

   TWO COLOURS, and what each is for:
     pine  — structure and income. Primary actions, the anchor figure, positive.
     clay  — attention only. Active tab, over-budget, refund marks. Never
             decoration; footprint stays well under the 5% accent budget.
*/

@import url("fonts.css");

/* ===========================================================================
   Tokens — every colour and font in this file resolves to one of these.
   =========================================================================== */

:root {
  /* --- the two colours --- */
  --color-pine:        oklch(38% 0.055 175);
  --color-pine-hover:  oklch(33% 0.058 175);
  --color-pine-soft:   oklch(92% 0.028 175);
  --color-pine-ink:    oklch(98.5% 0.006 175);   /* text ON pine */

  --color-clay:        oklch(64% 0.145 52);
  --color-clay-soft:   oklch(94% 0.038 52);
  --color-clay-ink:    oklch(24% 0.045 52);      /* text ON clay */

  /* --- paper + ink. Every neutral is tinted toward the pine hue; pure grey
         reads as flat, and pure #fff/#000 are never used as a base. --- */
  --color-paper:       oklch(98.6% 0.006 175);
  --color-paper-2:     oklch(96.4% 0.008 175);
  --color-paper-3:     oklch(93.6% 0.010 175);
  --color-line:        oklch(88.5% 0.012 175);
  --color-line-strong: oklch(80% 0.014 175);
  --color-ink:         oklch(24% 0.020 175);
  --color-ink-2:       oklch(40% 0.018 175);
  --color-muted:       oklch(50% 0.016 175);     /* 50%, not 55%: clears 4.5:1 on paper-3 */

  --color-focus:       oklch(48% 0.13 235);      /* distinct from both brand colours */

  /* --- type. Archivo is self-hosted (offline-first). The CJK fallback is
         mandatory: a Latin-only stack renders Chinese as PMingLiU. --- */
  --font-display: "Archivo", "Microsoft JhengHei", "微軟正黑體",
                  "PingFang TC", "Noto Sans TC", sans-serif;
  --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI",
               "Microsoft JhengHei", "微軟正黑體", "PingFang TC",
               "Noto Sans TC", sans-serif;

  --text-2xs: 0.6875rem;
  --text-xs:  0.75rem;
  --text-sm:  0.8125rem;
  --text-base: 0.9375rem;
  --text-md:  1.0625rem;
  --text-lg:  1.3125rem;
  --text-xl:  1.75rem;
  --text-2xl: 2.25rem;
  --text-3xl: 3rem;

  --lh-tight: 1.08;
  --lh-snug:  1.3;
  --lh-body:  1.55;

  /* --- spacing: multiples of 4, named. No arbitrary px anywhere. --- */
  --space-3xs: 4px;
  --space-2xs: 8px;
  --space-xs:  12px;
  --space-sm:  16px;
  --space-md:  20px;
  --space-lg:  24px;
  --space-xl:  32px;
  --space-2xl: 40px;
  --space-3xl: 48px;
  --space-4xl: 64px;
  --space-5xl: 80px;

  --radius-sm: 8px;
  --radius:    12px;
  --radius-lg: 18px;
  --radius-pill: 999px;

  --shadow-low:  0 1px 2px oklch(24% 0.02 175 / 0.06);
  --shadow-lift: 0 6px 20px oklch(24% 0.02 175 / 0.10);

  /* Two sticky layers, deliberately split: the top bar must always out-paint an
     in-page sticky header, or a section head bleeds through it on scroll. */
  --z-sticky: 200;
  --z-sticky-nav: 300;
  --z-sheet: 400;
  --z-toast: 500;

  --bar-h: 56px;
  --tab-h: 60px;
  --tap: 44px;          /* minimum touch target */
  --measure: 62ch;      /* prose width, inside the 45–75ch band */

  --ease: cubic-bezier(0.32, 0.72, 0, 1);
  --dur-fast: 120ms;
  --dur: 200ms;
}

/* ===========================================================================
   Reset + page frame
   =========================================================================== */

*, *::before, *::after { box-sizing: border-box; }

/* The `hidden` attribute is only a UA `display: none`, so ANY explicit display
   in this file silently beats it — a `.field { display: grid }` with hidden set
   stays on screen. This project has already shipped that bug once (a modal that
   wouldn't close). One rule removes the whole class of it. */
[hidden] { display: none !important; }

html, body {
  /* clip, not hidden: hidden would break position: sticky on descendants. */
  overflow-x: clip;
  margin: 0;
}

html {
  background: var(--color-paper-2);
  -webkit-text-size-adjust: 100%;
}

body {
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--lh-body);
  color: var(--color-ink);
  background: var(--color-paper);
  /* No italic CJK anywhere: there is no true italic and the browser shears the
     glyphs, which looks broken. */
  font-style: normal;
}

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 700;
  line-height: var(--lh-tight);
  font-style: normal;
  margin: 0;
  /* Long compound words and CJK strings must be able to break, or a title
     overflows the viewport on a 320px phone. */
  overflow-wrap: anywhere;
  min-width: 0;
}

p { margin: 0; }
button { font: inherit; color: inherit; }

/* Numerals must not jitter as they change — this is a ledger. */
.num {
  font-family: var(--font-display);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  letter-spacing: -0.01em;
}

.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* Focus rings appear instantly — a keyboard user needs the indicator now, and a
   fading ring reads as lag. Never transitioned. */
:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* ===========================================================================
   App shell
   =========================================================================== */

/* height, not min-height: with min-height the grid grows past the viewport, the
   PAGE scrolls instead of the content region, and the bar and tabs only stay put
   because they're sticky. Constraining the height makes the middle row the only
   scroller — which is what an installed app should feel like, and it means
   resetting scroll on a screen change actually works instead of relying on the
   browser happening to clamp scrollY when the content gets shorter. */
.app {
  height: 100dvh;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  max-width: 34rem;
  margin-inline: auto;
  background: var(--color-paper);
  position: relative;
}

/* --- top bar. No position: sticky needed — it's a fixed grid row now, which
       also means there is never a second sticky element fighting it. --- */
.bar {
  z-index: var(--z-sticky-nav);
  height: var(--bar-h);
  padding-inline: var(--space-sm);
  padding-top: env(safe-area-inset-top);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  background: color-mix(in oklch, var(--color-paper) 88%, transparent);
  backdrop-filter: blur(12px) saturate(150%);
  -webkit-backdrop-filter: blur(12px) saturate(150%);
  border-bottom: 1px solid var(--color-line);
}

.bar__mark {
  display: flex;
  align-items: center;
  /* The two glyphs of 記帳 must not stack when the actions beside them get wider. */
  flex: none;
  white-space: nowrap;
  gap: var(--space-2xs);
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--text-md);
  letter-spacing: -0.02em;
  line-height: 1;
}
.bar__glyph {
  width: 22px; height: 22px;
  flex: none;
  color: var(--color-pine);
}
.bar__actions { display: flex; align-items: center; gap: var(--space-3xs); }

/* --- scroll region --- */
.scroll {
  overflow-y: auto;
  overflow-x: clip;
  /* contain stops a scroll that reaches the end from pulling the page behind it,
     which on iOS is what produces the rubber-band-the-whole-app feel. */
  overscroll-behavior-y: contain;
  -webkit-overflow-scrolling: touch;
  padding-bottom: var(--space-xl);
}

.screen { display: none; }
.screen[data-active] { display: block; }

/* --- bottom tabs. App chrome: three destinations, icon + label, one active
       marker in clay. Not the marketing wordmark+links+button pattern. --- */
.tabs {
  z-index: var(--z-sticky-nav);
  height: calc(var(--tab-h) + env(safe-area-inset-bottom));
  padding-bottom: env(safe-area-inset-bottom);
  display: grid;
  /* Derived from the number of tabs, not hard-coded: adding 分析 as a fourth tab
     against a `repeat(3, …)` pushed 設定 onto a second row at 320px. */
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  align-items: center;
  background: color-mix(in oklch, var(--color-paper) 92%, transparent);
  backdrop-filter: blur(12px) saturate(150%);
  -webkit-backdrop-filter: blur(12px) saturate(150%);
  border-top: 1px solid var(--color-line);
}

.tab {
  appearance: none;
  border: 0;
  background: none;
  min-height: var(--tap);
  display: grid;
  justify-items: center;
  gap: 2px;
  padding: var(--space-2xs) var(--space-3xs);
  color: var(--color-muted);
  cursor: pointer;
  position: relative;
  transition: color var(--dur-fast) var(--ease);
}
.tab__icon { width: 20px; height: 20px; }
.tab__label {
  font-size: var(--text-2xs);
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;   /* a tab label must never wrap to two lines */
}
.tab:hover { color: var(--color-ink-2); }
.tab:active { color: var(--color-pine); }
.tab[aria-selected="true"] { color: var(--color-pine); }
.tab[aria-selected="true"]::after {
  content: "";
  position: absolute;
  top: 0;
  inline-size: 26px;
  block-size: 3px;
  border-radius: 0 0 var(--radius-pill) var(--radius-pill);
  background: var(--color-clay);
}
.tab:disabled { opacity: 0.55; cursor: not-allowed; }

/* ===========================================================================
   Section rhythm — sections are separated by a shift of surface or a rule,
   never by equal whitespace alone.
   =========================================================================== */

.sec { padding: var(--space-lg) var(--space-sm) var(--space-md); }
.sec--tint { background: var(--color-paper-2); border-block: 1px solid var(--color-line); }

/* Eyebrow sits directly ABOVE its heading in the same column — never beside it. */
.sec__head { display: flex; flex-direction: column; gap: 2px; margin-bottom: var(--space-xs); }
.sec__eyebrow {
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--color-muted);
}
.sec__title { font-size: var(--text-md); font-weight: 700; }
.sec__aside { font-size: var(--text-xs); color: var(--color-muted); }
.sec__cta { margin-top: var(--space-sm); }

/* ===========================================================================
   Anchor figure — net worth
   =========================================================================== */

.anchor {
  padding: var(--space-lg) var(--space-sm) var(--space-xl);
  /* Bottom padding is heavier than top so the block sits INTO the page rhythm
     rather than floating above it. */
  display: grid;
  gap: var(--space-3xs);
}
.anchor__label {
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-muted);
}
.anchor__figure {
  font-size: clamp(2rem, 11vw, var(--text-3xl));
  font-weight: 800;
  line-height: 1.02;   /* ≥1.0 so cap-tops can't collide when it wraps */
  color: var(--color-pine);
}
/* A figure never stands alone as the headline — this line says what it means. */
.anchor__gloss {
  font-size: var(--text-sm);
  color: var(--color-ink-2);
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2xs);
}
.anchor__sep { color: var(--color-line-strong); }

/* ===========================================================================
   Ledger rows — the core pattern. Rows, not cards: no nesting, no side stripes,
   and figures line up in a single right-hand column you can scan down.
   =========================================================================== */

.ledger { display: grid; }
/* A ledger that sits directly on the page rather than inside .sec has to carry
   its own gutter, or the right-aligned figures run into the screen edge. */
.ledger--page { padding-inline: var(--space-sm); }
@media (min-width: 30rem) { .ledger--page { padding-inline: var(--space-md); } }

.row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-xs);
  min-height: var(--tap);
  padding: var(--space-xs) 0;
  border-top: 1px solid var(--color-line);
  text-align: start;
  background: none;
  border-inline: 0;
  border-bottom: 0;
  width: 100%;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease);
}
.row:first-child { border-top: 0; }
.row:hover { background: var(--color-paper-2); }
.row:active { background: var(--color-paper-3); }
.row:disabled { opacity: 0.55; cursor: not-allowed; }
/* No leading mark column — for lists whose rows have no glyph to show, where a
   reserved empty column just indents everything for nothing. */
.row--plain { grid-template-columns: minmax(0, 1fr) auto; }
.row--static { cursor: default; }
.row--static:hover, .row--static:active { background: none; }

.row__mark {
  width: 34px; height: 34px;
  flex: none;
  border-radius: var(--radius-sm);
  display: grid;
  place-items: center;
  font-size: var(--text-md);
  line-height: 1;
  background: var(--color-paper-3);
}
/* Uncategorised: a hairline ring, not an empty filled square — a blank tinted
   box reads as a placeholder that failed to load. */
.row__mark--bare {
  background: none;
  border: 1px solid var(--color-line);
  border-radius: 50%;
  width: 22px; height: 22px;
  margin-inline: 6px;
}
.row__body { min-width: 0; display: grid; gap: 1px; }
.row__name {
  font-weight: 600;
  font-size: var(--text-base);
  line-height: var(--lh-snug);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.row__meta {
  font-size: var(--text-xs);
  color: var(--color-muted);
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  min-width: 0;
}
.row__meta > span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* "this row has a receipt", in the icon set rather than as an emoji. */
.row__meta svg { width: 13px; height: 13px; flex: none; opacity: 0.75; }
.row__dot { width: 4px; height: 4px; border-radius: 50%; background: var(--color-line-strong); flex: none; }

.row__figure {
  text-align: end;
  font-size: var(--text-base);
  font-weight: 700;
  white-space: nowrap;
}
.row__figure--in  { color: var(--color-pine); }
.row__figure--out { color: var(--color-ink); }
.row__figure--owe { color: var(--color-clay); }
.row__sub {
  display: block;
  font-size: var(--text-2xs);
  font-weight: 500;
  color: var(--color-muted);
}

/* ===========================================================================
   Month sentence — prose, not a stat tile. A sentence carries the comparison
   better than four numbers in boxes, and it can't be mistaken for a fake metric.
   =========================================================================== */

.month { display: grid; gap: var(--space-xs); }
.month__line {
  font-size: var(--text-base);
  line-height: var(--lh-body);
  max-width: var(--measure);
  color: var(--color-ink-2);
}
.month__line b { color: var(--color-ink); font-weight: 700; }
.month__line .num { font-weight: 700; color: var(--color-ink); }

.meter {
  block-size: 6px;
  border-radius: var(--radius-pill);
  background: var(--color-paper-3);
  /* Not `overflow: hidden`: the goal pace tick (.meter__mark) is deliberately
     taller than the bar so it reads as a marker rather than a segment of the fill. */
  position: relative;
}
.meter__fill {
  block-size: 100%;
  border-radius: var(--radius-pill);
  background: var(--color-pine);
  transition: inline-size var(--dur) var(--ease);
}
.meter__fill[data-over] { background: var(--color-clay); }
.meter__legend {
  display: flex;
  justify-content: space-between;
  gap: var(--space-2xs);
  font-size: var(--text-2xs);
  color: var(--color-muted);
}

/* ===========================================================================
   Buttons — 8 states covered: default, hover, focus-visible, active, disabled,
   plus the loading/selected variants used by the record sheet.
   =========================================================================== */

.btn {
  appearance: none;
  border: 1px solid transparent;
  border-radius: var(--radius);
  min-height: var(--tap);
  padding: 0 var(--space-md);
  display: inline-flex;
  align-items: center;      /* mixed-height rows must be explicitly centred */
  justify-content: center;
  gap: var(--space-2xs);
  font-weight: 700;
  font-size: var(--text-base);
  line-height: 1;
  white-space: nowrap;      /* a button label never wraps */
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}
.btn--primary { background: var(--color-pine); color: var(--color-pine-ink); }
.btn--primary:hover { background: var(--color-pine-hover); }
.btn--primary:active { background: var(--color-pine-hover); }
.btn--quiet {
  background: var(--color-paper-2);
  color: var(--color-ink);
  border-color: var(--color-line);
}
.btn--quiet:hover { background: var(--color-paper-3); }
.btn--ghost { background: none; color: var(--color-ink-2); }
.btn--ghost:hover { background: var(--color-paper-2); color: var(--color-ink); }
.btn--block { width: 100%; }
.btn:disabled { opacity: 0.55; cursor: not-allowed; }
.btn[aria-busy="true"] { cursor: progress; }
/* An inline <svg> has no intrinsic size, so inside a flex button it stretches to
   fill the whole thing — which is exactly what 新增帳戶 shipped as: a 44px-tall
   plus sign next to its own label. Sized here rather than at each call site. */
.btn svg { width: 18px; height: 18px; flex: none; }

.icon-btn {
  appearance: none;
  border: 1px solid transparent;
  background: none;
  inline-size: var(--tap);
  block-size: var(--tap);
  border-radius: var(--radius);
  display: grid;
  place-items: center;
  color: var(--color-ink-2);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}
.icon-btn:hover { background: var(--color-paper-2); color: var(--color-ink); }
.icon-btn:active { background: var(--color-paper-3); }
.icon-btn:disabled { opacity: 0.55; cursor: not-allowed; }
.icon-btn svg { width: 20px; height: 20px; }

/* Chips — the tap-to-change affordance for category / account / date. */
.chip {
  appearance: none;
  border: 1px solid var(--color-line);
  background: var(--color-paper);
  border-radius: var(--radius-pill);
  min-height: 36px;
  padding: 0 var(--space-xs);
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease);
}
.chip:hover { background: var(--color-paper-2); }
.chip:active { background: var(--color-paper-3); }
.chip[aria-pressed="true"] {
  background: var(--color-pine-soft);
  border-color: var(--color-pine);
  color: var(--color-pine);
}
.chip:disabled { opacity: 0.55; cursor: not-allowed; }
.chip__swatch { width: 10px; height: 10px; border-radius: 50%; flex: none; }

/* ===========================================================================
   Segmented control — 支出 / 收入 / 轉帳
   =========================================================================== */

.seg {
  display: grid;
  /* Auto columns rather than a fixed 3, so the two-option version (支出 / 收入 in
     the category manager) fills the width instead of leaving a third empty. */
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: var(--space-3xs);
  padding: var(--space-3xs);
  background: var(--color-paper-2);
  border: 1px solid var(--color-line);
  border-radius: var(--radius);
}
.seg__btn {
  appearance: none;
  border: 1px solid transparent;
  background: none;
  border-radius: var(--radius-sm);
  min-height: 36px;
  font-size: var(--text-sm);
  font-weight: 700;
  line-height: 1;
  white-space: nowrap;
  color: var(--color-muted);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}
.seg__btn:hover { color: var(--color-ink); }
.seg__btn:active { background: var(--color-paper-3); }
.seg__btn[aria-selected="true"] {
  background: var(--color-paper);
  color: var(--color-pine);
  box-shadow: var(--shadow-low);
}
.seg__btn:disabled { opacity: 0.55; cursor: not-allowed; }

/* ===========================================================================
   Fields — every state kept to one border-width so nothing shifts on focus.
   =========================================================================== */

.field { display: grid; gap: var(--space-3xs); }
.field__label {
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--color-muted);
}
.input {
  appearance: none;
  inline-size: 100%;
  min-height: var(--tap);          /* same height as .btn — no 38/44 mismatch */
  padding: 0 var(--space-xs);
  font: inherit;
  /* 16px minimum, overriding the 15px body size. Below 16px, iOS Safari zooms the
     page when a field takes focus and does not zoom back out — with the split
     rows that would happen on every second tap. */
  font-size: max(1rem, var(--text-base));
  color: var(--color-ink);
  background: var(--color-paper);
  border: 1px solid var(--color-line);
  border-radius: var(--radius);
  /* Reserved transparent ring so activating focus can't shift geometry. */
  outline: 2px solid transparent;
  outline-offset: 1px;
  transition: background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease);
}
/* `appearance: none` above also removes a select's arrow, which leaves a picker
   looking exactly like a text field — the account type, the parent category and
   the per-item category all shipped with nothing to say they open a list. The
   chevron is drawn here (inline SVG in a data URI, so it stays offline) and the
   right padding is widened so a long option can't run underneath it. */
select.input {
  padding-inline-end: var(--space-xl);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%234a5a56' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9.5l6 5 6-5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-xs) center;
  background-size: 18px 18px;
}
select.input:disabled { background-image: none; }

.input:hover { border-color: var(--color-line-strong); }
.input:focus-visible { outline-color: var(--color-focus); border-color: var(--color-line-strong); }
.input[aria-invalid="true"] { border-color: var(--color-clay); background: var(--color-clay-soft); }
.input:disabled { opacity: 0.55; cursor: not-allowed; background: var(--color-paper-2); }
.input::placeholder { color: var(--color-muted); }

/* Reserved even when empty, so an appearing error never pushes the form down. */
.field__help {
  min-block-size: 1lh;
  font-size: var(--text-xs);
  color: var(--color-muted);
}
.field__help[data-error] { color: var(--color-clay); font-weight: 600; }

/* ===========================================================================
   Record sheet — the amount keypad
   =========================================================================== */

.sheet-backdrop {
  position: fixed;
  inset: 0;
  z-index: var(--z-sheet);
  background: oklch(24% 0.02 175 / 0.34);
  display: grid;
  align-items: end;
  justify-items: center;
}

.sheet {
  inline-size: 100%;
  max-inline-size: 34rem;
  max-block-size: 94dvh;
  display: grid;
  /* head · scrolling body · pinned footer. The footer exists so the primary
     action is always reachable: with the save button inside the scroll area, a
     long form on a small phone hides it, and a list of 30 categories buries
     "新增" under everything you were trying to avoid scrolling through. */
  grid-template-rows: auto minmax(0, 1fr) auto;
  background: var(--color-paper);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  box-shadow: var(--shadow-lift);
  padding-bottom: env(safe-area-inset-bottom);
  animation: sheet-in var(--dur) var(--ease);
}
@keyframes sheet-in { from { transform: translateY(6%); } to { transform: none; } }

.sheet__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding: var(--space-xs) var(--space-2xs) var(--space-xs) var(--space-sm);
  border-bottom: 1px solid var(--color-line);
}
.sheet__title { font-size: var(--text-md); font-weight: 700; }
.sheet__body {
  overflow-y: auto;
  overflow-x: clip;
  overscroll-behavior-y: contain;
  padding: var(--space-sm);
  display: grid;
  gap: var(--space-sm);
  align-content: start;
}

.sheet__foot {
  display: grid;
  gap: var(--space-2xs);
  padding: var(--space-xs) var(--space-sm) var(--space-sm);
  border-top: 1px solid var(--color-line);
  background: var(--color-paper);
}
.sheet__foot:empty { display: none; }

/* The amount, as the unmistakable focus of the screen. */
.amount {
  display: grid;
  gap: var(--space-3xs);
  padding: var(--space-2xs) 0 var(--space-xs);
  border-bottom: 1px solid var(--color-line);
}
.amount__row {
  display: flex;
  align-items: baseline;
  gap: var(--space-2xs);
}
.amount__cur {
  font-size: var(--text-md);
  font-weight: 700;
  color: var(--color-muted);
}
.amount__value {
  font-size: clamp(2.25rem, 13vw, var(--text-3xl));
  font-weight: 800;
  line-height: 1.02;
  color: var(--color-ink);
  overflow-wrap: anywhere;
  min-width: 0;
}
.amount__value[data-empty] { color: var(--color-line-strong); }
/* The caret is motivated: it marks where the next keypad digit lands. */
.amount__caret {
  inline-size: 2px;
  block-size: 1em;
  background: var(--color-clay);
  align-self: center;
  animation: caret 1.1s steps(2, jump-none) infinite;
}
@keyframes caret { 0%, 100% { opacity: 1; } 50% { opacity: 0.15; } }

.keypad {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-2xs);
}
.key {
  appearance: none;
  border: 1px solid var(--color-line);
  background: var(--color-paper);
  border-radius: var(--radius);
  min-height: 52px;
  font-family: var(--font-display);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-lg);
  font-weight: 700;
  line-height: 1;
  color: var(--color-ink);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease);
}
.key:hover { background: var(--color-paper-2); }
.key:active { background: var(--color-pine-soft); border-color: var(--color-pine); }
.key:disabled { opacity: 0.55; cursor: not-allowed; }
.key--util { background: var(--color-paper-2); color: var(--color-ink-2); }
.key--util svg { width: 20px; height: 20px; }

.pickers { display: flex; flex-wrap: wrap; gap: var(--space-2xs); }

/* ===========================================================================
   #6 品項拆帳 — the basket inside one transaction
   =========================================================================== */

.split { display: grid; gap: var(--space-2xs); }
.split__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2xs);
}
.split__rows { display: grid; gap: var(--space-sm); }

/* name + amount on the first line, category under them, remove spanning both:
   at 320px there is no honest way to fit four controls on one row, and shrinking
   them below 44px to manage it would cost more than the second line does. */
.split__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 5.5rem var(--tap);
  gap: var(--space-2xs);
  align-items: center;
}
.split__amt { text-align: right; }
.split__cat { grid-column: 1 / 3; }
/* On the first line, level with the amount it belongs to. Spanning both rows
   centres it between them, where it reads as if it removes the category. */
.split__del { grid-column: 3; grid-row: 1; justify-self: center; }

.split__note { display: grid; justify-items: start; gap: var(--space-3xs); }

/* ===========================================================================
   #7 收據照片
   =========================================================================== */

.shots { display: grid; gap: var(--space-2xs); }

/* Scrolls sideways rather than wrapping: receipts arrive in ones and twos, and a
   strip that grows downward pushes the keypad off a small screen. */
.shots__strip {
  display: flex;
  gap: var(--space-2xs);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  padding-block-end: var(--space-3xs);
  scrollbar-width: thin;
}

.shot { position: relative; flex: none; }
.shot__open {
  appearance: none;
  display: block;
  padding: 0;
  inline-size: 72px;
  block-size: 72px;
  overflow: hidden;
  border: 1px solid var(--color-line);
  border-radius: var(--radius);
  background: var(--color-paper-2);
  cursor: pointer;
}
.shot__open:hover { border-color: var(--color-line-strong); }
.shot__img {
  inline-size: 100%;
  block-size: 100%;
  /* cover, so a portrait receipt fills its tile instead of sitting in bars. The
     full image is one tap away and is never cropped there. */
  object-fit: cover;
  display: block;
}
.shot__blank {
  display: grid;
  place-items: center;
  block-size: 100%;
  color: var(--color-muted);
}
.shot__blank svg { width: 22px; height: 22px; }

/* Overlapping the corner rather than sitting under the tile: at 72px there is no
   room for a second row. Smaller than the 44px .icon-btn on purpose — a 44px
   circle on a 72px tile covers the photo it is attached to — and the strip has
   clear space around it, so the target is still comfortable. */
.shot__del {
  position: absolute;
  inset-block-start: -7px;
  inset-inline-end: -7px;
  inline-size: 28px;
  block-size: 28px;
  border-radius: 50%;
  background: var(--color-paper);
  border: 1px solid var(--color-line-strong);
  box-shadow: var(--shadow-low);
}
.shot__del svg { width: 13px; height: 13px; }

.shot__badge {
  position: absolute;
  inset-block-end: 4px;
  inset-inline-start: 4px;
  padding: 1px var(--space-3xs);
  border-radius: var(--radius-sm);
  background: var(--color-clay-soft);
  color: var(--color-clay-ink);
  font-size: var(--text-2xs);
  font-weight: 700;
  line-height: 1.5;
}

/* #12 the two answers to a due recurring transaction. Wrapping rather than
   shrinking: at 320px 記下來 and 跳過 do not fit beside a name and a date, and
   chips below 36px stop being reliable targets. */
.due__acts { display: flex; flex-wrap: wrap; gap: var(--space-2xs); justify-content: end; }

/* ===========================================================================
   #8 budgets / #10 goals / #13 subscriptions
   =========================================================================== */

.monthnav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2xs);
}
.monthnav__label { font-weight: 700; font-size: var(--text-md); }
/* The chevrons are text, not icons: ‹ › at 20px are unambiguous, and two more
   entries in the sprite for something a character already says is waste. */
.monthnav .icon-btn { font-size: var(--text-lg); line-height: 1; }

/* A budget row carries a bar under its two lines, so the meter has to sit inside
   the body column rather than being a third child of .row. */
.row__body .meter { margin-block-start: var(--space-3xs); }

.goals { display: grid; gap: var(--space-sm); }
.goal {
  display: grid;
  gap: var(--space-3xs);
  padding: var(--space-xs) 0;
  border-top: 1px solid var(--color-line);
}
.goal:first-child { border-top: 0; }
.goal__head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-2xs); }
.goal__name { font-weight: 700; min-width: 0; overflow-wrap: anywhere; }
.goal__pct { font-weight: 700; color: var(--color-pine); flex: none; }
.goal__figures { font-size: var(--text-sm); color: var(--color-ink-2); }
.goal__acts { display: flex; flex-wrap: wrap; gap: var(--space-2xs); margin-block-start: var(--space-3xs); }

/* Where you should be by now, if the money had kept pace with the time. A tick on
   the bar, because "behind" is a comparison and a comparison needs two marks.
   (.meter itself is positioned where it is defined, ~480 lines up.) */
.meter__mark {
  position: absolute;
  inset-block: -3px;
  inline-size: 2px;
  background: var(--color-ink-2);
  border-radius: 1px;
}

/* #13 the two findings worth interrupting for: a charge after you cancelled, and a
   price rise. Clay-tinted because both cost money you didn't agree to. */
.alert {
  display: grid;
  gap: 2px;
  padding: var(--space-2xs) var(--space-xs);
  background: var(--color-clay-soft);
  border-radius: var(--radius);
  border-inline-start: 3px solid var(--color-clay);
}
.alert__title { font-weight: 700; font-size: var(--text-sm); color: var(--color-clay-ink); }
.alert__body { font-size: var(--text-sm); color: var(--color-clay-ink); line-height: var(--lh-snug); }
/* #11 the three figures a month comes down to. */
.report__figures {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: var(--space-2xs);
  margin-block: var(--space-xs);
}
.report__figure { display: grid; gap: 2px; }
.report__label { font-size: var(--text-xs); color: var(--color-muted); }
.report__value { font-size: var(--text-md); font-weight: 700; }
.report__title { font-size: var(--text-lg); margin-block-end: var(--space-2xs); }

.alerts { display: grid; gap: var(--space-2xs); }
.alert__acts { display: flex; flex-wrap: wrap; gap: var(--space-2xs); margin-block-start: var(--space-3xs); }
/* Chips on a clay ground need their own border, or they read as part of it. */
.alert .chip { background: var(--color-paper); border-color: var(--color-clay); }

/* #17 what moved / #21 weekday distribution */
.cmp { display: grid; gap: var(--space-2xs); margin-block-start: var(--space-xs); }
/* Named for its container, not `.bar` — which is the top app bar, three hundred
   lines up. A second `.bar` rule turned the header into a three-column grid and
   stacked the two glyphs of 記帳 on top of each other. */
.bars { display: grid; gap: var(--space-3xs); margin-block: var(--space-xs); }
.bars__row {
  display: grid;
  /* Fixed label and figure columns so the bars share one baseline: ragged bar
     starts make three-day-apart values look equal. */
  grid-template-columns: 2.6rem minmax(0, 1fr) 5.5rem;
  align-items: center;
  gap: var(--space-2xs);
}
.bars__label { font-size: var(--text-xs); color: var(--color-muted); }
.bars__value { font-size: var(--text-xs); text-align: end; color: var(--color-ink-2); }

/* The backup nudge is the one alert that is also a button. */
.alert--tap {
  appearance: none;
  inline-size: 100%;
  text-align: start;
  cursor: pointer;
  font: inherit;
}
.alert--tap:hover { background: oklch(90% 0.05 52); }

/* What the importer worked out, in sentences. Ruled down the side rather than
   boxed: this is reasoning to read, not a status badge to glance at. */
.notes {
  display: grid;
  gap: var(--space-3xs);
  padding-inline-start: var(--space-xs);
  border-inline-start: 2px solid var(--color-pine-soft);
}
.notes__line {
  font-size: var(--text-sm);
  line-height: var(--lh-snug);
  color: var(--color-ink-2);
}

/* Contained rather than cover: a receipt is only useful if all of it is there. */
.shot__full {
  inline-size: 100%;
  max-block-size: 68dvh;
  object-fit: contain;
  border-radius: var(--radius);
  background: var(--color-paper-2);
}

/* ===========================================================================
   Empty + toast
   =========================================================================== */

.empty {
  padding: var(--space-2xl) var(--space-sm);
  display: grid;
  gap: var(--space-2xs);
  justify-items: start;
  color: var(--color-muted);
}
.empty__title { font-size: var(--text-md); font-weight: 700; color: var(--color-ink); }
.empty__body { font-size: var(--text-sm); max-width: var(--measure); }

/* Toasts are for failures and for effects you cannot see — never a celebration
   of something already visible on screen. */
/* Top, not bottom. Every primary action in this app lives at the bottom — the tab
   bar, the sheet footer's 儲存 — and a toast down there covers the button the user
   is reaching for. The top edge has nothing to collide with. */
.toasts {
  position: fixed;
  inset-inline: 0;
  top: calc(var(--bar-h) + env(safe-area-inset-top) + var(--space-2xs));
  z-index: var(--z-toast);
  display: grid;
  justify-items: center;
  gap: var(--space-2xs);
  padding-inline: var(--space-sm);
  pointer-events: none;
}
.toast {
  max-inline-size: 32rem;
  inline-size: 100%;
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  padding: var(--space-xs) var(--space-sm);
  border-radius: var(--radius);
  font-size: var(--text-sm);
  font-weight: 600;
  background: var(--color-ink);
  color: var(--color-paper);
  box-shadow: var(--shadow-lift);
  animation: toast-in var(--dur) var(--ease);
}
.toast[data-kind="error"] { background: var(--color-clay); color: var(--color-clay-ink); }
.toast__num { font-variant-numeric: tabular-nums; }
@keyframes toast-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }

/* ===========================================================================
   Reduced motion — every animation above has an alternative here.
   =========================================================================== */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .amount__caret { animation: none; opacity: 1; }
  .sheet { animation: none; }
}

/* ===========================================================================
   Wider viewports — the phone layout is the primary target; this only relaxes
   the padding once there's room.
   =========================================================================== */

@media (min-width: 30rem) {
  .sec { padding-inline: var(--space-md); }
  .anchor { padding-inline: var(--space-md); }
  .sheet { border-radius: var(--radius-lg); margin-bottom: var(--space-md); }
  .sheet-backdrop { align-items: center; }
}

/* ===========================================================================
   #30 Print — this is how a month becomes a PDF.
   =========================================================================== */

/* Deliberately no PDF library. The operating system already produces PDFs, and it
   does it better than a few hundred kilobytes of JavaScript would in an app whose
   whole point is that it works offline. What prints is the open report panel: the
   app shell goes away and the sheet becomes the page. */
.print-only { display: none; }

@media print {
  /* Ink, not screen: a tinted background wastes toner and reads worse on paper. */
  :root {
    --color-paper: #fff;
    --color-paper-2: #fff;
    --color-paper-3: #f0f0f0;
    --color-ink: #111;
    --color-ink-2: #333;
    --color-muted: #555;
    --color-line: #ccc;
  }
  html, body { background: #fff; }

  .app, .toasts, .tabs, .bar, .no-print { display: none !important; }
  .print-only { display: revert; }

  /* The panel stops being a modal and becomes the document. */
  .sheet-backdrop {
    position: static;
    display: block;
    background: none;
    inset: auto;
  }
  /* With two panels open, only the top one is the thing being printed. */
  .sheet-backdrop:not(:last-of-type) { display: none !important; }
  .sheet {
    max-inline-size: none;
    max-block-size: none;
    display: block;
    border-radius: 0;
    box-shadow: none;
    animation: none;
    padding-bottom: 0;
  }
  /* The panel's own title bar goes too: the printed page has its own heading, and
     "月結報告 / 2026 年 7 月 月結報告" twice over is just noise on paper. */
  .sheet__head, .sheet__foot { display: none !important; }
  .sheet__body { overflow: visible; padding-inline: 0; }

  /* A figure split across two pages is worse than a shorter page. */
  .row, .sec, .alert, .bars__row, .goal, .notes { break-inside: avoid; }
  .field__label { break-after: avoid; }

  /* Borders survive printing; background fills usually don't, so the meter needs
     an outline to still read as a bar. */
  .meter { border: 1px solid var(--color-line); }
  .meter__fill { background: #444; print-color-adjust: exact; }
  .meter__fill[data-over] { background: #000; }

  @page { margin: 14mm; }
}

/* ===========================================================================
   #22 the answer to a question
   =========================================================================== */

.answer { display: grid; gap: var(--space-2xs); }

/* A result set has arbitrary columns, so it scrolls sideways rather than being
   squeezed into 320px. Wrapped in its own box so the scroll is obviously the
   table's and not the page's. */
.table-wrap {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  border: 1px solid var(--color-line);
  border-radius: var(--radius);
}
.table { border-collapse: collapse; inline-size: 100%; font-size: var(--text-sm); }
.table th, .table td {
  padding: var(--space-2xs) var(--space-xs);
  text-align: start;
  white-space: nowrap;
  border-block-end: 1px solid var(--color-line);
}
.table th { font-weight: 700; color: var(--color-muted); font-size: var(--text-xs); }
.table tr:last-child td { border-block-end: 0; }
.table td.num { text-align: end; font-family: var(--font-display); font-variant-numeric: tabular-nums; }

/* The generated SQL, always available and never in the way. */
.answer__sql summary {
  font-size: var(--text-xs);
  color: var(--color-muted);
  cursor: pointer;
  min-height: var(--tap);
  display: flex;
  align-items: center;
}
.answer__code {
  margin: 0;
  padding: var(--space-2xs);
  background: var(--color-paper-2);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
  line-height: var(--lh-snug);
  /* SQL has meaningful line breaks and is often long: wrap it rather than making
     the panel scroll sideways. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
