/* OPN Compliance UI stylesheet.
 *
 * All styling lives here (or in per-page rules below) — templates must not use
 * `style="..."` attributes or `<style>` blocks: the strict Content-Security-
 * Policy (`style-src 'self'`, see app/security_headers.py) blocks both, and
 * tests/test_security_headers.py gates the templates. Compose the utility
 * classes at the bottom instead of adding new inline styles.
 */

/* Every colour is a token here; rules below never name a colour (tests/test_theme.py holds the
   line, with a short allowlist: the QR code and the video player stay dark-on-light in either
   theme). The light palette is the default; the dark one applies when the system asks for it and
   the person hasn't said otherwise, or when they chose it on Your details (data-theme on <html>).
   tests/test_theme.py checks every text/surface pair in both palettes for WCAG contrast. */
:root {
  color-scheme: light;
  --bg: #eaebed; --card: #fff; --ink: #1c2330; --muted: #606a7e;
  --line: #e3e7ee; --line-strong: #c9d0dc; --brand: #1f5fd1; --brand-ink: #fff;
  --ok: #197945; --warn: #926018; --bad: #c0392b; --chip: #eef2f8; --chip-ink: #54617a;
  /* The tinted families: a badge sits on -bg, a banner or a card wash on -wash with -ink text, -line edges them. */
  --ok-bg: #e4f5ec; --ok-line: #bfe6cf; --ok-ink: #164d31; --ok-wash: #f3faf6;
  --warn-bg: #fff4e0; --warn-line: #f0dcb4; --warn-ink: #5b3f0a; --warn-wash: #fffaf0;
  --bad-bg: #fbe6e3; --bad-line: #f0c4bd; --bad-rgb: 192, 57, 43;
  /* A fourth hue. The family has good/attention/bad/neutral; a support case list also needs
     "informational, not a verdict" — a new case, a scheduled send, a Claude-drafted response.
     Brand blue, tinted, so it reads as "the app is telling you something" rather than "something is wrong". */
  --info: #1f5fd1; --info-bg: #e3ecfb; --info-line: #bdd0f2; --info-ink: #163c7a;
  --agent: #6b3fa0; --agent-bg: #f3ecfa;             /* Claude / an MCP token: its own hue */
  --diff-was: #fbeeec; --diff-now: #ebf6ef; --bg-soft: #fbfcfe;
  --tip-bg: #1c2330; --tip-ink: #fff;                 /* the copied tooltip and the toast: inverse of the page */
  --shadow-rgb: 28, 35, 48; --backdrop: rgba(28, 35, 48, .45);
  /* What a clamped narrative field fades out to — the surface behind it. Rebound
     wherever that surface isn't the card (a narrative sub-row sits on --chip). */
  --clamp-fade: var(--card);
  /* The wash a section's heading takes when a TOC click lands on it. */
  --flash: rgba(31, 95, 209, .13);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --bg: #14171c; --card: #1d2229; --ink: #e6e9ef; --muted: #a3adbf;
    --line: #2c333d; --line-strong: #3d4653; --brand: #7aa6f6; --brand-ink: #0c1730;
    --ok: #5cc98a; --warn: #e2aa56; --bad: #f28b7c; --chip: #262d37; --chip-ink: #c2cad8;
    --ok-bg: #16302a; --ok-line: #24563c; --ok-ink: #b6e8c8; --ok-wash: #182820;
    --warn-bg: #3a2c12; --warn-line: #6a4d1e; --warn-ink: #f2d6a0; --warn-wash: #2b2416;
    --bad-bg: #3d1f1c; --bad-line: #6a2f28; --bad-rgb: 242, 139, 124;
    --info: #7aa6f6; --info-bg: #1c2a44; --info-line: #2f4a7a; --info-ink: #c0d2f7;
    --agent: #cdb0f2; --agent-bg: #2d2440;
    --diff-was: #3a2422; --diff-now: #1f3529; --bg-soft: #1a1f26;
    --tip-bg: #e6e9ef; --tip-ink: #14171c;
    --shadow-rgb: 0, 0, 0; --backdrop: rgba(0, 0, 0, .6);
    --flash: rgba(122, 166, 246, .22);
  }
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: #14171c; --card: #1d2229; --ink: #e6e9ef; --muted: #a3adbf;
  --line: #2c333d; --line-strong: #3d4653; --brand: #7aa6f6; --brand-ink: #0c1730;
  --ok: #5cc98a; --warn: #e2aa56; --bad: #f28b7c; --chip: #262d37; --chip-ink: #c2cad8;
  --ok-bg: #16302a; --ok-line: #24563c; --ok-ink: #b6e8c8; --ok-wash: #182820;
  --warn-bg: #3a2c12; --warn-line: #6a4d1e; --warn-ink: #f2d6a0; --warn-wash: #2b2416;
  --bad-bg: #3d1f1c; --bad-line: #6a2f28; --bad-rgb: 242, 139, 124;
  --info: #7aa6f6; --info-bg: #1c2a44; --info-line: #2f4a7a; --info-ink: #c0d2f7;
  --agent: #cdb0f2; --agent-bg: #2d2440;
  --diff-was: #3a2422; --diff-now: #1f3529; --bg-soft: #1a1f26;
  --tip-bg: #e6e9ef; --tip-ink: #14171c;
  --shadow-rgb: 0, 0, 0; --backdrop: rgba(0, 0, 0, .6);
  --flash: rgba(122, 166, 246, .22);
}
* { box-sizing: border-box; }
body {
  margin: 0; background: var(--bg); color: var(--ink);
  font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
a { color: var(--brand); text-decoration: none; }
a:hover { text-decoration: underline; }
/* The topbar WRAPS. One nav link per domain means it grows every time we add one, and
   a flex row that can't wrap can't shrink below its content either — it just pushes the
   page wider than the viewport, which scrolls the whole layout (and everything anchored
   to its right edge, like a field's Expand control) off to the right. The page must
   never scroll horizontally; the topbar folds onto a second line instead. */
header.topbar {
  background: var(--card); border-bottom: 1px solid var(--line);
  padding: 12px 24px; display: flex; align-items: center; gap: 8px 16px;
  flex-wrap: wrap;
  position: sticky; top: 0; z-index: 100;
}
header.topbar .brand { font-weight: 700; font-size: 16px; }
header.topbar .brand a { display: flex; }
header.topbar .brand-logo { display: block; width: 32px; height: 32px; }
header.topbar .who { color: var(--muted); font-size: 13px; }
/* min-width: 0 lets the nav shrink at all (a flex item's default min-width: auto pins it
   to its content width — that's what made the whole page overflow); flex-wrap lets it
   fold its links once it has to. Do NOT give it `flex: 1 1 0`: squeezed to the leftover
   space it folds into a stack of short rows, and the sticky header grows to eat the
   screen on a laptop. */
header.topbar nav { display: flex; gap: 8px 16px; flex-wrap: wrap; min-width: 0; }
/* The search + persona + name + sign-out travel together: as one flex item they wrap to
   the next line as a unit, instead of the header shedding whichever control happened to
   land last (Sign out, on its own, under an otherwise empty row). */
header.topbar .topbar-right {
  display: flex; align-items: center; gap: 8px 16px; margin-left: auto;
}
header.topbar form.topbar-search input { width: 180px; max-width: 100%; }
/* Full-width: pages span the window (minus side gutters) so wide tables
   aren't arbitrarily constrained. */
main { margin: 24px 0; padding: 0 24px; }
h1 { font-size: 22px; margin: 0 0 16px; }
h2 { font-size: 16px; margin: 24px 0 10px; }
.card {
  background: var(--card); border: 1px solid var(--line);
  border-radius: 10px; padding: 18px 20px; margin-bottom: 16px;
}
/* Card tints: a success / caution wash over .card (seed notices, live stats). */
.card-ok { background: var(--ok-bg); border-color: var(--ok-line); }
.card-warn { background: var(--warn-wash); border-color: var(--warn-line); }
/* Dismissable banner (e.g. seed-result notices): close button on the right. */
.banner { display: flex; align-items: center; gap: 12px; }
.banner > .banner-msg { flex: 1; }
/* The three kinds of banner — a warning, a confirmation, a note — are a tinted box with an accent
   rule on the left and a glyph in the accent colour, drawn by the stylesheet so every banner in the
   app gets them from its class alone (the strict CSP rules out an SVG data URL here, and the rail's
   icons are glyphs too). The washes are the card tints' (.card-warn, .card-ok). */
.banner.warn, .banner.ok, .banner.info {
  padding: 12px 16px; margin: 16px 0; border-radius: 8px; border: 1px solid; border-left-width: 4px; font-size: 14px; line-height: 1.45;
}
/* Room above and below comes from the rule, not from ad-hoc margin classes on each banner; inside
   a dialog body or as a card's first child it keeps a little air rather than a full gap. */
.modal-body > .banner:first-child, .card > .banner:first-child { margin-top: 4px; }
.card > .banner:last-child, .modal-body > .banner:last-child { margin-bottom: 4px; }
.banner.warn { background: var(--warn-wash); border-color: var(--warn-line); border-left-color: var(--warn); color: var(--warn-ink); }
.banner.ok { background: var(--ok-bg); border-color: var(--ok-line); border-left-color: var(--ok); color: var(--ok-ink); }
.banner.info { background: var(--info-bg); border-color: var(--info-line); border-left-color: var(--brand); color: var(--info-ink); }
.banner.warn::before, .banner.ok::before, .banner.info::before { flex: none; font-size: 16px; line-height: 1; }
.banner.warn::before { content: "\26A0"; color: var(--warn); }          /* ⚠ — escaped so the glyph survives any charset the sheet is read with */
.banner.ok::before { content: "\2713"; color: var(--ok); font-weight: 700; }   /* ✓ */
.banner.info::before { content: "\24D8"; color: var(--brand); }         /* ⓘ */
.banner.warn > span, .banner.ok > span, .banner.info > span { flex: 1; }
.banner.warn .banner-close, .banner.ok .banner-close, .banner.info .banner-close { color: inherit; opacity: .7; }
.banner-close {
  background: none; border: none; cursor: pointer; padding: 0 4px;
  font-size: 20px; line-height: 1; color: var(--muted);
}
.banner-close:hover { color: var(--ink); }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 9px 10px; border-bottom: 1px solid var(--line); }
th { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
th a.sortlink { color: inherit; cursor: pointer; }
th a.sortlink:hover { color: var(--ink); text-decoration: underline; }
.btn {
  display: inline-block; background: var(--brand); color: var(--brand-ink);
  border: none; border-radius: 7px; padding: 8px 14px; font-size: 14px;
  cursor: pointer; font-weight: 600;
}
.btn:hover { filter: brightness(1.05); text-decoration: none; }
.btn.ghost { background: var(--chip); color: var(--ink); }
.btn.danger { background: var(--bad); }
.btn.small { padding: 5px 10px; font-size: 13px; }
input, select, textarea {
  font: inherit; padding: 8px 10px; border: 1px solid var(--line);
  border-radius: 7px; width: 100%; background: var(--card); color: var(--ink);
}
/* One height for every single-line control. A text input's box comes from the inherited
   line-height (1.5 × 15px), a <select>'s from the browser's "normal" one, and a monospace input's
   from its smaller font — three heights side by side in one row, and the row's bottom alignment
   then staggered their labels. Pin the box instead; the browser centres the text. Checkboxes,
   radios, file pickers and the few deliberately small inputs opt out below. */
input, select { height: 40px; box-sizing: border-box; }
input[type="checkbox"], input[type="radio"], input[type="file"], input[type="range"], input[type="hidden"] { height: auto; }
label { display: block; font-size: 13px; color: var(--muted); margin: 10px 0 4px; }
.row { display: flex; gap: 12px; flex-wrap: wrap; }
.row > * { flex: 1; min-width: 160px; }
.badge {
  display: inline-block; padding: 3px 9px; border-radius: 999px;
  font-size: 12px; font-weight: 700; background: var(--chip); color: var(--muted);
}
.badge.draft, .badge.retracted { background:var(--chip); color:var(--chip-ink); }
.badge.submitted, .badge.approved { background:var(--warn-bg); color: var(--warn); }
.badge.deployed, .badge.closed { background:var(--ok-bg); color: var(--ok); }
.badge.rejected, .badge.rolled_back { background:var(--bad-bg); color: var(--bad); }
/* Derived open/closed status (closed shares the green rule above). */
.badge.open { background:var(--warn-bg); color: var(--warn); }
/* Derived schedule status (recurring compliance tasks). */
.badge.on_track { background:var(--ok-bg); color: var(--ok); }
.badge.due_soon { background:var(--warn-bg); color: var(--warn); }
.badge.overdue { background:var(--bad-bg); color: var(--bad); }
/* Lapsed: the work itself hasn't happened in over a cadence, whatever the due date
   says. Shares the bad colour with overdue — it is the same seriousness — but is
   outlined so the two read as different statements at a glance. */
.badge.lapsed { background:var(--bad-bg); color: var(--bad); box-shadow: inset 0 0 0 1px var(--bad); }
.badge.inactive { background:var(--chip); color:var(--chip-ink); }
/* Derived BCDR recovery posture. */
.badge.compliant { background:var(--ok-bg); color: var(--ok); }
.badge.at_risk { background:var(--warn-bg); color: var(--warn); }
.badge.untested { background:var(--chip); color:var(--chip-ink); }
.badge.breached { background:var(--bad-bg); color: var(--bad); }
/* BCDR criticality tiers. */
.badge.critical { background:var(--bad-bg); color: var(--bad); }
.badge.high { background:var(--warn-bg); color: var(--warn); }
.badge.medium { background:var(--chip); color:var(--chip-ink); }
.badge.low { background:var(--chip); color:var(--chip-ink); }
/* Trusted-timestamp (stamp) status. */
.badge.pending { background:var(--warn-bg); color: var(--warn); }
.badge.confirmed { background:var(--ok-bg); color: var(--ok); }
td.bad { color: var(--bad); font-weight:600; }
/* `minmax(0, 1fr)` + `min-width: 0`, not plain `1fr`: a grid item's default
   `min-width: auto` floors it at its content's MIN-CONTENT width, and a Markdown table
   (or any element that won't wrap) has a large one. The value column would then grow past
   the card — dragging the page into a horizontal scroll — and `.md table`'s own
   `max-width: 100%` could never bind, because 100% of an over-wide <dd> is still over-wide.
   Floor the track at 0 and the table scrolls inside its cell, as it was built to. */
.meta { display: grid; grid-template-columns: 160px minmax(0, 1fr); gap: 6px 16px; font-size: 14px; }
.meta dt { color: var(--muted); }
.meta dd { margin: 0; min-width: 0; }
.gates { list-style: none; padding: 0; margin: 0; }
.gates li { padding: 4px 0; }
.gates .done::before { content: "\2714  "; color: var(--ok); font-weight: 700; }
.gates .todo::before { content: "\25CB  "; color: var(--muted); }
.timeline { list-style: none; padding: 0; margin: 0; font-size: 14px; }
.timeline li { padding: 10px 0; border-bottom: 1px dashed var(--line); }
.timeline li:last-child { border-bottom: none; }
/* INDENTATION carries the hierarchy, not shading. Only an event header starts at the
   left margin — everything else on the card is indented under one — so the eye finds
   entry boundaries by scanning a single column rather than by counting dashed rules.
   An entry can be one line or a page (an opened diff of two descriptions), and unlike a
   background tint an indent stays legible at any height and prints. Three levels:
     x=0    the "<action> by <actor>" header
     x=18   what belongs to that event (its note, its collapsed summary)
     x=34   an opened body, behind its own rail (.event-body below). */
.timeline li > :not(.event-head),
.records li > :not(.event-head) { margin-left: 18px; }
/* Action + actor on the left, timestamp on the right, always one line. The actor is
   muted so the action — the thing you scan for — carries the line. */
.event-head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.event-head .who { color: var(--muted); font-weight: 400; }
.event-head .when { flex: none; }
.timeline .when, .event-head .when { color: var(--muted); font-size: 12px; }
/* The "Add a …" form under a record list. A heading, not a fourth entry: without the
   rule and the title, the form's own Label / Evidence / Format captions read as another
   record's fields running on from the last one. */
.entry-form-title { font-size: 13px; font-weight: 700; margin: 4px 0 10px; }
.err { background:var(--bad-bg); color: var(--bad); border:1px solid var(--bad-line);
       padding: 8px 12px; border-radius: 7px; margin-bottom: 12px; font-size: 14px; }
.muted { color: var(--muted); }
.req { color: var(--bad); }
/* An "Actions" card holds several INDEPENDENT forms, not one form — so each is a
   titled panel in a grid, and the grid's track floor is what keeps a form readable.
   (They used to be `display: inline-block`: each became a shrink-to-fit box floating
   beside its neighbour behind a 6px gutter, which read as ONE run-on form, squeezed
   `.row` columns down toward their 160px floor until labels wrapped and their
   controls fell out of line, and left a third of the card empty.)
   `auto-fill`, NOT `auto-fit`: auto-fit collapses the empty tracks, which would
   stretch a lone "Retire" form across the whole window — a small form should stay
   small. The 340px floor is the fix for wrapped labels: it fits a field label plus
   its parenthetical hint. `min-width: 0` because a grid item's automatic minimum is
   its content, so a long placeholder would otherwise push a panel past its track. */
.action-grid {
  display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
}
.action {
  border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px;
  background: var(--chip); min-width: 0;
}
/* A row of actions shares ONE button line. The panels stretch to the row's height
   (the grid default) and each is a column whose submit button is pushed to the
   bottom edge, so the buttons don't stair-step down the row behind fields of
   different heights. The slack is absorbed by the element ABOVE the button rather
   than by the button's own margin-top: that leaves the button's `mt-8` doing its
   job, so the tallest panel in a row — the one with no slack to absorb — still
   keeps its gap. A lone button has nothing above it, so it absorbs the slack
   itself. `align-self` keeps a button its own width; as a flex item it would
   otherwise stretch across the panel. */
/* `:is(form.action, details.action > form)` — the panel IS the form, or (inside a
   disclosure) contains one; both need the same column treatment. */
:is(form.action, details.action > form) { display: flex; flex-direction: column; }
:is(form.action, details.action > form) > :has(+ .btn:last-child) { margin-bottom: auto; }
:is(form.action, details.action > form) > .btn:last-child { align-self: flex-start; }
form.action > .btn:only-child { margin-top: auto; }
/* A CLOSED disclosure is one line by design, so it must not stretch — a full-height
   box holding a single summary would read as an empty panel. An OPEN one is a panel
   like any other: it stretches with the row, and its form fills whatever the summary
   leaves so its button lands on the same button line as its neighbours'. */
details.action:not([open]) { align-self: start; }
details.action[open] { display: flex; flex-direction: column; }
details.action[open] > form { flex: 1; }
/* Chrome (131+) wraps everything after the summary in a `::details-content` box, so
   the form is NOT a direct flex item of the <details> there and the rule above can't
   reach it. Make that box a flex column too. Browsers without the pseudo-element
   drop this rule and the one above already does the job. */
details.action[open]::details-content { display: flex; flex-direction: column; flex: 1; min-height: 0; }
/* The everyday form (record an assessment / a test / a review) takes the whole row,
   then caps its measure — a full-window card would otherwise stretch a one-line
   "Evidence URL" input to 1500px. */
.action.wide { grid-column: 1 / -1; max-width: 960px; }
.action-title { font-size: 13px; font-weight: 700; margin: 0 0 10px; }
/* `.btn.ghost` is chip-coloured — which is now also the panel behind it, so a ghost
   button inside a panel would read as plain bold text. Give it the card's white and
   a hairline ring; `inset` box-shadow rather than a border so its size is unchanged
   (`.btn` sets `border: none`, and a real border would nudge the metrics). */
.action .btn.ghost { background: var(--card); box-shadow: inset 0 0 0 1px var(--line); }
/* An explanatory line dropped straight into the grid (the change panel's `no_review`
   and emergency-review notes) introduces the panels under it, so it spans the row
   instead of standing in a track as if it were an action. */
.action-grid > p { grid-column: 1 / -1; margin: 0; }
/* An off-the-happy-path action (retire, cancel, reject, roll back, reopen) folds
   away — the same rule the audit trail follows: what a person came here to do stays
   visible, the rare thing folds. It also puts a deliberate click in front of an
   action that can't be undone. The data-confirm / hx-confirm guard is unaffected. */
details.action > summary { cursor: pointer; font-size: 13px; font-weight: 700; color: var(--bad); }
details.action[open] > summary { margin-bottom: 10px; }
/* A card holding exactly ONE form needs no panel — its own <h2> is the title — but
   it still caps its measure. */
.actions > form { max-width: 960px; }
/* A label may wrap to two lines; its control must not drop below its neighbours'.
   Bottom-aligning the field boxes puts every control back on one line, because a
   label is the only thing above a control and the controls are the same height.
   The one exclusion is a row holding a datetime field: its local-time-zone hint
   sits UNDER the input, so the bottom of that box is not the bottom of its control
   and aligning on it would lift the input off the line instead of onto it. */
.row:not(:has(.local-dt-field)) { align-items: flex-end; }
/* A commit SHA is shown in full (it's the evidence — an abbreviation is only
   unambiguous by luck), so let it wrap rather than blow out its column. */
.sha { word-break: break-all; }
/* Rendered Markdown (narrative fields) — block children inside a <dd>. */
/* A long unbroken string — a URL in a description — must wrap inside the card rather than
   push the column under the sidebar. `anywhere`, not `break-word`: only `anywhere` lets the
   min-content width shrink, so the grid track can stay at the card's width (see `.meta` above).
   `pre` keeps scrolling (no wrap opportunities in `white-space: pre`), and a table keeps
   scrolling inside its cell as it was built to. */
.md { overflow-wrap: anywhere; }
.md > :first-child { margin-top: 0; }
.md > :last-child { margin-bottom: 0; }
/* A narrative is a FIELD on a page, not a page. Markdown headings render at document
   scale by default, so an `# Heading` in a 14px card came out a 2em title that
   out-shouted the card's own <h2> — a pasted report then read as the page's subject
   rather than as one event's note. Capped near body size: the structure still reads
   (bold, spaced, hierarchical by weight), but the field can't claim the page. */
.md h1, .md h2, .md h3, .md h4, .md h5, .md h6 {
  font-size: 1em; font-weight: 700; line-height: 1.4; margin: 12px 0 4px;
}
.md h1 { font-size: 1.12em; }
.md h2 { font-size: 1.06em; }
.md h4, .md h5, .md h6 { color: var(--muted); }
.md p { margin: 0 0 8px; }
.md ul, .md ol { margin: 0 0 8px; padding-left: 22px; }
.md code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .9em; background: var(--chip); padding: 1px 5px; border-radius: 4px;
}
.md pre {
  background: var(--chip); padding: 10px 12px; border-radius: 7px;
  overflow-x: auto; margin: 0 0 8px;
}
.md pre code { background: none; padding: 0; }
/* A rendered field + its "Raw" / "Expand" toggles (partials/_md.html). Both sit BELOW
   the body in one left-aligned row, under the fade that says the text continues — that
   is where a reader looks for "there's more", and it keeps them attached to their own
   text. (Raw used to float to the block's top-right; on a full-width block, such as an
   audit-trail entry, that put it an inch of whitespace away from the field it belongs
   to, reading as page furniture. Expand had already been moved out of that corner for
   the same reason.) */
.md-raw-toggle, .md-more-toggle {
  background: none; border: none; padding: 0 2px;
  font-size: 12px; color: var(--muted); cursor: pointer; text-decoration: underline;
}
/* STICKY: an expanded block can be many screens tall and its controls are at the end,
   so collapsing a 10 MB transcript meant scrolling to the bottom of it first. The row
   pins to the viewport bottom while you are anywhere inside the block, and sits in its
   natural place otherwise (a block shorter than the viewport never sticks at all).
   It paints on the block's own surface — `--clamp-fade` already tracks that for the
   clamp's fade — so the text it passes over doesn't show through it. */
.md-controls {
  display: flex; gap: 10px; padding: 3px 0;
  position: sticky; bottom: 0; z-index: 1; background: var(--clamp-fade);
}
/* ...and when the visible body is a <pre> — a verbatim block, or a rendered one flipped
   to Raw — that surface is --chip, not the surrounding one. A stuck row painted the
   card's white over a grey transcript reads as a hole in it. Mirrors the clamp fade's
   own override below. */
.mdblock.verbatim .md-controls, .mdblock.md-raw .md-controls { background: var(--chip); }
/* Load-bearing, not defensive: the buttons ship `hidden` and JS reveals them, but an
   author-stylesheet `display` on .md-*-toggle would otherwise beat the UA's
   `[hidden] { display: none }` and show a no-JS reader two dead controls. */
.md-more-toggle[hidden], .md-raw-toggle[hidden] { display: none; }
.md-raw-toggle:hover, .md-more-toggle:hover { color: var(--ink); }
/* Collapsed to a few lines. Applied by JS (never in the markup — see _md.html), so
   a reader without JS gets the whole field rather than a truncated one they can't
   open. The fade tells you the record continues past the cut. */
.md-body.md-clamp {
  max-height: 6em; overflow: hidden; position: relative;
}
/* A narrative TABLE (a release-notification matrix, a test grid) needs more room than
   prose: 6em of a table is its header row and nothing else, so the collapsed view cost
   a lot of vertical space to show column headings and no data. Give it enough height to
   carry a few rows, which is usually the whole point of the table.
   `:has()` is progressive — where it isn't supported this degrades to the 6em clamp,
   i.e. exactly today's behaviour, so nothing depends on it. */
.md-body.md-clamp:has(table) { max-height: 16em; }
.md-body.md-clamp::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2em;
  background: linear-gradient(to bottom, transparent, var(--clamp-fade));
}
/* Showing the raw source instead: that <pre> carries its own --chip background,
   so the fade has to land on that, whatever surface the block sits on. A verbatim block
   (a plain-format log — md.text_block) is *always* that <pre>, so it fades the same. */
.mdblock.md-raw .md-body.md-clamp::after,
.mdblock.verbatim .md-body.md-clamp::after {
  background: linear-gradient(to bottom, transparent, var(--chip));
}
/* The verbatim source, shown in place of the rendered view. */
.md-src {
  background: var(--chip); padding: 10px 12px; border-radius: 7px;
  overflow-x: auto; margin: 0; font-size: .9em;
}
/* A narrative sub-row under a history-table record (findings / notes / summary):
   full width, so bulleted evidence and Markdown tables get real room instead of
   being crushed into the last column. */
.md-row > td { background: var(--chip); padding: 8px 10px; --clamp-fade: var(--chip); }
/* A labelled narrative field. Used inside .md-row and on its own (the change's
   approval notes / rollback reason, under their control gate). */
.md-label {
  font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted);
  margin-bottom: 2px;
}
.md-field + .md-field { margin-top: 10px; }
.gates .md-field { margin: 4px 0 2px; }
/* An audit-event note is narrative Markdown too (see partials/_audit_trail.html),
   sitting under the "<action> by <actor>" line rather than beside it. */
.timeline .mdblock { margin: 4px 0 2px; color: var(--muted); }
/* An event's recorded details — most of all a creation event's full attribute snapshot
   (app/audit_snapshot.py). Collapsed behind a native <details> so a snapshot of twenty
   fields doesn't bury the timeline; native, so it opens without JS. */
.event-details { margin: 4px 0 2px; }
.event-details > summary { cursor: pointer; width: fit-content; }
.event-details > summary:hover { color: var(--ink); }
/* The third indent level: an opened event's body sits behind a rail, so its content is
   visibly subordinate to the summary that opened it — and the rail's full height tells
   you at a glance how much of the page this one event owns. Without it, four nested
   levels (event -> field -> was/now -> body) all sat at the same margin and full width
   and read as one flat wall. */
.event-body {
  border-left: 2px solid var(--line); padding-left: 12px; margin: 6px 0 2px 6px;
}
/* One field's before/after. The tag column is the diff's spine: you read down it to
   find the version you want instead of parsing two look-alike labelled blocks. */
.diff { margin: 8px 0; }
.diff + .diff { padding-top: 8px; border-top: 1px dashed var(--line); }
.diff-side {
  display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 8px;
  padding: 4px 6px; border-radius: 5px; margin-top: 2px;
}
/* Shaded by direction — the old version recedes, the new one is what took effect.
   Deliberately faint washes of the existing bad/ok hues rather than the full colours:
   an edit is not an error or a success, and a saturated red/green row would read as a
   verdict on the change. */
.diff-side.was { background: var(--diff-was); --clamp-fade: var(--diff-was); }
.diff-side.now { background: var(--diff-now); --clamp-fade: var(--diff-now); }
.diff-tag {
  font-size: 11px; text-transform: uppercase; letter-spacing: .04em; font-weight: 600;
  padding-top: 3px;
}
.diff-side.was .diff-tag { color: var(--bad); }
.diff-side.now .diff-tag { color: var(--ok); }
.diff-text { min-width: 0; overflow-wrap: anywhere; }
.diff-text .mdblock { margin: 0; }
.kv { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 2px 12px;
      margin: 6px 0 2px; }
.kv dt { color: var(--muted); text-transform: uppercase; font-size: 11px;
         letter-spacing: .04em; padding-top: 3px; }
.kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
/* A narrative inside the snapshot clamps like any other; it fades to the card. */
.kv dd .mdblock { margin: 0; --clamp-fade: var(--card); }
/* A narrative table (release notes, test matrices) is prose, not a data grid: it
   scrolls itself rather than overflowing its <dd>, and its headers keep the
   surrounding type instead of the uppercase/muted grid header style above. */
/* `display: block` + `overflow-x: auto` makes this a SCROLL CONTAINER, whose min-content
   contribution is zero — that's the whole point. Do not put `width: max-content` back: it
   gives the box a large min-content width, which a container that can't be floored at zero
   (a table cell — a narrative sub-row under a history table) is then forced to grow to,
   dragging the card and the page into a horizontal scroll. Width auto fills the cell; the
   real table inside still shrink-wraps, so a small table still looks small. */
.md table {
  display: block; max-width: 100%;
  overflow-x: auto; margin: 0 0 8px; overflow-wrap: normal;
}
.md th {
  font-size: inherit; text-transform: none; letter-spacing: normal; color: inherit;
}
.md blockquote {
  margin: 0 0 8px; padding: 2px 12px; border-left: 3px solid var(--line);
  color: var(--muted);
}

/* --- Access-tokens page (/ui/tokens) ------------------------------------- */
/* Wide enough for the local-time-zone hint to sit on one line; the datetime
   input fills that width so the field and its hint stay aligned. */
.token-expires { width: 21rem; max-width: 100%; }
.token-expires .ldt-dt { width: 100%; box-sizing: border-box; }
/* The top row: Purpose / Expires / button, top-aligned so labels and controls line up. */
.token-top-row { display: flex; gap: 10px; align-items: flex-start; flex-wrap: wrap; }
/* margin:0 overrides the global `label { margin:10px 0 4px }` that would drop
   the Purpose column below its sibling <div> columns. */
.token-field { display: flex; flex-direction: column; gap: 4px; margin: 0; }
.token-roles { margin-top: 18px; }
.token-role-opts { display: flex; flex-direction: column; gap: 8px; margin-top: 6px; }
/* Checkboxes wrap under the "specific" radio, indented to align past the dot.
   The [hidden] override beats `.token-role-grid` so the JS toggle actually
   hides the grid (an author `display:flex` otherwise outranks the UA
   `[hidden] { display:none }` rule and it'd always show). */
.token-role-grid { display: flex; flex-wrap: wrap; gap: 8px 20px; padding: 8px 0 2px 26px; }
.token-role-grid[hidden] { display: none; }
/* A radio/checkbox + its text. width:auto + flex:0 0 auto undo the global
   `input { width:100% }`, which otherwise stretches each control full-width
   and scatters the row. */
.role-opt { display: flex; align-items: center; gap: 8px; margin: 0; font-weight: normal; }
.role-opt input[type="radio"],
.role-opt input[type="checkbox"] { width: auto; flex: 0 0 auto; margin: 0; }
/* The freshly-created token secret: shown once, click selects the whole value. */
.token-secret {
  display: block; padding: 10px; background: var(--chip);
  border-radius: 6px; word-break: break-all; user-select: all;
}

/* --- Utilities (replace inline style= attributes; compose freely) --------- */
/* Horizontal flex row; add a .gap-* and margins as needed. */
.hrow { display: flex; align-items: center; }
.wrap { flex-wrap: wrap; }
.gap-6 { gap: 6px; }
.gap-8 { gap: 8px; }
.gap-10 { gap: 10px; }
.gap-12 { gap: 12px; }
.gap-16 { gap: 16px; }
.ai-center { align-items: center; }
.grow { flex: 1; }
.minw-200 { min-width: 200px; }
.minw-320 { min-width: 320px; }   /* a subject column beside no-wrap address columns */
.w-auto { width: auto; }
.block { display: block; }
.inline { display: inline; }
.hide { display: none; }
/* A checkbox/radio + label pair sitting inline in a row of controls. The input
   needs its intrinsic size back: the global `input { width: 100% }` is meant for
   text fields and would otherwise stretch the tick box across the label. */
.check-label {
  display: inline-flex; align-items: center; gap: 6px;
  margin: 0; flex: 0 0 auto; width: auto; color: var(--ink);
}
.check-label input { width: auto; flex: 0 0 auto; margin: 0; }
.fs-12 { font-size: 12px; }
.fs-13 { font-size: 13px; }
.pointer { cursor: pointer; }
.ink-ok { color: var(--ok); }
.ink-warn { color: var(--warn); }
.ink-bad { color: var(--bad); }
/* Dashboard-style stat number. */
.stat { font-size: 16px; font-weight: 700; }
.prewrap { white-space: pre-wrap; overflow-wrap: anywhere; }
.plainlist { list-style: none; padding: 0; margin: 0; }
.mono-input { font-family: monospace; font-size: 12px; padding: 4px 8px; min-width: 220px; }
.maxw-560 { max-width: 560px; }
.maxw-70ch { max-width: 70ch; }
/* A lone centered card (login, forbidden). */
.solo-center { max-width: 480px; margin: 56px auto; text-align: center; }
/* Section divider row (e.g. list footers). */
.rule-top { border-top: 1px solid var(--line); padding: 12px 0; }
/* Wrapping grid of checkbox options (e.g. the column picker). */
.opt-grid { display: flex; flex-wrap: wrap; gap: 6px 18px; }
/* A hint line pulled up tight under its heading. */
.sub-hint { margin: -6px 0 10px; font-size: 13px; }
/* Margins last so they win inside any utility combo (e.g. "m-0 mt-8"). */
.m-0 { margin: 0; }
.m-2 { margin: 2px; }
.ml-4 { margin-left: 4px; }
.mt-0 { margin-top: 0; }
.mt-4 { margin-top: 4px; }
.mt-6 { margin-top: 6px; }
.mt-8 { margin-top: 8px; }
.mt-10 { margin-top: 10px; }
.mt-12 { margin-top: 12px; }
.mt-14 { margin-top: 14px; }
.mt-16 { margin-top: 16px; }
.mt-18 { margin-top: 18px; }
.mt-20 { margin-top: 20px; }
.mb-0 { margin-bottom: 0; }
.mb-4 { margin-bottom: 4px; }
.mb-6 { margin-bottom: 6px; }
.mb-8 { margin-bottom: 8px; }
.mb-10 { margin-bottom: 10px; }
.mb-12 { margin-bottom: 12px; }
.mb-14 { margin-bottom: 14px; }
.mb-16 { margin-bottom: 16px; }

/* Witness attestations — a sub-row under an evidence-collection record. */
.witness-row > td { background: var(--chip); padding: 8px 10px; }
.witness-main { justify-content: space-between; row-gap: 8px; }
.witness-item {
  font-size: 13px; background: var(--card); border: 1px solid var(--line);
  border-radius: 999px; padding: 2px 8px;
}
.witness-form select, .witness-form textarea { padding: 5px 8px; font-size: 13px; }
/* The note is a textarea (it renders as Markdown on the trail), but this form is an
   inline row in a history table — so it stays compact and grows only if dragged. */
.witness-form textarea { min-width: 180px; resize: vertical; }
.witness-form :disabled { opacity: .55; cursor: not-allowed; }
/* The disabled-reason sits on its own line under the form, aligned to it. */
.witness-reason { margin-top: 6px; font-style: italic; text-align: right; }

/* Persona picker chip in the top bar (only shown when personas are enabled). */
.persona-chip {
  border: 1px solid var(--line); border-radius: 999px; padding: 2px 9px;
  color: var(--brand); background: none; font: inherit; font-size: 13px; cursor: pointer;
}
.persona-chip:hover { background: var(--chip); }

/* --- floating table of contents (app.js builds it; base.html opts a page in) ---
   A detail page is a tall stack of cards; the rail says what's on the page, how much
   of it there is (the counts ride along from the headings), and where you are.
   It's `fixed`, and `main.with-toc` reserves the width by padding — no grid, so the
   cards' own layout is untouched and an HTMX swap of the panel can't disturb it.
   Below 1100px there isn't room for a rail beside full-width cards, so it's dropped
   entirely (the padding goes with it). */
.toc { display: none; }
/* Where a TOC click landed: the section's heading warms briefly and settles. After the
   page jumps the reader has to find the thing they asked for, and a rail highlight
   several hundred pixels away is not where they are looking.
   Restrained on purpose — this is a system of record, not a notification. One pass, no
   repeat, no movement: the brand tint at low alpha fading out, spread by box-shadow
   rather than padding so nothing on the page shifts. Being colour-only it is safe under
   prefers-reduced-motion (which targets motion), so it is not suppressed there — a
   reader who asked for less animation still gets told where they landed.
   NOT inside the rail's media query below: it styles the CARD, and the same call would
   be right for any future "jump to this section" affordance. */
@keyframes toc-flash {
  from { background: var(--flash); box-shadow: 0 0 0 7px var(--flash); }
  to   { background: transparent; box-shadow: 0 0 0 7px transparent; }
}
.toc-flash { animation: toc-flash 1.2s ease-out; border-radius: 5px; }

@media (min-width: 1100px) {
  main.with-toc { padding-right: 232px; }
  .toc {
    display: block; position: fixed; right: 24px; width: 176px;
    /* --topbar-h is measured in app.js: the topbar is sticky AND wraps, so its
       height is not a constant we can hard-code here. */
    top: calc(var(--topbar-h, 64px) + 24px);
    max-height: calc(100vh - var(--topbar-h, 64px) - 48px);
    overflow-y: auto;
    font-size: 13px;
  }
  .toc ul { list-style: none; margin: 0; padding: 0; }
  .toc a {
    display: flex; justify-content: space-between; gap: 8px; align-items: baseline;
    padding: 5px 10px; color: var(--muted); text-decoration: none;
    border-left: 2px solid var(--line); border-radius: 0 4px 4px 0;
  }
  .toc a:hover { color: var(--ink); background: var(--chip); }
  /* The section the reader is actually in (currentSection, app.js). */
  .toc a.current {
    color: var(--ink); font-weight: 600; border-left-color: var(--brand);
    background: var(--chip);
  }
  .toc-count {
    color: var(--muted); font-weight: 400; font-variant-numeric: tabular-nums;
  }
  /* The anchor lands under the sticky topbar without JS scroll math. */
  main.with-toc .card { scroll-margin-top: calc(var(--topbar-h, 64px) + 16px); }
}

/* --- print ----------------------------------------------------------------
 * A printed (or PDF'd) page from this tool is EVIDENCE — a change request handed to
 * an auditor, a policy with its review history. So nothing on paper may be truncated
 * or folded away: every screen affordance that hides content has to be undone, or the
 * reader gets six-line stubs of every narrative and closed field diffs with no way to
 * open them. This is correctness, not polish.
 */
@media print {
  /* Un-clamp every narrative and drop the fade that says "there's more". */
  .md-body.md-clamp { max-height: none; overflow: visible; }
  .md-body.md-clamp::after { content: none; }
  /* Controls do nothing on paper. */
  .md-controls, .btn, .actions, form { display: none; }
  /* Force every <details> open: field diffs, creation snapshots, long notes. A closed
     one would silently omit the very record the page exists to show. app.js also sets
     `open` on beforeprint (and restores it after) — browsers differ on whether the
     content of a closed <details> can be revealed by CSS at all, and this is not a
     guarantee to leave to a vendor quirk. This rule is the no-JS fallback. */
  details > summary { list-style: none; font-weight: 700; }
  details:not([open]) > :not(summary) { display: block; }
  /* Chrome that repeats or floats: the topbar prints once at the top; the sticky
     control row and the floating TOC are screen-only navigation. */
  header.topbar { position: static; }
  .toc, header.topbar nav { display: none; }
  body { background: #fff; }   /* print: paper is white whatever the theme */
  .card { box-shadow: none; border: 1px solid var(--line); }
  /* A link's target is invisible on paper; print it after the text. */
  .md a[href^="http"]::after { content: " (" attr(href) ")"; font-size: .85em; }
}

/* Risk assessment: the three statuses the other registers don't have. */
.badge.non_compliant { background:var(--bad-bg); color: var(--bad); }
.badge.unassessed { background:var(--chip); color:var(--chip-ink); }
.badge.not_applicable { background:var(--chip); color:var(--chip-ink); }

/* Control register: assessment posture (unassessed/at_risk/lapsed/inactive shared
   above). `stale` is the register's word for overdue; `failing` is the alarm. */
.badge.fresh { background:var(--ok-bg); color: var(--ok); }
.badge.stale { background:var(--bad-bg); color: var(--bad); }
.badge.failing { background:var(--bad-bg); color: var(--bad); box-shadow: inset 0 0 0 1px var(--bad); }

/* The register score, shown as earned/possible with its coverage line. */
.score-figure { font-size:1.6rem; font-weight:600; line-height:1.1; }
.score-pct { font-size:1.1rem; font-weight:500; }
.tally-area > summary { cursor:pointer; padding:6px 0; }
.tally-area > summary::marker { color:var(--muted); }
.tally-num { text-align:right; white-space:nowrap; }


/* =====================================================================
 * OPN Support additions.
 *
 * Everything ABOVE this line is opn-compliance's app.css, copied verbatim so
 * the two tools stay visually one family. Everything BELOW is what the
 * support tool needs that the compliance tool doesn't. Add here, don't edit
 * above, so a diff against the sibling stays readable.
 * ===================================================================== */

:root {
  --rail-w: 232px;
  --topbar-h: 52px;
}
.ink-info { color: var(--info); }
header.topbar nav a[aria-current="page"] { color: var(--ink); font-weight: 600; }

/* ---- State badges — raw enum value is the class name ------------------ */
/* Case.status */
.badge.new { background: var(--info-bg); color: var(--info); }
.badge.pending_customer { background:var(--chip); color:var(--chip-ink); }
.badge.pending_internal { background:var(--warn-bg); color: var(--warn); box-shadow: inset 0 0 0 1px var(--warn); }
.badge.resolved { background:var(--ok-bg); color: var(--ok); }
/* Terminal, but no longer news — grey, not the compliance tool's green. */
.badge.closed { background:var(--chip); color:var(--chip-ink); }
/* Case.priority — critical/high/low inherit the BCDR rules above */
.badge.normal { background:var(--chip); color:var(--chip-ink); }
/* Derived */
.badge.escalated { background:var(--bad-bg); color: var(--bad); box-shadow: inset 0 0 0 1px var(--bad); }
.badge.overdue { background:var(--bad-bg); color: var(--bad); }
.badge.archived { background:var(--chip); color:var(--chip-ink); box-shadow: inset 0 0 0 1px var(--line-strong); }
/* Response.status + review */
.badge.pending_send, .badge.awaiting_review { background:var(--warn-bg); color: var(--warn); }
.badge.sent_back { background:var(--warn-bg); color: var(--warn); box-shadow: inset 0 0 0 1px var(--warn); }
.badge.scheduled { background: var(--info-bg); color: var(--info); }
.badge.held { background: var(--info-bg); color: var(--info); }   /* a hold quiets the alarm: informational, not red */
.badge.sent { background:var(--ok-bg); color: var(--ok); }
.badge.failed, .badge.bounced, .badge.dropped { background:var(--bad-bg); color: var(--bad); }
.badge.discarded { background:var(--chip); color:var(--chip-ink); }
/* Response.delivery_status */
.badge.delivered { background:var(--ok-bg); color: var(--ok); }
.badge.deferred { background:var(--warn-bg); color: var(--warn); }
/* Response.source */
.badge.mcp_upload { background: var(--info-bg); color: var(--info); }
.badge.system_auto, .badge.web_ui { background:var(--chip); color:var(--chip-ink); }
/* Library, tokens, owners */
.badge.approved { background:var(--ok-bg); color: var(--ok); }
.badge.pending_approval { background:var(--warn-bg); color: var(--warn); }
.badge.retired, .badge.revoked, .badge.read_only { background:var(--chip); color:var(--chip-ink); }
.badge.full { background: var(--info-bg); color: var(--info); }
.badge.expired { background:var(--bad-bg); color: var(--bad); }
.badge.opn, .badge.fi, .badge.integrator { background:var(--chip); color:var(--chip-ink); font-weight: 600; }
.badge.admin { background: var(--info-bg); color: var(--info); }
.badge.operator { background:var(--chip); color:var(--chip-ink); }

/* ---- Owner path: FI › integrator ------------------------------------- */
.entities { display: inline-flex; flex-wrap: nowrap; align-items: center; gap: 2px 4px; font-size: 13px; white-space: nowrap; }
.entities .sep { color: var(--muted); }
.entities .owner { font-weight: 600; }
.entities .involved { color: var(--muted); }

/* ---- Summary band ------------------------------------------------------ */
.stats { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
.stat-card { margin: 0; padding: 12px 14px; min-width: 0; }
.stat-num { font-size: 26px; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }
.stat-label { font-size: 13px; color: var(--muted); margin-top: 2px; }
.stat-sub { font-size: 12px; color: var(--muted); margin-top: 6px; }
.stat-card.attention { border-color: var(--bad-line); }
.stat-card.attention .stat-num { color: var(--bad); }
.stat-card.info .stat-num { color: var(--info); }
.stat-card .badge { margin: 2px 4px 2px 0; }

/* A direct-to-bucket upload's progress (the Add video dialog). */
.upload-progress progress { width: 100%; height: 8px; }
.upload-progress[hidden], .upload-error[hidden] { display: none; }
/* ---- Reports: three breakdown tables side by side, one column when narrow --- */
.report-tables { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.report-tables .card { margin: 0; min-width: 0; }
.report-tables table { width: 100%; }
.report-text { margin: 0; white-space: pre-wrap; font-size: 13px; }
.btn.is-disabled { opacity: .5; cursor: default; }
/* ---- Cases table -------------------------------------------------------- */
table.cases th, table.cases td { padding: 8px 8px; }
table.cases td.type { font-size: 13px; color: var(--muted); }
td.ref a { font-weight: 600; white-space: nowrap; }
.legacy-ref { color: var(--muted); font-size: 12px; font-weight: 400; }
.subject-flags { display: inline-flex; gap: 4px; margin-left: 6px; vertical-align: middle; }
.blocked-on, .countdown { display: block; font-size: 12px; color: var(--muted); max-width: 24ch;
                          overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
td.age { white-space: nowrap; font-variant-numeric: tabular-nums; }
td.age.late { color: var(--bad); font-weight: 600; }
.unassigned { color: var(--muted); font-style: italic; }
tr.needs-review > td:first-child { box-shadow: inset 3px 0 0 var(--info); }
tr.escalated-row > td:first-child { box-shadow: inset 3px 0 0 var(--bad); }
.scope-toggle { display: flex; gap: 8px; flex-wrap: wrap; }
.scope-toggle .count { opacity: .7; font-weight: 400; margin-left: 5px; }
.poll-note { font-size: 12px; color: var(--muted); }
.poll-note::before { content: "●"; color: var(--ok); margin-right: 5px; font-size: 9px; vertical-align: middle; }
td .btn.small { white-space: nowrap; }

/* ---- Two-column case detail -------------------------------------------- */
/* New to the family: opn-compliance has no content sidebar (its rail is an
   auto-generated TOC). design.md asks for Related Cases, assignment and time
   tracking beside the correspondence, not below it. */
.detail { display: grid; gap: 16px; grid-template-columns: minmax(0, 1fr) 320px; align-items: start; }
.detail-main, .detail-side { min-width: 0; }
.detail-side .meta { grid-template-columns: 104px minmax(0, 1fr); font-size: 13px; }
.detail-side .meta .entities, .detail-side .meta dd { white-space: normal; flex-wrap: wrap; overflow-wrap: anywhere; }
.detail-side h2 { font-size: 15px; }
@media (max-width: 1100px) { .detail { grid-template-columns: minmax(0, 1fr); } }
.title-row { flex-wrap: wrap; row-gap: 8px; }
.inline-edit { display: inline-flex; align-items: center; gap: 6px; }
.inline-edit select { width: auto; padding: 3px 26px 3px 8px; font-size: 13px; }
.inline-edit input { width: 26ch; padding: 3px 8px; font-size: 13px; }
.inline-edit[hidden] { display: none; }
.assignment-box .who-big { font-weight: 600; font-size: 15px; }
/* The Assign-to select takes the room the card has, never the width of its longest name: a select
   sized to its options ran past the side column once operators had long names. */
.assignment-box select.assign-to { flex: 1 1 140px; min-width: 0; width: auto; max-width: 100%; }
.assignment-note { font-size: 13px; color: var(--muted); }
.side-actions { display: flex; flex-direction: column; gap: 8px; }
.side-actions .btn { text-align: left; }
.side-actions .btn.ghost { box-shadow: inset 0 0 0 1px var(--line); background: var(--card); }

/* Compose + review */
.card-info { background: var(--info-bg); border-color: var(--info-line); }
.provenance { display: flex; align-items: flex-start; gap: 10px; }
.ai-mark { flex: none; width: 28px; height: 28px; border-radius: 7px; background: var(--info); color: var(--brand-ink);
           font-weight: 700; display: inline-flex; align-items: center; justify-content: center; font-size: 12px; }
.review-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
/* More… reveals a row of its own under the buttons (a flex item spanning the row). */
.review-actions .more-actions { flex-basis: 100%; display: flex; gap: 8px 20px; flex-wrap: wrap; align-items: center;
                                margin-top: 2px; padding-top: 8px; border-top: 1px dashed var(--line); }
.review-actions .more-actions[hidden] { display: none; }
.lint { margin: 8px 0 0; padding-left: 20px; font-size: 13px; color: var(--warn); }
.lint li { margin: 2px 0; }
.compose textarea { min-height: 220px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
                    font-size: 13px; line-height: 1.45; }
.compose-toolbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin: 8px 0; }
.email-preview { border: 1px solid var(--line); border-radius: 7px; padding: 12px 14px; background: var(--card); font-size: 14px; }
.email-preview .hdr { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 2px 10px;
                      font-size: 12px; color: var(--muted); margin: 0 0 10px; }
.email-preview .hdr dd { margin: 0; color: var(--ink); overflow-wrap: anywhere; }
.email-preview .body p { margin: 0 0 10px; }
.email-preview .body p:last-child { margin-bottom: 0; }
.email-preview .body pre { background: var(--chip); padding: 8px 10px; border-radius: 6px; font-size: 12px; overflow-x: auto; }
.email-preview .body img { max-width: 100%; border: 1px solid var(--line); border-radius: 5px; }
.attachments { display: flex; flex-wrap: wrap; gap: 6px; }
.attachment { font-size: 12px; border: 1px solid var(--line); border-radius: 999px; padding: 2px 9px; background: var(--card); }
.attachment.from-library::before { content: "✓ "; color: var(--ok); }
.response-list > li { padding: 12px 0; border-bottom: 1px solid var(--line); }
.response-list > li:last-child { border-bottom: none; }

/* Timeline extensions */
.event-head .actor-sys { color: var(--muted); font-style: italic; font-weight: 400; }
.timeline li.inbound { background: var(--chip); border-radius: 7px; padding: 10px 12px; margin: 6px 0; border-bottom: none; }
.timeline li.inbound.sms { box-shadow: inset 3px 0 0 var(--ok); }
.timeline li.inbound.email { box-shadow: inset 3px 0 0 var(--brand); }
.timeline .ev-actions { margin: 8px 0 0 18px; display: flex; gap: 8px; flex-wrap: wrap; }
.timeline .ev-note { margin: 4px 0 0 18px; font-size: 13px; }

/* Time tracking */
.timer { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.timer .elapsed { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 20px;
                  font-weight: 600; font-variant-numeric: tabular-nums; }
.timer.running .elapsed { color: var(--ok); }
.timer.running .elapsed::before { content: "●"; font-size: 10px; margin-right: 6px; vertical-align: middle;
                                  animation: pulse 1.4s infinite; }
@keyframes pulse { 50% { opacity: .25; } }
table.compact th, table.compact td { padding: 5px 6px; font-size: 13px; }
table.compact td.num, table.compact th.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Related cases */
.related { list-style: none; margin: 0; padding: 0; }
.related li { padding: 7px 0; border-bottom: 1px dashed var(--line); font-size: 13px; }
.related li:last-child { border-bottom: none; }
.rel-type { display: inline-block; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); margin-right: 6px; }
.ref-chain { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 13px; }
.ref-chain .arrow { color: var(--muted); }

/* ---- Dialogs ------------------------------------------------------------ */
/* Native <dialog>. Used where a flow gathers several inputs before one
   committing action (escalate, new case, notify external, insert image);
   <details> stays for plain disclosure. */
dialog.modal { border: 1px solid var(--line); border-radius: 10px; padding: 0; background: var(--card); color: var(--ink);
               width: min(640px, calc(100vw - 32px)); box-shadow: 0 20px 60px rgba(var(--shadow-rgb), .28); }
dialog.modal.wide { width: min(880px, calc(100vw - 32px)); }
dialog.modal::backdrop { background: var(--backdrop); }
/* A modal owns the scroll. While one is open the page behind it does not move — wheeling over the
   dialog, or past the end of its body or of an open picker's list, used to scroll the page under
   the backdrop. The gutter stays reserved so the page doesn't jump sideways when the scrollbar goes. */
html { scrollbar-gutter: stable; }
html:has(dialog[open]) { overflow: hidden; }
dialog.modal, .modal-body, .picker-menu, .sheet-body { overscroll-behavior: contain; }
dialog.modal > form { display: contents; }
/* Dialogs whose content changes size while open — tabs, a filtered thumbnail grid — keep one
   height so switching tabs doesn't move the footer; the body scrolls instead. */
dialog.modal.tall .modal-body { height: min(560px, 70vh); }
.modal-head { display: flex; align-items: center; gap: 12px; padding: 14px 18px; border-bottom: 1px solid var(--line); }
.modal-head h2 { margin: 0; font-size: 16px; }
/* A dialog may use most of the viewport before its body scrolls; the head and foot take the rest. */
.modal-body { padding: 14px 18px; max-height: calc(100vh - 150px); overflow-y: auto; }
.modal-body > label:first-child { margin-top: 0; }
.modal-foot { display: flex; gap: 8px; align-items: center; justify-content: flex-end; padding: 12px 18px;
              border-top: 1px solid var(--line); background: var(--chip); border-radius: 0 0 10px 10px; }
.modal-foot .muted { margin-right: auto; }
.modal-close { margin-left: auto; background: none; border: none; font-size: 22px; line-height: 1;
               color: var(--muted); cursor: pointer; padding: 0 4px; }
.modal-close:hover { color: var(--ink); }

/* ---- Field-level validation -------------------------------------------- */
/* Departs from opn-compliance's single page-level .err: the invalid control
   itself is marked and its message sits under it. .err is kept for errors
   that aren't about one field (a lost claim race, a SendGrid failure). */
[aria-invalid="true"] { border-color: var(--bad); box-shadow: 0 0 0 2px rgba(var(--bad-rgb), .14); }
.field-error { color: var(--bad); font-size: 13px; margin: 4px 0 0; }
.field-error::before { content: "⚠ "; }
.field-hint { color: var(--muted); font-size: 12px; margin: 4px 0 0; }
label .req { margin-left: 2px; }

/* ---- Tabs ---------------------------------------------------------------- */
[role="tablist"] { display: flex; gap: 2px; border-bottom: 1px solid var(--line); margin-bottom: 14px; }
[role="tab"] { background: none; border: none; border-bottom: 2px solid transparent; padding: 8px 12px; font: inherit;
               font-size: 14px; color: var(--muted); cursor: pointer; margin-bottom: -1px; }
[role="tab"]:hover { color: var(--ink); }
[role="tab"][aria-selected="true"] { color: var(--brand); border-bottom-color: var(--brand); font-weight: 600; }
[role="tab"] .count { opacity: .7; font-weight: 400; margin-left: 4px; font-size: 12px; }
[role="tabpanel"][hidden] { display: none; }

/* ---- Library ------------------------------------------------------------ */
.thumb-grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); }
.thumb { border: 1px solid var(--line); border-radius: 8px; padding: 8px; background: var(--card); font-size: 12px; min-width: 0; }
.thumb .img { height: 92px; border-radius: 5px; background: var(--chip); display: flex; align-items: center;
              justify-content: center; color: var(--muted); margin-bottom: 6px; overflow: hidden; }
.thumb .img svg { max-height: 76px; max-width: 100%; }
.thumb .label { font-weight: 600; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.thumb .meta-line { color: var(--muted); display: block; margin-top: 2px; }
.thumb.pending { border-style: dashed; }
.thumb.retired { opacity: .6; }
.thumb.selected { box-shadow: 0 0 0 2px var(--brand); }
.thumb .hrow { margin-top: 8px; }
.dropzone { border: 2px dashed var(--line-strong); border-radius: 8px; padding: 28px 16px; text-align: center; color: var(--muted);
            background: var(--chip); }
.dropzone input[type="file"] { width: auto; margin-top: 8px; }
.video-row .url { color: var(--muted); font-size: 12px; overflow-wrap: anywhere; }

/* ---- Login --------------------------------------------------------------- */
.login-brand { display: flex; align-items: center; justify-content: center; gap: 10px; font-weight: 700; font-size: 18px; margin-bottom: 4px; }
.login-brand img { width: 36px; height: 36px; }
.sso-list { display: flex; flex-direction: column; gap: 8px; margin: 16px 0 8px; text-align: left; }
.sso-list .btn { display: flex; align-items: center; gap: 10px; }
.sso-list .idp { color: inherit; opacity: .7; font-weight: 400; margin-left: auto; font-size: 12px; }
.sso-list .btn.ghost .idp { color: var(--muted); opacity: 1; }
.login-owner { display: flex; gap: 8px; align-items: center; }
.login-owner select { width: auto; flex: 1; }

/* ---- Left rail (leftnav layout only) ------------------------------------ */
.shell { display: grid; grid-template-columns: var(--rail-w) minmax(0, 1fr); min-height: 100vh; }
.shell.no-rail { grid-template-columns: minmax(0, 1fr); }
.rail { background: var(--card); border-right: 1px solid var(--line); position: sticky; top: 0; height: 100vh;
        overflow-y: auto; display: flex; flex-direction: column; padding: 12px; gap: 2px; }
.rail-body { display: flex; flex-direction: column; gap: 2px; flex: 1; min-height: 0; }
.rail-brand { display: flex; align-items: center; gap: 10px; font-weight: 700; padding: 4px 6px 12px; }
.rail-brand img { width: 28px; height: 28px; flex: none; }
.rail-brand .toggle { margin-left: auto; padding: 4px 6px; line-height: 0; }
.rail-brand .toggle svg { display: block; width: 16px; height: 16px; }
.rail-brand .toggle .ico-menu, .rail-brand .toggle .ico-close { display: none; }
html.rail-collapsed .rail-brand .toggle .ico-collapse { transform: scaleX(-1); }
.rail-search { margin: 0 0 8px; }
.rail-search input { padding: 6px 10px; font-size: 13px; }
.rail-section { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); padding: 14px 8px 4px; }
.rail-link { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 7px; color: var(--ink); font-size: 14px; }
.rail-link:hover { background: var(--chip); text-decoration: none; }
.rail-link[aria-current="page"] { background: var(--chip); font-weight: 600; color: var(--brand); }
.ico { width: 18px; text-align: center; color: var(--muted); flex: none; font-size: 13px; }
.rail-link[aria-current="page"] .ico { color: var(--brand); }
.rail-count { margin-left: auto; font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums;
              background: var(--chip); border-radius: 999px; padding: 1px 7px; }
.rail-link[aria-current="page"] .rail-count { background: var(--card); }
.rail-count.hot { background: var(--bad-bg); color: var(--bad); font-weight: 600; }
.rail-count.info { background: var(--info-bg); color: var(--info); font-weight: 600; }
.rail-foot { margin-top: auto; padding-top: 10px; border-top: 1px solid var(--line); display: flex; flex-direction: column;
             gap: 2px; font-size: 13px; align-items: stretch; }
.rail-foot .timer-widget { margin-bottom: 6px; }
/* Foot rows share the nav link's shape. Buttons reset to it; .static rows don't react; .sub rows
   are indented under the user to the text column of the row above (icon width + gap). */
.rail-foot .foot-row { width: 100%; background: none; border: 0; font: inherit; font-size: 13px; text-align: left; cursor: pointer; }
.rail-foot .foot-row.static { cursor: default; }
.rail-foot .foot-row.static:hover { background: none; }
.rail-foot .foot-row.sub { padding-left: 30px; padding-top: 2px; padding-bottom: 2px; font-size: 12px; }
.rail-foot .foot-row.sub.muted { color: var(--muted); cursor: default; }
.rail-foot .foot-label { color: var(--muted); }
.rail-foot .foot-value { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rail-foot .chev { margin-left: auto; color: var(--muted); font-size: 11px; }
.rail-foot form { margin: 0; }
html.rail-collapsed .shell { grid-template-columns: 56px minmax(0, 1fr); }
html.rail-collapsed .rail { padding: 12px 8px; }
/* The collapsed rail keeps the timer as a dot + h:mm; everything else in the foot hides. */
.timer-mini { display: none; }
html.rail-collapsed .timer-mini { display: flex; flex-direction: column; align-items: center; gap: 2px; margin-top: auto; padding: 8px 0;
                                  background: none; border: 0; border-top: 1px solid var(--line); width: 100%; cursor: pointer;
                                  color: var(--muted); font: inherit; font-size: 11px; }
html.rail-collapsed .timer-mini.running { color: var(--ok); }
html.rail-collapsed .timer-mini .js-tick-hm { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; }
html.rail-collapsed .timer-mini .dot { font-size: 12px; }
html.rail-collapsed .timer-mini:hover { background: var(--chip); }
html.rail-collapsed .rail-link > span:not(.ico), html.rail-collapsed .rail-section, html.rail-collapsed .rail-foot,
html.rail-collapsed .rail-search, html.rail-collapsed .rail-brand .name, html.rail-collapsed .rail-brand img { display: none; }
html.rail-collapsed .rail-brand { padding: 0 0 8px; justify-content: center; }
html.rail-collapsed .rail-brand .toggle { margin: 0; }
html.rail-collapsed .rail-link { justify-content: center; padding: 8px 0; }
/* Narrow screens: the rail is a one-line top bar (brand + ☰). Opening it slides the whole rail in
   as a drawer over a backdrop; the desktop collapse state is ignored here. */
.rail-backdrop { display: none; }
@media (max-width: 900px) {
  .shell, html.rail-collapsed .shell { grid-template-columns: minmax(0, 1fr); }
  /* The rail is a fixed top bar (brand + menu). Its body — search, nav, timer, foot — is a separate
     fixed drawer beneath the bar, so the bar never moves and the whole drawer slides as one box. */
  .rail, html.rail-collapsed .rail { position: fixed; top: 0; left: 0; right: 0; height: var(--topbar-h); z-index: 90; border-right: none; border-bottom: 1px solid var(--line);
                                     padding: 0 12px; overflow: visible; display: block; }
  .shell-main { padding-top: var(--topbar-h); }
  /* Not sticky here — a phone can't spare the height. Only the top bar stays fixed. */
  .shell-main .page-head { position: static; margin-top: -16px; }
  .rail-brand, html.rail-collapsed .rail-brand { height: var(--topbar-h); padding: 0; justify-content: flex-start; }
  html.rail-collapsed .rail-brand .name, html.rail-collapsed .rail-brand img { display: inline-block; }
  .rail-brand .toggle, html.rail-collapsed .rail-brand .toggle { margin-left: auto; }
  .rail-brand .toggle .ico-collapse { display: none; }
  .rail-brand .toggle .ico-menu { display: block; }
  html.rail-open .rail-brand .toggle .ico-menu { display: none; }
  html.rail-open .rail-brand .toggle .ico-close { display: block; }
  html.rail-collapsed .rail-brand .toggle .ico-collapse { transform: none; }
  .rail-body, html.rail-collapsed .rail-body { display: none; }
  html.rail-open .rail-body { display: flex; position: fixed; top: var(--topbar-h); left: 0; bottom: 0; width: min(320px, 88vw); overflow-y: auto; z-index: 90;
                              padding: 8px 12px 12px; background: var(--card); border-right: 1px solid var(--line); box-shadow: 0 0 40px rgba(var(--shadow-rgb), .3); }
  html.rail-open .rail-backdrop { display: block; position: fixed; top: var(--topbar-h); left: 0; right: 0; bottom: 0; background: var(--backdrop); z-index: 80; }
  .rail .timer-mini, html.rail-collapsed .timer-mini { display: none; }
  html.rail-collapsed .rail-search { display: block; }
  html.rail-collapsed .rail-foot { display: flex; }
  html.rail-collapsed .rail-link { justify-content: flex-start; padding: 6px 8px; }
  html.rail-collapsed .rail-link > span:not(.ico) { display: inline; }
  html.rail-collapsed .rail-link > .rail-count { display: inline-block; }
  html.rail-collapsed .rail-section { display: block; }
  html.rail-collapsed .timer-widget, html.rail-open .rail-foot .timer-widget { display: block; }
  /* The drawer slides in as one box; the bar doesn't move. */
  html.rail-open .rail-body { animation: drawer-in .22s ease-out; }
  html.rail-open .rail-backdrop { animation: fade-in .22s ease-out; }
  html.rail-open.rail-closing .rail-body { animation: drawer-out .18s ease-in forwards; }
  html.rail-open.rail-closing .rail-backdrop { animation: fade-out .18s ease-in forwards; }
}

/* ---- Misc ------------------------------------------------------------------ */
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .92em; }
.sms-preview { background: var(--ok-wash); border: 1px solid var(--ok-line); border-radius: 12px 12px 12px 2px; padding: 10px 12px;
               font-size: 13px; max-width: 46ch; white-space: pre-wrap; overflow-wrap: anywhere; }
.seg-info { font-size: 12px; color: var(--muted); }
.seg-info.warn { color: var(--warn); }
.redacted { color: var(--muted); font-style: italic; }
.card.archived-note { background: var(--chip); }
.workload { display: grid; grid-template-columns: max-content minmax(0, 1fr) max-content; gap: 6px 12px; align-items: center; font-size: 13px; }
.workload .bar { height: 8px; background: var(--chip); border-radius: 999px; overflow: hidden; }
.workload .bar > span { display: block; height: 100%; background: var(--brand); }
.workload .bar.hot > span { background: var(--bad); }
.pill-list { display: flex; flex-wrap: wrap; gap: 4px; }
.index-list { list-style: none; padding: 0; margin: 0; }
.index-list li { padding: 10px 0; border-bottom: 1px solid var(--line); }
.index-list li:last-child { border-bottom: none; }
.index-list .note { color: var(--muted); font-size: 13px; margin-top: 2px; }
.workload .bar .w-1 { width: 14%; } .workload .bar .w-2 { width: 29%; } .workload .bar .w-3 { width: 43%; }
.workload .bar .w-4 { width: 57%; } .workload .bar .w-5 { width: 71%; } .workload .bar .w-6 { width: 86%; } .workload .bar .w-7 { width: 100%; }

/* ---- Revision: entities, requested reviews, MFA ---------------------- */
.badge.owner { background: var(--info-bg); color: var(--info); font-weight: 600; }
.badge.involved, .badge.service_provider, .badge.core_processor { background:var(--chip); color:var(--chip-ink); font-weight: 600; }
.badge.review_required { background: var(--info-bg); color: var(--info); }
.badge.review_requested { background:var(--warn-bg); color: var(--warn); }
.entities .owner .mark { font-size: 10px; color: var(--muted); font-weight: 400; margin-left: 2px; }
.pill-list .badge { margin: 2px 4px 2px 0; }
input.otp { width: 9ch; max-width: 100%; height: 40px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 20px; letter-spacing: .3em;
            text-align: center; padding: 0 10px; }
.qr { width: 132px; height: 132px; flex: 0 0 132px; border: 1px solid var(--line); border-radius: 6px; background: #fff; }   /* a QR code is dark on light in either theme: scanners expect it */
/* The generated <svg> carries only a viewBox (mfa.totp_qr_svg strips its mm size), so it fills the box. */
.qr svg { display: block; width: 100%; height: 100%; }
/* The column beside the QR may shrink below its content's natural width (the key breaks anywhere). */
.qr + .grow { min-width: 0; }
.login-or { display: flex; align-items: center; gap: 10px; color: var(--muted); font-size: 12px; margin: 14px 0 10px; }
.login-or::before, .login-or::after { content: ""; flex: 1; border-top: 1px solid var(--line); }

/* ---- Revision: entity tree, broadcasts ------------------------------- */
/* Depth in the entity tree is carried by a class, never an inline style. */
table.tree td, table.tree th { padding-left: 8px; padding-right: 8px; }
table.tree td { font-size: 13px; }
table.tree td.name { white-space: nowrap; }
table.tree td.name.depth-1 { padding-left: 34px; }
table.tree td.name.depth-2 { padding-left: 60px; }
table.tree td.name.depth-3 { padding-left: 86px; }
table.tree td.name.depth-1::before, table.tree td.name.depth-2::before, table.tree td.name.depth-3::before {
  content: "└"; color: var(--line-strong); margin-right: 8px; }
.bc-unread { font-weight: 600; }
.bc-unread::before { content: "●"; color: var(--info); margin-right: 6px; font-size: 9px; vertical-align: middle; }
.badge.production_update, .badge.sandbox_update, .badge.product_update { background: var(--info-bg); color: var(--info); }
.badge.service_disruption { background:var(--bad-bg); color: var(--bad); }
.badge.service_restored { background:var(--ok-bg); color: var(--ok); }
.badge.planned_maintenance { background:var(--warn-bg); color: var(--warn); }
.badge.general { background:var(--chip); color:var(--chip-ink); }
.reach { font-size: 13px; }
.reach strong { font-variant-numeric: tabular-nums; }
.rule-box { background: var(--chip); border-radius: 7px; padding: 10px 12px; font-size: 13px; }
.rule-box code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; }
table.tree th, table.tree td { padding: 8px 8px; }
table.tree .badge { white-space: nowrap; }
table.tree td.actions { white-space: nowrap; }
.nowrap { white-space: nowrap; }

/* ---- Case description ----------------------------------------------- */
/* Reuses opn-compliance's .mdblock / .md-clamp / .md-controls machinery; the
   clamp is 5 lines here (7.5em at line-height 1.5) rather than its 6em. */
.case-description .md-body.md-clamp { max-height: 7.5em; }
.case-description .md-src { white-space: pre-wrap; }
/* The original request is a verbatim <pre> (an email isn't Markdown): the same clamp,
   wrapped so a long header line or a pasted paragraph never scrolls sideways. */
.case-request .md-body.md-clamp { max-height: 7.5em; }
.case-request .md-src { white-space: pre-wrap; }

/* ---- Expandable Markdown — improvements over the base .mdblock pattern ---- */
/* Clamp by line height so it's exactly five lines whatever the font size (em fallback first). */
.md-body.md-clamp { max-height: 9em; max-height: 6lh; cursor: pointer; }   /* five readable lines + one fading */
.case-description .md-body.md-clamp { max-height: 9em; max-height: 6lh; }
/* A taller, eased fade over the last three lines: the text visibly continues rather than being cut. */
.md-body.md-clamp::after {
  height: 4.5em; height: 3lh;
  background: linear-gradient(to bottom,
    transparent 0%,
    color-mix(in srgb, var(--clamp-fade) 25%, transparent) 30%,
    color-mix(in srgb, var(--clamp-fade) 70%, transparent) 65%,
    var(--clamp-fade) 100%);
}
/* Controls: a centred pill that overlaps the fade, Raw pushed to the right and muted. The row keeps the
   base rule's `position: sticky; bottom: 0` (a `relative` here once overrode it, and Show less on a
   2,000-line description was a screen-height scroll away); Raw's absolute offset works inside sticky. */
.md-controls { justify-content: center; padding: 0; margin-top: 6px; min-height: 26px; }
/* Only while clamped does the pill overlap the fade; expanded, it sits below the text. */
.mdblock:has(.md-body.md-clamp) .md-controls { margin-top: -14px; }
.md-more-toggle {
  text-decoration: none; font-size: 12px; font-weight: 600; color: var(--brand);
  border: 1px solid var(--line); border-radius: 999px; padding: 3px 12px; background: var(--card);
  box-shadow: 0 1px 2px rgba(var(--shadow-rgb), .08);
}
.md-more-toggle:hover { color: var(--brand); background: var(--chip); }
.md-more-toggle::after { content: " ▾"; }
.md-more-toggle[aria-expanded="true"]::after { content: " ▴"; }
.md-more-toggle .n { font-weight: 400; color: var(--muted); }
.md-raw-toggle { position: absolute; right: 0; top: 4px; font-size: 11px; }
.mdblock.md-raw .md-controls { margin-top: 6px; }
/* Expand/collapse animation. JS sets explicit pixel max-heights on the way in and out, so this
   transition has two lengths to interpolate; initial render and remembered-state restore go
   none → 6lh, which isn't interpolable, so they don't animate. */
.md-body { transition: max-height .28s ease; }
.md-body.md-animating { overflow: hidden; }
@media (prefers-reduced-motion: reduce) { .md-body { transition: none; } }
/* Page-level Expand all / Collapse all — JS shows it only when 2+ blocks on the page are clampable. */
[data-md-expand-all][hidden] { display: none; }
.timeline .mdblock { margin-left: 18px; color: var(--ink); }
.badge.junior_operator { background:var(--chip); color:var(--chip-ink); box-shadow: inset 0 0 0 1px var(--line-strong); }
td select.role { width: auto; padding: 3px 26px 3px 8px; font-size: 13px; }

/* ---- Help ------------------------------------------------------------- */
/* The one consistent way into help: a "?" at the right end of every page header. */
.help-link { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; flex: none;
             border-radius: 999px; border: 1px solid var(--line); background: var(--card); color: var(--muted);
             font-weight: 700; font-size: 13px; text-decoration: none; }
.help-link:hover { color: var(--brand); border-color: var(--brand); text-decoration: none; }
/* The mark is an inline SVG (like the rail toggle's), so it sits dead centre in the circle at any font. */
.help-link svg { display: block; width: 14px; height: 14px; }
/* Help is Markdown rendered through .md, on a full page (article.help) or in the side sheet. */
.help { font-size: 15px; }
article.help { max-width: 82ch; }
.help h1 { font-size: 22px; margin: 0 0 12px; }
.help h2 { font-size: 17px; margin: 22px 0 8px; }
.help h3 { font-size: 15px; margin: 16px 0 6px; }
.help p, .help li { line-height: 1.55; }
.help table { display: table; width: 100%; margin: 8px 0 12px; }
.help th { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
.help td { font-size: 14px; vertical-align: top; }
.help-link { cursor: pointer; font-family: inherit; padding: 0; }
.link-btn { background: none; border: none; padding: 0; font: inherit; color: var(--brand); cursor: pointer; }
.link-btn:hover { text-decoration: underline; }

/* ---- Side sheet ----------------------------------------------------- */
/* Slides in from the right over the page; NOT modal — no backdrop, the page stays usable, so
   nothing typed into a form is lost. Esc or × closes it. */
.sheet { position: fixed; top: 0; right: 0; bottom: 0; width: min(480px, 100vw); z-index: 200;
         background: var(--card); border-left: 1px solid var(--line); box-shadow: -8px 0 28px rgba(var(--shadow-rgb), .14);
         display: flex; flex-direction: column;
         transform: translateX(100%); visibility: hidden;
         transition: transform .25s ease, visibility 0s linear .25s; }
.sheet.open { transform: none; visibility: visible; transition: transform .25s ease, visibility 0s; }
@media (prefers-reduced-motion: reduce) { .sheet { transition: none; } }
.sheet-head { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-bottom: 1px solid var(--line); }
.sheet-head h2 { font-size: 15px; }
.sheet-body { flex: 1; overflow-y: auto; padding: 14px 18px 24px; }
.sheet-body h1 { font-size: 18px; }
.sheet-body h2 { font-size: 15px; margin-top: 18px; }
.sheet-body td, .sheet-body th { padding: 6px 6px; }
.sheet-foot { padding: 8px 16px; border-top: 1px solid var(--line); }
/* A standard holiday OPN works: listed, struck through, restorable. */
tr.removed td { color: var(--muted); }
/* The audit row whose event is open in the sheet. */
tr.is-open td { background: var(--info-bg); }
.linklike.plain { color: inherit; text-decoration: none; text-align: left; }
.linklike.plain:hover { color: var(--brand); }

/* ---- Sticky page head (list pages) ------------------------------------ */
/* Title row + filter bar stay put while the table scrolls beneath. Bleeds to main's edges so
   content passes under it cleanly. The scope (All open, Mine, Unassigned…) is chosen in the rail,
   so the head carries only search, filters and the column picker. */
.page-head { position: sticky; top: 0; z-index: 50; background: var(--bg);
             margin: -24px -24px 0; padding: 20px 24px 12px; border-bottom: 1px solid var(--line); }
.page-head + * { margin-top: 8px; }
/* The title row of a list page never wraps: the subtitle shrinks to an ellipsis first, and the
   buttons and the help "?" keep their line. (Case heads keep wrapping — they hold controls.) */
.page-head:not(.case-head) > .hrow { flex-wrap: nowrap; }
.page-head:not(.case-head) > .hrow > h1 { white-space: nowrap; }
.page-head:not(.case-head) > .hrow > .muted.fs-13, .page-head:not(.case-head) > .hrow > .scope-now { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.page-head:not(.case-head) > .hrow > .btn, .page-head:not(.case-head) > .hrow > .help-link, .page-head:not(.case-head) > .hrow > .poll-note { flex: none; white-space: nowrap; }
@media (max-width: 600px) { .page-head:not(.case-head) > .hrow > .muted.fs-13 { display: none; } }   /* + the head's 12px bottom padding = 20px visible */
.card.compact { padding: 10px 12px; }
.card.compact .row { gap: 8px; }
.card.compact .row > * { min-width: 140px; }
.card.compact input, .card.compact select, .card.compact .picker-search { height: 36px; box-sizing: border-box; padding: 0 10px; font-size: 14px; line-height: 34px; }
.card.compact input[type="search"] { -webkit-appearance: none; appearance: none; }
.card.compact input[type="checkbox"] { height: auto; line-height: normal; }
.card.compact .columns { margin-top: 6px; }
.scope-now { font-size: 13px; color: var(--muted); }
.scope-now strong { color: var(--ink); font-weight: 600; }
.page-head [role="tablist"] { border-bottom: none; margin: 10px 0 0; }
/* Case pages: the same sticky head around the polled header. Tighter, so the title row, subject
   line and (when present) the blocked-on note fit in about 100px. */
.page-head.case-head { padding-top: 14px; padding-bottom: 10px; }
.page-head.case-head .title-row { margin-top: 0; }
.page-head.case-head h1 { font-size: 20px; }
.page-head.case-head .inline-edit input { width: 20ch; }
.stats { margin-bottom: 20px; }   /* same 20px below the cards as above them */

/* ---- Infinite scroll ---------------------------------------------------- */
/* The last row/item is a "Load more" button that htmx fires when it scrolls into view
   (hx-trigger="revealed"); without JS it is simply a button. An end marker replaces it once
   everything matching is loaded. */
.more-row > td, .more-row { text-align: center; padding: 10px; border-bottom: none; }
.more-row .btn { min-width: 12ch; }
.end-marker { text-align: center; color: var(--muted); font-size: 13px; padding: 10px; }

/* ---- Entity picker ------------------------------------------------------ */
/* A drop-down: the list exists only while its box is active. It is position:fixed and placed by
   JS (pickerPlace) so a dialog body's scroll box can't clip it; JS shows at most data-limit
   matches and says how many more there are. */
.picker { position: relative; }
.picker-search { width: 100%; }
.picker-menu { position: fixed; z-index: 60; max-height: 216px; overflow-y: auto; margin: 0; border: 1px solid var(--line); border-radius: 7px;
               background: var(--card); box-shadow: 0 10px 30px rgba(var(--shadow-rgb), .16); width: max-content; max-width: min(520px, calc(100vw - 16px)); }
.picker-menu[hidden], .picker-item[hidden] { display: none; }
/* hidden always wins, whatever display a class sets — the pickers, banners and conflict notes rely on it. */
[hidden] { display: none !important; }
.picker-item { display: flex; align-items: center; gap: 8px; width: 100%; padding: 6px 10px; margin: 0; border: 0; background: none;
               font: inherit; font-size: 14px; color: var(--ink); text-align: left; cursor: pointer; }
.picker-item:hover, .picker-item.is-active { background: var(--chip); }
.picker-item.is-selected { background: var(--info-bg); }
.picker-item.is-active.is-selected { box-shadow: inset 3px 0 0 var(--info); }
.picker-item input[type="checkbox"] { width: auto; margin: 0; flex: none; }
.picker-item .picker-name { min-width: 0; white-space: nowrap; }
.picker-item .path { color: var(--muted); font-size: 12px; margin-left: auto; white-space: nowrap; }
/* A filtered or sorted tree table is flat: every row at depth 0 with its ancestors after the name. */
table.tree td.name .path { color: var(--muted); font-size: 12px; margin-left: 6px; }
.picker-item.depth-2 { padding-left: 26px; } .picker-item.depth-3 { padding-left: 42px; }
.picker-more { padding: 6px 10px; font-size: 12px; color: var(--muted); border-top: 1px solid var(--line); position: sticky; bottom: 0; background: var(--card); }
.picker-pills { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 6px; min-height: 0; }
.picker-pill { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; padding: 2px 8px 2px 10px; border-radius: 999px;
               background: var(--info-bg); color: var(--info); }
.picker-pill button { background: none; border: 0; padding: 0; color: inherit; font-size: 14px; line-height: 1; cursor: pointer; }
.picker-pills:empty::before { content: "Nothing selected"; font-size: 12px; color: var(--muted); }
.hrow > .picker { flex: 0 1 260px; min-width: 200px; }

/* ---- Personas and Audit -------------------------------------------------- */
.badge.own_name { background: var(--info-bg); color: var(--info); font-weight: 600; }
.badge.yours { background:var(--ok-bg); color: var(--ok); font-weight: 600; }
.badge.shared { background:var(--chip); color:var(--chip-ink); font-weight: 600; }
.badge.claude { background:var(--agent-bg); color:var(--agent); }
.badge.mcp { background:var(--agent-bg); color:var(--agent); }
.badge.email, .badge.sms { background: var(--info-bg); color: var(--info); }
.badge.job { background:var(--chip); color:var(--chip-ink); }
table.audit .actor-sys { color: var(--muted); font-style: italic; }
table.audit td { vertical-align: top; }
/* A plain vertical list of radio or checkbox rows (the export dialog's row choice, the video
   picker); the number input inside a row stays inline. */
.contact-pick { display: flex; flex-direction: column; gap: 8px; }
.contact-pick label { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 0; font-size: 14px; color: var(--ink); cursor: pointer; }
.contact-pick label input[type="radio"], .contact-pick label input[type="checkbox"] { width: auto; flex: 0 0 auto; margin: 0; }
.contact-pick label input[type="number"] { height: auto; width: 6em; flex: 0 0 auto; margin: 0; padding: 2px 6px; font-size: 13px; }
.persona-pick { display: flex; flex-direction: column; gap: 6px; }
.persona-pick label { display: flex; align-items: center; gap: 10px; margin: 0; padding: 8px 10px; border: 1px solid var(--line); border-radius: 7px; color: var(--ink); cursor: pointer; }
.persona-pick label:has(input:checked) { background: var(--info-bg); border-color: var(--info); }
.persona-pick label.is-disabled { color: var(--muted); cursor: not-allowed; }
.persona-pick input { width: auto; margin: 0; }
.persona-pick .muted { font-size: 12px; margin-left: auto; }

/* ---- Time ------------------------------------------------------------------ */
/* The timer widget in the rail foot: one place, every page. */
.timer-widget { padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px; margin-bottom: 8px; font-size: 13px;
                align-self: stretch; width: 100%; box-sizing: border-box; min-width: 0; overflow: hidden; }
.timer-widget .hrow { flex-wrap: nowrap; min-width: 0; }
.timer-widget .timer-case { white-space: nowrap; }
.timer-widget.running { border-color: var(--ok); background: var(--ok-wash); }
.timer-widget .timer-case { font-weight: 600; }
.timer-widget .elapsed { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; white-space: nowrap; color: var(--ok); margin-left: auto; }
.timer-widget.running .elapsed::before { content: "●"; font-size: 9px; margin-right: 5px; vertical-align: middle; }
.timer-widget .timer-subject { color: var(--muted); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.linklike { background: none; border: 0; padding: 0; color: var(--brand); font: inherit; cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
.timer-btn.running { border-color: var(--ok); color: var(--ok); background: var(--ok-wash); }
.timer-btn .js-tick { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.inline-input { display: inline-block; width: auto; margin-left: 6px; padding: 2px 6px; font-size: 13px; }
/* The Time page: day rows, voided entries, revision trail rows. */
table.time .day-row td { background: var(--chip); padding: 6px 10px; }
table.time tr.voided td { color: var(--muted); }
table.time tr.voided td:not(.actions) { text-decoration: line-through; }
table.time tr.voided td .badge, table.time tr.voided td .linklike { text-decoration: none; }
table.time tr.rev td, table.compact tr.rev td { background: var(--bg-soft); padding: 4px 10px 8px 28px; border-left: 3px solid var(--line); }
.timeline.compact li { padding: 6px 0; }
.rev-diff .rev-field { display: inline-block; color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .03em; margin-right: 4px; }
.rev-diff s { text-decoration-color: var(--muted); }
.badge.timer { background: var(--info-bg); color: var(--info); }
.badge.manual { background:var(--chip); color:var(--chip-ink); }
.ink-ok { color: var(--ok); font-weight: 600; }
.picker-case .picker-name { white-space: normal; }
.picker-case .picker-item .path { align-self: flex-start; }

/* ---- Save conflicts ---------------------------------------------------------- */
table.conflict td { vertical-align: top; }
table.conflict td.was { color: var(--muted); }
table.conflict tr.is-conflict td.now, table.conflict tr.is-conflict td.mine { font-weight: 600; }
table.conflict tr.is-conflict td.now { background: var(--warn-wash); }
table.conflict tr.is-conflict td.mine { background: var(--info-bg); }
table.conflict tr.is-theirs td, table.conflict tr.is-mine td { color: var(--muted); }
table.conflict tr.is-theirs td.now, table.conflict tr.is-mine td.mine { color: var(--ink); }
table.conflict td.choice label { display: inline-flex; align-items: center; gap: 4px; margin: 0 10px 0 0; font-size: 13px; }
table.conflict td.choice input { width: auto; margin: 0; }
.inline-conflict { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.inline-conflict .field-error { display: inline; }
.inline-conflict[hidden] { display: none; }

/* ---- Broadcast compose: recipients and previews ------------------------------ */
table.recipients td, table.recipients th { padding: 5px 8px; font-size: 13px; }
table.recipients input[type="checkbox"] { width: auto; margin: 0; }
table.recipients tr.muted td { color: var(--muted); }
.bc-preview { border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; font-size: 13px; background: var(--card); }
.bc-preview .hdr { color: var(--muted); font-size: 12px; }
.bc-preview .hdr .k { display: inline-block; width: 56px; }
.bc-preview .body { white-space: pre-wrap; margin-top: 8px; }
.modal-body [role="tablist"] { margin-top: 0; }
.row.top, .hrow.top { align-items: flex-start; }

/* ---- Security ------------------------------------------------------------------ */
.btn.wide { display: block; width: 100%; }
.recovery { display: grid; grid-template-columns: repeat(5, max-content); gap: 6px 18px; }
.recovery code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 14px; }
.recovery code.used { text-decoration: line-through; color: var(--muted); }

/* ---- Videos: posters, player, processing ------------------------------------- */
table.videos td.poster-cell { width: 72px; padding-right: 0; }
.poster-btn { display: block; width: 64px; height: 36px; border: 0; padding: 0; background: var(--chip); border-radius: 4px; cursor: pointer; overflow: hidden;
              color: var(--ink); text-align: center; line-height: 36px; font-size: 14px; text-decoration: none; }
.poster-btn svg { display: block; width: 64px; height: 36px; }
.poster-btn.ext { background: var(--info-bg); color: var(--info); }
.poster-btn.ext svg { display: block; width: 64px; height: 36px; }
.poster-btn.pending { color: var(--muted); cursor: default; }
.poster-btn.failed { background: var(--bad-bg); color: var(--bad); cursor: default; }
.progress { height: 6px; width: 120px; background: var(--chip); border-radius: 3px; overflow: hidden; }
.progress > span { display: block; height: 100%; background: var(--brand); }
.progress .w-62 { width: 62%; }
.player { position: relative; aspect-ratio: 16 / 9; background: #1c2330; border-radius: 8px; overflow: hidden; }
.player > svg { width: 100%; height: 100%; display: block; }
.player .play { position: absolute; inset: 0; margin: auto; width: 64px; height: 64px; padding: 0; border: 0; background: none; cursor: pointer; }
.player .play svg { display: block; width: 64px; height: 64px; }
.player .play:hover svg circle { fill-opacity: 1; }
.player .bar { position: absolute; left: 12px; right: 12px; bottom: 14px; height: 4px; background: rgba(255,255,255,.35); border-radius: 2px; }
.player .bar > span { display: block; height: 100%; background: #fff; border-radius: 2px; }
.player .bar .w-0 { width: 0; }
.player .time { position: absolute; right: 12px; bottom: 22px; color: #fff; font-size: 12px; font-variant-numeric: tabular-nums; }
.video-page { max-width: 720px; }
.fs-18 { font-size: 18px; }

/* ---- Copy buttons ------------------------------------------------------------ */
.has-tip { position: relative; }
.copied-tip { position: absolute; left: 50%; bottom: calc(100% + 6px); transform: translateX(-50%); background: var(--tip-bg); color: var(--tip-ink);
              font-size: 12px; font-weight: 600; padding: 3px 8px; border-radius: 6px; white-space: nowrap; pointer-events: none; z-index: 70; }
.copy-fallback { display: inline-block; margin-left: 8px; font-size: 12px; user-select: all; overflow-wrap: anywhere; }
.copied-tip::after { content: ""; position: absolute; left: 50%; top: 100%; transform: translateX(-50%); border: 5px solid transparent; border-top-color: var(--ink); }

/* ---- Batch review: selection bar, checkbox column, draft list ------------------- */
.select-bar { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; }
table.cases th.sel, table.cases td.sel { width: 28px; padding-right: 0; }
table.cases td.sel input, table.cases th.sel input { width: auto; margin: 0; }
.drafts .draft { padding: 10px 0; border-bottom: 1px dashed var(--line); }
.drafts .draft:last-child { border-bottom: none; }
.drafts .draft input[type="checkbox"] { width: auto; margin: 0; }
.drafts .draft .result { display: inline-block; min-width: 56px; font-size: 12px; font-weight: 600; color: var(--ok); }
.drafts .draft.bad .result { color: var(--bad); }
.drafts .draft.bad { background: var(--warn-wash); margin: 0 -12px; padding: 10px 12px; border-radius: 6px; }
.drafts .draft .event-body { white-space: pre-wrap; }

/* ---- Response targets ------------------------------------------------------------ */
.hours { display: flex; flex-direction: column; gap: 4px; }
.hours-row { display: flex; align-items: center; gap: 8px; margin: 0; font-size: 13px; color: var(--ink); }
.hours-row input[type="checkbox"] { width: auto; margin: 0; }
.hours-row .day { width: 32px; font-weight: 600; }
.hours-row input[type="time"] { height: auto; width: auto; padding: 3px 6px; font-size: 13px; }
.num-in { width: 64px; padding: 3px 6px; font-size: 13px; }
table.targets td { vertical-align: middle; }
.mt-2 { margin-top: 2px; }

/* ---- Verified marks on intake addresses and numbers ------------------------------- */
.verify { display: inline-block; vertical-align: -3px; width: 16px; height: 16px; padding: 0; border: 0; background: none; cursor: pointer; }
.verify svg { display: block; width: 16px; height: 16px; }
.verify.ok { color: var(--ok); }
.verify.unknown { color: var(--warn); }
.verify.inline { cursor: default; vertical-align: -3px; }
.verify.none { cursor: default; }

@keyframes drawer-in { from { transform: translateX(-100%); } to { transform: translateX(0); } }
@keyframes drawer-out { from { transform: translateX(0); } to { transform: translateX(-100%); } }
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes fade-out { from { opacity: 1; } to { opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .rail-body, .rail-backdrop { animation: none !important; } }

/* Every table sits in a .table-scroll: a wide table scrolls sideways inside its card at any width;
   the page never scrolls horizontally. */
.table-scroll { overflow-x: auto; max-width: 100%; }
.table-scroll > table { width: 100%; }
/* While a wide table is taller than the window, its scrollbar is docked at the window's bottom
   edge (app.js "Table scrollbars"): a sticky sibling of the same proportions, kept in step with
   the box, whose own scrollbar is hidden meanwhile — the box's own sits under the "Load more"
   sentinel, which on a long list loads another page before it can be reached. Without
   JavaScript the box keeps its native scrollbar. */
.table-scrollbar { position: sticky; bottom: 0; z-index: 5; overflow-x: auto; overflow-y: hidden; height: 16px; background: var(--card); }
.table-scrollbar > div { height: 1px; }
.table-scroll.has-scrollbar { scrollbar-width: none; }
.table-scroll.has-scrollbar::-webkit-scrollbar { display: none; }
@media print { .table-scrollbar { display: none; } }
.detail-side table.compact th, .detail-side table.compact td { padding: 5px 4px; }

/* A block that appears in response to an action — the created-token banner — slides and fades in. */
.reveal { animation: reveal-in .3s ease-out; }
@keyframes reveal-in { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .reveal { animation: none; } }
.ellip { display: inline-block; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; }
.maxw-60ch { max-width: 60ch; }

/* ---- Token grant matrix ------------------------------------------------------ */
table.grant-matrix { width: auto; min-width: 560px; margin-top: 6px; }
table.grant-matrix th, table.grant-matrix td { padding: 6px 12px; font-size: 13px; }
table.grant-matrix th:nth-child(2), table.grant-matrix th:nth-child(3), table.grant-matrix th:nth-child(4),
table.grant-matrix td:nth-child(2), table.grant-matrix td:nth-child(3), table.grant-matrix td:nth-child(4) { text-align: center; width: 84px; }
table.grant-matrix input[type="radio"] { width: auto; margin: 0; }

/* Toast: an in-page action failed unexpectedly (the server unreachable, a server error) — app.js. */
.toast { position: fixed; left: 50%; bottom: 20px; z-index: 300; transform: translate(-50%, 24px); opacity: 0; visibility: hidden;
  transition: transform .18s ease, opacity .18s ease, visibility 0s linear .18s;
  display: flex; align-items: center; gap: 12px; max-width: min(560px, calc(100vw - 32px)); padding: 10px 10px 10px 14px;
  background: var(--tip-bg); color: var(--tip-ink); border-radius: 8px; box-shadow: 0 8px 24px rgba(var(--shadow-rgb), .28); font-size: 13.5px; line-height: 1.4; }
.toast.show { transform: translate(-50%, 0); opacity: 1; visibility: visible; transition: transform .18s ease, opacity .18s ease; }
.toast-close { background: none; border: 0; color: inherit; font-size: 18px; line-height: 1; cursor: pointer; opacity: .75; padding: 0 4px; }
.toast-close:hover { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .toast, .toast.show { transition: none; } }
