/* app.css — design tokens and layout.
   Palette values come from a validated reference set; the blue/red pairing the
   chart switches between clears CVD and contrast gates in both modes. Status
   amber is deliberately sub-3:1 on light, so it never appears without an icon
   and a text label beside it. */

:root {
  color-scheme: light;
  --page:            #f9f9f7;
  --surface-1:       #fcfcfb;
  --surface-2:       #f2f2ee;
  --text-primary:    #0b0b0b;
  --text-secondary:  #52514e;
  --text-muted:      #898781;
  --grid:            #e1e0d9;
  --axis:            #c3c2b7;
  --border:          rgba(11, 11, 11, 0.10);
  --border-strong:   rgba(11, 11, 11, 0.18);
  --series-1:        #2a78d6;
  /* The berm, as colour. A berm is the bank of earth between dry ground and
     the water it holds back, and the app is the same shape: the buffer is the
     earthwork, the low point of the cycle is the water, and everything here is
     about which side of it you are on. So the palette has two sides — sunlit
     green where you are clear, storm and water where you are not — with the
     sand of the bank between them.

     These are atmosphere, not status. --status-* is untouched: it carries
     meaning under colour blindness and at 3:1, and a metaphor is not a good
     enough reason to put that at risk. These appear as washes and in the mark,
     where looking like something is the whole job. */
  --sun:             #ffd34d;
  --meadow:          #3f9150;
  --sand:            #f0e3c8;
  --water:           #245f8f;
  --storm:           #2e4a5e;
  --status-good:     #0ca30c;
  --status-warning:  #fab219;
  --status-serious:  #ec835a;
  --status-critical: #d03b3b;
  --text-good:       #006300;
  --text-critical:   #a52a2a;
  --wash-critical:   rgba(208, 59, 59, 0.09);
  --wash-warning:    rgba(250, 178, 25, 0.14);
  --wash-good:       rgba(12, 163, 12, 0.09);
  --wash-series:     rgba(42, 120, 214, 0.09);
  /* Text on a card's band chip, and the critical banner's button: each 4.5:1
     or better on what it sits on, the chips on their own washes. Status
     amber and the series blue are both under that as small text, which is
     why the chips do not simply take them. */
  --text-soon:       #9a4618;
  --text-look:       #1d5ca8;
  --btn-critical:    #b3302f;
  --shadow:          0 1px 2px rgba(11, 11, 11, .05), 0 4px 16px rgba(11, 11, 11, .05);
  --radius:          10px;

  /* The mark's own ground, so the opening tile and the app icon cannot drift
     apart. tests/run.js checks these against tools/mark.mjs. */
  --mark-from:       #0e9fdd;
  --mark-to:         #252562;
  --font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) {
    color-scheme: dark;
    --page:           #0d0d0d;
    --surface-1:      #1a1a19;
    --surface-2:      #232321;
    --text-primary:   #ffffff;
    --text-secondary: #c3c2b7;
    --text-muted:     #898781;
    --grid:           #2c2c2a;
    --axis:           #383835;
    --border:         rgba(255, 255, 255, 0.10);
    --border-strong:  rgba(255, 255, 255, 0.20);
    --series-1:       #3987e5;
    --sun:            #ffd34d;
    --meadow:         #4aa55d;
    --sand:           #e8d9b8;
    --water:          #3a7fb8;
    --storm:          #22394a;
    --text-good:      #0ca30c;
    --text-critical:  #e88080;
    --wash-critical:  rgba(208, 59, 59, 0.16);
    --wash-warning:   rgba(250, 178, 25, 0.14);
    --wash-good:      rgba(12, 163, 12, 0.14);
    --wash-series:    rgba(57, 135, 229, 0.14);
    --text-soon:      #ec835a;
    --text-look:      #5598ec;
    --btn-critical:   #c43838;
    --shadow:         0 1px 2px rgba(0, 0, 0, .4), 0 4px 16px rgba(0, 0, 0, .3);
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --page:           #0d0d0d;
  --surface-1:      #1a1a19;
  --surface-2:      #232321;
  --text-primary:   #ffffff;
  --text-secondary: #c3c2b7;
  --text-muted:     #898781;
  --grid:           #2c2c2a;
  --axis:           #383835;
  --border:         rgba(255, 255, 255, 0.10);
  --border-strong:  rgba(255, 255, 255, 0.20);
  --series-1:       #3987e5;
  --sun:            #ffd34d;
  --meadow:         #4aa55d;
  --sand:           #e8d9b8;
  --water:          #3a7fb8;
  --storm:          #22394a;
  --text-good:      #0ca30c;
  --text-critical:  #e88080;
  --wash-critical:  rgba(208, 59, 59, 0.16);
  --wash-warning:   rgba(250, 178, 25, 0.14);
  --wash-good:      rgba(12, 163, 12, 0.14);
  --wash-series:    rgba(57, 135, 229, 0.14);
  --text-soon:      #ec835a;
  --text-look:      #5598ec;
  --btn-critical:   #c43838;
  --shadow:         0 1px 2px rgba(0, 0, 0, .4), 0 4px 16px rgba(0, 0, 0, .3);
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
/* THE PHONE NEVER ZOOMS BY ITSELF. Two of the ways it did are here; the
   third, fields under 16px, is under forms below. Double-tap zoom: two
   quick taps on a pill or a line of text and iOS enlarges the page round
   it. manipulation keeps panning and a deliberate pinch and drops only
   that, and every element under the root inherits it. text-size-adjust
   stops iOS enlarging the text when the phone turns sideways. Pinch-zoom
   itself is never switched off: see the viewport in index.html. */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; touch-action: manipulation; }
body {
  background: var(--page);
  color: var(--text-primary);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}
h1, h2, h3, h4 { margin: 0; font-weight: 600; line-height: 1.25; }
a { color: var(--series-1); }
:focus-visible { outline: 2px solid var(--series-1); outline-offset: 2px; border-radius: 4px; }

/* ---- shell ------------------------------------------------------------- */

.wrap { max-width: 1080px; margin: 0 auto; padding: 0 20px 64px; }

.topbar {
  position: sticky; top: 0; z-index: 20;
  background: color-mix(in srgb, var(--page) 88%, transparent);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--border);
}
/* A word, a glyph, and the sentence under them. The heading used to BE the
   sentence, which reads well over a card and badly in a menu of sixteen. */
.feed-title { display: flex; align-items: center; gap: 9px; }
.feed-icon { width: 20px; height: 20px; flex: none; color: var(--text-muted); }
.feed-subtitle { color: var(--text-secondary); font-size: 14px; margin-top: 2px; }
/* Beside the word in a row, not above it: the row is a target to press, so
   the glyph shares the row's vertical centre rather than the title's. */
.menu-icon { width: 17px; height: 17px; flex: none; color: var(--text-muted); align-self: center; }
.menu-row[aria-current="true"] .menu-icon { color: var(--series-1); }

/* The pace strip, under the bar and inside the same sticky header so the two
   travel together. Not in .topbar-inner: that is three fixed tracks with the
   mark pinned to the middle, and a fourth child pushes the mark off centre —
   which is exactly what the live location readout did before it was removed.

   Deliberately quiet. It is on screen the whole time, so it is sized and
   coloured to be read when looked at rather than to keep asking to be. */
/* Centred, under the mark the bar centres. */
.pacebar {
  display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap;
  max-width: 1080px; margin: 0 auto; padding: 0 20px 9px;
  font-size: 13px; color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}
.pacebar-note { color: var(--text-muted); }

/* Which period the Budget and Period cards show, when it is not the one
   being lived in (paintPeriodBar): a strip under the pills, in the same
   sticky header, washed so it reads as the page being somewhere else. */
.periodbar {
  display: flex; align-items: center; justify-content: center; gap: 6px 10px; flex-wrap: wrap;
  padding: 6px 14px; font-size: 13px; color: var(--text-primary);
  background: var(--wash-series); border-top: 1px solid var(--border);
}
.periodbar[hidden] { display: none; }
.periodbar-says { display: inline-flex; align-items: center; min-width: 0; white-space: nowrap; }
.periodbar-icon { width: 15px; height: 15px; flex: none; margin-right: 6px; color: var(--series-1); }
.periodbar-dates, .periodbar-where { white-space: pre; }
.periodbar-where { color: var(--text-secondary); }
.periodbar-back { padding-top: 2px; padding-bottom: 2px; white-space: nowrap; }
/* One line at every width: which cards follow it goes first (the title
   still says it), and on the narrowest phones the button says "Back". */
@media (max-width: 560px) { .periodbar-where { display: none; } }
@media (max-width: 420px) { .periodbar { font-size: 12px; padding: 5px 10px; gap: 8px; } }
@media (max-width: 380px) { .periodbar-long { display: none; } }

/* Actual, safe, planned: three pills coloured by where each rate lands
   against the safe one. The colour is a tint and an edge rather than the
   text, because the yellow is unreadable as text on cream; the figure takes
   the ink where the ink is legible. Each is a button — pressing one moves
   every slider and the sky to its rate — and the pressed one wears a ring. */
.pace-pills { display: inline-flex; gap: 5px; flex-wrap: wrap; }
/* THE SAMPLE'S TOUR (startTour in ui.js): a finger pressing the pills, the
   ring a press leaves on one, and the line in the sample's banner saying what
   each is. The finger is over the page and never in its way: it takes no
   touch, and it is put up and taken down by the code. The line is in the
   page, where it covers nothing. Under reduced motion the tour is not started
   at all. */
.tour-finger {
  position: fixed; z-index: 60; pointer-events: none; transform: translate(-50%, 0);
  transition: left .6s ease, top .6s ease; line-height: 0;
  filter: drop-shadow(0 2px 3px rgba(0, 0, 0, .35));
}
/* The hand is drawn (art.js, finger), where the 👆 emoji was. */
.tour-finger span { display: inline-block; width: 38px; height: 38px; }
.tour-finger svg { display: block; width: 100%; height: 100%; }
.tour-finger.is-press span { animation: tour-press .42s ease-in-out; }
.tour-finger.is-point span { animation: tour-point 1.2s ease-in-out 2; }
@keyframes tour-press { 0% { transform: translateY(0) scale(1); } 45% { transform: translateY(-7px) scale(.88); } 100% { transform: translateY(0) scale(1); } }
@keyframes tour-point { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }
.pace-pill.tour-tap { position: relative; }
.pace-pill.tour-tap::after {
  content: ''; position: absolute; inset: -3px; border-radius: 999px; pointer-events: none;
  border: 2px solid var(--series-1); animation: tour-tap .45s ease-out forwards;
}
@keyframes tour-tap { from { opacity: .9; transform: scale(.92); } to { opacity: 0; transform: scale(1.18); } }
.tour-line {
  margin: 6px 0 2px; padding: 7px 10px; border-radius: 8px; font-size: 14px; line-height: 1.35;
  background: var(--wash-series); color: var(--text-primary); border-left: 3px solid var(--series-1);
}
.pace-pill {
  appearance: none; font: inherit; cursor: pointer; white-space: nowrap;
  display: inline-flex; align-items: baseline; gap: 4px;
  padding: 2px 8px; border-radius: 999px; line-height: 1.35;
  border: 1px solid var(--pill-edge, var(--border-strong));
  background: var(--pill-wash, transparent); color: var(--text-primary);
}
.pace-pill-k { color: var(--text-secondary); }
.pace-pill-v { font-weight: 650; color: var(--pill-ink, var(--text-primary)); }
/* The tones are shared with the budget panel's pills, so red, yellow and
   green are one definition wherever the three paces appear. */
.pace-pill.is-red,    .budget-pill.is-red    { --pill-wash: var(--wash-critical); --pill-edge: var(--status-critical); --pill-ink: var(--text-critical); }
.pace-pill.is-yellow, .budget-pill.is-yellow { --pill-wash: var(--wash-warning);  --pill-edge: var(--status-warning);  --pill-ink: var(--text-primary); }
.pace-pill.is-green,  .budget-pill.is-green  { --pill-wash: var(--wash-good);     --pill-edge: var(--status-good);     --pill-ink: var(--text-good); }
.pace-pill.is-none,   .budget-pill.is-none   { --pill-ink: var(--text-muted); border-style: dashed; }
.pace-pill[aria-pressed="true"], .budget-pill[aria-pressed="true"] { box-shadow: 0 0 0 2px var(--pill-edge, var(--border-strong)); }
/* One line is the requirement, so the width gives way before the line does:
   the trailing "a day" goes first — every pill's title and the group's label
   still say it — and on the narrowest phones "Planned" becomes "Plan". */
@media (max-width: 440px) { .pacebar-note { display: none; } }
@media (max-width: 420px) {
  .pacebar { font-size: 12px; gap: 6px; padding-left: 14px; padding-right: 14px; }
  .pace-pills { gap: 4px; }
  .pace-pill { padding: 2px 6px; gap: 3px; }
}
@media (max-width: 380px) { .pace-pill-long { display: none; } }
/* A negative safe rate is a character wider, and at 320 that was the one
   that pushed the third pill onto a second line. */
@media (max-width: 340px) {
  .pacebar { font-size: 11.5px; padding-left: 12px; padding-right: 12px; }
  .pace-pill { padding: 2px 5px; }
}

/* Three columns rather than a row, so the mark sits in the true centre of the
   bar however wide the two buttons beside it happen to be. A flex row with a
   spacer centres it between the buttons, which is not the same thing and is
   visibly off whenever they differ. */
.topbar-inner {
  max-width: 1080px; margin: 0 auto; padding: 12px 20px;
  /* minmax(0, …), not a bare 1fr. A bare fr keeps an automatic minimum, so a
     track never shrinks below the widest thing in it, and whatever is in the
     outer tracks then pushes the mark off centre — or over, which is what the
     menu button did at phone width, by about 25px, back when it grew a live
     location readout. That readout is gone; the minimum stays zeroed, because
     "the button will never be wide" is not a thing to rely on. */
  display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center; gap: 12px;
  /* The containing block for the build number below, which is positioned
     against the mark's track rather than placed in one. */
  position: relative;
  /* The mark's height, in one place because the build number is set on the
     mark's bottom edge and has to know where that is. */
  --mark-h: 19px;
}
/* Named tracks, not counted ones. Auto-placement fills tracks in document
   order and display:none takes an element out of that order entirely, so a
   hidden control would slide the mark into the track beside it — which is
   exactly what the lock screen is, a bar with neither button on it. Pinning
   the mark to the middle track makes it centred whatever else is there. */
.topbar-inner > :first-child { grid-column: 1; justify-self: start; }
.topbar-inner > .brand { grid-column: 2; }
.topbar-inner > :last-child { grid-column: 3; justify-self: end; }
/* Either outer control may be the wide one — they have swapped sides once
   already — so both are allowed to shrink inside their track rather than push
   the mark along. */
.topbar-inner > :first-child, .topbar-inner > :last-child { min-width: 0; max-width: 100%; }

/* The mark itself, from the same file the icons are cut from. It is a mask
   rather than an image because an <img> cannot be recoloured and this has to
   be ink on cream in one scheme and ink on near-black in the other. The ratio
   is the traced artwork's own; tests/run.js checks it against tools/mark.mjs,
   because a wrong one here silently squashes the word. */
.brand {
  display: block;
  height: var(--mark-h, 19px);
  aspect-ratio: 100 / 25.265;
  background: var(--text-primary);
  -webkit-mask: url(../icons/mark.svg) no-repeat center / contain;
  mask: url(../icons/mark.svg) no-repeat center / contain;
}

/* Which build this is, in small type off the right of the mark.

   OUT OF FLOW, and that is the only reason it is allowed in the bar. The grid
   is three tracks with the mark pinned to the middle one, and a fourth child
   in flow pushes the mark off centre — two live readouts have been taken out
   for exactly that. An absolutely positioned child takes no track: with the
   grid as its containing block and grid-column: 2 / 3, it is positioned
   against the mark's own column, so left: 100% is the mark's right edge
   whatever the mark's width, and nothing else in the bar moves by a pixel.

   Both lines, not `grid-column: 2`. That is 2 / auto, and for an absolutely
   positioned child an auto line is the grid's padding edge — so the column
   ran from the mark to the far end of the bar, and the number drew 163px
   to the right, off the screen.

   Set on the mark's bottom edge, which is where the letters stand, so it
   reads as part of one line — BERM v1.19.2. Centred on the mark's box it sat
   level with the wave's crest instead and looked like it had drifted. The
   mark is centred in the bar, so its bottom is half its height below the
   middle.

   Quiet on purpose. It is there for the moment somebody needs to know which
   build they are on — after a deploy, or telling us about a problem — and
   the rest of the time it should read as part of the chrome. */
.brand-version {
  position: absolute; grid-column: 2 / 3;
  left: 100%; top: calc(50% + var(--mark-h) / 2); transform: translateY(-100%);
  margin-left: 6px;
  font-size: 10px; line-height: 1; letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums; white-space: nowrap;
  color: var(--text-muted);
}

/* The search, and room for anything else that earns a place on the bar. */
.bar-btn {
  appearance: none; cursor: pointer; font: inherit;
  display: grid; place-items: center; flex: none;
  width: 34px; height: 34px; padding: 0; border-radius: 999px;
  border: 1px solid var(--border-strong); background: var(--surface-1);
  color: var(--text-primary);
}
.bar-btn:hover { background: var(--surface-2); }
.bar-btn[aria-expanded="true"] { background: var(--surface-2); border-color: var(--series-1); }

/* Both bar controls are hidden from script before the store is open, and both
   need saying twice: an author `display` beats the UA's [hidden] { display:
   none } whatever the specificity, so .bar-btn { display: grid } and .menu-btn
   { display: flex } were quietly cancelling the attribute. "On this page" sat
   on the setup and lock screens for as long as it has had a hidden attribute,
   opening an empty panel. .menu[hidden] a few rules down is the same fix,
   already made once for the panel and not for the button that opens it. */
.bar-btn[hidden], .menu-btn[hidden] { display: none; }
.bar-btn svg { width: 18px; height: 18px; display: block; }
/* The mark carries its own tile, so this only has to place it. Same geometry
   as the favicon and the home-screen icon, from the same three numbers. */
/* The mark carries its own tile and its own weather, so this only has to
   place it. Filled by scene.js on every repaint, including the ones a slider
   causes mid-drag. */

/* The sky beside its words: the weather's glyph over the mark's outline,
   and the word, the sentence and the figure beside it. */
.sky { display: flex; align-items: stretch; gap: 14px; margin-bottom: 16px; }
/* Square, and centred rather than stretched. The picture is square — the
   drawing's viewBox is — so a box that took its height from the sentences
   beside it either letterboxed the drawing or stretched the render.

   Six places draw one: the Forecast card, the plan editor, the plan over
   the envelopes, the Period card's story, a scan's review and the
   affordability asker; the lock screen's is the wide band. The drawing is
   scene.js's — a thin tapered glyph over the mark's outline, on nothing,
   since 2026-10-09 — so nothing is fetched for it and nothing is laid
   over it. No ground and no corners: the card shows through, and the
   mark's hairline is drawn in the secondary text's colour (currentColor). */
.sky-art { flex: none; position: relative; width: 108px; aspect-ratio: 1; align-self: center;
           color: var(--text-secondary); }
.sky-art svg { display: block; width: 100%; height: 100%; }
.sky-said { min-width: 0; align-self: center; }
.sky-label { font-size: 17px; font-weight: 650; letter-spacing: -0.01em; }
.sky-say { color: var(--text-secondary); font-size: 14px; margin-top: 2px; }
.sky-fig { color: var(--text-muted); font-size: 12.5px; margin-top: 4px; font-variant-numeric: tabular-nums; }
@media (max-width: 620px) { .sky-art { width: 84px; } .sky-label { font-size: 16px; } }
/* THE VERDICT, ONCE (2026-10-11): the Forecast card's sentence beside the
   sky, in the ink of its tone and with no box — the glyph and the card's
   chip carry the alarm the red banner did — and under the sky what
   explains it and its one action (verdict-more, drawBalance in ui.js). */
.sky-verdict { font-weight: 650; font-size: 15px; margin-top: 2px; }
.sky-verdict.is-critical { color: var(--text-critical); }
.sky-verdict.is-warning { color: var(--status-serious); }
.verdict-more { margin: -6px 0 16px; }
.verdict-more .told > summary { font-size: 14px; }
.verdict-more .told-text { font-size: 14px; }
.verdict-more .banner-actions { margin-top: 10px; }
/* Its one button in the overdraft's own colour, as the banner's was. */
.verdict-more.is-critical .btn-primary { background: var(--btn-critical); border-color: var(--btn-critical); }
.verdict-more.is-critical .btn-primary:hover { background: color-mix(in srgb, var(--btn-critical) 88%, #000); }

/* ---- the one menu -------------------------------------------------------

   The only control in the shell. It says what is on the page, where you are
   in it, and takes you to any of it; everything else is scrolling. */

.menu-btn {
  appearance: none; cursor: pointer; font: inherit; font-size: 13px;
  display: flex; align-items: center; gap: 9px; min-width: 0;
  padding: 7px 12px; border-radius: 999px;
  border: 1px solid var(--border-strong); background: var(--surface-1);
  color: var(--text-primary);
}
.menu-btn:hover { background: var(--surface-2); }
.menu-btn[aria-expanded="true"] { background: var(--surface-2); border-color: var(--series-1); }
.menu-bars { display: grid; gap: 3px; flex: none; }
.menu-bars span { display: block; width: 14px; height: 1.5px; border-radius: 1px; background: currentColor; }
.menu-label { font-weight: 600; white-space: nowrap; }

/* Over the page, not part of it. In the flow it made the header taller, which
   pushed every card down by the height of the panel — so opening the menu to
   find something moved the something. */
.menu {
  position: absolute; left: 0; right: 0; top: 100%;
  background: var(--page); border-bottom: 1px solid var(--border);
  box-shadow: var(--shadow);
  max-height: min(70vh, 560px); overflow-y: auto;
  padding: 6px 20px 14px;
}
.menu > * { max-width: 1080px; margin-left: auto; margin-right: auto; }
.menu[hidden] { display: none; }
/* minmax(0,1fr), not 1fr: a grid track sizes to its content by default, so
   one long note pushed every row past the right edge instead of ellipsing. */
.menu-list { display: grid; gap: 2px; grid-template-columns: minmax(0, 1fr); }
.menu-row {
  appearance: none; cursor: pointer; font: inherit; text-align: left; width: 100%;
  display: flex; align-items: baseline; gap: 12px;
  padding: 9px 11px; border-radius: 8px;
  border: 1px solid transparent; background: transparent; color: inherit;
}
.menu-row { min-width: 0; }
.menu-row:hover { background: var(--surface-2); }
.menu-row[aria-current] { border-color: var(--border-strong); background: var(--surface-2); }
.menu-main { min-width: 0; flex: 1; }
.menu-title { font-weight: 600; font-size: 14px; }
.menu-note {
  font-size: 12.5px; color: var(--text-secondary); margin-top: 2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.menu-flag { flex: none; font-size: 11.5px; font-weight: 600; white-space: nowrap; }
.menu-foot {
  display: flex; align-items: center; gap: 10px;
  margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border);
}
/* Yours and Ours, above the cards, once there is a joint budget to go to
   (budgetSwitch in ui.js). The one open is pressed, as a goal kind is. */
.menu-budgets {
  display: flex; gap: 6px; padding: 6px 0 10px; margin-bottom: 6px;
  border-bottom: 1px solid var(--border);
}
.menu-budget[aria-pressed="true"] { background: var(--wash-series); border-color: color-mix(in srgb, var(--series-1) 45%, transparent); color: var(--series-1); }

/* Search reuses .menu for the panel and .menu-row for the results; what is
   its own is the field and the tag on the right saying where a row goes. */
.search-field { padding: 8px 0 10px; }
.search-field input {
  width: 100%; font: inherit; font-size: 15px;
  padding: 9px 12px; border-radius: 9px;
  border: 1px solid var(--border-strong);
  background: var(--surface-1); color: var(--text-primary);
}
.search-empty { padding: 4px 11px 12px; font-size: 13px; color: var(--text-secondary); }
.search-where {
  color: var(--text-secondary); font-weight: 700; font-size: 10.5px;
  letter-spacing: 0.04em; text-transform: uppercase;
}

.view { padding-top: 22px; }
.view[hidden] { display: none; }

/* ---- the feed -----------------------------------------------------------

   Cards are separated by air and a hairline rather than boxed, because they
   already contain boxes — a card inside a card reads as two levels of nesting
   and there is only one. What a raised card gets instead is a line of type
   saying why it was raised, which is the only thing about it that is new. */

.feed-card { scroll-margin-top: 92px; }
.feed-card:focus { outline: none; }
.feed-card:focus-visible { outline: 2px solid var(--series-1); outline-offset: 10px; border-radius: 4px; }
.feed-card + .feed-card { margin-top: 34px; padding-top: 30px; border-top: 1px solid var(--border); }
/* THE BAND IS A CHIP ON THE TITLE ROW (2026-10-07). It was a line of its
   own above the name, so a raised card opened on four lines of head — the
   band, the name, the sentence and the note — before anything on it. A grid
   rather than flex, so the chip keeps its corner whatever order the head is
   built in, and the sentence and the note run under both. */
.feed-head { margin-bottom: 15px; display: grid; grid-template-columns: minmax(0, 1fr) auto;
  column-gap: 10px; align-items: center; }
.feed-head > .feed-titlerow { grid-column: 1; grid-row: 1; }
.feed-head > .feed-subtitle, .feed-head > .feed-note { grid-column: 1 / -1; }
.feed-flag {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 11px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
  grid-column: 2; grid-row: 1; justify-self: end; white-space: nowrap;
  padding: 4px 9px 4px 8px; border-radius: 999px;
}
.feed-flag.is-now { background: var(--wash-critical); }
.feed-flag.is-soon { background: var(--wash-warning); }
.feed-flag.is-look { background: var(--wash-series); }
.feed-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: none; }
.feed-titlerow { display: flex; align-items: baseline; gap: 12px; }
.feed-title { font-size: 19px; letter-spacing: -0.015em; text-wrap: balance; min-width: 0; }
.feed-note { margin: 5px 0 0; color: var(--text-secondary); font-size: 14px; max-width: 64ch; }

/* Severity by colour and by word, never by colour alone: the flag carries its
   own label, so these only have to tell two raised cards apart at a glance. */
.is-now .feed-flag, .menu-flag.is-now { color: var(--text-critical); }
.is-soon .feed-flag, .menu-flag.is-soon { color: var(--status-serious); }
.is-look .feed-flag, .menu-flag.is-look { color: var(--series-1); }
/* On its wash the chip's word needs the darker end of its colour. */
.feed-flag.is-soon { color: var(--text-soon); }
.feed-flag.is-look { color: var(--text-look); }

/* ---- cards ------------------------------------------------------------- */

.card {
  background: var(--surface-1); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 18px; box-shadow: var(--shadow);
}
.card + .card { margin-top: 16px; }
.card-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 14px; flex-wrap: wrap; }
.card-head h2 { font-size: 15px; }
.card-head .card-sub { color: var(--text-muted); font-size: 13px; }
.card-head .spacer { flex: 1; }

.section-title { font-size: 13px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); margin: 26px 0 10px; font-weight: 600; }

/* ---- alert banner ------------------------------------------------------ */

.banner {
  display: flex; gap: 13px; align-items: flex-start;
  border: 1px solid var(--border); border-left-width: 3px;
  border-radius: var(--radius); padding: 15px 17px; margin-bottom: 16px;
  background: var(--surface-1);
}
.banner-icon { flex: none; width: 21px; height: 21px; margin-top: 1px; }
.banner-body { flex: 1; min-width: 0; }
.banner-title { font-weight: 650; font-size: 15.5px; }
.banner-detail { color: var(--text-secondary); font-size: 14px; margin-top: 3px; }
.banner-actions { margin-top: 11px; display: flex; gap: 8px; flex-wrap: wrap; }
.banner.is-critical { border-left-color: var(--status-critical); background: var(--wash-critical); }
.banner.is-critical .banner-icon { color: var(--status-critical); }
/* Its one button in its own colour: the series blue on the red wash was the
   loudest clash on the first screen. */
.banner.is-critical .btn-primary { background: var(--btn-critical); border-color: var(--btn-critical); }
.banner.is-critical .btn-primary:hover { background: color-mix(in srgb, var(--btn-critical) 88%, #000); }
.banner.is-warning  { border-left-color: var(--status-warning); background: var(--wash-warning); }
.banner.is-warning .banner-icon { color: var(--status-serious); }
.banner.is-ok { border-left-color: var(--status-good); background: var(--wash-good); }
.banner.is-ok .banner-icon { color: var(--status-good); }
.banner.is-empty { border-left-color: var(--axis); }
.banner.is-empty .banner-icon { color: var(--text-muted); }
/* The sample before an account: above the feed, in the mark's blue rather
   than a status colour, because it is not saying anything about money. */
.banner.is-demo { border-left-color: var(--series-1); margin-top: 16px; }
.banner.is-demo .banner-icon { color: var(--series-1); }
/* Which budget is open, wherever the joint one is on screen (oursNote in
   ui.js): the same quiet blue as the sample's, because it says which budget
   and nothing about money. */
.banner.ours-note { border-left-color: var(--series-1); }
.banner.ours-note .banner-icon { color: var(--series-1); }
#view > .banner.ours-note { margin-top: 16px; }

/* ---- stat tiles -------------------------------------------------------- */

/* Wrapping flex rather than a grid of fixed tracks (2026-10-07): a row is
   always filled, so the fifth tile runs across the foot instead of sitting
   alone beside a hole — two a row on a phone, four on a desk, and the one
   left over taking the width either way. Raised by the shadow alone. */
.kpis { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 16px; }
.kpis > .kpi { flex: 1 1 158px; min-width: 0; }
.kpi {
  background: var(--surface-1); border: 1px solid transparent;
  border-radius: var(--radius); padding: 14px 15px; box-shadow: var(--shadow);
}
.kpi-label { font-size: 12.5px; color: var(--text-muted); margin-bottom: 5px; }
.kpi-value { font-size: 25px; font-weight: 650; letter-spacing: -0.02em; }
.kpi-value.is-critical { color: var(--text-critical); }
.kpi-value.is-good { color: var(--text-good); }
/* No --text-warning exists: the warning wash is a background colour and fails
   contrast as text. --status-serious is the darker end of that ramp and is
   already used for warning icons. */
.kpi-value.is-warning { color: var(--status-serious); }
.kpi-meta { font-size: 12.5px; color: var(--text-secondary); margin-top: 3px; }

/* ---- filter row -------------------------------------------------------- */

.filterbar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 16px; }
.filterbar .filter-label { font-size: 13px; color: var(--text-muted); }
.segmented { display: inline-flex; background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px; padding: 2px; }
/* A filter bar's control wraps rather than running off the screen. Its
   buttons are single words like "30d" that cannot shrink, and the horizon
   control is seven of them once "To overdraft" joins it — 364px, which at
   320px wide pushed the whole page sideways. Only here: a stacked control's
   labels wrap inside their buttons instead, and wrapping the buttons would
   break a pair of choices onto two rows at ordinary phone width. */
.filterbar .segmented { flex-wrap: wrap; max-width: 100%; }
.segmented button {
  appearance: none; border: 0; background: transparent; cursor: pointer; font: inherit;
  font-size: 13px; color: var(--text-secondary); padding: 5px 11px; border-radius: 6px;
}
.segmented button[aria-pressed="true"] { background: var(--surface-1); color: var(--text-primary); font-weight: 600; box-shadow: 0 1px 2px rgba(0,0,0,.08); }
/* The forecast's six horizons fit one line down to 320px (horizonFilter). */
@media (max-width: 360px) { .filterbar .segmented button { padding-left: 8px; padding-right: 8px; } }
/* The urgent option carries its colour whether or not it is the selected one:
   it only exists when something is actually due, and that is worth seeing
   without having to press it. */
.segmented button.is-critical { color: var(--text-critical); font-weight: 600; }
.segmented button.is-warning  { color: var(--status-serious); font-weight: 600; }
.segmented button.is-good     { color: var(--text-good); font-weight: 600; }
.segmented button.is-critical[aria-pressed="true"] { background: var(--wash-critical); }
.segmented button.is-warning[aria-pressed="true"]  { background: var(--wash-warning); }
.segmented button.is-good[aria-pressed="true"]     { background: var(--wash-good); }
/* The three keep-back choices carry an amount under the word, so they are
   taller than the segmented control's usual one-line labels. */
.segmented.is-stacked button { display: grid; gap: 1px; line-height: 1.25; padding: 6px 12px; }
.segmented.is-stacked .seg-amount { font-size: 12px; opacity: .85; font-variant-numeric: tabular-nums; }

/* ---- chart ------------------------------------------------------------- */

.chart-host { position: relative; width: 100%; min-width: 0; }
/* min-width: 0, because an inline SVG drawn with a viewBox and a fixed
   height has an intrinsic width — the viewBox's — and a grid track sized
   auto grows to it, so a chart drawn once at a width and redrawn at the
   track's width settles wherever it first landed, wider than the card. The
   Goals card's future section found it at 320px; the same guard as
   .env-list and .goal-list. */
.chart-svg { display: block; overflow: visible; touch-action: pan-y; }
.chart-tick { font-size: 11px; font-family: var(--font); font-variant-numeric: tabular-nums; }
.chart-ref  { font-size: 10.5px; font-family: var(--font); }
.chart-label { font-size: 12px; font-weight: 650; font-family: var(--font); }
.chart-tip {
  position: absolute; z-index: 5; pointer-events: none; opacity: 0;
  min-width: 172px; max-width: 268px;
  background: var(--surface-1); border: 1px solid var(--border-strong);
  border-radius: 8px; padding: 9px 11px; box-shadow: var(--shadow);
  transition: opacity .09s ease;
}
.chart-tip.is-visible { opacity: 1; }
.tip-date { font-size: 12px; color: var(--text-muted); }
.tip-value { font-size: 19px; font-weight: 650; letter-spacing: -0.02em; margin-top: 1px; }
.tip-value.is-critical { color: var(--text-critical); }
.tip-value.is-warning { color: var(--text-primary); }
.tip-sub { font-size: 11.5px; color: var(--text-muted); }
.tip-items { margin-top: 8px; border-top: 1px solid var(--border); padding-top: 7px; display: grid; gap: 3px; }
.tip-item { display: flex; gap: 10px; font-size: 12.5px; }
.tip-item-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-secondary); }
.tip-item-amt { font-variant-numeric: tabular-nums; white-space: nowrap; }
.tip-item-amt.is-out { color: var(--text-critical); }
.tip-item-amt.is-in { color: var(--text-good); }
.tip-more { font-size: 11.5px; color: var(--text-muted); }

/* ---- tables ------------------------------------------------------------ */

.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table.data { width: 100%; border-collapse: collapse; font-size: 14px; }
table.data th, table.data td { text-align: left; padding: 8px 11px; border-bottom: 1px solid var(--border); white-space: nowrap; }
table.data th {
  font-size: 11.5px; text-transform: uppercase; letter-spacing: .05em;
  color: var(--text-muted); font-weight: 600; position: sticky; top: 0;
  background: var(--surface-1); z-index: 1;
}
table.data td.num, table.data th.num { text-align: right; font-variant-numeric: tabular-nums; }
table.data td.wide { white-space: normal; min-width: 190px; }
table.data tbody tr:hover { background: var(--surface-2); }
table.data tr.is-critical td { background: var(--wash-critical); }
table.data tr.is-critical:hover td { background: color-mix(in srgb, var(--wash-critical) 70%, var(--surface-2)); }
table.data tr.is-warning td { background: var(--wash-warning); }
table.data tr.is-weekend td:first-child { color: var(--text-muted); }
table.data tr.is-today td { box-shadow: inset 2px 0 0 var(--series-1); }
.amt-out { color: var(--text-critical); }
.amt-in { color: var(--text-good); }
.rowflag { display: inline-flex; align-items: center; gap: 4px; font-size: 11.5px; font-weight: 650; }
.rowflag svg { width: 13px; height: 13px; flex: none; }
.rowflag.is-critical { color: var(--text-critical); }
.rowflag.is-warning { color: var(--text-secondary); }

/* ---- forms & buttons --------------------------------------------------- */

/* align-content:start keeps the input directly under its label, so fields with
   a hint line below do not push their input out of line with their neighbours */
.field { display: grid; gap: 5px; align-content: start; }
.field label { font-size: 12.5px; color: var(--text-secondary); font-weight: 500; }
.field .hint { font-size: 12px; color: var(--text-muted); }
/* What the password manager should do, and — folded — what to do when it
   does not. The summary reads as a link: it is the one thing on the line to
   press, and it opens in place rather than going anywhere. */
.pm-note { display: grid; gap: 4px; }
.pm-help summary { font-size: 12.5px; color: var(--series-1); cursor: pointer; width: max-content; }
.pm-help ol { margin: 6px 0 2px; padding-left: 20px; display: grid; gap: 6px;
              font-size: 12.5px; color: var(--text-secondary); line-height: 1.45; }
input, select, textarea {
  font: inherit; font-size: 14px; color: var(--text-primary);
  background: var(--surface-1); border: 1px solid var(--border-strong);
  border-radius: 8px; padding: 8px 10px; width: 100%; min-width: 0;
}
/* Reported as "often times I will open the app and then have to pinch to
   zoom out". iOS zooms into any field whose text is under 16px the moment
   it takes focus — the lock screen's password box, every picker, the date
   boxes, the search — and stays zoomed after it has gone. So tapping the
   password box for the password manager opened the app enlarged. Every
   field here was 14px, the search 15px. On anything with a touch screen
   they are 16px, the size iOS leaves alone; the desktop keeps the compact
   size. !important because a more specific rule setting a smaller size
   brings the zoom back without a sound, as .search-field input would have. */
@media (hover: none), (any-pointer: coarse) {
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]),
  select, textarea { font-size: 16px !important; }
}
/* A date box on an iPhone stuck out of its card by its own padding.
   Reported from a phone on 2026-10-04, on Settings' "That balance is
   correct as of": WebKit's iOS theme lays input[type=date] out as
   content-box whatever box-sizing says, and floors its width at the widest
   date the locale prints, so width: 100% was the content alone and the
   20px of padding sat outside it. With its appearance none the theme
   leaves the box to the rules above, and the picker still opens. iOS then
   centres the value and gives an empty box no height, so the value is kept
   at the left and the box keeps a line's height. */
input[type="date"] { -webkit-appearance: none; appearance: none; }
input[type="date"]::-webkit-date-and-time-value { text-align: left; min-height: 1.5em; }
input[type="checkbox"] { width: auto; min-width: 0; accent-color: var(--series-1); }
input[type="file"] { padding: 7px; background: var(--surface-2); }
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 13px; }
/* The payment form hides what only a repeating payment asks (paymentCard),
   and [hidden] needs saying twice against an author display: see .bar-btn. */
.form-grid[hidden], .field[hidden] { display: none; }
.form-row { display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; }

.btn {
  appearance: none; font: inherit; font-size: 14px; font-weight: 550; cursor: pointer;
  border: 1px solid var(--border-strong); background: var(--surface-1); color: var(--text-primary);
  border-radius: 8px; padding: 8px 14px; white-space: nowrap;
}
.btn:hover { background: var(--surface-2); }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn-primary { background: var(--series-1); border-color: var(--series-1); color: #fff; }
.btn-primary:hover { background: color-mix(in srgb, var(--series-1) 88%, #000); }
.btn-danger { color: var(--text-critical); border-color: color-mix(in srgb, var(--status-critical) 45%, transparent); }
.btn-danger:hover { background: var(--wash-critical); }
.btn-sm { font-size: 13px; padding: 5px 10px; }
/* A toggle that is currently on. Pressed state has to survive being read at a
   glance across a list of rows, so it is a fill rather than an outline. */
.btn.is-on { background: var(--wash-series); border-color: color-mix(in srgb, var(--series-1) 45%, transparent); color: var(--series-1); }
/* Budgeted mode is a different question, not a different theme, so the cue is
   a hairline at the top of the page rather than a recolour: present enough
   that a screenshot cannot be mistaken for the real projection, quiet enough
   to live with. */
.btn.is-on:hover { background: color-mix(in srgb, var(--series-1) 16%, transparent); }
.lever-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 8px; }
.lever-note { color: var(--text-secondary); font-size: 13px; flex: 1; min-width: 200px; }
.lever-note b { color: var(--text-primary); font-weight: 600; }
.btn-ghost { border-color: transparent; background: transparent; color: var(--text-secondary); }
.btn-ghost:hover { background: var(--surface-2); color: var(--text-primary); }
.btn-row { display: flex; gap: 9px; flex-wrap: wrap; align-items: center; }
/* A link wearing a button, which is the support link and nothing else. An
   <a> is inline, so vertical padding does nothing to the box until display
   is set, and it arrives underlined and in the link colour. Scoped to a.btn
   so not one existing <button> changes. */
a.btn { display: inline-flex; align-items: center; gap: 7px; text-decoration: none; color: var(--text-primary); }
.btn-icon { width: 17px; height: 17px; flex: none; color: var(--text-muted); }
/* The one action on a panel, rather than one of several in a row. */
.btn-wide { display: block; width: 100%; text-align: center; padding: 11px 14px; }
/* "or use your password", with the hairline running out to both edges. */
.or-rule {
  display: flex; align-items: center; gap: 12px;
  font-size: 12.5px; color: var(--text-secondary);
}
.or-rule::before, .or-rule::after {
  content: ""; flex: 1; height: 1px; background: var(--border);
}

.chip {
  display: inline-flex; align-items: center; gap: 5px; font-size: 12px;
  border: 1px solid var(--border); border-radius: 999px; padding: 2px 9px;
  color: var(--text-secondary); background: var(--surface-2);
}
.chip.is-detected { border-color: color-mix(in srgb, var(--series-1) 40%, transparent); color: var(--series-1); background: var(--wash-series); }
.chip.is-off { opacity: .65; }
/* How old the imported data is. Neutral rather than green when current: a
   badge that is always lit stops being read, and "fine" is the state you
   should not have to notice. */
.chip.is-hidden { display: none; }

.empty { text-align: center; padding: 38px 20px; color: var(--text-muted); }
.empty h3 { color: var(--text-primary); font-size: 15px; margin-bottom: 6px; }
.empty p { margin: 0 auto 14px; max-width: 420px; font-size: 14px; }

.toast {
  position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%) translateY(12px);
  background: var(--text-primary); color: var(--page);
  padding: 10px 16px; border-radius: 8px; font-size: 14px; font-weight: 500;
  box-shadow: var(--shadow); opacity: 0; pointer-events: none;
  transition: opacity .16s ease, transform .16s ease; z-index: 60; max-width: 90vw;
  /* Sized to what it says. Placed at left: 50%, a box with no width can only
     shrink into the half of the screen right of that, so every toast on a
     phone wrapped in a column half the screen wide. */
  width: max-content;
}
.toast.is-visible { opacity: 1; transform: translateX(-50%) translateY(0); }
/* The toast holds text and, for undo, one button.
   pointer-events stays `none` on the toast itself and is turned back on for
   the BUTTON ALONE. The toast is a fixed strip across the bottom of the
   screen; making the whole thing clickable so that a button inside it works
   would swallow every tap on whatever is underneath it for six seconds. */
.toast { display: flex; align-items: center; gap: 14px; }
.toast-action {
  pointer-events: auto; appearance: none; border: 0; background: none; cursor: pointer;
  font: inherit; font-weight: 700; color: inherit; padding: 2px 2px; margin: -2px 0;
  text-decoration: underline; text-underline-offset: 3px; flex: none;
}
.toast-action:hover { opacity: .8; }

/* A LITTLE CELEBRATION (ui.js, confetti): a goal within reach or sooner at
   an import, a card paid off or paid down, a payment back recorded. Three
   dozen pieces burst from the toast that says it, rise, turn and fall. The
   layer is taken down by code (CHEER_MS), never by an animation ending —
   the rule below turns every animation off under reduced motion, and a
   layer waiting on animationend would stay up for ever, the splash's
   lesson — and under reduced motion no piece is made at all. It takes no
   touch: it covers the whole screen for two seconds, over the toast whose
   Undo must still work. */
.cheer { position: fixed; inset: 0; pointer-events: none; z-index: 61; overflow: hidden; }
.cheer-piece {
  position: absolute; width: var(--w, 8px); height: var(--h, 12px); background: var(--c, #0e9fdd); border-radius: 2px;
  transform: translate(-50%, -50%);
  animation: cheer-fly var(--dur, 1.4s) cubic-bezier(.15, .6, .35, 1) var(--delay, 0s) both;
}
@keyframes cheer-fly {
  0%   { transform: translate(-50%, -50%) rotate(0deg); opacity: 1; }
  40%  { transform: translate(calc(-50% + var(--dx) * .7), calc(-50% + var(--up))) rotate(calc(var(--rot) * .4)); opacity: 1; }
  100% { transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--down))) rotate(var(--rot)); opacity: 0; }
}
/* A GOOD MOMENT'S PICTURE (ui.js, cheer and momentArt): in the
   celebration's toast where the party popper stood, and beside the Goals
   card's line for a buffer full for good or everything owed paid off. A
   picture rather than an animation, so reduced motion keeps it. Drawn
   (art.js), so it is a span holding an svg. */
.toast.is-cheer { padding: 8px 16px 8px 8px; gap: 12px; }
.toast-art, .moment-art { display: block; flex: none; }
.toast-art svg, .moment-art svg { display: block; width: 100%; height: 100%; }
/* Line art on nothing since 2026-10-09: the mark's hairline is the text's
   colour around it, the toast's own on a toast. */
.moment-art { color: var(--text-secondary); }
.toast-art { width: 56px; height: 56px; }
.moment-art { width: 48px; height: 48px; }

.note { font-size: 13px; color: var(--text-secondary); background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; }
.note strong { color: var(--text-primary); }
/* What the forecast did with a payment the history disagrees with — under the
   balance tiles, one line each. See settledNote() in ui.js. */
.settled-note { display: grid; gap: 6px; margin-top: 12px; }

/* The what-if lever on the recovery card. Sized for a thumb rather than a
   mouse: the whole point is dragging it, and on the device this app is
   actually read on there is no cursor to be precise with. */
.rate-range { display: block; width: 100%; margin: 10px 0 2px; accent-color: var(--series-1); height: 24px; }
.chart-svg:focus-visible { outline: 2px solid var(--series-1); outline-offset: 2px; }
.rate-range:focus-visible { outline: 2px solid var(--series-1); outline-offset: 3px; border-radius: 4px; }
.mono { font-variant-numeric: tabular-nums; }
.muted { color: var(--text-muted); }
.stack { display: grid; gap: 13px; }
.list-row {
  display: flex; gap: 12px; align-items: center; padding: 11px 0;
  border-bottom: 1px solid var(--border); flex-wrap: wrap;
}
.list-row:last-child { border-bottom: 0; }
/* Candidate rows run to three lines; a centred checkbox floats oddly against them. */
.list-row.is-pick { align-items: flex-start; }
.list-row.is-pick input[type="checkbox"] { margin-top: 4px; }
.list-main { flex: 1; min-width: 180px; }
.list-title { font-weight: 550; }
.list-sub { font-size: 12.5px; color: var(--text-muted); margin-top: 2px; }
.list-sub.is-critical { color: var(--text-critical); }
/* The recheck's button names the schedule and, where it changes it, the
   price: a sentence on a button, so it goes under the sentence it answers
   and wraps there, where on one line it ran past a 320px card. */
.recheck-go { display: block; margin-top: 6px; max-width: 100%; white-space: normal; text-align: left; }
/* A password the manager did not keep. Amber rather than red: nothing is
   lost yet, and the button beside it is the fix. */
.list-sub.is-unverified { color: var(--status-serious); font-weight: 600; }

/* Notifications, in Settings: a row a kind, the switch beside its name and
   when it comes, then where this device stands. Rows rather than a list of
   bare checkboxes, because "Payday" alone does not say what would arrive or
   when, and a switch nobody can predict is not an opt-in. */
.notice-kinds { display: grid; }
.notice-kind {
  display: flex; gap: 11px; align-items: flex-start; padding: 9px 0;
  border-bottom: 1px solid var(--border); cursor: pointer;
}
.notice-kind:last-child { border-bottom: 0; }
.notice-kind input[type="checkbox"] { margin-top: 3px; flex: none; }
.notice-text { display: grid; gap: 1px; min-width: 0; }
.notice-text strong { font-weight: 600; }
.push-device, .push-row { display: grid; gap: 8px; }
.push-line { font-size: 13px; color: var(--text-secondary); }
.list-amt { font-variant-numeric: tabular-nums; font-weight: 600; white-space: nowrap; }

/* An envelope is a name and an amount on one line. Inputs are width:100% by
   default, which makes each one claim a whole row and pushes the button onto a
   third — three lines for two fields. */
.env-row { gap: 9px; flex-wrap: nowrap; }
.env-row .list-main { min-width: 0; }
.env-row input[type="number"] { width: 100px; flex: none; text-align: right; font-variant-numeric: tabular-nums; }
.env-row select, .env-row .bsel { max-width: 150px; flex: none; }
.env-row.is-unsorted { background: var(--wash-warning); border-radius: 8px; padding-left: 8px; padding-right: 8px; }
/* The row a search result landed on: a merchant's, on whichever card holds
   it. The wash goes with the next redraw, which rebuilds the row. */
.list-row.is-found { background: var(--wash-series); border-radius: 8px; padding-left: 8px; padding-right: 8px; }
/* The merchants, on the Budget card since 2026-10-04: the unsorted pile at
   the top, a plain merchant's row under its category in Who you pay, and
   where a vendor on a rhythm is sorted, with its picker, under its levers. */
.sorting-rows { display: grid; min-width: 0; }
.cat-vendors > .merchant-row { border-left: 2px solid var(--border-strong); padding-left: 12px; }
.vendor-sort { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 8px; }
.vendor-sort .list-sub { flex: 1; min-width: 140px; }
.vendor-sort select, .vendor-sort .bsel { max-width: 170px; }
/* How full an envelope is, under its name. A number says how much is left; a
   bar says whether that is a lot, which is the thing you read at a glance. */
.env-bar { display: block; height: 4px; border-radius: 2px; background: var(--surface-2); margin: 6px 0 2px; overflow: hidden; }
/* An envelope's face beside its name, fixed with the name — every envelope
   is one of the categories. One from before the list was fixed may have no
   face; its box keeps its place so the names still line up. The name is
   text, never a field — see envelopeRow(). */
.env-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
.env-head select, .env-head .bsel { flex: 1; min-width: 0; max-width: none; }
.env-name { font-weight: 550; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.env-icon {
  flex: none; display: inline-grid; place-items: center; width: 30px; height: 30px;
  font-size: 18px; line-height: 1; border-radius: 8px; background: var(--surface-2);
}
.env-icon.is-empty { visibility: hidden; }
/* Under an envelope from before the list was fixed: what it is, and the
   choice that folds it into the list. */
.env-move { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 10px; margin-top: 6px; }
/* The sheet's lines are one row a category (.cat-line above): the face and
   the name across the top, and a lever a unit under them, which fits a phone
   at any width. min-width on the list is what stops one long row widening the
   whole card's grid track instead of wrapping. */
.env-list { min-width: 0; }
.env-add { display: grid; gap: 9px; }
.env-name { min-width: 0; }
/* The amount is a slider with its figure above it, not a box to type in.
   The warning line uses the same shape, wider, on the Buffer card and in
   Settings. */
.env-amount { display: grid; gap: 2px; flex: 0 1 190px; min-width: 130px; }
.buffer-slider { max-width: 360px; }
.field-label { font-size: 12.5px; color: var(--text-secondary); font-weight: 500; }
/* A field that takes the whole row of a form grid: the amount control, whose
   joystick wants the width, and the balance. */
.field-wide { grid-column: 1 / -1; }
/* An amount chosen, not typed: see amountControl() in ui.js. The figure and
   which way it goes; the history's own figures as buttons; the slider to get
   near; and the joystick to land it. */
.amt-control { display: grid; gap: 8px; min-width: 0; max-width: 460px; }
.amt-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.amt-figure { font-size: 22px; font-weight: 650; font-variant-numeric: tabular-nums; }
.amt-presets { display: flex; gap: 6px; flex-wrap: wrap; }
.amt-presets[hidden] { display: none; }
.amt-preset { font-variant-numeric: tabular-nums; }
.amt-preset[aria-pressed="true"] { background: var(--wash-series); border-color: color-mix(in srgb, var(--series-1) 45%, transparent); color: var(--series-1); }
.amt-control .amt-range { margin: 2px 0; }
/* The joystick: a knob that springs back to the middle of its track, and the
   further it is held from where the drag began, the faster the figure moves
   (jogPace in ui.js). touch-action is belt and braces — the touchstart claims
   the gesture — and the ticks, taller as they go out, mark where each step ten
   times larger begins. Under reduced motion the blanket rule above takes the
   spring's transition away, and the knob is simply back. */
.amt-jog { position: relative; height: 44px; border-radius: 22px; background: var(--surface-2);
           border: 1px solid var(--border); touch-action: none; user-select: none; -webkit-user-select: none;
           -webkit-touch-callout: none; cursor: grab; }
.amt-jog.is-held { cursor: grabbing; }
.amt-jog:focus-visible { outline: 2px solid var(--series-1); outline-offset: 2px; }
.amt-jog-knob { position: absolute; top: 3px; left: calc(50% - 18px); width: 36px; height: 36px; border-radius: 50%;
                display: grid; place-items: center; background: var(--series-1); color: #fff;
                font-size: 11px; font-weight: 700; letter-spacing: -0.02em; white-space: nowrap;
                font-variant-numeric: tabular-nums; box-shadow: 0 1px 3px rgba(11, 11, 11, .28);
                transition: transform .2s ease-out; pointer-events: none; }
.amt-jog.is-held .amt-jog-knob { transition: none; }
.amt-jog-tick { position: absolute; top: 50%; width: 2px; border-radius: 1px; background: var(--border-strong);
                transform: translate(-50%, -50%); pointer-events: none; }
.amt-jog-end { position: absolute; top: 0; bottom: 0; left: 12px; display: flex; align-items: center;
               color: var(--text-muted); font-size: 18px; font-weight: 600; pointer-events: none; }
.amt-jog-more { left: auto; right: 12px; }
/* An amount, or an envelope's, is also a button, which opens the keypad
   (keyedFigure in ui.js): dressed as the figure it was, with a dotted rule
   under it to say it can be pressed, and none where it cannot be. The reset is :where() so whatever
   class the figure wears — its size, its weight, which way it sits —
   wins over it, wherever that class is in this file. The block padding
   widens what a finger can press without moving anything around it. */
:where(.pad-fig) {
  appearance: none; -webkit-appearance: none; background: none; border: 0; margin: -6px 0; padding: 6px 0;
  font: inherit; font-weight: 650; color: inherit; text-align: inherit; cursor: pointer;
}
/* The same rule under a line that opens what it hides (told() in ui.js),
   so the two marks are one: press it. */
.told-word,
.pad-fig {
  text-decoration-line: underline; text-decoration-style: dotted; text-decoration-thickness: 1.5px;
  text-underline-offset: 4px; text-decoration-color: var(--text-muted); border-radius: 4px;
}
.pad-fig:disabled { cursor: default; text-decoration-line: none; }
.pad-fig:focus-visible { outline: 2px solid var(--series-1); outline-offset: 2px; }
/* WHAT IS TOLD (told() in ui.js): a details whose summary is one line under
   the dotted rule and whose body is the explanation, opened in place. No
   box and no chevron - the rule is the whole affordance, as it is on a
   figure - so the line reads as a line of the card until it is pressed. The
   body is set in from the line by a hairline, so what was opened is told
   apart from what was there. The summary is the width of its words, so a
   press beside them presses nothing: fit-content, not max-content, which a
   grid track sizing to its content read as the line's whole length, so a
   long line in a grid cell ran off the screen instead of wrapping (the
   coaching's, 2026-10-11). */
.told { margin: 0; min-width: 0; }
.told > summary {
  list-style: none; cursor: pointer; width: fit-content; max-width: 100%;
  font-size: 13.5px; line-height: 1.4; color: var(--text-secondary); border-radius: 4px;
}
.told > summary::-webkit-details-marker { display: none; }
.told > summary:focus-visible { outline: 2px solid var(--series-1); outline-offset: 2px; }
.told-body { display: grid; gap: 8px; margin: 8px 0 2px; padding-left: 10px; border-left: 2px solid var(--border-strong); min-width: 0; }
.told-text { margin: 0; font-size: 13.5px; line-height: 1.45; color: var(--text-secondary); max-width: 64ch; }
.told-body .card-sub, .told-body .note { margin: 0; }
.banner .told > summary { font-size: 14px; margin-top: 3px; }
.banner .told-text { font-size: 14px; }
.list-main .told > summary, .list-main .told-text { font-size: 12.5px; }
.list-main .told { margin-top: 2px; }
.goal-rec .told > summary, .goal-rec .told-text { color: inherit; font-size: inherit; }
.fut-line .told > summary { color: var(--text-primary); font-size: 14px; }
.note .told > summary, .note .told-text { font-size: 13px; }
/* THE KEYPAD (numberPad in ui.js): a 10-key across the foot of the screen,
   opened by pressing an amount or an envelope's figure and never otherwise. A sheet at the foot
   rather than a box in the middle, because that is where a thumb is, and
   the figure being keyed is shown in it. Over a dialog, since a dialog's
   amount opens it too, and under the opening tile. Put up and taken down
   by the code; the slide in is only the look of it, and under reduced
   motion it is simply there. */
.pad-scrim {
  position: fixed; inset: 0; z-index: 85; background: rgba(11, 11, 11, .32);
  display: flex; align-items: flex-end; justify-content: center;
}
.pad {
  width: 100%; max-width: 420px; background: var(--surface-1);
  border: 1px solid var(--border-strong); border-bottom: 0; border-radius: 16px 16px 0 0;
  box-shadow: 0 -8px 32px rgba(0, 0, 0, .22);
  padding: 10px 12px calc(12px + env(safe-area-inset-bottom, 0px));
  animation: pad-in .16s ease-out;
}
.pad:focus { outline: none; }
@keyframes pad-in { from { transform: translateY(24px); opacity: 0; } to { transform: none; opacity: 1; } }
.pad-head { display: flex; align-items: center; gap: 10px; }
.pad-what { flex: 1; min-width: 0; font-size: 13.5px; color: var(--text-secondary); }
.pad-signs { margin-top: 6px; }
.pad-display {
  text-align: right; padding: 2px 4px 0; min-height: 48px; overflow-wrap: anywhere;
  font-size: 34px; font-weight: 650; font-variant-numeric: tabular-nums; line-height: 1.35;
}
/* The figure as it stood, shown as selected text is: a key replaces it. */
.pad-display.is-fresh .pad-shown { background: var(--wash-series); border-radius: 6px; }
.pad-display.is-empty .pad-shown { color: var(--text-muted); }
.pad-max { min-height: 18px; padding: 0 4px; text-align: right; font-size: 12.5px; color: var(--text-muted); }
.pad-max.is-over { color: var(--text-critical); font-weight: 600; }
/* 7 8 9 across the top, as an adding machine's are, with Done two keys
   tall at the right and 0 two keys wide at the left of the bottom row.
   Those three are placed outright; the rest flow round them. */
.pad-keys { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; margin-top: 8px; }
.pad-key {
  appearance: none; -webkit-appearance: none; min-height: 52px; padding: 0; border-radius: 10px;
  border: 1px solid var(--border); background: var(--surface-2); color: var(--text-primary);
  font: inherit; font-size: 22px; font-weight: 550; font-variant-numeric: tabular-nums;
  cursor: pointer; display: grid; place-items: center; user-select: none; -webkit-user-select: none;
}
.pad-key:active { background: var(--wash-series); }
.pad-key:focus-visible { outline: 2px solid var(--series-1); outline-offset: 2px; }
.pad-key.is-fn { font-size: 15px; font-weight: 600; color: var(--text-secondary); }
.pad-icon { width: 24px; height: 24px; }
.pad-key.is-done { grid-column: 4; grid-row: 3 / span 2; background: var(--series-1); border-color: var(--series-1);
                   color: #fff; font-size: 17px; font-weight: 650; }
.pad-key.is-zero { grid-column: 1 / span 2; grid-row: 4; }
.pad-key.is-point { grid-column: 3; grid-row: 4; }
.balance-field { display: grid; gap: 6px; }
/* The plan, set on the Balance card (planEditor in ui.js). Shut, it is one
   line under the tiles, saying the plan if there is one; open, the whole card
   above it shows the draft. A box rather than a link-sized summary, because
   it is where a plan is made, not help about one. */
.plan-edit { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface-1); }
.plan-edit > summary { display: flex; align-items: center; gap: 10px; padding: 11px 14px; cursor: pointer;
                       font-size: 14px; font-weight: 600; list-style: none; }
.plan-edit > summary::-webkit-details-marker { display: none; }
.plan-edit > summary::after { content: '›'; margin-left: auto; color: var(--text-muted); font-size: 18px;
                              line-height: 1; transition: transform .15s ease-out; }
.plan-edit[open] > summary::after { transform: rotate(90deg); }
.plan-edit-fig { color: var(--series-1); font-variant-numeric: tabular-nums; }
/* Why Safe is what it is, folded under the Forecast card's levels: shut,
   one line, like the plan editor's summary. The day-by-day table under the
   chart (.day-table) is the same kind of line, since the Days card folded
   into the Forecast card. */
.why-safe,
.day-table { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface-1); }
.why-safe > summary,
.day-table > summary { padding: 11px 14px; cursor: pointer; font-size: 14px; font-weight: 600; list-style: none;
                      display: flex; align-items: center; gap: 10px; }
.why-safe > summary::-webkit-details-marker,
.day-table > summary::-webkit-details-marker { display: none; }
.why-safe > summary::after,
.day-table > summary::after { content: '›'; margin-left: auto; color: var(--text-muted); font-size: 18px;
                             line-height: 1; transition: transform .15s ease-out; }
.why-safe[open] > summary::after,
.day-table[open] > summary::after { transform: rotate(90deg); }
.day-table-n { color: var(--text-muted); font-size: 13px; font-weight: 400; }
.day-table .filterbar { margin: 8px 0 12px; }
.why-safe-body { padding: 2px 14px 14px; }
/* The transactions fold's row of controls (Period card): the scope pair, the
   filter and the export, wrapping at phone width with the filter taking what
   is left. */
.tx-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.tx-controls input[type=search] { flex: 1 1 140px; min-width: 0; }
/* A line that goes to where something is done on another card: a payment is
   added on the Period card, from the Bills card (payElsewhere). One box, one
   line, a chevron, as the plan's is. */
.go-elsewhere { display: flex; align-items: center; gap: 10px; width: 100%; padding: 11px 14px;
  border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface-1);
  font: inherit; font-size: 14px; font-weight: 600; color: inherit; text-align: left; cursor: pointer; flex-wrap: wrap; }
.go-elsewhere::after { content: '›'; margin-left: auto; color: var(--text-muted); font-size: 18px; line-height: 1; }
.go-elsewhere-where { color: var(--text-muted); font-size: 13px; font-weight: 400; }

.plan-edit-fig.is-bad { color: var(--text-critical); }
.plan-edit-body { display: grid; gap: 10px; padding: 2px 14px 14px; }
/* On the Forecast card it is always open, with no summary above the body. */
.plan-edit.is-inline > .plan-edit-body { padding-top: 14px; }
/* Up to five rungs are a ladder, not a paragraph: two even columns, the
   figure under its name, so the verdict the draft redraws stays near. */
.plan-edit .amt-presets:not([hidden]) { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.plan-edit .amt-preset { display: grid; gap: 1px; text-align: left; white-space: normal; padding: 6px 10px; }
.plan-edit .amt-preset-label { font-size: 12px; font-weight: 500; color: var(--text-muted); }
.plan-edit .amt-preset[aria-pressed="true"] .amt-preset-label { color: inherit; }
.plan-edit .amt-preset-fig { font-size: 14px; }
/* The draft's own weather, beside the figure, so it is on the screen while a
   thumb is on the joystick; the strip at the top of the card is not. */
.plan-edit-sky { display: flex; align-items: center; gap: 12px; }
.plan-edit-sky .sky-art { width: 80px; }
/* The plan at the top of the Budget card, whose envelopes move it: the
   figure, what a drag has moved it by, and its weather beside it. See
   budgetPlanLine() in ui.js. */
/* Directly above the envelopes, so what a drag does is beside the slider
   doing it. The painting and the figure on one line; how the two move
   together under both, full width, in small type. */
.plan-line { display: grid; gap: 10px; padding: 12px 14px;
             border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface-1); }
.plan-line-top { display: flex; align-items: center; gap: 12px; }
.plan-line .sky-art { width: 72px; }
.plan-line-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 8px; }
.plan-line-k { font-size: 14px; font-weight: 600; }
.plan-line-fig { font-size: 17px; font-weight: 650; color: var(--series-1); font-variant-numeric: tabular-nums; }
.plan-line-fig.is-bad { color: var(--text-critical); }
.plan-line-moved { font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums; }
.plan-line-moved:empty { display: none; }
.plan-line-moved.is-up { color: var(--text-critical); }
.plan-line-moved.is-down { color: var(--text-good); }
.plan-line .sky-label { font-size: 14px; margin-top: 2px; }
.plan-line-how { font-size: 13px; color: var(--text-secondary); }
@media (max-width: 380px) { .plan-line .sky-art { width: 60px; } }
/* What you are saving for: a row a rung — the buffer, then each goal on top
   of it — with its face, when it can be had, and the next step down. The
   words take the middle column and wrap there, so a long date never pushes
   the amount off a phone; the buttons go under the words. */
.goal-list { display: grid; min-width: 0; }
.goal-row {
  display: grid; grid-template-columns: 30px minmax(0, 1fr) auto; gap: 4px 10px;
  align-items: start; padding: 11px 0; border-bottom: 1px solid var(--border);
}
.goal-row:last-child { border-bottom: 0; }
/* The buffer's detail folds under its own row (what was the Recovery card),
   so the row's rule is not drawn between the two: the fold is a box of its
   own, and space sets it off from the row after it. */
.goal-row.has-fold { border-bottom: 0; padding-bottom: 6px; }
.goal-list > .recovery-fold { margin-bottom: 11px; }
/* The buffer set on its own row (bufferEditor), under it like the fold:
   what a buffer is, what the suggestion is sized on, the slider and the
   answers, spaced as a stack. */
.buffer-edit { display: grid; gap: 10px; margin: 2px 0 12px; }
.buffer-edit .buffer-slider { max-width: none; }
.goal-face {
  display: inline-grid; place-items: center; width: 30px; height: 30px;
  font-size: 18px; line-height: 1; border-radius: 8px; background: var(--surface-2);
}
.goal-main { display: grid; gap: 2px; min-width: 0; }
.goal-name { font-weight: 550; }
.goal-when { font-size: 13px; color: var(--text-secondary); }
.goal-when.is-good { color: var(--text-good); font-weight: 600; }
.goal-when.is-far { color: var(--text-muted); }
/* A goal with a target date that is short for it, or missed it. */
.goal-when.is-late { color: var(--status-serious); }
/* What an as-soon-as-I-can goal goes ahead of, or what a dated one sits on. */
.goal-warn { font-size: 12.5px; color: var(--text-secondary); }
/* The warning's triangle, in the page's line glyphs, where ⚠️ was. */
.goal-warn-icon { width: 14px; height: 14px; margin-right: 5px; vertical-align: -2px; color: var(--status-serious); }
.goal-warn:empty { display: none; }
.goal-next { font-size: 12.5px; color: var(--text-muted); }
.goal-amt { font-variant-numeric: tabular-nums; font-weight: 600; white-space: nowrap; padding-top: 5px; }
/* The buffer against the recommendation (bufferStanding): the figure and
   the line under its date take one colour each — below it red, at it green,
   above it the app's blue, which is a fact and not a verdict — and the line
   says what the colour means. The editor's figure takes the same colour as
   the thumb moves. */
.goal-rec { font-size: 12.5px; color: var(--text-secondary); }
.goal-rec:empty { display: none; }
.goal-amt.is-below, .goal-amt-inline.is-below, .goal-rec.is-below, .buffer-edit .env-amt.is-below { color: var(--text-critical); }
.goal-amt.is-at, .goal-amt-inline.is-at, .goal-rec.is-at, .buffer-edit .env-amt.is-at { color: var(--text-good); }
.goal-amt.is-above, .goal-amt-inline.is-above, .goal-rec.is-above, .buffer-edit .env-amt.is-above { color: var(--series-1); }
.goal-amt-inline { font-variant-numeric: tabular-nums; font-weight: 600; }
.goal-actions { grid-column: 2 / -1; display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
.goal-form {
  display: grid; gap: 9px; padding: 12px; min-width: 0;
  border: 1px solid var(--border); border-radius: 10px; background: var(--surface-2);
}
.goal-kinds { display: flex; flex-wrap: wrap; gap: 6px; }
.goal-kind { display: inline-flex; align-items: center; gap: 6px; }
.goal-kind[aria-pressed="true"] { background: var(--wash-series); border-color: color-mix(in srgb, var(--series-1) 45%, transparent); color: var(--series-1); }
.goal-kind-face { font-size: 15px; line-height: 1; }
.goal-preview, .goal-how { font-size: 13px; color: var(--text-secondary); }
/* CAN I AFFORD THIS? (affordCard in ui.js, the Afford card since
   2026-10-09): what it is for, how much and how often, then two answers in
   order — its envelope, then the goals — each a verdict in a few words and
   a line under it, with the sky with the money gone and what it does to
   every goal (the habits' own lines). */
/* [hidden] said twice: an author display beats the attribute, so the sky
   with the money gone (.plan-edit-sky) stayed up over an answer that fits
   in its envelope, and the way to the Budget card (.go-elsewhere) under an
   envelope worked out (found driving it). Everything the card hides and
   shows is under this. */
.afford-box [hidden] { display: none; }
.afford-step { margin-top: 6px; font-size: 12px; font-weight: 650; letter-spacing: .04em; text-transform: uppercase; color: var(--text-muted); }
.afford-answer { display: grid; gap: 3px; }
.afford-verdict { font-weight: 650; font-size: 15px; }
.afford-verdict.is-good { color: var(--text-good); }
.afford-verdict.is-dip { color: var(--status-serious); }
.afford-verdict.is-late { color: var(--text-critical); }
.afford-says { font-size: 13px; color: var(--text-secondary); }
.afford-goals .habit-gets { margin-top: 0; }

/* How a period went (periodReviewCard in ui.js): the sky beside the net
   result, the forecast's chart locked to the period and kept small, the
   story as lines, and the coaching fold, which takes the why-safe fold's
   look. */
.pr-head { display: flex; align-items: center; gap: 12px; }
.pr-head .sky-art { width: 72px; }
.pr-fig { min-width: 0; flex: 1; }
.pr-fig .kpi-value { font-size: 23px; }
.pr-fig .sky-label { font-size: 14px; margin-top: 4px; }
.pr-fig .sky-say { font-size: 13px; }
.pr-sky { font-size: 18px; line-height: 1; }
.pr-chart { margin-top: 2px; }
.pr-lines { margin: 0; padding-left: 18px; display: grid; gap: 5px; font-size: 13.5px; color: var(--text-secondary); }
.pr-lines li::marker { color: var(--text-muted); }
.coach-line { display: grid; gap: 6px; padding: 8px 0; border-top: 1px solid var(--border); font-size: 13.5px; }
.coach-line:first-child { border-top: 0; padding-top: 0; }
.coach-line.is-good .coach-text { color: var(--text-good); }
.coach-line .btn { justify-self: start; }
/* A coaching line's graph: a bar a period, the dates under them, and what a
   bar is said over them; or, for a history out of date, a strip of days. */
.coach-graph { margin: 0; display: grid; gap: 4px; min-width: 0; }
.coach-says { font-size: 12px; color: var(--text-muted); }
.coach-dates { display: grid; font-size: 11px; color: var(--text-muted); text-align: center; }
.coach-dates span { overflow: hidden; white-space: nowrap; }
.coach-strip { display: flex; gap: 2px; height: 14px; border-radius: 4px; overflow: hidden; }
.coach-seg { flex-basis: 0; min-width: 6px; }
.coach-seg.is-known, .coach-key.is-known::before { background: var(--series-1); }
.coach-seg.is-missing, .coach-key.is-missing::before { background: repeating-linear-gradient(135deg, var(--status-critical) 0 3px, transparent 3px 6px); }
.coach-seg.is-ahead, .coach-key.is-ahead::before { background: var(--surface-2); box-shadow: inset 0 0 0 1px var(--axis); }
.coach-keys { display: flex; flex-wrap: wrap; gap: 2px 12px; font-size: 11.5px; color: var(--text-muted); }
.coach-key::before { content: ''; display: inline-block; width: 9px; height: 9px; border-radius: 2px; margin-right: 5px; vertical-align: -1px; }
.coach-act { display: grid; gap: 6px; justify-items: start; min-width: 0; }
.coach-act > .merchant-row { justify-self: stretch; }
.coach-done { display: flex; gap: 6px; align-items: center; color: var(--text-good); font-size: 13px; }
.coach-done-icon { width: 15px; height: 15px; flex: none; }
/* Spending on the Period card: planned against spent, a category at a time. */
.spend-fig { font-size: 14px; color: var(--text-secondary); }
.spend-fig strong { color: var(--text-primary); font-size: 16px; }
.spend-fig.is-over strong { color: var(--text-critical); }
.spend-ends { display: flex; justify-content: space-between; font-size: 11px; color: var(--text-muted); margin-top: -4px; }
.spend-rows { gap: 0; }
.spend-row { display: grid; gap: 4px; padding: 8px 0; border-top: 1px solid var(--border); min-width: 0; }
.spend-row:first-child { border-top: 0; padding-top: 0; }
.spend-head { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 8px; align-items: center; font-size: 13.5px; }
.spend-badge.badge { width: 22px; height: 22px; }
.spend-badge.badge:empty { box-shadow: inset 0 0 0 1px var(--axis); }
.spend-name { overflow-wrap: anywhere; }
.spend-amt { font-variant-numeric: tabular-nums; color: var(--text-secondary); font-size: 12.5px; text-align: right; }
.spend-row.is-over .spend-amt { color: var(--text-critical); }
@media (max-width: 380px) { .pr-head .sky-art { width: 60px; } .pr-fig .kpi-value { font-size: 21px; } }

/* Where what you do now is heading (futureSection in ui.js, on the Goals
   card): the two ends, the chart, the switches, the lines and the folds.
   The fold takes the why-safe look; the switches are rows a thumb can hit. */
.fut-fold { min-width: 0; }
.fut-body > * { min-width: 0; }
/* The match's answers are sentences on buttons, which do not wrap: let
   these wrap, since six of them in a row at 320px is the one place a label
   is longer than the screen. */
.fut-line .goal-kinds .goal-kind { white-space: normal; text-align: left; }
.fut-fold > summary { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 10px; }
.fut-sum-figs { font-weight: 400; font-size: 13px; color: var(--text-secondary); }
.fut-ends { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.fut-end .kpi-value { font-size: 21px; }
.fut-asis { color: var(--text-secondary); }
.fut-chart { margin-top: 2px; }
.fut-legend { display: flex; gap: 14px; font-size: 12px; color: var(--text-secondary); margin-top: 2px; }
.fut-key::before { content: ''; display: inline-block; width: 18px; height: 0; margin-right: 6px; vertical-align: middle; border-top: 2px solid var(--series-1); }
.fut-key-asis::before { border-top-style: dashed; border-top-color: var(--text-secondary); }
.fut-tweaks { display: grid; gap: 6px; }
.fut-tweak { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 44px; padding: 8px 12px; text-align: left;
  border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface-1); color: var(--text-primary); font: inherit; font-size: 14px; cursor: pointer; }
.fut-tweak.is-on { border-color: color-mix(in srgb, var(--series-1) 45%, transparent); background: var(--wash-series); }
.fut-tweak-box { width: 18px; height: 18px; flex: none; }
/* A payment's tick on the sheet (tickMark), where ✓ and ○ were. */
.tick-mark { width: 16px; height: 16px; display: block; }
.fut-tweak-label { flex: 1; min-width: 0; }
.fut-gain { flex: none; font-weight: 650; font-variant-numeric: tabular-nums; color: var(--text-muted); }
.fut-gain.is-good { color: var(--text-good); }
.fut-gain.is-bad { color: var(--text-critical); }
.fut-line { display: grid; gap: 5px; padding-top: 10px; border-top: 1px solid var(--border); }
.fut-line-title { font-weight: 650; font-size: 14px; }
.fut-line-text { font-size: 13.5px; color: var(--text-secondary); }
.fut-actions { margin-top: 2px; }
/* A lever's words may wrap, as the match's answers may: set in by the told
   body's rule, a button of a sentence ran a pixel past a 320px screen. */
.fut-actions .btn { white-space: normal; text-align: left; }
.fut-line { min-width: 0; }
.fut-sub { margin-top: 4px; }
.fut-asm-row { display: grid; gap: 2px; }
.fut-asm-head { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; font-size: 14px; }
.fut-asm-label { font-weight: 600; }
.fut-asm-fig { font-variant-numeric: tabular-nums; color: var(--series-1); font-weight: 650; }
.fut-asm-row .rate-range { margin: 4px 0 0; }
.fut-links { margin: 0; padding-left: 18px; display: grid; gap: 6px; font-size: 14px; }
.fut-disclaimer { font-size: 12px; line-height: 1.45; color: var(--text-muted); border-top: 1px solid var(--border); padding-top: 10px; }
@media (max-width: 340px) { .fut-ends { grid-template-columns: 1fr; } }
/* A buffer that is full and stays full: a line, not a goal (goalsCard). */
/* The way to the sample from the lock screen (showSample): under the way in,
   quiet, because unlocking is what the screen is for. */
/* THE LOCK OPENS ON THE PICTURE (2026-10-07; lockPanel): the clear day,
   drawn wide across the top of the card. 5:2 exactly, the wide drawing's
   own shape (scene.js), and the card held to 560px so a desk does not get
   a picture half a metre wide. Line art on nothing since 2026-10-09 — the
   sun over the mark's outline — so it sits inside the card's padding
   like everything else on it; it ran out to the edges while it was a
   picture with a sky. */
.lock-card { max-width: 560px; margin: 0 auto; }
.lock-art { position: relative; aspect-ratio: 5 / 2; margin: 0 0 12px; color: var(--text-secondary); }
.lock-art svg { display: block; width: 100%; height: 100%; }
.lock-sample { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; margin-top: 6px;
  padding-top: 12px; border-top: 1px solid var(--border); }
/* A buffer full for good: one line under the goals, with Change. A grid
   rather than a wrapping flex, or at phone width the sentence wraps as a
   whole onto a line of its own, away from its umbrella. Its buttons go
   under the sentence, as a goal row's do, since Use the recommended
   figure joined Change (2026-10-11): two beside the sentence left it a
   word a line at 320. */
.goal-done {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center;
  gap: 4px 8px; padding-top: 10px; font-size: 13px; color: var(--text-secondary);
}
.goal-done-btns { grid-column: 2 / -1; display: flex; flex-wrap: wrap; gap: 6px; }
/* PAYING OFF WHAT YOU OWE (debtsSection in ui.js), on the Goals card: the
   suggestion, the walk through it in three steps, and the plan as it
   stands. A box like the goal form's, so it reads as the card's own; the
   steps a row of three words, the one being done in the series colour. */
.debt-box {
  display: grid; gap: 10px; padding: 12px; margin-top: 12px; min-width: 0;
  border: 1px solid var(--border); border-radius: 10px; background: var(--surface-2);
}
.debt-head { font-weight: 650; display: flex; align-items: center; gap: 8px; }
.debt-head-badge.badge { width: 22px; height: 22px; }
.debt-steps { display: flex; flex-wrap: wrap; gap: 4px 14px; margin: 0; padding: 0; list-style: none;
  font-size: 13px; color: var(--text-muted); counter-reset: debt-step; }
.debt-steps li { counter-increment: debt-step; }
.debt-steps li::before { content: counter(debt-step) ". "; }
.debt-steps li.is-on { color: var(--series-1); font-weight: 650; }
/* The options walk under the overdraft (optionsWalk in ui.js) is the debt
   box's shape; a pending payment's two answers sit under its words. */
.risk-walk { margin-top: 0; }
.risk-walk .walk-pending { padding: 8px 0; }
.risk-walk .walk-pending .btn-row { margin-top: 6px; }
.debt-row { display: grid; gap: 8px; padding: 10px 0; border-top: 1px solid var(--border); min-width: 0; }
.debt-row-head { display: grid; grid-template-columns: 30px minmax(0, 1fr); gap: 10px; align-items: start; }
.debt-row-head .goal-name { overflow-wrap: anywhere; }
.debt-owed { display: grid; gap: 4px; min-width: 0; }
.debt-done { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.debt-row-btns { margin-top: 0; }
/* .btn sets its own display, which beats [hidden]; see .bar-btn above. */
.debt-plan .btn[hidden] { display: none; }
.debt-need { color: var(--status-serious); }
.debt-need:empty { display: none; }
.debt-choices { display: grid; gap: 8px; }
.debt-choice {
  display: grid; gap: 3px; text-align: left; padding: 10px 12px; font: inherit; color: inherit; cursor: pointer;
  border: 1px solid var(--border-strong); border-radius: 10px; background: var(--surface-1);
}
.debt-choice[aria-pressed="true"] { border-color: var(--series-1); background: var(--wash-series); }
.debt-choice[aria-pressed="true"] strong { color: var(--series-1); }
.debt-why { display: grid; gap: 6px; padding: 10px 12px; border-radius: 10px; background: var(--surface-1);
  border-left: 3px solid var(--series-1); }
.debt-why ul { margin: 0; padding-left: 18px; display: grid; gap: 4px; font-size: 14px; }
.debt-one { display: grid; grid-template-columns: 30px minmax(0, 1fr); gap: 10px; align-items: start; }
/* A CARD'S PAYMENT INFORMATION BOX (statementBox in ui.js), drawn as a
   statement prints it so its figures are copied across by place: the title
   bar, the New Balance beside the Minimum Payment Due, and the warning's
   table. Every figure is a button; one not yet set is dashed, and the one
   open wears the series ring. Its control opens under the box. */
/* One column that nothing in it can widen: a button that does not wrap
   ("Mine says to pay a promotional balance by a date" is 387px) took the
   whole box 80px past the card at 390px, from the day it was added. */
.stmt-wrap { display: grid; grid-template-columns: minmax(0, 1fr); gap: 6px; min-width: 0; }
.stmt { border: 1px solid var(--border-strong); border-radius: 8px; overflow: hidden; background: var(--surface-1); min-width: 0; }
.stmt-title { background: var(--text-primary); color: var(--page); font-weight: 650; text-align: center; padding: 6px 8px; font-size: 14px; }
.stmt-two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); border-bottom: 1px solid var(--border); }
.stmt-field { display: grid; gap: 3px; padding: 8px 10px; min-width: 0; align-content: start; }
.stmt-field + .stmt-field { border-left: 1px solid var(--border); }
.stmt-k { font-size: 12px; line-height: 1.3; color: var(--text-secondary); }
.stmt-warn { padding: 8px 10px 4px; font-size: 12px; font-weight: 650; }
.stmt-table {
  display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr) minmax(0, 1fr);
  margin: 0 10px 10px; border: 1px solid var(--border); border-radius: 4px; align-items: stretch;
}
.stmt-table > * { padding: 5px 6px; border-top: 1px solid var(--border); min-width: 0; font-size: 12px; line-height: 1.3; }
.stmt-table > :nth-child(-n + 3) { border-top: 0; }
.stmt-th { background: var(--surface-2); color: var(--text-secondary); }
.stmt-td { align-self: center; }
.stmt-table > .stmt-cell { margin: 4px; padding: 3px 5px; border-top: 1px dashed var(--border-strong); }
.stmt-table > .stmt-cell.is-set { border-top: 1px solid var(--border); }
.stmt-apr { display: grid; gap: 3px; padding: 0 10px 10px; }
.stmt-cell {
  appearance: none; font: inherit; font-size: 15px; font-weight: 650; font-variant-numeric: tabular-nums;
  color: var(--series-1); text-align: left; cursor: pointer; min-height: 34px; overflow-wrap: normal;
  padding: 3px 6px; border: 1px dashed var(--border-strong); border-radius: 6px; background: var(--surface-1);
}
.stmt-cell.is-set { color: var(--text-primary); border-style: solid; border-color: var(--border); }
.stmt-cell[aria-expanded="true"] { border: 1px solid var(--series-1); box-shadow: 0 0 0 2px var(--series-1); }
.stmt-table > .stmt-cell { font-size: 13.5px; }
.stmt-says:empty { display: none; }
.stmt-toggle { justify-self: start; white-space: normal; text-align: left; }
.stmt-edit { display: grid; gap: 6px; padding: 10px 12px; border-radius: 8px; background: var(--wash-series);
  border-left: 3px solid var(--series-1); min-width: 0; }
.stmt-edit-k { font-weight: 650; }
.stmt-edit .btn-row { margin-top: 2px; }
/* A promotion's sentence, as the statement prints it, under the box: its
   two figures are buttons in the run of the words. */
.stmt-promo { padding: 8px 10px; border: 1px solid var(--border-strong); border-radius: 8px; background: var(--surface-1);
  font-size: 13px; line-height: 2.2; min-width: 0; }
.stmt-promo .stmt-cell { display: inline-block; min-height: 30px; padding: 1px 6px; font-size: 14px; line-height: 1.6; vertical-align: baseline; }
.stmt-date { font: inherit; padding: 6px 8px; border: 1px solid var(--border-strong); border-radius: 6px;
  background: var(--surface-1); color: var(--text-primary); justify-self: start; }
/* Whether a promotion is paid off before its date, under the goal's. */
.goal-due { font-size: 13px; color: var(--text-secondary); }
.goal-due.is-late { color: var(--status-serious); }
/* A figure off a statement, typed (typedFigure in ui.js): the field is as
   wide as the longest figure a statement prints, with its unit beside it,
   so it reads as the figure it is rather than as a form. */
.stmt-typed-row { display: grid; gap: 4px; min-width: 0; }
.stmt-typed-box { display: flex; align-items: center; gap: 6px; min-width: 0; }
.stmt-typed { width: 12ch; flex: 0 1 auto; font-variant-numeric: tabular-nums; font-weight: 650; }
.stmt-unit { color: var(--text-secondary); font-size: 14px; white-space: nowrap; }
.stmt-typed[aria-invalid="true"] { border-color: var(--status-serious); }
.stmt-bad { color: var(--status-serious); }
.stmt-bad:empty { display: none; }
/* Pasting the box (stmtPaste): a button until it is asked for, then how to
   copy the box off a phone or a PDF, and where to put it. */
.stmt-paste { display: grid; gap: 6px; min-width: 0; }
.stmt-paste-btn { justify-self: start; }
.stmt-paste.is-open { padding: 10px 12px; border-radius: 8px; background: var(--wash-series); border-left: 3px solid var(--series-1); }
.stmt-paste.is-open > .btn { justify-self: start; }
.stmt-how { margin: 0; padding-left: 20px; display: grid; gap: 4px; font-size: 13px; color: var(--text-secondary); line-height: 1.45; }
.stmt-read { color: var(--text-primary); }
/* A figure never breaks inside itself: at phone width the table's cells
   give up their margin and a little of their type first, and under 340px
   the New Balance and the minimum stand one above the other. */
@media (max-width: 380px) {
  .stmt-table > * { padding: 4px; }
  .stmt-table > .stmt-cell { margin: 2px; padding: 3px 4px; font-size: 12.5px; }
}
@media (max-width: 340px) {
  .stmt-two { grid-template-columns: minmax(0, 1fr); }
  .stmt-field + .stmt-field { border-left: 0; border-top: 1px solid var(--border); }
}

/* Who added something, in a budget more than one person is in (addedBy in
   ui.js): small, under what it is about. */
.added-by { display: block; font-size: 12px; color: var(--text-muted); margin-top: 2px; }
.goal-preview:empty { display: none; }
/* The household (householdCard in ui.js): what is owed, the one figure,
   first and large; then the bills, the goals and the people, each under a
   heading like the Habits card's sections. */
.hh-tally { display: grid; gap: 2px; }
.hh-tally-figure { font-size: 20px; font-weight: 650; font-variant-numeric: tabular-nums; }
.hh-tally.is-owed .hh-tally-figure { color: var(--text-good); }
/* Nothing owed is a fact, not a headline. */
.hh-tally:not(.is-owed):not(.is-owing) .hh-tally-figure { font-size: 16px; font-weight: 550; }
.hh-tally-sub, .hh-offer-text { font-size: 13.5px; color: var(--text-secondary); }
/* A payment picked from the ledger, or a bill from what repeats: a column of
   buttons, the pressed one marked as the goal kinds are. */
.hh-picks { display: grid; gap: 6px; }
.hh-pick { justify-content: flex-start; text-align: left; white-space: normal; }
.hh-pick[aria-pressed="true"] { background: var(--wash-series); border-color: color-mix(in srgb, var(--series-1) 45%, transparent); color: var(--series-1); }
.hh-split { max-width: 360px; }
.hh-history > summary { cursor: pointer; font-size: 13px; color: var(--text-secondary); padding: 4px 0; }
.hh-offer { display: grid; gap: 8px; border-top: 1px solid var(--border); padding-top: 12px; }
/* A payment back the ledger already has (hhSeen in ui.js): what it looks
   like, the row itself, and the two answers under it. */
.hh-seen { display: grid; gap: 6px; }
.hh-seen + .hh-seen { border-top: 1px dashed var(--border); padding-top: 8px; }
.hh-seen strong { display: block; color: var(--text-primary); font-weight: 600; }
.hh-seen-row { display: block; overflow-wrap: anywhere; }
/* The Habits card in two sections: each category by days a month and how
   much a time, then what is on a rhythm. A category's two sliders sit on a
   grid, the word, the slider and the figure, so the two line up. */
/* A ledger row's category: where it is sorted, with the bank's own word
   under it when that says something else. */
.tx-cat { display: inline-grid; gap: 2px; justify-items: start; }
.tx-cat-bank { font-size: 11px; color: var(--text-muted); }
/* Which way everyday spending is running, at the top of the card
   (spendingLately in ui.js): one line, the recent figure in bold. */
.habit-lately { font-size: 14px; color: var(--text-secondary); margin: 0 0 12px; display: grid; gap: 4px; }
.habit-lately strong { color: var(--text-primary); font-weight: 600; font-variant-numeric: tabular-nums; }
.habit-lately-way { white-space: nowrap; }
/* The same heading, over the people you plan with in Settings'
   notifications (newsBlock), and over the two ways an assistant connects
   (assistantsCard). */
.habit-section-head, .news-head, .sub-head { display: grid; gap: 2px; margin: 4px 0 2px; }
.habit-section-head h3, .news-head h3, .sub-head h3 { font-size: 15px; font-weight: 650; margin: 0; }
.habit-section-head .card-sub, .sub-head .card-sub { color: var(--text-muted); font-size: 13px; }
.habit-cats { display: grid; margin-bottom: 14px; min-width: 0; }
/* ONE ROW A CATEGORY (categoryLine), on the Budget card: the face and the
   name across the top with what it costs a day, then a lever a unit — this
   period's envelope, days a month, a time — each a label, a slider and its
   figure, so the three read as one kind of thing. The envelope's bar and
   what has gone out of it sit under its slider. */
.cat-line { display: block; }
.cat-line-head { display: flex; gap: 10px; align-items: flex-start; }
.cat-line-head .list-main { min-width: 0; flex: 1; }
.cat-line-head .env-name { display: block; line-height: 30px; }
.cat-line-head .list-amt { line-height: 30px; }
.cat-lever {
  display: grid; grid-template-columns: 96px minmax(0, 1fr) minmax(72px, auto); gap: 10px;
  align-items: center; margin-top: 8px;
}
.cat-lever-k { font-size: 13px; color: var(--text-secondary); }
.cat-lever-v { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.cat-line > .env-bar, .cat-line > .env-bar + .list-sub { margin-left: 106px; }
.cat-line-none { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 8px; }
/* How readily a category gives, set on its row: the levels as the goal
   kinds are drawn, the one in force pressed, wrapping at any width. */
.cat-give { margin-top: 8px; }
.cat-give-k { display: block; margin-bottom: 4px; }
.cat-give-levels { margin-top: 2px; }
.cat-line-btns { margin-top: 8px; }
.cat-line .env-habits, .cat-line .env-move { margin-top: 6px; }
.habit-held { margin-top: 6px; color: var(--status-serious); }
/* The vendors bought in a category on a rhythm, under its row on the Budget
   card since the Habits card folded into it: set in from the row and hung
   off a rule, so they read as the category's own. Less set in on a phone,
   where the room is the sliders'. */
.cat-vendors { display: grid; min-width: 0; margin: 0 0 8px 40px; }
.cat-vendors > .vendor-row { border-left: 2px solid var(--border-strong); padding-left: 12px; }
.cat-unsorted .list-main { min-width: 0; }
@media (max-width: 480px) { .cat-vendors { margin-left: 12px; } }
/* THE BUDGET CARD, FOLDED (budgetCategories in ui.js). A category shuts to
   one line: its face, its name with its envelope's bar under it, and what is
   left, which is the figure it is read by. Open, its levers; under them, who
   it is paid to, a fold of its own, and each vendor another, shut to what it
   takes a day and what has gone to it. A chevron says which way each goes,
   and the whole line is what is pressed, so it is a finger wide. */
.cat-fold { border-bottom: 1px solid var(--border); min-width: 0; }
.cat-fold:last-child { border-bottom: 0; }
.cat-fold > summary, .who-fold > summary, .vendor-fold > summary { list-style: none; cursor: pointer; }
.cat-fold > summary::-webkit-details-marker, .who-fold > summary::-webkit-details-marker,
.vendor-fold > summary::-webkit-details-marker { display: none; }
.cat-sum { display: grid; grid-template-columns: 30px minmax(0, 1fr) auto 12px; gap: 10px; align-items: center;
  padding: 10px 0; min-height: 44px; }
.cat-sum::after, .vendor-sum::after { content: '\203A'; color: var(--text-muted); font-size: 18px; line-height: 1;
  justify-self: end; transition: transform .15s ease-out; }
.cat-fold[open] > .cat-sum::after, .vendor-fold[open] > .vendor-sum::after { transform: rotate(90deg); }
.cat-sum-main { display: grid; gap: 3px; min-width: 0; }
.cat-sum-main .env-bar { margin: 0; }
/* The weather at the end of the bar (EV.envelopeWeather): under the name
   rather than beside the figure, so it takes nothing from the name's width. */
.cat-sum-bar { display: flex; align-items: center; gap: 6px; min-width: 0; }
.cat-sum-bar .env-bar { flex: 1; min-width: 0; }
.env-sky { flex: none; font-size: 15px; line-height: 1; }
.cat-sum-sub { font-size: 12.5px; color: var(--text-secondary); }
.cat-sum-fig { display: inline-flex; align-items: baseline; justify-content: flex-end; gap: 0 .3em;
  font-weight: 650; font-variant-numeric: tabular-nums; white-space: nowrap; text-align: right; }
.cat-sum-fig.is-critical { color: var(--status-critical); }
.cat-sum-fig.is-quiet { font-weight: 400; font-size: 13px; color: var(--text-muted); }
/* At a phone's narrowest the word goes under its money, so a long figure
   does not cut the category's name short. */
@media (max-width: 360px) {
  .cat-sum-fig { flex-direction: column; align-items: flex-end; gap: 0; }
  .cat-sum-word { font-size: 12px; font-weight: 400; }
  .cat-sum-fig:not(.is-critical):not(.is-quiet) .cat-sum-word { color: var(--text-secondary); }
}
/* Open, the row hangs under its name, set in by the face's width. */
.cat-fold-body.list-row { border-bottom: 0; padding: 0 0 14px 40px; }
.cat-group .cat-fold-body { display: grid; gap: 8px; }
.cat-who { margin-bottom: 2px; }
/* HOW IT GIVES, AND HOW OFTEN (categoryLine in ui.js, 2026-10-11): one
   line that says where they stand, with the chevron every fold here has,
   one press from the picker and the levers it used to show open. */
.cat-more { margin-top: 10px; min-width: 0; }
.cat-more > summary { list-style: none; cursor: pointer; display: flex; align-items: center; gap: 6px;
  min-height: 32px; font-size: 13px; color: var(--text-secondary); }
.cat-more > summary::-webkit-details-marker { display: none; }
.cat-more > summary::after { content: '\203A'; color: var(--text-muted); font-size: 16px; line-height: 1;
  transition: transform .15s ease-out; }
.cat-more[open] > summary::after { transform: rotate(90deg); }
.cat-more > summary:focus-visible { outline: 2px solid var(--series-1); outline-offset: 2px; }
.cat-more-body { display: grid; gap: 10px; margin-top: 8px; min-width: 0; }
.who-fold { margin-top: 12px; }
.who-sum { display: inline-flex; align-items: center; gap: 8px; padding: 7px 12px; border: 1px solid var(--border-strong);
  border-radius: 999px; font-size: 13.5px; font-weight: 600; background: var(--surface-1); }
.who-sum::after { content: '\203A'; color: var(--text-muted); font-size: 16px; line-height: 1; transition: transform .15s ease-out; }
.who-fold[open] > .who-sum::after { transform: rotate(90deg); }
.who-n { min-width: 20px; padding: 1px 6px; border-radius: 999px; background: var(--surface-2); font-size: 12px;
  text-align: center; font-variant-numeric: tabular-nums; }
.who-fold > .cat-vendors, .cat-group .cat-vendors { margin: 8px 0 0; }
.cat-vendors > .vendor-fold { border-left: 2px solid var(--border-strong); padding-left: 12px; }
.vendor-sum { display: grid; grid-template-columns: minmax(0, 1fr) auto 12px; gap: 2px 10px; align-items: center;
  padding: 8px 0; min-height: 44px; }
.vendor-sum::after { grid-row: 1 / span 2; grid-column: 3; }
/* A vendor's name, and a group's, wrap rather than lose their ends: at 320px
   the figure beside them leaves half a line, and "CHIPOTLE 2…" is not a
   name anybody knows their spending by. */
.vendor-sum-name { font-weight: 550; min-width: 0; overflow-wrap: anywhere; }
.cat-group .env-name { white-space: normal; }
.vendor-sum-name .chip { margin-left: 3px; }
.vendor-sum-fig { font-variant-numeric: tabular-nums; white-space: nowrap; text-align: right; }
.vendor-sum-sub { grid-column: 1 / 3; font-size: 12.5px; color: var(--text-secondary); }
.vendor-fold > .vendor-row { border-bottom: 0; padding-top: 0; }
.vendor-fold.is-bill > .vendor-row { display: grid; gap: 6px; }
@media (max-width: 480px) { .cat-fold-body.list-row { padding-left: 12px; } }
/* What a habit's budget gets you: under its levers, and on the Goals card
   for all of them at once. Sooner is green and later red, and the words say
   which either way, so the colour never carries it alone. */
.habit-gets { display: grid; gap: 3px; margin-top: 8px; font-size: 13px; color: var(--text-secondary); }
.habit-gets strong { color: var(--text-primary); font-weight: 600; }
.habit-gets .is-sooner { color: var(--text-good); font-weight: 600; }
.habit-gets .is-later { color: var(--text-critical); font-weight: 600; }
/* What a little more would do is an offer, not a result, so it is not green. */
.habit-gets .is-next { color: var(--text-primary); font-weight: 600; }
.habit-gets .btn { margin-left: 4px; }
/* The same lines under a plan (planGoals), in the plan editor and over the
   envelopes, whose own grid gap spaces them. */
.plan-goals .habit-gets { margin-top: 0; }
/* The same, over the repeating items found in the history: the forecast as
   accepting the ticked ones would make it. See renderRecurring(). */
.cand-sky { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.cand-sky .sky-art { width: 80px; }
.cand-without { color: var(--text-muted); font-size: 13px; margin-top: 4px; }
/* The importer, sure of the file: one line for what was read from where,
   and the columns folded under it. See foundColumns() in ui.js. */
.import-found { color: var(--text-secondary); font-size: 14px; }
.import-map > summary { cursor: pointer; color: var(--text-muted); font-size: 13px; }
/* The bank walkthrough on the Import card: shut until asked for, like the
   paste box beside it, because most people know where their export is. */
.bank-help > summary { cursor: pointer; color: var(--text-secondary); font-size: 14px; font-weight: 550; }
.bank-help[open] > summary { margin-bottom: 12px; }
.bank-step-list { margin: 0; padding-left: 1.3em; display: grid; gap: 6px; }
.import-map[open] > summary { margin-bottom: 10px; }
/* A subscription asking to repeat: the note on the Sorting card and the
   review an import opens. See subscriptionsNote() and reviewSubscriptions()
   in ui.js. The review's rows are labels, so the whole row ticks the box. */
.sub-note { display: grid; gap: 6px; }
.sub-note .list-row { border-color: var(--border); }
.sub-review { display: grid; margin: 0 0 12px; }
.sub-row { display: flex; gap: 10px; align-items: flex-start; padding: 10px 0; border-bottom: 1px solid var(--border); cursor: pointer; }
.sub-row:last-child { border-bottom: 0; }
.sub-row input[type="checkbox"] { margin-top: 3px; flex: none; }
.sub-row .list-main { display: grid; gap: 2px; min-width: 0; }

.balance-line { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.env-amt { font-weight: 650; font-variant-numeric: tabular-nums; text-align: right; }
.env-bar span { display: block; height: 100%; background: var(--series-1); border-radius: 2px; }
/* "How did this happen?" and a line saying where it goes, on the cards
   whose answer is the Spending card. See howLink(). */
/* The budget panel's three paces, as what each leaves at the period's end.
   Three across, like the strip; one under another on the narrowest phones,
   where three across would cut the figures. The meter is the buffer wanted,
   filled by how much of it each pace keeps. See autoBudgetPanel(). */
.budget-pills { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; margin-top: 10px; }
.budget-pill {
  appearance: none; font: inherit; cursor: pointer; text-align: left; min-width: 0;
  display: grid; gap: 2px; align-content: start; padding: 7px 9px 8px; border-radius: 10px;
  border: 1px solid var(--pill-edge, var(--border-strong));
  background: var(--pill-wash, transparent); color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}
.budget-pill-k { font-size: 12px; color: var(--text-secondary); }
/* The pace, as the strip says it, between the word and what it leaves: the
   word alone over a figure that is not the strip's read as a second "Safe"
   that disagreed with the first. */
.budget-pill-rate { font-size: 11.5px; color: var(--text-secondary); white-space: nowrap; }
.budget-pill-v { font-size: 15px; font-weight: 650; color: var(--pill-ink, var(--text-primary)); white-space: nowrap; }
.budget-pill-when { font-size: 11px; color: var(--text-secondary); white-space: nowrap; }
.budget-meter { display: block; height: 4px; border-radius: 2px; background: rgba(127, 127, 127, .22); overflow: hidden; margin-top: 4px; }
.budget-meter span { display: block; height: 100%; background: var(--series-1); border-radius: 2px; }
.budget-meter.is-full span { background: var(--status-good); }
.budget-pill-goal { font-size: 11.5px; color: var(--text-secondary); white-space: nowrap; }
@media (max-width: 380px) {
  .budget-pills { grid-template-columns: minmax(0, 1fr); }
  .budget-pill { grid-template-columns: minmax(0, 1fr) auto; align-items: baseline; column-gap: 10px; }
  .budget-pill-k { grid-column: 1; grid-row: 1; }
  .budget-pill-v { grid-column: 2; grid-row: 1; }
  .budget-pill-rate { grid-column: 1; grid-row: 2; }
  .budget-pill-when { grid-column: 2; grid-row: 2; text-align: right; }
  .budget-meter { grid-column: 1 / -1; grid-row: 3; }
  .budget-pill-goal { grid-column: 1 / -1; grid-row: 4; }
}
.env-bar.is-over span { background: var(--status-critical); }
/* What the Bills card brought into the Budget card when it folded in
   (2026-10-04): the bill question in an inset box of its own, a bill's
   category to pick on a line inside its fold, and a paused bill's name
   dimmed on its line. */
.inset { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface-1); padding: 14px; }
.inset > h3 { font-size: 15px; margin: 0 0 10px; }
.bill-cat { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.vendor-fold.is-paused > .vendor-sum .vendor-sum-name { opacity: .7; }
.cand-fold .list-row.is-pick { border-color: var(--border); }
/* Date and amount side by side, name across the top: a payment added by hand
   is three fields and a button, and four stacked rows for that reads as a form
   to be filled in rather than a line to be added. */
.add-line { display: grid; grid-template-columns: 1fr auto; gap: 9px; align-items: center; }
.add-line .add-wide { grid-column: 1 / -1; }

/* ---- one surface ---------------------------------------------------------

   A card is the one raised thing on the page (2026-10-07). What sits inside
   one — a fold, the line to another card, the level editor, a tile, a note,
   a form — used to carry a border of its own, so the Forecast card was three
   boxes deep in places, and a box inside a box reads as a level of nesting
   the content does not have. Each takes a quiet fill instead, the way a row
   of a grouped list does: still plainly a thing of its own, still plainly
   pressable where it is, and no second outline. Last in the cascade on
   purpose, so it outranks each component's own border above. */
.plan-edit, .why-safe, .day-table, .go-elsewhere, .plan-line, .inset, .note, .goal-form, .debt-box,
.card .kpi { border-color: transparent; background: var(--surface-2); box-shadow: none; }

/* ---- badges ---------------------------------------------------------------

   A white glyph on a disc of a colour of its own, drawn by art.js (badgeEl
   in ui.js): a category's, a goal kind's or the weather's, where the face
   was an emoji (2026-10-07; a medallion off a sprite for its first day,
   drawn since 2026-10-08, and in colour since that evening). The disc is
   its own ground, so the grey square an emoji sat in goes. Sized to the box
   the emoji had, so no row moves. */
.badge { display: inline-block; flex: none; width: 30px; height: 30px; border-radius: 50%; vertical-align: middle; }
.badge svg { display: block; width: 100%; height: 100%; }
.env-icon.is-badge, .goal-face.is-badge { background-color: transparent; }
.goal-kind-face.is-badge { width: 20px; height: 20px; }
.goal-done-face.badge { width: 22px; height: 22px; }
.chip-badge { width: 16px; height: 16px; margin-right: 5px; vertical-align: -3px; }
/* A rung's badge in front of a line about it (goalMark), where the kind's
   emoji was. */
.goal-line-badge.badge { width: 16px; height: 16px; margin-right: 5px; vertical-align: -3px; }
/* A celebration with no moment of its own shows the star, a circle. */
.badge.toast-art { width: 44px; height: 44px; border-radius: 50%; }
.env-sky-badge { width: 20px; height: 20px; }
.env-sky-badge.is-small { width: 16px; height: 16px; margin-right: 6px; vertical-align: -3px; }
.pr-sky-badge { width: 28px; height: 28px; }

/* A dropdown with badges (badgeSelect in ui.js, 2026-10-08): the select
   hidden and a button in its place, drawn as a select is — the same box,
   the same border, a chevron — with the chosen option's badge in front of
   its words; and the list it opens, over a dialog and under the keypad,
   each option with its badge. An option with no badge keeps an empty ring
   so the words line up. */
.bsel { display: inline-flex; width: 100%; min-width: 0; vertical-align: middle; }
.bsel-native { display: none; }
.bsel-btn {
  position: relative; display: flex; align-items: center; gap: 8px; width: 100%; min-width: 0; min-height: 38px;
  font: inherit; font-size: 14px; color: var(--text-primary); text-align: left; cursor: pointer;
  background: var(--surface-1); border: 1px solid var(--border-strong); border-radius: 8px; padding: 5px 30px 5px 7px;
}
.bsel-btn::after {
  content: ''; position: absolute; right: 12px; top: 50%; width: 7px; height: 7px; margin-top: -6px;
  border-right: 2px solid var(--text-secondary); border-bottom: 2px solid var(--text-secondary); transform: rotate(45deg);
}
.bsel-btn[aria-expanded="true"] { border-color: var(--series-1); }
.bsel-btn:disabled { opacity: .55; cursor: default; }
.bsel-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bsel-badge.badge, .bsel-blank { flex: none; width: 24px; height: 24px; }
.bsel-blank { display: inline-block; border-radius: 50%; border: 1.5px dashed var(--border-strong); }
.bsel-list {
  position: fixed; z-index: 82; overflow-y: auto; overscroll-behavior: contain; padding: 4px;
  background: var(--surface-1); border: 1px solid var(--border-strong); border-radius: 10px;
  box-shadow: 0 12px 36px rgba(0, 0, 0, .24);
}
.bsel-group + .bsel-group, .bsel-opt + .bsel-group { border-top: 1px solid var(--border); margin-top: 4px; }
.bsel-group-label { padding: 8px 8px 4px; font-size: 12px; font-weight: 600; color: var(--text-muted); }
.bsel-opt { display: flex; align-items: center; gap: 10px; padding: 6px 8px; border-radius: 7px; font-size: 14px; cursor: pointer; }
/* In the list a long name wraps, never ends in an ellipsis: "SQ *BLUE
   BOTTLE C…" is not a name anybody knows their spending by. */
.bsel-opt .bsel-text { white-space: normal; overflow-wrap: anywhere; }
.bsel-opt.is-active { background: var(--surface-2); }
.bsel-opt[aria-selected="true"] { font-weight: 600; }
.bsel-opt[aria-selected="true"]::after {
  content: ''; flex: none; margin: -3px 4px 0 auto; width: 5px; height: 10px;
  border-right: 2px solid var(--series-1); border-bottom: 2px solid var(--series-1); transform: rotate(45deg);
}
.bsel-opt[aria-disabled="true"] { opacity: .5; cursor: default; }
@media (hover: none), (any-pointer: coarse) {
  .bsel-btn { font-size: 16px; min-height: 42px; }
  .bsel-opt { padding: 9px 8px; font-size: 16px; }
}

/* ---- modal -------------------------------------------------------------- */

/* The page, held still behind something that covers it. See holdPage() in
   ui.js for why this is overflow rather than a fixed body.

   The root only, never body as well. The root's overflow propagates to the
   viewport, which keeps its scroll offset and stays the scrollport the sticky
   header sticks within. Put the same rule on body and body becomes a
   scrollport of its own with nothing scrolled in it — so the bar stops being
   pinned and rides up with the content, which is 85px off the top of the
   screen at the moment a panel opens over it, and the page comes back at the
   wrong scroll position when the panel shuts. Both measured. */
html.is-held { overflow: hidden; }

.modal-overlay {
  position: fixed; inset: 0; z-index: 80;
  background: rgba(11, 11, 11, .45);
  display: flex; align-items: center; justify-content: center; padding: 20px;
  backdrop-filter: blur(2px);
}
.modal-card {
  background: var(--surface-1); border: 1px solid var(--border-strong);
  border-radius: var(--radius); box-shadow: 0 12px 48px rgba(0,0,0,.28);
  padding: 22px; width: 100%; max-width: 460px; max-height: 90vh; overflow-y: auto;
}
.modal-title { font-size: 17px; margin-bottom: 7px; }
.modal-desc { margin: 0 0 15px; font-size: 13.5px; color: var(--text-secondary); line-height: 1.5; }
.modal-error { color: var(--text-critical); font-size: 13px; min-height: 18px; margin: 2px 0 6px; }

/* The bar fills a fixed-width track so a full-strength result cannot push the
   label out of the dialog. */
/* A label with its own control beside it, which is where "Generate one"
   belongs: next to the thing it fills, not below the form. */
.label-row { display: flex; align-items: center; gap: 10px; margin-bottom: 4px; }
.label-row label { margin-bottom: 0; }
.label-row .btn { margin-left: auto; }

/* Read-only, and looks it. The field exists so a password manager has a
   username to key its entry on; there is nothing here to fill in. */
.pw-user { background: var(--surface-2); color: var(--text-secondary); cursor: default; }
.pw-meter { display: flex; align-items: center; gap: 9px; margin-top: 4px; }
.pw-track { flex: none; width: 120px; height: 4px; border-radius: 2px; background: var(--surface-2); overflow: hidden; }
.pw-bar { display: block; height: 100%; width: 0; border-radius: 2px; background: var(--status-critical);
          transition: width .15s ease, background-color .15s ease; }
.pw-bar.s0 { width: 12%; background: var(--status-critical); }
.pw-bar.s1 { width: 30%; background: var(--status-critical); }
.pw-bar.s2 { width: 52%; background: var(--status-serious); }
.pw-bar.s3 { width: 76%; background: var(--status-warning); }
.pw-bar.s4 { width: 100%; background: var(--status-good); }
.pw-label { font-size: 12px; color: var(--text-muted); flex: 1; min-width: 0; }

@media (max-width: 620px) {
  .wrap { padding: 0 14px 56px; }
  .topbar-inner { padding: 10px 14px; }
  .kpi-value { font-size: 22px; }
  .card { padding: 14px; }
  /* The bar is shorter here, so a jump that used the desktop figure landed
     with a gap above the heading rather than under it. */
  .feed-card { scroll-margin-top: 78px; }
  .feed-card + .feed-card { margin-top: 28px; padding-top: 24px; }
  .feed-title { font-size: 17px; }
  /* Only the bars at this width: "On this page" is what the icon already
     says, and the room is the mark's. */
  .menu-label { display: none; }
  /* Under the title rather than beside it: at this width the row is all the
     room the sentence has. A grid rather than a block, because the row grew
     an icon — as a block child it became a line of its own above the title,
     which is what a browser run caught. NAMED TRACKS, not counted ones: a
     row with no flag has one child fewer and auto-placement would slide the
     title into the icon's column, the same trap the topbar has a note about. */
  .menu-row { display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: 12px; }
  .menu-row > .menu-icon { grid-column: 1; grid-row: 1 / -1; align-self: center; }
  .menu-row > .menu-main { grid-column: 2; grid-row: 1; }
  .menu-row > .menu-flag { grid-column: 2; grid-row: 2; }
  .menu-flag { display: block; margin-bottom: 3px; }
  /* Except a search row, whose tag says where the row goes rather than how
     urgent it is — dropped under the note it reads as a caption on the result
     instead of a destination for it. Two tracks, so it stays beside the title
     and the note still gets the width. */
  .menu-row.search-row {
    display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: baseline; column-gap: 10px;
  }
  /* Three tracks, and every child pinned: a transaction result has no icon
     and a card result has no "where", so between them every column can be
     the missing one. */
  .search-row > .menu-icon { grid-column: 1; grid-row: 1; align-self: center; }
  .search-row > .menu-main { grid-column: 2; grid-row: 1; }
  .search-row > .search-where { grid-column: 3; grid-row: 1; }
  .search-row .search-where { display: block; margin-bottom: 0; }
  .menu { padding: 0 14px 12px; }
}
@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* The recovery code, at the one moment it is ever shown.

   Big, monospaced and spaced out, because it gets copied onto paper by hand
   and then typed back months later by somebody having a bad day. Selectable
   and wrapping rather than truncated: a code that is cut off on a narrow
   phone is a code that was never really shown. */
.recovery-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 1.25rem;
  line-height: 1.7;
  letter-spacing: 0.08em;
  text-align: center;
  /* Wrap at the dashes, never inside a group. break-all does not care where
     it lands and split Q94P into Q94 and P on a narrow window — which is a
     fine way to lose your place halfway through copying a code onto paper,
     and this code is only ever read at the worst moment anybody will use this
     app. Normal breaking treats each dash as the opportunity it already is. */
  word-break: normal;
  overflow-wrap: break-word;
  user-select: all;
  -webkit-user-select: all;
  padding: 14px 12px;
  margin: 0 0 12px;
  border: 1px solid var(--line, #d0d0d0);
  border-radius: 8px;
  background: var(--surface-2, rgba(127, 127, 127, 0.08));
}

/* An invite link, shown once to be sent. Long and random, so it breaks
   anywhere rather than pushing the dialog wider than a phone; selectable in
   one press, because copying it by hand is the fallback when a browser will
   not copy for us. */
.invite-link {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.9rem;
  line-height: 1.5;
  word-break: break-all;
  user-select: all;
  -webkit-user-select: all;
  padding: 12px;
  margin: 0 0 12px;
  border: 1px solid var(--line, #d0d0d0);
  border-radius: 8px;
  background: var(--surface-2, rgba(127, 127, 127, 0.08));
}
/* Settings to paste, laid out as they will be pasted. */
.invite-link.is-code { white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere; }

/* Printing this page is one of the two offered ways to keep the code, so the
   code has to survive it: no dark background burning through the ink, and
   nothing else on the page competing for the paper. */
@media print {
  .modal-overlay { position: static; background: none; }
  .modal-card { box-shadow: none; border: 0; }
  .recovery-code { background: none; border: 1px solid #000; color: #000; }
  .btn-row, .modal-error { display: none; }
}


/* The icon, opening
   ------------------------------------------------------------------------
   The platform shows its own launch screen first — the app icon on the page
   colour — and then hands over to this, which is the same icon on the same
   colour. So nothing jumps: the tile a person tapped is still there, and the
   mark then draws itself across it, left to right, the way the thing is
   built — up the near slope, along the crest the word makes, down the far
   slope, and the wave last.

   It is a wipe rather than a stroke because the mark is a filled outline with
   its counters punched out, not a line: there is no path to walk along. A
   wipe from the left arrives at the wave at the end, which is the same
   sentence the drawing tells.

   Two things take it away, and it needs both. The animation below is the one
   that looks like anything. app.js then removes the element outright once the
   app has rendered, because an animation is not a guarantee: this stylesheet
   already turns every animation off under prefers-reduced-motion, with
   !important, and the first version of this relied on one to fade itself out
   — which left the app covered for ever for exactly the people least able to
   work out why. Anything that hides the app has to be removed by something
   that cannot itself be switched off. */
.splash {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: grid;
  place-items: center;
  background: var(--page);
  pointer-events: none;
  animation: splash-go .26s ease .46s both;
}
.splash-tile {
  width: 112px;
  aspect-ratio: 1;
  border-radius: 24%;
  overflow: hidden;
  display: grid;
  place-items: center;
  background: linear-gradient(90deg, var(--mark-from), var(--mark-to));
  animation: splash-settle .72s cubic-bezier(.2, .7, .3, 1) both;
}
.splash-mark {
  display: block;
  width: 80%;
  clip-path: inset(0 100% 0 0);
  animation: splash-draw .44s cubic-bezier(.35, 0, .2, 1) .04s both;
}

@keyframes splash-draw { to { clip-path: inset(0 0 0 0); } }
@keyframes splash-settle {
  from { transform: scale(.94); }
  60%  { transform: scale(1); }
  to   { transform: scale(1.04); }
}
/* visibility, not just opacity: an invisible thing that is still there can
   still be found by a screen reader walking the page. */
@keyframes splash-go { to { opacity: 0; visibility: hidden; } }

/* Someone who has asked for less movement gets the icon and then the app,
   with nothing drawn and nothing scaled. */
/* Someone who has asked for less movement does not get a launch animation at
   all. Not a faster one: none. The rule above this file's own
   `* { animation: none !important }` cannot animate anyway, and a tile that
   sits there until script removes it is worse than no tile. */
@media (prefers-reduced-motion: reduce) {
  .splash { display: none; }
}

/* Staging, in orange. The Worker marks staging's page data-env="staging"
   (serveStaging in worker/index.mjs) and serves it orange icons, so here the
   rest of what says "Berm" follows: the opening tile, which is the icon on the
   page colour and so has to match the orange one that was tapped; the mark in
   the header, on every screen; and a label in the corner. Production's page is
   never marked, so on production none of this matches anything. The two
   colours are STAGING_GROUND in tools/mark.mjs, and the suite checks they
   still are. */
html[data-env="staging"] {
  --mark-from: #ea7a0c;
  --mark-to:   #7c2d12;
}
html[data-env="staging"] .brand { background: #ea7a0c; }
/* The sample, in the app's blue: the same pill in the other corner, so
   anybody who has scrolled the banner off the top still sees which copy of
   the figures they are looking at — asked for on 2026-10-02 as "a sample
   pill like staging so people remember it's a sample", and then "the
   sample pill should be on the right or staging should get pushed to the
   right": staging keeps the left, the sample takes the right, so on staging
   the two never share a corner. startDemo() in ui.js marks the page
   data-sample while the sample runs, and leaving it is a reload, so nothing
   has to take the mark off. body::before, because staging's is body::after. */
html[data-sample] body::before {
  content: 'Sample';
  position: fixed;
  right: calc(env(safe-area-inset-right, 0px) + 8px);
  bottom: calc(env(safe-area-inset-bottom, 0px) + 8px);
  z-index: 2147483647;
  pointer-events: none;
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--series-1);
  color: #fff;
  font: 600 11px/1.4 var(--font);
  letter-spacing: .08em;
  text-transform: uppercase;
}
html[data-env="staging"] body::after {
  content: 'Staging';
  position: fixed;
  left: calc(env(safe-area-inset-left, 0px) + 8px);
  bottom: calc(env(safe-area-inset-bottom, 0px) + 8px);
  z-index: 2147483647;
  pointer-events: none;
  padding: 3px 10px;
  border-radius: 999px;
  background: #ea7a0c;
  color: #0b0b0b;
  font: 600 11px/1.4 var(--font);
  letter-spacing: .08em;
  text-transform: uppercase;
}
