/* portal.css — single stylesheet for the Equistamp contractor portal.

   Organized as a small generic framework: tokens (colors, fonts, radii,
   shadows), base element rules, layout, reusable components (.card, .btn,
   .form, .banner, .pill, tables) and single-purpose utilities (.w-*,
   .nowrap, .text-*, .label-caps, .mt-* and .mb-*). Every class is named for
   what it is, never for a page; deliberate exceptions (the .pill.resp-*
   role hues) say so where they're defined. */

:root {
  --ink:        #1a1a1a;
  --ink-soft:   #555;
  --ink-faint:  #6b6b6b;
  --rule:       #e6e6e6;
  --bg:         #ffffff;
  --bg-soft:    #fafafa;
  --link:       #1d4ed8;
  --good:       #166534;
  --warn:       #92400e;
  --bad:        #991b1b;
  --info:       #1e40af;
  /* One two-tier tint family per status color: -soft is the surface/chip
     tint, -rule its border. Every warn/bad/good surface derives from these —
     no per-component ambers or reds. */
  --good-soft:  #dcfce7;
  --good-rule:  #bbf7d0;
  --bad-soft:   #fee2e2;
  --bad-rule:   #fecaca;
  --warn-soft:  #fef3c7;
  --warn-rule:  #fde68a;
  --info-soft:  #dbeafe;
  /* Neutral chip/band surface, one step deeper than --bg-soft. */
  --bg-mute:    #ececec;
  /* The stock-yellow "check this" accent (consent callback and kin). */
  --accent-warn: #fcd34d;
  --serif:      "Source Serif 4", Georgia, serif;
  --sans:       "Figtree", -apple-system, BlinkMacSystemFont, sans-serif;
  --mono:       ui-monospace, "SF Mono", Menlo, monospace;
  /* Structure — one scale for corners and elevation. */
  --radius-xs:  2px;
  --radius-sm:  4px;
  --radius-md:  6px;
  --radius-lg:  8px;
  --radius-xl:  10px;
  --shadow-pop:   0 8px 24px rgba(0, 0, 0, .12);
  --shadow-modal: 0 12px 40px rgba(0, 0, 0, .18);
  /* header.topbar has no explicit height — it's padding plus whatever the
     "who" cluster currently holds (env badge, avatar, name), so a longer
     name, a narrower viewport, or another badge can make it taller. Every
     in-page jump target needs scroll-margin-top at least that tall, or the
     sticky bar tucks the heading under itself on arrival. This used to be
     two independently hardcoded guesses at the same fact — payouts'
     .section-anchor at 5rem (80px), the FAQ's section.qa-section at a
     different 6rem (96px) — and neither matched the topbar's real ~96.19px.
     Both now read this ONE property instead. Sized with headroom above the
     current measured height so ordinary future variation (a slightly longer
     name, one more badge) doesn't reopen the gap; a header tall enough to
     wrap onto two lines is a layout event of its own, not something a single
     constant can chase — see the report to `main` for why that dynamic case
     is out of scope here. */
  --header-height: 6.5rem; /* 110.5px: ~14px of headroom over the measured ~96.19px */
}
*, *::before, *::after { box-sizing: border-box; }
/* Smooth scrolling is opt-OUT, not opt-in: CSS `scroll-behavior` has no
   built-in `prefers-reduced-motion` awareness (unlike `animation`/
   `transition`, which the freeze block below silences explicitly), so an
   unconditional `smooth` here animated every same-document jump — including
   a plain `<a href="#section">`'s native fragment navigation, which has no
   JS in the loop to opt out on a case-by-case basis — for readers who asked
   their OS for less motion, with nothing in this file naming it. Gating it
   here, once, is the only place a native fragment jump CAN be made to
   respect that preference; components that scroll via JS (SectionNav) make
   their own explicit "instant" call instead of relying on this. */
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}
html, body { margin: 0; padding: 0; background: var(--bg); color: var(--ink); font: 17px/1.55 var(--sans); }
a { color: var(--link); text-decoration: none; }
a:hover { text-decoration: underline; }
code { font-family: var(--mono); font-size: .9em; }

/* Layout shell — shared top bar */
.topbar {
  display: flex; align-items: center; justify-content: space-between;
  padding: 1.25rem 2rem; border-bottom: 1px solid var(--rule);
  position: sticky; top: 0; background: var(--bg); z-index: 10;
}
.brand { font: 400 1.4rem/1 var(--serif); letter-spacing: .005em; }
.topbar-right { display: flex; align-items: center; gap: 1.5rem; }
.brand a { color: var(--ink); }
.brand a:hover { text-decoration: none; }
.topnav { display: flex; align-items: center; gap: 1.25rem; font-size: .95rem; }
.topnav a { color: var(--ink-soft); }
.topnav a.active { color: var(--ink); font-weight: 500; }
/* One dropdown in the top nav (components/nav-dropdown.tsx). The trigger
   matches a plain nav link's look-and-feel (same color/weight rules,
   including .active) so collapsing several links into a group changes
   nothing about how the bar reads. The panel reuses the combobox popover's
   own positioning/surface (relative parent, absolute list, --shadow-pop,
   same radius/border) rather than inventing a second popover style. */
.nav-group { position: relative; }
.nav-group-trigger {
  display: inline-flex; align-items: center; gap: .3rem;
  border: 0; background: transparent; padding: 0; font: inherit; color: var(--ink-soft); cursor: pointer;
}
.nav-group-trigger.active { color: var(--ink); font-weight: 500; }
.nav-group-caret { font-size: .7em; }
.nav-dropdown {
  position: absolute; z-index: 10; top: calc(100% + .85rem); left: 0;
  display: flex; flex-direction: column; margin: 0; padding: .25rem; min-width: 11rem;
  background: var(--bg); border: 1px solid var(--rule); border-radius: var(--radius-md);
  box-shadow: var(--shadow-pop);
}
/* Required, not belt-and-braces: the `display` above beats the UA stylesheet's
   `[hidden] { display: none }`, so without this the menu's `hidden` attribute
   does nothing at all and every dropdown in the bar renders open at once. */
.nav-dropdown[hidden] { display: none; }
.nav-dropdown a {
  padding: .4rem .55rem; border-radius: var(--radius-sm); color: var(--ink);
}
.nav-dropdown a:hover, .nav-dropdown a:focus { background: var(--bg-soft); text-decoration: none; }
.nav-dropdown a.active { font-weight: 500; }
.who { display: flex; align-items: center; gap: .75rem; color: var(--ink-soft); font-size: .95rem; }
main { max-width: 1080px; margin: 0 auto; padding: 2.5rem 2rem 6rem; }
/* Data-heavy pages whose tables need more room than the 1080px default. */
main.page-wide { max-width: 1340px; }
footer {
  max-width: 1080px; margin: 0 auto; padding: 2rem 2rem 4rem;
  border-top: 1px solid var(--rule); color: var(--ink-faint); font-size: .9rem;
}

/* Headings */
h1.serif { font: 400 2.5rem/1.15 var(--serif); margin: 0 0 1rem; }
h1.app-h1 { font-size: 2rem; margin: 0 0 2rem; outline: none; }
h2.section { font: 400 1.6rem/1.2 var(--serif); margin: 2.5rem 0 1rem; display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
h2.section .tools { display: flex; gap: .5rem; align-items: baseline; font: 400 .9rem/1 var(--sans); }
h2.section .section-total { font: 400 .9rem/1 var(--sans); color: var(--ink-soft); }
h2.section .section-total strong { font-weight: 600; color: var(--ink); font-variant-numeric: tabular-nums; }
.section-total .ccy-sums { font-weight: 600; color: var(--ink); font-variant-numeric: tabular-nums; }
.section-rule { height: 1px; background: var(--rule); margin: 0 0 1.25rem; }
/* Collapsible section header: the title becomes a bare toggle button (looks like
   the heading text, not a control) with a leading caret. */
h2.section .section-toggle { appearance: none; background: none; border: 0; margin: 0; padding: 0; font: inherit; color: inherit; cursor: pointer; display: inline-flex; align-items: baseline; gap: .5rem; text-align: left; }
h2.section .section-toggle:hover { color: var(--ink-soft); }
h2.section .section-toggle:focus-visible { outline: 2px solid var(--accent, #5b8def); outline-offset: 3px; border-radius: 2px; }
.table-card { position: relative; }
.table-loading-overlay {
  /* Keep column names readable while the rows beneath are being replaced. */
  position: absolute; inset: 3rem 0 0; display: grid; place-items: center;
  background: color-mix(in srgb, var(--bg-soft) 72%, transparent);
  color: var(--ink-soft); font-weight: 600; pointer-events: none;
}
.settled-filters { align-items: end; gap: .8rem; }
.settled-filters label { display: grid; gap: .3rem; color: var(--ink-soft); }
.settled-filters input, .settled-filters select { min-height: 2.5rem; }
.settled-pager { display: flex; flex-direction: column; align-items: center; gap: 1.25rem; padding: 1.25rem 0 1rem; }
.settled-page-label { margin: 0; text-align: center; }
[data-pager="true"] { justify-content: center; margin: 0; }
.payment-method-tabs { display: flex; flex-wrap: wrap; gap: .5rem; border-bottom: 1px solid var(--rule); padding-bottom: 1rem; }
.payment-method-panel { padding-top: 1.25rem; }
.payment-method-panel > .charge-form { max-inline-size: 62rem; margin-inline: auto; }
.payment-batch-rows { display: grid; gap: 1.5rem; }
.payment-batch-row { border: 1px solid var(--rule); border-radius: .5rem; padding: 1.25rem; }
.payment-batch-row .payment-method-panel > .charge-form { max-inline-size: none; }
.payment-batch-form { display: grid; gap: 1.25rem; }
.payment-batch-allocations { display: grid; gap: .75rem; }
.payment-batch-allocation { display: grid; grid-template-columns: minmax(9rem, 1.1fr) minmax(9rem, 1fr) minmax(8rem, .7fr) minmax(14rem, 1.5fr); gap: .75rem; align-items: end; padding: .9rem; border: 1px solid var(--rule); border-radius: .4rem; }
.payment-batch-allocation > a { align-self: center; font-weight: 600; }
.payment-batch-allocation > .muted { align-self: center; }
.form-row-4 { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1rem; align-items: start; }
@media (max-width: 720px) { .payment-batch-allocation, .form-row-4 { grid-template-columns: 1fr; } }
.charge-account-choice { display: flex; align-items: end; gap: .75rem; max-inline-size: 40rem; }
.charge-account-choice > .field { flex: 1 1 auto; min-inline-size: 0; }
.charge-account-choice > .btn { flex: 0 0 auto; margin-bottom: .05rem; }
@media (max-width: 720px) { .charge-account-choice { align-items: stretch; flex-direction: column; } }
.form-row-2, .form-row-3, .form-row-5 { display: grid; gap: 1rem; align-items: start; }
.form-row-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.form-row-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.form-row-5 { grid-template-columns: 1fr .9fr 1fr 1fr 1fr; }
@media (max-width: 720px) { .form-row-2, .form-row-3, .form-row-5 { grid-template-columns: 1fr; } }
.section-caret { font-size: .7em; color: var(--ink-soft); line-height: 1; }

/* Buttons */
.btn {
  display: inline-flex; align-items: center; gap: .5rem;
  padding: .65rem 1.1rem; border: 1px solid var(--ink); background: var(--ink); color: #fff;
  font: 500 .95rem/1 var(--sans); border-radius: var(--radius-xs); cursor: pointer;
  transition: background .15s, color .15s, opacity .15s;
}
.btn:hover { background: #000; }
.btn:disabled { opacity: .4; cursor: not-allowed; }
.btn:disabled:hover { background: var(--ink); }
.btn.secondary { background: transparent; color: var(--ink); }
.btn.secondary:hover { background: var(--bg-soft); }
.btn.secondary:disabled:hover { background: transparent; }
.btn.small { padding: .55rem .85rem; font-size: .85rem; }

/* Demo sign-in role picker: space the stacked choices apart and give each a
   >=44px tap target so adjacent roles aren't mis-tapped on a phone (WCAG 2.5.5). */
.demo-roles { display: flex; flex-direction: column; gap: .6rem; list-style: none; padding: 0; margin: 0; }
.demo-roles .btn { min-height: 44px; }

/* Cards & data display */
.card {
  background: var(--bg-soft); border: 1px solid var(--rule); border-radius: var(--radius-md);
  padding: 1.5rem 1.75rem;
}
/* A muted footnote inside a card gets breathing room from the content above
   (the bare .muted negative margin is tuned for snugging under headings). */
.card .muted { margin: 1.25rem 0 0; }
.card.table-card {
  padding: 0; overflow-x: auto; scrollbar-width: thin; overscroll-behavior-inline: contain;
  /* Scroll-shadow technique: faint inner shadows on left/right hint that
     there's content to scroll to, and fade away as you reach each edge. */
  background:
    linear-gradient(to right, var(--bg-soft) 30%, rgba(250,250,250,0)),
    linear-gradient(to right, rgba(250,250,250,0), var(--bg-soft) 70%) 100% 0,
    radial-gradient(farthest-side at 0    50%, rgba(0,0,0,.08), rgba(0,0,0,0)),
    radial-gradient(farthest-side at 100% 50%, rgba(0,0,0,.08), rgba(0,0,0,0)) 100% 0;
  background-repeat: no-repeat;
  background-size: 40px 100%, 40px 100%, 14px 100%, 14px 100%;
  background-attachment: local, local, scroll, scroll;
}
.grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 1.1rem 2rem;
}
.field { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
/* The caps label — the one treatment for small field/section labels. Compose
   .label-caps directly; the grouped selectors give the existing containers'
   labels the same voice without extra markup. */
.label-caps, .field label, .doc-label, .filters label, .form label {
  font-size: .8rem; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-faint);
}
.field span, .field a { color: var(--ink); word-break: break-word; }
.field a.field-link { color: var(--link); }
.field span.empty { color: var(--ink-faint); font-style: italic; }
.invoice-date-fields { display: flex; flex-wrap: wrap; gap: 1.1rem 2rem; }
.invoice-date-fields > .field { flex: 0 1 auto; }
.field.num span { font-variant-numeric: tabular-nums; }
.field label { display: flex; align-items: center; gap: .3rem; }
.field-edit { border: 0; background: transparent; color: var(--ink-faint); cursor: pointer; font: inherit; line-height: 1; padding: 0; }
.field-edit:hover { color: var(--ink); }
.field-edit:disabled { cursor: wait; opacity: .5; }
.inline-field-form { align-items: center; display: flex; gap: .35rem; }
.inline-field-form input { min-width: 0; width: 100%; }

/* Documents row */
.docs { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: .75rem 2rem; }
.doc { display: flex; align-items: baseline; gap: .5rem; min-width: 0; }
.doc-label { min-width: 7em; }
.doc-value { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.doc-value.missing { color: var(--ink-faint); font-style: italic; }

/* Pill */
.pill {
  display: inline-block; padding: .15rem .55rem; border-radius: 999px;
  font-size: .8rem; line-height: 1.5; background: var(--bg-mute); color: var(--ink-soft);
}
.pill.good { background: var(--good-soft); color: var(--good); }
.pill.info { background: var(--info-soft); color: var(--info); }
.pill.warn { background: var(--warn-soft); color: var(--warn); }
.pill.bad  { background: var(--bad-soft);  color: var(--bad);  }
/* Role-colored pills (who fixes this) — a deliberate, documented exception to
   the token rule: a role is not a status, so these get three distinguishable
   hues with no semantic claim (text stays the neutral pill grey) rather than
   borrowing good/warn/bad tints they don't mean. */
.pill.resp-operator { background: #e7f0ff; }
.pill.resp-accountant { background: #f3e8ff; }
.pill.resp-engineer { background: #ffe8e8; }

/* Tables */
table { width: 100%; border-collapse: collapse; font-size: .95rem; }
th, td { padding: .65rem .9rem; text-align: left; border-bottom: 1px solid var(--rule); }
th { font-weight: 500; color: var(--ink-soft); font-size: .82rem; text-transform: uppercase; letter-spacing: .04em; white-space: nowrap; }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
td.date, td.status { white-space: nowrap; }
tbody tr:hover { background: var(--bg-soft); }

/* Fixed layout: column widths come from a <colgroup> of .w-* utilities
   instead of content sizing, so a long cell can't crush its neighbours to a
   1-char ribbon. min-width keeps columns sane on narrow screens — the
   .table-card's overflow-x then scrolls rather than crushing. */
.table-fixed { table-layout: fixed; min-width: 720px; }
/* A failed/attention row. */
tr.bad td { background: var(--bad-soft); }
/* A sortable column header (click cycles the sort). The click target is a real
   <button> inside the th (components/table.tsx) so the th keeps its implicit
   columnheader role — an explicit role="button" on the th would replace it and
   cost screen readers the column association. Stripped back to plain header
   text: a button carries its own font, colour, padding, border, background and
   centred text, none of which the header wants. `inherit` rather than repeating
   the th values, so the two can't drift apart.
   width: 100% keeps the whole cell clickable, as it was when the th itself
   carried the handler. */
th.sortable { cursor: pointer; user-select: none; white-space: nowrap; }
.sort-btn {
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  text-align: inherit;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  width: 100%;
  cursor: inherit;
}
/* On the th, not the button: hovering anywhere in the cell underlines, which is
   what the whole-cell click target promises. A text-decoration set on the th
   does NOT reach the button (decorations don't propagate into an atomic inline
   box), so it has to be named here. */
th.sortable:hover .sort-btn { text-decoration: underline; }
/* Rows that toggle something when clicked (e.g. a selection checkbox). */
tr.row-pick { cursor: pointer; }
tr.row-pick:hover td { background: var(--bg-soft); }
/* A bill may be saved before every project share is known.  Keep it in the
   table, but tint the entire row so the outstanding allocation is obvious. */
tr.vendor-payment-unallocated > td { background: var(--bad-soft); }
tr.vendor-payment-unallocated:hover > td { background: var(--bad-soft); }
/* Full-width section header row inside a grouped table (e.g. by month). */
tr.group-header td {
  background: var(--bg-mute);
  padding: 1rem 1rem .85rem;
  border-top: 2px solid var(--ink);
  border-bottom: 1px solid var(--ink);
}
tr.group-header:first-child td { border-top: none; }
tr.group-footer td { text-align: right; border-top: 1px solid var(--rule); font-weight: 600; }
tfoot .table-footer td { text-align: right; border-top: 2px solid var(--ink-faint); font-weight: 700; }
/* The panel an expanded row reveals (DataTable's rowDetail), sitting in a
   full-width row directly under its own row. It is NOT a pickable row: the
   hover tint every tbody row gets would otherwise read as "click me" over a
   form whose own controls do the clicking, so it is overridden back to the
   panel's own recessed background. The inner .card drops its border and
   radius — the row it lives in already draws the box — but keeps the padding,
   which is what holds the form off the table's edges. */
tr.row-detail > td { background: var(--bg-mute); padding: 0; }
tr.row-detail:hover > td { background: var(--bg-mute); }
tr.row-detail .card { background: transparent; border: none; border-radius: 0; }
tr.row-detail .card.table-card { background: var(--bg-soft); }
/* The panel's own first heading sits flush against the row above it. */
tr.row-detail .card > h3:first-child { margin-top: 0; }
.group-name {
  font: 600 1.05rem/1.2 var(--sans); color: var(--ink); margin-right: 1rem; font-variant-numeric: tabular-nums;
}
.group-stats {
  font-size: .85rem; color: var(--ink-soft); font-variant-numeric: tabular-nums;
}
tfoot td { font-weight: 500; background: var(--bg-soft); }

/* A keyboard focus landing inside a table can be scrolled to just behind the
   sticky topbar — Tab reaches a row's link, a sortable header, the status
   disclosure, or the table-card region itself, and the browser reveals it
   with the bare minimum scroll needed. That minimum has no idea the topbar
   exists, so the newly-focused element (and its focus ring, which IS there)
   ends up tucked under it. Same defect and same fix as .section-anchor /
   section.qa-section above: read the one --header-height instead of guessing
   a second number.
   .table-card is the ONE wrapper class every DataTable-rendered table shares
   (components/table.tsx) — this fixes every table in the app, not just the
   Paid ledger the defect was reported against, without reaching into content
   outside tables (forms, the TOC rail, modals) that wasn't shown to have the
   same problem.
   :focus-visible, not :focus: a mouse click that lands on a row-pick row or a
   link never triggers the browser's scroll-into-view and must not get an
   unrequested nudge either. */
.table-card:focus-visible,
.table-card :focus-visible {
  scroll-margin-top: var(--header-height);
}

/* Utilities — compose these before reaching for a named class. */
/* Percentage widths, mainly for <col> in fixed-layout tables. Extend the
   scale as tables need it. */
.w-9  { width: 9%; }
.w-10 { width: 10%; }
.w-11 { width: 11%; }
.w-12 { width: 12%; }
.w-14 { width: 14%; }
.w-15 { width: 15%; }
.w-16 { width: 16%; }
.w-20 { width: 20%; }
.w-22 { width: 22%; }
.w-24 { width: 24%; }
.w-28 { width: 28%; }
.w-30 { width: 30%; }
/* A table's leading selection column: one native checkbox plus the standard
   horizontal cell padding. Percentage widths waste a large, variable gap. */
.w-check { width: 3rem; }
.nowrap { white-space: nowrap; }
/* Compact numeric input (min-hours style filters). */
.input-narrow { width: 5rem; }
/* Push everything after this element to the right in a flex row. */
.mr-auto { margin-right: auto; }
/* Right-align a flex row's content (dialog/panel action rows). */
.justify-end { justify-content: flex-end; }
/* Snug a subline up under its heading. */
.mt-tight { margin-top: .35rem; }
/* Space a follow-on section down from the content above it. */
.mt-section { margin-top: 2rem; }
/* Space below a grouped page section (a heading plus its tables). */
.mb-section { margin-bottom: 2.5rem; }
/* The standard gap under a block in a page flow. */
.mb-block { margin-bottom: 1.5rem; }
/* Start a visually separated block within the same card. */
.rule-above { border-top: 1px solid var(--rule); padding-top: 1.25rem; margin-top: 1.5rem; }
/* The mirror, for a block that LEADS its card rather than closing it — the
   access section's whole-person cut, which sits above the provider trees. */
.rule-below { border-bottom: 1px solid var(--rule); padding-bottom: 1.25rem; margin-bottom: 1.5rem; }
.break-word { word-break: break-word; }
/* Break even mid-token — for unbroken machine strings (URIs, ids, hashes). */
.break-all { word-break: break-all; }
/* Wrap at any point — for long unbroken tokens ("[CLEANUP]…",
   "EquiStamp/Management_Issues") that would otherwise overflow. Text with
   natural spaces should NOT use this: it would split "EquiStamp Inc."
   mid-word. td.task shares this rule rather than carrying its own
   word-break: task names ARE exactly this shape ("[CLEANUP] …",
   "Org/Repo_Name"), and a second bespoke declaration here would drift from
   this one silently. */
.wrap-anywhere, td.task { word-break: normal; overflow-wrap: anywhere; }
/* overflow-wrap: anywhere legitimately shrinks a cell's MIN-CONTENT width
   toward a single character (that's what lets it wrap an unbroken token at
   all) — so in an auto-layout table, td.task's floor is only as wide as
   whatever slack its neighbours leave it. One more column (e.g. the gated
   table's Recheck action, absent from the no-assignee table next to it) or
   one longer neighbouring cell is enough to squeeze an ordinary multi-word
   title below its own word width, and it starts breaking one syllable per
   line instead of at spaces. This min-width is a floor under EVERY td.task,
   not just the table that happened to cross the line — table-fixed columns
   already get a floor from their .w-* <colgroup>; auto-layout ones need one
   stated directly. A cell that still doesn't fit (a genuinely unbreakable
   id/slug longer than this) keeps wrapping via overflow-wrap: anywhere
   above, never overflows; .table-card's overflow-x scrolls the table itself
   if the floor doesn't fit the viewport, same as .table-fixed's min-width.
   14rem isn't tuned to one table: it's room for two ordinary task words at
   this page's 17px root font, comfortably above the widest single word
   (118px) measured across every row in the live gated table this was
   fixed against — narrowing a neighbour instead (e.g. capping Reason's
   pill) was tried and rejected: on the live gated row the five OTHER
   columns (Contributor/Client/Type/Amount/Recheck) are already near their
   own content-driven minimums, so freed width spreads across all of them
   rather than landing on Task, and capping Reason alone only recovered
   80px to 105px — nowhere near enough. */
td.task { min-width: 14rem; }
/* For elements carrying a hover explanation — a table column's Column.title,
   or a plain title attribute outside a table. */
.cursor-help { cursor: help; }

/* The hover/focus popup (components/tooltip.tsx), which is what a table
   column's Column.title renders as. It replaces the native title tooltip:
   that one waited a fixed ~1s, could not be styled, and could not be reached
   or dismissed from the keyboard.

   .tip-pop is PORTALLED to <body> and positioned in viewport coordinates by
   the component (top/left inline), so `position: fixed` here is load-bearing
   rather than decorative — .card.table-card sets overflow-x: auto, which
   makes both axes scrollable per CSS, and any popup inside a cell would be
   clipped at the card's edge.

   pointer-events: none because the popup is never a hover target: it can
   overlap the very row it describes, and a box that ate the pointer there
   would flicker itself in and out and block the row's own click.

   white-space: pre-line renders a "\n"-joined detail (e.g. page-issues'
   forecastTooltip) as the lines it reads as in the source. */
.tip-trigger { cursor: help; }
/* IN A CELL, THE TRIGGER IS THE CELL. The trigger is an inline <span> wrapping
   the cell's own content, so it shrinks to that content — and the content is
   often one em-dash, because several columns carry a detail ONLY when their
   figure is missing (pages/payments/parts.tsx's maxPaymentNote returns a note
   exactly when max_payment is null, which <Money> renders as "—"). Measured on
   a real eight-column payments row at 1400px: the trigger was 16.1x18.0 inside
   a 155.5x48.1 cell — 3.9% of the area the native `title` attribute used to
   cover, hard against the right edge of a right-aligned .num cell, while
   .cursor-help on the <td> advertised the whole of it. The affordance and the
   behaviour disagreed by the width of a money column.

   Scoped to `td >` on purpose: .tip-trigger is also used inline inside prose
   and headings, where a block would break the line flow. In a cell there is
   nothing to flow with — the wrapper IS the cell's content — so filling it
   restores the target the column had before the tooltip migration, and text
   alignment (including .num's right) is unaffected because it is inherited.

   The cell's padding moves ONTO the trigger rather than staying outside it: a
   block child fills its parent's CONTENT box, and a td's padding is not in
   that box — leaving the trigger at ~78% of the cell width and, worse, one
   line tall in a two-line-tall cell, so the cell's own centre could still miss
   it. With the padding on the trigger the two boxes coincide exactly. Gated on
   :has() so only cells that HAVE a trigger give theirs up; a browser without
   :has() ignores both rules together and keeps the old, un-padded target
   rather than a cell with no padding at all. */
td:has(> .tip-trigger) { padding: 0; }
td > .tip-trigger { display: block; padding: .65rem .9rem; }
.tip-pop {
  position: fixed; z-index: 120; max-width: 22rem;
  padding: .45rem .6rem;
  background: var(--bg); color: var(--ink);
  border: 1px solid var(--rule); border-radius: var(--radius-md);
  box-shadow: var(--shadow-pop);
  font: 400 .85rem/1.45 var(--sans);
  /* The columns that carry a detail are usually numeric, hence right-aligned;
     the popup is prose and reads left-aligned wherever it is anchored. */
  text-align: left; white-space: pre-line;
  pointer-events: none;
}
/* A structured detail: the column's own caveat, then the row's values under a
   rule. Blocks, because .tip-pop is a span. */
.tip-lead, .tip-lines { display: block; }
.tip-lines {
  margin-top: .4rem; padding-top: .4rem; border-top: 1px solid var(--rule);
  color: var(--ink-soft); font-variant-numeric: tabular-nums;
}
/* Opt out of a bold context (e.g. an aside inside a 600-weight question). */
.fw-normal { font-weight: 400; }
.text-soft  { color: var(--ink-soft); }
.text-faint { color: var(--ink-faint); }
.text-good  { color: var(--good); }
.text-warn  { color: var(--warn); }
.text-bad   { color: var(--bad); }
.text-sm    { font-size: .85rem; }
/* Monospace run outside a <code> element (URLs, tokens). */
.mono { font-family: var(--mono); font-size: .9em; }
/* Left accent border — the stock yellow "check this" security cue. */
.accent-warn { border-left: 3px solid var(--accent-warn); }

/* Input chrome — every text control shares this box; each context sets its
   own padding and corner radius below. */
.input, .filters input, .filters select, .form input, .form select, .form textarea {
  font: inherit; border: 1px solid var(--rule); background: var(--bg);
}

/* Filters bar */
.filters {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: .75rem 1rem; align-items: end; margin-bottom: 1rem;
}
.filters label { display: flex; flex-direction: column; gap: .25rem; }
.filter-pair {
  display: grid; grid-template-columns: 1fr 1fr; gap: .5rem;
  grid-column: span 2; min-width: 0;
}
.filter-pair label { min-width: 0; }
.filters input, .filters select { padding: .5rem .6rem; border-radius: var(--radius-sm); }
/* A filter whose control is a short closed list — a status select, a yes/no —
   opts out of stretching to a full 1fr track. A bar with two filters on a
   page-wide layout gives each one half of ~1340px, so the select ends up many
   times the width of its longest option ("prospective") and reads as a text
   field. Capping the LABEL, not the select, keeps the grid track intact so the
   neighbouring controls do not reflow. */
.filters label.narrow { max-width: 12rem; }
.filters .check {
  flex-direction: row; align-items: center; gap: .5rem;
  text-transform: none; letter-spacing: 0; color: var(--ink); font-size: .95rem;
  padding-top: 1.1rem;
}

.summary {
  color: var(--ink-soft); font-size: .95rem; margin: 0 0 .9rem;
}
/* A count on the left, its row action (e.g. Download CSV) on the right, sharing
   one baseline. The inner .summary keeps its own bottom margin for spacing to
   the table below. */
.summary-row {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
}
.summary strong { color: var(--ink); font-weight: 500; font-variant-numeric: tabular-nums; }
.summary .sum-sep { color: var(--ink-faint); margin: 0 .35rem; }
.summary .sum-total-line {
  display: block; font-variant-numeric: tabular-nums; padding-left: 0;
}
.summary .sum-total-line + .sum-total-line { margin-top: .15rem; }
.summary .sum-total-label {
  display: inline-block; min-width: 3.5em; color: var(--ink-faint);
  font-size: .85rem; text-transform: uppercase; letter-spacing: .04em;
}
.empty-row { text-align: center; padding: 2rem; color: var(--ink-faint); font-style: italic; }
.weekly-hours-bars { display: grid; grid-template-columns: repeat(10, minmax(2.5rem, 1fr)); gap: .65rem; align-items: end; min-height: 12rem; }
.weekly-hours-bar { display: flex; flex-direction: column; align-items: center; gap: .35rem; min-width: 0; height: 12rem; }
.weekly-hours-value, .weekly-hours-label { font-size: .75rem; color: var(--ink-soft); font-variant-numeric: tabular-nums; white-space: nowrap; }
.weekly-hours-track { display: flex; align-items: end; width: 100%; height: 8rem; background: var(--bg-mute); border-radius: var(--radius-xs); overflow: hidden; }
.weekly-hours-fill { display: block; width: 100%; min-height: 2px; background: var(--link); }
.people-hours-chart { margin: 1rem 0; }
.people-hours-plot { overflow-x: auto; }
/* The tooltip is positioned as a percentage of this frame, which is exactly the
   box the SVG's viewBox maps onto — so the box tracks the hovered week at any
   width without measuring anything in JS. */
.people-hours-frame { position: relative; min-width: 36rem; }
.people-hours-svg { display: block; width: 100%; height: auto; overflow: visible; }
.people-hours-grid line { stroke: var(--rule); stroke-width: 1; }
.people-hours-grid text, .people-hours-label { fill: var(--ink-faint); font-size: 11px; font-variant-numeric: tabular-nums; }
.people-hours-area { fill-opacity: .82; stroke: none; }
.people-hours-guide { stroke: var(--ink-faint); stroke-width: 1; stroke-dasharray: 3 3; }
.people-hours-dot { stroke: var(--bg); stroke-width: 1.5; }
/* A whole week column is the hit target, so the focus ring has to be drawn on
   it rather than left to the browser's default outline on a transparent rect. */
.people-hours-band { outline: none; }
.people-hours-band:focus-visible { fill: var(--bg-mute); fill-opacity: .55; }
.people-hours-tooltip {
  position: absolute; top: .25rem; z-index: 2; pointer-events: none;
  display: flex; flex-direction: column; gap: .1rem;
  padding: .45rem .6rem; max-width: 16rem;
  background: var(--bg); border: 1px solid var(--rule); border-radius: var(--radius-sm);
  box-shadow: 0 2px 8px rgb(0 0 0 / .1);
  color: var(--ink-soft); font-size: .75rem; font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.people-hours-legend { display: flex; flex-wrap: wrap; gap: .45rem .9rem; margin: .25rem 0 .8rem 2.6rem; }
.people-hours-legend-item { display: inline-flex; align-items: center; gap: .35rem; color: var(--ink-soft); font-size: .78rem; }
.people-hours-legend-swatch { width: .65rem; height: .65rem; border-radius: 50%; }
.error { background: var(--bad-soft); border: 1px solid var(--bad-rule); color: var(--bad); padding: .75rem 1rem; border-radius: var(--radius-sm); margin: 1rem 0; }
.muted { color: var(--ink-faint); font-size: .85rem; margin-top: -.5rem; }
.loading { color: var(--ink-faint); font-style: italic; padding: 2rem 0; text-align: center; }
.loading::before {
  content: ""; display: inline-block; width: 1em; height: 1em;
  border: 2px solid var(--rule); border-top-color: var(--ink-soft);
  border-radius: 50%; vertical-align: -0.18em; margin-right: .6em;
  animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.hidden { display: none !important; }

/* Forms — .form stacks its rows; .form-grid lays fields out in responsive
   columns. Labels wrap their input (no for/id bookkeeping). */
.form { display: flex; flex-direction: column; gap: 1.1rem; margin-top: 1.5rem; }
/* A <fieldset> is how a set of fields is disabled as a set (person/payment-
   method.tsx disables the whole Wise and bank groups that way, rather than
   nine inputs one at a time), but the UA stylesheet does not treat it as a
   neutral box: it brings a 2px groove border, its own padding and margin, and
   `min-inline-size: min-content`. That last one is the reason this rule is not
   cosmetic — it overrides the `minmax(0, 1fr)` tracks below from outside, so a
   fieldset holding a long email or account number refuses to shrink and the
   card overflows horizontally at exactly the width where .form-grid is meant
   to collapse to one column. */
fieldset { border: 0; margin: 0; padding: 0; min-inline-size: 0; }
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.1rem 1.5rem; }
@media (max-width: 720px) { .form-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 460px) { .form-grid { grid-template-columns: 1fr; } }

/* The Wise send form has one compact amount box, with the recipient and
   corridor facts alongside it rather than below it. */
.wise-payment-details { display: grid; grid-template-columns: minmax(12rem, 18rem) minmax(0, 1fr); gap: 1rem 2rem; align-items: start; }
.wise-payment-amount input { max-inline-size: 12rem; }
.wise-payee-summary { padding-top: .15rem; }
.wise-payee-summary > :first-child { margin-top: 0; }
.wise-payment-details > .refusal { grid-column: 1 / -1; }
@media (max-width: 620px) { .wise-payment-details { grid-template-columns: 1fr; gap: .75rem; } .wise-payment-amount input { max-inline-size: none; } }
.form label { display: flex; flex-direction: column; gap: .3rem; }
.add-vendor-form > label { width: min(100%, 32rem); }
.vendor-reactivate { background: #e2f6e8 !important; border-color: #82c69a !important; }
.vendor-reactivate:hover { background: #ccefd8 !important; }
.vendor-deactivate { background: #fde5e5 !important; border-color: #e7a0a0 !important; }
.vendor-deactivate:hover { background: #facfcf !important; }
.form .opt { text-transform: none; letter-spacing: 0; font-weight: 400; }
.form input, .form select, .form textarea {
  padding: .55rem .65rem; border-radius: var(--radius-sm);
  color: var(--ink); text-transform: none; letter-spacing: 0;
}
.form input.field-invalid, .form select.field-invalid, .form textarea.field-invalid, input.field-invalid {
  border: 2px solid var(--bad) !important;
}
.field-invalid:focus {
  outline-color: var(--bad);
}
.field-error, .field .field-error, .form .field-error {
  color: var(--bad); font-size: .85rem; font-weight: 600;
}
.vendor-actions { text-align: right; }
.vendor-expenses-detail { padding: 1rem; }
.search-select { position: relative; }
.search-select > input { width: 100%; }
.search-select-list { position:fixed; z-index:200; margin:0; padding:.25rem; list-style:none; background:#fff; border:1px solid var(--rule); border-radius:var(--radius-sm); box-shadow:var(--shadow); max-height:16rem; overflow:auto; }
.search-select-list li { display:flex; justify-content:space-between; gap:.75rem; padding:.5rem .6rem; cursor:pointer; }
.search-select-list li:hover { background:var(--bg-soft); }
.search-select-list span { color:var(--ink-soft); font-size:.85rem; }
/* The expense dialog has several single-field rows.  Keep their labels
   visibly attached to the input, without inheriting an accidental centred
   alignment from an embedding surface. */
.vendor-expense-form fieldset { display: flex; flex-direction: column; gap: 1.25rem; }
.vendor-expense-form .field { gap: .4rem; }
.vendor-expense-form .field > label { text-align: left; align-self: flex-start; }
.vendor-expense-form .field-hint { margin-top: .05rem; }
.form textarea {
  resize: vertical; min-height: 9rem;
  font: .9rem/1.5 var(--mono);
}
/* A row of buttons. .form-actions is the variant snugged under form rows. */
.btn-row, .form-actions { display: flex; gap: .75rem; }
.form-actions { margin-top: .25rem; }
.team-identity-row { display: flex; align-items: end; gap: .75rem; }
.team-identity-row > label { flex: 1 1 20rem; }
.team-identity-row .form-actions { flex: 0 0 auto; margin: 0; }
.team-provider-section { margin-top: 1.25rem; }
.team-provider-section h5 { margin-bottom: .35rem; }
.team-group-picker { margin-top: .65rem; }
@media (max-width: 560px) {
  .team-identity-row { align-items: stretch; flex-direction: column; }
}
/* One transition control: a button plus the plain-language reason it is
   disabled (components/task-tables' TransitionControl). These sit directly in
   .form-actions, so each is a flex item — as a bare inline span the button and
   a multi-word reason ran together on one line with no wrapping rule of their
   own, and three of them on the approved table shared one line between them.
   A column keeps the reason under the button it explains, and the flex-basis
   lets the sentence wrap instead of stretching the row. */
.transition-actions { flex-wrap: wrap; align-items: flex-start; }
.transition-control {
  display: flex; flex-direction: column; align-items: flex-start; gap: .3rem;
  flex: 0 1 auto; min-width: 0; max-width: 22rem;
}

/* Inline checkbox label, for places outside .filters (which lays out and
   aligns its own .check labels). */
label.check {
  display: inline-flex; align-items: center; gap: .4rem;
  text-transform: none; letter-spacing: 0; color: var(--ink); font-size: .95rem;
}

/* Outcome banner for a just-completed action. The .slim variant is the inline
   per-widget version (save results, row errors): quieter, snug to its widget.
   Success must never render in the error style — a saved note reading red is a
   bug, so pick .good or .bad per outcome, not one box for both. */
.banner { padding: .8rem 1rem; border-radius: var(--radius-sm); margin: 1.75rem 0 1rem; font-weight: 500; }
.banner.good { background: var(--good-soft); color: var(--good); border: 1px solid var(--good-rule); }
.banner.bad  { background: var(--bad-soft);  color: var(--bad);  border: 1px solid var(--bad-rule); }
.banner.warn { background: var(--warn-soft); color: var(--warn); border: 1px solid var(--warn-rule); }
.banner.slim { margin: 0 0 1rem; padding: .6rem .85rem; font-size: .88rem; font-weight: 400; }

/* Sticky-TOC layout: a 220px table of contents beside the content column.
   The content is a regular <main>, so its own max-width/padding apply. */
.toc-layout {
  max-width: 1080px; margin: 0 auto; padding: 2.5rem 2rem 6rem;
  display: grid; grid-template-columns: 220px 1fr; gap: 3rem; align-items: start;
}
.toc {
  position: sticky; top: 6rem; font-size: .92rem;
  max-height: calc(100vh - 8rem); overflow-y: auto; scrollbar-width: thin;
}
.toc h2 {
  font: 500 .78rem/1 var(--sans); text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink-faint); margin: 0 0 .9rem;
}
.toc ol { list-style: none; margin: 0; padding: 0; }
.toc li { margin: 0 0 .55rem; }
.toc a { color: var(--ink-soft); display: block; line-height: 1.35; }
.toc a:hover { color: var(--ink); text-decoration: none; }

/* Page hero/lead */
.lead h1 { font: 400 2.6rem/1.15 var(--serif); margin: 0 0 .75rem; }
.lead p { color: var(--ink-soft); margin: 0 0 2rem; max-width: 60ch; }

/* A Google-Doc-backed page's rendered body (admin.gdoc_render): one
   .gdoc-body per GdocPage, holding whatever mix of section/h2-h4/p/ul-ol/
   table/a/img the source document produced — never a fixed shape the way
   the hand-written FAQ markup (.qa-section/.qa/.num, now gone) was. Scoped
   under .gdoc-body rather than bare element selectors so this never leaks
   into unrelated content that happens to contain a <table> or a <ul>. */
.gdoc-body section { margin: 0 0 2.75rem; scroll-margin-top: var(--header-height); }
.gdoc-body h2 { font: 400 1.65rem/1.2 var(--serif); margin: 0 0 .9rem; }
.gdoc-body h3 { font: 600 1.2rem/1.35 var(--serif); color: var(--ink); margin: 1.4rem 0 .5rem; }
.gdoc-body h4 { font: 600 1rem/1.4 var(--sans); color: var(--ink); margin: 1.1rem 0 .4rem; }
.gdoc-body p { color: var(--ink-soft); margin: 0 0 .9rem; }
.gdoc-body p:last-child { margin-bottom: 0; }
.gdoc-body strong { color: var(--ink); font-weight: 600; }
.gdoc-body ul, .gdoc-body ol { margin: 0 0 .9rem; padding-left: 1.4rem; }
.gdoc-body li { margin: 0 0 .3rem; }
.gdoc-body img { max-width: 100%; border-radius: var(--radius-sm); margin: .4rem 0 .9rem; }
.gdoc-body code {
  font-family: var(--mono); font-size: .88em;
  background: var(--bg-soft); border: 1px solid var(--rule); border-radius: var(--radius-sm);
  padding: .08em .35em;
}
.gdoc-body table { width: 100%; border-collapse: collapse; margin: 0 0 1.25rem; }
.gdoc-body th, .gdoc-body td { text-align: left; padding: .5rem .7rem; border-bottom: 1px solid var(--rule); }
.gdoc-body th { font-weight: 600; color: var(--ink); }
/* A checklist item the author wrote with a ballot box, now a real checkbox
   (components/gdoc-page's withCheckboxes). The bullet goes because the box IS
   the marker, and the negative indent pulls the box back into the space the
   bullet occupied so the text stays on the list's own left edge. The label is
   the whole item, so the row is one tap target rather than a 13px one; the
   grid keeps a wrapped second line aligned under the first instead of running
   back under the box. */
.gdoc-body li.gdoc-check {
  list-style: none; margin-left: -1.4rem;
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: .5rem;
  align-items: start;
}
/* start, not baseline: a checkbox has no text baseline of its own, so browsers
   fall back to its margin edge and the row sits visibly low. The small top
   margin does the optical alignment against the first line instead. */
.gdoc-body li.gdoc-check input { margin: .25em 0 0; cursor: pointer; }
.gdoc-body li.gdoc-check label { cursor: pointer; }
/* Ticked reads as done without relying on colour alone. */
.gdoc-body li.gdoc-check input:checked + label { color: var(--ink-faint); text-decoration: line-through; }

/* Callout for the ⚠️ "in progress" notes */
.note {
  display: flex; gap: .6rem; margin: .65rem 0 0;
  background: var(--warn-soft); border: 1px solid var(--warn-rule); border-left: 3px solid var(--accent-warn);
  border-radius: var(--radius-sm); padding: .7rem .9rem; font-size: .92rem; color: var(--warn);
}
.note .ico { flex: none; }
.note em { font-style: italic; }

@media (max-width: 820px) {
  .toc-layout { grid-template-columns: 1fr; gap: 1.5rem; }
  .toc { position: static; max-height: none; border-bottom: 1px solid var(--rule); padding-bottom: 1.25rem; }
  .lead h1 { font-size: 2.1rem; }
}

/* ── Narrow flows — generics the single-purpose pages compose ──────────── */
/* A 560px single-column page (login, OAuth consent). */
main.page-narrow { max-width: 560px; margin: 4rem auto; padding: 0 2rem; }
/* A quiet expandable "advanced details" disclosure. */
details.disclosure summary { cursor: pointer; color: var(--ink-soft); font-size: .9rem; }
/* The gated-table "+N more" blockers disclosure: same widget, sitting inline
   beside the row's pill instead of the page-flow indent .disclosure normally
   gets. Collapsed it is just the "+N more" summary text — a single-blocker
   row (no other_reasons, so this element is absent entirely) is unaffected. */
.gated-more { display: inline-block; vertical-align: middle; }
.gated-more ul { margin: .25rem 0 0 1.1rem; padding: 0; }
/* Key–value list — a dt/dd grid for compact metadata dumps. break-all keeps
   attacker-length tokens (URIs, ids) inside the column. */
.kv-list { display: grid; grid-template-columns: max-content 1fr; gap: .35rem 1rem; margin: 1rem 0 0; font-size: .9rem; }
.kv-list dt { color: var(--ink-faint); }
.kv-list dd { margin: 0; word-break: break-all; }

/* Panel — a soft-background card for focused one-at-a-time flows, with a
   head row (title + tools) and a right-aligned action row. */
.panel { margin-top: 1.5rem; border: 1px solid var(--rule); border-radius: var(--radius-xl); padding: 1.25rem 1.4rem; background: var(--bg-soft); }
.panel-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.panel-head h2 { margin: 0; font-size: 1.5rem; }
.panel-actions { margin-top: 1.1rem; display: flex; justify-content: flex-end; }
/* Secondary identifier under a heading (e.g. an @handle). */
.handle { display: inline-block; margin-top: .1rem; font-size: .85rem; }
/* Not-built-yet slot, visibly provisional. */
.placeholder { margin: .75rem 0 1.1rem; padding: .6rem .8rem; border: 1px dashed var(--rule); border-radius: var(--radius-lg); font-style: italic; }
/* Centered end-of-flow state ("all caught up"). */
.done-card { text-align: center; padding: 2rem 0; }
.done-card h2 { margin: 0 0 .35rem; }

/* Radio row — a legend + inline radio options, with an optional hint. */
.radio-row { border: 0; margin: 1rem 0 0; padding: 0; }
.radio-legend { font-weight: 600; font-size: .92rem; padding: 0; margin-bottom: .4rem; }
.radio-opts { display: flex; flex-wrap: wrap; align-items: center; gap: .35rem 1.2rem; }
.radio-opt { display: inline-flex; align-items: center; gap: .4rem; font-size: .9rem; }
.radio-opt input { accent-color: var(--ink); }
.radio-hint { font-size: .78rem; }

/* Slider rows — name / range / value / note rows plus an anchors legend. */
.slider-rows { display: flex; flex-direction: column; gap: .65rem; }
.slider-row { display: grid; grid-template-columns: 9rem 1fr 2.5rem minmax(0, 14rem); align-items: center; gap: .8rem; }
.slider-name { font-weight: 600; font-size: .9rem; }
.slider-range { width: 100%; accent-color: var(--ink); }
.slider-val { font-variant-numeric: tabular-nums; text-align: right; color: var(--ink-soft); font-size: .9rem; }
.slider-note { padding: .4rem .55rem; border: 1px solid var(--rule); border-radius: var(--radius-md); font: inherit; font-size: .85rem; }
.slider-anchors { margin-top: 1rem; font-size: .78rem; }
@media (max-width: 640px) {
  .slider-row { grid-template-columns: 1fr; gap: .25rem; }
  .slider-val { text-align: left; }
}

/* Busy state for buttons that fire a request. */
.btn.is-busy::before {
  content: ""; display: inline-block; width: .9em; height: .9em;
  border: 2px solid rgba(255,255,255,.4); border-top-color: #fff;
  border-radius: 50%; vertical-align: -0.12em; margin-right: .5em;
  animation: spin .8s linear infinite;
}
/* Secondary buttons are light-on-transparent, so the white spinner above is
   invisible on them — a busy Recheck/sweep button would read as a hang. Give it
   the dark ring colors the row-spinner uses so the busy state actually shows. */
.btn.secondary.is-busy::before {
  border-color: var(--rule); border-top-color: var(--ink-soft);
}
/* Busy is not unavailable. A button that fires a request is held disabled for
   the length of that request, but `.btn:disabled`'s 40% wash says "you may not
   do this" — the wrong message when the answer is "this is running". A busy
   button therefore keeps full contrast and takes a progress cursor, so the
   faded, not-allowed look is left to mean only what it should: no permission,
   or nothing selected. */
.btn.is-busy:disabled { opacity: 1; cursor: progress; }
/* Secondary buttons are outline-only, so a busy one is a bare grey rule around
   a spinner. A soft fill and a firmer border make it read as a control doing
   work. The :disabled:hover selector is repeated because .btn.secondary:disabled:hover
   is more specific than the plain class chain and would otherwise win. */
.btn.secondary.is-busy,
.btn.secondary.is-busy:disabled:hover {
  background: var(--bg-soft); border-color: var(--ink-soft); color: var(--ink);
}

/* An action row whose status line belongs to the button beside it (the payouts
   sweep). Two things it must do that a bare .form-actions does not:
   - the button's box may not depend on the text. Items align to the start, so
     a status line that appears, changes length or wraps never moves the button
     out from under a cursor about to click it.
   - the text must read as this control's status, not as page furniture: it
     stretches to the row's height and centres its own line, which puts it on
     the button's centre line rather than floating above it (which is what the
     generic .muted did here, via its negative top margin). */
.sweep-actions { align-items: flex-start; gap: .55rem; }
.sweep-note {
  align-self: stretch; display: flex; align-items: center;
  color: var(--ink-faint); font-size: .85rem;
}

/* Compact card variant for dense card lists (one record per card). */
.card.compact { padding: .7rem .9rem; margin-bottom: .5rem; }
/* A verbatim preformatted block (runbook/remedy text): preserved whitespace,
   quiet background. */
.pre-block { white-space: pre-wrap; font-size: .85rem; background: var(--bg-soft); padding: .5rem .7rem; border-radius: var(--radius-md); }
/* Optimistic "scheduling" spinner: a row moved to In-flight on click shows this
   while its batch POST is in flight. Reuses the shared `spin` keyframe. */
.row-spinner {
  display: inline-block; width: .85em; height: .85em;
  border: 2px solid var(--rule); border-top-color: var(--ink-soft);
  border-radius: 50%; vertical-align: -0.12em; margin-right: .45em;
  animation: spin .8s linear infinite;
}
/* Reduced motion: freeze every spinner, in one place. Adding a spinner without
   adding it here leaves it animating for readers who asked for less motion, so
   the list is enforced — tests/test_portal_css.py names anything missing. */
@media (prefers-reduced-motion: reduce) {
  .loading::before, .btn.is-busy::before, .row-spinner { animation: none; }
  .loading::before, .row-spinner { border-top-color: var(--rule); }
}
/* Visually hidden but announced to assistive tech (spinner's text label). */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Confirm panel — an amber are-you-sure block with per-item required notes.
   Its action row is just a .btn-row.justify-end. */
.confirm-significant {
  margin-top: 1.1rem; padding: .9rem 1rem;
  border: 1px solid var(--warn-rule); border-radius: var(--radius-lg); background: var(--warn-soft);
}
.confirm-head { margin: 0 0 .6rem; font-size: .9rem; font-weight: 600; color: var(--warn); }
.confirm-row { display: flex; flex-direction: column; gap: .3rem; margin-bottom: .7rem; }
.confirm-q { font-size: .88rem; }
.confirm-req { font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; color: var(--warn); }
.confirm-note { padding: .45rem .6rem; border: 1px solid var(--rule); border-radius: var(--radius-md); font: inherit; font-size: .9rem; background: var(--bg); }

/* Key–value table — a two-column label/value table (dependency health,
   config dumps). The base th/td rules still apply; this narrows the label
   column and tightens the rows. */
.kv-table { width: 100%; border-collapse: collapse; }
.kv-table th, .kv-table td { text-align: left; padding: .45rem .7rem; border-bottom: 1px solid var(--rule); vertical-align: top; }
.kv-table th { width: 12rem; font-weight: 500; color: var(--ink-soft); white-space: nowrap; }
.kv-table td code { word-break: break-all; }
.kv-table .empty { color: var(--ink-faint); font-style: italic; }
/* A "##" subsection heading nested under an h2.section group: lighter, with
   room above its nested content. */
h3.section-sub { font: 400 1.15rem/1.2 var(--serif); margin: 1.75rem 0 .75rem; color: var(--ink); }
/* A prominent one-line stat above the sections ("Overall · ok"). */
.stat-line { margin: 0 0 1.5rem; font-size: 1.1rem; }
.stat-line .stat-label { font-weight: 600; margin-right: .4rem; }

/* Status page — the External providers subsection's per-provider cell. The
   state pill leads; the provider's mode and then the probe latency trail it as
   subtle, muted notes, and the humanized identity sits below as a compact
   label/value list. */
.cred-head { display: flex; align-items: baseline; gap: .5rem; }
.cred-mode { color: var(--ink-soft); font-size: .82rem; }
.cred-latency { color: var(--ink-faint); font-size: .82rem; font-variant-numeric: tabular-nums; }
.cred-identity { margin: .4rem 0 0; display: grid; grid-template-columns: auto 1fr; gap: .1rem .75rem; }
.cred-identity-row { display: contents; }
.cred-identity dt { color: var(--ink-soft); font-size: .82rem; }
.cred-identity dd { margin: 0; color: var(--ink); }
/* An ok:false probe's reason, in error styling; the muted variant carries the
   ok:null "unconfigured" note. */
.cred-detail-error { display: inline-block; margin-top: .3rem; color: var(--bad); font-size: .88rem; }
.muted-inline { color: var(--ink-faint); font-size: .88rem; }

/* A page toolbar: a right-aligned strip of controls above the sections (the
   Payouts page's "QuickBooks accounts" modal trigger lives here). */
.toolbar { display: flex; justify-content: flex-end; gap: .6rem; margin: 0 0 1rem; }

/* Modal dialog — the one shared overlay primitive (components/ui Modal). A dim
   backdrop centers a scrollable panel; the panel traps focus and closes on Esc
   or a backdrop click (behaviour lives in the component, styling here). */
.modal-backdrop {
  position: fixed; inset: 0; z-index: 100;
  background: rgba(26, 26, 26, .45);
  display: flex; align-items: center; justify-content: center;
  padding: 3rem 1rem; overflow-y: auto;
}
.modal-panel {
  background: var(--bg); border: 1px solid var(--rule); border-radius: var(--radius-xl);
  width: 100%; max-width: 720px; max-height: calc(100dvh - 6rem); padding: 1.5rem 1.75rem;
  display: flex; flex-direction: column;
  box-shadow: var(--shadow-modal);
}
.modal-panel.modal-wide { max-width: 1180px; }
.modal-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }
.modal-head h2 { margin: 0 0 1rem; font-size: 1.4rem; }
.modal-body { margin-top: .25rem; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }

/* Text-control chrome as a composable class (comboboxes, dialog fields). */
.input {
  padding: .5rem .6rem; border-radius: var(--radius-md); color: var(--ink);
  text-transform: none; letter-spacing: 0;
}
.input:focus { outline: 2px solid var(--link); outline-offset: -1px; }
.input:disabled { background: var(--bg-soft); color: var(--ink-faint); }
.input.small { padding: .4rem .55rem; font-size: .9rem; }

/* Control row — a label on the left, a growing control (combobox/input), and
   an action button. Small screens wrap the label onto its own line. */
.control-row { display: flex; align-items: center; gap: .75rem; }
/* control-row-label is flex: 0 0 auto, so with no upper bound it sizes to
   its own content on one line — fine for a short label, but the QuickBooks
   account-mapping panel's task-type names are operator-authored and some run
   long ("Redwood - Software engineering: environment creation"). The sibling
   combobox below is flex: 1 1 auto and absorbs whatever the label doesn't
   take, so an unbounded label starves it. Measured live on that panel at a
   626px control-row: the "Software Cost" row (an ordinary label) split
   204px label / 337px input; the Redwood row split 522px label / 19px input
   — an unusable sliver, and the label wasn't even wrapping, just overflowing
   its intended column. The max-width below caps the label at 16rem so it
   wraps onto multiple lines instead (still fully readable, just taller —
   confirmed live: 3 lines, full text shown, no truncation) and leaves the
   input a real share of the row (measured after: 269px, close to the
   short-label baseline). 16rem was fitted to the longest label live at the
   time of this fix, not to some absolute limit — a future longer name just
   wraps taller, which is acceptable. */
.control-row-label { flex: 0 0 auto; min-width: 12rem; max-width: 16rem; display: flex; align-items: center; gap: .4rem; }
.control-row-label .muted { margin: 0; }
/* min-width: 9rem is the combobox's own floor, independent of the label cap
   above — it guarantees a usable search input even if some other sibling
   (not just a long label) ever eats into this row's width. */
.control-row .combobox { flex: 1 1 auto; min-width: 9rem; }
/* A bare .input sharing the row with its submit button — the project page's
   "add a repository" field. Same deal as the combobox above: without an
   explicit grow the flex default (0 1 auto) sizes the box to its placeholder
   and leaves the rest of the row empty, which for a field that takes a URL is
   a box narrower than the thing being pasted into it. */
.control-row .input { flex: 1 1 auto; min-width: 9rem; }
.control-row .btn { flex: 0 0 auto; }
@media (max-width: 560px) {
  .control-row { flex-wrap: wrap; }
  /* At this width the row already stacks the label onto its own full-width
     line (flex-basis: 100%), so the max-width cap above would only pinch it
     for no reason — turn it off here. */
  .control-row-label { min-width: 0; max-width: none; flex-basis: 100%; }
}

/* The account combobox: a relatively-positioned input with an absolutely-
   positioned suggestion list dropped below it. */
.combobox { position: relative; }
.combobox .input { width: 100%; }
.combobox-list {
  position: absolute; z-index: 10; top: calc(100% + 2px); left: 0; right: 0;
  margin: 0; padding: .25rem; list-style: none;
  background: var(--bg); border: 1px solid var(--rule); border-radius: var(--radius-md);
  box-shadow: var(--shadow-pop); max-height: 15rem; overflow-y: auto;
}
.combobox-list li { margin: 0; }
.combobox-option {
  display: flex; justify-content: space-between; align-items: baseline; gap: .75rem;
  width: 100%; padding: .4rem .55rem; border: 0; border-radius: var(--radius-sm);
  background: transparent; text-align: left; font: inherit; color: var(--ink); cursor: pointer;
}
.combobox-option:hover, .combobox-option:focus { background: var(--bg-soft); }
.combobox-name { font-size: .9rem; }
.combobox-meta { font-size: .78rem; color: var(--ink-faint); white-space: nowrap; }
.combobox-create { color: var(--link); font-size: .9rem; font-weight: 500; }

/* In-page section rail (components/section-nav) — a persistent list of a
   page's sections beside its content, with the section being read highlighted.
   The rail is a NAVIGATION LANDMARK, not decoration: its entries are real
   links, so focus, hover and current-ness are all styled here rather than
   simulated.

   Nothing in this block animates or transitions, deliberately: the jump is an
   instant scroll, so there is nothing here for the prefers-reduced-motion
   enumeration above to have to name. Keep it that way — anything animated
   added here has to be added there too. */
.with-rail { display: grid; grid-template-columns: 13rem minmax(0, 1fr); gap: 0 2.25rem; align-items: start; }
/* min-width:0 or a wide table in the body refuses to shrink and blows the
   grid past the viewport. */
.with-rail-body { min-width: 0; }
.section-nav {
  position: sticky; top: 5rem;
  max-height: calc(100vh - 7rem); overflow-y: auto;
}
.section-nav-caption {
  margin: 0 0 .5rem; font: 400 .72rem/1 var(--sans);
  text-transform: uppercase; letter-spacing: .06em; color: var(--ink-faint);
}
.section-nav-list {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column;
  border-left: 1px solid var(--rule);
}
/* The 2px transparent border and the -1px pull put the active marker exactly
   over the list's own rule, so the line does not jog as the highlight moves. */
.section-nav-link {
  display: flex; flex-direction: column; gap: .05rem;
  padding: .3rem 0 .3rem .65rem; margin-left: -1px;
  border-left: 2px solid transparent;
  color: var(--ink-soft); font-size: .88rem; line-height: 1.3;
}
.section-nav-link:hover { color: var(--ink); text-decoration: none; border-left-color: var(--rule); }
.section-nav-link:focus-visible { outline: 2px solid var(--link); outline-offset: -2px; }
.section-nav-link[aria-current] { color: var(--ink); font-weight: 600; border-left-color: var(--ink); }
.section-nav-count { font-size: .76rem; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.section-nav-link[aria-current] .section-nav-count { color: var(--ink-soft); }

/* A region the rail can point at. scroll-margin keeps a jumped-to section
   clear of the sticky topbar. The focus outline is dropped because the rail
   moves focus HERE on a jump purely so the keyboard carries on from the
   section: it is a region, not a control, and ringing a screen-tall box adds
   nothing the scroll and the highlighted entry have not already said. */
.section-anchor { scroll-margin-top: var(--header-height); }
.section-anchor:focus { outline: none; }

/* Narrow viewports get no side rail — a fixed column beside the content is a
   bug on a phone. The same links become a horizontally scrollable strip of
   chips above the content, which keeps the landmark, the tab order and the
   current-section marker while costing no width. */
@media (max-width: 1000px) {
  .with-rail { grid-template-columns: minmax(0, 1fr); }
  .section-nav { position: static; max-height: none; overflow: visible; margin-bottom: 1.25rem; }
  .section-nav-list {
    flex-direction: row; gap: .4rem; border-left: 0;
    overflow-x: auto; padding-bottom: .35rem;
  }
  .section-nav-link {
    flex: 0 0 auto; flex-direction: row; align-items: baseline; gap: .35rem;
    margin-left: 0; padding: .3rem .65rem; white-space: nowrap;
    border: 1px solid var(--rule); border-radius: var(--radius-lg);
  }
  .section-nav-link:hover { border-color: var(--ink-faint); }
  .section-nav-link[aria-current] { border-color: var(--ink); }
}

/* Person access — the revoke modal's ways-to-cut chooser. Stacked, one option
   per line, so each server-composed action sentence reads whole; the shared
   .radio-opts row layout is inline and would fold them together. */
.access-revoke-options { border: 0; margin: 1rem 0 0; padding: 0; }
.access-revoke-option {
  display: flex; align-items: baseline; gap: .45rem;
  font-size: .9rem; margin: .3rem 0;
}
.access-revoke-option input { accent-color: var(--ink); flex: 0 0 auto; }

/* ── Person → Payment method: the per-kind tab panels ──────────────────── */
/* Each tab states what is on file for its kind and then offers the form, and
   when a method already exists the form sits behind a "replace what is there"
   <details>. That disclosure follows a facts grid inside a card, so it needs
   the separating rule and the breathing room a bare <details> has none of.
   Scoped to .payment-panel so no other .disclosure on the portal moves. */
.payment-panel > details.disclosure {
  border-top: 1px solid var(--rule); margin-top: 1.5rem; padding-top: 1rem;
}
.payment-panel > details.disclosure > summary { color: var(--ink); font-weight: 500; }

/* Person page → Onboarding section (pages/person/onboarding.tsx).
   Three small things the checklist table did not need before: the note naming
   which personal channel is being checked, the category picker that sits in a
   row's action cell beside its Create button, and the issue block's link row in
   the two states that have no form. Everything else in the section reuses
   .kv-table, .pill, .form and .form-actions unchanged. */
.onboarding-channel-note { margin-top: -.35rem; }
.onboarding-category { display: inline-flex; align-items: center; gap: .35rem; margin-right: .5rem; }
.onboarding-category select { max-width: 14rem; }
.onboarding-category-note { display: inline-block; margin-right: .5rem; max-width: 22rem; }
.onboarding-issue { padding-top: .75rem; }
.onboarding-issue .pill { margin-left: .35rem; }

/* ── H2: the paginated table's page-range label ────────────────────────────
   components/table's PaginatedTable renders its footer as a bare
   `.form-actions` flex row: Rows select, ‹ Prev, "1–5 of 5", Next ›. Two
   inherited rules conspired to lift that middle label off the buttons' centre
   line — `.form-actions` never sets align-items (so items stretch, and a
   stretched span paints its single line at the TOP of a button-height box),
   and `.muted` carries `margin-top: -.5rem`, spacing meant for a paragraph
   sitting under a heading rather than for a flex item between two buttons.
   Scoped to `.table-pager` so the fix reaches every paginated table and no
   other `.form-actions` row. Same shape as `.sweep-note` above, and for the
   same reason. */
.form-actions.table-pager { align-items: center; }
.table-pager-range { margin-top: 0; }

/* ── I2: the client page's Billing section ─────────────────────────────────
   Two different things share one card and must not read as one list: who to
   TALK TO (koseki's contacts, with a route per channel) and where the invoices
   GO (invoice_prefs.send_to / cc_to, which is a list because a client can want
   several). `.route-list` is the read view of either — one address per line,
   no bullets, so a three-recipient client does not read as prose — and
   `.route-row` is the edit view's input-plus-remove pair. */
.route-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .15rem; }
.route-list li { margin: 0; }
.route-kind { color: var(--ink-faint); font-size: .8rem; text-transform: uppercase; letter-spacing: .04em; margin-right: .4rem; }
.route-row { display: flex; gap: .4rem; align-items: center; margin-bottom: .3rem; }
.route-row input { flex: 1 1 auto; min-width: 0; }

/* The contact book's two add buttons — "Add channel" under each person, and
   "Add contact" under the list — are the same KIND of action at two levels, so
   they are drawn to one width rather than each shrink-wrapping its own label.
   `.btn` is an inline-flex whose width is its text, and the
   two labels differ by a few characters, which is exactly the mismatch that
   reads as sloppiness rather than as hierarchy. `min-width` rather than a fixed
   `width` so a longer label in future grows instead of overflowing, and
   `justify-content` because the flex box would otherwise leave the text hard
   left inside the wider box. */
.contact-action { min-width: 12rem; justify-content: center; }
/* And they are given room to be told apart. Without this the channel button of
   one contact sits flush against the next contact's name, and the last one sits
   flush against "Add contact" — two different actions a few pixels apart. */
.contact-entry { margin-bottom: .9rem; }
.contact-entry .contact-action { margin-top: .35rem; }
/* THE `min-width` ABOVE WAS ONLY HALF THE STORY, because the two buttons sit at
   different depths. "Add channel" is nested inside a `.contact-entry`, so it is
   an inline-flex sized by `max(label, 12rem)` — 12rem. "Add contact" is a DIRECT
   child of the `.field` flex column, whose default `align-items: stretch` blew
   it out to the full column width, so the pair still did not match. Opting this
   one out of the stretch hands the sizing back to the shared `min-width`, which
   is the single mechanism that should be deciding it. */
.field > .contact-action { align-self: flex-start; margin-top: .9rem; }

/* A `.grid` child that is not a field: a banner, a note, a form. `.grid` packs
   `auto-fit` 220px columns, so anything dropped into it is squeezed into one
   of them and reads as a value beside the fields rather than as something
   about them. This spans the row instead. */
.span-all { grid-column: 1 / -1; }

/* A MULTI-FIELD EDITOR, STACKED. `.inline-field-form` is the other editing
   shape on these pages and it is a flex ROW — right for the one-input-plus-two-
   buttons case it was written for, and wrong for anything larger: an editor
   with two recipient lists, an explanatory paragraph, a notes box and a button
   pair laid those five out side by side, vertically centred, each squeezed to
   a fraction of the column. That is the invoice-routing editor "going all
   weird".

   So a form with more than one field says so and stacks. The gap is larger
   than `.field`'s own .15rem because these ARE separate fields rather than a
   label and its value. */
.stacked-form { display: flex; flex-direction: column; gap: .9rem; align-items: stretch; }
.stacked-form input, .stacked-form select { width: 100%; min-width: 0; }
/* The prose between fields keeps the paragraph measure it would have outside a
   form; a full-width line of small grey text is hard to come back from. */
.stacked-form .muted { margin: 0; max-width: 60ch; }
.required-mark { color: var(--bad); font-weight: 700; }
.field-help-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1rem; height: 1rem; border: 1px solid var(--ink-faint); border-radius: 50%;
  color: var(--ink-faint); font-size: .7rem; font-weight: 700; line-height: 1;
}
.form.invoice-outcome-form { display: flex; flex-direction: column; gap: 1rem; }
.form.invoice-outcome-form > * { margin: 0; }
.form.invoice-outcome-form .outcome-field > label,
.form.invoice-outcome-form .receipt-foreign-toggle {
  display: flex; flex-direction: row; align-items: center; gap: .35rem;
}
.form.invoice-outcome-form .receipt-foreign-toggle { width: fit-content; color: var(--ink); font-size: .9rem; text-transform: none; letter-spacing: 0; }
.form.invoice-outcome-form .receipt-foreign-toggle .field-help-mark { margin-left: .1rem; }
.invoice-outcome-confirmation {
  display: flex; flex-direction: column; gap: 1rem;
  padding: 1.25rem; border: 1px solid var(--good-rule); border-radius: var(--radius);
  background: var(--good-soft); color: var(--good);
}
.invoice-outcome-confirmation h3, .invoice-outcome-confirmation p { margin: 0; }
/* A note attached to ONE input — how a value will be interpreted, not what the
   form does. Sits under its box rather than in the label, which is where the
   eye already is when the box is the thing being questioned. */
.field-hint { font-size: .8rem; max-width: 60ch; }

/* Person page — the Notes section's list of individual notes.
   koseki holds one commentary string per person; the server reads it as a list
   of {text, author, at} rows and this styles them as one. No bullet glyph: the
   byline is what separates one note from the next, and a marker column would
   push short notes away from the label grid the rest of the card uses. */
.person-notes { list-style: none; margin: 0; padding: 0; }
.person-notes li {
  display: flex;
  align-items: flex-start;
  gap: .75rem;
  padding: .4rem 0;
  border-bottom: 1px solid var(--rule);
}
.person-notes li:last-child { border-bottom: 0; }
.person-note-content { flex: 1 1 auto; min-width: 0; }
/* The checked-in app bundle is rebuilt only during integration. Keep its
   pre-wrapper markup correct until that batch consumes the source change. */
.person-notes li > .person-note-text { flex: 1 1 auto; min-width: 0; }
.person-note-text { white-space: pre-wrap; }
/* Provenance is secondary to the note itself, and absent entirely on notes
   written before it was recorded — so it must read as an aside rather than as
   a field whose value is missing. */
.person-note-by { color: var(--ink-faint); font-size: .85em; }
/* The withdraw control is anchored at the row's far edge. The note owns the
   flexible left column, so prose wraps beneath itself rather than pushing the
   destructive action to a different horizontal position on every row. */
.person-note-actions {
  display: flex;
  align-items: center;
  gap: .4rem;
  margin-left: auto;
  flex: 0 0 auto;
}
.person-add-note { margin-top: .75rem; }

/* People list — the filter bar's dropdown multiselect pickers.
   components/ui's MultiSelect, used by pages/page-people for all six
   closed-list filters (project, milestone, task type, status, currency,
   missing docs).

   WHY THESE EXIST AT ALL: the same six filters shipped briefly as
   `<select multiple>`, which a browser renders as an always-expanded list box
   — every option on screen at once, ctrl-click to add one — so the filter bar
   grew as tall as the table it filters. A picker has to be SHUT most of the
   time and say what it is doing while shut, which is what
   `.multiselect-trigger` and `.multiselect-value` are for.

   The popover itself is not a new style: `.multiselect-list` takes the
   combobox popover's surface (relative parent, absolute list, --shadow-pop,
   same radius/border/max-height), exactly as `.nav-dropdown` does.
   `.multiselect-label` restates `.label-caps`'s four properties rather than
   joining that rule's selector list, because this block is appended and must
   not reach back into the shared rules above it. */
.multiselect { display: flex; flex-direction: column; gap: .25rem; min-width: 0; }
.multiselect-label {
  font-size: .8rem; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-faint);
}
.multiselect-control { position: relative; display: flex; align-items: stretch; gap: .3rem; min-width: 0; }
.multiselect-trigger {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: center; justify-content: space-between; gap: .5rem;
  padding: .5rem .6rem; border: 1px solid var(--rule); border-radius: var(--radius-sm);
  background: var(--bg); color: var(--ink); font: inherit; text-align: left; cursor: pointer;
}
.multiselect-trigger:focus-visible { outline: 2px solid var(--link); outline-offset: -1px; }
/* An active filter reads as active from across the bar, not only on open. */
.multiselect.has-selection .multiselect-trigger { border-color: var(--ink-soft); font-weight: 500; }
.multiselect-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.multiselect-caret { flex: 0 0 auto; font-size: .7em; color: var(--ink-faint); }
.multiselect-clear {
  flex: 0 0 auto; padding: 0 .45rem; border: 1px solid var(--rule); border-radius: var(--radius-sm);
  background: var(--bg); color: var(--ink-faint); font: inherit; line-height: 1; cursor: pointer;
}
.multiselect-clear:hover:not(:disabled) { color: var(--ink); }
/* Disabled rather than absent when there is nothing to clear — see the
   component's note: a control that unmounts on use drops focus onto <body>
   and the blur would shut the list the reader is still working in. */
.multiselect-clear:disabled { opacity: .35; cursor: default; }
.multiselect-list {
  position: absolute; z-index: 10; top: calc(100% + 2px); left: 0; right: 0;
  margin: 0; padding: .25rem; list-style: none;
  background: var(--bg); border: 1px solid var(--rule); border-radius: var(--radius-md);
  box-shadow: var(--shadow-pop); max-height: 15rem; overflow-y: auto;
}
/* Stated even though nothing above sets `display` on the list: the component
   toggles the popover with the `hidden` attribute, and the day someone gives
   this rule a `display` the UA's `[hidden] { display: none }` would silently
   stop working — the bug `.nav-dropdown[hidden]` was written for. */
.multiselect-list[hidden] { display: none; }
.multiselect-option {
  display: flex; align-items: baseline; gap: .45rem; margin: 0;
  padding: .35rem .5rem; border-radius: var(--radius-sm);
  color: var(--ink); font-size: .9rem; cursor: pointer;
  text-transform: none; letter-spacing: 0;
}
.multiselect-option:hover { background: var(--bg-soft); }
.multiselect-option:focus { background: var(--bg-soft); outline: 2px solid var(--link); outline-offset: -2px; }
.multiselect-option.on { font-weight: 500; }
.multiselect-tick { flex: 0 0 .9rem; color: var(--link); }
.multiselect-empty { margin: 0; padding: .35rem .5rem; color: var(--ink-faint); font-size: .9rem; }

/* ── The client Billing card's contact-gap notice ──────────────────────────
   The paragraph that says the contact half cannot be saved, what to do
   instead, and what NOT to type into the billing notes six inches below it.
   It is the only thing standing between an operator with a changed contact and
   a name parked in `invoice_prefs.notes`, where nothing will ever read it —
   so it has to survive being skimmed, which the bare `.muted` grey it used to
   wear did not.

   A left rule and a soft panel rather than a warning colour: this is a fact
   about the API, not an error the operator caused or can fix here, and the red
   that `.error` owns on this page belongs to saves that actually failed. The
   `strong` lead-in is the skim target; the sentence after it is the server's,
   and can be long, so this block sets normal line length and drops the
   negative top margin `.muted` carries for snugging under a heading.

   Scoped to `.contact-gap`, which only the Billing card emits. */
.contact-gap.muted {
  margin: 1rem 0 0;
  padding: .55rem .7rem;
  border-left: 3px solid var(--rule);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  background: var(--bg-soft);
  color: var(--ink);
  font-size: .85rem;
  line-height: 1.5;
}
.card .contact-gap.muted { margin: 1rem 0 0; }
.contact-gap strong { color: var(--ink); }

/* The person page's withheld Status row (frontend/src/pages/person/profile.tsx).
   NOT `.empty`: that class is this portal's "nothing recorded" em-dash, and
   every person koseki holds has a lifecycle — so a withheld status has to read
   as recorded-but-not-shown rather than as blank. Styled like the value it
   stands in for (the same weight as the field's `span`) and left in the ink
   the rest of a field's value uses, with the reason beside it in
   `.muted-inline`, matching how the payment-method section renders its
   withheld "Account details". */
.status-withheld { font-style: italic; }
