/* 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);
  /* One step BEYOND surface-alt (darker on light, lighter on dark), used for the
     report ROW-HOVER highlight. The emphasis rows (subtotal/section-total/total)
     rest at surface-alt, so their hover MUST land on a distinct tint or the
     highlight "disappears" on those rows -- alt2 is that clearly-distinct step, and
     normal rows (resting at surface) move surface -> alt2 on hover too. Both sides
     keep AA text contrast (dark text on the lighter light tint, light text on the
     darker dark tint). */
  --color-surface-alt2: light-dark(#d3e1f3, #204880);
  --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 is the root font size ALL rem-based sizing scales from (html reads
     it below). The per-user text-size (density) preference overrides it on <html>
     via data-text-size (like data-theme). "normal" (the default, applied here) is a
     touch smaller than the old 16px baseline -- a small, non-jarring reduction every
     user gets; "compact" (the override block below) goes smaller still, and because
     the report table sizes in rem it shrinks a bit more than the rest at each level. */
  --font-size: 15px;
  --line-height: 1.5;
  --shadow: 0 1px 3px rgb(0 0 0 / 12%);

  /* Height of the sticky app chrome (.app-topbar = .app-header + the optional
     .app-subnav). Sticky elements INSIDE the page that pin to the viewport top
     (.recon-summary, .txn-grid thead th) offset by it so they land just under the
     bar instead of behind it. Derived from the tokens that actually build each box,
     in rem, so a padding change is traceable here and html[data-text-size="compact"]
     (which changes --font-size) rescales it for free. No JS measures this (rule 12).

     --app-header-h: the header's tallest flex item is NOT the 1.75rem brand icon but
     a .btn (the New-transaction button / the always-present Sign-out button):
     its line box (--line-height * 1rem) + var(--space-2) padding top+bottom + its
     1px border top+bottom. Around that, the header's own var(--space-3) padding
     top+bottom and its 1px bottom border.

     --app-subnav-h: a LINKS-ONLY section bar. Its <a> is inline, so the link's own
     padding and 2px marker border do NOT grow the line box -- the row is one line
     box + the bar's var(--space-1) padding top+bottom + its 1px bottom border.
     Bars that carry FILTER controls instead (reports, /accounts, the register) are
     taller and wrap to a second line (p26.86), so --topbar-h under-states them; that
     is harmless today because those pages' sticky elements (the .report-scroll
     theads) are container-relative, but a future viewport-sticky element on a filter
     page would need its own measure. */
  --app-header-h: calc(var(--line-height) * 1rem + var(--space-2) * 2 + 2px + var(--space-3) * 2 + 1px);
  --app-subnav-h: calc(var(--line-height) * 1rem + var(--space-1) * 2 + 1px);
  --topbar-h: var(--app-header-h);
}

/* The subnav is conditional (only sections with sub-pages or page controls render
   it), so the chrome's height is per-page. :has() reads that off the DOM in pure
   CSS -- the stylesheet already relies on :has() (.recon-row:has(.recon-toggle),
   .app-subnav-controls:has(.subnav-split)), so this introduces nothing new. */
body:has(.app-subnav) { --topbar-h: calc(var(--app-header-h) + var(--app-subnav-h)); }

/* 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; }

/* Per-user TEXT-SIZE (density). data-text-size is set server-side on <html> (like
   data-theme) from the user's setting (resolveTextSize) -- no flash, no inline
   script. It overrides the root --font-size token, which `html { font-size }` reads
   below, so ALL rem-based sizing (including .report-table's rem font-size) scales
   from it. "normal" keeps the :root 15px baseline (no rule needed). "compact" drops
   it a step; reports, being rem-sized, shrink slightly more at that setting. Must
   live on <html>, not <body>: custom properties inherit DOWNWARD only, so a --font-size
   set on <body> would NOT reach the `html { font-size }` rule. */
html[data-text-size="compact"] { --font-size: 14px; }

/* ------------------------------------------------------------------ *
 * 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. */

/* The app chrome (header + the optional section bar) is ONE sticky block, so both
   rows stay pinned while the page scrolls -- on /accounts, the register and every
   report the section bar carries the page's filter form, and losing it on scroll
   would be a regression. Screen-only: `position: sticky` leaking into print
   duplicates or clips a paginated report, and every other sticky rule in this file
   is already screen-scoped.

   z-index 5 sits in this file's live stack deliberately: ABOVE ordinary page content
   and the report-scroll sticky headers (1-3) so nothing paints over the bar, but
   BELOW .skip-link:focus (10) -- else the focused skip target renders behind an
   opaque bar and is invisible -- and below the combobox listboxes (30/50), so a
   dropdown opened near the top of a grid is not clipped by the nav.

   No background here: .app-header (--color-surface) and .app-subnav
   (--color-surface-alt) are each already opaque, so nothing bleeds through. */
@media screen {
  .app-topbar {
    position: sticky;
    top: 0;
    z-index: 5;
  }
  /* The skip link jumps to #main, which the bar would otherwise cover. */
  #main { scroll-margin-top: var(--topbar-h); }
}

.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; }
/* Destructive confirm (void, delete a budget plan, commit an import-row discard). The
   class was already in those templates with NO rule behind it, so every "dangerous"
   confirm rendered as a plain button; this gives it the danger tone.
   TINTED, not filled: --color-danger is a dark red in light mode and a light salmon in
   dark mode, so a solid fill has no single readable text color (there is no
   --color-danger-text token, and inventing one for two buttons is not worth it). The
   tint mirrors .alert, which solves the same problem the same way and reads red in both
   themes. The :hover rule PRESERVES the danger border -- without it `.btn:hover` (equal
   specificity, declared earlier) would repaint it primary-blue on hover. */
.btn-danger {
  background: var(--color-danger-bg);
  color: var(--color-danger);
  border-color: var(--color-danger);
}
.btn-danger:hover { border-color: var(--color-danger); }
.btn-danger:disabled,
.btn-danger[disabled] {
  background: var(--color-surface-alt);
  color: var(--color-text-muted);
  border-color: var(--color-surface-alt);
}
/* 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;
}
/* p33: the new-user create page's ADMIN LOCK. adminperms.js disables the permission
   controls an admin makes moot and tags them `perm-locked`; this dims them and their
   wrapping label so the page reads as "these no longer apply", matching the p28
   disabled-control convention. Scoped to the create form so no other disabled control
   in the app is restyled. Display only -- userCreate is the enforcement. */
.user-create-form .perm-locked { opacity: 0.5; cursor: not-allowed; }
.user-create-form label:has(.perm-locked),
.user-create-form .field:has(.perm-locked) > label { opacity: 0.6; }

/* 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-alt); }
/* Highlight the hovered account row. The chart was the only row list without one
   (.report-table tbody tr:hover, .recon-row:hover, .register-table .reg-row:hover).
   Scoped to REAL account rows: the injected type header is display-only -- no
   register link, no actions -- so highlighting it would advertise an interaction it
   does not have, the same reason .recon-row hover is scoped to rows carrying a
   toggle. (It also paints its background on the <td>, which would mask a <tr>
   background regardless.) surface-alt2 rather than surface-alt for the p28.26
   reason: the type headers REST at surface-alt, so an alt hover would make a
   hovered account row indistinguishable from a header. No cursor change -- only the
   name cell is a link, and the rest of the row is not clickable. */
.tree-table tbody tr.acct-row:not(.acct-type-header):hover { background: var(--color-surface-alt2); }
.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. Bigger and clearer than a
   bare glyph (user feedback): a boxed, primary-tinted disclosure chip so the "click to collapse
   this group's columns" affordance is obvious. The parent span cell is right-aligned (.amount),
   which would push the toggle to the RIGHT edge; float it LEFT so the chip sits at the LEFT edge
   of the group span — directly above the parent's own rollup ("Total") column, the leftmost of
   the subtree (columns are emitted in tree pre-order). ▾ = OPEN (click to collapse down to the
   group's rollup); ▸ = COLLAPSED (click to expand). CSP-safe: a plain <button> styled by class,
   no inline handler (colcollapse.js wires the click). */
.col-toggle {
  float: left;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
  cursor: pointer;
  padding: 0;
  margin-right: var(--space-2);
  color: var(--color-primary);
  font-size: 1rem;
  line-height: 1;
}
.col-toggle:hover {
  background: var(--color-primary);
  color: var(--color-primary-text);
  border-color: var(--color-primary);
}
.col-toggle:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 1px;
}
.col-toggle::before { content: "\25BE"; } /* ▾ open — click collapses to the group rollup */
.col-toggle.is-collapsed::before { content: "\25B8"; } /* ▸ collapsed — click expands */
/* 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; }
/* p31 refinement 3: PROGRAM-STATEMENT subtree column boundaries. reports.go stamps
   data-col-depth="N" on a PARENT program column's header <th>(s) AND every body <td> in that
   column — N is the program's tree depth (root = 0). Because columns are emitted in tree
   pre-order, a parent's column is the LEFTMOST column of its own subtree, so a solid border-left
   there runs down from the header through the whole body to the bottom of the table, bounding
   that subtree's column block. This REPLACES the old alternating background tint (which wrongly
   read Admin/Fundraising/programs as alternating peers). Real border, not box-shadow: box-shadow
   does not survive the sticky-header seam (see the header override below), whereas a vertical
   cell border between two same-row cells moves with them and holds.

   A CHILDLESS program gets NO border: it bounds no subtree, and bordering every leaf turned the
   grouping back into a plain grid. Only real group boundaries are drawn.

   Weighted by DEPTH so the nesting reads: a root (depth 0) boundary is the heaviest, deeper
   boundaries progressively lighter. Depths beyond the explicit tiers fall back to the base thin
   rule (arbitrary program depth degrades gracefully). Not inside @media screen, so the boundaries
   print too (like .col-hidden).

   ORDER MATTERS: the base [data-col-depth] rule and the depth-specific [data-col-depth="N"]
   rules have EQUAL specificity (0,2,1), so the LATER rule wins. The base MUST stay ABOVE the
   depth rules — reorder it below and every depth flattens to the 1px base. */
.report-table th[data-col-depth],
.report-table td[data-col-depth] { border-left: 1px solid var(--color-border); }
.report-table th[data-col-depth="2"],
.report-table td[data-col-depth="2"] { border-left: 1px solid var(--color-text-muted); }
.report-table th[data-col-depth="1"],
.report-table td[data-col-depth="1"] { border-left: 2px solid var(--color-text-muted); }
.report-table th[data-col-depth="0"],
.report-table td[data-col-depth="0"] { border-left: 3px solid var(--color-primary); }
/* The FIXED-BLOCK separator: the divide between Account/Total/Admin/Fundraising and the program
   tree, stamped as data-col-tree-start="1" on the first program column (header + body). It used
   to be a side effect of that column's depth-0 boundary, which held only while EVERY program
   column was a boundary; now that a childless program draws nothing, a leaf first program would
   have erased the divide. Same weight as a root boundary — where both land on one cell (a parent
   first program) they agree, and this rule sits AFTER the depth rules so the winner is defined. */
.report-table th[data-col-tree-start],
.report-table td[data-col-tree-start] { border-left: 3px solid var(--color-primary); }
/* 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-text-muted);
}
/* 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-text-muted);
}
/* 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-text-muted); }
/* 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-text-muted); }

/* 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-text-muted); 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,
.recon-view-form.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,
.recon-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,
.recon-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,
  .recon-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); }
/* Reports read a touch smaller than the surrounding UI (dense figures pack better),
   and being rem-based they scale WITH the per-user text-size token -- so at "compact"
   the table shrinks a bit more than the rest of the chrome (0.9375 * a smaller root). */
.report-table { font-size: 0.9375rem; }
/* 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;
  /* Vertically CENTER cell content: if a cell wraps or a row is taller than its
     neighbours (a long label, a tall total tier), its content sits centered rather
     than pinned to the baseline/top, so mismatched-height rows read evenly. */
  vertical-align: middle;
}
/* Row-label column (the first cell of every report row) must NOT wrap, so all rows keep a
   uniform single-line height and a long account/program name never stacks. Widening the
   column is safe -- the table scrolls horizontally inside .report-scroll. */
.report-table th:first-child, .report-table td:first-child { white-space: nowrap; }
/* p33: a FREE-TEXT column (reports.Column.FreeText -- today the open-items DESCRIPTION)
   holds user prose, not a label, so it cannot be sized by its content: in the real book
   descriptions run to hundreds of characters and the column took 82% of a 1280px table,
   pushing Status/Lines/dates/amounts thousands of pixels off to the right (and, being the
   sticky frozen first column, covering the viewport so scrolling right looked broken).
   Cap it and ellipsize.

   The cap lives on a BLOCK SPAN inside the cell, not on the <td>: under table-layout:auto
   a cell's max-width is only a hint the auto algorithm may override, whereas a block box
   inside it is sized normally, so the span is what actually bounds the column. The rule
   sets its own white-space rather than inheriting from the first-child nowrap rule above,
   so the flag keeps working if a later report clamps a column that is not the first.
   The FULL text stays in the DOM and on the cell's title (find-in-page,
   copy and hover all see it) -- the truncation is presentational only; server-side
   shortening would merge distinct items, since the description is the matching key.

   TWO lines, not one. Measured on the real book (1,495 rows): a single ellipsized line
   shows ~67 characters and clips 46% of the descriptions, which is precisely where
   Spanish sentence-descriptions still differ, so many rows would read identically. Two
   lines show ~134 and clip 8%, for ~40% more table height -- the better trade on a report
   whose whole job is identifying an item by its description. A browser without
   line-clamp support degrades to a plainly wrapped cell (still capped, nothing hidden).

   Print keeps the same clamp deliberately: the printed sheet then matches what the reader
   saw on screen, and letting 400-character descriptions wrap unbounded would repaginate
   the whole report. */
.report-table .report-clamp {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  max-width: 32rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: normal;
  /* break-word, NOT anywhere: `anywhere` also drops the box's MIN-CONTENT width to one
     character, and the auto table layout then starves this column wherever space is
     tight -- on the printed page (narrower than the screen, every other column nowrap)
     the description collapsed to a two-character ribbon. break-word still breaks an
     over-long unbroken token, but leaves the longest WORD as the column's floor. */
  overflow-wrap: break-word;
}
/* p33: in a TREE report the first cell of a PARENT row also carries the disclosure
   button treetable.js injects before it, and a -webkit-box clamp span is BLOCK-level --
   which dropped every side/account/status name onto the line below its own triangle.
   An inline box keeps the clamp (and its two-line cap) while sitting beside the button.
   Scoped to .tree-name-cell, which treetable.js sets only on cells that HAVE a toggle,
   so the leaf/item rows the clamp was built for render exactly as before. */
.report-table td.tree-name-cell .report-clamp { display: -webkit-inline-box; }
/* p33: a DATE cell is an atom -- keep it on one line. Once the clamped description
   column stopped hogging the table, the freed width let the auto layout squeeze the
   First/Last columns just under a formatted date, which then broke after a hyphen
   ("2026-02-" / "03"). Money cells get the same treatment from .amount below. */
.report-table td.report-nowrap { white-space: nowrap; }
/* 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.
   Hover uses surface-alt2 (one step BEYOND surface-alt): the emphasis rows
   (subtotal/section-total/total) REST at surface-alt, so a surface-alt hover was
   invisible on them -- surface-alt2 is a distinct step, so the highlight is now
   perceptible on BOTH plain rows (surface -> alt2) and emphasis rows (alt -> alt2). */
.report-table tbody tr:hover { background: var(--color-surface-alt2); }

/* 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. Covers up to ten program-tree levels (a very deep tree); a still-deeper
     tree's overflow rows fall back to the base top:0 (harmless — no real hierarchy is that deep). */
  .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 thead tr:nth-child(7) th { top: calc((1.4rem + var(--space-2) * 2 + 2px) * 6); }
  .report-scroll .report-table thead tr:nth-child(8) th { top: calc((1.4rem + var(--space-2) * 2 + 2px) * 7); }
  .report-scroll .report-table thead tr:nth-child(9) th { top: calc((1.4rem + var(--space-2) * 2 + 2px) * 8); }
  .report-scroll .report-table thead tr:nth-child(10) th { top: calc((1.4rem + var(--space-2) * 2 + 2px) * 9); }
  .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 (surface-alt2) so the highlight spans
     the full row -- including on emphasis rows whose frozen cell rests at
     surface-alt (they too move to alt2, so the hover shows on the frozen column). */
  .report-scroll .report-table tbody tr:hover td:first-child {
    background: var(--color-surface-alt2);
  }
}

/* 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; }
  /* p33: the PER-ROW disclosure triangles are the same collapse/expand affordance as the
     button bar above -- an interactive control, not data -- so they leave the paper too.
     What a collapsed row means (a subtree the reader chose not to open) is carried by the
     indentation and by the roll-up figures, which do print. */
  .tree-toggle { display: none !important; }

  /* These two live INSIDE #main, so the chrome-stripping rule above does not reach
     them, and both are viewport-sticky on screen (offset by --topbar-h). Sticky
     positioning on paper duplicates or clips the element across page breaks, so
     unstick them explicitly -- no position:sticky reaches print.
     !important because both rules are declared LATER in this file than this print
     block and match at the same specificity, so source order would otherwise win.
     (The chrome-stripping rules above carry !important for their own reasons; this
     block already reads that way.) */
  .recon-summary,
  .txn-grid thead th { position: static !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). */
/* p35.1 the debits/credits view switch: a small inline filter bar sitting directly
   above the split table (this page has no section-bar controls row, so the switch stays
   with the table it filters rather than materializing a whole nav row). Same shape as
   the register/report filter bars -- label + select + the "updating" cue. */
.recon-view-form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-4) 0 0;
}
.recon-view-form label { font-weight: 600; font-size: 0.9rem; }
.recon-view-form select { width: auto; }
/* The table already carries its own top margin; keep the switch hugging it. */
.recon-view-form + #recon-results .recon-table { margin-top: var(--space-2); }
.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;
  /* Under the sticky app chrome, not behind it (/reconciliations/{id} renders a
     section bar, so --topbar-h resolves to header + subnav here). */
  top: var(--topbar-h);
  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 for the native title
   tooltip (matches the datefield/badge title= affordance). No inline handlers.
   It is deliberately NOT a tab stop (owner directive): browsers do not draw a title tooltip
   on keyboard focus, so the stop revealed nothing while costing a keystroke on the hottest
   entry path. The text reaches keyboard/AT users through the field it annotates instead --
   aria-describedby where there is a bound control, the accessible name otherwise. The
   matching :focus-visible border rule was removed with the tabindex: it can never match. */
.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-text-muted);
  color: var(--color-text-muted);
  font-size: 0.75em;
  line-height: 1;
  cursor: help;
  user-select: none;
  vertical-align: middle;
}
.help-tip:hover { 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: 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-alt);
  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;
  /* Under the sticky app chrome, not behind it (the editor renders no section bar,
     so --topbar-h resolves to the header alone here). */
  top: var(--topbar-h);
  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-text-muted);
}
/* p37: the Description / Memo column headers carry help tips too (the per-line override of
   the header defaults). .help-tip sizes its circle in em, and these header lines are already
   small (0.85rem, 0.75rem on the sub line), so an inherited size would shrink the glyph to
   an unreadable speck -- and to two DIFFERENT specks, one per line. Pin it in rem so both
   read the same and stay legible. */
.txn-grid-2row thead th .help-tip { font-size: 0.7rem; }
/* 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-primary-text); }

/* 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; }
/* p38: the dropdown is now a PANEL holding the listbox plus the keys hint, so the
   floating chrome (position, z-index, surface, border) moved here from the <ul> -- the
   hint has to be inside the same floating box but outside the role="listbox". */
.desc-suggest-panel {
  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);
  min-width: max-content;
}
.desc-suggestions {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 14rem;
  overflow-y: auto;
}
/* The keys hint: quiet, one line, and never a click target -- it is a legend for the
   keyboard contract, not an option. */
.desc-suggest-hint {
  margin: 0;
  padding: var(--space-1) var(--space-3);
  border-top: var(--border);
  font-size: 0.85em;
  color: var(--color-text-muted);
  white-space: nowrap;
  cursor: default;
}
.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-primary-text); }

.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 editor: separate the Save / Submit / Discard actions from the grid so the
   buttons don't hug its bottom border. .expense-actions is the ONE form's action row --
   "Save to complete later" first (so a stray Enter picks the harmless one) then the
   primary Submit; .expense-submit holds the separate discard form below it. */
#expense-grid-form .form-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
  margin-top: var(--space-3);
}
.expense-submit { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; margin-top: var(--space-3); }
/* The subsidiary picker is a plain field of the save form (no auto-submit), so it needs
   the row layout its old wrapping <form> used to get for free. */
.expense-sub-field {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
  margin-bottom: 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.
   The margin was written as var(--space-5), a token the scale never defined (it goes
   1,2,3,4,6,8), so the whole declaration was invalid at computed-value time and the
   section rendered with margin:0. Substituted --space-6, not --space-4: on this
   scale (N * 0.25rem) a "5" would be 1.25rem, exactly between the two, and --space-6
   is the value .hub-cards below already uses as the gap BETWEEN sections. Measured
   effect of the fix, in Chromium: because .hub-cards carries its own margins and
   they collapse through this box, the inter-section and trailing gaps do not move at
   all; the single change is the FIRST section's leading gap, which goes from 1rem to
   1.5rem and so matches the gap between sections. (--space-4 measures identical to
   margin:0 in every position, i.e. it would be a no-op.) */
.hub-section { margin: var(--space-6) 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); }

/* The bulk page's two downloads -- the sample template and the name reference -- read
   as a pair of links on one line rather than two runs of text jammed together. */
.import-bulk-templates { display: flex; flex-wrap: wrap; gap: var(--space-4); }

/* The bulk PREVIEW's changed half is a transaction row followed by its split rows in
   one grid (bulk-plan-detail). The split rows are the detail, not the subject, so they
   read one step quieter than the transaction they belong to and the account column is
   indented -- the same "child line" treatment the register uses. The first four cells
   of a split row are empty by construction (they belong to the transaction above), so
   nothing needs to hide them. */
.import-bulk-detail { width: 100%; border-collapse: collapse; }
.import-bulk-detail th, .import-bulk-detail td {
  padding: var(--space-1) var(--space-2);
  vertical-align: top;
}
.import-bulk-detail .num { text-align: right; white-space: nowrap; }
.import-bulk-row > td { border-top: var(--border); }
.import-bulk-txn-fill { color: var(--color-text-muted); }
.import-bulk-split > td { color: var(--color-text-muted); font-size: 0.95em; }
.import-bulk-split > td:nth-child(5) { padding-left: var(--space-4); }

/* The import REVIEW QUEUE tables (the landing roster and the per-account row list).
   They shipped with no styling at all, so they inherited the UA default -- no cell
   padding, no rules -- and every action cell stacked its three controls into a
   three-deck row. Give them the .expense-reports-table treatment (a header rule,
   per-cell padding, compact content-width columns) and lay the actions out on ONE
   line. */
.import-queue-table,
.import-queue-groups {
  width: 100%;
  border-collapse: collapse;
  margin-top: var(--space-4);
}
.import-queue-table th, .import-queue-table td,
.import-queue-groups th, .import-queue-groups 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),
   so a date never breaks across two lines and the flexible payee/memo columns take
   the slack. Status is deliberately NOT in this set: it carries the duplicate-match
   disclosure, which must wrap. */
.import-queue-table .import-cell-date,
.import-queue-table .import-cell-amount {
  width: 1%;
  white-space: nowrap;
}

/* Row actions on ONE line -- "Edit & post", the discard reason and "Discard" side by
   side rather than stacked. Same idiom as .acct-actions above, and for the same
   reason: the cell stays a real table-cell (no display:flex on a <td>, which would
   drop it out of the table layout) and its inline-block controls simply do not wrap.
   The discard <form> is a block element by default -- that alone is what pushed the
   reason input and its button onto their own lines. */
.import-cell-actions {
  width: 1%;
  white-space: nowrap;
  vertical-align: middle;
}
.import-cell-actions .btn {
  padding: var(--space-1) var(--space-2);
  font-size: 0.9rem;
  vertical-align: middle;
}
.import-cell-actions .import-discard-form {
  display: inline-block;
  margin: 0 0 0 var(--space-2);
  vertical-align: middle;
}
/* The reason input is the one control that decides whether the row reads as clean:
   left to size itself it is wider than the two buttons put together. A fixed 10rem
   holds a real short reason ("duplicate", "personal card") and keeps the whole
   cluster near the width of the Status column beside it. */
.import-cell-actions .import-discard-reason {
  width: 10rem;
  margin-right: var(--space-2);
  vertical-align: middle;
}
/* p34.6 two-step discard: the reason + commit + cancel ride in one inline span that the
   module flips with [hidden], and the opener is hidden while it is open. */
.import-cell-actions .import-discard-confirm { vertical-align: middle; }
.import-cell-actions .import-discard-cancel { margin-left: var(--space-1); }
/* [hidden] must actually hide. The UA rule is `[hidden]{display:none}` with NO
   !important, and an AUTHOR display rule beats any UA rule -- so `.import-cell-actions
   .btn{display:inline-block}` above would leave the hidden opener on screen. Same
   specificity (0,2,0), declared after, so this wins the tie without !important. */
.import-cell-actions [hidden] { display: none; }
/* A nowrap action cluster can make the table wider than a narrow viewport, so the
   queue scrolls INSIDE its own container rather than scrolling the page -- the same
   rule the reports follow (.report-scroll), minus the max-height and sticky headers
   that a short queue has no use for. */
.import-queue-scroll { overflow-x: auto; }

/* p35.4 match rules. Two surfaces: an advisory FLAG in the queue (a saved coding fits
   this line) and the OFFER above the editor grid (the rules that fire, clickable).
   The flag deliberately reads quieter than the duplicate flag beside it -- a duplicate
   is a warning, a match is a convenience -- so it borrows the muted/info tokens rather
   than a warning color, and it is never the only signal (it carries a title and a
   role="note" like its neighbor). */
.import-match-flag {
  display: inline-block;
  padding: 0 var(--space-1);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  font-size: 0.75rem;
  color: var(--color-text-muted);
  white-space: nowrap;
}

/* The offer sits between the editor header and the grid: a label plus the matching
   rules as small buttons, wrapping on a narrow viewport. */
.txn-match-rules {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-3) 0 var(--space-2);
}
.txn-match-label { color: var(--color-text-muted); font-size: 0.9rem; }
.txn-match-list {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding: 0;
  margin: 0;
}
/* The applied rule is marked by WEIGHT and a border, not by color alone (it also
   carries aria-current for anything that cannot see either). */
.txn-match-option.txn-match-applied {
  font-weight: 700;
  border-color: currentColor;
}
.txn-match-list li { display: inline-flex; align-items: center; gap: var(--space-1); }
.txn-match-capture { margin: 0 0 var(--space-3); display: flex; align-items: center; gap: var(--space-2); }

/* The app's FIRST modal, styled as a reusable `.modal` so later dialogs inherit it
   rather than re-inventing one. A native <dialog>: the UA centers it, renders the
   ::backdrop and traps focus, so all that is left here is the box and its contents. */
.modal {
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 0;
  max-width: 32rem;
  width: calc(100vw - 2 * var(--space-4));
  background: var(--color-surface);
  color: var(--color-text);
}
.modal::backdrop { background: rgb(0 0 0 / 45%); }
.modal-body { padding: var(--space-4); display: flex; flex-direction: column; gap: var(--space-3); }
.modal-heading { margin: 0; }
.modal-field { display: flex; flex-direction: column; gap: var(--space-1); }
.modal-note { margin: 0; font-size: 0.85rem; }
.modal-actions { display: flex; gap: var(--space-2); margin-top: var(--space-2); }
/* The captured splits are hidden inputs: the container must take no layout space. */
.modal-captured { display: contents; }

/* -------------------------------------------------------------------------
   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); }
