/*
 * Shared components — cards, dialogs, row lists, badges/status pills,
 * alerts, and small utilities (including classes that templates already
 * reference but no stylesheet previously defined).
 * Tokens come from tokens.css; no :root declarations here.
 */

/* ---- Card (rendered by {% card %}) ---- */
/* ---- Surfaces -----------------------------------------------------------
   `.card` is the page-level surface; `.subcard` is a block INSIDE one. Each
   sets its own locals and reads only its own -- never the parent's. That is
   deliberate: `.card[data-tone]` sets inheritable custom properties and its
   chip rule is a DESCENDANT selector, so a nested surface that read what its
   parent set would silently take the parent's colour the moment nesting
   changed or a wrapper appeared.

   Owner ruling 9: plain sections KEEP their card look, so `.card` is unchanged
   here -- the tokens are its existing values by another name. Only the nested
   level gains a distinct treatment, so nesting reads from the inside out.

   There is no level 3. `.card` inside `.card` occurs nowhere in the tree, and
   outside the filter workspace nesting bottoms out at two. A `.subcard` inside
   a `.subcard` renders the same and is only inset, which is what tables.css
   already did for its one nesting case. */
.card{margin:var(--space-3) 0;padding:var(--space-3);border:1px solid var(--surface-edge);border-radius:var(--r-3);background:var(--surface);box-shadow:0 1px 0 rgba(var(--shadow-color)/.04)}
.subcard{padding:var(--space-2);border:1px solid var(--surface-nested-edge);border-radius:var(--r-2);background:var(--surface-nested)}
/* Dashed changes the border STYLE only. Reading the outer level's edge
   colour here would couple the two settings that are meant to be set
   independently -- caught by
   test_a_nested_surface_does_not_read_its_parents_level. */
.subcard--dashed{border-style:dashed}
.subcard--plain{background:none}
.subcard--scroll{max-height:260px;overflow:auto}
/* Retired onto .subcard: each of these was a near-copy of it differing only
   in radius, padding or which of the two greys it used for its edge. Kept as
   names their call sites still write; the treatment lives in one place now. */
.deal-panel,.scroll-box,.backup-fieldset,.gdpr-fieldset,.overview-item,
.note-item,.backup-clarification{padding:var(--space-2);border:1px solid var(--surface-nested-edge);border-radius:var(--r-2);background:var(--surface-nested)}
:where(.card,.subcard) > .subcard{margin-inline:0}
.card-head{display:flex;align-items:baseline;gap:var(--space-2);flex-wrap:wrap;margin-bottom:var(--space-2)}
.card-head h2{margin:0}
.card-head .subdued{margin:0;flex-basis:100%;font-size:var(--fs-1)}
.card-actions{margin-left:auto;display:flex;gap:var(--space-2);align-items:center}
.card-body{min-width:0}
/* Subheadings inside a card (e.g. Documents / Pipelines under one Candidate card) */
.card-subhead{margin:var(--space-4) 0 var(--space-2);font-size:var(--fs-2);text-transform:uppercase;letter-spacing:.04em;color:var(--muted)}
.card-subhead-row{display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);margin-top:var(--space-4)}
.card-subhead-row .card-subhead{margin:0}
/* Convert-action buttons row in the page header */
.header-convert{margin-top:6px}

/* Deep-linked card — the job page's apply-requirements panel links straight
   at /account/candidate/#languages and /personal/#personal-details. Without
   the ring the jump reads as "nothing happened" on a page of near-identical
   cards; scroll-margin keeps it clear of the sticky header. */
.card:target{outline:2px solid var(--focus-ring);outline-offset:3px;scroll-margin-top:var(--space-4)}

/* Group identity = hue: tone accent on the left border + header chip. */
.card[data-tone]{border-left:3px solid var(--tone-ink,var(--border))}
.card[data-tone] .card-head .card-chip{display:inline-block;padding:2px 8px;border-radius:var(--r-1);background:var(--tone,var(--panel));color:var(--tone-ink,var(--ink))}
.card[data-tone=identity]{--tone:var(--tone-identity);--tone-ink:var(--tone-identity-ink)}
.card[data-tone=contact]{--tone:var(--tone-contact);--tone-ink:var(--tone-contact-ink)}
.card[data-tone=languages]{--tone:var(--tone-languages);--tone-ink:var(--tone-languages-ink)}
.card[data-tone=system]{--tone:var(--tone-system);--tone-ink:var(--tone-system-ink)}
.card[data-tone=finance]{--tone:var(--tone-finance);--tone-ink:var(--tone-finance-ink)}
.card[data-tone=compliance]{--tone:var(--tone-compliance);--tone-ink:var(--tone-compliance-ink)}
.card[data-tone=danger]{--tone:var(--tone-danger);--tone-ink:var(--tone-danger-ink)}

/* ---- Modal (native <dialog>, wired by js/ui/modal.js) ----
   Scoped to dialog.modal so it cannot collide with the legacy div.modal
   overlays that remain on unconverted pages. */
/* max-height+overflow: on very short viewports the footer buttons must stay
   reachable by scrolling the dialog, never hang outside it (GUI pass D10). */
/* `background` is NOT optional here, and its absence was invisible on a light
   palette. A native <dialog> falls back to the UA's Canvas -- white -- so this
   rule set themed ink on a fixed white fill. Themed light-on-white: measured
   1.12:1 on the bulk-delete confirmation, i.e. an agency with a dark theme
   could not read WHICH RECORDS they were about to permanently destroy, nor
   the line saying it could not be undone. The sibling `.modal-overlay
   .modal-dialog` had `background: var(--surface)` all along. */
dialog.modal{width:min(92vw,560px);max-height:calc(100vh - 2rem);overflow:auto;padding:0;border:1px solid var(--border);border-radius:var(--r-3);box-shadow:0 10px 30px rgba(var(--shadow-color)/.25);background:var(--surface);color:var(--ink)}
dialog.modal::backdrop{background:rgba(var(--scrim-color)/.35)}
dialog.modal[data-wide]{width:min(92vw,720px)}
dialog.modal .modal-head{display:flex;align-items:center;gap:var(--space-2);padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--border)}
dialog.modal .modal-head h2{margin:0;flex:1 1 auto}
/* S-3.1b: the body scrolls. The departure wizard renders one row per
   project a leaver is the only member of — uncapped, and there is no
   reason that stops at a screenful. */
dialog.modal .modal-body{padding:var(--space-4);max-height:70vh;overflow:auto}
dialog.modal .modal-foot{display:flex;justify-content:flex-end;gap:var(--space-2);padding:var(--space-3) var(--space-4);border-top:1px solid var(--border)}
/* The house mark for a dangerous modal. Widened to the .modal-overlay shape
   (S-D.4/ADH-015): pipeline_actions.js painted its ban dialog inline because
   this rule only knew the <dialog> form and could not reach it. */
/* The header of the dialog that says something cannot be undone. Its title ran
   at 1.67:1 and its close button at 1.02:1 under a dark theme, because the tint is
   fixed and the ink was not. */
dialog.modal[data-danger] .modal-head,
.modal-overlay[data-danger] .modal-header{background:var(--danger-soft);color:var(--danger-ink)}
dialog.modal .confirm-code,.modal-overlay .confirm-code{display:inline-block;padding:6px 10px;border:1px solid var(--border);border-radius:var(--r-1);background:var(--panel);font-weight:700;font-size:var(--fs-5);letter-spacing:3px;user-select:all}
dialog.modal .confirm-phrase{display:inline-block;padding:4px 8px;border:1px solid var(--border);border-radius:var(--r-1);background:var(--panel);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;user-select:all}
dialog.modal .confirm-input-code,.modal-overlay .modal-item-list{max-height:9rem;overflow-y:auto;margin:0 0 var(--space-2);padding-left:var(--space-3)}
.confirm-input-code{width:120px}

/* ---- Legacy overlay modals (restyled in place; each page's JS toggles
   style.display between 'none' and 'block'). New class names on purpose:
   bare .modal is claimed by the job/order editors' flex+.hidden contract,
   and dialog.modal is the native-dialog system above. ---- */
/* overflow:auto — a fixed, inset-0 overlay CLIPS anything taller than the
   viewport, so a dialog whose content exceeds it puts its own confirm
   control out of reach. Caught in the S-3.1b browser round: the close
   wizard's footer sat at 907px in a 720px viewport. Harmless for
   overlays that fit (no scrollbar appears), and every one of them
   previously relied on the hand-rolled inline overflow this class
   replaced. */
.modal-overlay{display:none;position:fixed;inset:0;background:rgba(var(--scrim-color)/.35);z-index:1000;overflow:auto}
.modal-overlay.z-above{z-index:1001}
.modal-overlay.z-top{z-index:1002}
/* job_summary.html stacks FIVE overlays and its intended order is not DOM order,
   so the ladder needs two more rungs (S-D.1). Kept as classes rather than inline
   z-indexes so the whole stack is readable in one place. */
.modal-overlay.z-top-2{z-index:1003}
.modal-overlay.z-top-3{z-index:1004}
.modal-overlay .modal-dialog{background:var(--surface);max-width:720px;width:92%;margin:10vh auto;border-radius:var(--r-3);box-shadow:0 10px 30px rgba(var(--shadow-color)/.25)}
.modal-overlay--narrow .modal-dialog{max-width:640px}
.modal-overlay--wide .modal-dialog{max-width:960px}
.modal-overlay .modal-header{display:flex;align-items:center;justify-content:space-between;padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--border)}
.modal-overlay .modal-header h3{margin:0;font-size:18px}
.modal-overlay .modal-body{padding:var(--space-3) var(--space-4);display:grid;gap:var(--space-3)}
.modal-overlay .modal-footer{display:flex;justify-content:flex-end;gap:var(--space-2);padding:var(--space-3) var(--space-4);border-top:1px solid var(--border)}
.modal-overlay .modal-list-box{max-height:300px;overflow:auto;border:1px solid var(--border);border-radius:var(--r-1);padding:8px}
.modal-overlay .modal-scroll{overflow:auto;max-height:42vh}
.modal-overlay .confirm-code.danger{background:var(--danger-soft);border-color:var(--danger-border);color:var(--danger-ink)}
/* The field you type that code into. Only the bits app.css:17 does not already
   give an input: it stays narrow rather than filling the dialog. */
.modal-overlay .confirm-code-input{display:block;max-width:160px;margin-top:var(--space-1)}
/* app.css:17 gives every textarea its width/padding/border but no height, and
   this one has no `rows`, so its floor has to be stated. */
.notes-textarea{min-height:60px}
/* help.html's section stack, out of its inline style. Named for its context,
   not as a `.stack` utility — the house has no spacing-utility family and
   minting one for a single page is the defect this sprint removes. It is also
   deliberately NOT `.filters`, whose rule happens to be byte-identical but
   whose meaning is a filter bar. */
.help-sections{display:grid;gap:10px}

/* Scheduled-emails list rows, out of pipeline_actions.js's innerHTML. The status
   chip is `.status[data-status]`, the ramp every other status already uses. */
.scheduled-row{display:flex;align-items:center;gap:var(--space-2);font-size:var(--fs-1);border:1px solid var(--surface-nested-edge);border-radius:var(--r-1);padding:4px 8px}
.scheduled-subject{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.scheduled-when{color:var(--ink-faint);white-space:nowrap}
/* The CV-zip modal's label/value rows, out of job_recruiting.js's innerHTML. */
.detail-row{display:grid;grid-template-columns:140px 1fr;gap:10px;padding:4px 0;border-bottom:1px solid var(--rule-faint)}

/* Stat-tile rows (CV processing dashboard + batch detail). Same `--cols` idiom
   as .btn-grid, including SR-12's `:where()` so the count never outranks the
   step-down. `--min` is the tile's floor, which differs per surface. */
.stat-grid{display:grid;gap:var(--space-3);grid-template-columns:repeat(var(--cols,4),minmax(var(--stat-min,140px),1fr))}
.stat-grid:where(.cols-7){--cols:7;--stat-min:110px}

/* ADH-042 — the two prospective-deal panels, swept out of job_summary.html's
   inline styles. `tokens.css:104` had already spotted these two `#fcfdff`
   literals and recorded that they were unreachable from CSS. `#fcfdff` is
   --tile-bg and `8px` is --r-2, both exact; `#e5e7eb` has no token, so it
   stays a literal but a COUNTABLE one. A solid edge, unlike .overview-item's
   dashed tile, which is why this is its own class and not that one. */
.deal-panel{padding:var(--space-4)}
.deal-panel--display{padding:var(--space-3);margin-bottom:var(--space-3)}

/* S-D.1 commit 3 — job_summary.html's remaining one-offs, at their existing
   values. Named rather than utility-generated: the house has no spacing-utility
   idiom and inventing one would be the defect this sprint removes. */
.scroll-box{max-height:260px;overflow:auto}
.empty-note-cell{color:var(--ink-faint);font-style:italic;padding:var(--space-3)}
.warn-line{margin:0;color:var(--warn-ink)}  /* #92400e exactly, not --st-interview-ink's #9a3412 */
.grid-2-even{display:grid;gap:var(--space-3);grid-template-columns:repeat(2,1fr)}
.inline-row{display:flex;align-items:center;gap:var(--space-2)}
.inline-row--spaced{margin-top:var(--space-2)}
.inline-row--spaced-lg{margin-top:var(--space-3)}
.push-right{margin-left:auto}
.badge-inline-note{margin-left:6px;font-size:var(--fs-1);padding:2px 6px}
.ruled-top{border-top:1px solid var(--rule-soft);padding-top:var(--space-2)}
.strong-cell{font-weight:600}
.col-shrink{width:1%;white-space:nowrap}
.col-34{width:34px}
.col-120{width:120px}
.col-260{width:260px}
.gap-top{margin-top:var(--space-3)}
.gap-top-sm{margin-top:var(--space-2)}
.gap-top-lg{margin-top:var(--space-4)}
.gap-bottom{margin-bottom:10px}
.gap-bottom-lg{margin-bottom:var(--space-4)}
.w-full{width:100%}
.prewrap{white-space:pre-wrap}
.readable-width{max-width:760px}
.indent-note{margin:var(--space-2) 0 0 18px}
.flush-x{margin-left:0;margin-right:0}
.two-col-even{display:grid;gap:var(--space-4);grid-template-columns:repeat(2,minmax(280px,1fr))}
.gap-bottom-sm{margin-bottom:var(--space-1)}
.indent-note-sm{margin:2px 0 0 var(--space-4)}
.inline-row-lg{display:flex;align-items:center;gap:var(--space-3)}
/* the erasure page's per-item blocks */
.erasure-block{margin-bottom:10px;padding:10px}
.d-inline{display:inline}
.gap-left-sm{margin-left:var(--space-2)}
.gap-y{margin:10px 0}
.cell-subtext{margin-top:6px;white-space:pre-line;color:var(--ink-soft)}
.gap-y-lg{margin:var(--space-3) 0}
.gap-top-xs{margin-top:var(--space-1)}
/* email-policy rows: a four-column editor line plus its validation message */
.policy-row{display:grid;gap:var(--space-2);align-items:start;grid-template-columns:minmax(130px,180px) minmax(220px,1fr) minmax(140px,180px) auto}
.policy-error{margin-top:6px;padding:var(--space-2);border:1px solid var(--danger-strong);background:var(--danger-wash);color:var(--danger-ink)}
.gap-left{margin-left:var(--space-3)}
.flush{margin:0}
/* Structural helpers used inside overlay modal bodies. */
.modal-overlay .modal-section{border-top:1px solid var(--border);padding-top:8px;display:grid;gap:8px}
.modal-overlay .modal-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.modal-overlay .modal-row-end{margin-left:auto;display:flex;gap:8px}
.modal-overlay .modal-subhead{font-size:var(--fs-2)}
.modal-overlay .modal-stack{display:grid;gap:6px;margin-top:6px;max-height:160px;overflow:auto}
.modal-overlay .hint-block{display:block;margin-top:4px}
.modal-overlay th.col-actions{width:1%;white-space:nowrap}
.modal-overlay .ban-note{margin:0 0 6px;color:var(--st-rejected-ink);font-weight:600}

/* Page-header action rows (create + bulk buttons above list pages). */
.page-actions{display:flex;gap:var(--space-2);align-items:center;margin:0 0 var(--space-4)}
/* Narrow leading select-checkbox column on list tables. */
th.col-select{width:34px}
/* Inline office checkbox matrix used by the older staffing list filters. */
.office-filter{display:inline-block;vertical-align:top}
.office-filter .office-filter-head{font-weight:600;margin-bottom:4px}
.office-table{border-collapse:separate;border-spacing:6px 0}
.office-table td{text-align:center}

/* ---- Row list (extensible add/remove/reorder rows, js/ui/rowlist.js) ---- */
.rowlist-items{display:flex;flex-direction:column;gap:var(--space-1)}
.rowlist-row{display:grid;grid-template-columns:var(--row-cols,auto 1fr auto);gap:var(--space-2);align-items:center;padding:var(--space-1) var(--space-2);border:1px solid var(--border);border-radius:var(--r-2);background:var(--surface)}
.rowlist-row .drag-handle,
.editor-sort-row .drag-handle{cursor:move;color:var(--muted);user-select:none}
/* the editor rows' handle is larger; the rowlist rows keep their size */
.editor-sort-row .drag-handle{font-size:18px;line-height:1}
.rowlist-row.is-removing{opacity:.55;background:var(--danger-soft)}
.rowlist-add-row{margin-top:var(--space-2)}
.rowlist-row .person-links{font-size:var(--fs-1);color:var(--muted)}
.rowlist-row label{display:block;min-width:0}
.rowlist-row select,.rowlist-row input{width:100%}
/* The main-contact radio must not stretch to its grid track (ownership). */
.rowlist-row input[type=radio]{width:auto}
/* Row layouts (set per row partial; replaces the inline grid styles) */
.rowlist-row.row-select{--row-cols:1fr auto}
.rowlist-row.row-drag-select{--row-cols:auto 1fr auto}
/* Team rows carrying the main-contact radio cell (ownership sprint).
   Five tracks: handle | person | move arrows | remove | contact radio
   (the arrow cluster is the keyboard/no-drag reorder, follow-up item 6). */
.rowlist-row.row-drag-select-contact{--row-cols:auto 1fr auto auto auto}
.row-move-cluster{display:inline-flex;gap:2px;white-space:nowrap}
.rowlist-row.row-contact{--row-cols:1fr 1fr 1fr auto}
.rowlist-row.row-drag-contact{--row-cols:auto 1fr 1fr 1fr auto}
@media(max-width:720px){
  .rowlist-row.row-contact{--row-cols:1fr auto}
  .rowlist-row.row-drag-contact{--row-cols:auto 1fr auto}
}

/* ---- Attachment panel (ui/attachment_panel.html) ---- */
.attachment-upload{display:grid;grid-template-columns:1fr 1fr 1fr auto;gap:var(--space-2);align-items:end;margin-top:var(--space-2);max-width:760px}
@media(max-width:720px){.attachment-upload{grid-template-columns:1fr}}
/* Per-row editable document-kind control */
.kind-edit-form{display:flex;gap:var(--space-1);align-items:center}
.kind-edit-form select{width:auto}

/* ---- Status pills (workflow state = the status ramp, never group hue) ---- */
.status,
select.status-select{border-radius:var(--r-1);padding:4px 8px;border:1px solid rgba(var(--scrim-color)/.08);font-weight:600}
.status[data-status],select.status-select[data-status]{background:var(--st-bg,var(--panel));color:var(--st-ink,var(--muted))}
/* RULING REVERSED (owner, 2026-08-17). The `[data-status=X]` rules below are
   BARE attribute selectors, so they already set --st-bg/--st-ink on any element
   carrying the attribute -- including a `.badge`. Only this consumer rule was
   missing, and its absence is why every badge-shaped status in the product
   hand-picked its own colours, and why several simply had none: the
   employee-import table painted every status green, the scheduled-e-mail list
   rendered Failed, Sent, Superseded and Cancelled byte-identically grey, and
   the research run list emitted an empty class. The fixed status language was
   not diverging there, it was being ERASED.

   The superseded ruling (`staffing/tests.py:2580`, `utils/tests_status_cell.py`)
   read "a badge carrying a data-status attribute renders grey with nothing
   failing" -- true as a description, wrong as an intention.

   `border-color:transparent` because family 1 carries a 1px border and the
   tinted fill supplies its own edge; leaving the neutral border would ring
   every tinted badge in grey. */
/* The outline STAYS. An earlier version of this rule set
   `border-color:transparent`, reasoning that a tinted fill supplies its own
   edge -- but that overrode the reference specimen the owner named, whose 1px
   outline is part of what was asked for. Not a call to make silently.
   This rule is now the exception rather than the norm: by the owner's ruling a
   status that is a SINGLE FIELD on a record wears family 2 (`.status`), so a
   badge carries a status only where the value is one of many in a list. */
.badge[data-status]{background:var(--st-bg,var(--panel));color:var(--st-ink,var(--muted));border-color:var(--st-ink,var(--border))}
/* Custom properties INHERIT, and `people/.../bmd_import_run.html:60` puts
   `data-status` on the `<tr>` -- so without this reset a plain `.badge` inside
   a PENDING or SKIPPED row would silently take the ROW's tint instead of its
   own fallback, and the fallback could never fire. Zero specificity via
   `:where()` so a badge with its own mapped status still wins at (0,2,0).
   Exactly the device the `<option>` reset below uses, for the same reason. */
:where(.badge){--st-bg:var(--panel);--st-ink:var(--muted)}
[data-status=APPLIED]{--st-bg:var(--st-applied);--st-ink:var(--st-applied-ink)}
[data-status=PROSPECT]{--st-bg:var(--st-prospect);--st-ink:var(--st-prospect-ink)}
[data-status=QUALIFIED]{--st-bg:var(--st-qualified);--st-ink:var(--st-qualified-ink)}
[data-status=CV_SENT]{--st-bg:var(--st-cv-sent);--st-ink:var(--st-cv-sent-ink)}
[data-status=SHORTLISTED]{--st-bg:var(--st-shortlisted);--st-ink:var(--st-shortlisted-ink)}
[data-status=INTERVIEW]{--st-bg:var(--st-interview);--st-ink:var(--st-interview-ink)}
[data-status=OFFERED]{--st-bg:var(--st-offered);--st-ink:var(--st-offered-ink)}
[data-status=ACCEPTED]{--st-bg:var(--st-accepted);--st-ink:var(--st-accepted-ink)}
[data-status=HIRED]{--st-bg:var(--st-hired);--st-ink:var(--st-hired-ink)}
[data-status=REJECTED]{--st-bg:var(--st-rejected);--st-ink:var(--st-rejected-ink)}
[data-status=UNCLARIFIED]{--st-bg:var(--st-unclarified);--st-ink:var(--st-unclarified-ink)}
/* Batch 3: UNCLARIFIED's display label is "Not specified", and a select may
   carry the label-derived key — it aliases UNCLARIFIED's tokens, the same way
   PLACED aliases HIRED's below. */
[data-status=NOT_SPECIFIED]{--st-bg:var(--st-unclarified);--st-ink:var(--st-unclarified-ink)}
/* Placement, billing-document and payment-direction states (detail-page
   header chips) — mapped onto the existing ramp tokens. */
[data-status=ACTIVE]{--st-bg:var(--st-accepted);--st-ink:var(--st-accepted-ink)}
[data-status=ENDED]{--st-bg:var(--st-unclarified);--st-ink:var(--st-unclarified-ink)}
[data-status=ON_HOLD]{--st-bg:var(--st-offered);--st-ink:var(--st-offered-ink)}
/* S-D.4: PLACED was the one status `pipeline_actions.js` could colour and the
   ramp could not — it returned HIRED's hex, so it aliases HIRED's tokens, the
   same way ON_HOLD aliases OFFERED's above. */
[data-status=PLACED]{--st-bg:var(--st-hired);--st-ink:var(--st-hired-ink)}
/* The open dropdown must agree with the closed control: both now read the same
   ramp instead of the select taking CSS and its options taking a JS hex table. */
/* Custom properties INHERIT, so an option whose status has no `[data-status=X]`
   rule would take the *select's* tint rather than the `var(…, fallback)` — the
   fallback can never fire. Reset at zero specificity (`:where()`, the same
   device SR-12 uses) so a mapped status still wins at (0,1,0). */
:where(select.status-select option){--st-bg:var(--panel);--st-ink:var(--muted)}
select.status-select option[data-status]{background:var(--st-bg);color:var(--st-ink)}
[data-status=DRAFT]{--st-bg:var(--st-prospect);--st-ink:var(--st-prospect-ink)}
[data-status=SENT]{--st-bg:var(--st-applied);--st-ink:var(--st-applied-ink)}
[data-status=PARTIAL]{--st-bg:var(--st-offered);--st-ink:var(--st-offered-ink)}
[data-status=PAID]{--st-bg:var(--st-hired);--st-ink:var(--st-hired-ink)}
[data-status=OVERDUE]{--st-bg:var(--st-rejected);--st-ink:var(--st-rejected-ink)}
[data-status=CANCELLED]{--st-bg:var(--st-unclarified);--st-ink:var(--st-unclarified-ink)}
/* P3: CREDITED shipped with the terminal state in R2-8 and never got a rule,
   so a credited invoice has been rendering untinted since. Reads like
   CANCELLED because both mean "this document no longer asks for money" —
   the difference is how it got there, which the label says. */
[data-status=CREDITED]{--st-bg:var(--st-unclarified);--st-ink:var(--st-unclarified-ink)}
[data-status=IN]{--st-bg:var(--st-accepted);--st-ink:var(--st-accepted-ink)}
[data-status=OUT]{--st-bg:var(--st-offered);--st-ink:var(--st-offered-ink)}
/* GDPR contact-request states (gdpr_requests_detail/list) — mapped onto the
   existing tone tokens: OPEN amber, IN_PROGRESS blue, RESOLVED green,
   CLOSED grey. */
[data-status=OPEN]{--st-bg:var(--warn-bg);--st-ink:var(--warn-ink)}
[data-status=IN_PROGRESS]{--st-bg:var(--info-bg);--st-ink:var(--info-ink)}
/* Vacation requests (absences) — REJECTED/CANCELLED reuse the entries above. */
[data-status=PENDING]{--st-bg:var(--st-offered);--st-ink:var(--st-offered-ink)}
[data-status=APPROVED]{--st-bg:var(--st-accepted);--st-ink:var(--st-accepted-ink)}
/* Vacation cover-chain configuration states. */
[data-status=CHAIN_UNSET]{--st-bg:var(--st-rejected);--st-ink:var(--st-rejected-ink)}
[data-status=CHAIN_EMPTY]{--st-bg:var(--st-offered);--st-ink:var(--st-offered-ink)}
[data-status=CHAIN_CONFIGURED]{--st-bg:var(--st-accepted);--st-ink:var(--st-accepted-ink)}
/* Document-processing runs & batches (CV pipeline dashboards). */
[data-status=QUEUED]{--st-bg:var(--st-prospect);--st-ink:var(--st-prospect-ink)}
[data-status=RUNNING]{--st-bg:var(--st-applied);--st-ink:var(--st-applied-ink)}
[data-status=DONE]{--st-bg:var(--st-accepted);--st-ink:var(--st-accepted-ink)}
[data-status=DONE_WITH_ERRORS]{--st-bg:var(--st-offered);--st-ink:var(--st-offered-ink)}
[data-status=FAILED]{--st-bg:var(--st-rejected);--st-ink:var(--st-rejected-ink)}
/* GDPR runs dashboard badge row (gdpr_page_overhaul_2026_08 §5) — the
   policy in one glance on the vacation-approvals pill component. */
[data-status=GDPR_ANONYMISE]{--st-bg:var(--st-rejected);--st-ink:var(--st-rejected-ink)}
[data-status=GDPR_WARN]{--st-bg:var(--st-offered);--st-ink:var(--st-offered-ink)}
[data-status=GDPR_KEEP]{--st-bg:var(--st-accepted);--st-ink:var(--st-accepted-ink)}
[data-status=GDPR_PROTECTED]{--st-bg:var(--st-unclarified);--st-ink:var(--st-unclarified-ink)}
/* Correction-review sprint: "needs review" borrowed GDPR_WARN, which made a
   backlog of people the clock could not date look like a warning band —
   two different problems wearing one colour. Purple is unused by the GDPR
   family and reads as "look at this", not "this is due". */
[data-status=GDPR_REVIEW]{--st-bg:var(--st-qualified);--st-ink:var(--st-qualified-ink)}
[data-status=GDPR_AUTO_ON]{--st-bg:var(--st-accepted);--st-ink:var(--st-accepted-ink)}
[data-status=GDPR_AUTO_OFF]{--st-bg:var(--st-unclarified);--st-ink:var(--st-unclarified-ink)}
/* S-D.4: the scheduled-emails list kept its own hex map in pipeline_actions.js
   (the SECOND such table — see D-39 for the first). Four of its statuses were
   already here; these three simply had no entry, which is why the JS had one.
   Aliased to the tone each hex was reaching for: SUPERSEDED greyed like
   CANCELLED, SENDING amber like PENDING, HELD purple like QUALIFIED. */
[data-status=SUPERSEDED]{--st-bg:var(--st-unclarified);--st-ink:var(--st-unclarified-ink)}
[data-status=SENDING]{--st-bg:var(--st-offered);--st-ink:var(--st-offered-ink)}
[data-status=HELD]{--st-bg:var(--st-qualified);--st-ink:var(--st-qualified-ink)}
[data-status=SKIPPED]{--st-bg:var(--st-unclarified);--st-ink:var(--st-unclarified-ink)}
/* CV backlog states (lowercase slugs — attribute selectors are
   case-sensitive, so these cannot collide with the enum keys above). */
[data-status=unprocessed]{--st-bg:var(--st-prospect);--st-ink:var(--st-prospect-ink)}
[data-status=stale_schema]{--st-bg:var(--st-offered);--st-ink:var(--st-offered-ink)}
[data-status=active]{--st-bg:var(--st-applied);--st-ink:var(--st-applied-ink)}
[data-status=processed_current]{--st-bg:var(--st-accepted);--st-ink:var(--st-accepted-ink)}
[data-status=failed]{--st-bg:var(--st-rejected);--st-ink:var(--st-rejected-ink)}
[data-status=skipped]{--st-bg:var(--st-unclarified);--st-ink:var(--st-unclarified-ink)}
[data-status=missing_file]{--st-bg:var(--st-rejected);--st-ink:var(--st-rejected-ink)}
[data-status=needs_processing]{--st-bg:var(--st-offered);--st-ink:var(--st-offered-ink)}
[data-status=RESOLVED]{--st-bg:var(--ok-bg);--st-ink:var(--ok-ink)}
[data-status=CLOSED]{--st-bg:var(--st-unclarified);--st-ink:var(--st-unclarified-ink)}

/* ---- Status vocabulary completed (owner-ruled 2026-08-17) ----------------
   Every value below was emitted by a real enum and had NO rule, so it rendered
   the `var(--st-bg, var(--panel))` fallback: neutral grey, identical to every
   other unmapped state. Colour was not diverging here, it was absent.

   Each maps onto an EXISTING ramp pair rather than minting a new hue, so the
   product gains no new colours and the fixed good/medium/bad/neutral language
   stays exactly as wide as it was.

   Deliberately NOT coloured (owner ruling): PublicationStatus
   (TODAY_HOMEPAGE / EXTERNAL_SITES / DO_NOT_POST). Where an advert is posted is
   a CHOICE, not a state, and a good/bad tint would imply a verdict that does
   not exist. */

/* Client projects — a progression, so it reuses the pipeline progression
   rather than being forced into good/bad. */
[data-status=LEAD]{--st-bg:var(--st-prospect);--st-ink:var(--st-prospect-ink)}
[data-status=ACTIVE_CONTACTED]{--st-bg:var(--st-applied);--st-ink:var(--st-applied-ink)}
[data-status=ACTIVE_POSITIVE]{--st-bg:var(--st-qualified);--st-ink:var(--st-qualified-ink)}
[data-status=ACTIVE_SECURE]{--st-bg:var(--st-shortlisted);--st-ink:var(--st-shortlisted-ink)}
[data-status=WON]{--st-bg:var(--st-hired);--st-ink:var(--st-hired-ink)}

[data-status=LOST]{--st-bg:var(--st-rejected);--st-ink:var(--st-rejected-ink)}
[data-status=ARCHIVED]{--st-bg:var(--st-unclarified);--st-ink:var(--st-unclarified-ink)}

/* Did the placement actually begin. */
[data-status=PLANNED]{--st-bg:var(--st-applied);--st-ink:var(--st-applied-ink)}
[data-status=STARTED]{--st-bg:var(--st-accepted);--st-ink:var(--st-accepted-ink)}
[data-status=WITHDRAWN]{--st-bg:var(--st-rejected);--st-ink:var(--st-rejected-ink)}

/* E-mail send outcomes. TRANSPORT_DISABLED is amber, not red: the message did
   not go, but that is a configuration state rather than a failure of this
   message. LOG_ONLY is neutral — a deliberate no-send is not a problem. */
[data-status=BLOCKED]{--st-bg:var(--st-rejected);--st-ink:var(--st-rejected-ink)}
[data-status=LOG_ONLY]{--st-bg:var(--st-unclarified);--st-ink:var(--st-unclarified-ink)}
[data-status=TRANSPORT_DISABLED]{--st-bg:var(--st-offered);--st-ink:var(--st-offered-ink)}

/* How an imported payroll row was matched to a person. Green = a strong or
   human-confirmed match; amber = a weaker signal, or none, that wants a look. */
[data-status=AUTO_EMAIL]{--st-bg:var(--st-accepted);--st-ink:var(--st-accepted-ink)}
[data-status=AUTO_EMAIL_INTERNAL]{--st-bg:var(--st-accepted);--st-ink:var(--st-accepted-ink)}
[data-status=LINK_CONFIRMED]{--st-bg:var(--st-accepted);--st-ink:var(--st-accepted-ink)}
[data-status=AUTO_PHONE]{--st-bg:var(--st-offered);--st-ink:var(--st-offered-ink)}
[data-status=AUTO_PRIOR]{--st-bg:var(--st-offered);--st-ink:var(--st-offered-ink)}
[data-status=NO_MATCH]{--st-bg:var(--st-offered);--st-ink:var(--st-offered-ink)}

/* Import run lifecycle. ABANDONED is neutral, not amber (owner ruling):
   stopping an import is not an error. */
[data-status=UPLOADED]{--st-bg:var(--st-prospect);--st-ink:var(--st-prospect-ink)}
[data-status=REVIEWING]{--st-bg:var(--st-applied);--st-ink:var(--st-applied-ink)}
[data-status=FINALISED]{--st-bg:var(--st-accepted);--st-ink:var(--st-accepted-ink)}
[data-status=ABANDONED]{--st-bg:var(--st-unclarified);--st-ink:var(--st-unclarified-ink)}

/* Nobody is covering this holiday. Rendered neutral grey here while the SAME
   fact rendered as a red badge on the approvals screen — one of the sprint's
   clearest "same thing, two answers" cases. */
[data-status=UNRESOLVED]{--st-bg:var(--st-rejected);--st-ink:var(--st-rejected-ink)}

/* Search-run states. LOWERCASE on purpose — `research/models.py` stores them
   that way and attribute selectors are case-sensitive; `canceled` is also ONE
   L, deliberately, and must not be "corrected" to match CANCELLED above.
   These replace the private colour table that lived in research.css. */
[data-status=running]{--st-bg:var(--st-applied);--st-ink:var(--st-applied-ink)}
[data-status=completed]{--st-bg:var(--st-accepted);--st-ink:var(--st-accepted-ink)}
[data-status=canceled]{--st-bg:var(--st-unclarified);--st-ink:var(--st-unclarified-ink)}

/* ---- Badge tones — ONE table, one consumer ------------------------------
   Tone was spelled four ways: `.badge.tone-X` (7 names), the colour-named
   legacy set `.badge-blue/-green/-purple/-gray/-warning` (5), `.badge.ct-X`
   (3, kept separate below by ruling) and `.badge.success/.warn` in app.css (2).
   Seventeen declarations for eight actual tones, and they had already drifted:
   the `tone-*` spelling set its border to the tone's ink while the legacy
   spelling left the neutral grey border in place, so the SAME tone rendered
   two different edges depending on which name a template happened to use.

   Now a single table sets a local triple on a bare `[data-tone=X]` attribute —
   the idiom `[data-status=X]` already uses — and each family consumes it once.
   Adding a family is one rule, not eight.

   The consumer is scoped `.badge[data-tone]` AND the triple is reset at zero
   specificity below. Scoping alone is not enough: `[data-tone=X]` is a bare
   attribute selector, so it also sets the triple on `.card[data-tone=X]`, and
   custom properties inherit -- a badge carrying an UNMAPPED tone would take
   the enclosing card's colour. Both halves are needed, exactly as the status
   ramp has both. (`.card` reads --tone / --tone-ink, deliberately different
   names, so those two never collide.) */
[data-tone=identity]{--tone-bg:var(--tone-identity);--tone-fg:var(--tone-identity-ink);--tone-edge:var(--tone-identity-ink)}
[data-tone=contact]{--tone-bg:var(--tone-contact);--tone-fg:var(--tone-contact-ink);--tone-edge:var(--tone-contact-ink)}
[data-tone=languages]{--tone-bg:var(--tone-languages);--tone-fg:var(--tone-languages-ink);--tone-edge:var(--tone-languages-ink)}
[data-tone=system]{--tone-bg:var(--tone-system);--tone-fg:var(--tone-system-ink);--tone-edge:var(--tone-system-ink)}
[data-tone=finance]{--tone-bg:var(--tone-finance);--tone-fg:var(--tone-finance-ink);--tone-edge:var(--tone-finance-ink)}
[data-tone=compliance]{--tone-bg:var(--tone-compliance);--tone-fg:var(--tone-compliance-ink);--tone-edge:var(--tone-compliance-ink)}
[data-tone=danger]{--tone-bg:var(--tone-danger);--tone-fg:var(--tone-danger-ink);--tone-edge:var(--tone-danger-ink)}
/* Fixed semantics, not a group: warn is the amber WARNING pair, never
   client-themeable. Split from `compliance` in S1. */
[data-tone=warn]{--tone-bg:var(--warn-bg);--tone-fg:var(--warn-ink);--tone-edge:var(--warn-ink)}
/* The other two fixed semantics, so a badge meaning "this succeeded" or "for
   your information" has somewhere correct to go. `.badge.success` used to be
   its own rule in app.css reading the fixed --ok pair; without these a
   re-class would have had to send it to `contact`, which is the themeable
   identity green — same colour today, but a client re-skin would then have
   moved a SUCCESS marker. */
[data-tone=ok]{--tone-bg:var(--ok-bg);--tone-fg:var(--ok-ink);--tone-edge:var(--ok-ink)}
[data-tone=info]{--tone-bg:var(--info-bg);--tone-fg:var(--info-ink);--tone-edge:var(--info-ink)}
/* `ui_tags.py` has defaulted `badge_tone="muted"` since it was written, and
   `.tone-muted` was defined NOWHERE — so every `{% field %}` badge that did not
   pass a tone (the unverified-email markers among them) rendered with no tone
   rule at all. Defined here at last, at the neutral values it was falling back
   to, so nothing moves and the default finally names something. */
[data-tone=muted]{--tone-bg:var(--panel);--tone-fg:var(--muted);--tone-edge:var(--border)}

.badge[data-tone]{background:var(--tone-bg,var(--panel));color:var(--tone-fg,var(--muted));border-color:var(--tone-edge,var(--border))}
/* The reset the status ramp got and this table did not. `[data-tone=X]` is a
   bare attribute selector, so it also sets the triple on `.card[data-tone=X]`
   -- and custom properties inherit. Without this, a badge carrying a tone with
   NO table row (both `ui/card.html` and `ui/field.html` interpolate a caller's
   string, so any value can arrive) inherited the enclosing card's colour
   instead of falling back to neutral, and the `var(…, fallback)` above could
   never fire.

   The comment that used to sit here claimed requiring the attribute on the
   badge made that "structurally impossible". That was wrong: it is impossible
   only for values that HAVE a row. Zero specificity so a mapped tone still
   wins at (0,1,0). */
:where(.badge){--tone-bg:var(--panel);--tone-fg:var(--muted);--tone-edge:var(--border)}

/* ---- The removable affordance (owner ruling 2026-08-17) -------------------
   Six competing remove-"x" implementations existed. This is the filter-chip
   one promoted to serve every family, because it was the only one of the six
   that is actually reachable: `.skill-chip .remove-skill` and
   `.people-filter-remove` have no reveal and no focus ring, `.flexi-col-remove`
   is a different concept (it removes a table COLUMN), and `.chip-x` has no rule
   at all — so its 21 instances on the permissions page fall through to the bare
   `button` treatment and render as full-size lime buttons inside 22px chips.

   Two declarations here are load-bearing and will look redundant to the next
   reader. They are not:
     * `:focus-within` alongside `:hover` — without it the control cannot be
       reached by keyboard at all.
     * the `(hover:none),(pointer:coarse)` block — without it the control is
       PERMANENTLY invisible on a touch device.
   Deleting either makes the affordance unusable for a whole class of user.

   The negative margins pull the 18px box into the badge's 3px/8px padding so a
   removable badge stays 24.797px tall — that only holds at the reference
   specimen's padding, so re-measure if it changes. */
:where(.badge,.status,.chip) > .badge-remove,
.badge-remove{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:18px;height:18px;min-height:0;margin:-1px -5px -1px 1px;padding:0;border:0;border-radius:var(--r-pill,999px);background:transparent;color:inherit;cursor:pointer;opacity:0}
:where(.badge,.status,.chip):hover > .badge-remove,
:where(.badge,.status,.chip):focus-within > .badge-remove{opacity:1}
.badge-remove:focus-visible{outline:2px solid var(--focus-ring,var(--accent));outline-offset:-2px}
@media(hover:none),(pointer:coarse){.badge-remove{opacity:1}}

/* The class spellings that used to live here (.badge.tone-X, .badge-blue,
   .badge-green, .badge-purple, .badge-gray, .badge-warning, .badge.success)
   are gone: every call site now writes `class="badge" data-tone="X"`, so the
   aliases had nothing left to alias. Seventeen declarations for eight tones
   became one table plus one consumer per family. */

/* ---- Candidate status tones (Sprint1 WP3, P1-2) — /applications/ badge.
   Keyed on the candidate WORDING server-side (staffing/enums.py
   candidate_status_tone); three buckets so the colour can never become the
   marker the §3.2 mask removed. ---- */
.badge.ct-active{background:var(--ct-active);border-color:var(--ct-active-ink);color:var(--ct-active-ink)}
.badge.ct-good{background:var(--ct-good);border-color:var(--ct-good-ink);color:var(--ct-good-ink)}
.badge.ct-closed{background:var(--ct-closed);border-color:var(--ct-closed-ink);color:var(--ct-closed-ink)}


/* ---- Skill badges (display-only cells: recruiting board + person-edit
   pipelines). Two layers: pipeline-specific (tinted + dashed border as the
   non-colour cue) and candidate-profile (neutral). The display toggle is one
   body class — the flexi popout is body-appended, so this covers cells and
   popout clones alike. ---- */
.skill-badge{margin:1px 2px 1px 0}
.skill-badge--pipeline{background:var(--tone-identity);border:1px dashed var(--tone-identity-ink);color:var(--tone-identity-ink)}
.skill-badge--candidate{background:var(--neutral-soft);border:1px solid var(--border);color:var(--ink-faint)}
body.skills-pipeline-only .skill-badge--candidate{display:none}
/* Empty-state dash: nothing at all in union view, or no pipeline layer in
   pipeline-only view. Flags are stamped server-side / by the row builder. */
.skills-empty-marker{display:none;color:var(--muted)}
[data-has-pipeline-skills="0"][data-has-candidate-skills="0"] .skills-empty-marker{display:inline}
body.skills-pipeline-only [data-has-pipeline-skills="0"] .skills-empty-marker{display:inline}
/* Display toggle: state-describing label + a live tint swatch (never a
   colour word). Both labels ship in the DOM; the body class picks one. */
.skills-display-toggle{display:inline-flex;align-items:center;gap:6px;border-radius:8px;padding:4px 10px;cursor:pointer;font-size:12px}
.skills-toggle-swatch{display:inline-block;width:12px;height:12px;border-radius:4px;vertical-align:middle}
.skills-toggle-swatch--candidate{background:var(--neutral-soft);border:1px solid var(--border)}
.skills-toggle-swatch--pipeline{background:var(--tone-identity);border:1px dashed var(--tone-identity-ink)}
body.skills-pipeline-only .skills-display-toggle .skills-toggle-label-union{display:none}
body:not(.skills-pipeline-only) .skills-display-toggle .skills-toggle-label-pipeline{display:none}
/* Modal layer rows */
.pipeline-skills-layer{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin:6px 0 12px}
.pipeline-skills-sublabel{font-size:12px;color:var(--muted);margin:0}
.pipeline-skills-propagate-row{display:flex;align-items:center;gap:8px;margin-top:10px;font-size:13px}
/* Bulk skills bar (recruiting board + person-edit pipelines) */
.skills-bulk-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:8px 0}
.skills-bulk-bar input[data-skills-field]{min-width:240px}
.skills-bulk-propagate{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--ink)}

/* ---- Skill chips (editable contexts: pipeline skills modal) ----
   Migrated from job_pages.css so pages without that sheet (person edit,
   recruiting board) can render removable chips. Display-only table cells
   use .skill-badge below instead. */
.skills-chips{display:flex;flex-wrap:wrap;gap:6px}
.skill-chip{display:inline-flex;align-items:center;gap:6px;background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:4px 8px;font-size:12px}
/* Retired onto .badge-remove: these hard-coded #8a8f98 and #000, had no
   reveal and no focus ring. Kept as an alias until the call sites are
   re-classed, so there is one implementation from today rather than three. */
.skill-chip .remove-skill,.people-filter-remove,.chip-x{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:18px;height:18px;min-height:0;padding:0;border:0;border-radius:var(--r-pill,999px);background:transparent;color:inherit;cursor:pointer}
.skill-chip .remove-skill:focus-visible,.people-filter-remove:focus-visible,.chip-x:focus-visible{outline:2px solid var(--focus-ring,var(--accent));outline-offset:-2px}

/* Tone-tinted buttons (same palette as card/badge tones) — for colour-coding
   action banners by function. Their 1px contrasting tone-ink borders are gone
   (R4/SR-4), and so are their per-variant :hover rules: with app.css's global
   brightness layer in place those only differed from it by hundredths (.97 and
   .95 against .96), which is three darkening steps where the sprint's whole
   point is one. */
.btn-tone-identity{background:var(--tone-identity);color:var(--tone-identity-ink)}
.btn-tone-contact{background:var(--tone-contact);color:var(--tone-contact-ink)}
.btn-tone-compliance{background:var(--tone-compliance);color:var(--tone-compliance-ink)}
/* Archive actions: a solid but calmer red than the intense delete red (.btn-danger). */
.btn-danger-muted{background:var(--danger-muted);color:var(--on-danger-ink)}

/* ---- Confirm / accept / execute (owner ruling 2026-08-17) ----------------
   The product had no way to say "this button commits the thing". A harmless
   "explain this" trigger and a "run correction assessment" execute button
   rendered byte-identically on the GDPR dashboard.

   Same brand lime as the create action, distinguished by a slight raised
   edge. That REVERSES GLW-001, which stripped shadow and press-transform from
   every button in the tree, so it is carved out BY NAME in
   ButtonTreatmentTests.CARVE_OUTS citing this ruling — never by raising the
   budget, which would silently re-open the whole button family to shadows.

   inset, not a drop shadow: it lifts the edge without the glow GLW-001
   removed, and it costs no layout because an inset shadow does not affect the
   box. The press state removes the lift rather than translating the button,
   so nothing moves in the row. */
.btn-confirm{background:var(--btn-confirm-bg);color:var(--btn-confirm-ink);box-shadow:inset 0 -2px 0 rgba(var(--btn-lift-shade)/.28),inset 0 1px 0 rgba(var(--btn-lift-highlight)/.35)}
.btn-confirm:active{box-shadow:inset 0 1px 2px rgba(var(--btn-lift-shade)/.30)}

/* Action banner. The min-height:38px floor that used to live on .btn here
   existed ONLY to paper over the border-induced height drift R7 removes — a
   bordered button was 2px taller than its border:0 sibling. Post-SR-13 the
   natural height is 35.59px, so a 38px floor now STRETCHES every button and
   would force .btn-sm (24.8px) to 38px inside any converted row.
   No replacement is needed: with the borders gone every variant in this row
   is naturally 35.59px, so they line up without being made to. align-items
   stays `center` — this row is NOT all buttons (job_summary.html:24 puts a
   <span class="badge"> in it for closed jobs), and `stretch` would blockify
   that pill to the full line height. Converting the row is WID-060, phase B. */
/* WID-060, the row the owner named as worst: 7-8 buttons whose widths varied
   by 122.8px. It takes the step-down GRID rather than the hugging-cluster
   treatment, and that choice is forced by the row's size — a cluster is
   `grid-auto-flow:column`, which does NOT wrap, so eight equal tracks at
   max-content would run off the header. Four columns wrapping to two then one
   is criterion 2's own answer, and it is what `flex-wrap:wrap` was
   approximating badly here.
   `margin-left:auto` goes with the flex: a grid row that spans its container
   has nothing to push away from, and keeping it would leave the row
   right-hugging AND full-width at once. */
.btn-banner{display:grid;gap:var(--space-2);align-items:center;grid-template-columns:repeat(var(--cols,4),minmax(0,1fr))}
/* SR-15: "Parent project: {{ title }}" is DYNAMIC — a short client name
   today is a long one tomorrow — so SR-16's shorter button label does not fix
   this row on its own. Cap and ellipsise instead of letting one label set the
   row's width. 22ch is about the width of the longest static label beside it
   ("+ Create pipeline"), so a truncated project name reads as one of the row
   rather than as the row's outlier.
   The template's title="" carried only the generic "Go to parent project", so
   truncating would have LOST the project name outright — it now carries the
   full name, which is the half of this fix that keeps the text reachable. */
.btn-banner .btn{display:inline-flex;align-items:center;justify-content:center;max-width:22ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The closed-job badge is a grid item like everything else, so it was being
   stretched to a full button-sized track and reading as a button. It is the
   one non-button this row is known to contain (job_summary.html:24), and it
   has its own class, so size it to its content in place rather than reverting
   the whole banner to flex on closed jobs — which would drop the fix on
   exactly the jobs carrying the most buttons. */
.btn-banner .badge{justify-self:start}

/* ---- Flattened non-button controls (SR-4 + SR-10) ----
   The three tab idioms were three answers to one question, in two sheets: a
   20px white pill with a 1px border and --muted ink (.tabs), a 6px grey slab
   with two hardcoded hexes (.order-tabbar), and a transparent 3px underline
   (.vc-tabs). SR-4 collapses them onto ONE solid-fill treatment; SR-10 grants
   the rest fill (--btn-neutral-bg + --ink, "adopted by .btn-secondary AND
   every flattened non-button control").

   ONE rule, here, rather than three identical blocks in three places — that
   duplication is the defect this sprint removes, and components.css loads
   before both job_pages.css and vacation_chains.css so the per-sheet rules
   could only be deleted, not overridden. Same move S-A.4 made aliasing
   .btn-ghost onto .btn-secondary's block.

   Fill and ink ONLY. Padding, radius, weight and typography already come from
   the base `button` rule plus SR-13's :where() layer, so the three paddings
   (6/8/10px) and two radii collapse without a single new number being chosen.
   .dev-tools summary is the exception that proves it: a <summary> is not a
   <button>, so no base rule reaches it and it keeps its own geometry below.

   S-A.7 EXTENDS THIS RULE — .md-toolbar, .pipeline-menu-btn, .chip-btn
   and the rest join the selector list. Do not mint a parallel neutral for
   them.

   S9 REMOVES .flexi-toggle-btn from this list. It is a filter chip, and
   filter chips are badges now, so its neutral is `.badge`'s --panel. Leaving
   it here was not merely redundant: this rule loads AFTER app.css, so at
   equal specificity it BEAT the badge and the toggle went on rendering the
   old #d9e0e7 page-edge fill while the four chips beside it turned pale --
   one member of a family visibly out of step, which is the exact defect S9
   exists to remove. Measured, not reasoned: every other chip came back
   rgb(247,248,250) and this one rgb(217,224,231). */
.tabs button,
.order-tabbar button,
.vc-tabs button,
.tab-toolbar button,
.dev-tools summary,
.alert-dismiss,
.te-nav > a,
.dropdown-toggle,
.skills-display-toggle,
.md-toolbar button,
.pipeline-menu-btn,
.editor-nav a,
.preset-chips .chip-btn,
.location-row .remove-location,
.editor-sort-row .remove-office-btn,
.editor-sort-row .remove-team,
.vc-linear-remove,
.vc-linear-move,
.layout-tile{background:var(--btn-neutral-bg);color:var(--ink)}
.tabs button.active,
.order-tabbar button.active,
/* S-F.6: the underline carries the selected state without colour. These three
   strips inherit `font-weight:700` from app.css:28 at rest, so the cue
   `.stepper li.active` uses is not available here — there is no heavier step
   that renders reliably in system-ui. See SelectedStateTests. */
.vc-tabs button.active{background:var(--accent);color:var(--accent-ink);text-decoration:underline;text-underline-offset:3px}

/* A <summary class="btn"> renders the UA disclosure triangle INSIDE the
   button — 6 call sites (_composer_rows.html:29, _cv_profile_cards.html:117,
   :141, :192, :235, table_page.html:119). .dev-tools summary already
   suppressed it for exactly this reason; this generalises that, it does not
   invent it — including over tables.css's near-identical .flexi-add-col rule,
   which was the sixth site's own copy and won only by loading later. Both spellings
   are needed: Safari ignores list-style on a summary.
   .dev-tools summary joins it: it carries no .btn class but is styled as a
   button by the rule above, and it is where the house idiom came from — its
   own `list-style:none` moved here rather than being left behind. */
summary.btn,
.dev-tools summary{list-style:none}
summary.btn::-webkit-details-marker,
.dev-tools summary::-webkit-details-marker{display:none}

/* ---- Button grid (SR-12) ----
   The layout half of the sprint. Rows of buttons are flex today, so they size
   to their labels and a long one drags the row out of alignment; SR-12 rules
   they become a grid that steps 4 -> 2 -> 1.

   This is forms.css:38-56's --cols idiom, not a new one: same custom property,
   the same 880px/560px breakpoints, the same :where() override. Converging
   matters more than usual here because that file also records the BUG this
   shape exists to avoid.

   THE :where() IS LOAD-BEARING. A plain `.btn-grid.cols-N` is (0,2,0) and
   outranks the (0,1,0) media rules below, so the modifier silently beats the
   breakpoints and pins the grid at its desktop column count — forms.css:39-42
   documents that exact defect being found and fixed once already. It fails
   invisibly: the desktop layout looks perfect and only narrow viewports break.

   auto-fill is rejected by the ruling, on measurement: it left-aligned at a
   fixed ~173px and never stepped down.

   minmax(0,1fr), not 1fr: a grid track's default min-width is auto, so a long
   unbreakable label would force the track wider than its share and reintroduce
   the ragged row this replaces. That is also what makes the SR-15 ellipsis
   below possible — an overflow rule cannot bite in an auto-sized track. */
/* `.actions` joins the same definition rather than getting a second grid with
   the same intent and its own track list. It is the phase's cheapest lever —
   one selector converts 25 ledger rows, every `div.actions` filter/bulk row in
   the tree — and it defaults to 2 columns because those rows are almost all
   Filter/Clear pairs.

   `:not([hidden])` IS MANDATORY, and not as belt-and-braces. `#flexi-bulk-bar`
   (table_page.html:132) and `#bulk-bar` (role_fixing_list.html:72) both ship
   `hidden` and are revealed by JS. An author `display` rule beats the UA's
   `[hidden]{display:none}` by ORIGIN — specificity never enters it — so a bare
   `.actions{display:grid}` puts an empty bulk bar on those pages at every load.
   S-0.6's `[hidden]{display:none!important}` currently catches that, but a
   rule that relies on another rule's !important to not be visibly wrong is one
   edit away from breaking. This is why UNSCOPED_DISPLAY_BUDGET is 0.

   `:not(.layout .actions)` EXCLUDES the editors' sticky bar, and the first
   version of this comment was wrong about why that is needed. It claimed
   job_pages.css "loads later and keeps winning" — but load order only breaks
   ties, and these do not tie: this selector is (0,4,1) against that rule's
   (0,1,0), because :not()/:has() contribute their most specific argument. The
   shared rule was therefore silently converting the job/order editors' sticky
   bar, and flipping it back to flex whenever `job.closed_at` put a <span
   class="badge"> in the row. `.layout` is job_pages.css's root grid and exists
   only on those two pages, so this is precise. That bar is OTH-001, S-C.2's.

   THE :has() IS THE CARVE-OUT, and it is there because measurement forced it.
   `.actions` is a generic container name, NOT a button-row marker: revealed,
   `#bulk-bar` leads with a "0 selected" count <span> and
   `role_fixing_list.html:64` holds two <input>s and a <small class="hint">.
   Blanket-converting gave the count span a full 413.5px column — half the bar
   for two words — and wrapped its three buttons 2x2, which is worse than the
   flex row it replaced. Same "this row is not all buttons" defect S-A.5's gate
   caught on .btn-banner's badge.
   `:not(:has(> :not(.btn):not(button):not([hidden]):not([type=hidden])))` states the real precondition — every
   child is a button — so asymmetric rows keep flex automatically, including
   ones nobody has enumerated yet. The house has no existing idiom for this;
   :has() is not overriding one. See D-21.

   S-F.3: the carve-out now sits on EVERY rule that reaches the bar, not just
   this one. It was written here and nowhere else, so `:613`'s `min-width:0`
   and `:662`'s ellipsis still found the bar — the first removing its buttons'
   min-content floor, the second turning the resulting shrink into silent
   truncation. In the 420px left column (`job_pages.css:8`) five buttons
   rendered as ~71px stubs. It is NOT on `:530`'s `--cols`, and must not be:
   see `ColsStepDownTests` for why that one would break the step-down. */
.btn-grid,
.actions:not([hidden]):not(.layout .actions):has(> :not([hidden]):not([type=hidden])):not(:has(> :not(.btn):not(button):not([hidden]):not([type=hidden]))),
.wizard-actions-row:not([hidden]):has(> :not([hidden]):not([type=hidden])):not(:has(> :not(.btn):not(button):not([hidden]):not([type=hidden]))){display:grid;gap:var(--space-2);grid-template-columns:repeat(var(--cols,4),minmax(0,1fr))}
.actions:not([hidden]),
.wizard-actions-row{--cols:2}

/* HUGGING CLUSTERS — criterion 1 without criterion 2.
   The owner asked for two different things: "uniform button widths in rows"
   (1) and "responsive step-down grids" (2). A right-aligned cluster in a card
   head or a sticky save bar needs the FIRST and must not get the SECOND —
   stretching two buttons across a whole header is not what
   `margin-left:auto` / `justify-content:flex-end` were written to do.

   grid-auto-flow:column with 1fr tracks gives exactly that: the container
   still sizes to its content and stays where its parent puts it, while its
   buttons divide that width equally. No breakpoints, because these clusters
   are already narrow.

   Same all-children-are-buttons precondition as the full-width rows, so a
   cluster holding a count <span> or a hint keeps its flex layout untouched.
   NOT included: .modal-foot / .wizard-foot. Those are `Back | Cancel Next`
   split footers — the plan carves modal footers out by name (asymmetric
   cancel), and equal columns would destroy the split.

   `width:max-content` IS REQUIRED, and measurement is why. Most of these are
   plain block-level <div>s, so they are full-width by default, NOT sized to
   their content — `.page-actions` on the orders list has one visible button
   (its two bulk siblings ship `display:none`) and a bare 1fr track stretched
   that single button to 850px of an 882px page. `max-content` makes the
   container hug first, so the tracks divide a content-sized box.

   NOT included: `.form-actions` and `.modal-actions`. Those are full-width
   bars with their own background and padding — a sticky save bar whose
   background shrank to its buttons would be a worse defect than the one being
   fixed. They need the bar full-width AND the buttons equal at its end, which
   is a different shape; S-B.3c treats them per case. */
/* .btn-cluster is this idiom with a name, for the full-width bars that
   cannot hug themselves: the bar keeps its background and end-alignment,
   this box holds the buttons at equal width inside it. Unconditional — an
   author writing the class has opted in, so no precondition is needed. */
/* S-D.3: `.inline-form` was used at 11 call sites and defined nowhere, so each
   rendered `display:block` and pushed its row onto its own line inside the table
   cell or action row it sits in. The class name always described this intent;
   only the rule was missing. All 11 wrap a single button plus hidden inputs
   (which are `display:none`, so they add no flex item and no phantom gap). */
.inline-form{display:inline-flex;gap:var(--space-1);align-items:center;flex-wrap:wrap}

/* The vacation-approvals decision rows. `.cover-override-form` is deliberately
   NOT `.inline-form`: it is a BLOCK-level flex row that stacks one per cover
   inside a single cell, and its 2px separation is what keeps one cover's
   controls readable against the next. Treating it as an inline form dropped
   that margin and butted the rows together — see D-37. */
.cover-override-form{display:flex;gap:var(--space-1);align-items:center;margin-bottom:2px;flex-wrap:wrap}
.decision-note-input{width:130px}
.cover-override-label{min-width:190px}

.btn-cluster:not([hidden]),
.create-actions:not([hidden]):has(> :not([hidden]):not([type=hidden])):not(:has(> :not(.btn):not(button):not([hidden]):not([type=hidden]))),
.card-actions:not([hidden]):has(> :not([hidden]):not([type=hidden])):not(:has(> :not(.btn):not(button):not([hidden]):not([type=hidden]))),
/* JS-populated: #tag-buttons swaps between a <span class="muted">Loading…</span>,
   an empty-state span, and a row of tag buttons. It goes in the PRECONDITIONED
   list, never in .btn-cluster — that class is unconditional by design, so it
   would put the loading span in an equal grid track. The precondition converts
   the row only once its children really are all buttons. */
#tag-buttons:not([hidden]):has(> :not([hidden]):not([type=hidden])):not(:has(> :not(.btn):not(button):not([hidden]):not([type=hidden]))),
.chips:not([hidden]):has(> :not([hidden]):not([type=hidden])):not(:has(> :not(.btn):not(button):not([hidden]):not([type=hidden]))),
.page-head-meta:not([hidden]):has(> :not([hidden]):not([type=hidden])):not(:has(> :not(.btn):not(button):not([hidden]):not([type=hidden]))),
.page-head-actions:not([hidden]):has(> :not([hidden]):not([type=hidden])):not(:has(> :not(.btn):not(button):not([hidden]):not([type=hidden]))),
.page-actions:not([hidden]):has(> :not([hidden]):not([type=hidden])):not(:has(> :not(.btn):not(button):not([hidden]):not([type=hidden]))),
.flexi-apply:not([hidden]):has(> :not([hidden]):not([type=hidden])):not(:has(> :not(.btn):not(button):not([hidden]):not([type=hidden]))),
.order-actions:not([hidden]):has(> :not([hidden]):not([type=hidden])):not(:has(> :not(.btn):not(button):not([hidden]):not([type=hidden]))){display:grid;grid-auto-flow:column;grid-auto-columns:minmax(0,1fr);gap:var(--space-2);align-items:center;width:max-content;max-width:100%}
/* S-F.4: `max-width:100%` is a pure clamp. `width:max-content` already hugs,
   so it is inert for every cluster that fits and engages only where one would
   otherwise render WIDER THAN ITS PARENT and scroll the page — measured on the
   shipped tree at `.create-actions` 1138px inside a 1068px wrapper, and
   `.chips` 376px in a 343px box on the candidate-facing account page. The
   children already carry `overflow:hidden` (:626-), so their automatic minimum
   size is 0 and the minmax(0,1fr) tracks genuinely shrink.
   NOT `width:fit-content`, which is the one-declaration equivalent:
   `_action_row_classes` derives the whole row census from `width:max-content`,
   and `TokenContractTests` asserts that spelling verbatim. */
.btn-grid:where(.cols-2){--cols:2}
.btn-grid:where(.cols-3){--cols:3}
/* SR-15: labels are dynamic ("Parent project: {{ title }}"), so a short name
   today is a long one tomorrow. Truncate rather than let one child set the
   row's width. Applies to the grid's children whatever element they are —
   these rows mix <a class="btn"> and <button>. */
/* SR-15 truncation applies to BUTTON LABELS, not to every child. These rows
   are not all buttons: `role_fixing_list.html:64` puts two <input>s and a
   <small class="hint"> in an `.actions` row, and `#bulk-bar` leads with a
   count <span>. A blanket `> *` gave that hint `white-space:nowrap` and
   clipped it to one ellipsised line — the same "this row is not all buttons"
   defect S-A.5's gate caught when `.btn-banner`'s `stretch` blockified a
   badge. `min-width:0` is still safe for every child, since it only stops a
   long word forcing a track wider than its share. */
.btn-grid > *,
.actions:not([hidden]):not(.layout .actions) > *{min-width:0}
/* F5: grid items are blockified, so for the first time a .btn's box is wider
   than its label — and <button> centres by UA default while <a class="btn">
   does not, leaving one centred and one start-aligned label in the very rows
   this sprint exists to make uniform. The house already fixed this twice:
   tables.css's `.flexi-views-grid .btn{text-align:center}` and
   `.btn-banner .btn{justify-content:center}`. Both spellings are needed —
   text-align for the inline-block form, justify-content for the inline-flex
   one that .btn-banner and .btn-grid's own children use. */
.btn-grid > .btn,
.btn-grid > button,
.actions:not([hidden]):not(.layout .actions) > .btn,
.actions:not([hidden]):not(.layout .actions) > button,
.btn-cluster > .btn,
.btn-cluster > button,
.create-actions > .btn,
.create-actions > button,
.card-actions > .btn,
.card-actions > button,
#tag-buttons > .btn,
#tag-buttons > button,
.chips > .btn,
.chips > button,
.page-head-meta > .btn,
.page-head-meta > button,
.page-head-actions > .btn,
.page-head-actions > button,
.page-actions > .btn,
.page-actions > button,
.flexi-apply > .btn,
.flexi-apply > button,
.order-actions > .btn,
.order-actions > button,
/* S-B.3c's per-shape rows. These are NOT gridded — each holds a span, input,
   label or heading beside its buttons, so the all-button precondition declines
   them and B-plan rules that correct. But a button is a button: SR-15's
   truncation had shipped only on the converted containers, so an identical
   label wrapped here and ellipsised three rows away. */
.vc-actions > .btn,
.vc-actions > button,
.skills-bulk-bar > .btn,
.skills-bulk-bar > button,
.tag-toolbar > .btn,
.tag-toolbar > button,
.flexi-head > .btn,
.flexi-head > button,
/* the filter bars: one or two buttons beside a form's worth of inputs, so never
   gridded — but their buttons wrapped where every other row's ellipsised */
.filters > .btn,
.filters > button{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:center;justify-content:center}
@media(max-width:880px){
  .btn-grid,
  .stat-grid,
  .btn-banner{--cols:2}
}
@media(max-width:560px){
  /* Every container joined to the --cols GRID must appear here, not just the
     ones that started at 4 columns: `.wizard-actions-row` was given --cols:2
     and no step-down, a direct SR-12 violation ("grids step down 4 -> 2 -> 1
     via breakpoints") that left a two-up form footer two-up at 375px.
     `.create-actions` is NOT listed and must not be: it joined the CLUSTER
     idiom instead (:575, grid-auto-flow:column at max-content), which hugs its
     content and never had columns to step down. An earlier draft of this
     comment named it, which reads as an omission until you check line 575. */
  .btn-grid,
  .actions:not([hidden]),
  .wizard-actions-row,
  .btn-banner,
  .stat-grid{--cols:1}
  /* S-F.4: clusters step down too, and the comment above used to say they
     could not. That claim rested on "a cluster is already narrow", which the
     measurement falsifies: `.btn-cluster`'s help tabs render 596.9px in a
     500px viewport and `.chips` 376px in a 343px box at 375px. With
     `max-width:100%` clamping them the overflow becomes ellipsis instead —
     truncated buttons rather than a scrolling page, which is a different bad
     answer, not a good one. Returning to row flow at the same 560px
     breakpoint every other grid uses gives full-width buttons, which is
     exactly SR-12's 4 -> 2 -> 1 endpoint.
     The full precondition is repeated because it has to be: these rules must
     match the base rule's specificity to win on source order, and the base
     rule is (0,7,1). `.create-actions` IS listed here — it was correctly
     excluded from the `--cols` list above, which is a different mechanism. */
  .btn-cluster:not([hidden]),
  .create-actions:not([hidden]):has(> :not([hidden]):not([type=hidden])):not(:has(> :not(.btn):not(button):not([hidden]):not([type=hidden]))),
  .card-actions:not([hidden]):has(> :not([hidden]):not([type=hidden])):not(:has(> :not(.btn):not(button):not([hidden]):not([type=hidden]))),
  #tag-buttons:not([hidden]):has(> :not([hidden]):not([type=hidden])):not(:has(> :not(.btn):not(button):not([hidden]):not([type=hidden]))),
  .chips:not([hidden]):has(> :not([hidden]):not([type=hidden])):not(:has(> :not(.btn):not(button):not([hidden]):not([type=hidden]))),
  .page-head-meta:not([hidden]):has(> :not([hidden]):not([type=hidden])):not(:has(> :not(.btn):not(button):not([hidden]):not([type=hidden]))),
  .page-head-actions:not([hidden]):has(> :not([hidden]):not([type=hidden])):not(:has(> :not(.btn):not(button):not([hidden]):not([type=hidden]))),
  .page-actions:not([hidden]):has(> :not([hidden]):not([type=hidden])):not(:has(> :not(.btn):not(button):not([hidden]):not([type=hidden]))),
  .flexi-apply:not([hidden]):has(> :not([hidden]):not([type=hidden])):not(:has(> :not(.btn):not(button):not([hidden]):not([type=hidden]))),
  .order-actions:not([hidden]):has(> :not([hidden]):not([type=hidden])):not(:has(> :not(.btn):not(button):not([hidden]):not([type=hidden]))){grid-auto-flow:row;width:auto}
}

/* ADH-064: the Delete-account menu row carried `style="color: #b00020;"`, the
   tree's only use of that hex. --danger-ink is the palette's destructive text
   colour and is what every other destructive label already uses, so this is a
   tokenisation, not a recolour — the row keeps reading as destructive. The
   row itself stays a plain menu row (SR-9/SR-4 exempt dropdown rows). */
.dropdown-item-danger{color:var(--danger-ink)}

/* ---- Alerts (real component replacing the .toast position:static hack) ---- */
.alert{margin:var(--space-3) 0;padding:var(--space-2) var(--space-3);border:1px solid var(--border);border-radius:var(--r-2);background:var(--panel)}
.alert.alert-danger{border-color:var(--danger);background:var(--danger-soft);color:var(--danger-ink)}
.alert.alert-warning{border-color:var(--warn-ink);background:var(--warn-bg);color:var(--warn-ink)}
/* The family had four variants in live use and two defined, and the only
   `alert-error` rule was scoped inside `.messages-inline` (the flash-message
   area). `vacation_chains.js:1077-1078` toggles `alert-error`/`alert-success`
   on the cover-rebuild result panel, which is NOT in that area — so rebuilding
   every holiday-cover chain reported total success and total failure with
   byte-identical neutral chrome. `alert-info` (3 sites) was undefined outright.
   Completed here on the fixed semantic tokens, so the alert family speaks the
   same good/medium/bad language as every status surface. */
.alert.alert-error{border-color:var(--danger);background:var(--danger-soft);color:var(--danger-ink)}
.alert.alert-success{border-color:var(--ok-ink);background:var(--ok-bg);color:var(--ok-ink)}
.alert.alert-info{border-color:var(--info-ink);background:var(--info-bg);color:var(--info-ink)}

/* Persistent flow-level messages (candidate_ux_2026_07 P0-8). Errors and
   warnings render here instead of a 3-second toast, because they carry the
   recovery instructions — the toast channel keeps success/info only. */
.messages-inline{margin:var(--space-3) 0}
.messages-inline .alert{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--space-3)}
.messages-inline .alert.alert-error{border-color:var(--danger);background:var(--danger-soft);color:var(--danger-ink)}
/* SR-4 names this control verbatim. Its fill and ink come from the flattened
   -control rule above. What went FROM here: `background:none`,
   `color:inherit` and `opacity:.7` — a transparent glyph washed to 70%,
   which is the ghost treatment S-A.4 retired everywhere else. Full opacity because the rest
   state is now a real slab: .7 on it was a contrast reduction with nothing
   left to justify it, and it made the ex-hover rule (`opacity:1`) dead. */
.alert-dismiss{flex:0 0 auto;border:0;font-size:1.1em;line-height:1;cursor:pointer}

/* ---- Public job page facts + CTA (candidate_ux_2026_07 §9). The role's
   decision-relevant facts read as a scannable row above the prose, and the
   apply control sits with them instead of below an unbounded description. */
.job-facts{list-style:none;margin:var(--space-3) 0;padding:0;display:flex;flex-wrap:wrap;gap:var(--space-2) var(--space-4)}
.job-facts li{display:flex;gap:6px;align-items:baseline}
.job-fact-label{color:var(--muted);font-size:.85em;text-transform:uppercase;letter-spacing:.03em}
.job-skills{margin:var(--space-2) 0}
.job-cta{margin:var(--space-3) 0 var(--space-4)}
.job-cta-note{margin:calc(var(--space-3) * -1) 0 var(--space-4)}

/* Apply-requirements panel: the account-level fields the gate will reject,
   which have no input anywhere on the job page. */
.requirement-list{margin:0 0 var(--space-2);padding-left:var(--space-4)}
.requirement-list li{margin:2px 0}
@media (max-width:640px){
  .job-facts{flex-direction:column;gap:var(--space-1)}
}

/* Platform-settings live preview iframe (cv_gen P4). */
.branding-preview-frame{width:100%;height:640px;border:1px solid var(--border);border-radius:var(--r-2);background:var(--surface)}

/* ---- Field-group bubble: chrome-only, opt-in by class, so any label+control
   group anywhere can adopt it (first adopter: the flexi filter panel). The
   host keeps its own display (works on div.field, label, …). --pair is the
   horizontal label|control row variant. Never set overflow/height here —
   native controls must stay visible when JS is off. ---- */
.field-bubble{background:var(--bubble-bg);border:1px solid var(--bubble-border);border-radius:var(--r-2);padding:6px 10px;min-width:0}
.field-bubble--pair{display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);padding:4px 10px}
.field-bubble--pair:hover{border-color:var(--accent)}
.field-bubble:focus-within{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}

/* ---- Public legal pages: language picker row + GDPR contact form ---- */
.lang-picker{display:flex;gap:var(--space-2);justify-content:flex-end;margin:0 0 var(--space-4);flex-wrap:wrap}
.gdpr-contact-form{max-width:600px}
.gdpr-contact-form form{display:grid;gap:var(--space-3)}

/* ---- Urgency row tints (GDPR requests list). td-level rules with :hover
   overrides: .table-rows paints td backgrounds white — including on hover —
   so a tr-level background never shows through. ---- */
/* Signal tints. The `.table` half exists because the zebra rule in app.css is a
   two-selector list (`.table-rows` and `.table`) while these were `.table-rows`
   only -- so on a `.table` table the stripe would have won on even rows with
   nothing objecting. Zero-pixel today: no `.table` table carries these classes. */
/* Fixed tint, fixed ink. Background alone left the text on themed `--ink`,
   so a dark theme rendered late items at 1.00:1 -- the rows that are late
   are the ones that disappeared. */
.table-rows tr.row-overdue td,.table tr.row-overdue td{background:var(--st-rejected);color:var(--st-rejected-ink)}
.table-rows tr.row-due-soon td,.table tr.row-due-soon td{background:var(--warn-bg);color:var(--warn-ink)}
.table-rows tr.row-overdue:hover td,.table tr.row-overdue:hover td{background:var(--st-rejected)}
.table-rows tr.row-due-soon:hover td,.table tr.row-due-soon:hover td{background:var(--warn-bg)}

/* ---- Phantom utility/button classes referenced across templates ---- */
/* Primary action (SR-11): ONE solid step above the accent, --btn-strong-bg.
   It used to draw a 1.5px inset ring with box-shadow — the "outline around a
   green button" the owner objected to — plus a drop shadow that grew on
   hover. Both gone. The old comment explained the ring was a box-shadow
   *because* the global .btn:hover would wipe a border; that constraint dies
   with the hover glow in S-A.5, but the ring goes now regardless, because a
   ring IS the treatment being removed.
   Never an alias of bare .btn: _saved_views_section.html:12 distinguishes the
   ACTIVE saved view from identical-label siblings by this class alone. */
.btn-primary{background:var(--btn-strong-bg);color:var(--accent-ink)}
/* SUP-015 / D-11: the CURRENT item in a set of same-shaped buttons.
   `_saved_views_section.html` distinguished the active saved view from
   identical-label siblings by `.btn-primary` alone — the case SR-11 cites to
   justify keeping the variant. After S-A.3 flattened it that was a fill
   difference only: #aec21f vs #c1d72e is 1.2364:1, which FAILS WCAG 1.4.11's
   3:1 non-text floor where the 1.5px inset ring it replaced passed at 8.658:1.

   The ring comes back as a STATE marker rather than a decorative treatment —
   the same distinction S-A.7 drew keeping `.layout-tile.is-selected`'s ring
   while deleting its hover border. --accent-ink is the value the old ring
   already used, so the 8.658:1 is restored exactly and no new colour enters.

   `box-shadow`, not `outline`: `:focus-visible` owns the outline property, so
   an outline here would be REPLACED when the current item is focused, losing
   the state marker exactly when a keyboard user is on it. Different
   properties compose.

   Keyed on [aria-current], not on a variant class, so the visual state and the
   assistive-tech state cannot drift apart — and it reaches the house's other
   current-item set for free (legal/page_template.html:7-10, pinned by
   utils/tests.py:2366). */
.btn[aria-current]{box-shadow:inset 0 0 0 2px var(--accent-ink)}
.btn-sm{padding:0 8px;font-size:var(--fs-1);min-height:var(--control-h-sm)}

/* Page-level create CTAs (Create Job / Create Person / …). Was the sprint's
   worst offender: a gradient, a text-shadow, an inset white highlight, a drop
   shadow, a hover glow and a translateY lift. (All 9 occurrences are internal
   pages extending base.html — finance lists, orgs/projects forms, jobs_list,
   table_page. Earlier wording here said "public/candidate-facing"; that was
   wrong.) Now the same solid step as .btn-primary, which is SR-11 as ruled:
   both mean "the page's main action", and the sprint's point is one aesthetic.
   It keeps its own rule because plan.md §7.4 forbids renaming variants (9
   template occurrences) and because it owns the focus ring below.
   Ink moved --cta-ink -> --accent-ink: the same #24300f, named for the palette
   it now belongs to. The four --cta-* tokens are dead; S-A.8 deletes them in
   its own commit so this visual diff stays reviewable. */
.btn-cta{background:var(--btn-strong-bg);color:var(--accent-ink)}
/* Kept because plan.md §6 forbids the :focus-visible rule COUNT decreasing —
   not because it does anything the global ring at app.css does not. With the
   gradient gone its declarations are byte-identical to
   `button:focus-visible,.btn:focus-visible,…`, and all 9 .btn-cta occurrences
   are `class="btn btn-cta"`, so the global already reaches every one.
   Its offset was 1px against the global's 2px — a divergence a consistency
   sprint should not be preserving, so it is unified here. The rule is now
   provably redundant; S-A.8 owns dead-rule deletion and should take it,
   raising the §6 count constraint as it does.
   (Earlier revisions of this comment claimed the rule "had no test backstop".
   That was false — utils/tests_button_consistency.py's
   test_focus_visible_rules_survive_every_treatment_pass already pinned it by
   name. The claim came from plan.md §6, which predates that guard, and was
   copied forward without checking.) */
.btn-cta:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}
.muted{color:var(--muted)}
.mt-0{margin-top:0}
.mb-0{margin-bottom:0}
.minw-160{min-width:160px}
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
@media(max-width:560px){.grid-2{grid-template-columns:1fr}}
.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* ---- Pipeline rows (people_form + job recruiting share these) ---- */
.cell-actions{display:flex;gap:6px}

/* ---- Notes modal (ui/notes_modal.html + js/ui/notes.js) ---- */
.notes-list{list-style:none;margin:0 0 var(--space-3);padding:0;display:flex;flex-direction:column;gap:var(--space-2);max-height:50vh;overflow-y:auto}
/* .note-item's treatment lives on the shared nested-surface rule above; the
   copy that used to sit here won on source order and kept its literals. */
.note-item .note-meta{display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);margin-top:var(--space-1);font-size:var(--fs-1)}
/* Archived notes stay readable but visibly parked after the active ones */
.note-item.is-archived{opacity:.55;background:var(--panel)}
.notes-composer textarea{width:100%;resize:vertical}
/* ---- Flexi people filter (js/ui/people_filter.js) ---- */
.flexi-people-filter{position:relative}
.people-filter-selected{display:flex;flex-wrap:wrap;gap:var(--space-1);margin-bottom:var(--space-1)}
.people-filter-bubble{display:inline-flex;align-items:center;gap:2px;background:var(--accent-soft);color:var(--accent-soft-ink);border-radius:var(--r-2);padding:0 var(--space-1);font-size:var(--fs-1)}

.people-filter-dropdown{position:absolute;z-index:30;background:var(--surface);border:1px solid var(--border);border-radius:var(--r-2);box-shadow:0 4px 12px rgba(var(--shadow-color)/.08);max-height:14rem;overflow-y:auto;min-width:14rem}
.people-filter-option{display:block;width:100%;text-align:left;border:0;background:none;padding:var(--space-1) var(--space-2);cursor:pointer}
.people-filter-option:hover{background:var(--panel)}

/* ---- Bell-feed priority chip (P5) ---- */
.notif-priority-badge{border-radius:var(--r-1);padding:0 var(--space-1);font-size:var(--fs-1);font-weight:600}
.notif-priority-badge.notif-priority-urgent{background:var(--danger-soft);color:var(--danger-ink)}
.notif-priority-badge.notif-priority-high{background:var(--accent-soft);color:var(--accent-soft-ink)}

/* ---- Shared dropzone decoration (js/ui/dropzone.js) ---- */
.dropzone{border:2px dashed var(--border);border-radius:var(--r-2);padding:var(--space-2)}
.dropzone.is-dragover{background:var(--panel);border-color:var(--accent)}
.dropzone .dropzone-hint{margin:0 0 var(--space-1);font-size:var(--fs-1)}

.note-item .note-actions{display:flex;gap:var(--space-1);flex-wrap:wrap}
.note-item .note-edit-area{width:100%;resize:vertical}
.note-item .note-edit-actions{display:flex;gap:var(--space-1);margin-top:var(--space-1)}

/* ---- Conflict-resolution option rows (person type modal) ---- */
.conflict-options{display:grid;gap:8px}
.conflict-option{display:flex;align-items:center;gap:8px;padding:8px;background:var(--panel);border-radius:var(--r-1)}
.conflict-option input{width:auto}

/* ---- Copyable identifier chip (TEID, refs) ---- */
.chip.copyable{user-select:all;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}

/* ---- Page header band (edit-page identity strip) ---- */
.page-head{display:flex;gap:var(--space-3);align-items:center;margin-bottom:var(--space-3)}
.page-head-avatar{width:64px;height:64px;object-fit:cover;border-radius:var(--r-2);border:1px solid var(--border)}
.page-head h1{margin:0}
.page-head-meta{display:flex;gap:var(--space-2);align-items:center;flex-wrap:wrap;margin:4px 0 0}
.page-head-actions{margin-left:auto;display:flex;gap:var(--space-2);align-items:center}
.nowrap{white-space:nowrap}

/* ---- Two-column detail layout (main form + side rail of cards) ---- */
.two-col{display:grid;grid-template-columns:2fr 1fr;gap:0 var(--space-3);align-items:start}
@media(max-width:1080px){.two-col{grid-template-columns:1fr}}

/* Sticky side rail ("review + update" pages, e.g. gdpr_requests_detail) */
.sticky-rail{position:sticky;top:16px}

/* Preserve user line breaks (message bodies, free-text quotes) */
.pre-wrap{white-space:pre-wrap}

/* ---- Key/value grid (label/value pairs inside cards; replaces the
   table-only .kv class that div-based templates misused) ---- */
.kv-grid{display:grid;grid-template-columns:180px 1fr;gap:var(--space-1) var(--space-3);margin:0}
.kv-grid dt,.kv-grid .kv-key{color:var(--muted);font-weight:600}
.kv-grid dd{margin:0}
@media(max-width:560px){.kv-grid{grid-template-columns:1fr}.kv-grid dd{margin-bottom:var(--space-2)}}

/* ---- Narrow public card (token/confirmation flow pages) ---- */
.narrow-card{max-width:420px;margin:40px auto;padding:var(--space-4);border:1px solid var(--border);border-radius:var(--r-2);background:var(--surface);box-shadow:0 1px 0 rgba(var(--shadow-color)/.04)}
.narrow-card h1{font-size:var(--fs-5)}

/* ---- Cookie/legal note under public auth forms (moved out of the
   signup_form.html inline style) ---- */
.legal-note{margin-top:12px;font-size:0.9em;color:var(--muted)}

/* ---- Wizard stepper ---- */
.stepper{display:flex;gap:var(--space-2);margin:0 0 var(--space-3);padding:0;list-style:none}
.stepper li{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:var(--r-pill);border:1px solid var(--border);background:var(--panel);color:var(--muted);font-size:var(--fs-1)}
.stepper li.active{background:var(--accent-soft);border-color:var(--accent);color:var(--accent-soft-ink);font-weight:600}
.stepper li.done{background:var(--ok-bg);border-color:var(--ok-ink);color:var(--ok-ink)}

/* ---- Candidate waiting room (Sprint1 WP3) — the classes the
   candidate_ux sprint shipped unstyled, styled via existing idioms.
   None declares a layout prop that a co-occurring class also declares
   (.muted is colour-only; .stepper owns the flex). ---- */
.consent-line{margin:0 0 var(--space-3);color:var(--muted);font-size:var(--fs-1)}
.consent-line.consent-line-action{padding:var(--space-2) var(--space-3);border:1px solid var(--warn-ink);border-radius:var(--r-2);background:var(--tone-compliance);color:var(--warn-ink)}
.last-change,.cv-submitted{display:block;margin-top:var(--space-1);font-size:var(--fs-1)}
.interview-note{margin-top:var(--space-1);padding:var(--space-1) var(--space-2);border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:var(--r-1);background:var(--panel);font-size:var(--fs-1)}
.status-timeline{flex-wrap:wrap;margin:var(--space-2) 0 0}
.status-timeline li .muted{font-weight:400}
.continuation-note{margin-top:var(--space-2);font-size:var(--fs-1)}
.consultant-name{font-weight:600}
.doc-list{list-style:none;margin:0;padding:0;display:grid;gap:var(--space-1)}
.doc-list li .muted{font-size:var(--fs-1)}

/* ---- Upload progress (Sprint1 WP6) — submit_guard.js drives the fill on
   XHR submits. Deliberately NOT research.css's .progress-track: that is a
   page sheet with its own hexes, and a candidate-page dependency on it
   would be a cascade trap. The [hidden] attribute does the hiding — no
   CSS reveal (HiddenAttributeTests budget 0). ---- */
.upload-progress{margin:var(--space-2) 0}
.upload-progress-track{height:8px;border-radius:var(--r-pill);background:var(--panel);border:1px solid var(--border);overflow:hidden}
.upload-progress-fill{height:100%;width:0;border-radius:var(--r-pill);background:var(--accent);transition:width .3s ease}
.upload-progress-note{margin:var(--space-1) 0 0;font-size:var(--fs-1)}

/* ---- Full-bleed pages (editors) — set via {% block main_class %} ---- */
.content-wrapper.full-bleed{max-width:none;padding:0}

/* ---- Template editor chrome (utils email/page template editors) ---- */
.tag-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:6px 0}
.tag-toolbar #tag-buttons{display:inline-flex;gap:4px;flex-wrap:wrap}
.preview-grid{display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)}
@media(max-width:720px){.preview-grid{grid-template-columns:1fr}}
.preview-text{white-space:pre-wrap;background:var(--panel);padding:10px;border-radius:var(--r-1)}
.preview-frame{width:100%;height:400px;border:1px solid var(--border);border-radius:var(--r-1)}
.preview-frame-wrap{border:1px solid var(--border);border-radius:var(--r-1);overflow:hidden}
.preview-frame-wrap .preview-frame--tall{height:500px;border:none;border-radius:0}

/* Markdown editor toolbar (page template editor; markdown_utils.js injects
   .md-toolbar into #md-toolbar-container — scoped under the container so the
   different .md-toolbar skin in job_pages.css is unaffected). Moved here from
   the in-content <style> block of internal/page_templates/edit.html. */
#md-toolbar-container .md-toolbar{display:flex;gap:4px;margin-bottom:6px;flex-wrap:wrap}
#md-toolbar-container .md-toolbar button{padding:4px 8px;border-radius:4px;cursor:pointer;font-weight:600;min-width:28px}
#md-content{font-family:'Consolas','Monaco','Lucida Console',monospace;font-size:14px;line-height:1.5}

/* ---- Backups dashboard. Moved out of the in-content <style> block of
   internal/backups/dashboard.html, the same lift components.css already did
   for page_templates/edit.html above. Declarations are verbatim; the classes
   below the moved rules replace that template's 12 inline styles, also at
   their existing values.
   NOT converged onto .modal-overlay — see D-52: .backup-modal is a near
   duplicate of it but scrolls differently (flex column + max-height, versus
   the house's margin + overlay scroll that S-3.1b ruled on), so merging them
   is a scroll-behaviour decision, not a rename. ---- */
.backup-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(280px, 1fr)); gap:16px; align-items:start; }
.backup-panel { border:1px solid var(--surface-nested-edge); border-radius:8px; padding:16px; background:var(--surface); }
.backup-panel h2 { margin:0 0 10px; font-size:20px; }
.backup-modal { display:none; position:fixed; inset:0; overflow-y:auto; padding:24px 0; background:rgba(var(--scrim-color)/.35); z-index:1000; }
.backup-modal-dialog { width:92%; max-width:980px; max-height:calc(100vh - 48px); margin:0 auto; background:var(--surface); border-radius:8px; box-shadow:0 10px 30px rgba(var(--shadow-color)/.25); display:flex; flex-direction:column; }
.backup-modal-body { overflow-y:auto; padding:16px; }
.backup-policy-grid { display:grid; grid-template-columns:minmax(260px, .9fr) minmax(300px, 1.1fr); gap:16px; align-items:start; }
.backup-stack { display:grid; gap:12px; }
/* .backup-clarification takes the shared nested-surface rule above; the copy
   that sat here won on source order and kept four literals. */
.checkpoint-detail[hidden] { display:none !important; }
.backup-form-row { margin:0 0 10px; }
.backup-form-row label { display:block; font-weight:600; margin-bottom:4px; }
/* Fixed wash, fixed ink. This is the form that says the database and media
   will be REPLACED, and its copy inherited the themed ink -- 1.17:1 on a
   palette the validator accepts. Same class as the confirmation dialog, on
   the page where the stakes are highest. */
.backup-danger { border-color:var(--danger-ink); background:var(--danger-wash); color:var(--danger-ink); }
@media (max-width:760px) { .backup-policy-grid { grid-template-columns:1fr; } }
.backup-panel-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
.backup-modal-head{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--rule-soft)}
.backup-modal-head h3{margin:0;font-size:18px}
.backup-fieldset{padding:var(--space-3)}
.backup-modal-foot{display:flex;justify-content:flex-end;gap:8px;border-top:1px solid var(--rule-soft);padding-top:12px;margin-top:12px}
.backup-stack-tight{display:grid;gap:6px;margin-top:8px}
.backup-section{margin-top:32px}
.flush-top{margin-top:0}
.flush-bottom{margin-bottom:0}

/* ---- GDPR runs dashboard (overhaul 2026-08 rebuilt the page; the old
   modal/panel shell classes died with the old template — gate review 2
   dead-code sweep). ---- */
.gdpr-fieldset{padding:var(--space-3)}
.gdpr-runs-heading{margin-top:32px}
/* Overhaul 2026-08 dashboard shell (design §5): popout bar, badge row,
   popover body sizing, automation gate list, retention-floor section. */
.gdpr-popout-bar{display:flex;gap:var(--space-2);align-items:center}
.gdpr-badge-row{display:flex;flex-wrap:wrap;gap:var(--space-2);margin:var(--space-3) 0}
.gdpr-popover-card{min-width:min(560px,90vw);max-width:640px}
/* The enhanced card is the ONE scroll container (tables.css sets
   max-height+overflow on it) — the body must never scroll separately or the
   popout grows a second scrollbar. */
.gdpr-popover-body{padding:var(--space-2) var(--space-3) var(--space-3)}
.gdpr-field{margin:0 0 var(--space-2)}
.gdpr-field small{display:block}
.gdpr-popover-body h3{margin:var(--space-3) 0 var(--space-1)}
.gdpr-gate-list{display:grid;gap:var(--space-2);padding-left:0;list-style:none}
.gdpr-progress-track{background:var(--panel);border:1px solid var(--border);border-radius:var(--r-1);height:12px;overflow:hidden;margin:var(--space-2) 0}
.gdpr-progress-fill{background:var(--st-applied);height:100%;width:0}

/* The translations strings screen (translation sprint M2): an inline edit form
   in every row -- the textarea takes the cell's width, the cells top-align so
   a long English text and its edit box start on the same line. */
.translations-strings{table-layout:fixed;min-width:64rem}
.translations-strings th:nth-child(1){width:22%}
.translations-strings th:nth-child(2){width:34%}
.translations-strings th:nth-child(3){width:32%}
.translations-strings th:nth-child(4){width:12%}
.translations-strings td{vertical-align:top}
.translations-strings code{word-break:break-all}
.translations-edit textarea{display:block;width:100%;min-width:12rem;margin-bottom:var(--space-1)}

/* Staleness of a "days since" figure in the job editor. Moved out of
   job_editor.js, which wrote the hexes inline; same values, now themeable and
   greppable. See D-44 on --age-ageing's contrast. */
[data-age=ageing]{color:var(--age-ageing)}
[data-age=stale]{color:var(--age-stale)}

/* job_editor.js built its office/location/employee rows from three near-identical
   inline strings. Collapsed at their EXISTING values — deliberately NOT reusing
   `.rowlist-row`, which is a bordered grid on #fff and would be an unruled
   visual change on a drag-sortable flex row. */
.editor-sort-row{display:flex;align-items:center;gap:8px;background:var(--panel);padding:6px;border-radius:4px}
.editor-sort-row.is-draggable{cursor:move}
/* The parts job_editor.js builds inside those rows, out of its innerHTML.
   `.drag-handle` already existed but only under `.rowlist-row`; the same handle
   in an editor row got nothing, so its rule is widened rather than copied. */
.row-grow{flex:1;min-width:0}
.row-stack{display:flex;flex-direction:column}
.row-sub{font-size:11px;color:var(--ink-faint)}
.col-80{width:80px}
.col-140{width:140px}
.semi{font-weight:500}

/* The GDPR run detail's chip row is `.chips` (byte-identical rule in app.css:194);
   only its trailing gap was bespoke, so that alone stays named here. */
.gdpr-chip-row{margin-bottom:var(--space-3)}

/* Promoted from job_pages.css (S-D.3). Shared by the order editor and the
   canonical Flexi Orders reference cell; one definition keeps the retention
   status visually consistent across both surfaces. */
.locked-badge{margin-left:6px;font-size:11px;background:var(--warn-wash);border:1px solid var(--warn-wash-border);color:var(--warn-wash-ink);border-radius:6px;padding:1px 6px;vertical-align:middle}

/* ---- Toasts (moved out of base.html inline <style>) ---- */
.toast-root{position:fixed;right:20px;bottom:20px;z-index:9999;display:grid;gap:8px}
.toast{background:var(--toast-bg);color:var(--toast-ink);padding:10px 12px;border-radius:var(--r-1);box-shadow:0 6px 24px rgba(var(--shadow-color)/.2)}
.toast.success{background:var(--toast-ok-bg)}
.toast.error{background:var(--toast-danger-bg)}
.toast.info{background:var(--toast-bg)}
.toast.warning{background:var(--toast-warn-bg)}

/* ---- Site footer (moved out of base.html inline <style>) ---- */
.site-footer{margin-top:40px;padding:16px 0;border-top:1px solid var(--border);color:var(--muted);font-size:.9em}
.site-footer .container{max-width:1100px;margin:0 auto;padding:0 16px;display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}
.site-footer a{color:var(--ink-strong);text-decoration:underline}
/* The site-language select in the footer (translation sprint D6/D22): a bare
   <select> with a label, no form, no button. Shipped before the markup (the
   phantom census is markup->CSS only); it stays inert until the footer
   renders the control. */
.site-lang{display:flex;align-items:center;gap:6px;color:var(--muted);font-size:.9em}
.site-lang label{white-space:nowrap}
.site-lang select{font:inherit;color:var(--ink-strong);background:var(--surface);border:1px solid var(--border);border-radius:var(--r-1);padding:2px 6px;min-height:var(--control-h-sm)}

/* ---- Job recruiting summary page (staffing/internal/job_summary.html) ----
   Moved out of that template's in-content <style> block, which outranked
   every stylesheet on the page. Sits beside the .pipeline-menu* rules below
   because those came out of the same block, in the same way, earlier.
   Tokenised on the way across, all at their existing values:
   #d9e0e7 == --border, #f7f8fa == --panel, #fff == --bg, #fcfdff == --tile-bg,
   10px == --r-3, 8px == --r-2.

   The values kept are the ones that were live inline — NOT job_pages.css's
   dead 100vh/1400px copy. Note, though, that three of them are inert on the
   page as it renders today, so "preserved" here means zero-risk, not
   load-bearing: this <main> is nested inside base.html:32's
   .content-wrapper (max-width 1100 - 32 padding = 1068px available), so
   `max-width:1200px` can never bind and `margin:0 auto` has nothing to
   centre; and the measured page is 1320px tall, well past `min-height:60vh`.
   They are kept because changing them is a visual gamble with no upside,
   not because they do anything at 1068px. A slice that wants to delete them
   should re-measure a *sparse* job first — 60vh does bind on a short page.

   The block's bare `table` and `th,td` rules were dropped, not moved: they
   restate app.css:34-35 exactly, which also supplies the vertical-align:top
   the inline copy never overrode. */
.summary-layout{display:grid;grid-template-rows:auto 1fr;min-height:60vh}
.summary-header{display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid var(--border);background:var(--panel)}
.summary-main{display:grid;gap:16px;padding:16px;max-width:1200px;margin:0 auto}
.summary-card{background:var(--bg);border:1px solid var(--border);border-radius:var(--r-3);padding:12px}
.overview-grid{display:grid;grid-template-columns:repeat(2, minmax(240px, 1fr));gap:10px}
.overview-item{display:grid;grid-template-columns:140px 1fr;gap:10px;border-style:dashed}

/* ---- Pipeline row kebab menu (job recruiting + person edit pipelines;
   moved out of job_summary.html inline <style>) ---- */
.pipeline-menu-wrap{position:relative}
/* ADH-030 (SR-9): the panel's hexes are tokenised AT THEIR EXISTING VALUES —
   #fff is --bg, #d9e0e7 is --border. The menu ROWS keep their treatment:
   SR-9 rules them dropdown menu rows, inside SR-4's own exemption, exactly
   as CLS-006 kept .notif-group-head. The kebab TRIGGER is a control and does
   flatten (OUT-012/OUT-038) — its fill comes from the shared rule above and
   its bespoke #f3f4f6 hover is deleted, since S-A.5's global darkening layer
   now covers it. */
.pipeline-menu-btn{border-radius:4px;padding:4px 10px;cursor:pointer;font-size:18px;line-height:1}
.pipeline-menu{display:none;position:absolute;right:0;top:100%;min-width:150px;background:var(--bg);border:1px solid var(--border);border-radius:6px;box-shadow:0 4px 12px rgba(var(--shadow-color)/.15);z-index:100}
.pipeline-menu.open{display:block}
.pipeline-menu button{display:block;width:100%;text-align:left;padding:8px 12px;border:none;background:none;cursor:pointer;font-size:14px}
/* R18: server-computed scope reason shown above disabled row actions. */
.pipeline-menu .menu-scope-note{padding:8px 12px;font-size:12px;line-height:1.4;color:var(--muted);border-bottom:1px solid var(--border);max-width:240px}
/* R28/R11: §3.4 batch accounting — the named outcome panel and the
   proceed-with-remainder tick inside the destructive dialog. */
.flexi-bulk-report{display:flex;justify-content:space-between;align-items:flex-start;gap:var(--space-3)}
.flexi-proceed-remainder-wrap{display:block;margin-top:var(--space-2);font-weight:600}
/* S-1.3: consequence-report groups inside the destructive modal —
   Blocks → Decide → Warn → Note ordering comes from the server. */
.modal-item-list .wizard-group{font-weight:600;margin-top:var(--space-2)}
.modal-item-list .wizard-group-blocks{color:var(--danger-ink)}
.modal-item-list .wizard-group-warn{color:var(--warn-ink)}
.modal-item-list .wizard-group-row{padding-left:var(--space-3)}
.pipeline-menu button:hover{background:var(--neutral-soft)}
.pipeline-menu button:first-child{border-radius:6px 6px 0 0}
.pipeline-menu button:last-child{border-radius:0 0 6px 6px}
.pipeline-menu .menu-save{color:var(--link)}
.pipeline-menu .menu-send{color:var(--ok-ink)}
.pipeline-menu .menu-ban{color:var(--danger)}
.pipeline-menu .menu-delete{color:var(--danger)}

/* Notification bell (navbar) — plan Phase 5 */
.notif-bell-wrap .dropdown-toggle{position:relative}
/* `:where(:not([hidden]))` and not a bare `:not([hidden])` (S-F.5).
   An author `display` rule beats the UA `[hidden]{display:none}` by cascade
   ORIGIN, so this rule was un-hiding every element the page JS hides by
   attribute. `:where()` contributes ZERO specificity, so the scope is
   provably free of cascade side effects — a bare `:not([hidden])` would
   raise this rule and change which of its neighbours wins. */
.notif-count-badge:where(:not([hidden])){display:inline-block;min-width:16px;padding:0 4px;border-radius:var(--r-pill);background:var(--danger);color:var(--on-danger-ink);font-size:11px;font-weight:700;line-height:16px;text-align:center;vertical-align:top;margin-left:2px}
.notif-menu{min-width:340px;max-width:420px;max-height:70vh;overflow:auto;padding:8px}
.notif-menu-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:2px 4px 8px;border-bottom:1px solid var(--border)}
.notif-group{border-bottom:1px solid var(--border)}
.notif-group:last-child{border-bottom:none}
.notif-group-head{position:relative;display:flex;align-items:flex-start;gap:8px;width:100%;min-height:52px;background:none;border:none;padding:8px 52px 12px 4px;cursor:pointer;font:inherit;text-align:left}
.notif-group-label{flex:1;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Expand/collapse chevron: a large circular chip pinned to the bottom right
   of each group header (the whole header row stays the click target). The
   off-shade-green circle + size beat app.css's generic 10px .dropdown .caret. */
/* SR-4 names "the notification caret ring": the 2px --accent-alt ring is
   gone, the solid --accent-soft disc stays. The ROW keeps its treatment —
   SR-9 rules .notif-group-head a dropdown menu row, inside SR-4's own
   exemption (CLS-006, the phase's one no-op-confirmed). The caret is a
   separate ledger row with disposition=fix, so the exemption does not
   reach it. */
.notif-menu .notif-group-head .caret{position:absolute;right:6px;bottom:6px;display:flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:50%;background:var(--accent-soft);color:var(--accent-soft-ink);font-size:18px;line-height:1;transition:transform .15s ease,background .15s ease}
/* Restates the ink because the fill changes: this caret is on the pale
   tint at rest and on the SOLID accent here, so it is the one element
   that needs both inks. Without this it kept the tint's ink on the
   solid fill. */
.notif-menu .notif-group-head:hover .caret{background:var(--accent);color:var(--accent-ink)}
.notif-menu .notif-group-head[aria-expanded="true"] .caret{transform:rotate(180deg)}
.notif-group-items{padding:0 4px 8px}
.notif-item{display:flex;flex-direction:column;gap:1px;padding:5px 6px;border-radius:6px;font-size:13px}
.notif-item time{font-size:11px;color:var(--muted)}
/* Only UNREAD items carry a tint, so without a matching ink they are the
   only ones a dark theme hides -- measured 1.02:1, on a menu reachable from
   every page. */
.notif-item.notif-unread{background:var(--info-bg);color:var(--info-ink);font-weight:600}
.notif-empty{color:var(--muted);font-size:13px;padding:10px 6px;margin:0}

/* ED-10 (employment overhaul): hired-without-placement inline warnings
   in the person-form pipelines card and the recruiting applicants table. */
.pipeline-placement-warning{margin-top:6px}
.pipeline-placement-warning.alert-danger{font-weight:700}

/* Date sub-badges (person-form pipelines card + recruiting applicants
   table): "Applied:" under the row's identity link, "Updated:" under the
   status select. Zero-height in-flow blocks — the text overflows into the
   cell's spare vertical space, so the row keeps its exact height; static
   with pointer-events:none, so they can never catch clicks or paint over
   the status select, the kebab menu or the ED-10 warning boxes (later
   siblings paint above them). */
.sub-badge{display:block;height:0;overflow:visible;font-size:9px;line-height:1;font-weight:600;letter-spacing:.2px;white-space:nowrap;color:var(--muted);opacity:.7;pointer-events:none}

/* ---- Busy state (apply_fix 1.2): submit_guard.js flags the clicked
   button while its POST is in flight. Sits ABOVE the disabled layer so
   disabled's opacity/cursor still win — a busy button IS disabled; this
   layer only adds the spinner. currentColor keeps it legible on every
   variant. ---- */
.btn.is-busy::after{
  content:"";
  display:inline-block;
  width:.85em;
  height:.85em;
  margin-left:.5em;
  vertical-align:-.1em;
  border:2px solid currentColor;
  border-right-color:transparent;
  border-radius:50%;
  animation:btn-busy-spin .7s linear infinite;
}
@keyframes btn-busy-spin{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion: reduce){
  .btn.is-busy::after{animation:none}
}

/* ---- Disabled buttons, all variants — LAST button layer in the cascade.
   The :hover forms are (0,3,0) so they beat every variant's :hover
   (.btn-primary/.btn-cta/.btn-danger — all (0,2,0)) regardless of file
   order. Background stays per-variant: a faded red still reads
   "danger, unavailable". ---- */
button:disabled,.btn:disabled,.btn[disabled],.btn.disabled,
button:disabled:hover,.btn:disabled:hover,.btn[disabled]:hover,.btn.disabled:hover{
  opacity:.55;
  cursor:not-allowed;
  box-shadow:none;
  transform:none;
  filter:none;
  text-shadow:none;
}
/* Class-based disabled = spans/links (e.g. pagination ends): not clickable. */
.btn.disabled{pointer-events:none}

/* S-6.1 departure handover wizard */
.departure-wizard-shell { min-width: 560px; max-width: 720px; }
.departure-wizard-group { margin-bottom: 10px; padding: 10px; }
.departure-wizard-group .badge { margin-left: 8px; }

/* Job/project close wizards (P9, job-closure hardening S-3.1b). The decide
   groups are UNCAPPED since S-3.1a, so a busy job's list is as long as its
   candidate list — it has to scroll inside the dialog rather than push the
   confirm control off-screen. */
.modal-overlay .modal-error{color:var(--danger-ink)}
.modal-overlay .cw-row{display:block}
.modal-overlay .cw-note{margin:4px 0}
.modal-overlay .cw-tight{margin:2px 0}
.modal-overlay .cw-late{border:1px solid var(--warn-border);border-radius:var(--r-1);padding:10px}
.modal-overlay .cw-indent{margin:4px 0 4px 22px}
.modal-overlay .cw-reason{margin-left:22px;max-width:calc(100% - 22px)}
.modal-overlay .cw-jobs{width:100%;border-collapse:collapse;margin-top:6px}
.modal-overlay .cw-jobs th,.modal-overlay .cw-jobs td{text-align:center}
.modal-overlay .cw-jobs th:first-child,.modal-overlay .cw-jobs td:first-child{text-align:left}
.modal-overlay .cw-jobs th:last-child{text-align:left}

/* ── TomSelect (vendored 2.3.1) — app-side override ─────────────────────────
   SR-3: the vendor file is never edited. It loads from {% block extra_head %}
   (base.html:25), i.e. AFTER this sheet (base.html:13), so an app rule at the
   vendor's own specificity would lose the tie on all 11 consumer pages. Each
   selector below is the vendor's own with `html` prefixed — +(0,0,1), the
   smallest bump that clears it, and the reason this needs no include-order
   change (plan.md §5: "wins by specificity", "no include-order normalisation").
   Values are the house neutral SR-10 already gives every flattened control;
   the 13px is tokenised at its existing value per SR-9, not resized.
   -------------------------------------------------------------------------- */
html .ts-wrapper.multi .ts-control > div{background:var(--btn-neutral-bg);color:var(--ink);border:0}
/* the selected + disabled chips are (0,4,1) in the vendor sheet — one class
   ABOVE the resting chip, so they need their own overrides or selecting a chip
   would make it *lighter* than at rest (the vendor's #e8e8e8 is lighter than
   --btn-neutral-bg). Active takes SR-4's solid-fill selected treatment, the
   same one the three tab bars collapsed onto. */
html .ts-wrapper.multi .ts-control > div.active{background:var(--accent);color:var(--accent-ink);border:0}
html .ts-wrapper.multi.disabled .ts-control > div,
html .ts-wrapper.multi.disabled .ts-control > div.active{background:var(--btn-neutral-bg);color:var(--muted);border:0}
html .ts-wrapper.plugin-remove_button:not(.rtl) .item .remove{border-left:0}
html .ts-wrapper.plugin-remove_button.rtl .item .remove{border-right:0}
/* the vendor's bespoke grey wash goes; the × is an <a>, so app.css:109's
   a:not(.btn):hover{opacity:.9} is the house hover it falls back to — the same
   collapse S-A.5 and S-A.7 applied to every other bespoke hover. */
html .ts-wrapper.plugin-remove_button .item .remove:hover{background:none}
/* colour and line-height ride here too: overriding only font-size would leave
   the control and dropdown at the vendor's #303030 while the chips above moved
   to --ink, i.e. two text colours inside one field. */
/* --fs-2 -> --fs-3 in the same commit as the control-typography change. The
   enhanced select renders a `div.ts-control`, NOT a `<select>`, so
   `:where(input,select,textarea)` cannot reach it -- leaving this at 13px
   would have put a plain select and an enhanced one side by side at 14px and
   13px on the 16 templates that load the widget. `TomSelectOverrideTests`
   pins the property and the specificity win, never the value, so it stays
   green. The vendor sheet is never edited (SR-3); this block wins by
   prefixing each vendor selector with `html`. */
html .ts-dropdown,html .ts-control,html .ts-control input{color:var(--ink);font-size:var(--fs-3);line-height:1.4}
/* The enhanced control joins the shared floor. Nothing else can reach it, and
   without this a plain select is 36px while its enhanced twin is content-sized.
   The inner chips keep the small floor so a multi-select holding one chip does
   not outgrow a single-line control. */
html .ts-wrapper .ts-control{min-height:var(--control-h);padding:0 var(--control-pad-x);display:flex;align-items:center;flex-wrap:wrap;gap:var(--space-1)}
html .ts-wrapper.multi .ts-control > div{min-height:var(--control-h-xs)}

/* Sprint 2 — office-scope toggle matrix + permission grid. Token-only so
   both themes inherit; no new colours, no hex literals. */
.office-scope-matrix,.authz-grid,.authz-people{border-collapse:collapse;width:100%}
.office-scope-matrix th,.office-scope-matrix td,.authz-grid th,.authz-grid td{
  border:1px solid var(--border);padding:var(--space-1) var(--space-2);text-align:center}
.office-scope-matrix th[scope="row"],.authz-grid th[scope="row"]{text-align:left}
.authz-grid-danger th,.authz-grid-danger td{border-color:var(--danger)}
.authz-cell{border:0;background:none;cursor:pointer;font-size:var(--fs-3);
  width:2rem;height:2rem;border-radius:var(--r-1);color:var(--muted)}
.authz-cell-granted{color:var(--on-page-accent-ink);font-weight:700}
.authz-cell-partial{color:var(--age-ageing);font-weight:700}
.authz-cell:hover:not(:disabled){background:var(--tile-bg)}
.authz-cell:disabled{cursor:not-allowed;opacity:.5}
.authz-reverse-search{margin:var(--space-3) 0}
.authz-reverse-results{margin-top:var(--space-1);padding:var(--space-1);
  border:1px solid var(--border);border-radius:var(--r-2)}
.authz-reverse-row{padding:var(--space-1) 0;border-bottom:1px solid var(--border)}
.authz-chips .chip{display:inline-flex;align-items:center;gap:var(--space-1);margin:.15rem}

/* M4 GUI pass: a wide table scrolls inside its own box, never the page. */
.table-overflow{max-width:100%;overflow-x:auto;overscroll-behavior-inline:contain}
