/*
 * Application styles. Deliberately plain CSS — no framework, no build step.
 * Propshaft serves this file directly.
 */

/* Served from this machine, not from a CDN. A webfont request carries the
   reader's IP and the page they are on to whoever serves it, and every other
   byte here comes off this box. Both are variable fonts, so one file covers
   every weight the app uses.
 *
 * `swap` because the alternative is text that is invisible while the font
 * loads, and this app is read on a phone over a home connection. */
@font-face {
  font-family: "Inter var";
  src: url("/assets/Inter-latin-1f021a24.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Playfair Display var";
  src: url("/assets/PlayfairDisplay-latin-759b7dc1.woff2") format("woff2");
  font-weight: 500 800;
  font-style: normal;
  font-display: swap;
}

:root {
  /* Headlines are set in a serif with real contrast between thick and thin,
     which is what makes a page of figures read as a document rather than a
     dashboard. Body text is Inter, drawn for screens, with tabular figures
     available — this whole app is columns of numbers. */
  --font-body: "Inter var", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-display: "Playfair Display var", Georgia, "Times New Roman", serif;
}

:root {
  --bg: #fbfbfa;
  --surface: #ffffff;
  --text: #1c1c1a;
  /* Secondary text, and there is a lot of it: table headers at .76rem,
     merchants, dates, chart captions, legends. It was #565651 — 7.4:1, which
     passes every check and still reads grey at those sizes. Darkened to 10.9:1,
     leaving it 1.6x lighter than --text rather than 2.3x: still clearly the
     second rank, no longer something to squint at. */
  --muted: #3d3d38;
  --line: #e3e3df;
  /* A hairline that has to hold its own against the *page* rather than against
     a card sitting on it. `--line` is drawn to separate a row from the white
     surface it sits on, and on the page background it very nearly disappears:
     #e3e3df against #fbfbfa is 1.23:1, where the same pair in dark mode is
     1.39:1. That gap is the whole of why the nav's group boxes were visible in
     dark mode and not in light. Stepped to match dark's separation rather than
     to a round number — see `.site-nav__group`. */
  --line-on-page: #d8d8d3;
  --edge: #7d7d76;
  --accent: #2f5d50;
  --out: #9a3412;
  --in: #166534;

  /* Money crossing the account boundary. Green in, red out — validated in
     band and above 3:1 against the surface. The pair is ΔE 4 apart under
     deuteranopia, which is to say identical, so nothing in either chart may
     rest on the hue alone: the bars separate in and out by which side of zero
     they sit on, and the lines are dashed and directly labelled. */
  --flow-in: #166534;
  --flow-out: #991b1b;
  --flow-net: #4f4f4a;

  /* The alert hue for when it is set as words rather than drawn as a mark. A
     dot or a dashed rule carries at 3.95:1; an 11px label does not, and nor
     does the error on a sign-in form somebody is already stuck on. Same hue,
     stepped down to 6.6:1. At :root because that form is not a chart —
     `.signin__errors` asked for the chart's `--series-alert`, got nothing, and
     had been rendering in plain body colour. */
  --series-alert-text: #b02a26;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #16171a;
    --surface: #1e1f23;
    --text: #e8e8e4;
    /* The same widening, in the direction that means it here. */
    --muted: #bfbfb9;
    --line: #303138;
    /* Dark mode is where this already worked, so the value is `--line` itself:
       #303138 on #16171a is the 1.39:1 the light value was chosen to match. */
    --line-on-page: #303138;
    --edge: #8e8f99;
    --accent: #7fb4a2;
    --out: #f0a882;
    --in: #86c99b;

    /* Re-stepped for the dark surface rather than flipped: the dark band is
       only L 0.48-0.67, so the light-mode steps fall outside it. */
    --flow-in: #16a34a;
    --flow-out: #ef4444;
    --flow-net: #b4b4ae;
    --series-alert-text: #ef8a8a;
  }
}

* { box-sizing: border-box; }

/* Nothing on any screen needs the page itself to scroll sideways. Wide content
   — a twelve-column table, a chart drawn at 960 units — scrolls inside its own
   box, and when something escapes that box the reader gets two horizontal
   scrolls stacked on each other and no way to tell which one they are moving.
 *
 * On `html` only. Setting it on the body as well made the body a scroll
 * container — `overflow-x: hidden` forces the other axis to `auto` — and a
 * scroll container clips its absolutely positioned descendants, which cut the
 * bottom off the nav menu wherever the page content happened to end. */
html { overflow-x: hidden; }

body {
  margin: 0;
  max-width: 100%;
  padding: 2rem 1.5rem 4rem;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.55 var(--font-body);
  -webkit-font-smoothing: antialiased;
}

/* Form controls do not inherit the page's font — that is the browser's default,
   not something we chose — so every input and button drew in Helvetica or
   Segoe UI while everything around it was Inter. Four components already opted
   back in one at a time; the sign-in and password forms, and every submit
   button, never did. Set once, so a control added later cannot forget. */
button,
input,
select,
textarea {
  font: inherit;
  letter-spacing: inherit;
}

.page-header { margin-bottom: 1.5rem; }
.page-header h1 {
  margin: 0 0 .25rem;
  font-family: var(--font-display);
  font-size: 1.85rem;
  font-weight: 600;
  letter-spacing: 0;
  line-height: 1.15;
}
.summary { margin: 0; color: var(--muted); }
.summary .as-of { font-size: .85rem; }

/* Explains a column whose meaning is not obvious from its header. */
.note { margin: .5rem 0 0; color: var(--muted); font-size: .85rem; max-width: 62ch; }

/* Recessive against Amount — it is context for the row, not the row's fact. */
td.balance { color: var(--muted); }

/* --- the balances comparison on Home --- */

.balances-bars { margin: 0 0 2rem; }

/* The section heading, above the accounts it covers. It was 1.15rem while each
   account name below it was 1.25rem, so the label read as smaller than the
   things it labelled and the page looked like a list of accounts that happened
   to start with a caption. Sized between the page title and the account names
   so the three levels read in the order they nest. */
.holdings-section__heading {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 600;
  margin: 0 0 .6rem;
}

.holdings-section { margin-bottom: .5rem; }

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

/* Three columns on a screen with room: name, bar, figure. The name column is
   sized to the longest account rather than to a guess, and the figure is
   tabular so the column reads down. */
.balances-bars__row {
  align-items: center;
  display: grid;
  gap: .75rem;
  grid-template-columns: minmax(9rem, 17rem) 1fr minmax(4.5rem, auto);
  padding: .3rem 0;
}

.balances-bars__label { font-size: .9rem; }

.balances-bars__track {
  background: color-mix(in srgb, var(--line) 55%, transparent);
  border-radius: 3px;
  display: block;
  height: 1.1rem;
  overflow: hidden;
}

.balances-bars__bar {
  background: var(--accent);
  border-radius: 3px;
  display: block;
  height: 100%;
  min-width: 2px;
}

/* Debts read as debts without relying on the colour alone: they carry a
   negative figure beside them and sit at the bottom of a list sorted by worth. */
.balances-bars__bar--owed {
  background: repeating-linear-gradient(135deg,
    var(--flow-out) 0 5px, color-mix(in srgb, var(--flow-out) 55%, var(--surface)) 5px 10px);
}

.balances-bars__value {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  text-align: right;
  white-space: nowrap;
}

/* --- allocation --- */

/* The sleeve table borrows the standard table styling and changes two things.

   The symbols do not wrap. A sleeve names every fund it may be filled with, so
   the cell holds "AVUS, SPY, VTI" — and wrapped, that is a three-line cell,
   which makes a three-line row, in a column that is off the right-hand edge on
   a laptop. Nine rows of unexplained height, sized by content nobody could see.
   The panel already scrolls sideways; this lets it. */
table.allocation__sleeves td.symbol { white-space: nowrap; }

/* A fund the policy names for this sleeve and the household does not own.
   Recessive, because the column's other symbols are read off a statement and
   this one is only a permission — the two were previously indistinguishable,
   under a heading that called all of them held. */
.allocation__unheld { color: var(--muted); font-weight: 400; }

/* A group's line: the sum of the sleeves above it, ruled off from them the way
   a total is. Not `.row-heading`, which is a flex layout for the inside of a
   cell and collapses a whole `tr` into one line if it is put there — which is
   exactly what it did. */
tr.allocation__subtotal th,
tr.allocation__subtotal td {
  border-top: 1px solid var(--line);
  font-weight: 600;
}

table.allocation__sleeves tfoot th,
table.allocation__sleeves tfoot td {
  border-top: 2px solid var(--line);
  font-weight: 600;
}

/* Actual and target are what this table is for, so they carry the weight and
   the money columns beside them recede. Bolding every figure would be the same
   as bolding none; what makes these two read first is that the four columns
   around them do not compete. */
table.allocation__sleeves td.allocation__cell-share {
  color: var(--text);
  font-size: 1rem;
  font-weight: 600;
}
table.allocation__sleeves td.allocation__cell-money { color: var(--muted); }

/* The share, beside its own bar. Sized to be the figure the row is read for —
   see the view for why it leads and the amount follows. Tabular so the column
   of them lines up on the decimal, which is the whole reason a column of
   percentages is worth reading down. */
.allocation__share {
  font-size: 1.05rem;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

.allocation__share-target {
  color: var(--muted);
  font-size: .82rem;
  font-variant-numeric: tabular-nums;
  font-weight: 400;
  margin-left: .3rem;
}

/* The dollar figure, on its own line under the share. Muted and small: it is
   the same fact in units that move every session. */
.allocation__amount {
  color: var(--muted);
  display: block;
  font-size: .78rem;
  font-variant-numeric: tabular-nums;
  font-weight: 400;
}

/* The value column now holds two lines rather than one figure, so it is given
   room to stop "19.17% of 20%" wrapping mid-phrase. */
.allocation__bars .balances-bars__value { min-width: 8.75rem; }

/* A group's line keeps the hierarchy it had: its share is still the figure, but
   a step down from a sleeve's, because it is a sum of the rows above it. */
.allocation__row--subtotal .allocation__share { font-size: .88rem; }
.allocation__row--subtotal .allocation__amount { font-size: .74rem; }

/* The Allocation screen's target mark, standing on the same track as the bar
   that has to reach it.

   `overflow: hidden` on the track is what clips the bar to its own rounded
   corners, and it would clip this too, so the mark is inset rather than
   overhanging: a full-height rule at the exact target, two pixels wide. The
   track is given a positioning context here rather than on `.balances-bars__track`
   itself, because nothing else that uses that class has anything to position. */
.balances-bars__track:has(.allocation__target) { position: relative; }

.allocation__target {
  background: var(--text);
  bottom: 0;
  display: block;
  position: absolute;
  top: 0;
  transform: translateX(-1px);
  width: 2px;
}

/* Over target reads as over target without the colour carrying it alone: the
   bar runs past a mark that is right there beside it, which is the comparison.
   The hatch is the second channel, for the same reason the spending page
   hatches an uncategorised bar. */
.allocation__bar--over {
  background: repeating-linear-gradient(135deg,
    var(--accent) 0 5px, color-mix(in srgb, var(--accent) 55%, var(--surface)) 5px 10px);
}

/* A group's line under its members. Lighter and thinner than a sleeve, because
   it is a sum of the rows above rather than another one of them &mdash; the same
   relationship the spending page draws between a root and its children, in the
   other direction. */
.allocation__row--subtotal { padding-top: .1rem; }
.allocation__row--subtotal .balances-bars__label { color: var(--muted); font-size: .82rem; }
.allocation__row--subtotal .balances-bars__track { background: none; height: .4rem; }
.allocation__row--subtotal .balances-bars__bar {
  background: color-mix(in srgb, var(--muted) 45%, transparent);
}
.allocation__row--subtotal .balances-bars__value {
  color: var(--muted);
  font-size: .82rem;
  font-weight: 400;
}

.balances-bars__totals {
  border-top: 1px solid var(--line);
  display: flex;
  flex-wrap: wrap;
  gap: 1.1rem;
  margin: .7rem 0 0;
  padding-top: .6rem;
  font-size: .9rem;
}

.balances-bars__as-of { color: var(--muted); }

/* On a phone the name goes above its own bar rather than fighting it for width:
   a 9rem label column beside a bar leaves neither enough to be read. */
@media (max-width: 620px) {
  .balances-bars__row {
    grid-template-columns: 1fr auto;
    grid-template-areas: "label value" "track track";
    gap: .2rem .75rem;
    padding: .45rem 0;
  }
  .balances-bars__label { grid-area: label; }
  .balances-bars__value { grid-area: value; }
  .balances-bars__track { grid-area: track; }
}

/* Subcategory rows under their root on the spending page: indented, thinner
   and lighter, but on the same scale as the roots — a bar is read against the
   whole list, not just against its siblings. The track loses its background
   because a full-width trough under a half-height bar reads as a second bar. */
.balances-bars__row--sub { padding: .1rem 0; }
.balances-bars__row--sub .balances-bars__label { font-size: .82rem; padding-left: 1.1rem; }
.balances-bars__row--sub .balances-bars__label a { color: var(--muted); }
.balances-bars__row--sub .balances-bars__track { background: none; height: .55rem; }
.balances-bars__row--sub .balances-bars__bar {
  background: color-mix(in srgb, var(--accent) 45%, transparent);
}
.balances-bars__row--sub .balances-bars__value {
  color: var(--muted);
  font-size: .82rem;
  font-weight: 400;
}

/* Spending filed under a root directly when the root also has children. */
.balances-bars__direct { color: var(--muted); font-style: italic; }

/* Money that left without a category yet. Hatched in grey rather than drawn
   in the accent, because the bar states a backlog, not a kind of spending. */
.balances-bars__bar--unexplained {
  background: repeating-linear-gradient(135deg,
    var(--muted) 0 5px, color-mix(in srgb, var(--muted) 40%, var(--surface)) 5px 10px);
}

/* An internal movement's category cell: the counterparty account, because
   there is no category to choose and a blank select would read as a decision
   waiting to be made. */
.categorize__internal { color: var(--muted); font-size: .85rem; white-space: nowrap; }

/* Sortable column headings on the transactions table. The arrow marks the
   column doing the ordering; the heading keeps its colour so the header row
   does not turn into a row of links. */
table.transactions thead th a.sort { color: inherit; text-decoration: none; }
table.transactions thead th a.sort:hover { text-decoration: underline; }
.sort-arrow { color: var(--accent); }

/* One decision applied to a whole filtered set, sitting between the filters
   that chose the set and the table that shows it. */
.bulk-categorize {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: .6rem;
  margin: -.5rem 0 1.25rem;
}
.bulk-categorize__label { color: var(--muted); font-size: .85rem; }
.bulk-categorize select {
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 5px;
  color: var(--text);
  font-size: .85rem;
  padding: .3rem .4rem;
}
.bulk-categorize input[type="submit"] {
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: 5px;
  color: #fff;
  cursor: pointer;
  font-size: .85rem;
  padding: .3rem .9rem;
}

/* The category table nests the same way the bars above it do. */
table.transactions tr.category-sub th {
  color: var(--muted);
  font-weight: 400;
  padding-left: 1.4rem;
}
table.transactions tr.category-sub th a { color: var(--muted); }
table.transactions tr.category-sub td { color: var(--muted); }

/* --- holdings: positions & basis --- */

.holdings { margin-bottom: 2.5rem; }
.holdings h2 { font-family: var(--font-display); font-size: 1.25rem; font-weight: 600; margin: 0 0 .3rem; }

.fund-heading {
  align-items: baseline;
  display: flex;
  flex-wrap: wrap;
  font-size: .95rem;
  gap: .5rem;
  margin: 1.75rem 0 .5rem;
}

.fund-heading__meta { color: var(--muted); font-size: .8rem; font-weight: 400; }

/* Tabular figures so share counts and money line up column-wise. */
.symbol { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 600; }
table.positions .numeric, table.basis .numeric { font-variant-numeric: tabular-nums; }

table.positions tfoot th,
table.positions tfoot td,
table.basis tfoot th,
table.basis tfoot td {
  border-top: 2px solid var(--line);
  font-weight: 600;
  padding-top: .55rem;
}

.pct { color: var(--muted); font-size: .81rem; margin-left: .3rem; }
td.pct-cell { color: var(--muted); }
td.share-of-position { color: var(--muted); font-variant-numeric: tabular-nums; }

/* A flagged lot is a data-quality warning, not a value judgement about the
   money, so the row is tinted rather than coloured like a loss — and the flag
   glyph carries the meaning so it is never colour alone. */
tr.is-suspect { background: color-mix(in srgb, var(--out) 8%, transparent); }
tr.is-suspect td.share-of-position { color: var(--text); font-weight: 600; }
.flag { color: var(--out); cursor: help; margin-left: .15rem; }

/* The running total is a reading aid for the column beside it, so it sits back
   from the per-lot gain rather than competing with it. */
td.cumulative { color: var(--muted); font-variant-numeric: tabular-nums; }
table.basis tfoot td.cumulative { color: var(--text); }

/* Distinguishes what created a lot without leaning on colour alone. */
.lot-kind {
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--muted);
  font-size: .77rem;
  padding: .1rem .5rem;
  white-space: nowrap;
}

.lot-kind--capital-gain { border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); }
.lot-kind--transfer-in { border-style: dashed; }

/* --- filters --- */

.filters {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 1rem;
  margin-bottom: 1.5rem;
}

.filters__row {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  align-items: flex-end;
}

.filters__row + .filters__row { margin-top: .875rem; }

/* The submit disappears only once JavaScript has taken over submitting for it.
   Hiding it in the stylesheet alone would strand anybody the controller never
   reached with a filter that cannot be applied. */
.filters.is-auto-submitting .filters__actions { display: none; }

.filters label {
  display: flex;
  flex-direction: column;
  gap: .3rem;
  font-size: .8rem;
  color: var(--muted);
}

.filters input,
.filters select {
  font-size: .9rem;
  padding: .4rem .5rem;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 5px;
  min-width: 11rem;
}

.filters__actions { display: flex; gap: .5rem; align-items: center; margin-left: auto; }

.filters input[type="submit"] {
  min-width: 0;
  cursor: pointer;
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  padding: .45rem 1rem;
}

.button-link { color: var(--muted); font-size: .85rem; text-decoration: none; }
.button-link:hover { color: var(--text); text-decoration: underline; }

/* --- table --- */

.transactions {
  width: 100%;
  border-collapse: collapse;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
}

.transactions th,
.transactions td {
  text-align: left;
  padding: .55rem .75rem;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
}

.transactions thead th {
  font-size: .76rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
  font-weight: 600;
  background: var(--bg);
}

.transactions tbody tr:last-child td { border-bottom: 0; }
.transactions tbody tr:hover { background: color-mix(in srgb, var(--accent) 6%, transparent); }

.numeric { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.date { font-variant-numeric: tabular-nums; color: var(--muted); white-space: nowrap; }
.account { color: var(--muted); font-size: .875rem; white-space: nowrap; }

.merchant { display: block; font-size: .8rem; color: var(--muted); }
.uncategorized { color: var(--muted); font-style: italic; }
.muted { color: var(--muted); }

/* A payer as the bank prints it, which is in capitals. Under the name of the
   row rather than as it, small and recessive, so a column of them reads as
   provenance rather than as shouting — title-casing them would turn STRS into
   Strs and OSU's payroll line into something no statement says. */
.payer {
  color: var(--muted);
  display: block;
  font-size: .72rem;
  font-weight: 400;
  letter-spacing: .02em;
}

/* A figure nothing else on the page is allowed to use: it has not happened
   yet. Recessive against the observed column beside it. */
td.projected { color: var(--muted); }

.amount--out { color: var(--out); }
.amount--in { color: var(--in); font-weight: 600; }

/* --- pagination --- */

.pagination {
  display: flex;
  gap: 1rem;
  align-items: center;
  justify-content: center;
  margin-top: 1.25rem;
  font-size: .9rem;
  color: var(--muted);
}

.pagination a { color: var(--accent); text-decoration: none; }
.pagination a:hover { text-decoration: underline; }

.empty { color: var(--muted); padding: 2rem; text-align: center; }

@media (max-width: 800px) {
  .filters__actions { margin-left: 0; }
  .account { display: none; }
}

/* --- navigation --- */

.site-nav {
  align-items: center;
  display: flex;
  font-size: .92rem;
  gap: 1rem;
  margin-bottom: 1.5rem;
}

.site-nav__links { display: flex; flex-wrap: wrap; gap: .4rem .5rem; }

/* A group is a column: what it is called, centred over the screens it covers,
   and those screens in a row beneath it. Centred rather than left-aligned
   because a label sitting over the first link of four reads as belonging to
   that link rather than to the set. */
.site-nav__group {
  align-items: center;
  border-radius: 7px;
  display: flex;
  flex-direction: column;
  gap: .1rem;
  padding: .2rem .6rem .25rem;
}

/* Shaded so each set is bounded at a glance.
 *
 * `--surface` with a hairline rather than a tint of `--line`: a tint of the
 * line colour is nearly invisible in dark mode, where --line sits a few points
 * off the page background. Surface is a step away from the background in both
 * themes by construction, which is the whole reason it exists.
 *
 * That reasoning was right about dark mode and never checked against light,
 * where both halves of it are much weaker. `--surface` is #ffffff on a #fbfbfa
 * page — 1.03:1, a difference of four points per channel — and `--line` is
 * 1.23:1 against it where dark's is 1.39:1. So the fill carries nothing in
 * light mode and the border carried nearly nothing, and Expenses, Investments
 * and Bookkeeping had no visible box at all. The fill stays as it is; the
 * border is the half that has to work, and `--line-on-page` is it. */
.site-nav__group:not(.site-nav__group--unlabelled) {
  background: var(--surface);
  border: 1px solid var(--line-on-page);
}

/* The unlabelled group carries no box, so its links would sit a border-width
   higher than everyone else's. */
.site-nav__group--unlabelled { border: 1px solid transparent; }

.site-nav__group-links { display: flex; flex-wrap: wrap; gap: 1.15rem; }

/* Not a link, and deliberately quiet: there is no page behind it. */
.site-nav__group-label {
  color: var(--muted);
  font-size: .68rem;
  letter-spacing: .07em;
  opacity: .75;
  text-align: center;
  text-transform: uppercase;
}

/* A group with no name — Overview and Taxes — would ride up level with the
   labels while every other link sat below them. Pushing its links to the
   bottom of the column keeps one baseline across the row without inventing a
   heading to fill the space. */
.site-nav__group--unlabelled { justify-content: flex-end; }

/* Pushed to the far right by the auto margin, and first in source order after
   the menu so it keeps that corner when the links wrap to a second line. */
.site-nav__household {
  align-items: center;
  border: 1px solid var(--line);
  border-radius: 999px;
  display: inline-flex;
  flex: none;
  font-size: .82rem;
  font-weight: 600;
  gap: .4rem;
  margin-left: auto;
  order: 99;
  padding: .18rem .6rem;
  white-space: nowrap;
}

.site-nav a {
  border-bottom: 2px solid transparent;
  color: var(--muted);
  padding-bottom: .2rem;
  text-decoration: none;
}
.site-nav a:hover { color: var(--text); }
.site-nav a.is-current { border-bottom-color: var(--accent); color: var(--text); font-weight: 600; }

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

.menu { position: relative; flex: none; }

.menu__button {
  align-items: center;
  border: 1px solid var(--line);
  border-radius: 7px;
  cursor: pointer;
  display: flex;
  height: 2rem;
  justify-content: center;
  list-style: none;
  width: 2.25rem;
}
.menu__button::-webkit-details-marker { display: none; }
.menu__button:hover { border-color: var(--edge); }
.menu__button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Three bars from one element: the middle is the box, the others its borders
   held off at a distance. */
.menu__bars,
.menu__bars::before,
.menu__bars::after {
  background: var(--text);
  border-radius: 1px;
  content: "";
  display: block;
  height: 2px;
  position: absolute;
  width: 1.05rem;
}
.menu__bars { position: relative; }
.menu__bars::before { top: -6px; }
.menu__bars::after { top: 6px; }

.menu[open] .menu__button { border-color: var(--edge); }

.menu__panel {
  background: var(--surface);
  border: 1px solid var(--edge);
  border-radius: 9px;
  box-shadow: 0 6px 24px rgb(0 0 0 / 14%);
  left: 0;
  margin-top: .4rem;
  min-width: 13rem;
  padding: .35rem;
  position: absolute;
  top: 100%;
  z-index: 40;
}

.menu__section { padding: .3rem 0; }
.menu__section + .menu__section { border-top: 1px solid var(--line); }

.menu__heading {
  color: var(--muted);
  display: block;
  font-size: .72rem;
  letter-spacing: .06em;
  padding: .25rem .55rem .3rem;
  text-transform: uppercase;
}

.menu__panel a,
.menu__link,
.menu__option {
  background: none;
  border: 0;
  border-radius: 5px;
  color: var(--text);
  cursor: pointer;
  display: block;
  padding: .38rem .55rem;
  text-align: left;
  text-decoration: none;
  width: 100%;
}
.menu__panel a:hover,
.menu__link:hover,
.menu__option:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); }
.menu__panel a.is-current { border-bottom: 0; font-weight: 600; }
.menu__form { display: block; margin: 0; }

.menu__option.is-current { font-weight: 600; }
.menu__option.is-current::after { content: " ·"; color: var(--accent); }

.menu__link--signout { color: var(--out); }

/* The screens are only in the menu where they do not fit beside it. */
.menu__section--pages { display: none; }

@media (max-width: 720px) {
  .site-nav__links { display: none; }
  .menu__section--pages { display: block; }
}

/* --- stat row --- */

.stat-row { display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: 1.25rem; }

.stat {
  flex: 1 1 12rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: .75rem .9rem;
  display: flex;
  flex-direction: column;
  gap: .15rem;
}

.stat__label { font-size: .76rem; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
.stat__value { font-size: 1.4rem; font-weight: 600; font-variant-numeric: tabular-nums; }
.stat__note { font-size: .8rem; color: var(--muted); }

/* A second summary line under the first — the last session's move, which is a
   different question from the window's and sits below rather than beside it. */
.summary--secondary { align-items: baseline; display: flex; gap: .5rem; margin-top: -.35rem; }

/* A year in progress states its headline as the whole year and then breaks it
   in two underneath: what has actually arrived, and what is still expected.
   Two figures rather than a sentence, because the pair is the thing being
   read — a projection is only worth showing beside the observation it is
   built from.

   Side by side, so the split reads across rather than as a second stack of
   labels under the first. */
.stat__split { display: flex; gap: 1.1rem; margin-top: .3rem; }
.stat__part { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
.stat__part-label {
  color: var(--muted);
  font-size: .66rem;
  letter-spacing: .05em;
  text-transform: uppercase;
}
.stat__part-value { font-size: .95rem; font-variant-numeric: tabular-nums; font-weight: 600; }

/* What has not happened yet, in the same tone the tables already use for a
   figure the ledger has not seen. */
.stat__part--projected .stat__part-value { color: var(--muted); }

/* --- balance chart ---
   Colours are the validated data-viz palette slot 1 (blue) with the status red
   reserved for a negative balance. Both modes pass the six checks against
   their own surface; the dark steps are chosen for the dark band, not flipped. */

.chart {
  --series: #2a78d6;
  --series-alert: #e34948;
  margin: 0 0 1.5rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: .75rem;
}

@media (prefers-color-scheme: dark) {
  .chart {
    --series: #3987e5; --series-alert: #e66767; --series-2: #d95926;
  }
}

/* Second categorical slot, paired with --series. Validated against both
   surfaces: worst CVD separation 24.7 light / 26.8 dark, well clear of the
   floor, so the two series stay distinct for colourblind readers. */
.chart { --series-2: #eb6834; }

/* Zero here is the axis the bars stand on, not the "you are overdrawn" line the
   balance chart marks, so it is a plain rule rather than a dashed alert. */
.chart--stack .chart__zero { stroke: var(--line); stroke-dasharray: none; opacity: 1; }

.bar__seg--dividend { fill: var(--series); }
.bar__seg--capital-gain { fill: var(--series-2); }

/* The non-qualified half of a dividend bar. The same hue as the qualified half
   because it is the same money — a tax character, not a second series — at a
   lighter value so the division inside the band reads without competing with
   the division between dividends and capital gains, which is a hue apart.

   Drawn as stripes first, on the argument that an apportioned figure should
   look apportioned. At the width these bars are actually rendered — twenty-odd
   years across 960px — a 6px hatch had barely two stripes to show and read as
   noise rather than as a texture. A tint survives the width. */
.bar__seg--dividend-ordinary { fill: color-mix(in srgb, var(--series) 40%, var(--surface)); }

/* Hover raises the whole year, not one segment, because the stack is read as a
   unit and the native title covers both. */
.bar:hover .bar__seg { fill-opacity: .82; }

/* The current year has not finished distributing. Lightened and labelled in
   the caption and table rather than hidden, so the series stays continuous. */
.bar--partial .bar__seg { fill-opacity: .45; }

/* Where a tenth of the position is reached, selling cheapest first — the same
   steps the projection's unwind dial offers, marked on the row a sale of that
   size stops on. A tag rather than a column: it lands on one row in ten and a
   column of blanks would cost more space than it earns. */
.unwind-step {
  background: color-mix(in srgb, var(--accent) 14%, var(--surface));
  border-radius: 3px;
  color: var(--muted);
  font-size: .7rem;
  font-variant-numeric: tabular-nums;
  margin-left: .35rem;
  padding: .05rem .3rem;
}

.chart__value { font-size: 10px; font-weight: 600; fill: var(--muted); }
/* A clamped bar understates itself, so its real figure is not optional. */
.chart__value--clipped { fill: var(--text); }

/* Small multiples share a scale, so they are laid out in one column at equal
   width — side by side at different widths would break the comparison. */
.small-multiples { display: grid; gap: .5rem; }

/* A grid item will not shrink below its content's min-content width unless it
   is told it may, and the chart inside carries a `min-width` on narrow screens
   so its axis stays readable. Without this the item is pushed out to that
   width, taking the page with it — the chart scrolls inside its own box *and*
   the page scrolls under it, which is two horizontal scrolls over one chart. */
.small-multiples__item { min-width: 0; }
.small-multiples__item .chart { margin-bottom: .5rem; }
.small-multiples h4 { font-size: .9rem; margin: .5rem 0 .35rem; }

.legend { display: flex; flex-wrap: wrap; gap: 1rem; margin: 0 0 .5rem; font-size: .8rem; color: var(--muted); }
.legend__item { align-items: center; display: inline-flex; gap: .35rem; }

.legend__swatch { border-radius: 2px; display: inline-block; height: 10px; width: 10px; }
.legend__swatch--dividend { background: var(--series); }
.legend__swatch--capital-gain { background: var(--series-2); }

.legend__swatch--dividend-ordinary { background: color-mix(in srgb, var(--series) 40%, var(--surface)); }

/* Every chart scales proportionally. This used to be a fixed 240px height with
   the two line charts stretching to fill it via preserveAspectRatio="none",
   which is not "fine for a line": a 960-wide viewBox in a 1232px box is scaled
   1.28x across and 1.0x down, so the axis labels smear, the marker dots become
   ellipses, and every slope on the line reads flatter than it is. The Forecast
   chart was the obvious victim, being the one whose values move least.

   One rule rather than a blanket and two overrides. The overrides existed only
   to undo this one, and the comment they carried recorded an ordering bug —
   identical specificity meant whichever came last won, and the stack charts sat
   pinned to 240px until somebody noticed. */
.chart svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
  /* Deliberately no `touch-action` override. It was `pan-y`, which hands the
     app every horizontal gesture — and the horizontal gesture on a chart wider
     than the screen is how you scroll it. The browser should keep both
     directions: vertical scrolls the page, horizontal scrolls the chart, and
     reading a value is a tap rather than a drag. */
}

.chart__grid { stroke: var(--line); stroke-width: 1; }
/* Solid and in the text colour, not a red dash. Zero is the baseline the whole
   chart is measured from now that the axis always includes it, and a dashed red
   line reads as a warning threshold rather than as the floor. */
.chart__zero { stroke: var(--text); stroke-width: 1.5; opacity: .8; }
.chart__axis { font-size: 11px; fill: var(--muted); }

/* Time gridlines sit under the money ones rather than beside them — two grids
   of equal weight read as a mesh and neither gets used. */
.chart__grid--time { opacity: .45; }
.chart__axis--time { font-size: 10.5px; }

.chart__area { fill: var(--series); opacity: .1; stroke: none; }
.chart__line { fill: none; stroke: var(--series); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.chart__line--projected { stroke-dasharray: 5 4; opacity: .85; }

/* Three rules, three different claims about the line, so they are told apart
   by weight rather than by colour alone — the labels carry the meaning and
   these carry the emphasis. Today is solid because it is the only one of the
   three that is a fact about the calendar rather than about the data. */
.chart__rule { stroke: var(--muted); stroke-width: 1; stroke-dasharray: 3 3; }
.chart__rule--last-transaction { stroke-dasharray: 2 4; opacity: .7; }
.chart__rule--today { stroke-dasharray: none; opacity: .55; }

.chart__rule-label {
  fill: var(--muted);
  font-size: 9.5px;
  letter-spacing: .03em;
  text-transform: uppercase;
}

/* 2px surface ring so the marker reads against the line it sits on */
.chart__marker { fill: var(--series); stroke: var(--surface); stroke-width: 2; }
.chart__marker--alert { fill: var(--series-alert); }
.chart__label { font-size: 11px; font-weight: 600; fill: var(--text); }
.chart__label--alert { fill: var(--series-alert-text); }

.chart__caption--note { display: block; text-align: center; }

.chart__caption {
  display: flex;
  justify-content: space-between;
  font-size: .81rem;
  color: var(--muted);
  padding: .35rem .25rem 0;
}

.chart__caption-mid { font-style: italic; }

/* --- projected rows --- */

.transactions tr.divider td {
  background: var(--bg);
  font-size: .81rem;
  color: var(--muted);
  text-align: center;
  padding: .5rem;
  border-top: 2px solid var(--accent);
}

.row--projected td { opacity: .82; }
.row--projected .date { font-style: italic; }

.tag {
  display: inline-block;
  margin-left: .4rem;
  padding: .05rem .35rem;
  font-size: .68rem;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--muted);
  border: 1px solid var(--line);
  border-radius: 3px;
}

.filters--inline { display: flex; flex-wrap: wrap; gap: 1rem; align-items: flex-end; }
.filters--inline input[type="number"] { min-width: 6rem; }

/* --- chart hover layer --- */

.chart { position: relative; }

.chart__crosshair { stroke: var(--muted); stroke-width: 1; opacity: .55; pointer-events: none; }
.chart__marker--hover { pointer-events: none; }

.chart__tooltip {
  position: absolute;
  top: .35rem;
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: .05rem;
  padding: .3rem .5rem;
  font-size: .81rem;
  line-height: 1.3;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 5px;
  border-color: var(--edge);
  pointer-events: none;
  white-space: nowrap;
}

.chart__tooltip strong { font-variant-numeric: tabular-nums; font-size: .9rem; }
.chart__tooltip span { color: var(--muted); font-size: .76rem; }

/* One row per series, each keyed to its band by colour — the reason for
   stacking in the first place. Follows the cursor, so it is positioned from
   JS and only its appearance lives here. */
.chart-tip {
  position: absolute;
  z-index: 3;
  min-width: 13rem;
  max-width: min(26rem, 90%);
  padding: .45rem .55rem;
  font-size: .81rem;
  line-height: 1.45;
  background: var(--surface);
  border: 1px solid var(--edge);
  border-radius: 6px;
  box-shadow: 0 2px 10px rgb(0 0 0 / 12%);
  pointer-events: none;
  white-space: nowrap;
}

.chart-tip[hidden] { display: none; }

.chart-tip__heading {
  margin-bottom: .3rem;
  padding-bottom: .3rem;
  font-weight: 600;
  border-bottom: 1px solid var(--line);
}

/* The value column is its own track so figures line up under each other
   however long the labels are. */
.chart-tip__row {
  display: grid;
  grid-template-columns: 10px 1fr auto;
  align-items: center;
  gap: .45rem;
}

.chart-tip__row--total {
  margin-top: .3rem;
  padding-top: .3rem;
  font-weight: 600;
  border-top: 1px solid var(--line);
}

.chart-tip__swatch { border-radius: 2px; height: 10px; width: 10px; }

/* Net is the dashed line, so its key is dashed rather than a fourth hue that
   would read as a fourth series. */
.chart-tip__swatch--dashed {
  background: repeating-linear-gradient(90deg, var(--muted) 0 3px, transparent 3px 5px);
}

/* The flow tooltips are pinned to the top of the figure and slide along it,
   rather than following the cursor as the performance ones do — the crosshair
   is already tracking the pointer, and a panel doing the same would jitter
   against it. */
.chart-tip--flow { top: .35rem; }

/* A year's whole band is hoverable, so the gap at zero and the space above a
   short bar are not dead. */
.bar__hit { fill: transparent; }
.chart-tip__label { overflow: hidden; text-overflow: ellipsis; }
.chart-tip__value { font-variant-numeric: tabular-nums; }

/* --- notice --- */

.notice {
  background: color-mix(in srgb, var(--accent) 8%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--line));
  border-radius: 8px;
  padding: .7rem .9rem;
  margin: 0 0 1.25rem;
  font-size: .85rem;
  line-height: 1.5;
  color: var(--text);
}

/* --- inline categorization --- */

.flash {
  background: color-mix(in srgb, var(--in) 14%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--in) 35%, var(--line));
  border-radius: 6px;
  padding: .5rem .8rem;
  margin: 0 0 1rem;
  font-size: .85rem;
}

/* A refusal or a warning, in the outflow red the tables already use. */
.flash--alert {
  background: color-mix(in srgb, var(--flow-out) 12%, var(--surface));
  border-color: color-mix(in srgb, var(--flow-out) 35%, var(--line));
}

.categorize__form { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }

.categorize__select {
  font-size: .85rem;
  padding: .15rem .3rem;
  max-width: 15rem;
  color: var(--text);
  background: var(--bg);
  border: 1px solid transparent;
  border-radius: 4px;
}

/* Recessive until you go looking for it — this column is read far more often
   than it is edited. */
.transactions tr:hover .categorize__select,
.categorize__select:focus { border-color: var(--line); }
.categorize__select:invalid, .categorize__select option[value=""] { color: var(--muted); }

.categorize__bulk {
  display: flex;
  align-items: center;
  gap: .2rem;
  font-size: .76rem;
  color: var(--muted);
  white-space: nowrap;
  cursor: pointer;
}

.categorize__bulk:hover { color: var(--text); }

/* --- reconciliation --- */

.reconcile-period { margin-bottom: 1.75rem; }
.reconcile-period h2 { font-size: .95rem; margin: 0 0 .4rem; font-variant-numeric: tabular-nums; }
.reconcile-period__meta { color: var(--muted); font-size: .81rem; font-weight: 400; margin-left: .5rem; }

/* The two checks answer different questions, so they are labelled on every
   row rather than only in the prose above the table. */
.check-kind {
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--muted);
  font-size: .7rem;
  letter-spacing: .03em;
  padding: .05rem .45rem;
  text-transform: uppercase;
}

.check-kind--position { border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); }

table.reconcile .numeric { font-variant-numeric: tabular-nums; }
tr.is-break { background: color-mix(in srgb, var(--out) 8%, transparent); }
tr.is-break td { font-weight: 600; }

.filters__check { flex-direction: row; align-items: center; gap: .4rem; }
.in { color: var(--in); }
.out { color: var(--out); }

/* --- performance ---
   The validated eight-slot categorical palette, assigned in fixed order so a
   holding keeps its colour across every year, every chart and the table.
   Both modes pass the six checks against their own surface; the dark steps are
   chosen for the dark band rather than flipped. Three light steps sit below
   3:1 contrast, which the legend and the table below satisfy as relief. */

.chart {
  --cat-1: #2a78d6; --cat-2: #eb6834; --cat-3: #1baf7a; --cat-4: #eda100;
  --cat-5: #e87ba4; --cat-6: #008300; --cat-7: #4a3aa7; --cat-8: #e34948;
  --cat-9: #6b6b66;

  /* The net-worth bands. Not part of the categorical ramp and deliberately so:
     cash and home equity are a different kind of thing from an investment
     account, and the ramp had run out anyway — nine accounts and two bands is
     eleven series against nine hues, so home equity was drawn with a custom
     property that did not exist and came out black.

     Muted against the vivid nine, so the accounts stay the subject and these
     read as the ground the total stands on. */
  --band-cash: #7d8a94;
  --band-equity: #a1785a;
}

@media (prefers-color-scheme: dark) {
  .chart {
    --cat-1: #3987e5; --cat-2: #d95926; --cat-3: #199e70; --cat-4: #c98500;
    --cat-5: #d55181; --cat-6: #008300; --cat-7: #9085e9; --cat-8: #e66767;
    --cat-9: #9a9a94;
    --band-cash: #93a1ac;
    --band-equity: #bd9270;
  }
}

table.performance .numeric { font-variant-numeric: tabular-nums; }
table.performance td.total { font-weight: 600; }

/* Transfers: diverging bars and cumulative lines.
   ------------------------------------------------------------------ */

.chart--flow .chart__zero {
  stroke: var(--text);
  stroke-width: 1.5;
  stroke-dasharray: none;
  opacity: .55;
}

.chart--flow .bar__seg:hover { fill-opacity: .75; }

.chart__marker--in { fill: var(--flow-in); }
.chart__marker--out { fill: var(--flow-out); }
.chart__marker--net { fill: var(--flow-net); }

/* A transfer description is a line off a statement and can be long, so this
   one wraps where the balance chart's single figure never needs to. */
.chart__tooltip--flow {
  max-width: 22rem;
  white-space: normal;
}

/* A short dashed rule rather than a filled block: the net line is dashed, and
   the legend should look like the thing it names. */
.legend__swatch--dashed {
  background: repeating-linear-gradient(90deg,
    var(--flow-net) 0 4px, transparent 4px 7px);
  border-radius: 0;
  height: 2px;
  width: 16px;
}

.line {
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.line--in { stroke: var(--flow-in); }
.line--out { stroke: var(--flow-out); }

/* Dashed, because a third line that a red-green viewer reads as a duplicate
   of one of the other two is worse than no third line. */
.line--net { stroke: var(--flow-net); stroke-dasharray: 5 4; }

.chart__label { font-size: 11px; font-weight: 600; }
.chart__label--in { fill: var(--flow-in); }
.chart__label--out { fill: var(--flow-out); }
.chart__label--net { fill: var(--flow-net); }

/* Expandable year rows.
   ------------------------------------------------------------------ */

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

.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}

.section-head__actions { display: flex; gap: .5rem; }

/* The year cell is the control, so the whole thing is the hit target rather
   than a caret small enough to miss. */
.disclosure {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  color: inherit;
  cursor: pointer;
}

.disclosure:hover { color: var(--accent); }

.disclosure:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}

.disclosure__caret {
  border-left: 5px solid currentColor;
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  transition: transform .12s ease;
}

.disclosure[aria-expanded="true"] .disclosure__caret { transform: rotate(90deg); }

.detail-row > td {
  padding: 0 0 .6rem 1.6rem;
  background: color-mix(in srgb, var(--text) 3%, var(--surface));
}

table.detail {
  width: 100%;
  border-collapse: collapse;
  font-size: .85rem;
}

table.detail th {
  font-size: .7rem;
  text-transform: uppercase;
  letter-spacing: .03em;
  color: var(--muted);
  font-weight: 600;
  text-align: left;
  padding: .35rem .5rem;
}

table.detail th.numeric,
table.detail td.numeric { text-align: right; }

table.detail td {
  padding: .3rem .5rem;
  border-top: 1px solid var(--line);
}

.nowrap { white-space: nowrap; }

/* Statement column on the transactions list.
   ------------------------------------------------------------------ */

.transactions td.statement { font-size: .8rem; white-space: nowrap; }
.transactions td.statement a { color: var(--muted); text-decoration: none; border-bottom: 1px dotted var(--line); }
.transactions td.statement a:hover { color: var(--accent); border-bottom-color: currentColor; }
.transactions td.statement a.is-break { color: var(--out); }
.transactions td.statement .unchecked { color: var(--line); cursor: help; }

/* Console (development only, bin/console-server on 3006).
   ------------------------------------------------------------------ */

.console-households { display: flex; gap: .75rem; align-items: baseline; margin-bottom: 1.25rem; font-size: .875rem; }
.console-households a { color: var(--accent); text-decoration: none; }
.console-households a:hover { text-decoration: underline; }
.console-households__current { font-weight: 600; }

.console-tables { margin-top: 1.5rem; }
.console-tables th[scope="row"] { font-weight: 600; text-align: left; white-space: nowrap; }

/* An empty table is the useful signal here — it means an importer has not run
   for this household — so it recedes rather than reading as a real count. */
.console-tables__empty { color: var(--line); }

.console-tables__columns {
  color: var(--muted);
  font-size: .81rem;
  line-height: 1.45;
  max-width: 52ch;
}

.console-tables code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .85em; }

/* Console row browser: raw table contents.
   ------------------------------------------------------------------ */

/* A table with 20 columns of raw values will not fit, and must not be allowed
   to widen the page — it scrolls inside its own box instead. */
.console-scroll { overflow-x: auto; margin-top: 1.25rem; }

.console-rows { font-size: .8rem; min-width: 100%; }
.console-rows th, .console-rows td { white-space: nowrap; max-width: 34ch; overflow: hidden; text-overflow: ellipsis; }
.console-rows td { font-variant-numeric: tabular-nums; }
.console-rows thead th a { color: inherit; text-decoration: none; }
.console-rows thead th a:hover { color: var(--accent); }
.console-rows thead th a.is-sorted { color: var(--text); }
.console-rows__arrow { margin-left: .25rem; font-size: .7em; }

/* Absent, not zero or empty — the distinction matters when reading an import. */
.console-rows__null { color: var(--line); font-style: italic; }

/* Balance history: statement anchors along the axis.
   ------------------------------------------------------------------ */

.chart__anchor { stroke: var(--muted); stroke-width: 1; opacity: .5; }
.chart__anchor--break { stroke: var(--out); stroke-width: 2; opacity: 1; }

/* --- reconciliation -------------------------------------------------------
   No new colours. --in and --out already carry direction, --out already marks
   a break, --accent already marks focus. Reusing them means nothing here needs
   validating against either surface again; what changes is structure, which is
   what was actually hard to read.

   Rows get a band and a rule, columns get a rule where the arithmetic changes
   hands: opening | change | closing is one claim, and expected | verdict is
   the test of it. */

table.reconcile { border-collapse: separate; border-spacing: 0; width: 100%; }

table.reconcile thead th {
  background: var(--surface);
  border-bottom: 2px solid var(--line);
  color: var(--muted);
  font-size: .7rem;
  font-weight: 600;
  letter-spacing: .06em;
  padding: .5rem .7rem;
  position: sticky;
  text-align: left;
  text-transform: uppercase;
  top: 0;
  z-index: 1;
}

table.reconcile th.numeric, table.reconcile td.numeric { text-align: right; }
table.reconcile .numeric { font-variant-numeric: tabular-nums; }

table.reconcile tbody td {
  border-bottom: 1px solid var(--line);
  padding: .45rem .7rem;
  vertical-align: top;
}

/* Banding, not borders, for rows — a rule between every row competes with the
   column rules and the table stops having a grain. */
table.reconcile tbody tr:nth-child(even) {
  background: color-mix(in srgb, var(--text) 3%, transparent);
}
table.reconcile tbody tr:hover {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}

/* Where one claim ends and the next begins. */
table.reconcile .col-edge { border-left: 1px solid var(--line); }

table.reconcile tfoot td {
  border-top: 2px solid var(--line);
  font-weight: 600;
  padding: .55rem .7rem;
}

/* Direction, on the one column that has one. */
.delta--in { color: var(--in); }
.delta--out { color: var(--out); }
.delta--none { color: var(--muted); }

/* A break is marked by weight and a tinted band, never by colour alone. */
table.reconcile tr.is-break { background: color-mix(in srgb, var(--out) 10%, transparent); }
table.reconcile tr.is-break td { font-weight: 600; }
table.reconcile tr.is-break:hover { background: color-mix(in srgb, var(--out) 16%, transparent); }

.verdict { font-size: .81rem; white-space: nowrap; }
.verdict--ok { color: var(--in); }
.verdict--break { color: var(--out); font-weight: 600; }
.verdict--none { color: var(--muted); }

/* The security column carries a ticker and a name; the ticker is the key and
   should read as one. */
table.reconcile .symbol { font-weight: 600; letter-spacing: .01em; }
table.reconcile .security { color: var(--muted); font-size: .84rem; }

/* A detail row is one cell spanning the table, so it needs its own inset to
   read as nested rather than as another row of the same table. */
table.reconcile .detail-row > td { background: var(--bg); padding: .6rem .9rem 1rem; }
table.reconcile table.detail { margin-top: .5rem; width: 100%; }
table.reconcile table.detail thead th { border-bottom: 1px solid var(--line); }
table.reconcile--expandable > tbody > tr:not(.detail-row):nth-of-type(4n+3) {
  background: color-mix(in srgb, var(--text) 3%, transparent);
}
/* The banding above counts detail rows too, so the plain nth-child rule stripes
   in pairs. Detail rows carry their own background and are excluded. */
table.reconcile--expandable > tbody > tr:nth-child(even):not(.detail-row) { background: none; }

/* --- account picker -------------------------------------------------------
   A row of links was hard to scan and harder to hit. Each account is now a
   rectangle with its own target, and accounts of a kind stack together — an
   IRA, another IRA and a 403(b) read as one group whoever has custody of them,
   which a flat alphabetical row could not show. */

.account-picker {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
  margin-bottom: 1.25rem;
}

/* The stack. Groups sit side by side; accounts within one sit above each
   other, so the grouping is legible without a heading naming it. */
.account-picker__group {
  display: flex;
  flex-direction: column;
  gap: .3rem;
}


.account-pill__name { display: block; font-size: .92rem; }
.account-pill__meta { display: block; color: var(--muted); font-size: .77rem; }

/* --- account picker outlines ----------------------------------------------
   Scoped to the picker: --line is faint on purpose because it also draws the
   rules inside a table, where a strong stroke fights the banding and the table
   stops having a grain. A control you click is a different job.

   --edge clears 3:1 against both surfaces, which is the threshold for a
   boundary that carries meaning rather than decorating one. Flat: a border
   marks an edge without pretending the element floats above a surface it is
   not above. */

.account-pill {
  background: var(--surface);
  border: 2px solid var(--edge);
  border-radius: 6px;
  color: var(--text);
  display: block;
  min-width: 11rem;
  padding: .4rem .7rem;
  text-decoration: none;
}

.account-pill:hover {
  border-color: color-mix(in srgb, var(--accent) 60%, var(--edge));
  background: color-mix(in srgb, var(--accent) 6%, var(--surface));
}

/* Selection is carried by the rule, the fill and the weight together, so it
   survives being read without colour. */
.account-pill.is-current {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, var(--surface));
}
.account-pill.is-current .account-pill__name { font-weight: 700; }

.account-pill:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* A nav link whose screen still queries the old schema. Marked rather than
   hidden — these are the remaining work, and dropping them would make the
   migration look further along than it is.

   Colour alone does not say why, so the link carries a title and
   screen-reader text; the dotted underline gives it a second signal for
   anyone who cannot see the red. */
.site-nav a.is-unported {
  color: var(--flow-out);
  text-decoration: underline dotted;
  text-underline-offset: 3px;
}
.site-nav a.is-unported:hover { color: var(--flow-out); opacity: .8; }

/* --- who is signed in --- */


/* Named rather than implied by a greyed-out control: a page that silently
   ignores clicks is worse than one that says why. */
.whoami__role {
  border: 1px solid var(--edge);
  border-radius: 3px;
  color: var(--muted);
  font-size: .68rem;
  letter-spacing: .02em;
  padding: .1rem .3rem;
  text-transform: uppercase;
}


/* --- sign in / register --- */

.signin { margin: 3rem auto; max-width: 22rem; }
/* The display face, like every other page title. This one set a size and no
   family, so the sign-in screen — the first thing anybody sees — was the only
   heading in the app drawn in the body font. */
.signin h1 {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 600;
  margin-bottom: 1rem;
}
.signin__form { display: flex; flex-direction: column; gap: .8rem; }
.signin__form label { display: flex; flex-direction: column; font-size: .85rem; gap: .25rem; }
.signin__form input[type="submit"] { cursor: pointer; margin-top: .4rem; padding: .45rem; }
.signin__errors { color: var(--series-alert-text); font-size: .85rem; margin: 0 0 .6rem; padding-left: 1.1rem; }
.signin .optional { color: var(--muted); font-weight: normal; }

/* --- annual returns (bars) and the growth lines (its own figure below) --- */

.chart--returns svg,
.chart--growth svg { height: auto; width: 100%; }
.chart--returns .chart__zero,
.chart--growth .chart__zero { stroke: var(--text); stroke-dasharray: none; opacity: .45; }
.chart__axis--right { fill: var(--muted); }

/* The growth chart sits directly under the bars and shares its year slots, so
   it is pulled up to read as the second half of one figure rather than as an
   unrelated chart that happens to follow. */
.chart--growth { margin-top: .25rem; }

/* Up and down carry position as well as hue — above or below the zero line —
   so the pair never rests on colour alone. */
.bar__seg--up { fill: var(--cat-1); }
.bar__seg--down { fill: var(--cat-8); }

/* Three lines, and they must be told apart without colour. The measured one is
   heaviest and solid; the two constructed ones are lighter, and the benchmark
   dashed as well as differently coloured.

   The measured line is warm where the other two are cool, rather than a third
   green: --cat-3 sat next to the benchmark's --cat-6 and the two greens were
   telling apart only by their dashes. */
/* Hover dots, one per line, keyed to the line they sit on. */
.chart__marker--actual { fill: var(--cat-2); }
.chart__marker--growth { fill: var(--cat-1); }
.chart__marker--index { fill: var(--cat-6); }

.line--actual { fill: none; stroke: var(--cat-2); stroke-width: 3; }
.line--account { fill: none; stroke: var(--cat-1); stroke-width: 2.5; }
.line--benchmark { fill: none; stroke: var(--cat-6); stroke-width: 2; stroke-dasharray: 5 3; }

.legend__swatch--line { border-radius: 0; height: 3px; }

/* Paired bars: these accounts against the index, side by side each year. */
.bar__seg--ours { fill: var(--cat-1); }
.bar__seg--index { fill: var(--cat-6); }
.legend__swatch--dashed-line {
  background: repeating-linear-gradient(90deg, var(--cat-6) 0 5px, transparent 5px 8px);
}

/* --- charts narrower than they were drawn --- */

/* A chart scaled to fit a phone is a chart nobody can read: every geometry here
   draws at 960 units, so at 340px the 10px axis text renders at 3.5px and the
   bars become slivers. Below 960 the chart keeps its drawn size and scrolls
   inside its own box instead — one horizontal scroll, over the chart, on a page
   that has none of its own.
   
   Bounded at 960 rather than at a breakpoint width because that is the real
   question: not "is this a phone" but "is this narrower than the chart". Above
   it the chart fits and scales up, and the figure keeps `overflow: visible` so
   a tooltip near an edge is not clipped. */
@media (max-width: 960px) {
  .chart { overflow-x: auto; }
  .chart svg { min-width: 960px; }
}

/* --- the investment chart, drawn for the screen it is on --- */

/* The exception to the rule above, and the only chart that has one. It is the
   page's first figure and the reason to open the app on a weekday, so needing a
   sideways drag to see the shape of it is worse here than anywhere else.

   It is drawn twice — see shared/_value_chart — and the copy that fits is
   shown. The narrow one carries no `min-width`, so it scales down the little it
   has to rather than overflowing: 380 units into ~340px is 0.9x, where 960 into
   the same space is 0.36x and turns the axis into grey dust. */
.chart--narrow { display: none; }

@media (max-width: 620px) {
  .chart--wide { display: none; }
  .chart--narrow { display: block; overflow-x: visible; }
  .chart--narrow svg { min-width: 0; }
}

/* --- collapsible table panels --- */

/* A table folded away until asked for. See shared/_table_panel. */
.panel { border-top: 1px solid var(--line); margin-top: 1rem; }

.panel__summary {
  align-items: center;
  cursor: pointer;
  display: flex;
  gap: .5rem;
  list-style: none;
  padding: .7rem .1rem;
  user-select: none;
}
/* Safari draws its own triangle through ::-webkit-details-marker. */
.panel__summary::-webkit-details-marker { display: none; }
.panel__summary:hover .panel__title { color: var(--text); }
.panel__summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The disclosure arrow, drawn rather than typed so it can rotate. */
.panel__marker {
  border-right: 2px solid var(--muted);
  border-bottom: 2px solid var(--muted);
  height: .42rem;
  width: .42rem;
  transform: rotate(-45deg);
  transition: transform .15s ease;
  flex: none;
  margin-left: .15rem;
}
.panel[open] .panel__marker { transform: rotate(45deg); }

.panel__title { color: var(--muted); font-size: .85rem; font-weight: 600; }
.panel[open] .panel__title { color: var(--text); }
.panel__count { color: var(--muted); font-size: .81rem; font-variant-numeric: tabular-nums; }

.panel__body { padding-bottom: .5rem; }

/* Wide content scrolls inside its own box rather than widening the page.
   Basis prints twelve columns; no phone fits them, and scrolling keeps every
   figure reachable where dropping columns would not. */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.table-scroll > table { margin-top: 0; }

/* --- narrow screens --- */

@media (max-width: 720px) {
  /* An SVG with a 960-unit viewBox and width:100% is 106px tall on a phone:
     the bars become slivers and the axis is unreadable. Holding a minimum
     width keeps the proportions the chart was drawn at and lets the reader
     swipe, which is the same bargain the tables make. */
  /* Tighter padding buys back nearly a centimetre of usable width. */
  body { padding: 1rem .75rem 3rem; }

  /* The nav is nine screens plus a household switch; on one line it runs off
     the side and the last links cannot be reached at all. */
  .site-nav { flex-wrap: wrap; gap: .75rem 1rem; }  
  /* Columns that repeat what the row already says, or that only matter when
     comparing across rows. Marked in the view rather than by position, so
     inserting a column cannot silently hide a different one. */
  .col-secondary { display: none; }

  table.transactions { font-size: .85rem; }
  .page-header h1 { font-size: 1.5rem; }

  /* `min-width: 11rem` keeps the filter controls comfortable side by side on a
     desktop and pushes the widest of them — the category select, whose options
     run to "Investment Income > Capital Gain Distributions" — straight out of
     the card on a phone. Stacked, they should simply take the width there is. */
  .filters label { width: 100%; }
  .filters input,
  .filters select { min-width: 0; width: 100%; }
  .filters__actions { width: 100%; }
}

/* The crosshair charts carry a tooltip, a rule and a dot per line in the markup
   from the start, and `connect()` hiding them is all that keeps them off the
   page — so when the JavaScript does not run, an empty box and three stray dots
   render over the chart. Hidden in CSS as well, which `line-hover` can afford
   because it sets an explicit `display` to show.

   Scoped to that controller deliberately. `chart-tooltip` shows its panel with
   `hidden = false`, and an author rule here would outrank the `hidden`
   attribute and keep it invisible for good; `balance-chart` shows with
   `display = ""`, which would fall back to whatever this said. Both would break
   silently, and only on hover. */
.chart[data-controller~="line-hover"] .chart-tip,
.chart[data-controller~="line-hover"] .chart__marker--hover,
.chart[data-controller~="line-hover"] .chart__crosshair,
.chart[data-controller~="balance-chart"] .chart__tooltip,
.chart[data-controller~="balance-chart"] .chart__marker--hover,
.chart[data-controller~="balance-chart"] .chart__crosshair { display: none; }


/* What a holding is worth now, which is the figure the page is opened for.
   Last column and heavier than the rest: the eye lands on the right-hand edge
   of a table of numbers, and this was in the middle competing with basis. */
/* The day's move is context for the value beside it, not a rival to it: a step
   down in size, and no weight of its own beyond the colour the sign carries.

   Allowed to wrap, which it was not. Held on one line the cell demanded about
   nine characters of width it did not always have, and a fund name three lines
   deep sat beside a figure that would not give an inch — so the table went over
   the side and the value column, which is what the page is opened for, had to
   be scrolled to. The only break in the cell is the space between the dollars
   and the percentage, so what a squeeze buys is those two stacked; neither
   figure can break inside itself.

   `white-space: normal` says it out loud because `.numeric` — which every
   figure column carries, and which is right for the five that hold one number
   — has already said nowrap. This is the one numeric cell holding two. */
table.positions td.day { font-size: .9rem; white-space: normal; }
table.positions td.day .amount, table.positions td.day .day__percent { white-space: nowrap; }

/* The percentage is the second reading of one figure, so it is quieter than the
   dollars it follows and takes no colour of its own — two coloured numbers in
   one cell compete, and the sign is already on both. */
.day__percent { color: var(--muted); font-size: .8rem; }

table.positions td.value,
table.positions th.value { font-weight: 600; }
table.positions td.value { color: var(--text); font-size: .95rem; }
table.positions thead th.value { color: var(--text); }

/* --- window selectors, under the chart they change --- */

.windows { display: flex; flex-wrap: wrap; gap: 1.25rem; margin: -.75rem 0 1.5rem; }
.windows__group { align-items: center; display: inline-flex; gap: .35rem; }
.windows__label {
  color: var(--muted);
  font-size: .72rem;
  letter-spacing: .06em;
  text-transform: uppercase;
}
.windows__option {
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--muted);
  font-size: .8rem;
  padding: .12rem .6rem;
  text-decoration: none;
}
.windows__option:hover { border-color: var(--edge); color: var(--text); }
.windows__option.is-current {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-color: var(--accent);
  color: var(--text);
  font-weight: 600;
}

/* The custom window's date fields, sized to sit in the same row as the pills
   rather than at the filters' form scale. The apply button is a pill like the
   named windows, because pressing it makes "custom" the current window. */
.windows__date {
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 5px;
  color: var(--text);
  font-size: .8rem;
  padding: .1rem .35rem;
}
.windows__option--apply { background: none; cursor: pointer; }

/* --- the rate ladders on Taxes ---
   Three ladders drawn upwards and stood next to each other, because they are
   read against each other: what the same dollar costs federally on wages, on a
   gain, and in Ohio. Stacked horizontally they were a screen apart.

   One hue throughout, as a sequential scale should be, on one ramp set by the
   partial: every band carries its own rate's shade, 18% to 38% of --accent over
   the card. The rate is printed on every band besides, so nothing rests on the
   shade alone. Where the income actually lands is not drawn yet — see the
   partial for why the first attempt at it was withdrawn.

   The 0% federal band used to be drawn in green as a special case. It is now on
   the same ramp as the rest: a second hue on a one-hue scale reads as a
   category rather than as a step, and the band is already the palest one and
   labelled 0%. */

/* Sized to the bars rather than to the page. Three ladders stretched across a
   desktop width are three very wide columns with nothing in them. */
.ladders {
  /* Colour carries tax character, the same way in every column: what is taxed
     as ordinary income is blue wherever it appears, what is taxed at the
     capital-gains rate is violet, a deduction is orange. So the filed income
     stack reads against the two federal ladders by hue alone — the blue part of
     the stack is the part the blue ladder taxes.

     Ohio keeps the app's accent, because it is the one column where the
     distinction does not exist: it taxes a gain exactly as it taxes a paycheck,
     and painting half of it blue and half violet would say otherwise.

     Slots 1, 7 and 2 of the validated categorical palette (see `--cat-1`),
     taken as they are rather than mixed fresh, so they keep the separation that
     set was checked for. */
  --tax-ordinary: #2a78d6;
  --tax-gains: #4a3aa7;
  --tax-deduction: #eb6834;
  --tax-flat: var(--accent);

  align-items: start;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  display: grid;
  gap: 1.25rem;
  grid-template-columns: repeat(3, minmax(0, 11rem));
  justify-content: start;
  margin: 0 0 1rem;
  padding: 1rem 1.25rem 1.25rem;
  /* The hover panel is placed against this box. */
  position: relative;

  /* As wide as the columns need and no wider, capped at the page. Past that cap
     the card scrolls sideways on its own — one scroll, inside the box, which is
     what the rest of this app does with anything too wide for a phone.
     The alternative was letting six columns share 390px, which gave each of
     them forty pixels: the tick labels took all of it and the bars came out as
     slivers a few pixels across. A chart that fits and cannot be read is worse
     than one you have to push. */
  max-width: 100%;
  overflow-x: auto;
  width: max-content;
}
@media (prefers-color-scheme: dark) {
  .ladders {
    --tax-ordinary: #3987e5;
    --tax-gains: #9085e9;
    --tax-deduction: #d95926;
  }
}
/* Two more columns when the year has a filed return to stand beside them:
   what never reached a bracket, and the income that climbed out of it. */
.ladders--filed { grid-template-columns: repeat(6, minmax(0, 10rem)); }

/* Narrower columns on a small screen, so fewer of them are off the edge — but
   still a width a bar can be read at, rather than whatever is left over. */
@media (max-width: 640px) {
  .ladders { gap: .6rem; grid-template-columns: repeat(3, 8.5rem); padding: .75rem .8rem 1.1rem; }
  .ladders--filed { grid-template-columns: repeat(6, 8.5rem); }
}

.ladder { margin: 0; min-width: 0; }
/* Centred on the bar, which is now the whole width of the column. It was left
   aligned when a tick column stood to the left of every bar: the title then sat
   over the figure, and the bar it named started an inch to the right of it. */
.ladder__title {
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.25;
  margin-bottom: .5rem;
  min-height: 2.5em;
  text-align: center;
}
@media (max-width: 560px) { .ladder__title { font-size: .82rem; } }

/* One track. The dollar thresholds used to have a column to themselves outside
   the bands, because a muted grey label fails contrast in dark mode on a filled
   band and a short band clipped it outright. Both are still true and both are
   answered rather than avoided: the figures take --text, which is the colour
   the rate label on the same band already proves legible in both themes, and
   the 2.4rem floor below is what guarantees the room to put them.

   `minmax(2.4rem, Nfr)` is what keeps the small brackets legible. The federal
   10% band is 2% of the ladder by width; at any honest scale that is nine
   pixels and no room for a label. The floor costs the tall brackets a little
   height and buys every band its own. */
.ladder__bar {
  align-items: stretch;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  height: 21rem;
  margin-bottom: .5rem;
  row-gap: 2px;
}
@media (max-width: 560px) { .ladder__bar { height: 19rem; } }

/* A band. Separated from its neighbours by the card showing through the row
   gap rather than by a border drawn around it. */
.ladder__step {
  background: color-mix(in srgb, var(--band, var(--accent)) var(--pale), var(--surface));
  border-radius: 3px;
  grid-column: 1;
  min-height: 0;
  overflow: hidden;
  position: relative;
}
/* The top band where the ladder was cut rather than finished. A rounded top
   would say the bracket ends there, and it does not — what is above is named
   under the bar. */
.ladder__step.is-cut { border-radius: 0 0 3px 3px; }

/* A dollar figure inside a segment is longer than a rate, and this is a column
   of them, so it is set a size down and tabular to read as a column. */
.ladder__rate--amount { font-size: .66rem; }

.ladder__rate {
  color: var(--text);
  font-size: .72rem;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  left: 0;
  position: absolute;
  right: 0;
  text-align: center;
  top: 50%;
  transform: translateY(-50%);
}

/* The dollar the band tops out at, set inside it and just under that break.
   --text rather than --muted because this now sits on a filled band — muted
   grey is what failed there, and the rate label beside it has been carrying
   --text on the same background all along.

   Set a size down and slightly lifted off full strength, so the rate stays the
   thing the eye lands on and the dollar figure is what it checks against. */
.ladder__edge {
  color: var(--text);
  font-size: .6rem;
  font-variant-numeric: tabular-nums;
  left: 0;
  line-height: 1;
  opacity: .78;
  position: absolute;
  right: 0;
  text-align: center;
  top: .18rem;
  white-space: nowrap;
}
@media (max-width: 560px) { .ladder__edge { font-size: .54rem; } }

/* A band carrying a figure under its ceiling has 2.4rem to fit both in, so the
   rate gives up a little of the middle rather than the two meeting in it. */
.ladder__step:has(.ladder__edge) .ladder__rate { top: 57%; }

/* The filed columns keep their labels off the bars: those mark a stack's top
   and the zero line, not a band's own floor, and there is already a dollar
   figure inside every segment they would land on. Centred over the bar now
   that the bar has the whole column. */
.ladder__tick {
  align-self: end;
  color: var(--muted);
  grid-column: 1;
  font-size: .66rem;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  text-align: center;
  transform: translateY(50%);
  white-space: nowrap;
  z-index: 2;
}
@media (max-width: 560px) { .ladder__tick { font-size: .58rem; } }

.ladder__cap {
  color: var(--muted);
  font-size: .7rem;
  margin: 0;
}

/* --- the filed return, as a fourth column ---
   Income stacks up from the baseline on the same scale as the ladders beside
   it; the deductions hang below that line, because a deduction is not income of
   the opposite sign — it is the part of the stack that never reaches a bracket.

   Its own family of hues, deliberately not the ladders' green: this column is a
   different kind of claim. The three beside it are the rate tables the ledger's
   estimate is measured against; this one is a document somebody signed, and
   painting it in the same hue would invite reading it as a fourth ladder.

   The pair is the chart palette's slot 1 and slot 2 — blue for the income
   above the line, orange for the deductions below it. Already validated against
   both surfaces at 24.7 / 26.8 CVD separation (see `--series-2`), and warm
   against cool is what makes a diverging pair read as opposite. */

/* Taller than a ladder by exactly the depth it hangs below zero, and it holds
   its own height inline rather than taking the ladders' 21rem. The grid sits at
   the top of the row either way, so zero lands on the ladders' baseline. */
.ladder__bar--offset { height: auto; }

/* Zero, drawn across the income column rather than at a row boundary — the
   segment that spans it is one line of the return, and cutting it in half to
   make a gridline would invent a split the form does not have.

   A grid item in the bars' own column, spanning every row, carrying the rule as
   a gradient stop. Absolutely positioned it ran back under the tick labels,
   because the tick column is part of the same grid. */
   Positioned, and last in the document, so it paints over the segments it
   crosses. Being last is not enough on its own: `.ladder__step` is
   `position: relative`, and a positioned element paints above a static one
   whatever the document order, so the rule was drawn underneath the bar. */
.ladder__zero {
  align-self: end;
  border-top: 1px solid var(--edge);
  grid-column: 1;
  pointer-events: none;
  position: relative;
  z-index: 1;
}

/* Stepped by line within each half, so a stack of five segments separates into
   five rather than reading as one block with gaps in it. The hue comes from
   `--band`, set per segment by what taxes it. */

/* Zero, on the line the deductions hang from. Weightier than the other ticks
   because it is the axis rather than a threshold. */
.ladder__tick--zero {
  align-self: start;
  color: var(--text);
  font-weight: 600;
  transform: translateY(-50%);
}

/* The two figures the columns exist to state: how far the deductions reach
   below the line, and where the income clears it. */
.ladder__tick--floor { align-self: end; transform: translateY(50%); }
.ladder__tick--taxable {
  align-self: end;
  color: var(--text);
  font-weight: 600;
  transform: translateY(50%);
}

/* The prose the bars cannot carry. In one column under the grid rather than
   inside each figure: a note is sixty characters wide and a third of the grid
   is not, so in the figure all three came out as ribbons. */
.ladders__notes {
  color: var(--muted);
  font-size: .85rem;
  margin: 0;
  max-width: 78ch;
}
.ladders__notes dt { font-weight: 600; margin-top: .6rem; }
.ladders__notes dt:first-child { margin-top: 0; }
.ladders__notes dd { margin: 0; }

/* The one figure the screen exists to state. */
.note--headline {
  border-left: 3px solid var(--accent);
  font-size: 1rem;
  margin: 1.2rem 0 1.6rem;
  max-width: 70ch;
  padding-left: .8rem;
}

/* The month in progress. Hatched rather than solid, because its bar is short
   for a reason that has nothing to do with spending. */
.balances-bars__bar--partial {
  background: repeating-linear-gradient(135deg,
    var(--accent) 0 5px, color-mix(in srgb, var(--accent) 45%, var(--surface)) 5px 10px);
}

/* The everything chip. Marked as different in kind from an account rather than
   just another one in the row: it is a question, not a place. */
.account-pill--all {
  border-color: var(--edge);
  border-style: dashed;
}
.account-pill--all.is-current { border-style: solid; }

/* The account a period belongs to, shown only when the rows come from more
   than one. Recessive: the reader came for the verdict, not the account. */
.reconcile .account-cell { font-size: .88rem; }
.reconcile .account-cell a { color: var(--text); text-decoration: none; }
.reconcile .account-cell a:hover { text-decoration: underline; }

/* --- Notes ---
   Two records on one screen: the running log of what changed, and the sisters'
   tally of what each has spent on their parents. */

.stacked-form { display: flex; flex-direction: column; gap: .5rem; }
.stacked-form label { font-size: .8rem; font-weight: 600; }
.stacked-form textarea,
.stacked-form input[type="text"],
.stacked-form input[type="date"],
.stacked-form select {
  background: var(--surface);
  border: 1px solid var(--edge);
  border-radius: 5px;
  color: var(--text);
  padding: .45rem .55rem;
  width: 100%;
}
.stacked-form textarea { resize: vertical; }
.stacked-form__actions { align-items: center; display: flex; gap: .75rem; margin-top: .25rem; }
.stacked-form input[type="submit"] {
  background: var(--accent);
  border: 0;
  border-radius: 5px;
  color: var(--surface);
  cursor: pointer;
  font-weight: 600;
  padding: .45rem 1rem;
}

/* Four fields that belong on one line where there is room and wrap where there
   is not. The description gets twice the room because it is the only one whose
   length is not known in advance. */
.expense-fields {
  display: grid;
  gap: .75rem;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
}
.expense-fields__field { display: flex; flex-direction: column; gap: .35rem; min-width: 0; }
.expense-fields__field--wide { grid-column: span 2; }
@media (max-width: 560px) { .expense-fields__field--wide { grid-column: span 1; } }

/* A link and a button_to's form sit on different baselines by default, so the
   two actions are centred against each other rather than left to the text. */
.update__actions { align-items: center; display: flex; gap: .6rem; }
.update__form { display: flex; margin: 0; }
.update__action {
  background: none;
  border: 0;
  color: var(--accent);
  cursor: pointer;
  font-size: .74rem;
  line-height: 1.3;
  padding: 0;
  text-decoration: none;
}
.update__action:hover { text-decoration: underline; }
.update__action--danger { color: var(--series-alert-text); }

.note-form { margin-bottom: 1rem; }

/* A row being edited in place. Its inputs belong to a form that sits outside
   the table — see the `form` attribute in the view — so nothing here can rely
   on them being nested in one. */
tr.is-editing { background: color-mix(in srgb, var(--accent) 6%, transparent); }
tr.is-editing input[type="text"],
tr.is-editing input[type="date"],
tr.is-editing select,
tr.is-editing textarea {
  background: var(--surface);
  border: 1px solid var(--edge);
  border-radius: 4px;
  color: var(--text);
  font: inherit;
  max-width: 100%;
  padding: .25rem .35rem;
  width: 100%;
}
tr.is-editing textarea { display: block; resize: vertical; }
tr.is-editing input[inputmode="decimal"] { text-align: right; font-variant-numeric: tabular-nums; }

/* Under the amount it qualifies, not beside it: inline, the select was pushed
   out of a numeric column sized for a figure and spilled into the next one. */
tr.is-editing select { display: block; margin-top: .35rem; width: 100%; }

.row-actions { white-space: nowrap; }
.row-actions .update__action { margin-right: .6rem; }
.row-actions input[type="submit"] { font-weight: 600; }

/* The date beside a free-text field. Sized to the control rather than stretched
   across the form, which is what a full-width date input looks like. */
.stacked-form__date { display: flex; flex-direction: column; gap: .35rem; max-width: 11rem; }

/* --- the log tables ---
   The spending log and the financial updates, which are the same table read the
   same way: a date, what it was, and columns saying who. Sized to fit a phone
   without pushing sideways. Edit and Remove are not among the columns — they
   open in a row underneath the one you tap, which is what freed the width. */
.log-table .date { white-space: nowrap; }

/* Amounts against the right edge, so the decimal points fall under one another.
   `.numeric` asks for this already and loses: `.transactions td` sets
   `text-align: left` with a selector one element heavier, which is how both
   columns of this table came to be ranged left and no two figures lined up.
   The green keeps its colour and gives up its weight here — tabular figures
   are only the same width at the same weight, and a bold $1,500.00 pushes its
   own point a pixel or two off the column. */
.log-table th.numeric,
.log-table td.numeric { text-align: right; }
/* The name over the middle of the figures, not the middle of the column. The
   figures stand against the right edge of a column much wider than they are,
   so a name centred in the cell sat well to their left. It is ranged right
   with them instead, in a box about as wide as a usual amount — $225.00 —
   and centred inside that. */
.log-table__payer { display: inline-block; min-width: 4rem; text-align: center; }
.log-table td.amount--in { font-weight: inherit; }


/* A quarter's heading: quieter than a row, and the whole of it opens the
   quarter. The general rule that drops the border under a tbody's last row is
   put back, because here a tbody ends where the next heading begins. */
.log-table__quarter th {
  background: var(--bg);
  color: var(--muted);
  font-size: .8rem;
  font-weight: 600;
}
.log-table tbody tr:last-child td { border-bottom: 1px solid var(--line); }

/* What is owed, directly under the names it belongs to. The weight the foot
   used to carry, and a heavier rule under it so it reads as the answer and
   the quarters below as the working. Written after the two rules it has to
   outweigh: the one above, and the one that
   takes the weight off a body row's heading. */
.log-table tbody.log-table__owed tr th[scope="row"],
.log-table tbody.log-table__owed tr td { border-bottom: 2px solid var(--line); font-weight: 600; }

/* A block, not the flex row a disclosure usually is. There is no caret to sit
   beside here, and as flex items a long description and its tag stood side by
   side, neither able to give way, and pushed the second amount column off a
   phone. As a block the words wrap and the tag follows them. */
.log-table .disclosure--bare { display: block; width: 100%; }

/* Whole or not at all: at 390px the tag broke between its two words and stood
   as a box two lines tall beside a one-line description. */
.log-table .tag { white-space: nowrap; }

/* The row's own actions, wherever they appear — under a tapped row, or beside
   the description while it is being edited. */
.row-actions { display: inline-flex; align-items: center; gap: .75rem; white-space: nowrap; }
tr.is-editing .row-actions { margin-top: .35rem; }

/* Edit and Remove beside the name rather than behind a disclosure. The name
   takes the space and the actions sit hard against the right of the cell, so
   a column of them lines up and reads as one thing to reach for. Recessive
   until the row is under the pointer: they are always there, and always
   shouting would make the table about its own controls. */
.row-heading { display: flex; align-items: baseline; gap: .75rem; justify-content: space-between; }
.row-heading .row-actions--inline { gap: .6rem; opacity: .55; transition: opacity .12s ease; }
tr:hover .row-heading .row-actions--inline,
.row-heading .row-actions--inline:focus-within { opacity: 1; }

/* On a phone the cell padding is most of the table. Trimmed, and the numbers
   given only the room they need, so four columns fit in 390px. */
@media (max-width: 560px) {
  .log-table th,
  .log-table td { padding: .5rem .35rem; }
  .log-table { font-size: .88rem; }
  .log-table .date { font-size: .78rem; }
  .log-table thead th { font-size: .68rem; letter-spacing: .03em; }
  /* On a line of its own under the description. Kept whole beside it, the
     tag took the width the second amount column needed and pushed the table
     sideways. */
  .log-table .tag { display: table; margin: .2rem 0 0; }
  .log-table__payer { min-width: 3.5rem; }
}

/* The whole row opens the actions under it, so it says so on hover and takes a
   pointer. The disclosure button inside is still the accessible control; it is
   simply no longer the only part of the row worth aiming at. */
.log-table tr.is-tappable { cursor: pointer; }
.log-table tr.is-tappable:hover { background: color-mix(in srgb, var(--accent) 6%, transparent); }

/* A disclosure with no caret. The row is the control here, so a mark against
   one word in it would point at the wrong thing; the button remains for the
   keyboard and the screen reader, and reads as the plain text it sits on. */
.disclosure--bare {
  color: inherit;
  font: inherit;
  padding: 0;
  text-align: left;
}

/* The description is the row's heading for a screen reader — `th scope="row"`
   is what ties the amounts to it — but it is not emphasis, and a column of
   bold sentences reads as a column of headings. The total in the foot keeps
   its weight, which is why this is scoped to the body. */
.log-table tbody th[scope="row"],
.log-table tbody th[scope="row"] .disclosure { font-weight: 400; }

/* --- financial updates ---
   The same table with prose where the log has a few words, so the description
   is given the room and the two narrow columns are held to what they need.
   Line breaks somebody typed are kept, without wrapping the text in paragraphs
   — a <button> may hold phrasing content and nothing more. */
.updates-log tbody th[scope="row"] { width: 100%; }
.updates-log__text { white-space: pre-line; }
.updates-log .author,
.updates-log thead th:last-child { white-space: nowrap; }
.updates-log .author { color: var(--muted); font-size: .8rem; }

/* A note can run to several lines, and a date or a name sitting in the middle
   of them reads as belonging to the middle of them. */
.updates-log tbody td,
.updates-log tbody th { vertical-align: top; }

/* Ten years of stacks side by side. The card and the tax hues come from
   `.ladders`; only the column count differs, and the wrapper carries no box of
   its own so there is one card rather than two. */
/* The hover panel is placed against this box, as it is on `.ladders`, so this
   has to be the positioned ancestor — without it the panel resolved against the
   page and landed nowhere near the bar. */
.projection { margin: 0 0 1rem; position: relative; }
.ladders--projection {
  column-gap: .35rem;
  /* Ten pairs: a thin column for the deductions and the stack beside it. */
  grid-template-columns: repeat(10, minmax(0, .34fr) minmax(0, 1fr));
  margin: 0;
  /* `.ladders` is `width: max-content`, which is right for six fixed columns and
     wrong for ten flexible ones — the fr tracks collapsed to their content and
     the card came out a third of the page. */
  width: 100%;
}

/* Comparing two courses: a thin deduction column, then the same year drawn
   twice. The pair is tied together by a tighter gap between the two stacks than
   between one year and the next — without it thirty columns read as thirty
   years rather than ten.

   Fixed widths rather than fractions, and the chart scrolls. Thirty columns
   sharing the page gave each stack about 40px, which is not enough to set
   "$102,629" or "15% · $18,021" without wrapping into its neighbour — so the
   comparison lost every number on it. A bar wide enough to be read is worth
   more than a chart that fits, and this one is read a year at a time. */
.ladders--compare {
  column-gap: .18rem;
  grid-template-columns: repeat(10, 1.6rem 4.7rem 4.7rem);
  overflow-x: auto;
  padding-bottom: .35rem;
}
.ladders--compare > .ladder:nth-child(3n + 1) { margin-left: .5rem; }
/* One bar standing where a year's two stacks stand, so the cumulative chart
   lines up column for column with the ones it is summarising. */
.ladder--wide { grid-column: span 2; }

/* What happens today, against what would happen instead. Faded rather than
   recoloured: a second palette would compete with the rate colours the bands
   already carry, and the question here is which bar is taller. */
.ladder--baseline .ladder__step { opacity: .55; }
.ladder--baseline .ladder__title { color: var(--muted); }

.projection__legend {
  color: var(--muted);
  display: flex;
  font-size: .82rem;
  gap: 1.1rem;
  margin: 0 0 .5rem;
}
.projection__key { align-items: center; display: flex; gap: .35rem; }
/* `currentColor`, not a palette colour: the rate palette is declared on
   `.ladders` and the legend sits outside it, and a single blue chip would
   misdescribe a bar made of four colours anyway. What the two keys have to tell
   apart is faded from solid, which is the one thing this does say. */
.projection__key::before {
  background: currentColor;
  border-radius: .15rem;
  content: "";
  height: .7rem;
  width: .7rem;
}
.projection__key--baseline::before { opacity: .45; }

/* Two ticks a year sit closer together than one ever does, so they are set a
   step smaller to keep a gap between them. */
.ladders--compare .ladder__tick { font-size: .7rem; }

/* Zero is the line every figure on this chart is measured from — the stack
   above it is taxable income and the deductions below it are what never reached
   a bracket. At `--edge` it was invisible, so the ticks had nothing to be read
   against. Ten columns all cut at the same height read as one axis. */
.ladders--projection .ladder__zero { border-top-color: var(--muted); }
@media (max-width: 900px) {
  .ladders--projection:not(.ladders--compare) { grid-template-columns: repeat(10, 1.6rem 3.6rem); overflow-x: auto; }
}

/* A year: deductions in a thin column on the left hanging below zero, the value
   beside it climbing above. Both are placed against this box rather than
   stacked, so the zero line runs straight across the chart. */
.projection-year { position: relative; }
.projection-bar { display: grid; position: absolute; row-gap: 2px; }
.projection-bar--deduction { left: 0; width: 30%; }
.projection-bar--value { right: 0; width: 62%; }
.projection-bar--wide { left: 0; width: 100%; }
.projection-zero {
  border-top: 1px solid var(--edge);
  left: 0; position: absolute; right: 0;
}

/* The two figures each column exists to state: where the stack tops out, and
   how far the deductions reach below it. */
.projection-tick {
  color: var(--text);
  font-size: .6rem;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  line-height: 1;
  position: absolute;
  right: 0;
  white-space: nowrap;
}
.projection-tick--top { transform: translateY(-115%); }
.projection-tick--floor { color: var(--muted); font-weight: 400; left: 0; right: auto; transform: translateY(25%); }

/* The alternatives box: a set of single decisions, checked on and off against
   the base course drawn below it. It sits under the year pills because it is
   the same kind of control — what am I looking at — rather than a filter over
   the data.

   `fieldset` and `legend` rather than a div and a heading: this is a group of
   related controls, and a screen reader should say "Alternative" before each
   one rather than leaving them as four loose checkboxes on a page. */
.alternatives { display: flex; flex-wrap: wrap; gap: .6rem; margin: 0 0 1rem; }
.alternatives fieldset:first-child { flex: 1 1 20rem; }
.alternatives__select { font: inherit; font-size: .92rem; padding: .1rem .2rem; }
.alternatives fieldset {
  border: 1px solid var(--edge);
  border-radius: .5rem;
  display: flex;
  flex-wrap: wrap;
  gap: .35rem 1.1rem;
  padding: .55rem .9rem .7rem;
}
.alternatives legend {
  color: var(--muted);
  font-size: .78rem;
  letter-spacing: .06em;
  padding: 0 .35rem;
  text-transform: uppercase;
}
.alternatives__option {
  align-items: center;
  cursor: pointer;
  display: flex;
  font-size: .92rem;
  gap: .4rem;
}
/* The way to the lots a threshold names. Recessive: it is a way through to
   another screen, not a fourth thing to decide here. */
.alternatives__link { align-self: center; color: var(--muted); font-size: .85rem; }

/* Shown until the controller connects, so the form still works without it. */
.alternatives.is-auto-submitting .alternatives__show { display: none; }

/* --- a line item's own working ------------------------------------------ */

/* The third fold on the Taxes screen: a tile summarises a group, a panel
   itemises it, and this opens one item onto the payslips, months, funds or
   sales it was added up from. Nested disclosures rather than a fourth screen —
   the figures only make sense beside the one they explain. */
.line > .line__summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem;
}

.line > .line__summary::-webkit-details-marker { display: none; }

/* The marker is drawn rather than inherited from the panel above, so the two
   levels of disclosure do not look like the same control. */
.line > .line__summary::before {
  content: "";
  flex: none;
  width: 0.34rem;
  height: 0.34rem;
  border-right: 1.5px solid var(--edge);
  border-bottom: 1.5px solid var(--edge);
  transform: rotate(-45deg);
  transition: transform 120ms ease;
}

.line[open] > .line__summary::before { transform: rotate(45deg); }

.line__name { font-weight: 600; }

.line__parts {
  margin: 0.5rem 0 0.25rem 0.9rem;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 0.15rem;
  max-width: 34rem;
}

.line__part {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  font-weight: 400;
  padding: 0.1rem 0;
  border-bottom: 1px dotted var(--line);
}

.line__part-label { color: var(--muted); }

.line__part-note {
  color: var(--muted);
  font-size: 0.85em;
  margin-left: 0.35rem;
}

.line__part-figure { font-variant-numeric: tabular-nums; white-space: nowrap; }
.line__part-figure.projected { color: var(--muted); }

/* Assumed is not one basis among four. It means nothing here supports the
   figure, and it should not read as quietly as `observed`. */
.tag--assumed {
  border-color: var(--out);
  color: var(--out);
}

/* --- fetch quotes now ---------------------------------------------------- */

/* The refresh control sits at the far end of the window row: same row, because
   it belongs to the same chart, and far end because it is an action rather than
   one more choice of span. */
.windows__group--action { margin-left: auto; }

.windows__option--refresh { white-space: nowrap; }

/* A frame is inline by default, which is wrong for one wrapping whole sections:
   the overview's frame holds a chart, a list of bars and a stack of tables, and
   as an inline box its margins collapse differently from the block it replaced. */
turbo-frame { display: block; }

/* Turbo marks a frame busy while it is loading. This is the only place in the
   app where a reader is deliberately made to wait, so it is the only place that
   has to say it is working. */
turbo-frame[busy] .windows__option--refresh,
turbo-frame[aria-busy="true"] .windows__option--refresh {
  opacity: 0.55;
  pointer-events: none;
}

turbo-frame[busy] .chart,
turbo-frame[aria-busy="true"] .chart { opacity: 0.6; transition: opacity 120ms ease; }

/* --- recurring ------------------------------------------------------------ */

/* The count on the Recurring link: how many recorded price changes and
   expiring promos await a look. A count rather than a dot, because "3" says
   whether it is worth going now and a dot only nags. */
.nav-badge {
  display: inline-block;
  margin-left: .25rem;
  padding: 0 .35rem;
  font-size: .68rem;
  font-variant-numeric: tabular-nums;
  color: var(--surface);
  background: var(--out);
  border-radius: 999px;
  vertical-align: text-top;
}

/* A recorded price change under its item: the figures and the date on their
   own line, tinted with the outflow red the tables already use, so the row
   above stays a row. */
.price-flag td {
  background: color-mix(in srgb, var(--flow-out) 8%, var(--surface));
  font-size: .85rem;
}

.price-flag__text { margin-right: .6rem; color: var(--out); }

.price-flag .update__form { display: inline-block; }

/* --- the statements intake ---
   The page's one action, so it is a button rather than the text link a row
   action uses: pressing it fetches from Drive and rebuilds part of the ledger,
   which is not a thing to do by brushing past a link. The status sits beside it
   rather than under, so the button and what it last did read as one sentence. */
.intake { align-items: baseline; display: flex; flex-wrap: wrap; gap: .75rem; margin-bottom: 1.5rem; }
.intake__form { display: flex; margin: 0; }
.intake__action {
  background: var(--surface);
  border: 1px solid var(--edge);
  border-radius: 4px;
  color: var(--text);
  cursor: pointer;
  font: inherit;
  font-size: .8rem;
  font-weight: 600;
  padding: .4rem .7rem;
}
.intake__action:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
/* Disabled while a run is in flight — the page is the only thing that reports
   the result, so a second press would queue a run nobody would see finish. */
.intake__action:disabled { cursor: default; opacity: .5; }
.intake__status { color: var(--muted); font-size: .85rem; margin: 0; }
/* The panels below the button take the full width, so they break the flex row. */
.intake > .panel { flex-basis: 100%; }
.intake__output {
  font-size: .74rem;
  line-height: 1.45;
  margin: 0;
  overflow-x: auto;
  white-space: pre-wrap;
  word-break: break-word;
}

/* --- a group heading inside a table ---
   Recurring splits the books into fixed and variable prices. A heading row
   rather than two tables, because the edit form and the row-expander wrap the
   table and two of each would be two of both. Styled as a band rather than a
   row so it does not read as an item with missing columns. */
.transactions .group-heading th {
  background: var(--surface);
  border-bottom: 1px solid var(--edge);
  color: var(--text);
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .04em;
  padding: .7rem .5rem .35rem;
  text-align: left;
  text-transform: uppercase;
}
.transactions .group-heading:first-child th { padding-top: .2rem; }
.group-heading__note {
  color: var(--muted);
  font-weight: 400;
  letter-spacing: 0;
  margin-left: .5rem;
  text-transform: none;
}
