/* cuento — p10.2 CSS foundation.
 *
 * Hand-written, no framework/bundler/CDN (rule 12). Strict-CSP-safe: styles live
 * ONLY in this file (style-src 'self'), never in inline style="" or <style>
 * (D9). Three layers: (1) TOKENS as CSS custom properties, (2) RESET, (3)
 * COMPONENT primitives.
 *
 * Light/dark: each theme token is a light-dark(light, dark) pair. The active
 * side is chosen by `color-scheme`, which the shell sets per data-theme on
 * <html> (server-side, no flash — see base.tmpl):
 *   data-theme="auto"  -> color-scheme: light dark  (follows the OS)
 *   data-theme="light" -> color-scheme: light
 *   data-theme="dark"  -> color-scheme: dark
 * No media query and no JS are needed: light-dark() + color-scheme do it in CSS.
 */

/* ------------------------------------------------------------------ *
 * 1. Tokens
 * ------------------------------------------------------------------ */
:root {
  /* color-scheme drives which side of every light-dark() pair is used AND styles
     native form controls / scrollbars to match. Overridden per data-theme below. */
  color-scheme: light dark;

  /* Brand palette ("Open Ledger & Star"): Primary Blue #0072CE, Accent Gold
     #FFB81C, White, deep-navy dark surface (~#0A1A2F). Two fixed anchors —
     --brand-blue and --brand-gold hold the light-mode brand hues for the logo
     and accents; on dark surfaces they lighten for legibility (gold-on-navy) and
     AA contrast (a mid #0072CE is too dark for link text on #0A1A2F, so the dark
     side uses a lightened blue). Gold is used ONLY for non-text accents (the
     star, the active-nav bar) — #FFB81C on white is ~1.5:1, unusable as text. */
  --brand-blue:        light-dark(#0072ce, #4da3ff);
  --brand-gold:        light-dark(#ffb81c, #ffca4d);

  /* Palette — each is light-dark(<light value>, <dark value>). */
  --color-bg:          light-dark(#f4f7fb, #0a1a2f);
  --color-surface:     light-dark(#ffffff, #0f2440);
  --color-surface-alt: light-dark(#e9f0f9, #163257);
  --color-text:        light-dark(#0d1b2a, #eaf1fb);
  --color-text-muted:  light-dark(#4a5a6e, #9db4d1);
  --color-border:      light-dark(#cfdcec, #24456f);
  --color-primary:     light-dark(#0072ce, #4da3ff);
  --color-primary-hover: light-dark(#005ba6, #74baff);
  --color-primary-active: light-dark(#00477f, #9acbff);
  --color-primary-text: light-dark(#ffffff, #071726);
  --color-accent:      light-dark(#ffb81c, #ffca4d);
  --color-danger:      light-dark(#b3261e, #ff8079);
  --color-danger-bg:   light-dark(#fde8e6, #3a1c1a);
  --color-success:     light-dark(#1f7a3d, #6fdd8b);
  --color-success-bg:  light-dark(#e6f5ec, #0f2f1c);
  --color-warning:     light-dark(#8a5a00, #ffca4d);
  --color-warning-bg:  light-dark(#fdf3d9, #3a2f0f);
  --color-focus:       light-dark(#0072ce, #7ab8ff);

  /* Spacing scale (rem). */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-6: 1.5rem;
  --space-8: 2rem;

  /* Radii, borders, typography. */
  --radius: 6px;
  --border: 1px solid var(--color-border);
  --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-size: 16px;
  --line-height: 1.5;
  --shadow: 0 1px 3px rgb(0 0 0 / 12%);
}

/* Explicit theme overrides. data-theme is set server-side on <html> from the
   theme cookie (no flash). "auto" keeps the :root default (light dark). */
html[data-theme="light"] { color-scheme: light; }
html[data-theme="dark"]  { color-scheme: dark; }

/* ------------------------------------------------------------------ *
 * 2. Reset
 * ------------------------------------------------------------------ */
*, *::before, *::after { box-sizing: border-box; }

html { font-size: var(--font-size); }

body {
  margin: 0;
  font-family: var(--font-sans);
  line-height: var(--line-height);
  color: var(--color-text);
  background: var(--color-bg);
}

h1, h2, h3, p, ul, figure { margin: 0 0 var(--space-4); }
ul { padding: 0; }

a { color: var(--color-primary); }

img, svg { max-width: 100%; height: auto; }

button, input, select, textarea { font: inherit; color: inherit; }

/* Visible, consistent focus for keyboard users (accessibility). */
:where(a, button, input, select, textarea):focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* Skip link: off-screen until focused, then anchored top-left. */
.skip-link {
  position: absolute;
  left: -999px;
  top: 0;
  padding: var(--space-2) var(--space-4);
  background: var(--color-surface);
  border: var(--border);
  border-radius: var(--radius);
}
.skip-link:focus { left: var(--space-2); z-index: 10; }

/* ------------------------------------------------------------------ *
 * 3. Component primitives
 * ------------------------------------------------------------------ */

/* App shell layout. */
.app-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-6);
  background: var(--color-surface);
  border-bottom: var(--border);
  box-shadow: var(--shadow);
}
.app-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-weight: 700;
  text-decoration: none;
  color: var(--color-text);
}
.app-brand-name { letter-spacing: 0.01em; }

/* Brand mark (inline SVG). Colors come from tokens so the mark themes itself:
   the book strokes take the brand blue, the star the accent gold. Element
   classes only — no <style> in the SVG, no inline style attribute (rule 12). */
.brand-icon { display: block; width: 1.75rem; height: 1.75rem; flex: none; }
.logo-book { stroke: var(--brand-blue); }
.logo-star { fill: var(--brand-gold); }

.app-nav { flex: 1 1 auto; }
.app-nav ul {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin: 0;
  list-style: none;
}
.app-nav a {
  text-decoration: none;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius);
  /* Reserve the gold active-marker gutter so hovering doesn't shift the label. */
  border-bottom: 2px solid transparent;
}
.app-nav a:hover { background: var(--color-surface-alt); }
/* Gold as a sparing accent: the current section gets a gold underline. */
.app-nav a[aria-current="page"] {
  border-bottom-color: var(--color-accent);
  font-weight: 600;
}

/* p26.48: "New transaction" is a DISTINCT right-aligned primary action, not an inline
   nav link. .app-nav flexes to fill, so margin-left:auto pins this button (and the
   logout after it) to the right edge, visually separated from the section links. It
   reuses the .btn/.btn-primary tokens, so it themes for light + dark automatically. */
.app-newtxn { margin-left: auto; white-space: nowrap; text-decoration: none; }

/* p23.5 second-row section navigation: a lighter bar under the header carrying the
   current section's sub-pages (and, later, per-page filters/search). Rendered only
   when the section has sub-nav. */
.app-subnav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  /* p28.28: tighter top/bottom padding (was space-2); keep the space-6 horizontal
     gutter so the bar aligns with the header/main edges. */
  padding: var(--space-1) var(--space-6);
  background: var(--color-surface-alt);
  border-bottom: var(--border);
}
/* p28.28: the subnav links carry their own space-2 left padding (for the hover
   pill), which pushed the FIRST link's TEXT space-2 to the right of the page
   content gutter. Pull the whole links row left by that padding so the label text
   lines up flush under the <main> content edge (which sits at the space-6 gutter). */
.app-subnav-links { margin-left: calc(-1 * var(--space-2)); }
/* p23.10 page controls in the section bar: filters + action buttons, pushed to the
   right of the sub-nav links. Compact so the whole bar stays one row where it fits. */
.app-subnav-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  margin-left: auto;
}
/* p28.28: on pages whose section bar carries ONLY filters (reports, accounts,
   register -- no sub-nav links), drop the margin-left:auto so the filter labels
   sit flush at the space-6 gutter, lined up under the page content, instead of
   floating mid-bar. On link pages the controls stay pushed right of their links. */
.app-subnav-controls:only-child { margin-left: 0; }
.subnav-filters {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
}
.subnav-filters label { font-weight: 600; font-size: 0.9rem; }
.subnav-filters .check { display: inline-flex; align-items: center; gap: var(--space-1); font-weight: 400; }
/* p26.8: the date filters carry a fixed short character count — size them to a
   10-char ISO date + padding so the whole bar collapses onto one line. Target
   the input, not the .datefield-wrap datefield.js appends around it (widthing the
   wrapper would squash the pick button). The text search is capped a touch, and
   the selects size to their content. flex-wrap stays as the narrow-width fallback. */
.subnav-filters .js-datefield { width: 8rem; }
.subnav-filters #reg-text { width: 9rem; }
/* Cap the fund/sub/program selects so a long option label can't blow the bar to a
   second line; the chosen value truncates within the box. */
.subnav-filters select { width: auto; max-width: 9rem; }
/* Tighten the inter-control gap so the whole bar fits one desktop line. */
.subnav-filters { gap: var(--space-1) var(--space-2); }
.subnav-actions { display: inline-flex; flex-wrap: wrap; gap: var(--space-2); }
/* Reusable second-nav "left content / right actions" primitive. A page that wants
   its filters/search on the LEFT and its action buttons pinned to the far RIGHT of
   the section bar wraps them in a .subnav-split and makes the action group the
   trailing .subnav-actions child: the container spans the full bar width, and
   margin-left:auto pushes the actions to the right edge (the same mechanism that
   pins .app-newtxn / logout in the top nav). Full width is load-bearing — without
   it the auto-margin has no free space to consume and the pin is a no-op. flex-wrap
   keeps the actions dropping below the left content on a too-narrow viewport. */
.subnav-split {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--space-1) var(--space-3);
  width: 100%;
}
/* The .subnav-split above is width:100%, but that resolves against its parent
   .app-subnav-controls, which — as a shrink-to-fit flex item of .app-subnav —
   only sizes to its content, not the full bar. So the split (and its trailing
   .subnav-actions margin-left:auto) had NO free space to consume: the actions
   landed at the content edge, not the far right of the bar. Make the wrapper
   grow to fill the bar so the 100% (and the auto-margin pin) has real width to
   work with. Scoped via :has(.subnav-split) so it touches ONLY the accounts
   toolbar — reports/register controls-only bars (.report-params / .subnav-filters,
   no .subnav-split) keep their existing shrink-to-fit sizing untouched. */
.app-subnav-controls:has(.subnav-split) { flex: 1 1 auto; }
.subnav-split .subnav-actions { margin-left: auto; align-self: center; }
/* p26.11: after p26.7 the New/Merge/Reconcile triggers in the section bar are anchors
   (<a class="btn ...">). The generic nav-link rule `.app-subnav a` (specificity 0,1,1)
   sets color:var(--color-text-muted) and out-specifies the `.btn`/`.btn-primary` color
   (0,1,0), so a link-as-button rendered muted gray text -- unreadable on the primary blue
   in DARK mode (and off on the ghost buttons too). Set the button text color in the
   controls scope (0,2,0, which beats the nav-link rule); the -primary rule follows so
   equal-specificity source order lands the on-primary color. */
.app-subnav-controls .btn {
  padding: var(--space-1) var(--space-3);
  font-size: 0.9rem;
  color: var(--color-text);
}
.app-subnav-controls .btn-primary { color: var(--color-primary-text); }
/* p26.11 fix: scope this to the section-nav LINKS list only. A bare `.app-subnav ul`
   also matched the fuzzy-combobox popup (`ul.combo-list`, rendered inside the report
   filter bar in `.app-subnav-controls`), leaking `display:flex; flex-direction:row`
   onto it so the program filter's options laid out horizontally. The links list always
   carries `.app-subnav-links`, so target that class directly. */
.app-subnav-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  margin: 0;
  list-style: none;
}
.app-subnav a {
  text-decoration: none;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius);
  color: var(--color-text-muted);
  border-bottom: 2px solid transparent;
}
.app-subnav a:hover { background: var(--color-surface); color: var(--color-text); }
.app-subnav a[aria-current="page"] {
  color: var(--color-text);
  border-bottom-color: var(--color-accent);
  font-weight: 600;
}

.app-main {
  max-width: 60rem;
  margin: 0 auto;
  padding: var(--space-8) var(--space-6);
}
/* Full-width opt-out (p23.2): data-dense pages (the transaction editor) drop the
   60rem reading cap and use the horizontal space, keeping a small viewport gutter. */
.app-main-wide {
  max-width: min(100rem, 100% - 2 * var(--space-6));
}
/* Full-viewport opt-out (p29.11): a many-column comparative statement (the monthly
   Statement of Activities, the per-program statement) drops even the 100rem cap so
   every period/program column shows without a horizontal scroll, keeping only the
   page gutter. Layered over app-main-wide (both classes are present). */
.app-main-full {
  max-width: min(100%, 100% - 2 * var(--space-6));
}
/* p30.11: CENTER the report block when it's NARROWER than the full-viewport shell.
   After p30.7 a comparative statement's table sizes to its CONTENT (width:auto), so an
   annual Statement of Activities (~few columns) is much narrower than app-main-full's
   near-full width and, left-aligned, stranded the table at the left with a wide empty
   gutter + scrollbar on the right. Center the WHOLE report block as a unit so the title,
   CSV link, tree controls, print-meta and table stay coherent (never a left-stranded
   heading over a centered table): the results block shrinks to its content
   (width:fit-content) and centers (margin-inline:auto), and the <h1> centers with it.
   max-width:100% keeps the WIDE case correct — a weekly (52-column) statement exceeds
   the viewport, fit-content collapses to 100%, and .report-scroll's overflow:auto still
   scrolls it full-width with no clipping. Scoped to app-main-full so the centered-column
   shells (app-main / app-main-wide: balance sheet, trial balance, …) are untouched. */
.app-main-full > h1,
.app-main-full > #report-results {
  width: fit-content;
  max-width: 100%;
  margin-inline: auto;
}

.app-footer {
  padding: var(--space-4) var(--space-6);
  color: var(--color-text-muted);
  border-top: var(--border);
}

/* Theme control + language switcher clusters. */
.theme-control,
.lang-switch,
.cluster {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}
.lang-switch { margin-top: var(--space-6); color: var(--color-text-muted); }
.lang-switch a { text-decoration: none; }
.lang-switch a[aria-current="true"] { font-weight: 700; text-decoration: underline; }

/* Buttons. */
.btn {
  display: inline-block;
  padding: var(--space-2) var(--space-4);
  border: var(--border);
  border-radius: var(--radius);
  background: var(--color-surface-alt);
  color: var(--color-text);
  cursor: pointer;
}
.btn:hover { border-color: var(--color-primary); }
.btn-primary {
  background: var(--color-primary);
  color: var(--color-primary-text);
  border-color: var(--color-primary);
}
.btn-primary:hover {
  background: var(--color-primary-hover);
  border-color: var(--color-primary-hover);
}
.btn-primary:active {
  background: var(--color-primary-active);
  border-color: var(--color-primary-active);
}
.btn-ghost { background: transparent; }
/* A disabled button LOOKS disabled: dimmed + a not-allowed cursor, with the primary
   fill neutralized. Placed after the :hover/:active rules so it wins on a hovered
   disabled button. Used e.g. by the reconcile Finalize button, which stays disabled
   until the reconciliation difference is zero (.Summary.Finalizable). */
.btn:disabled,
.btn[disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}
.btn-primary:disabled,
.btn-primary[disabled] {
  background: var(--color-surface-alt);
  color: var(--color-text-muted);
  border-color: var(--color-surface-alt);
}

/* Form fields. */
.field { margin-bottom: var(--space-4); }
.field label { display: block; margin-bottom: var(--space-1); font-weight: 600; }
.field input,
.field select,
select,
input[type="text"],
input[type="password"],
/* p30.4: every textarea shares the themed text-input chrome (border/radius/surface/
   padding) so it honours the light/dark tokens like input[type=text]. Per-textarea
   SIZING (rows/min-height/max-width, e.g. .txn-notes, #sf-custom) is layered on top of
   this by the individual rules; only the VISUAL theming lives here, once. */
textarea {
  padding: var(--space-2) var(--space-3);
  border: var(--border);
  border-radius: var(--radius);
  background: var(--color-surface);
}

/* p30.5: fully custom checkboxes app-wide (the Active-only filter, account flag
   checkboxes current_cash/receivable_payable, grant/budget/expense checkboxes,
   everywhere). The former `accent-color` only tinted the CHECKED tick and left the
   UNCHECKED box as the unstyled UA default (visibly wrong on dark themes). We now
   render BOTH states from theme tokens: appearance:none strips the native box, then a
   themed border + surface fill draws the empty box, and :checked fills with the primary
   token and draws a CSS-only rotated-border tick (::after). All colors are single
   light-dark() tokens, so it themes for light/dark automatically. The
   `<label><input>…</label>` markup and the shared focus-visible ring (see :where(...)
   above) are untouched, so keyboard operation and accessibility are preserved. */
input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  width: 1.05rem;
  height: 1.05rem;
  flex-shrink: 0;
  margin: 0;
  border: var(--border);
  border-radius: 4px;
  background: var(--color-surface);
  vertical-align: middle;
  cursor: pointer;
}
input[type="checkbox"]:checked {
  border-color: var(--color-primary);
  background: var(--color-primary);
}
/* CSS-only tick: a rotated L drawn from two borders, in the on-primary token so it
   stays legible on the primary fill in every theme. */
input[type="checkbox"]:checked::after {
  content: "";
  position: absolute;
  left: 0.32rem;
  top: 0.14rem;
  width: 0.28rem;
  height: 0.55rem;
  border: solid var(--color-primary-text);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
/* Disabled matches the p28 disabled-control convention (dimmed + not-allowed). */
input[type="checkbox"]:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
/* p26.87: the new-user password field + its Regenerate button on one row. The button
   is server-rendered `hidden` (no-JS fallback) and pwgen.js unhides it, so the wrap
   collapses to just the input when JS is off. */
.pwgen-field { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.pwgen-field input { flex: 1 1 16rem; }

/* Alerts / flash. */
.alert {
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--color-danger);
  border-radius: var(--radius);
  background: var(--color-danger-bg);
  color: var(--color-danger);
}
/* Success variant (p10.3: valid form submit). */
.alert-ok {
  border-color: var(--color-success);
  background: var(--color-success-bg);
  color: var(--color-success);
}
/* Caution variant (p31: a non-blocking "heads up" advisory -- not an error, not a
   success). Amber tone so it reads as unusual-but-allowed. */
.alert-warn {
  border-color: var(--color-warning);
  background: var(--color-warning-bg);
  color: var(--color-warning);
}
/* Per-field validation error (p10.3 form-error convention): a small danger-toned
   message under the field, plus a danger border on the invalid input. Styles live
   here, never inline (strict CSP, rule 12). */
.field-error {
  margin: var(--space-1) 0 0;
  color: var(--color-danger);
  font-size: 0.875rem;
}
.field input[aria-invalid="true"],
input[aria-invalid="true"] {
  border-color: var(--color-danger);
}

/* p26.29: horizontal-ROW layout for the schedule + budget-line entry forms.
   Both are htmx-swapped partials (stable ids preserved -- CSS-only, no template
   restructuring) whose fields normally stack vertically. Here they flow as a
   wrapping inline row of labelled controls, mirroring .txn-main-header.

   The schedule form has kind-specific `.kind-block` wrappers that budgetkind.js
   shows/hides via the `hidden` attribute. We must NOT fight that: the layout is
   scoped to `:not([hidden])` blocks, so a JS-hidden block falls back to the UA
   `[hidden]{display:none}` with no specificity battle -- the toggle stays intact.
   The VISIBLE kind-block uses `display: contents` so its inner `.field`s flatten
   into the same row as name/kind/notes (the wrapper carries no border/padding). */
.schedule-form,
.line-form {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-3) var(--space-4);
}
/* The heading + the action buttons each own a full row (don't get pulled into
   the input row). */
.schedule-form > h2,
.schedule-form > .form-actions,
.line-form > h3,
.line-form > .form-actions {
  flex-basis: 100%;
  margin-bottom: 0;
}
/* Each field becomes an inline column: label above control, with the error/hint
   still stacked under the control (wraps sensibly on a 422 re-render). */
.schedule-form .field,
.line-form .field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin: 0;
}
.schedule-form .field > label,
.line-form .field > label { margin-bottom: 0; }
.schedule-form .field input,
.schedule-form .field select,
.line-form .field input,
.line-form .field select { min-width: 9rem; }
.schedule-form .field .hint,
.schedule-form .field .field-error,
.line-form .field .hint,
.line-form .field .field-error { margin: 0; max-width: 16rem; }
/* Flatten the ACTIVE kind-block so its fields join the row; hidden ones fall
   back to the UA hidden rule untouched. */
.schedule-form .kind-block:not([hidden]) { display: contents; }
/* The custom-dates textarea is genuinely multi-line; give it room without
   stretching the whole row. */
.schedule-form #sf-custom { min-width: 18rem; }

/* p-golive: the expense-report header (date/description/memo/notes). Editable form is
   a wrapping row of themed .field controls; the notes textarea gets its own full-width
   line. The read-only view is a compact two-column definition list. */
.expense-header-form {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  align-items: flex-start;
  margin-bottom: var(--space-4);
}
.expense-header-form .field { margin-bottom: 0; }
.expense-header-form .field input { min-width: 12rem; }
.expense-header-form .field:has(textarea) { flex-basis: 100%; }
.expense-header-form textarea { min-width: 24rem; width: 100%; max-width: 40rem; }
.expense-header-view {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--space-1) var(--space-4);
  margin-bottom: var(--space-4);
}
.expense-header-view dt { font-weight: 600; }
.expense-header-view dd { margin: 0; }

/* 8a: a LOCKED main-header field (the AP account + the description) rendered as read-only
   static text inside the reused .txn-main-header, so the submitter sees but cannot edit them
   (only the reviewer can). Styled to sit where the equivalent input would, visually inert. */
.er-locked {
  display: inline-block;
  padding: var(--space-1) 0;
  font-weight: 500;
  min-height: 1.5rem;
}

/* 8b: the submitter's "my reports" list -- a clear, scannable table keyed on the report's
   stored header (date/description/memo) + status + actions. Mirrors .register-table: a
   header rule, per-cell padding, compact (content-width) date/status/action columns, and
   flexible description/memo columns that truncate rather than wrap. */
.expense-reports-table {
  width: 100%;
  border-collapse: collapse;
  margin-top: var(--space-4);
}
.expense-reports-table th,
.expense-reports-table td {
  padding: var(--space-2) var(--space-3);
  border-bottom: var(--border);
  text-align: left;
  vertical-align: top;
}
/* Compact columns pin to their content width (width:1% + nowrap under auto layout). */
.expense-reports-table .er-date,
.expense-reports-table .er-status,
.expense-reports-table .er-actions {
  width: 1%;
  white-space: nowrap;
}
/* Flexible text columns take the remaining width and truncate with an ellipsis. */
.expense-reports-table .er-description,
.expense-reports-table .er-memo {
  max-width: 18rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Keep the per-row actions on one line (Open + Discard side by side). */
.expense-reports-table .er-actions {
  display: flex;
  gap: var(--space-3);
  align-items: center;
}
/* The rejection reason wraps under the status badge, so that cell must not nowrap. */
.expense-reports-table .er-status { white-space: normal; }
.expense-reports-table .er-reason {
  margin: var(--space-1) 0 0;
  font-size: 0.85rem;
  color: var(--color-text-muted, inherit);
}

.flash:empty { display: none; }
.flash {
  max-width: 60rem;
  margin: var(--space-4) auto 0;
  padding: 0 var(--space-6);
}

/* Login card. */
.login-body {
  display: grid;
  place-items: center;
  min-height: 100vh;
  padding: var(--space-6);
}
.login-card {
  width: min(24rem, 100%);
  padding: var(--space-8);
  background: var(--color-surface);
  border: var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.login-card .btn-primary { width: 100%; }

/* Login brand lockup: centered mark + wordmark + Spanish tagline above the form. */
.login-brand { text-align: center; margin-bottom: var(--space-6); }
.brand-lockup {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  text-decoration: none;
  color: var(--color-text);
  font-weight: 700;
  font-size: 1.75rem;
}
.brand-lockup .brand-icon { width: 2.75rem; height: 2.75rem; }
.brand-tagline {
  margin: var(--space-2) 0 0;
  color: var(--color-text-muted);
  font-size: 0.9rem;
}

/* p11.1 chart of accounts. Tree-table indentation is expressed as depth classes
   (no inline style attribute -- strict CSP style-src 'self', rule 12). A handful
   of levels covers a realistic chart; deeper accounts simply stop indenting. */
.tree-table { width: 100%; border-collapse: collapse; margin-top: var(--space-4); }
.tree-table th, .tree-table td {
  text-align: left;
  padding: var(--space-2) var(--space-3);
  border-bottom: var(--border);
  vertical-align: top;
}
/* Blue-accented header row anchors the table in the brand (bottom rule + tint). */
.tree-table thead th {
  color: var(--color-primary);
  border-bottom: 2px solid var(--color-primary);
  background: var(--color-surface-alt);
}
/* p26.25: the balance column (header + cells) is right-aligned with tabular figures,
   matching amounts elsewhere (register/report/recon tables). */
.tree-table th.acct-balances, .tree-table td.acct-balances {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.tree-table .acct-inactive { opacity: 0.6; }
/* p26.74: injected TYPE-tier header row ("Assets"/"Liabilities"/…). Display-only and
   non-selectable -- a bold, faintly-shaded band grouping each type's accounts. It is
   not a real account (no register link/actions); collapse/expand works on it. */
.tree-table .acct-type-header > td { background: var(--color-surface-muted, rgba(0,0,0,0.03)); }
.tree-table .acct-type-header .acct-name { font-weight: 600; text-transform: uppercase; letter-spacing: 0.02em; font-size: 0.85rem; }

/* p26.25: tree collapse/expand controls sit above the tree; JS reveals them (they
   render `hidden`). A tight horizontal cluster like the section actions. */
.tree-controls { display: flex; gap: var(--space-2); margin: var(--space-3) 0; }
/* Per-row disclosure toggle injected into the parent row's name cell. A borderless
   glyph button; ▸ collapsed, ▾ expanded (rotates the same glyph). */
.tree-toggle {
  border: 0;
  background: none;
  cursor: pointer;
  padding: 0;
  margin-right: var(--space-1);
  color: var(--color-text-muted);
  font-size: 0.85rem;
  line-height: 1;
}
.tree-toggle::before { content: "\25BE"; } /* ▾ expanded */
.tree-toggle.is-collapsed::before { content: "\25B8"; } /* ▸ collapsed */
/* p26.55: a parent's NAME cell is clickable to toggle its subtree (treetable.js adds
   .tree-name-cell only to cells that HAVE a disclosure toggle). Genuine links inside
   keep their own cursor via the more-specific rule. */
.tree-name-cell { cursor: pointer; }
.tree-name-cell a { cursor: pointer; }

/* p31 10b: program-statement COLUMN-group collapse. colcollapse.js hides a collapsed
   program COLUMN (its header <th> + every body <td>, matched by data-program) with
   .col-hidden, injects a ▸/▾ disclosure into each collapsible parent header, and marks the
   header .col-group-header (clickable). The hide rule is NOT inside @media screen, so a
   collapsed column stays collapsed in print/PDF too (a printed statement reflects exactly
   what's on screen). No inline style (strict CSP, rule 12). */
.col-hidden { display: none; }
/* The disclosure toggle injected into a collapsible program header; ▾ expanded, ▸ collapsed
   (mirrors .tree-toggle). The parent span cell is right-aligned (.amount), which would push
   the toggle to the RIGHT edge; float it LEFT so the ▸/▾ sits at the LEFT edge of the group
   span — directly above the parent's own rollup ("Total") column, which is the leftmost
   column of the subtree (columns are emitted in tree pre-order). */
.col-toggle {
  float: left;
  border: 0;
  background: none;
  cursor: pointer;
  padding: 0;
  margin-right: var(--space-1);
  color: var(--color-text-muted);
  font-size: 0.85rem;
  line-height: 1;
}
.col-toggle::before { content: "\25BE"; } /* ▾ expanded */
.col-toggle.is-collapsed::before { content: "\25B8"; } /* ▸ collapsed */
/* A collapsible program header is clickable to fold its child columns. A genuine link
   inside keeps its own cursor via the more-specific rule. */
.col-group-header { cursor: pointer; }
.col-group-header a { cursor: pointer; }
/* Group separation (user feedback): a heavier left border marks the LEFTMOST column of each
   TOP-LEVEL (root) program's subtree, so it reads clearly which leaf programs sit under which
   parent group. Stamped by reports.go (GroupStart) on the root program's header span + its
   own-leaf rollup + every body cell of that leftmost column — a continuous full-height rule.
   Heavier than the default 1px cell border (line ~1025) so the group boundary stands out. The
   leading FIXED columns (Account/Total/Admin/Fundraising) never get GroupStart, so they're
   unaffected. Not inside @media screen, so the separator prints too (matches .col-hidden). */
.report-table th.col-group-start,
.report-table td.col-group-start { border-left: 2px solid var(--color-text-muted); }
/* Tree indentation. The selectors are qualified with `.tree-table td` so they
   OUTRANK `.tree-table td`'s own padding shorthand (equal-specificity padding-left
   would otherwise win and flatten the tree). A nesting guide line makes the
   hierarchy legible beyond the indent alone. */
.tree-table td.acct-depth-0 { padding-left: var(--space-3); }
.tree-table td.acct-depth-1 { padding-left: calc(var(--space-3) + 1.5rem); }
.tree-table td.acct-depth-2 { padding-left: calc(var(--space-3) + 3rem); }
.tree-table td.acct-depth-3 { padding-left: calc(var(--space-3) + 4.5rem); }
.tree-table td.acct-depth-4 { padding-left: calc(var(--space-3) + 6rem); }
.tree-table td.acct-depth-5 { padding-left: calc(var(--space-3) + 7.5rem); }
/* A child row's name is prefixed with a subtle guide so depth reads at a glance. */
.tree-table td.acct-depth-1, .tree-table td.acct-depth-2, .tree-table td.acct-depth-3,
.tree-table td.acct-depth-4, .tree-table td.acct-depth-5 { border-left: 2px solid var(--color-border); }

/* Row actions on one line: a tight, non-wrapping cluster (was a vertical stack).
   The cell stays a real table-cell (no display:flex on <td>, which would drop it
   out of the table layout); its inline-block controls just don't wrap. Applies to
   the chart of accounts, subsidiaries, and programs tables. */
.acct-actions, .sub-actions, .prog-actions { white-space: nowrap; }
.acct-actions .btn, .sub-actions .btn, .prog-actions .btn {
  padding: var(--space-1) var(--space-2);
  font-size: 0.9rem;
  margin-right: var(--space-1);
}
.acct-actions form.inline, .sub-actions form.inline, .prog-actions form.inline {
  display: inline-block;
  margin: 0;
}
.acct-balances .bal { display: inline-block; margin-right: var(--space-3); }
/* Per-currency activity/balance amounts sit side by side; space them so multiple
   currencies don't run together (programs activity, fund balances). */
.prog-activity .amount,
.fund-balance .amount { display: inline-block; margin-right: var(--space-3); }
.badge {
  font-size: 0.75em;
  padding: 0 var(--space-2);
  border: var(--border);
  border-radius: var(--radius);
  color: var(--color-muted, inherit);
}
/* p11.1: an "active" badge reads as affirmative (not muted) so the account
   settings status line reads at a glance; inherits the base .badge chrome. */
.badge-active { color: var(--color-success, inherit); }

/* Admin full-access banner (admin_user_detail.tmpl). An admin bypasses every grant/
   permission, so the report-group / transaction-permission / expense-submit sections
   are hidden and this prominent themed callout explains why (replacing the old small
   hint). Accent left border + surface-alt fill so it reads as an unmistakable notice. */
.admin-full-access {
  margin: var(--space-4) 0;
  padding: var(--space-3) var(--space-4);
  background: var(--color-surface-alt);
  border: var(--border);
  border-left: 4px solid var(--color-accent);
  border-radius: var(--radius);
}
.admin-full-access-title {
  margin: 0;
  font-weight: 700;
  font-size: 1.05rem;
}
.admin-full-access-detail {
  margin: var(--space-2) 0 0;
  color: var(--color-muted, inherit);
}
/* p11.1: the account-settings active/inactive status line sits above the form,
   a single flex row: label, status badge, toggle button. */
.account-status {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.account-status-label { color: var(--color-muted, inherit); }
/* p28.8: the chart's per-account free-text Notes column (replaces the redundant
   Type). Muted + small so it reads as secondary annotation, not the account name. */
.acct-notes { font-size: 0.85em; color: var(--color-muted, inherit); }

/* p28.9/p29.4: EPHEMERAL chart search box + its filter mechanism. The box now lives in
   the second-level nav toolbar (a SIBLING of the filter form, .accounts-subnav), rendered
   as a label-above .report-field like the reports/filter controls. Typing narrows the
   visible rows client-side; it never submits. */
/* The accounts toolbar is a .subnav-split (full-width flex row, tops aligned, New/Merge
   pinned right) plus the accounts-only wiring below. The base layout — display/flex-wrap/
   align-items:flex-start/gap/width:100% — comes from .subnav-split; flex-start top-aligns
   the row so the filter form's (opacity-0) "updating..." indicator below its fieldset can't
   bottom-align the form and shove the filter fields up relative to the sibling .chart-search
   (the p30/p31 search-offset bug). */
/* Keep the transient "updating..." pill OUT of the form's block height so the form
   hugs its fieldset (no dead space below the filters); it overlays at the row's
   start-edge when a filter request is in flight. */
.accounts-subnav .accounts-filters { position: relative; }
.accounts-subnav .subnav-updating {
  position: absolute;
  top: calc(100% + var(--space-1));
  left: 0;
}
/* p30.3: the filter form must size to its content so the ephemeral Search sits on the
   SAME row as Subsidiary/Type/Active, not wrap below it. The generic subnav rule
   `.app-subnav-controls .report-params { width: 100% }` (0,2,0) would otherwise stretch
   the form full-width and push the sibling .chart-search onto a second line; this rule
   raises specificity (0,3,0) so the form hugs its fields and the search flows inline.
   The search stays a SIBLING of the form (never a field), preserving ephemerality. */
.app-subnav-controls .accounts-subnav .accounts-filters { width: auto; }
/* The search box is a label-above column just like the .report-field filters, so it aligns
   with Subsidiary/Type/Active in the shared flex row. */
/* p30.5: the search column mirrors the sibling .report-field filters (Subsidiary/Type)
   EXACTLY — same label font/weight/margin and the same base input chrome (border/radius/
   surface/padding from the shared select,input[type=text] rule) — so its box metrics and
   vertical alignment line up with the other filter controls on the row. */
.chart-search { display: flex; flex-direction: column; }
.chart-search label { font-weight: 600; font-size: 0.85rem; margin-bottom: 0; }
.chart-search-input { min-width: 14rem; }
/* New/Merge pinning is handled by the reusable .subnav-split .subnav-actions rule above. */
/* While searching (chartsearch.js adds `searching`), an author `display` rule wins
   over the UA `[hidden]{display:none}` treetable uses -- so search visibility governs
   without the two mechanisms fighting over `tr.hidden`. Rows not marked
   `search-visible` are hidden; when the box empties, `searching` is dropped and
   treetable's collapse state governs again. */
.tree-table.searching tr.acct-row { display: none; }
.tree-table.searching tr.acct-row.search-visible { display: table-row; }

/* The inline create/edit form region. */
.account-form-region { margin: var(--space-4) 0; }
.account-form { max-width: 40rem; }
.account-form .field { margin-bottom: var(--space-3); }
.account-form .subs-checklist { margin: var(--space-3) 0; padding: var(--space-3); border: var(--border); border-radius: var(--radius); }
.account-form .hint { font-size: 0.85em; color: var(--color-muted, inherit); margin: 0 0 var(--space-2); }
.filters { display: flex; gap: var(--space-3); align-items: center; flex-wrap: wrap; margin-bottom: var(--space-3); }
.filters .check { display: inline-flex; gap: var(--space-2); align-items: center; }

/* ------------------------------------------------------------------ *
 * Brand styleguide (ux: brand identity)
 * ------------------------------------------------------------------ */

/* Palette swatches: a labelled color chip grid. Each chip's fill is a modifier
   class (never an inline style attribute — strict CSP, rule 12). */
.swatches { display: flex; flex-wrap: wrap; gap: var(--space-4); margin: var(--space-4) 0; padding: 0; list-style: none; }
.swatch { display: flex; flex-direction: column; gap: var(--space-1); width: 8rem; }
.swatch-chip { height: 3.5rem; border: var(--border); border-radius: var(--radius); }
.swatch-chip.is-blue    { background: #0072ce; }
.swatch-chip.is-gold    { background: #ffb81c; }
.swatch-chip.is-white   { background: #ffffff; }
.swatch-chip.is-navy    { background: #0a1a2f; }
.swatch-chip.is-primary { background: var(--color-primary); }
.swatch-chip.is-accent  { background: var(--color-accent); }
.swatch-name { font-weight: 600; }
.swatch-hex  { color: var(--color-text-muted); font-size: 0.85rem; font-variant-numeric: tabular-nums; }

/* Side-by-side logo/palette in BOTH themes on one page: each pane pins its own
   color-scheme so light-dark() resolves per pane (no JS, no second page load). */
.theme-previews { display: flex; flex-wrap: wrap; gap: var(--space-4); margin: var(--space-4) 0; }
.theme-preview {
  flex: 1 1 16rem;
  padding: var(--space-6);
  border: var(--border);
  border-radius: var(--radius);
  background: var(--color-surface);
  color: var(--color-text);
}
.theme-preview.is-light { color-scheme: light; }
.theme-preview.is-dark  { color-scheme: dark; }
.theme-preview h3 { margin: 0 0 var(--space-3); }
.theme-preview .brand-lockup { font-size: 1.5rem; }
.theme-preview .brand-lockup .brand-icon { width: 2.25rem; height: 2.25rem; }

/* p15.1 reports framework: the generic report page (report.tmpl). The shared
   params form is a plain inline row of controls; the table renders any Table with
   indent (tree depth), subtotal/total emphasis, and the D19 warning row. Amounts
   are right-aligned via the .amount marker the renderer stamps on money cells. No
   inline style anywhere (strict CSP, rule 12) -- everything is here. */
.report-params { margin: var(--space-4) 0; }
.report-params fieldset {
  border: var(--border);
  border-radius: var(--radius);
  padding: var(--space-3) var(--space-4);
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: var(--space-3);
}
/* p26.52: each label+control is a .report-field flex COLUMN, so the label always sits
   directly above its own input as ONE non-breaking unit (the fieldset wraps whole
   fields, never orphaning a label from its control). The From/To pair are two adjacent
   fields, so they flow together on one row when space allows and wrap as label+input
   pairs otherwise. */
.report-params .report-field { display: flex; flex-direction: column; }
.report-params label { font-weight: 600; margin-bottom: var(--space-1); }
/* p29.4: a boolean filter (the accounts active-only checkbox) renders its checkbox
   inline with its own label — the label wraps the input, so it is NOT a stacked
   label-above-control field. Keep it a compact inline unit, aligned with its neighbours. */
.report-params .check {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-weight: 400;
}
.report-params .report-run { align-self: end; }

/* p26.76/p26.86: EVERY report's filter form renders in the second-level nav bar (the
   subnav controls slot). Compact it for the bar — drop the outer margin/border chrome
   and tighten fields/labels/selects — so the filters read as a toolbar row (like the
   register/accounts subnav filters), not a boxed fieldset. The fieldset keeps the base
   `flex-wrap: wrap`, so a WIDE filter set (period = from+to, plus granularity/currency)
   wraps onto a second line within the band rather than overflowing. The controls keep
   their report-params class + names, so the form submits identically. */
.app-subnav-controls .report-params { margin: 0; width: 100%; }
.app-subnav-controls .report-params fieldset {
  border: 0;
  border-radius: 0;
  padding: 0;
  gap: var(--space-1) var(--space-3);
}
.app-subnav-controls .report-params label { font-size: 0.85rem; margin-bottom: 0; }
.app-subnav-controls .report-params select { max-width: 11rem; }
/* p28.29: the subsidiary-scope select shows a full subsidiary name (e.g.
   "UrbanPromise Comunidad ...") which the generic 11rem cap truncated. Give this
   one control a wider cap without blowing out the other filters. */
.app-subnav-controls .report-params select.report-scope-select { max-width: 18rem; }
.app-subnav-controls .report-params .js-datefield { width: 8rem; }
.app-subnav-controls .report-params .report-run { padding: var(--space-1) var(--space-3); }
/* p28.30: non-disruptive "updating…" cue on a filter change. htmx's own indicator
   styles are disabled (includeIndicatorStyles:false, strict CSP), so app.css owns
   the .htmx-indicator rule: hidden by default, faded in only while the request is
   in flight. htmx sets .htmx-request on the element that issued the GET -- here the
   .report-params form -- so the descendant indicator shows for that window without
   touching or reflowing #report-results. */
.htmx-indicator { opacity: 0; transition: opacity 0.15s ease-in; }
.report-params.htmx-request .htmx-indicator,
.subnav-filters.htmx-request .htmx-indicator { opacity: 1; }
/* p30.5: the "updating…" cue reads as an obvious (but tasteful) working badge — a
   primary-tinted pill with stronger text + a small spinner — so a filter change is
   clearly acknowledged. The show/hide channel is untouched: opacity is still flipped
   0->1 by .htmx-request (above); the animation lives on the ::before spinner only, so it
   never fights the fade. */
.report-updating,
.subnav-updating {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  align-self: center;
  padding: var(--space-1) var(--space-3);
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--color-primary);
  background: var(--color-surface-alt);
}
.report-updating::before,
.subnav-updating::before {
  content: "";
  width: 0.8rem;
  height: 0.8rem;
  border: 2px solid var(--color-border);
  border-top-color: var(--color-primary);
  border-radius: 50%;
  animation: cuento-spin 0.7s linear infinite;
}
@keyframes cuento-spin { to { transform: rotate(360deg); } }
/* Respect reduced-motion: keep the badge, drop the spin. */
@media (prefers-reduced-motion: reduce) {
  .report-updating::before,
  .subnav-updating::before { animation: none; }
}
.report-export { margin: var(--space-3) 0; }
.report-table { width: 100%; border-collapse: collapse; margin-top: var(--space-4); }
/* p30.7: a comparative statement's width scales with its GRANULARITY. In the
   full-viewport shell (app-main-full — the monthly/weekly Statement of Activities,
   the per-program statement) the table sizes to its CONTENT instead of forcing
   width:100%, so an annual view (few period columns) stays narrow while a 52-column
   weekly view grows to fill the viewport. The .report-scroll wrapper is
   overflow:auto (@media screen) inside app-main-full (max-width = viewport − gutter),
   so a table wider than the viewport scrolls, but a narrow one never stretches. The
   right-aligned amount columns keep their tabular-nums alignment either way. */
.app-main-full .report-table { width: auto; }
.report-table th, .report-table td {
  padding: var(--space-2) var(--space-3);
  border-bottom: var(--border);
  text-align: left;
}
/* p28.25: money cells never wrap -- a 6-figure negative like "-123,456.78" must
   keep its sign and digits on one line (the minus was wrapping to its own row). */
.report-table th.amount, .report-table td.amount {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* p28.27: deeper per-level indent (~50% bigger step) so nesting reads clearly;
   level 3 stays well within a normal-width statement's first column. */
.report-indent-1 td:first-child { padding-left: calc(var(--space-4) * 2.25); }
.report-indent-2 td:first-child { padding-left: calc(var(--space-4) * 4.5); }
.report-indent-3 td:first-child { padding-left: calc(var(--space-4) * 6.75); }
/* p28.24 / p30.6 / p30.10: accounting rules for the report's KEY TOTALS, in THREE
   visually distinct, clearly RANKED tiers so a reviewer reads the hierarchy at a glance:
     1. .report-subtotal — a placeholder-PARENT roll-up (Revenue/Expenses and nested
        parents, Total assets' sub-parents) = the LIGHTEST emphasis: 600 weight, a light
        2px rule above, a subtle surface tint.
     2. .report-section-total — the SECTION total ("Total revenue"/"Total expenses",
        "Total assets"/"Total liabilities"/"Total net assets", the program statement's
        per-currency section totals) = MORE emphasis than the parent (it is the definitive
        section figure): 700 weight, a HEAVIER (darker + thicker) rule above AND a thin
        rule below, same tint — so in a single-parent section "Total revenue" no longer
        reads identically to its "Revenue" parent.
     3. .report-total — the GRAND total (Change in Net Assets, Total liabilities and net
        assets) = the STRONGEST: 700 weight and the accounting DOUBLE rule above + below.
   The tiers are ordered by border WEIGHT/DARKNESS and font-weight, not by the fill, so
   they survive @media print (which forces the light tokens and lets browsers strip the
   background): a light single rule < a heavy single rule < a double rule carries the rank
   on paper. Uses theme tokens (border / text-muted / surface-alt) so it stays legible in
   both themes. */
.report-subtotal {
  font-weight: 600;
  border-top: 2px solid var(--color-border);
  background: var(--color-surface-alt);
}
.report-section-total {
  font-weight: 700;
  border-top: 2px solid var(--color-text-muted);
  border-bottom: 1px solid var(--color-border);
  background: var(--color-surface-alt);
}
.report-total {
  font-weight: 700;
  border-top: 3px double var(--color-border);
  border-bottom: 3px double var(--color-border);
  background: var(--color-surface-alt);
}
.report-warning { background: var(--color-danger-bg); color: var(--color-danger); font-weight: 600; }
/* p28.26: highlight the hovered data row (mirrors .recon-row:hover). The sticky
   first-column override lives in the @media screen block below, so the frozen
   column picks up the same highlight instead of masking it with its opaque bg. */
.report-table tbody tr:hover { background: var(--color-surface-alt); }

/* p30.9 BUDGET-VARIANCE measure toggle + magnitude color-coding.
   Each grid cell (.bv-cell) folds all three measures into three spans; the table's
   data-measure attribute (flipped by budgetvariance.js, defaulted server-side to
   "variance") selects which spans show — no server round-trip, no client money math
   (the values are pre-formatted, rule 10). No-JS: the default data-measure shows the
   variance spans and the inert buttons do nothing. */
.bv-measure { display: none; }
.bv-table[data-measure="budgeted"] .bv-budgeted,
.bv-table[data-measure="actual"] .bv-actual,
.bv-table[data-measure="variance"] .bv-variance { display: inline; }
/* The toggle button group: mark the active measure. aria-pressed is the source of truth
   (accessible); the visual pressed style mirrors it so the choice is obvious. */
.bv-measure-toggle { display: flex; gap: var(--space-2); }
.bv-measure-btn[aria-pressed="true"] {
  background: var(--color-primary);
  color: var(--color-primary-text);
  border-color: var(--color-primary);
}
/* Magnitude buckets on a variance TOTAL: over budget = red ramp, under = green ramp,
   deepening with |variance|/|budgeted| (slight → moderate → large). Theme-aware via the
   light-dark() token pairs. Color only REINFORCES the sign the number already carries
   (accessibility: never the sole cue) — a bold weight backs it up too. */
.bv-over-slight   { color: light-dark(#c25a52, #ff9a94); }
.bv-over-moderate { color: light-dark(#b3261e, #ff8079); font-weight: 600; }
.bv-over-large    { color: light-dark(#8f1d17, #ff5c52); font-weight: 700; }
.bv-under-slight   { color: light-dark(#3f8f5b, #8fe6a5); }
.bv-under-moderate { color: light-dark(#1f7a3d, #6fdd8b); font-weight: 600; }
.bv-under-large    { color: light-dark(#155c2c, #52d377); font-weight: 700; }

/* p26.54: a wide/tall report matrix (e.g. the program statement) scrolls inside
   .report-scroll with STICKY headers -- the top header ROW and the left (first) COLUMN
   stay visible while scrolling. Scoped to @media screen so print/PDF is never clipped by
   the max-height and the sticky offsets don't fight pagination. Sticky cells need an
   OPAQUE background (or scrolled content bleeds through) and a z-index stack: the header
   row above the body, the first column above ordinary cells, and the TOP-LEFT corner
   (header ∩ first column) highest of all. */
@media screen {
  .report-scroll { max-height: 75vh; overflow: auto; }
  .report-scroll .report-table thead th {
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--color-surface);
    /* p31.x: with border-collapse:collapse the header's bottom (underline) border
       belongs to the COLLAPSED table grid, not the cell — as a sticky <th> detaches and
       scrolls, that grid border does NOT repaint with it, so the underline VANISHES once
       vertical scrolling starts. Fix: paint the underline as an inset box-shadow, which
       belongs to the sticky element itself and survives scroll. Neutralize the grid
       border-bottom (from ~line 1040) to TRANSPARENT (screen-only) so we don't get a
       doubled 1px line un-scrolled — keep its 1px WIDTH so the header box height (baked
       into the nested-header top: offsets below as +2px) is unchanged; the box-shadow is
       then the single visible underline both un-scrolled and scrolled. Print keeps the
       real grid border (this override is @media screen). Covers every sticky report
       header (flat trial-balance/balance-sheet + the nested program-statement rows). */
    border-bottom-color: transparent;
    box-shadow: inset 0 -1px 0 var(--color-border);
  }
  /* p31: the program statement has a NESTED multi-row thead (root programs on top, their
     children below, etc). Every header <th> is sticky; without staggered offsets the rows
     would all stick at top:0 and OVERLAP. Offset each header <tr> by its index times one
     header row's box height (~line 1.4rem + padding var(--space-2)*2 + its 2px bottom
     border), so the rows ABUT as the body scrolls under them. A cell rowspanning from an
     upper row keeps that row's (smaller) offset and stays pinned, which is correct. A hair
     generous over the measured height (~41px) rather than short, so a scrolled body never
     peeks BETWEEN the rows. Covers up to six program-tree levels; a deeper tree simply lets
     its lowest rows abut. A flat single-row header (every other report) is nth-child(1),
     top:0, unaffected. */
  .report-scroll .report-table thead tr:nth-child(1) th { top: 0; }
  .report-scroll .report-table thead tr:nth-child(2) th { top: calc((1.4rem + var(--space-2) * 2 + 2px) * 1); }
  .report-scroll .report-table thead tr:nth-child(3) th { top: calc((1.4rem + var(--space-2) * 2 + 2px) * 2); }
  .report-scroll .report-table thead tr:nth-child(4) th { top: calc((1.4rem + var(--space-2) * 2 + 2px) * 3); }
  .report-scroll .report-table thead tr:nth-child(5) th { top: calc((1.4rem + var(--space-2) * 2 + 2px) * 4); }
  .report-scroll .report-table thead tr:nth-child(6) th { top: calc((1.4rem + var(--space-2) * 2 + 2px) * 5); }
  .report-scroll .report-table th:first-child,
  .report-scroll .report-table td:first-child {
    position: sticky;
    left: 0;
    z-index: 1;
    background: var(--color-surface);
  }
  /* The top-left corner is BOTH a sticky header and the sticky first column -- keep it
     above both so neither the scrolling body nor a scrolling header slides under it. */
  .report-scroll .report-table thead th:first-child { z-index: 3; }
  /* Subtotal/section-total/total/warning rows carry their own emphasis background; give
     their sticky first cell the matching surface so the frozen column matches the row it
     belongs to. p30.6/p30.10: all three total tiers tint to surface-alt, so their frozen
     first cell must pick up the same tint (else it masks with the plain surface). */
  .report-scroll .report-table tr.report-warning td:first-child {
    background: var(--color-danger-bg);
  }
  .report-scroll .report-table tr.report-subtotal td:first-child,
  .report-scroll .report-table tr.report-section-total td:first-child,
  .report-scroll .report-table tr.report-total td:first-child {
    background: var(--color-surface-alt);
  }
  /* p28.26: the sticky first cell is opaque, so on hover it would keep its own
     surface bg and break the row highlight at the frozen column. Give the hovered
     row's first cell the same hover surface so the highlight spans the full row. */
  .report-scroll .report-table tbody tr:hover td:first-child {
    background: var(--color-surface-alt);
  }
}

/* p29.17 report-oriented print CSS. Users print (or save-as-PDF) a report to keep a
   shareable, unchanging snapshot of its current state, so @media print turns the app
   page into a clean sheet: the app chrome is stripped, a print-only title+date header
   identifies the record, the table repeats its header on every page and avoids
   mid-row breaks, and the LIGHT theme is forced (ink on white) regardless of the
   user's on-screen theme. All of this stays in app.css (strict CSP, rule 12 — no
   inline style). Per-report print refinements can extend this same block.

   The .report-print-meta element itself is hidden on screen (the dates already show
   in the filter bar) and revealed only in print — see the two rules just below. */
.report-print-meta { display: none; }

@media print {
  /* 1. STRIP THE CHROME. Hide every top-level body sibling that is NOT the report
     <main> — this covers the top nav + New-transaction button + logout (.app-header),
     the second-level filter/subnav bar (.app-subnav), the footer, the skip link, and
     the flash region in one future-proof rule (a new chrome sibling is hidden too).
     Inside <main>, hide the in-results chrome: the CSV/export link, the tree
     collapse/expand controls, and the htmx "updating…" indicator. What remains is the
     report title, its print date header, and the table. */
  body > *:not(#main) { display: none !important; }
  .report-export,
  .report-controls,
  .tree-controls,
  .htmx-indicator,
  .report-updating { display: none !important; }

  /* 2. SELF-IDENTIFYING PRINT HEADER: hidden on screen, shown on paper so the sheet
     carries what the report is (the <h1>) and as-of/period + scope/currency. */
  .report-print-meta { display: block; margin: 0 0 var(--space-3); }
  .report-print-meta p { margin: 0; }

  /* 3a. FORCE THE LIGHT THEME by remapping the theme tokens to ink-on-white on the
     root. Every existing var() — table text, the subtotal rule, the total's double
     underline — then resolves to black/white for free, so a dark-theme user still
     prints a legible black-on-white statement (browsers strip backgrounds in print,
     and the accounting rules rest on borders + font-weight, not fills). */
  /* The token declarations live on :root (specificity 0,1,0) and color-scheme is set
     per html[data-theme="…"] (0,1,1); to WIN over both — including a dark-theme user —
     the print override matches html[data-theme] (0,1,1) and, coming later in source
     order, takes the tie. The values are absolute (#000/#fff), so they no longer
     depend on color-scheme resolving light-dark(). */
  html[data-theme] {
    color-scheme: light;
    --color-text: #000;
    --color-text-muted: #333;
    --color-surface: #fff;
    --color-surface-alt: #fff;
    --color-bg: #fff;
    --color-border: #000;
  }
  body { background: #fff; color: #000; }

  /* 3b. Give every page a sensible margin; leave portrait/landscape to the browser. */
  @page { margin: 1.5cm; }

  /* 3c. The report main fills the sheet (drop the reading-width cap). */
  .app-main, .app-main-wide, .app-main-full { max-width: none; margin: 0; padding: 0; }

  /* 3d. The table prints full width; repeat the header on every page; keep a row (and
     the emphasized subtotal/total rows) from splitting across a page break; discourage
     a section header from being orphaned at the foot of a page. The .report-scroll
     max-height/overflow is @media screen-scoped, so it never clips the print. */
  .report-table { width: 100%; }
  /* Drillable cells (amounts / ledger lines) are <a> links on screen; a link takes its
     color from the UA link rule, not var(--color-text), so it would print blue +
     underlined. Force plain black text so a printed statement reads as figures, not
     unfired hyperlinks. */
  .report-table a,
  .report-drill-link { color: #000; text-decoration: none; }
  .report-table thead { display: table-header-group; }
  .report-table tr,
  .report-subtotal,
  .report-section-total,
  .report-total { break-inside: avoid; }
  .report-subtotal,
  .report-section-total,
  .report-total { break-before: avoid; }

  /* 3e. WIDE COMPARATIVE STATEMENTS (many period/program columns) can exceed portrait
     width. There is no reliable cross-browser pure-CSS shrink-to-fit, so the table
     stays full width and the user picks Landscape / the browser's "fit to page" for a
     very wide statement — this is a documented, accepted limitation (see AGENTS.md). */
}

/* p16.3 reconciliation workspace. The sticky summary keeps the difference chip in
   view while scrolling long split lists; the difference goes green at zero (the
   finalize gate). The cleared toggle is a plain focusable button (Space activates it
   natively -- CSP-safe, no JS); a cleared row is de-emphasized. No inline style
   anywhere (strict CSP, rule 12). */
.recon-list-table, .recon-table { width: 100%; border-collapse: collapse; margin-top: var(--space-4); }
.recon-list-table th, .recon-list-table td,
.recon-table th, .recon-table td {
  padding: var(--space-2) var(--space-3);
  border-bottom: var(--border);
  text-align: left;
}
.recon-table td.amount, .recon-table th.amount { text-align: right; font-variant-numeric: tabular-nums; }
.recon-row.is-cleared { color: var(--color-text-muted); }
/* p26.49: a row that HAS a toggle (non-finalized, so reconrow.js forwards a click to it)
   reads as clickable -- pointer cursor + a hover highlight over the whole row. A
   finalized row renders no .recon-toggle, so :has() keeps it inert (no cue, no toggle).
   Genuinely interactive children (the Edit link, the toggle button) keep their own
   default cursor via the more-specific rules below. */
.recon-row:has(.recon-toggle) { cursor: pointer; }
.recon-row:has(.recon-toggle):hover { background: var(--color-surface-alt); }
.recon-row a, .recon-row .recon-toggle { cursor: auto; }
.recon-toggle { cursor: pointer; }
.recon-toggle[aria-pressed="true"] { font-weight: 600; }
.recon-check { color: var(--color-success, green); font-weight: 700; }
.recon-summary {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--color-surface);
  border: var(--border);
  border-radius: var(--radius);
  padding: var(--space-3) var(--space-4);
  margin: var(--space-4) 0;
}
.recon-figures { display: flex; flex-wrap: wrap; gap: var(--space-4); margin: 0; }
.recon-figures div { display: flex; flex-direction: column; }
.recon-figures dt { font-weight: 600; }
.recon-figures dd { margin: 0; font-variant-numeric: tabular-nums; }
.recon-diff dd { font-weight: 700; }
.recon-diff.is-zero dd { color: var(--color-success, green); }
.recon-diff-hint { margin-top: var(--space-2); }
.recon-finalize-form { margin-top: var(--space-3); }

/* ------------------------------------------------------------------ *
 * p26.8 account register readability. The register table had no styling of
 * its own (it fell back to raw browser defaults), so it read poorly: no cell
 * padding, no zebra, and the flexible text columns pushed the fixed-width
 * date/amount/balance columns into multi-line wraps.
 *
 * Auto table-layout (NOT fixed): the sub/fund/recon columns are conditionally
 * rendered and the <thead> cells carry no width classes, so any positional
 * width scheme is fragile — everything is driven by the per-td classes instead.
 * Strict-CSP-safe: all styles here, none inline (rule 12).
 * ------------------------------------------------------------------ */
.register-table { width: 100%; border-collapse: collapse; margin-top: var(--space-4); }
.register-table th, .register-table td {
  padding: var(--space-2) var(--space-3);
  border-bottom: var(--border);
  text-align: left;
}
/* Amounts right-aligned with tabular figures (extend the shared .amount rule to
   this table — the .report-table/.recon-table rules don't reach here). */
.register-table th.amount, .register-table td.amount {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* Compact columns: fixed, tabular, or badge/action content that never needs to
   wrap. width:1% + nowrap pins each to its content width under auto layout. */
.register-table .reg-date,
.register-table .reg-amount,
.register-table .reg-running,
.register-table .reg-sub,
.register-table .reg-fund,
.register-table .reg-recon,
.register-table .reg-actions {
  width: 1%;
  white-space: nowrap;
}
/* Keep the per-row action links from wrapping into a tall stack. */
.register-table .reg-actions { display: flex; gap: var(--space-3); }

/* Flexible text columns: take the remaining width, truncate with ellipsis
   rather than wrap. The max-width is the bound the ellipsis needs to trigger. */
.register-table .reg-description,
.register-table .reg-memo,
.register-table .reg-counter {
  max-width: 14rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Zebra striping on data rows only (the sentinel + empty rows aren't .reg-row,
   so they're excluded). Hover rule follows the zebra rule so it wins on even
   rows (equal specificity — source order decides). */
.register-table .reg-row:nth-child(even) { background: var(--color-surface-alt); }
/* Hover: a saturated left-accent (inset shadow, no reflow) plus a surface tint.
   The neutral surface tokens are all near-white / near-navy by design, so a
   neutral bg swap alone reads as almost-invisible on odd rows in light mode and
   runs backwards on even rows in dark mode; the accent bar is distinct against
   every neutral background in both themes. Rule follows the zebra rule so it
   wins on even rows (equal specificity — source order decides). */
.register-table .reg-row:hover {
  background: var(--color-surface);
  box-shadow: inset 3px 0 0 var(--color-primary);
}

/* ------------------------------------------------------------------ *
 * p23.2 transaction editor — the high-traffic data-entry screen.
 *
 * Goals (user, 2026-07-13): use the horizontal space (the page is app-main-wide),
 * lay the header fields in a row instead of a cramped vertical stack, present the
 * splits as a bordered spreadsheet-style table with column rules, keep the header
 * row and the totals/actions bar in view (sticky) so a typical entry needs no
 * scroll, and keep keyboard navigation obvious. Strict-CSP-safe: all styles here,
 * none inline (rule 12).
 * ------------------------------------------------------------------ */

.txn-editor { margin-top: var(--space-4); }

/* p30.2 grouped transaction header: ONE cohesive block (.txn-main-header) holds BOTH the
   transaction-level fields (subsidiary / date / main-description / memo + hidden carriers)
   AND the main-split header (account + auto-balanced display-only amount), plus a status
   band (per-fund chips + balance error) below them. A wrapping row of labelled fields,
   accented with a primary left rule so it reads as THE transaction line the body splits
   balance against. (Was two adjacent bands, .txn-header + .txn-main-header, pre-p30.2.) */
.txn-main-header {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-3) var(--space-4);
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-3);
  background: var(--color-surface-alt);
  border: var(--border);
  border-left: 3px solid var(--color-primary);
  border-radius: var(--radius);
}
.txn-main-header .txn-field { display: flex; flex-direction: column; gap: var(--space-1); margin: 0; }
.txn-main-header .txn-field > label { font-weight: 600; font-size: 0.85rem; color: var(--color-text-muted); }
.txn-main-header .txn-field input,
.txn-main-header .txn-field select { min-width: 10rem; }
.txn-main-header .txn-date { min-width: 8rem; }
.txn-main-header .txn-memo-header { min-width: 14rem; }
/* p26.23: with the payee + apply-fund controls gone, the header memo grows to fill the
   freed horizontal space (a long, near-full-width memo field). The wrapping flex header
   stays responsive: the grow field takes the remaining row width, wrapping at narrow widths. */
.txn-main-header .txn-field-grow { flex: 1 1 20rem; }
.txn-main-header .txn-field-grow .txn-memo-header { width: 100%; }
.txn-main-header .txn-field-grow .desc-field { width: 100%; position: relative; }
.txn-main-header .txn-field-grow .txn-desc { width: 100%; }
/* p26.36: the main-split description is a grow field whose .desc-field wrapper + input fill
   the column width, matching the memo-header grow behaviour. */
.txn-main-header .txn-main-desc-field .desc-field { width: 100%; position: relative; }
.txn-main-header .txn-main-desc-field .txn-desc { width: 100%; }
/* Description-label help glyph: a small circled "?" the user can hover/focus for the native
   title tooltip (matches the datefield/badge title= affordance). No inline handlers -- the
   browser draws the title on hover/focus; keyboard users reach it via tabindex=0. */
.help-tip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.1em;
  height: 1.1em;
  margin-inline-start: var(--space-1, 0.25rem);
  border-radius: 50%;
  border: 1px solid var(--color-muted, #888);
  color: var(--color-muted, #888);
  font-size: 0.75em;
  line-height: 1;
  cursor: help;
  user-select: none;
  vertical-align: middle;
}
.help-tip:hover,
.help-tip:focus-visible { border-color: currentColor; color: var(--color-text, inherit); }
/* The balancing amount is a read-only display figure: right-aligned, muted, non-editable
   look so it never reads as a fillable field (the server computes it). */
.txn-main-header .txn-main-amount {
  text-align: right;
  font-variant-numeric: tabular-nums;
  background: var(--color-bg);
  color: var(--color-text-muted);
  cursor: default;
}
.txn-main-header .txn-main-amount-field { flex: 0 0 auto; }
/* p42: the per-fund breakdown of the fanned-out main split -- one chip-like item per fund the
   main balances, shown below the overall balancing amount. Empty (and thus invisible) for the
   single-fund case. Muted + tabular like the amount so it reads as a companion display figure. */
.txn-main-header .txn-main-breakdown {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-1);
  justify-content: flex-end;
  text-align: right;
}
.txn-main-header .txn-main-breakdown-item {
  font-size: var(--font-size-sm, 0.85em);
  font-variant-numeric: tabular-nums;
  color: var(--color-text-muted);
  white-space: nowrap;
}
/* p-fix2: the main ACCOUNT + BALANCING AMOUNT always occupy their OWN second header row,
   below the subsidiary/date/description/memo row. flex-basis 100% (same full-width break the
   .txn-status band uses) forces this wrapper onto a fresh line no matter how narrow the account
   select is -- so a subsidiary with few/short account names can't let the select wrap UP into
   leftover top-row space. Inside, its two fields lay out as a normal flex row, aligned to the
   bottom like the header fields above. This wrapper is txn-form-only; expense_detail's reused
   header does NOT use it (its locked AP-account field stays a plain top-row field). */
.txn-main-header .txn-main-line {
  flex: 1 1 100%;
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-3) var(--space-4);
}
/* p-curr: the txn-currency badge sits inline in the main-account label, reading as a small
   pill so the currency this transaction records in (the main account's currency, D3) is clear
   at a glance. Muted chrome; the code text stays legible (tabular, uppercase for consistency). */
.txn-currency-badge {
  display: inline-block;
  margin-inline-start: var(--space-2);
  padding: 0 var(--space-2);
  border: var(--border);
  border-radius: var(--radius, 4px);
  background: var(--color-surface-muted, #f1f1f1);
  color: var(--color-text, inherit);
  font-size: 0.8em;
  font-weight: 600;
  line-height: 1.5;
  letter-spacing: 0.04em;
  vertical-align: middle;
}
/* p30.2 status band: the per-fund imbalance chips + the balance-error slot, on their own
   full-width row at the foot of the grouped header (flex-basis 100% forces the wrap). */
.txn-main-header .txn-status {
  flex: 1 1 100%;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
}

/* The split grid: a real table with collapsed borders so every column is ruled. */
.txn-grid {
  width: 100%;
  border-collapse: collapse;
  border: var(--border);
  table-layout: fixed;
  font-variant-numeric: tabular-nums;
}
.txn-grid thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  text-align: left;
  padding: var(--space-2) var(--space-3);
  color: var(--color-primary);
  background: var(--color-surface-alt);
  border: var(--border);
  border-bottom: 2px solid var(--color-primary);
  font-size: 0.85rem;
}
/* p26.45 stacked two-line header (the single <thead> row labels BOTH physical split rows):
   line 1 (.txn-hdr-main) is the row-1 field (Description / Account / Amount); line 2
   (.txn-hdr-sub) is a smaller muted row-2 field (Fund / Program·Class / Memo), aligned in
   the same band. The trailing colspan-2 utility header (over error + delete) is blank. */
.txn-grid-2row thead th .txn-hdr-main { display: block; }
.txn-grid-2row thead th .txn-hdr-sub {
  display: block;
  font-size: 0.75rem;
  font-weight: normal;
  color: var(--color-muted);
}
/* p28: the trailing error + delete columns are pinned narrow via a <colgroup> so the
   delete cell is just wide enough for the x button (a tight utility cell), not a full
   grid band. Under table-layout:fixed the <col> width wins, so the 12 content columns
   stay flexible and only these two are clamped. */
.txn-grid-2row col.txn-col-util { width: 2.25rem; }
.txn-grid td.txn-cell {
  padding: 0;
  border: var(--border);
  vertical-align: middle;
}
/* Inputs and selects fill their cell edge-to-edge — the CELL border is the visual
   boundary, so the controls themselves lose their own border/radius/background for
   a clean spreadsheet feel. Focus still shows the accessible outline (below). */
.txn-grid td.txn-cell input,
.txn-grid td.txn-cell select {
  width: 100%;
  border: 0;
  border-radius: 0;
  background: var(--color-surface);
  padding: var(--space-2) var(--space-3);
}
.txn-grid td.txn-cell input:focus-visible,
.txn-grid td.txn-cell select:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: -2px;
}
.txn-grid td.txn-cell input:focus,
.txn-grid td.txn-cell select:focus { background: var(--color-surface-alt); }
/* Amount columns right-align their figures. */
.txn-grid .txn-amount-cell input { text-align: right; }
/* p26.23: description is now column 1 and account column 2, so the amount/debit HEADER is
   column 3 -- but ONLY in the two EDITABLE grids (the txn editor form + the editable
   expense grid). The read-only expense table also carries .txn-grid yet has NO description
   column (its column 3 is Fund), so scope this to the editable grids to avoid right-aligning
   the wrong header there. */
.txn-editor thead th:nth-child(3),
#expense-grid-form thead th:nth-child(3) { text-align: right; }

/* Sensible column widths (table-layout: fixed): the account, memo and error
   columns get the slack; the fixed selects stay compact. These apply to the
   SINGLE-ROW grids (the read-only expense table). The two-row txn/expense grids
   (p26.32) size their columns via the colspan model instead -- see below -- so
   they are excluded here (:not(.txn-grid-2row)). */
.txn-grid:not(.txn-grid-2row) col,
.txn-grid:not(.txn-grid-2row) .txn-account-cell { width: auto; }
.txn-grid:not(.txn-grid-2row) .txn-amount-cell { width: 8rem; }
.txn-grid:not(.txn-grid-2row) .txn-fund-cell,
.txn-grid:not(.txn-grid-2row) .txn-program-cell,
.txn-grid:not(.txn-grid-2row) .txn-class-cell { width: 10rem; }
.txn-grid:not(.txn-grid-2row) .txn-row-error { width: 12rem; }
.txn-grid:not(.txn-grid-2row) .txn-desc-cell { width: auto; }
.txn-delete-col,
.txn-grid:not(.txn-grid-2row) .txn-delete-cell { width: 2.5rem; text-align: center; }

/* p26.32 / p28 two-row split layout + per-split zebra. Each split is one <tbody
   class="txn-row"> holding a .txn-row-main (description / account / amount / error / delete)
   and a .txn-row-more (fund / program·class / memo -- the memo now spans to the right edge).
   The header colspans (5 / 4 / 5) and the row cells' colspans divide the 14 fixed-layout
   columns; the trailing 2 are pinned narrow by the <colgroup> (error + delete). The
   per-split zebra bands BOTH physical rows together by alternating the whole tbody --
   nth-of-type is self-maintaining across add/delete/reindex, and grouping by tbody means no
   data-split-index attribute is needed. */
/* p26.38 / p28: the expense grid (<tbody class="el-row">) and budget grid (<tbody
   class="bs-row">) reuse the SAME .txn-grid-2row table + .txn-cell cells, so the per-split
   zebra must band them too; their trailing status/delete cells are .el-row-error /
   .el-delete-cell and .bs-row-error / .bs-delete-cell. All three grids share these selectors
   so a split's two physical <tr> band together on the even tbodies. */
.txn-grid-2row tbody.txn-row:nth-of-type(even) td.txn-cell,
.txn-grid-2row tbody.txn-row:nth-of-type(even) td.txn-row-error,
.txn-grid-2row tbody.txn-row:nth-of-type(even) td.txn-delete-cell,
.txn-grid-2row tbody.el-row:nth-of-type(even) td.txn-cell,
.txn-grid-2row tbody.el-row:nth-of-type(even) td.el-row-error,
.txn-grid-2row tbody.el-row:nth-of-type(even) td.el-delete-cell,
.txn-grid-2row tbody.bs-row:nth-of-type(even) td.txn-cell,
.txn-grid-2row tbody.bs-row:nth-of-type(even) td.bs-row-error,
.txn-grid-2row tbody.bs-row:nth-of-type(even) td.bs-delete-cell {
  background: var(--color-surface-alt);
}
.txn-grid-2row tbody.txn-row:nth-of-type(even) td.txn-cell input,
.txn-grid-2row tbody.txn-row:nth-of-type(even) td.txn-cell select,
.txn-grid-2row tbody.txn-row:nth-of-type(even) td.txn-cell .combo > .combo-text,
.txn-grid-2row tbody.el-row:nth-of-type(even) td.txn-cell input,
.txn-grid-2row tbody.el-row:nth-of-type(even) td.txn-cell select,
.txn-grid-2row tbody.el-row:nth-of-type(even) td.txn-cell .combo > .combo-text,
.txn-grid-2row tbody.bs-row:nth-of-type(even) td.txn-cell input,
.txn-grid-2row tbody.bs-row:nth-of-type(even) td.txn-cell select,
.txn-grid-2row tbody.bs-row:nth-of-type(even) td.txn-cell .combo > .combo-text {
  background: var(--color-surface-alt);
}
/* p28: the per-split zebra (above) already demarcates each two-row split, so the inter-split
   top border was dropped -- the alternating band is the only separator. */

/* p26.4 expense line grid: the same .txn-cell spreadsheet layout (the grid table already
   carries .txn-grid), so the account combo overlay is contained by its cell -- fixing the
   account-cell bleed behind the amount/fund inputs. Column widths mirror the txn grid; the
   amount cell right-aligns its figure and reformats on blur (expensegrid.js). */
.txn-grid .el-amount-cell input { text-align: right; }
.el-account-cell { width: auto; }
.el-amount-cell { width: 8rem; }
.el-fund-cell, .el-program-cell { width: 10rem; }
.el-memo-cell { width: auto; }
.el-delete-col, .el-delete-cell { width: 2.5rem; text-align: center; }
.el-row-error { width: 12rem; }
/* The delete control is a compact borderless × that inherits the cell's spreadsheet feel.
   p26.11/p26.23: the delete cell is the trailing column (after the error column) and drops
   txn-cell, so it re-declares the ruled border + centering here. Shared by BOTH grids (the
   expense grid's .el-delete and the txn grid's .txn-delete). */
.txn-grid td.el-delete-cell,
.txn-grid td.bs-delete-cell,
.txn-grid td.txn-delete-cell {
  text-align: center;
  border: var(--border);
  vertical-align: middle;
  /* p28: a tight utility cell -- just the x button. No stretch beyond its content. */
  padding: 0;
  width: 2.25rem;
}
.el-delete,
.bs-delete,
.txn-delete {
  padding: var(--space-1) var(--space-2);
  line-height: 1;
  font-size: 1.1rem;
  color: var(--color-text-muted, inherit);
}
.el-delete:hover,
.bs-delete:hover,
.txn-delete:hover { color: var(--color-danger, inherit); }

/* Row states. A server-flagged row and a client-detected out-of-subsidiary row
   both tint danger so the eye lands on the problem. p26.32: these follow (and so win
   over) the per-split zebra, and also tint the cell's input/select/combo so an errored
   EVEN (zebra-alt) row reads danger, not zebra. */
.txn-row[data-row-error] td.txn-cell,
.txn-row.sub-conflict td.txn-cell { background: var(--color-danger-bg); }
.txn-row[data-row-error] td.txn-cell input,
.txn-row[data-row-error] td.txn-cell select,
.txn-row[data-row-error] td.txn-cell .combo > .combo-text,
.txn-row.sub-conflict td.txn-cell input,
.txn-row.sub-conflict td.txn-cell select,
.txn-row.sub-conflict td.txn-cell .combo > .combo-text { background: var(--color-danger-bg); }
.txn-row-error .field-error,
.el-row-error .field-error,
.bs-row-error .field-error { padding: 0; display: inline-block; }
/* p26.11: the trailing error cell is a STATUS/message strip, not an editable field. An
   empty error cell (borderless input in a bordered cell is the editable look) otherwise
   reads as a fillable input. Give it the page background (not the input --color-surface)
   and message padding so it is visibly a non-input cell. Applies to both grids' error
   columns (.txn-row-error on the txn grid, .el-row-error on the expense grid). */
.txn-grid td.txn-row-error,
.txn-grid td.el-row-error,
.txn-grid td.bs-row-error {
  background: var(--color-bg);
  padding: var(--space-1) var(--space-2);
  color: var(--color-danger);
  font-size: 0.85rem;
  border: var(--border);
  vertical-align: middle;
}

/* p26.2 fuzzy-filter combobox (combobox.js). The native <select class="combo-input">
   stays the value sink + no-JS fallback and stays laid out; the overlay text input sits
   ON TOP of it (never display:none -- keyboard + Playwright drive the select underneath).
   In a .txn-cell the wrapper fills the cell so the account input aligns edge-to-edge like
   the other spreadsheet cells (this also fixes the account-cell overlap: the input is
   contained by the cell, not floating over neighbours). */
.combo { position: relative; display: block; }
/* The overlay input covers the select's box. It carries the cell's own padding so its
   text lines up with the sibling cells; the select underneath keeps its intrinsic size so
   the cell reserves the right height. */
.combo > .combo-text {
  position: absolute;
  inset: 0;
  width: 100%;
  z-index: 1;
  border: var(--border);
  border-radius: var(--radius);
  background: var(--color-surface);
}
/* Inside the spreadsheet grids (transaction + expense) the combo overlay is a
   borderless, square cell edge-to-edge with its neighbours; the cell itself draws
   the grid lines. Standalone combos (admin program-scope, report program filter)
   keep the resting border from the base rule above so they are visible at rest. */
.txn-grid td.txn-cell .combo > .combo-text {
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-radius: 0;
}
.combo > .combo-text:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: -2px;
}
.combo > .combo-text:focus { background: var(--color-surface-alt); }
/* p26.11: account/fund/program combos keep the native <select> as the Tab stop, but the
   opaque overlay input (.combo-text, z-index:1, inset:0) COVERS the select's native focus
   ring -- so tabbing to the cell showed no focus at all. Paint the ring on the TOPMOST
   element (the overlay) whenever focus lands anywhere inside the wrapper -- the covered
   select OR the overlay input -- so every editable grid cell (combo, amount, memo) shows a
   visible ring, in both light and dark themes. This is the same focus token the sibling
   cells use, so the treatment matches. */
.combo:focus-within > .combo-text {
  outline: 2px solid var(--color-focus);
  outline-offset: -2px;
  background: var(--color-surface-alt);
}
/* The listbox popover: anchored under the cell, above everything, scrollable. */
.combo-list {
  list-style: none;
  margin: 0;
  padding: 0;
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 30;
  background: var(--color-surface);
  border: var(--border);
  border-radius: var(--radius);
  max-height: 14rem;
  overflow-y: auto;
  min-width: max-content;
}
.combo-list[hidden] { display: none; }
.combo-option {
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.combo-option:hover,
.combo-option.active { background: var(--color-primary); color: var(--color-on-primary, #fff); }

/* p26.19 per-split description field: a plain text input (like memo) whose suggestion
   dropdown mirrors the combobox listbox. The .desc-field wrapper is the positioning
   context so the absolutely-positioned .desc-suggestions listbox anchors under the input.
   In a grid cell the input fills the cell like the memo input; the listbox floats above. */
.desc-field { position: relative; }
/* A grid dropdown from the LAST row (description suggestions, or a combobox listbox) opens
   downward and visually overlaps the sticky totals bar (position:sticky, z-index 2). The
   dropdown's own z-index is trapped inside the table's stacking context, so it cannot beat
   the totals bar (a root-level sibling of the table). While the user is interacting with the
   grid (:focus-within), lift the whole grid table into a stacking context ABOVE the totals
   bar so its dropdowns stay visible + clickable; when idle the bar still covers rows that
   scroll under it. */
.txn-editor:focus-within .txn-grid,
#expense-grid-form:focus-within .txn-grid { position: relative; z-index: 3; }
.desc-field > .txn-desc,
.desc-field > .el-desc { width: 100%; }
/* The .desc-suggest-box is the per-row container the fetched <ul.desc-suggestions>
   fragment is injected into (a plain box; JS toggles [hidden]). The fetched <ul> is the
   actual positioned dropdown. */
.desc-suggest-box[hidden] { display: none; }
.desc-suggestions {
  list-style: none;
  margin: 0;
  padding: 0;
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  /* Above the sticky totals bar (z-index 2) AND the combobox listboxes (30) so a
     description dropdown near the bottom of a short grid is never obscured. */
  z-index: 50;
  background: var(--color-surface);
  border: var(--border);
  border-radius: var(--radius);
  max-height: 14rem;
  overflow-y: auto;
  min-width: max-content;
}
.desc-suggestion {
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
  white-space: nowrap;
}
.desc-suggestion:hover,
.desc-suggestion.active { background: var(--color-primary); color: var(--color-on-primary, #fff); }

.txn-actions { margin: var(--space-3) 0; }

/* p30.2: the per-fund chips + balance error render in the grouped header's .txn-status band
   (the old sticky-bottom .txn-totals bar was removed). The chip + error styling below is
   class-based and applies unchanged at the new location. */
.txn-fund-chips { display: inline-flex; flex-wrap: wrap; gap: var(--space-2); }
/* p28/p29.5: a per-fund chip is NEUTRAL by default (a running per-fund sum is not a
   problem) -- muted theme surface + text, NOT the red error token. Only a genuine
   imbalance flips to danger. (The always-zero overall Total chip was removed in p29.5.) */
.txn-fund-chip {
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  background: var(--color-surface);
  color: var(--color-text-muted);
}
/* A live imbalance is the alarm state; it flips the chip to danger so it stays distinct. */
.txn-fund-chip.imbalanced {
  background: var(--color-danger-bg);
  color: var(--color-danger);
}
.txn-totals-error:not(:empty) { color: var(--color-danger); font-weight: 600; }

/* p24.2 transaction-level notes: a full-width multiline field below the grid, for a
   longer explanation than the short per-split memos. */
.txn-notes-field { display: flex; flex-direction: column; gap: var(--space-1); margin-top: var(--space-4); }
.txn-notes-field > label { font-weight: 600; font-size: 0.85rem; color: var(--color-text-muted); }
/* p28/p30.4: the notes area's bespoke SIZING only — a taller ~5-line min-height and a
   readable ~60ch max width so it reads as a notes box, not a stray full-bleed textarea.
   The VISUAL theming (border/radius/surface/padding) now comes from the shared `textarea`
   rule so every textarea is consistent (p30.4). */
.txn-notes {
  width: 100%;
  max-width: 60ch;
  min-height: 7.5rem;
  box-sizing: border-box;
  resize: vertical;
}
.txn-notes:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -1px; }

.txn-submit { display: flex; gap: var(--space-3); margin-top: var(--space-4); }

/* p25.4 expense line grid: separate the Save-lines / Submit / Discard actions from the
   grid so the buttons don't hug its bottom border. */
#expense-grid-form .form-actions { margin-top: var(--space-3); }
.expense-submit { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; margin-top: var(--space-3); }

/* A subsidiary that is locked (import / expense-review post) reads as a hint. */
.txn-subsidiary-locked { font-size: 0.85rem; color: var(--color-text-muted); }

/* ------------------------------------------------------------------ *
 * p23.4 date field + calendar popover. datefield.js wraps each
 * input.js-datefield, appends the pick button, and builds the popover; all
 * styling lives here (strict CSP, rule 12 — no inline style). The widget is a
 * hand-written calendar (rule 10 — never input[type=date]).
 * ------------------------------------------------------------------ */
.datefield-wrap { position: relative; display: inline-flex; align-items: stretch; }
.datefield-wrap > input { position: relative; }
/* p28.21 "not wider than needed": a date field carries a fixed ~10-char payload
   (YYYY-MM-DD / MM/DD/YYYY), so the input sizes to that plus a little slack — it
   must not stretch to fill a wide table cell (txn/budget grids) or form column. Width
   the INPUT (not the wrap — widthing the wrap squashes the pick button, per the p26.8
   note above); the inline-flex wrap then hugs input+button and never stretches. The
   two filter-bar rules (.subnav-filters / .app-subnav-controls .report-params, also
   8rem) already matched this; this generalizes it to every js-datefield. */
.js-datefield { width: 8rem; max-width: 100%; }
.datefield-wrap { max-width: 100%; }
/* The pick button hugs the right edge of the input. */
.datefield-pick {
  margin-left: calc(-1 * var(--radius));
  padding: 0 var(--space-2);
  border: var(--border);
  border-left: 0;
  border-radius: 0 var(--radius) var(--radius) 0;
  background: var(--color-surface-alt);
  cursor: pointer;
  line-height: 1;
}
.datefield-pick:hover { border-color: var(--color-primary); }

/* Popover: anchored under the field, above other content. */
.datefield-popover {
  position: absolute;
  top: calc(100% + var(--space-1));
  left: 0;
  z-index: 40;
  width: 16rem;
  padding: var(--space-2);
  background: var(--color-surface);
  border: var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.datefield-popover[hidden] { display: none; }

.datefield-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-2);
}
/* The header title is a button (p29.2: opens the month+year navigator); style it as
   plain bold text, not a control, but keep it focusable/hoverable. */
.datefield-title {
  border: 0;
  background: transparent;
  color: inherit;
  cursor: pointer;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius);
  font: inherit;
  font-weight: 600;
}
button.datefield-title:hover { background: var(--color-surface-alt); }
/* p29.2 month navigator: a 3-column grid of the 12 localized month names. */
.datefield-months {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 2px;
  text-align: center;
}
.datefield-month {
  padding: var(--space-2) 0;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: transparent;
  color: inherit;
  cursor: pointer;
}
.datefield-month:hover { background: var(--color-surface-alt); }
.datefield-month[aria-current="true"] {
  background: var(--color-primary);
  color: var(--color-primary-text);
  font-weight: 600;
}
.datefield-nav {
  padding: 0 var(--space-2);
  border: var(--border);
  border-radius: var(--radius);
  background: var(--color-surface-alt);
  cursor: pointer;
  line-height: 1.4;
}
.datefield-nav:hover { border-color: var(--color-primary); }

/* 7-column month grid: weekday headers, blank leading cells, day buttons. */
.datefield-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
  text-align: center;
}
.datefield-weekday {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--color-text-muted);
  padding: var(--space-1) 0;
}
.datefield-blank { visibility: hidden; }
.datefield-day {
  padding: var(--space-1) 0;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: transparent;
  color: inherit;
  cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.datefield-day:hover { background: var(--color-surface-alt); }
.datefield-day[aria-current="date"] {
  background: var(--color-primary);
  color: var(--color-primary-text);
  font-weight: 600;
}

/* p23.9 / p26.77 card grid ("All" landing, and the reusable card-grid layout): a
   responsive grid of tiles linking to destinations. Whole-card is the link (big hit
   target). p26.77 groups cards into labeled .hub-section blocks. */
.hub-cards {
  list-style: none;
  margin: var(--space-3) 0 var(--space-6);
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
  gap: var(--space-3);
}
.hub-card { display: flex; }
.hub-card-link {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  width: 100%;
  padding: var(--space-4);
  border: var(--border);
  border-radius: var(--radius);
  background: var(--color-surface);
  box-shadow: var(--shadow);
  text-decoration: none;
  color: var(--color-text);
}
/* p28.15 sparkle: a subtle gold left-accent slides in on card hover (a reversible
   3px inset ring, no layout shift — the border already reserves the box), echoing the
   gold active-nav identity. */
.hub-card-link {
  transition: box-shadow 0.12s ease, border-color 0.12s ease;
}
.hub-card-link:hover {
  border-color: var(--color-primary);
  box-shadow: inset 3px 0 0 var(--color-accent), var(--shadow);
}
.hub-card-label {
  font-weight: 700;
  color: var(--color-primary);
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}
.hub-card-desc { color: var(--color-text-muted); font-size: 0.9rem; }

/* p28.14: the ONE reusable inline-SVG icon component ("icon" template). Outline
   glyphs inherit the surrounding text color (stroke=currentColor) and size to the
   label (~1em); flex-shrink:0 keeps a card/button icon from squashing beside text.
   The new-txn button gets a small gap so the plus sits off the label. */
.icon {
  width: 1em;
  height: 1em;
  flex-shrink: 0;
}
.app-newtxn .icon { margin-right: var(--space-1); vertical-align: -0.125em; }

/* p26.77 "All" landing sections: a labeled group heading above each card grid. */
.hub-section { margin: var(--space-5) 0; }
.hub-section-title {
  font-size: 1rem;
  font-weight: 700;
  margin: 0;
  padding-bottom: var(--space-1);
  /* p28.15 sparkle: a thin gold rule under each section header, echoing the gold
     active-nav accent — the brand identity carried onto the card grids. */
  border-bottom: 2px solid var(--color-accent);
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* p26.64 bank-import horizontal column mapping: the file's columns run across the top
   (name + a sample value stacked), each with a "maps to" select beneath it. */
.import-map-table { border-collapse: collapse; margin: var(--space-3) 0; }
.import-map-table th.import-map-col,
.import-map-table td.import-map-cell {
  border: var(--border);
  padding: var(--space-2);
  vertical-align: top;
  text-align: left;
}
.import-map-col-name { display: block; font-weight: 700; }
.import-map-col-sample {
  display: block;
  color: var(--color-text-muted);
  font-size: 0.85rem;
  margin-top: var(--space-1);
}
.import-map-label { display: block; font-size: 0.8rem; color: var(--color-text-muted); }
.import-map-error, .import-map-pending { margin-top: var(--space-3); }

/* -------------------------------------------------------------------------
   p29.16 transaction history: a vertical timeline of SAVED-STATE cards. Each
   card is the full transaction at one version (header fields + split rows);
   changes since the prior version are highlighted -- added (green/+), removed
   (red/struck/-), changed (amber/~). Color is never the only signal: each
   change also carries a +/-/~ marker and a status word. Print-friendly (no
   info hidden behind hover).
   ------------------------------------------------------------------------- */
.hist-back { margin-bottom: var(--space-2); }
.hist-intro { color: var(--color-text-muted); max-width: 60ch; margin-bottom: var(--space-4); }

/* Legend of the three change markers. */
.hist-legend {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  padding: 0;
  margin: 0 0 var(--space-6);
  font-size: 0.85rem;
}
.hist-legend-item { display: inline-flex; align-items: center; gap: var(--space-1); color: var(--color-text-muted); }
.hist-legend-item .hist-marker { width: 1.4em; height: 1.4em; }
.hist-legend-item.is-added   .hist-marker { background: var(--color-success-bg); color: var(--color-success); }
.hist-legend-item.is-changed .hist-marker { background: var(--color-warning-bg); color: var(--color-warning); }
.hist-legend-item.is-removed .hist-marker { background: var(--color-danger-bg);  color: var(--color-danger); }

/* Timeline: a rail down the left with a card per version. */
.hist-timeline {
  list-style: none;
  margin: 0;
  padding: 0 0 0 var(--space-6);
  border-left: 3px solid var(--color-border);
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}
.hist-version {
  position: relative;
  background: var(--color-surface);
  border: var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: var(--space-4);
}
/* The node dot on the rail. */
.hist-version::before {
  content: "";
  position: absolute;
  left: calc(-1 * var(--space-6) - 8px);
  top: var(--space-4);
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: var(--color-primary);
  border: 2px solid var(--color-surface);
}
.hist-version.is-voided { border-color: var(--color-danger); }
.hist-version.is-voided::before { background: var(--color-danger); }

.hist-version-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
  padding-bottom: var(--space-2);
  border-bottom: var(--border);
}
.hist-op {
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  font-size: 0.8rem;
  padding: 0.1rem 0.5rem;
  border-radius: 999px;
  background: var(--color-surface-alt);
  color: var(--color-text);
}
.is-voided .hist-op { background: var(--color-danger-bg); color: var(--color-danger); }
.hist-seq { font-weight: 700; }
.hist-actor { color: var(--color-text); }
.hist-actor::before { content: "\00b7"; margin-right: var(--space-3); color: var(--color-text-muted); }
.hist-date { color: var(--color-text-muted); margin-left: auto; font-variant-numeric: tabular-nums; }

/* Header fields as a compact label/value grid. */
.hist-fields {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--space-1) var(--space-4);
  margin: 0 0 var(--space-4);
}
.hist-field { display: contents; }
.hist-field-label { color: var(--color-text-muted); font-size: 0.9rem; }
.hist-field-value { margin: 0; }
.hist-field.is-changed .hist-field-label,
.hist-field.is-changed .hist-value { font-weight: 700; }
.hist-field.is-changed .hist-field-value {
  background: var(--color-warning-bg);
  border-radius: var(--radius);
  padding: 0 var(--space-2);
  width: fit-content;
}

/* The struck prior value shown inline before a changed value. */
.hist-old {
  text-decoration: line-through;
  color: var(--color-text-muted);
  margin-right: var(--space-1);
}

/* Split table: full row set at this version, per-row status. */
.hist-splits {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.95rem;
}
.hist-splits th, .hist-splits td {
  border: var(--border);
  padding: var(--space-2);
  text-align: left;
  vertical-align: top;
}
.hist-splits thead th {
  background: var(--color-surface-alt);
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}
.hist-col-amount { text-align: right; font-variant-numeric: tabular-nums; }
.hist-col-line { text-align: right; width: 3ch; color: var(--color-text-muted); }
.hist-col-marker { width: 1%; white-space: nowrap; }

/* The +/-/~ marker glyph. */
.hist-marker {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5em;
  height: 1.5em;
  border-radius: var(--radius);
  font-weight: 700;
  line-height: 1;
}
.hist-status-word { font-size: 0.8rem; font-weight: 700; margin-left: var(--space-1); }

/* Row status tints (paired with the marker + status word -- color is a second cue). */
.hist-split-row.is-create { background: var(--color-success-bg); }
.hist-split-row.is-create .hist-marker { background: var(--color-success); color: var(--color-primary-text); }
.hist-split-row.is-create .hist-status-word { color: var(--color-success); }

.hist-split-row.is-update { background: var(--color-warning-bg); }
.hist-split-row.is-update .hist-marker { background: var(--color-warning); color: var(--color-primary-text); }
.hist-split-row.is-update .hist-status-word { color: var(--color-warning); }

.hist-split-row.is-delete { background: var(--color-danger-bg); }
.hist-split-row.is-delete .hist-marker { background: var(--color-danger); color: var(--color-primary-text); }
.hist-split-row.is-delete .hist-status-word { color: var(--color-danger); }
.hist-split-row.is-delete td { text-decoration: line-through; color: var(--color-text-muted); }

/* The exact changed cell within an updated row gets extra emphasis. */
.hist-split-row.is-update td.is-cell-changed { font-weight: 700; outline: 2px solid var(--color-warning); outline-offset: -2px; }

.hist-none { color: var(--color-text-muted); }
/* Visually-hidden label paired with a glyph for screen readers / non-color cue. */
.hist-vis-label {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* Admin user permissions: each report group is its own labeled card (fieldset +
   legend = the translated group name) so its grant checkbox and — for a
   program-dimensioned group — its program-scope selector read as one unit and there
   is no doubt which group a scope applies to. One form wraps every card; the single
   Save button diffs the whole desired set server-side. */
.grant-groups {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
  margin-bottom: var(--space-4);
}
.grant-group {
  margin: 0;
  padding: var(--space-3) var(--space-4) var(--space-4);
  border: var(--border);
  border-radius: var(--radius);
  background: var(--color-surface);
}
.grant-group > legend {
  padding: 0 var(--space-2);
  font-weight: 600;
}
.grant-group .grant-row { margin-bottom: 0; }
/* The program-scope selector is subordinate to the group's grant checkbox: indent it
   under the checkbox and tie it visually to the card with a left rule. The handler
   ignores program_<group> for an ungranted group, so leaving it enabled is safe. */
.grant-group .grant-scope {
  margin-top: var(--space-3);
  padding-left: var(--space-3);
  border-left: 2px solid var(--color-border);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.grant-group .grant-scope > label { font-weight: 500; }
.grant-group .grant-scope-current { margin: 0; }
.hist-empty { color: var(--color-text-muted); }
