/* Nucleus — the Signal design system in production.
 *
 * ADR backend/0051 owns this file. Read it before changing anything here.
 *
 *   - DESIGN.md owns every token VALUE. The :root block below mirrors its
 *     frontmatter and nothing else; `nucleus.web.design-test` asserts that in
 *     the gate, so an edit here that DESIGN.md does not carry fails the build.
 *   - docs/product/frontend-design-constitution.md owns the information
 *     design rules the component styles below realise.
 *   - The only @media print rules here are the ADMIN SHELL's own, added by
 *     BOB-269 because ADR 0051 D1 assigned a shell's print behaviour to the
 *     slice that builds it. They cover the chrome the shell puts around a
 *     page, INCLUDING the masthead band — whose heading they keep, because
 *     it is the page's only <h1>. Making a general page printable beyond
 *     that is still opt-in per artifact page through `views/print-styles`.
 *   - var(--brand) is the per-org identity accent. It is confined to the
 *     allowlist in `nucleus.web.design-tokens`, executed by the same test.
 *     It never carries action, focus, danger, or success (ADR 0051 D4).
 *
 * `header.site` is now the chrome of the FAMILY document and of the
 * audience-neutral refusals only: the admin document left it for the Signal
 * shell below (BOB-269). The family shell is still a recorded deviation from
 * the constitution, owned by BOB-270.
 */

/* ------------------------------------------------------------------ */
/* Typefaces (ADR 0051 D3) — vendored, no third-party runtime request. */

@font-face {
  font-family: "Geist Variable";
  src: url("/fonts/geist-variable.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Geist Mono Variable";
  src: url("/fonts/geist-mono-variable.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* `font-display: swap` shows operating text immediately, which a calm
 * control room wants. These two faces REDUCE what the swap then costs: they
 * dress a metric-compatible local font in Geist's geometry, so the pre-swap
 * paint occupies close to the same space.
 *
 * They do not eliminate reflow, and this comment previously claimed they
 * did. Matching one sample's mean advance scales glyphs uniformly; it does
 * not make every glyph sequence or synthesised weight identical. And a
 * reader with none of the named local faces gets the generic families in
 * the token stacks, which receive no overrides at all.
 *
 * Derivation — measured from the real font tables, not remembered:
 *   size-adjust       = target mean advance (em) / fallback mean advance (em)
 *   ascent-override   = target ascent  / (target upem * size-adjust)
 *   descent-override  = |target descent| / (target upem * size-adjust)
 *   line-gap-override = target lineGap / (target upem * size-adjust)
 *
 * The mean advance is measured over one fixed sample (A-Z a-z 0-9 space)
 * read the same way from both fonts. It is deliberately NOT OS/2
 * xAvgCharWidth: Apple's Arial declares OS/2 v3 but carries a legacy
 * lowercase-weighted value (0.4414em against a real 0.5743em), and using it
 * yields size-adjust 131.62% — a fallback painted 29% too wide, i.e. exactly
 * the reflow these rules exist to prevent.
 *
 * Geist: upem 1000, ascent 1005, descent -295, lineGap 0, mean 0.5879em.
 * Arial: upem 2048, ascent 1854, descent -434, lineGap 67, mean 0.5743em.
 * Geist Mono: mean 0.6000em. Courier New: mean 0.6001em.
 *
 * Only metric-compatible locals are named. Helvetica and Liberation Sans are
 * metric-compatible with Arial; Liberation Mono with Courier New. Anything
 * else falls through to the generic families in the token stacks.
 */

@font-face {
  font-family: "Geist Fallback";
  src: local("Arial"), local("Helvetica"), local("Liberation Sans");
  size-adjust: 102.36%;
  ascent-override: 98.18%;
  descent-override: 28.82%;
  line-gap-override: 0%;
}

@font-face {
  font-family: "Geist Mono Fallback";
  src: local("Courier New"), local("Liberation Mono");
  size-adjust: 99.98%;
  ascent-override: 100.52%;
  descent-override: 29.50%;
  line-gap-override: 0%;
}

/* ------------------------------------------------------------------ */
/* Tokens (ADR 0051 D2) — mirrors DESIGN.md frontmatter EXACTLY.
 *
 * Add nothing else to :root. The drift check fails on any custom property
 * here that DESIGN.md does not carry, and on any token DESIGN.md carries
 * that is missing here. Anything DESIGN.md states only in prose — the two
 * shadows, for instance — would be written as a literal at its point of use
 * with a citation; nothing in this file needs one yet.
 */

:root {
  /* colors.<name> -> --<name> */
  --nucleus-navy: #173c73;
  --signal-blue: #2563eb;
  --cool-paper: #f6f8fb;
  --signal-chrome: #fffdf8;
  --warm-white: #ffffff;
  --operational-ink: #172033;
  --operational-slate: #63738a;
  --structural-line: #dce3eb;
  --active-blue-wash: #e8f0ff;
  --assistance-blue-wash: #edf6ff;
  --reconciliation-red: #b42318;
  --verified-green: #067a55;

  /* typography.<role>.<property> -> --type-<role>-<property> */
  --type-headline-family: Geist Variable, Geist Fallback, ui-sans-serif, system-ui, sans-serif;
  --type-headline-size: 27px;
  --type-headline-weight: 750;
  --type-headline-leading: 1.1;
  --type-headline-tracking: -0.03em;
  --type-title-family: Geist Variable, Geist Fallback, ui-sans-serif, system-ui, sans-serif;
  --type-title-size: 17px;
  --type-title-weight: 700;
  --type-title-leading: 1.15;
  --type-body-family: Geist Variable, Geist Fallback, ui-sans-serif, system-ui, sans-serif;
  --type-body-size: 13px;
  --type-body-weight: 400;
  --type-body-leading: 1.45;
  --type-label-family: Geist Mono Variable, Geist Mono Fallback, ui-monospace, SFMono-Regular, Menlo, monospace;
  --type-label-size: 10px;
  --type-label-weight: 700;
  --type-label-leading: 1.3;
  --type-label-tracking: 0.06em;
  --type-value-family: Geist Mono Variable, Geist Mono Fallback, ui-monospace, SFMono-Regular, Menlo, monospace;
  --type-value-size: 23px;
  --type-value-weight: 730;
  --type-value-leading: 1.1;
  --type-value-tracking: -0.04em;

  /* rounded.<kind> -> --radius-<kind> */
  --radius-control: 7px;
  --radius-surface: 8px;
  --radius-feature: 12px;

  /* spacing.<step> -> --space-<step> */
  --space-xs: 4px;
  --space-sm: 8px;
  --space-md: 12px;
  --space-lg: 16px;
  --space-xl: 24px;
  --space-page: 28px;
}

/* ------------------------------------------------------------------ */
/* Base.
 *
 * The root font-size is deliberately left at the browser default: every
 * `rem` length in this file and in the page widths predates Signal, so
 * rescaling the root would silently resize the whole product. Operating
 * type is set on `body` from the body role instead.
 */

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

/* Visually hidden content remains in the accessibility tree. The focus and
 * active exclusions keep this utility safe for interactive skip controls. */
.sr-only:not(:focus):not(:active) {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

body {
  margin: 0;
  background: var(--cool-paper);
  color: var(--operational-ink);
  font-family: var(--type-body-family);
  font-size: var(--type-body-size);
  font-weight: var(--type-body-weight);
  line-height: var(--type-body-leading);
}

main {
  max-width: 40rem;
  margin: 0 auto;
  padding: var(--space-lg) var(--space-lg) 3rem;
}

p { margin: var(--space-md) 0; }

a { color: var(--signal-blue); }

/* Focus is a semantic role: fixed Signal Blue, never the org accent
 * (constitution — an org that picks pale yellow must not be able to make
 * its own focus indicator invisible). */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--signal-blue);
  outline-offset: 2px;
  border-radius: var(--radius-control);
}

h1 {
  margin: var(--space-lg) 0 var(--space-sm);
  padding-bottom: var(--space-sm);
  border-bottom: 1px solid var(--structural-line);
  color: var(--operational-ink);
  font-family: var(--type-headline-family);
  font-size: var(--type-headline-size);
  font-weight: var(--type-headline-weight);
  line-height: var(--type-headline-leading);
  letter-spacing: var(--type-headline-tracking);
  text-wrap: balance;
}

/* Section and panel headings, wherever they sit. These carried no rule at
 * all before Signal, so every heading outside a card fell back to the
 * browser's own scale — which, against a 13px body, rendered LARGER than
 * the tokens meant it to. */
h2,
h3 {
  margin: var(--space-xl) 0 var(--space-sm);
  color: var(--operational-ink);
  font-family: var(--type-title-family);
  font-size: var(--type-title-size);
  font-weight: var(--type-title-weight);
  line-height: var(--type-title-leading);
}

/* The org display name, and the ONE interim consumer of the org accent
 * (ADR 0051 D4). The accent is a decorative rule, never the text: an
 * arbitrary club colour cannot reduce the contrast of anything readable.
 * Where identity accent belongs in the finished shells is BOB-269/270's
 * question. `--brand` is supplied per request by the #brand-style element,
 * so the fallback covers a page rendered without it. */
.org-name {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin: var(--space-lg) 0 calc(var(--space-md) * -1);
  color: var(--operational-slate);
  font-family: var(--type-label-family);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  line-height: var(--type-label-leading);
  letter-spacing: var(--type-label-tracking);
  text-transform: uppercase;
}

.org-name::before {
  content: "";
  flex: 0 0 auto;
  width: 3px;
  height: 14px;
  border-radius: 2px;
  background: var(--brand, var(--nucleus-navy));
}

/* Secondary and supplementary text. `.muted` explains, `.empty` states an
 * honest absence — neither is an error, so neither takes a semantic colour. */
.muted,
.empty,
.optional,
.hint {
  color: var(--operational-slate);
}

.muted,
.empty { margin: var(--space-md) 0; }

.optional { font-weight: 400; }

.hint {
  margin: 0 0 var(--space-xs);
  font-size: 12px;
}

.preserve-lines { white-space: pre-line; }

.amount {
  font-family: var(--type-value-family);
  font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------------------ */
/* Labels, badges, and trust states. */

label {
  display: block;
  margin: var(--space-lg) 0 var(--space-xs);
  color: var(--operational-ink);
  font-weight: 600;
}

.badge {
  display: inline-block;
  padding: var(--space-xs) var(--space-sm);
  border: 1px solid transparent;
  border-radius: 999px;
  font-family: var(--type-label-family);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  line-height: var(--type-label-leading);
  letter-spacing: var(--type-label-tracking);
  text-transform: uppercase;
  vertical-align: 0.1em;
}

.badge-current {
  border-color: var(--verified-green);
  background: color-mix(in srgb, var(--verified-green) 8%, var(--warm-white));
  color: var(--verified-green);
}

.badge-archived {
  border-color: var(--structural-line);
  background: var(--cool-paper);
  color: var(--operational-slate);
}

/* DESIGN.md tokenises no warning colour — its palette is navy/blue,
 * semantic red, semantic green, and neutrals. These literals — here, on
 * .receipt-warning, and on .trust-state.unavailable — are the only COLOUR
 * values in this file with no token behind them (lengths and sizes outside
 * :root are ordinary CSS, not tokens), kept because "pending" is neither
 * an error nor a settled
 * state and slate would erase that distinction.
 *
 * That is a real gap in the design system, not a private palette, and it is
 * written down: Q-136 in docs/questions.md. .badge-pending is also reused
 * across a dozen further states — draft, deleted, removed, redacted,
 * queued, reversed, cancelled, voided, inactive — so the name badly
 * understates what it carries; the same question owns that. */
.badge-pending {
  border-color: #8a6f36;
  background: #fdf6e7;
  color: #6f5522;
}

.badge-danger {
  border-color: var(--reconciliation-red);
  background: color-mix(in srgb, var(--reconciliation-red) 7%, var(--warm-white));
  color: var(--reconciliation-red);
}

/* The constitution's six trust states, as one shared vocabulary.
 *
 * HONESTY NOTE: no production page renders `.trust-state` yet. It is
 * declared here because the constitution defines the vocabulary and
 * BOB-269/270 build the surfaces that speak it. Nothing in the product
 * currently claims a trust state, and this rule does not make it do so.
 * The prototype carried four of the six (attention, verified, ready,
 * unavailable); `in-progress` and `blocked` are added here. */
.trust-state {
  display: inline-flex;
  align-items: center;
  width: max-content;
  max-width: 100%;
  color: var(--nucleus-navy);
  font-family: var(--type-label-family);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  line-height: var(--type-label-leading);
  letter-spacing: var(--type-label-tracking);
  text-transform: uppercase;
}

.trust-state::before {
  content: "";
  width: 7px;
  height: 7px;
  margin-right: 7px;
  border-radius: 50%;
  background: var(--operational-slate);
}

.trust-state.attention::before { background: var(--reconciliation-red); }
.trust-state.ready::before { background: var(--signal-blue); }
.trust-state.in-progress::before { background: var(--nucleus-navy); }
.trust-state.blocked::before { background: var(--operational-ink); }
.trust-state.completed::before { background: var(--verified-green); }
.trust-state.unavailable::before { background: #8a6f36; }

/* ------------------------------------------------------------------ */
/* Surfaces. Flat by default: a border and a surface tone carry structure,
 * so ordinary cards do not each claim their own elevation (DESIGN.md,
 * The Flat-By-Default Rule). */

.card {
  margin: var(--space-lg) 0;
  padding: 18px;
  background: var(--warm-white);
  border: 1px solid var(--structural-line);
  border-radius: var(--radius-surface);
}

.card h2,
.card h3 {
  margin: 0 0 var(--space-xs);
  font-family: var(--type-title-family);
  font-size: var(--type-title-size);
  font-weight: var(--type-title-weight);
  line-height: var(--type-title-leading);
}

.card p { margin: var(--space-xs) 0 0; }

.summary {
  display: grid;
  /* The label column WRAPS before it starves the value column: `auto`
   * resolves toward max-content, and the receipt's longest label
   * ("Added to cover online processing costs", BOB-33 S6) took nearly
   * the whole 390px card, wrapping the method sentence letter-by-letter
   * and breaking "$40.00" across lines. Capped, a long label folds to
   * two lines and every value keeps its room; short-labelled summaries
   * (the pay review's Total) size exactly as `auto` sized them. */
  grid-template-columns: fit-content(55%) 1fr;
  gap: var(--space-sm) var(--space-xl);
  margin: var(--space-lg) 0;
  padding: 18px;
  background: var(--warm-white);
  border: 1px solid var(--structural-line);
  border-radius: var(--radius-surface);
}

.summary dt {
  color: var(--nucleus-navy);
  font-family: var(--type-label-family);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  line-height: var(--type-label-leading);
  letter-spacing: var(--type-label-tracking);
  text-transform: uppercase;
}

.summary dd {
  margin: 0;
  overflow-wrap: anywhere;
}

/* A figure in a summary sits on the right, the way it already does in
 * every table this product draws (`table.ledger td.amount`,
 * `.workspace-table td.amount`, and the phone rendering of both in
 * `.record-summary dd`). This was the one place the rule went unstated,
 * and the cost showed on the pay review's Total: the label column is
 * `auto`, so the figures moved horizontally between the fee-accepted
 * and fee-declined states — the longest label changes — and one table
 * read as two. Anchored to the right edge they hold still, and
 * `.amount`'s tabular figures line the decimal points up (Bob's
 * attended pass, 2026-08-19). */
.summary dd.amount { text-align: right; }

.tiles {
  display: grid;
  gap: var(--space-md);
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  margin: var(--space-lg) 0;
}

.tiles .card { margin: 0; }

/* A TILE IS A CARD THAT IS ENTIRELY A LINK (BOB-351). `.tile` was an
 * inert class until now — `money/views` emitted it and no rule in this
 * file matched it — so it had the card's treatment and nothing of its
 * own. The column layout is what lets the label, figure and detail keep
 * their block rhythm inside an inline element.
 *
 * THE LABEL CARRIES THE LINK COLOUR, AND THAT IS THE WHOLE AFFORDANCE
 * AT REST. Before this the title was `h3 > a` and took the global
 * `a { color: var(--signal-blue) }`, so a reader could see the card was
 * a door without touching it. Making the whole card the anchor removed
 * that, and hover does not exist on a phone — which matters most on the
 * Money overview, where the Return surface's DELIBERATELY unlinked
 * `.evidence-item` cards lead the page and resolve to the same fill,
 * border, radius, label role and figure treatment as these. Blue on the
 * label is what tells the two apart with no pointer, and it is what ADR
 * backend/0051 D4 already assigns to a link. The figure and the detail
 * stay ink: they are a measure and a sentence, not the word you press. */
.tile {
  display: flex;
  flex-direction: column;
  color: var(--operational-ink);
  text-decoration: none;
}

.tile:hover { border-color: var(--nucleus-navy); }
.tile:hover .tile-label { text-decoration: underline; }

.tile-label {
  color: var(--signal-blue);
  font-family: var(--type-label-family);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  line-height: var(--type-label-leading);
  letter-spacing: var(--type-label-tracking);
  text-transform: uppercase;
}

.tile-detail {
  margin-top: var(--space-xs);
  color: var(--operational-slate);
  font-size: 12px;
}

/* A metric is an exact operational measure: the value role, tabular. */
.tile-figure {
  margin: var(--space-xs) 0 0;
  font-family: var(--type-value-family);
  font-size: var(--type-value-size);
  font-weight: var(--type-value-weight);
  line-height: var(--type-value-leading);
  letter-spacing: var(--type-value-tracking);
  font-variant-numeric: tabular-nums;
}

.picker-scroll {
  max-height: 16rem;
  overflow-y: auto;
  padding: var(--space-xs) var(--space-md);
  background: var(--warm-white);
  border: 1px solid var(--structural-line);
  border-radius: var(--radius-control);
}

.picker-scroll .choice { margin: 0; padding: var(--space-xs) 0; }

.deposit-confirm {
  margin: 0 0 var(--space-xl);
  color: var(--operational-ink);
  background: var(--assistance-blue-wash);
  border: 1px solid var(--operational-slate);
  border-radius: var(--radius-feature);
}

.deposit-confirm-body { padding: var(--space-xl); }
.deposit-confirm-body h2 { margin-top: 0; }
.deposit-confirm-body form { margin-top: var(--space-lg); }
.deposit-confirm-body form + form { margin-top: var(--space-sm); }

@media (max-width: 640px) {
  .deposit-confirm-body { padding: var(--space-lg); }
  .deposit-confirm-body button { width: 100%; min-height: 44px; }
}

/* ------------------------------------------------------------------ */
/* Consequential authoring: the live consequence rail, and the compact
 * confirmation that carries the final assent (`guided-posting` and
 * `rail-dialog`). The rail is a PREVIEW — it is styled as the quiet
 * assistance surface, never as a receipt, because nothing has
 * happened yet. */

.consequence-rail {
  margin: var(--space-lg) 0;
  padding: var(--space-lg);
  background: var(--assistance-blue-wash);
  border: 1px solid var(--operational-slate);
  border-radius: var(--radius-feature);
}

.consequence-rail .summary {
  margin: var(--space-sm) 0 0;
  background: transparent;
  border: 0;
  padding: 0;
}

.consequence-rail .hint { margin: var(--space-md) 0 0; }

/* The rail stays in view while the reader scans a long resolution
 * table and reaches the commit beneath it — the figures and the button
 * that promises them are never on screen apart. Sticky only where
 * there is height to spare: on a phone a pinned block would eat the
 * viewport the table needs. */
@media (min-width: 48rem) {
  .consequence-rail { position: sticky; top: var(--space-md); z-index: 1; }
}

.preview-again { margin: var(--space-md) 0; }

#charge-commit { margin: var(--space-lg) 0 0; }
#charge-commit .hint { margin: var(--space-sm) 0 0; }

dialog {
  max-width: 32rem;
  padding: 0;
  color: var(--operational-ink);
  background: var(--warm-white);
  border: 1px solid var(--structural-line);
  border-radius: var(--radius-feature);
}

dialog::backdrop { background: rgba(15, 23, 42, 0.45); }

.confirm-body { padding: var(--space-xl); }
.confirm-body h2 { margin-top: 0; }
.confirm-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md);
  justify-content: flex-end;
  padding: var(--space-lg) var(--space-xl) var(--space-xl);
}

/* The dialog's own commit reads as the weightier of its two buttons
 * without claiming the page's single primary — the control that opened
 * the dialog already holds that. */
.confirm-actions .confirm-commit {
  color: var(--warm-white);
  background: var(--nucleus-navy);
  border-color: var(--nucleus-navy);
}

@media (max-width: 640px) {
  .consequence-rail { padding: var(--space-md); }
  .confirm-body { padding: var(--space-lg); }
  .confirm-actions { padding: var(--space-md) var(--space-lg) var(--space-lg); }
  .confirm-actions button { width: 100%; min-height: 44px; }
  .preview-again button,
  #charge-commit button { width: 100%; min-height: 44px; }
}

/* ------------------------------------------------------------------ */
/* Consequential authoring, the batch case (BOB-308 slice 4): the roster
 * import's plan region carries the same rail, the same explicit
 * re-plan and the same labelled commit as the posting form above, so
 * it takes the same spacing. Its own block rather than extra selectors
 * on the rules above, so the two slices of this pass never edit one
 * line from two branches. */

#import-commit { margin: var(--space-lg) 0 0; }
#import-commit .hint { margin: var(--space-sm) 0 0; }

/* The import's commit label states four figures, so it wraps on a
 * narrow screen rather than pushing the page sideways. */
#import-commit button { white-space: normal; text-align: left; }

@media (max-width: 640px) {
  #import-commit button { width: 100%; min-height: 44px; }
}

/* The credit batch's reviewed rows (BOB-350). `[:ol.review-rows]` and
 * `[:li.review-row]` matched no rule in this file at all, so the one
 * review that a treasurer reads line by line before posting money
 * rendered as a browser-default numbered list. Each row is a bounded
 * accounting of one pasted line — what it resolved to, the line as it
 * was typed, and either its exact planned consequence or its refusals —
 * so it takes a contained row rather than a list item with a marker.
 *
 * The list numbering is dropped because every row already prints its
 * own row number, which is the number the refusals and the error
 * summary's anchors name; a second, browser-generated count beside it
 * would be a different number the moment the review shows a subset. */
.review-rows {
  display: grid;
  gap: var(--space-sm);
  margin: var(--space-lg) 0 0;
  padding: 0;
  list-style: none;
}

.review-row {
  padding: var(--space-md);
  background: var(--warm-white);
  border: 1px solid var(--structural-line);
  border-radius: var(--radius-surface);
}

.review-row > .who { margin: 0; font-weight: 600; }
.review-row > .meta { color: var(--operational-slate); }
.review-row > .meta,
.review-row > .hint,
.review-row > .field-error { margin: var(--space-xs) 0 0; }

/* The pasted line, verbatim: it is evidence, so it wraps rather than
 * pushing the page sideways, and it reads as data rather than prose. */
.review-row .meta code {
  padding: 0 var(--space-xs);
  background: var(--cool-paper);
  border-radius: var(--radius-control);
  font-family: var(--type-label-family);
  overflow-wrap: anywhere;
}

/* ------------------------------------------------------------------ */
/* Panels that state a consequence. Red and green are semantic here: they
 * describe what the records prove, never decoration. */

.errors {
  margin: var(--space-lg) 0;
  padding: var(--space-md) var(--space-lg);
  background: color-mix(in srgb, var(--reconciliation-red) 6%, var(--warm-white));
  border: 1px solid var(--reconciliation-red);
  border-radius: var(--radius-surface);
}

.errors h2 {
  margin: 0 0 var(--space-xs);
  color: var(--reconciliation-red);
  font-family: var(--type-title-family);
  font-size: var(--type-title-size);
  font-weight: var(--type-title-weight);
  line-height: var(--type-title-leading);
}

.errors ul { margin: var(--space-xs) 0 0; padding-left: var(--space-xl); }
.errors a { color: var(--reconciliation-red); }

.field-error {
  margin: 0 0 var(--space-xs);
  color: var(--reconciliation-red);
  font-size: 12px;
  font-weight: 600;
}

.receipt {
  margin: var(--space-lg) 0;
  padding: var(--space-md) var(--space-lg);
  background: color-mix(in srgb, var(--verified-green) 7%, var(--warm-white));
  border: 1px solid var(--verified-green);
  border-radius: var(--radius-surface);
  color: var(--operational-ink);
}

/* A receipt's own list — the households a bulk send could not reach
 * (BOB-308 slice 3). Denser than a workspace row and without its dividers:
 * this is an accounting closing out one act inside a panel, not a queue to
 * work through, and the rules would read as structure it does not have. */
.receipt ul.rows { margin: var(--space-xs) 0 0; }
.receipt ul.rows li { padding: var(--space-xs) 0; border-bottom: 0; }

.receipt-warning {
  background: #fdf6e7;
  border-color: #8a6f36;
  color: #4f3d18;
}

.notice {
  margin: var(--space-lg) 0;
  padding: var(--space-md) var(--space-lg);
  background: var(--assistance-blue-wash);
  border: 1px solid var(--structural-line);
  border-radius: var(--radius-surface);
  color: var(--operational-ink);
}

/* ------------------------------------------------------------------ */
/* Controls.
 *
 * DESIGN.md sets a 36px minimum visual height and says production touch
 * targets expand on compact devices. A coarse pointer therefore restores
 * the 44px target the product has always honoured, at every width.
 */

/* A control's own edge must reach 3:1 (WCAG 1.4.11) — it is what tells a
 * user where the field is. --structural-line measures 1.29:1 on white, so
 * it stays on dividers, rules, and panel edges, and the control border
 * takes --operational-slate (4.83:1). The stylesheet this replaces used
 * #8a93a3 here, which passed at 3.10:1; keeping the divider colour would
 * have been a regression. */
input,
select {
  width: 100%;
  min-height: 36px;
  padding: 0 var(--space-md);
  background: var(--warm-white);
  border: 1px solid var(--operational-slate);
  border-radius: var(--radius-control);
  color: var(--operational-ink);
  font-family: inherit;
  font-size: var(--type-body-size);
}

textarea {
  width: 100%;
  min-height: 10rem;
  padding: var(--space-md);
  background: var(--warm-white);
  border: 1px solid var(--operational-slate);
  border-radius: var(--radius-control);
  color: var(--operational-ink);
  font-family: inherit;
  font-size: var(--type-body-size);
  line-height: var(--type-body-leading);
}

input[aria-invalid=true],
select[aria-invalid=true],
textarea[aria-invalid=true] {
  border-color: var(--reconciliation-red);
  border-width: 2px;
}

input[type=file] { min-height: 36px; padding: var(--space-sm); }

/* ------------------------------------------------------------------ */
/* The field group (BOB-349).
 *
 * A label, its guidance, its refusal and its control are ONE thing, and
 * before this they were four siblings spliced into a form. Nothing could
 * be said about the group as a group — not its rhythm, not its
 * alignment, and above all not its WIDTH, because the only element CSS
 * could reach was the control, and a control that sets its own width
 * sets it for the filter bar and the search box too.
 *
 * `ui/text-field`, `ui/textarea-field` and `ui/select-field` emit the
 * wrapper, so all seventy-two shared-helper call sites, across thirteen
 * view namespaces, take this without a markup change of their own.
 *
 * NOT every form. The rule, which is the only part worth stating: a form
 * takes this system through the SHARED HELPERS and by no other route, so
 * a form that hand-builds a control — the announcement attachment's file
 * input, the Stripe disposition note's textarea — gets none of it for
 * that control, and confirmation, inline and filter forms hold no field
 * group by design. A form can be both: the document upload hand-builds
 * its file input AND calls `text-field` for the display name.
 *
 * NO COUNT IS GIVEN, deliberately. Three attempts were made at one and
 * all three were wrong — "every form in the product", then "the" live
 * example, then an exhaustive three, which was wrong in BOTH directions
 * at once. An enumeration in a comment is a claim that nothing rechecks
 * and that every later view invalidates. The rule above is checkable by
 * reading a call site. The figure that IS given — seventy-two call sites
 * across thirteen namespaces — is countable by grep, which is why it is
 * the one that survived. Neither is ADR 0051's "64 pages", which counts
 * every web page and not every form.
 */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  /* the GROUP takes the reading measure, because a label and its
   * guidance are prose and prose has one comfortable width whatever the
   * control below it holds */
  max-width: 36rem;
  margin: var(--space-lg) 0 0;
}

/* The group owns the spacing now, so the parts inside it stop carrying
 * their own — otherwise the label's block margin and the hint's
 * paragraph margin stack on top of the gap and the rhythm reads as
 * accidental. */
.field > label,
.field > .hint,
.field > .field-error { margin: 0; }

/* WIDTH IS A CLAIM ABOUT CONTENT (`ui/field-width` derives which), AND
 * IT IS A CLAIM ABOUT THE CONTROL. The first shape of this rule measured
 * the whole group, which read correctly until a compact field carried a
 * hint: the four-character year box was right and its two-sentence
 * explanation was folded into a five-line ribbon 8rem wide. A label and
 * a hint are prose; only the box is sized by what it holds.
 *
 * The control fills its group by default (`input, select { width: 100% }`
 * above), so a control OUTSIDE a group — the filter bar's search box, a
 * workspace picker — is untouched by any of this. */
.field-compact > input,
.field-compact > select { max-width: 8rem; }
.field-short > input,
.field-short > select   { max-width: 13rem; }
.field-medium > input,
.field-medium > select  { max-width: 24rem; }
.field-wide > input,
.field-wide > select    { max-width: 36rem; }

/* A field nested inside a choice's detail is already indented under its
 * radio and bounded by the card; it takes the group's rhythm and gives
 * up the extra separation a top-level field earns. */
.choice-detail > .field:first-child { margin-top: 0; }

/* ------------------------------------------------------------------ */
/* Authoring forms sit in something (BOB-349).
 *
 * Record a payment and New participant were a single column of controls
 * on the page background: nothing said where the form began, where it
 * ended, or that the page's prose above it was not part of it.
 *
 * SELECTED BY WHAT THE FORM CONTAINS, not by a class each view would
 * have to remember. A form holding a field group as a DIRECT child is an
 * authoring form by construction; a confirmation form (`.choice` only),
 * an inline row form, and the filter bar's GET form all hold none and
 * are left alone — as is any form whose only field group sits deeper,
 * inside a row or a disclosure.
 *
 * THE TWO DEPTHS ARE MEASURED, NOT ASSUMED. Every form in the product
 * that holds a field group as a direct child was enumerated from
 * rendered markup: the authoring pages put it straight in `main`, and
 * the record-detail edit forms plus Settings put it in `main`'s own
 * `<section>`. Nothing deeper exists, and nothing that does exist sits
 * inside a surface that would double-card it. The first shape of this
 * rule said `main >` only, and Settings — a form page by any reading —
 * silently kept the bare canvas. BOTH depths are pinned against rendered
 * product markup — `people-flows-test` for `main > form` and for the
 * record-detail `main > section > form` — so a view that nests its form
 * further fails rather than quietly losing its container;
 * `web/design_test` separately drives the walker that reads them over
 * literals, including the shapes it must refuse.
 */
main > form:not(.inline):not(.filter-form):not(.confirm):has(> .field),
main > section > form:not(.inline):not(.filter-form):not(.confirm):has(> .field) {
  /* The admin canvas is unbounded (BOB-279) so that registers and tables
   * can use it. A single column of controls cannot: at 1440px the card
   * ran the full workspace with its fields huddled at the left, which is
   * a container that describes nothing. The form is bounded here rather
   * than by `main`, so the masthead and any register on the same page
   * keep the canvas. */
  max-width: 46rem;
  margin: var(--space-lg) 0 0;
  padding: var(--space-xl);
  background: var(--warm-white);
  border: 1px solid var(--structural-line);
  border-radius: var(--radius-surface);
}

/* The card's own padding is the first field's separation. `:first-of-type`
 * rather than `:first-child` because the CSRF token and any recording key
 * are hidden inputs the form opens with (ADR backend/0011). */
main > form:not(.inline):not(.filter-form):not(.confirm):has(> .field) > .field:first-of-type,
main > section > form:not(.inline):not(.filter-form):not(.confirm):has(> .field) > .field:first-of-type { margin-top: 0; }

@media (max-width: 48rem) {
  main > form:not(.inline):not(.filter-form):not(.confirm):has(> .field),
  main > section > form:not(.inline):not(.filter-form):not(.confirm):has(> .field) {
    padding: var(--space-lg);
  }
}

/* ------------------------------------------------------------------ */
/* Numbered steps (BOB-350).
 *
 * An authoring form is a sequence, and a form that says so only in the
 * words of its headings — `1. What, and how much` — says it where no
 * rule can reach and no reader can scan. `ui/form-step` emits the badge
 * and the heading together from ONE number, so the digit cannot appear
 * twice or drift from its position in the form.
 *
 * The Money authoring screens take it. Forms elsewhere still write
 * their own numbered headings — the roster import's four are literal
 * `<h2>`s — and none of what follows reaches them.
 */

.form-step { margin: 0 0 var(--space-lg); }

/* Every step but the one that OPENS the form is separated from the work
 * above it. The base rule carries no top margin and this adds it, so
 * "the first step" needs no positional selector at all — `:first-child`
 * would be wrong (a form opens with hidden inputs) and `:first-of-type`
 * would be a claim about which `<div>` a view happens to write first.
 *
 * The second selector reaches the step the posting section HOLDS. Its
 * DOM siblings are that section's own children rather than the form's,
 * so `~` alone stops at step 3 — while on screen step 4 sits directly
 * under it, because the section generates no box (see below). */
.form-step ~ .form-step,
.form-step ~ * .form-step {
  margin-top: var(--space-xl);
  padding-top: var(--space-xl);
  border-top: 1px solid var(--structural-line);
}

.form-step > h2 {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin: 0;
}

/* The label role, tabular-sized to a circle: a step number is an
 * operational marker, not prose. */
.step-number {
  display: grid;
  place-items: center;
  flex: 0 0 26px;
  height: 26px;
  background: var(--signal-blue);
  border-radius: 50%;
  color: var(--warm-white);
  font-family: var(--type-label-family);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  letter-spacing: 0;
  line-height: 1;
}

.step-title { min-width: 0; }

.form-step > .hint { max-width: 36rem; margin: var(--space-xs) 0 0; }

/* ------------------------------------------------------------------ */
/* The live consequence BESIDE the form (BOB-350).
 *
 * The reference is the prototype's New Charge: `minmax(0, 1fr) 340px`,
 * the preview sticky at the top of the second column, one column again
 * on a narrow screen.
 *
 * `#charge-posting` IS THE SWAP UNIT AND MUST NOT BE SPLIT. The preview
 * endpoint replaces that element whole, so that every figure derived
 * from the target the reader just changed ships in one response and
 * none can drift from another. Lifting the rail out of it into a
 * sibling panel would be a change to that mechanism dressed as a
 * layout. `display: contents` is what makes the layout possible without
 * touching it: the section generates no box, so its children — the step
 * heading, the rail, the resolution table, the commit — become grid
 * items of the FORM's grid and can be placed independently, while the
 * element itself survives untouched, keeps its id, and re-adopts these
 * rules the moment htmx swaps a new copy in.
 *
 * ROW GAP IS ZERO ON PURPOSE. A grid row is shared between its columns,
 * so a rail sitting in one row makes that row as tall as the rail and
 * the form's first field would start three hundred pixels below its own
 * heading. The rail therefore spans past the end of the form; an item
 * that spans more rows than exist simply creates empty implicit ones,
 * and those are only invisible while no row gap separates them.
 *
 * SO THE VERTICAL RHYTHM IS THE ITEMS' OWN MARGINS — AND THAT IS NOT
 * FREE, because grid items never collapse margins with each other. In
 * block flow a step's 16px bottom margin and the following field's 16px
 * top margin collapse to 16px; as grid items they SUM to 32px, and an
 * earlier shape of this block claimed the spacing was unchanged while
 * measurement said 16px below the breakpoint against 32px above it.
 *
 * THE RULE THAT REPLACES THE COLLAPSE IS ONE OWNER PER BOUNDARY, AND
 * THAT OWNER IS THE ITEM BELOW. A collapse takes the larger of two
 * margins, so an owner-below rule is only equal to it where the item
 * below already carried the larger number. Every place it did not is
 * stated below, and each is a place a first shape of this block got a
 * boundary wrong: the item that opens the grid, the item that closes
 * it, a refusal, the re-preview button, and a table whose margins the
 * wrapper contains rather than lets through.
 */

@media (min-width: 75rem) {
  .rail-beside {
    display: grid;
    /* ONE explicit column, so everything auto-places into it and no
     * view has to pin its own children. The rail asks for column 2 and
     * `grid-auto-columns` gives that implicit track the prototype's
     * 340px, which at the 16px root is what 21.25rem measures. */
    grid-template-columns: minmax(0, 1fr);
    grid-auto-columns: 21.25rem;
    column-gap: var(--space-lg);
    row-gap: 0;
    align-items: start;
    max-width: 72rem;
  }

  main > form.rail-beside:not(.inline):not(.filter-form):not(.confirm):has(> .field) {
    max-width: 72rem;
  }

  #charge-posting { display: contents; }

  /* THE SEPARATION IS OWNED BY THE ITEM BELOW. Two adjacent margins
   * collapsed in block flow and sum here, so a bottom margin with an
   * item under it is dropped and each item's own top margin becomes the
   * whole gap above it — one number per boundary rather than two.
   *
   * `:last-child` keeps its bottom margin. Nothing is under it to
   * double-count against, and that margin is doing real work: a
   * REFUSED credit review ends its section in a notice whose 16px is
   * the whole separation from the commit form beneath the section, and
   * dropping it put the two flush. A postable one ends in
   * `.review-rows`, which carries no bottom margin at all and is
   * separated from that form by whatever the rail leaves behind —
   * see the rail rule at the foot of this block.
   *
   * The second selector reaches the items the posting section HOLDS:
   * they are grid items of THIS grid (the section generates no box)
   * while remaining that section's DOM children, so `>` alone stops at
   * the section itself.
   *
   * A `<dialog>` is excluded, and not as a special case: an open modal
   * is in the top layer rather than in this grid, and `margin: auto` is
   * what centres it there. */
  .rail-beside > *:not(dialog):not(:last-child),
  .rail-beside > #charge-posting > *:not(dialog):not(:last-child) {
    margin-bottom: 0;
  }

  /* AND THE ITEM BELOW HAS TO STATE THE WHOLE GAP, which is not the
   * same as keeping the margin it already had. A collapse took the
   * LARGER of the two margins; this rule takes the lower item's own.
   * Where they were equal — most of this form — nothing moves, and
   * where the lower item's was smaller it now understates the boundary
   * and says the real number here instead.
   *
   * The explicit re-preview carries a paragraph's 12px and follows a
   * table or a notice ending in 16px. It is only ever a grid item of
   * this grid, so it takes a descendant selector; a refusal below is
   * not, and takes child selectors to stay out of the field group, the
   * choice row and the review row that each own one. */
  .rail-beside .preview-again { margin-top: var(--space-lg); }

  /* A refusal carries NO top margin at all — under the collapse the
   * item above it did every bit of the separating — so as a grid item
   * it sat flush against the step heading or paragraph above it. */
  .rail-beside > .field-error,
  .rail-beside > #charge-posting > .field-error {
    margin-top: var(--space-lg);
  }

  /* …except under another refusal, where the boundary is the refusals'
   * own 4px: consecutive refusals are one list to read down, not one
   * separated block each. */
  .rail-beside > .field-error + .field-error,
  .rail-beside > #charge-posting > .field-error + .field-error {
    margin-top: var(--space-xs);
  }

  /* NOTHING ESCAPES THE GRID'S TOP EDGE EITHER. A block container lets
   * its first child's top margin collapse OUT through its own edge, so
   * that margin never added to the separation above the container; a
   * grid container contains it instead, and the credit review's
   * opening paragraph pushed the whole section 12px further from the
   * masthead at 1200px than at 1199px. What separates this container
   * from the work above it is the container's own business. */
  .rail-beside > :first-child { margin-top: 0; }

  /* THE `<table>` STATES NEITHER OF ITS EDGES; THE WRAPPER STATES THE
   * ONE ABOVE IT AND NOTHING ELSE. The wrapper is the grid item, and a
   * grid item establishes an independent formatting context — which
   * does not delete its `<table>` child's margins, it CONTAINS them.
   * So a margin given to the wrapper landed on top of the 16px the
   * table was still carrying inside it: 32px above the table at 1200px
   * against 16px at 1199px, and 28px below it.
   *
   * The boundary BELOW the wrapper is not the wrapper's to state, and
   * saying it was is how this comment read until round 3. The wrapper's
   * own bottom margin is zeroed by the owner-below rule above — the
   * commit always follows it, so it is never `:last-child` — and the
   * gap under it belongs to whatever comes next: `.preview-again`'s
   * 16px, or the truncation notice's own 16px in the state that
   * renders one. Measured in Chrome at a 1200px viewport: the
   * wrapper's margins are 16px and 0, the table's are 0 and 0, and the
   * wrapper-to-button gap is 16px on both sides of the breakpoint. */
  .rail-beside #charge-resolution-table { margin-top: var(--space-lg); }
  .rail-beside #charge-resolution-table > table { margin: 0; }

  /* AND WHAT THE SPAN LEAVES BEHIND IS NOT A BOUNDARY ANY OF THE RULES
   * ABOVE OWNS. The rail spans past the last row in column one, so
   * where the rail is the taller of the two columns the difference
   * lands in the empty implicit rows below that last item, INSIDE this
   * container. On a short postable credit review — two items in column
   * one against a 209px rail — that is 85px between the reviewed rows
   * and the commit form at 1200px, against 0px at 1199px, measured in
   * Chrome. It is not spacing to repair: the rail simply is the tallest
   * thing on that page, and a two-column layout is at least as tall as
   * its taller column. It is stated here so that the owner-per-boundary
   * rules above are not read as a claim about this gap too. */
  .rail-beside .consequence-rail {
    grid-column: 2;
    grid-row: 1 / span 99;
    margin-top: 0;
    align-self: start;
  }
}

/* Blue marks a committing action, and the split falls exactly on the
 * element: a `<button>` submits a form — it is the page's bounded action —
 * while an `<a class="button">` navigates somewhere that a commit might
 * later happen. Painting both filled put fourteen equally loud blue
 * rectangles on the org home page and four more on the money page, which
 * is DESIGN.md's One Signal Rule broken outright ("Signal Blue identifies
 * action or location; it does not wash whole screens in brand color").
 * Navigation therefore takes the secondary treatment. */
button,
.button,
button.primary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 36px;
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--operational-slate);
  border-radius: var(--radius-control);
  background: var(--warm-white);
  color: var(--operational-ink);
  font-family: inherit;
  font-size: var(--type-body-size);
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}

button,
button.primary {
  background: var(--signal-blue);
  border-color: var(--signal-blue);
  color: var(--warm-white);
}

/* A submit button that is NOT the page's primary action (BOB-308 slice 3).
 * DESIGN.md's secondary treatment already existed for `.button` — a LINK
 * styled as a control — but a form's second submit had no way to say it, so
 * a review page carrying both a commit and a way back rendered two equally
 * loud blue controls. The One Signal Rule is the reason it matters: Signal
 * Blue identifies THE consequential action, and a page with two of them
 * identifies neither. A page whose commit is unavailable drops the class,
 * because then this button really is the primary thing to press. */
button.secondary {
  background: var(--warm-white);
  border-color: var(--operational-slate);
  color: var(--operational-ink);
}

/* …including on hover. `button:hover` below paints a navy fill at equal
 * specificity, which under ink-coloured text left the way back off a
 * refused review unreadable exactly while a reader was pointing at it.
 * A secondary control deepens its surface instead (the `.button` link
 * treatment, which is what this class exists to give a submit). */
button.secondary:hover {
  background: var(--cool-paper);
  border-color: var(--operational-slate);
  color: var(--operational-ink);
}

button { margin-top: var(--space-lg); }
.button { margin-top: var(--space-sm); }

button:hover,
button.primary:hover {
  background: var(--nucleus-navy);
  border-color: var(--nucleus-navy);
}

.button:hover { background: var(--cool-paper); }

/* A disabled control must not look operable. Without this it kept the
 * filled primary treatment and a pointer cursor while refusing every
 * click — the presentation promising what the control does not do. */
/* Secondary: white, ink, structural line. */
form.danger button,
li.grant button,
ul.attachments button,
.composer-toolbar button {
  margin: 0;
  background: var(--warm-white);
  border-color: var(--operational-slate);
  color: var(--operational-ink);
}

form.danger button,
li.grant button,
ul.attachments button {
  border-color: var(--reconciliation-red);
  color: var(--reconciliation-red);
}

form.danger button:hover,
li.grant button:hover,
ul.attachments button:hover {
  background: color-mix(in srgb, var(--reconciliation-red) 6%, var(--warm-white));
  border-color: var(--reconciliation-red);
  color: var(--reconciliation-red);
}

.composer-toolbar button:hover {
  background: var(--cool-paper);
  border-color: var(--operational-slate);
  color: var(--operational-ink);
}

/* A form that sits inside a row: no stacked spacing, no block layout. */
form.inline {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  margin: 0;
}

form.inline button { margin: 0; }

/* Placed after every secondary override above, because each of those is
 * 0-1-1 and would otherwise win: a disabled danger button kept the full
 * operable red-on-white treatment, and only the cursor changed. */
button:disabled,
button:disabled:hover,
form.danger button:disabled,
li.grant button:disabled,
ul.attachments button:disabled,
.composer-toolbar button:disabled {
  background: var(--cool-paper);
  border-color: var(--structural-line);
  color: var(--operational-slate);
  cursor: not-allowed;
}

/* A confirmation form is a decisive commit point: it gets separation from
 * the evidence above it rather than a new colour. */
form.confirm {
  margin: var(--space-lg) 0 0;
  padding-top: var(--space-lg);
  border-top: 1px solid var(--structural-line);
}

.quick-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
}

fieldset.choices {
  margin: var(--space-lg) 0 0;
  padding: 0;
  border: 0;
}

fieldset.choices legend {
  padding: 0;
  font-weight: 600;
}

.choice {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0 var(--space-sm);
  align-items: start;
  margin: var(--space-sm) 0;
  padding: var(--space-sm);
  border-radius: var(--radius-control);
}

.choice input[type=checkbox],
.choice input[type=radio] {
  width: 16px;
  height: 16px;
  min-height: 0;
  margin: 3px 0 0;
  accent-color: var(--signal-blue);
}

/* A WIDER CLICK TARGET, AND IT IS THE LABEL — NOT THE ROW (BOB-349).
 * A `<label>` is only as pressable as it is wide, so the label of a
 * two-word choice was a target the width of two words sitting in a row
 * the width of the form. Filling its grid cell is the whole fix: the
 * label still labels exactly its own control, so nothing about the
 * association changes, and a pointer landing anywhere on that line
 * toggles it.
 *
 * WHAT IT DOES NOT DO is make the ROW a target, and the first version of
 * these rules said it did. Measured on Record payment: the row is 58px
 * tall and the label 23px, and `elementFromPoint` over the hint returns
 * the `<p>`, not the label. The hint, the detail panel and the row's own
 * padding are not pressable and must not be painted as though they were
 * — a choice may carry a detail panel with its own controls (the payment
 * form's platform picker sits inside one), which is also why the label
 * must not be stretched over the row to make the claim true.
 *
 * The child combinators are load-bearing. `.choice label` and
 * `.choice .hint` are descendant selectors, and a field group inside
 * `.choice-detail` has both — so the nested field's label took this
 * padding and its hint took a 2px margin that beat `.field > .hint`,
 * measured at 2px against 0px for a top-level one. The group owns its
 * own rhythm only if these rules stop at the row's own children. */
.choice > label {
  display: block;
  margin: 0;
  padding: 2px 0;
  font-weight: 600;
  cursor: pointer;
}

/* The affordance appears while the pointer is over something that
 * actually responds — the label OR the box itself. `.choice:hover`
 * painted the whole row pressable; a label-only version then missed the
 * input, so pointing straight at the radio produced no wash while
 * clicking there selected it. Both are targets, so both wash. It
 * deliberately applies to a choice carrying a detail panel too, since
 * that choice's own label is as pressable as any other. */
.choice:has(> label:hover),
.choice:has(> input[type=checkbox]:hover),
.choice:has(> input[type=radio]:hover) { background: var(--cool-paper); }

.choice > .hint { grid-column: 2; margin: 2px 0 0; }
.choice > .choice-detail { grid-column: 2; margin: var(--space-xs) 0 var(--space-xs); font-weight: 400; }

/* A choice's own refusal sits under its label rather than beside its
 * box — it used to say so through an inline `style` attribute in
 * `ui/confirm-checkbox`, which is one declaration no reader of this
 * file could have found. */
.choice > .field-error { grid-column: 2; margin: 2px 0 0; }

.swatch {
  display: inline-block;
  width: 1em;
  height: 1em;
  margin-right: 0.35em;
  border: 1px solid var(--structural-line);
  border-radius: 3px;
  vertical-align: -0.15em;
  background: var(--swatch);
}

.composer-toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin: var(--space-xs) 0;
}

/* `display` beats the hidden attribute, so a display rule on an element
   the server ships `hidden` has to say so — otherwise the toolbar the
   server deliberately withholds is painted anyway, and a reader without
   scripting gets five buttons with no handlers behind them. Same shape
   as `.search-results[hidden]` below, and the same reason. */
.composer-toolbar[hidden] { display: none; }

.composer-toolbar button { min-width: 36px; }

details.formatting-help { margin: var(--space-sm) 0 0; font-size: 12px; }

details.formatting-help summary,
li.entry summary {
  padding: var(--space-xs) 0;
  color: var(--signal-blue);
  cursor: pointer;
}

details.formatting-help table { border-collapse: collapse; margin: var(--space-sm) 0; }
details.formatting-help td { padding: 3px var(--space-md) 3px 0; vertical-align: top; }

details.formatting-help code {
  padding: 0 var(--space-xs);
  background: var(--cool-paper);
  border-radius: var(--radius-control);
  font-family: var(--type-label-family);
}

/* ------------------------------------------------------------------ */
/* Navigation and chrome. This is the shared shell BOB-269/270 replace. */

header.site {
  background: var(--signal-chrome);
  border-bottom: 1px solid var(--structural-line);
}

header.site nav {
  max-width: 40rem;
  margin: 0 auto;
  padding: var(--space-sm) var(--space-lg);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-lg);
}

header.site .brand {
  margin-right: auto;
  padding: var(--space-sm) 0;
  color: var(--nucleus-navy);
  font-weight: 700;
  letter-spacing: 0.02em;
  text-decoration: none;
}

header.site a { padding: var(--space-sm) 0; }

header.site .who {
  color: var(--operational-slate);
  font-size: 12px;
  overflow-wrap: anywhere;
}

header.site form { margin: 0; }

header.site button {
  margin: 0;
  background: var(--warm-white);
  border-color: var(--operational-slate);
  color: var(--operational-ink);
}

header.site button:hover {
  background: var(--cool-paper);
  border-color: var(--operational-slate);
  color: var(--operational-ink);
}

nav.crumbs {
  margin-top: var(--space-lg);
  font-size: 12px;
}

/* ------------------------------------------------------------------ */
/* The Signal admin shell (BOB-269).
 *
 * Every selector below is rendered ONLY by `views/layout`, the admin
 * document. Family pages and the audience-neutral refusals never carry
 * this markup, so nothing here can reach them — which is the point of
 * the three-document split (BOB-276).
 *
 * Geometry follows the admin prototype's Signal block
 * (`backend/dev/prototypes/admin/styles.css:263-322`): a 228px labelled
 * rail beside the workspace, a 58px utility bar, a 43px local tab row,
 * and the whole rail replaced at phone width by a labelled "All areas"
 * disclosure. Never an icon-only rail (DESIGN.md, Don'ts).
 *
 * The content column keeps its existing 40rem measure. Widening the
 * canvas is a per-area archetype pass (BOB-267), not this slice: the
 * shell is added around 47 pages here without restyling one of them.
 */

.skip-link {
  position: absolute;
  left: -9999px;
  z-index: 30;
  padding: var(--space-sm) var(--space-md);
  background: var(--warm-white);
  border: 1px solid var(--signal-blue);
  border-radius: var(--radius-control);
  color: var(--nucleus-navy);
  font-weight: 700;
}

.skip-link:focus {
  left: var(--space-sm);
  top: var(--space-sm);
}

/* The rail is sticky and one viewport tall, so on a long page its own
 * box ends well above the fold's bottom. The SHELL carries the chrome
 * tone and the workspace paints the paper back over its own column, so
 * the rail's band runs the full height of the page without a wrapper
 * element existing only to be painted. */
.admin-shell {
  display: grid;
  grid-template-columns: 228px minmax(0, 1fr);
  min-height: 100vh;
  background: var(--signal-chrome);
}

.admin-sidebar {
  position: sticky;
  top: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  height: 100vh;
  min-width: 0;
  padding: var(--space-lg) var(--space-md);
  overflow-y: auto;
  background: var(--signal-chrome);
}

.sidebar-identity {
  padding-bottom: var(--space-md);
  border-bottom: 1px solid var(--structural-line);
}

.shell-brand {
  display: block;
  padding: var(--space-sm) var(--space-xs);
  color: var(--nucleus-navy);
  font-family: var(--type-label-family);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  letter-spacing: var(--type-label-tracking);
  text-decoration: none;
  text-transform: uppercase;
}

/* The organization's name lives in the shell now, carrying the stable
 * id a settings save swaps out of band — so renaming the club updates
 * the chrome the operator is looking at, not a paragraph on one page.
 * `.org-name::before` remains the single allowlisted consumer of the
 * organization accent (ADR 0051 D4): moving the name did not widen the
 * accent's reach, and the floor — never semantic — is untouched. */
.admin-sidebar .org-name {
  margin: var(--space-xs) 0 0;
  padding: 0 var(--space-xs);
  color: var(--operational-ink);
  font-family: var(--type-body-family);
  font-size: var(--type-body-size);
  font-weight: 700;
  letter-spacing: normal;
  line-height: 1.25;
  text-transform: none;
  overflow-wrap: anywhere;
}

.primary-nav,
.mobile-areas {
  display: grid;
  gap: 2px;
}

.primary-nav a,
.mobile-areas a {
  display: flex;
  align-items: center;
  min-height: 40px;
  padding: 0 var(--space-md);
  border-radius: var(--radius-control);
  color: var(--operational-ink);
  font-weight: 600;
  text-decoration: none;
}

.primary-nav a:hover,
.mobile-areas a:hover { background: var(--cool-paper); }

/* Location, never action: the current area takes the wash and navy the
 * constitution assigns to "where you are". */
.primary-nav a[aria-current=page],
.mobile-areas a[aria-current=page] {
  background: var(--active-blue-wash);
  color: var(--nucleus-navy);
  font-weight: 700;
}

.nav-mark {
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  margin-right: var(--space-sm);
  border: 1px solid var(--operational-slate);
  border-radius: 2px;
}

.primary-nav a[aria-current=page] .nav-mark,
.mobile-areas a[aria-current=page] .nav-mark {
  background: var(--signal-blue);
  border-color: var(--signal-blue);
}

.admin-workspace {
  display: flex;
  min-width: 0;
  min-height: 100vh;
  flex-direction: column;
  background: var(--cool-paper);
  /* the rule between rail and workspace belongs to the WORKSPACE, which
   * is as tall as the page — on the sticky rail it stopped at 100vh and
   * left a visible seam halfway down a long page */
  border-left: 1px solid var(--structural-line);
}

.utility-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm) var(--space-lg);
  min-height: 58px;
  padding: var(--space-sm) var(--space-page);
  background: var(--signal-chrome);
  border-bottom: 1px solid var(--structural-line);
}

.mobile-brand,
.areas-disclosure { display: none; }

.utility-location,
.utility-crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
  min-width: 0;
  color: var(--operational-slate);
  font-size: 12px;
}

.utility-location { margin-right: auto; }

.utility-location a { color: var(--operational-slate); }
.utility-location strong { color: var(--nucleus-navy); }
.crumb-sep { color: var(--structural-line); }

/* The Season rides the location band rather than the rail, so it
 * survives the phone — where the rail is a disclosure and the band is
 * the only standing statement of where the operator is. */
.season-context {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-sm);
  padding-left: var(--space-md);
  border-left: 1px solid var(--structural-line);
}

.season-context-label {
  color: var(--operational-slate);
  font-family: var(--type-label-family);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  letter-spacing: var(--type-label-tracking);
  text-transform: uppercase;
}

.season-context strong {
  color: var(--nucleus-navy);
  overflow-wrap: anywhere;
}

.season-context .season-none {
  color: var(--operational-slate);
  font-weight: 400;
}

.utility-account {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm) var(--space-md);
}

.utility-account .who {
  color: var(--operational-slate);
  font-size: 12px;
  overflow-wrap: anywhere;
}

.utility-account form { margin: 0; }

.utility-account button {
  margin: 0;
  background: var(--warm-white);
  border-color: var(--operational-slate);
  color: var(--operational-ink);
}

.utility-account button:hover {
  background: var(--cool-paper);
  border-color: var(--operational-slate);
  color: var(--operational-ink);
}

/* Global search (BOB-272) — the bar's accelerator, not a workspace.
 * The field is a real GET form and the panel is a real list of links,
 * so everything below is presentation: the popover positioning lives
 * on `.search-results` alone, and the group/result rules are shared
 * with the plain results page, which renders the same list unwrapped.
 * `:empty` keeps the panel out of the layout between keystrokes — the
 * server ships the element always, so the swap has a stable target. */
.global-search {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.global-search input[type=search] {
  width: 15rem;
  min-height: 34px;
  margin: 0;
  padding: 0 var(--space-md);
  background: var(--warm-white);
  border: 1px solid var(--structural-line);
  border-radius: var(--radius-control);
  color: var(--operational-ink);
  font-family: var(--type-body-family);
  font-size: var(--type-body-size);
}

.global-search button {
  margin: 0;
  background: var(--warm-white);
  border-color: var(--operational-slate);
  color: var(--operational-ink);
}

.global-search button:hover {
  background: var(--cool-paper);
  border-color: var(--operational-slate);
  color: var(--operational-ink);
}

.search-results {
  position: absolute;
  z-index: 15;
  top: calc(100% + var(--space-xs));
  right: 0;
  width: min(24rem, 80vw);
  max-height: 60vh;
  padding: var(--space-sm);
  overflow-y: auto;
  background: var(--warm-white);
  border: 1px solid var(--structural-line);
  border-radius: var(--radius-surface);
  box-shadow: 0 14px 30px rgba(15, 23, 42, .24);
}

.search-results:empty,
.search-results[hidden] { display: none; }

.search-group + .search-group {
  margin-top: var(--space-sm);
  padding-top: var(--space-sm);
  border-top: 1px solid var(--structural-line);
}

.search-group-label {
  margin: 0 0 var(--space-xs);
  padding: 0 var(--space-sm);
  color: var(--operational-slate);
  font-family: var(--type-label-family);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  letter-spacing: var(--type-label-tracking);
  text-transform: uppercase;
}

.search-group ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

.search-result {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  min-height: 44px;
  padding: var(--space-sm);
  border-radius: var(--radius-control);
  color: var(--operational-ink);
  text-decoration: none;
  overflow-wrap: anywhere;
}

.search-result small {
  color: var(--operational-slate);
  font-size: 12px;
}

.search-result:hover,
.search-result-active {
  background: var(--active-blue-wash);
  text-decoration: none;
}

.search-empty {
  margin: 0;
  padding: var(--space-sm);
  color: var(--operational-slate);
}

/* What the search could not read. It is guidance, not a result, so it
 * carries the assistance wash and sits above the groups behind its own
 * rule — a reader scanning destinations must not mistake it for one. */
.search-truncated {
  margin: 0 0 var(--space-sm);
  padding: var(--space-sm);
  background: var(--assistance-blue-wash);
  border-radius: var(--radius-control);
  color: var(--operational-ink);
  font-size: 12px;
  line-height: 1.4;
}

.search-page-results .search-truncated { margin-bottom: var(--space-md); }

.search-page-results {
  max-width: 40rem;
  padding: var(--space-md);
  background: var(--warm-white);
  border: 1px solid var(--structural-line);
  border-radius: var(--radius-surface);
}

/* Page-local navigation: labelled, horizontally scannable, and it
 * scrolls itself rather than making the page overflow sideways
 * (constitution — Accessibility and responsive behavior). An area with
 * one destination renders no row at all. */
.local-nav {
  display: flex;
  gap: var(--space-xl);
  min-height: 43px;
  padding: 0 var(--space-page);
  overflow-x: auto;
  background: var(--signal-chrome);
  border-bottom: 1px solid var(--structural-line);
}

.local-nav a {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  min-height: 43px;
  border-bottom: 2px solid transparent;
  color: var(--operational-slate);
  font-size: 12px;
  text-decoration: none;
  white-space: nowrap;
}

.local-nav a:hover { color: var(--operational-ink); }

.local-nav a[aria-current=page] {
  border-bottom-color: var(--signal-blue);
  color: var(--nucleus-navy);
  font-weight: 700;
}

.admin-workspace main { max-width: none; padding: var(--space-page) var(--space-page) 3rem; }

/* No rule here for a page's own breadcrumbs. They used to be a loose
 * `nav.crumbs` row directly under the utility bar, which is what this
 * selector existed to tidy; since BOB-343 the trail is part of the
 * masthead composition and is styled with it, and no admin page renders
 * a crumb row outside that band. `nav.crumbs` itself still stands for
 * the family shell, whose pages carry no masthead to join (BOB-378). */

.site-footer {
  margin-top: auto;
  padding: var(--space-lg) var(--space-page);
  background: var(--signal-chrome);
  border-top: 1px solid var(--structural-line);
}

.site-footer p {
  margin: 0;
  color: var(--operational-slate);
  font-size: 12px;
}

/* ------------------------------------------------------------------ */
/* Mastheads. A warm band that says what kind of surface this is, what
 * it is, and — where the page starts work — the workflow starters. The
 * actions live here and nowhere lower on the page. */

.page-masthead {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-xl);
  margin: 0 calc(var(--space-page) * -1) var(--space-xl);
  padding: var(--space-lg) var(--space-page) var(--space-xl);
  background: var(--signal-chrome);
  border-bottom: 1px solid var(--structural-line);
}

.page-masthead:first-child { margin-top: calc(var(--space-page) * -1); }

.masthead-copy {
  min-width: 0;
  flex: 1 1 auto;
  overflow-wrap: anywhere;
}

/* The way back rides INSIDE the band (BOB-343), above the eyebrow, the
 * way the admin prototype's New Charge page draws it. It used to be a
 * loose row above the masthead — a line of chrome with no band of its
 * own, sitting on the page's background, which is what the review call
 * of "it looks weird floating there" was about.
 *
 * Inside the copy column rather than as a third child of the header:
 * the band is `justify-content: space-between` with the actions on the
 * right, so a trail sitting beside those two would be laid out across
 * from the heading rather than above it.
 *
 * The prototype gets the same look with negative margins and a padding
 * collapse on the sibling element. That is plumbing for a trail that is
 * OUTSIDE the band and has to pretend otherwise; inside it, none of it
 * is needed. */
.page-masthead .crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
  margin: 0 0 var(--space-sm);
}

.page-masthead .crumbs a { color: var(--operational-slate); }
.page-masthead .crumb-here { color: var(--nucleus-navy); font-weight: 700; }

.page-masthead h1 {
  margin: var(--space-xs) 0 0;
  padding: 0;
  border: 0;
}

.eyebrow {
  margin: 0;
  color: var(--nucleus-navy);
  font-family: var(--type-label-family);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  line-height: var(--type-label-leading);
  letter-spacing: var(--type-label-tracking);
  text-transform: uppercase;
}

.masthead-lede {
  max-width: 62ch;
  margin: var(--space-sm) 0 0;
  color: var(--operational-slate);
}

.workflow-actions {
  display: flex;
  flex: 0 1 auto;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-sm);
  margin: 0;
}

/* Some masthead actions are verbs, not destinations — a POST form whose
 * one button is the action (start an announcement from this template,
 * connect a Stripe account). The form is a wrapper the row must not see:
 * it and its button lose the stacking margins the page body gives them. */
.workflow-actions .button,
.workflow-actions form,
.workflow-actions button { margin: 0; }

/* The page's ONE primary action. `.button` is deliberately the
 * secondary treatment (BOB-268: fourteen filled blue rectangles on one
 * page is DESIGN.md's One Signal Rule broken outright), so the filled
 * treatment has to be asked for by name — and a masthead asks for it
 * exactly once. */
.button.primary {
  background: var(--signal-blue);
  border-color: var(--signal-blue);
  color: var(--warm-white);
}

.button.primary:hover {
  background: var(--nucleus-navy);
  border-color: var(--nucleus-navy);
}

/* ------------------------------------------------------------------ */
/* The shell at intermediate and phone widths. */

@media (max-width: 69rem) {
  .admin-shell { grid-template-columns: 210px minmax(0, 1fr); }
  .utility-bar,
  .local-nav,
  .site-footer { padding-right: var(--space-xl); padding-left: var(--space-xl); }
  .admin-workspace main {
    padding-right: var(--space-xl);
    padding-left: var(--space-xl);
  }
  .page-masthead {
    margin-right: calc(var(--space-xl) * -1);
    margin-left: calc(var(--space-xl) * -1);
    padding-right: var(--space-xl);
    padding-left: var(--space-xl);
  }
  .page-masthead:first-child { margin-top: calc(var(--space-xl) * -1); }
}

@media (max-width: 48rem) {
  /* The rail is not collapsed to icons — it is replaced by a labelled
   * disclosure holding every one of the same destinations. */
  .admin-shell { display: block; }
  .admin-sidebar { display: none; }

  .utility-bar {
    gap: var(--space-sm);
    padding: var(--space-sm) var(--space-lg);
  }

  .mobile-brand {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    color: var(--nucleus-navy);
    font-family: var(--type-label-family);
    font-size: var(--type-label-size);
    font-weight: var(--type-label-weight);
    letter-spacing: var(--type-label-tracking);
    text-decoration: none;
    text-transform: uppercase;
  }

  .areas-disclosure {
    display: block;
    position: relative;
    margin-right: auto;
  }

  .areas-disclosure summary {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 0 var(--space-md);
    background: var(--warm-white);
    border: 1px solid var(--operational-slate);
    border-radius: var(--radius-control);
    color: var(--nucleus-navy);
    font-weight: 700;
    cursor: pointer;
  }

  .areas-disclosure .mobile-areas {
    position: absolute;
    z-index: 15;
    top: calc(100% + var(--space-xs));
    left: 0;
    min-width: 15rem;
    padding: var(--space-sm);
    background: var(--warm-white);
    border: 1px solid var(--structural-line);
    border-radius: var(--radius-surface);
    box-shadow: 0 14px 30px rgba(15, 23, 42, .24);
  }

  .areas-disclosure .mobile-areas a { min-height: 44px; }

  /* The band is not hidden here: with the rail behind a disclosure it
   * is the only standing statement of the organization, the area and
   * the Season. It takes the whole second row and wraps rather than
   * pushing the bar sideways. */
  .utility-location {
    order: 3;
    width: 100%;
    margin-right: 0;
  }

  .season-context {
    padding-left: 0;
    border-left: 0;
  }

  /* Search takes its own full row under the location band rather than
   * competing with it for a 390px line, and the panel spans that row
   * instead of hanging off the right edge. Both controls carry the
   * 44px touch target: administration works on a phone. */
  .global-search {
    order: 4;
    width: 100%;
  }

  .global-search input[type=search] {
    flex: 1 1 auto;
    width: auto;
    min-width: 0;
    min-height: 44px;
  }

  .global-search button { min-height: 44px; }

  .search-results {
    right: 0;
    left: 0;
    width: auto;
  }

  .local-nav,
  .site-footer { padding-right: var(--space-lg); padding-left: var(--space-lg); }

  /* THE TAB ROW WRAPS HERE (BOB-333). Finances has five destinations and
   * they do not fit a 390px line: measured 2026-08-12 in the browser
   * lane, Fundraising's right edge landed at 401.31px against a 390px
   * viewport, so the last destination was off-screen at a
   * representative 390px phone width. Wrapping is what puts it back; the scroller above
   * is NOT removed, because a single label wider than the viewport is
   * the one case no wrap can rescue and `overflow-x` is its last-resort
   * containment.
   *
   * The two gaps are separate on purpose. The `gap` shorthand sets both
   * axes, so keeping the 24px tab rhythm would put 24px between the
   * wrapped rows as well and break the band into two floating strips.
   * The rows sit flush instead: each tab keeps its own 44px underline
   * lane, and the band reads as one block of tabs rather than as two. */
  .local-nav {
    flex-wrap: wrap;
    column-gap: var(--space-xl);
    row-gap: 0;
  }

  /* 44px on the short axis for EVERY pointer at this width. The base
   * tab is 43px and the 44px minimum lives under `pointer: coarse`
   * further down, which a 390px viewport driven by a mouse does not
   * match — and the bar this has to meet is stated in widths, not in
   * pointers. Desktop widths keep 43px. */
  .local-nav a { min-height: 44px; }

  .admin-workspace main {
    padding: var(--space-xl) var(--space-lg) 2.5rem;
  }

  .page-masthead {
    display: block;
    margin-right: calc(var(--space-lg) * -1);
    margin-left: calc(var(--space-lg) * -1);
    padding: var(--space-lg);
  }

  .page-masthead:first-child { margin-top: calc(var(--space-xl) * -1); }

  /* Two by two, so the primary action stays above the fold on a phone
   * instead of being pushed off by a four-high stack. */
  .workflow-actions {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-sm);
    margin-top: var(--space-lg);
  }

  .workflow-actions .button,
  .workflow-actions form,
  .workflow-actions button {
    width: 100%;
    white-space: normal;
  }

  /* A page with ONE action gets the whole row. The pair of columns
   * exists to keep a four-action masthead above the fold; applied to a
   * lone action it only wraps the label into a half-width box. */
  .workflow-actions > :only-child { grid-column: 1 / -1; }
}

/* Location, not action: the selected filter uses the navy/wash pair the
 * constitution assigns to current location. */
nav.state-filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin: var(--space-lg) 0;
}

nav.state-filters a {
  padding: var(--space-sm) var(--space-md);
  background: var(--warm-white);
  border: 1px solid var(--operational-slate);
  border-radius: 999px;
  color: var(--operational-slate);
  font-size: 12px;
  text-decoration: none;
}

nav.state-filters a[aria-current=page] {
  background: var(--active-blue-wash);
  border-color: var(--signal-blue);
  color: var(--nucleus-navy);
  font-weight: 700;
}

.filter-bar {
  margin: var(--space-lg) 0;
  padding: var(--space-md);
  background: var(--warm-white);
  border: 1px solid var(--structural-line);
  border-radius: var(--radius-surface);
}

.filter-bar nav.state-filters { margin: 0 0 var(--space-md); }

.filter-form {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: var(--space-sm);
  margin: 0;
}

.filter-form label { flex-basis: 100%; margin: 0; }
/* A caller-supplied filter control (filter-bar's `fields` slot) is a
   labelled field box, and it has to share the row the way the search
   input does. The audit trail's two selects previously lived in the
   page's own `form.filters`, whose `> div { flex: 1 1 12rem }` let them
   grow and shrink; that block died with the bespoke form (BOB-305), so
   the rule moves here with the fields rather than being lost with their
   old home. Its own label IS a flex item in this box, so the
   flex-basis:100% rule above does not reach it. */
.filter-form > div { flex: 1 1 12rem; display: flex; flex-direction: column; }
.filter-form > div label { flex-basis: auto; margin: 0 0 var(--space-xs); }
.filter-form > div select { margin: 0; width: 100%; }
.filter-form input[type=search] { flex: 1 1 16rem; margin: 0; }
.filter-form button { margin: 0; }
.clear-filter { display: inline-flex; align-items: center; min-width: 44px; min-height: 44px; }
.filter-summary { margin: var(--space-sm) 0 0; color: var(--operational-slate); }

.aside-links { margin-top: var(--space-xl); font-size: 12px; }
.aside-links a { display: inline-block; padding: var(--space-xs) 0; }

/* ------------------------------------------------------------------ */
/* Rows and records. */

ul.rows {
  list-style: none;
  margin: var(--space-lg) 0;
  padding: 0;
}

ul.rows li {
  padding: var(--space-md) var(--space-xs);
  border-bottom: 1px solid var(--structural-line);
  overflow-wrap: anywhere;
}

/* A QUEUE LIST ON THE BARE CANVAS GETS ITS OWN SURFACE (BOB-351).
 * `ul.rows` is hand-written at forty-odd call sites across twelve
 * namespaces, so containment is reached here rather than by editing
 * every view — the BOB-349 shape, where seventy-two call sites changed
 * with no view edited.
 *
 * SELECTED BY STRUCTURE, AND THE RULE IS THE CLAIM — not a census.
 * THE RULE: a `ul.rows` takes a surface when it is a direct child of
 * `<main>`, or of one or two `section`s under `<main>` that are not
 * themselves cards. Anything else — a list in a `div`, in a form, in a
 * `.card`, or one section deeper — matches nothing here and renders
 * exactly as it did before. The depths come from walking real renders
 * rather than from reasoning about the markup, and
 * `element-test-support/row-list-chains` is that walk in executable
 * form, so any page can be asked the question again instead of
 * trusting a number written here. What is deliberately NOT claimed is
 * that no list sits anywhere else: several do, and they are the ones
 * the rule declines to reach.
 *
 * BOTH HALVES OF THE SELECTOR CARRY NEGATIVE SPACE. `:not(.card)` is
 * load-bearing where the chain does match — the family shell writes
 * `main > section.card > ul.rows`, and without the exclusion a list
 * already inside a surface would be given a second one. The `section`
 * tag choice does the rest of the excluding: a list inside a `div` or
 * inside a form's fieldset never reaches `:not(.card)` at all. A list
 * nested deeper than this simply renders as it does today — the rule
 * fails safe, which a global rule with a list of exceptions would
 * not. */
main > ul.rows,
main > section:not(.card) > ul.rows,
main > section:not(.card) > section:not(.card) > ul.rows {
  padding: 0 var(--space-md);
  background: var(--warm-white);
  border: 1px solid var(--structural-line);
  border-radius: var(--radius-surface);
}

/* the last row's separator IS the container's bottom edge, and drawing
 * both puts a double line there */
main > ul.rows > li:last-child,
main > section:not(.card) > ul.rows > li:last-child,
main > section:not(.card) > section:not(.card) > ul.rows > li:last-child {
  border-bottom: 0;
}

ul.rows input.row-pick {
  width: 16px;
  height: 16px;
  min-height: 0;
  margin: 0 var(--space-sm) 0 0;
  accent-color: var(--signal-blue);
  vertical-align: -0.15em;
}

.row-selection { display: inline-flex; align-items: center; min-height: 36px; }
.row-selection input.row-pick {
  width: 16px;
  height: 16px;
  min-height: 0;
  padding: 0;
  margin: 0 var(--space-sm) 0 0;
  accent-color: var(--signal-blue);
}

.batch-workspace-table { margin: var(--space-lg) 0; }
.batch-workspace-table .workspace-table { margin: 0; }

ul.rows .meta {
  display: block;
  color: var(--operational-slate);
  font-size: 12px;
}

/* A row's metadata rule is more specific than .notice, so a span carrying
 * BOTH classes rendered slate on the assistance wash: 4.42:1 at 12px, under
 * the AA floor. Restated here rather than by darkening a token that a
 * hundred other rules depend on. */
ul.rows .meta.notice { color: var(--operational-ink); }

/* A skipped notification is a WARNING, not an aside: it renders on its own
 * line so a narrow viewport never leaves it trailing off a name, and it
 * carries the semantic attention colour because somebody has to act on it
 * (BOB-73). */
ul.rows .warn {
  display: block;
  color: var(--reconciliation-red);
  font-size: 12px;
  font-weight: 600;
}

/* A row that leads with a NAME and its state (BOB-308 slice 3, the statement
 * review's audience). The name is the row's subject, so it carries the row's
 * weight; the badge sits on the same line and wraps under it rather than
 * squeezing the name at 390px. `.who` already means this on li.entry — this
 * generalises it to any row, adding nothing new to the vocabulary. */
ul.rows .who {
  margin: 0;
  font-weight: 600;
  overflow-wrap: anywhere;
}

/* A previewed message body, rendered as the recipient would read it: the
 * source's own line breaks, wrapped to the reader's width. NOT a <pre> —
 * a pre would force a 390px viewport to scroll sideways to read a
 * statement, which is exactly what the preview exists to prevent. */
.statement-preview {
  margin: var(--space-sm) 0 0;
  padding: var(--space-md);
  background: var(--cool-paper);
  border: 1px solid var(--structural-line);
  border-radius: var(--radius-surface);
  color: var(--operational-ink);
  overflow-wrap: anywhere;
}

li.grant,
li.recipient,
ul.attachments li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-md);
}

li.grant .who { overflow-wrap: anywhere; }
li.grant form { margin: 0 0 0 auto; }
li.recipient .badge,
li.recipient .meta { margin-left: auto; }

li.entry .who { font-weight: 600; overflow-wrap: anywhere; }
li.entry details { margin-top: var(--space-xs); }
li.entry .summary { margin: var(--space-sm) 0 var(--space-xs); }

.entry-values del {
  background: color-mix(in srgb, var(--reconciliation-red) 8%, var(--warm-white));
  text-decoration-color: var(--reconciliation-red);
}

.entry-values ins {
  background: color-mix(in srgb, var(--verified-green) 8%, var(--warm-white));
  text-decoration: none;
}

ul.attachments {
  list-style: none;
  margin: var(--space-md) 0;
  padding: 0;
}

ul.attachments li {
  padding: var(--space-sm) var(--space-xs);
  border-bottom: 1px solid var(--structural-line);
}

ul.attachments .meta { color: var(--operational-slate); font-size: 12px; }
ul.attachments form { margin: 0 0 0 auto; }

/* One workspace cell holding SEVERAL facts of one kind — every live
 * invitation on one record, each with its own address and its own expiry
 * (BOB-305). It stacks rather than joining them into a sentence: one
 * trailing date beside a list of addresses reads as one expiry for all of
 * them, which is the wrong-end defect this cell replaced. It renders in
 * both workspace renderings, so it carries no assumptions about sitting
 * in a `td`. */
ul.record-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

ul.record-list li + li { margin-top: var(--space-sm); }
ul.record-list .meta {
  display: block;
  color: var(--operational-slate);
  font-size: 12px;
}

/* ------------------------------------------------------------------ */
/* Registers. Squarer joins than a card: continuous rows matter more than
 * card identity (DESIGN.md, Shapes). */

/* A REGISTER IS SEPARATE-BORDERED WITH ZERO SPACING, NEVER COLLAPSED.
 * `border-radius` does not apply to a table whose borders are
 * collapsed, and the combination this file used to carry —
 * `border-collapse: collapse` with a radius and `overflow: hidden` —
 * does not merely fail to round: the clip swallows the table's own
 * border, so the register rendered with no outer edge at all. Verified
 * in headless Chromium against the exact declarations (BOB-351 fix
 * round 1). `separate` with `border-spacing: 0` draws both, and costs
 * nothing here because the cells carry only a `border-bottom`, so
 * there is no adjacent pair to double.
 *
 * `design_test` PINS EVERY `border-collapse` IN THIS FILE — all three
 * — keyed by `[media-context selector value]`, the property being
 * invariant because the guard filters to `border-collapse` first. A
 * fourth declaration differing in any one of those three, at any
 * specificity and under any media query, is a red gate rather than a
 * silent restoration of the defect. A rule that merely restates a
 * tuple already pinned is
 * tolerated, because it re-declares the value the pin already fixes for
 * that selector and cannot change which declaration wins. It also
 * refuses `collapse` and `border-radius` on one selector, which is
 * always-inert CSS. What neither sees is the two arriving on two
 * different selectors that match one table; that is a rendering
 * question and belongs to the browser lane.
 *
 * NOTHING RENDERS `table.ledger` TODAY. No view in `src/` emits the
 * class — the rules below are live in the sheet and apply nowhere, so
 * this block is a shape kept correct rather than a second register in
 * service. It is repaired alongside the workspace table because a
 * half-repaired idiom is what let the same defect be written twice,
 * and because `design_test` holds both to the same declarations. Read
 * "both registers" here as both rule sets, not two live surfaces. */
table.ledger {
  width: 100%;
  margin: var(--space-lg) 0;
  border-collapse: separate;
  border-spacing: 0;
  background: var(--warm-white);
  border: 1px solid var(--structural-line);
  border-radius: var(--radius-surface);
  overflow: hidden;
}

table.ledger th {
  padding: var(--space-sm) var(--space-md);
  background: var(--cool-paper);
  border-bottom: 1px solid var(--structural-line);
  color: var(--nucleus-navy);
  font-family: var(--type-label-family);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  letter-spacing: var(--type-label-tracking);
  text-align: left;
  text-transform: uppercase;
}

table.ledger td {
  padding: var(--space-md);
  border-bottom: 1px solid var(--structural-line);
  vertical-align: middle;
}

table.ledger tr:last-child td { border-bottom: 0; }
table.ledger th.amount,
table.ledger td.amount { text-align: right; }

/* A workspace table is a REGISTER IN A CONTAINER, exactly as the ledger
 * above it is: the border and the rounded corners are what make the
 * rows read as one surface instead of a rule drawn around text — which
 * is why this takes the ledger's `separate` / zero-spacing borders too,
 * and why the comment there explains what a collapsed table does to
 * both. What it deliberately does NOT take from the prototype is that
 * prototype's
 * `.table-shell` — a `max-height: 590px; overflow: auto` box under a
 * sticky header. Production answers the same unbounded-list question
 * with `ui/pagination` and a bounded record count, and a scroll box on
 * top of a pager is two answers to one question: it would put the pager
 * outside the reader's view (BOB-351 decision 2).
 *
 * The header keeps its `--cool-paper` fill and its navy label role. The
 * prototype's dark `#475569` thead belongs to that prototype's
 * SUPERSEDED style-guide layer; its canonical Signal layer restyles
 * `thead`, `th` and `td` not at all (BOB-351 decision 1). */
.workspace-table table {
  width: 100%;
  margin: var(--space-lg) 0;
  border-collapse: separate;
  border-spacing: 0;
  background: var(--warm-white);
  border: 1px solid var(--structural-line);
  border-radius: var(--radius-surface);
  overflow: hidden;
}

.workspace-table th,
.workspace-table td { padding: var(--space-md) var(--space-lg); border-bottom: 1px solid var(--structural-line); text-align: left; overflow-wrap: anywhere; }
/* the header band is tighter than a body row, which is the register
 * density `table.ledger` already uses. This rule follows the shared one
 * above on purpose: same specificity, so source order decides. */
.workspace-table th { padding: var(--space-sm) var(--space-lg); background: var(--cool-paper); color: var(--nucleus-navy); font-family: var(--type-label-family); font-size: var(--type-label-size); text-transform: uppercase; letter-spacing: var(--type-label-tracking); }
.workspace-table tr:last-child td { border-bottom: 0; }
.workspace-table .actions { text-align: right; }
/* A MONEY COLUMN IS RIGHT-ALIGNED, HEADER INCLUDED. `.amount` gives a
 * cell the tabular value family wherever it appears, but the only
 * right-alignment rules for it were scoped to `table.ledger` — so every
 * `.amount` cell in a workspace table sat left-aligned under the
 * `.workspace-table th, td` rule above, and a treasurer comparing six
 * balances had to read the digits rather than the column edge. The
 * header takes it too, or the label floats away from its own figures. */
.workspace-table th.amount,
.workspace-table td.amount { text-align: right; }
.phone-record-summaries { display: none; }

.pagination { display: flex; justify-content: flex-end; align-items: center; gap: var(--space-md); margin: var(--space-md) 0; }
.pagination a,
.pagination-disabled { display: inline-flex; align-items: center; justify-content: center; min-width: 44px; min-height: 44px; }
.pagination-disabled { color: var(--operational-slate); }

@media (max-width: 40rem) {
  .workspace-table { display: none; }
  .batch-workspace-table .workspace-table { display: block; }
  .batch-workspace-table table,
  .batch-workspace-table tbody,
  .batch-workspace-table tr,
  .batch-workspace-table td { display: block; width: 100%; min-width: 0; }
  .batch-workspace-table table { margin: 0; border: 0; background: transparent; }
  .batch-workspace-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; }
  .batch-workspace-table tr { margin-bottom: var(--space-md); padding: var(--space-md); background: var(--warm-white); border: 1px solid var(--structural-line); border-radius: var(--radius-surface); }
  .batch-workspace-table tr:last-child { margin-bottom: 0; }
  .batch-workspace-table .workspace-table td { display: grid; grid-template-columns: minmax(6rem, auto) minmax(0, 1fr); gap: var(--space-lg); padding: var(--space-sm) 0; text-align: right; }
  .batch-workspace-table .workspace-table td[data-label]::before { content: attr(data-label); color: var(--nucleus-navy); font-family: var(--type-label-family); font-size: var(--type-label-size); font-weight: var(--type-label-weight); letter-spacing: var(--type-label-tracking); text-align: left; text-transform: uppercase; }
  .batch-workspace-table .workspace-table td:first-child { padding-top: 0; font-weight: 700; }
  .batch-workspace-table .workspace-table td:last-child { padding-bottom: 0; border-bottom: 0; }
  .batch-workspace-table .workspace-table .actions { text-align: right; }
  .batch-workspace-table .row-selection { justify-self: end; min-height: 44px; text-align: left; }
  .phone-record-summaries { display: grid; gap: var(--space-md); margin: var(--space-lg) 0; }
  .record-summary { padding: var(--space-md); background: var(--warm-white); border: 1px solid var(--structural-line); border-radius: var(--radius-surface); }
  .record-summary h3 { margin: 0 0 var(--space-md); overflow-wrap: anywhere; }
  .record-summary dl { display: grid; grid-template-columns: minmax(5rem, auto) 1fr; gap: var(--space-sm) var(--space-lg); margin: 0; }
  .record-summary dt { color: var(--nucleus-navy); font-family: var(--type-label-family); font-size: var(--type-label-size); font-weight: var(--type-label-weight); letter-spacing: var(--type-label-tracking); text-transform: uppercase; }
  .record-summary dd { margin: 0; text-align: right; overflow-wrap: anywhere; }
  .filter-form > input[type=search], .filter-form > button, .filter-form > .clear-filter { flex-basis: 100%; width: 100%; }
  .clear-filter { justify-content: center; min-height: 44px; }
}

/* ------------------------------------------------------------------ */
/* Composed email preview. */

.email-preview {
  margin: var(--space-lg) 0;
  padding: 0;
  background: var(--warm-white);
  border: 1px solid var(--structural-line);
  border-radius: var(--radius-surface);
}

.email-preview .headers {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-xs) var(--space-md);
  margin: 0;
  padding: var(--space-md) var(--space-lg);
  border-bottom: 1px solid var(--structural-line);
  font-size: 12px;
}

.email-preview .headers dt {
  color: var(--nucleus-navy);
  font-family: var(--type-label-family);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  letter-spacing: var(--type-label-tracking);
  text-transform: uppercase;
}

.email-preview .headers dd { margin: 0; overflow-wrap: anywhere; }

.email-preview .email-body {
  margin: 0;
  padding: var(--space-md) var(--space-lg);
  white-space: normal;
  overflow-wrap: anywhere;
}

.email-body h1,
.email-body h2,
.email-body h3 {
  border: 0;
  padding: 0;
  margin: var(--space-md) 0 var(--space-xs);
  font-family: var(--type-title-family);
  letter-spacing: normal;
}

.email-body h1 { font-size: 20px; font-weight: var(--type-title-weight); }
.email-body h2 { font-size: 17px; font-weight: var(--type-title-weight); }
.email-body h3 { font-size: 15px; font-weight: var(--type-title-weight); }
.email-body p { margin: var(--space-sm) 0; }
.email-body ul,
.email-body ol { margin: var(--space-sm) 0; padding-left: var(--space-xl); }

.email-preview .email-attachments {
  margin: 0;
  padding: var(--space-sm) var(--space-lg);
  border-top: 1px solid var(--structural-line);
  font-size: 12px;
}

.email-preview .email-attachments li { padding: 2px 0; }

/* ------------------------------------------------------------------ */
/* Compact devices.
 *
 * A coarse pointer restores the 44px touch target. This is DESIGN.md's own
 * rule ("production touch targets expand appropriately on compact
 * devices"), not a deviation from it.
 *
 * What grows is the thing a thumb LANDS on, which is not always the element
 * that draws the control: a checkbox stays 16px — a 44px one looks broken —
 * and its row and label grow around it instead. Each rule below names the
 * exact selector it has to beat, because the generic `input` rule loses the
 * cascade to `.choice input[type=checkbox]` however late it is written.
 */

@media (pointer: coarse) {
  button,
  .button,
  button.primary,
  input,
  select,
  input[type=file] { min-height: 44px; }

  /* A toolbar button is square, so the WIDTH has to grow too — raising
   * only the height leaves a 36x44 target. */
  .composer-toolbar button { min-width: 44px; }

  /* These need their own selectors, not the bare `input` rule above:
   * `.choice input[type=checkbox]` and `ul.rows input.row-pick` are more
   * specific, so they win the cascade and the box stays 16px however
   * late the coarse rule is written. The box stays 16px on purpose —
   * a 44px checkbox looks broken — and the LABEL carries the target
   * instead.
   *
   * It used to say the ROW carried it, which BOB-349 measured and found
   * false: the row's padding is not clickable, so the real target was
   * the label's 24px however tall the row looked. The label reaches the
   * product's 44px comfort target below, and the row's own padding drops
   * to match so the row does not grow by the difference.
   *
   * This is NOT BOB-275, which is the row-pick checkbox: that control
   * has no `<label>` at all, so nothing can grow around it without the
   * markup change that issue owns. A choice has one. */
  .choice {
    padding: var(--space-xs);
    align-items: center;
  }

  .choice input[type=checkbox],
  .choice input[type=radio] { margin: 0; }

  /* Named explicitly because it is more specific than the coarse
   * `.choice` above, and because the base rule at the top of this file
   * gives this surface `var(--space-xs)` — which under a 44px label would
   * be a 52px row rather than the 48px it has always had.
   *
   * (This comment used to say the base rule "was leaving the densest
   * checkbox surface at 32px". True while the label was 24px; stale the
   * moment the label became 44px, in the very commit that changed it.
   * Flagged by the exit round.)
   *
   * The figure is 2px rather than a token, and it is arithmetic: the
   * label carries the 44px target, so this padding is only what keeps
   * the ROW at the 48px it has always been. Raising the target without
   * lowering this took each row to 68px — measured, not reasoned — which
   * on a 16rem scroll area is five visible choices dropping to under
   * four. That surface is the charge form's participant and family
   * pickers and the deposit batch's payment list, which is where a
   * mis-tap costs the most, so growing the target must not cost the
   * overview that makes the right row easy to find. */
  .picker-scroll .choice { padding: 2px 0; }

  /* The roster row-pick checkbox carries no <label> — the row uses
   * aria-label — so nothing can grow around it without a markup change.
   * The box itself grows to 24px instead, which is WCAG 2.2 Target Size
   * (Minimum). The 44px comfort target needs that markup, and BOB-275
   * owns it; claiming 44px here would claim a fix that is not made. */
  ul.rows input.row-pick { width: 24px; height: 24px; }
  .row-selection { min-height: 44px; }
  .row-selection input.row-pick { width: 24px; height: 24px; min-height: 0; }

  /* 44px, and a CHILD combinator (BOB-349).
   *
   * The height is the change that matters: this was 24px, so the actual
   * activation area was 24px however tall the row looked. Measured at
   * `pointer: coarse` rather than read — target 24px → 44px, row 67px →
   * 71px.
   *
   * The combinator matters because the descendant form reached the
   * labels of field groups nested in `.choice-detail`, which are not
   * grid items and did take the wrong geometry.
   *
   * `flex` rather than `inline-flex` is a NO-OP here, and is written
   * this way only so the declaration says what it means. An earlier
   * comment claimed `inline-flex` had been shrinking the label to the
   * width of its words: false, and measured false — `.choice` is a grid
   * container, grid items blockify, so the computed display was already
   * `flex` and the label already filled its cell (654px of a 686px row).
   *
   * The anchors below keep `inline-flex` for the reason their own
   * comment gives; a choice label is not running prose. */
  .choice > label { min-height: 44px; display: flex; align-items: center; }

  /* Anchors keep inline-flex: they already reached 44px that way, and
   * `flex` made the ones inside running prose full-width blocks —
   * `.aside-links` went from a 44px line to 105px with its connecting
   * text stranded. */
  header.site a,
  .aside-links a,
  nav.state-filters a,
  .utility-location a,
  .site-footer a {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }

  /* `display` is deliberately NOT set on these two. `.mobile-brand`
   * and `.areas-disclosure` are hidden while the rail is on screen, and
   * a touchscreen laptop is a coarse pointer at desktop width — giving
   * them a display here would put a second brand and an "All areas"
   * button beside the rail they replace. The phone media query is what
   * reveals them; this only makes them comfortable once revealed. */
  .shell-brand,
  .mobile-brand { min-height: 44px; }

  /* The shell's own navigation. The rail rows are full-width, so only
   * their height moves — and the local tab row's own band grows with
   * its tabs, or the underline would sit above the target's edge. */
  .primary-nav a,
  .mobile-areas a,
  .local-nav a { min-height: 44px; }

  .local-nav { min-height: 45px; }

  /* A tab is only as wide as its word, and the shortest one measured
   * 25.6 x 44 in the browser — tall enough and far too narrow. The tab
   * takes horizontal padding and a floor, and the row's gap comes down
   * by the same amount so the rhythm between tabs is unchanged.
   *
   * `column-gap` rather than the `gap` shorthand, and that is
   * load-bearing rather than tidiness: this block carries no width
   * condition, so it lands AFTER the phone block above and the
   * shorthand would reset that block's `row-gap: 0` back to 8px —
   * prising the wrapped rows apart on exactly the pointer the tabs
   * are widest on. */
  .local-nav { column-gap: var(--space-sm); }

  .local-nav a {
    justify-content: center;
    min-width: 44px;
    padding: 0 var(--space-sm);
  }

  /* Every disclosure control, not only the two that carry a class. The
   * display type is deliberately NOT touched: `summary` is a `list-item`,
   * and both `flex` and `inline-flex` remove the triangle — which on the
   * unclassed <details> in the money views is the only affordance they
   * have. Padding buys the target instead. */
  summary {
    min-height: 44px;
    padding-top: var(--space-md);
    padding-bottom: var(--space-md);
  }
}

/* ------------------------------------------------------------------ */
/* The family shell (BOB-270).
 *
 * Admin and family share every token above this line — colours, type roles,
 * cards, rows, badges, receipts — and deliberately do not share a shell
 * (the frontend design constitution, "Identity and shell"). Nothing here is
 * a new visual language: the topbar takes the same Signal Chrome band and
 * 40rem measure as `header.site`, the destinations take DESIGN.md's
 * two-pixel active underline, and the switcher takes the Active Blue Wash
 * the constitution assigns to current location.
 *
 * Every piece carries `.no-print` in the markup, which `views/print-styles`
 * already hides — so the season statement stays a paper artifact with no
 * chrome on it, and no rule here has to reach a page it does not own.
 */

/* WCAG 2.4.1: the chrome above the content is three blocks on every family
 * page now, so there is a way past it. Visible only on focus, and it moves
 * focus rather than only scrolling — each family page's <main> is
 * `id="main" tabindex="-1"`. */
.skip-link {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.skip-link:focus {
  width: auto;
  height: auto;
  overflow: visible;
  clip-path: none;
  left: var(--space-sm);
  top: var(--space-sm);
  z-index: 10;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: var(--space-sm) var(--space-md);
  background: var(--warm-white);
  border: 1px solid var(--signal-blue);
  border-radius: var(--radius-control);
  color: var(--signal-blue);
  font-weight: 600;
  text-decoration: none;
}

main:focus { outline: none; }

.family-topbar {
  background: var(--signal-chrome);
  border-bottom: 1px solid var(--structural-line);
}

.family-topbar .topbar-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-md);
  max-width: 40rem;
  margin: 0 auto;
  padding: var(--space-sm) var(--space-lg);
}

.family-topbar .brand {
  padding: var(--space-sm) 0;
  color: var(--nucleus-navy);
  font-weight: 700;
  letter-spacing: 0.02em;
  text-decoration: none;
}

/* The organization identifies the page; the account is metadata beside it.
 * `.org-name` keeps its own rule — including the accent bar that is the ADR
 * 0051 D4 consumer of var(--brand) — and only loses the page margins it was
 * given for use inside `main`. */
.family-topbar .org-name {
  margin: 0;
  margin-right: auto;
  overflow-wrap: anywhere;
}

.family-topbar .who {
  color: var(--operational-slate);
  font-size: 12px;
  overflow-wrap: anywhere;
}

.family-topbar form { margin: 0; }

.family-topbar button {
  margin: 0;
  background: var(--warm-white);
  border-color: var(--operational-slate);
  color: var(--operational-ink);
}

.family-topbar button:hover {
  background: var(--cool-paper);
  border-color: var(--operational-slate);
  color: var(--operational-ink);
}

/* Home and Finances — the family surface's whole destination set. Labelled,
 * never icons; horizontally scannable at phone width, and a cut-off tab is
 * the cue that the row continues. */
.family-nav {
  background: var(--warm-white);
  border-bottom: 1px solid var(--structural-line);
}

.family-nav ul,
.family-switcher ul {
  display: flex;
  gap: var(--space-md);
  max-width: 40rem;
  margin: 0 auto;
  padding: 0 var(--space-lg);
  overflow-x: auto;
  list-style: none;
}

.family-nav a {
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  padding: var(--space-sm) 0;
  border-bottom: 2px solid transparent;
  color: var(--operational-slate);
  text-decoration: none;
  white-space: nowrap;
}

/* Weight and an underline carry the state, not colour alone. */
.family-nav a[aria-current] {
  border-bottom-color: var(--signal-blue);
  color: var(--nucleus-navy);
  font-weight: 700;
}

.family-switcher {
  background: var(--cool-paper);
  border-bottom: 1px solid var(--structural-line);
}

.family-switcher ul {
  gap: var(--space-sm);
  padding-top: var(--space-sm);
  padding-bottom: var(--space-sm);
}

.family-switcher li { flex: 0 0 auto; }

.family-switcher a {
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  padding: var(--space-xs) var(--space-md);
  background: var(--warm-white);
  border: 1px solid var(--operational-slate);
  border-radius: 999px;
  color: var(--operational-slate);
  font-size: 12px;
  text-decoration: none;
  white-space: nowrap;
}

.family-switcher a[aria-current] {
  background: var(--active-blue-wash);
  border-color: var(--signal-blue);
  color: var(--nucleus-navy);
  font-weight: 700;
}

.family-switcher .hint {
  max-width: 40rem;
  margin: 0 auto var(--space-sm);
  padding: 0 var(--space-lg);
}

.family-footer {
  border-top: 1px solid var(--structural-line);
  background: var(--signal-chrome);
}

.family-footer p {
  max-width: 40rem;
  margin: 0 auto;
  padding: var(--space-md) var(--space-lg);
  color: var(--operational-slate);
  font-family: var(--type-label-family);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  letter-spacing: var(--type-label-tracking);
  text-transform: uppercase;
}

/* The family return surface's hero. The next thing on the calendar is a
 * NAME, not a measure, so it takes the title role rather than the mono
 * value role the metric strip uses. */
.family-next .next-event {
  margin: var(--space-xs) 0 var(--space-sm);
  font-family: var(--type-title-family);
  font-size: var(--type-title-size);
  font-weight: var(--type-title-weight);
  line-height: var(--type-title-leading);
}

.family-next .next-event .meta {
  display: block;
  color: var(--operational-slate);
  font-family: var(--type-body-family);
  font-size: 12px;
  font-weight: var(--type-body-weight);
}

.family-next .hint { margin-top: var(--space-md); }

/* The season statement's own letterhead. The topbar names where the reader
 * is standing and disappears on paper with the rest of the chrome; a
 * printed statement still has to say whose it is. */
.letterhead {
  margin: var(--space-lg) 0 calc(var(--space-md) * -1);
  color: var(--operational-ink);
  font-family: var(--type-label-family);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  line-height: var(--type-label-leading);
  letter-spacing: var(--type-label-tracking);
  text-transform: uppercase;
}

/* A coarse pointer restores the 44px target on the shell's own controls,
 * for the reason the shared block above gives. Written here rather than
 * added to that block so the family shell's rules stay in one place.
 *
 * The forward look's own links are raised too. They are `ul.rows` links,
 * which product-wide are ordinary 22px text rows — a comfort-target gap
 * BOB-275 owns and this slice does not close. What it does close is its
 * OWN surface: this is the phone-first return page, and its route into the
 * next event is its primary action, not a row in a treasurer's register. */
@media (pointer: coarse) {
  .family-topbar .brand,
  .family-nav a,
  .family-switcher a,
  .family-next .next-event a,
  .family-forward ul.rows a {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }
}

/* A register cannot scroll sideways on a phone: each row becomes a record
 * summary carrying the same facts, labelled from `data-label`
 * (constitution — Record workspace). */
@media (max-width: 30rem) {
  table.ledger,
  table.ledger tbody,
  table.ledger tr,
  table.ledger td { display: block; }

  table.ledger thead { display: none; }

  table.ledger { border: 0; background: transparent; }

  table.ledger tr {
    margin-bottom: var(--space-md);
    padding: var(--space-md);
    background: var(--warm-white);
    border: 1px solid var(--structural-line);
    border-radius: var(--radius-surface);
  }

  table.ledger td {
    display: flex;
    justify-content: space-between;
    gap: var(--space-lg);
    padding: var(--space-xs) 0;
    border-bottom: 0;
    text-align: right;
  }

  table.ledger td::before {
    content: attr(data-label);
    color: var(--nucleus-navy);
    font-family: var(--type-label-family);
    font-size: var(--type-label-size);
    font-weight: var(--type-label-weight);
    letter-spacing: var(--type-label-tracking);
    text-align: left;
    text-transform: uppercase;
  }

  table.ledger td.amount { text-align: right; }
}

/* ------------------------------------------------------------------ */
/* Print — the ADMIN SHELL's own, and nothing else (BOB-269).
 *
 * ADR 0051 D1 kept `@media print` out of this file in BOB-268 for one
 * reason: the prototype's print reset targets `.admin-sidebar`,
 * `.utility-bar` and masthead geometry, chrome that did not exist yet,
 * "so those slices own their shells' print behaviour". It exists now,
 * so its rules land with it.
 *
 * Every selector here is chrome the admin shell puts around a page —
 * `views/layout`'s own markup, plus the masthead band `views/masthead`
 * renders at the top of every admin page's body. Nothing here reaches a
 * page's CONTENT: the artifact pages that ARE printable keep including
 * `views/print-styles` exactly as before, and making a general page
 * printable is still a product decision nobody has taken.
 */

@media print {
  .admin-sidebar,
  .utility-bar,
  .local-nav,
  .site-footer,
  .skip-link { display: none !important; }

  .admin-shell { display: block; }
  .admin-workspace { display: block; min-height: 0; }
  .admin-workspace main { max-width: none; padding: 0; }

  /* On paper the masthead is the document's HEADING, not a band. It
   * carries the page's only <h1> (BOB-280 moved every page title into
   * it), so hiding the band prints every administrative page untitled —
   * the fix-range defect three reviewers found. What goes is the band
   * GEOMETRY: a bleed into a margin that does not exist on a sheet, and
   * a fill most browsers drop and every printer would spend ink on. */
  .page-masthead { display: block; }

  .page-masthead,
  .page-masthead:first-child {
    margin: 0 0 var(--space-lg);
    padding: 0 0 var(--space-md);
    background: none;
  }

  /* The one part of the band that means nothing on paper: a sheet
   * cannot be pressed. */
  .workflow-actions { display: none; }
}

/* Schedule area (BOB-293) */
nav.crumbs,
.record-context { overflow-wrap: anywhere; }

/* ------------------------------------------------------------------ */
/* The Return surface (BOB-271, ADR backend/0052) — the constitution's
 * archetype for someone resuming work: one Task Spotlight, a terse
 * evidence strip, then a Triage Stream.
 *
 * The Spotlight is the ONE feature surface in the product. DESIGN.md's
 * flat-by-default rule says an ordinary card does not claim its own
 * elevation; this is not an ordinary card. It is the page's hero, it
 * appears once, and what raises it is a tinted panel and the feature
 * radius rather than a shadow — the same treatment `.deposit-confirm`
 * already uses for the other surface that states a consequence.
 *
 * Its action is deliberately NOT `.primary`: the Overview's band holds
 * the page's one loudest control, and emphasis is a comparison. */

.task-spotlight {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--space-xl);
  margin: 0 0 var(--space-xl);
  padding: var(--space-xl);
  background: var(--assistance-blue-wash);
  border: 1px solid var(--structural-line);
  border-radius: var(--radius-feature);
}

.spotlight-copy { flex: 3 1 22rem; min-width: 0; overflow-wrap: anywhere; }
.spotlight-copy h2 {
  margin: var(--space-xs) 0 var(--space-sm);
  padding: 0;
  border: 0;
  font-family: var(--type-headline-family);
  font-size: var(--type-headline-size);
  font-weight: var(--type-headline-weight);
  line-height: var(--type-headline-leading);
  letter-spacing: var(--type-headline-tracking);
}

.spotlight-copy p { max-width: 62ch; margin: 0 0 var(--space-sm); }
.spotlight-exemplar { color: var(--operational-ink); }

/* The suggest-then-approve pledge, in two parts: what Nucleus narrows,
 * and what stays with the person. The second half is the emphasized
 * one, because it is the half a reader needs to believe. */
.spotlight-assistance {
  margin: var(--space-md) 0 0;
  padding-top: var(--space-md);
  border-top: 1px solid var(--structural-line);
  color: var(--operational-slate);
}

.spotlight-assistance strong { color: var(--operational-ink); }

.spotlight-action {
  flex: 1 1 13rem;
  min-width: 0;
  padding: var(--space-lg);
  background: var(--warm-white);
  border: 1px solid var(--structural-line);
  border-radius: var(--radius-surface);
}

.spotlight-action p { margin: 0 0 var(--space-xs); }
.spotlight-action .button { display: block; margin-top: var(--space-md); text-align: center; }

/* A cleared or unavailable Spotlight has no action column and no wash:
 * a calm state must not look like work. */
.spotlight-clear { background: var(--cool-paper); }

/* The evidence strip: exact figures, no links. Every kind here is also
 * the Spotlight or a stream row, each offering one control to the same
 * place, so a linked strip would offer each one twice. */
.evidence-strip {
  display: grid;
  gap: var(--space-md);
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  margin: 0 0 var(--space-xl);
}

.evidence-item {
  padding: var(--space-md);
  background: var(--warm-white);
  border: 1px solid var(--structural-line);
  border-radius: var(--radius-surface);
  overflow-wrap: anywhere;
}

.evidence-item p { margin: 0; }
.evidence-item .tile-figure { margin: var(--space-xs) 0; }

.triage-stream { margin: 0 0 var(--space-xl); }
.stream-heading h2 {
  margin: var(--space-xs) 0 var(--space-md);
  padding: 0;
  border: 0;
  font-family: var(--type-title-family);
  font-size: var(--type-title-size);
  font-weight: var(--type-title-weight);
  line-height: var(--type-title-leading);
}

/* A triage row is a dense, comparable line: state, what it is with its
 * figure, and one verb. The row is not itself a link — the verb is —
 * so a reader dragging to select the figure does not navigate. */
.triage-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm) var(--space-md);
  padding: var(--space-md) var(--space-xs);
  border-bottom: 1px solid var(--structural-line);
}

.triage-row .trust-state { margin: 0; flex: 0 0 auto; }
.triage-copy { flex: 1 1 16rem; min-width: 0; overflow-wrap: anywhere; }
.triage-copy .meta {
  display: block;
  color: var(--operational-slate);
  font-size: 12px;
}

/* The stream's verb is a TAP TARGET, and it measured 27px (BOB-334):
 * 4px of block padding around a 13px/1.45 line. The bar is 44px on the
 * short axis at 390x844 stated plainly — not only under
 * `pointer: coarse`, which is where this stylesheet usually puts touch
 * enlargement and which a 390px viewport driven by a mouse does not
 * match.
 *
 * WHAT GROWS IS THE HIT AREA, NOT THE SPACE THIS VERB RESERVES. The
 * padding buys the target and a negative block margin hands the same
 * distance straight back, so the link's margin box — the box a flex
 * line takes — keeps the height it always had. That is what keeps the
 * Spotlight the page's hero: six rows grown into six buttons would
 * compete with it, and the issue refuses `.button` promotion for
 * exactly that reason.
 *
 * SAY WHAT THAT DOES NOT COVER, because this rule is the binding owner
 * of the mechanism and two other documents send their readers here for
 * it. It is a claim about the VERB's box, not about the row's height.
 * At phone width the row IS taller after this fix: the verb wraps onto
 * its own flex line there and the line gap has to open for the target
 * and its focus ring, which the `max-width: 40rem` block below states
 * and owns. Desktop rows are untouched.
 *
 * THE 44 IS DECLARED, AND THE GIVE-BACK IS THE CALC — not the other way
 * round, and that order was measured rather than chosen. Padding
 * computed as (44 - 13 x 1.45) / 2 produced a 43.969px target, because
 * Chrome resolves lengths onto a 1/64px grid and the two roundings both
 * went down. `min-height: 44px` cannot round short of itself. What the
 * calc then returns is the difference between that target and the
 * 26.85px box the verb already reserved — (13 x 1.45 + 4 x 2 - 44) / 2
 * = -8.575px on each side — so that outer box is unchanged. Written
 * over the type tokens rather than as a literal so a change to either
 * carries the give-back with it.
 *
 * The link becomes its own flex line so the label stays CENTRED in the
 * taller box. Without that the text sits at the top of it and, once the
 * negative margin pulls the box up, renders 8.575px above where it used
 * to — a hit-area fix that silently moves the type. Centred, the label's
 * midline lands exactly where it did.
 *
 * `margin-block` and `padding-block`, never the shorthands: `margin`
 * would take `margin-left: auto` with it, and that auto is the whole of
 * the verb's right-hand alignment at desktop width. */
.triage-row > a {
  display: flex;
  align-items: center;
  margin-left: auto;
  min-height: 44px;
  margin-block: calc((var(--type-body-size) * var(--type-body-leading)
                      + var(--space-xs) * 2 - 44px) / 2);
  padding-block: var(--space-xs);
  padding-inline: 0;
  white-space: nowrap;
}

/* At phone width the Spotlight stops being two columns and the action
 * card sits under the copy it is about — the same order a screen reader
 * already hears, so nothing moves relative to anything else. */
@media (max-width: 40rem) {
  .task-spotlight { padding: var(--space-lg); gap: var(--space-lg); }
  .spotlight-copy,
  .spotlight-action { flex: 1 1 100%; }
  .triage-row > a { margin-left: 0; }

  /* The verb wraps onto its own flex line here, so the line gap is the
   * only thing between its hit area and the copy above it. That area
   * now reaches 8.575px past the link's outer box, and the focus ring
   * — 2px wide at 2px offset — another 4px past that, so an 8px gap
   * would leave the target and its ring overlapping the row's own copy.
   * 16px clears both with room, and it is the only place in this fix
   * where the stream's density moves at all. */
  .triage-row { row-gap: var(--space-lg); }
}

