/* =====================================================================================
   The Ledger — the design system Slice 9a promised and never delivered.

   Chosen by the owner on 2026-08-10 from five directions (`review/mockups/`). Hairlines
   instead of cards, 40px rows, near-monochrome: the only colour on a screen is a status.

   Organised by *what it styles*, not by when it was written. The file it replaces was a
   chronological log — "Slice 9a", "Slice 9f-ii", "2026-07-28 UI review" — which is how it
   ended up with `.pill` declared twice, `<select>` never styled at all, and three button
   systems whose different `margin-top` was literally why buttons did not line up on a row.

   Layer order is declared once, up front, so a later rule cannot win by accident.
   ===================================================================================== */

@layer tokens, base, layout, components, utilities, responsive;

/* Latin plus U+20A6, the naira sign. The standard `latin` subset stops short of the
   Currency Symbols block, so subsetting without naming it explicitly would strip ₦ from
   every money figure in the product. */
@font-face {
  font-family: "InterVar";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("fonts/InterVariable-subset.woff2") format("woff2");
}

@layer tokens {
  :root {
    --font: "InterVar", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    --font-mono: ui-monospace, Menlo, Consolas, monospace;

    /* Ink and rules. A near-black rather than pure black, and a warm-free grey, so the
       four status colours are the only hues that carry meaning. */
    --ink: #16181d;
    --ink-2: #5b616e;
    --ink-3: #8b909b;
    --line: #e4e6ea;
    --line-strong: #c2c6cf;

    --bg: #ffffff;
    --surface: #ffffff;
    --surface-2: #fafafa;
    --surface-3: #f4f5f7;

    /* The brand is graphite, deliberately. It used to be `#0f5132` — the same green as
       `--good`, which under §10d means *complete* — so every primary button and brand mark
       on the platform was speaking in the "complete" colour, and the status system read as
       noise. Green is spoken for. */
    --brand: #16181d;
    --brand-ink: #ffffff;
    --brand-soft: #f1f2f4;
    --link: #16181d;
    --focus: #1b4dab;

    /* §10d, and these four are the whole palette: amber = needs your attention, grey =
       simply not done yet (Unpaid and Undisbursed are normal states), green = complete,
       red = destructive or blocked. */
    --good: #0a7c42;   --good-bg: #eaf6ef;
    --amber: #8f5600;  --amber-bg: #fdf3e3;
    --bad: #b3261e;    --bad-bg: #fceceb;
    --grey: #5b616e;   --grey-bg: #f1f2f4;

    --r-sm: 2px;
    --r: 3px;
    --r-lg: 4px;
    --r-pill: 3px;

    --s-1: 4px; --s-2: 8px; --s-3: 12px; --s-4: 16px; --s-5: 20px; --s-6: 24px; --s-7: 32px;

    --fs: 13px; --fs-sm: 12px; --fs-xs: 11px;
    --fs-h1: 20px; --fs-h2: 13px; --fs-tile: 22px;

    --row-h: 40px;
    --cell-y: 9px;
    --pad: 20px;
    --measure: 1660px;

    color-scheme: light;
  }
}

@layer base {
  *, *::before, *::after { box-sizing: border-box; }
  html { -webkit-text-size-adjust: 100%; }

  body {
    margin: 0;
    background: var(--bg);
    color: var(--ink);
    font-family: var(--font);
    font-size: var(--fs);
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
  }

  h1, h2, h3 { margin: 0; font-weight: 600; letter-spacing: -.011em; }
  h1 { font-size: var(--fs-h1); line-height: 1.25; }
  h2 { font-size: var(--fs-h2); line-height: 1.3; }
  h3 { font-size: var(--fs-sm); }
  p { margin: 0 0 .6em; }
  p:last-child { margin-bottom: 0; }

  a { color: var(--link); text-decoration: underline; text-underline-offset: 2px;
      text-decoration-thickness: 1px; text-decoration-color: var(--line-strong); }
  a:hover { text-decoration-color: currentColor; }

  /* One focus rule for everything. There was exactly one `:focus-visible` selector in the
     285 lines this replaces, which is why the app felt unfinished the moment anyone used a
     keyboard. 2px at 3:1 against both grounds is the WCAG 2.2 floor. */
  :focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: var(--r-sm); }

  details summary { cursor: pointer; }
  code { font-family: var(--font-mono); font-size: .95em; }
  hr { border: 0; border-top: 1px solid var(--line); margin: var(--s-5) 0; }
}

@layer layout {
  .content { max-width: var(--measure); margin: 0 auto; padding: var(--s-6) var(--pad) 64px; }

  .page-head { display: flex; align-items: flex-start; justify-content: space-between;
    gap: var(--s-3); flex-wrap: wrap; }
  .head-right { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }
  .rowline { display: flex; align-items: center; justify-content: space-between;
    gap: var(--s-3); flex-wrap: wrap; }

  .cols { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-4); align-items: start;
    margin: var(--s-4) 0; }
  @media (max-width: 900px) { .cols { grid-template-columns: 1fr; } }

  .grid-dl { display: grid; grid-template-columns: max-content 1fr; gap: var(--s-2) var(--s-5);
    margin: 0; font-size: var(--fs-sm); }
  .grid-dl dt { color: var(--ink-2); }
  .grid-dl dd { margin: 0; font-weight: 500; font-variant-numeric: lining-nums tabular-nums; }
}

@layer components {

  /* ---- the shell ------------------------------------------------------------------
     Fourteen undifferentiated links in one flex row, mixed in with the account controls
     and carrying no active-state marker — that was "navigation buttons are raw", exactly.
     The app links and the account cluster are now two groups with a rule between them. */
  .topbar {
    display: flex; align-items: center; gap: var(--s-5);
    padding: 0 var(--pad); height: 52px;
    background: var(--surface); border-bottom: 1px solid var(--line-strong);
  }
  .brand {
    display: flex; align-items: center; gap: var(--s-2);
    color: var(--ink); font-weight: 700; font-size: var(--fs); letter-spacing: -.015em;
    text-decoration: none; white-space: nowrap;
  }
  .brand .mark {
    display: grid; place-items: center; width: 24px; height: 24px; border-radius: var(--r-sm);
    background: var(--brand); color: var(--brand-ink); font-size: 10px; font-weight: 700;
    letter-spacing: .02em;
  }
  .who { display: flex; align-items: center; gap: var(--s-1); margin-left: auto; flex-wrap: wrap; }
  .mainnav { display: flex; align-items: center; gap: 2px; flex-wrap: wrap; }

  .navlink {
    padding: 6px 9px; border-radius: var(--r-sm);
    color: var(--ink-2); font-size: var(--fs-sm); font-weight: 500;
    text-decoration: none; white-space: nowrap;
  }
  .navlink:hover { background: var(--surface-3); color: var(--ink); }
  .navlink.on { color: var(--ink); background: var(--brand-soft); font-weight: 600;
    box-shadow: inset 0 -2px 0 var(--brand); }

  .who .name { font-size: var(--fs-sm); font-weight: 600; color: var(--ink); }
  .who .role { font-size: var(--fs-xs); color: var(--ink-3); }
  .who .sep { width: 1px; height: 18px; background: var(--line); margin: 0 var(--s-2); }

  /* ---- flash messages -------------------------------------------------------------
     `base.html` emits `<li class="{{ message.tags }}">` — success / error / warning / info
     — and not one of those four selectors existed, so every message the app has ever shown
     rendered as an unstyled list item. That was the single most visibly raw thing on screen. */
  .messages { list-style: none; max-width: var(--measure); margin: var(--s-3) auto 0;
    padding: 0 var(--pad); }
  .messages li {
    position: relative; padding: 9px 12px 9px 34px; margin-bottom: var(--s-2);
    border: 1px solid var(--line); border-left-width: 3px; border-radius: var(--r);
    font-size: var(--fs-sm);
  }
  .messages li::before {
    content: ""; position: absolute; left: 12px; top: 11px; width: 13px; height: 13px;
    background: currentColor; -webkit-mask: var(--icon) center / contain no-repeat;
    mask: var(--icon) center / contain no-repeat;
  }
  /* Added by `forms.js`, never rendered by the server — with scripting off a button here would
     do nothing, and a dead control is worse than no control. Padding on the `li` above leaves
     room for it, so a long message wraps before it reaches the button rather than under it. */
  .messages li { padding-right: 34px; }
  .message-close {
    position: absolute; top: 4px; right: 6px; width: 24px; height: 24px; padding: 0;
    background: none; border: 0; border-radius: var(--r); cursor: pointer;
    font-size: 18px; line-height: 1; color: inherit; opacity: .6;
  }
  .message-close:hover { opacity: 1; background: rgb(0 0 0 / .06); }
  .message-close:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; opacity: 1; }

  .messages li.success { background: var(--good-bg); color: var(--good); border-color: var(--good); --icon: var(--i-check); }
  .messages li.error   { background: var(--bad-bg);  color: var(--bad);  border-color: var(--bad);  --icon: var(--i-x); }
  .messages li.warning { background: var(--amber-bg); color: var(--amber); border-color: var(--amber); --icon: var(--i-alert); }
  .messages li.info    { background: var(--grey-bg); color: var(--grey); border-color: var(--line-strong); --icon: var(--i-info); }

  /* ---- notices --------------------------------------------------------------------
     A bare `.notice` had padding and a radius and nothing else — no background, no colour
     — in 14 places, including the sign-in timeout whose own comment says "Amber, not red"
     while emitting a class that rendered as invisible plain text. */
  .notice {
    position: relative; padding: 9px 12px 9px 34px; margin: var(--s-2) 0;
    background: var(--grey-bg); color: var(--ink-2);
    border: 1px solid var(--line); border-radius: var(--r); font-size: var(--fs-sm);
    --icon: var(--i-info);
  }
  .notice::before {
    content: ""; position: absolute; left: 12px; top: 11px; width: 13px; height: 13px;
    background: currentColor; -webkit-mask: var(--icon) center / contain no-repeat;
    mask: var(--icon) center / contain no-repeat;
  }
  .notice.good  { background: var(--good-bg);  color: var(--good);  border-color: #b8e3cb; --icon: var(--i-check); }
  .notice.bad   { background: var(--bad-bg);   color: var(--bad);   border-color: #f2c2bf; --icon: var(--i-alert); }
  .notice.amber { background: var(--amber-bg); color: var(--amber); border-color: #ecd0a4; --icon: var(--i-alert); }

  /* The idle warning, built by `session.js` two minutes before the session ends. It borrows
     `.notice.amber` entirely — amber because §10d reserves it for *needs your attention*, which
     this is — so nothing here restates a colour. Only the placement is its own: an in-flow
     notice scrolls off the top of a long client profile, which is the page somebody would be
     reading when the clock runs down, so the one thing it must do is stay in sight. */
  #session-warning {
    position: fixed; z-index: 50; right: var(--s-5); bottom: var(--s-5);
    max-width: min(420px, calc(100vw - 2 * var(--s-5))); margin: 0;
    display: flex; align-items: baseline; gap: var(--s-2);
    box-shadow: 0 6px 20px rgb(0 0 0 / 14%);
  }
  #session-warning button { color: inherit; font-weight: 600; white-space: nowrap; }

  /* ---- the back link ---------------------------------------------------------------
     25 screens carry one, and until now every one of them was a bare `<p class="muted small">`
     with a browser-default underlined anchor — the owner's *"I don't want it raw as it is. I
     still want the links."* This component was in the direction that was chosen and simply never
     made it across into the shipped stylesheet.

     The arrow stays a literal character in the markup rather than becoming a CSS icon: three
     assertions read `"← Clients"` and `"← Approvals"` out of the rendered page, and drawing it
     here would break them to no purpose. The negative margin is optical — it pulls the link's
     padding box back so the *text* lines up with the `h1` beneath it. */
  .crumb {
    display: flex; align-items: center; gap: 4px; flex-wrap: wrap;
    margin: 0 0 var(--s-3); font-size: var(--fs-sm); color: var(--ink-3);
  }
  .crumb a {
    display: inline-flex; align-items: center; gap: 5px;
    margin-left: -7px; padding: 3px 7px; border-radius: var(--r-sm);
    color: var(--ink-2); font-weight: 600; text-decoration: none;
  }
  .crumb a + a { margin-left: 0; }
  .crumb a:hover { background: var(--surface-3); color: var(--ink); }

  /* ---- surfaces -------------------------------------------------------------------
     The Ledger has no cards. A section is a heading, a rule and its content — which is
     why the direction reads as an instrument rather than a dashboard. `.card` keeps its
     name because 92 places in the markup use it; only its appearance changes. */
  .card { background: transparent; border: 0; border-radius: 0; padding: 0; box-shadow: none; }
  .card + .card,
  .card + section,
  section + .card { margin-top: var(--s-7); }
  .card > h2 {
    padding-bottom: 6px; margin-bottom: var(--s-3);
    border-bottom: 1px solid var(--line-strong);
    font-size: var(--fs-h2); text-transform: uppercase; letter-spacing: .07em;
    color: var(--ink-2); font-weight: 600;
  }
  .card.narrow { max-width: 380px; margin: 9vh auto; }
  details.card > summary { padding-bottom: 6px; border-bottom: 1px solid var(--line-strong);
    font-size: var(--fs-h2); text-transform: uppercase; letter-spacing: .07em;
    color: var(--ink-2); font-weight: 600; margin-bottom: var(--s-3); }

  /* Cards whose *tint* is the message keep a panel, because removing it would remove the
     signal: a setup checklist, a change-request impact, a locked record, a destructive
     confirmation. These are the deliberate exceptions to "no cards". */
  .card.setup, .card.impact, .card.locked, .card.danger, .card.confirm {
    padding: var(--s-4); border: 1px solid var(--line); border-radius: var(--r);
    background: var(--surface-2);
  }
  .card.setup { background: var(--good-bg); border-color: #cfe6db; }
  .card.impact { background: var(--surface-2); }
  .card.locked { background: var(--surface-3); }
  .card.locked .uploads li { color: var(--ink-2); }
  .card.danger, .card.confirm { background: var(--bad-bg); border-color: #f2c2bf; }
  .card.confirm { background: var(--surface-2); border-color: var(--line-strong); }
  .lead { font-size: var(--fs); color: var(--ink); margin-bottom: var(--s-3); }

  /* ---- buttons --------------------------------------------------------------------
     Three parallel systems shipped: the `<button>` element, `.button` on anchors, and
     `.btn`. They had different padding, different radii and — the actual complaint —
     different `margin-top`, so a `.btn` and a `<button>` on the same row sat at different
     heights. The class names stay (75 call sites, several test-locked); they now share one
     base and one size scale, and the meanings the markup already implies are kept:
     element = primary, `.button` = secondary, `.btn` = primary, `.linkish` = ghost. */
  button, .button, .btn {
    display: inline-flex; align-items: center; justify-content: center; gap: 6px;
    margin: 0; padding: 7px 13px;
    border: 1px solid transparent; border-radius: var(--r-sm);
    font-family: inherit; font-size: var(--fs-sm); font-weight: 600; line-height: 1.2;
    text-decoration: none; white-space: nowrap; cursor: pointer;
    transition: background .12s ease, border-color .12s ease;
  }
  button, .btn { background: var(--brand); color: var(--brand-ink); }
  button:hover, .btn:hover { background: #2b2f38; }
  .button { background: var(--surface); color: var(--ink); border-color: var(--line-strong); }
  .button:hover { background: var(--surface-3); border-color: var(--ink-3); }
  .btn.sm, .button.sm { padding: 5px 10px; font-size: var(--fs-xs); }

  .linkish {
    background: none; border: 0; padding: 2px 4px; color: var(--link);
    font-weight: 600; text-decoration: underline; text-underline-offset: 2px;
    text-decoration-color: var(--line-strong);
  }
  .linkish:hover { background: var(--surface-3); text-decoration-color: currentColor; }

  button.danger, .btn.danger { background: var(--bad); color: #fff; border-color: transparent; }
  button.danger:hover { background: #98201a; }

  /* Not available right now — a server-rendered precondition (offboarding, template
     publish) and `forms.js` swapping a pressed button read the same to the person looking
     at them, so they get the same treatment. Grey, never amber: §10d reserves amber for
     *needs your attention*, and work proceeding normally is the opposite of that. */
  button[disabled], .btn[disabled], button.danger[disabled] {
    background: var(--surface-3); color: var(--ink-3);
    border-color: var(--line); cursor: not-allowed;
  }
  form[aria-busy="true"] { opacity: .85; }
  form.inline { display: inline; }

  /* ---- fields ---------------------------------------------------------------------
     `<select>` and `<textarea>` had never been styled — only `input` got padding, a border
     and a radius — so every dropdown in the product rendered as a raw browser control
     beside a designed one. That is most of what "forms are just there" meant. */
  label { display: block; margin: var(--s-3) 0; font-weight: 600; font-size: var(--fs-xs);
    color: var(--ink-2); letter-spacing: .01em; }

  input, select, textarea {
    display: block; width: 100%; margin-top: 4px; padding: 7px 9px;
    background: var(--surface); color: var(--ink);
    border: 1px solid var(--line-strong); border-radius: var(--r-sm);
    font-family: inherit; font-size: var(--fs); font-weight: 400; line-height: 1.4;
  }
  input::placeholder { color: var(--ink-3); }
  input:hover, select:hover, textarea:hover { border-color: var(--ink-3); }
  input:focus, select:focus, textarea:focus {
    outline: 2px solid var(--focus); outline-offset: 0; border-color: var(--focus);
  }
  textarea { resize: vertical; min-height: 58px; }

  select {
    appearance: none; padding-right: 30px;
    background-image: var(--i-caret);
    background-repeat: no-repeat; background-position: right 9px center; background-size: 11px;
  }
  select[multiple], select[size] { appearance: auto; background-image: none; padding-right: 9px; }

  input[type="file"] { padding: 5px 8px; color: var(--ink-2); }
  input[type="file"]::file-selector-button {
    margin-right: 9px; padding: 4px 10px;
    background: var(--surface-3); color: var(--ink);
    border: 1px solid var(--line-strong); border-radius: var(--r-sm);
    font: inherit; font-size: var(--fs-sm); font-weight: 600; cursor: pointer;
  }
  input[type="file"]::file-selector-button:hover { border-color: var(--ink-3); }

  input[type="checkbox"], input[type="radio"] { width: auto; display: inline-block; margin: 0; }
  label.check { display: flex; align-items: center; gap: var(--s-2); font-weight: 500;
    font-size: var(--fs); color: var(--ink); }
  label.check input { width: auto; margin: 0; }
  label.inline { display: inline-flex; align-items: center; gap: 6px; margin: 0; }

  /* A figure being typed is read digit by digit; grouping is what makes 12000000 legible.
     Right-aligned only while it is a *box* — once the record is saved the border goes and a
     right-aligned number drifts to the far side of the page, a hand's width from the label
     it belongs to. Read-only, it is a value, and a value sits under its label. */
  input[data-thousands] { font-variant-numeric: lining-nums tabular-nums; text-align: right; }
  input[data-thousands][readonly] { text-align: left; }

  /* Read-only until Edit (owner's ruling, 2026-08-02). A saved record's form rests in view
     mode: the fields are still fields, so nothing moves when Edit is pressed, but they stop
     looking like somewhere to type.

     Styled off `[readonly]` and `[disabled]` rather than a class, because the lock *is*
     those attributes (`ui.http.lock_state`) and a class would be a second thing to
     remember. `readonly`, never `disabled`, on text inputs — a disabled input is not
     submitted, and on the house table that would post a blank ceiling. */
  input[readonly], textarea[readonly],
  select[disabled], input[type="checkbox"][disabled] {
    background: transparent; border-color: transparent;
    border-bottom: 1px dashed var(--line-strong);
    border-radius: 0; padding-left: 0; padding-right: 0;
    box-shadow: none; cursor: default; color: var(--ink);
    opacity: 1;   /* browsers grey a disabled control out; these are values, not dead controls */
  }
  select[disabled] { background-image: none; }
  /* The old rule removed the outline here entirely, which took away the only affordance a
     keyboard user had for knowing where they were. A locked value is still focusable. */
  input[readonly]:focus-visible, textarea[readonly]:focus-visible {
    outline: 2px solid var(--focus); outline-offset: 2px;
  }

  /* Entry forms. House-type names run to "5 BEDROOM FULLY-DETACHED BUNGALOW WITH BQ" and
     ranges to eight digits with separators, so a single column truncated both. */
  .entry fieldset { border: 1px solid var(--line); border-radius: var(--r);
    margin: 0 0 var(--s-4); padding: var(--s-4); }
  .entry legend { font-weight: 600; padding: 0 6px; font-size: var(--fs-sm);
    text-transform: uppercase; letter-spacing: .06em; color: var(--ink-2); }
  /* A form that *is* the page, rather than one sitting inside a column. `--measure` is 1660px
     because a client table needs it; a single column of labels stretched to that is unreadable —
     the eye loses the line between a caption on the left and its box on the right, and every
     field looks unrelated to the one above it. 46rem holds the longest label and the widest
     value (an eight-digit figure with separators) without either touching an edge.

     Named to match `.card.narrow`, and it replaces the `style="max-width:..."` that two forms had
     grown individually — the same decision made three times in three places is how they drift. */
  .entry.narrow { max-width: 46rem; margin-inline: auto; }

  .entry.wide { display: grid; grid-template-columns: 1fr 1fr; gap: 0 var(--s-5); }
  .entry.wide label { margin: var(--s-2) 0; }
  .entry.wide .span-2 { grid-column: 1 / -1; }
  /* 900px, matching `.cols`. At 760px the two disagreed, so an iPad in portrait (768px) had
     already collapsed the page layout while still holding a two-column entry form. */
  @media (max-width: 900px) { .entry.wide { grid-template-columns: 1fr; } }
  .entry-foot { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap;
    margin-top: var(--s-4); }

  .filters { display: flex; gap: var(--s-2); margin: var(--s-4) 0; flex-wrap: wrap;
    align-items: center; }
  .filters label { margin: 0; }
  .filters input, .filters select { width: auto; margin: 0; min-width: 170px; }
  .filters input[type="search"] { min-width: 240px; }

  .choices { display: flex; gap: var(--s-3); align-items: center; flex-wrap: wrap; }
  /* ---- the client profile's Manage section (owner, 2026-08-15) ---------------------------
     Four actions that are **not the same shape** — Assignment has two selects, the fee and the
     PEN one field each, Reset a reason — laid out as flex rows on `align-items: end`. A `label`
     carries a top margin and a bare input does not, so the four buttons landed at four different
     heights and no two fields aligned. The old rule could not have worked: `end` aligns the
     *bottom* of items whose heights are decided by content nobody made equal.

     `stretch` plus a footer on `margin-top: auto` is the fix, and it is the whole fix: every
     panel fills the row's height and every button sits on the same line, whatever is above it. */
  .manage-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(248px, 1fr));
    gap: var(--s-3); align-items: stretch; }
  .manage-grid .mini {
    display: flex; flex-direction: column; margin: 0; gap: 0;
    background: var(--surface); border: 1px solid var(--line); border-radius: var(--r);
    padding: var(--s-3) var(--s-3) var(--s-3);
  }
  .manage-grid .mini h3 { margin: 0; font-size: var(--fs-sm); font-weight: 700; color: var(--ink); }
  .manage-grid .mini .hint { margin: 4px 0 0; font-size: var(--fs-xs); color: var(--ink-2);
    line-height: 1.45; }
  .manage-grid .mini label { margin: var(--s-3) 0 0; }
  /* Pushed to the bottom, which is what makes the buttons agree across panels of unequal
     height — not a margin somebody has to keep tuning as fields are added. */
  .manage-grid .mini-foot { margin-top: auto; padding-top: var(--s-3); }
  .manage-grid .mini-foot button { width: 100%; }
  /* **The destructive one reads as destructive** (§10d: red is destructive/blocked). It is the
     only action here that undoes something, and it sat in the same graphite as "Record a
     payment". `--bad-bg` is the existing soft-red token the notices already use — a tint rather
     than a new colour, so the panel is findable without shouting, and no `color-mix` (which
     appears nowhere else in this file). */
  .manage-grid .danger-panel { background: var(--bad-bg); }
  .manage-grid .danger-panel h3 { color: var(--bad); }
  .manage-grid .mini label { flex: 1; min-width: 120px; }
  .inline-grant { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: var(--s-3); align-items: end; }
  .inline-grant button { align-self: end; }
  .request-credits { display: flex; gap: var(--s-2); align-items: center; margin: var(--s-4) 0;
    flex-wrap: wrap; }
  .request-credits input { width: auto; margin: 0; flex: 1; min-width: 200px; }

  /* The live PEN check's message. Quiet until it has something to say, and reserving its
     line either way — an empty note that collapses makes the form jump as you tab. */
  .pen-note { display: block; min-height: 1em; }
  .pen-note.bad { color: var(--bad); }

  /* ---- tiles, which are also filters ----------------------------------------------
     Three dashboard tiles are links: the count was the thing you wanted to act on, and
     having no way to reach it is what invites the separate Disbursed / Unpaid screens §12
     forbids. A linked tile still has to read as a tile — no underline, no link colour. */
  .tiles { display: flex; gap: var(--s-3); flex-wrap: wrap; margin: var(--s-4) 0; }
  .tile { flex: 1 1 0; min-width: 128px; padding: 4px 0 4px var(--s-3);
    border-left: 2px solid var(--line); background: transparent; }
  .tile .n { display: block; font-size: var(--fs-tile); font-weight: 600; line-height: 1.15;
    letter-spacing: -.02em; font-variant-numeric: lining-nums tabular-nums; }
  .tile .k { display: block; margin-top: 2px; font-size: var(--fs-xs); color: var(--ink-2); }
  a.tile { text-decoration: none; color: inherit; cursor: pointer; transition: border-color .12s; }
  a.tile:hover { border-left-color: var(--ink-3); }
  a.tile:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
  a.tile.on { border-left-color: var(--brand); background: var(--brand-soft); }
  .tile.amber, .tile.flag { border-left-color: var(--amber); }
  .tile.amber .n, .tile.flag .n { color: var(--amber); }
  .balance { font-size: var(--fs-sm); color: var(--ink-2); }

  .filter-active { display: flex; gap: var(--s-2); align-items: center;
    margin: 6px 0 var(--s-3); font-size: var(--fs-sm); }
  .filter-active .chip { background: var(--brand-soft); border: 1px solid var(--line-strong);
    border-radius: var(--r-pill); padding: 2px 9px; font-weight: 600; font-size: var(--fs-xs); }

  /* ---- tables ---------------------------------------------------------------------
     Fourteen columns fit no screen, so the table scrolls — and the edge has to say so.
     Pure CSS (background-attachment: local vs scroll), so the affordance survives
     JavaScript being off, like everything else here. */
  .table-scroll {
    overflow-x: auto; border: 1px solid var(--line); border-radius: var(--r);
    background:
      linear-gradient(to right, var(--surface) 40%, rgba(255,255,255,0)) left / 30px 100% no-repeat local,
      linear-gradient(to left, var(--surface) 40%, rgba(255,255,255,0)) right / 30px 100% no-repeat local,
      radial-gradient(farthest-side at 100% 50%, rgba(15,20,30,.18), rgba(15,20,30,0)) right / 13px 100% no-repeat scroll,
      var(--surface);
  }
  table.grid { width: 100%; border-collapse: separate; border-spacing: 0; font-size: var(--fs-sm); }
  table.grid th {
    position: sticky; top: 0; z-index: 2;
    padding: var(--s-2) 10px; background: var(--surface-2);
    color: var(--ink-3); font-size: var(--fs-xs); font-weight: 600;
    text-transform: uppercase; letter-spacing: .06em; text-align: left; white-space: nowrap;
    border-bottom: 1px solid var(--line-strong);
  }
  table.grid td {
    padding: var(--cell-y) 10px; height: var(--row-h);
    border-bottom: 1px solid var(--line); white-space: nowrap; vertical-align: middle;
  }
  table.grid tbody tr:last-child td { border-bottom: 0; }
  table.grid tbody tr:hover td { background: var(--surface-2); }
  table.grid td.num, table.grid th.num { text-align: right; }
  table.grid td.num { font-variant-numeric: lining-nums tabular-nums; letter-spacing: -.005em; }
  table.grid .pin, table.grid th.pin {
    position: sticky; left: 0; z-index: 3; background: var(--surface);
    border-right: 1px solid var(--line); box-shadow: 6px 0 8px -8px rgba(0,0,0,.32);
  }
  table.grid th.pin { z-index: 4; background: var(--surface-2); }
  table.grid tbody tr:hover .pin { background: var(--surface-2); }
  /* A row's link is the row's subject, so it carries weight rather than an underline: in a
     14-column table an underline on every first cell reads as noise, not as an affordance. */
  table.grid td a { text-decoration: none; font-weight: 600; }
  table.grid td a:hover { text-decoration: underline; }
  td.empty { text-align: center; padding: var(--s-7); color: var(--ink-3); }
  tr.faded { opacity: .55; }
  tr.amber-row td { background: var(--amber-bg); }
  .reason-cell { max-width: 280px; white-space: normal; }
  .ord { width: 3.5rem; text-align: center; }
  .bank { text-align: right; }

  /* Repeatable rows (onboarding's house table) need room to read while typing. */
  table.grid input, table.grid select { width: 100%; margin: 0; padding: 5px 7px; }
  table.grid td:has(input), table.grid td:has(select) { padding: 5px 6px; }

  /* Sortable column headers (2026-08-11). A link, not a script: the sort is a URL, so it
     survives JavaScript being off, the back button works and a sorted view can be shared.
     The arrow is the current state; the link carries the *next* one. */
  th.sortable { padding: 0; }
  th.sortable > a {
    display: flex; align-items: center; gap: 5px;
    padding: var(--s-2) 10px; color: inherit; text-decoration: none;
    font: inherit; letter-spacing: inherit; text-transform: inherit;
  }
  th.sortable.num > a { justify-content: flex-end; }
  th.sortable > a:hover { color: var(--ink); background: var(--surface-3); }
  th.sortable > a::after {
    content: ""; width: 9px; height: 9px; flex: none; opacity: .28;
    background: currentColor;
    -webkit-mask: var(--i-sort) center / contain no-repeat;
    mask: var(--i-sort) center / contain no-repeat;
  }
  th[aria-sort="ascending"] > a::after { --i-sort: var(--i-up); opacity: 1; }
  th[aria-sort="descending"] > a::after { --i-sort: var(--i-down); opacity: 1; }
  th[aria-sort]:not([aria-sort="none"]) { color: var(--ink); }

  table.mini { border-collapse: collapse; font-size: var(--fs-sm); }
  table.mini td, table.mini th { padding: 4px 10px; }
  table.mini .old { color: var(--ink-2); }
  table.mini .new { font-weight: 600; }
  table.mini tr.moved .new { color: var(--amber); }
  table.mini.figures td.num { text-align: right; font-variant-numeric: tabular-nums; }

  /* ---- status pills ---------------------------------------------------------------
     Icon *and* colour, never colour alone — WCAG 2.2, and a screen that means something to
     someone who cannot separate green from amber. The icon is a `::before` mask so no
     markup changes: `<span class="pill grey">Being produced` is test-locked. */
  .pill {
    display: inline-flex; align-items: center; gap: 4px;
    padding: 1px 7px 1px 6px; border: 1px solid transparent; border-radius: var(--r-pill);
    font-size: var(--fs-xs); font-weight: 600; line-height: 1.6; white-space: nowrap;
  }
  .pill::before {
    content: ""; width: 10px; height: 10px; flex: none; background: currentColor;
    -webkit-mask: var(--icon) center / contain no-repeat;
    mask: var(--icon) center / contain no-repeat;
  }
  .pill.grey  { background: var(--grey-bg);  color: var(--grey);  border-color: #d5d9df; --icon: var(--i-dot); }
  .pill.green { background: var(--good-bg);  color: var(--good);  border-color: #b8e3cb; --icon: var(--i-check); }
  .pill.amber { background: var(--amber-bg); color: var(--amber); border-color: #ecd0a4; --icon: var(--i-alert); }
  .pill.red   { background: var(--bad-bg);   color: var(--bad);   border-color: #f2c2bf; --icon: var(--i-x); }
  .pill.click { cursor: pointer; font-family: inherit; justify-content: center; }
  .pill.click:hover { filter: brightness(.97); }
  .pill-form { display: inline; margin: 0; }

  /* A toggle reserves the width of its **longer** label, so changing state never changes the
     cell. Without this, Unpaid→Paid narrows the cell, and because every table here is
     auto-layout that reflows all the columns to its right — which the owner saw as the button
     jumping sideways when they marked a pay run.
     `em`, not `rem`: the pill's font-size grows at the small-screen step and the reserved
     width has to grow with it. Two values because the pairs differ — Unpaid/Paid against
     Undisbursed/Disbursed. */
  .pill-staff_pay .pill.click,
  .pill-agent_pay .pill.click { min-width: 6.4em; }
  .pill-disbursement .pill.click { min-width: 9.4em; }
  .badge { display: inline-block; font-size: var(--fs-xs); font-weight: 600;
    background: var(--grey-bg); color: var(--grey); padding: 1px 7px;
    border-radius: var(--r-pill); margin-left: 6px; }

  /* ---- profile, packs, uploads ----------------------------------------------------- */
  .profile-head { display: flex; align-items: flex-start; justify-content: space-between;
    gap: var(--s-4); flex-wrap: wrap; }
  .profile-head .house { margin: 2px 0; font-weight: 600; color: var(--ink); }
  /* **Four stages should read as four things** (owner, 2026-08-15). `.card` is deliberately flat
     platform-wide — transparent, no border, no padding, a spacing container rather than a panel —
     which is right for a page of one card and wrong for a stack of four that each carry a
     heading, a status, a meta line and a row of buttons. With only a bottom rule between them the
     section read as one long list, and stage 3's buttons looked like they belonged to stage 4.
     Scoped to `.pack` so nothing else on the platform gains a box. */
  .card.pack { background: var(--surface); border: 1px solid var(--line);
    border-radius: var(--r); padding: var(--s-4); }
  .card.pack + .card.pack { margin-top: var(--s-3); }
  /* The heading carries the stage name; the status pill sits with it rather than below, so the
     first line of every card answers "what is this and where is it up to". */
  .card.pack h2 { margin: 0 0 4px; font-size: var(--fs); }
  .card.pack .pack-head { gap: var(--s-3); }
  /* The button row keeps clear of the status text above it at every width — it wraps under on a
     narrow screen, where without this it landed flush against the meta line. */
  .card.pack .pack-actions { margin-left: auto; }
  @media (max-width: 700px) {
    .card.pack .pack-actions { margin-left: 0; margin-top: var(--s-3); width: 100%; }
    .pack-actions > form { flex: 1 1 auto; }
    .pack-actions > form > button { width: 100%; }
  }
  /* These wrap. Measured at 390px before the rule was added: the stage cards' button row was
     the one thing still pushing the page sideways — `.pack-actions` held a 182px button that
     could not move down, so the document ended up 21px wider than the screen. Every other
     button row in the file already wrapped; these three were simply missed. */
  .pack-row, .pack-head { display: flex; align-items: flex-start;
    justify-content: space-between; gap: var(--s-4); flex-wrap: wrap; }
  .pack-actions { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }
  /* **The four stage cards line up** (owner, 2026-08-15). Same markup on every card, but the
     label is not: "Generate" is eight characters, "Re-run & re-assemble" twenty and stage 4's
     "Build the full pack" nineteen — so the action column was ragged from card to card and stage
     4, the odd one out in wording, looked misaligned rather than merely wider. A floor wide
     enough for the longest label makes every primary action the same size; `white-space: nowrap`
     above already keeps them on one line, and `min-width` (not `width`) means a longer label in
     future grows rather than truncates. */
  .pack-actions > form > button { min-width: 11rem; }
  .uploads { list-style: none; padding: 0; margin: var(--s-2) 0 0; display: grid; gap: 5px;
    font-size: var(--fs-sm); }
  .uploads li { display: flex; align-items: center; gap: var(--s-2); }

  /* ---- Manage documents (owner, 2026-08-15: "forms and buttons are all disfigured") -------
     Three separate faults, and the first is the one that made the screen look broken.

     1. the upload form was a bare `.entry`, which sets no width — so `input, select { width:
        100% }` stretched a type selector and a file field across the full 1660px `--measure`;
     2. its button sat directly after a paragraph with no footer, hard against the text above;
     3. the list of held documents was a tick and a sentence per line, so nothing lined up and a
        missing signature was no easier to spot than a present one.

     None of it needed a new component: `.entry.wide` and `.entry-foot` already existed and are
     what every settings form uses. What is added here is the width cap and the list grid. */
  .upload-form { max-width: 54rem; }
  /* The file input is the one control that cannot be made to match the others — the browser
     draws its button — so it gets the same height as a select rather than being left short. */
  .upload-form input[type="file"] { padding: 6px 8px; }
  .upload-form .field-help { margin: var(--s-1) 0 0; line-height: 1.5; }

  /* One row per document: mark, name, file, consequence. `auto auto 1fr auto` so the names form
     a column the eye can run down, which a flex row of sentences never does. */
  .uploads .upload-row { display: grid; gap: var(--s-2) var(--s-3); align-items: baseline;
    grid-template-columns: 1.25rem minmax(9rem, auto) minmax(0, 1fr) auto;
    padding: 7px 0; border-bottom: 1px solid var(--line); }
  .uploads .upload-row:last-child { border-bottom: 0; }
  .uploads .mark { color: var(--good); font-weight: 700; text-align: center; }
  /* **Grey, not amber** (§10d): a client nobody has reached yet has no signature, and that is a
     normal state rather than something demanding attention. The row says the consequence. */
  .uploads .upload-row.is-absent .mark { color: var(--ink-3); }
  .uploads .file { overflow-wrap: anywhere; }
  .uploads .note { color: var(--ink-3); text-align: right; white-space: nowrap; }
  @media (max-width: 760px) {
    /* The consequence wraps under the name rather than being squeezed into a fourth column. */
    .uploads .upload-row { grid-template-columns: 1.25rem 1fr; }
    .uploads .note { text-align: left; white-space: normal; grid-column: 2; }
    .uploads .file { grid-column: 2; }
  }

  .request-head { display: flex; justify-content: space-between; gap: var(--s-4);
    align-items: flex-start; }
  .request-actions { display: flex; gap: var(--s-2); }
  .reason { font-style: italic; color: var(--ink); margin: 6px 0 0; }
  .impact-flags { display: flex; gap: var(--s-2); margin: 4px 0 var(--s-3); flex-wrap: wrap; }

  /* ---- checklists, timelines, sequences -------------------------------------------- */
  .setup-head { display: flex; justify-content: space-between; align-items: flex-start;
    gap: var(--s-4); }
  .setup-steps { list-style: none; padding: 0; margin: var(--s-2) 0 0; display: flex;
    gap: var(--s-4); flex-wrap: wrap; }
  .setup-steps li { font-size: var(--fs-sm); color: var(--ink-2); }
  .setup-steps li.done { color: var(--good); font-weight: 600; }

  .timeline { list-style: none; padding: 0; margin: var(--s-4) 0;
    border-left: 2px solid var(--line); }
  .timeline .stage { position: relative; padding: var(--s-2) 0 var(--s-3) var(--s-5); }
  .timeline .dot { position: absolute; left: -7px; top: 11px; width: 12px; height: 12px;
    border-radius: 50%; background: var(--line-strong); }
  .timeline .stage.done .dot { background: var(--good); }
  .timeline .stage.amber .dot { background: var(--amber); }
  .timeline .stage.pending .dot { background: var(--line-strong); }

  .sequence { list-style: none; padding: 0; margin: var(--s-2) 0 0; display: grid;
    gap: var(--s-4); }
  .sequence li { display: grid; grid-template-columns: 30px 1fr; gap: var(--s-3);
    align-items: start; }
  .sequence .mark { width: 28px; height: 28px; border-radius: 50%; display: grid;
    place-items: center; background: var(--surface-3); color: var(--ink-2); font-weight: 700;
    font-size: var(--fs-sm); }
  .sequence li.done .mark { background: var(--good); color: #fff; }
  .sequence p { margin: 2px 0 var(--s-2); }

  .pairing { list-style: none; padding: 0; margin: var(--s-2) 0 0; display: grid; gap: 6px; }
  .pairing li { display: grid; grid-template-columns: 140px 1fr 2.5rem; align-items: center;
    gap: var(--s-2); font-size: var(--fs-sm); }
  .pairing .bar { background: var(--surface-3); border-radius: var(--r-pill); height: 10px;
    overflow: hidden; }
  .pairing .fill { display: block; height: 100%; background: var(--brand); }
  .pairing .pair-n, .pairing .pair-name { font-variant-numeric: tabular-nums; }
  .pairing .pair-n { text-align: right; }

  .fieldlist { list-style: none; padding: 0; margin: var(--s-2) 0 0; display: grid; gap: 5px; }
  .fieldlist li { display: flex; align-items: center; gap: var(--s-2); }
  .fieldlist code { background: var(--surface-3); padding: 1px 5px; border-radius: var(--r-sm); }

  .tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--line-strong);
    margin: var(--s-4) 0 var(--s-2); }
  .tab { padding: var(--s-2) var(--s-3); text-decoration: none; color: var(--ink-2);
    border-bottom: 2px solid transparent; font-weight: 600; font-size: var(--fs-sm); }
  .tab.on { color: var(--ink); border-bottom-color: var(--brand); }
  .tab:hover { color: var(--ink); }

  /* The 2FA secret is 32 base32 characters with no spaces in it, so without an explicit break
     it cannot wrap — 422px of unbreakable text that scrolled the enrolment page sideways on a
     phone. 2FA is mandatory for admins and supervisors, so that was the *first* screen a new
     user met on a phone. The `provisioning_uri` beside it already used `.wrap`; this was missed. */
  .secret code { font-size: var(--fs-h1); letter-spacing: .06em; word-break: break-all; }

  /* ---- the cockpit's masked figures -----------------------------------------------
     The owner's commercial numbers should not greet a room the moment they sign in.
     Rendered masked server-side and revealed by `forms.js`, so with the script absent they
     stay hidden — for a privacy control that is the safe direction, and the opposite of the
     progressive enhancement everything else here follows.

     `blur` rather than `visibility:hidden` on purpose: the layout does not move when the
     figures appear, so revealing does not reflow the page under the cursor. The threat is
     somebody reading the screen, not somebody reading the DOM. */
  .figures-masked .tile .n,
  /* The summary panel's figures joined the mask when the panel was added (2026-08-14). It sits
     inside `#figures`, but the mask names the elements it blurs rather than blurring the
     container — so a new kind of figure is unmasked by default, and the one on this screen is
     platform revenue. The chart's bars are left alone: they carry shape, not a readable number,
     and the `<details>` table under them is `td.num`, already covered above. */
  .figures-masked .summary .figure-value,
  .figures-masked .summary .figure-note,
  .figures-masked table.grid td.num { filter: blur(.42rem); user-select: none; }
  .figures-masked .tile .n { color: var(--ink); }
  #figures-toggle { white-space: nowrap; }

  /* ---- the confirmation dialog ------------------------------------------------------
     `forms.js` opens this instead of costing a page load. The server's confirmation page
     (`ui/confirm.py`) is unchanged and is still what appears when scripting is
     unavailable: this is the shortcut, never the guarantee. */
  dialog.confirm-dialog {
    border: 1px solid var(--line-strong); border-radius: var(--r-lg);
    padding: var(--s-5) var(--s-6); max-width: 34rem;
    background: var(--surface); color: var(--ink);
    box-shadow: 0 12px 40px rgba(0, 0, 0, .22);
  }
  dialog.confirm-dialog::backdrop { background: rgba(15, 23, 42, .45); }
  dialog.confirm-dialog h2 { margin: 0 0 var(--s-2); font-size: var(--fs-h1);
    text-transform: none; letter-spacing: -.011em; color: var(--ink); border: 0; padding: 0; }
  dialog.confirm-dialog p { margin: 0 0 var(--s-5); color: var(--ink-2); }
  dialog.confirm-dialog .rowline { justify-content: flex-end; gap: var(--s-2); }
}

@layer components {
  /* The landing summary (owner's item 7, 2026-08-14). Money first, drawn by the server — see
     `ui/charts.py` for why there is no charting library. */
  .summary .figures { display: flex; flex-wrap: wrap; gap: var(--s-6); align-items: baseline; }
  .summary .figure { display: flex; flex-direction: column; gap: 2px; }
  .summary .figure-value { font-size: var(--fs-tile); font-weight: 600; line-height: 1.15;
    font-variant-numeric: tabular-nums; }
  .summary .figure-value.strong { color: var(--ink); }
  .summary .figure-label { font-size: var(--fs-xs); color: var(--ink-2); }
  .summary .figure-note { font-size: var(--fs-xs); color: var(--ink-3); }

  .summary .chart { margin: var(--s-5) 0 0; }
  /* `height` in CSS with a non-uniform `preserveAspectRatio` so the bars stretch to the panel
     rather than the viewBox dictating the height at every width. */
  /* **The chart is capped in width, not in height** (corrected 2026-08-16).
     Dropping the fixed 120px was right — the SVG preserves its aspect ratio now, so a forced
     height would letterbox it and squash the value labels. But `auto` means the *width* decides
     the height, and on a maximised window the container is ~1600px against a 782-wide viewBox:
     the chart resolved to **~430px tall** for six bars, one of which had data, and swallowed the
     page. The owner's screenshot is the evidence.
     `max-height` would be the obvious fix and is the wrong one: `preserveAspectRatio` fits the
     drawing *inside* its viewport, so a 1600 x 240 box would render an 886px chart with 350px of
     dead space either side. Capping the **figure** keeps the proportions honest and takes the
     axis labels and the numbers table with it, because they are inside it. */
  .summary .chart { max-width: 46rem; }
  .summary svg.bars { width: 100%; height: auto; display: block; }
  /* Gridlines: present enough to read a value against, faint enough that the bars stay the
     subject. The top line is the one carrying the maximum, so it earns a little more. */
  .summary .grid { stroke: var(--line); stroke-width: 1; }
  .summary .grid-top { stroke: var(--line-strong); }
  .summary .axis { stroke: var(--ink-3); stroke-width: 1.5; }
  .summary .tick { fill: var(--ink-3); font-size: 11px; font-variant-numeric: tabular-nums; }
  /* The figure on each bar. Tabular numerals so a column of them lines up, and the current
     month a shade darker to match its bar rather than by changing hue (§10d). */
  .summary .bar-value { fill: var(--ink-2); font-size: 11.5px; font-weight: 600;
    font-variant-numeric: tabular-nums; }
  .summary .bar-value.now { fill: var(--ink); }
  /* **Graphite, not a status colour.** §10d gives green, amber, red and grey their meanings, so
     a chart that painted its bars green would be speaking in "complete" about a month. The brand
     is graphite for exactly this reason (see the token block), and the emphasis below is weight
     rather than hue. */
  .summary .bar { fill: var(--line-strong); }
  /* The current month, so "ahead or behind" reads without counting along the axis.
     **Mid-graphite, not the brand** (owner, 2026-08-15): `--brand` is `#16181d`, near black, and
     against `--line-strong` `#c2c6cf` it did not read as emphasis — it read as a black slab
     dropped into a pale chart, which is what the owner saw on August. `--ink-2` is still a step
     up in weight from the other bars and still says nothing in the status palette, which is the
     property §10d cares about. Emphasis, not alarm. */
  .summary .bar-now { fill: var(--ink-2); }
  /* **Inset to match the plot area**, or the month labels drift left of the bars they name. The
     SVG is a 782-wide viewBox whose 720-wide plot starts at x=56, so the gutters are 56/782 and
     6/782 — percentages, because the SVG scales with the container and a pixel inset would only
     be right at one width. */
  .summary .chart-axis { display: flex; margin-top: var(--s-1);
    padding-left: 7.16%; padding-right: 0.77%; }
  .summary .chart-axis span { flex: 1 1 0; text-align: center; font-size: var(--fs-xs);
    color: var(--ink-2); }
  .summary .chart-axis .now { color: var(--ink); font-weight: 600; }
  .summary .chart-table { margin-top: var(--s-3); }
  .summary .chart-table table { margin-top: var(--s-2); }

  /* ---- the credit composition bar (integrity, 2026-08-14) --------------------------
     One track, segmented, drawing *granted = consumed + remaining*. Greys and a graphite,
     not the status palette: §10d spends green, amber, red and grey on meaning, and a green
     "not yet used" slice would be saying *complete* about credits nobody has spent. The
     ordering carries the emphasis instead — consumption first, darkest for allocations. */
  .segbar { display: flex; width: 100%; height: 22px; margin: var(--s-4) 0 var(--s-2);
    border-radius: var(--r-sm); overflow: hidden; background: var(--surface-3); }
  .segbar .seg { display: block; height: 100%; }
  .seg-allocation_consume { background: var(--ink); }
  .seg-pen_edit_consume { background: var(--ink-2); }
  .seg-reallocation_consume { background: var(--ink-3); }
  .seg-expiry { background: var(--line-strong); }
  .seg-remaining { background: var(--brand-soft); }

  .seglegend { display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-5);
    list-style: none; margin: 0 0 var(--s-2); padding: 0; font-size: var(--fs-sm); }
  .seglegend li { display: flex; align-items: center; gap: var(--s-2); }
  .seglegend .swatch { width: 10px; height: 10px; border-radius: 2px; flex: none;
    border: 1px solid var(--line); }

  /* ---- the Overview's lower panels (owner, 2026-08-15) -----------------------------
     Disbursement, credits, and the two category breakdowns. Two columns where there is room,
     one where there is not: each panel is a self-contained answer, so they tile rather than
     needing an order. `auto-fit` with a floor wide enough for a PFA's full name — a bar with
     "Trustfund Pensions Limited" wrapped over two lines beside it reads as a fault. */
  .panels { display: grid; grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr));
    gap: var(--s-4); margin-top: var(--s-5); align-items: stretch; }
  /* **The bug the owner saw** (2026-08-16): *"Credit tab seems to be higher up than the rest."*
     `.card + .card { margin-top: 32px }` above is a rule for cards **stacked vertically**, and
     every panel here is a `.card` — so inside this grid the second, third and fourth each took
     32px of top margin and the **first took none**. Whichever panel rendered first sat 32px
     proud of the row: Credits on the owner's page, Disbursement on the supervisor's, which is
     exactly what the two screenshots showed.
     The grid's own `gap` is the spacing; the stacking margin has no business inside it. */
  .panels > .card { margin-top: 0; }
  .panels .panel { background: var(--surface); border: 1px solid var(--line);
    border-radius: var(--r); padding: var(--s-4); }
  .panels .panel h2 { margin: 0; font-size: var(--fs); }
  .panels .panel > p { margin: 4px 0 0; }
  /* One rule for both kinds of visual. They had different top margins — `.segbar` 12px against
     `.pairing`'s 8px — so a bar and a list started four pixels apart even with equal captions. */
  .panels .panel .segbar,
  .panels .panel .pairing { margin-top: var(--s-3); }
  /* **The name column is not 140px here** (2026-08-16). `.pairing` was written for the agent
     roster, where the names are people's. These are institutions — "Adamawa Mortgage Bank 200207"
     is 28 characters — and every one of them wrapped to two lines, which is half of why the deck
     looked ragged. One flexible column, truncated with the full name on hover, and the count
     column keeps its width so the numbers stay in line. Scoped to `.panels`, so the roster the
     pattern was designed for is untouched. */
  .panels .pairing li { grid-template-columns: minmax(0, 1fr) 5rem 2.5rem; }
  .panels .pairing .pair-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}

@layer utilities {
  /* Present to a screen reader, absent to the eye. The summary's heading is here because the
     panel is identified by its figures visually and needs a name programmatically. */
  .visually-hidden {
    position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
    overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
  }

  .muted { color: var(--ink-2); }
  .small { font-size: var(--fs-sm); }

  /* A note explaining a form sat directly beneath its Save button, close enough to read as part
     of the control rather than as a comment on it (owner, 2026-08-14). Thirteen screens had it.
     One rule rather than thirteen edits, so the next such paragraph is spaced without anybody
     remembering — and in `utilities`, after `components`, so it wins over the element defaults
     without an `!important` or a specificity contest. */
  form + .muted,
  form + .notice,
  .rowline + .muted { margin-top: var(--s-5); }
  .mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
  .wrap { word-break: break-all; }
  .nowrap { white-space: nowrap; }
  .right { text-align: right; }
  .span-2 { grid-column: 1 / -1; }

  /* Column minimums for the two *editable* house tables, which were inline `style` attributes
     on the `<th>`s. An inline style cannot be reached by a media query — and these tables hold
     text inputs, so on a phone you were typing into a field half off-screen behind a sticky
     column. As classes they can stand down when there is no room. */
  .minw-lg { min-width: 19rem; }
  .minw-md { min-width: 9rem; }
  .minw-sm { min-width: 7rem; }
  .minw-note { min-width: 12rem; }

  /* Icons, as mask sources. Inline SVG in a custom property keeps them out of the markup
     entirely, so a pill or a notice gains its icon without any template changing — which
     matters because several of those strings are asserted by the suite. */
  :root {
    --i-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 8.5 6.5 12 13 4.5' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    --i-x: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 4l8 8M12 4l-8 8' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
    --i-alert: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 2.4 15 13.6H1z' fill='none' stroke='black' stroke-width='1.8' stroke-linejoin='round'/%3E%3Cpath d='M8 6.4v3.2' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3Ccircle cx='8' cy='11.8' r='.9' fill='black'/%3E%3C/svg%3E");
    --i-info: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='6.4' fill='none' stroke='black' stroke-width='1.8'/%3E%3Cpath d='M8 7.4v3.8' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3Ccircle cx='8' cy='4.9' r='.9' fill='black'/%3E%3C/svg%3E");
    --i-dot: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3.4' fill='black'/%3E%3C/svg%3E");
    --i-up: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 3l5 6H3z' fill='black'/%3E%3C/svg%3E");
    --i-down: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 13 3 7h10z' fill='black'/%3E%3C/svg%3E");
    --i-sort: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1.5 12 6H4zM8 14.5 4 10h8z' fill='black'/%3E%3C/svg%3E");
    --i-caret: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 4.5 6 8l3.5-3.5' fill='none' stroke='%23888f9c' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  }

  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; }
  }

  @media print {
    .topbar, .filters, .pack-actions, .head-right, .messages { display: none; }
    .table-scroll { overflow: visible; border: 0; background: none; }
    body { font-size: 11px; }
  }
}

/* =====================================================================================
   Small screens (2026-08-11).

   Mobile was never specified, never deferred and never mentioned — not in the SRS, the
   wireframes, CLAUDE.md or the session log. The one line anybody could point at defers a
   *native mobile app*, which is a different question, and is probably why nobody asked.

   Everything below is either a fix for something genuinely broken at 390px or a density
   step for a scale that was tuned at 1660px. The product's saving grace is that all 21
   tables were already wrapped in `.table-scroll`, so none of them was ever the thing
   pushing the page sideways — the top bar was.
   ===================================================================================== */

@layer responsive {

  /* ---- the sticky header, which had never worked --------------------------------------
     `table.grid th` has said `position: sticky; top: 0` since it was written, and it has never
     stuck to anything on any screen size. `overflow-x: auto` forces the computed `overflow-y`
     to `auto`, which makes `.table-scroll` the sticky element's scrollport — and that container
     has no height, so it never scrolls vertically and the header never has anything to stick
     against. The column headings simply scrolled away on a long list.

     Capping the height is the only fix that actually works: `overflow-y: clip` would stop it
     being a *scroll* container but the header would still have no vertical range to stick
     within. Wide screens only — on a phone an inner scroll region fighting the page scroll is
     worse than losing the header, and the row count that makes a header matter is a desktop
     problem anyway. `max-height` caps; a short table is unaffected. */
  @media (min-width: 900px) {
    .table-scroll { max-height: 72vh; }
  }

  /* ---- table column tiers ------------------------------------------------------------
     Fourteen columns fit no phone, and dragging a ~1,800px table through a ~170px window is
     not a feature. Columns retire in reverse order of what anyone acts on (owner's ranking:
     figures first, then the clerical toggles, then the reference fields).

     A class per tier on the `<th>` **and** its `<td>`s, never `nth-child`: the column count
     changes with `admin_view` and `can_use_documents`, so a positional selector would point
     at the wrong column on three of the four variants — silently, and differently per role. */
  @media (max-width: 1099px) { .t5 { display: none; } }
  @media (max-width: 899px)  { .t4 { display: none; } }
  @media (max-width: 699px)  { .t3 { display: none; } }
  @media (max-width: 479px)  { .t2 { display: none; } }

  /* ---- the shell --------------------------------------------------------------------
     `.topbar` had `height: 52px` and no `flex-wrap`, with three children whose nowrap
     minimums summed to more than a phone is wide. That did two things at once: it scrolled
     **every page in the product** sideways, and it laid the wrapped nav rows out at negative
     y — where nothing can be scrolled to, so several destinations could not be tapped at all.

     The nav becomes its own full-width row that scrolls horizontally rather than wrapping.
     One swipeable strip keeps every destination reachable and keeps the bar one row tall. */
  @media (max-width: 900px) {
    .topbar {
      flex-wrap: wrap; height: auto; min-height: 52px;
      padding: 8px var(--pad); column-gap: var(--s-3); row-gap: 6px;
    }
    /* **`min-width: 0` is the load-bearing line here.** A flex item's default `min-width` is
       `auto`, which means min-content — so the account cluster refused to shrink below the
       width of its own longest unbroken row and pushed the page sideways instead. Wrapping
       was already allowed; it could never take effect while the item would not narrow. */
    .topbar > * { min-width: 0; }
    .mainnav {
      order: 3; width: 100%; flex-wrap: nowrap;
      overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: none;
    }
    .mainnav::-webkit-scrollbar { display: none; }
    .who { margin-left: auto; flex-wrap: wrap; row-gap: 2px; }
  }

  @media (max-width: 480px) {
    /* The mark still says which product this is; the full name does not fit beside a name
       and three account links, and it is the least load-bearing text on the screen. */
    .brand span:not(.mark) { display: none; }
    .who .role { display: none; }
  }

  /* ---- fields -----------------------------------------------------------------------
     **16px is not a design choice.** iOS Safari zooms the whole page when a focused field is
     under 16px, and does not zoom back out when it blurs — so every form became a sequence of
     pinch-outs. The viewport meta deliberately does not carry `user-scalable=no`, because
     suppressing zoom to dodge this would take pinch-zoom away from everyone who needs it. */
  @media (max-width: 768px) {
    input, select, textarea { font-size: 16px; }
  }

  /* ---- touch targets ----------------------------------------------------------------
     Measured before changing: buttons were 30.4px, `.linkish` 18.4px and `.pill.click`
     **21.6px** — the last is the pay/disbursement toggle, three to a row, and the most-tapped
     control in the product. Two of those were under WCAG 2.2 §2.5.8's 24px floor. 44px is the
     comfortable target rather than the legal one. */
  @media (max-width: 700px) {
    button, .button, .btn {
      min-height: 44px; padding: 11px 16px; font-size: var(--fs);
    }
    .btn.sm, .button.sm { min-height: 40px; padding: 9px 13px; font-size: var(--fs-sm); }
    .linkish { min-height: 44px; padding: 11px 8px; }
    .pill { padding: 5px 11px 5px 9px; font-size: var(--fs-sm); }
    .pill::before { width: 12px; height: 12px; }
    .pill.click { min-height: 40px; }
    input[type="checkbox"], input[type="radio"] { width: 20px; height: 20px; }

    /* The pinned name column held the client's name *and* a 12-digit PEN on one nowrap line —
       about 220px, or 56% of a 390px screen, permanently frozen. The PEN takes its own line
       and the column stops being sticky: there is not enough width left to give any away. */
    table.grid .pin, table.grid th.pin {
      position: static; box-shadow: none; border-right: 0;
    }
    table.grid td.pin { white-space: normal; }
    table.grid td.pin span { display: block; }

    /* The two editable house tables demanded 704–896px of horizontal scroll while you typed. */
    .minw-lg, .minw-md, .minw-sm, .minw-note { min-width: 0; }

    .filters input[type="search"] { flex: 1 1 100%; min-width: 0; }
    .filters select { flex: 1 1 45%; min-width: 0; }
  }

  /* ---- density ----------------------------------------------------------------------
     The Ledger was chosen and tuned at 1660px, where 13px body text and 40px rows read as
     precision. At arm's length on a phone they read as small. The tokens step up; nothing
     else has to know. */
  @media (max-width: 600px) {
    :root {
      --fs: 14px; --fs-sm: 13px; --fs-xs: 12px;
      --fs-h1: 21px; --fs-tile: 24px;
      --row-h: 46px; --cell-y: 12px; --pad: 14px;
    }
    /* A grid, not a flex row. `flex: 1 1 0` with `min-width: 104px` gave each tile about
       104px while `₦16,493,739.44` needs ~150 and cannot break — so on the client profile the
       four figures were drawn **on top of one another**, and pushed the page 39px wider than
       the screen. `minmax(0, 1fr)` is the part that matters: a plain `1fr` floors at
       min-content and would do the same thing again. */
    .tiles { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-3); }
    .tile { min-width: 0; }
    /* Sized to the viewport rather than to a guess: `₦16,800,000.00` at a fixed 20px still
       lost its last digit at 390px, and a money figure that is *nearly* right is worse than
       one that is obviously small. `overflow-wrap` stays as the backstop for a figure long
       enough to defeat even this. */
    .tile .n { font-size: clamp(15px, 4.6vw, 22px); overflow-wrap: anywhere; }
    /* The author `max-width` cancelled the UA's own margin, so the dialog sat flush to both
       screen edges — it worked, and it looked like a mistake. */
    dialog.confirm-dialog { max-width: calc(100vw - 24px); padding: var(--s-4); }
  }
}
