/* ledger — theming.

   TWO INDEPENDENT AXES, not one list of five.

     data-theme  the STYLE:      absent/plain | glass | brutalist
     data-mode   the APPEARANCE: absent (follow the system) | light | dark

   Every style exists in both appearances. Folding the two into a single
   dropdown meant Midnight Glass forced dark and Neo-Brutalist forced light,
   so wanting the brutalist look at night was simply not expressible.

   The mechanism is `light-dark()` plus `color-scheme`, which is why each style
   is ONE token block holding both palettes rather than a block plus a
   prefers-color-scheme copy plus an explicit-override copy. data-mode does
   nothing but set color-scheme; light-dark() reads it. That also fixes native
   form controls and scrollbars for free, which the media-query version never
   did. */

:root {
  /* Absent data-mode = honour the system. The two rules below are the whole
     of the appearance switch. */
  color-scheme: light dark;

  --bg:        light-dark(#f6f8fa, #0e1116);
  --bg-card:   light-dark(#ffffff, #161b22);
  --bg-sunken: light-dark(#eef1f4, #0a0d12);
  --line:      light-dark(#d8dee4, #262d38);
  --fg:        light-dark(#1f2328, #e6edf3);
  --fg-dim:    light-dark(#656d76, #8b949e);
  --accent:    light-dark(#1a7f37, #3fb950);
  --accent-dim:light-dark(#a6dcb2, #1f6f32);
  --danger:    light-dark(#cf222e, #f85149);
  --radius: 12px;
  --pad: clamp(16px, 4vw, 28px);
  --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
}
:root[data-mode="light"] { color-scheme: light; }
:root[data-mode="dark"]  { color-scheme: dark; }

* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100dvh;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font);
  line-height: 1.5;
  -webkit-text-size-adjust: 100%;
}

/* THE LOAD-BEARING RULE. A screen is invisible unless it is the active one.
   Nothing else in this file may give .screen a display value. */
.screen { display: none; }
.screen[data-active] { display: block; }
#login[data-active] {
  display: grid;
  place-items: center;
  min-height: 100dvh;
  padding: var(--pad);
}

.card {
  background: var(--bg-card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--pad);
  width: 100%;
  max-width: 24rem;
}
.card.wide { max-width: none; }

.wordmark {
  font-size: 1.6rem;
  font-weight: 650;
  letter-spacing: -0.02em;
  margin: 0;
}
.wordmark.sm { font-size: 1.05rem; }
/* The quill-and-book mark. Swapped per theme via a custom property rather than
   per-theme selectors on the element, so a new theme only has to set --mark.
   A background-image cannot be recoloured, which is exactly why the handoff
   ships two files instead of one. */
/* Brutalist is the DEFAULT mark and the favicon — the owner's pick, not the
   handoff's alphabetical accident. Glass still gets its own, because its
   round-capped translucent quill is drawn for a dark ground and the hard #111
   strokes of the brutalist one disappear against it. */
/* The mark is an image, so light-dark() cannot reach it — it is not a colour.
   The brutalist mark is a white book with a black outline and loses that
   outline entirely on a dark ground, so dark appearances get the inverted
   file. The glass mark is drawn in the accent blue and reads on both. */
:root { --mark: url('/img/brutalist-mark.svg'); }
@media (prefers-color-scheme: dark) {
  :root:not([data-mode="light"]):not([data-theme="glass"]) { --mark: url('/img/brutalist-mark-dark.svg'); }
}
:root[data-mode="dark"]:not([data-theme="glass"]) { --mark: url('/img/brutalist-mark-dark.svg'); }
:root[data-theme="glass"] { --mark: url('/img/glass-mark.svg'); }
.wordmark::before {
  content: "";
  display: inline-block;
  width: 1.15em; height: 1.15em;
  margin-right: 0.4em;
  background: var(--mark) center / contain no-repeat;
  vertical-align: -0.22em;
}
.wordmark.sm::before { width: 1.3em; height: 1.3em; vertical-align: -0.3em; }

.sub  { color: var(--fg-dim); margin: 0.35rem 0 1.4rem; font-size: 0.92rem; }
#iv-change { margin-bottom: 0.2rem; }
.foot { color: var(--fg-dim); margin: 1.2rem 0 0; font-size: 0.8rem; }
.muted { color: var(--fg-dim); }

label {
  display: block;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--fg-dim);
  margin: 0.9rem 0 0.3rem;
}
input {
  width: 100%;
  padding: 0.65rem 0.75rem;
  font: inherit;
  color: var(--fg);
  background: var(--bg-sunken);
  /* THE THEME OWNS THE BORDER AND THE CORNER. Hard-coding 1px and 8px here is
     what left brutalist with three radii on one screen -- square cards and
     buttons beside rounded inputs -- and with a pale hairline round every box
     you type in while everything else wore the 2px rule. */
  border: var(--bw) solid var(--line);
  border-radius: var(--radius-sm);
}
input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* THE PLACEHOLDER IS TEXT, SO IT IS THEMED LIKE TEXT. Left to the browser it
   renders #757575 in every theme -- below AA against the sunken fill in both
   appearances, and in dark it reads as a disabled field rather than an empty
   one. --fg-dim is the app's own quiet colour and clears 4.5:1 everywhere.
   opacity:1 because Firefox dims it again on top of the colour. */
::placeholder { color: var(--fg-dim); opacity: 1; }


button {
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  border-radius: var(--radius-sm);
  border: var(--bw) solid transparent;
}
#login-btn {
  width: 100%;
  margin-top: 1.3rem;
  padding: 0.7rem;
  color: #fff;
  background: var(--accent);
}
#login-btn:disabled { opacity: 0.6; cursor: progress; }
/* EVERY .link IS A BUTTON, because every one of them performs an action.
   It used to be an underlined word, with a separate rule converting it to a
   proper control in seven named places -- which is the same as admitting it
   should always have been one. The named list is gone; this is it.
   Borders and radii come from the theme, so all three style it for free. */
.link {
  font: inherit; font-size: 0.78rem; font-weight: 600;
  padding: 0.35rem 0.65rem; cursor: pointer; text-decoration: none;
  color: var(--fg); background: var(--bg-card);
  border: var(--bw) solid var(--line); border-radius: var(--radius-sm);
  box-shadow: var(--shadow-btn);
  transition: box-shadow 0.1s ease-out, transform 0.1s ease-out;
}
.link:hover {
  color: var(--fg); box-shadow: var(--shadow-hover);
  transform: translate(1px, 1px); text-decoration: none;
}

.err {
  color: var(--danger);
  font-size: 0.86rem;
  margin: 0.9rem 0 0;
}

.spacer { flex: 1; }
.who { color: var(--fg-dim); font-size: 0.86rem; }

h2 { margin: 0 0 0.2rem; font-size: 1.1rem; }

.kv {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.35rem 1rem;
  margin: 0;
  font-size: 0.9rem;
}
.kv dt { color: var(--fg-dim); }
.kv dd { margin: 0; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }

.phases { margin: 0; padding-left: 1.2rem; display: grid; gap: 0.5rem; font-size: 0.92rem; }
code {
  font-family: var(--mono);
  font-size: 0.9em;
  background: var(--bg-sunken);
  border: 1px solid var(--line);
  border-radius: 5px;
  padding: 0.05em 0.35em;
}

/* The API-unreachable banner. It is a class rather than an inline style
   because the CSP here is style-src 'self' with no 'unsafe-inline' — an
   inline style attribute would leave the ONE screen you see when things are
   broken as the only screen that is itself broken. */
.err.banner { padding: 1rem; margin: 0; }

/* ── Phase 1 ────────────────────────────────────────────────────────────── */

:root {
  --pos: light-dark(#1a7f37, #3fb950);
  --neg: light-dark(#cf222e, #f85149);
  --grid: color-mix(in srgb, var(--line) 70%, transparent);
}

.tabs { display: flex; gap: 0.15rem; margin-left: 0.5rem; }
.tab {
  background: none; border: none; color: var(--fg-dim);
  padding: 0.4rem 0.7rem; border-radius: 7px; font-size: 0.9rem;
}
.tab:hover { color: var(--fg); background: var(--bg-sunken); }
.tab[data-on] { color: var(--fg); background: var(--bg-sunken); font-weight: 650; }

/* THE SAME TRAP AS .screen, AND I FELL INTO IT ONCE HERE. `hidden` is
   display:none in the USER-AGENT stylesheet, and any author rule that sets
   display beats it — so `.view { display: grid }` alone renders every view at
   once, stacked, with the hidden ones fully interactive underneath. This is the
   boop bug (an unhideable #login overlay swallowing every click) in a different
   costume. The [hidden] rule below is what actually does the hiding; do not
   remove it, and do not set display on .view without it. */
/* grid-auto-rows: max-content is a BUG FIX, not tidiness. `overflow-x: auto`
   on a table card (below) makes it a scroll container in both axes — CSS
   coerces the other axis's `visible` to `auto` — and a grid item that is a
   scroll container has its automatic minimum size resolve to ZERO rather than
   to its content height. With default `auto` rows the Holdings row therefore
   collapsed to the height of the heading above the table (64px): the card's
   own box grew, but its TRACK did not, so the Allocation and Gain cards were
   laid out over the top of the table and the Investments > Holdings box read
   as empty. Sizing rows to max-content asks each row for the item's content
   height, which a scroll container does report. */
.view { display: grid; gap: var(--pad); grid-auto-rows: max-content; }
.view[hidden] { display: none; }
[hidden] { display: none !important; }

.hero-head { display: flex; align-items: flex-start; gap: 1rem; justify-content: space-between; flex-wrap: wrap; }
.label { color: var(--fg-dim); font-size: 0.8rem; font-weight: 600; margin: 0 0 0.2rem; text-transform: uppercase; letter-spacing: 0.04em; }
.big { font-size: clamp(2rem, 7vw, 3rem); font-weight: 700; letter-spacing: -0.03em; margin: 0; line-height: 1.05; }
.med { font-size: 1.5rem; font-weight: 650; margin: 0.1rem 0 0.7rem; letter-spacing: -0.02em; }
.money { font-variant-numeric: tabular-nums; }
.pos { color: var(--pos); }
.neg { color: var(--neg); }

/* Privacy blur. A net-worth dashboard gets opened in public; this is a toggle,
   not security, and it says so by revealing on hover. */
/* PRIVACY BLUR SCALES WITH THE FIGURE.
   A flat radius hides $12.34 completely and barely troubles $467,297.07. The
   long one is not harder to blur because its glyphs are bigger -- they are the
   same size -- but because it carries so much more SHAPE: digit count, where
   the commas fall, the overall width. The eye reconstructs the number from that
   silhouette long after no single digit is legible, so a figure eight digits
   long leaks its order of magnitude and roughly its leading digits through a
   blur that erases a small one entirely.
   So the radius grows with how many digits are in the figure. It is set in em
   rather than px so it follows font size as well, which is the other half of
   the same problem: the same 7px that hid a 0.88rem table cell did very little
   to a 3rem hero figure. */
body[data-blur] .money { filter: blur(var(--blur-r, 0.45em)); transition: filter 0.12s; }
body[data-blur] .money[data-digits="4"] { --blur-r: 0.55em; }
body[data-blur] .money[data-digits="5"] { --blur-r: 0.7em; }
body[data-blur] .money[data-digits="6"] { --blur-r: 0.85em; }
body[data-blur] .money[data-digits="7"] { --blur-r: 1em; }
body[data-blur] .money[data-digits="8"] { --blur-r: 1.15em; }
body[data-blur] .money[data-digits="9"] { --blur-r: 1.3em; }
/* Hovering still reveals it. It is a screen, not a lock. */
body[data-blur] .money:hover { filter: none; }

.deltas { display: grid; gap: 0.25rem; text-align: right; font-size: 0.86rem; }
.deltas div { color: var(--fg-dim); }
.deltas b { font-variant-numeric: tabular-nums; font-weight: 650; }

.range { display: flex; gap: 0.35rem; margin: 1rem 0 0.5rem; }
.range button {
  background: var(--bg-card); border: var(--bw) solid var(--line); color: var(--fg-dim);
  padding: 0.25rem 0.6rem; font-size: 0.8rem; font-weight: 600;
  border-radius: var(--pill-radius); box-shadow: var(--shadow-btn);
  transition: box-shadow 0.1s ease-out, transform 0.1s ease-out;
}
.range button:hover { color: var(--fg); box-shadow: var(--shadow-hover); transform: translate(1px, 1px); }
.range button[data-on] {
  color: var(--brut-on-accent, #fff); background: var(--accent); border-color: var(--accent);
}

.chartwrap { width: 100%; }
.chartwrap svg { display: block; width: 100%; height: 240px; overflow: visible; }
.chartwrap.sm svg { height: 130px; }
.chart-empty { color: var(--fg-dim); text-align: center; padding: 2rem 0; margin: 0; }

.split { display: grid; gap: var(--pad); grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); }
.split .card { max-width: none; }

.minilist { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.3rem; font-size: 0.88rem; }
.minilist li { display: flex; justify-content: space-between; gap: 1rem; }
.minilist span:first-child { color: var(--fg-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.minilist span:last-child { font-variant-numeric: tabular-nums; white-space: nowrap; }

/* A TABLE, NOT A BLOCK. `display: block` was used here to let wide tables
   scroll instead of forcing their container wider — and it worked, but it also
   made every table shrink to its content, so the budget stopped at its widest
   cell with a third of the card empty to the right.
   The scrolling belongs on the CONTAINER. The table stays a table, fills the
   width, and the card scrolls when the columns genuinely need more room. */
table.grid { width: 100%; border-collapse: collapse; font-size: 0.88rem; margin-top: 0.8rem; }

/* ONLY cards that contain a table are scroll containers. `overflow-x: auto` on
   every card was reserving a scrollbar gutter on machines that draw persistent
   scrollbars, so cards with nothing to scroll — the net-worth hero, Assets,
   Liabilities, Allocation — showed a bar for content that fits. A card without
   a table has no wide content to rescue, so it has no business being a scroll
   container. */
.card:has(table.grid) { overflow-x: auto; }
table.grid thead th {
  text-align: left; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--fg-dim); border-bottom: 1px solid var(--line); padding: 0.4rem 0.6rem; white-space: nowrap;
}
table.grid td { padding: 0.45rem 0.6rem; border-bottom: 1px solid var(--grid); white-space: nowrap; }
table.grid tbody tr:hover { background: var(--bg-sunken); }
/* A DIALOG HAS NO ROOM TO SCROLL INTO. `white-space: nowrap` is right for a
   register full of dates and figures, which live in a card that can scroll
   sideways — but the auto-assign table's middle column is a SENTENCE ("$500.00
   every month — the total of the investment weights"), and nowrap pushed the
   table past the dialog's edge. The Add column, the one figure the dialog
   exists to show you before you commit to it, then sat off-screen behind a
   horizontal scrollbar. Descriptive columns wrap; the figures still do not. */
#auto-table td:nth-child(2) { white-space: normal; min-width: min(12rem, 38vw); }
#auto-table { table-layout: auto; }
@media (max-width: 30rem) {
  /* Three columns plus 0.6rem of padding either side of each is more than a
     phone dialog has. The category name wraps too at this width — a long one
     is still readable over two lines, which a clipped one is not. */
  #auto-table td, #auto-table th { padding-left: 0.3rem; padding-right: 0.3rem; }
  #auto-table td:first-child { white-space: normal; }
}
table.grid .num { text-align: right; font-variant-numeric: tabular-nums; }

/* ── The column-width pass ────────────────────────────────────────────────────
   Auto table layout hands every column an equal share of the surplus, so a
   date sat as wide as a payee. width:1% plus the table's blanket nowrap is
   shrink-to-fit: the narrow, fixed-shape columns (numbers, dates, pills,
   action buttons) take only what their content needs and the text columns
   inherit the rest. */
table.grid th.num, table.grid td.num { width: 1%; }
table.grid .clearedcol { width: 1%; }
/* Leading date columns and trailing one-button columns. The register's own
   first column is the flag, not the date, since the flag column was added
   (2026-09-24) — see the nth-child(2) rule below for the date's own width
   now that it has moved one seat over. */
#rg-table th:first-child, #rg-table td:first-child,
#uc-table th:first-child, #uc-table td:first-child,
#ramp-table th:first-child, #ramp-table td:first-child,
#sched-table th:first-child, #sched-table td:first-child { width: 1%; }
#rg-table th:last-child, #rg-table td:last-child { width: 1%; }
#rg-table th:nth-child(2), #rg-table td:nth-child(2) { width: 1%; }
/* Notes live under the category now, so Category is the column that earns
   the room; Payee keeps enough to read, Category takes the rest. Indices are
   +1 from what they'd be without the leading flag column. */
#rg-table th:nth-child(4) { width: 22%; }
#rg-table th:nth-child(5) { width: 44%; }
/* The flag column: just wide enough for the icon button, never for the
   colgroup-driven resize the other columns get (see makeColsResizable). */
#rg-table th.flagcol, #rg-table td.flagcol { width: 1.6rem; padding-left: 0.35rem; padding-right: 0.35rem; }
/* The accounts table: everything but the name is fixed-shape. */
#acct-table th:nth-child(2), #acct-table td:nth-child(2),
#acct-table th:nth-child(5), #acct-table td:nth-child(5) { width: 1%; }
/* Uncategorized: the chip and the picker size themselves. */
#uc-table th:nth-child(5), #uc-table td:nth-child(5) { width: 1%; }

/* ── Resizable columns ────────────────────────────────────────────────────────
   Drag the boundary between two headings. The grip is INVISIBLE until the
   pointer is over it — a thin accent line then marks the boundary — because
   the owner asked for the clean look to survive. Widths persist per table in
   this browser (localStorage); double-click a grip to reset the table. */
th .colgrip {
  position: absolute; top: 0; right: -5px; width: 11px; height: 100%;
  cursor: col-resize; z-index: 3; background: none; border: none; padding: 0;
}
th .colgrip::after {
  content: ''; position: absolute; top: 12%; bottom: 12%; right: 5px; width: 2px;
  border-radius: 2px; background: var(--accent); opacity: 0;
}
/* A HANDLE NOBODY CAN FIND IS NOT A FEATURE. At rest the grips are invisible,
   which is the clean look the tables are for — but that left the only clue as
   to where a boundary can be dragged being the boundary itself, and on a table
   of right-aligned headings there is no line drawn between two columns to aim
   at. Every boundary in a header the pointer is over shows a faint mark; the
   one actually under the pointer brightens. Costs nothing at rest, and touch
   devices (no hover, no grips) are unaffected. */
thead:hover th .colgrip::after { opacity: 0.22; }
th .colgrip:hover::after, th .colgrip[data-dragging]::after { opacity: 0.7; }
/* With fixed layout a squeezed cell would clip mid-letter; elide instead. */
table.grid.colsized { table-layout: fixed; }
table.grid.colsized td { overflow: hidden; text-overflow: ellipsis; }
@media (hover: none) { th .colgrip { display: none; } }
tr.clickable { cursor: pointer; }
tr.dim td { color: var(--fg-dim); }

.pill {
  display: inline-block; font-size: 0.72rem; font-weight: 650; padding: 0.1rem 0.45rem;
  border-radius: 999px; background: var(--bg-sunken); border: 1px solid var(--line); color: var(--fg-dim);
}
.pill.guess { border-color: var(--neg); color: var(--neg); }

.droprow { display: flex; align-items: center; gap: 0.8rem; flex-wrap: wrap; margin: 0.6rem 0; }
.filebtn { position: relative; overflow: hidden; display: inline-block; margin: 0; }
.filebtn input { position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; height: 100%; }
.filebtn span {
  display: inline-block; padding: 0.55rem 0.9rem; border-radius: 8px;
  background: var(--bg-sunken); border: 1px solid var(--line); font-weight: 600; font-size: 0.9rem;
}
.filename { color: var(--fg-dim); font-size: 0.86rem; }
.hint { color: var(--fg-dim); font-size: 0.82rem; margin: 0.4rem 0 0; }
.check { display: flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; color: var(--fg-dim); margin: 0; }

select {
  font: inherit; font-size: 0.85rem; color: var(--fg); background: var(--bg-sunken);
  border: var(--bw) solid var(--line); border-radius: var(--radius-sm); padding: 0.2rem 0.4rem;
}
input[type="date"] { max-width: 12rem; }

.actions { display: flex; align-items: center; gap: 0.8rem; margin-top: 1.2rem; }
button.primary { padding: 0.6rem 1.2rem; color: #fff; background: var(--accent); }
button.primary:disabled { opacity: 0.6; cursor: progress; }
button.danger { color: var(--neg); background: none; border: 1px solid var(--line); padding: 0.25rem 0.6rem; font-size: 0.8rem; }
/* SECONDARY. The other half of a decision, and it has to look like a control.
   Every dialog's Cancel used to be `.link` -- a bare underlined word sitting
   next to a solid primary button -- which reads as a footnote rather than as
   the thing you press to back out. Same box, same height and same padding as
   .primary so the pair sits on one baseline; quieter fill so it does not
   compete for the press. Themed from tokens, so it follows plain, glass and
   brutalist without a per-theme rule. */
button.secondary {
  padding: 0.6rem 1.2rem; color: var(--fg);
  background: var(--bg-sunken); border: 1px solid var(--line);
}
button.secondary:hover { border-color: var(--fg-dim); color: var(--fg); }

.verdict { margin-top: 1rem; padding: 0.8rem 1rem; border-radius: 8px; border: 1px solid var(--line); font-size: 0.9rem; }
.verdict.ok { border-color: var(--pos); }
.verdict.bad { border-color: var(--neg); }
.verdict ul { margin: 0.5rem 0 0; padding-left: 1.2rem; }
.verdict code { font-variant-numeric: tabular-nums; }

@media (max-width: 40rem) {
  .tabs { order: 3; width: 100%; margin: 0.4rem 0 0; }
  .bar { flex-wrap: wrap; }
}

/* Chart internals. These belong to js/chart.js and are here rather than in a
   <style> block it injects, because the CSP is style-src 'self' — an injected
   <style> element is blocked exactly like a style attribute, and the chart
   would render as unstyled black paths. */
.c-grid { stroke: var(--grid); stroke-width: 1; vector-effect: non-scaling-stroke; }
.c-zero { stroke: var(--fg-dim); stroke-width: 1; stroke-dasharray: 4 3; vector-effect: non-scaling-stroke; }
.c-lbl  { fill: var(--fg-dim); font-size: 11px; font-family: var(--mono); }
/* THE AXIS GIVES THE GAME AWAY. Blurring every figure on the page and then
   printing "$500k" up the side of the chart defeats the whole feature -- it is
   the one label that states the order of magnitude outright. Hidden rather than
   blurred: a smudge in a known position still carries its width, and width is
   most of what you need to guess a number. visibility keeps the layout, so the
   plot area does not resize when the setting is toggled. The date axis is
   untouched; when something happened is not private. */
body[data-blur] .c-ylbl { visibility: hidden; }
.c-area { fill: var(--accent); opacity: 0.13; }
.c-line { fill: none; stroke: var(--accent); stroke-width: 1.25; vector-effect: non-scaling-stroke;
          stroke-linejoin: round; stroke-linecap: round; }
.c-dot  { fill: var(--accent); }
/* An ESTIMATED stretch of a line. It carries no styling of its own BY OWNER
   DECISION (2026-08-29): the dashes used to mark worked-out days, and with
   catch-up ramps most of the history is worked-out, so whole charts drew
   dashed end to end. This is a single-household dashboard and the owner knows
   which figures are estimates; the distinction still exists in the data
   (`est`/`src`) and in the sentences under the charts — just not in the
   stroke. The .c-est class stays on the paths so those sentences and the
   probes can keep finding the estimated runs. */

/* PICKING WHICH ACCOUNTS THE PORTFOLIO SHOWS. Sixteen of them, so a dropdown
   would hide the current selection behind a click and a chip row would not --
   the whole value of this control is being able to see at a glance which
   accounts the number above you refers to.

   They are buttons, not links: each one performs an action and gets the box,
   the border and the hover the rest of the app's controls have. Selected is
   the FILLED state rather than an outline, so a glance separates the chosen
   from the rest without reading any of them. */
.acctpick { display: flex; flex-wrap: wrap; gap: 0.3rem; margin: 0.9rem 0 0.2rem; }
.acctpick button {
  font: inherit; font-size: 0.74rem; font-weight: 600; cursor: pointer;
  padding: 0.25rem 0.6rem; border-radius: var(--pill-radius);
  border: var(--bw) solid var(--line);
  background: var(--bg-card); color: var(--fg-dim);
  /* At rest, not only on hover: in brutalist the offset block IS the button,
     and these were the one row of controls on the site drawn without it. */
  box-shadow: var(--shadow-btn);
  transition: box-shadow 0.1s ease-out, transform 0.1s ease-out;
}
.acctpick button:hover { box-shadow: var(--shadow-hover); color: var(--fg); transform: translate(1px, 1px); }
.acctpick button:active { transform: translate(1px, 1px); }
.acctpick button[data-on] {
  background: var(--accent); color: var(--brut-on-accent, #fff);
  border-color: var(--accent);
}
.acctpick button.pickall { font-style: italic; }
.acctpick .picknote { font-size: 0.72rem; color: var(--fg-dim); align-self: center; padding-left: 0.2rem; }
@media (max-width: 40rem) { .acctpick button { font-size: 0.7rem; padding: 0.22rem 0.45rem; } }

/* ── Update balances ─────────────────────────────────────────────────────── */
.upd-date { display: flex; align-items: center; gap: 0.5rem; }
.upd-date label { margin: 0; }
.upd-date input { width: auto; }

table.upd td { padding: 0.3rem 0.6rem; }
/* The whole point of this screen is speed, so the inputs are big enough to hit
   on a phone and the row is dense enough to see a dozen accounts at once. */
/* EVERY BOX IN A COLUMN IS THE SAME BOX. `.hbal` -- the total for a holdings
   account -- and `.uval` -- the new value of one position -- sit in the NEW
   BALANCE column with `.bal`, and were left at the blanket `input{width:100%}`:
   wider than the boxes above them and left-aligned where the rest right-align,
   so the column's edge jogged ~30px at every section boundary. The quantity and
   basis boxes get the same treatment for the same reason. */
table.upd input.bal, table.upd input.hbal, table.upd input.uval,
table.upd input.uqty, table.upd input.ubasis {
  width: 9rem; text-align: right; font-variant-numeric: tabular-nums;
  padding: 0.4rem 0.5rem;
}
table.upd input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
table.upd tr[data-dirty] { background: color-mix(in srgb, var(--accent) 8%, transparent); }
table.upd tr[data-dirty]:hover { background: color-mix(in srgb, var(--accent) 12%, transparent); }
.delta { font-variant-numeric: tabular-nums; font-weight: 650; }
.stale { color: var(--neg); }

.upd-actions { position: sticky; bottom: 0; background: var(--bg-card);
  padding: 0.9rem 0 0.2rem; border-top: 1px solid var(--line); margin-top: 0.5rem; }
.upd-summary { color: var(--fg-dim); font-size: 0.88rem; }

@media (max-width: 40rem) {
  /* The money boxes keep enough room for the figure they are pre-filled with;
     a share count and a cost basis are shorter and give the room back, which
     is what lets each of them clear the pinned columns either side on a phone. */
  table.upd input.bal, table.upd input.hbal, table.upd input.uval { width: 7rem; }
  table.upd input.uqty, table.upd input.ubasis { width: 5rem; }
}

/* ── Holdings & prices ───────────────────────────────────────────────────── */
h3 { margin: 1.2rem 0 0.2rem; font-size: 1rem; }
#ad-mode-box { margin: 1rem 0 0.4rem; padding-top: 0.9rem; border-top: 1px solid var(--grid); }
#ad-mode-box .sub { margin: 0 0 0.3rem; }

.pill.stale { border-color: var(--neg); color: var(--neg); }
.pill.fresh { border-color: var(--accent-dim); color: var(--pos); }
.pill.parked { border-color: var(--neg); color: var(--neg); }
.pill.off { opacity: 0.65; }

dialog {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-card);
  color: var(--fg);
  padding: var(--pad);
  max-width: min(46rem, 94vw);
  width: 100%;
}
dialog::backdrop { background: rgb(0 0 0 / 0.55); }
dialog h2 { margin-top: 0; }
#pos-table input, #pos-table select { width: 100%; padding: 0.35rem 0.45rem; font-size: 0.88rem; }
#pos-table td { padding: 0.25rem 0.35rem; }
#pos-table .qty, #pos-table .basis { text-align: right; font-variant-numeric: tabular-nums; }
#pos-table .sym { text-transform: uppercase; }
.rm { background: none; border: 1px solid var(--line); color: var(--fg-dim);
      border-radius: 6px; padding: 0.15rem 0.45rem; line-height: 1; }
.rm:hover { color: var(--neg); border-color: var(--neg); }

/* ── Register: flag for review ───────────────────────────────────────────────
   A marker, not a filter or a nag — it only ever means "the owner asked to
   look at this again". Quiet until touched: barely-there outline colour at
   rest, full colour once flagged, and never louder than the row it sits in. */
.flag {
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: none; box-shadow: none; padding: 0.15rem;
  color: var(--fg-dim); opacity: 0.35; cursor: pointer;
  border-radius: var(--radius-sm);
}
.flag:hover, .flag:focus-visible { opacity: 1; }
.flag:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.flag[aria-pressed="true"] { color: var(--danger); opacity: 1; }
/* The row itself carries a whisper of the same colour, so a flagged
   transaction reads at a glance without a second glance at the icon. */
#rg-table tr[data-flagged] td:first-child { box-shadow: inset 2px 0 0 var(--danger); }

/* ── Budget & register ───────────────────────────────────────────────────── */
/* ── Month navigation ─────────────────────────────────────────────────────────
   Two bare arrows and an underlined "This month" read as leftover markup rather
   than as the control that moves the whole screen through time. The arrows
   become real square buttons either side of the label, and the label is the
   heading it deserves to be. */
.monthnav { display: flex; align-items: center; gap: 0.5rem; }
.monthnav button.link {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.1rem; height: 2.1rem; padding: 0; font-size: 1rem; line-height: 1;
  text-decoration: none; cursor: pointer;
  color: var(--fg); background: var(--bg-card);
  border: var(--bw) solid var(--line); border-radius: var(--radius-sm);
  box-shadow: var(--shadow-btn);
  transition: box-shadow 0.1s ease-out, transform 0.1s ease-out;
}
.monthnav button.link:hover {
  box-shadow: var(--shadow-hover); transform: translate(1px, 1px); text-decoration: none;
}
/* "This month" is a word, not a glyph, so it keeps its width. */
.monthnav #bg-today { width: auto; padding: 0 0.7rem; font-size: 0.78rem; font-weight: 600; }
.monthlabel {
  font-size: 1.15rem; font-weight: 650; min-width: 7.5rem; text-align: center;
  font-variant-numeric: tabular-nums;
}
/* The count on the Uncategorized nav item. Small and seated after the label;
   hidden entirely at zero — a "0" badge is a nag about nothing. */
.navitem .navcount { margin-left: auto; font-size: 0.68rem; padding: 0 0.4rem; }

/* ── Long-press targets ───────────────────────────────────────────────────────
   Everything that opens a context menu on right-click opens it on long-press
   too, and a long press on selectable text is ALSO the gesture for "select
   this word" — so the menu arrived with half a row highlighted behind it and
   a selection handle to dismiss. Suppressed on TOUCH DEVICES ONLY: a mouse
   right-click never starts a selection, so desktop keeps the ability to
   select and copy a figure out of the table.

   -webkit-touch-callout also goes, or iOS Safari raises its own share sheet
   over the app's menu. The editable cells opt back IN below: tapping into an
   inline editor has to be able to place a caret and select what is there. */
@media (hover: none) {
  #side-accounts .acctrow,
  #bg-table tbody tr[data-cat], #bg-table tr.grouprow,
  #ib-table tbody tr[data-cat],
  .target-btn {
    -webkit-touch-callout: none; -webkit-user-select: none; user-select: none;
  }
  #bg-table input, #ib-table input, #bg-table .editable, #rg-table .editable,
  .memoline {
    -webkit-user-select: text; user-select: text;
  }
}

.rta { text-align: right; }
.rta b { display: block; font-size: 1.5rem; font-weight: 700; font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em; line-height: 1.15; }
.rta span { display: block; color: var(--fg-dim); font-size: 0.78rem; line-height: 1.3;
  text-transform: uppercase; letter-spacing: 0.05em; }
.rta.over b { color: var(--neg); }
.rta.ok b { color: var(--pos); }
/* Money that has not arrived yet. Deliberately smaller and quieter than the
   headline it sits under: it is context for that figure, not a rival to it,
   and it is never part of it. */
.rta .upcoming { display: block; margin-top: 0.1rem; text-transform: none;
  letter-spacing: 0; font-size: 0.78rem; line-height: 1.3; color: var(--fg-dim); }
.rta .upcoming b { display: inline; font-size: inherit; font-weight: 650;
  letter-spacing: 0; }
.rta .upcoming b.pos { color: var(--pos); }
.rta .upcoming b.neg { color: var(--neg); }
/* The emergency fund badge, directly under Ready to Assign. Smaller than the
   headline on purpose: one is this month's working figure, the other is the
   standing cushion, and they must not read as rivals. */
.rta.efund { margin-top: 0.45rem; }
.rta.efund > b { color: var(--pos); font-size: 1.15rem; }
.rta .efparts { display: block; margin-top: 0.1rem; text-transform: none;
  letter-spacing: 0; font-size: 0.78rem; line-height: 1.3; color: var(--fg-dim); }
.rta .efparts b { display: inline; font-size: inherit; font-weight: 650;
  letter-spacing: 0; }

table.budget tr.grouprow td { background: var(--bg-sunken); font-weight: 750; font-size: 0.8rem;
  text-transform: uppercase; letter-spacing: 0.04em; color: var(--fg-dim); }
table.budget input.assign { width: 7.5rem; text-align: right; padding: 0.3rem 0.45rem;
  font-variant-numeric: tabular-nums; }
table.budget td { padding: 0.3rem 0.6rem; }
/* NOT HAPPENED YET. A row dated after today is italic and quiet until the day
   it comes of age. No stored flag decides this — the date already is the fact,
   and a boolean beside it could only ever disagree with it. */
#rg-table tr[data-future] td { font-style: italic; color: var(--fg-dim); }
#rg-table tr[data-future] td.money { opacity: 0.85; }

/* The cleared/reconciled column is off unless asked for. Every imported row
   arrives 'reconciled', so it read the same word all the way down the register
   and told nobody anything except while actually reconciling an account. */
body:not([data-cleared]) .clearedcol { display: none; }

/* A SPLIT IS ONE PURCHASE WITH ITS PARTS UNDER IT. The header carries the date,
   the payee and the total the shop charged; the legs below carry only what
   differs between them — the category and its share. The rule under the header
   is dropped so the group reads as one block, and the last leg keeps its rule
   to close it off. The legs are indented and tinted, which is the whole
   difference between "two categories of one shop" and "two shops". */
#rg-table tr.splithead td { border-bottom: none; }
/* nth-child(2), not first-child: the date is the second cell now that the flag
   column leads the row. */
#rg-table tr.splithead td:nth-child(2) { font-weight: 600; }
/* Tinted, but only just. At the full --bg-sunken the legs read as a solid block
   of colour four columns wide — most of it under the empty date, account and
   payee cells — which is louder than the thing it is grouping. */
#rg-table tr.splitleg td {
  border-bottom: none;
  background: color-mix(in srgb, var(--bg-sunken) 45%, transparent);
}
#rg-table tr.splitleg:not(:has(+ tr.splitleg)) td { border-bottom: 1px solid var(--grid); }
#rg-table tr.splitleg td.legcat { padding-left: 2rem; position: relative; }
/* The elbow. Drawn rather than a character, so it lines up with the text at any
   font size and cannot be selected or read aloud as content. */
#rg-table tr.splitleg td.legcat::before {
  content: ''; position: absolute; left: 0.9rem; top: 0; bottom: 0;
  border-left: 2px solid var(--line);
}
#rg-table tr.splitleg td.money { color: var(--fg-dim); }

/* COLOUR, NOT WEIGHT. The sign is the verdict and the colour carries it; a
   whole column of semi-bold figures made every category row shout as loudly as
   the group headings above them, so the headings stopped reading as headings.
   The group rows and Ready to Assign are bold in their own rules, which is
   where the emphasis belongs. */
.avail-pos { color: var(--pos); }
.avail-neg { color: var(--neg); }

/* THE AVAILABLE FIGURE IS A BUTTON, and it has to keep looking like a figure —
   a column of money that suddenly grew boxes would read as a form, not as a
   total. Bare until hovered, then a quiet fill and the pointer, which is the
   same bargain the resize grips make.
   .actbtn (the Activity figure) shares this shape rule rather than repeating
   it: same bare-until-hovered treatment, same reason. */
.availbtn, .actbtn {
  background: none; border: 1px solid transparent; border-radius: var(--radius-sm);
  padding: 0.1rem 0.35rem; margin: -0.1rem -0.35rem;
  /* font-family/size, NOT the `font` shorthand: the shorthand resets
     font-weight, which is how .avail-pos and .avail-neg draw a figure that has
     money in it. */
  font-family: inherit; font-size: inherit; line-height: inherit;
  /* ...and the weight comes from the row, not from `button`, which this sheet
     sets to 600 for every control on the site. A figure in a column of figures
     is not a control's label. */
  font-weight: inherit;
  font-variant-numeric: tabular-nums; cursor: pointer; box-shadow: none;
}
/* THE FIGURE KEEPS ITS COLOUR. Green in credit, red overspent — that is what
   the column is read for, and a `color: inherit` here (or a button's own UA
   colour) flattens the whole lot to body text, which is exactly what happened
   the first time this was a button. .avail-pos / .avail-neg supply both the
   colour and the weight; only the zero case has nothing to say. */
.availbtn.avail-zero { color: inherit; }
/* Activity does not get its own pos/neg pair — it reuses the plain `.neg`
   the register already colours negative amounts with, exactly the way a
   register row never gets a `.pos` either (see the row template in app.js).
   The two-class selector beats `.neg` on specificity regardless of source
   order, so a positive activity figure is not left wearing a button's own UA
   text colour -- the same bug .avail-zero exists to avoid, one class over. */
.actbtn:not(.neg) { color: inherit; }
.availbtn:hover, .availbtn:focus-visible,
.actbtn:hover, .actbtn:focus-visible {
  background: var(--bg-sunken); border-color: var(--line);
}
/* Its own width, because the menu is a small form and the ctxmenu default is
   sized for a list of verbs. */
/* .menu.availmenu, not .availmenu: .ctxmenu sets width:max-content further down
   the file and would otherwise win on order. */
.menu.availmenu { width: 19rem; max-width: min(19rem, 92vw); padding: 0.5rem; }
.availmenu .menuhead { display: flex; justify-content: space-between; gap: 0.8rem; }
.availmenu label {
  display: block; margin: 0.5rem 0 0.2rem; font-size: 0.74rem; font-weight: 650;
  text-transform: uppercase; letter-spacing: var(--label-track); color: var(--fg-dim);
}
.availmenu input {
  width: 100%; font: inherit; font-size: 0.9rem; padding: 0.4rem 0.5rem;
  border: var(--bw) solid var(--line); border-radius: var(--radius-sm);
  background: var(--bg-sunken); color: var(--fg);
}
.availmenu #avail-amt { text-align: right; font-variant-numeric: tabular-nums; }
.availmenu .hint { margin: 0.5rem 0 0; font-size: 0.74rem; }
.availmenu .err { margin: 0.4rem 0 0; }
/* The suggestion list hangs off the search box, so the box is the frame. */
.availsearch { position: relative; }
/* Capped by JS to the room actually left below it; this is the ceiling for a
   tall window, and the scrolling the cap depends on. */
.availmenu .suggest { max-height: 40vh; overflow-y: auto; overscroll-behavior: contain; }
.availmenu .suggest button { justify-content: space-between; }
/* A source with nothing in it. Dimmed rather than hidden: "why is that not in
   the list" is a worse question than seeing it there at $0.00. */
.availmenu .suggest button[data-empty] { opacity: 0.5; }
/* What the last move actually did — a result, not a caption, so it is body
   colour with the accent marking it as new since the menu opened. */
.availmenu .note {
  color: var(--fg); border-left: 2px solid var(--accent);
  padding-left: 0.5rem; margin: 0.35rem 0 0.1rem;
}
.availmenu .sg-name { overflow: hidden; text-overflow: ellipsis; }
.availmenu .sg-amt { color: var(--fg-dim); flex: none; font-variant-numeric: tabular-nums; }
.avail-zero { color: var(--fg-dim); }

/* ── The Activity sheet ───────────────────────────────────────────────────────
   Behind an Activity figure: the rows categoryActivity() actually summed, so
   the sheet can be checked against the figure that opened it instead of
   trusted on faith. Drawn by openActivity()/activityRow() in app.js -- this
   file only styles what those already computed. */
#act-dialog { max-width: min(30rem, 94vw); }
#act-sub { margin-bottom: 0.2rem; }
.act-total {
  font-size: 1.5rem; font-weight: 700; margin: 0 0 1rem;
  font-variant-numeric: tabular-nums;
}
/* Scrolls INSIDE the sheet rather than growing it past the viewport -- a
   category with eighty rows in it must not push Close off-screen. */
.act-list {
  max-height: 60vh; overflow-y: auto; overscroll-behavior: contain;
  display: flex; flex-direction: column;
}
.act-row { padding: 0.45rem 0; border-bottom: 1px solid var(--line); }
.act-row:last-child { border-bottom: none; }
.act-line1 {
  display: grid; grid-template-columns: auto 1fr auto;
  gap: 0.5rem; align-items: baseline;
}
/* auto, not a fixed rem: the date is nowrap tabular-nums, so the column sizes
   to it exactly -- a fixed 4.2rem clipped "09/24/2026" into the payee next to
   it the moment the year made the date four digits wider than guessed. */
.act-date { white-space: nowrap; font-variant-numeric: tabular-nums; color: var(--fg-dim); }
.act-payee { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.act-amt { text-align: right; }
.act-line2 {
  color: var(--fg-dim); font-size: 0.8rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A row dated after today has not happened yet — same rule and the same
   look the register already uses for a future-dated entry. */
.act-row[data-future] { font-style: italic; color: var(--fg-dim); }
.act-row[data-future] .money { opacity: 0.85; }

/* THE ANIMATION. The sheet grows out of the figure that was clicked:
   openActivity() sets transform-origin to that figure's centre (via the
   CSSOM — this CSP blocks a style="" attribute outright), so the scale runs
   from there instead of the dialog's own middle, which is what makes it read
   as "coming from" the row rather than just fading in over it. Close is the
   reverse — closeActivity() adds .closing, which plays act-out, and waits for
   animationend (or its own 300ms fallback) before actually calling
   dlg.close(). */
@keyframes act-in {
  from { transform: scale(0.9); opacity: 0; }
  to   { transform: scale(1);   opacity: 1; }
}
@keyframes act-out {
  from { transform: scale(1);   opacity: 1; }
  to   { transform: scale(0.9); opacity: 0; }
}
@keyframes act-backdrop-in  { from { opacity: 0; } to { opacity: 1; } }
@keyframes act-backdrop-out { from { opacity: 1; } to { opacity: 0; } }
#act-dialog[open] { animation: act-in 220ms cubic-bezier(.2, .8, .2, 1); }
#act-dialog.closing { animation: act-out 220ms cubic-bezier(.2, .8, .2, 1); }
#act-dialog[open]::backdrop { animation: act-backdrop-in 220ms cubic-bezier(.2, .8, .2, 1); }
#act-dialog.closing::backdrop { animation: act-backdrop-out 220ms cubic-bezier(.2, .8, .2, 1); }

/* A PHONE GETS A BOTTOM SHEET, not a floating card — pinned to the bottom
   edge, full width, rounded only on top, sliding up from off-screen rather
   than scaling from a point a thumb cannot see past the keyboard.
   `display: flex` is scoped to [open]: this ID selector otherwise outranks
   the UA rule that hides a dialog with no `open` attribute at all, which
   would leave the sheet permanently on screen. */
@media (max-width: 40rem) {
  #act-dialog {
    margin: auto 0 0; width: 100%; max-width: none;
    border-radius: var(--radius) var(--radius) 0 0;
    max-height: 85vh;
  }
  #act-dialog[open] { display: flex; flex-direction: column; }
  /* The list takes whatever room the sheet itself has left, not a fixed vh
     figure — on a phone the sheet's OWN height is the budget. */
  .act-list { max-height: none; flex: 1 1 auto; min-height: 0; }
  @keyframes act-up   { from { transform: translateY(100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
  @keyframes act-down { from { transform: translateY(0); opacity: 1; } to { transform: translateY(100%); opacity: 0; } }
  #act-dialog[open] { animation: act-up 220ms cubic-bezier(.2, .8, .2, 1); }
  #act-dialog.closing { animation: act-down 220ms cubic-bezier(.2, .8, .2, 1); }
}

/* Reduced motion drops every transform and animation — the sheet simply
   appears and disappears. closeActivity() checks the same media query so its
   close path never waits on an animationend this rule guarantees will not
   fire; placed last so it wins the cascade tie against the phone block above
   when both conditions are true at once. */
@media (prefers-reduced-motion: reduce) {
  #act-dialog[open], #act-dialog.closing,
  #act-dialog[open]::backdrop, #act-dialog.closing::backdrop { animation: none; }
}

/* Date, account, payee, category, memo, outflow, inflow, Add — one row.
   Explicit fractions rather than auto-fit so the columns keep a sensible
   relationship as the pane narrows; below ~64rem of CARD it becomes two rows
   rather than eight cramped ones.
   The breakpoints are CONTAINER queries on the form, not viewport ones
   (ledger#29). The minimum tracks below add up to ~63rem, and the viewport
   rule that used to sit at 62rem measured the window, not the card — once the
   sidebar had taken its 16rem the form went back to one row before it
   actually fit, and from ~62rem to ~1300px the Add button was clipped at the
   card's edge reading "Ad". */
.txnform { container-type: inline-size; }
.txnrow {
  display: grid; gap: 0.4rem; align-items: center;
  grid-template-columns: 9.5rem 10rem minmax(7rem, 1.2fr) minmax(9rem, 1.4fr)
                         minmax(6rem, 1fr) 7rem 7rem auto;
}
.txnrow input, .txnrow select { padding: 0.45rem 0.5rem; font-size: 0.88rem; }
/* The payee type-ahead. The wrapper takes the input's grid slot so the popup
   can anchor to it; the popup itself paints like the chart readout. */
.payeewrap { position: relative; min-width: 0; }
.payeewrap input { width: 100%; }
.suggest {
  position: absolute; top: calc(100% + 2px); left: 0; min-width: 100%; z-index: 6;
  background: var(--bg-raised); border: 1px solid var(--line); border-radius: var(--radius-sm, 8px);
  box-shadow: 0 0.4rem 1.2rem rgb(0 0 0 / 0.25); overflow: hidden;
}
.suggest button {
  display: flex; align-items: center; gap: 0.45rem; width: 100%; text-align: left;
  background: none; border: none; border-radius: 0; box-shadow: none;
  padding: 0.45rem 0.6rem; font-size: 0.85rem; color: var(--fg); cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.suggest button:hover, .suggest button[data-on] { background: var(--bg-sunken); }
.suggest .sg-tag {
  font-size: 0.62rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--accent); border: 1px solid var(--accent); border-radius: 999px;
  padding: 0 0.4rem; flex: none;
}
/* The category type-ahead's group label -- the optgroup a match came from,
   pushed to the far edge of the row rather than competing with the name. */
.suggest .sg-group { color: var(--fg-dim); font-size: 0.72rem; margin-left: auto; flex: none; }
/* The note under a payee. Quiet when present; the affordance to add one is
   quieter still, and always there — hover-only does not exist on a phone. */
.memoline { display: block; font-size: 0.74rem; color: var(--fg-dim); cursor: pointer;
  /* white-space: normal, or the table's blanket nowrap wins and a long note
     sets the whole payee column's minimum width — the register then scrolls
     sideways to honour one sentence. The note wraps; the table fits. */
  white-space: normal; overflow-wrap: anywhere; }
/* Always visible, owner's ask: the affordance to add a note should not need
   discovering. Quiet at rest, a touch brighter under the pointer. */
.memoline.empty { opacity: 0.5; font-style: italic; }
#rg-table tr:hover .memoline.empty, .memoline.empty:focus-visible,
.memoline.empty:hover { opacity: 0.85; }
.memoinput { width: 100%; font-size: 0.78rem; }
.txnrow .num { text-align: right; font-variant-numeric: tabular-nums; }
.txnrow button { padding: 0.5rem 1.1rem; white-space: nowrap; }
.txnfoot { display: flex; align-items: center; gap: 0.8rem; margin-top: 0.5rem; flex-wrap: wrap; }
/* The category a budget-crossing transfer owes the budget. */
.xfercat { display: flex; align-items: center; gap: 0.4rem; margin: 0; font-size: 0.8rem; }
.xfercat[hidden] { display: none; }
.xfercat > span { color: var(--fg-dim); white-space: nowrap; }
.xfercat select { padding: 0.25rem 0.4rem; font-size: 0.82rem; max-width: 14rem; }
@container (max-width: 64rem) {
  .txnrow { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .txnrow button { grid-column: span 4; }
}
/* FOUR COLUMNS OF A 358px CARD IS 85px EACH, and a select that wide reads
   "Ally C" and "Uncat" while the date field hides its year behind the calendar
   icon. Two columns is the most a phone can say anything in. The memo is the
   one field whose value is prose, so it takes the full width and lets outflow
   and inflow pair up. */
@container (max-width: 40rem) {
  .txnrow { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .txnrow #tx-memo, .txnrow button { grid-column: span 2; }
  .txnrow input[type="date"] { max-width: none; }
}
.filters { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }
.filters { flex: 1 1 auto; justify-content: flex-end; }
.filters input { width: auto; min-width: 14rem; }
/* "Search payee, memo, catego" -- the box was narrower than the placeholder it
   ships with, with spare room on the card either side of it. */
#rg-search { flex: 0 1 24rem; min-width: 17rem; }
/* ...but a CHECKBOX in a filter bar is not a search box. The min-width above
   was stretching the tick to 14rem, so its label sat a hand's width away from
   the box it belonged to and read as unrelated text. */
.filters .check input[type="checkbox"] { min-width: 0; }
.filters .check { gap: 0.4rem; white-space: nowrap; }

/* The fund name under a ticker. A wrong-but-valid ticker prices perfectly and
   is invisible until a statement disagrees, so the name is the check. */
.symname { font-size: 0.75rem; color: var(--fg-dim); margin-top: 0.15rem; min-height: 1em; }
.symname.found { color: var(--pos); }
.symname.missing { color: var(--neg); }

/* The derived per-share cost, shown next to the TOTAL cost basis. Typing a
   per-share figure into the total field makes this come out absurdly small,
   which is visible at a glance in a way a column heading is not. */
.pershare { font-size: 0.72rem; color: var(--fg-dim); margin-top: 0.15rem; min-height: 1em;
  text-align: right; font-variant-numeric: tabular-nums; }

/* Where a cash quantity nobody typed came from: the stored figure plus whatever
   has arrived by transfer since the last positions save. Same weight and colour
   as the per-share caption above, because it answers the same kind of question
   about the figure beside it. */
/* CAPPED, BECAUSE A CAPTION MUST NOT SET A COLUMN'S WIDTH. Left to size
   itself the sentence is one 257px line, which drags the quantity column from
   99px to 276px and pushes the whole table a further 170px into its horizontal
   scroll on a phone -- the figure it is annotating then scrolls off. It wraps
   instead: wider than the column on a desktop, narrow and two or three lines on
   a phone, and the number stays where it was either way. */
/* And it WRAPS, which a figure never does: table.grid td is nowrap, so without
   this the sentence runs straight out of its cell and over the price beside it.
   Same rule as the auto-assign dialog above -- descriptive columns wrap, the
   figures still do not. */
.cashnote {
  font-size: 0.72rem; color: var(--fg-dim); margin-top: 0.15rem;
  max-width: 17rem; white-space: normal;
}
/* Right-aligned under the figure it annotates, in both tables. */
#pos-table .cashnote, #ad-positions .cashnote { text-align: right; margin-left: auto; }
/* The editor's quantity column is only as wide as its input, so there the
   caption needs a FLOOR as well as a ceiling -- without one it broke over four
   lines in an 88px ribbon on a 736px dialog. */
#pos-table .cashnote { min-width: 13rem; }
@media (max-width: 40rem) {
  /* Narrow enough not to widen the table much, wide enough that the sentence
     reads as a sentence: at the quantity column's own width it broke over four
     lines in an 80px ribbon. The editor's rule is repeated rather than
     inherited, because an id selector outranks a media query. */
  .cashnote { max-width: 9rem; min-width: 8rem; }
  #pos-table .cashnote { max-width: 9rem; min-width: 8rem; }
}

/* THE CONTROLS IN A POSITION ROW ALL START AT THE SAME LINE.
   Two of the four cells carry a caption underneath -- the fund name and the
   derived per-share cost -- which makes those cells taller than the other two.
   A td centres its content vertically by default, so the type and quantity
   boxes sank by half a caption relative to the symbol and cost boxes, and the
   row read as four controls at three different heights.
   Aligning to the top fixes the cause rather than nudging margins, and the
   captions reserve their line whether or not they have text, so the row does
   not jump when a ticker resolves to a name. */
#pos-table td { vertical-align: top; }
#pos-table td input,
#pos-table td select {
  /* Selects and inputs disagree about intrinsic height by a pixel or two, and
     at this size that reads as a misalignment rather than as nothing. */
  height: 2.45rem;
  width: 100%;
}
#pos-table td:last-child { width: 1%; white-space: nowrap; }
#pos-table td:last-child .rm { margin-top: 0.35rem; }

/* ── The three-pane shell ─────────────────────────────────────────────────────
   Sidebar (nav + accounts) | workspace | inspector.

   Each pane scrolls independently: the account list is long, the budget is
   longer, and a single page scroll means losing your place in one to read the
   other. `min-height: 0` on the grid children is what actually allows that --
   without it a grid item refuses to shrink below its content and the inner
   overflow never engages. */
#app[data-active] { display: block; height: 100dvh; overflow: hidden; }

/* The detail pane is CLOSED by default and slides in when something is
   selected. An always-present pane reading "pick something on the left" is a
   fifth of the screen spent saying nothing.
   grid-template-columns is animatable, so the pane genuinely slides rather
   than appearing; the inspector clips its own content so the text does not
   reflow visibly while it moves. */
.shell {
  display: grid;
  grid-template-columns: 16rem minmax(0, 1fr) 0;
  height: 100dvh;
  overflow: hidden;
  transition: grid-template-columns 0.22s ease;
}
.shell[data-inspector] { grid-template-columns: 16rem minmax(0, 1fr) 21rem; }
@media (prefers-reduced-motion: reduce) { .shell { transition: none; } }

.sidebar {
  grid-column: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  background: var(--bg-card);
  border-right: 1px solid var(--line);
  overflow: hidden;
}
.side-top {
  display: flex; align-items: center; justify-content: space-between;
  padding: 0.9rem 1rem 0.7rem;
}
#side-close { display: none; font-size: 1.2rem; line-height: 1; padding: 0.2rem 0.5rem; }

.mainnav { display: grid; gap: 1px; padding: 0 0.5rem 0.6rem; }
.navitem {
  display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem;
  background: none; border: none; color: var(--fg-dim); text-align: left;
  padding: 0.45rem 0.6rem; border-radius: 7px; font-size: 0.9rem; width: 100%;
}
.navitem:hover { background: var(--bg-sunken); color: var(--fg); }
.navitem[data-on] { background: var(--bg-sunken); color: var(--fg); font-weight: 650; }

/* The account list is its OWN box, and the border says so. Without a top edge
   it ran straight on from the nav above it, so the scroll -- which belongs to
   the accounts alone -- looked like it might carry Net worth and Budget away
   with it. The footer already had a border; this is its other half. */
/* THE LIST IS SCROLLABLE AND HAS TO LOOK IT. Sliced dead level by the ORGANISE
   footer it read as truncated -- as though those were all the accounts there
   were. The mask fades the last few pixels of whatever is under the edge, which
   works on any ground (the glass wash included) in a way a gradient painted in
   --bg-card does not; the bottom padding is deeper than the fade, so once you
   reach the end the last row is clear of it and the fade has gone. The thin
   scrollbar is the other half: an overlay bar that only appears while dragging
   tells you nothing before you drag. */
.side-accounts {
  flex: 1; min-height: 0; overflow-y: auto; padding: 0.5rem 0.5rem 1.4rem;
  border-top: var(--bw) solid var(--line);
  scrollbar-width: thin;
  scrollbar-color: var(--line) transparent;
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 1rem), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 1rem), transparent);
}
.acctgroup { margin-top: 0.9rem; }
.acctgroup > h4 {
  display: flex; justify-content: space-between; gap: 0.5rem; align-items: baseline;
  flex-wrap: nowrap;
  margin: 0 0.6rem 0.25rem; font-size: 0.7rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em; color: var(--fg-dim);
}
.acctgroup > h4 > span:first-child {
  flex: 1; min-width: 0; overflow: hidden; display: flex; align-items: baseline; gap: 0.4rem;
  text-overflow: ellipsis; white-space: nowrap;
}
.acctgroup > h4 > span:last-child {
  /* Without flex-shrink:0 the total was squeezed to nothing by the rename and
     delete buttons appearing beside the name, and rendered on top of them. */
  flex: 0 0 auto; font-variant-numeric: tabular-nums; letter-spacing: 0; white-space: nowrap;
}
.acctrow {
  display: flex; justify-content: space-between; gap: 0.5rem; width: 100%;
  background: none; border: none; color: var(--fg); text-align: left;
  padding: 0.35rem 0.6rem; border-radius: 6px; font-size: 0.86rem;
}
.acctrow:hover { background: var(--bg-sunken); }
.acctrow[data-on] { background: var(--bg-sunken); font-weight: 650; }
.acctrow .an { flex: 1; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.acctrow .av { font-variant-numeric: tabular-nums; white-space: nowrap; }
.acctrow.closed .an { color: var(--fg-dim); }

.side-foot { padding: 0.6rem 1rem; border-top: 1px solid var(--line); }

/* min-WIDTH:0 as well as min-height:0, and it is not decoration. A grid and
   flex child defaults to min-width:auto, which means "never shrink below your
   content" — so a wide table pushed the whole workspace out from under itself
   and the register ran on beneath the inspector. The track was the right size
   all along; the content simply refused to be contained by it. */
.workspace {
  grid-column: 2;
  min-height: 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.topbar {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.6rem var(--pad);
  border-bottom: 1px solid var(--line);
  background: var(--bg-card);
  flex: none;
  /* Stated rather than left to the tallest thing inside it, because the
     inspector's strip continues this rule across the page and has nothing as
     tall as the avatar button to grow to. A rule that arrives at two heights
     is worse than no rule. */
  min-height: var(--topbar-h);
}
.crumb { font-weight: 650; }
.menubtn { display: none; font-size: 1.1rem; line-height: 1; padding: 0.3rem 0.55rem; }
.workspace .view { padding: var(--pad); overflow-y: auto; min-height: 0; min-width: 0; flex: 1; align-content: start; }
/* Same reason one level down: the cards are grid items too. */
.workspace .card, .workspace .setpanel, .setpanel > .card { min-width: 0; max-width: 100%; }

/* THE THIRD PANE IS PART OF THE SAME PAGE. It used to be a flat slab of card
   colour whose content began level with the top of the window -- so the topbar
   rule stopped dead at its left edge, its first heading floated ~56px above the
   first card in the workspace, and the one block on the site that was not a
   bordered card was the one summarising the screen beside it. Now it has the
   same ground as the workspace, its own header strip continuing the rule across
   the page, and blocks that are cards. */
.inspector {
  grid-column: 3;
  min-height: 0;
  min-width: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  background: none;
  border-left: 1px solid var(--line);
}
.insp-top {
  display: flex; align-items: center; flex: none;
  min-height: var(--topbar-h);
  width: 21rem;
  padding: 0.6rem var(--pad);
  border-bottom: 1px solid var(--line);
  background: var(--bg-card);
}
/* The padding lives on an inner wrapper so the pane can collapse to zero width
   without its own padding holding it open. */
.inspector > .insp-inner { padding: var(--pad); width: 21rem; flex: 1; min-height: 0; overflow-y: auto; }
.insp-close { float: right; background: none; border: none; color: var(--fg-dim);
  font-size: 1.2rem; line-height: 1; padding: 0 0.2rem; }
.insp-close:hover { color: var(--fg); }
.inspector h3 { margin-top: 0; }
.inspector .card { border: none; padding: 0; background: none; max-width: none; }
.insp-empty { color: var(--fg-dim); font-size: 0.88rem; }
.insp-block {
  padding: 1rem; margin-bottom: var(--pad);
  background: var(--bg-card);
  border: var(--bw) solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
}
.insp-block:last-child { margin-bottom: 0; }

/* The workspace cards no longer need their own frame width. */
.workspace .card.wide { max-width: none; }

@media (max-width: 75rem) {
  .shell, .shell[data-inspector] { grid-template-columns: 15rem minmax(0, 1fr); }
  .inspector { display: none; }
}
@media (max-width: 60rem) {
  /* BOTH selectors, and that is the whole fix. In a selector LIST each selector
     carries its own specificity, so `.shell[data-inspector]` in the 75rem block
     above scores (0,2,0) and outranks a bare `.shell` (0,1,0) here no matter
     how much later this block appears. Budget is the only view that sets
     data-inspector, so it was the only one where the phantom 15rem sidebar
     track came back on a phone — leaving the workspace 150px of a 390px screen
     while the sidebar itself was translated off-canvas and drawing nothing.
     Two explicit tracks rather than one, because .workspace is `grid-column: 2`
     and a single track just makes an implicit second one anyway. */
  .shell, .shell[data-inspector] { grid-template-columns: 0 minmax(0, 1fr); }
  .menubtn, #side-close { display: inline-block; }
  /* Drawer, not a column. Three panes on a phone is three unusable panes. */
  .sidebar {
    position: fixed; inset: 0 auto 0 0; width: min(18rem, 84vw); z-index: 20;
    transform: translateX(-100%); transition: transform 0.18s ease;
  }
  /* THE SHADOW BELONGS TO THE OPEN DRAWER, NOT TO THE ELEMENT. A 0 0 2rem
     spread reaches 2rem in every direction, so on a closed drawer -- which is
     translated fully off-canvas and drawing nothing -- it still bled back over
     the left edge of the screen as a permanent grey band down every page. */
  .sidebar[data-open] { transform: none; box-shadow: 0 0 2rem rgb(0 0 0 / 0.35); }
}

/* Sidebar organising */
.side-foot { display: block; }
.organise-tools {
  display: flex; flex-direction: column; align-items: flex-start; gap: 0.5rem;
  margin-bottom: 0.7rem;
}
.organise-tools .hint { margin: 0; font-size: 0.75rem; }
.organise-tools .check { margin: 0; }

/* The one control under the account list. It is a real button rather than a
   text link because it is the only thing there and it toggles a mode -- a link
   that changes the whole behaviour of the list above it undersells what it
   does. It reads its border, radius and shadow from the theme, so it is a hard
   offset block in brutalist and a soft tinted pill in the others. */
.organise-btn {
  width: 100%; padding: 0.5rem 0.8rem; cursor: pointer;
  font: inherit; font-size: 0.76rem;
  font-weight: var(--label-weight); letter-spacing: var(--label-track);
  text-transform: uppercase;
  color: var(--fg); background: var(--bg-card);
  border: var(--bw) solid var(--line); border-radius: var(--radius-sm);
  box-shadow: var(--shadow-btn);
  transition: box-shadow 0.1s ease-out, transform 0.1s ease-out;
}
.organise-btn:hover { box-shadow: var(--shadow-hover); transform: translate(1px, 1px); }
/* In the mode, the button is the way out of it, so it takes the active fill
   the nav uses -- the same visual language for "this is switched on". */
.sidebar[data-organise] .organise-btn {
  background: var(--nav-active-bg); color: var(--brut-on-accent, var(--fg));
}
.sidebar[data-organise] .acctrow { cursor: grab; }
.sidebar[data-organise] .acctrow::before,
.sidebar[data-organise] .acctgroup > h4::before {
  content: "⠿"; color: var(--fg-dim); margin-right: 0.4rem; font-size: 0.8em;
}
.sidebar[data-organise] .acctgroup > h4 { cursor: grab; }
.acctrow.dragging, .acctgroup.dragging { opacity: 0.4; }
/* The drop line. A border on the row itself rather than a floating indicator,
   so it cannot drift out of sync with what the drop will actually do. */
.acctrow.over-top { box-shadow: inset 0 2px 0 var(--accent); }
.acctrow.over-bottom { box-shadow: inset 0 -2px 0 var(--accent); }
.acctgroup.over { outline: 2px dashed var(--accent); outline-offset: 2px; border-radius: 8px; }
/* While rearranging, the group's total is not the question being asked and it
   is what the rename/delete buttons were colliding with. Hiding it gives the
   heading its whole width back for the name and the tools. */
.sidebar[data-organise] .acctgroup > h4 > span:last-child { display: none; }
.grouptools { display: none; gap: 0.3rem; flex: 0 0 auto; }
.sidebar[data-organise] .grouptools { display: inline-flex; }
.grouptools button { background: none; border: none; color: var(--fg-dim); font-size: 0.75rem; padding: 0 0.2rem; }
.grouptools button:hover { color: var(--fg); }

.splitsum { margin: 0.9rem 0 0.3rem; font-size: 0.9rem; display: grid; gap: 0.2rem; }
.splitsum b { font-variant-numeric: tabular-nums; }
#split-table input, #split-table select { width: 100%; padding: 0.35rem 0.45rem; font-size: 0.88rem; }
#split-table td { padding: 0.25rem 0.35rem; }
#split-table .amt { text-align: right; font-variant-numeric: tabular-nums; }

/* ── Account / settings menu ─────────────────────────────────────────────── */
.usermenu { position: relative; }
.userbtn {
  display: flex; align-items: center; gap: 0.45rem;
  background: none; border: 1px solid transparent; color: var(--fg);
  padding: 0.3rem 0.5rem; border-radius: 8px; font-size: 0.88rem;
}
.userbtn:hover { background: var(--bg-sunken); }
.userbtn[aria-expanded="true"] { background: var(--bg-sunken); border-color: var(--line); }
.avatar {
  width: 1.55rem; height: 1.55rem; border-radius: 50%; flex: none;
  background: var(--accent); color: #fff; display: grid; place-items: center;
  font-size: 0.72rem; font-weight: 700;
}
.caret { color: var(--fg-dim); font-size: 0.7rem; }
.menu {
  position: absolute; right: 0; top: calc(100% + 0.35rem); z-index: 30;
  min-width: 12rem; padding: 0.3rem;
  background: var(--bg-raised); border: 1px solid var(--line); border-radius: 10px;
  box-shadow: 0 0.5rem 1.5rem rgb(0 0 0 / 0.28);
  display: grid; gap: 1px;
}
.menu button {
  background: none; border: none; color: var(--fg); text-align: left;
  padding: 0.45rem 0.6rem; border-radius: 6px; font-size: 0.88rem; width: 100%;
}
.menu button:hover { background: var(--bg-sunken); }
.menusep { height: 1px; background: var(--line); margin: 0.25rem 0; }

.setgroup { padding-bottom: 1rem; margin-bottom: 1rem; border-bottom: 1px solid var(--line); }
.setgroup:last-of-type { border-bottom: none; }
.setgroup h3 { margin: 0 0 0.3rem; font-size: 0.95rem; }
#settings-dialog { max-width: min(34rem, 94vw); }
/* ONE JOB, ONE SIZE. Style and Appearance are the same kind of choice sitting
   one above the other, and they were 121px and 157px wide with chevrons to
   match. */
#set-theme, #set-mode { width: 14rem; max-width: 100%; }
/* THE NOTE BELONGS UNDER THE THING IT EXPLAINS. Flush to the card's left edge
   it started a tick's width left of the label it is about, so it read as a
   sentence about the section rather than about the checkbox above it. */
.check + .hint, .check + .sub { margin-left: calc(var(--check-size) + 0.4rem); }

.acctgroup .insp-empty { padding: 0 0.6rem; font-size: 0.78rem; }
.acctgroup.system > h4 { opacity: 0.75; }

/* Settings sub-navigation */
.subnav { display: flex; gap: 0.25rem; flex-wrap: wrap; margin-bottom: 0.2rem;
  border-bottom: 1px solid var(--line); padding-bottom: 0.6rem; }
/* EVERY TAB GETS A BOX. An inactive tab drawn as bare text beside a filled
   active one reads as a caption rather than as the other place you can go --
   and the range pills one card below already box every option. */
.subtab { background: var(--bg-card); border: var(--bw) solid var(--line); color: var(--fg-dim);
  padding: 0.35rem 0.75rem; border-radius: var(--radius-sm); font-size: 0.88rem; }
.subtab:hover { background: var(--bg-sunken); color: var(--fg); }
.subtab[data-on] { background: var(--nav-active-bg); color: var(--fg); font-weight: 650; border-color: var(--accent); }
.setpanel { display: grid; gap: var(--pad); }
.setpanel[hidden] { display: none; }

/* Right-click menu on an account. Positioned at the cursor and clamped to the
   viewport, because a menu that opens half off-screen is worse than none. */
/* right:auto is load-bearing: .menu sets right:0 for the account dropdown, and
   with a left coordinate ALSO set the element stretches from the cursor to the
   right edge of the window. width:max-content then keeps it as wide as its
   longest item rather than as wide as the grid will allow. */
.ctxmenu {
  position: fixed; right: auto; top: auto;
  width: max-content; min-width: 12rem; max-width: min(20rem, 90vw);
}
.ctxmenu .menuhead {
  padding: 0.35rem 0.6rem 0.4rem; font-size: 0.74rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.05em; color: var(--fg-dim);
  border-bottom: 1px solid var(--line); margin-bottom: 0.2rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ctxmenu button[disabled] { opacity: 0.45; cursor: not-allowed; }
.ctxmenu button[disabled]:hover { background: none; }
.ctxmenu .danger-item { color: var(--neg); }
.mi-note { float: right; color: var(--neg); font-size: 0.75rem; }
#bal-dialog { max-width: min(24rem, 94vw); }

/* The solve preview is seven columns wide and cannot rely on
   .card:has(table.grid){overflow-x:auto} -- a dialog is not a card. Without a
   scrollport of its own the table pushes the dialog wider than the viewport and
   the right-hand columns become unreachable on a phone, which is exactly the
   defect the positions editor already has (ui-audit-2026-08-21 #5). */
#solve-dialog { max-width: min(52rem, 94vw); }
.solvewrap { overflow-x: auto; }

/* The catch-up ramp dialog: a list of steps on top, the preview of the one
   selected below it. Same scrollport reasoning as the solve table, plus a cap
   on the list itself -- a household with years of history has more candidates
   than fit on a screen, and a dialog that grows past the viewport puts its own
   buttons out of reach. */
#ramp-dialog { max-width: min(46rem, 94vw); }
#ramp-table { font-size: 0.88rem; display: block; max-height: 13rem; overflow-y: auto; }
#ramp-table thead, #ramp-table tbody { display: table; width: 100%; table-layout: fixed; }
#ramp-table thead { position: sticky; top: 0; background: var(--bg-card); z-index: 1; }
/* A row IS the control that picks which step you are looking at, so it has to
   look pressable and say which one is selected. Hover and selection are
   different states: one is where the pointer is, the other is what the chart
   below is currently drawing. */
#ramp-table tbody tr { cursor: pointer; }
#ramp-table tbody tr:hover { background: var(--bg-sunken); }
#ramp-table tbody tr[data-on] {
  background: var(--bg-sunken);
  box-shadow: inset 3px 0 0 var(--accent);
}
.gaplbl { display: none; }
/* FIVE COLUMNS DO NOT FIT ON A PHONE, and a fixed layout does not tell you so
   — it overlaps them silently. At 390px the date ran through the account name
   and the amount ran through both, and the "spread" pill left the dialog
   entirely. Below 560px each row becomes a two-line block instead: what and
   how much on the first line, when and how long a gap on the second. Nothing
   is dropped; the header goes, so the gap cell says the word itself. */
/* 40rem, matching the app's existing phone breakpoint rather than inventing a
   second one -- between two breakpoints this table would be a five-column grid
   wearing the sticky-first-column treatment below, and the selected row would
   show the same white band that rule warns about. */
@media (max-width: 40rem) {
  #ramp-table { max-height: 40vh; }
  #ramp-table thead { display: none; }
  /* THE STICKY FIRST COLUMN IS FOR TABLES THAT SCROLL SIDEWAYS, and a stacked
     one does not. Left on, it paints bg-card over the selected row's highlight
     -- which is exactly the failure its own comment predicts. */
  #ramp-table tbody td:first-child { position: static; background: none; }
  #ramp-table tbody { display: block; }
  #ramp-table tbody tr {
    display: grid; grid-template-columns: 1fr auto;
    gap: 0 0.6rem; padding: 0.45rem 0.5rem; border-bottom: 1px solid var(--line);
  }
  #ramp-table tbody td { display: block; padding: 0; border: none; }
  #ramp-table tbody td:nth-child(1) { grid-area: 2 / 1; font-size: 0.78rem; color: var(--fg-dim); }
  #ramp-table tbody td:nth-child(2) { grid-area: 1 / 1; font-weight: 600; }
  #ramp-table tbody td:nth-child(3) { grid-area: 1 / 2; text-align: right; }
  #ramp-table tbody td:nth-child(4) { grid-area: 2 / 2; text-align: right; font-size: 0.78rem; color: var(--fg-dim); }
  /* The status cell is empty on every row that has not been spread, and an
     empty block still takes a line. */
  #ramp-table tbody td:nth-child(5) { grid-area: 3 / 1 / auto / -1; }
  #ramp-table tbody td:nth-child(5):empty { display: none; }
  #ramp-table tbody td:nth-child(5) .pill { margin-top: 0.3rem; }
  .gaplbl { display: inline; }
}
#ramp-dialog .chartwrap.sm { margin-top: 0.6rem; }
/* A secondary button sitting under a paragraph rather than beside a primary in
   a dialog's action row. Same tokens and the same border, so it is the site's
   button in every theme -- just sized for prose instead of for a decision. */
button.secondary.sm {
  padding: 0.35rem 0.7rem; font-size: 0.82rem; margin-top: 0.45rem;
}
#ramp-shape { margin-top: 0.9rem; }
/* The caution is not an error -- the entry is rampable, it just probably
   should not be -- so it reads as a warning rather than a failure. */
#ramp-caution { color: var(--neg); }
#solve-table { font-size: 0.88rem; }
#solve-table th, #solve-table td { padding-left: 0.4rem; padding-right: 0.4rem; }
/* The fund name and the dollar change each go on their own line, the same way
   a position row does on the update screen. Inline they run straight into the
   figure beside them -- "TIEIXNuveen Equity Index Fund R6" -- and the long
   names then force the first column wide enough to push price and staleness
   out of the dialog entirely. */
#solve-table .sub {
  display: block; font-size: 0.72rem; color: var(--fg-dim); font-weight: 400;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 13rem;
}
#solve-table td:first-child { font-weight: 600; }
#solve-table td.fixchk, #solve-table th:last-child { text-align: center; }
#solve-table td.fixchk input { margin: 0 auto; }
#solve-table td.was { color: var(--fg-dim); }
#solve-table tr.isfixed td.becomes { color: var(--fg-dim); }

@media (max-width: 40rem) {
  /* SEVEN COLUMNS DO NOT FIT A PHONE, and a horizontal scrollport with no
     affordance is not a fix -- the Fixed checkbox is the one control in this
     dialog and it was the column that fell off the edge. So the phone keeps
     the four columns a decision is made from (which holding, what it is now,
     what it becomes, hold it) and drops the three that are context. The price
     and its staleness still reach the reader: the summary line underneath
     names the oldest price used. */
  #solve-table .col-price, #solve-table .col-value, #solve-table .col-priced { display: none; }
  #solve-table .sub.fund { display: none; }
  #solve-table { font-size: 0.82rem; }
  #solve-table th, #solve-table td { padding-left: 0.25rem; padding-right: 0.25rem; }
}

/* Inline editing in the register. The affordance is deliberately quiet — a
   dotted underline on hover rather than a permanent box, because every row is
   editable and boxing them all turns the register into a form. */
td.editable { cursor: text; }
td.editable:hover { box-shadow: inset 0 -1px 0 var(--fg-dim); }
td .inline { width: 100%; min-width: 6rem; padding: 0.15rem 0.3rem; font: inherit;
  font-size: 0.88rem; background: var(--bg-sunken); border: 1px solid var(--accent);
  border-radius: 5px; color: var(--fg); }
td .inline.num { text-align: right; font-variant-numeric: tabular-nums; }

/* ── Multi-series chart ──────────────────────────────────────────────────── */
.chartbar { display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap; margin: 1rem 0 0.4rem; }
.legend { display: flex; gap: 0.9rem; font-size: 0.78rem; color: var(--fg-dim); }
.lg { display: inline-flex; align-items: center; gap: 0.35rem; white-space: nowrap; }
.lg::before { content: ""; width: 0.7rem; height: 0.18rem; border-radius: 2px; }
.lg-a::before { background: var(--accent); }
.lg-b::before { background: var(--ser-b); }
.lg-c::before { background: var(--neg); }

/* Net-worth pillars: green net worth up from zero, blue capping the column
   to assets, red hanging below the line for what is owed. The hovered column
   goes solid so the readout visibly belongs to it. */
.c-bar { fill: var(--accent); opacity: 0.75; }
/* Its own token, not --ser-b: brutalist restates that series colour to
   near-black, and the owner asked for BLUE assets in so many words. */
.c-bar.c-bar-asset { fill: var(--bar-asset, light-dark(#2563eb, #5b96f5)); opacity: 0.5; }
.c-bar.c-bar-liab { fill: var(--neg); opacity: 0.55; }
.c-bar[data-on] { opacity: 1; }
.c-bar.c-bar-asset[data-on], .c-bar.c-bar-liab[data-on] { opacity: 0.85; }
/* Year boundaries on the quarterly zooms: a firmer vertical than the money
   grid, with the year named at its top. */
.c-ytick { stroke: var(--fg-dim); stroke-width: 1; opacity: 0.45; }
.c-ytick-lbl { text-anchor: start; }

:root { --ser-b: light-dark(#1d4ed8, #3b82f6); }

.c-line.c-a { stroke: var(--accent); }
.c-line.c-b { stroke: var(--ser-b); }
.c-line.c-c { stroke: var(--neg); }
.c-area.c-a { fill: var(--accent); }
.c-dot.c-a, .c-hdot.c-a { fill: var(--accent); }
.c-dot.c-b, .c-hdot.c-b { fill: var(--ser-b); }
.c-dot.c-c, .c-hdot.c-c { fill: var(--neg); }
.c-rule { stroke: var(--fg-dim); stroke-width: 1; stroke-dasharray: 3 3; vector-effect: non-scaling-stroke; }
.c-hdot { stroke: var(--bg-card); stroke-width: 2; }
.c-capture { fill: transparent; cursor: crosshair; }

/* The hover readout. Positioned by JS via CSSOM, which the CSP allows —
   only inline style ATTRIBUTES in markup are blocked, not CSSOM writes. */
.chartwrap { position: relative; }
/* Seven ranges is more than fits beside a headline on a phone, and a row that
   scrolls sideways hides the ones on the end. */
.range { flex-wrap: wrap; justify-content: flex-end; }
.readout {
  /* ABOVE the plot, not on it. Anchored inside, the box blanketed a
     phone-height chart entirely — a hover that hides the line it describes.
     Hanging off the top edge it covers header chrome nobody is reading
     mid-hover, and the curve stays visible under the crosshair. */
  position: absolute; bottom: calc(100% + 0.3rem); top: auto; pointer-events: none;
  background: var(--bg-raised); border: 1px solid var(--line); border-radius: 8px;
  padding: 0.5rem 0.65rem; font-size: 0.8rem; min-width: 10rem;
  box-shadow: 0 0.4rem 1.2rem rgb(0 0 0 / 0.25); z-index: 2;
  /* NEVER WIDER THAN THE CHART IT SITS ON. The horizontal clamp pins it to the
     left edge when it does not fit, so on a phone a long label ran straight off
     the right of the card. The label column shrinks and wraps instead; the
     figures keep their own column and stay readable. */
  max-width: 100%;
}
.readout .rd-date { color: var(--fg-dim); font-size: 0.72rem; margin-bottom: 0.3rem; }
.readout dl { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 0.15rem 0.8rem; margin: 0; }
.readout dt { display: inline-flex; align-items: center; gap: 0.35rem; color: var(--fg-dim);
  min-width: 0; }
.readout dt::before { content: ""; width: 0.6rem; height: 0.16rem; border-radius: 2px; }
.readout dt.k-a::before { background: var(--accent); }
/* Assets: the same blue as the pillar segment it points at, in every theme —
   --ser-b is restated near-black by brutalist and no longer matches a chart. */
.readout dt.k-b::before { background: var(--bar-asset, light-dark(#2563eb, #5b96f5)); }
.readout dt.k-c::before { background: var(--neg); }
/* A change row names no series, so it gets no colour swatch -- but it keeps the
   swatch's width, or its label starts a few pixels left of every other one. */
.readout dt.k-none::before { background: none; }
.readout dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; font-weight: 650; }

/* ══ Themes ═══════════════════════════════════════════════════════════════════
   Per PERSON, not per site: two adults share this login and read the same
   numbers, and there is no reason they have to look at them the same way.

   Both fonts are self-hosted. The CSP here is font-src 'self', and relaxing it
   to reach Google would have traded a real boundary for a convenience on a
   site that is deliberately LAN-only. Latin subset, variable weight, ~55 KB
   for the pair. */
@font-face {
  font-family: 'Space Grotesk'; font-style: normal; font-weight: 300 700;
  font-display: swap; src: url('/fonts/spacegrotesk.woff2') format('woff2');
}
@font-face {
  font-family: 'Archivo'; font-style: normal; font-weight: 100 900;
  font-display: swap; src: url('/fonts/archivo.woff2') format('woff2');
}

/* Tokens the two new themes need beyond the original set. Declared on :root so
   every theme has a value and no rule can fall through to nothing. */
:root {
  --bw: 1px;
  --shadow-card: none;
  --shadow-btn: none;
  --shadow-hover: none;
  --radius-sm: 8px;
  --pill-radius: 999px;
  --card-blur: none;
  --nav-active-bg: var(--bg-sunken);
  --label-weight: 600;
  --label-track: 0.04em;
  --hero-weight: 700;
  --row-hover: var(--bg-sunken);
  --check-size: 1.05rem;
  --topbar-h: 3.75rem;
  /* THE GROUND UNDER ANYTHING THAT FLOATS OVER THE PAGE, and it must be
     OPAQUE. A card is translucent on purpose in glass — it sits on the wash and
     is meant to show it through. A menu, a suggestion list, a chart readout and
     a native <select> popup do not sit on the wash: they sit on whatever
     content happens to be underneath, so at 4% white over a dark page they read
     as a faint rectangle of text mixed with the table behind it.
     A theme whose --bg-card is translucent MUST override this with a solid
     colour; one whose card is already solid needs to do nothing. */
  --bg-raised: var(--bg-card);
}

/* NATIVE POPUPS ARE PAINTED BY THE BROWSER, OVER ITS OWN GROUND. Chrome carries
   the select's author-set background and colour into the dropdown list, and
   glass's --bg-sunken is rgba(255,255,255,0.05) — 5% white composited over the
   browser's white popup, with glass's white --fg on top of it. The result is a
   white box of invisible options where only the highlighted row can be read.
   The closed control is fine, because there the translucency lands on the page.
   So the options get an opaque ground of their own. */
option, optgroup {
  background-color: var(--bg-raised);
  color: var(--fg);
}

/* ── Glass ────────────────────────────────────────────────────────────────────
   Midnight after dark, Daylight in the light. The frosted-panel construction is
   the theme; the time of day is not. Both palettes live in this one block —
   light-dark() picks between them from color-scheme, which data-mode sets. */
:root[data-theme="glass"] {
  /* The daylight wash is deliberately a real blue, not an off-white. Glass is
     translucent panels floating over a ground -- if the ground is nearly the
     same value as the panels there is nothing for them to float over, and the
     whole theme flattens into plain-light with a different font. The first
     attempt at this was #eef2fb and did exactly that. */
  --bg:         light-dark(#d7e1f5, #0c1120);
  --bg-card:    light-dark(rgba(255, 255, 255, 0.78), rgba(255, 255, 255, 0.04));
  --bg-sunken:  light-dark(rgba(15, 23, 42, 0.07), rgba(255, 255, 255, 0.05));
  --line:       light-dark(rgba(15, 23, 42, 0.14), rgba(255, 255, 255, 0.10));
  --grid:       light-dark(rgba(15, 23, 42, 0.09), rgba(255, 255, 255, 0.07));
  --fg:         light-dark(#0b1020, #ffffff);
  --fg-dim:     light-dark(#5a6785, #8b97b8);
  --accent:     light-dark(#4c6ef5, #7c9fff);
  --accent-dim: light-dark(rgba(76, 110, 245, 0.28), rgba(124, 159, 255, 0.35));
  /* THE SECOND SERIES CANNOT BE THE ACCENT. It was the same blue, so Net worth
     and Assets were plotted in one indistinguishable stroke -- legend swatches
     included -- and only Liabilities could be told apart. Violet reads clearly
     against both the blue and the red at a 2.5px stroke, in both appearances. */
  --ser-b:      light-dark(#7048e8, #b197fc);
  --pos:        light-dark(#0ca678, #4ade9d);
  --neg:        light-dark(#e03131, #ff8f8f);
  --danger:     light-dark(#e03131, #ff8f8f);
  --radius: 16px;
  --radius-sm: 10px;
  --card-blur: blur(8px);
  --nav-active-bg: light-dark(rgba(76, 110, 245, 0.14), rgba(124, 159, 255, 0.14));
  --font: 'Space Grotesk', ui-sans-serif, system-ui, sans-serif;
  --hero-weight: 700;
  /* The gradient wash and the panel tint are the two things that cannot be a
     single colour token, so they get their own. */
  /* light-dark() TAKES COLOURS, NOT IMAGES. Wrapping two whole gradients in it
     produces a value that is invalid at computed-value time, so `background`
     silently fell back to nothing and the wash disappeared entirely -- in both
     appearances, not just the new one. The switch belongs on the colour stops,
     which are colours. */
  --glass-wash: linear-gradient(150deg,
    light-dark(#dde6f8, #0c1120) 0%,
    light-dark(#c6d6f2, #101830) 55%,
    light-dark(#d8e3f9, #0c1526) 100%);
  --glass-chrome: light-dark(rgba(255, 255, 255, 0.42), rgba(255, 255, 255, 0.03));
  --glass-cta: linear-gradient(135deg,
    light-dark(#4c6ef5, #7c9fff), light-dark(#0ca678, #4ade9d));
  --glass-cta-fg: light-dark(#ffffff, #0b1020);
  /* Solid, because the frosted panel is a card's job and not a menu's. Both
     values are what --bg-card actually composites to over the wash, lifted a
     little so a floating surface still reads as raised rather than as a hole. */
  --bg-raised: light-dark(#f4f7fd, #1b2338);
}
:root[data-theme="glass"] body {
  background: var(--glass-wash);
  background-attachment: fixed;
}
/* .menu is deliberately NOT in this list any more: it draws on --bg-raised,
   which is opaque, and a backdrop blur behind an opaque surface is a stacking
   context that costs a composite and shows nothing. */
:root[data-theme="glass"] .card,
:root[data-theme="glass"] .insp-block,
:root[data-theme="glass"] dialog { backdrop-filter: var(--card-blur); }
:root[data-theme="glass"] .sidebar,
:root[data-theme="glass"] .insp-top,
:root[data-theme="glass"] .topbar { background: var(--glass-chrome); }
:root[data-theme="glass"] #login-btn,
:root[data-theme="glass"] button.primary {
  background: var(--glass-cta); color: var(--glass-cta-fg);
}
:root[data-theme="glass"] .avatar {
  background: var(--glass-cta); color: var(--glass-cta-fg);
}
:root[data-theme="glass"] .c-area.c-a { fill: var(--accent); opacity: light-dark(0.22, 0.3); }

/* ── Neo-Brutalist ────────────────────────────────────────────────────────────
   Cream by day, ink by night. The hard 2px border, the zero radius and the
   offset shadow are the theme and do not change; only what they are drawn in
   does. The night palette keeps the border as the bright element — cream on
   ink — because that inversion is what stops dark brutalism reading as a
   generic dark mode with sharp corners. */
:root[data-theme="brutalist"] {
  --bg:         light-dark(#f5f1e8, #14130f);
  --bg-card:    light-dark(#ffffff, #1d1b16);
  --bg-sunken:  light-dark(#fff8dd, #2b2721);
  --line:       light-dark(#111111, #f0ece2);
  --grid:       light-dark(#ddd6c8, #3b362d);
  --fg:         light-dark(#111111, #f5f1e8);
  --fg-dim:     light-dark(#555555, #a8a294);
  --accent:     light-dark(#22c55e, #2ee06c);
  --accent-dim: light-dark(#c6f6d5, #14532d);
  --ser-b:      light-dark(#111111, #f0ece2);
  --pos:        light-dark(#17803d, #4ade80);
  --neg:        light-dark(#e5484d, #ff6b6b);
  --danger:     light-dark(#e5484d, #ff6b6b);
  --radius: 0px;
  --radius-sm: 0px;
  --pill-radius: 0px;
  --bw: 2px;
  /* A pure-black offset on an ink background is invisible, so the night
     shadow is a warm grey: still hard-edged and unblurred, still clearly a
     block rather than a glow. */
  --shadow-card:  5px 5px 0 light-dark(#111111, #4a443a);
  --shadow-btn:   3px 3px 0 light-dark(#111111, #4a443a);
  --shadow-hover: 2px 2px 0 light-dark(#111111, #4a443a);
  --nav-active-bg: #ffd43b;
  --font: 'Archivo', ui-sans-serif, system-ui, sans-serif;
  --label-weight: 900;
  --label-track: 0.1em;
  --hero-weight: 900;
  --row-hover: light-dark(#fff8dd, #2b2721);
  /* Yellow and green blocks keep black text in both appearances: they are the
     same saturated colour either way, and inverting the text on them would
     make the one bright element in the night palette unreadable. */
  --brut-on-accent: #111111;
}
/* Everything that is a surface gets the hard border and the offset shadow.
   Set here rather than by editing every original rule, so the base stylesheet
   stays theme-agnostic and a third theme is another block rather than a diff. */
:root[data-theme="brutalist"] .card,
:root[data-theme="brutalist"] .insp-block,
:root[data-theme="brutalist"] dialog,
:root[data-theme="brutalist"] .menu {
  border: var(--bw) solid var(--line); box-shadow: var(--shadow-card);
}
/* THE BOXES YOU TYPE IN ARE PART OF THE THEME TOO. Square corners come from
   --radius-sm now that `input` reads it, and the hard offset block is what made
   everything else on the screen brutalist -- these were the one family of
   controls still drawn as a flat rounded field. Inside a table row the offset
   would collide with the row below, so a dense grid keeps the border and drops
   the shadow. */
:root[data-theme="brutalist"] input,
:root[data-theme="brutalist"] select,
:root[data-theme="brutalist"] .filebtn span {
  border: var(--bw) solid var(--line); background: var(--bg-card); color: var(--fg);
  box-shadow: var(--shadow-btn);
}
:root[data-theme="brutalist"] table input,
:root[data-theme="brutalist"] table select,
:root[data-theme="brutalist"] input[type="checkbox"] { box-shadow: none; }
:root[data-theme="brutalist"] #login-btn,
:root[data-theme="brutalist"] button.primary,
:root[data-theme="brutalist"] .userbtn {
  border: var(--bw) solid var(--line); box-shadow: var(--shadow-btn);
  transition: box-shadow 0.1s ease-out, transform 0.1s ease-out;
}
:root[data-theme="brutalist"] #login-btn:hover,
:root[data-theme="brutalist"] button.primary:hover {
  box-shadow: var(--shadow-hover); transform: translate(1px, 1px);
}
:root[data-theme="brutalist"] .navitem[data-on],
:root[data-theme="brutalist"] .subtab[data-on],
:root[data-theme="brutalist"] .range button[data-on] {
  background: var(--nav-active-bg); color: var(--brut-on-accent);
  border: var(--bw) solid var(--line);
  box-shadow: var(--shadow-btn); font-weight: 900;
}
:root[data-theme="brutalist"] #login-btn,
:root[data-theme="brutalist"] button.primary { color: var(--brut-on-accent); }
:root[data-theme="brutalist"] .pill.fresh { color: light-dark(#111, #c6f6d5); }
:root[data-theme="brutalist"] .pill {
  border: var(--bw) solid var(--line); border-radius: 0; font-weight: 900; background: var(--bg-card);
}
:root[data-theme="brutalist"] .pill.fresh { background: var(--accent-dim); }
:root[data-theme="brutalist"] .pill.stale { background: light-dark(#fecaca, #7f1d1d); color: light-dark(#111, #ffe4e6); }
:root[data-theme="brutalist"] .acctgroup > h4 { font-weight: 900; }
:root[data-theme="brutalist"] .avatar { background: var(--accent); color: var(--brut-on-accent); border: var(--bw) solid var(--line); }
:root[data-theme="brutalist"] .sidebar { border-right: var(--bw) solid var(--line); }
:root[data-theme="brutalist"] .inspector { border-left: var(--bw) solid var(--line); }
:root[data-theme="brutalist"] .topbar,
:root[data-theme="brutalist"] .insp-top { border-bottom: var(--bw) solid var(--line); }
/* THE FIRST SERIES IS THE ACCENT, like it is in the legend beside it. This
   used to restate `.c-a` as `--ser-b`, which `.c-b` already is -- so Net worth
   and Assets came out as one ink-coloured line while the legend went on
   promising a green swatch for the first of them. The heavier stroke is the
   theme and belongs to all three. */
:root[data-theme="brutalist"] .c-line { stroke-width: 1.5; }
:root[data-theme="brutalist"] .c-area.c-a { fill: light-dark(#c6f6d5, #16351f); opacity: 1; }
:root[data-theme="brutalist"] .readout { border: var(--bw) solid var(--line); box-shadow: var(--shadow-card); }

/* Shared, driven by the tokens above so both themes get them for free. */
.big { font-weight: var(--hero-weight); }
.label, .acctgroup > h4, table.grid thead th, .rta span {
  font-weight: var(--label-weight); letter-spacing: var(--label-track);
}
.pill { border-radius: var(--pill-radius); }
table.grid tbody tr:hover { background: var(--row-hover); }

/* Ready to Assign, pinned above every category group. Sticky so it stays put
   while the categories scroll under it — it is the figure you are budgeting
   against, and losing it off the top is losing the point of the screen. */
table.budget tr.rtarow td {
  position: sticky; top: 0; z-index: 1;
  background: var(--bg-card);
  border-bottom: var(--bw) solid var(--line);
  padding: 0.55rem 0.6rem;
}
table.budget tr.rtarow:hover td { background: var(--bg-card); }
table.budget tr.rtarow .sub { color: var(--fg-dim); }
:root[data-theme="brutalist"] table.budget tr.rtarow td,
:root[data-theme="brutalist"] table.budget tr.rtarow:hover td {
  background: #ffd43b; color: var(--brut-on-accent);
}
:root[data-theme="glass"] table.budget tr.rtarow td,
:root[data-theme="glass"] table.budget tr.rtarow:hover td { background: var(--nav-active-bg); }

/* Budget tools */
/* THE BUDGET HEADER IS ONE BLOCK, not four stacked ones. Ready to Assign grew a
   third line ("coming in"), and because it is the tall side of the flex row it
   pushed the note and the toolbar down with it. These are the four gaps between
   the month and the table; they are set together so the block cannot drift
   apart again one rule at a time. */
/* Centred, not top-aligned. Ready to Assign is three lines and the month nav is
   one, so aligning their tops left a hole under the nav the height of the extra
   two -- and the note and toolbar had to start below all of it either way.
   Centring spends that space on both sides of the nav instead of stacking it
   all underneath, which is what read as a gap. */
/* ONE ROW: month nav, the tools, and Ready to Assign. The tools used to sit on
   their own line underneath, which cost a whole row to hold three buttons that
   fit beside the nav with room to spare. margin-right:auto on the tools pushes
   Ready to Assign to the far edge and keeps the nav and the tools together on
   the left, so the header reads left-to-right as "when, what you can do, and
   how much there is". */
#view-budget > .card > .hero-head {
  align-items: flex-start; gap: 0.5rem 1rem; margin-bottom: 0.3rem;
}
/* 0.4rem, and that is the whole point of this block: the toolbar is a second
   line of the SAME control group, not a separate band under the header.
   
   width:max-content sizes this column to its WIDEST row -- the month nav -- and
   the toolbar below then stretches to exactly that, so the two rows start and
   stop on the same two vertical lines instead of the lower one stopping short.
   grid-auto-flow:column with 1fr columns shares that width evenly however many
   buttons there are, which matters because Organise reveals a fourth. */
.bgleft { display: grid; gap: 0.4rem; width: max-content; max-width: 100%; }
#view-budget > .card > .hero-head .bgtools {
  margin: 0; display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 0.5rem;
}
#view-budget > .card > .hero-head .bgtools button { width: 100%; }
/* THE INFO DOT IS OUT OF THE MEASUREMENT. It is a 13px affordance hanging off
   the end of the month row, not part of the row's shape, and while it counted
   towards the width the toolbar below stretched past "This month" to reach it.
   Taking it out of flow leaves the month row ending at the last real button,
   which is the line the second row is supposed to stop on. */
#view-budget .monthnav { position: relative; }
#view-budget .monthnav .infodot {
  position: absolute; left: calc(100% + 0.55rem); top: 50%; transform: translateY(-50%);
}
.rtacol { text-align: right; }
#bg-note { margin: 0.15rem 0 0; text-align: right; }
/* If it does have to wrap, Ready to Assign STAYS on the right. A wrapped row
   that also jumps to the left edge reads as a mistake rather than as a second
   line, which is what the first attempt at this looked like. */
#view-budget > .card > .hero-head .bgleft { flex: none; }
#view-budget > .card > .hero-head .rtacol { flex: none; margin-left: auto; }
@media (max-width: 40rem) {
  /* A phone has one column and no right edge worth aligning to. */
  #view-budget > .card > .hero-head { align-items: flex-start; }
  #view-budget > .card > .hero-head .rtacol { margin-left: 0; }
  .rtacol, #bg-note, .rta { text-align: left; }
}
/* An empty note is not a thin note -- it is no note. Without this the margin
   was still there on every month that had nothing to say. */
#bg-note:empty { display: none; }
.bgtools { display: flex; gap: 0.8rem; margin: 0.35rem 0 0.5rem; }
/* The flex lives INSIDE the cell, not on it. `display: flex` on a <td> takes it
   out of table-cell layout, so its border-bottom renders at the flex box's
   height while every other cell in the row draws at the row's height — the
   dividers then step up and down across a row instead of forming one line. */
table.budget td .catcell {
  display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem;
}
/* .catmeta holds the target pill and, on a phone only, the inline Activity
   copy -- .catcell's own space-between still puts this whole group against
   the right edge, opposite the name. Desktop never shows .act-inline: the
   column already has the figure, and showing it twice would just be noise. */
.act-inline { display: none; }
table.grid td, table.grid th { vertical-align: middle; }
/* SMALL CONTROLS INSIDE A ROW STILL WEAR A CONTROL'S CLOTHES.
   Both of these were `border:none; background:none` with an underline on
   hover -- a bare word that happens to be pressable, which is the thing this
   app has a rule against. They are quieter than a toolbar button because they
   sit inside a table row, but they are unmistakably boxes, and they get their
   border width and radius from the theme so all three style them for free. */
.target-btn, .upd .snooze {
  font: inherit; font-size: 0.7rem; font-weight: 600; cursor: pointer;
  padding: 0.1rem 0.45rem; border-radius: var(--radius-sm);
  border: var(--bw) solid var(--line);
  background: var(--bg-card); color: var(--fg-dim);
  text-decoration: none; white-space: nowrap;
  transition: box-shadow 0.1s ease-out, color 0.1s ease-out;
}
.target-btn:hover, .upd .snooze:hover {
  color: var(--fg); box-shadow: var(--shadow-btn); text-decoration: none;
}
.target-btn { text-align: right; }
.target-btn.under { color: var(--neg); border-color: var(--neg); }
#auto-table { max-height: 22rem; }
#move-dialog, #target-dialog { max-width: min(26rem, 94vw); }
#auto-dialog { max-width: min(38rem, 94vw); }

#txsplit-dialog { max-width: min(40rem, 94vw); }
#txsplit-table input, #txsplit-table select { width: 100%; padding: 0.35rem 0.45rem; font-size: 0.88rem; }
#txsplit-table td { padding: 0.25rem 0.35rem; }
#txsplit-table .sa { text-align: right; font-variant-numeric: tabular-nums; }

/* Reordering the budget. Same affordance as the account sidebar. */
.card[data-organise] table.budget tr[data-cat],
.card[data-organise] table.budget tr.grouprow { cursor: grab; }
.card[data-organise] table.budget tr[data-cat] td:first-child::before,
.card[data-organise] table.budget tr.grouprow td:first-child::before {
  content: "⠿"; color: var(--fg-dim); margin-right: 0.4rem; font-size: 0.8em;
}
table.budget tr.dragging { opacity: 0.4; }
table.budget tr.over-top td { box-shadow: inset 0 2px 0 var(--accent); }
table.budget tr.over-bottom td { box-shadow: inset 0 -2px 0 var(--accent); }
.grp-rename { background: none; border: none; color: var(--fg-dim); font-size: 0.7rem;
  text-transform: none; letter-spacing: 0; padding: 0 0.3rem; }
.grp-rename:hover { color: var(--fg); text-decoration: underline; }

/* ── The group header row ─────────────────────────────────────────────────────
   It used to be one colspan=4 cell holding a name. A heading that spans the
   money columns is a heading that cannot answer the question the money columns
   ask -- "what does this whole group come to" -- so it now carries the same
   three totals as the rows beneath it, in the same columns, and the name cell
   keeps the controls. */
table.budget tr.grouprow td.grptot {
  text-align: right; font-variant-numeric: tabular-nums;
  /* Deliberately quieter than a category row: this is a summary of what is
     already on screen, not a fourth thing to read. Same weight as the label. */
  color: var(--fg-dim);
}
table.budget tr.grouprow td.grptot.avail-pos { color: var(--pos); }
table.budget tr.grouprow td.grptot.avail-neg { color: var(--neg); }
.grphead { display: flex; align-items: center; gap: 0.35rem; }
/* The fold chevron: quiet like grp-rename, but always visible — a control
   that only appears on hover cannot be found on a phone at all. */
.grp-tgl { background: none; border: none; color: var(--fg-dim); cursor: pointer;
  padding: 0 0.15rem; font-size: 0.85rem; line-height: 1; }
.grp-tgl:hover, .grp-tgl:focus-visible { color: var(--fg); }
.grp-mv { font-size: 0.85rem; }
/* By CLASS, not position: the fold chevron sits before the name now, and a
   :first-child selector aimed at the name quietly restyled the button — the
   name then wrapped one letter per line the moment the header got crowded. */
.grphead .grpname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.grphead button { flex: none; }

/* The circled + that adds a category straight into this group. Always present
   rather than Organise-only: adding a category is an everyday act, and the old
   route asked for the name and then the group in two stacked browser prompts,
   which is two questions to answer the one the row already knows. */
.grp-add {
  flex: none; width: 1.15rem; height: 1.15rem; padding: 0; line-height: 1;
  display: inline-grid; place-items: center;
  border: 1px solid var(--line); border-radius: 50%;
  background: none; color: var(--fg-dim);
  font-size: 0.85rem; font-weight: 400;
  /* The row's own text-transform is uppercase; a "+" does not care, but the
     letter-spacing shifts the glyph off centre inside a circle this small. */
  letter-spacing: 0;
  /* Present at rest rather than revealed on hover: a control you cannot see is
     a control nobody uses, and this one is the only route to adding a category
     that already knows which group you meant. It brightens on hover instead. */
  opacity: 0.55; transition: opacity 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
tr.grouprow:hover .grp-add, .grp-add:focus-visible { opacity: 1; }
.grp-add:hover { opacity: 1; color: var(--accent); border-color: var(--accent); }

/* ── Asking for things ────────────────────────────────────────────────────────
   The browser's own prompt/confirm/alert cannot be styled, ignore the theme,
   and on a phone arrive as a system sheet that looks like it came from another
   application entirely. These classes dress the <dialog> that replaced all of
   them. CSP here is style-src 'self' with no 'unsafe-inline', so everything is
   a class -- an inline style= attribute is dropped on the floor. */
/* ── Scheduled transactions ───────────────────────────────────────────────── */
#sched-dialog, #acct-dialog { max-width: min(34rem, 94vw); }
.acctools { display: flex; align-items: center; gap: 0.9rem; flex-wrap: wrap; }
.acctools button.primary { padding: 0.4rem 0.9rem; font-size: 0.85rem; }
/* The checkbox sits in the value column of the label/value grid, so it needs no
   label of its own -- hence the empty <span> beside it in the markup.
   width:auto is load-bearing: .schedgrid stretches its inputs to 100% of the
   column, which on a checkbox means a stretched box that shoves its own text
   off to the right and wraps it. */
.schedgrid .check { margin: 0; }
.schedgrid .check input[type="checkbox"] { min-width: 0; }
.schedgrid {
  display: grid; grid-template-columns: 9rem minmax(0, 1fr);
  gap: 0.45rem 0.7rem; align-items: center; margin-top: 0.9rem;
}
.schedgrid label { margin: 0; font-size: 0.82rem; color: var(--fg-dim); }
.schedgrid input, .schedgrid select { width: 100%; padding: 0.4rem 0.5rem; font-size: 0.9rem; }
.schedgrid .num { text-align: right; font-variant-numeric: tabular-nums; }
.schedevery { display: flex; gap: 0.4rem; }
.schedevery input { width: 4.5rem; flex: none; }
@media (max-width: 30rem) {
  /* One column: a 9rem label track leaves the inputs too narrow to type a date
     into on a phone. */
  .schedgrid { grid-template-columns: 1fr; gap: 0.15rem; }
  .schedgrid label { margin-top: 0.4rem; }
}
#sched-table td, #sched-table th { white-space: nowrap; }
#sched-table tr[data-off] td { opacity: 0.5; font-style: italic; }

.askdlg { max-width: min(28rem, 94vw); }
.askform { display: grid; gap: 0.75rem; }
.askform h2 { margin: 0; font-size: 1.05rem; }
.askmsg { display: grid; gap: 0.5rem; color: var(--fg-dim); font-size: 0.9rem; line-height: 1.45; }
.askmsg p { margin: 0; }
.askmsg strong { color: var(--fg); font-weight: 650; }
.askfield { display: grid; gap: 0.3rem; }
.askfield > span { font-size: 0.74rem; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--fg-dim); }
.askinput { width: 100%; padding: 0.5rem 0.6rem; font-size: 0.95rem; }
.askinput.num { text-align: right; font-variant-numeric: tabular-nums; }
/* A destructive confirmation keeps the primary button's shape -- it is still
   the thing you came here to press -- but wears the negative colour so the
   press is a decision rather than a reflex. */
.askok.is-danger { background: var(--neg); border-color: var(--neg); color: #fff; }
.askerr { margin: 0; }
.askerr[hidden] { display: none; }
.askrow { display: flex; align-items: center; justify-content: flex-end;
  gap: 0.6rem; margin-top: 0.3rem; }
/* Cancel first in the DOM so Tab reaches it before the confirming button, but
   the confirming button is the one Enter fires and the one the eye lands on. */
.askrow .askcancel { margin-right: auto; }
@media (max-width: 26rem) {
  .askrow { flex-direction: column-reverse; align-items: stretch; }
  .askrow button { width: 100%; }
  .askrow .askcancel { margin-right: 0; }
}

/* Reports */
.alloc { list-style: none; margin: 0.6rem 0 0; padding: 0; display: grid; gap: 0.45rem; }
.alloc li { display: grid; grid-template-columns: 1fr auto; gap: 0.3rem 0.8rem; font-size: 0.88rem; }
.alloc .bar { grid-column: 1 / -1; height: 5px; border-radius: 3px; background: var(--bg-sunken); overflow: hidden; }
.alloc .bar i { display: block; height: 100%; background: var(--accent); }
.alloc .amt { font-variant-numeric: tabular-nums; }
.alloc .pc { color: var(--fg-dim); font-size: 0.78rem; }
td .minibar { display: block; height: 4px; border-radius: 2px; background: var(--neg); opacity: 0.5; margin-top: 3px; }
.gain-pos { color: var(--pos); font-weight: 650; }
.gain-neg { color: var(--neg); font-weight: 650; }

/* Holdings and Gain tables list every account a security sits in — the box is
   aggregated by security, so this cell is where "and where is it" lives.
   `white-space: normal` overrides table.grid's blanket nowrap: a security held
   in four accounts makes this the longest cell in the app, and nowrap stretched
   the table far past the width of the card it lives in. */
/* The list of accounts a security is held in, and underneath it the share
   count in each. THE WRAPPING IS LOAD-BEARING.
   `table.grid td` sets white-space: nowrap and outranks a bare .acctlist, so
   the cell inherited nowrap; the inner line is display:block with visible
   overflow, which means its text PAINTED outside the cell rather than widening
   it. Six accounts holding VT spilled 234px straight across the quantity
   column, and nothing about the box model said so -- the element's own
   rectangle stayed inside the cell the whole time.
   Selectors here carry an element as well as a class so they beat the td rule
   they are correcting. */
table.grid td.acctlist { font-size: 0.9rem; white-space: normal; max-width: 22rem; }
.acctlist .dot { color: var(--fg-dim); padding: 0 0.15rem; }
table.grid td.acctlist .sub {
  font-size: 0.78rem; white-space: normal; overflow-wrap: anywhere;
}
@media (max-width: 40rem) {
  /* On a phone the per-account quantity breakdown is the first thing worth
     losing — the aggregate row is the point of the box. */
  .acctlist .sub { display: none; }
  .acctlist { max-width: 11rem; }
}

/* The line in the split dialog that says net worth is about to move. It is a
   warning, not decoration: everything else the dialog reports is bookkeeping
   that nets to zero, and this is the one number that changes what the
   household is worth. */
.split-adj {
  margin-top: 8px;
  padding: 8px 10px;
  border-radius: var(--radius-sm, 8px);
  background: var(--warn-bg, rgba(255, 212, 59, 0.14));
  border: 1px solid var(--warn-border, rgba(255, 212, 59, 0.4));
  color: var(--text-body);
}

/* Marks an account whose value comes from its POSITIONS rather than a typed
   balance. Worth a mark because the two behave differently in ways that are
   otherwise invisible from the sidebar: a holdings account reprices on its own
   every hour, its balance cannot be edited directly, and it has to be turned
   back into a typed balance before it can be split. */
.hmark {
  display: inline-block; font-size: 0.62rem; font-weight: 700; line-height: 1;
  padding: 0.15em 0.3em; margin-left: 0.3em; vertical-align: 0.1em;
  border: 1px solid var(--line); border-radius: var(--radius-sm, 4px);
  color: var(--fg-dim); background: var(--bg-sunken);
}
:root[data-theme="brutalist"] .hmark {
  border-width: var(--bw); border-radius: 0; font-weight: 900; color: var(--fg);
}

/* ── Update balances & holdings ───────────────────────────────────────────────
   Two kinds of row in one table: an account, and a position inside one. The
   indent and the lighter rule are the whole distinction, so a position never
   reads as an account with a strange name. */
.upd .updhead td { font-weight: 600; }
.upd .updpos td:first-child { padding-left: 1.9rem; }
.upd .updpos td { border-top-style: dotted; }
.upd .updpos .sym { font-weight: 600; }
.upd .updpos .sub {
  display: block; font-size: 0.72rem; color: var(--fg-dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 22rem;
}
/* A BOX THE APP FILLED IN, not one you typed. Same figure, quieter, so a
   screen full of numbers still says at a glance which of them are yours --
   and a keystroke in it makes it yours, which the colour then reflects. */
.upd input[data-solved] { color: var(--fg-dim); font-style: italic; }
.upd td.dim { color: var(--fg-dim); font-weight: 400; }
.upd .delta.dim { color: var(--fg-dim); }
.upd tr[data-dirty] td { background: var(--row-hover); }

/* A group heading inside the update table. Mirrors the sidebar's group label
   so the two lists read as the same arrangement of the same accounts. */
.upd .updgroup th {
  text-align: left; padding: 0.9rem 0.6rem 0.3rem;
  font-size: 0.7rem; font-weight: var(--label-weight); letter-spacing: var(--label-track);
  text-transform: uppercase; color: var(--fg-dim);
  border-bottom: var(--bw) solid var(--line); background: none;
}
.upd .updgroup th.num { text-align: right; letter-spacing: 0; }
.upd .updgroup:first-child th { padding-top: 0.3rem; }

/* Says which grain a chart is actually drawing when it could not draw the one
   that was asked for. Intraday samples begin the day the feature ships and
   cannot be reconstructed, so for a while every hourly request legitimately
   has nothing — and an unexplained flat line reads as a bug. */

/* An account nobody has touched in over a month. Marked on the row rather than
   in a column of dates, because the question being asked while working down
   this screen is "which of these still needs me", not "when exactly was each
   one". The tint is deliberately light: a dozen rows in full red is a wall, and
   a wall gets ignored, which is the failure mode this is meant to prevent. */
.upd tr.late > td { background: color-mix(in srgb, var(--neg) 10%, transparent); }
.upd tr.late > td:first-child { box-shadow: inset 3px 0 0 var(--neg); }
.upd .upd-when {
  display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap;
  font-size: 0.72rem; font-weight: 400; margin-top: 0.15rem;
}
.upd .upd-when.late { color: var(--neg); }
.upd .upd-when.snoozed { color: var(--fg-dim); font-style: italic; }
.upd .upd-when.off { color: var(--fg-dim); }

/* ── The investments budget ───────────────────────────────────────────────────
   A second pool under the first, so it reads as a separate box rather than more
   of the same table. */
#ib-card .ibname { cursor: text; }
#ib-card .ibname:hover { background: var(--row-hover); }
/* One line, one height. A select and an input disagree about their intrinsic
   box by nearly twenty pixels here, which reads as two controls at two
   different heights rather than as one row -- the same thing that had to be
   fixed on the positions editor. */
#ib-card tbody td { vertical-align: middle; }
/* SEVEN COLUMNS, AND THE MONEY ONE HAS TO FIT ITS NUMBER. Left to share the
   width evenly, the Holds box clipped $3,659.33 to "3659" — a box showing a
   figure that is not the figure in it is worse than no box. The two editable
   columns get a floor and the derived ones give way, since those are three or
   four characters that can be read at any width. */
/* FIXED LAYOUT, so seven columns divide the card instead of demanding more of
   it than there is. With auto layout the minimum widths and the nowrap derived
   columns added up to 50px more than the card, and the last column -- the
   remove button -- was painted outside it entirely. Proportions rather than
   pixels, so it holds at any width. */
#ib-card table.grid { table-layout: fixed; width: 100%; }
/* The EDITABLE columns get the room; the derived ones are three or four
   characters and can give it up. A box clipping "3659.33" to "3659.3" shows a
   figure that is not the figure in it, which is worse than no box. Columns 3 and
   4 are now both editors (Deposits and Gains), so the percentage each used to
   get a column for sits under its own box instead and the name gives up two
   points to Share. */
#ib-card th:nth-child(1), #ib-card td:nth-child(1) { width: 26%; }
#ib-card th:nth-child(2), #ib-card td:nth-child(2) { width: 18%; }
#ib-card th:nth-child(3), #ib-card td:nth-child(3) { width: 13%; }
#ib-card th:nth-child(4), #ib-card td:nth-child(4) { width: 13%; }
#ib-card th:nth-child(5), #ib-card td:nth-child(5) { width: 15%; }
#ib-card th:nth-child(6), #ib-card td:nth-child(6) { width: 15%; }
/* The remove column only exists while organising, so it claims no width until
   then and the six real columns divide the whole card. */
#ib-card th:nth-child(7), #ib-card td:nth-child(7) { width: 0; padding: 0; }
#ib-card[data-organise] th:nth-child(7),
#ib-card[data-organise] td:nth-child(7) { width: 8%; padding: 0.45rem 0.6rem; }
/* Padding is what was eating the last character inside each box. */
#ib-card td input { padding-left: 0.35rem; padding-right: 0.35rem; }
#ib-card td input.ibtgt { width: 100%; height: 2.3rem; text-align: right; }
#ib-card th { overflow-wrap: anywhere; }
#ib-card td input.ibbal,
#ib-card td input.ibwt,
#ib-card td input.ibgw,
#ib-card td input.ibrename { width: 100%; height: 2.3rem; text-align: right; }
#ib-card td input.ibrename { text-align: left; }
/* THE DERIVED PERCENTAGE MOVED UNDER ITS OWN BOX. Deposits and Gains are both
   editors now, so neither has a column to put its share in -- but a bare weight
   of 80 says nothing on its own, which is what that figure was for. A block
   <small> under the input keeps it at a glance and costs the row a few pixels
   rather than the table a column. */
#ib-card td .ibfrac {
  display: block; margin-top: 0.15rem; color: var(--fg-dim);
  font-size: 0.72rem; line-height: 1.1; text-align: right;
}
/* "/month" is the unit of the deposit-weight total and belongs IN that cell:
   left in a column of its own it labelled whichever figure happened to sit
   beside it. */
#ib-card tfoot .ibper {
  color: var(--fg-dim); font-weight: 400; margin-left: 0.15rem; font-size: 0.85em;
}
#ib-card td.dim { color: var(--fg-dim); }
#ib-card tr.dragging { opacity: 0.45; }
#ib-card tr.dropto > td { box-shadow: inset 0 2px 0 var(--accent); }
#ib-card[data-organise] tbody tr[data-cat] { cursor: grab; }
#ib-card[data-organise] tbody tr[data-cat] td:first-child::before {
  content: "\2059"; color: var(--fg-dim); margin-right: 0.4rem;
}
/* THE TICKS HAVE TO FORM A COLUMN. The cell is right-aligned and the label
   beside each box varies in width -- "100%", "20%", "no", an em-dash -- so an
   inline-flex put every checkbox at a different x and the column read as a
   ragged edge. A grid with a fixed slot for the label pins both: the ticks line
   up with each other and so do the percentages. */
#ib-card .ibloss-wrap {
  display: grid; grid-template-columns: auto 2.6rem;
  align-items: center; gap: 0.4rem; justify-content: end; margin: 0;
}
#ib-card .ibloss-wrap > span { text-align: right; }
/* Ticked but sitting out losses while below zero: the state is the point of
   the label, so it wears the negative colour rather than the dim one. */
#ib-card .ibpaused { color: var(--neg); }
#ib-card tfoot th { border-top: var(--bw) solid var(--line); font-weight: var(--label-weight); }
#ib-card tfoot th.dim { color: var(--fg-dim); font-weight: 400; }

/* The inspector's target editor: a heading with a way out of it. */
.insp-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem; }
.insp-head h3 { margin: 0; }
#target-editor select, #target-editor input { width: 100%; }
#target-editor label { display: block; margin-top: 0.7rem; font-size: 0.78rem; color: var(--fg-dim); }
#target-editor .kv { margin-top: 0.8rem; }

/* ── Toolbar buttons ──────────────────────────────────────────────────────────
   A row of underlined words is not a toolbar. `.link` is right for an action
   inside a sentence; for the controls that sit under a heading and DO things it
   read as unstyled text, because that is what it is. These take the theme's
   border, radius and shadow, so they are hard offset blocks in brutalist and
   soft tinted ones elsewhere -- the same treatment the sidebar's Organise
   button already had. */
/* .bgtools holds plain <button>s rather than .link ones, so it still needs the
   treatment named for it. Everything else that used to be listed here is a
   .link and gets it from the rule above. */
.bgtools button {
  font: inherit; font-size: 0.78rem; font-weight: 600;
  padding: 0.35rem 0.65rem; cursor: pointer; text-decoration: none;
  color: var(--fg); background: var(--bg-card);
  border: var(--bw) solid var(--line); border-radius: var(--radius-sm);
  box-shadow: var(--shadow-btn);
  transition: box-shadow 0.1s ease-out, transform 0.1s ease-out;
}
.bgtools button:hover {
  box-shadow: var(--shadow-hover); transform: translate(1px, 1px); text-decoration: none;
}
.bgtools { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
#ad-holdings .hero-head span { display: inline-flex; gap: 0.5rem; }

/* ── Info dots ────────────────────────────────────────────────────────────────
   The explanation for a section, parked beside its heading. Sized to the text
   around it so it reads as punctuation rather than as another control. */
.infodot {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.15em; height: 1.15em; margin-left: 0.4em; padding: 0;
  font-size: 0.72em; font-weight: 700; font-family: var(--font); line-height: 1;
  vertical-align: 0.15em; cursor: pointer;
  color: var(--fg-dim); background: none;
  border: 1px solid var(--line); border-radius: 999px;
}
.infodot:hover { color: var(--fg); border-color: var(--fg-dim); }
.infodot[aria-expanded="true"] { color: var(--accent); border-color: var(--accent); }
:root[data-theme="brutalist"] .infodot { border-width: var(--bw); border-radius: 999px; }
.infopop {
  margin: 0.6rem 0 0.9rem; padding: 0.8rem 1rem;
  font-size: 0.82rem; line-height: 1.5; color: var(--fg-dim);
  background: var(--bg-sunken);
  border: var(--bw) solid var(--line); border-radius: var(--radius-sm);
  max-width: 46rem;
}
.infopop p { margin: 0 0 0.6rem; }
.infopop p:last-child { margin-bottom: 0; }
.infopop strong { color: var(--fg); }
.infopop code, .infopop kbd {
  font-family: var(--mono); font-size: 0.92em;
  background: var(--bg-card); border: 1px solid var(--line);
  border-radius: 4px; padding: 0.05em 0.3em;
}
/* The target editor when it has to live inline, because there is no pane. */
.inline-editor { margin: 0.8rem 0; }
.inline-editor .insp-block { padding: 0; }

/* ── Settings → Payees ────────────────────────────────────────────────────────
   A tidying screen, so it is a table of names with a checkbox column and three
   quiet actions per row. Everything here reads the theme tokens, so brutalist
   gets its hard offsets and glass its tint without a second set of rules. */
.paysel { width: 2.2rem; }
.paysel input { margin: 0; }

/* The name is the control that renames it -- the register's inline-edit
   gesture, not a link. It stays body text and grows an underline on hover,
   because a row of blue words down a table column reads as navigation and this
   navigates nowhere. */
.payname {
  font: inherit; color: var(--fg); background: none; border: 0;
  padding: 0; text-align: left; cursor: text; border-radius: 0;
}
.payname:hover { box-shadow: inset 0 -1px 0 var(--fg-dim); }
.payname:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The actions column. `white-space: nowrap` keeps Hide/Rename/Delete on one
   line; without it the third wraps under the first at phone width and the row
   doubles in height for no reason. */
.payacts { white-space: nowrap; }
.payacts button.link + button.link { margin-left: 0.35rem; }

/* Nothing selected is not the same as working -- the shared
   button.primary:disabled sets cursor:progress, which is right for "Checking..."
   and wrong for "pick two first". */
#pay-merge:disabled { cursor: not-allowed; }

#pay-table tbody tr.dim td { color: var(--fg-dim); }
#pay-table tbody tr.dim .payname { color: var(--fg-dim); }

/* One suggested merge. The kept name leads, the ones that would go follow it,
   and the button that acts on it sits at the far end so the eye reads the
   consequence before the control. */
.dupe {
  display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: center;
  justify-content: space-between;
  padding: 0.6rem 0; border-bottom: 1px solid var(--grid);
}
.dupe:last-child { border-bottom: 0; }
.dupe-names { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: baseline; }
.dupe-drop {
  color: var(--fg-dim); text-decoration: line-through;
  text-decoration-color: var(--fg-dim);
}
.dupe-drop .hint { text-decoration: none; display: inline; }

/* Phone: the actions ARE the screen, so they must not be the thing that falls
   off the right-hand edge. `.card:has(table.grid)` makes the card the
   scrollport, which is fine for a wide report and wrong here — you would have
   to drag the table sideways to reach the Hide button on every row. Dropping
   the least load-bearing column ("Last") buys back the width instead. */
@media (max-width: 620px) {
  #pay-table th:nth-child(4), #pay-table td:nth-child(4) { display: none; }
  /* The name is the column that has to give. `table.grid td` is nowrap, which
     is right for a register full of dates and figures but wrong for
     "Manual Balance Adjustment" — it claims 200px of a 358px row and pushes
     the actions off the edge. Same trade the auto-assign dialog makes: a long
     name over two lines is readable, a clipped one is not. */
  #pay-table td:nth-child(2) { white-space: normal; }
  #pay-table td, #pay-table th { padding-left: 0.3rem; padding-right: 0.3rem; }
  .paysel { width: 1.6rem; }
  .payacts button.link { padding: 0.35rem 0.45rem; font-size: 0.72rem; }
  .payacts button.link + button.link { margin-left: 0.2rem; }
}

/* ── Legends sit in the same place on every chart ─────────────────────────────
   Net worth put its key above the plot at top right; Reports put its below the
   x-axis labels, which is the one place on the card the eye has already left.
   Same component, one convention: above the plot, right-aligned. */
.chartbar.chartbar-end { justify-content: flex-end; }

/* ── A PHONE IS 358px WIDE AND THESE TABLES ARE 900 ───────────────────────────
   `.card:has(table.grid){overflow-x:auto}` makes the card a horizontal
   scroller, which is a fair rescue for a wide report and a disaster for the
   four screens you actually work in: nothing says the scroll is there, and what
   sits off the right-hand edge by default is the part you came for -- the NEW
   BALANCE input on Update (the only control on the page), CATEGORY and AMOUNT
   in the register, ACTIVITY and AVAILABLE in the budget, and half of BALANCE on
   All accounts. A figure cut mid-glyph is worse than one plainly absent.

   So on a phone each of these tables drops the columns a decision is NOT made
   from, and keeps the ones it is. Everything dropped is derived, or already
   stated elsewhere on the row, or is not a figure anybody acts on:

     update    CURRENT   -- it is already printed inside the input, as its
                            placeholder
     register  ACCOUNT   -- it is the filter directly above the table
     budget    ACTIVITY  -- assigned and available are what you act on
     accounts  ENTRIES, LAST ACTIVITY

   That is enough for the register, the budget and All accounts to fit a phone
   outright. Update cannot: its middle columns are the quantity and cost-basis
   boxes for a position, and dropping those would make a holdings account
   uneditable on a phone. It pins both ends instead -- see below.

   Anything still too wide keeps the scrollport, but now with an affordance: a
   thin scrollbar that is always drawn rather than an overlay bar that appears
   only once you are already dragging, and a first column that stays put so you
   never lose which row you are on. */
@media (max-width: 40rem) {
  .card:has(table.grid) { scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
  table.grid th:first-child, table.grid td:first-child {
    position: sticky; left: 0; z-index: 1; background: var(--bg-card);
  }
  /* A sticky cell paints its own background, so it has to be told about every
     state the row behind it can be in or it stays card-coloured while the rest
     of the row lights up. */
  table.grid tbody tr:hover td:first-child { background: var(--row-hover); }
  table.budget tr.grouprow td:first-child,
  .upd .updgroup th:first-child { background: var(--bg-sunken); }
  table.budget tr.rtarow td:first-child { z-index: 2; }
  :root[data-theme="brutalist"] table.budget tr.rtarow td:first-child { background: #ffd43b; }
  .upd tr[data-dirty] td:first-child { background: color-mix(in srgb, var(--accent) 8%, var(--bg-card)); }
  .upd tr.late > td:first-child { background: color-mix(in srgb, var(--neg) 10%, var(--bg-card)); }

  /* UPDATE PINS BOTH ENDS. Which row you are on and the box you came to type
     in are the two things that must never scroll away, so the account name
     stays put on the left and NEW BALANCE on the right; quantity, cost basis
     and the change slide between them. Measured: 322px of card, 104 pinned
     left, 123 pinned right, and each of the three middle columns is 91 -- so
     whichever one you scroll to arrives whole rather than half under an edge,
     which is what the shrunken qty/basis boxes below are buying. */
  #upd-table th:nth-child(2), #upd-table td:nth-child(2) { display: none; }
  #upd-table th:nth-child(5), #upd-table td:nth-child(5) {
    position: sticky; right: 0; z-index: 1; background: var(--bg-card);
  }
  #upd-table tbody tr:hover td:nth-child(5) { background: var(--row-hover); }
  .upd .updgroup th:nth-child(5) { background: var(--bg-sunken); }
  .upd tr[data-dirty] td:nth-child(5) { background: color-mix(in srgb, var(--accent) 8%, var(--bg-card)); }
  .upd tr.late > td:nth-child(5) { background: color-mix(in srgb, var(--neg) 10%, var(--bg-card)); }
  /* A LONG WORD MUST NOT SET THE WIDTH OF A COLUMN. With only `normal` the
     minimum width of these cells is still their longest unbreakable word --
     one 22-character payee, one long account name -- and that word alone was
     enough to push the money column back off the screen. Breaking mid-word is
     ugly perhaps twice in a table; a figure you cannot see is worse. */
  #upd-table td:first-child { white-space: normal; overflow-wrap: anywhere; }
  /* The fund name under a ticker is nowrap and 200px wide, so it set the
     minimum width of the whole first column and pushed the input column off
     the screen on its own. The symbol is the identifier; the name is the check
     you do on a screen with room for it. Same trade the solve dialog makes. */
  .upd .updpos .sub { display: none; }
  .upd .upd-when { max-width: 100%; }
  #upd-table th, #upd-table td { padding-left: 0.35rem; padding-right: 0.35rem; }
  /* "TOTAL COST BASIS" on one line is 155px -- wider than the box underneath it
     and wider than the gap between the two pinned columns. */
  #upd-table thead th { white-space: normal; }

  /* Register: the account is the filter above the table, not a column.
     Indices are +1 from before the flag column, which stays visible. */
  #rg-table th:nth-child(3), #rg-table td:nth-child(3) { display: none; }
  #rg-table { font-size: 0.8rem; }
  #rg-table th, #rg-table td { padding-left: 0.3rem; padding-right: 0.3rem; }
  #rg-table td:nth-child(4), #rg-table td:nth-child(5) { white-space: normal; overflow-wrap: anywhere; }
  #rg-table .rm { padding-left: 0.25rem; padding-right: 0.25rem; }
  /* Every pixel of the flag seat comes out of Payee on a phone. */
  #rg-table th.flagcol, #rg-table td.flagcol { width: 1.1rem; padding-left: 0.1rem; padding-right: 0.05rem; }

  /* Budget: Activity is dropped as a COLUMN on a phone -- four money columns
     at 390px clips, and un-hiding it does not fit no matter how tight the
     padding gets. The figure itself is not dropped, though: it is a button
     that opens the sheet of transactions behind it, which is exactly the
     thing a phone is most often used to check. So it moves INTO the name
     cell instead, as the phone-only `.act-inline` copy beside the target
     pill in `.catmeta` (see loadBudget() in app.js and `.act-inline` below)
     -- same button, same data-act, wired by the same `.actbtn` query. */
  #bg-table th:nth-child(3), #bg-table td:nth-child(3) { display: none; }
  #bg-table th, #bg-table td { padding-left: 0.35rem; padding-right: 0.35rem; }
  table.budget input.assign { width: 5.5rem; }
  table.budget td:first-child { white-space: normal; overflow-wrap: anywhere; }
  /* ALWAYS two lines on a phone, never "wrap if it happens not to fit".
     Wrapping on demand meant a short name kept its target pill inline while
     the next row pushed its own onto a second line — every row a different
     shape, which is what made the column read as chaos. Name on top, pill
     underneath, same on every row. */
  table.budget td .catcell {
    flex-direction: column; align-items: flex-start; gap: 0.2rem;
  }
  table.budget td .catcell .target-btn { margin-left: 0; }
  /* The target pill and, here only, the Activity figure that lost its own
     column above -- one row, wrapping if a long "needs $X" pill leaves no
     room. */
  .catmeta { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
  /* The column is gone, so the figure needs its own label to read as
     Activity rather than a second Available. Text, not money -- it can blur
     or not under body[data-blur] without it mattering either way. */
  .act-inline { display: inline-block; }
  .act-inline::before {
    content: 'activity'; margin-right: 0.35rem; color: var(--fg-dim);
    font-size: 0.68rem; font-weight: 650; text-transform: uppercase;
    letter-spacing: var(--label-track);
  }
  /* "Credit Card Payments", uppercase and nowrap, is 172px on its own -- two
     thirds of the phone's whole table. */
  /* Wrap on words. anywhere let a crowded organise header break the name one
     letter per line; a group name has spaces to break at. */
  .grphead .grpname { white-space: normal; overflow-wrap: break-word; }

  /* INVESTMENTS BUDGET: SEVEN COLUMNS DO NOT FIT 358px. Deposits, Gains and
     Losses are all set once — when a category is created, when the monthly
     contribution changes, or when growth is pointed somewhere new — and then
     left alone for months. Holds is the one that moves and Share is the figure
     you read the card for, so the phone keeps those two and the name, and drops
     all three set-and-forget editors. That is also what was squeezing the Holds
     box down to a clipped figure. All three are still there on any screen wider
     than this. */
  #ib-table th:nth-child(3), #ib-table td:nth-child(3),
  #ib-table th:nth-child(4), #ib-table td:nth-child(4),
  #ib-table th:nth-child(5), #ib-table td:nth-child(5) { display: none; }
  /* The widths are proportions of a fixed-layout table, so the survivors have to
     add up to the card again or they leave a gap on the right. */
  #ib-table th:nth-child(1), #ib-table td:nth-child(1) { width: 44%; }
  #ib-table th:nth-child(2), #ib-table td:nth-child(2) { width: 30%; }
  #ib-table th:nth-child(6), #ib-table td:nth-child(6) { width: 26%; }
  /* Organising adds the remove column, so the three have to make room for it
     rather than the table growing past the card -- 108% of a fixed-layout
     table is 11px of sideways scroll on a 390px phone. */
  #ib-card[data-organise] #ib-table th:nth-child(1),
  #ib-card[data-organise] #ib-table td:nth-child(1) { width: 40%; }
  #ib-card[data-organise] #ib-table th:nth-child(2),
  #ib-card[data-organise] #ib-table td:nth-child(2) { width: 28%; }
  #ib-card[data-organise] #ib-table th:nth-child(6),
  #ib-card[data-organise] #ib-table td:nth-child(6) { width: 24%; }
  #ib-card[data-organise] #ib-table th:nth-child(7),
  #ib-card[data-organise] #ib-table td:nth-child(7) { width: 8%; padding-left: 0.2rem; padding-right: 0.2rem; }
  #ib-table th, #ib-table td { padding-left: 0.35rem; padding-right: 0.35rem; }
  /* A TOTAL BROKEN MID-NUMBER IS NOT THE NUMBER. `overflow-wrap: anywhere` is
     right for a category name and wrong for money -- it rendered the footer as
     "$7,749.1" over "1". The totals row shrinks instead. */
  #ib-table tfoot th { font-size: 0.72rem; white-space: nowrap; overflow-wrap: normal; }
  /* The rule that used to hide the footer's fourth cell is gone with the column
     it was covering for: "/month" now lives inside the deposit-weight cell, and
     every column the phone drops drops its footer cell with it, so nothing is
     left to mis-label. */

  /* All accounts: what it is and what it is worth. */
  #acct-table th:nth-child(4), #acct-table td:nth-child(4),
  #acct-table th:nth-child(5), #acct-table td:nth-child(5) { display: none; }
  #acct-table td:first-child { white-space: normal; overflow-wrap: anywhere; }
}

/* ── Checkboxes ──────────────────────────────────────────────────────────────
   LAST IN THE FILE ON PURPOSE. Every rule below has the same specificity as
   the `.schedgrid input { width: 100% }` and `.filters input { width: auto }`
   rules that size an input for one particular box, so the cascade is decided
   by order alone: declared earlier, a tick stretched to the full width of a
   dialog row and shoved its own label off the edge. */
/* A CHECKBOX IS A CONTROL AND WEARS THE APP'S CLOTHES. These were the OS
   default box: nearly invisible dark-grey-on-dark at night, and a bare white
   square beside heavily styled controls by day. Same border width, same radius
   and same accent fill as everything else, so all three themes get it free.
   The tick is a clip-path rather than a glyph -- a font-dependent character
   would not centre the same way in three different typefaces. */
input[type="checkbox"] {
  appearance: none; -webkit-appearance: none;
  width: var(--check-size); height: var(--check-size);
  flex: none; margin: 0; padding: 0;
  display: inline-grid; place-content: center; vertical-align: -0.18em;
  background: var(--bg-card);
  border: var(--bw) solid var(--line);
  /* The card radius on a 17px box is a circle, and a circle is a radio button.
     min() keeps brutalist's hard corner at 0 without a per-theme rule. */
  border-radius: min(4px, var(--radius-sm));
  cursor: pointer;
  transition: background 0.1s ease-out, border-color 0.1s ease-out;
}
input[type="checkbox"]::before {
  content: ""; width: 0.62rem; height: 0.62rem;
  background: var(--brut-on-accent, #fff);
  transform: scale(0); transition: transform 0.08s ease-out;
  clip-path: polygon(14% 44%, 0 60%, 40% 100%, 100% 18%, 84% 0, 38% 68%);
}
input[type="checkbox"]:checked { background: var(--accent); border-color: var(--accent); }
input[type="checkbox"]:checked::before { transform: scale(1); }
input[type="checkbox"]:hover:not(:checked) { border-color: var(--fg-dim); }
input[type="checkbox"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ── Spending from an investment category ─────────────────────────────────────
   The dialog and the list of recent spends under the investments table. Every
   colour here is a token, so plain light/dark, glass and brutalist are all
   covered by construction rather than by a per-theme rule — and the Cancel and
   Undo buttons are `button.secondary`, which already carries the house rule
   that a control performing an action has to look like a control. */
#ib-spend-dialog { max-width: min(30rem, 94vw); }
/* The selects are the same size as the boxes they sit between. The bare `select`
   rule is sized for a table cell, and a 0.2rem-tall picker next to a 0.65rem
   text input reads as two different kinds of field in one form. */
#ib-spend-dialog select {
  width: 100%;
  padding: 0.6rem 0.7rem;
  font-size: inherit;
  border: var(--bw) solid var(--line);
  border-radius: var(--radius-sm);
}
#ib-spend-dialog select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Two fields that are one sentence — "cash leaves X and lands in Y", "on D with
   note N" — so they sit side by side and wrap together on a narrow screen. */
.ibs-route {
  display: grid; grid-template-columns: 1fr 1fr; gap: 0 0.8rem;
}
.ibs-route input[type="date"] { max-width: none; }
/* The single pool account, drawn as text because a select with one option is a
   control that cannot be used. Sized to sit on the same line a select would. */
.ibs-one {
  margin: 0; padding: 0.6rem 0.7rem;
  border: var(--bw) solid transparent;
  font-variant-numeric: tabular-nums;
}
#ibs-dupe { margin-top: 0.6rem; }

.ibspends { margin-top: 1.2rem; border-top: 1px solid var(--line); padding-top: 0.8rem; }
.ibspends-h {
  margin: 0 0 0.4rem; font-size: 0.8rem; font-weight: 600;
  color: var(--fg-dim); text-transform: uppercase; letter-spacing: 0.04em;
}
.ibspend-list { list-style: none; margin: 0; padding: 0; }
/* Date, what moved where, the amount, Undo. `auto` on the date rather than a
   fixed rem: it is nowrap tabular-nums, so the column sizes to it exactly —
   the same reasoning as .act-date, where a guessed 4.2rem clipped the year. */
.ibspend {
  display: grid; grid-template-columns: auto 1fr auto auto;
  gap: 0.6rem; align-items: center;
  padding: 0.4rem 0; border-bottom: 1px solid var(--line);
}
.ibspend:last-child { border-bottom: none; }
.ibspend-date { white-space: nowrap; font-variant-numeric: tabular-nums; color: var(--fg-dim); }
.ibspend-what { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ibspend-amt { text-align: right; white-space: nowrap; }
/* The Undo buttons sit in a row of their own, so the shared .sm top margin
   would push each one off its row's baseline. */
.ibspend button.ibs-undo { margin-top: 0; }

/* The sheet grows and shrinks like the Activity sheet, and for the same reason:
   it is opened from a control on the card it belongs to. */
@keyframes ibs-in  { from { transform: scale(0.95); opacity: 0; } to { transform: scale(1); opacity: 1; } }
@keyframes ibs-out { from { transform: scale(1); opacity: 1; } to { transform: scale(0.95); opacity: 0; } }
#ib-spend-dialog[open] { animation: ibs-in 220ms cubic-bezier(.2, .8, .2, 1); }
#ib-spend-dialog.closing { animation: ibs-out 220ms cubic-bezier(.2, .8, .2, 1); }
#ib-spend-dialog[open]::backdrop { animation: act-backdrop-in 220ms cubic-bezier(.2, .8, .2, 1); }
#ib-spend-dialog.closing::backdrop { animation: act-backdrop-out 220ms cubic-bezier(.2, .8, .2, 1); }

@media (max-width: 40rem) {
  /* A phone gets a bottom sheet, same as #act-dialog. `display: flex` is scoped
     to [open] — this ID selector otherwise outranks the UA rule that hides a
     dialog with no `open` attribute, leaving the sheet permanently on screen. */
  #ib-spend-dialog {
    margin: auto 0 0; width: 100%; max-width: none;
    border-radius: var(--radius) var(--radius) 0 0;
    max-height: 90vh; overflow-y: auto;
  }
  #ib-spend-dialog[open] { display: block; }
  @keyframes ibs-up   { from { transform: translateY(100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
  @keyframes ibs-down { from { transform: translateY(0); opacity: 1; } to { transform: translateY(100%); opacity: 0; } }
  #ib-spend-dialog[open] { animation: ibs-up 220ms cubic-bezier(.2, .8, .2, 1); }
  #ib-spend-dialog.closing { animation: ibs-down 220ms cubic-bezier(.2, .8, .2, 1); }
  /* One column: two selects side by side on a 390px screen truncate both
     account names to nothing useful. */
  .ibs-route { grid-template-columns: 1fr; }
  /* The amount stays on the first line and Undo drops under it rather than
     squeezing the names out. */
  .ibspend { grid-template-columns: auto 1fr auto; }
  .ibspend button.ibs-undo { grid-column: 2 / -1; justify-self: end; margin-top: 0.3rem; }
}

/* Reduced motion drops the transform, and closeSpend() checks the same query so
   its close path never waits on an animationend this rule guarantees will not
   fire. Last, so it wins the cascade tie against the phone block above. */
@media (prefers-reduced-motion: reduce) {
  #ib-spend-dialog[open], #ib-spend-dialog.closing,
  #ib-spend-dialog[open]::backdrop, #ib-spend-dialog.closing::backdrop { animation: none; }
}
