@font-face {
  font-family: "Fira Sans";
  src: url("./vendor/fira-sans.woff2") format("woff2");
  font-weight: 300 700;
  font-display: swap;
}

@font-face {
  font-family: "Fira Code";
  src: url("./vendor/fira-code.woff2") format("woff2");
  font-weight: 400 700;
  font-display: swap;
}

/* Two tones, same token names. Nothing below this block knows which is active.
   --color-active is separate from --color-primary because a fill dark enough
   for white text is too dark to read as a label on the tab bar, and vice
   versa. Ratios are measured against the surface each token actually sits on. */

/* Tone A — carbon & volt. AMOLED first: the canvas is true #000,
   so on an OLED phone those pixels are switched off rather than lit dark
   grey. Every surface above it is a near-black with a green cast, never a
   neutral grey — a warm-neutral ladder next to a green accent reads as two
   unrelated palettes.
   Sporty comes from the accent doing one job loudly and the rest of the
   screen staying almost invisible: high-chroma green on black is the whole
   contrast story, so nothing else needs to shout. The green is deliberately
   at the top of its range — a mid-green on true black reads as muted, because
   there is no lit surface anywhere near it to measure against. */
:root {
  --color-primary: #00EB62;        /* 13.06:1 on the page */
  --color-primary-hover: #3BFF87;
  --color-on-primary: #04140A;     /* 11.77:1 on primary */
  --shadow-primary: none;          /* signal green on black separates unaided */
  /* Blacker and deeper than a grey-canvas shadow: over #000 a soft shadow is
     invisible, so the popover is separated by its own surface plus a hard
     edge rather than by a gradient nobody can see. */
  --shadow-popover: 0 10px 30px rgba(0, 0, 0, 0.85), 0 0 0 1px rgba(20, 40, 30, 0.9);
  --color-scrim: rgb(0 0 0 / 0.72);
  --color-tabbar: color-mix(in srgb, var(--color-muted) 92%, transparent);
  --color-active: #00EB62;         /* tab label + focus ring, 13.1:1 */
  --color-accent: #2ED08A;         /* income, 10.5:1 */
  --color-destructive: #F2543D;    /* spending, 6.1:1 */
  --color-background: #000000;     /* AMOLED: the pixel is off */
  --color-foreground: #D2D8D5;     /* 14.5:1 */
  /* The ladder climbs in near-blacks so a section still reads as raised on
     an OLED panel without lighting a large area. */
  --color-muted: #0C100E;
  --color-input: #131816;
  --color-subtle: #7E938A;         /* 6.4:1 on the page */
  --color-border: #1C2420;
  --color-border-strong: #313D37;
  color-scheme: dark;

  /* A quarter off every step. The scale was drawn on a desktop window and
     read as too zoomed-in on a phone: the Add form ran 1188px on a 812px
     screen, so the Save button was always below the fold. Type is untouched —
     it was the air between things that was oversized, not the words. */
  --space-0: 3px;
  --space-1: 6px;
  --space-2: 9px;
  --space-3: 12px;
  --space-4: 18px;
  --space-5: 24px;
  --space-6: 30px;

  /* For marks small enough that 6px would round them into circles. */
  --radius-xs: 3px;
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 16px;
  --radius-xl: 20px;

  --font-ui: "Fira Sans", system-ui, -apple-system, sans-serif;
  --font-num: "Fira Code", ui-monospace, "SF Mono", Menlo, monospace;

  /* Motion — direction M2, "paper". One curve for the whole app: a hard
     start that decelerates to a stop, which is what a surface with weight
     does. Deliberately NOT an overshoot; a budget bar that springs past its
     stop draws a number that is not true.
     Three durations, and every animation in the app uses one of them. */
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --dur-fast: 140ms;   /* a control acknowledging a press */
  --dur: 300ms;        /* a surface arriving or leaving */
  --dur-slow: 620ms;   /* a value being drawn */
}

/* Tone B — peach room. Cards sit lighter than the canvas, matching the
   reference where the frame interior reads brighter than the wall. */
:root[data-theme="peach"] {
  /* Light pink fill, so the label goes dark rather than white. A pale fill
     on a pale card is only 1.6:1 against it, so the button is lifted off
     the card by a shadow rather than ringed by an outline. Nothing lighter
     than the salmon reaches 3:1 here, so the boundary is a depth cue and
     the label carries identification at 6.66:1. */
  --color-primary: #D7B4AD;        /* saturation 41% -> 34%, hue held at rose */
  --color-primary-hover: #E0C2BC;
  --color-on-primary: #4A2A22;     /* 6.66:1 on primary */
  --shadow-primary: 0 1px 3px rgba(74, 42, 34, 0.20), 0 1px 2px rgba(74, 42, 34, 0.12);
  --shadow-popover: 0 8px 24px rgba(74, 42, 34, 0.20), 0 2px 6px rgba(74, 42, 34, 0.12);
  /* Lighter than graphite's: the same opacity over a pale canvas reads as
     the lights going out rather than the page stepping back. */
  --color-scrim: rgb(74 42 34 / 0.38);
  /* The tab strip is the room's floor: a step off the canvas, not a tint of
     the card, which is what the graphite mix produces on a light ground. */
  --color-tabbar: #EFC5B7;
  --color-active: #8E3A26;         /* 4.8:1 on the tab strip */
  --color-accent: #2D674C;         /* income, 4.57:1 on the page */
  --color-destructive: #9F3B29;    /* spending, 4.60:1 on the page */
  --color-background: #F3CEC2;
  --color-foreground: #4A2A22;     /* 13:1 */
  /* The ladder steps up from the canvas without reaching white — a near-white
     input inside a peach room reads as a hole punched in the wall. */
  --color-muted: #F7DCD3;
  --color-input: #FBE8E1;
  --color-subtle: #7E5144;         /* 4.58:1 on the page, 5.63:1 on an input */
  --color-border: #E9C2B5;
  --color-border-strong: #D9A493;
  color-scheme: light;
}

/* Declared on the root as well as in the meta above. The meta is what the
   browser reads before any CSS arrives; this is what keeps the declaration
   true once the tone class has been applied. */
:root { color-scheme: light dark; }

* { box-sizing: border-box; }

/* Any `display` rule outranks the [hidden] attribute's UA style, so the moment
   an element is laid out the attribute silently stops hiding it. Both of the
   List's empty states are display:flex and were painting under a full list,
   1450px down where nobody scrolled. One guard rather than a patch per class. */
[hidden] { display: none !important; }

body {
  margin: 0;
  /* Clearance for the fixed tab bar: its 49px plus a step of air. The old flat
     76px both wasted 18px on every screen and ignored the inset, so on a
     notched phone the last element tucked under the bar anyway. */
  padding: 0 0 calc(58px + env(safe-area-inset-bottom));
  /* The view slide translates a full-width section 26px sideways, which makes
     the document 401px wide on a 375px phone for the length of the animation
     — a real horizontal scrollbar, and on a touch screen a page that can be
     dragged off its own left edge mid-transition. Clipping the axis the app
     never scrolls costs nothing and contains it. */
  overflow-x: hidden;
  background: var(--color-background);
  color: var(--color-foreground);
  font-family: var(--font-ui);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

/* App title bar, not a display heading: the tab bar already names the view,
   so this stays out of the way and lets content own the fold. The right slot
   makes the strip earn its space — it carries the running total, the one
   figure worth seeing before you have typed anything. */
h1 {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  position: sticky;
  top: 0;
  z-index: 5;
  margin: calc(var(--space-3) * -1) calc(var(--space-3) * -1) var(--space-3);
  padding: var(--space-2) var(--space-3);
  background: color-mix(in srgb, var(--color-background) 88%, transparent);
  backdrop-filter: blur(12px);
  font-size: 16px;
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: -0.01em;
}

/* Small, selectable, and wraps — it is meant to be read aloud or copied out
   of a phone, not admired. */
.diagnostics {
  margin: 0 0 var(--space-2);
  padding: var(--space-2);
  background: var(--color-input);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  font-family: var(--font-num);
  font-size: 11px;
  line-height: 1.6;
  white-space: pre-wrap;
  word-break: break-word;
  color: var(--color-foreground);
  user-select: text;
}

/* The sprite itself is never drawn; only its <use> references are. */
.sprite { display: none; }

.mark { width: 22px; height: 22px; flex: none; }
.mark__bar { fill: var(--color-primary); }
/* The two lower bars are the same hue receding, not separate colours — so the
   mark follows a tone switch with no second palette to keep in step. */
.mark__bar--1 { fill: color-mix(in srgb, var(--color-primary) 32%, transparent); }
.mark__bar--2 { fill: color-mix(in srgb, var(--color-primary) 62%, transparent); }

.h1-name { flex: 1; min-width: 0; }

.h1-meta {
  font-size: 12px;
  font-weight: 400;
  letter-spacing: 0;
  color: var(--color-subtle);
}
.h1-meta:empty { display: none; }

/* A section marker, not a heading with presence — but readable. The Ledger
   pass put this at 10px and field labels at 11px, which is what made the app
   feel cramped: everything that was not a figure had shrunk. */
/* Sentence case at a readable size. Small-caps with wide tracking was doing
   two things at once — shouting at the thing it names, and costing height to
   do it. A section marker only has to be quieter than its content. */
h2 {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--color-subtle);
  margin: 0 0 var(--space-2);
}

/* Two jobs, two treatments.
   A figure in a COLUMN has to align with the figures above and below it, so
   it stays monospaced and tabular — that is the only reason a money app uses
   a mono face at all.
   A DISPLAY figure has no column to align with. Setting it in the UI face at
   a light weight and tight tracking is what makes it read as a headline
   rather than as terminal output. */
.amount { font-family: var(--font-num); font-variant-numeric: tabular-nums; }

.amount--in { color: var(--color-accent); }
.amount--out { color: var(--color-destructive); }

/* Phone-first, but a laptop should not get 1400px-wide inputs. */
.view {
  max-width: 620px;
  margin: 0 auto;
  padding: var(--space-3) var(--space-3) var(--space-6);
}
/* The period control: month picker and a two-way switch on one line, above
   everything they govern. */
.period {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.period input[type="month"] { margin: 0; flex: 1; min-width: 0; }

.seg {
  display: flex;
  flex: none;
  padding: 3px;
  background: var(--color-input);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
}
.seg__button {
  min-height: 34px;
  padding: 0 var(--space-2);
  background: none;
  border: none;
  border-radius: var(--radius-xs);
  color: var(--color-subtle);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
.seg__button[aria-pressed="true"] {
  background: var(--color-primary);
  color: var(--color-on-primary);
  font-weight: 600;
}

/* Settings is one page at a time, picked from a rail of every section on
   the right. Eleven tiles opening in place made a long screen of one open
   form among ten closed ones; here the rail is the whole list, always in
   view, and the page beside it is the one thing being changed.

   On a phone the rail is a column of icons with a one-word name, so the page
   keeps most of the 375px. Wider, it has room for each section's full name. */
.settings__body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 64px;
  gap: var(--space-2);
  align-items: start;
  margin-bottom: var(--space-5);
}
.settings__hits {
  margin: 0 0 var(--space-2);
  font-size: 12px;
  color: var(--color-subtle);
}
.settings__hits:empty { display: none; }

.settings__rail {
  position: sticky;
  top: var(--space-3);
  display: flex;
  flex-direction: column;
  gap: 2px;
  /* Clear of the fixed tab bar; past that the rail scrolls on its own. */
  max-height: calc(100dvh - 132px);
  overflow-y: auto;
  padding-left: var(--space-1);
  border-left: 1px solid var(--color-border);
}
.rail__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  min-height: 52px;
  padding: var(--space-1) 2px;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  color: var(--color-subtle);
  font: inherit;
  font-size: 11px;
  line-height: 1.2;
  text-align: center;
  cursor: pointer;
}
.rail__item .tile__icon { stroke: currentColor; }
.rail__item:hover { background: var(--color-muted); color: var(--color-foreground); }
.rail__item:focus-visible { outline: 2px solid var(--color-active); outline-offset: -2px; }
/* The open section: no fill, no box. Its icon and name step up from the
   quiet tone to the text colour, and a short accent mark sits on the rail's
   own hairline beside it — the same idea as the tab bar's marker riding its
   top edge. The filled, outlined item it replaced read louder than the page
   it pointed at. */
.rail__item { position: relative; }
.rail__item[aria-current="page"] { color: var(--color-foreground); font-weight: 600; }
.rail__item[aria-current="page"] .tile__icon { stroke: var(--color-active); }
.rail__item[aria-current="page"]::before {
  content: '';
  position: absolute;
  left: calc(var(--space-1) * -1);
  top: 50%;
  width: 2px;
  height: 20px;
  margin-top: -10px;
  border-radius: var(--radius-xs);
  background: var(--color-active);
}
/* Not removed while searching, so the rail never changes shape under the
   thumb — only dimmed. */
.rail__item.is-dim { opacity: 0.35; }
.rail__full { display: none; }

@media (min-width: 640px) {
  .settings__body { grid-template-columns: minmax(0, 1fr) 172px; }
  .rail__item {
    flex-direction: row;
    justify-content: flex-start;
    gap: var(--space-2);
    min-height: 40px;
    padding: 0 var(--space-2);
    font-size: 13px;
    text-align: left;
  }
  .rail__short { display: none; }
  .rail__full { display: inline; }
}

.setting {
  background: var(--color-muted);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-3);
}
.setting[hidden] { display: none; }
.setting__head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.setting__head h2 { margin: 0; flex: 1; }

/* A search match inside the open page: outlined in the accent, so the word
   that was asked for is found without reading the page. */
.setting .is-hit {
  outline: 2px solid var(--color-active);
  outline-offset: 2px;
  border-radius: var(--radius-xs);
}

.tile__icon {
  width: 20px;
  height: 20px;
  flex: none;
  fill: none;
  /* --color-active, not --color-primary. On carbon the two are the same volt
     green; on peach the primary is a pale rose fill meant to carry dark text,
     and drawn as a 1.75px stroke on the card it is 1.3:1 — an icon that is
     not there. Active is the tone's readable accent: 13:1 on carbon, 5.9:1
     on the peach card, both clear of the 3:1 a graphical object owes. */
  stroke: var(--color-active);
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The section's answer, beside its name: how many accounts, what the split
   is. */
.tile__state {
  font-size: 13px;
  color: var(--color-foreground);
  line-height: 1.3;
}
.tile__state:empty { display: none; }

/* Two related fields on one line. auto-fit rather than a fixed two columns,
   so when one of the pair is hidden — To account outside a transfer, Type
   outside an expense — the other takes the whole row instead of leaving a
   gap where a field used to be. */
.pair {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--space-2);
}

/* One step of air between fields, not two. */
.view input + label,
.view select + label,
.view .chips + label,
.view .datefield + label { margin-top: var(--space-2); }
.view[hidden] { display: none; }

/* The tab bar is a place, and moving along it has a direction. A view
   entered by moving right along the bar arrives from the right; going back
   arrives from the left. That is a spatial model people already hold, and it
   is what turns four tabs into one app rather than four screens.

   Going from display:none to block restarts a CSS animation, so this fires on
   every switch. .is-back is set by showView before the swap. */
@keyframes view-in {
  from { opacity: 0; transform: translateX(26px); }
  to   { opacity: 1; transform: none; }
}
@keyframes view-in-back {
  from { opacity: 0; transform: translateX(-26px); }
  to   { opacity: 1; transform: none; }
}
.view:not([hidden]) { animation: view-in var(--dur) var(--ease); }
.view:not([hidden]).is-back { animation-name: view-in-back; }

/* All four views arrive the same way. Add and List used to opt out, on the
   grounds that 300ms of slide is 300ms before the first keystroke; the owner
   preferred one consistent arrival, and the curve is mostly settled in the
   first 100ms anyway. Settings used to layer two more fades on top of this
   one (its rail and its open page, at different speeds), which is what made
   arriving there look glitchy. It now gets this slide and nothing else. */

/* Vault: the screen is a tray of objects, not a continuous sheet. A section
   is a filled, rounded surface sitting on the canvas — which is the opposite
   of the Ledger pass that preceded it, and deliberately so. Ledger dissolved
   every boundary until nothing had presence; here each thing is a thing. */
.card {
  background: var(--color-muted);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-3);
  margin-bottom: var(--space-2);
}
.view > .card:last-of-type { margin-bottom: var(--space-5); }

.tabbar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  justify-content: center;
  background: var(--color-tabbar);
  backdrop-filter: blur(12px);
  border-top: 1px solid var(--color-border);
  padding-bottom: env(safe-area-inset-bottom);
}

.tabbar button {
  position: relative;
  flex: 1;
  max-width: 155px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: 48px;
  background: none;
  border: none;
  color: var(--color-subtle);
  font: inherit;
  font-size: 11px;
  letter-spacing: 0.01em;
  cursor: pointer;
  transition: color 150ms ease, transform var(--dur-fast) var(--ease);
}

.tabbar svg {
  width: 20px;
  height: 20px;
  stroke: currentColor;
  stroke-width: 1.75;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.tabbar button:hover { color: var(--color-foreground); }

.tabbar button[aria-current="page"] {
  /* --color-active, not --color-primary: the fill tint fails AA at 11px */
  color: var(--color-active);
  font-weight: 600;
}

/* Active tab marker rides the top hairline. One element for the whole bar,
   moved with a transform, rather than a ::before on whichever button happens
   to be current — a pseudo-element cannot travel from one element to another,
   so the marker used to teleport while the view slid. Its position is set in
   showView from the button's measured box, because the buttons are flexible
   and their width is not known to the stylesheet. */
.tabbar__marker {
  position: absolute;
  top: -1px;
  left: 0;
  width: 28px;
  height: 2px;
  background: var(--color-active);
  transform: translateX(var(--marker-x, 0));
  transition: transform var(--dur) var(--ease);
}

:focus-visible {
  outline: 2px solid var(--color-active);
  outline-offset: 2px;
}

/* Field labels share the section marker's voice — small, tracked, quiet —
   so the form reads as one ruled column rather than as boxes with captions. */
/* Labels sit close to their field — they belong to it, and the gap was
   reading as separation rather than as pairing. */
label {
  display: block;
  margin-bottom: var(--space-0);
  font-size: 12px;
  font-weight: 400;
  color: var(--color-subtle);
}

/* Flatter than before. Softly rounded boxes read as an app made of panels;
   this direction is ruled paper, so the corners come in and the fill does the
   work of saying "type here". */
/* 40px, not 44. Every field in this app is a form control the user steps
   through in sequence rather than a target they stab at, and seven fields at
   44 with 16px between them made the Add screen taller than the phone. The
   primary button keeps 44 below, because that one IS stabbed at. */
input, select {
  width: 100%;
  min-height: 40px;
  padding: 0 var(--space-2);
  margin-bottom: var(--space-2);
  background: var(--color-input);
  color: var(--color-foreground);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  font: inherit;
  /* Pinned, and it must not follow the body down. iOS Safari zooms the whole
     page when a field smaller than 16px takes focus, and it does not zoom
     back out — you finish the form at 1.3x with the tab bar off the screen.
     This is the one place the type scale does not apply. */
  font-size: 16px;
  transition: border-color 150ms ease;
}

input:hover, select:hover { border-color: var(--color-border-strong); }
input:focus, select:focus { border-color: var(--color-border-strong); }
input[type="number"], input[type="date"], input[type="month"] { font-family: var(--font-num); }

/* The native calendar popup sizes itself to its own content and ignores the
   field's width, so a wide date input leaves the popup floating under a line
   twice as wide. The popup is browser chrome and cannot be styled, so the
   field comes down to meet it instead.
   Only above phone width: a phone opens a full-screen date picker that has no
   relationship to the field at all, and there a short field is just a short
   field sitting out of line with its siblings. */
@media (min-width: 520px) {
  input[type="date"], input[type="month"] {
    width: auto;
    min-width: 280px;
    max-width: 100%;
    display: block;
  }
}

/* Without this the arrow renders in the platform's own colour, which is black
   on the graphite tone and invisible. */
select {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right var(--space-2) center, calc(100% - var(--space-2) + 5px) center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: var(--space-5);
}

/* The amount is what the user opened the app to type. Size says so. */
/* The browser default file button ignores every token in this file. */
input[type="file"] {
  padding: var(--space-1) var(--space-2);
  cursor: pointer;
}

input[type="file"]::file-selector-button {
  margin-right: var(--space-2);
  padding: 0 var(--space-2);
  min-height: 28px;
  background: var(--color-muted);
  color: var(--color-foreground);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}

input[type="file"]::file-selector-button:hover { background: var(--color-input); }

/* font and radius are stated here because buttons do not inherit them.
   This rule was split out of `input, select, button.primary` (ee3094e) to
   keep its 44px when fields went to 40, and lost both in the move: every
   primary button rendered in the browser's Arial with square corners until
   v121. */
button.primary {
  margin-top: var(--space-1);
  background: var(--color-primary);
  color: var(--color-on-primary);
  border: none;
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-primary);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  /* transform is listed here as well as in the shared press rule below: this
     shorthand is later and more specific, so leaving it out silently removed
     the press feedback from the app's most-used button. */
  transition: background 150ms ease, transform var(--dur-fast) var(--ease);
}

button.primary {
  width: 100%;
  min-height: 44px;
  margin-bottom: var(--space-2);
}
button.primary:hover { background: var(--color-primary-hover); }
button.primary:active { filter: brightness(0.92); }

/* The second action on a page — Remove sample data, Copy diagnostics,
   Create account. The primary's size, shape and type, in the outlined look
   the dialogs' Cancel already uses, so it reads as the quieter partner of
   the primary above or beside it. */
button.secondary {
  width: 100%;
  min-height: 44px;
  margin: var(--space-1) 0 var(--space-2);
  padding: 0 var(--space-3);
  background: var(--color-input);
  color: var(--color-foreground);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  transition: background 150ms ease, transform var(--dur-fast) var(--ease);
}
button.secondary:hover { background: var(--color-muted); }

/* An action inside a list row — Edit on a month plan, Delete on a recurring
   rule. A word, not a box: the row is already the object, so the button
   only needs its 44px target and a hover. */
button.ghost {
  flex: none;
  min-width: 44px;
  min-height: 44px;
  padding: 0 var(--space-2);
  background: none;
  border: none;
  border-radius: var(--radius-md);
  color: var(--color-subtle);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  transition: background 150ms ease, color 150ms ease, transform var(--dur-fast) var(--ease);
}
button.ghost:hover { background: var(--color-input); color: var(--color-foreground); }

/* Every control acknowledges the press itself, not just its result. 3% is
   small enough to read as the surface taking the weight rather than as the
   button shrinking. */
button, .chip, .seg__button { transition: transform var(--dur-fast) var(--ease); }
button:active, .chip:active, .seg__button:active { transform: scale(0.97); }

/* A dialog is a surface arriving, so it rises and settles; the scrim behind
   it only fades, because a scrim is not an object. */
@keyframes dialog-in {
  from { opacity: 0; transform: translateY(16px) scale(0.97); }
  to   { opacity: 1; transform: none; }
}
@keyframes scrim-in { from { opacity: 0; } }
.dialog[open] { animation: dialog-in var(--dur) var(--ease); }
.dialog[open]::backdrop { animation: scrim-in var(--dur-fast) linear; }

/* A settings page picked from the rail slides out of it: the same arrival
   as a view, over a shorter distance, because it travels across a column
   rather than a screen. Keyed on a class settings.js sets only for a pick,
   not on [hidden] — a rule on [hidden] restarts every time the Settings view
   itself appears, and that second animation inside the view's slide was the
   glitch on arriving there. */
@keyframes page-in {
  from { opacity: 0; transform: translateX(12px); }
  to   { opacity: 1; transform: none; }
}
.setting.is-opening { animation: page-in var(--dur) var(--ease); }

/* Popovers (the calendar, the month grid, every dropdown) unfold from the
   field they belong to instead of appearing whole. A short drop toward where
   they open — down normally, up when there is no room below. */
@keyframes pop-in {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}
@keyframes pop-in-up {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
.calendar:not([hidden]), .pick__list:not([hidden]) { animation: pop-in var(--dur) var(--ease); }
.calendar.is-up:not([hidden]), .pick__list.is-up:not([hidden]) { animation-name: pop-in-up; }

/* "Saved." rises into place. The status line is the only confirmation the
   form gives, so it is worth the 300ms of attention. */
@keyframes note-in {
  from { opacity: 0; transform: translateY(7px); }
  to   { opacity: 1; transform: none; }
}
#add-status.is-fresh, #list-status.is-fresh { animation: note-in var(--dur) var(--ease); }

/* Figures being drawn, not chrome moving — so this is the slow duration.
   Only on entering the Summary: the bars are repainted after every save, and
   re-growing them on each one would animate a screen nobody is looking at.
   The bar's real width is inline, so a keyframe that only sets `from` grows
   it to whatever that value is. */
/* The funds bar unrolls from the left, its parts in proportion throughout. */
@keyframes bar-grow { from { transform: scaleX(0); } }
.view.is-entering .funds__bar { transform-origin: left center; animation: bar-grow var(--dur-slow) var(--ease); }
/* The Budget card's dials draw round from twelve, at the same pace. */
@keyframes dial-draw { from { stroke-dasharray: 0 100; } }
.view.is-entering .dial__value { animation: dial-draw var(--dur-slow) var(--ease); }

@keyframes slice-in { from { opacity: 0; transform: scale(0.82); } }
.view.is-entering #pie > * {
  transform-origin: 50% 50%;
  animation: slice-in var(--dur) var(--ease) backwards;
}
/* Clockwise from twelve, one slice after another, the same order the reader
   picks them up in. Capped at six: past that the last slice would arrive
   after the eye had already moved on. */
.view.is-entering #pie > :nth-child(2) { animation-delay: 60ms; }
.view.is-entering #pie > :nth-child(3) { animation-delay: 120ms; }
.view.is-entering #pie > :nth-child(4) { animation-delay: 180ms; }
.view.is-entering #pie > :nth-child(5) { animation-delay: 240ms; }
.view.is-entering #pie > :nth-child(n+6) { animation-delay: 300ms; }

/* The List's rows settle in behind the view's slide, and again whenever the
   month or the Month / Year / All switch changes what they show — the same
   page turning, rather than rows swapped under the reader's eye. */
@keyframes settle-in { from { opacity: 0; } }
.view.is-entering #list-rows { animation: settle-in var(--dur) var(--ease) backwards; }

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* Segmented control, built from the existing radio markup. */
#add-kind {
  display: flex;
  gap: 2px;
  padding: 3px;
  margin-bottom: var(--space-2);
  background: var(--color-input);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
}

#add-kind label {
  position: relative;
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 34px;
  margin: 0;
  border-radius: var(--radius-sm);
  font-size: 13px;
  color: var(--color-subtle);
  cursor: pointer;
  transition: background 150ms ease, color 150ms ease;
}

#add-kind label:has(input:checked) {
  background: var(--color-primary);
  color: var(--color-on-primary);
  box-shadow: var(--shadow-primary);
  font-weight: 600;
}

/* The filled option already says which kind is chosen, so the radio dot
   beside it said it twice. The input stays in the page, invisible, so the
   group still works by keyboard and screen reader; its focus ring moves to
   the label it sits in. */
#add-kind input {
  position: absolute;
  width: 1px;
  height: 1px;
  min-height: 0;
  margin: 0;
  opacity: 0;
  pointer-events: none;
}
#add-kind label:has(input:focus-visible) {
  outline: 2px solid var(--color-active);
  outline-offset: 2px;
}

/* Every other native control that paints itself: checkboxes, the month
   picker's spinners, focus rings inside file inputs. Set once at the root so
   a control added later cannot reintroduce the blue. */
:root { accent-color: var(--color-active); }
#add-status { min-height: 24px; font-size: 13px; color: var(--color-accent); }
/* Nothing sits below it, so reserving 24px of blank stabilises nothing — it
   only pushes Save off the bottom of a phone. Collapsed until it speaks. */
#add-status:empty { min-height: 0; margin: 0; }

.rows { list-style: none; margin: 0; padding: 0; }

/* ── The spine ──────────────────────────────────────────────────────────────
   One line down the screen instead of a boundary drawn around each group.
   Days on the List, steps on the Add form and blocks on the Summary hang off
   it as nodes, so a screen reads as one sequence rather than as a stack of
   separately fenced things.

   It costs 20px of a 375px screen on every screen that uses it, and that is
   the deliberate trade: the rows lost width, the screen gained an order.

   Drawn entirely in CSS on ::before, so there is no extra element for a
   screen reader to meet — the grouping is carried by the real markup
   (<fieldset>/<legend> on Add, the day heading on List, <h2> on Summary). */
.spine {
  --spine-inset: 20px;
  --spine-x: 5px;          /* the line's own offset inside that inset */
  --spine-dot: 9px;
  position: relative;
  padding-left: var(--spine-inset);
}

/* An empty list would otherwise draw a 1px line down a screen with nothing
   on it, which reads as a rendering fault rather than as structure. */
.spine:empty::before { display: none; }

.spine::before {
  content: "";
  position: absolute;
  left: var(--spine-x);
  /* Stops short at both ends rather than running to the edge: a line that
     touches the first and last node reads as a border, not as a thread. */
  top: var(--space-2);
  bottom: var(--space-2);
  width: 1px;
  background: var(--color-border);
}

.node { position: relative; }

.node::before {
  content: "";
  position: absolute;
  left: calc((var(--spine-inset) - var(--spine-x) + var(--spine-dot) / 2) * -1);
  top: 0.5em;
  width: var(--spine-dot);
  height: var(--spine-dot);
  border-radius: 50%;
  /* Filled with the canvas so the line does not show through the hollow
     ones. --color-background, not the card colour: the cards are gone on
     every screen the spine is used on. */
  background: var(--color-background);
  /* --color-subtle, not --color-border-strong. A node marks where a step
     begins, so it is a graphical object and owes 3:1 against the canvas;
     border-strong is 1.9:1 on carbon and the ring simply vanished. Subtle is
     6.4:1 there and 4.6:1 on peach. */
  border: 1.5px solid var(--color-subtle);
  box-sizing: border-box;
}

/* The steps that carry the money: a filled node, so the eye can find the
   amount and the balance without reading. Everything else is hollow. */
.node--lead::before,
.card.stats.node::before {
  background: var(--color-primary);
  border-color: var(--color-primary);
}

/* On the Summary the spine replaces the card, rather than sitting beside it.
   Keeping both would be two structures saying the same thing, and the fill
   is what was making the screen read as nineteen boxes. */
.spine > .card.node {
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: var(--space-3) 0;
  margin-bottom: 0;
}
.spine > .card.stats.node {
  padding: var(--space-2) 0 var(--space-3);
  border-radius: 0;
}
.spine > .card.node + .card.node { border-top: 1px solid var(--color-border); }
.spine > .card.node::before { top: var(--space-3); }
.spine > .card.node:first-child::before { top: var(--space-2); }

/* The Add form's steps. <fieldset> and <legend> carry the grouping for
   assistive tech, so both are stripped of the browser's own box and the
   legend is set as the same quiet marker every other section heading uses. */
.spine > fieldset.node {
  min-width: 0;         /* a fieldset defaults to min-content, which breaks
                           the two-column .pair inside it at 375px */
  border: 0;
  margin: 0;
  padding: var(--space-1) 0 var(--space-2);
}
.spine > fieldset.node + fieldset.node { border-top: 1px solid var(--color-border); }
.spine > fieldset.node > legend {
  padding: 0;
  font-size: 12px;
  font-weight: 600;
  color: var(--color-subtle);
}
.spine > fieldset.node::before { top: var(--space-2); }
.spine > fieldset.node > :first-child + * { margin-top: var(--space-1); }

.row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-1);
  margin: 0 calc(var(--space-1) * -1);
  border-radius: var(--radius-md);
  border-bottom: 1px solid var(--color-border);
  transition: background 150ms ease;
}

.row:hover { background: var(--color-muted); }
.row:last-child { border-bottom: none; }

.row__main { flex: 1; min-width: 0; }

/* A name that is also a control says so on hover and focus rather than by
   carrying an icon: the row already has three. */
.row__title--action { cursor: pointer; }
.row__title--action:hover { text-decoration: underline; }
.row__title--action:focus-visible {
  outline: 2px solid var(--color-active);
  outline-offset: 2px;
  border-radius: var(--radius-xs);
}

.prompt__label { margin-top: var(--space-2); }
.prompt__error:empty { display: none; }
.prompt__error { margin: calc(var(--space-2) * -1) 0 var(--space-2); font-size: 12px; }

/* A transaction row: day, tag, name, amount over account, delete. Five things
   on one line at 375px, so every fixed part is measured and only the name is
   allowed to take what is left. */
/* Three things on a line instead of five, because the date moved to a group
   heading and delete moved behind a swipe. The name gets what both gave up. */
.row--txn {
  gap: var(--space-2);
  padding: var(--space-3);
  border-bottom: none;
  cursor: pointer;
}

/* The day heading floats on the canvas BETWEEN groups of tiles, so it needs
   air above it rather than a rule beneath it. */
.dayhead {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: var(--space-4) 0 var(--space-1);
}
.dayhead.node::before { top: calc(var(--space-4) + 3px); }
.dayhead.node:first-child::before { top: calc(var(--space-1) + 3px); }
.dayhead b { font-size: 12px; font-weight: 600; letter-spacing: -0.005em; }
.dayhead i { flex: 1; }
.dayhead__net { font-size: 11px; color: var(--color-subtle); }
.dayhead:first-child { padding-top: var(--space-1); }
/* .row's negative side margins let a row bleed past the card padding it was
   designed inside. Against the spine that bleed reaches back into the gutter
   and crowds the line, so the left one is dropped here only. */
.rows.spine .row--txn { margin: 0; padding-left: 0; padding-right: 0; }

/* A row on the spine is not a tile. Keeping the filled, rounded surface put a
   box inside a structure that exists to replace boxes, and the 3-letter chip
   inside it started every transaction name 48px right of the day label it
   belongs under — which is the stagger that made the column look broken.

   So: no surface, and the tag becomes a colour rail on the row itself. The
   name now begins one rail-width from the left, in line with its day. */
/* The canvas colour, NOT `transparent`. .swipe__under is a red delete
   surface sitting directly behind this one, revealed by sliding the row
   sideways — make the row see-through and every row in the list turns red
   with a delete icon showing through it. The row must still be opaque; it
   just stops looking like a tile. */
.rows.spine .swipe__over {
  background: var(--color-background);
  border: 0;
  border-radius: 0;
}
.rows.spine .row--txn {
  padding-top: var(--space-2);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--color-border);
}
.rows.spine .dayhead + .swipe .row--txn { border-top: 0; }
.rows.spine .swipe:last-child .row--txn { border-bottom: 0; }

/* The rail. The code is dropped to `font-size: 0` rather than removed from
   the DOM: the element still carries its title and aria-label, so the full
   category name is what a screen reader and a long press get — the same
   contract the chip had. */
.rows.spine .tag {
  align-self: stretch;
  width: 3px;
  min-height: 22px;
  padding: 0;
  font-size: 0;
  border-radius: var(--radius-xs);
  background: var(--tag-colour);
}
.rows.spine .tag.tag--plain { background: var(--color-border-strong); }

/* The row rides on .swipe__over; .swipe__under sits behind it and is only
   seen once the surface has moved. */
.swipe {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-md);
  margin-bottom: var(--space-1);
}

.swipe__under {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-1);
  padding-right: var(--space-3);
  background: color-mix(in srgb, var(--color-destructive) 22%, var(--color-background));
  color: var(--color-destructive);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}
.swipe__under svg { width: 18px; height: 18px; }

.swipe__over {
  position: relative;
  background: var(--color-muted);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  /* Vertical scrolling stays the browser's; the horizontal axis is ours.
     Without this the page and the row fight over every drag. */
  touch-action: pan-y;
  transition: transform var(--dur) var(--ease);
}
/* While a finger is down the surface must track it exactly, so the easing is
   dropped for the duration of the gesture. Driven by a class rather than
   :active, which is not reliably set once the pointer is captured. */
.swipe__over.is-dragging { transition: none; }


/* The category, as colour first and letters second. The tint is mixed from
   the category's own colour so one stored value drives both the text and the
   fill, and neither can drift from the other. */
.tag {
  flex: none;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  padding: 3px 7px;
  border-radius: var(--radius-sm);
  font-family: var(--font-num);
  color: var(--tag-colour);
  background: color-mix(in srgb, var(--tag-colour) 26%, transparent);
}
.tag--plain { background: var(--color-input); color: var(--color-subtle); }

.row--txn .row__title {
  font-size: 14px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.row--txn .row__meta { font-size: 11px; }
.row--txn > .amount { font-size: 14px; font-weight: 500; white-space: nowrap; }

/* An empty screen is still a screen. The mark plus a sentence that says what
   would fill it beats a bare line of grey text. */
.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-1);
  padding: var(--space-6) var(--space-3);
}
.empty--inline { padding: var(--space-5) var(--space-3); }
.empty__mark { width: 40px; height: 40px; opacity: 0.55; margin-bottom: var(--space-1); }
.empty__title { margin: 0; font-size: 14px; font-weight: 600; }
.empty__body { margin: 0; font-size: 12px; color: var(--color-subtle); max-width: 34ch; }
.row__title { display: block; }
.row > .amount { font-size: 14px; font-weight: 500; white-space: nowrap; }
.row__meta {
  display: block;
  font-size: 11px;
  font-family: var(--font-num);
  color: var(--color-subtle);
}

.row__delete {
  min-width: 44px;
  min-height: 44px;
  background: none;
  border: none;
  color: var(--color-subtle);
  cursor: pointer;
  border-radius: var(--radius-md);
  transition: background 150ms ease, color 150ms ease;
}

.row:hover .row__delete { color: var(--color-destructive); }
.row__delete:hover { background: color-mix(in srgb, var(--color-destructive) 15%, transparent); }

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* Balance is the headline — "what do I hold" is the question the screen is
   opened to answer. Net, income and spent are the supporting line beneath it.
   Only one figure per screen gets to be large; giving two that weight is what
   made the old stats block read as four competing numbers. */
/* The hero object. Balance is the reason the screen is opened, so it gets a
   surface of its own and the largest figure in the app; the supporting three
   sit under it as a quiet strip rather than as three competing headlines. */
.card.stats {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-3) var(--space-4);
  padding: var(--space-4) var(--space-4) var(--space-4);
  border-radius: var(--radius-xl);
}

.stat { display: flex; flex-direction: column; gap: var(--space-0); }

.stat__label {
  font-size: 12px;
  font-weight: 400;
  color: var(--color-subtle);
}

.stat .amount { font-size: 16px; font-weight: 400; }

/* Explicit order rather than HTML order: balance leads, net follows full
   width, and income and spent pair off beneath. Three items in a two-column
   grid otherwise leaves one stranded on a row of its own. */
.stat--balance {
  grid-column: 1 / -1;
  order: -2;
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--color-border);
}
.stat--balance .stat__label { color: var(--color-subtle); }

.stat--balance .amount {
  font-family: var(--font-ui);
  font-size: 34px;
  font-weight: 300;
  line-height: 1;
  letter-spacing: -0.045em;
}

/* Net keeps its sign colour but not its size: it is the change, read against
   the balance above it. */
.stat--net {
  grid-column: 1 / -1;
  order: -1;
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--color-border);
}
.stat--net .amount { font-size: 18px; font-weight: 400; }

/* Savings by fund: one bar for the split, then a table of what each fund
   gets this period and has had this year. A real <table>, because it is
   one: two columns of figures that are read across a row. */
.funds__bar {
  display: flex;
  gap: 2px;
  height: 10px;
  margin: var(--space-1) 0 var(--space-3);
}
.funds__bar i { flex: 1 1 0; min-width: 2px; border-radius: var(--radius-xs); }
.funds__table { width: 100%; border-collapse: collapse; font-size: 13px; }
.funds__table th, .funds__table td { padding: var(--space-1) 0; font-weight: 400; }
.funds__table thead th {
  padding-top: 0;
  font-size: 12px;
  color: var(--color-subtle);
  text-align: right;
}
.funds__table tbody th {
  max-width: 0;
  width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
}
.funds__table td { padding-left: var(--space-3); text-align: right; white-space: nowrap; }
.funds__table .funds__year { color: var(--color-subtle); }
.funds__table tfoot th, .funds__table tfoot td {
  padding-top: var(--space-2);
  border-top: 1px solid var(--color-border);
  font-weight: 600;
  text-align: left;
}
.funds__table tfoot td { text-align: right; }
.funds__table tfoot .funds__year { color: var(--color-foreground); }
/* The column heads name periods; the first column's head is for screen
   readers only, since the fund names say what they are. Not .visually-hidden:
   that takes the cell out of the table's layout and shifts every column. */
.funds__table .visually-hidden-cell { font-size: 0; }

/* The Budget card, answer first: per bucket, a dial for how much is used
   and the amount left (or over) as the row's big figure. Rows on hairlines,
   like every other list on the Summary. */
.dials { display: flex; flex-direction: column; }
.dial {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) 0;
}
.dial + .dial { border-top: 1px solid var(--color-border); }
.dial:first-child { padding-top: 0; }
.dial__ring { width: 32px; height: 32px; flex: none; }
.dial__track { fill: none; stroke: var(--color-border); stroke-width: 4; }
/* --color-active, not --color-primary: on peach the primary is a pale fill
   that a 4-unit stroke on the card would barely show. */
.dial__value { fill: none; stroke: var(--color-active); stroke-width: 4; stroke-linecap: round; }
.dial__value.is-over { stroke: var(--color-destructive); }
.dial__value.is-savings { stroke: var(--color-accent); }
.dial__text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.dial__label { font-size: 12px; color: var(--color-subtle); }
.dial__figure { font-size: 16px; font-weight: 400; color: var(--color-foreground); }
.dial__figure.amount--in { color: var(--color-accent); }
.dial__figure.amount--out { color: var(--color-destructive); }
.dial__figure small { font-family: var(--font-ui); font-size: 12px; color: var(--color-subtle); }
.dial__figure.amount--out small { color: inherit; }

/* A label and a figure on one line, with an optional bar behind it. This
   replaced two separate table styles — .bars and .budget — that between them
   needed six rules to keep four money columns on a 375px screen. */
.figures { display: flex; flex-direction: column; }

.figures__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2);
  font-size: 13px;
}
.figures__row + .figures__row { border-top: 1px solid var(--color-border); }
.figures__row:last-child { border-bottom: none; }

.figures__label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.figures__row .amount { white-space: nowrap; }

.figures__row--total {
  border-top: 1px solid var(--color-border-strong);
  border-bottom: none;
  font-weight: 600;
  margin-top: var(--space-1);
}


/* The space above the two chips belongs to the note, not to one chip: a
   margin on the first chip alone pushed it 9px below its neighbour, and the
   flex row stretched the second to match, so the pair sat at two heights. */
#budget-note, #funds-note {
  font-size: 12px;
  line-height: 1.5;
  margin: var(--space-3) 0 var(--space-2);
  color: var(--color-subtle);
}
.budget-note__warning { color: var(--color-destructive); }
/* The funds note carries the warning class itself, and the id rule above
   would otherwise grey out "Fund shares total 90%". */
#funds-note.budget-note__warning { color: var(--color-destructive); }

/* Three controls that describe one chart, so they sit on one line and wrap
   together rather than reading as three unrelated settings stacked above it.
   The minimum keeps a select from collapsing narrower than its own label. */
.filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}

.filters__field {
  flex: 1 1 8rem;
  min-width: 8rem;
}

.filters__field label {
  margin-bottom: var(--space-0);
}

/* Says a field can be left alone without the label having to grow a sentence.
   Quieter than the label it follows, because it is a permission, not a name. */
.label-optional {
  font-size: 12px;
  color: var(--color-subtle);
  opacity: 0.7;
  margin-left: var(--space-0);
}

/* The negative top margin below assumes a .hint follows an INPUT, pulling it
   up under the field it explains. Following a label it climbs over the label
   instead — which it did, under "Default account". */
label + .hint { margin-top: 0; }

.hint {
  font-size: 12px;
  line-height: 1.5;
  color: var(--color-subtle);
  margin: calc(var(--space-2) * -1) 0 var(--space-3);
}

/* Capped because a pie gains nothing from being wider than it is tall, and a
   full-width one on a laptop would dwarf the table that labels it. */
#pie {
  display: block;
  width: 100%;
  max-width: 240px;
  height: auto;
  margin: 0 auto var(--space-4);
}
#pie[hidden] { display: none; }

.swatch {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: var(--radius-xs);
  margin-right: var(--space-1);
  vertical-align: baseline;
}


/* An ordered row wraps its controls under the name on a narrow screen rather
   than crushing four of them onto one line. */
/* Tuned so the longest default name ("Health & Skincare", 133px at 15px)
   still shares a line with the controls at 375px. It wraps below that
   rather than truncating: a category you cannot read the name of is worse
   than a taller row. */
.row--ordered {
  flex-wrap: wrap;
  gap: var(--space-1);
  /* Narrower side padding than a plain row: these carry a grip and three
     controls, and the eight pixels reclaimed here are what let the longest
     default name stay on one line at 375px. */
  padding-left: var(--space-0);
  padding-right: var(--space-0);
}
.row--ordered .row__main { flex: 1 1 7rem; min-width: 0; }

.row__controls {
  display: flex;
  align-items: center;
  gap: var(--space-0);
  flex: 0 0 auto;
}

/* The ordered rows carry three controls plus a name; the delete target
   shrinks from 44 to 36 there so all three fit beside the name on a phone.
   Still well above the 24px minimum, and it is not a primary action. */
.row--ordered .row__delete { min-width: 36px; min-height: 36px; }

.grip {
  display: flex;
  align-items: center;
  color: var(--color-subtle);
  cursor: grab;
  /* Stops the browser claiming the gesture for scrolling before the drag
     starts, which is what makes a touch drag impossible otherwise. */
  touch-action: none;
  flex: 0 0 auto;
}
.grip svg { fill: currentColor; }
.grip:active { cursor: grabbing; }

.reorder { display: flex; gap: 2px; flex: 0 0 auto; }

/* Borderless. Two boxed buttons per row read as heavily as the name they sit
   beside; a bare chevron is the smallest thing that still says "move". The
   tap target stays 32px even though the mark inside it is 14px. */
.reorder__button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 32px;
  padding: 0;
  background: none;
  color: var(--color-subtle);
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.reorder__button svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 2.25;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.reorder__button--down svg { transform: rotate(180deg); }
.reorder__button:hover:not(:disabled) {
  color: var(--color-foreground);
  background: var(--color-input);
}
/* Kept in place rather than hidden at the ends, so the controls do not shift
   under the pointer as rows change places. */
.reorder__button:disabled { opacity: 0.3; cursor: default; }

/* The colour, as the control. A select naming ten colours in words was three
   times the width and had to be read rather than seen. */
.swatch-button {
  width: 24px;
  height: 24px;
  padding: 0;
  border: 1px solid var(--color-border-strong);
  border-radius: 50%;
  cursor: pointer;
  flex: 0 0 auto;
}
.swatch-button--none {
  background: transparent;
  border-style: dashed;
}
.swatch-button:hover { border-color: var(--color-foreground); }

.group__heading {
  font-size: 12px;
  font-weight: 600;
  color: var(--color-subtle);
  margin: var(--space-3) 0 var(--space-1);
}

/* A drop target has to have height even when empty, or a group emptied of
   every category can never be dropped back into. */
.rows--drop { min-height: var(--space-5); }
.rows--drop.is-drop-into {
  outline: 2px dashed var(--color-active);
  outline-offset: 2px;
  border-radius: var(--radius-md);
}
.group__empty {
  font-size: 12px;
  color: var(--color-subtle);
  margin: 0 0 var(--space-2);
}

/* The editor is a form in a dialog, so it scrolls on a short screen rather
   than pushing its own buttons off the bottom. */
.dialog--editor { max-height: 88vh; overflow-y: auto; }
.editor__kind { margin-top: calc(var(--space-1) * -1); }
.editor__error:empty { display: none; }
.editor__error { margin: 0 0 var(--space-2); font-size: 12px; }
.editor__actions { display: flex; gap: var(--space-1); align-items: center; }
.editor__delete {
  margin-right: auto;
  min-height: 44px;
  padding: 0 var(--space-2);
  background: none;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  color: var(--color-destructive);
  font: inherit;
  cursor: pointer;
}
.editor__delete:hover { border-color: var(--color-destructive); }

.palette {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr));
  gap: var(--space-1);
  margin-bottom: var(--space-3);
}
.palette__item {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  width: 100%;
  min-height: 44px;
  padding: 0 var(--space-2);
  background: var(--color-input);
  color: var(--color-foreground);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: 13px;
  text-align: left;
  cursor: pointer;
}
.palette__item:hover { border-color: var(--color-border-strong); }
.palette__item[aria-pressed="true"] { border-color: var(--color-active); border-width: 2px; }
.palette__chip {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  flex: 0 0 auto;
}
.palette__chip--none { border: 1px dashed var(--color-border-strong); }

.row.is-dragging { opacity: 0.4; }

/* The drop marker is a line where the row will land, drawn on the row being
   hovered rather than by moving anything, so nothing jumps until the drop. */
.row.is-drop-before { box-shadow: inset 0 2px 0 0 var(--color-active); }
.row.is-drop-after { box-shadow: inset 0 -2px 0 0 var(--color-active); }

/* Rp lives in the label, not in the field. Inside it, it was small enough to
   read as noise, and the flex gap that separated it drew a hairline nobody
   asked for. */
.moneyfield { font-family: var(--font-num); text-align: right; }
#add-amount {
  font-family: var(--font-ui);
  min-height: 60px;
  font-size: 34px;
  font-weight: 300;
  letter-spacing: -0.045em;
  margin-bottom: var(--space-1);
}

/* Chips add, they do not step. Reaching 50rb by nudging a spinner is fifty
   taps; the amounts people actually enter are round. */
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-bottom: var(--space-3);
}
.chip {
  min-height: 36px;
  padding: 0 var(--space-2);
  background: var(--color-input);
  color: var(--color-foreground);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
.chip:hover { border-color: var(--color-border-strong); background: var(--color-muted); }
.chip:active { filter: brightness(0.92); }
.chip--clear { margin-left: auto; color: var(--color-subtle); }

/* The native date popup is browser chrome that ignores the field's width and
   takes no styling. This one is ours, so it matches every other picker. */
.datefield { position: relative; margin-bottom: var(--space-3); }
/* In a .pair the field is half a phone wide, and a seven-column month inside
   161px is unusable. The popup keeps a real width and hangs off the right edge
   of its column instead of matching it — and opens upward, because the paired
   field is the last row of the form and downward is off the bottom of a phone
   with nothing below to scroll to. */
.pair .datefield { margin-bottom: 0; }
.pair .datefield > .calendar {
  left: auto;
  right: 0;
  width: auto;
  min-width: 280px;
  top: auto;
  bottom: calc(100% + var(--space-0));
}
.datefield > button {
  width: 100%;
  min-height: 44px;
  padding: 0 var(--space-2);
  text-align: left;
  background: var(--color-input);
  color: var(--color-foreground);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  font: inherit;
  font-family: var(--font-num);
  cursor: pointer;
}
.datefield > button:hover { border-color: var(--color-border-strong); }

.calendar {
  position: absolute;
  z-index: 20;
  top: calc(100% + var(--space-0));
  left: 0;
  /* Matches the field it belongs to, which is the whole point. */
  width: 100%;
  padding: var(--space-2);
  background: var(--color-muted);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-popover);
}
.calendar[hidden] { display: none; }
.calendar__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-2);
}
.calendar__label { display: flex; align-items: center; gap: 2px; font-size: 13px; font-weight: 500; }
/* The month name and the year in the heading are buttons that open their
   own grid. A faint fill says so on a touch screen, where there is no hover
   to discover it by. */
.calendar__jump {
  min-height: 32px;
  padding: 0 var(--space-2);
  background: color-mix(in srgb, var(--color-input) 60%, transparent);
  border: none;
  border-radius: var(--radius-sm);
  color: var(--color-foreground);
  font: inherit;
  cursor: pointer;
}
.calendar__jump:hover { background: var(--color-input); }
.calendar__nav {
  width: 32px;
  height: 32px;
  background: none;
  border: none;
  color: var(--color-foreground);
  font-size: 18px;
  cursor: pointer;
  border-radius: var(--radius-sm);
}
.calendar__nav:hover { background: var(--color-input); }
.calendar__grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
}
.calendar__weekday {
  text-align: center;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: var(--color-subtle);
  padding-bottom: var(--space-0);
}
.calendar__day {
  aspect-ratio: 1;
  min-height: 34px;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  color: var(--color-foreground);
  font: inherit;
  font-family: var(--font-num);
  font-size: 12px;
  cursor: pointer;
}
.calendar__day:hover { background: var(--color-input); }
.calendar__day.is-outside { color: var(--color-subtle); opacity: 0.55; }
.calendar__day.is-today { box-shadow: inset 0 0 0 1px var(--color-border-strong); }
.calendar__day.is-selected {
  background: var(--color-primary);
  color: var(--color-on-primary);
  font-weight: 500;
}
.calendar__today {
  width: 100%;
  min-height: 36px;
  margin-top: var(--space-2);
  background: none;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  color: var(--color-foreground);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
.calendar__today:hover { background: var(--color-input); }
/* Opens upward when there is no room below — set by picker.js on open. */
.calendar.is-up, .pick__list.is-up { top: auto; bottom: calc(100% + var(--space-0)); }

/* The month picker: the calendar's popup, twelve months instead of a month
   of days. */
.monthgrid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 2px; }
.monthgrid .calendar__day { aspect-ratio: auto; min-height: 40px; font-family: inherit; }
.period .datefield { flex: 1; min-width: 0; margin: 0; }
.datefield > button:disabled { opacity: 0.45; cursor: default; }

/* The app's own dropdown (picker.js). The button is drawn exactly like the
   select it replaces, chevron included; the list is the calendar's popup. */
.pick { position: relative; margin-bottom: var(--space-2); }
.pick[hidden] { display: none; }
.pick__native {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  margin: 0;
}
.pick__button {
  width: 100%;
  min-height: 40px;
  padding: 0 var(--space-5) 0 var(--space-2);
  text-align: left;
  background-color: var(--color-input);
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right var(--space-2) center, calc(100% - var(--space-2) + 5px) center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  color: var(--color-foreground);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: 16px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
}
.pick__button:hover, .pick__button[aria-expanded="true"] { border-color: var(--color-border-strong); }
.pick__button:disabled { opacity: 0.45; cursor: default; }
.pick__list {
  position: absolute;
  z-index: 20;
  top: calc(100% + var(--space-0));
  left: 0;
  min-width: 100%;
  max-height: min(320px, 50vh);
  overflow-y: auto;
  padding: var(--space-0);
  background: var(--color-muted);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-popover);
}
.pick__list[hidden] { display: none; }
.pick__option {
  display: block;
  width: 100%;
  min-height: 40px;
  padding: 0 var(--space-2);
  text-align: left;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  color: var(--color-foreground);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}
.pick__option:hover, .pick__option:focus-visible { background: var(--color-input); outline: none; }
.pick__option.is-selected { background: var(--color-primary); color: var(--color-on-primary); font-weight: 500; }
.pick__option:disabled { color: var(--color-subtle); cursor: default; }
.pick__group {
  padding: var(--space-1) var(--space-2) var(--space-0);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-subtle);
}
.view .pick + label { margin-top: var(--space-2); }
.filters__field .pick { margin-bottom: 0; }


/* Overlay dialogs. The native confirm cannot be styled and reads as the
   browser interrupting rather than the app asking. */
.dialog {
  width: min(420px, calc(100vw - var(--space-4)));
  padding: var(--space-4);
  background: var(--color-muted);
  color: var(--color-foreground);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-popover);
}
.dialog::backdrop { background: var(--color-scrim); backdrop-filter: blur(2px); }
.dialog__title {
  margin: 0 0 var(--space-2);
  font-size: 16px;
  font-weight: 600;
  letter-spacing: -0.01em;
}
.dialog__body {
  margin: 0 0 var(--space-4);
  font-size: 13px;
  line-height: 1.5;
  color: var(--color-subtle);
}
.dialog__actions { display: flex; gap: var(--space-2); justify-content: flex-end; }
.dialog__actions button {
  min-height: 44px;
  padding: 0 var(--space-3);
  margin: 0;
  width: auto;
  border-radius: var(--radius-md);
  font: inherit;
  cursor: pointer;
}
.dialog__cancel {
  background: var(--color-input);
  color: var(--color-foreground);
  border: 1px solid var(--color-border-strong);
}
.dialog__cancel:hover { background: var(--color-muted); }
/* A destructive action is not the accent colour: the accent means "the
   ordinary thing to do here", and this is not that. */
.dialog__confirm.is-danger {
  background: var(--color-destructive);
  color: var(--color-background);
  border: none;
}

/* One status tone in each direction, so no handler reaches for a raw colour. */
.is-ok { color: var(--color-accent); }

#trend { width: 100%; height: auto; overflow: visible; }
#summary-status, #list-status { min-height: 20px; font-size: 13px; color: var(--color-subtle); }
/* Same as #add-status: 48px of reserved blank between the month picker and the
   first day, holding a place for a line that is usually not there. */
#summary-status:empty, #list-status:empty { min-height: 0; margin: 0; }

/* Per-month budget dialog. The mode switch reuses the period control, and the
   two field sets swap in place so the dialog does not change height much. */
.dialog .seg { margin-bottom: var(--space-3); }
.plan__fields[hidden] { display: none; }
.plan__fields input { margin-bottom: var(--space-2); }
.plan__status { margin: var(--space-2) 0 var(--space-3); }
.dialog__actions .plan__reset { margin-right: auto; }

/* Category limits: a name and an amount on one line per category, so a dozen
   of them fit a phone screen rather than stacking label over field twelve
   times. On the Budget card the group follows the note and keeps the dials'
   hairlines. */
.limits__fields { margin-bottom: var(--space-3); }
.limits__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 150px;
  align-items: center;
  gap: var(--space-2);
}
.limits__row label {
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.limits__row input { margin-bottom: var(--space-1); }
#limits { margin-bottom: var(--space-2); }
#limits .group__heading { margin-top: 0; }

/* The backup reminder. A card like every other, not a red alarm: nothing has
   gone wrong yet, and the point is that it stays that way. */
.backup-nudge { margin-bottom: var(--space-3); }
.backup-nudge p { margin: 0 0 var(--space-3); }
.backup-nudge__actions { display: flex; gap: var(--space-2); justify-content: flex-end; }
.backup-nudge__actions button { width: auto; min-height: 44px; padding: 0 var(--space-3); margin: 0; border-radius: var(--radius-md); }
#storage-status:empty { display: none; }

/* The privacy eye. Sits at the right of every title bar, after the running
   total it governs. 44px is the touch target; the glyph stays tile-sized. */
.privacy-toggle {
  flex: none;
  display: inline-grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin: calc(var(--space-2) * -1) calc(var(--space-2) * -1) calc(var(--space-2) * -1) 0;
  padding: 0;
  background: none;
  border: none;
  border-radius: var(--radius-md);
  color: var(--color-subtle);
  cursor: pointer;
}
.privacy-toggle[aria-pressed="true"] { color: var(--color-active); }
.privacy-toggle:focus-visible { outline: 2px solid var(--color-active); outline-offset: -2px; }
/* Grey while hidden, the accent while figures are showing: the icon's colour
   is the state, readable at a glance before the shape is. */
.privacy-toggle .tile__icon { stroke: currentColor; }

/* A section's own eye, at the end of its heading. Smaller than the title
   bar's master and pulled into the heading's line by negative margins, so a
   heading with an eye is no taller than one without. */
.privacy-toggle--section {
  width: 36px;
  height: 36px;
  margin: -10px -8px -10px auto;
  border-radius: var(--radius-sm);
}
.privacy-toggle--section .tile__icon { width: 18px; height: 18px; }
.section-head,
.stat--balance .stat__label { display: flex; align-items: center; gap: var(--space-2); }

/* The List's filter pills share their line with the rows' eye. */
.filters--list { align-items: center; }
.filters--list .privacy-toggle--section { margin: -2px -8px -2px 0; }

/* App version: when, what's new, and the earlier ones folded away. */
.news { margin: 0 0 var(--space-3); }
.news__when { margin: 0 0 var(--space-2); font-size: 12px; color: var(--color-subtle); }
.news__list { margin: 0 0 var(--space-3); padding-left: var(--space-4); font-size: 13px; line-height: 1.5; }
.news__list li + li { margin-top: var(--space-1); }
/* Left as a list item, which is what draws the disclosure arrow; a flex
   summary loses it and stops looking openable. */
.news__earlier summary {
  min-height: 44px;
  padding-top: 12px;
  font-size: 13px;
  color: var(--color-subtle);
  cursor: pointer;
}
.news__earlier summary:hover { color: var(--color-foreground); }
.news__release { margin-top: var(--space-2); }
.news__release .news__when { margin-bottom: var(--space-1); }
.news__release .news__list { margin-bottom: 0; color: var(--color-subtle); }

/* PIN for amounts: the frequency is one choice of three, as plain radios —
   a short list read once, not a control used daily. */
.pin-ask { margin: 0 0 var(--space-3); padding: 0; border: 0; }
.pin-ask legend { margin-bottom: var(--space-1); padding: 0; font-size: 12px; color: var(--color-subtle); }
.pin-ask label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  margin: 0;
  font-size: 13px;
  color: var(--color-foreground);
  cursor: pointer;
}
.pin-ask input[type="radio"] { flex: none; width: 18px; min-height: 0; height: 18px; margin: 0; }
.pin-ask input[type="number"] { width: 3.5em; min-height: 36px; margin: 0; padding: 0; text-align: center; }

/* Four hidden digits: spaced out, centred, in the figure face. */
.prompt__pin { font-family: var(--font-num); letter-spacing: 0.5em; text-align: center; }

/* Masked figures. The dots stand in for a number; they should read as an
   absence, not as a value in the figure's own colour and weight. */
.mask {
  color: var(--color-subtle);
  opacity: 0.55;
  letter-spacing: 0.05em;
  font-weight: 400;
}

/* The title-bar figure is this month's net. The month name is its label,
   quieter than the figure. */
.h1-meta { display: inline-flex; align-items: baseline; gap: var(--space-1); }
.h1-meta__label { color: var(--color-subtle); font-family: var(--font-ui); }

/* Chart filters as two quiet pills on one line. Labels move to screen
   readers: "All accounts" and "All buckets" name themselves. */
.filters--compact { flex-wrap: nowrap; margin-bottom: var(--space-3); }
.filters--compact .filters__field { flex: 1 1 0; min-width: 0; }
.filters--compact .pick { margin-bottom: 0; }
.filters--compact .pick__button {
  min-height: 32px;
  border-radius: 999px;
  padding-left: var(--space-3);
  font-size: 13px;
  color: var(--color-subtle);
}

/* The ring: stroked arcs on the card itself, so there is no hole to paint. */
#pie text { text-anchor: middle; font-family: var(--font-num); }
.pie__caption { fill: var(--color-subtle); }
.pie__total { fill: var(--color-foreground); }

/* The categories folded into the ring's Other slice, listed under it: no
   swatch, indented to the label column, tighter and in the quiet tone, so
   the legend still reads as five lines at a glance. Same size as the rows
   above, so the share and amount columns stay aligned. */
.figures__row--sub {
  padding-top: var(--space-1);
  padding-bottom: var(--space-1);
  color: var(--color-subtle);
}
.figures__row--sub + .figures__row--sub { border-top: none; }
.figures__row--sub .figures__label { padding-left: 16px; }
/* The share sits against the amount, so amounts of different lengths would
   walk it left and right. A fixed amount column keeps the shares in one. */
.figures--legend .amount { min-width: 13ch; text-align: right; }

/* Share column in the legend, between the name and the amount. */
.figures__share {
  margin-left: auto;
  padding-right: var(--space-3);
  color: var(--color-subtle);
  font-family: var(--font-num);
  font-size: 13px;
}
#pie .mask { fill: var(--color-subtle); }
