@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');

/* ---- Fonts ----
   The product UI uses the shared --font-sans token (the Inter/system stack)
   and .mono uses --font-mono. The ONLY exception is the brand wordmark, which
   is set in Roobert (bundled below, scoped to .wordmark). Roobert must not be
   used by any other product-UI element. */

:root {
  /* ---- design tokens — Verqa design system, v2 "Claude Desktop" ----
     Token NAMES unchanged (PRD v2.1 §6.1); VALUES retuned to the v2 language:
     warm paper canvas, warm near-black ink, serif display headings, one
     #335CFF brand blue reserved for links/selection/focus (NOT default
     buttons). Keep style.css and candidate.css in lock-step. */
  /* color — Vanta-style cool neutrals (2026-07 restyle).
     White panels on white/very-light-grey; colour is reserved for status
     (ok/warn/bad) and a single restrained accent (links, focus, selection,
     active nav, data-viz). Token NAMES are frozen; only VALUES change, and
     style.css + candidate.css stay in lock-step. */
  --bg: #ffffff;          /* canvas */
  --panel: #ffffff;       /* elevated surface (dialogs, popovers, drawer) */
  --ink: #17181c;         /* near-black text / primary button */
  --muted: #62666e;       /* sub text */
  --faint: #6b6f77;       /* eyebrows, hints — 5.04:1 on --bg, 4.83:1 on
                             --canvas, 4.66:1 on --hover. Was #8b8f98, which
                             is 3.24:1 on white and fails AA for body text
                             (UI review #11). The DARK theme's --faint is a
                             different value and already passes at 4.90:1 —
                             this is a light-theme-only fix and must not be
                             "harmonised" by copying one into the other. */
  --line: #e5e6ea;        /* hairline */
  --line-soft: #eef0f2;
  --accent: #3b5bdb;      /* restrained cool blue — links, selection, focus, active nav, data-viz */
  --accent-d: #2f49b0;    /* hover */
  --accent-tint: #eef1fd; /* soft accent tint */
  --accent-edge: #c9d4f8; /* hairline for --accent-tint at small sizes */
  --ring: #5f7fdc;        /* focus ring — 3.78:1 on --bg. Was #9db0f0, which
                             is 2.12:1 on white and fails the 3:1 WCAG 1.4.11
                             non-text floor, i.e. the focus indicator was
                             itself invisible on the surface it rings
                             (UI review #10). Kept lighter than --accent so a
                             ring still reads as a ring, not as a border. */
  /* Fano Blue — LOGO ONLY (wordmark "q" + checkmark + app icon). Approved brand
     constant; deliberately NOT wired into --accent or any UI token. */
  --brand-blue: #012AFF;
  --ok: #12844a;   --ok-tint: #e4f3ea;   --ok-ink: #0c6b3b;
  /* --ok2: THE SECOND-BEST BAND on a four-band scale (owner, 2026-08-06) --
     "Good match" on the fit scale, "Advance" on the interview scale. It is NOT
     a fourth `tone`: tone stays the three-value semantic ok/warn/bad that chips
     and the funnel dots read, and only the .score-pill ramp has four steps
     (scoring._BAND_PILL_CLASS). Teal rather than a second green: Strong and
     Good sharing --ok-tint is the defect this token exists to fix, and two
     greens one step apart would have reproduced it at lower contrast. Held
     clear of --accent (#3b5bdb) so a band never reads as an interactive
     affordance. 7.2:1 ink-on-tint. */
  --ok2: #0f7b8a;  --ok2-tint: #e0f0f3;  --ok2-ink: #0a6070;
  --warn: #c07f18; --warn-tint: #fbf0da; --warn-ink: #8a5b00;
  --bad: #d33f37;  --bad-tint: #fbe9e7;  --bad-ink: #b3261e;
  --neutral: #62666e; --neutral-tint: #f1f2f4;   /* default chip fill */
  /* THE STAGE RAMP, and it arrived only once the arithmetic allowed it (UX-59,
     owner 2026-08-06). UX-53 declared --stage-1/--stage-2 here for the handoff's
     proportional stage bar and then DELETED them, because that bar could not be
     drawn from the counts available: the funnel `db.positions_board` shipped was
     CUMULATIVE, not a partition, so a role whose candidates are all hired
     reported screened=3 AND decided=3 and a width-proportional bar drew it 50%
     decided. That measurement is not overturned. What changed is the data --
     `db.positions_board` now also buckets on `status`, which is one column and
     therefore disjoint, and `app._stage_mix` turns it into widths.

     THESE ARE ALIASES, NOT A NEW PALETTE, and that is deliberate on three counts.
     (1) No new hex enters the sheet, so nothing has to be re-derived for dark
     mode -- each target token is already themed, and the ramp follows it. (2) The
     hues already MEAN these things elsewhere on the same board: the amber is the
     `chip-warn` amber the "awaiting invite" chip uses, and the green is the green
     the single-fill progress bar used for `decided`. A recruiter is not asked to
     learn a second colour vocabulary. (3) Each stage's colour is named ONCE, here,
     so the bar and its legend key cannot drift -- they read the same token
     through the same `.s-*` class.

     The progression is quiet -> attention -> in-flight -> advanced -> done:
     --accent for the interview stage is the one that looks arguable, and it is the
     same choice `.funnel-fill` already makes for the drawer's funnel bar. (The
     "hold clear of --accent" note under --ok2 above is about BAND PILLS, where a
     tinted pill could read as a button; a 6px bar segment cannot be clicked.) */
  --stage-new:       var(--line);      /* applied, CV not scored yet   */
  --stage-screened:  var(--warn);      /* scored, waiting on you       */
  --stage-interview: var(--accent);    /* AI interview in flight       */
  /* SIX STAGES LOCKED (owner, 2026-08-20). The sixth ramp step, and it obeys
     the alias rule above rather than bending it: --accent-d is the accent's
     HOVER token, already themed in both palettes (#2f49b0 light, #9db0f8 dark),
     so no new hex enters the sheet and dark mode needs no re-derivation.
     THE DEEPER BLUE IS THE POINT, not a shortage of hues: the two interview
     rounds are one family, read as one phase, and the deeper of the two is the
     later one -- which is what the ramp's quiet -> attention -> in-flight ->
     advanced -> done progression already says in colour. It must stay DISTINCT
     from --stage-interview (a bar that paints two stages the same colour is a
     blind spot no source scan can see), and one step of the same hue is the
     smallest distinction that still reads as an ordering rather than a new
     category. */
  --stage-f2f:       var(--accent-d);  /* face-to-face round booked    */
  --stage-shortlist: var(--ok2);       /* shortlisted, awaiting a call */
  --stage-decided:   var(--ok);        /* offered / hired / rejected   */
  --stage-other:     var(--neutral);   /* not eligible, off-ladder     */
  --field: #ffffff;         /* input surface */
  --danger-edge: #f0cfcc;   /* .btn.danger hairline */
  /* surfaces used for table headers / subtle insets / hover */
  --canvas: #fafafa;        /* grey table-header row, section insets */
  --hover: #f5f6f7;         /* row / ghost hover */
  /* §6 fixed assessment-pillar accents (data-viz only) */
  --pillar-tech: #3b5bdb;   /* = accent           */
  --pillar-iq:   #7d52f4;   /* purple             */
  --pillar-val:  #0095e0;   /* sky                */
  --pillar-hr:   #7d52f4;   /* purple             */
  --pillar-pers: #c07f18;   /* amber              */
  /* type — system/licensed sans everywhere; serif token kept (unused) so the
     token check passes, but no heading renders serif ("no serif remnants"). */
  --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
  --font-serif: Georgia, 'Times New Roman', serif; /* legacy token — no longer applied */
  --font-mono: ui-monospace, "SF Mono", "Cascadia Code", Consolas, monospace; /* refs, IDs, tokens ONLY */
  --text-xs: 12px; --text-sm: 13px; --text-base: 14px; --text-md: 15px;
  --text-lg: 17px; --text-xl: 22px; --text-2xl: 30px;
  /* space (4px scale) */
  --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 24px; --s6: 32px; --s7: 48px;
  /* The horizontal inset INSIDE a form control, named rather than repeated as a
     literal. UX-05(3)/UX-09a: a column header's text starts at the cell's
     padding, but the text of a control inside that cell starts at the cell's
     padding PLUS this -- so every table whose cells hold full-width controls
     rendered its headers offset from the values beneath them. `.tbl-fields`
     below is the one fix; this token is what makes it traceable instead of a
     magic 13. */
  --field-pad-x: 13px;
  /* The horizontal text inset of a SIDEBAR RAIL ROW, named for the same reason
     as --field-pad-x above rather than repeated as a sixth literal 10. It is
     `.nav-item`'s own inset, and G17 (owner, 2026-07-31) established the nav
     rail as the reference every other sidebar row is read against: a 2px
     disagreement in `.side-foot` was a real, measured defect. A new rail row
     takes this token instead of picking a number.
     IT IS OFF THE 4px SCALE, and naming it does not put it on the scale. The
     8.13 off-scale counter reads px literals and therefore cannot see a `var()`
     -- that is the counter's blind spot, stated here rather than exploited: the
     value is the rail's EXISTING inset, not a new one, and the token exists so
     the next rail row cannot pick a seventh number. */
  --rail-inset: 10px;
  /* shape — 8px controls, 10px containers, pill chips */
  --r-sm: 6px; --r-md: 8px; --r-lg: 10px; --r-xl: 14px;
  /* Account-calendar grid geometry. OFF THE 4px SCALE and deliberately so: a
     30-row week has to fit a card without scrolling, and 19px is the handoff's
     measured row. Neither is a spacing property -- they are the size of a thing,
     which is what `height` and a grid TRACK are for -- so naming them here is
     not a route around arm 8.13's counter. */
  --cal-row-h: 19px;      /* one half-hour row */
  /* ---- THE FIVE CELL STATES OF THE HOURS GRID (scheduling hub, A3) ----
     FOUR TOKENS AND NOT FOUR HEX LITERALS, because this app has a dark theme and
     the handoff is light-only: a raw #dfe6ff in a rule is a cell that is
     unreadable in dark mode and nothing that says so. The fifth state is
     `--panel` (neither published nor busy) and needs no token of its own.

     THE HATCH IS A GRADIENT, and it is the only non-flat fill in this sheet.
     That is deliberate: `busy-in` means "you published this hour and you are NOT
     free in it", which is neither of its neighbours and must not read as a shade
     of either. A colourblind reader gets the texture; a flat third blue would
     have given them nothing. */
  /* TWO HAIRLINES THE HANDOFF NAMES AND THIS SHEET HAD NO TOKEN FOR. Declared
     rather than reached for through a `var(--x, var(--line))` fallback chain,
     which renders the fallback forever and reads as if the token existed
     (check_tokens 8.11 is the arm that catches exactly that). */
  --line-input-empty: #c9ccd3;  /* a select with nothing chosen -- drawn unfinished */
  --ok-edge: #cfe3d6;           /* the success panel's hairline */
  --cal-offered: #dfe6ff;   /* published, free, still ahead -> OFFERED */
  /* `--cal-past`, `--cal-busy-out` and the `--cal-busy-in` HATCH went with
     the busy layer on 2026-09-04. `--cal-offered` STAYS: it is the published
     fill, and the only one the pattern grid draws. Both themes lose all
     three -- `check_tokens` asserts the two sheets stay in lock-step, so a
     token dropped from one only is a half-fix. */
  --cal-gutter: 56px;     /* the time-label column */
  --shadow-1: none;                                        /* flat: no lift on static surfaces */
  --shadow-2: 0 8px 24px rgba(20,22,26,.10);              /* menus, dialogs, drawer only */
  /* legacy aliases (kept for compatibility — do not remove) */
  --radius: 10px;
  --shadow: none;   /* static cards are flat */
}

/* ── Dark theme ──────────────────────────────────────────────────────────
   Personal, per-device preference set from Settings → Language & Appearance
   (localStorage 'tl-theme'; the <html data-theme="dark"> attribute is applied
   by a boot script in base.html before CSS to avoid a flash). Only remaps the
   color tokens — every surface is token-driven, so the whole authenticated
   workspace follows. Brand --accent is kept unchanged so primary buttons keep
   white text at AA contrast. Candidate/auth pages don't run the boot script
   and stay light by design. */
:root[data-theme="dark"] {
  /* Softened 2026-07: lifted off near-black to a Claude-style soft dark grey.
     Lower-contrast, greyer canvas (was #0f1115); off-white ink and hairline
     borders keep AA. Hue stays the system's cool-neutral, just lighter. */
  --bg: #202124;          /* soft dark grey canvas (was #0f1115) */
  --panel: #26282c;       /* surface — a hair lighter for subtle elevation */
  --field: #24262a;       /* input surface — a touch below the panel */
  --ink: #e6e7ea;         /* soft off-white text (not pure white) */
  --muted: #a6aab2;       /* sub text — softened, still AA on canvas */
  --faint: #8a8e97;       /* eyebrows/hints */
  --line: #34373d;        /* low-contrast hairline on the lighter canvas */
  --line-soft: #2c2f34;
  --accent: #7f97f5;      /* accent stays readable on the lighter canvas */
  --accent-d: #9db0f8;    /* hover */
  --accent-tint: #262c3d; /* soft accent tint bg */
  --accent-edge: #3c4763; /* hairline for --accent-tint at small sizes */
  --ring: #7f97f5;
  --ok:  #35c07a;  --ok-tint:  #14271c;  --ok-ink:  #86dcae;
  /* The dark half of the second-best band. Same direction as --bad's note
     below: the light theme's instinct is "one step darker", which on this
     canvas makes it worse -- the ink LIGHTENS and the tint darkens. 8.9:1
     ink-on-tint, and still clearly separable from --ok-ink's green. */
  --ok2: #2fb3c4;  --ok2-tint: #10262b;  --ok2-ink: #7fd3e0;
  --warn:#d99a3a;  --warn-tint:#2e2412;  --warn-ink:#f0c084;
  /* --bad was #ef5a5a, measured live at 4.42:1 on --panel — a marginal AA
     fail on a DELETE affordance (UI review, live, #11). Lightened one step:
     5.46:1 on --bg, 5.01:1 on --panel, 4.74:1 on --hover. Note the direction:
     the review said "one step darker", which is the light-theme instinct and
     would have made it WORSE here. On --bad FILLS the foreground inverts to
     near-black in the accent-fill block above — white on either red fails. */
  --bad: #f36b6b;  --bad-tint: #2f1618;  --bad-ink: #f6a5a5;
  --neutral: #a6aab2; --neutral-tint: #2c2f34;
  /* No dark stage ramp either -- the handoff's #3d4250 / #5a6ba8 were declared
     here and are removed with their light counterparts. See the light block. */
  --danger-edge: #4f2f32;
  /* The A3 cell fills, dark. SAME ROLES, RE-DERIVED rather than lightened:
     the light set is four near-whites separated by 6-10 points of luminance,
     and darkening those by a fixed amount collapses them into one another. The
     offered blue keeps the accent hue at panel-relative lightness; the two
     greys sit either side of --canvas; the hatch keeps its 5px stripe so the
     texture -- which is the only thing distinguishing it for a colourblind
     reader -- survives the theme. */
  --line-input-empty: #4a4d55;
  --ok-edge: #2f4a3a;
  --cal-offered: #2f3a63;
  --canvas: #26282c;      /* grey table-header row / insets — lifts above bg */
  --hover: #2a2c31;       /* row / ghost hover on dark */
  --shadow-1: none;
  --shadow-2: 0 12px 32px rgba(0,0,0,.45), 0 4px 10px rgba(0,0,0,.32);
}

/* ── Dark theme: FOREGROUND ON ACCENT FILLS ──────────────────────────────
   UI review (live, 2026-08-01) finding #1 / UI #12. The dark theme lightens
   --accent to #7f97f5 so that accent-AS-TEXT reads on the #202124 canvas,
   and that half works: #7f97f5 on #202124 is 5.86:1. What nobody re-checked
   is white-ON-accent, which the light theme relied on and the dark theme
   breaks:

       light  #ffffff on #3b5bdb = 5.67:1   (passes AA)
       dark   #ffffff on #7f97f5 = 2.75:1   (FAILS AA, needs 4.5:1)
       dark   #ffffff on #9db0f8 = 2.10:1   (--accent-d, the hover state)

   The measured surface was .skip-link, but .btn.accent is the product-wide
   primary action and inherits the same failure, as does every other white
   -on-accent fill.

   The fix is the inversion .bulk-float already uses further down this file
   (`:root[data-theme="dark"] .bulk-float { background: #e7e8ea; color: #17181c; }`):
   on a light fill, use near-black ink.

       dark   #17181c on #7f97f5 = 6.46:1   (passes AA)
       dark   #17181c on #9db0f8 = 8.45:1   (passes AA)

   --accent itself is deliberately UNCHANGED: it is correct as text and as a
   border on the dark canvas, and darkening it would break that half instead.
   Only the FOREGROUND on accent FILLS moves. Same treatment for --bad fills,
   which failed identically (#ffffff on #ef5a5a = 3.34:1).

   Guard: tests/test_layout_brand.py, marker `G18: accent fill`.

   `.pcard-next` LEFT THIS LIST at UX-52, and it is a DELETION rather than a
   relaxation. It was the pipeline card's accent-filled "computed next action";
   the owner's row design folds that action into the row's `...` menu, where it is
   a menu row distinguished by WEIGHT and carries no fill at all. The selector no
   longer exists in this stylesheet, so keeping it here would assert a contract
   over a rule that is gone -- which is the "guard asserting its own expectation
   instead of the artefact" failure the G18 block itself warns about. Every
   surviving accent fill is still named below; the rule did not move, it went.

   `.nav-badge`, `.nav-badge.accent` and `.nav-badge.bad` LEFT THIS LIST on
   2026-08-07 for the same reason and by the same rule: both sidebar count
   badges were deleted (owner ruling), so `.nav-badge` has no render site and
   its rules are gone from this stylesheet. Same paired arm as `.pcard-next` --
   G18 fails if the selector returns to the stylesheet without returning here. */
:root[data-theme="dark"] .skip-link,
:root[data-theme="dark"] .brand-mark,
:root[data-theme="dark"] .btn.accent,
:root[data-theme="dark"] .btn.accent:hover,
:root[data-theme="dark"] .wiz-step.active .wiz-num,
:root[data-theme="dark"] .wz-chip:has(input:checked),
:root[data-theme="dark"] .wz-draft:has(input:checked) .wz-box,
:root[data-theme="dark"] .askai-launch,
:root[data-theme="dark"] .askai-send,
:root[data-theme="dark"] .settings-toast.error { color: #17181c; }

/* ── Appearance toggle (Settings → Language & Appearance) ───────────────── */
.switch-row { display: flex; align-items: center; justify-content: space-between; gap: var(--s4);
  cursor: pointer; margin: var(--s3) 0 var(--s2); }
/* `padding-inline: 0` IS THE UA-DEFAULT RESET AND IT IS LOAD-BEARING (measured in
   a browser, 2026-08-08). `.switch-copy` is used on both a <span> inside a
   <label class="switch-row"> and a <legend> inside a <fieldset class="set-row
   set-row-wide">, and browsers give <legend> a default `padding-inline: 2px`
   that nothing here reset. So on the role form's step 2 the two fieldset blocks
   (Job location, Qualifying languages) sat 2px right of the two label blocks
   (Work authorization, Job-hopping) -- four controls in one column, two of them
   indented, which is exactly the "a correct rule surviving into a container it
   was never measured in" defect this stylesheet keeps paying for.
   Scoped to the CLASS, not to `legend`, so it cannot reach a fieldset that never
   opted in. Harmless on the <span> arm, which has no default padding. */
.switch-copy { display: flex; flex-direction: column; gap: 2px; padding-inline: 0; }
/* `.switch-copy.is-inline`: TITLE AND SUB ON ONE LINE (owner, 2026-09-09, of the
   job-hopping row: *"it should be one row instead of 2 row"*).

   A SANCTIONED VARIANT, NOT A CALL-SITE NUDGE, on `.set-row-wide`'s own
   precedent -- which is the rule this stylesheet keeps paying for when it is
   ignored. Everything else about the row is unchanged: the same primitive, the
   same padding reset, the same hairline, the same 44px switch beside it. What
   changes is the axis, for a row whose sub IS the setting rather than a gloss
   on it.

   `flex-wrap: wrap` IS NOT OPTIONAL. The rule sentence is composed from
   `job_history_rules.FIXED_HOP_PATTERN` and grows if those thresholds ever
   change, so a narrow pane must be able to break the line rather than push the
   switch off the row -- one line is the intent, not a promise the layout can
   keep at every width.

   `align-items: baseline` so the 12.5px sub sits on the title's baseline rather
   than being centred against it, which is what makes it read as one line. */
.switch-copy.is-inline { flex-direction: row; align-items: baseline;
  gap: var(--s2); flex-wrap: wrap; }
.switch-title { font-weight: 500; color: var(--ink); }
.switch-sub { font-size: 12.5px; color: var(--muted); }
.switch { position: relative; flex: 0 0 auto; display: inline-flex; }
.switch input { position: absolute; opacity: 0; width: 44px; height: 26px; margin: 0; cursor: pointer; }
.switch-track { width: 44px; height: 26px; border-radius: 999px; background: var(--line);
  transition: background .15s ease; }
.switch-track::after { content: ""; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px;
  border-radius: 50%; background: var(--panel); box-shadow: var(--shadow-1); transition: transform .15s ease; }
.switch input:checked + .switch-track { background: var(--accent); }
.switch input:checked + .switch-track::after { transform: translateX(18px); }
.switch input:focus-visible + .switch-track { outline: 2px solid var(--ring); outline-offset: 2px; }

/* ── Claude-Desktop-style settings pane ─────────────────────────────────
   On the /settings page the cards flatten into one continuous page:
   plain sections separated by hairlines, semibold section headings, and
   label-left / control-right rows — scoped to .settings-pane. The page
   renders in the normal content column (padding comes from .content). */
.settings-pane { padding: 0; }
.settings-pane .sec-head { font-size: var(--text-xl); border-bottom: 0; padding-bottom: 0; margin: 0 0 4px; }
/* The UNIT, carried in the heading rather than repeated down a column of helper
   sentences (owner ruling 2026-08-08: "Visibility windows (months)", "Retention
   by data class (months)"). It is part of the heading's sentence, so it is a
   span inside the `h2` and not a second element beside it -- that is what keeps
   it on the same baseline and in the heading's accessible name. */
.sec-head-unit { font-weight: 400; color: var(--muted); }
.settings-pane .panel { background: transparent; border: 0; border-radius: 0; box-shadow: none;
  padding: 22px 0; margin-bottom: 0; border-bottom: 1px solid var(--line-soft); }
.settings-pane .panel:last-of-type { border-bottom: 0; }
/* Settings panels keep their own vertical padding after a sec-head (the settings
   sec-head is a tighter variant); don't let the global .sec-head + .panel reset
   collapse the intended breathing room here. */
.settings-pane .sec-head + .panel { padding-top: 22px; }
/* A SECOND SECTION HEADING IN THE SAME PANE NEEDS CLEARANCE, and until UX-83 no
   pane had one. `.settings-pane .sec-head` zeroes `margin-top` -- correct when the
   heading is the pane's first child, which it always was -- so the AI-interview
   tab's second and third headings sat hard against the preceding panel's bottom
   hairline with 0px above them. MEASURED at 1440px: 20px above the first heading
   (from the global `.sec-head:first-of-type`) and 0px above the other two.

   SCOPED TO THE ADJACENCY rather than counter-ruled per heading, which is CLAUDE.md
   layout rule 5 and the reason a `mt-4` utility was REMOVED from those three
   headings instead: `.settings-pane .sec-head` is two classes and beats `.mt-4`'s
   one, so the utility was applied in the markup and inert in the browser -- the M-4
   dead-class defect exactly. A no-op on every other settings pane, none of which
   puts a panel before a section heading. */
.settings-pane .panel + .sec-head { margin-top: var(--s5); }
.settings-pane .panel > h2 { font-size: var(--text-md); font-weight: 600; margin: 0 0 2px; }
.settings-pane .panel > .meta:first-of-type { margin-top: 2px; }
/* Setting rows: title + description left, control right (mirrors .switch-row).
   The copy column owns the leftover width; controls keep a fixed width so long
   descriptions never crush the label into a one-word-per-line column. */
/* UX-12a / UX-13a (remainder-a, 2026-08-03): ONE LEFT EDGE FOR THE CONTROL
   COLUMN. Phase 17 diagnosed this and did not fix it, and its diagnosis is the
   fix used here:

     `.set-row` was `display: flex; justify-content: space-between`, so each
     control was pushed to the RIGHT edge and its LEFT edge landed wherever its
     own width put it. A 320px input, a 110px `.input-narrow`, a `Test
     connection` button and a two-button logo control therefore started at four
     different x positions down one page. `max-width: 45%` made it worse: the
     same input started somewhere else again at a different window width.

   A GRID WITH A FIXED CONTROL TRACK is the whole fix. The track is one length
   for every row on every settings page, and `justify-self: start` puts every
   control on its left edge -- so a narrow input is narrow WITHIN the track
   instead of sliding right. This is a DIFFERENT container and a different cause
   from the table alignment Phase 17 did ship (`--field-pad-x` + `.tbl-fields`);
   that fix does not generalise here and was not reused.

   NOT PROVEN IN A BROWSER: no suite in this repo executes JavaScript or renders
   CSS. The guard asserts these declarations exist and that no `.set-row` control
   re-introduces a floating width; it cannot measure a pixel. */
:root { --set-ctl-track: 320px; }
.set-row { display: grid; grid-template-columns: minmax(0, 1fr) var(--set-ctl-track);
  align-items: center; gap: var(--s5); padding: var(--s3) 0; }
.set-row > .switch-copy { min-width: 0; max-width: none; }
/* CONTROLS SIT AT THE RIGHT EDGE OF THEIR TRACK (owner, 2026-08-03: "these
   boxes should be same size, and align right"). This does NOT undo the earlier
   one-left-edge fix, and that is why `end` is safe: every full-width control
   already fills the 320px track, so both its edges are fixed either way, and the
   narrow numeric inputs are all the same width -- so aligning their RIGHT edges
   aligns their LEFT edges too. One vertical line, now at the panel's edge
   instead of floating mid-row with dead space to its right. */
.set-ctl { justify-self: end; }
input.set-ctl, select.set-ctl { width: 100%; max-width: none; }
input.set-ctl.input-narrow, select.set-ctl.input-narrow { width: 110px; min-width: 90px; }
/* `.input-mid` NEEDS THE SAME PAIR, for the same reason, and did not have it.
   `.input-mid { max-width: 280px }` at :1583 is (0,1,0) and loses to the
   `max-width: none` directly above at (0,1,1), so it contributed NOTHING: five
   controls authored 280px rendered at the full 320px track. `.input-narrow`
   escaped only because the line above exists for it, and the comment at :1585
   documents that hazard for `.input-narrow` while never checking its neighbour.
   Owner ruling 2026-08-03 was "these boxes should be same size, and align
   right" -- the right edges did align; the sizes were two and rendered as one. */
input.set-ctl.input-mid, select.set-ctl.input-mid { width: 280px; max-width: 100%; }
/* A MULTI-SELECT IN THE CONTROL TRACK (UX-57, owner 2026-08-06: job location and
   qualifying languages become multi-select dropdowns). A `<select multiple>` is
   the one control whose UA height depends on HOW MANY OPTIONS IT HOLDS, so the two
   rows on `_role_eligibility.html` -- six jurisdictions and three languages --
   would otherwise be two different-height boxes in one column, which is the
   per-call-site drift CLAUDE.md layout rule 2 is about. ONE height here rather
   than a `size` attribute per control: `size` puts the decision at the call site
   and would give the same answer twice only by coincidence.
   The longer list scrolls, which is what a list control does. `height`, not
   `min-height`, so a third row with a longer vocabulary cannot silently grow the
   column. */
select.set-ctl[multiple] { height: 96px; padding-top: 4px; padding-bottom: 4px; }
/* A Tier 1 credential row is field + explicit Save button (Phase 22's commit
   step). Both must live in ONE grid cell or the button would wrap to a second
   grid row under the label. The wrapper carries the track; the field owns the
   leftover width inside it, so the field's left edge is still the shared one. */
/* A `.set-ctl` whose members are LABELLED CONTROLS laid out as ONE ROW (UX-77,
   owner: "the match bands box numbers should be one row, instead of 3 rows").

   WHY A MODIFIER AND NOT A FIX TO `.set-ctl`. The markup was already the
   sanctioned `set-row set-row-wide` + single `.set-ctl` shape and STILL rendered
   three rows, because `style.css`'s global `label { display: block; margin: 14px
   0 6px }` (the stacked-form rule) applies to every `<label>` inside it. Each
   label became a full-width block, so they stacked -- and the third box drifted
   19px right of the first two, because a block label puts its input after its own
   text and "Possible match" is longer than "Strong match". Measured in a browser
   at 1440px, not inferred: the panel is not in tests/test_ui_alignment.py's
   SURFACES and nothing else reports it.

   THIS IS THE SAME DISEASE AS THE 2026-08-03 BATCH -- a stacked-form `label`
   rule reaching inside a row container it was never measured in -- and the fix
   follows `.chip-check`, which solves it the same way: an inline label needs an
   explicit display, it does not inherit one.

   SCOPED TO A MODIFIER because `.set-row-wide > .set-ctl` has nine other call
   sites (the chip rows in talent.html and bank_screen.html, and the role form's
   location/language SET controls). Turning the SHARED selector into a flex row
   would re-space every one of them, which is a regression hunt this change does
   not need to start.

   All spacing is token-based, so `offscale_spacing` does not move; `display:
   flex` + `gap` is not one of the three counted drift primitives
   (`space-between`, `margin-left: auto`, `margin: ... auto`). */
.set-ctl-inline { display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--s3) var(--s5); }
/* `white-space: nowrap` KEEPS EACH LABEL ON ONE LINE, and it is not decoration.
   Without it the first render of this row broke every label across two lines
   ("Strong / match =>"), because a flex item is sized to its content and the text
   wrapped inside it -- one row of controls, three two-line labels. Measured in a
   browser at 1440px, which is the only reason it was seen.
   THE CONTAINER'S `flex-wrap: wrap` IS WHAT MAKES THIS SAFE at narrow widths: the
   whole label+input pair moves to the next line rather than the text breaking
   mid-label, which is the correct behaviour at the 940px floor. */
.set-ctl-inline > label { display: inline-flex; align-items: center;
  gap: var(--s2); margin: 0; white-space: nowrap; }
.set-ctl-commit { display: flex; align-items: center; gap: 8px; }
.set-ctl-commit > input { flex: 1 1 auto; min-width: 0; width: auto; }
.set-ctl-commit > .btn { flex: 0 0 auto; }
/* AN ESCAPE FOR ROWS WHOSE CONTROL IS A SET, NOT A CONTROL.
   The 320px track above was measured for a settings row holding ONE widget, and
   that is what every settings page has. The talent-bank filter is the same
   markup with a different content shape: `bank_screen.html` puts 12 domain
   pills, 6 seniority pills, one pill per work-authorization jurisdiction and
   ONE PILL PER ROLE IN THE WORKSPACE -- unbounded -- into that 320px cell.
   Three things then went wrong at once, all downstream of the track: the pills
   wrapped to a 6-to-8-line stack inside 320px, `justify-self: end` made the
   ragged edge ragged on the LEFT where the eye does not expect it, and
   `align-items: center` centred a one-line label against that stack.
   So the label goes ABOVE the set and the set gets the full row. This is a
   variant rather than a new component because everything else about the row --
   the padding, the hairline separator it inherits from `.settings-pane`, the
   prose measure on its `.switch-sub` -- is still exactly right. */
.set-row.set-row-wide { grid-template-columns: minmax(0, 1fr); gap: var(--s2); }
.set-row.set-row-wide > .set-ctl { justify-self: stretch; }
/* ---- `.set-2up`: two label/control rows side by side ----------------------
   Owner ruling 2026-08-08 (the settings redesign). Pairs of related rows share
   ONE line instead of taking a full-width row each -- the two retention
   visibility windows, the five data classes, the eight role-family weights. On
   a pane this wide a stack of one-control rows puts each control ~1300px from
   its own label, which is the same reading failure UX-77 fixed for the match
   bands: correctly aligned, and too far apart to read as the SET they are.

   IT IS A GRID *OF* `.set-row`s, NEVER A REPLACEMENT FOR ONE. Each cell is
   still exactly one label and one control on the shared primitive, so the
   label/control contract is untouched and only the column count moves. A
   hand-rolled pair in here is what layout rule 1 forbids, and the `.set-row`
   ratchet (check_tokens.py 8.13) counts the call sites that prove it.

   `--set-ctl-track` IS OVERRIDDEN, exactly as `.wt-grid` overrides it 190 lines
   below and for the same measured reason: 320px is the track for a row that
   spans the pane, a column of this grid is roughly half that, so the shared
   track would eat the column and leave the label nothing. The control it holds
   is `.input-narrow` (110px), so 112px is the honest track width. That is rule
   5 -- scope the rule, do not counter it -- and the variable exists to be
   re-measured per container.

   EXACTLY TWO COLUMNS, WHICH IS WHAT THE HANDOFF SAYS, and the `auto-fit`
   version this replaces was a real defect found on the deployed dev server at
   1920px (2026-08-09). `repeat(auto-fit, minmax(min(360px,100%), 1fr))` fits as
   many 360px columns as the container affords -- so on a 1520px pane it laid
   out THREE, and the retention windows' two controls ended up 776px apart with
   an empty third track beside them. Measured: `744px 744px 0px` for the pair,
   `485 485 485` for the five data classes.

   THE SUITE COULD NOT SEE IT. tests/test_ui_alignment.py measures ONE viewport
   (1440px), where the pane is ~1050px and `auto-fit` happens to land on two --
   the right answer for the wrong reason. That blind spot is named in CLAUDE.md
   and this is the first defect to walk through it, so the wide arm added with
   this fix is the more valuable half of the change.

   THE COLLAPSE IS A MEDIA QUERY, not a floor. `auto-fit` picks the count from
   the container and cannot be told "two, or one when cramped"; a query can, and
   it states the breakpoint instead of leaving it to be derived from a track
   width. 820px is the same breakpoint `.grid2` and `.wt-editor` already use, so
   the whole redesign reflows at one width rather than three.

   THE COLUMN GAP IS 32px AND THE HANDOFF SAID 40px. 40 is off the 4px spacing
   scale (0/4/8/12/16/24/32/48/64) that check_tokens.py 8.13 ratchets, and that
   ratchet may only FALL -- it had zero slack when this work started. Snapped to
   the nearest scale value rather than spent, which is the same trade the
   background-work lane recorded making against this same handoff's metrics.

   NO ROW GAP, deliberately: the rows keep `.set-row`'s own `padding: var(--s3)
   0` and the `.settings-pane .set-row + .set-row` hairline, so vertical rhythm
   inside a column is the pane's, not this grid's. A row-gap here would put a
   second, different rhythm inside one page. */
/* ---- Roles & permissions: the members toolbar and pager -----------------
   Owner ruling 2026-08-08. Search, role filter and the two actions share ONE
   row; the pager's count and its buttons share another.

   THE SPACER IS A REAL TRACK, not `margin-left: auto` on the thing being
   pushed. That is layout rule 2 and it is what 8.13's drift counter watches
   for: an auto margin aligns the pushed item to its OWN content width, so two
   rows with different content disagree about where the right group starts. One
   flexible child in the middle gives both rows the same right edge. */
.members-toolbar, .members-foot {
  display: flex; align-items: center; gap: var(--s2);
  flex-wrap: wrap; padding: var(--s3) 0; }
.members-toolbar-spacer { flex: 1 1 auto; }
.members-foot { border-top: 1px solid var(--line-soft); }
/* ---- Project access: one row per PROJECT --------------------------------
   The editor opens in the row BELOW the project it belongs to, so the two are
   read as one thing. `.access-editor-row`'s cell drops the table's own
   left/right padding to nothing extra and carries the form at the row's full
   width; the wash marks it as belonging to the row above rather than being a
   sibling of it. */
/* ---- Project access (Settings -> Roles & permissions) --------------------
   Owner handoff 2026-09-07. `pa-` prefixed on the sheet's own convention --
   see its note about `iv-q` colliding across three templates.

   TOKENS, NOT THE HANDOFF'S PIXELS. Its own Fidelity section says every value
   is a token or on the 4px scale, and then specifies `padding: 18px 20px` and
   `margin-top: 3px`, which are neither. `check_tokens` 8.13 ratchets off-scale
   spacing with ZERO slack, so those round to the scale: `--s4`/`--s5` for the
   panel and `--s1` for the nudge. The intent -- a roomy condition panel, a
   hairline gap under the sub-line -- survives the rounding.

   AND NO INLINE STYLES AND NO HARD-CODED HEXES. The mock uses both because of
   how it was authored and hard-codes the DARK theme's resolved values so it can
   be viewed standalone; the handoff says twice not to copy either. Using the
   tokens is what makes both themes work without a second rule. */
/* A TWO-TRACK GRID AND NOT `space-between`, which the handoff asks for as a
   flex row. `check_tokens` 8.13 ratchets the three drift primitives and it may
   only FALL -- and the rule behind it is the one this repo has paid for four
   times in a day: `space-between` aligns each row to its OWN content width, so
   two of these panels would pin their action to two different edges. The track
   is the fix `.set-row` already uses for exactly this shape. */
.pa-cond { display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: center; gap: var(--s4);
  background: var(--canvas); border: 1px solid var(--line);
  border-radius: var(--r-lg); padding: var(--s4) var(--s5);
  margin-bottom: var(--s4); }
@media (max-width: 700px) { .pa-cond { grid-template-columns: minmax(0, 1fr); } }
.pa-cond-t { min-width: 0; }
.pa-cond-h { font-size: 15px; font-weight: 600; letter-spacing: -.01em; }
.pa-cond-b { margin-top: var(--s1); font-size: 13px; line-height: 1.55;
  color: var(--muted); max-width: 620px; }
/* `.pa-attn` / `.pa-attn-s` (the attention line) and `.pa-owner` (the owner
   inside the merged Access cell) ARE DELETED -- owner ruling 2026-09-07,
   retiring the project OWNER construct. Both attention groups were owner
   states, so the line had nothing left to count; see views.py for the argument.

   `.pa-cond` ABOVE SURVIVES, and the distinction matters: that banner is about
   MEMBERSHIP -- every active account being an admin makes every grant inert --
   which is untouched by ownership going. */
/* THE ACCESS CELL'S HEADER CARRIES THE RULE (the copy hoist, owner ruling
   2026-09-07). This is the line the cells' sentences were hoisted into, so a
   short chip is honest down the column.

   4px, NOT THE HANDOFF'S 3px: 3 is not on the token ladder
   ({0,4,8,12,16,24,32,48,64}) and 8.13's off-scale counter has zero slack. The
   `max-width` is a prose measure and not spacing, so the counter ignores it. */
.pa-head-sub { display: block; margin-top: var(--s1); max-width: 460px;
  font-size: 11.5px; font-weight: 400; line-height: 1.45; color: var(--faint); }
/* `.pa-access` AND `.pa-line` ARE DELETED with the two sentences they carried
   (the copy hoist, owner ruling 2026-09-07). They were a two-row grid, and the
   reason is worth keeping because it names a defect class rather than a bug:
   the cell's two STATEMENTS were separated by nothing but the table column
   boundary until the columns merged, and inline in one flow they ran together
   as a single garbled claim (measured at 1440px; the guard reproduced it by
   forcing both display rules back to inline).

   CHIPS CANNOT DO THAT, which is why nothing replaces these rules. Each chip is
   a box with its own background and padding, so two adjacent chips read as two
   things by construction, and `.chip-row` -- the shared primitive -- already
   wraps. The lesson survives for the next cell that carries prose. */
/* `.pa-tag` IS DELETED with the owner it labelled (owner ruling 2026-09-07).
   It existed because merging the columns deleted the `Owner` header, leaving a
   configured row showing the same name twice with nothing saying which was the
   owner. No owner, no tag. */

.access-table td, .access-table th { vertical-align: middle; }
/* THE HEADER ROW IS TOP-ALIGNED, and only the header. The `Access` cell carries
   the hoisted rule as a second line, so it is taller than its neighbours -- and
   `middle` centred the one-line labels against it, putting `Project` and
   `Members` 11px BELOW `Access`. Measured at 1440px, not guessed.

   TOP, NOT THE HANDOFF'S `bottom`. Bottom alignment shares a baseline too, but
   it shares the WRONG one: it would level `Project` and `Members` with the end
   of the sub-line rather than with the label the sub-line belongs to. Top puts
   all three labels on one line and lets the sentence hang beneath its own. */
.access-table thead th { vertical-align: top; }
.access-editor-row > td { background: var(--canvas); padding: var(--s3); }
/* THE OPEN EDITOR IS ATTACHED TO ITS ROW (owner handoff 2026-09-07). Its cell
   spans the whole table, so without an edge it read as a panel that had appeared
   below a nine-row list rather than as this project's editor. `--accent` is the
   same colour the active tab and the focus ring use, which is what makes it read
   as "here" rather than as a state.

   24px, NOT THE 20px THE HANDOFF ASKED FOR: 20 is not on the token ladder
   ({0,4,8,12,16,24,32,48,64}) and 8.13's off-scale counter has zero slack. The
   4px tail is the handoff's own -- the last form carries its own bottom margin,
   so a full pad below it would double. */
.pa-editor { border-left: 2px solid var(--accent);
  padding: var(--s5) var(--s5) var(--s1); }
/* A hairline BETWEEN forms, because two forms here are two separate decisions
   with two separate Saves and nothing else would say so. On the LAST form's top
   rather than the first's bottom, so a future third form inherits the rule.

   IT DRAWS NOTHING TODAY and is kept deliberately: the owner form is deleted
   (owner ruling 2026-09-07) so the editor holds ONE form, and `form + form`
   matches nothing. The rule is about a SECOND form rather than about either of
   the two that used to be here, so it is the next form's rule and not debt. */
.pa-editor > form + form { border-top: 1px solid var(--line);
  padding-top: var(--s4); margin-top: var(--s4); }
/* `.pa-ed-h` AND `.pa-ed-sel` ARE DELETED with the owner picker they styled
   (owner ruling 2026-09-07). The first made the question `Who records the
   interview outcome` the select's own `<label for>` at heading weight -- one
   element rather than a heading plus a label, so the control kept an accessible
   name. The second held the select to a 220px floor so the widest name in the
   roster did not decide its width. Both had exactly one call site and it is
   gone; the surviving Access section heads itself with `.switch-title`.

   THE ACCESSIBILITY LESSON OUTLIVES THEM: a heading that REPLACES a field label
   leaves the control unnamed. Any future control here needs its question to be
   the label, not a sibling of one. */
/* THIS RULE'S SUBJECT IS GONE AND THE RULE IS KEPT, which is a deliberate
   choice rather than an oversight. It gave the OWNER hint its own line and a
   620px prose measure, because `.member-inline` is a plain block -- it has no
   flex rule -- so a long hint was an inline span continuing the line after the
   Save button and wrapping mid-phrase across the editor's full 740px. Measured,
   not guessed.

   The owner form is deleted (owner ruling 2026-09-07) and `.member-inline` has
   no caller left inside `.pa-editor`, so this selector matches nothing today.
   It stays because the HAZARD is a property of `.member-inline` rather than of
   that one hint, and the next hint placed in this editor would hit it again.
   Deleting it is a decision, not a tidy-up. */
.pa-editor .member-inline .hint { display: block; max-width: 620px;
  margin-top: var(--s2); }
.set-2up { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--s6); --set-ctl-track: 112px; }
@media (max-width: 820px){ .set-2up { grid-template-columns: minmax(0, 1fr); } }
/* THE INHERITED HAIRLINE IS ALWAYS CANCELLED IN HERE, and that is not a style
   choice. `.settings-pane .set-row + .set-row` draws a rule between CONSECUTIVE
   rows, and inside a two-column grid "consecutive" is DOM order rather than
   visual order: cell 2 is the top of the RIGHT column, so it would draw a rule
   above itself that lines up with nothing on the page. Cancelled for every
   `.set-2up`, then reinstated as a bottom rule by the ruled variant below.
   Scoped to the adjacency rather than counter-ruled per call site (rule 5). */
.settings-pane .set-2up > .set-row + .set-row { border-top: 0; }
/* `.set-2up-ruled` -- the variant whose COLUMNS ARE LISTS. Two shapes occur in
   the settings redesign and they are genuinely different, so they are two
   spellings of one primitive rather than one spelling nudged per call site (the
   `.set-row` / `.set-row-wide` precedent):

     .set-2up        a PAIR. One row per column, so a hairline under each would
                     sit ~24px above the block's own bottom rule and read as a
                     mistake. The retention visibility windows; the company
                     name / logo pair.
     .set-2up-ruled  a LIST flowed into two columns -- the five data classes,
                     the eight role-family weights. Every cell needs a rule
                     under it, INCLUDING the last of each column, because the
                     eye reads down a column and the rule is what separates one
                     entry from the next.

   Why this cannot be `:last-child`: with five cells in two columns, cells 4 and
   5 are both bottom-of-column and only cell 5 is `:last-child`, so any
   automatic rule would drop one of the two and keep the other. The distinction
   is about what the block IS, which the author knows and the selector cannot. */
.settings-pane .set-2up-ruled > .set-row { border-bottom: 1px solid var(--line-soft); }
/* UXA 2.9 (UX audit, owner 2026-08-05): a `.set-row` that is a real FIELD GROUP
   may be a `<fieldset>` instead of a `<div>`. This resets the three UA styles that
   would otherwise fight the grid and NOTHING ELSE -- no padding, because
   `.set-row` already supplies `padding: var(--s3) 0` and setting it here would
   race the row for it depending on sheet order.

   MEASURED both shapes under Playwright before shipping, `<div>` against
   `<fieldset>` at 1440px: identical row box, identical `.set-ctl` x and width,
   identical `.switch-copy` x and width, identical height, `display: grid`
   resolved on both. The group's NAME comes from an `sr-only` `<legend>`, which is
   `position: absolute` and so is not the "rendered legend" -- it takes no space
   and does not become a grid item. The visible `.switch-title` is unchanged, so
   the group is named for a screen reader without a second visible label.
   `min-inline-size: 0` is the one that matters: the UA gives a fieldset
   `min-content`, which would stop `minmax(0, 1fr)` from shrinking. */
.set-row.filter-group { border: 0; margin: 0; min-inline-size: 0; }
/* The required marker. `<abbr>` with a title rather than a bare `*`, because
   this form has no legend explaining the glyph -- so the asterisk has to carry
   its own meaning. Not `aria-hidden`: with no legend, a screen reader announcing
   nothing is exactly the gap being closed. `text-decoration: none` because UA
   default styling of `<abbr[title]>` is a dotted underline that reads as a typo
   on a single character. */
.req-mark { color: var(--bad); text-decoration: none; margin-left: 2px;
  cursor: help; font-weight: 600; }
@media (max-width: 760px) {
  .set-row { grid-template-columns: minmax(0, 1fr); }
}
.settings-pane .switch-row { margin: 0; padding: var(--s3) 0; }
.settings-pane .switch-row + .switch-row, .settings-pane .set-row + .set-row,
.settings-pane .switch-row + .set-row, .settings-pane .set-row + .switch-row {
  border-top: 1px solid var(--line-soft);
}
.switch-copy { max-width: 560px; }
.switch-sub { line-height: 1.45; }
/* ---- The grouped-cap-vs-flex pattern (owner comment 15, 2026-08-01) --------
   A grouped `max-width` listed LATER in this file beats an earlier
   `max-width: none` at equal specificity, so a flex child that was given
   `flex: 1 1 auto; min-width: 0; max-width: none` to absorb leftover width can
   be silently re-capped by a prose rule it was never meant to be in. Two
   symptoms, both reported by the owner:
     * the capped child stops at the cap and the row looks half-empty;
     * a SIBLING with a large min-content (a select with `min-width: 160px`, an
       input with a fixed `width`, a nowrap title) refuses to shrink, so the
       child WITHOUT `min-width: 0` collapses to one word per line instead.
   Before adding any selector to a grouped `max-width` group below, check it is
   not a flex child that owns the leftover width.

   MEASURED, not inferred (2026-08-01, computed values off the rendered pages
   rather than read off this file). Exactly ONE of the five reported symptoms
   is this pattern:
     * owner comment 3 -- SAME ROOT CAUSE. `.set-row > .switch-copy` declares
       `max-width: none` above and computed `622.027px` on /settings, because
       the group below re-caps it at `68ch`. This is the bug, and it is the
       only instance of it.
     * owner comment 14 -- a DIFFERENT member of the same FAMILY, and no cap is
       involved. `.v-bannertext` is `flex: 1 1 0%; min-width: 0`, so it yields
       width to a sibling that will not shrink; it is missing a FLOOR, not
       carrying a cap.
     * owner comment 4 -- DIFFERENT. `.row-between` is `flex-wrap: nowrap`, so
       the row cannot stack and both children shrink together. A breakpoint,
       not a cap.
     * CR-1 (talent row menu) -- DIFFERENT. Paint order plus clipping.
     * the settings label rail -- DIFFERENT. A percentage width, not a `ch`
       cap. */
/* ---- Prose measure (X-3 / G-3 / I-4) -------------------------------------
   Descriptions, hints and helper sentences are running prose, and running
   prose stops being readable past ~75 characters a line. On a wide window
   these were measuring ~235 (1528px), because nothing capped them.

   Cap the MEASURE, never the layout: only paragraph-ish selectors appear
   below, so settings rows, their controls, tables and code blocks keep their
   full width. `.set-row > .switch-copy` is listed explicitly and must stay —
   the rule above sets `max-width: none` at the same specificity (0,2,0), so
   a bare `.switch-copy` cap loses to it and the worst offenders (the Settings
   rows) would stay uncapped. Later in the file is what wins here.

   Precedent for the unit: `.consent-script-text` and `.pipe-sum` already
   measure in `ch`. */
/* Owner comment 3 (2026-08-01, traced and live-tested by the owner).
   `max-width` AS A PERCENTAGE OF THE PARENT can never overflow, so modals and
   cards stay contained; `.content` already carries `padding: 28px 40px`, so
   `100%` lands the subtitle flush with the `h1` above it. `120ch` (~1098px) is a
   readability guard for a very wide window and does nothing at laptop widths.

   TWO SELECTORS LEFT THIS GROUP AND MUST NOT COME BACK:
     * `.set-row > .switch-copy` already has `flex: 1 1 auto; min-width: 0;
       max-width: none` a few rules above. This group is LATER in the file and at
       equal specificity, so it was silently overriding that and the intended
       full-width behaviour never took effect. MEASURED on /settings before this
       change: the element's computed `max-width` was `622.027px`, i.e. the 68ch
       below, NOT the `none` its own rule declares. See the pattern note above.
     * `.home-ask-hint` styled an element owner comment 2 deleted from
       dashboard.html. Its other rules survived on module_home.html until
       2026-08-10, when the owner retired the home search whole -- the class now
       has no markup and no rules anywhere. It is still NAMED here because the
       point of this list is that the selector must not rejoin the cap, and a
       reader who ever re-adds the box needs to find that. */
/* `.switch-sub` AND `.hint` LEAVE THE `.settings-pane` SCOPE (2026-08-03 UI
   review). The reasoning three comments up is unconditional -- running prose
   stops being readable past ~75 characters -- but the cap was written when
   `.set-row` lived only on settings pages. It now has 56 call sites across 13
   files, 10 of them NOT settings pages, and `.switch-sub` is used in 13 files of
   which three are inside a `.settings-pane`. The sentences left uncapped were
   the worst possible ones to leave: the long fail-closed guarantees this product
   is careful about, like bank_screen's 190-character "never where they have
   been" paragraph. On bank_screen the page `.sub` was capped (it is unscoped
   below) while every row description under it ran to full column width.
   `.meta` STAYS SCOPED, deliberately: it is the generic metadata class and
   labels table cells, chip rows and inline counts all over the product, where a
   `ch` cap is meaningless or harmful. Only the two running-prose classes move. */
.settings-pane .meta,
.hint,
.switch-sub,
.sub,
.attn-desc { max-width: min(100%, 120ch); }
/* `.home-ask-hint { margin-left: auto; margin-right: auto }` stood here, keeping
   the search hint's box centred under the centred 680px search bar. THE BAR IS
   GONE (owner ruling 2026-08-10 -- the whole home search retired), so the class
   has no markup left. The two `auto` margins are DELETED rather than orphaned,
   which also takes two drift primitives off the layout ratchet. */
/* Instant apply on the settings page — the Save bar is a <noscript> fallback,
   so no CSS hiding is needed. */

/* THE v2 SETTINGS SUMMARY ROWS ARE DELETED (owner ruling 2026-08-08, the
   settings redesign: no disclosure anywhere in the settings pane). Eleven rules
   went -- the `<details>` row, its summary flexbox and marker suppression, the
   `Edit…` trigger in two states, the body's top hairline, the chip nudge, and
   the two-state trigger swap UX-31 added.

   THEY HAD EXACTLY TWO CALLERS AND BOTH ARE FLAT NOW: the AI interview tab's
   four disclosures (`_interview_settings.html`) and the per-family weights
   editor (`_screening_reference.html`, now master-detail). A rule with no
   markup is what the class sweep exists to find, and the same sweep would have
   reported the markup with no rule had this gone the other way.

   WHAT THE DELETION TAKES WITH IT, both in the right direction: ONE drift
   primitive -- the summary's `justify-content: space-between`, the named cause
   of rows aligning to their own content width (layout rule 2) -- and the 12.5px
   off-scale font is not spacing so it counts for nothing either way. The
   replacements spend NOTHING new: the AI interview tab's controls are plain
   `.set-row`/`.switch-row`s the pane already styles, and `.wt-editor`'s own
   right edges come from grid tracks.

   UX-31'S CLAIM IS NOT LOST WITH ITS MECHANISM. "One label at a time" is
   satisfied structurally by master-detail: there is no open/closed state, so
   there is no trigger to hide. Guard: the converted `UX-31 EDITOR / redesign`
   arms in tests/test_settings_surface.py, which assert the absence on both
   sides -- no markup and no rule -- so a half-removal fails rather than rots. */

/* ---- The stage-2 weights editor (templates/_screening_reference.html) ------
   One <details> per role family, OUT of the table it used to sit inside. The
   fields lay out in as many columns as the panel affords rather than in one
   ten-row stack: 10 rows in a 499px table cell rendered 860px tall against a
   1000px viewport, which is the defect this rule closes.

   `--set-ctl-track` IS OVERRIDDEN HERE ON PURPOSE, and it is the whole reason a
   grid works out of the cell and would not have worked inside it. 320px is the
   track measured for the settings column, where a row spans the panel; a column
   of this grid is roughly half that, so the shared track would have eaten it and
   left the label nothing. The control it holds is `.input-narrow` (110px), so
   112px is the honest track width -- the variable exists to be re-measured per
   container, which is exactly rule 5 in CLAUDE.md's layout contract: scope the
   rule, do not counter it.

   360px is the column floor: below it the labels ("Relevant experience & domain
   fit") wrap, so `auto-fit` drops to one column instead of producing a ragged
   two. No `space-between` and no `margin-left: auto` -- the right edge comes
   from `.set-row`'s own track, per grid, which is what tests/test_ui_alignment.py
   measures.

   EXACTLY TWO COLUMNS SINCE 2026-08-09, and the `auto-fit` form this replaces
   was the same defect `.set-2up` had, found on the deployed dev server at
   1920px: the detail panel is 1228px there, so `auto-fit` laid the eight
   weights out in THREE columns of 393px. The handoff draws two, and three makes
   the last row a ragged pair with an empty third track.

   UX-31's REASONING IS UNAFFECTED and is why this is a column-count fix rather
   than a revisit. What UX-31 measured was HEIGHT -- ten `.set-row`s in a 499px
   table cell rendering an 860px row against a 1000px viewport -- and the remedy
   was getting the editor out of the cell so the fields could have real columns.
   Two real columns is what that asked for; `auto-fit` was how it was spelled,
   not what it was for.

   The collapse is a media query at the same 820px `.set-2up` and `.wt-editor`
   use, so the whole redesign reflows at one width. */
.wt-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--s5); --set-ctl-track: 112px; }
@media (max-width: 820px){ .wt-grid { grid-template-columns: minmax(0, 1fr); } }
/* ---- The weights editor is MASTER-DETAIL (owner ruling 2026-08-08) --------
   `.wt-fam` and its `+ .wt-fam` hairline are DELETED with the per-family
   `<details>` they styled. One disclosure per family is fine for two families
   and unusable for the twenty-plus this editor is now specified to hold:
   twenty summaries to scroll past, no way to find one by name, and a page whose
   height is set by how many families a customer happens to have.

   THE LIST SCROLLS INSIDE ITSELF so the page does not. `max-height` on the
   scroller is what makes the twenty-family case the same height as the
   two-family one -- which is the whole point of the shape.

   THE LIST TAKES 40% SINCE UX-95 (owner comment 2026-08-09: "For the left box
   under role-family weights, make that take 40% (now taking only ~20%)"), which
   REVERSES the 2026-08-08 handoff's own `268px`. At the 1520px panel the owner
   measured, 268px is 17.6% -- the ~20% reported. A track is not spacing, so
   8.13 does not count either spelling and no ratchet entry moves.

   `minmax(0, 2fr) minmax(0, 3fr)` RATHER THAN `40% minmax(0,1fr)`: percentages
   are taken before the gap is subtracted, so 40% plus a 24px gap lands nearer
   38% and leans the same way the `.grid2-lead` overhang did. `fr` distributes
   what is left AFTER the gap, which is what 40/60 means.

   `minmax(0, …)` ON BOTH TRACKS, and it is load-bearing in both directions: an
   `auto` detail column takes its max-content from the eight weight labels,
   which would let a long label widen the editor and squeeze the list back to
   where it started; an `fr` track's own default minimum is `auto`, which is the
   same thing spelled differently.

   MEASURED, AND THE MEASUREMENT DID NOT COME OUT CLEAN. Record it here rather
   than in a commit message, because the next person to widen this list needs
   the numbers and not a reassurance. `.wt-grid` splits the detail column into
   two equal columns with a 112px control track and a 24px gap, so each label
   gets `(detail - 24)/2 - 136`. The longest of the eight, "Relevant experience
   & domain fit", needs 227.2px to stay on one line. That makes the editor's own
   width the whole story:

     editor 1520px (viewport 1920)  label track 300.8  -- 0 of 8 wrap
     editor 1290px (viewport 1680)  label track 231.8  -- 0 of 8 wrap
     editor 1050px (viewport 1440)  label track 159.8  -- 5 of 8 wrap
     editor  890px (viewport 1280)  label track 111.8  -- 7 of 8 wrap

   1520px IS THE PANEL THE OWNER MEASURED -- it is the width in the screenshot
   attached to UX-95 -- and at that width 40% is clean. Below about a 1275px
   editor it is not, and 40% is shipped anyway because it is what was asked for
   at the width it was asked at.

   THE 1280px COLUMN IS NOT A REGRESSION THIS INTRODUCED: at a 890px editor the
   268px track ALREADY wrapped 5 of the 8, measured the same session. What this
   change does is take 1440 from 0 wrapped to 5 and 1280 from 5 to 7.

   IF THAT HAS TO BE FIXED, THE FIX IS `.wt-grid`, NOT THIS RULE. UX-31's own
   reasoning applies: two columns are worth having only while they are wide
   enough for the labels, and below that one tall column beats two ragged ones.
   Narrowing the list back is the one thing that does NOT work -- it takes the
   owner's request away and, at 1280, still does not stop the wrap.

   IT COLLAPSES TO ONE COLUMN at the same 820px the other pairs on these pages
   use. Below that the list keeps its own scroller -- it is still a list of
   twenty things -- but takes the full width, and a percentage track makes that
   collapse MORE necessary rather than less: 40% of a narrow viewport is a list
   too narrow to read a family name in. */
/* UX-107 (owner 2026-08-10, option A): `align-items: STRETCH`, and the master
   list becomes a THREE-ROW GRID so the scroller absorbs the slack.

   `align-items: start` stood here, and it is the normal, correct default for a
   master-detail pair -- each column sizes to its own content. It stays invisible
   until the master is a bordered CARD and the detail is BARE, which is exactly
   this surface: the reader is asked to compare a drawn edge against an undrawn
   one, so two columns ending in different places read as a defect rather than as
   two independent columns. Layout rule 5 -- a correct rule surviving into a
   container it was never measured in.

   WHICH COLUMN IS TALLER DEPENDS ON THE DATA, which is why the fix could not be a
   height on either one. MEASURED in the browser at 1440px, BOTH ways:
     * 3 families (the owner's screenshot): list 255px, detail 311px -- the
       DETAIL overhangs by 56px.
     * 8 families: the scroller reaches its 296px cap, list 439px, detail 311px --
       the LIST overhangs by 127px and the mismatch INVERTS.
   A fix measured at one of those looks right and is wrong at the other.

   `grid-template-rows: auto 1fr auto` is what keeps the FOOTER at the bottom of a
   stretched card: search, scroller, footer, with the scroller taking the slack.
   Deliberately NOT `margin-top: auto` on `.wt-list-foot` -- that is one of the
   three drift primitives `check_tokens` 8.13 counts down, and it is layout rule 2
   exactly.

   THE COST, WHICH THE OWNER WAS SHOWN AND CHOSE: where the detail column is
   taller than the scroller may grow, `.wt-list-scroll`'s own cap stops the
   scroller short and the card carries dead space between its last row and its
   footer. At the family counts measured above the slack lands inside the cap and
   there is none.

   `.wt-list-scroll`'s `max-height` IS NOT TOUCHED HERE. It is shared with the
   vocab picker, which UX-112 shortens the same day from the other end; moving the
   shared rule would change that measurement and this one at once, which is why
   UX-112's is scoped to `.vocab-list`. */
/* ONE COLUMN FROM 2026-09-07 (owner: one set of weights). This was a
   master-detail pair, `minmax(0, 2fr) minmax(0, 3fr)` with `align-items:
   stretch`, and the whole comment above is the record of what that pair cost to
   get right -- read it before building another one. The 820px collapse went with
   the second column: one column does not collapse.

   `.wt-list*` RULES ALL STAY AND ARE NOT DEAD. `templates/_vocab_picker.html`
   wears `.wt-list`, `.wt-list-search`, `.wt-list-scroll`, `.wt-list-row`,
   `.wt-list-foot` and `.wt-list-count` -- the list primitive outlived the
   family list, which is why deleting them by proximity would have un-styled the
   vocabulary picker. Verified by reading that template, not inferred. */
.wt-editor { display: grid; grid-template-columns: minmax(0, 1fr); }
.wt-list { display: grid; grid-template-rows: auto 1fr auto;
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--panel); overflow: hidden; }
/* `.wt-list-search` IS DELETED (2026-09-09) with the vocab picker's search box.
   It had ONE renderer -- `_vocab_picker.html` -- and the owner removed it there,
   so these two rules styled a `<div>` nothing emits. The `.wt-list` family it
   belonged to is otherwise untouched: the scroll pane, the rows and the footer
   count are all still drawn. */
/* 296px is the handoff's figure and it is a deliberate ~7.5 rows: a list whose
   last visible row is half-cut is the standard signal that there is more below,
   and a whole number of rows reads as a complete list that happens to end. */
.wt-list-scroll { max-height: 296px; overflow-y: auto; }
.wt-list-row { display: block; padding: var(--s2) var(--s3);
  font-size: var(--text-sm); color: var(--ink); text-decoration: none;
  border-bottom: 1px solid var(--line-soft); }
.wt-list-row:last-child { border-bottom: 0; }
.wt-list-row:hover { background: var(--hover); }
/* The active wash is the SAME `rgba(28,27,24,.06)` the sidebar's active nav row
   uses, not a second selected-state colour. One vocabulary for "this is the one
   you are looking at". */
/* `.wt-list-row.is-active` IS DELETED (2026-09-07) with the family list -- the
   vocab picker's rows are checkboxes and never carried it. Its rule is worth a
   line: the active wash was the SAME `rgba(28,27,24,.06)` the sidebar's active
   nav row uses, not a second selected-state colour, because one vocabulary for
   "this is the one you are looking at" is the point. */
.wt-list-row:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; }
/* THE FOOTER STACKS, and the single row it replaces was a defect found on the
   deployed dev server (2026-08-09): a text input, a `＋ New family` button and
   an `N families` count do not fit across 268px, so the button's label wrapped
   onto three lines and the input was left about eight characters wide.

   The handoff's footer is one row -- a button and a count -- because in the
   design the NAME is not typed there. Two rows is the honest way to keep a
   real input at a real width inside a 268px rail: the field takes the full
   width, and the button sits beside the count beneath it. */
.wt-list-foot { display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--s2); padding: var(--s2);
  border-top: 1px solid var(--line-soft); background: var(--canvas); }
/* `.wt-newfam` (three rules) IS DELETED with `+ New family` (2026-09-07). THE
   DEFECT IT FIXED IS THE REUSABLE PART, found on the deployed dev server on
   2026-08-09: a text input, a button and a count do not fit across a 268px rail,
   so the button's label wrapped onto THREE LINES and the input was left about
   eight characters wide. `white-space: nowrap` on the trigger is what actually
   stopped it -- the button was the flex item being squeezed, and a button whose
   own label breaks is never the right answer to a narrow container. The
   two-row footer was the other half. */
/* `flex: 1 1 auto` on the form above and a plain `flex: 0 0 auto` here, rather
   than `margin-left: auto` on the count -- the row's own tracks push it right.
   That is layout rule 2 and it is what 8.13's drift counter watches for. */
.wt-list-count { flex: 0 0 auto; }
/* `min-width: 0` is what stops the eight weight labels from setting the detail
   column's max-content width and squeezing the 268px list -- the grid track is
   `minmax(0, 1fr)` for the same reason, and both are needed: the track can only
   shrink to the item's own min-content unless the item says otherwise. */
.wt-detail { min-width: 0; }
.wt-detail-head { display: flex; align-items: center; gap: var(--s3);
  flex-wrap: wrap; margin-bottom: var(--s2); }
/* `.wt-detail-head > h3` AND `.wt-detail-actions` ARE DELETED (2026-09-07):
   the head held the family NAME and the `Remove` form, and now holds only the
   total pill. THE RULE THEY DEMONSTRATED STILL BINDS THIS FILE -- the actions
   were pushed right by a flexible SPACER on the heading, never by an auto
   margin on themselves, which is layout rule 2 and one of the three drift
   primitives 8.13 counts down. */

/* ---- Settings steps: a label rail on the left, controls on the right.
   Each step's panels keep the flattened .settings-pane look (hairline rows).

   Owner comment 3 follow-up (2026-08-01), raised separately by the external
   reviewer. The rail was `flex: 0 0 15%; min-width: 120px; max-width: 15%`, and
   a PERCENTAGE rail scales the wrong way: widest on a wide window, where the
   heading already fits, and narrowest on a laptop, where it does not.

   A `ch` FLOOR tracks the text; the percentage stays only as a CEILING so the
   rail can never eat the content column on a very wide window.

   MEASURED, and the floor is derived from the longest rail heading in
   templates/ -- "Band Thresholds and Scorecard" (29 characters,
   templates/_interview_settings.html), not from the "Company branding" the
   reviewer named.
   THAT HEADING IS NO LONGER A RAIL (2026-08-06): the AI interview tab became one
   column of collapsible groups by owner ruling, and the string is now a group
   summary. The floor is left exactly where the measurement put it -- re-deriving
   it from whatever the remaining callers happen to be widest at would be fitting
   a shared track to today's content, which is the drift `minmax(26ch, 24%)`
   replaced. What is recorded here is the number's PROVENANCE, and provenance
   does not expire when its subject moves.
   Sweeping the `.settings-step` container:

     container            1120   960   800
     BEFORE  rail          168   144   120  px
             longest head    2     2     3  lines
     AFTER   rail          269   238   238  px
             longest head    1     1     1  lines

   `22ch` (the plan's starting point) already keeps every heading to two lines
   or fewer, but still lets the longest reach two at an 800px container; `26ch`
   is the first floor that holds every heading in this tree to ONE line at every
   width, and it costs the control column 37px at the narrowest of those.

   The reviewer's "Company branding wraps to four lines at 1440px" did NOT
   reproduce: measured, it is one line at a 1440px window and two at 1280px and
   1024px. The rail is genuinely cramped -- its description paragraph ran to six
   and seven lines -- but the specific number was wrong, and the fix is the same
   either way.

   (The previous comment here said "a 30% label rail" while the rule said 15%.
   Somebody halved the rail and left the prose. Fixed rather than left, because
   a comment that disagrees with its own declaration is how the wrong number
   gets "restored".) ---- */
/* UX-12a, the OTHER half (remainder-a, 2026-08-03). The rail was
   `flex: 0 1 auto`, i.e. a flex BASIS taken from its own content, shrunk in
   proportion to that basis against a `.settings-step-main` whose max-content
   also differs per step. Two steps on one page therefore ended up with two
   DIFFERENT rail widths, which is why the Appearance theme picker started
   further right than the Company-branding fields above it -- the owner's red
   line. It was never the theme cards; it was the rail in front of them.

   A GRID TRACK cannot drift: `minmax(26ch, 24%)` states exactly the intent the
   comment above already documented (a `ch` FLOOR that tracks the longest rail
   heading, a percentage CEILING so the rail never eats the content column on a
   very wide window) and states it as ONE length shared by every step. When 24%
   is narrower than 26ch the min wins, which is the documented behaviour the old
   `max-width: 24%` actually broke at small container widths.

   NOT PROVEN IN A BROWSER: no suite here renders CSS. */
.settings-step { display: grid; grid-template-columns: minmax(26ch, 24%) minmax(0, 1fr);
  align-items: start; gap: var(--s5); margin-top: var(--s5); }
.settings-step + .settings-step { border-top: 1px solid var(--line); padding-top: var(--s5); }
/* The TRACK above owns the rail's width now. Leaving `min-width: 26ch;
   max-width: 24%` on the item as well would re-introduce the drift the track
   removes: a `max-width` narrower than the track leaves a gap the next step
   does not have. */
.settings-step-side { min-width: 0; }
.settings-step-side .eyebrow { margin: 0 0 4px; }
/* Step title: one consistent size across all tabs. The AI Interview partial's
   .settings-pane bumps .sec-head to --text-xl; hold the rail title at --text-md
   to match Personality / Written. */
.settings-step-side .sec-head { font-size: var(--text-md); margin: 0 0 6px; }
.settings-step-side .meta { margin: 0; }
.settings-step-main { flex: 1 1 auto; min-width: 0; }
@media (max-width: 760px) {
  .settings-step { grid-template-columns: minmax(0, 1fr); gap: var(--s2); }
  /* `min-width: 0` is required here now that the rail carries a `ch` floor:
     stacked, the rail is full width anyway, and a 26ch floor (~238px) would
     otherwise put a minimum on a column that no longer needs one. */
  .settings-step-side { flex: 0 0 auto; min-width: 0; max-width: 100%; }
}

/* ---- v2 score-weighting: pillar dots + live normalized stacked bar ---- */
.pillar-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  margin-right: 8px; vertical-align: baseline; }
.pillar-dot.tech { background: var(--pillar-tech); }
.pillar-dot.val  { background: var(--pillar-val); }
.pillar-dot.hr   { background: var(--pillar-hr); }
.pillar-dot.pers { background: var(--pillar-pers); }
.weight-bar { display: flex; height: 8px; border-radius: 999px; overflow: hidden;
  background: var(--line-soft); max-width: 520px; }
.weight-bar .wb { flex: 1 1 0; transition: flex-grow .15s ease; }
.weight-bar .wb.tech { background: var(--pillar-tech); }
.weight-bar .wb.val  { background: var(--pillar-val); }
.weight-bar .wb.hr   { background: var(--pillar-hr); }
.weight-bar .wb.pers { background: var(--pillar-pers); }

/* THE v2 THEME PICKER'S THREE PREVIEW CARDS ARE DELETED (owner ruling
   2026-08-08, the settings redesign). `.theme-picker`, `.theme-card`,
   `.tc-thumb`, `.tc-rail`, `.tc-lines`, `.tc-line`, `.tc-label`, `.tc-half`,
   `.tc-light`, `.tc-dark` and `.tc-system` went together -- 27 rules drawing
   three 112x68px miniature UI thumbnails.
   Theme is a `.segmented` control in a `.set-row` on Settings -> General now.
   That partial was their ONLY render site in the tree, so these are deleted
   rather than left: a rule with no markup is dead weight the class scanner in
   check_tokens.py exists to find, and the same sweep would have reported the
   markup with no rule if this had gone the other way.
   THE `--accent` SELECTED TREATMENT IS NOT LOST -- `.segmented`'s own
   `:checked + label` draws the active segment, which is the primitive doing the
   job these bespoke rules were doing.
   IT TAKES FOUR OFF-SCALE SPACING VALUES WITH IT (8.13's counter, which reads
   margin/padding/gap only): `.theme-picker`'s `gap: 14px`, `.theme-card`'s
   `padding: 10px` and its selected state's `padding: 9px`, and `.tc-line`'s
   `margin-bottom: 6px`. The thumbnail's own 112x68/26px/34px numbers are box
   dimensions and offsets, which that counter does not look at and which must not
   be read as hiding under the lowered figure. The replacement spends NOTHING:
   `.segmented` was already in the sheet, and the three rules that commit added
   (`.grid2-lead`, `.logo-tile`, and the `.btn-text` UX-94 later deleted) were on
   the scale or used tokens. All three are gone now -- see the deleted-cluster
   note below. */
/* right-aligned action cells in the unified worklist */
td.col-right, th.col-right { text-align: right; }
/* THE COMPANY-LOGO CLUSTER IS DELETED (internal-tool cut-down lane L5, owner
   ruling 2026-09-01): `.set-logo`, `.set-ctl.set-logo`, `.set-logo .btn`,
   `.set-logo-spacer`, `.set-logo label.btn`, `.logo-tile`, `.logo-tile img` and
   the `.grid2.grid2-lead` modifier with its stacking query. Verqa is one
   company's internal hiring tool; there is no tenant to white-label for, so
   `/brand/logo`, the upload and the `company_logo` settings key went together
   and every one of these rules lost its only call site. A rule with no caller
   is what the next reader restores the deleted look from -- that is the
   argument `.btn-text`'s own note below makes, applied to its neighbours.

   WHAT THE CLUSTER KNEW, AND WHERE IT STILL APPLIES. Three of its findings were
   about the SHEET rather than about logos and are recorded here so they are not
   re-learned:

   * `justify-content: flex-end` was written for a `.set-ctl` control track and
     survived into a STACKED `.grid2` cell, where it drove the control 367px
     away from its own label (measured at 1920px on the dev server, 2026-08-09).
     That is CLAUDE.md's layout rule 5 verbatim -- a correct rule in a container
     it was never measured in -- and the remedy was to SCOPE it, not counter it.

   * Flexbox breaks lines on each item's HYPOTHETICAL main size, before
     `flex-shrink` applies, so a shrinkable item still wraps. `flex-wrap: nowrap`
     plus a chosen shrink target is the fix; letting things shrink is not.

   * `%` tracks plus a `gap` overflow their container -- `65% + 35%` is the whole
     box and the gap is added on top of it, which hung the right-hand cell 20px
     past every other control on the page. `minmax(0, Nfr)` distributes what is
     left AFTER the gap. `.set-2up`'s note above carries the same lesson.

   `.visually-hidden-input` SURVIVES BELOW AND HAS NO CALL SITE TODAY. It is
   kept deliberately, unlike the rest of this cluster: it is a general
   accessibility primitive (a real input the pointer cannot see, driven by a
   wrapping label) rather than a picture of one deleted control, and it is what
   the next hidden-input control should use instead of inventing an off-screen
   idiom of its own. */
.logo-preview.sm { max-height: 36px; }
/* `.btn-text` IS DELETED (UX-94, owner comment 2026-08-09: "change and remove a
   concrete button aligned on right"), AND THE REVERSAL IS RECORDED RATHER THAN
   THE RULE LEFT BEHIND. It was a button that read as a link, added by the
   2026-08-08 handoff, which drew Change and Remove as text rather than as the
   boxed `.btn.ghost.sm` pair they had been. The owner asked for the boxed pair
   back and the 2026-08-07 override ruling covers it.

   The class had exactly two call sites -- those two triggers -- so it went with
   them. A rule with no caller is what the next reader restores the old look
   from, and this comment exists so that reader does not have to guess whether
   the text style was a decision that got lost or one that was overturned.

   `.link-danger` IS A DIFFERENT COMPONENT and stays: it is the destructive
   text-link the role-family Remove uses in `_screening_reference.html`.

   WHAT THE DELETED RULE KNEW, kept because the NEXT control of this shape will
   meet the same trap even though this one is gone: a trigger that wraps a hidden
   input is a `<label>`, and the global stacked-form rule
   `label { display: block; margin: 14px 0 6px }` turns a bare label into a
   full-width block with 20px of vertical margin inside an inline cluster. The
   neutraliser was `.set-logo label.btn { position: relative; margin: 0; cursor:
   pointer; font-weight: 500; }`, deleted with the rest of the logo cluster at
   cut-down lane L5; write it again, scoped to the new cluster, rather than
   unscoping it. */
/* A real input the pointer cannot see, driven by a wrapping `label.btn` that is
   the whole visible control. Its one call site (the logo picker) is deleted at
   cut-down lane L5 and the PRIMITIVE is kept on purpose: it is an accessibility
   idiom, not a picture of that control, and it is what the next hidden-input
   control should use instead of inventing an off-screen trick of its own. */
.visually-hidden-input { position: absolute; opacity: 0; width: 1px; height: 1px;
  margin: 0; padding: 0; border: 0; clip: rect(0,0,0,0); }

/* Native file inputs: style the picker as a proper secondary button. */
input[type=file] { color: var(--muted); }
input[type=file]::file-selector-button {
  appearance: none; -webkit-appearance: none;
  background: var(--panel); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--r-md);
  padding: 6px 14px; margin-right: 10px; min-height: 32px;
  font: inherit; font-size: var(--text-sm); font-weight: 500; cursor: pointer;
  transition: background .15s ease;
}
input[type=file]::file-selector-button:hover { background: var(--bg); }

/* Browser autofill paints its own white background — repaint it with the
   theme's field tokens so autofilled inputs don't glow white in dark mode. */
input:-webkit-autofill, input:-webkit-autofill:hover,
input:-webkit-autofill:focus, input:-webkit-autofill:active {
  -webkit-box-shadow: 0 0 0 1000px var(--field) inset;
  -webkit-text-fill-color: var(--ink);
  caret-color: var(--ink);
  transition: background-color 99999s ease-out 0s;
}

/* ── Sequenced invitation steps (candidate detail) ─────────────────────── */
.invite-steps { list-style: none; margin: var(--s2) 0 0; padding: 0;
  counter-reset: invite-step; border-left: 2px solid var(--line); }
.invite-steps li { position: relative; display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--s2); padding: var(--s2) 0 var(--s2) var(--s4); margin-left: 6px; }
.invite-steps li::before { counter-increment: invite-step; content: counter(invite-step);
  position: absolute; left: -13px; top: 50%; transform: translateY(-50%);
  width: 22px; height: 22px; border-radius: 50%; display: grid; place-items: center;
  font-size: 11px; font-weight: 600; background: var(--panel); color: var(--muted);
  border: 2px solid var(--line); }
.invite-steps li.is-done::before { content: "✓"; background: var(--ok); color: #fff; border-color: var(--ok); }
.invite-step-t { font-weight: 500; color: var(--ink); }

/* ── Members (Settings → User Rights) ──────────────────────────────────── */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
/* Skip-to-content link (F23): visually hidden until keyboard-focused, then a
   pinned pill above all chrome. */
.skip-link { position: absolute; left: -9999px; top: 0; z-index: 200;
  padding: 8px 14px; border-radius: var(--r-md); background: var(--accent);
  color: #fff; font-size: var(--text-sm); text-decoration: none; }
.skip-link:focus { left: 10px; top: 10px; outline: 2px solid var(--accent-d); outline-offset: 2px; }
#main:focus { outline: none; }
/* FIVE RULES DELETED HERE (owner ruling 2026-08-08, the settings redesign):
   `.members-head`, `.members-head-actions`, `.members-count`,
   `.members-filters` and `.members-filters select`. The roster's head block, its
   separate action cluster and its count line are one toolbar row now, and the
   markup those rules styled is gone.

   TWO OF THEM WERE DRIFT PRIMITIVES -- `justify-content: space-between` on
   `.members-head` and on the OLD `.members-toolbar`, the named cause of a row
   aligning to its own content width (layout rule 2). The replacement at the top
   of this file pushes with a flexible SPACER track instead, which is the
   sanctioned form and is not counted.

   A SECOND `.members-toolbar` RULE STOOD HERE AND IT WAS WINNING. It is the
   same specificity as the redesign's rule ~540 lines above and comes LATER, so
   the cascade took this one: `flex-wrap: nowrap` plus `space-between` and no
   spacer meant the search input absorbed the whole row -- MEASURED at 1600px,
   873px wide, forcing a 71px two-line toolbar. Two rules for one component,
   drifting later, is exactly the defect the layout contract is about; the fix
   is one rule, not a third one to counter this.

   `.members-search` KEEPS ITS WIDTH and moves up with its siblings. It is a
   bounded 220px because a search box that grows to fill a toolbar is what
   pushes everything else off the row.

   IT NEEDS THE CONTAINER, AND WITHOUT IT THE RULE HAS NEVER APPLIED. This is
   the M-4 dead-class defect on a class that has carried it since it was
   written. The global control rule is `input[type="search"], … { width: 100% }`
   -- (0,1,1) -- so a bare `.members-search` at (0,1,0) lost outright, and even
   `input.members-search` only TIES it, which source order then decides in the
   global rule's favour because it is declared later in this file. MEASURED at
   1600px through both attempts: 1200px wide against an authored 220px, which is
   what forced the toolbar onto two lines and hid the older `space-between` bug
   underneath it.

   TWO CLASSES PLUS THE TAG is the weight that actually wins, and it is the
   idiom this sheet already uses for exactly this collision --
   `input.set-ctl.input-narrow` and `input.set-ctl.input-mid` are the same shape
   for the same reason (see their note above). Scoping to the container rather
   than inventing a second class is layout rule 5: scope the rule, do not
   counter it. */
.members-toolbar input.members-search { width: 220px; max-width: 100%; }
.seg-count { display: inline-block; margin-left: 4px; padding: 0 6px; border-radius: 999px;
  font-size: 11px; font-variant-numeric: tabular-nums; background: var(--line-soft); color: var(--faint); }
.segmented a.active .seg-count { background: var(--accent-tint); color: var(--accent-d); }
.members-table { width: 100%; }
.members-table th, .members-table td { vertical-align: middle; }
.member-name { font-weight: 500; color: var(--ink); }
.member-email { font-size: var(--text-xs); color: var(--faint); }
.member-inline { margin: 0; }
.members-table .col-menu { width: 44px; text-align: right; }
/* `.members-empty` IS DELETED with the client-side filter it belonged to. It
   was a hidden block a script un-hid when every rendered row was filtered out;
   the filter is server-side now, so "no members match your filters" is a real
   row in the table body -- rendered by the `{% else %}` of the roster loop,
   which is a state the server knows and a script no longer has to infer. */
/* Phase 28 (UX-14): the members table lost its Project column, so each row
   states the ACCESS OUTCOME under the email instead, and the grants are
   edited once, in #project-access. The rows are a LIST rather than a second
   table: one member per row, identity left, grant chips right, wrapping to
   stacked on narrow screens -- the same shape the roster uses, so the two
   read as one surface. */
.member-access { margin-top: 2px; font-size: var(--text-xs); }
.access-list { list-style: none; margin: 0; padding: 0; }
.access-row { display: flex; flex-wrap: wrap; gap: var(--s2) var(--s4);
  align-items: flex-start; justify-content: space-between;
  padding: var(--s3) 0; border-bottom: 1px solid var(--line-soft); }
.access-row:last-child { border-bottom: 0; }
.access-who { min-width: 180px; }
.access-grants { flex: 1 1 320px; }
/* UX-84 (owner 2026-08-07): `min-inline-size: 0` was MISSING here, and this is
   the third fieldset in the tree rather than one of the two the owner
   photographed. It is a SECOND, partial copy of the reset `.set-row.filter-group`
   already carries -- and the declaration it dropped is the one that sheet's own
   comment calls "the one that matters", because the UA gives a fieldset
   `min-content` and this box is a flex child of `.access-grants { flex: 1 1 320px }`.
   Border and margin were already 0, so there is no white box here; the intrinsic
   minimum was simply never reset. Guard: check_tokens.py 8.18, which derives the
   reset-providing selectors from THIS sheet rather than pinning a class list, so
   a fieldset reset by any spelling passes and an unreset one cannot. */
.access-set { display: flex; flex-wrap: wrap; gap: var(--s2);
  margin: 0; padding: 0; border: 0; min-inline-size: 0; }
/* The checkbox stays a real checkbox -- it is an authorization control, and a
   styled-away input is one an assistive technology cannot report the state
   of. The chip is the label around it. */
/* `.chip-check` IS THIS COMPONENT, built a second time 749 lines below under a
   transposed name (Phase 25's bank-screen filter). The copy dropped the
   `:focus-within` rule whose reason is written directly above, used the
   sheet's only `rem` font-size, and set `background: var(--card)` -- an
   undeclared token with no fallback, which makes the declaration invalid at
   computed-value time and renders the fill `transparent`. One component now,
   with the differences kept where they belong: see :1309. */
.check-chip, .chip-check { display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 10px; border: 1px solid var(--line); border-radius: 999px;
  font-size: var(--text-xs); cursor: pointer; }
.check-chip:focus-within, .chip-check:focus-within { outline: 2px solid var(--accent); outline-offset: 1px; }
/* `.cap-matrix` IS DELETED WITH THE PERMISSIONS TAB (owner, 2026-09-06). It had
   exactly one caller, `templates/settings/_user_rights.html`'s capability table,
   and that tab is retired -- so these rules would have been a stylesheet's worth
   of dead weight describing a table nothing renders. The `min-width` note that
   stood here recorded a re-derivation from the catalogue's longest label rather
   than a measurement, and it goes with the rule it explained. */

/* Row overflow menu (⋮) — reuses the <details.popover> primitive */
.row-menu-btn { display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; border-radius: var(--r-sm); color: var(--muted);
  font-size: 18px; line-height: 1; }
.row-menu-btn:hover { background: var(--line-soft); color: var(--ink); }
details.popover.menu-end > .popover-body { left: auto; right: 0; }
/* -- #25: row menus opened 328px outside their own clip box ----------------
   MEASURED on /library at documentElement.clientWidth 365, first
   details.popover in the table:

     .popover-body                      260 x 121, right edge 659
     clipping ancestor .table-scroll    overflow-x/y auto, right edge 331
     clipping ancestor .panel           overflow-x/y auto, right edge 349
     .popover-body position             absolute -- it cannot escape

   659 against a clip box ending at 331: the menu is not "cut short", it is
   effectively entirely invisible. Row actions were unusable on every
   table-bearing page at phone width. `.panel` is confirmed as the SECOND
   clipping ancestor exactly where predicted -- it computes overflow-x: auto at
   365/758/860 and visible at 861/880/900.

   The cause is the one CR-1 cause 2 already documented near `.v-matchcol`
   below: `overflow-x: auto` computes overflow-y to auto as well (the spec
   forbids one axis visible while the other is not), so any scroll pane is a
   clip on BOTH axes and an absolutely-positioned popover body is trapped in
   it. `.table-scroll` is such a pane at every width; `.panel` becomes one
   under the mobile block.

   THIS IS NOT A NEW MECHANISM. It is the proven CR-1 fix -- `position: fixed`
   plus JavaScript that places the box against its trigger -- widened from
   `.tb-menu`, the one row menu that happened to be reported, to `.menu-end`,
   which is what every row overflow menu in the product is marked with. Read
   the long note above `.v-matchcol` before touching this: it records the two
   candidate fixes that were tested against a rendered page and FAILED, and why
   bare `position: fixed` without the JavaScript does not work (it escapes the
   clip but has no useful static position -- the same menu measured 825,1124 as
   absolute and 904,1180 as bare fixed).

   `.tb-menu`'s own redundant copy of this rule stood further down and was kept
   solely as the anchor for that history. It is DELETED (owner, 2026-08-21) with
   the talent bank's Actions column -- the last `.tb-menu` usage in the product --
   and the note it anchored SURVIVES IN PLACE, above `.v-matchcol`. This rule is
   the reference to it now, which is what stopped the note being stranded.

   The JavaScript half is the `placeRowMenu` block in templates/base.html --
   ONE copy, on the `toggle` listener that already runs, reused rather than
   duplicated so the aria-expanded sync, the Escape path and the outside-click
   dismissal stay the single copy they have always been.

   Guard: tests/test_layout_brand.py, marker `G27:`. */
details.popover.menu-end > .popover-body { position: fixed; top: auto; }
/* -- `top: auto` IS NOT TIDYING; WITHOUT IT THIS RULE OPENS EVERY MENU-END
   POPOVER BELOW THE FOLD (2026-08-27) ---------------------------------------
   The base rule at :2691 sets `top: calc(100% + var(--s1))`, which is correct
   for the ABSOLUTE variant: `100%` there is the `<details>`'s own height, so the
   body lands just under its trigger. Flipping to `position: fixed` re-bases that
   percentage on the VIEWPORT without changing it, so `100%` became 1000px and
   the body opened at `top: 1004px` -- four pixels below the bottom of a 1000px
   window, with no scroll able to reach it because the box is fixed.

   MEASURED on /talent at 1440x1000, inline styles stripped so only the cascade
   is speaking: body y=1004, height 274, the LAST domain checkbox (`Unknown`)
   at y=1226. With `top: auto` a fixed box sits at its static position: y=203,
   last checkbox y=425. The comment at :6953 already SAID this rule places the
   menu "at its static position" -- it did not, until this line.

   WHAT MADE IT INVISIBLE is that `placeRowMenu` (templates/base.html) writes an
   inline `top` on every `toggle`, so the wrong value is overwritten about a
   frame later and nobody ever sees it. It is real in exactly two windows: the
   frame between the <details> opening and that handler running, and any load
   where the script is late, blocked or broken -- in which case all SEVEN
   `.popover.menu-end` users (the four talent-bank facets, Members' rights menu,
   trust audit, trust integrity) open entirely off-screen with no way back.

   IT WAS ALSO A REAL TEST FAILURE, which is how it was found: the TB APPLY
   block's `page.check` on the last domain chip lost that race and retried for
   30s against a box at y=1226, reporting "element is outside of the viewport"
   while every geometry probe taken a beat later showed it correctly placed. A
   flake with a cause.

   NO MARGIN, DELIBERATELY. The 8px trigger-to-menu gap is `placeRowMenu`'s own
   GAP constant; a `margin-top` here would ADD to the inline `top` that handler
   writes and shift every placed menu by that much, including the ones
   `ROW MENU CLIP` measures.

   `right: 0` at :1356 IS THE SAME SHAPE AND IS LEFT ALONE, said here so the next
   reader does not conclude the rule was only half read. It re-bases on the
   viewport too, pinning the pre-JS box to the window's right edge instead of the
   trigger's -- but that is ON SCREEN and `placeRowMenu` replaces it with an
   explicit `left`, so there is no defect to fix and no measurement to justify
   changing it. Guard: `MENU-END FALLBACK` in tests/test_ui_alignment.py. */
/* -- board-menu-clip-scroller, 2026-08-05: THE SAME FIX, SECOND PRIMITIVE ------
   The rule above reads `details.popover`, and there are TWO <details> menu
   primitives in this product. `.pcard-menu` / `.pcard-menu-pop` (:2790) is the
   other, and every row overflow menu on /pool is built from it -- so #25's fix
   never reached the positions board, and `.menu-end` was not even in the page.
   That is the gap CLAUDE.md already recorded in writing: *"every row now carries
   a ... menu the suite never opens."*

   MEASURED on /pool, seeded workspace, 1745x828 (the owner's own window), each
   menu opened one at a time:

     open board   row 3   clipped  58px   .table-scroll scrollHeight 401 -> 459
     open board   row 4   clipped 149px                                401 -> 550
     closed board row 1   clipped 157px                                112 -> 268
     drawer rows (1440x600)  clipped 64px and 117px against `.drawer`

   THE CLIP AND THE SCROLLBAR ARE ONE NUMBER, which is what makes them one bug:
   the pane grew by exactly what it cut off. An absolutely-positioned popover
   EXTENDS its scroll pane's content box, so `overflow-y: auto` -- which
   `overflow-x: auto` forces per spec, the note above -- both grows a scrollbar
   and clips what caused it. The owner reported the scrollbar; the clipped menu is
   the same event.

   NOT `overflow: visible` on `.table-scroll`, and not a height on the board:
   forbidden for the reason the note above gives, and the board's HORIZONTAL
   scroll below its 940px floor is legitimate and has to survive. This escapes the
   pane instead of deleting it. Measured after: 0px clipped, scrollHeight back to
   clientHeight on both boards, and the horizontal pane still absorbing 940px at
   860px wide.

   THE JS HALF IS THE SAME `placeRowMenu`, not a second copy -- two lines in
   templates/base.html admit `details.pcard-menu` to the toggle hook that already
   ran and teach the body lookup this primitive's class name. Bare
   `position: fixed` is NOT the fix and never was: read the note above and the
   longer one over `.v-matchcol`, which record it measured 79px down and 56px
   right of where it belonged.
   Guard: tests/test_ui_alignment.py, marker `ROW MENU CLIP` -- measured in a
   real browser, with a negative self-test, because this file's own history is two
   candidate fixes that looked right in the cascade and failed in the page. */
details.pcard-menu.menu-end > .pcard-menu-pop { position: fixed; }
.popover-body.menu { min-width: 200px; padding: var(--s1); }
.menu-item { display: block; width: 100%; text-align: left; box-sizing: border-box;
  padding: 8px 10px; border: 0; border-radius: var(--r-sm); background: none;
  font: inherit; font-size: var(--text-sm); color: var(--ink); cursor: pointer; text-decoration: none; }
.menu-item:hover { background: var(--line-soft); }
.menu-item.danger { color: var(--bad); }
.menu-item.danger:hover { background: var(--bad-tint); }
.menu form { margin: 0; }
* { box-sizing: border-box; }
html { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
/* Slim, subtle scrollbar everywhere — the one and only scrollbar format across the
   product so nothing shows the chunky OS default. Thumb = hairline token, track
   transparent, radius 999px; both axes get 8px (width for vertical, height for
   horizontal). The nav rail (.sidebar-nav), re-match lists (.v-rolelist) and the
   full-bank table (.v-fbtable) keep their own matching blocks below — identical look. */
html { scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { background: var(--line); border-radius: 999px; }
body {
  margin: 0; display: flex; min-height: 100vh;
  font-family: var(--font-sans);
  font-size: 14.5px; line-height: 1.6;
  background: var(--bg); color: var(--ink);
}
/* Form controls do NOT inherit fonts by default — without this, <button>
   elements render in the browser's UA font while <a class="btn"> links
   inherit Inter, making "identical" buttons differ in face AND size. */
button, input, select, textarea { font-family: inherit; }

/* calm but visible focus (techy-without-noisy §3) */
a:focus-visible, button:focus-visible, summary:focus-visible {
  outline: 2px solid var(--ring); outline-offset: 2px;
}

/* tabular figures for scores, counts, dates, refs */
.tnum { font-variant-numeric: tabular-nums; }
/* UX-77 (owner 2026-08-07): "the number box inside should be align right, not
   left". THE BOXES WERE ALREADY FLUSH RIGHT -- `.set-ctl { justify-self: end }`
   does that -- so what the screenshot showed left-aligned was the DIGITS INSIDE
   them. Tabular figures plus right alignment is what makes a column of numbers
   comparable, so `.tnum` is the natural place for it.

   SCOPED TO `input`, NOT PUT ON `.tnum` ITSELF, and the reason is that `.tnum` is
   shared far beyond number fields: it is on `<td>`s, `<strong>`s and `<span>`s
   across the report, the positions board, the compare table and the retention
   panels, several of which already carry `.ta-r` where right alignment was wanted
   and are deliberately left-aligned where it was not. Widening `.tnum` would have
   silently re-aligned all of them. Every `input.tnum` in the tree is a narrow
   numeric field -- the two match-band cutoffs' siblings, and the years-range pairs
   in `bank_screen.html` and `talent.html` -- and right-aligned digits are correct
   for all of them, which is what makes this ONE shared rule rather than a nudge at
   the call site that reported it. */
input.tnum { text-align: right; }
/* quiet uppercase section labels.
   THE GAP TO WHAT IT LABELS BELONGS HERE, NOT AT THE CALL SITE (UX-120, owner
   2026-08-10: "After 'Summary' there should have some spacing, similar to 'What
   the CV evidences' and its 1st bullet"). This rule set no bottom margin, so
   every container that wanted one had to remember: `.peek-cols .eyebrow` (6px),
   `.peek-sec > .eyebrow:first-child` (8px), `.evi-grp-head` (8px) and `.evi-lbl`
   (4px) each wrote their own, and the CV drawer's `Summary` -- an `.eyebrow` in
   its own `<section>` above the block those rules were written for -- fell
   through every one of them onto a `<p class="pf-prose m-0">` that zeroes its own
   top margin. MEASURED: 0px, against 14px for the heading two inches below it.
   That is layout rule 5 for the fourth time in this batch, and layout rule 1 is
   the fix: one primitive owns the decision, and the four scoped rules above still
   win where a container has a REASON to differ.
   12px, not 14. The 14px the owner is comparing against was `.pf-evi-list`'s own
   `margin-top` and has been retired into this rule, so the two headings he named
   now read from ONE declaration -- 12px is the 4px-scale neighbour, which the
   off-scale ratchet (check_tokens 8.13) requires and which nets that counter one
   lower rather than one higher. The 2px is the price of the two gaps being the
   same number instead of two numbers that look the same.
   ADJOINING MARGINS COLLAPSE, so this is a FLOOR and not an addition: a labelled
   block whose first child already carries a larger top margin keeps it. And every
   call site that ALREADY asked for a different gap still gets it: `.mb-0`, `.m-0`
   and `.mb-2` are the same specificity and are declared later in this sheet, and
   candidate.css's `.ivl-eyebrow` is a later sheet again.
   `var(--s3)` RATHER THAN A TYPED 12px -- both stylesheets define the scale, and
   a shared primitive is the last place a hand-picked number belongs.
   Guard: `UX-120` in tests/test_ui_alignment.py. */
.eyebrow { text-transform: uppercase; letter-spacing: .08em; font-size: 11px;
           font-weight: 600; color: var(--faint); margin-bottom: var(--s3); }

/* ---- v2 sidebar: flat, canvas-colored, borderless ---- */
.sidebar {
  width: 310px; background: var(--panel); color: var(--ink);
  padding: 14px 10px; flex-shrink: 0; border-right: 1px solid var(--line);
  box-sizing: border-box;
  position: sticky; top: 0; align-self: flex-start;
  /* The rail itself never scrolls (scrolling would clip the collapsed-rail
     hover flyouts); the inner .sidebar-nav is the scroll container instead. */
  height: 100vh;
  display: flex; flex-direction: column;
  transition: width .15s ease;
}
/* wordmark: Talent(ink)Lens(blue) + focus dot; company identity lives at foot */
.side-brand { display: flex; align-items: center; justify-content: space-between;
  gap: 8px; padding: 2px 10px 6px; line-height: 1.25; }
/* wordmark + version sit together on the left; the version is a subtle muted
   label baseline-aligned to the wordmark. Hidden on the collapsed rail. */
.side-brand-l { display: inline-flex; align-items: baseline; gap: 6px; min-width: 0; }
/* ---- Verqa wordmark (brand) ----
   "Ver" + "q" + "a" set in Roobert (the brand face), tight tracking. The "q"
   is Fano Blue and carries an inline-SVG checkmark as its tail (the
   verification cue) — the blue "q" + checkmark read as the mark. Roobert is
   scoped to the wordmark only; falls back to --font-sans while the webfont
   loads or if it is unavailable. */
@font-face {
  font-family: 'Roobert';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('fonts/Roobert-SemiBold.woff2') format('woff2'),
       url('fonts/Roobert-SemiBold.woff') format('woff');
}
/* THE ONE SANCTIONED "occupies space but is not painted" CLASS (2026-09-08).
   Added with `check_tokens.py` 8.12's widening to cover `visibility`: the
   wizard's Back button needs to hold its slot on step 1 so the footer's other
   controls do not shift when it appears, which is what `visibility` buys and
   `display: none` does not. One class, in the sheet, rather than a decision
   repeated per call site. */
.is-invisible { visibility: hidden; }
.wordmark { display: inline-flex; align-items: baseline; gap: 0; text-decoration: none;
  font-family: 'Roobert', var(--font-sans);
  font-size: 18px; font-weight: 600; letter-spacing: -.025em; color: var(--ink); }
.wordmark .wm-ver, .wordmark .wm-end { color: var(--ink); }
.wordmark .wm-q { position: relative; color: var(--brand-blue); }
.wordmark .wm-check { position: absolute; left: .34em; bottom: .01em;
  width: .60em; height: .60em; overflow: visible; pointer-events: none; }
.wordmark .wm-check path { fill: none; stroke: var(--brand-blue); stroke-width: 2.6;
  stroke-linecap: round; stroke-linejoin: round; }
.wordmark:hover { color: var(--ink); }
/* Dark-mode pairing (per brand README): on a dark rail the letters and the
   checkmark go white — Fano Blue is not legible on near-black, so the check
   itself becomes the brand signature. */
:root[data-theme="dark"] .wordmark,
:root[data-theme="dark"] .wordmark .wm-ver,
:root[data-theme="dark"] .wordmark .wm-end,
:root[data-theme="dark"] .wordmark .wm-q { color: #ffffff; }
:root[data-theme="dark"] .wordmark .wm-check path { stroke: #ffffff; }
.rail-collapse { display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; flex: 0 0 auto; border: 0; border-radius: 7px;
  background: none; color: var(--faint); cursor: pointer; }
.rail-collapse:hover { background: rgba(28,27,24,.045); color: var(--ink); }
:root[data-theme="dark"] .rail-collapse:hover { background: rgba(236,233,226,.06); }
/* legacy brand block (login page still uses it) */
.brand { display: flex; align-items: center; gap: 10px; padding: 4px 10px 18px; line-height: 1.25; }
.brand-mark { width: 28px; height: 28px; flex-shrink: 0; display: flex; align-items: center;
  justify-content: center; border-radius: 8px; background: var(--accent); color: #fff;
  font-size: 13px; font-weight: 700; }
.brand-text { min-width: 0; }
.brand-name { display: block; font-size: 15px; font-weight: 600;
              letter-spacing: -.01em; color: var(--ink); }
.brand-sub { display: block; font-size: 11px; color: var(--faint); }
.nav-label { padding: 14px 12px 5px; font-size: 10.5px; font-weight: 600;
  letter-spacing: .09em; text-transform: uppercase; color: var(--faint); }
.nav-group { display: flex; flex-direction: column; gap: 1px; padding: 0 2px; }
.nav-item {
  display: flex; align-items: center; gap: 10px; padding: 7px 10px;
  border-radius: 8px; color: var(--muted); text-decoration: none;
  font-size: 13.5px; font-weight: 500; line-height: 1.4; transition: background .15s ease, color .15s ease;
}
.nav-item:hover { background: rgba(28,27,24,.045); color: var(--ink); }
:root[data-theme="dark"] .nav-item:hover { background: rgba(236,233,226,.06); }
/* active: neutral wash + ink text + accent ICON (no blue tint bar) */
.nav-item.active { background: rgba(28,27,24,.06); color: var(--ink); font-weight: 600; }
:root[data-theme="dark"] .nav-item.active { background: rgba(236,233,226,.09); }
.nav-item.active .nav-ico { color: var(--accent); }
.nav-ico { flex-shrink: 0; }
.nav-txt { flex: 1; min-width: 0; }
/* A nav row rendered as a <button> (e.g. the "New CV" create row) — reset the
   UA button chrome so it matches the <a> rows exactly. */
button.nav-item { width: 100%; border: 0; background: none; font-family: inherit;
  font-size: 13.5px; cursor: pointer; text-align: left; }
/* The two "create" quick-rows pinned atop the nav, fenced off from Home + the
   groups by a hairline. */
.nav-create { display: flex; flex-direction: column; gap: 1px;
  padding-bottom: 5px; margin-bottom: 5px; border-bottom: 1px solid var(--line-soft); }
/* `.nav-badge` IS DELETED (owner ruling 2026-08-07). It styled the sidebar count
   pills; both — Pipeline's open-roles count and the Trust Center's overdue-
   retention count — were removed from templates/base.html, so the class had no
   render site left in the product. The retention signal is the admin warnbar in
   that same template, not a pill. Guard: `G25:` in tests/test_layout_brand.py
   asserts the absence, so re-adding a badge re-arms the accessible-name rule
   instead of silently repeating the "Pipeline2" screen-reader defect. */

/* ---- Vanta-style collapsible nav groups (Home + Hiring) ---- */
.sidebar-nav { display: flex; flex-direction: column; gap: 1px; padding: 4px 2px 0;
  /* Expanded rail: this is the scroll container when the nav is taller than
     the viewport (the .sidebar itself must stay overflow-visible). */
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden;
  /* Slim, subtle scrollbar (thumb = hairline token, track transparent) so the
     scroll cue is unobtrusive in both themes instead of the chunky OS default. */
  scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
/* Slim, subtle scrollbar shared with re-match scroll containers (.v-rolelist) so
   the two can't drift; thumb = hairline token, track transparent, both themes. */
.sidebar-nav, .v-rolelist { scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
.sidebar-nav::-webkit-scrollbar, .v-rolelist::-webkit-scrollbar { width: 8px; }
.sidebar-nav::-webkit-scrollbar-track, .v-rolelist::-webkit-scrollbar-track { background: transparent; }
.sidebar-nav::-webkit-scrollbar-thumb, .v-rolelist::-webkit-scrollbar-thumb { background: var(--line); border-radius: 999px; }
/* THE FOCUS RING IS DRAWN INSIDE, because this pane CLIPS (measured 2026-08-08).
   `.sidebar-nav` is `overflow-x: hidden` with 2px of horizontal padding, and the
   shared ring is `outline: 2px solid var(--ring); outline-offset: 2px` — 4px of
   growth on each side of a row that fills the pane's content width. So the outer
   2px of the keyboard focus indicator on EVERY primary nav row, and on every
   group header, was cut off: 11 of 40 tab stops on /pool, 22 of 39 on
   /hiring/screening. Nothing in the cascade says so — the rule is correct and
   the CONTAINER is what makes it wrong, which is CLAUDE.md's layout rule 5 and
   is why only a browser found it.

   Scoped to the PANE rather than to `.nav-item`, and negative-offset rather than
   a second colour, because both are what this sheet already does for the same
   situation: `.posb-row[data-role-url]`, `.cand-row[data-cand-row]` and
   `.wz-card-head` are all full-bleed rows inside a clipping container and all
   draw at `outline-offset: -2px`. A per-row rule would have to be repeated for
   `.nav-item`, `.nav-group-h` and whatever lands here next; the pane's clip is
   the one fact they share. Guard: `HOVER FOCUS` in tests/test_ui_alignment.py. */
.sidebar-nav :focus-visible { outline-offset: -2px; }
/* Collapsed rail: the icon strip is short, so scrolling is unnecessary — keep
   overflow visible so the hover flyouts can escape the 56px rail. */
:root[data-rail="collapsed"] .sidebar-nav { overflow: visible; }
.nav-sect { display: flex; flex-direction: column; }
.nav-group-h {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 7px 10px; border: 0; background: none; cursor: pointer;
  font-family: inherit; font-size: 13.5px; font-weight: 500; line-height: 1.4; text-align: left;
  color: var(--muted); border-radius: 8px;
  transition: background .15s ease, color .15s ease;
}
.nav-group-h:hover { background: var(--hover); color: var(--ink); }
/* Group header is a pure fold toggle (no navigation); the whole header is the
   hit target and highlights on hover/active. */
.nav-group-h.active { background: var(--neutral-tint); color: var(--ink); font-weight: 600; }
.nav-chev { margin-left: auto; flex-shrink: 0; color: var(--faint); transition: transform .15s ease; }
.nav-sect:not(.is-open) .nav-chev { transform: rotate(-90deg); }
/* Pipeline focused view: the grouped one-line stage funnel (reuses the
   Positions .funnel look; segments highlight when their group filter is on). */
.pipe-stagefunnel .funnel-stage.active .funnel-label { color: var(--ink); font-weight: 600; }
/* These segments carry .stage-seg only for its hover/active label colour, not
   its tab underline — the funnel already has its own proportion track. Drop the
   .stage-seg border-bottom so the active stage doesn't show a second (blue)
   line under the grey track. */
.pipe-stagefunnel .stage-seg { border-bottom: 0; margin-bottom: 0; }
/* Entitlement padlock on a module group header (module licensed off): sits
   between the label and the chevron; hidden with the labels on the collapsed
   rail (the flyout still shows it via the group header clone). */
.nav-lock { margin-left: auto; flex-shrink: 0; color: var(--faint); }
.nav-lock + .nav-chev { margin-left: 6px; }
:root[data-rail="collapsed"] .sidebar .nav-lock { display: none; }
.nav-kids { display: none; flex-direction: column; gap: 1px; }
.nav-sect.is-open > .nav-kids { display: flex; }
/* Child text aligns exactly under the group-header text (10px pad + 18px icon
   + 10px gap = 38px), so "Positions" lines up with "Hiring". */
.nav-item.nav-sub { padding-left: 38px; font-weight: 500; }
/* A nested sub-group header (Voice interview): sits at the .nav-sub level but
   folds its own kids with the same chevron mechanics as the module groups. */
.nav-group-h.nav-subh { padding-left: 38px; }
/* Voice interview's sub-pages (third/fourth nav levels, shown while inside). */
.nav-item.nav-sub2 { padding-left: 52px; font-weight: 500; font-size: 13px; }
.nav-item.nav-sub3 { padding-left: 66px; font-weight: 400; font-size: 12.5px; }
:root[data-rail="collapsed"] .nav-item.nav-sub2,
:root[data-rail="collapsed"] .nav-item.nav-sub3 { display: none; }

/* collapsed rail: 56px icon strip, labels via native tooltips */
:root[data-rail="collapsed"] .sidebar { width: 56px; padding: 14px 6px; }
/* Collapsed rail: drop "Ver" and the trailing "a", leave the blue "q" + check
   as a compact brand glyph. */
:root[data-rail="collapsed"] .wordmark .wm-ver,
:root[data-rail="collapsed"] .wordmark .wm-end { display: none; }
:root[data-rail="collapsed"] .wordmark { font-size: 20px; }
/* Collapsed rail rhythm: the brand block carries no bottom padding of its own —
   the gap below the collapse toggle is owned by .sidebar-nav's collapsed
   padding-top so the toggle->first-icon gap matches the icon-to-icon rhythm. */
:root[data-rail="collapsed"] .side-brand { justify-content: center; padding: 2px 0 0; flex-direction: column; gap: 6px; }
:root[data-rail="collapsed"] .sidebar-nav { padding-top: 5px; }
:root[data-rail="collapsed"] .nav-label { display: none; }
/* Scoped to .sidebar: the positions rail (Pipeline) reuses .nav-item/.nav-txt
   but lives OUTSIDE the sidebar, so collapsing must not hide its labels or
   re-center its rows. (It reused .nav-badge too, until both count badges were
   deleted on 2026-08-07 and the class went with them.) */
:root[data-rail="collapsed"] .sidebar .nav-txt { display: none; }
:root[data-rail="collapsed"] .sidebar .nav-item { justify-content: center; padding: 10px 0; }
:root[data-rail="collapsed"] .side-user-text, :root[data-rail="collapsed"] .side-chev { display: none; }
/* G17: the collapsed rail carried the same 2px disagreement as the expanded one
   (8px here against the collapsed nav item's 10px above). Brought into line. */
:root[data-rail="collapsed"] .side-user { justify-content: center; padding: 10px 0; }
/* collapsed groups: icon-only header, children fly out to the right on hover */
:root[data-rail="collapsed"] .nav-group-h { justify-content: center; padding: 10px 0; }
:root[data-rail="collapsed"] .nav-group-h .nav-txt,
:root[data-rail="collapsed"] .nav-group-h .nav-chev { display: none; }
:root[data-rail="collapsed"] .nav-sect { position: relative; }
:root[data-rail="collapsed"] .nav-sect.is-open > .nav-kids { display: none; }
:root[data-rail="collapsed"] .nav-sect:hover > .nav-kids {
  display: flex; position: absolute; left: calc(100% + 6px); top: 0;
  min-width: 194px; padding: 6px; background: var(--panel);
  border: 1px solid var(--line); border-radius: 10px; box-shadow: var(--shadow-2); z-index: 45;
}
:root[data-rail="collapsed"] .nav-sect:hover > .nav-kids .nav-item {
  justify-content: flex-start; padding: 7px 10px;
}
:root[data-rail="collapsed"] .nav-sect:hover > .nav-kids .nav-txt { display: inline-flex; }

/* Settings sub-navigation: the four top-level sections only. */
.subnav { display: flex; flex-direction: column; gap: 1px; margin: 2px 0 4px 10px;
          padding-left: 8px; border-left: 1px solid var(--line); }
.subnav a { color: var(--ink); text-decoration: none; padding: 5px 10px;
            font-size: 13px; font-weight: 600; border-radius: 7px;
            transition: background .15s ease, color .15s ease; }
.subnav a:hover { background: rgba(236,234,227,.55); color: var(--accent); }
.subnav a.active { background: var(--accent-tint); color: var(--accent); }

/* ---- main column: centered, Apple/Claude pattern (complaint #2) ----
   Auto inline margins centre the capped column inside the flex row, so a
   maximised window shows a balanced canvas either side instead of a
   left-anchored column with dead space on the right. */
/* Main column fills all width left of the sidebar (no centered cap), so wide
   displays (27"+) use the full canvas instead of parking large empty margins.
   Reading-heavy views opt back into a narrow measure via .reading-col. */
/* ---- THE CONTENT COLUMN IS CAPPED (owner ruling, 2026-08-03) ---------------
   MEASURED on the owner's display: viewport 2502px, content column 2192px,
   panel 2112px. At that width `.sub`'s deliberate 120ch measure resolved to
   1098px and read as ~1000px of wasted space -- which is what the owner
   reported. The prose was not the defect: 120ch is already past the comfortable
   range (45-75 characters, ~90 at the outside), so filling 2112px would put
   about 146 characters on a line and read WORSE, not better.
   The defect was that the column itself had no cap, so on an ultra-wide display
   every sane text measure looks like a mistake. Capping the column is the fix
   the owner chose: prose then fills its column naturally, and the page reads as
   a designed layout instead of a stretched one.
   `margin-inline: auto` centres it in the space beside the fixed rail, so the
   slack is split rather than dumped on the right.
   The tables were checked, not assumed: the talent-bank grid's tracks are
   `26px + minmax(180,2.2fr) + minmax(104,1fr) + minmax(84,.9fr) + 384px`, whose
   fixed parts sum to 410px and whose minimums sum to 778px -- so it fits well
   inside this cap and simply distributes less to the `fr` tracks. If a future
   surface genuinely needs the full viewport it opts out with `max-width: none`,
   the way the print rule already does. */
.content { flex: 1; width: 100%; min-width: 0; padding: 28px 40px;
  max-width: 1600px; margin-inline: auto; }
/* Long-form form pages (new role / edit role / full-page settings): cap the
   column so fields don't stretch absurdly on wide monitors. .content itself
   stays full-width for tables and boards. */
.form-col { max-width: 860px; }
/* §1 per-screen persona label, sits just above the page headline */
.route-persona { margin: 0 0 6px; color: var(--faint); }
h1 { font-family: var(--font-sans); font-size: 21px; font-weight: 650;
     margin: 0 0 6px; letter-spacing: -.01em; }
/* h2 and .sec-head are ONE section-title step (var(--text-md)/600); .sec-head
   only adds divider chrome (hairline + spacing) on top of the same type. */
h2 { font-size: var(--text-md); font-weight: 600; margin: 0 0 12px; letter-spacing: -.01em; }
h3 { font-size: 14px; font-weight: 600; margin: 0 0 8px; }
.sub { color: var(--muted); margin: 0 0 28px; font-size: 14.5px; }
.sec-head { font-family: var(--font-sans); font-size: var(--text-md); font-weight: 600; margin: var(--s7) 0 var(--s4);
            padding-bottom: 10px; border-bottom: 1px solid var(--line); letter-spacing: -.01em;
            scroll-margin-top: 20px; }
.panel { scroll-margin-top: 20px; }
.sec-head:first-of-type { margin-top: 20px; }
.sec-note { color: var(--muted); font-size: 13.5px; margin: -8px 0 16px; }
a { color: var(--accent); }
a:hover { color: var(--accent-d); }

.flash { padding: 11px 16px; border-radius: 10px; margin-bottom: 16px; font-size: 14px; border: 1px solid transparent; }
.flash.ok { background: var(--ok-tint); color: var(--ok-ink); border-color: var(--ok-tint); }
.flash.error { background: var(--bad-tint); color: var(--bad-ink); border-color: var(--bad-tint); }

.cards { display: grid; grid-template-columns: repeat(auto-fit,minmax(170px,1fr)); gap: 16px; margin-bottom: 28px; }
.card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 20px 22px; box-shadow: var(--shadow); }
/* KPI cards: quiet sentence-case label + big tabular figure (v2: uppercase
   eyebrows are reserved for true eyebrows and nav group labels) */
.card .label { color: var(--faint); font-size: 12.5px; font-weight: 500; }
.card .value { font-size: 30px; font-weight: 600; margin-top: 8px; letter-spacing: -.02em;
               font-variant-numeric: tabular-nums; }

/* v2 de-boxed sections: hairlines + whitespace instead of cards. Card chrome
   stays only where elevation is real (dialogs, popovers, the peek drawer, and
   anything opting in with .card-keep). */
.panel { background: transparent; border: 0; border-radius: 0; padding: var(--s5) 0;
         box-shadow: none; margin-bottom: 0; border-bottom: 1px solid var(--line); }
.panel:last-of-type { border-bottom: 0; }
/* A .sec-head already supplies the space above its section (margin-bottom +
   hairline). When a .panel follows it directly, the panel's own top padding
   stacks on top of that AND the first field's top margin, leaving an oversized
   gap between the heading and the first control. Drop the redundant top padding
   so heading-to-first-control matches the normal h2->field rhythm. */
.sec-head + .panel { padding-top: 0; }
.panel.card-keep { background: var(--panel); border: 1px solid var(--line);
                   border-radius: var(--radius); padding: var(--s5) var(--s5); margin-bottom: 20px; }
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
@media (max-width: 820px){ .grid2 { grid-template-columns: 1fr; } }
/* 30/60/90 onboarding roadmap — three phase columns. */
.roadmap { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
@media (max-width: 820px){ .roadmap { grid-template-columns: 1fr; } }
.roadmap-col { border: 1px solid var(--line); border-radius: var(--r-md);
  padding: 14px 16px; background: var(--bg); }
.roadmap-col h3 { margin: 0; }
.roadmap-col .rm-theme { color: var(--muted); font-size: var(--text-xs); margin: 2px 0 8px; }
.roadmap-col ul { margin: 0; padding-left: 18px; font-size: var(--text-sm); line-height: 1.5; }
.roadmap-col li { margin-bottom: 6px; }

/* Sub-assessments nested under a recommendation stage (e.g. AI interview →
   job-related / HR / cultural & value lenses). */
.rec-subs { margin: 0 0 0 2px; padding-left: 14px; border-left: 2px solid var(--line);
  font-size: var(--text-sm); line-height: 1.5; }
.rec-subs li { margin-bottom: 5px; }
.rec-subs li:last-child { margin-bottom: 0; }
/* Corner headline verdict chip (replaces the numeric overall on the profile). */
.dh-verdict { margin: 2px 0 4px; }

label { display: block; font-size: 13px; font-weight: 500; margin: 14px 0 6px; color: var(--ink); }
input[type=text], input[type=email], input[type=password], input[type=number],
input[type=search], input[type=date], input[type=tel], select, textarea {
  width: 100%; padding: 10px var(--field-pad-x); border: 1px solid var(--line); border-radius: 8px;
  font-size: 14px; font-family: inherit; background: var(--field); color: var(--ink);
  transition: border-color .15s ease, box-shadow .15s ease;
}
input:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--ring); box-shadow: 0 0 0 3px var(--accent-tint);
}
textarea { resize: vertical; min-height: 120px; line-height: 1.5; }
/* NO NATIVE NUMBER SPINNER ANYWHERE IN THE PRODUCT (owner 2026-08-06, on the
   CV-screening step's "75 %" threshold: "remove that arrow up and down").
   PRODUCT-WIDE, AND THAT IS WIDER THAN WHAT WAS ASKED — recorded here rather
   than let pass as the narrow fix. The owner pointed at one field on one wizard
   step, but every number input in this product is a percentage, a count, a
   threshold or a month window: all typed, none scrubbed. Scoping it to that step
   would have been worse than doing nothing, because `_assess_cv_invite.html` and
   `_role_eligibility.html` are SHARED partials rendered on the wizard AND on
   edit-role, so a container-scoped rule makes one field look like two controls
   depending on which page included it. That is CLAUDE.md layout rule 5 exactly:
   the fix is to scope the rule to the container it was written for, and the
   container here is the product.
   IT REPLACES TWO SCOPED COPIES rather than becoming a third (they were in the
   wizard block below): `.wiz-panel[data-step-key="review"] input[type=number]
   .set-ctl`, which had MATCHED NOTHING since P1-08 moved the scorecard weights
   off the review step, and `.wz-timebar-r input[type=number]`, which was the
   same three declarations written again for the step-3 time limit. Two copies of
   one rule, one of them dead, is how the chrome came back one template at a
   time.
   STILL `type=number`: the numeric keypad on mobile and the browser's own
   min/max validation are the reason for that type, not the stepper buttons.
   Guard: tests/check_tokens.py arm 8.14, which asserts the selector is
   UNSCOPED, so a future rule cannot re-narrow it to one surface. */
input[type=number] { -moz-appearance: textfield; appearance: textfield; }
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
input[type=file] { font-size: 13px; margin-top: 4px; }
.hint { color: var(--faint); font-size: 12.5px; margin-top: 5px; }
.hint-ok { color: var(--ok-ink); }
.hint-bad { color: var(--bad-ink); }
/* label + right-aligned action (e.g. "✦ Generate from title") on one row */
.label-row { display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin: 14px 0 6px; }
.label-row label { margin: 0; }
.label-row-actions { display: flex; align-items: center; gap: 8px; }
.sec-head.label-row { margin: var(--s7) 0 var(--s4); }
.ai-gen { white-space: nowrap; }
.ai-spark { color: var(--accent); margin-right: 2px; }
/* On a FILLED button the accent spark would sit accent-on-accent (invisible in
   dark mode); inherit the button's text colour instead. Ghost / plain surfaces
   keep the accent spark above. */
.btn:not(.ghost):not(.danger) .ai-spark { color: currentColor; }
/* report-page action bar (back-link removed; actions sit right) */
.report-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; }

/* v2 buttons: primary = warm ink fill; brand blue is NOT a default button
   color (reserved for links/selection/focus/active-nav and candidate-facing
   hero CTAs via .btn.accent). Secondary (.ghost) = transparent + hairline. */
.btn { display: inline-flex; align-items: center; justify-content: center; min-height: 38px; gap: 7px;
  box-sizing: border-box; background: var(--ink); color: var(--bg); border: 1px solid var(--ink);
  padding: 8px 16px; border-radius: var(--r-md); cursor: pointer;
  font-family: var(--font-sans); font-size: 13.5px; font-weight: 500; line-height: 1.4;
  text-decoration: none; transition: opacity .15s ease, background .15s ease; }
.btn:hover { opacity: .88; }
.btn:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
/* The [data-busy] double-submit primitive (base.html) disables the submit button
   and relabels it — but only where the label is plain text. A button carrying
   child markup keeps its label, because overwriting textContent would delete
   that markup (the readiness flag is exactly that; deleting it was R-2). So on
   those buttons `disabled` is the ONLY signal, and this is the one shared rule
   that makes it visible instead of each call site inventing its own. */
.btn:disabled, .btn[disabled] { opacity: .55; cursor: not-allowed; }
.btn:disabled:hover, .btn[disabled]:hover { opacity: .55; }
.btn[aria-busy="true"] { cursor: progress; }
/* ---- THE ONE SPINNER (owner, 2026-08-21) ----------------------------------
   *"It make sense for the filter to load, so it should probably pop a loading
   circle."* There was no loading circle anywhere in this product: `[data-busy]`
   relabels a button, `.jobsq-wrap.is-busy` pulses a dot, and
   `.btn[aria-busy="true"]` above set a CURSOR and nothing visible. So this is a
   primitive rather than a one-off, and it is deliberately the only one -- a second
   spinner invented at the next call site is how four of them end up on one page at
   three different sizes.

   IT IS A SPAN THE CALLER PUTS INSIDE THE BUTTON, not a pseudo-element on
   `[aria-busy]`. A `::before` would have to fight every button that already has
   child markup (the readiness flag is exactly that, and overwriting it was R-2),
   and it could not be positioned relative to a label the caller controls.

   `currentColor` SO IT INHERITS, and `border-*-color: transparent` on one side is
   what makes a ring read as spinning. No margin: the flex `gap` on the button's
   own row spaces it, which is layout rule 2 -- the container aligns its children,
   the child does not push itself.

   REDUCED MOTION IS HONOURED. A spinner is decoration over a state that is
   already announced by `aria-busy`, so it stops moving rather than disappearing:
   the ring stays visible as a static "work in progress" mark. */
.spinner { display: inline-block; width: 12px; height: 12px; border-radius: 50%;
  border: 2px solid currentColor; border-top-color: transparent;
  animation: spin .6s linear infinite; vertical-align: -1px; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }
/* The rows the spinner is waiting on. Opacity ONLY -- no layout property, so the
   grid does not move while the request is in flight and nothing reflows when it
   lands. `aria-busy` is the same attribute assistive tech reads, so the visual
   and the announcement cannot drift. */
.v-fbtable[aria-busy="true"] { opacity: .5; transition: opacity .12s linear; }
.btn.accent { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn.accent:hover { background: var(--accent-d); border-color: var(--accent-d); opacity: 1; }
.btn.ghost { background: transparent; color: var(--ink); border: 1px solid var(--line); box-shadow: none; }
.btn.ghost:hover { background: var(--hover); border-color: var(--line); opacity: 1; }
.btn.danger { background: transparent; color: var(--bad); border: 1px solid var(--danger-edge); box-shadow: none; }
.btn.danger:hover { background: var(--bad-tint); opacity: 1; }
/* Dense recruiter buttons stay compact: 30px ≥ WCAG 2.2 AA (24px) target size —
   candidate-facing primary .btn is 44px; we don't bloat triage tables to match. */
.btn.sm { padding: 5px 11px; border-radius: 7px; font-size: 12.5px; min-height: 30px; }

/* v2 tables: sit directly on the canvas; sentence-case quiet headers. */
table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
th, td { text-align: left; padding: var(--s3) var(--s3) var(--s3) 0; border-bottom: 1px solid var(--line-soft);
         vertical-align: middle; }
th { background: var(--canvas); color: var(--faint); font-size: 11.5px; font-weight: 500;
     text-transform: none; letter-spacing: .01em; white-space: nowrap; }
tr:last-child td { border-bottom: 0; }
tr:hover td { background: rgba(28,27,24,.025); }
:root[data-theme="dark"] tr:hover td { background: rgba(236,233,226,.035); }
td a { color: var(--ink); text-decoration: none; font-weight: 500; }
td a:hover { color: var(--accent); }

/* .badge — shape from the shared pill group above; tone via the legacy .b-* classes. */
.score { font-weight: 600; font-variant-numeric: tabular-nums; }
.score.hi { color: var(--ok-ink); }
/* Second-best band -- see the .score-pill ramp. `.score` takes the same
   `fit_band().cls` / `interview_band().cls`, so it needs the same four steps or
   a Good/Advance mark falls through to the inherited ink and loses its tone. */
.score.hi2 { color: var(--ok2-ink); }
.score.mid { color: var(--warn-ink); }
.score.lo { color: var(--bad-ink); }

/* Consultant-style dashboard: observation cards + recommended actions.
   Coloured top borders retired (reference language: plain white cards);
   tone classes kept as selectors for compatibility. */
.metric { border-top: 0; }
.metric.info, .metric.good, .metric.warn { border-top: 0; }
.metric .note { color: var(--muted); font-size: 12.5px; margin-top: 6px; }
.actions { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 10px; }
.action { display: flex; justify-content: space-between; align-items: center; gap: 14px;
          padding: 13px 16px; border: 1px solid var(--line); border-left: 3px solid var(--neutral);
          border-radius: 8px; }
.action.info { border-left-color: var(--accent); }
.action.good { border-left-color: var(--ok); }
.action.warn { border-left-color: var(--warn); }
.action .a-text { font-size: 14px; line-height: 1.45; }
.action .btn { flex-shrink: 0; }

/* Candidate scorecard: 4 pillars → composite → 2 results */
.scorecard-top { display: flex; gap: 26px; align-items: flex-start; flex-wrap: wrap; }
.composite { flex: 0 0 180px; text-align: center; padding: 6px 24px 6px 4px;
             border-right: 1px solid var(--line); }
.composite .composite-score { font-size: 56px; font-weight: 600; line-height: 1.05;
                              letter-spacing: -.03em; font-variant-numeric: tabular-nums; }
.pillars { flex: 1; min-width: 300px; display: flex; flex-direction: column; gap: 14px; }
.pillar-row { display: grid; grid-template-columns: 210px 1fr 38px; align-items: center; gap: 12px; }
.pillar-row .plabel { font-size: 13.5px; }
.pillar-row .pscore { text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; }
/* v2: 8px pillar meters on reports */
.meter { background: var(--line-soft); border-radius: 6px; height: 8px; overflow: hidden; }
.meter > span { display: block; height: 8px; background: var(--accent); border-radius: 6px; }
.pillar-parts { font-size: 12px; color: var(--muted); margin: 2px 0 0 0; }
/* ONE COLUMN, because there is one card. This was `1fr 1fr` for "Result 1 ·
   role-profile fit" beside "Result 2 · best-fit positions"; IVR-03 (owner,
   2026-08-07) DELETED Result 2 and left the two-column track behind, so the
   surviving card rendered 334px wide in a 686px report column with 352px of
   dead space beside it -- the only panel on the whole report that did not share
   the page's left/right edges. Nothing in the diff showed it: the template lost
   a child, not a rule.

   The container is re-scoped rather than the card nudged (CLAUDE.md layout rule
   5), and the 760px query's clause for it is dropped in the same edit because it
   now says what the base rule already says. Restoring a second card is one word
   here. Guard: `IV REPORT` in tests/test_ui_alignment.py, which asserts every
   panel in `.report-main` shares one left and one right edge. */
.result-cards { display: grid; grid-template-columns: 1fr; gap: 18px; }
@media (max-width: 760px) { .composite { border-right: none; } }

@media print {
  .sidebar, .no-print, .flash { display: none !important; }
  .print-only { display: block !important; }
  .content { padding: 0 !important; max-width: none !important; }
  .panel, .card { box-shadow: none !important; break-inside: avoid; }
  a[href]::after { content: ""; }
}

/* legacy .badge tone zoo — remapped onto the muted semantic tones */
.b-New { background: var(--warn-tint); color: var(--warn-ink); }
.b-Shortlisted { background: var(--ok-tint); color: var(--ok-ink); }
.b-Interviewing { background: var(--accent-tint); color: var(--accent); }
.b-Offer { background: var(--neutral-tint); color: var(--neutral); }
.b-Hired { background: var(--ok-tint); color: var(--ok-ink); }
.b-Rejected { background: var(--bad-tint); color: var(--bad-ink); }
/* Phase 29: an EXIT, not a stage — muted rather than bad-toned, because it
   is not an adverse judgement about the person, it is this role's stated
   requirement not being met by the candidate's own declaration. */
.b-Not-Eligible { background: var(--neutral-tint); color: var(--neutral); }
.b-Written-Test-Assessment { background: var(--accent-tint); color: var(--accent); }
.b-AI-Interview-Invitation { background: var(--accent-tint); color: var(--accent); }
.b-AI-Interview-Completed { background: var(--ok-tint); color: var(--ok-ink); }
.v-Strong { color: var(--ok-ink); } .v-Possible { color: var(--warn-ink); } .v-Weak { color: var(--bad-ink); }

ul.clean { margin: 6px 0 0; padding-left: 18px; }
ul.clean li { margin-bottom: 7px; font-size: 14px; }
.bars { display:flex; flex-direction:column; gap:10px; }
.bar-row { display:grid; grid-template-columns: 96px 1fr 40px; align-items:center; gap:12px; font-size:13px;
           font-variant-numeric: tabular-nums; }
.bar { height: 16px; background: var(--accent); border-radius: 6px; min-width: 2px; }
.bar.alt { background: var(--ring); }
.toolbar { display:flex; gap:12px; align-items:flex-end; flex-wrap:wrap; margin-bottom:18px; }
.toolbar > div { min-width: 150px; }
.warnbar { background: var(--warn-tint); border:1px solid var(--warn-tint); color: var(--warn-ink); padding:11px 15px; border-radius:10px; font-size:13px; margin-bottom:18px; }
/* The voice-run page's warning bar sits inside a full-bleed stage rather than a
   padded column, so it supplies the gutters the stage does not. This WAS an
   inline `style="margin:16px 28px 0"` on the element -- a layout decision written
   at one call site, which is what the layout contract in CLAUDE.md forbids and
   what `check_tokens.py` 8.12 now refuses. The offsets are the component's, so
   they live with the component. `28px` matches `.voice-top`'s own horizontal
   padding: change them together or the bar stops lining up with the header. */
.warnbar-run { margin: var(--s4) 28px 0; }
.row-between { display:flex; justify-content:space-between; align-items:center; gap:14px; }
.meta { color: var(--muted); font-size:13px; font-variant-numeric: tabular-nums; }
.kv { display:grid; grid-template-columns: 130px 1fr; gap:9px 14px; font-size:14px; }
.kv .k { color: var(--muted); }

/* Candidate detail: main content + right-hand actions sidebar.
   STILL USED BY templates/candidate.html — the talent-bank person page — which
   is why these three survive. The candidate REPORT (templates/detail.html) left
   them for `.detail-report` below; `.detail-cols-even`, its 50/50 modifier, went
   with it because that modifier had exactly one caller. */
.detail-cols { display:flex; gap:28px; align-items:flex-start; }
.detail-main { flex:1; min-width:0; }
.detail-side { width:330px; flex-shrink:0; position:sticky; top:18px; }

/* ---- Candidate FULL REPORT: ONE COLUMN of collapsible groups --------------
   Owner, 2026-08-05: "For the candidate full report, it should be collapsible
   group, with one column layout instead of 2 column layout, default is
   collapsed."

   A ONE-COLUMN GRID, not a stack of block elements, and that distinction is the
   layout contract in miniature (CLAUDE.md rule 2: the grid lives on the ROW,
   never per item). A stack gives every child whatever width its own content and
   margins produce; ONE `grid-template-columns` on the CONTAINER means every
   group takes its left and right edge from the SAME declaration and they cannot
   drift apart. Measured rather than asserted: `UX-54` in
   tests/test_ui_alignment.py checks one left edge across every group at 1440px.

   `minmax(0, 1fr)` and never a bare `1fr`: a grid item's default
   `min-width: auto` lets wide content -- the evidence table, the start-link
   input -- push the track past the viewport, which is a horizontal scrollbar on
   the whole page rather than inside the thing that overflowed.

   NO `gap`, DELIBERATELY. `.cgroup + .cgroup { margin-top: 12px }` already owns
   the rhythm for stacked groups everywhere else in the product and it applies
   here unchanged. A gap on top of it would double the spacing -- which is
   precisely the defect `.jobsq-cols > .cgroup + .cgroup { margin-top: 0 }` had
   to undo one screen away in this same file. THAT RULE IS DELETED with the
   three-column background-work layout it existed for; the lesson is why this
   paragraph outlives it. Reuse the rhythm; do not re-specify it. */
.detail-report { display: grid; grid-template-columns: minmax(0, 1fr); }
/* Header recommendation summary lines (overall fit + other role fits). */
.dh-recs { margin-top: 12px; display: flex; flex-direction: column; gap: 5px; }
.dh-rec { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; font-size: 13.5px; }
.dh-rec .k { font-size: 12px; color: var(--faint); font-weight: 500; min-width: 148px; }
.dh-rec-txt { color: var(--ink); }
.dh-rec-txt a { color: var(--ink); font-weight: 500; }
.dh-rec-txt a:hover { color: var(--accent); }
.detail-side .panel { margin-bottom:0; }

/* ---- v2 candidate-detail header block ----
   `justify-content: space-between` REMOVED (2026-08-05). It was a no-op that
   counted: `.detail-head` has exactly ONE child (`.dh-main`) and has had for as
   long as `.dh-side` has been dead, so space-between resolved to flex-start and
   changed no pixel. It is also the named drift primitive of CLAUDE.md rule 2 --
   a rule that aligns a row to its own content width -- sitting on the one block
   of this page that is NOT collapsible and therefore always on screen. If a
   second child is ever added here it must take its edge from a grid track, not
   from this. Ratchet lowered in this commit: see tests/layout_ratchets.txt. */
.detail-head { display: flex; align-items: flex-end;
  gap: var(--s5); padding-bottom: 18px; border-bottom: 1px solid var(--line); }
.dh-title { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.dh-side { text-align: right; flex: 0 0 auto; display: flex; flex-direction: column;
  align-items: flex-end; gap: 6px; }
.dh-score { font-size: 30px; font-weight: 600; line-height: 1.1; letter-spacing: -.02em;
  font-variant-numeric: tabular-nums; color: var(--faint); }
.dh-score.hi { color: var(--ok-ink); } .dh-score.mid { color: var(--warn-ink); } .dh-score.lo { color: var(--bad-ink); }
/* inline status select styled as a tinted pill with chevron */
.status-select { border: 1px solid transparent; background-color: var(--accent-tint); color: var(--accent);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path d='M1 1l4 4 4-4' fill='none' stroke='%233b5bdb' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>"); }
/* strengths / gaps two-column block */
.sg-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
@media (max-width: 760px){ .sg-cols { grid-template-columns: 1fr; } }
.sg-h { font-size: 12px; font-weight: 600; margin-bottom: 6px; }
.sg-h.ok { color: var(--ok-ink); } .sg-h.warn { color: var(--warn-ink); }
/* attachment pill for the CV file */
.attach-pill { display: inline-flex; align-items: center; gap: 8px; padding: 5px 11px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 999px;
  font-size: 12.5px; color: var(--ink); text-decoration: none; }
.attach-pill:hover { border-color: var(--faint); color: var(--ink); }
.attach-pill .ap-tag { font-size: 10px; font-weight: 600; color: var(--faint);
  border: 1px solid var(--line-soft); border-radius: 5px; padding: 1px 5px; }
.link-edit { background: none; border: 0; padding: 0 2px; font: inherit; font-size: 12.5px;
  color: var(--accent); cursor: pointer; }
.link-edit:hover { text-decoration: underline; }
.link-danger.lg { font-size: 13px; padding: 0; }
@media (max-width: 980px){
  .detail-cols { flex-direction:column; }
  .detail-side { width:100%; position:static; }
}

/* Stacked bar chart (SVG segments; heights are SVG geometry, colours are the
   .seg tone classes below — zero inline styles, complaint #1/AC6) */
.stack { display:flex; align-items:flex-end; gap:16px; padding-top:8px; overflow-x:auto; }
.stack-col { display:flex; flex-direction:column; align-items:center; min-width:42px; }
.stack-bars { display:flex; flex-direction:column-reverse; width:44px; height:140px; justify-content:flex-start; background:var(--line-soft); border-radius:6px; overflow:hidden; }
.stack-seg { width:100%; }
.stack-svg { display:block; width:44px; height:140px; }
.stack-x { margin-top:6px; font-size:12px; color:var(--muted); font-variant-numeric: tabular-nums; white-space:nowrap; }
.stack-n { font-size:11px; color:var(--muted); margin-bottom:3px; font-variant-numeric: tabular-nums; }
.legend { display:flex; flex-wrap:wrap; gap:12px; margin-top:14px; font-size:12px; }
.legend span { display:inline-flex; align-items:center; gap:6px; }
.legend i { width:11px; height:11px; border-radius:3px; display:inline-block; }
.chart-legend { display:flex; flex-wrap:wrap; gap:14px; margin-top:10px; }
.chart-legend span { display:inline-flex; align-items:center; gap:6px; font-size:12.5px; color:var(--muted); }
.chart-legend i { width:10px; height:10px; border-radius:2px; display:inline-block; }
.chart-legend i.s1 { background:var(--pillar-tech); }
.chart-legend i.s2 { background:var(--pillar-val); }
.chart-legend i.s3 { background:var(--pillar-pers); }
.chart-legend i.s4 { background:var(--pillar-hr); }
.chart-legend i.s5 { background:var(--muted); }
.chart-empty { color:var(--muted); font-size:13px; padding:12px 0; }
/* Scorecard sub-weights nested under the AI Interview overall row (Step 5). */
.score-sub { margin:6px 0 10px 20px; padding-left:14px; border-left:2px solid var(--line); }
/* Candidate report comment thread — READ ONLY since 2026-08-05. `.cmt-actions`
   stood below `.cmt-body` and is deleted with the author's Edit / delete
   disclosure it styled; the three rules that remain render the historical rows
   the report still shows. */
.cmt { padding:10px 0; border-top:1px solid var(--line); }
.cmt:first-of-type { border-top:0; padding-top:4px; }
.cmt-head { margin-bottom:3px; }
.cmt-body { white-space:pre-wrap; }
/* Grouped horizontal outcome bars (channel effectiveness): pillar-tinted fills
   on the shared .bar-svg track. */
.bar-svg rect.tech { fill:var(--pillar-tech); }
.bar-svg rect.val  { fill:var(--pillar-val); }
.bar-svg rect.pers { fill:var(--pillar-pers); }
.ch-out { margin-top:16px; }
.ch-out:first-of-type { margin-top:8px; }
.ch-out-head { display:flex; justify-content:space-between; align-items:baseline;
  font-weight:600; margin-bottom:4px; }
/* status → muted series colours (token-derived only — no raw hex). One rule
   per config.ALL_STATUSES entry, in pipeline order, so the stacked HR-effort
   chart + its legend cover every status a row can hold. color-mix() shades
   stay sane in BOTH themes because they lean on --ink/--bg, which flip with
   the theme.

   LADDER COLLAPSE (owner, 2026-08-05): the rules for New, the two AI Interview
   halves, the two F2F markers and Commenced are DELETED. Their rows were
   migrated, so no series can carry them; `.seg` above is the fallback if one
   somehow does. `.seg-Received` and `.seg-AI-interview` replace them, and the
   AI-interview colour is the SOLID accent the completed half used, because the
   folded stage is the whole AI-interview stage rather than half of it. */
.seg { fill: var(--neutral); background: var(--neutral); }
.seg-Received { fill: var(--neutral); background: var(--neutral); }
.seg-AI-interview { fill: var(--accent); background: var(--accent); }
/* SIX STAGES LOCKED (owner, 2026-08-20). The two `.seg-Written-Assessment-*`
   rules above this line are DELETED with their statuses -- `features/written`
   went on 2026-07-31 and a segment colour for a status nothing can hold paints
   nothing. `--stage-f2f` is the ramp step the sheet already declares for this
   stage, so the chart and the pipeline bar cannot drift to two colours for one
   stage, which is the divergence no source scan can see. */
.seg-Face-to-Face-Interview { fill: var(--stage-f2f); background: var(--stage-f2f); }
.seg-Shortlisted { fill: var(--pillar-val); background: var(--pillar-val); }
.seg-Offered { fill: var(--ok); background: var(--ok); }
.seg-Hired { fill: var(--ok-ink); background: var(--ok-ink); }
.seg-Rejected { fill: var(--bad); background: var(--bad); }
.seg-Not-Eligible { fill: var(--neutral); background: var(--neutral); }

/* ---- Slice B structural helpers (PRD v2.1 §3/§5) ---- */
body.public { display: block; }               /* standalone (no-sidebar) pages */
.public-wrap { max-width: 640px; margin: 48px auto; padding: 0 20px; }
/* `.public-wrap .brandline` centred the uploaded logo above the privacy notice
   and had exactly that one call site; both are deleted at cut-down lane L5.
   NOT to be confused with `.candidate-brandline` in candidate.css, which is a
   different class on seven live templates. */
/* v2: the consent block is an indented hairline block (2px left border) —
   exact copy + checkboxes unchanged, chrome only. */
.consent-card { margin-top: 20px; padding: 4px 0 4px 18px; background: transparent;
                border: 0; border-left: 2px solid var(--line); border-radius: 0; font-size: 13px; }
.consent-card label.check { display: flex; gap: 8px; align-items: flex-start;
                            font-weight: 400; margin-top: 10px; font-size: 13px; }
/* THE `gap` IS THE ONLY HORIZONTAL SPACING, and it took a browser to find out it
   was not. Both rules below set `margin-top` and left the INLINE margins to the
   user agent -- and Chromium's differ BY INPUT TYPE (radio `3px 3px 0 5px`,
   checkbox `3px 3px 3px 4px`). Measured on `/apply/<token>` 2026-08-08: inside
   ONE `.consent-card`, under ONE rule, the two work-authorization RADIOS put
   their text at x=483 and the three acknowledgement CHECKBOXES at x=482, and
   every control sat 4-5px inside the card's own content edge (x=454) that its
   sibling <p>s and its language selects already share -- three left edges in a
   card that should have two.

   So the layout decision was being made by the INPUT TYPE rather than by the
   component, which is CLAUDE.md's layout rule 5 exactly: the fix is to scope the
   shared rule, never to nudge a call site. Written as a full `margin` shorthand
   rather than `margin-inline: 0` beside the old declaration, because a second
   rule for the same element is the counter-rule that lets the two drift.

   BOTH primitives get it in one edit ON PURPOSE. `.check-inline` carried
   byte-identical declarations and therefore the identical latent defect;
   fixing only the one that was measured is how they diverge.
   Guard: `APPLY PAGE` in tests/test_ui_alignment.py, whose negative arm puts
   Chromium's own radio margin back by hand. */
.consent-card label.check input { width: auto; margin: 3px 0 0; }
.check-inline { display: flex; gap: 8px; align-items: flex-start; font-weight: 400; }
.check-inline input { width: auto; margin: 3px 0 0; }
/* Per-role assessments: each test type is a visually separated sub-section with
   a consistent title + indented body (the enable box / source control). */
.assess-block { border: 1px solid var(--line); border-radius: 10px;
                padding: var(--s4) var(--s5); margin: var(--s4) 0; background: var(--panel); }
.assess-block + .assess-block { margin-top: var(--s4); }
.assess-title { font-weight: 400; }
.assess-title strong { font-weight: 600; }
/* NO LEFT INDENT (UX-40, owner 2026-08-03). This was `padding-left: 26px` -- a
   CHECKBOX COLUMN, so a block's body would line up under the label text of a
   checkbox in its title. NOT ONE `.assess-title` in the tree contains a checkbox:
   all four (`_assess_cv_invite`, `_assess_scorecard`, `_role_eligibility`,
   `_role_screening_rules`) are `<p class="check-inline assess-title">` wrapping a
   plain span. Every block was therefore indenting its body to clear a control
   that does not exist. If a checkbox title is ever wanted, the indent belongs on
   a modifier those blocks opt into -- not on the base rule where it silently
   applies to all four. */
.assess-body { margin-top: var(--s3); }
.assess-body[hidden] { display: none; }
.assess-body .panel { padding-top: 0; }
.chip-row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
/* G12: a chip that is a control. The name is a disclosure button and the x is a
   one-button form, so both need the browser's button chrome reset to keep the
   chip looking like a chip. */
.chip.chip-act { display: inline-flex; align-items: center; gap: 4px; padding-right: 4px; }
.chip-act .chip-btn, .chip-act .chip-x { border: 0; background: none; cursor: pointer;
  font: inherit; color: inherit; padding: 0; }
.chip-act .chip-x { padding: 0 4px; line-height: 1; color: var(--faint); }
.chip-act .chip-x:hover { color: var(--bad); }
.chip-x-form { display: inline; margin: 0; }
.mono { font-variant-numeric: tabular-nums; font-family: var(--font-mono); font-size: 12.5px; }
.blind-ref { color: var(--muted); }
.tag-pill { display: inline-flex; align-items: center; gap: 4px; padding: 2px 9px;
            border: 1px solid var(--line); border-radius: 999px; font-size: 12px;
            background: var(--panel); }
.tag-pill form { display: inline; margin: 0; }
.tag-pill button { border: 0; background: none; color: var(--muted); cursor: pointer;
                   padding: 0 2px; font-size: 12px; }
/* Chip / token input (Key competencies): a JS enhancement over the plain
   comma-separated text field. Looks like a normal input but wraps removable
   pills across rows as they grow — compact when few, never a tall empty box.
   The original input is hidden and still carries the comma-separated value. */
.chip-input { display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
              width: 100%; padding: 6px 8px; border: 1px solid var(--line);
              border-radius: 8px; background: var(--field); cursor: text;
              transition: border-color .15s ease, box-shadow .15s ease; }
.chip-input:focus-within { border-color: var(--ring); box-shadow: 0 0 0 3px var(--accent-tint); }
.chip-input .tag-pill { background: var(--neutral-tint); border-color: transparent; color: var(--ink); }
.chip-input .tag-pill button { font-size: 14px; line-height: 1; }
.chip-input .tag-pill button:hover { color: var(--ink); }
.chip-input .chip-entry { flex: 1 1 120px; min-width: 120px; width: auto;
                          border: 0; padding: 4px 2px; background: transparent;
                          font-size: 14px; color: var(--ink); }
.chip-input .chip-entry:focus { outline: none; border: 0; box-shadow: none; }
/* A very long chip (a wordy competency / position name) is capped and
   ellipsized rather than filling the row and shoving the entry input to a
   truncated edge: the pill caps at min(20rem, 100%) so the input still fits
   beside it on wide rows and wraps below on narrow ones; the text span shrinks
   (min-width:0) and truncates, while the ✕ button (not a span) stays visible. */
.chip-input .tag-pill { max-width: min(20rem, 100%); }
.chip-input .tag-pill > span { min-width: 0; overflow: hidden;
                               text-overflow: ellipsis; white-space: nowrap; }
.inline-form { display: inline; margin: 0; }
.stack-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.timeline { list-style: none; margin: 0; padding: 0; font-size: 13px; }
.timeline li { padding: 7px 0; border-bottom: 1px solid var(--line-soft); }
.timeline li:last-child { border-bottom: 0; }

/* ==============================================================   Slice D-TOKENS — PRD v2.1 §6.2/§6.3 component patterns.
   ===================================================================== */

/* ---- .chip family (§6.3) — reference pattern: rounded-full, soft-tint
   bg, tone-coloured 12px medium text, 6px dot before the label.
   Tones map ONLY via the status_chip macro in templates/_macros.html.
   Base .chip = the slatey neutral tone. ---- */
/* Shared pill shape — .chip is the reference; the once-bespoke pills
   (.badge, .attn, .st-item, .focus-pill, .todo-chip) converge on the SAME
   radius / padding / type here instead of each hand-rolling its own. Tone
   (fill + text) stays per-class but always maps onto the semantic tint/ink
   triplets, so every pill in the app reads as one family. */
.chip, .badge, .attn, .st-item, .focus-pill, .todo-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 2px 9px; border-radius: 999px;
  font-family: var(--font-sans); font-size: var(--text-xs); font-weight: 500;
  line-height: 1.6; white-space: nowrap;
  border: 1px solid transparent;
}
/* Base .chip = the slatey neutral tone (Vanta: light-grey fill by default). */
.chip { background: var(--neutral-tint); color: var(--muted); }
/* A checkbox that reads as a chip. Phase 25's bank-screen filter: several
   dozen tag values, each a plain multi-select checkbox, laid out as a wrapping
   row rather than a select so the whole vocabulary is visible at once. */
/* ONLY WHAT DIFFERS FROM `.check-chip` (:560), which now carries the geometry,
   the token font-size and the focus rule for both names. What is genuinely
   this component's own: the wrapping-row margin (its parent lays out dozens of
   these, so the gap rides on the chip, not on a `gap`), a visible fill so a
   dense filter reads as a set of pills, and `nowrap` -- a pill with a
   `border-radius: 999px` that wraps to two lines does not read as a pill. */
.chip-check { margin: 2px 4px 2px 0; background: var(--canvas); white-space: nowrap; }
.chip-check:hover { border-color: var(--accent); }
.chip-check input { margin: 0; }
/* UX-99's IN-PAGE VOCABULARY PICKER, and what it replaced.
   ---------------------------------------------------------------------------
   SPEC 2 built an open-vocabulary SET control here: removable `.chip-check` chips,
   a labelled quick-add row, and a `<datalist>`-backed search over the rest of the
   standard. `.vocab-quick` / `.vocab-quick-label` / `.vocab-add` WENT WITH THAT
   MARKUP on 2026-08-09 -- the datalist's popup is drawn by the user agent OUTSIDE
   the document, so inside the role wizard's `<dialog>` it painted down the right
   edge of the viewport and no author rule in this sheet could reach it. Their
   deletion is recorded rather than silent because `.vocab-add`'s explicit 280px
   width was itself a fix (the sheet's `input[type=text]` rule is `width: 100%` and
   its parent is a `.set-row-wide` `.set-ctl`, `justify-self: stretch`), and a
   reader who finds the class name in git history should not have to re-derive why.

   WHAT REPLACES THEM IS `.wt-list` (:741) WITH DIFFERENT ROWS, which is why almost
   nothing is declared here: the border, the radius, the search band, the 296px
   scroller and the footer are all inherited from that component. `.vocab-list` is
   a MODIFIER, not a second list -- layout rule 1, and the reason every alignment
   defect in this repo has been a decision taken at one call site.

   THE SCROLLER'S CAP IS THIS COMPONENT'S OWN NOW -- UX-112 + UX-112(a), owner
   2026-08-10: "All location and language, we should only show 2 rows instead of 6
   rows", confirmed as two rows OF THE LONG LIST with the pinned group still
   visible above it.

   THE PARAGRAPH THAT STOOD HERE ARGUED THE OPPOSITE and is replaced rather than
   left: it said 296px was re-measured for wrapping chips and kept deliberately.
   That reasoning was about the HALF-CUT (see below), which survives; what did not
   survive is the six-plus rows a 296px box gives a list of 249 entries, which is
   what the owner reported.

   SCOPED TO `.vocab-list`, NEVER THE SHARED RULE. `.wt-list-scroll`'s 296px is the
   WEIGHTS RAIL's height, and that rail is the subject of UX-107 the same day.
   Lowering the shared cap would shrink the family list to two rows as a side
   effect and move the very measurement UX-107 is about.

   184px IS MEASURED IN THE BROWSER, at the wizard dialog's own width, and it is
   NOT derivable from a chip's line-height -- a row of `Bosnia and Herzegovina BA`
   wraps differently from a row of short codes, so the pitch is content-dependent.
   What was measured at 1440px: the pinned group and its heading plus the `All
   locations` heading occupy the first ~104px of the scroller, the long list's row
   pitch is 33px (locations) / 32px (languages), and 184px therefore lands roughly
   halfway through the THIRD row.

   HALF-CUT, NOT A WHOLE NUMBER OF ROWS, and `.wt-list-scroll`'s own comment is
   where that reasoning comes from: a list whose last visible row is half-cut is
   the standard signal that there is more below, and a whole number of rows reads
   as a complete list that happens to end. At exactly two rows this reads as a
   two-row list of 249 locations -- the failure that comment was written to
   prevent, and worse here because the list is 40x longer. The `249 locations`
   count in the footer helps, but a count is not an affordance.

   WHAT THIS CAP CANNOT ADAPT TO, named rather than left to be discovered: the
   pinned group is INSIDE the scroller, so a workspace that pins many more
   jurisdictions than this fixture pushes the long list's first rows below the
   fold. The guard asserts the RELATION (pinned group whole, two long rows whole,
   the third cut) rather than this number, so that case fails the suite and says
   so instead of shipping.

   THE FOUR DECLARATIONS THAT ARE GENUINELY THIS COMPONENT'S OWN:

   1. `[hidden]` MUST BE RE-STATED, and it is not belt-and-braces. The UA's
      `[hidden] { display: none }` is specificity (0,1,0) and loses on ORIGIN to
      the author rule `.check-chip, .chip-check { display: inline-flex }` at
      (0,1,0), so `row.hidden = true` from the filter script would leave every
      chip on screen. Scoped to `.vocab-list` so it changes nothing else.
   2. THE GROUP HEAD is a block label, not a row: its group's chips carry their own
      gaps on `.chip-check`'s margin, so there is no `gap` here to double them.
   3. THE PINNED TINT is the SAME `rgba(28,27,24,.06)` wash `.wt-list-row.is-active`
      and the sidebar's active nav row use -- one vocabulary for "this is the one
      you are looking at", not a second selected-state colour.
   4. THE GROUP SEPARATOR is a top border on the SECOND group only, expressed as
      `+` rather than as a bottom border on the first -- a bottom border would
      still paint when the first group is hidden by the filter. */
.vocab-list .wt-list-scroll { max-height: 184px; }
.vocab-list [hidden] { display: none; }
.vocab-group-head { display: block; margin: 0;
  padding: var(--s2) var(--s3) var(--s1);
  font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .04em; }
.vocab-group-rows { padding: 0 var(--s3) var(--s2); }
.vocab-group + .vocab-group { border-top: 1px solid var(--line-soft); }
.vocab-group.is-pinned { background: rgba(28, 27, 24, .06); }
/* The code beside the name (`Hong Kong  HK`, `Korean  kor`). It is what the
   `<datalist>` option's `label` attribute rendered before, kept because a language
   name alone is ambiguous and the code is the identity being chosen. */
.vocab-note { color: var(--muted); font-size: var(--text-xs); }
/* Default (neutral) chips carry no dot — the leading dot is reserved for the
   coloured status/stage chips below, so grey metadata chips read clean. */
.chip::before { content: ""; width: 6px; height: 6px; border-radius: 50%;
                background: currentColor; opacity: .65; flex-shrink: 0; display: none; }
.chip-ok::before, .chip-warn::before, .chip-bad::before, .chip-info::before { display: inline-block; }
/* Chips rendered as links (e.g. the pool's Stage → report chips): keep the chip
   look, no underline; a touch of lift on hover to signal they're clickable. */
a.chip { text-decoration: none; cursor: pointer; }
a.chip:hover { filter: brightness(0.97); }

/* THE TYPEAHEAD COMPONENT IS DELETED (owner ruling 2026-08-10: *"now we no
   longer have home page, can retire the whole home search"*). Ten rules stood
   here -- `.typeahead` and its `.ta-inline` variant, plus the `.ta-menu` /
   `.ta-item` / `.ta-sub` / `.ta-tag` / `.ta-empty` dropdown `static/search.js`
   built at runtime. Every one of them is gone with that file.

   IT HAD EXACTLY TWO MARKUP CALLERS AND BOTH WENT FIRST: the Pipeline find bar
   (UX-113(a), 2026-08-10) and `module_home.html`'s `.home-ask` box (this
   ruling). `base.html`'s position picker is NOT this component -- it builds a
   `.posq-menu` of its own, deliberately, and `tests/test_wizard_js.py` §7 is the
   guard that keeps the two class families apart. Do not "reuse" `.ta-menu` for
   it if this ever comes back. */
/* tinted fills only for genuine semantic states */
.chip-info { background: var(--accent-tint); color: var(--accent);   border-color: transparent; }
.chip-ok   { background: var(--ok-tint);     color: var(--ok-ink);   border-color: transparent; }
.chip-warn { background: var(--warn-tint);   color: var(--warn-ink); border-color: transparent; }
.chip-bad  { background: var(--bad-tint);    color: var(--bad-ink);  border-color: transparent; }

/* ---- .chip-engine — provenance chips (§6.3 v1) ---- */
.chip-engine            { text-transform: uppercase; letter-spacing: .04em; font-size: 11px; }
.chip-engine.ai         { background: var(--accent-tint); color: var(--accent); }
.chip-engine.offline    { background: var(--warn-tint);   color: var(--warn-ink); }
.chip-engine.reviewer   { background: var(--ok-tint);     color: var(--ok-ink); }

/* ---- .consent-chip (§6.3 v1) — sans + tabular (mono is reserved for
   refs/IDs/tokens in the reference language) ---- */
.consent-chip { font-variant-numeric: tabular-nums; }
/* Consent-script text inside the versions table cell: readable, wrapped, and
   height-capped so long scripts don't blow out the row.

   Owner comment 3 follow-up (2026-08-01, owner-settled): the MEASURE widens to
   `min(100%, 120ch)` alongside the prose group. The `max-height: 8.5em;
   overflow-y: auto` below is the SCROLL BOX and is deliberately untouched --
   widening the measure is what takes a long script from six rendered lines to
   three; changing the height cap would change how much is hidden. */
.consent-script-text { max-width: min(100%, 120ch); max-height: 8.5em; overflow-y: auto;
  white-space: pre-wrap; color: var(--ink); font-size: var(--text-sm); line-height: 1.5; }

/* ---- .score-pill (§6.3) — tabular figure in a tinted pill.
   ≥75 ok · 60–74 warn · <60 bad · “–” faint. ---- */
.score-pill {
  display: inline-block; min-width: 34px; text-align: center;
  padding: 3px 10px; border-radius: 999px;
  font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 600;
  font-variant-numeric: tabular-nums; line-height: 1.5; white-space: nowrap;
  background: var(--neutral-tint); color: var(--faint);   /* “–” absent state */
  border: 1px solid transparent;                          /* size parity with .partial */
}
/* FOUR STEPS, NOT THREE (owner, 2026-08-06). `hi2` is the second-best band --
   Good match / Advance -- which shared `hi` with the best one until the four
   pipeline columns made the two indistinguishable side by side. The class comes
   from `scoring._BAND_PILL_CLASS`, indexed by the band's RANK in its own scale,
   so a scale whose labels are reworded keeps its colours. Anything with no band
   at all (an unscored row) still gets no class and keeps the neutral fill above,
   which is UI-35: an absence is not a verdict. */
.score-pill.hi  { background: var(--ok-tint);   color: var(--ok-ink); }
.score-pill.hi2 { background: var(--ok2-tint);  color: var(--ok2-ink); }
.score-pill.mid { background: var(--warn-tint); color: var(--warn-ink); }
.score-pill.lo  { background: var(--bad-tint);  color: var(--bad-ink); }

/* .score-pill.partial (§4.2/§6.3) — hollow pill: dashed hairline over a
   canvas fill, tone-coloured figure. Restyled into the reference language
   but still row-level distinguishable from the solid tinted pill (§8.5):
   the 1.5px dashed border + canvas (not tint) fill reads as “incomplete”.
   The paired .micro-label remains the semantic guarantee. */
.score-pill.partial {
  background: var(--bg);
  border: 1.5px dashed var(--line);
  color: var(--muted);
  padding: 3px 10px;
}
.score-pill.partial.hi  { color: var(--ok-ink);   border-color: var(--ok); }
.score-pill.partial.hi2 { color: var(--ok2-ink);  border-color: var(--ok2); }
.score-pill.partial.mid { color: var(--warn-ink); border-color: var(--warn); }
.score-pill.partial.lo  { color: var(--bad-ink);  border-color: var(--bad); }

/* Row-level micro-label paired with .score-pill.partial (§4.2) */
.micro-label {
  display: inline-block; margin-left: var(--s1);
  font-size: 11px; color: var(--faint);
  text-transform: uppercase; letter-spacing: .08em; white-space: nowrap;
}

/* ---- .segmented (§6.3) — segmented control for filters/tabs ---- */
/* ---- .tabs — underline page tabs (navigation between views of a page).
   Boxed .segmented stays for toolbar CONTROLS (sort, small filters); anything
   that switches what the page shows reads as a tab row with count badges. */
.tabs { display: flex; align-items: flex-end; gap: 22px;
        border-bottom: 1px solid var(--line); }
.tabs a { display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 2px 11px; margin-bottom: -1px;
  font-size: var(--text-sm); font-weight: 500; color: var(--muted);
  text-decoration: none; border-bottom: 2px solid transparent;
  transition: color .15s ease, border-color .15s ease; }
.tabs a:hover { color: var(--ink); }
.tabs a.active { color: var(--ink); font-weight: 600; border-bottom-color: var(--ink); }
.tabs a:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
.tabs .seg-count { margin-left: 0; }
.tabs a.active .seg-count { background: var(--accent-tint); color: var(--accent-d); }

/* ---- .trust-tabs AND #audit-feed's nowrap pair: DELETED 2026-09-01 ---------
   Both had exactly one subject -- the Trust Center's tab strip and its All
   Events Log -- and the owner deleted those pages (the internal-tool cut-down).
   A stylesheet rule whose markup is gone is dead CSS, so they go with it.

   WHAT THEY WERE FOR, kept because the reasoning generalises. `.trust-tabs`
   carried the strip's margin in ONE place because five call sites had disagreed
   -- the strip rendered at 28 / 85 / 113 / 119 / 119px depending on the tab, so
   the control moved under the cursor as you clicked through it. That is
   CLAUDE.md's layout contract exactly: a layout decision taken at the call site
   instead of in the shared component. `#audit-feed .audit-when` /
   `.audit-subject` stopped a 116px `When` column breaking
   "2026-08-05 18:10:19" over two lines on all 50 rows of a real page.

   THE REJECTED FIX IS THE PART WORTH CARRYING FORWARD: shrinking the last
   column with `td:last-child { width: 1% }` looked obvious and was wrong -- the
   `Detail` cell held a `<details>` popover, squeezing that column re-wrapped
   the summary and blew one row to 191px, and the 523px it was reclaiming was
   never the cause (`.popover-body` is `position: fixed`, so it contributed no
   width at all). That selector is GLOBAL, so the next wrapping table will hit
   the same wall; its absence is still asserted in tests/test_layout_brand.py.
   Guard: the `TRUST DELETED` arms there, converted rather than dropped. */

/* ---- .meta-row — label-over-value facts under a detail-page title
   (owner, source, dates, links) in one quiet horizontal band. */
.meta-row { display: flex; gap: 36px; flex-wrap: wrap; margin: 12px 0 2px; }
.meta-row .mi { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.meta-row .mi .k { font-size: 12px; color: var(--faint); font-weight: 500; }
.meta-row .mi .v { font-size: 13.5px; color: var(--ink); display: inline-flex;
                   align-items: center; gap: 6px; }
.meta-row .mi .v a { color: var(--ink); font-weight: 500; }
.meta-row .mi .v a:hover { color: var(--accent); }

.segmented {
  display: inline-flex; align-items: center; gap: 2px;
  padding: 2px; background: var(--line-soft);
  border: 1px solid var(--line); border-radius: var(--r-md);
}
.segmented input[type=radio] { position: absolute; opacity: 0; width: 0; height: 0; }
.segmented label, .segmented a {
  display: inline-block; margin: 0; padding: 5px 12px;
  border-radius: calc(var(--r-md) - 3px); cursor: pointer;
  font-size: var(--text-sm); font-weight: 500; color: var(--muted);
  text-decoration: none; transition: background .15s ease, color .15s ease;
}
.segmented label:hover, .segmented a:hover { color: var(--ink); }
.segmented input[type=radio]:checked + label,
.segmented a.active, .segmented label.active {
  background: var(--panel); color: var(--ink);
  box-shadow: 0 1px 2px rgba(31,30,27,.06); border: 0;
}
.segmented input[type=radio]:focus-visible + label {
  outline: 2px solid var(--ring); outline-offset: 1px;
}
/* UX-53: a PILL-RADIUS MODIFIER, which is the whole difference between the
   handoff's Open/Closed control and the primitive above. The handoff re-specified
   `.segmented` from scratch -- track padding, gap, colours, the active pill --
   and in dark mode the track colour it asked for (`--neutral-tint`, #2c2f34) is
   BYTE-IDENTICAL to what this block already paints (`--line-soft`, #2c2f34). A
   second segmented control that looks the same today is the exact shape of the
   four defects of 2026-08-03: two rules for one component, drifting later. The
   radius was the only real divergence, so it is a modifier and not a copy. */
.segmented.seg-pill { border-radius: 999px; }
.segmented.seg-pill label, .segmented.seg-pill a { border-radius: 999px; }

/* ---- .popover (§6.3) — CSS-only <details> popover ---- */
details.popover { position: relative; display: inline-block; }
/* SPLIT AT UX-74 (owner 2026-08-07: "the '...' alignment is off"), AND IT WAS A
   SPECIFICITY DEFEAT RATHER THAN A WRONG VALUE. This rule is `(0,1,2)` -- one
   class plus two element selectors -- and `.row-menu-btn` is `(0,1,0)`, so this
   won every property the two share AND comes later in the file. `display`
   resolved to `inline-block`, which makes the icon button's `align-items` and
   `justify-content` INERT: the glyph was laid out as inline text in a 32px box
   and inherited `text-align: right` from `.members-table .col-menu`, which is
   exactly where the owner's screenshot shows it. `font-size: 18px` and
   `color: var(--muted)` lost the same way.

   SCOPED, NOT COUNTER-RULED, and not fixed with `!important` or a specificity
   bump -- layout rule 5: ask which container a rule was written for. These two
   declaration sets were never the same thing. What every popover summary needs
   is the disclosure reset below; what only a TEXT-LINK trigger needs is
   inline-block, the accent colour and `--text-sm`. A row menu is a 32px icon
   target, so it is excluded from the second set rather than overriding it
   afterwards, and `.row-menu-btn` then applies unopposed.

   THIS FIXES BOTH CALL SITES AT ONCE -- `settings/_user_rights.html` and
   `talent.html` share `.row-menu-btn`, so the defect was in both. The talent
   bank's Actions column IS measured (`UX-50` in tests/test_ui_alignment.py), and
   that block was re-run and read rather than assumed.

   `.col-menu` IS STILL SPELLED TWO WAYS -- 44px under the members table, and
   `width: 1%` (the table idiom for "as narrow as the content") under the
   positions table, whose own rule carries a comment saying a fixed width clipped
   its actions column. Two idioms for one column type is the same disease one
   layer up. NOT folded in here: the measurement did not implicate the 44px, and
   changing it would be a second, unmeasured change riding a fix. Recorded so it
   is not rediscovered a third time.

   The positions-table selector is deliberately NOT spelled out above: this comment
   naming it verbatim was enough to make check_tokens.py's `white-space: nowrap`
   arm slice from the prose instead of from the rule, and go red on correct code. */
details.popover > summary { list-style: none; cursor: pointer; }
details.popover > summary:not(.row-menu-btn) {
  display: inline-block; color: var(--accent); font-size: var(--text-sm);
}
details.popover > summary::-webkit-details-marker { display: none; }
/* The OPEN state stays unscoped: `--ink` for an open trigger is right for a text
   link and for an icon button alike, and it is what `.row-menu-btn:hover` already
   moves to. Only the three TEXT-LINK declarations above needed excluding. */
details.popover[open] > summary { color: var(--ink); }
details.popover > .popover-body {
  position: absolute; z-index: 30; top: calc(100% + var(--s1)); left: 0;
  min-width: 260px; max-width: 360px; padding: var(--s4);
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-md); box-shadow: var(--shadow-2);
  font-size: var(--text-sm); text-align: left; white-space: normal;
}

/* ---- .empty-state (§6.3) ---- */
.empty-state {
  text-align: center; padding: var(--s7) var(--s5);
  color: var(--muted); font-size: var(--text-base);
}
.empty-state .empty-icon { font-size: 28px; line-height: 1; margin-bottom: var(--s3); color: var(--faint); }
.empty-state p { margin: 0 0 var(--s4); }
.empty-state .btn { margin-top: 0; }

/* ---- <dialog> (§6.3) — destructive confirms ---- */
dialog {
  border: 1px solid var(--line); border-radius: var(--r-xl);
  background: var(--panel); color: var(--ink);
  box-shadow: var(--shadow-2); padding: var(--s5) var(--s6);
  max-width: 440px; font-size: var(--text-base); line-height: 1.55;
}
dialog::backdrop { background: rgba(31,30,27,.32); }
/* ---- UX-02b-1 / UX-E2: a <select> in a <dialog> stays inside the dialog ----
   ONE rule, every dialog, rather than a width class per template -- which is
   how the four surfaces that render a select inside a dialog came to be sized
   four different ways. A select with `width: auto` (or none) sizes to its
   WIDEST OPTION, so one long option name pushes the control past the dialog's
   content box; this caps every one of them regardless of what else it carries.

   WHAT THIS DOES NOT FIX, and it is the half the owner actually screenshotted:
   the native OPTION-LIST POPUP. A browser renders that above the page, outside
   the dialog's box, positioned by the user agent -- CSS cannot clip, move or
   contain it, and no rule here will. The real levers are fewer and shorter
   options (UX-02b-3, a product decision on `weights.py`'s role families that
   this lane is explicitly not authorised to make) or replacing the native
   control with a scripted listbox, which would be a worse copy of a browser
   feature and would owe its own keyboard and screen-reader behaviour. Recorded
   here rather than papered over, so the next reader does not re-attempt CSS. */
dialog select { max-width: 100%; }
dialog h2, dialog h3 { margin-top: 0; }
dialog .dialog-actions {
  display: flex; justify-content: flex-end; gap: var(--s2); margin-top: var(--s5);
}

/* ---- retention countdown chip (talent pool, §6.4) ---- */
.retention-chip { font-variant-numeric: tabular-nums; }

/* §3.2 /talent column-collapse order: 1) Last activity drops <1024px;
   2) Retention merges into the consent chip as a coloured dot <900px;
   3) Top skills truncate to 2 <820px. Name and Best score never collapse. */
.ret-dot { display: none; width: 9px; height: 9px; border-radius: 999px; }
.ret-dot.ok { background: var(--ok); }
.ret-dot.warn { background: var(--warn); }
.ret-dot.bad { background: var(--bad); }
@media (max-width: 1024px) {
  .talent-table .col-activity { display: none; }
}
/* 774–889px band: tighten the frame so /talent fits without horizontal
   overflow (known gap fix — complaint #2 tail) */
@media (max-width: 920px) {
  .content { padding: 24px 20px; }
  /* -- #58: every section body indented 18px from its own heading -----------
     MEASURED against the <h1> in the same .content, first panel, first child:

       /usage         clientWidth  890   h1 left 330   panel content left 348   +18
       /trust/consent clientWidth  900   h1 left 330   panel content left 348   +18
       /usage         clientWidth 1400   h1 left 350   panel content left 350     0
       /trust/consent clientWidth 1400   h1 left 350   panel content left 350     0

     (`/usage` is DELETED as of 2026-08-05. This table is the RECORD of the
     measurement that found the defect, kept because it is the evidence for the
     rule below; it is not a pair of paths to re-measure. `/trust/consent` still
     exists and still carries `.panel`.)

     Exactly 18px, two independent pages, exactly zero above the breakpoint --
     which locates the cause precisely: `.panel { padding: var(--s5) 0 }` gives
     the panel NO horizontal padding at any other width, and this rule was the
     only thing reintroducing it. So the ragged left edge below 920px was not a
     narrow-screen compromise, it was a shorthand that overwrote the horizontal
     zero while meaning to change only the vertical 24px.

     Kept as padding-block, so the vertical tightening this rule was actually
     for survives and the horizontal value falls back to the sheet's own 0.

     Guard: tests/test_layout_brand.py, marker `G27:`. */
  .panel { padding-block: 18px; }
  .talent-table th, .talent-table td { padding-right: 8px; }
  .talent-table .btn.sm { padding: 5px 8px; }
}
@media (max-width: 900px) {
  .talent-table .col-retention { display: none; }
  .talent-table .ret-dot { display: inline-block; }
}
@media (max-width: 820px) {
  .talent-table .skill-3 { display: none; }
}

/* ==============================================================   Slice D-RESTYLE — PRD v2.1 §6.4 inline-style purge (AC6 §8.6).
   ===================================================================== */

/* -- generic utilities (spacing on the 4px scale, visibility, width) -- */
.hidden { display: none; }
.w-full { width: 100%; }
.center { text-align: center; }
.nowrap { white-space: nowrap; }
.m-0 { margin: 0; }
.mt-0 { margin-top: 0; }
.mt-n1 { margin-top: -4px; }
.mt-1 { margin-top: var(--s1); }
.mt-2 { margin-top: var(--s2); }
.mt-3 { margin-top: var(--s3); }
.mt-4 { margin-top: var(--s4); }
.mt-5 { margin-top: var(--s5); }
.mb-0 { margin-bottom: 0; }
.mb-2 { margin-bottom: var(--s2); }
.mb-3 { margin-bottom: var(--s3); }
.ml-2 { margin-left: var(--s2); }
.col-narrow { width: 70px; }
.input-narrow { max-width: 140px; }
.input-mid { max-width: 280px; }
.input-wide { max-width: 360px; }
/* Owner comment 13b (2026-08-01). The Interview language select carries
   `.input-narrow` -- and it is THIS rule that sizes it, not the
   `input.set-ctl.input-narrow, select.set-ctl.input-narrow` pair near the top
   of the file: the control has `class="input-narrow"` and NOT `set-ctl`, so
   that pair never matches it and a fix aimed there would have changed every
   settings row and not this control.

   RE-MEASURED 2026-08-03, BECAUSE THE OPTION THIS WAS SIZED AGAINST IS GONE.
   The original `34ch` was measured against "Use the interview template's
   language" (37 characters) — and UX-03a DELETED that option the same week,
   along with the inheritance it selected. The live options are
   `module_fixtures.IV_LANGUAGE_MODES` — Cantonese (9), Mandarin (8), English (7)
   — plus the disabled `Mixed (retired)` (15) that only appears on rows stored
   before the removal. So the widest possible content is 15 characters and the
   control was being held ~2.3x wider than anything it can display, on the same
   surface where UX-03a's third ask was that this drop-down "should be properly
   resized ... to avoid making it look bigger". `18ch` is the 15-character worst
   case plus room for the native chevron's reserved padding.
   A MEASUREMENT OUTLIVING THE CONTENT IT WAS MEASURED AGAINST is the same shape
   as the dead fallbacks arm 8.11 now catches: correct when written, silently
   wrong after the thing it referenced moved. If an option is ever added here,
   re-measure — do not add `ch` "to be safe".

   Sized in `ch` so it tracks the option text rather than a pixel guess, capped
   at 100% so it can never be the thing that overflows its column, and scoped to
   the id rather than widening `.input-narrow` -- which 8 templates use in 33
   places for controls that are narrow ON PURPOSE.

   This control renders in THREE places -- the New-role modal's interview step,
   the role assessments panel, and the position peek. It is NOT on
   /positions/new?step=3; that URL does not exist.

   The plan's companion `.chip-row { min-width: 0; flex-wrap: wrap; }` is NOT
   added: measured, `.chip-row` ALREADY computes both, and the description is a
   `<p class="hint">` on the line BELOW the row rather than a sibling beside it,
   so the overflow that rule was written for cannot occur here. */
select#ivLanguage { width: auto; min-width: 18ch; max-width: 100%; }
.sec-head .meta { font-weight: 400; }

/* -- sidebar (base.html) -- */
/* `.brand-logo` IS DELETED with the company logo (cut-down lane L5). Its one
   call site was join.html's uploaded mark; that template now paints the
   `.brand-mark` initial unconditionally, which is what it already painted on
   every deployment. */
/* G17 (owner, 2026-07-31): the account row read as more padded than the rest of
   the sidebar, and it was -- measured, not eyeballed. Nav items sit 12px from
   the edge (.sidebar-nav 2px + .nav-item 10px); this block sat at 13px
   (.side-foot 4px + .side-user 9px) with 16px above its content. The nav rail is
   the reference, not the brand row: it is the majority of the sidebar and it is
   what a row is read against.
   .side-foot's TOP padding stays -- that is the separator gap between the nav
   and the account block, not row padding. And the account row is still taller
   than a nav row because .side-avatar is 34px against .nav-ico's 18px: that is
   content, not padding, and shrinking the avatar to close it would be fixing the
   wrong thing. */
.side-foot { margin-top: auto; padding: 8px 2px 4px; }
/* THE CONTRACT PACKAGE READOUT'S RULES STOOD HERE (owner, 2026-08-05) --
   `.side-quota`, `.sq-row`, `.sq-lbl`, `.sq-num`, the `.is-low` colour and the
   collapsed-rail overrides. They are deleted with the entitlement they reported
   (owner ruling 2026-09-01, the internal-tool cut-down).

   WHAT THE BLOCK RECORDED IS WORTH KEEPING AND IS RESTATED HERE, because it was
   an applied example of two live layout rules rather than a description of one
   component. The grid was on the ROW (`minmax(0,1fr) auto`), never per item, so
   both pools shared a right edge whatever their labels were -- rule 2. And it
   deliberately was NOT `.set-row`: that primitive's control track is measured
   for a full-width settings column, and importing it into a 310px rail holding a
   bare count is exactly the "correct rule, wrong container" defect that was six
   of the last nine -- rule 5. Anything new in `.side-foot` inherits both.

   ITS COVERAGE GAP OUTLIVES IT AND IS STILL OPEN: tests/test_ui_alignment.py's
   SURFACES list does not reach the sidebar, so nothing measures `.side-foot` at
   any viewport. The readout's figures came from a one-off Playwright run. */
.side-divider { height: 1px; background: var(--line-soft); margin: 6px 8px; }
.side-user { display: flex; align-items: center; gap: 11px; padding: 7px 10px; border-radius: 11px; }
.side-avatar { width: 34px; height: 34px; flex-shrink: 0; border-radius: 999px;
  background: var(--accent-tint); color: var(--accent);
  display: flex; align-items: center; justify-content: center; font-size: 12.5px; font-weight: 600; }
.side-user-text { flex: 1; min-width: 0; line-height: 1.25; }
.side-user .name { display: block; font-size: 13px; font-weight: 600; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.side-user .role { display: block; font-size: 11px; color: var(--faint); }
.side-signout { flex-shrink: 0; color: var(--faint); display: flex; padding: 6px; border-radius: 8px; }
.side-signout:hover { background: var(--hover); color: var(--muted); }

/* -- public (no-sidebar) pages: apply.html wide variant -- */
.public-wrap.wide { max-width: 720px; margin-top: 36px; }

/* -- long-form text blocks (JD, company background, CV text, closing copy) -- */
.prose { white-space: pre-wrap; font-size: var(--text-base); color: var(--ink); }
.prose-scroll { max-height: 380px; overflow: auto; border: 1px solid var(--line);
                border-radius: 8px; padding: 14px; }
/* v2 candidate pages read as one continuous column — no inner scroll boxes. */
.candidate-page .prose-scroll { max-height: none; overflow: visible; border: 0;
                                border-radius: 0; padding: 0; }
.prose-box { white-space: pre-wrap; background: var(--bg); border: 1px solid var(--line);
             border-radius: 8px; padding: 14px; font-size: var(--text-base); color: var(--ink); }
.cv-pre { white-space: pre-wrap; font-size: var(--text-sm); color: var(--ink);
          margin-top: var(--s3); }

/* -- detail.html (hero screen, full purge) -- */
.card .value.value-md { font-size: 20px; }
.card .value.value-sm { font-size: 16px; }
details.panel > summary { cursor: pointer; font-weight: 600; }
.form-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
            margin-top: var(--s4); }
.form-row input { width: auto; }
.form-row.hidden { display: none; } /* beats .form-row on specificity so .hidden works */
.input-name { width: 220px; }
.input-email { width: 240px; }
.link-input { width: 100%; font-size: var(--text-xs); }
.copy-row { display: flex; gap: 6px; margin-top: 6px; align-items: center; }
.copy-row .ml-auto { margin-left: auto; }
/* Language switcher (sidebar + candidate pages). */
.langswitch { display: inline-flex; gap: 2px; flex-wrap: wrap; }
.langswitch .langopt { font-size: var(--text-xs); padding: 2px 8px; border-radius: 999px;
  color: var(--muted); text-decoration: none; white-space: nowrap; }
.langswitch .langopt:hover { color: var(--ink); background: var(--line-soft, var(--line)); }
.langswitch .langopt.active { color: var(--accent); background: var(--accent-tint); font-weight: 600; }

/* Assessment stage tracker (pool): three fixed stages, coloured when done,
   outlined when in progress, faint when not started. */
.stagetrack { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
/* .st-item — shape from the shared pill group; states below add dot + tone/border. */
.st-item::before { content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; }
a.st-item.done { text-decoration: none; }
.st-item.done.st-info { color: var(--accent);   background: var(--accent-tint); }
.st-item.done.st-warn { color: var(--warn-ink); background: var(--warn-tint); }
.st-item.done.st-ok   { color: var(--ok-ink);   background: var(--ok-tint); }
.st-item.active { color: var(--muted); border-color: var(--line); }
.st-item.todo { color: var(--muted); opacity: .5; border-color: var(--line); border-style: dashed; }

/* Drawer invite rows: one block per assessment with its shareable link. */
.invite-rows { display: flex; flex-direction: column; gap: 12px; }
.invite-row { border: 1px solid var(--line); border-radius: var(--r-md); padding: 10px 12px; }
.invite-row-h { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }

/* Minimal invite-to-assessment list at the top of the drawer report: one quiet
   line per assessment (name + status, actions on the right), no boxes. */
.assess-list { display: flex; flex-direction: column; }
.assess-row { display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--line-soft); }
.assess-row:last-child { border-bottom: 0; }
.assess-name { display: inline-flex; align-items: center; gap: 7px; font-weight: 500; color: var(--ink); }
.assess-acts { display: inline-flex; align-items: center; gap: 8px; flex: 0 0 auto; }
.assess-acts form { margin: 0; }
.assess-lbl { font-size: var(--text-xs); color: var(--muted); }
.assess-link { font-size: var(--text-sm); font-weight: 500; color: var(--accent); text-decoration: none; }
.assess-link:hover { color: var(--accent-d); }
.assess-overall { display: inline-block; margin-top: 12px; font-size: var(--text-sm);
  font-weight: 600; color: var(--ink); text-decoration: none; }
.assess-overall:hover { color: var(--accent); }
.preview-body { min-height: 160px; width: 100%; font-size: var(--text-xs); }
.history-item { padding: 10px 0; border-top: 1px solid var(--line); }
.q-text { font-size: 16px; white-space: pre-wrap; }

/* New Design port */
/* detail.html scorecard header: enlarged CV-score pill (mockup candidate.html) */
.score-pill.lg { font-size: var(--text-xl); padding: var(--s2) var(--s4); }

/* spacing utility (4px scale, matches .mt-*) */
.mb-4 { margin-bottom: var(--s4); }

/* positions.html: monospace public application-link box (mockup positions.html) */
.apply-link { display: inline-flex; align-items: center; gap: var(--s2);
              font-family: var(--font-mono); font-size: var(--text-xs); color: var(--muted);
              background: var(--bg); border: 1px solid var(--line); border-radius: var(--r-md);
              padding: 6px 12px; min-width: 0; max-width: 100%; overflow-wrap: anywhere; }

/* SVG value bars (report.html funnel/months, interview_report.html pillar meters):
   widths are SVG rect geometry, colours are classes — zero inline styles (§8.6). */
.bar-svg { display: block; width: 100%; height: 10px; border-radius: 5px;
           background: var(--line-soft); overflow: hidden; }  /* v2: thinner */
.bar-svg rect { fill: var(--accent); }
.bar-svg.alt rect { fill: var(--neutral); }                   /* v2: warm grey alt */
.meter-svg { display: block; width: 100%; height: 8px; border-radius: 5px;
             background: var(--line-soft); overflow: hidden; }
.meter-svg rect { fill: var(--accent); }
/* §6 pillar accents — one fixed colour per validity pillar, everywhere it appears */
.pillar-row.p-technical_assessment .meter-svg rect { fill: var(--pillar-tech); }
.pillar-row.p-iq                    .meter-svg rect { fill: var(--pillar-iq); }
.pillar-row.p-values                .meter-svg rect { fill: var(--pillar-val); }
.pillar-row.p-hr                     .meter-svg rect { fill: var(--pillar-hr); }
.pillar-row.p-personality           .meter-svg rect { fill: var(--pillar-pers); }
.pillar-row.trait .meter-svg rect { fill: var(--pillar-pers); } /* Big Five = Personality pillar */
.pillar-dot { display: inline-block; width: 8px; height: 8px; border-radius: 999px; margin-right: 7px; vertical-align: 1px; flex: none; }
.pillar-dot.p-technical_assessment { background: var(--pillar-tech); }
.pillar-dot.p-iq          { background: var(--pillar-iq); }
.pillar-dot.p-values      { background: var(--pillar-val); }
.pillar-dot.p-hr          { background: var(--pillar-hr); }
.pillar-dot.p-personality { background: var(--pillar-pers); }
.bar-row.wide { grid-template-columns: 150px 1fr 48px; }
.bar-row .bar-dashed { height: 10px; border: 1px dashed var(--line); border-radius: 6px; }

/* interview_report.html: transcript & per-answer blocks (mockup interview_report.html) */
.qa-item { padding-bottom: var(--s4); margin-bottom: var(--s4);
           border-bottom: 1px solid var(--line-soft); }
.qa-item:last-child { padding-bottom: 0; margin-bottom: 0; border-bottom: 0; }
.qa-head { display: flex; align-items: flex-start; gap: var(--s3); margin-bottom: 7px; }
.qa-q { flex: 1; font-size: 13.5px; font-weight: 600; line-height: 1.45; }
.qa-follow { font-size: 11px; font-weight: 600; color: var(--accent);
             flex-shrink: 0; margin-top: 2px; }
.qa-a { font-size: var(--text-sm); line-height: 1.6; color: var(--muted); margin: 0; }
/* `.mcq-review*` DELETED (offscale-headroom, 2026-08-09): the aptitude answer
   review styled the written test, which went with the edition collapse. No
   template, script or suite names any of the five selectors. */
.video-player { width: 100%; max-width: 640px; border-radius: 11px; background: var(--ink); }
.stars { color: var(--warn); }
.col-mid { width: 160px; }

/* batch 3 (tests · test papers · aptitude · questions · settings · login) */
/* width / whitespace utilities */
.w-auto { width: auto; }
.pre-wrap { white-space: pre-wrap; }
/* textarea heights (4px-scale steps; default textarea is 120px) */
.ta-xs { min-height: 52px; }
.ta-sm { min-height: 70px; }
.ta-md { min-height: 90px; }
.ta-lg { min-height: 180px; }
.ta-template { min-height: 220px; font-family: var(--font-sans); }
/* question-editor blocks (mockup test_manage.html / aptitude.html) */
.q-block { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--line); }
/* Shared scoring rubric block (template editor): one editor for the rubric all
   questions share, set apart from the question rows with a soft tinted card. */
.rubric-shared { border: 1px solid var(--line); border-radius: 10px;
  padding: 12px 14px; background: var(--neutral-tint); }
.q-item { margin-bottom: 14px; }
.q-card { margin: 8px 0 4px; padding: 12px 14px; border: 1px solid var(--line);
          border-radius: var(--r-md); background: var(--bg); }
.field-card { border: 1px solid var(--line); border-radius: var(--r-md);
              padding: 12px 14px; margin-top: 10px; }
.divider { border: none; border-top: 1px solid var(--line); margin: 18px 0; }
.link-summary { cursor: pointer; font-size: var(--text-sm); color: var(--accent); }
/* test_report.html Big Five trait rows (wider right column than .pillar-row) */
.pillar-row.trait { grid-template-columns: 150px 1fr 110px; }
/* settings.html: logo preview. `.team-edit` sat here until Phase 28 deleted
   the Teams management section from Roles & permissions (UX-14); its only
   subject is gone, and a rule with no subject is how the next reader concludes
   the panel still exists. */
.logo-preview { max-height: 60px; border: 1px solid var(--line);
                border-radius: var(--r-sm); padding: var(--s1); background: var(--panel); }
/* login.html (mockup): centered standalone card */
body.centered { align-items: center; justify-content: center; }
.login-wrap { width: 100%; max-width: 400px; padding: 20px; }
.login-wrap .brand { justify-content: center; padding-bottom: 24px; }
/* v2 login: borderless column on paper, wordmark + serif greeting */
.login-wrap.v2 { max-width: 360px; }
.login-brand { display: flex; justify-content: center; padding-bottom: 10px; }
.login-brand .wordmark { font-size: 19px; }
.login-title { text-align: center; margin: 0 0 22px; font-size: 26px; }
/* `.wsc-row` / `.wsc-row > .hint` / `.wsc-row > .wsc-name` STOOD HERE and are
   DELETED with `templates/workspace_choose.html` (internal-tool cut-down lane
   L4, 2026-09-02). They were named for that one page -- `wsc` is
   workspace-choose -- and it had no other caller; the WORKSPACE CHOOSER block
   in tests/test_ui_alignment.py that measured them goes with it.

   DELETED RATHER THAN KEPT AS AN ORPHAN, which is the opposite of the call L3
   made for `.set-2up`, and the difference is whether a rule is a PRIMITIVE.
   `.set-2up` is a settings-pane shape the next two-up row would otherwise
   hand-roll, so keeping it prevents layout rule 1's failure. There is no next
   workspace chooser, and what these rules taught is layout rule 2 -- THE GRID
   LIVES ON THE ROW, never per-item, and `space-between` / `margin-left: auto`
   align each row to its own content width -- which is stated in CLAUDE.md, at
   `.set-row` itself, and counted by ratchet 8.13's drift primitives. A rule
   with no subject is how the next reader concludes the page still exists.

   THE DEFECT IS WORTH KEEPING ON FILE because the CAUSE outlives the page:
   the row was a submit `<button>`, so it inherited `display: inline-flex` +
   `justify-content: center` from `.btn` and centred the name+hint pair as a
   GROUP -- every button box identical (measured 560->880, spread 0.00px) while
   the content inside each aligned to its OWN width, 12.08px apart on the name's
   left edge and 44.78px apart on the hint's right. Nothing was wrong with any
   one row. `.btn` was deliberately NOT touched then and is untouched now. */


/* ============================================================= */
/* Dashboard — action-queue hero (Dashboard Directions port)      */
/* ============================================================= */
.dash-hero { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--s5); }
.dash-hero .btn { flex-shrink: 0; }

/* Home launcher: a centered command-center — the Verqa mark + a time-based
   greeting, then an "Ideas for you" list of quick hiring actions. The triage
   queue continues below in .dash-main. */
.home-launch { max-width: 1240px; margin: 0 auto; padding: clamp(24px, 6vh, 64px) 0 8px; }
/* Two-zone body below the centered greeting (wide screens): attention cards
   as the main column, "Ideas for you" as a right rail. */
.home-zones { display: grid; grid-template-columns: 1fr 340px; gap: 48px;
  margin-top: clamp(24px, 4vh, 44px); align-items: start; }
/* Ideas rail: hairline divider on its left */
.home-zones .home-ideas { margin-top: 0; border-left: 1px solid var(--line-soft); padding-left: 32px; }
/* Attention cards: fixed 2-up in the main zone (replaces auto-fill) */
.home-zones .attn-wrap { margin-top: 0; }
.home-zones .attn-cards { grid-template-columns: 1fr 1fr; }
.home-zones .attn-card { padding: 18px 20px; }
/* Collapse back to the single column on narrow viewports */
@media (max-width: 1100px) {
  .home-launch { max-width: 640px; }
  .home-zones { grid-template-columns: 1fr; gap: 0; }
  .home-zones .home-ideas { border-left: 0; padding-left: 0; margin-top: clamp(28px, 5vh, 52px); }
}
.home-greet { text-align: center; }
/* Inline with the title text so a wrapping greeting keeps the star on line 1. */
.home-mark { color: var(--accent); display: inline-flex; vertical-align: -2px; margin-right: 14px; }
.home-greet-txt { font-size: clamp(30px, 4.5vw, 44px); font-weight: 600; letter-spacing: -.02em;
  color: var(--ink); text-align: center; line-height: 1.1; }

/* THE BIG "ask" BOX IS DELETED (owner ruling 2026-08-10). Seven rules stood
   here: the `.home-ask` wrapper and its centring `auto` margins, the
   Spotlight-style `.home-ask-input` pill in three states, the leading
   `.home-ask-ico` and the `.home-ask-hint` line under it. The hero above keeps
   its star, module name and greeting; only the search went. */
/* Attention summary cards: the "N need your attention" total broken into one
   card per action queue (count + label + a Review link to its section below). */
.attn-wrap { margin-top: clamp(24px, 4vh, 40px); }
.attn-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 12px; margin-top: 6px; }
.attn-card { display: flex; flex-direction: column; border: 1px solid var(--line);
  border-radius: 12px; padding: 15px 16px; text-decoration: none; color: var(--ink);
  background: transparent; }
.attn-card:hover, .attn-card:focus-visible { border-color: var(--accent); background: var(--hover); outline: none; }
/* The `outline: none` above is for the HOVER half. Cancelling it for
   :focus-visible too left a keyboard user with a background tint and a border
   -colour change — a colour-only focus indicator (WCAG 2.4.7 / 1.4.11), on the
   "Needs your attention" card, which is a primary control on the landing page.
   Restore a ring, exactly as .idea-row already does further down this file.
   Guard: tests/test_layout_brand.py, marker `G19:`. */
.attn-card:focus-visible { box-shadow: 0 0 0 2px var(--accent); }
.attn-n { font-size: 26px; font-weight: 600; line-height: 1; color: var(--accent); }
.attn-title { font-weight: 600; font-size: 14.5px; margin-top: 8px; }
.attn-desc { color: var(--muted); font-size: 12.5px; margin-top: 3px; }
.attn-cta { margin-top: 12px; color: var(--accent); font-size: 13px; font-weight: 500; }
/* Step card with a paired Settings link (Assessment home get-started). */
.attn-cta-row { display: flex; gap: 16px; align-items: center; margin-top: 12px; flex-wrap: wrap; }
.attn-cta-row .attn-cta { margin-top: 0; }
.attn-settings { color: var(--muted); font-size: 13px; }
.attn-settings:hover { color: var(--accent); }
/* Triage buckets (Decide / Advance / Chase): the "needs your attention" total
   collapsed into three cards, each with a total, breakdown lines that deep-link
   to a single status, and a "Review →" link that opens the whole bucket. */
/* -- #59: the triage buckets collapsed to ONE column on a 1280px laptop -----
   MEASURED on the dashboard at documentElement.clientWidth 1280:

     sidebar                       310px
     .content horizontal padding   40px / 40px
     .home-zones width             890      (.content box 970)
     .home-zones columns           502px 340px, gap 48px
     main zone                     890 - 340 - 48 = 502   <- confirmed exactly
     .bucket-cards columns         502px          <- ONE track
     (max-width: 1100px)           false          <- had not fired

   Every number in the mechanism is exact. `auto-fit` is already
   container-relative, so the fault is not the query, it is the 260px minimum:
   two tracks needed 2 x 260 + 16 = 536px inside a 502px column, so the grid
   correctly gave up and produced one. The card row was sized against the
   window while living in a right-railed 502px column.

   220px makes two tracks cost 2 x 220 + 16 = 456px, which fits 502 with room,
   and three still cost 692 so nothing crowds on a genuinely wide canvas. The
   `min(100%, ...)` guard is what stops the track exceeding a container
   narrower than the minimum itself -- the classic auto-fit overflow.

   THE "BELOW THE FOLD" CONSEQUENCE DOES NOT REPRODUCE and is not the reason
   for this change. Measured belowFold = 0: this instance renders two buckets,
   at tops 185 and 363 (bottom 524) against 800px. A third would end near 702,
   still above 800. Fix the collapse; do not cite the fold.

   Guard: tests/test_layout_brand.py, marker `G27:`. */
.bucket-cards { display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
  gap: var(--s4); margin-top: var(--s2); }
.bucket-card { display: flex; flex-direction: column; border: 1px solid var(--line);
  border-radius: 12px; padding: var(--s5); min-height: 160px; background: transparent; }
.bucket-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.bucket-title { font-weight: 600; font-size: var(--text-lg); }
.bucket-n { font-size: var(--text-2xl); font-weight: 650; line-height: 1; color: var(--ink); }
.bucket-lines { display: flex; flex-direction: column; gap: 4px; margin-top: var(--s4); }
.bucket-line { display: flex; gap: 8px; align-items: baseline; text-decoration: none;
  color: var(--muted); font-size: var(--text-sm); padding: 4px 6px; margin: 0 -6px; border-radius: 6px; }
.bucket-line:hover, .bucket-line:focus-visible { background: var(--hover); color: var(--ink); outline: none; }
/* Colour-only focus indicator — see the note at .attn-card. These are the
   Decide / Advance rows on the landing page. Guard: `G19:`. */
.bucket-line:focus-visible { box-shadow: 0 0 0 2px var(--accent); }
.bucket-line-n { font-weight: 600; color: var(--ink); min-width: 1.4em; }
.bucket-line-desc { flex: 1; }
.bucket-review { margin-top: auto; padding-top: var(--s3); color: var(--accent); font-size: var(--text-base); font-weight: 500; text-decoration: none; }
.bucket-review:hover { text-decoration: underline; }
/* The inline /ask answer panel (`.home-ask-answer` / `.home-ask-msg` /
   `.home-ask-go`) is DELETED. It was styling for markup `static/search.js` built
   under the hero omnibox, and it had been unreachable since owner comment 2
   (2026-08-01) took `data-ask-url` off the input -- the script's whole /ask
   branch never ran after that. search.js is deleted at the 2026-08-10 home
   search retirement and these four rules go with it. POST /ask itself is
   untouched and still routed; it simply has no UI entry, which is the standing
   ruling. */
/* Interview questions sub-page — question rows (HR tab + per-position drawer).
   A <details> whose summary is the Question N / text / "Updated" / Edit row;
   opening reveals the inline editor (.qrow-form). Read-only rows reuse the
   same head without the disclosure. */
.ivqrow { border: 1px solid var(--line); border-radius: 10px; margin-top: 8px; background: transparent; }
.ivqrow[open] { border-color: var(--accent); }
.ivqrow-head { display: flex; align-items: center; gap: 12px; padding: 11px 14px; cursor: pointer; list-style: none; }
.ivqrow-head::-webkit-details-marker { display: none; }
.ivqrow-ro .ivqrow-head { cursor: default; }
.ivqrow-idx { flex-shrink: 0; font-weight: 600; font-size: 13px; color: var(--muted); min-width: 74px; }
.ivqrow-text { flex: 1 1 auto; font-size: 13.5px; color: var(--ink); }
.ivqrow-meta { flex-shrink: 0; display: inline-flex; align-items: center; gap: 10px; }
/* Centrally-managed rows (HR questions in the position drawer): a muted hint
   pointing at the central tab instead of an Edit affordance. */
.ivqrow-ro-note { font-size: 12px; }
.ivqrow-ro-note a { color: var(--muted); text-decoration: underline; }
.ivqrow-edit { color: var(--accent); font-size: 13px; font-weight: 500; }
.ivqrow[open] .ivqrow-edit { color: var(--muted); }
.ivqrow-form { padding: 0 14px 14px; border-top: 1px solid var(--line-soft); }
/* Drag-to-reorder: grip handle + the row's dragging state, and the Save-order bar. */
.ivqrow-grip { flex-shrink: 0; cursor: grab; color: var(--faint); font-size: 15px; line-height: 1; user-select: none; }
.ivqrow-grip:active { cursor: grabbing; }
.ivq-dragging { opacity: .55; outline: 2px dashed var(--accent); }
/* Add-a-role (written test): a full-width form toggled by the + Add button. */
.wt-add-form { margin: 12px 0; padding: 14px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); }
.wt-add-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; }
.wt-add-form label { display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--muted); }
.wt-add-jd { margin-top: 10px; }
.ivqrow-form label { display: block; margin: 10px 0 3px; }
.ivqrow-form textarea, .ivqrow-form select { width: 100%; }
/* "+ Add …" disclosure: same row shell, dashed to read as an add affordance. */
.ivq-add { border-style: dashed; }
.ivq-add[open] { border-style: solid; }
.ivq-add > summary { justify-content: flex-start; color: var(--accent); font-weight: 500; }
.ivq-add > summary .ivq-add-label { font-size: 13.5px; }

/* A position row on the Questions-by-position tab: whole row opens its drawer. */
.qpos-row { display: flex; align-items: center; gap: 12px; margin-top: 8px;
  padding: 12px 14px; border: 1px solid var(--line); border-radius: 10px;
  text-decoration: none; color: var(--ink); background: transparent; }
.qpos-row:hover, .qpos-row:focus-visible { border-color: var(--accent); background: var(--hover); outline: none; }
/* Colour-only focus indicator — see the note at .attn-card. Guard: `G19:`. */
.qpos-row:focus-visible { box-shadow: 0 0 0 2px var(--accent); }
.qpos-main { flex: 1 1 auto; display: inline-flex; align-items: center; gap: 8px; }
.qpos-title { font-weight: 600; font-size: 14px; }
.qpos-meta { flex-shrink: 0; }
.qpos-cta { flex-shrink: 0; color: var(--accent); font-size: 13px; font-weight: 500; }

.home-ideas { margin-top: clamp(28px, 5vh, 52px); }
.home-ideas-h { color: var(--faint); font-size: 13px; font-weight: 500; margin: 0 0 6px; padding: 0 12px; }
/* One tappable idea row: rounded-square icon + label, whole row highlights. */
.idea-row { display: flex; align-items: center; gap: 16px; width: 100%;
  padding: 12px; border: 0; border-radius: 12px; background: transparent;
  color: var(--ink); font: inherit; text-align: left; text-decoration: none; cursor: pointer; }
.idea-row:hover, .idea-row:focus-visible { background: var(--hover); outline: none; }
.idea-row:focus-visible { box-shadow: 0 0 0 2px var(--accent); }
.idea-ico { width: 40px; height: 40px; flex: 0 0 auto; border-radius: 10px;
  border: 1px solid var(--line); display: flex; align-items: center; justify-content: center;
  color: var(--muted); background: transparent; }
.idea-row:hover .idea-ico { color: var(--accent); border-color: var(--accent); }
.idea-label { font-size: var(--text-md); font-weight: 500; }

.focus-banner { display: flex; align-items: center; gap: var(--s5);
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-xl);
  padding: var(--s4) var(--s5); margin: var(--s5) 0; box-shadow: var(--shadow-1); }
.focus-ico { width: 52px; height: 52px; flex-shrink: 0; border-radius: 14px;
  background: var(--accent-tint); color: var(--accent);
  display: flex; align-items: center; justify-content: center; }
.focus-title { font-size: var(--text-lg); font-weight: 600; color: var(--ink); letter-spacing: -.01em; }
.focus-pills { display: flex; flex-wrap: wrap; gap: var(--s2); margin-top: 9px; }
/* .focus-pill — shape from the shared pill group; tone maps onto chip triplets. */
.focus-pill.warn   { background: var(--warn-tint); color: var(--warn-ink); }
.focus-pill.accent { background: var(--accent-tint); color: var(--accent); }
.focus-pill.bad    { background: var(--bad-tint); color: var(--bad-ink); }

.dash-main { margin-top: var(--s5); margin-bottom: var(--s6); }
.queue-col { display: flex; flex-direction: column; gap: var(--s5); }
.queue { display: flex; flex-direction: column; gap: 10px; scroll-margin-top: 20px; }
/* Queue header is a full-width toggle button: chevron + dot + label + count.
   The count stays in the header so it is visible even when collapsed. */
.queue-head { display: flex; align-items: center; gap: var(--s2); margin: 0;
  width: 100%; border: 0; background: transparent; text-align: left; cursor: pointer;
  padding: 4px 2px; border-radius: 8px;
  font-size: var(--text-sm); font-weight: 600; text-transform: uppercase; letter-spacing: .06em;
  font-family: inherit; color: var(--muted); }
.queue-head:hover { background: var(--hover); color: var(--ink); }
.queue-head:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.queue-chev { flex-shrink: 0; color: var(--faint); transition: transform .15s ease; }
.queue.is-collapsed .queue-chev { transform: rotate(-90deg); }
.queue-count { color: var(--faint); font-variant-numeric: tabular-nums; }
.queue-count::before { content: "· "; }
.queue-body { display: flex; flex-direction: column; }
.qdot { width: 7px; height: 7px; border-radius: 999px; flex-shrink: 0; }
.qdot.warn { background: var(--warn); } .qdot.accent { background: var(--accent); } .qdot.bad { background: var(--bad); }
.qdot.ok { background: var(--ok); }

/* At-a-glance to-do strip under the greeting: one pill per non-empty queue,
   each jumping to its section below. */
.todo-strip { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
/* .todo-chip — shape from the shared pill group; a bordered, clickable variant. */
.todo-chip { border-color: var(--line); background: var(--panel); color: var(--ink);
  text-decoration: none; transition: border-color .15s ease, background .15s ease; }
.todo-chip:hover { border-color: var(--ring); background: rgba(28,27,24,.025); }
:root[data-theme="dark"] .todo-chip:hover { background: rgba(236,233,226,.06); }
.todo-chip .seg-count { background: var(--line-soft); color: var(--muted); }

/* `.qcard*` DELETED (offscale-headroom, 2026-08-09): the queue CARD was replaced
   by the `.qrow-*` row and nothing emits any of its eleven classes. The three
   `.qrow-top .qcard-name` rules went with it -- a live container styling a dead
   descendant matches nothing, which is why the row survives untouched. */
.btn.reject { background: var(--panel); color: var(--bad-ink); border: 1px solid var(--bad-tint); }
.btn.reject:hover { background: var(--bad-tint); border-color: var(--bad-tint); }

.rail { display: flex; flex-direction: column; gap: var(--s4); }
.rail-card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-xl); padding: 6px 20px; }
/* `.kpi-*` DELETED (offscale-headroom, 2026-08-09): the whole four-rule family is
   unreferenced -- `kpi-` appears nowhere outside this sheet. It also carried a
   `justify-content: space-between`, so the drift ratchet falls with it. */
.rail-link { font-size: var(--text-sm); color: var(--accent); text-decoration: none; }
.rail-link:hover { color: var(--accent-d); }

.allclear { display: flex; flex-direction: column; align-items: center; text-align: center; gap: var(--s3);
  padding: 48px 24px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-xl); }
.allclear-ico { width: 56px; height: 56px; border-radius: 999px; background: var(--ok-tint); color: var(--ok-ink);
  display: flex; align-items: center; justify-content: center; }
.allclear-title { font-size: var(--text-lg); font-weight: 600; color: var(--ink); }
.allclear-sub { max-width: 340px; }

/* ============================================================= */
/* Nav 3a interactions: search box + dropdown menus               */
/* ============================================================= */
.side-chev { margin-left: auto; flex-shrink: 0; color: var(--faint); }

.side-user-wrap { position: relative; }
.side-ver { font-size: 10px; color: var(--faint); white-space: nowrap; }
:root[data-rail="collapsed"] .side-ver { display: none; }
.side-user { width: 100%; background: none; border: none; cursor: pointer;
  font-family: inherit; text-align: left; color: inherit; }
.side-user:hover { background: var(--hover); }

/* ---- .filter-row AND .filter-pills: DELETED 2026-09-01 ---------------------
   Their only two consumers were `templates/trust_audit.html` and
   `templates/trust_consent.html`, and the owner deleted the Trust Center pages
   (the internal-tool cut-down). Nothing in `templates/` or `static/` names
   either class now, so the rules -- and the `[data-livefilter]` descendants of
   `.filter-row` further down this sheet -- are dead CSS and go with the markup.

   WHAT THEY WERE FOR, and the part that generalises. `.filter-pills` carried
   `--pill-h: 30px` because the audit log's filter row put two
   `input[type="date"]` pills beside several `<select>` pills, and a date
   input's intrinsic content box is taller -- the same cause `.filter-toolbar`
   below still handles for the interview-history page, but a different pattern
   (bare pills, no label element), so it needed its own scoped rule. The height
   was deliberately NOT put on the shared `.pill-select`: that geometry is also
   the pool filter pills and the bulk-bar select, and 30px was that row's.
   Guard: G10 in tests/test_layout_brand.py, converted to assert the absence.

   THE ONE-CUSTOM-PROPERTY IDIOM SURVIVES ELSEWHERE IN THIS SHEET and is still
   cited by name further down; a height consumed by two controls through one
   property is what stops the pair drifting. */
/* Pill-shaped selects — ONE shared look across the pool filter pills, the
   inline candidate-status select, and the bulk-bar select. Geometry converges
   here; each rule below only sets its own fill + chevron colour. */
.pill-select, input.pill-select, .status-select, .bulk-float select {
  width: auto; min-width: 0; border-radius: 999px; cursor: pointer;
  padding: 6px 30px 6px 13px;
  font-family: var(--font-sans); font-size: var(--text-xs); font-weight: 500;
  appearance: none; -webkit-appearance: none;
  background-repeat: no-repeat; background-position: right 12px center; }
.pill-select, input.pill-select { background-color: var(--panel); border: 1px solid var(--line); color: var(--ink);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path d='M1 1l4 4 4-4' fill='none' stroke='%238b8f98' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>"); }
.pill-select:hover { border-color: var(--faint); }
.pill-select option { color: var(--ink); background: var(--panel); }
.filter-clear { font-size: 12.5px; color: var(--faint); text-decoration: none; padding: 4px 6px; }
.filter-clear:hover { color: var(--accent); }
/* Labelled filter toolbar (Interview history): one cohesive row — a label above
   each control, consistent control widths + heights, even gaps, and the
   Filter/Clear actions inline at the end (not floated to the far edge). Wraps
   responsibly on narrow screens. Reuses the shared .switch-copy label + .set-ctl
   control look so it matches the settings surfaces. */
/* G10 (owner, 2026-07-31): ONE control height for the whole row.
   `input[type="date"]` has a taller intrinsic content box than a text input or a
   <select> -- the browser's internal date spinner is taller than a text run at
   the same font size. With no explicit height each control sized to its own
   content, so `align-items: flex-end` could line up the control BOTTOMS and
   nothing else: the labels above them staggered and the Filter/Clear buttons sat
   off the row's baseline. An explicit shared height makes every alignment mode
   agree, which is why this is a height fix and not an align-items fix.

   `box-sizing: border-box` is not optional here -- without it the height and the
   existing padding add rather than contain, and the dates get taller again. */
.filter-toolbar { --filter-ctl-h: 38px;
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--s3); }
.filter-toolbar .switch-copy { flex: 0 0 auto; }
.filter-toolbar input.set-ctl, .filter-toolbar select.set-ctl { width: 100%; max-width: none; }
.filter-toolbar .set-ctl,
.filter-actions .btn { height: var(--filter-ctl-h); box-sizing: border-box; }
/* The date input additionally needs its internal box neutralised: Chromium gives
   ::-webkit-datetime-edit its own vertical padding, which re-introduces the
   difference inside a fixed-height box. */
.filter-toolbar input[type="date"].set-ctl { padding-top: 0; padding-bottom: 0; }
.filter-toolbar .fld-grow { flex: 1 1 220px; min-width: 180px; }
.filter-toolbar .fld-sel { flex: 0 0 auto; }
.filter-toolbar .fld-sel select.set-ctl { width: 170px; }
.filter-toolbar .fld-date { flex: 0 0 auto; }
.filter-toolbar .fld-date input.set-ctl { width: 150px; }
.filter-actions { display: flex; align-items: flex-end; gap: var(--s2); margin-left: auto; }
.new-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  background: var(--accent); margin-left: 7px; vertical-align: middle; }
.pool-table .col-score { text-align: right; }
/* Each assessment cell (CV / Personality / Written / Interview) is a peek
   trigger opening the shared drawer on that component's analysis. The button
   fills the cell and reads as plain content until hovered/focused. */
.cell-peek { padding: 0; }
.cell-peek-btn { display: block; width: 100%; margin: 0;
  padding: var(--s2) var(--s3); border: 0; background: transparent;
  font: inherit; color: inherit; text-align: left; cursor: pointer;
  border-radius: 6px; }
.cell-peek-btn:hover { background: rgba(127,127,127,.12); }
.cell-peek-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* Resend-invite affordance under a "Not done" pipeline cell. */
.cell-invite { margin: 2px 0 6px; padding: 0 var(--s3); }
.cell-invite .btn.sm { padding: 2px 9px; min-height: 0; font-size: 11.5px; }
/* After an AJAX "Send invite" succeeds: a settled green tick, no longer a CTA. */
.btn.invited { color: var(--ok-ink); border-color: #b9e6cb; background: var(--ok-tint);
  cursor: default; opacity: 1; }

/* Pipeline page: positions rail (2nd column after the app sidebar) + content.
   The rail mirrors the app sidebar exactly — its items ARE .nav-item (same
   hover/active wash), flat with no panel box. It mirrored the sidebar's
   .nav-badge count pill too, until both count badges were deleted on
   2026-08-07. */
/* Even rhythm around the positions rail: the grid gap (--s5) is the rail's gap
   to the pipeline content; a matching negative margin trims the .content left
   padding (40px) down to the same --s5 so the sidebar->rail gap equals the
   rail->page gap. Neutralised in the ≤900px stacked layout below. */
.pipe-page { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: var(--s5);
  align-items: start; margin-left: calc(-1 * var(--s4)); }
.pos-rail { position: sticky; top: 12px; display: flex; flex-direction: column; gap: 1px; }
.pos-rail .pipe-head { font-size: 12px; font-weight: 600; color: var(--faint);
  text-transform: uppercase; letter-spacing: .04em; padding: 4px 10px 6px; }
/* Long role titles wrap to a second line (like the reference), badge centred. */
.pos-rail .nav-item { line-height: 1.35; }
/* Rail status filter (All / Open / Closed) — a compact full-width select that
   sits under the "POSITIONS" head. */
.pos-rail-filter { display: flex; flex-direction: column; gap: 6px; padding: 2px 8px 8px; }
.pos-rail-filter select { width: 100%; font-size: 12.5px; padding: 5px 8px; }
/* Team section subheader inside the positions rail — a quiet divider label that
   groups the roles beneath it (smaller than the "POSITIONS" head above). */
.pos-rail-team { font-size: 11px; font-weight: 600; color: var(--faint);
  text-transform: uppercase; letter-spacing: .04em; padding: 12px 10px 3px; }
.pos-rail-empty { padding: 8px 10px; }
.pos-rail-name { display: inline-flex; align-items: center; gap: 7px; min-width: 0; }
.pos-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--faint); }
.pos-dot.ok { background: var(--ok); }
.pos-dot.bad { background: var(--bad); }

/* Focused card: labelled profile-review rows (label column + substance). */
.pipe-rows { display: flex; flex-direction: column; gap: 7px; }
.pipe-row { display: grid; grid-template-columns: 104px minmax(0, 1fr); gap: 12px; align-items: baseline; }
.pipe-label { font-size: 12.5px; font-weight: 600; color: var(--muted); }
.pipe-val { min-width: 0; }

/* Pipeline focused view (one role): stages as a horizontal tab bar under the
   title (with per-stage counts), so the profile cards span the full width. This
   is a STATUS FILTER (narrows the card list by pipeline status), distinct from
   the per-candidate 5-stage journey funnel on each card. It must NEVER scroll:
   segments condense (tight padding, compact count pills, responsive label size)
   and wrap to a second line rather than overflow. */
.pipe-stagebar { display: flex; flex-wrap: wrap; gap: 1px 2px; border-bottom: 1px solid var(--line); }
.stage-seg { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
  padding: 7px 10px; margin-bottom: -1px; text-decoration: none; color: var(--muted);
  font-size: clamp(12px, 1.05vw, 13.5px); border-bottom: 2px solid transparent; }
.stage-seg:hover { color: var(--ink); }
.stage-seg.active { color: var(--ink); font-weight: 600; border-bottom-color: var(--accent); }
.stage-seg.is-zero { color: var(--faint); }
.stage-seg .seg-n { font-size: 12px; color: var(--faint); background: var(--neutral-tint);
  border-radius: 999px; padding: 1px 6px; min-width: 17px; text-align: center; }
.stage-seg.active .seg-n { color: var(--accent); }
/* Talent bank ▸ Records profile cards: header row (bulk checkbox + name + role/
   stage chips) aligned inline; otherwise the shared .pipe-card layout. */
.records-cards .rec-head-left { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* Talent bank ▸ compact "square" cards: a responsive grid of self-contained
   tiles (Name · Position applied {fit} · Other recommendations · footer with
   Applied / View report / Save-to). Every colour is a token (themes to dark). */
/* One profile per row: full-width cards stacked vertically (not a grid). */
.records-cards { display: flex; flex-direction: column; gap: 14px; }
.tb-card { display: flex; flex-direction: column; gap: 8px;
  border: 1px solid var(--line); border-radius: var(--radius); padding: 16px 18px;
  background: var(--panel); box-shadow: 0 1px 2px rgba(20, 22, 26, .04); }
.tb-head { display: flex; align-items: center; gap: 8px; }
.tb-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tb-spacer { flex: 1; }
/* `.tb-menu` AND `.tb-menu > summary` ARE DELETED (owner, 2026-08-21) with the
   talent bank's whole Actions column, which held the product's last `.tb-menu`
   usage. What went with them is a measurement worth keeping the reason for: the
   summary rule existed because the `⋮` glyph sat 3px below the controls beside
   it (Chrome, 2026-08-03 -- a 32px summary box with `line-height: 13px` and no
   padding put the glyph's mid-Y at 303 against 300 for the 30px Save button and
   Save-to select). It was fixed by giving the summary the controls' own height
   rather than by nudging a margin, which is the idiom to reuse if a row menu
   ever returns to any table here. All three of those controls are now gone. */
.tb-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.tb-field { display: flex; flex-direction: column; gap: 2px; }
.tb-field .tb-k { color: var(--muted); font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .03em; }
.tb-field .tb-v { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
/* `.tb-review` DELETED (UX-105, owner 2026-08-10) with the record card's review
   block and the whole talent-review construct behind it. */
.tb-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  margin-top: auto; padding-top: 8px; border-top: 1px solid var(--line-soft); }
.tb-foot-sep { color: var(--muted); }
/* `.tb-saveto` AND `.tb-saveto .pill-select` ARE DELETED (owner, 2026-08-21) with
   the per-row `Save to…` and `Screen against…` forms that were their only two
   call sites. `copy_to_position` did not lose its capability -- it moved to the
   bulk bar, where it is a `.bf-group` like every other control there and needs no
   class of its own.

   ONE RULE FROM HERE IS WORTH CARRYING FORWARD RATHER THAN REDISCOVERING. The
   select had `width: 168px` and NOT `max-width`, because a `<select>` with
   `width: auto` is as wide as its widest `<option>` -- and these options were the
   row's save-to TARGETS, i.e. open roles the candidate was not already in,
   EXCLUDING their own. So the width varied per ROW: a candidate on a long-titled
   role excluded that title and got a ~112px select, while a candidate on a
   short-titled role kept it as a target and clamped at 168px. Same column, two
   visibly different controls, moving again whenever a role opened or closed. The
   bulk control cannot have that defect -- its options are every open role, the
   same list for the whole page -- which is a second, quieter argument for the
   move. A fixed width is a property of the CLASS rather than of whichever roles
   happen to be open (layout rule 3), and that is the lesson to reuse. */
/* `.pipe-card`, `.pipe-card:first-child/:last-child` and `.pipe-sum` stood here
   and are DELETED at UX-52 with the card markup that was their only consumer.
   The comment they carried claimed Talent bank > Records "reuses the same flat
   .pipe-card block" -- that stopped being true when UX-50 moved Records onto
   `.v-fbtable` rows, so `templates/_pool_main.html` was the last template in the
   tree naming either class. Checked by reading `git ls-files`-wide, not inferred:
   `records-cards` survives (talent.html still carries it on the table container)
   and its own two rules below are untouched. */
.pipe-assess { display: flex; flex-wrap: wrap; gap: 6px 20px; align-items: center; }
.pipe-assess .pipe-a { display: inline-flex; align-items: center; gap: 6px; }
.pipe-assess .cell-peek-btn { display: inline-block; width: auto; padding: 2px 4px; }
.pipe-assess .cell-invite { margin: 0; padding: 0; display: inline-block; }

/* ---- UX-52: Pipeline role drawer -- ONE LINE PER CANDIDATE ---------------
   (owner design handoff, 2026-08-05:
   docs/design-handoffs/2026-08-05-pipeline-drawer-rows/HANDOFF.md)

   This replaces the "rich profile card" (`.pipe-cards > .pipe-card.pcard` and
   the whole `.pcard-head / -sum / -tiles / -foot / -view / -next` family, which
   stood exactly here). That card was ~320px tall, so a 1080px screen showed TWO
   candidates. A row is ~44px. Nothing was dropped -- the summary moved behind
   row expansion, the Overall-verdict peek and the next action moved into the
   row's `...` menu, and templates/_pool_main.html's macro comment lists the move
   item by item.

   `.pcard-menu`, `.pcard-menu > summary`, `.pcard-menu-pop` and the
   `.menu-div` rule SURVIVE, further down. They were always the overflow-menu
   primitive rather than part of the card, and both menus this design adds -- the
   row's and the role header's -- are built out of them rather than out of a
   second popover.

   `--cand-cols` IS THE WHOLE ALIGNMENT ARGUMENT. ONE declaration, consumed by
   `.cand-row` -- which is the class on the HEADER as well as on every candidate.
   There is no header-specific template and no per-cell width below, so the
   columns cannot drift apart, which is the defect this codebase keeps paying for
   (CLAUDE.md layout rules 1 and 2: the grid lives on the ROW, and the decision is
   taken once). Measured, not asserted: tests/test_ui_alignment.py, UX-52 block.
     = checkbox | candidate | CV | AI interview | overall | stage | applied | ...
   `--cand-inset` is ARITHMETIC, not a taste: 4px row padding + the 28px checkbox
   column + the gap, so an expanded summary's first character lands exactly
   on the candidate's name. It is a token so the sum stays derivable and so the
   off-scale-spacing ratchet is not asked to bless a number nobody can explain.

   REBALANCED 2026-08-06 (owner) for three changes that all cost width: the band
   pills now carry their MARK as well as their word ("Strong advance · 92", not
   "Strong advance"), and there is a fourth column -- Overall, the combined
   score, on the same interview vocabulary.

   THE BUDGET IS THE DRAWER, NOT THE PAGE, and that is the trap this repo keeps
   paying for (layout rule 5). These rows render in `#peekDrawer`, which is
   `.drawer` at 65vw: ~936px at a 1440 viewport, ~880 inside its 28px padding.
   The focused `/pool?view=role` page is far wider, so a template sized there
   overflows the container a recruiter actually opens -- exactly what happened to
   `.cand-filter`'s flex bases, whose note sits below.
     28 + 118 + 144 + 156 + 112 + 76 + 44 = 678 fixed, + 7 x 8px gap = 734,
     leaving ~146px for the name inside the drawer and everything else on the
     page. Stage 150->112 and Applied 96->76 were measured against their own
     content ("AI interview" is the longest stage; "10h ago" the longest recency)
     and were both carrying slack; the name column pays the rest, which is the
     trade the owner asked for.
   OVERALL IS 12px WIDER THAN AI INTERVIEW and it is not a typo. Both carry the
   same band vocabulary, but the combined score is `round(..., 1)` -- "Strong
   advance · 82.5", one character more than "Strong advance · 82". It was 144
   like its neighbour and the centring arm in tests/test_ui_alignment.py reported
   a 6.5px NEGATIVE right inset, i.e. a 150.5px pill in a 144px track. Rounding
   the pill's number to an integer would have been the smaller diff and the wrong
   fix: the candidate record renders the same quantity to one decimal, and two
   surfaces disagreeing about a score is worse than 12px.
   THE GAP DROPPED 12->8 to buy 28px back across seven gutters. It is on the
   spacing scale, so the off-scale ratchet (check_tokens 8.13) is unaffected, and
   `--cand-inset` follows it: 4 + 28 + 8 = 40.

   A NINTH COLUMN, 2026-08-08 -- the DECLARATION readout (Spec 1's three-state
   indicator; owner: "the drawer column with tick, cross and question mark for
   these 2"). It sits between Applied and the `...` menu, which is the position
   that costs least: the three band columns keep their child indices, so
   tests/test_ui_alignment.py's pill measurement and the <900px positional
   selector below both keep pointing at the cells they were written for.
   IT IS PAID FOR OUT OF STAGE, and the guard named the source before it was
   asked: the name column carried 26px over the 120px floor the suite measures,
   the new track needs more than that, and the failing message on that arm reads
   "widen nothing else without taking it from Stage or Applied". Stage is the one
   of the two with room -- Applied renders a full date ("12 Mar 2026") for any row
   older than eight weeks and is genuinely near its content, while Stage's longest
   LIVE label is "AI interview".
     28 + 118 + 144 + 156 + 88 + 76 + 32 + 44 = 686 fixed, + 8 x 8px gap = 750.
   THE BUDGET IS THE ROW'S CONTENT BOX, NOT THE PANE -- 880px of `.drawer` less
   this rule's own `4px` side padding, so 872 -- which leaves 122px for the name.
   That is MEASURED, not derived: the arithmetic here read 130 until the browser
   said 122, because the padding had been left out of it.
   EVERY NUMBER IN THIS PARAGRAPH IS MEASURED, by three arms in
   tests/test_ui_alignment.py's UX-52 block rather than one: the name column keeps
   its 120px floor (122.0px), the Stage track is checked against its own longest
   LIVE label -- INJECTED, because the seed renders only the short ones -- and the
   declaration track against BOTH its glyph pair and its header word. At these
   widths "AI interview" has 18.6px spare and "Req" has 7.9px.
   32px HOLDS THE PAIR AND CANNOT BE OUTGROWN BY DATA: the cell is one glyph per
   declaration source, always both, so unlike a band pill its content has no
   longest case still to discover. What DOES size it is the HEADER WORD, which is
   why the abbreviation is three letters and why it is measured.

   THE NAME COLUMN HAS A CEILING SINCE UX-119 (owner, 2026-08-10: "the candidate
   column width is too wide, we can narrow down that a bit, instead we could
   expand all other columns correspondingly"), AND THE PARAGRAPH ABOVE IS WHY IT
   WAS INVISIBLE FOR SO LONG. Every number in it is measured at 1440, where the
   name column sits at 122px -- its tightest, and the reason the arithmetic was
   ever tense. It was the ONLY flexible track, so above 1440 it absorbed 100% of
   the slack while every other column stayed frozen at the pixel width it was
   sized to. MEASURED, before this change: 122px at 1440, 278px at 1680, 434px at
   1920, 850px at 2560 -- against a CV column that never left 118px. The owner
   reads the board on a wide monitor; the suite measured one viewport. That is
   layout rule 5 in the VIEWPORT dimension rather than the container one.

   THE SHAPE IS A CEILING PLUS FIVE FLEXIBLE TRACKS, and each half is load-bearing:
     * `minmax(0, 220px)` on the name. THE `0` IS UNTOUCHED and must stay -- it is
       what lets a long name shrink instead of forcing the row to overflow, and at
       1200px (where the drawer floors at 620px but the wide template still
       applies) the track really does resolve to 0. THAT SENTENCE DESCRIBED A
       DEFECT AND READ IT AS CORRECT BEHAVIOUR -- a name track at 0 is a row with
       no candidate on it, and the owner reported exactly that on 2026-08-10. The
       `0` is still right and still must stay; what was wrong was letting the WIDE
       template apply at a width where the `0` is what makes it fit. See the
       UX-121 note below the `.pcard-menu` family: this template now stops at
       1436px. The 220 is a GROWTH LIMIT, so
       CSS's Maximize Tracks step still hands the name every spare pixel first,
       which is why 1440 is byte-identical to what the paragraph above measured.
       220px is not a new number: it is `.members-toolbar input.members-search`'s
       bounded width, chosen there for the same reason -- a box that grows to fill
       its container is what this is. It is 1.8x the 120px floor the suite already
       defends, ~25 characters at this row's own type.
     * `minmax(<the old fixed px>, 1fr)` on CV, AI interview, Overall, Stage and
       Applied. Their old fixed widths become MINIMUMS, so nothing shrinks and
       1440 is unchanged; the fr is what gives the freed space somewhere to go.
       Without it the slack past 220px would sit as a hole before the `...` menu,
       because `justify-content` cannot stretch a definite track.
   Measured after: 220px at 1920 with the five sharing 159.2px each, and 122px at
   1440 exactly as before. Guard: `UX-119` in tests/test_ui_alignment.py, which
   measures at 1920 -- at 1440 this change is a no-op by construction, so an arm
   at the sweep's own viewport could never have failed on the revert.
   THE <900px DECLARATION BELOW WAS RE-MEASURED AND DELIBERATELY LEFT ALONE. Its
   drawer is pinned at the 620px `min-width` floor for every viewport it applies
   to, so the name resolves to 370px and cannot run away; and the two columns
   beside it are a pill with a 78px floor and a glyph button. There is nothing
   there for a ceiling to hand the width back TO.

   THERE ARE THREE DECLARATIONS SINCE UX-121 (2026-08-10), NOT TWO, and this one is
   now the WIDEST of three rather than the default: it applies from 1437px up, which
   is the first width at which its own arithmetic closes. The band between it and
   the <900px set had never been measured by anything and the row did not fit in it.
   Full statement, with the measurements and the two rejected alternatives, at the
   UX-121 note below the `.pcard-menu` family. */
/* STAGE 88px -> 136px, FUNDED FROM THE NAME COLUMN (six stages locked, owner
   2026-08-20). `Face to Face Interview` is 22 characters where the longest label
   before it was `Shortlisted` at 11, and UX-52 measured the result: an 88px track
   with -45.3px of spare, i.e. the stage label silently ellipsised on every row.

   WHY THE NAME COLUMN AND NOT THE TWO UX-52 NAMES. The guard's own remedy says
   "take the width back from the declaration track or from Applied" -- but the
   declaration track is CIRCULAR here: the note below records that declaration was
   itself funded by shrinking Stage 112 -> 84, and a sibling arm requires the
   declaration header to fit with room to spare while forbidding widening it. So
   paying Stage back out of declaration just moves the failure. `Applied` holds
   "3d ago" in 76px and has no slack worth 48px.

   The name column is `minmax(0, 220px)`: 220 is its MAXIMUM, not its floor, so it
   already shrinks to nothing under pressure and a truncated person's name is the
   one truncation this grid handles gracefully -- it is the only cell whose content
   is unbounded in principle. Reducing the CAP frees 40px at wide viewports and
   changes nothing at narrow ones, where the breakpoints below drop columns 5-7
   entirely and no stage cell is rendered at all.

   REJECTED: a short display label for the board ("Face to Face"). `config.stage_label`
   returns the status verbatim except for the derived `Screened`, and the stage
   FILTER offers the raw `config.STATUSES` values -- so a shortened board label
   would put two names on one stage, one on the row and another in the dropdown
   filtering it. That is the defect class this product has spent the month removing. */
/* EIGHT TRACKS FROM 2026-09-05, not nine. The trailing 44px held the `...`
   overflow menu, which the owner deleted along with everything in it; the
   header shares this declaration, so dropping the track drops its empty cell
   too. The `:nth-child` hide lists in the two narrower sets below name children
   4-8 and are therefore unaffected -- it is the LAST child that went. */
:root { --cand-cols: 28px minmax(0, 220px) minmax(100px, 1fr) minmax(120px, 1fr)
                     minmax(156px, 1fr) minmax(136px, 1fr) minmax(68px, 1fr)
                     32px;
        --cand-inset: 40px; }
.cand-row { display: grid; grid-template-columns: var(--cand-cols);
  align-items: center; gap: 8px; padding: 12px 4px;
  border-bottom: 1px solid var(--line-soft); }
/* `display: grid` above BEATS the user agent's `[hidden] { display: none }`, so
   without this rule the client-side filter would set `hidden` and change
   nothing. If rows stop hiding, this is the line. */
.cand-row[hidden] { display: none; }
.cand-row[data-cand-row] { cursor: pointer; }
.cand-row[data-cand-row]:hover { background: var(--hover); }
.cand-row[data-cand-row]:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* The table owns its own top gap rather than the call site carrying an `mt-*`
   utility -- one component, one spacing decision, which is layout rule 1 applied
   to the block instead of to the row.
   NO `:last-child { border-bottom: 0 }` here, deliberately: an expanded row's
   summary is a SIBLING of it, so the last `.cand-row` is never the container's
   last child and such a rule could not match. Every row carries its hairline and
   the last one closes the table, matching `.cand-head`'s own bottom border. */
.cand-table { margin-top: 12px; }
.cand-head { padding: 8px 4px; cursor: default;
  border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
  font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--faint); }
.cand-sort { color: inherit; text-decoration: none; white-space: nowrap; }
.cand-sort:hover, .cand-sort.is-on { color: var(--ink); }
.cand-c { text-align: center; }
.cand-chk input { width: 15px; height: 15px; margin: 0;
  accent-color: var(--accent); cursor: pointer; }
.cand-name { font-size: 14px; font-weight: 600; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cand-stage { font-size: 12.5px; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cand-applied { font-size: 12.5px; color: var(--faint);
  font-variant-numeric: tabular-nums; }
/* The DECLARATION column (2026-08-08). ONE rule for the pair, on the CELL, not a
   width or a margin per glyph -- the layout decision is taken once here exactly
   as `--cand-cols` takes the column decision once (CLAUDE.md layout rules 1-2).
   `center` rather than `space-between`: the two glyphs are a PAIR reading as one
   readout, and space-between would align them to the cell's own content width,
   which is the drift primitive the ratchet counts.
   `--text-xs` is on the scale; `tabular-nums` is not asked for because none of
   the four glyphs is a digit. The header cell shares the class family so the
   abbreviation sits over the pair rather than over the cell's left edge. */
.cand-decl { display: flex; align-items: center; justify-content: center;
  gap: 4px; }
.cand-declh { white-space: nowrap; }
/* Each glyph carries its own tone token, so the column themes in light and dark
   with no raw hex and reads the same as every other tone in the product: ok for
   met, bad for unmet, warn for "we could not check", faint for "nothing to
   report". COLOUR IS NEVER THE ONLY CHANNEL -- the glyph itself differs in every
   state, so the column survives a colour-blind reader and a greyscale print. */
.dstate { font-size: var(--text-xs); line-height: 1; font-weight: 700;
  cursor: help; }
.d-met { color: var(--ok); }
.d-unmet { color: var(--bad); }
.d-unknown { color: var(--warn); }
.d-not_stated, .d-unrecorded { color: var(--faint); font-weight: 400; }
/* `.cand-acts` IS DELETED with the `...` menu it held (owner, 2026-09-05).
   Its `justify-content: flex-end` was legitimate because the track was a FIXED
   44px -- the same reasoning `.sch-bkt-act` and `.sch-offered` carry -- rather
   than a `margin-left: auto`, which is one of 8.13's drift primitives. */
/* The two band cells stay PEEK TRIGGERS, so the focus drawers the card's score
   tiles opened are still one click away -- but chrome-free, overriding
   `.cell-peek-btn`'s block fill the same way `.jn-pill` does. */
.cand-row > .cand-pk { padding: 0; text-align: center; border-radius: 999px; }
.cand-row > .cand-pk:hover { background: transparent; }
/* ONE width floor for the pills in all THREE band columns. Without it a short
   band word shrinks its own pill and the columns stop reading as columns -- the
   per-instance-vs-shared mistake in miniature.
   THE 12px IS LOAD-BEARING, not taste, and it was MEASURED rather than copied:
   `.score-pill` is `--text-sm`, at which the longest interview band label
   ("Strong advance") renders 121px and OVERFLOWED the 118px column -- caught by
   the centring arm in tests/test_ui_alignment.py's UX-52 block, which reported a
   negative right inset. The handoff specifies 12px for this pill; at 12px the
   longest label fits its column with room to spare. If a longer band word ever
   arrives, that arm is what will say so.
   THE PILLS GREW A MARK on 2026-08-06 ("Strong advance · 92"), which is what the
   144px band tracks above are sized for; the columns widened rather than the
   type shrinking, because 12px is already the floor at which these labels are
   legible. The 78px minimum is unchanged and is still a FLOOR, not a width: it
   is what stops the shortest cell ("Good · 92") collapsing.
   `text-overflow` is deliberately NOT set here. A clipped band word is a band
   word the recruiter cannot read, so the honest failure is the centring arm
   going red -- which is what it is for. */
.cand-pk .score-pill, .cand-pk .chip { min-width: 78px; box-sizing: border-box;
  text-align: center; font-size: 12px; }
/* `.chip` is `inline-flex`, on which `text-align` does NOTHING -- the floor
   above made its BOX 78px and left the word sitting at the left edge of it,
   beside a `.score-pill` (inline-block) whose word really was centred. Invisible
   while the only chip here was `Not done`, which is wide enough that the floor
   never bound; the 2026-08-06 Overall column's shorter `Pending` is what
   exposed it. The centring arm in tests/test_ui_alignment.py measures the
   pill's BOX within its cell, so it could not have caught this. */
.cand-pk .chip { justify-content: center; }
.cand-sum { padding: 12px 4px 16px var(--cand-inset);
  border-bottom: 1px solid var(--line-soft); }
.cand-sum p { margin: 0; font-size: 12.5px; line-height: 1.55; color: var(--muted); }
/* The filter bar. Its geometry is the shared `.pill-select` pill; only the flex
   bases are set here, because those are what let four controls, a count and a
   Clear share one row inside a 65vw drawer. `.cand-shown` ABSORBS the slack
   (`flex: 1 1 auto` + `text-align: right`) rather than the count being pushed
   over with `margin-left: auto` -- that primitive aligns a row to its own
   content width and is one of the three the 8.13 ratchet counts down. */
.cand-filter { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 12px 0 8px; }
/* THE FLEX BASES ARE SMALLER THAN THE HANDOFF'S (280 / 186), and the difference
   was MEASURED rather than argued. The handoff was drawn on a 1240px panel; the
   real container is `.drawer` at 65vw, whose content box is ~776px on a 1280
   screen. At 280 + 3x186 the four controls total 862px, so the bar broke 3 + 3 --
   one lonely select beside the count -- and cost 86px of vertical space. That is a
   rule correct in one container and wrong in another, which is the defect class
   this repo keeps paying for, and the fix belongs in the shared rule.
   `flex-wrap` is a LINE-BREAKING decision made from the BASES, not from the
   floors: shrink only applies once items are already on a line, so lowering
   `min-width` alone changed nothing. The four bases now sum under the drawer's
   content width, so they share one line and grow to fill it; on a narrower
   container the count and Clear wrap together as a pair, which is a clean second
   line rather than a stranded control. */
.cand-filter .cand-q { flex: 1 1 220px; min-width: 150px; }
.cand-filter .cand-sel { flex: 1 1 172px; min-width: 130px; }
.cand-shown { flex: 1 1 auto; text-align: right; font-size: 12.5px;
  color: var(--faint); }
.cand-shown .tnum { color: var(--ink); }
.cand-clear { border: 0; background: none; font: inherit; font-size: 12.5px;
  color: var(--faint); cursor: pointer; padding: 0;
  border-bottom: 1px dotted var(--line); }
/* UX-116 (owner 2026-08-10): "these two filter are too wide, make it narrower.
   Also it should be same height as the open and closed toggle in the same page".

   SCOPED TO THE BOARD'S OWN BAR, and the scope is the whole fix. The two rules
   above were MEASURED for the drawer at 65vw (~776px) and their comment says so:
   in that container `flex-grow: 1` is what lets four controls share one line.
   UX-53 moved the project select into `.cand-filter` on the BOARD without
   re-measuring it there, so a growth rule written for a crowding problem the
   board does not have made each select absorb the row -- 294px each here, and
   ~500px on a board with no `Showing` count beside them. Layout rule 5 again.

   `[data-board-filter]` IS THE DISCRIMINATOR, and `.pipe-main` is NOT: the drawer
   fetches `_pool_main.html` as a fragment and renders its own `.cand-filter`
   INSIDE the same `.pipe-main`, so a descendant selector on that wrapper would
   hit both bars and undo the drawer measurement this scoping exists to protect.
   The attribute is on the board's bar only.

   MEASURED at 1440px, before: segmented 36.8px tall, both selects 29px and
   294px wide. Three differences stack in the same direction -- the segmented
   pays for an outer track (1px border + 2px padding) AND an inner item
   (`padding: 5px 12px`), and its text is `--text-sm` against the pill's
   `--text-xs` -- so it was the taller of the two by 7.8px.

   THE HEIGHT IS ONE CUSTOM PROPERTY CONSUMED BY BOTH CONTROLS, which is the
   `--pill-h` idiom this sheet already used (on `.filter-pills`, until that
   class went with the Trust Center pages on 2026-09-01), not a new one. Two
   numbers would be two things to keep in step; one property means the pair
   cannot drift. It is deliberately NOT a product-wide control-height token:
   `.pill-select` is also the candidate status select and the bulk bar, and
   retrofitting those is a bigger ruling than was asked for. `box-sizing` comes
   from the global `*` reset, so the border is inside the 36px.

   THE NARROW BREAKPOINT STILL WINS: below 900px both bars stack their controls
   full-width, and the rule down there names this selector too so this higher
   specificity cannot defeat it. */
.cand-filter[data-board-filter] { --board-ctl-h: 36px; }
.cand-filter[data-board-filter] .cand-sel { flex: 0 1 auto; }
.cand-filter[data-board-filter] .segmented,
.cand-filter[data-board-filter] .cand-sel { height: var(--board-ctl-h); }
.cand-clear:hover { color: var(--ink); }
/* The role-actions menu: a wider popover than a row's, because its items are
   sentences ("Invite selected to AI interview") rather than verbs. Everything
   else about it -- the 28px summary, the box, the item rows, the dividers -- is
   the shared `.pcard-menu*` block. */
.cand-rolemenu { flex: none; }
/* THE DOUBLE CLASS IS LOAD-BEARING. `.pcard-menu-pop { min-width: 200px }` is
   declared LATER in this file, so a bare `.cand-rolemenu-pop` has equal
   specificity and LOSES -- the rule looked applied in the stylesheet and measured
   200px in the browser, which is the M-4 shape exactly (a class that does
   nothing). Caught by measuring the popover, not by reading the cascade. */
.pcard-menu-pop.cand-rolemenu-pop { min-width: 240px; }
/* `.cand-overall` AND `.cand-next` ARE DELETED with the candidate row's `...`
   menu, which was the only place either appeared. Two ideas in them generalise
   and are kept because the other two `.pcard-menu` callers still exist:
     * A MENU ITEM WHOSE TEXT CAN BE A SENTENCE must wrap rather than overflow
       the popover -- `.cand-overall` carried "Pending — awaiting AI interview,
       Written test", the only such item in the product.
     * THE PRIMARY ROW IN A MENU IS MARKED BY WEIGHT, NOT AN ACCENT FILL: a
       filled block inside a 240px popover reads as a second surface, and G18's
       dark-theme accent-fill foreground contract has one fewer selector to hold
       as a result. */
.pcard-menu-pop .cand-menu-invite:disabled { color: var(--faint); cursor: not-allowed; }
/* Assessment journey funnel: six nodes on one centre line, joined by flexible
   connectors so the gaps distribute evenly across the whole row. */
.jnode { display: inline-flex; align-items: center; gap: 6px; height: 24px; }
.jn-conn { flex: 1; min-width: 14px; height: 1.5px; background: var(--line); }
.jn-dot { width: 16px; height: 16px; border-radius: 999px; flex: none;
  background: var(--panel); border: 1.5px solid var(--line);
  display: flex; align-items: center; justify-content: center; }
.jn-done .jn-dot { border: 0; }
.jn-done .jn-dot::after { content: "\2713"; color: var(--bg); font-size: 9px; font-weight: 700; line-height: 1; }
.jn-dot.b-ok { background: var(--ok); }
.jn-dot.b-warn { background: var(--warn); }
.jn-dot.b-bad { background: var(--bad); }
.jn-label { font-size: 12px; color: var(--ink); }
.jn-not_done .jn-label { color: var(--muted); }
.jn-na .jn-label { color: var(--faint); }
/* The peek trigger inside a node: an inline, chrome-free wrapper around the
   band pill (overrides the block-fill .cell-peek-btn default). */
.jn-pill { display: inline-flex; width: auto; padding: 0; border-radius: 999px; text-decoration: none; }
.jn-pill:hover { background: transparent; }
/* Invite affordance inside a node — a small accent pill (not a hyperlink). */
.jn-invite-form { margin: 0; padding: 0; display: inline-flex; }
.jn-invite { display: inline-flex; align-items: center; gap: 4px; font-family: inherit;
  font-size: 10.5px; font-weight: 600; color: var(--accent); background: var(--accent-tint);
  border: 1px solid var(--accent-tint); border-radius: 999px; padding: 2px 9px;
  min-height: 0; cursor: pointer; }
.jn-invite:hover { border-color: var(--accent); }
.jn-invite-ico { flex: none; }
/* After the AJAX invite succeeds the shared handler flips text + adds .invited. */
.jn-invite.invited { color: var(--ok-ink); background: var(--ok-tint); border-color: var(--ok-tint); cursor: default; }
/* F2F half state: a scheduled (but not yet held) face-to-face interview renders
   a half-filled accent dot — the in-between signal between hollow and done. */
.jn-scheduled .jn-dot { border-color: var(--accent);
  background: linear-gradient(90deg, var(--accent) 0 50%, var(--panel) 50% 100%); }
.jn-scheduled .jn-label { color: var(--ink); }
/* F2F mark form: the one-click Mark pill + an optional-note disclosure. */
.f2f-form { align-items: center; gap: 5px; }
.f2f-note-d { display: inline-flex; align-items: center; gap: 5px; }
.f2f-note-d > summary { list-style: none; cursor: pointer; font-size: 10.5px;
  color: var(--faint); border-radius: 999px; padding: 2px 5px; white-space: nowrap; }
.f2f-note-d > summary::-webkit-details-marker { display: none; }
.f2f-note-d > summary:hover { color: var(--accent); }
.f2f-note-d[open] > summary { color: var(--accent); }
.f2f-note-in { font-size: 11px; min-height: 0; padding: 2px 8px; width: 150px; }
/* Saved F2F note: a small pencil marker whose tooltip carries the note text. */
.jn-f2f-note { color: var(--accent); font-size: 11px; cursor: help; line-height: 1; }
/* The peek drawer's F2F section renders the note inline after the status chip. */
.peek-f2f-note { color: var(--muted); font-size: 13px; margin-left: 6px; }
/* ---- the overflow-menu primitive, which OUTLIVED the card ------------------
   `.pcard-foot / -meta / -interest / -actions / -view / -divider / -offer /
   -reject`, the whole `.pcard-v2` group, `.pcard-tiles / -tile*` and
   `.pcard-next` stood here and are DELETED at UX-52 with the card that was
   their only consumer (see the UX-52 block above for where each datum went).
   `.pcard-menu*` below is NOT part of that deletion: it is the shared popover
   both of the new menus are built from, so it keeps its name -- renaming 200
   lines of guard selectors would have been the only thing a rename bought. */
.pcard-menu { position: relative; }
.pcard-menu > summary { list-style: none; cursor: pointer; width: 28px; height: 28px; display: inline-flex;
  align-items: center; justify-content: center; border-radius: 6px; color: var(--faint); font-size: 16px; }
.pcard-menu > summary::-webkit-details-marker { display: none; }
.pcard-menu > summary:hover { background: var(--neutral-tint); color: var(--ink); }
.pcard-menu-pop { position: absolute; right: 0; top: 32px; z-index: 30; min-width: 200px; padding: 5px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 10px; box-shadow: var(--shadow-2); }
.pcard-menu-pop a, .pcard-menu-pop button { display: block; width: 100%; text-align: left; box-sizing: border-box;
  border: 0; background: none; font: inherit; font-size: 13px; padding: 8px 10px; border-radius: 6px; color: var(--ink); cursor: pointer; text-decoration: none; }
.pcard-menu-pop a:hover, .pcard-menu-pop button:hover { background: var(--hover); }
.pcard-menu-pop .danger { color: var(--bad-ink); }
.pcard-menu-pop .danger:hover { background: var(--bad-tint); }
.pcard-menu-pop form { margin: 0; }
/* P0-5: the card menu's separators were UNSTYLED — `.menu-div` was only ruled
   for `.pos-menu`, so inside this popover the span rendered as nothing and the
   menu's group boundaries did not exist visually. Same 1px rule as .pos-menu. */
.pcard-menu-pop .menu-div { display: block; height: 1px; background: var(--line-soft); margin: 4px 6px; }

/* ---- UX-121: A THIRD `--cand-cols`, FOR THE BAND NOTHING HAD MEASURED -----
   Owner-reported 2026-08-10 with a screenshot of a ~950px window: "candidate
   name disappeared".

   THERE WERE TWO DECLARATIONS AND THREE SITUATIONS. The wide set above applies at
   every width over 900px, and `.drawer` is `width: 65vw; min-width: min(620px,
   96vw)` -- so from 901px to ~1250px the drawer is PINNED at its 620px floor
   while a template sized for an 880px pane is still the one in force. MEASURED
   before this rule existed, every number off the UX-52 fixture:

     vw     row content box    track span    overflow    name track
      901        556.0            750.0        +194.0        0.0
     1024        601.6            750.0        +148.4        0.0
     1200        716.0            750.0         +34.0        0.0
     1280        768.0            768.0           0.0       18.0
     1366        823.9            823.9           0.0       73.9
     1436        869.4            869.4           0.0      119.4
     1437        870.1            870.1           0.0      120.1

   TWO FAILURES SHARE ONE BAND, and only the second is visible in a screenshot.
   Below ~1252 the tracks OVERFLOW their row and the trailing cells bleed past the
   drawer's padding -- `.cand-row` sets no `overflow`, so what clips is whichever
   ancestor happens to: `.panel { overflow-x: auto }` below 920px, and `.drawer`'s
   own scrollbar above it, which is the scrollbar in the report. From ~1253 to 1436
   the row FITS, and it fits BECAUSE `minmax(0, 220px)`'s `0` has collapsed the
   NAME to nothing. That is minmax doing exactly what UX-119 designed it to do, on
   a row that then carries no candidate.

   SO 1437px IS WHERE THE WIDE SET BECOMES CORRECT, and it is DERIVED, not chosen:
   750px of tracks plus the 120px name floor is 870px of row box, and the row box is
   `0.65vw - 56 (the drawer's padding) - 8 (this rule's own)`. The breakpoint is
   WRITTEN as 1436 because a media query cannot compute one; what keeps it honest is
   that five widths across the band are MEASURED (`UX-121` in
   tests/test_ui_alignment.py), so changing any track re-fails the guard instead of
   quietly falsifying the number this comment states. It sits deliberately BELOW
   1440: every figure in the notes above was measured there, and the wide set has to
   keep applying at the suite's own viewport or all of them stop being checked.

   A VIEWPORT BREAKPOINT IS THE RIGHT INSTRUMENT HERE, which is not the usual
   answer in this file (layout rule 5: the budget is the container). It is right
   because `.drawer`'s width is a PURE FUNCTION of the viewport --
   `max(0.65vw, min(620px, 0.96vw))` capped at 96vw -- so the container's
   constraint and the viewport's are the same question asked twice. And
   `.cand-row` has exactly ONE container: VERIFIED, not assumed, on `/pool`,
   `?view=role&position_id=`, `?role=`, `?position_id=` and the raw fragment --
   zero rows render outside `#peekDrawer`, and the board's `.pipe-main` swap
   carries only `?order=` links. If the drawer's width ever stops being a function
   of the viewport, a container query is the primitive to reach for; this sheet
   uses none today, and introducing one would be a decision rather than a
   refactor (`container-type` applies `contain: layout`, which makes the drawer a
   containing block for fixed descendants).

   TWO ALTERNATIVES WERE MEASURED AND REJECTED, so they are not to be re-proposed:
     * SHRINKING THE WIDE TRACKS buys 40-55px in total (Overall 156->144, Stage
       88->80, Applied 76->70) and moves the floor 1437 -> 1397. It reaches neither
       a 1366 nor a 1280 laptop, and the note above already records Stage and
       Applied as near their own content.
     * RAISING `.drawer`'s `min-width` needs 926px to buy a readable name -- 93% of
       a 1000px window, against G7's own 80vw -> 65vw direction. At 814px, just
       enough to stop the overflow, the name is still 0.

   THE SET IS SIX COLUMNS, AND SIX IS THE CEILING -- measured at the tightest width
   in the band (901px, a 556px row box, a >=120px name):
     checkbox | candidate | CV | AI interview | Req | ...
   Overall at 156px is what makes seven impossible: keeping it puts the name at
   26px, which is the defect again. Owner's choice, 2026-08-10 -- keep both scored
   instruments' OWN bands plus the declaration readout, which at 32px is the
   cheapest data column; drop Overall, Stage and Applied. The combined score is
   derivable from the two bands that remain, Stage is redundant inside a drawer
   already scoped to one role, and Applied is the least decision-relevant of the
   three. STATED AS A NARROWING, exactly as the <900px note states its own: below
   1437px the Overall band, Stage and Applied are not shown and are not reachable
   from anywhere else ON THE ROW -- the row's `...` menu and the candidate record
   still carry them. Name resolves to 150px at 901 and reaches the 220px cap from
   ~1050 up; the slack past the cap goes to CV and AI interview, because a ceiling
   with nothing flexible beside it leaves a hole before the menu (UX-119).

   ONE DECLARATION, exactly as the wide set is one, and THE POSITIONAL HIDES ARE
   NESTED rather than three unrelated sets: this tier hides 5-7, the <900px tier
   below hides 4-8, and at a width where both apply the union is the phone row. Same
   trap the <900px note names -- the track COUNT and the positional selector are two
   halves of one decision. The six tracks here are the six VISIBLE cells IN DOM
   ORDER, so `Req` and the `...` menu auto-place into tracks 5 and 6; that is why
   they are written there and not at 8 and 9. `--cand-inset` is untouched and still
   derivable: 4px row padding + the 28px checkbox + the 8px gap = 40. */
@media (max-width: 1436px) {
  /* FIVE TRACKS, not six: the `...` column went on 2026-09-05. UX-121's
     arithmetic is unchanged -- what it measures is that the tracks FIT and the
     name holds its 120px floor, and losing a fixed 44px only ever helps both.
     THE RECORDED COST is that the Overall band, hidden in this band by the set
     above, was reachable HERE only through that menu. */
  :root { --cand-cols: 28px minmax(0, 220px) minmax(118px, 1fr)
                       minmax(144px, 1fr) 32px; }
  .cand-row > :nth-child(5), .cand-row > :nth-child(6),
  .cand-row > :nth-child(7) { display: none; }
}

@media (max-width: 900px) {
  .pipe-page { grid-template-columns: 1fr; margin-left: 0; }
  .pos-rail { position: static; flex-direction: row; flex-wrap: wrap; }
  .pipe-row { grid-template-columns: 1fr; gap: 2px; }
  /* UX-52, replacing the five `.pcard-*` stacking rules that stood here with the
     card they belonged to. 620px of FIXED track plus a name does not fit a phone:
     inside a 360px drawer the seven-column row squeezed the candidate's NAME to
     20px, which was measured, not guessed. So the narrow row keeps the checkbox,
     the name, the CV band and the `...` menu, and it does that by REDEFINING THE
     SHARED TOKEN, once, rather than by overriding cells -- the header is a
     `.cand-row` too, so it follows automatically and the columns still cannot
     drift apart at this width either.
     Positional, because the cells being dropped are `<span>`s in a row and sort
     `<a>`s in the header and the ONLY thing they share is their column. The
     selector sits directly beneath the template it is keyed to, so the two are
     read together.
     STATED AS A NARROWING, not a fix: below 900px the AI-interview band, the
     Overall band, Stage and Applied are not shown and are not reachable from
     anywhere else on the row. There is no mobile guard in this repo and no
     mobile requirement in the handoff -- tests/test_ui_alignment.py measures
     1440px only -- so this is a judgement, and an unreadable name was judged
     worse than a hidden band.
     THE HIDDEN RANGE GREW BY ONE at the 2026-08-06 Overall column (4-7, was
     4-6) and the CV track by 12px, because the surviving CV pill now carries
     its mark too. Positional selectors and a column count are the two halves of
     one decision: change either and the other must follow, or the row hides the
     wrong cell and the header hides a different one.
     AND BY ONE AGAIN at the 2026-08-08 declaration column (4-8, was 4-7). The
     four-track token is unchanged: the narrow row still keeps the checkbox, the
     name, the CV band and the `...` menu, so the new cell joins the hidden range
     rather than the visible one. STATED AS A NARROWING, exactly like the bands
     above it: below 900px the tick/cross/question-mark readout is not shown and
     is not reachable from anywhere else on the row -- the record itself still
     says WHY in words (ruling R-E) and carries its `Reasons:` chips, so nothing
     is lost that only this column could tell you. Same judgement, same reason: an
     unreadable name is worse than a hidden readout.

     THIS IS THE NARROWEST OF THREE DECLARATIONS SINCE UX-121, NOT ONE OF TWO, and
     its own rules are unchanged: four tracks, hides 4-8, and it still wins here
     because it comes LATER IN THE SOURCE than the 1436px tier at equal
     specificity. THE HIDES ARE NESTED, WHICH IS WHY IT STILL READS AS FOUR
     COLUMNS: the tier above hides 5-7, this one hides 4-8, and at a width where
     both apply the union is exactly the phone row. Do not "simplify" the two into
     one range-bounded query -- the nesting is what makes each tier's selector
     readable on its own, and it is what stops a column added in the middle from
     hiding the wrong cell in only one of them. "An unreadable name is worse than a
     hidden band" is now enforced at BOTH boundaries rather than asserted at one:
     `UX-121` in tests/test_ui_alignment.py measures the name against its 120px
     floor at five widths across the band this tier used to have to cover alone. */
  :root { --cand-cols: 28px minmax(0, 1fr) 90px; }
  .cand-row > :nth-child(4), .cand-row > :nth-child(5),
  .cand-row > :nth-child(6), .cand-row > :nth-child(7),
  .cand-row > :nth-child(8) { display: none; }
  /* UX-116's board-scoped `flex: 0 1 auto` is (0,3,0) and would otherwise beat
     this rule, so the board's select is NAMED here rather than left to lose the
     cascade below 900px. Sizing to content is a WIDE-BOARD decision; on a stacked
     bar both bars want the same full-width row. */
  .cand-filter .cand-q, .cand-filter .cand-sel,
  .cand-filter[data-board-filter] .cand-sel { flex: 1 1 100%; min-width: 0; }
}

/* Focused peek drawer (Pipeline test-cell analysis): a comprehensive, scannable
   per-component view. Reuses the report design system (score pills, .meter-svg
   bars, pillar accents) so it themes in light AND dark via tokens — no raw hex. */
/* UX-06b: the back link and `Open full record` share the top row. `space-between`
   puts the exit left and the escalation right, both at `btn ghost sm`, so
   neither reads as the page's primary action. */
.peek-navrow { display: flex; align-items: center; justify-content: space-between;
  gap: var(--s3); flex-wrap: wrap; }
/* UX-06c: candidate identity and the score are ONE header unit. They were
   `align-items: flex-start`, so a 40px number and a 20px name started at the
   same TOP edge and read as two unrelated blocks stacked at different scales.
   Centring pairs them on a shared optical line, and the number comes down from
   40px to --text-2xl so the two sides are a size RELATIONSHIP rather than a
   collision -- which is the "in terms of size / positioning" half of the
   comment. */
.focus-hero { display: flex; justify-content: space-between; align-items: center;
  gap: var(--s4); margin: var(--s3) 0 var(--s4); }
.focus-headline { text-align: right; flex: none;
  display: flex; flex-direction: column; align-items: flex-end; gap: var(--s1); }
.focus-num { font-size: var(--text-2xl); font-weight: 600; line-height: 1; letter-spacing: -.03em;
  font-variant-numeric: tabular-nums; }
.focus-num.hi  { color: var(--ok-ink); }
.focus-num.hi2 { color: var(--ok2-ink); }
.focus-num.mid { color: var(--warn-ink); }
.focus-num.lo  { color: var(--bad-ink); }
.focus-card { border: 1px solid var(--line); border-radius: 10px;
  padding: var(--s3) var(--s4); margin-bottom: var(--s3);
  background: transparent; }
.focus-card .eyebrow { margin-bottom: var(--s2); }
.focus-empty { border-style: dashed; }
/* UX-06a -- "the drawer didn't utilize all space, the bottom part is empty".
   The cause was `max-height: 58vh` on this body: a hardcoded fraction of the
   VIEWPORT, unrelated to how much room the drawer actually has, so the content
   stopped 40% down a full-height drawer and the rest was blank while this
   region scrolled inside it. The header still stays pinned -- that is what the
   old cap was for -- but the room it gets is now what is LEFT, via a flex
   chain, which is the same idiom `#screenForm .drop-zone` and the wizard's
   job-description textarea already use. No viewport units and no JS
   measurement. `min-height: 0` is what lets a flex child scroll rather than
   grow past its parent. */
.peek-focus { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.focus-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-right: 4px; }

/* ---- CV report drawer: the three-band shell (owner handoff, 2026-08-09) -----
   Header and action bar are `flex: none` so ONLY `.pf-body` scrolls; the action
   bar therefore stays reachable without scrolling, which is what makes the drawer
   end in a decision rather than in whatever happens to be at the bottom.

   `justify-content: space-between` IS THE HANDOFF'S CSS AND IS NOT USED HERE.
   The handoff writes both the header and the action bar as a space-between flex
   row. That is one of the three drift primitives CLAUDE.md's layout contract
   names and `check_tokens.py` 8.13 ratchets DOWNWARD: it aligns each row to its
   OWN content width, so two rows that look right alone disagree with each other.
   A two-track grid gives the identical result and shares the track, which is the
   whole point of rule 2. The VISIBLE layout matches the handoff exactly; only the
   mechanism differs, and it differs in the direction this codebase already chose.

   The 22px rhythm, 44px gutter, 14px stacks and 9px rows are LITERAL, per owner
   ruling R6 -- the `offscale-headroom` lane freed the room for them by deleting
   twelve dead components. Do not "tidy" them onto the 4px scale. */
/* THE FIXED BANDS PAY THE SCROLLER'S GUTTER, or they hang past the content they
   label (owner, 2026-08-09: "the top score and strong match is too right").
   MEASURED at 1440px before and after: `.pf-body` is the scroller, so its 15px
   scrollbar plus its own right padding inset every section to x=1871, while the
   header and the action bar -- which have no scrollbar -- ended at x=1892. The
   score sat 21px right of the summary it belongs to, and the two rules were each
   correct alone, which is why nothing in the cascade said so.
   16px IS ON THE 4px SCALE AND 21px IS NOT. Exact alignment would need the
   scrollbar's own width, which CSS cannot name and which differs per platform
   (15px here, 0 where scrollbars overlay). 16px lands within 1px of the section
   edge and costs the off-scale ratchet nothing; a literal 21px would have RAISED
   a counter that may only fall. The scroller's own off-scale 6px goes with it. */
.pf-head { flex: none; display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: start; gap: 24px; padding: 0 16px 22px 0; border-bottom: 1px solid var(--line); }
.pf-who { min-width: 0; }
.pf-name { font-size: 22px; font-weight: 600; letter-spacing: -.02em; margin: 0 0 3px; }
.pf-ctx { font-size: 13px; color: var(--muted); }
.pf-mark { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
/* 0, NOT 6px, and the header above explains why: the scroller's own right
   padding stacks on top of its scrollbar, so every px here is a px the fixed
   bands must guess at. Removing it leaves the scrollbar as the ONE inset. It
   also retires an off-scale value, which is the direction 8.13 allows. */
.pf-body { padding-right: 0; }
/* One rhythm for the whole drawer: 22px above and below every section rule. The
   last section drops its rule so the body does not end in a hairline floating
   above the action bar's own. */
.pf-sec { padding: 22px 0; border-bottom: 1px solid var(--line-soft); }
.pf-sec:last-child { border-bottom: 0; }
.pf-prose { font-size: 13.5px; line-height: 1.6; color: var(--ink); }

/* The two evidence columns read as ONE grid: the rule belongs to the right
   column, not to a gap between two cards. */
.pf-evi { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 44px; }
.pf-evi-r { border-left: 1px solid var(--line-soft); padding-left: 44px; }
.pf-evi-ok { color: var(--ok-ink); }
.pf-evi-warn { color: var(--warn-ink); }
/* NO TOP MARGIN OF ITS OWN (UX-120). It was `14px 0 0`, and that 14px WAS the
   gap the owner pointed at as the good example -- which meant the two headings
   he compared were spaced by two different rules in two different places, and
   only one of them existed. The heading's gap now comes from `.eyebrow`, one
   declaration for both; the `gap` below is a different decision (item to item)
   and is untouched. */
.pf-evi-list { list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 14px; }
/* A fixed marker track, so every line of a wrapped item aligns to the same left
   edge instead of to the marker's own width. No horizontal rules between items. */
.pf-evi-item { display: grid; grid-template-columns: 14px minmax(0, 1fr); gap: 10px;
  font-size: 13.5px; line-height: 1.6; color: var(--ink); }
.pf-evi-mark { font-weight: 600; text-align: center; }
.pf-evi-mark-ok { color: var(--ok-ink); }
.pf-evi-mark-warn { color: var(--warn-ink); }
.pf-evi-lead { font-weight: 600; color: var(--ink); }

/* The action bar. `justify-self: end` on the right group rather than an auto
   margin -- same reason as the header above, and it is the idiom `.set-ctl`
   already uses. */
/* THE BAR'S BOTTOM PADDING IS 0 BECAUSE THE DRAWER ALREADY PAYS 24px (owner,
   2026-08-09: the bottom "has too much spacing, that makes the main scrolling
   portion smaller than it should be"). MEASURED: the buttons ended at y=798 and
   the drawer at y=855 -- 16px of `.pf-bar` padding stacked under 24px of
   `.drawer` padding, 40px of dead rail under a fixed bar, all of it taken from
   the only band that scrolls.
   THE 24px IS NOT TOUCHED: `.drawer` is shared by every drawer in the product,
   so trimming there would move four other surfaces to fix one. Scoping the fix
   to `.pf-bar` is rule 5 -- change the rule for the container that owns it.
   The top padding goes 14px -> 12px in the same edit: it is off-scale, it was
   the only reason this declaration failed the 4px scale, and 12px reads
   identically against a 1px rule. Net: 18px back to the scroller, two off-scale
   values retired. Right padding matches `.pf-head` so the two bands share an
   edge. */
.pf-bar { flex: none; display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: center; gap: 16px; padding: 12px 16px 0 0; border-top: 1px solid var(--line); }
.pf-bar-l, .pf-bar-r { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.pf-bar-r { justify-self: end; }
/* The readiness note is a CONSEQUENCE of the row above it, not a third control,
   so it spans both tracks and sits under them. */
.pf-bar-note { grid-column: 1 / -1; }
.pf-bar-note .hint { margin: 8px 0 0; }

/* ---- Evidence-based scorecard (owner handoff, 2026-08-09) ------------------
   One row per dimension, quote on demand. Shared by four surfaces, so nothing
   here may assume the drawer: it renders inside `.detail-report`'s `.cgroup` on
   the candidate record too. */
.evi { display: block; }
.evi-rows { display: block; }
.evi-head { display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: center; gap: 12px; margin: 0 0 8px; }
.evi-title { font-size: 15px; font-weight: 600; letter-spacing: -.01em; margin: 0;
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.evi-all { min-height: 28px; padding: 0 10px; border: 1px solid var(--line);
  border-radius: 7px; background: none; font: inherit; font-size: 12px;
  font-weight: 500; color: var(--muted); cursor: pointer; }
.evi-all:hover { background: var(--hover); color: var(--ink); }

/* THE TWISTY BELONGS TO THE DIMENSION ROW AND TO NOTHING ELSE (owner ruling
   2026-08-09, variants A + B).

   It used to be shared: the group header and the dimension row were both
   `<details>` with the same rotating triangle at nearly the same indent, so the
   ONE thing separating a heading from a scored row was the mark on its right --
   and collapsing a group deleted even that, leaving a lone `JOB EVIDENCE` that
   reads as a row whose score failed to load. Sharing an idiom is only a virtue
   when the two things sharing it ARE the same kind of thing.

   The heading is now an `<h4>`; there is no group `<details>` and no marker to
   suppress. HIDING THE MARKER WHILE KEEPING THE `<details>` WAS REFUSED: the
   element is the affordance, so a heading that is still clickable but no longer
   says so is worse than what it replaced. */
.evi-dim > summary { cursor: pointer; list-style: none; }
.evi-dim > summary::-webkit-details-marker { display: none; }
.evi-tw { font-size: 10px; color: var(--faint); line-height: 1;
  transition: transform .12s ease; }
.evi-dim[open] > summary .evi-tw { transform: rotate(90deg); }
/* The heading resets the `<h4>` UA margin -- 1.33em top and bottom would open a
   gap the 4px scale never chose -- and carries the weight and colour that used to
   sit on a nested `.eyebrow` span. Both declarations stay SCOPED here rather than
   changing `.eyebrow`, which is shared by ~40 call sites: bolding it globally
   would restyle every settings section and every panel label to fix three
   headings. (Owner, 2026-08-09: "Job Evidence, Growth & Recognition and Stability
   & Consistency should be bolded so it's more clear.") */
.evi-grp-head { margin: 0 0 8px; font-weight: 700; color: var(--ink); }
/* THE RAIL. Containment is STRUCTURAL, not typographic: the rows are inset and
   hang off a hairline running from under the heading to the last row, so a reader
   sees which heading owns which rows without having to read the type. This is the
   work the heading's font-weight was being asked to do alone.

   SCOPED to a group's own rows. The trailing catch-all `.evi-rows` -- the legacy
   dimensions no group names -- has no heading above it, and a rail hanging off
   nothing would claim an ownership that does not exist.

   EVERY VALUE IS ON THE 4px SCALE (0/4/8/12/16/24/32/48/64). `check_tokens.py`
   8.13's off-scale counter sits at its recorded value with ZERO slack, so a 10px
   inset or a 14px gap is a build failure, not a taste question. The rail's 1px
   border is a hairline, not spacing, and is not counted. */
.evi-grp > .evi-rows { padding-left: 16px; border-left: 1px solid var(--line-soft); }
/* Groups need more air between them than they did as disclosures: nothing folds
   any more, so the gap is the whole boundary. 24px, on-scale. */
.evi-grp + .evi-grp { margin-top: 24px; }

/* The row grid. A FIXED mark track so every mark shares one right edge -- the
   numbers are what a reader scans down, and a track that sizes to its own content
   is the drift CLAUDE.md's rule 2 names. */
.evi-row { display: grid; grid-template-columns: 16px minmax(0, 1fr) auto 70px;
  align-items: center; gap: 12px; padding: 9px 0;
  border-top: 1px solid var(--line-soft); }
.evi-row:hover { background: var(--hover); }
.evi-name { font-size: 13.5px; font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.evi-mark { text-align: right; font-size: 13.5px; font-weight: 600;
  font-variant-numeric: tabular-nums; }
.evi-of { font-size: 12px; color: var(--faint); }
/* A row with no stored evidence still opens -- it says so rather than looking
   broken -- but it must not advertise a disclosure that reveals nothing. */
.evi-dim[data-evi-empty] > summary { cursor: default; }
.evi-dim[data-evi-empty] > summary .evi-tw { opacity: .35; }

/* Five pips. `.pips-ok` / `.pips-warn` carry the COLOUR so the half-fill can be a
   class rather than an inline gradient (check_tokens 8.12, hard zero). */
.pips { display: inline-flex; gap: 3px; }
.evi-nopips { display: inline-block; }
.pip { width: 7px; height: 7px; border-radius: 2px; background: var(--neutral-tint); }
.pips-ok .pip-on { background: var(--ok); }
.pips-warn .pip-on { background: var(--warn); }
.pips-ok .pip-half { background: linear-gradient(90deg, var(--ok) 50%, var(--neutral-tint) 50%); }
.pips-warn .pip-half { background: linear-gradient(90deg, var(--warn) 50%, var(--neutral-tint) 50%); }

/* The expanded body is inset to the name column, so a quote lines up under the
   dimension it belongs to rather than under the disclosure triangle. */
.evi-body { display: flex; flex-direction: column; gap: 12px; padding: 2px 0 16px 28px; }
.evi-lbl { font-size: 10.5px; margin-bottom: 4px; }
.evi-quote { margin: 0; padding: 10px 14px; background: var(--bg);
  border: 1px solid var(--line-soft); border-radius: 8px;
  font-size: 13.5px; line-height: 1.6; }
.evi-note { margin: 0; font-size: 13px; line-height: 1.6; color: var(--muted); }
/* ===== The AI interview drawer (owner ruling 2026-08-25) ==================
   Design: docs/design-handoffs/2026-08-24-interview-report-drawer/.

   EVERY VALUE HERE IS ON THE 4px SCALE, AND THAT WAS THE CONSTRAINT THAT SHAPED
   THE BLOCK RATHER THAN AN ACCIDENT. `check_tokens.py` 8.13 counts each
   off-scale px LITERAL inside a margin/padding/gap declaration and
   `offscale_spacing_max` had ONE unit of slack (634 live / 635 recorded) against
   a design carrying 22/18/14/10/9/7/3px rhythm. `drift_primitives_max` had ZERO
   (52/52), so `justify-content: space-between` was unavailable at any price --
   the design specifies it twice and both are two-track grids here instead, which
   is what `.pf-head`, `.pf-bar` and `.evi-head` already do and what CLAUDE.md's
   layout contract requires anyway (a row aligns to a shared grid, never to its
   own content width).

   The design's off-scale gaps snapped to the nearest scale step: 18 -> 16 (strip)
   and 10 -> 8 (label row, integrity list). At 1440px in a 936px drawer those are
   two-pixel changes inside a flex row; nothing else in the design moves, and the
   alternative was raising a ratchet that may only fall.

   WHAT IS DELIBERATELY REUSED RATHER THAN REDECLARED, because a reused class
   costs nothing against either ratchet and a redeclared one costs its whole
   spacing: `.evi-row` (the design's own row, to the pixel -- its live rule is
   `auto 70px` / `padding: 9px 0`, not the `auto 78px` / `9px 8px 9px 0` the
   handoff quotes), `.qa-seek` (already the seek pill exactly, and it brings a
   hover state, a focus ring and a print arm), `.evi-all` (already the 28px
   pill-button the size toggle and Download are drawn as), `.pf-sec`, `.chip`,
   `.focus-card`, `.pips`, `.langswitch`, `.tabs`. */

/* ---- Band 2: the recording strip ----------------------------------------
   FIXED, above the tab strip, because a seek has to be watchable. The old
   drawer put a player card at the FOOT of the scroller, where the thing a
   timestamp jumps to was the one thing off screen.

   SIZE FOLLOWS INTENT: `bar` while nobody is watching, `watch` on play and on
   ANY seek, `theatre` only when asked. Theatre is never automatic -- it costs
   the scroller ~400px, and a jump that buries the transcript defeats itself. */
.ivr-strip { flex: none; display: flex; align-items: center; gap: 16px;
  padding: 16px 16px 16px 0; border-bottom: 1px solid var(--line); }
/* `.is-watch` and `.is-theatre` were DELETED here on 2026-09-07 (owner): the
   strip is a two-mode switch on both drawers and there is no intermediate size
   left to paint. `.rc-switch` below is the open/collapsed pair. */

/* NOT `.video-player`, and the difference is measured rather than stylistic:
   that class is `max-width: 640px; border-radius: 11px`, so the theatre size --
   100% of an 880px content column -- would silently cap at 640 and the radius
   would not match the design's 8px plate. The plate is the sized box and the
   <video> fills it, so one rule owns the geometry. */
.ivr-plate { flex: none; border-radius: 8px; background: var(--ink);
  overflow: hidden; display: none; }
/* THE TWO MODES, shared by both drawers. `.oa-rc` below overrides the height
   for the overall pane, where the player shares its scroller with a transcript
   and 456px would push the rows it seeks off screen. */
.rc-switch .ivr-strip { flex-direction: column; align-items: stretch; }
.rc-switch .ivr-plate { display: block; width: 100%; height: 456px; }
.rc-switch[data-oa-rc="off"] .ivr-strip { flex-direction: row; align-items: center; }
/* COLLAPSED HIDES THE FRAME WITHOUT UNRENDERING IT (2026-09-12), and
   `display: none` here is why a collapsed player could not start. A media
   element that is not rendered does not load: measured on the deployment, a
   collapsed `<video preload="none">` sat at `networkState: 2` (loading) and
   `readyState: 0` for as long as it was watched, so `loadedmetadata` never
   fired and the pending seek never landed. Nothing played and nothing sought.

   It did not show while the player auto-expanded on play and on every seek,
   because it never stayed collapsed long enough to matter. The owner's ruling
   that the strip is fully operable collapsed -- seek, play, pause -- is what
   made this the blocker.

   IT IS TAKEN OUT OF FLOW, NOT SHRUNK, and `height: 0` alone was not enough --
   it pushed the whole control row off the pane. `.rc-switch .ivr-plate` is
   `display: block; width: 100%`, and collapsed `.ivr-strip` is a ROW: zeroing
   only the height left a full-width flex item still taking the row, so the play
   button, the bar and the labels were shoved past the right edge. `display:
   none` had removed it from the flow, which is the part that had to be
   replaced, not just the height.

   `position: absolute` with a 1px box is the standard rendered-but-invisible
   shape. 1px rather than 0 because a zero-area element is the case browsers are
   entitled to treat as not rendered, which is the whole bug. `pointer-events:
   none` so an invisible frame cannot swallow a click. */
.rc-switch[data-oa-rc="off"] .ivr-plate { position: absolute; width: 1px;
  height: 1px; overflow: hidden; opacity: 0; pointer-events: none; }
.ivr-video { display: block; width: 100%; height: 100%; }

.ivr-meta { flex: 1 1 auto; min-width: 0; width: 100%; }
.ivr-lbl { display: flex; align-items: baseline; gap: 8px; margin-bottom: 8px; }
/* `flex: 1 1 auto; min-width: 0` IS REQUIRED, not decoration: without it a long
   question pushes the scrub bar and both buttons off the row. Ellipsised, with
   the full text in `title`. */
.ivr-now { flex: 1 1 auto; min-width: 0; font-size: 12.5px; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ivr-ctl { display: flex; align-items: center; gap: 12px; }
.ivr-play { flex: none; width: 26px; height: 26px; padding: 0;
  border: 1px solid var(--line); border-radius: 999px; background: var(--panel);
  cursor: pointer; display: inline-flex; align-items: center;
  justify-content: center; color: var(--ink); }
/* ONE BUTTON, TWO ICONS, SWAPPED BY STATE. `data-state` is written by
   `report.js` from the element's own play/pause events, so the glyph cannot
   disagree with what the media is doing -- which is what a script that toggled
   the markup itself would eventually manage. */
.ivr-play[data-state="paused"] .ivr-ico-pause,
.ivr-play[data-state="playing"] .ivr-ico-play { display: none; }
.ivr-pos { flex: none; font-size: 12.5px; font-weight: 500; color: var(--ink);
  font-variant-numeric: tabular-nums; }
.ivr-dur { flex: none; font-size: 12.5px; color: var(--faint);
  font-variant-numeric: tabular-nums; }
/* THE PAINTED TRACK STAYS 4px AND THE HIT AREA IS 24px (2026-09-12). The bar
   became clickable, and a 4px-tall pointer target is the kind nobody can hit --
   WCAG 2.5.8 asks for 24. `background-clip: content-box` paints the track in the
   content box only, so the padding is invisible hit area and the hairline looks
   exactly as it did. Padding rather than a pseudo-element because `overflow:
   hidden` -- which keeps the fill's right edge inside the rounded track -- would
   clip an ::after out of existence. 12px rather than 10 because the 4px spacing
   scale is a ratchet with zero slack (check_tokens 8.13) -- and 28px matches the
   row's other controls, so the strip's height does not move.

   THE HEIGHT IS THE WHOLE BOX, NOT THE TRACK, AND GETTING THAT WRONG ERASED THE
   TRACK. This said `height: 4px` with the same padding. `box-sizing: border-box`
   is global here, so the declared height is the BORDER box: 4px against 24px of
   padding clamps to 24 and leaves a content box 0px tall -- and
   `background-clip: content-box` then paints the grey track into nothing. It
   looked fine at a glance only because `.ivr-fill` is a 4px CHILD that
   `overflow: hidden` clips to the padding box, so the played portion still drew
   while the unplayed track had vanished. 28 - 12 - 12 = the 4px strip. Measured
   in the browser, not reasoned about. */
.ivr-scrub { flex: 1 1 auto; min-width: 0; height: 28px; border-radius: 999px;
  background: var(--line-soft); overflow: hidden;
  padding: 12px 0; background-clip: content-box; cursor: pointer; }
.ivr-scrub:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.ivr-fill { height: 4px; border-radius: 999px; background: var(--ink); }
/* Download is `.evi-all` with the primary ink -- same metrics, different weight
   of intent. A second full rule would have redeclared its padding. */
.evi-all.is-ink { color: var(--ink); }

/* ---- Band 4a: the score breakdown group header --------------------------
   THE ONE PLACE THIS SCORECARD DIFFERS FROM THE CV ONE, and it is legitimate
   rather than drift: `_cv_scorecard.html`'s header says "NO GROUP AVERAGE,
   SUBTOTAL OR BAND, ever" because no CV group composite exists, while
   `category_marks` IS a real stored per-category mark from
   `scoring._assessment_buckets`. A mark on this header is a stored fact.
   Recorded here because a later reader will otherwise read the two files as
   inconsistent.

   THE MARK TRACK IS 70px TO MATCH `.evi-row`'s, and the header carries NO
   `padding-right`. The design asks for 8px; `.evi-row` has none, so 8px here
   would put the group mark 8px left of every row mark under it -- the exact
   defect class CLAUDE.md's layout contract is about. Aligning to the shared
   grid wins over the drawn value. */
.ivr-grp { display: grid; grid-template-columns: minmax(0, 1fr) 70px;
  align-items: center; gap: 12px; margin: 0 0 8px; }
.ivr-grp-mark { text-align: right; font-size: 13.5px; font-weight: 600;
  font-variant-numeric: tabular-nums; }
.ivr-wt { font-weight: 500; color: var(--faint); text-transform: none;
  letter-spacing: 0; font-size: 11.5px; }

/* NOT `.evi-name`, which is `white-space: nowrap`. A truncated CV dimension
   name is still recognisable; a truncated QUESTION is unreadable, and this row
   holds a whole spoken question. Two lines, then clamp. */
.ivr-q { min-width: 0; font-size: 13.5px; font-weight: 500; line-height: 1.45;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; }

/* ---- The three unscored phases ------------------------------------------
   ONE CARD, TONED, never a second block. The retired page showed a coloured
   `.warnbar` AND a state paragraph, which in a 936px drawer is two blocks for
   one fact; the bar's sentences moved into the card. This is
   `_peek_focus.html`'s own `focus-card focus-empty` idiom, so it is the
   drawer's existing vocabulary rather than a new panel.

   THE BORDER STAYS `--line` ON THE WARM FILL. The design asks for `#e8d9b4` on
   the processing card and that value has no token; SPEC item 7 forbids a new
   colour. Painting the dashed border in `--warn-tint` -- the handoff's own
   second option -- would make it the same colour as the fill and therefore
   invisible, so the card would lose its dashed edge entirely. Keeping `--line`
   is the reading that preserves the component. */
.ivr-state { display: flex; flex-direction: column; gap: 8px; }
.ivr-state.is-pending { background: var(--neutral-tint); }
.ivr-state.is-processing { background: var(--warn-tint); }
.ivr-state.is-error { background: var(--bad-tint); border-color: var(--danger-edge); }
.ivr-state-body { margin: 0; font-size: 13.5px; line-height: 1.6; color: var(--ink); }

/* ---- Band 4c: integrity --------------------------------------------------
   Camera findings and connection gaps are ONE list: both are facts about the
   session for a human to weigh and neither moves a number. A gap carries NO
   seek pill -- `db.interview_disconnects` stores a wall-clock time written when
   the candidate came back, not a position in the recording, and the stored
   recording covers only the final page load. See `interviews._iv_drawer_ctx`. */
/* THE ACTIVE TIMESTAMP, so a reviewer can see which row the player is sitting
   on. Border and text go to the primary ink; nothing moves, because a row that
   shifts when you click it is a row you lose your place in. */
.qa-seek.active { border-color: var(--ink); color: var(--ink); }

/* The evidence body's label row: an eyebrow on the left, a seek pill hard right.
   A TWO-TRACK GRID, NOT `justify-content: space-between`. The design specifies
   space-between; `drift_primitives_max` has ZERO slack, and the layout contract
   forbids it anyway -- a row aligned to its own content width is the named cause
   of alignment drift. `justify-self: end` on the second cell is visually
   identical and aligns to a shared track. */
.evi-lbl-row { display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: center; gap: 12px; margin-bottom: 4px; }
.evi-lbl-row > .qa-seek { justify-self: end; }

.ivr-int-list { display: flex; flex-direction: column; gap: 8px; }
.ivr-int { display: flex; align-items: flex-start; gap: 12px;
  font-size: 13.5px; line-height: 1.6; }
.ivr-int-txt { flex: 1 1 auto; min-width: 0; }

/* The Integrity tab's status glyph. A TICK IS A CLEAN BILL, so it is only ever
   earned: an unknown or partial status renders nothing at all rather than green,
   because "we have not looked" must not read as "we looked and it is fine".
   That distinction is `interviews._iv_integrity_findings`' own docstring. */
.ivr-glyph { display: inline-flex; align-items: center; justify-content: center;
  width: 15px; height: 15px; border-radius: 999px; font-size: 10px;
  font-weight: 700; line-height: 1; }
.ivr-glyph.is-warn { background: var(--warn-tint); color: var(--warn-ink); }
.ivr-glyph.is-ok { background: var(--ok-tint); color: var(--ok-ink); }

/* ---- The interview drawer's header alignment ----------------------------
   SCOPED, and this is CLAUDE.md layout rule 5 applied deliberately. `.pf-head`
   is the SHARED shell header -- the CV drawer, the Overall drawer and every
   feature focus key render it -- and it declares `align-items: start`. The
   design wants the interview header's two columns centred. Editing the bare
   selector would silently re-lay-out two other drawers that nothing measures,
   which is how six of the last nine alignment defects happened: a correct rule
   surviving into a container it was never measured in. `align-items` is not a
   spacing property, so a scoped override is free against 8.13. */
.peek-focus.is-interview .pf-head { align-items: center; }

/* ===== The Overall assessment drawer (owner design 2026-08-25) ============
   docs/design-handoffs/2026-08-25-overall-assessment-drawer/.

   EVERY VALUE HERE IS ON THE 4px SCALE. `drift_primitives_max` is at ZERO slack
   (52/52) so `justify-content: space-between` is unavailable at any price, and
   `offscale_spacing_max` has ONE unit (634/635). The design's 22/14/26/24px
   rhythm is paid for by REUSING classes that already declare it -- `.pf-sec`,
   `.pf-evi`, `.evi-row`, `.focus-card`, `.chip`, `.evi-all`, `.attach-pill`,
   `.tabs` -- rather than by re-declaring it here. A reused class costs nothing
   against either ratchet; a redeclared one costs its whole spacing.

   THE TWO PANES ARE INCLUDES, NOT CLONES, so most of this drawer needs no CSS
   at all: the CV pane is `_peek_focus_cv.html` whole and the interview
   scorecard is `_iv_scorecard.html`, both already styled. What is left is the
   Recommendation band, the tab scores and the pending box. */

/* ---- Band 2: the Recommendation ----------------------------------------
   A native <details>, so it works with scripts off and gets `aria-expanded`
   free. The label swaps on `[open]` in CSS rather than by script: this fragment
   is injected via innerHTML, where a parsed <script> is flagged "already
   started" and never runs. */
.oa-rec { flex: none; border-bottom: 1px solid var(--line); padding: 0 16px 0 0; }
.oa-rec-head { display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: center; gap: 12px; padding: 16px 0; cursor: pointer;
  list-style: none; }
/* The default disclosure marker is suppressed on BOTH engines: the button IS
   the affordance here, and a triangle beside it would be two of them. */
.oa-rec-head::-webkit-details-marker { display: none; }
.oa-rec-head::marker { content: ""; }
.oa-rec[open] .oa-rec-head { padding: 16px 0 8px; }
.oa-rec-btn { justify-self: end; color: var(--ink); }
.oa-rec .oa-rec-close, .oa-rec[open] .oa-rec-open { display: none; }
.oa-rec[open] .oa-rec-close { display: inline; }
.oa-rec-body { margin: 0 0 16px; font-size: 13.5px; line-height: 1.6;
  color: var(--ink); text-wrap: pretty; }

/* ---- Band 3: the tab scores --------------------------------------------
   BAND-COLOURED BECAUSE THE DIVERGENT CASE IS THE POINT: a CV `Good 71` against
   an interview `Limited 42` is a disagreement a recruiter must see without
   clicking, and a grey number hides it. The colour carries DATA; the active
   tab's underline carries STATE. The band class is composed in Python, so this
   rule only sets weight and rhythm. */
.oa-tab-score { margin-left: 8px; font-weight: 600;
  font-variant-numeric: tabular-nums; }

/* ---- The pending box ----------------------------------------------------
   `.focus-card.focus-empty` supplies the dashed border and the radius; this
   only stacks the chip above the sentence. */
.oa-pending { display: flex; flex-direction: column; gap: 8px; }
.oa-pending-body { margin: 0; font-size: 13.5px; line-height: 1.6;
  color: var(--ink); max-width: 92ch; }

/* ---- The Overall drawer's Recording tab: a two-mode sticky strip --------
   IT REUSES THE INTERVIEW DRAWER'S STRIP and overrides three things: the
   geometry, the sticky position, and how many modes the toggle has. Nothing
   here re-declares the controls -- the play button, the scrub, the position and
   the duration are `.ivr-*` and unchanged, which is what keeps one player in
   the product rather than two.

   STICKY IS THE POINT OF THE TAB. A timestamp clicked at the bottom of a long
   transcript must seek something still on screen; that is the reason the retired
   report page had a sticky rail. `--panel` behind it, because a transparent
   sticky element lets the transcript scroll visibly through the controls.

   330px, NOT THE INTERVIEW DRAWER'S 456px: there the player owns the whole body,
   here it shares the pane with the transcript it seeks. */
.oa-rc { position: sticky; top: 0; z-index: 1; background: var(--panel); }
/* The overall pane's own height. The FLEX rules moved to `.rc-switch`, which
   this element also carries -- only the number is specific to this pane. */
.oa-rc .ivr-plate { height: 330px; }
/* COLLAPSED MEANS COLLAPSED -- no frame at all, and the transcript gets the
   pane. The row layout returns with it, so the controls sit on one line. */
/* The collapsed rules moved to `.rc-switch` with the rest of the behaviour. */

/* ---- The Overall drawer's Interview guide -------------------------------
   TWO COLUMNS, HEADED ONCE. `LISTEN FOR` appeared on all seven rows in an
   earlier draft, and a label that repeats on every row is not a label, it is a
   column -- print it once and let position identify it. Same reason the
   scorecard's marks carry no per-row label.

   A GRID, NOT `space-between`: `drift_primitives_max` is at zero slack, and the
   layout contract forbids a row aligned to its own content width regardless.

   60 / 40, PROPORTIONAL, NOT A FIXED TRACK (owner, 2026-08-25). `Listen for` was
   `300px` against `minmax(0, 1fr)`, which MEASURES 71.6 / 28.4 at 1745px -- and a
   fixed track takes a SMALLER share the wider the window, so the column the owner
   called too narrow was at its worst on the widest screen. `3fr / 2fr` is 60 / 40
   of the track space at every width. `minmax(0, ...)` on BOTH so a long unbroken
   string in either column cannot push its own track past the ratio; an `fr`
   track's floor is `auto`, and that is what silently reintroduces the drift the
   ratio was set to remove. Costs nothing against either ratchet -- no px value
   enters, and one leaves. */
.oa-guide-head, .oa-guide-row { display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); gap: 24px; }
.oa-guide-row { padding: 16px 0; border-top: 1px solid var(--line-soft); }
.oa-guide-ask { margin: 0; font-size: 13.5px; font-weight: 500; line-height: 1.5;
  color: var(--ink); }
.oa-guide-why { margin: 4px 0 0; font-size: 12.5px; line-height: 1.5;
  color: var(--faint); }
.oa-guide-listen { padding-left: 24px; border-left: 1px solid var(--line-soft);
  font-size: 13px; line-height: 1.6; color: var(--ink); }

/* The drawer is the definite height the chain above resolves against, and it
   must not scroll itself -- otherwise the body's scrollbar and the drawer's
   render side by side, which is the double-gutter the wizard already fixed. */
.drawer:has(.peek-focus) { overflow-y: hidden; display: flex; flex-direction: column; }
/* ...AND THE DIV BETWEEN THEM HAS TO CARRY THE CHAIN. `.drawer-body#peekBody` is
   the fragment target the peek HTML is written into; it is an otherwise UNSTYLED
   div, so it defaulted to `display:block` with `min-height:auto` and sat between
   the drawer and `.peek-focus` as a flex ITEM that grows to its content. The
   chain above then resolved against nothing: `.peek-focus` was a block child,
   `.focus-body` never received a constrained height, and the drawer clipped the
   overflow it was told not to scroll.
   MEASURED on verqa-dev, 1440x843, a real CV report: drawer-body 1775px inside an
   843px drawer, 980px of the report unreachable, `.focus-body` scrollable by 0.
   With this rule: drawer-body 795px, drawer clips 0, `.focus-body` scrollable by
   980. The rule was authored 2026-08-03 (44f2408c) against `.drawer > .peek-focus`;
   `#peekBody` already sat between them at that commit, so this never worked.
   Scoped to the focus drawer rather than `.drawer-body` globally -- the plain
   quick-view drawer wants the base rule's own `overflow-y: auto` and must not
   gain a second scroll region. CLAUDE.md's layout rule 5: scope the rule to the
   container it was written for, do not add a counter-rule.
   Do NOT write the base selector followed by a brace anywhere in this comment:
   G7 in tests/test_layout_brand.py finds that rule by splitting the stylesheet on
   the FIRST such literal, so prose above line 3968 that spells it is read as the
   rule body and takes G7 red. That is how this comment failed the sweep once. */
.drawer:has(.peek-focus) > .drawer-body {
  display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
/* Collapsed extracted-CV-text preview: sits below the decision bullets, one
   click to expand. Reuses .cv-pre (detail page) but caps its own height. */
.cv-peek-details > summary { cursor: pointer; font-size: var(--text-sm); color: var(--ink); }
.cv-peek-details .cv-pre { max-height: 220px; overflow-y: auto; margin-top: var(--s2);
  background: var(--bg); border: 1px solid var(--line-soft); border-radius: 8px; padding: 10px 12px; }
/* Narrower pillar grid so meters fit the drawer column comfortably. */
.peek-focus .pillar-row { grid-template-columns: 116px 1fr 54px; gap: 10px; }
.peek-focus .pillar-row .plabel { font-size: 13px; }

/* Pipeline per-role collapse header (mirrors .queue-head): a button toggle that
   folds the role's candidate table, count badge staying in the header. */
.role-head-row { align-items: center; }
.role-head { display: flex; align-items: center; gap: var(--s2); margin: 0;
  border: 0; background: transparent; text-align: left; cursor: pointer;
  padding: 4px 6px 4px 2px; border-radius: 8px; font-family: inherit;
  font-size: var(--text-lg); font-weight: 600; color: var(--ink); }
.role-head:hover { background: var(--hover); }
.role-head:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Owner comment 4 (2026-08-01). DIFFERENT ROOT CAUSE from owner comments 3 and
   14, and the diagnostic says so: no grouped `max-width` re-caps anything here,
   and there is no absorber that yields everything. `.row-between` is
   `flex-wrap: nowrap`, so the row can never stack; `.role-head` and
   `.stack-actions` are both `flex: 0 1 auto` and simply shrink TOGETHER until
   the title shreds and the buttons stack three deep.

   MEASURED on /pool focused on a role, sweeping the `.role-panel` container.
   The action container is `.stack-actions` -- confirmed in
   templates/_pool_main.html, NOT the `.role-head-actions` class that also
   exists in this file.

     panel     1200  1100  1040   950   850   750   650   560
     BEFORE
       title lines  1     2     2     2     2     2     4     5
       action rows  1     2     2     2     2     2     2     3
     AFTER
       title lines  1     1     1     1     1     1     1     2
       action rows  1     1     1     1     1     1     1     1

   NOTE THE VIEWPORT NUMBERS, because the plan's `@media (max-width: 900px)`
   was written against the wrong axis: at a 1440px WINDOW this panel is 1040px,
   so the title was already on two lines on an ordinary laptop and a 900px
   viewport query would not have fired until the panel was ~500px. The trigger
   is the CONTAINER, so the fix is expressed on the container -- which also
   means the 65vw peek drawer is handled by the same two rules and needs no
   `.drawer` special case and no second breakpoint.

   `flex: 1 1 auto` gives `.role-head` a max-content flex base, so the two
   items share a line only while BOTH fit at their natural width; below that
   the line breaks and the title gets the full panel. That is the opposite of
   what the same declaration would do on `.v-bannertext` (see owner comment 14
   above, where it is deliberately NOT used) -- because there the goal is to
   keep one line and here the goal is to stop keeping it. */
.role-head-row { flex-wrap: wrap; row-gap: 10px; }
.role-head-row > .role-head { flex: 1 1 auto; }
.role-panel.is-collapsed .queue-chev { transform: rotate(-90deg); }
.role-title { font-weight: 600; }
/* "Not done" chip — an outlined, muted pending state (matches .st-item.todo). */
.chip-todo { color: var(--muted); border-color: var(--line); border-style: dashed; }
/* THE BAR SIZES TO ITS CONTENT, NOT TO HALF THE SCREEN, and `width: max-content`
   is the whole of that fix. A `position: fixed` box with `left` set, `right:
   auto` and `width: auto` is shrink-to-fit into the space from `left` to the
   containing block's right edge -- and `left: 50%` makes that space exactly HALF
   THE VIEWPORT, whatever `translateX(-50%)` does afterwards, because the
   transform moves the painted box and never the layout box.

   MEASURED, before and after, in the real /pool shell (CLAUDE.md layout rule 6).
   The bar was 807px of content squeezed into 720px at 1440 and 768px at 1536, so
   `14 selected`, `Re-screen CV` and `Invite to AI interview` were each wrapped
   onto two and three lines and the pill stood 75px tall -- ALREADY, before this
   lane, on the viewport the alignment suite measures at. It is not in that
   suite's SURFACES list, which is why nothing had seen it. With `max-content` the
   bar is one 47px line at every width down to 940.

   THE OWNER LICENSED THE WIDENING for the CV export ("we can make it wider if
   insufficient space") and this is what taking that permission properly looks
   like: the shared component learns to be as wide as it is, rather than the new
   button being made smaller at one call site to fit a clamp nobody meant.

   `max-width` KEEPS THE OLD PROPERTY THAT ACTUALLY MATTERED -- the bar can never
   run off the screen -- and it is `calc(100% - 32px)` rather than `100vw` so a
   vertical scrollbar is not counted into the width. 32px is on the 4px scale, so
   check_tokens 8.13's off-scale counter is untouched; nothing here is one of the
   three drift primitives either. Below that width the content wraps exactly as it
   does today, which is the honest fallback for a bar with five controls. */
.bulk-float { position: fixed; left: 50%; bottom: 26px; transform: translateX(-50%); z-index: 60;
  display: flex; align-items: center; gap: 12px; padding: 9px 12px 9px 18px;
  width: max-content; max-width: calc(100% - 32px);
  background: #17181c; color: #ffffff; border-radius: 999px;
  box-shadow: 0 12px 32px rgba(20,22,26,.28), 0 4px 10px rgba(20,22,26,.18);
  font-size: 13px; }
:root[data-theme="dark"] .bulk-float { background: #e7e8ea; color: #17181c; }
.bulk-float select { background-color: transparent; color: inherit;
  border: 1px solid rgba(255,255,255,.28);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path d='M1 1l4 4 4-4' fill='none' stroke='%23ffffff' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>"); }
:root[data-theme="dark"] .bulk-float select { border-color: rgba(23,24,28,.28);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path d='M1 1l4 4 4-4' fill='none' stroke='%2317181c' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>"); }
.bulk-float select option { color: var(--ink); background: var(--panel); }
.bf-apply { background: #ffffff; color: #17181c; border: 0; border-radius: 999px;
  padding: 6px 14px; font-size: 12.5px; font-weight: 600; cursor: pointer; }
:root[data-theme="dark"] .bf-apply { background: #17181c; color: #e7e8ea; }
/* Status | Decision groups inside the floating bar (bulk-decision, R1b). */
.bf-group { display: inline-flex; align-items: center; gap: 8px; }
.bf-label { font-size: 11px; letter-spacing: .04em; text-transform: uppercase; opacity: .72; }
.bf-sep { width: 1px; align-self: stretch; margin: 2px 2px; background: currentColor; opacity: .22; }
.bf-clear { background: none; border: 0; color: inherit; opacity: .7; cursor: pointer;
  font-size: 13px; padding: 4px 6px; }
.bf-clear:hover { opacity: 1; }
/* The bar's appearance now reads as an intentional entry, not a silent toggle. */
.bulk-float:not([hidden]) { animation: bulkFloatIn .18s ease-out; }
@keyframes bulkFloatIn {
  from { opacity: 0; transform: translateX(-50%) translateY(12px); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}
/* Discoverability strip above the records table: a persistent select-all +
   helper so the bulk flow is visible before any row is ticked. */
.bulk-hint { display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 8px 2px 12px; }
/* UX-26: THE LABEL CENTRES ITS OWN CONTENTS HERE. `.check-inline` is
   `align-items: flex-start` with `input { margin-top: 3px }` -- correct for a
   MULTI-LINE consent-style checkbox, where the box belongs on the first line
   rather than centred against a paragraph. This row's label is ONE line, so the
   3px was an unexplained nudge and `flex-start` had nothing to align to, while
   the `.meta` sentence beside it centred as a flex item of `.bulk-hint`. The
   owner reported it twice ("this is STILL not aligned"), which is what a nudge
   applied to the wrong element looks like.
   Scoped to `.bulk-hint` rather than changing `.check-inline`, because the
   multi-line consent case it was written for is real and still in use --
   `.check-inline.cv-invite-switch` already opts one other call site back to
   centre, which is evidence the default is wrong more often than it is right.

   AND THE MARGIN IS THE HALF NOBODY WOULD HAVE GUESSED. `label` at :1020 carries
   `margin: 14px 0 6px` -- correct for a STACKED form label sitting above its
   input, wrong for a label that IS the control, inline in a toolbar row.
   `align-items: center` centres the MARGIN BOX, so an asymmetric 14/6 margin
   pushes the visible box down by exactly half the difference: (14-6)/2 = 4px.
   MEASURED on the running build: label box top 409 against the sentence's 405,
   both 20.8px tall, same font, same line-height. That 4px is why the owner
   reported this row as "STILL not aligned" after an earlier attempt -- the
   earlier fix corrected the alignment property while the margin went on quietly
   shifting the box, so the symptom survived every plausible-looking change. */
.bulk-hint > .check-inline { align-items: center; margin: 0; }
.bulk-hint > .check-inline input { margin-top: 0; }
.bulk-selectall { display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
  font-size: 13px; font-weight: 600; color: var(--ink); }
.bulk-selectall input { width: auto; cursor: pointer; }

/* ---- v2 report reading column (interview/test reports) ---- */
.reading-col { max-width: 720px; margin-inline: auto; }
/* v2 whole-row click targets: listing rows read as clickable */
main.content tr:has(td a[href]) { cursor: pointer; }
/* talent toolbar extras */
/* Min-score input shares the pill geometry via input.pill-select above
   (which outranks the base input[type=number] rule); it just isn't a select,
   so drop the chevron and its reserved right padding. */
input.pill-num { max-width: 110px; background-image: none; padding-right: 13px; }
.filter-search { display: flex; align-items: center; gap: 8px; }
.filter-search input[type=search] { width: 220px; border-radius: 999px; padding: 6px 14px; font-size: 12.5px; }
/* The `.filter-row[data-livefilter]` block that stood here is DELETED with
   `.filter-row` itself (2026-09-01) -- every selector in it was a descendant of
   that class, so none of them could match anything once it went. It kept the
   dropdowns, Min score and Search on one desktop row, with the search input
   yielding width first and the bar wrapping below 920px.

   `.filter-search` above is NOT deleted and was ALREADY orphaned before this:
   no template or script names it, and it did not become dead here. Cleaning it
   up is a dead-CSS pass rather than this deletion's business. */

/* ---- v2 dashboard: hero actions, flat queue rows, flat rail ---- */
.dash-actions { display: flex; gap: 8px; flex: 0 0 auto; }
.qrow { display: flex; gap: 14px; align-items: center; padding: 13px 2px;
  border-bottom: 1px solid var(--line-soft); }
.qrow:last-of-type { border-bottom: 0; }
.qrow { cursor: pointer; }
.qrow:hover { background: rgba(28,27,24,.02); }
:root[data-theme="dark"] .qrow:hover { background: rgba(236,233,226,.03); }
.qrow-main { flex: 1; min-width: 0; }
.qrow-top { display: flex; align-items: center; gap: 8px; }
.qrow-meta { font-size: 12.5px; color: var(--muted); margin-top: 1px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.qrow-actions { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; }
/* Compact per-assessment bucket strip on each queue row — the four Pipeline
   buckets (CV/Per/Wr/Int) as small labelled mini-cells pushed to the right of
   the name so a long name truncates rather than crowding the strip. */
.qrow-top .bstrip { margin-left: auto; }
.bstrip { display: inline-flex; gap: 4px; flex: 0 0 auto; }
.bcell { display: inline-block; font-size: 10.5px; font-weight: 600; line-height: 1;
  padding: 3px 5px; border-radius: 5px; border: 1px solid transparent;
  white-space: nowrap; }
.bcell-ok   { background: var(--ok-tint);   color: var(--ok-ink); }
.bcell-warn { background: var(--warn-tint); color: var(--warn-ink); }
.bcell-bad  { background: var(--bad-tint);  color: var(--bad-ink); }
.bcell-todo { background: transparent; color: var(--faint); border-color: var(--line); border-style: dashed; }
.bcell-na   { background: var(--neutral-tint); color: var(--faint); opacity: .75; }
/* One-row legend for the whole queue (styled like the summary/todo chips). */
/* `.bstrip-legend*` DELETED (offscale-headroom, 2026-08-09): the LEGEND above the
   bucket strip is unreferenced -- no template emits it and no suite names it.
   `.bstrip`, `.bcell*`, `.bl-key`/`.bl-band` and `.bswatch` all STAY: `bcell` is
   named by check_tokens.py and smoke_test.py, so the strip itself is not this
   lane's to touch. Deleting the legend is not a half-deletion of the strip --
   they are separate rules and the strip never referenced the legend. */
.bl-key, .bl-band { display: inline-flex; gap: 5px; align-items: center; }
.bswatch { width: 13px; height: 13px; border-radius: 4px; border: 1px solid transparent;
  display: inline-block; flex: 0 0 auto; }
.bl-sep { width: 1px; height: 14px; background: var(--line); }
@media (max-width: 560px) { .bstrip .bcell { padding: 3px 4px; font-size: 10px; } }
.queue-more { display: block; padding: 11px 2px 2px; font-size: 12.5px; color: var(--faint);
  text-decoration: none; }
.queue-more:hover { color: var(--accent); }
.link-danger { background: none; border: 0; padding: 4px 6px; font: inherit; font-size: 12.5px;
  color: var(--faint); cursor: pointer; border-radius: 6px; }
.link-danger:hover { color: var(--bad); background: var(--bad-tint); }
.rail-h { font-size: 12px; font-weight: 600; color: var(--muted); margin: 0 0 2px; }
.rail-sep { height: 1px; background: var(--line); margin: 14px 0 12px; }
.offline-ind { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--muted); }
.offline-ind .oi-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--warn); flex: 0 0 auto; }

/* v2 account-menu popover: white, r13, hairline, soft shadow, 6px padding */
.side-menu { position: absolute; left: 8px; right: 8px; z-index: 30; margin-top: 4px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 13px;
  box-shadow: var(--shadow-2); padding: 6px; min-width: 216px; }
.side-menu.up { bottom: 100%; margin-top: 0; margin-bottom: 4px; }
.side-menu { max-height: calc(100vh - 96px); overflow-y: auto; }
.side-menu-head { padding: 8px 10px 8px; border-bottom: 1px solid var(--line-soft);
  margin-bottom: 4px; overflow: hidden; }
.side-menu-head .smh-name { display: block; font-size: 13px; font-weight: 600; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.side-menu-head .smh-email { display: block; font-size: 11.5px; color: var(--faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.side-menu-label { padding: 8px 10px 3px; font-size: 10.5px; font-weight: 600;
  letter-spacing: .06em; text-transform: uppercase; color: var(--faint); }
.side-menu-sep { height: 1px; background: var(--line-soft); margin: 4px 6px; }
.side-menu a { display: block; padding: 8px 10px; border-radius: 8px; color: var(--ink);
  text-decoration: none; font-size: 13px; }
.side-menu a:hover { background: rgba(28,27,24,.045); color: var(--ink); }
:root[data-theme="dark"] .side-menu a:hover { background: rgba(236,233,226,.06); }
.side-menu a.active { background: var(--accent-tint); color: var(--accent); font-weight: 600; }
.side-menu a.menu-danger { color: var(--bad); }
.side-menu a.menu-danger:hover { background: var(--bad-tint); color: var(--bad-ink); }
/* inline rows (Dark mode toggle · Language value) */
.side-menu-row { display: flex; align-items: center; justify-content: space-between;
  gap: 10px; padding: 8px 10px; border-radius: 8px; font-size: 13px; color: var(--ink);
  cursor: pointer; margin: 0; font-weight: 400; }
.side-menu-row:hover { background: rgba(28,27,24,.045); }
:root[data-theme="dark"] .side-menu-row:hover { background: rgba(236,233,226,.06); }
.side-menu-row.is-static { cursor: default; }
.side-menu-row .smr-val { color: var(--faint); font-size: 12.5px; }
/* G-6: the account menu's workspace list. A switch is a form POST, so the row
   is a <button> -- it must be indistinguishable from the <a> and <div> rows
   beside it, which means resetting the chrome the browser supplies. */
.side-menu-form { margin: 0; }
/* `font-size` RE-DECLARED AFTER `font: inherit`, and that is the whole fix for
   the inconsistent workspace-row size the owner reported (2026-08-03).
   `font` is a SHORTHAND: `font: inherit` resets every longhand it owns,
   font-size included, so it silently overrode `.side-menu-row`'s 13px. The
   CURRENT workspace renders as a `div.side-menu-row` and kept 13px; a
   SWITCHABLE one renders as `button.side-menu-row` and inherited the menu's
   size instead. Same class, two sizes, decided by which tag the row happens to
   be -- which is why it looked arbitrary.
   `font: inherit` still earns its place: it is what stops the UA's default
   button font-family. Only the size needed saying again.
   The WEIGHT difference is deliberate and stays: `.side-menu-row.is-current`
   is 600 because it marks where you are. */
button.side-menu-row { width: 100%; border: 0; background: none; font: inherit;
  font-size: 13px; text-align: left; }
.side-menu-lbl { padding: 6px 10px 2px; font-size: 11px; font-weight: 600;
  letter-spacing: .04em; text-transform: uppercase; color: var(--faint); }
.side-menu-row.is-current { font-weight: 600; color: var(--ink); }
.side-menu-row.is-current .meta { color: var(--faint); font-size: 12px;
  text-transform: capitalize; }
/* compact switch inside the menu */
.side-menu-row .switch input { width: 34px; height: 20px; }
.switch-track.sm { width: 34px; height: 20px; }
.switch-track.sm::after { width: 14px; height: 14px; }
.side-menu-row .switch input:checked + .switch-track.sm::after { transform: translateX(14px); }
/* company identity foot row. `.side-co-logo` (the uploaded mark) is deleted at
   cut-down lane L5; the initial tile below is no longer a fallback, it is the
   only thing this row paints. */
.side-avatar.co { border-radius: 8px; background: var(--neutral-tint); color: var(--muted); }
[hidden] { display: none !important; }

/* ============================================================= */
/* Positions — pipeline board + new-role wizard                   */
/* ============================================================= */
.role-row { padding: var(--s5) var(--s5); }
.role-row + .role-row { margin-top: 14px; }
.role-row h3 { font-size: 16px; }

/* .attn — shape from the shared pill group; dot + tone below. */
.attn::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; opacity: .7; }
.attn.warn { background: var(--warn-tint); color: var(--warn-ink); }
.attn.accent { background: var(--accent-tint); color: var(--accent); }

/* v2 funnel: 4 equal stages, label + number on a baseline row, 3px track.
   No boxes, no arrows. */
.funnel { display: flex; align-items: flex-end; gap: 18px; }
.funnel-stage { flex: 1; min-width: 0; text-decoration: none; }
.funnel-line { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
.funnel-n { font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums; letter-spacing: -.01em; line-height: 1.1; color: var(--ink); }
.funnel-label { font-size: 12px; font-weight: 500; color: var(--faint); text-transform: none; letter-spacing: 0; margin: 0; }
.funnel-stage:hover .funnel-label { color: var(--ink); }
.funnel-track { height: 3px; border-radius: 999px; background: var(--line-soft); overflow: hidden; }
.funnel-svg { display: block; width: 100%; height: 3px; }
.funnel-fill { fill: var(--accent); }
.funnel-fill.ok { fill: var(--ok); }
.funnel-fill.bad { fill: var(--bad); }
/* Extended 9-stage funnel: allow the stages to wrap on narrower widths. */
.funnel-wide { flex-wrap: wrap; row-gap: 16px; }
.funnel-wide .funnel-stage { flex: 1 1 15%; min-width: 96px; }
.funnel-arrow { display: none; }
.role-title a { color: var(--ink); text-decoration: none; font-size: 16px; }
.role-title a:hover { color: var(--accent); }
.role-head-actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
.rmenu .side-menu .menu-item { font-size: 13px; padding: 8px 10px; }

/* `.role-actions` AND `.role-actions-right` ARE BOTH DELETED (UX-53), and the
   sequence is worth keeping because it is a whole class of defect in three steps.
   The class carried `margin-top: 16px; padding-top: 14px; border-top` for a PANEL
   FOOTER. Phase 17 reused it for the pipeline row's four buttons, dragging a
   divider and 30px of top spacing into a table cell -- the stray horizontal line
   the owner reported twice. 2026-08-03 stripped the footer chrome from the base
   rule and recorded that "there are no such uses" of it as a footer, leaving a
   bare flex row that only the pipeline board exercised. UX-53 then folded those
   four buttons into a `...` menu, which removed BOTH remaining call sites, so
   what was left was a base rule nothing in the tree used -- by that same
   comment's own argument a trap for the next reader, who adopts the class and
   inherits whatever it accumulates. Deleted here because this lane is what
   orphaned it. Restore it only together with a call site that wants it. */
.rmenu { position: relative; }
.rmenu .side-menu { left: auto; right: 0; min-width: 150px; }
.rmenu .side-menu form { margin: 0; }
/* `.rmenu-danger` DELETED (offscale-headroom, 2026-08-09): unreferenced. The
   `.rmenu .side-menu` rules above it are live and stay. */

/* UX-06f in-app CV viewer. Sized like the wizard so the two modals read as one
   family; the frame takes the remaining height so a page of A4 is legible
   without an inner scrollbar fighting the PDF viewer's own. */
dialog.cv-viewer { max-width: 1100px; width: min(1100px, calc(100vw - 24px));
  height: min(900px, calc(100vh - 48px)); padding: 0; border-radius: 16px;
  display: none; flex-direction: column; }
dialog.cv-viewer[open] { display: flex; }
.cv-viewer-frame { flex: 1; width: 100%; border: 0; background: var(--panel); }
@media (max-width: 720px) {
  dialog.cv-viewer { width: auto; margin: 12px; height: calc(100vh - 24px); }
}

/* New-role wizard modal */
dialog.wizard { max-width: 1100px; width: min(1100px, calc(100vw - 24px)); padding: 0; border-radius: 16px; }
dialog.wizard form { display: flex; flex-direction: column; max-height: calc(100vh - 48px); }
/* Constant dialog size across all 6 steps: a FIXED height (not content-sized)
   so the outer box never jumps/grows/shrinks as you navigate. The body
   (.wiz-body) is the single flex:1 scroll region — head/stepper and footer stay
   pinned — so tall steps scroll internally and short steps leave the box the
   same size. On short viewports it caps to the viewport (dvh) instead of
   overflowing; the dialog stays centered. Scoped to the new-role wizard so the
   screen-CV modal (also .wizard) keeps its own sizing. */
/* The fixed-height form fills the dialog exactly, so the dialog itself must
   never scroll — otherwise a browser UA-default dialog overflow renders a
   SECOND scrollbar/gutter beside the real one (.wiz-body). Scoped to
   #newRoleDialog: the screen-CV modal (also .wizard) keeps head+stepper
   OUTSIDE its form and may legitimately need dialog-level scroll. */
#newRoleDialog { overflow: hidden; }
#newRoleDialog form { height: min(700px, calc(100dvh - 48px)); max-height: calc(100dvh - 48px); }
.wiz-head { display: flex; justify-content: space-between; align-items: center; padding: 18px 22px; }
/* A wizard's title is NOT a document heading. Both quick-action modals are
   included by base.html on every page, so an <h2> in here landed in every
   page's heading outline at offset 0 while the <dialog> was still closed. The
   <dialog> already names itself with aria-label, so nothing is lost. Sizing is
   copied from the h2 it replaces (the base `h2` rule, --text-md; .wiz-head
   never overrode it) so the modals look identical. */
.wiz-title { font-size: var(--text-md); font-weight: 600; color: var(--ink); letter-spacing: -.01em; }
.wiz-close { background: none; border: none; cursor: pointer; font-size: 18px; line-height: 1; color: var(--faint); }
.wiz-close:hover { color: var(--ink); }
.wiz-steps { display: flex; align-items: center; gap: 8px; padding: 0 22px 16px; border-bottom: 1px solid var(--line); }
.wiz-step { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--faint); white-space: nowrap; }
.wiz-num { width: 24px; height: 24px; border-radius: 999px; background: var(--line-soft); color: var(--faint);
  display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; }
.wiz-step.active { color: var(--ink); }
.wiz-step.active .wiz-num { background: var(--accent); color: #fff; }
.wiz-step.done .wiz-num { background: var(--ok-tint); color: var(--ok-ink); }
/* Edit mode: steps are directly clickable, so give them a button affordance. */
.wiz-step-jump { cursor: pointer; }
.wiz-step-jump:hover { color: var(--ink); }
.wiz-step-jump:hover .wiz-num { box-shadow: 0 0 0 2px var(--accent-tint, var(--line)); }
.wiz-step-jump:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }
.wiz-join { flex: 1; height: 1.5px; background: var(--line); }
.wiz-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 18px 22px; }
.wiz-foot { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 14px 22px; border-top: 1px solid var(--line); }
.wiz-foot-r { display: inline-flex; align-items: center; gap: 8px; }
/* Compact PDPO consent line — pinned above the footer, no bulky panel. */
.wiz-consent { flex: 0 0 auto; padding: 10px 22px; border-top: 1px solid var(--line); }
.wiz-consent .chk { align-items: flex-start; gap: 8px; font-size: 12.5px; color: var(--muted); }
.wiz-consent .chk input { margin-top: 2px; }
.wiz-panel .sec-head:first-child { margin-top: 0; }
/* Screen-CV Step 1 multi-file drop zone (mirrors the apply page uploader) */
.wiz-body .drop-zone { display: block; margin-top: 6px; padding: 26px 18px; text-align: center;
  border: 1.5px dashed var(--line); border-radius: var(--r-lg); background: var(--panel);
  cursor: pointer; transition: border-color .15s ease, background .15s ease; font-weight: 400; }
.wiz-body .drop-zone:hover, .wiz-body .drop-zone.is-over { border-color: var(--accent); background: var(--accent-tint); }
.wiz-body .drop-zone input[type=file] { position: absolute; opacity: 0; width: 1px; height: 1px;
  margin: 0; padding: 0; border: 0; clip: rect(0,0,0,0); }
/* Same defect and same fix as the apply page's uploader -- see candidate.css.
   The input is clipped to 1x1, so `input:focus` paints its border and shadow
   somewhere invisible; the indicator belongs on the frame. This is UX-17's
   merged multi-file New CV control, so it is the recruiter's only upload too. */
.wiz-body .drop-zone:focus-within { outline: 2px solid var(--ring); outline-offset: 2px;
  border-color: var(--accent); }
.wiz-body .drop-zone .dz-text { color: var(--muted); font-size: var(--text-base); }
.wiz-body .drop-zone .dz-text strong { color: var(--ink); }
.cvscreen-panel { max-width: 720px; }
/* "New with AI" job-test question-type picker — a wrapping row of checkboxes. */
/* Step 1 only: let the dropzone fill the tall modal body instead of leaving a
   small box floating above dead space. The body becomes a flex column and the
   zone grows; its text centers vertically. */
#screenForm .wiz-body { display: flex; flex-direction: column; }
#screenForm .drop-zone { flex: 1 1 auto; min-height: 220px; display: flex;
  flex-direction: column; align-items: center; justify-content: center; }
/* New-role wizard, Step 1 only: the job-description box absorbs the leftover
   height of the panel, instead of ending ~70px above the footer and leaving dead
   space under its hint. Same idiom as #screenForm's drop-zone directly above — a
   flex chain, no viewport units and no JS measurement.

   The chain hangs off the PANEL, not the shared .wiz-body: every step renders
   into that one scroll region, and making it a flex column would reflow all four.
   `min-height: 100%` reaches the body's definite height (the form is a fixed
   min(700px, 100dvh - 48px)) without naming a viewport unit here.

   Short viewports and mobile: the textarea keeps its base min-height, so when the
   panel no longer fits, it stops shrinking and .wiz-body scrolls — the same
   single scrollbar as every other step, never a taller-than-viewport dialog. The
   hint and the inline .wiz-err both sit after the textarea in flow, so they stay
   visible above the footer as it grows. */
#newRoleDialog .wiz-panel[data-step-key="basics"] { display: flex; flex-direction: column;
  min-height: 100%; }
#newRoleDialog .wiz-panel[data-step-key="basics"] > textarea[name="job_description"] {
  flex: 1 1 auto; }
/* Missing-role row highlight (review step guard). */
.rev-row.rev-need-role td:last-child { box-shadow: inset 0 0 0 1.5px var(--bad); border-radius: 8px; }
.dz-list { list-style: none; margin: 10px 0 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.dz-item { display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 7px 10px; border: 1px solid var(--line); border-radius: var(--r-md); background: var(--panel); }
.dz-item-name { color: var(--ink); font-size: 13.5px; word-break: break-word; }
.dz-item-rm { flex: 0 0 auto; border: 0; background: transparent; color: var(--faint); cursor: pointer;
  font-size: 14px; line-height: 1; padding: 4px 6px; border-radius: var(--r-sm); }
.dz-item-rm:hover { color: var(--ink); background: var(--line-soft); }
/* Review step: label/value rows */
.rv-row { display: flex; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--line-soft); }
.rv-row:last-child { border-bottom: 0; }
.rv-k { flex: 0 0 130px; color: var(--faint); font-size: 12.5px; }
.rv-v { flex: 1 1 auto; color: var(--ink); font-size: 13.5px; white-space: pre-wrap; word-break: break-word; }
.rv-v.rv-scroll { max-height: 168px; overflow-y: auto; padding: 8px 10px; border: 1px solid var(--line-soft);
  border-radius: 8px; background: var(--line-soft); }
.rv-v.rv-empty { color: var(--faint); font-style: italic; }
.rv-sec { margin-top: var(--s4); font-size: 12.5px; font-weight: 600; color: var(--faint);
  text-transform: uppercase; letter-spacing: .04em; }
/* Review step: authored written / interview questions, listed for confirmation */
.rv-qlist { margin: 6px 0 0; padding-left: 22px; }
.rv-qlist li { font-size: 13px; color: var(--ink); padding: 3px 0; word-break: break-word; }
.rv-qlist li .chip { margin-right: 6px; }
/* A written-question's labelled parts, each on its own line for legible review. */
.rv-qseg { margin-top: 2px; }
.rv-qseg:first-child { margin-top: 0; }
.rv-qseg strong { color: var(--ink); }
/* Step-4 interview builder (1b): ONE interleaved, fully interactive list.
   Category is a colour dot (Standard grey / Job-related blue / Cultural amber)
   with a legend above the list + a 3-way pill group in each row's editor. */
.iv-empty { color: var(--faint); font-style: italic; }
/* Colour dot by category — shared by the legend, the row head and the Sets menu. */
.iv-dot { flex: 0 0 auto; width: 9px; height: 9px; border-radius: 999px;
  display: inline-block; background: var(--faint); }
.iv-dot.cat-standard  { background: var(--faint); }
.iv-dot.cat-technical { background: var(--accent); }
.iv-dot.cat-values    { background: var(--warn); }
/* List header + legend */
.iv-legend-row { display: flex; align-items: center; gap: 16px; margin-top: 4px; }
.iv-list-title { font-weight: 600; font-size: 13px; }
.iv-legend { display: inline-flex; align-items: center; gap: 14px; margin-left: auto;
  font-size: 11.5px; color: var(--muted); }
.iv-legend-i { display: inline-flex; align-items: center; gap: 5px; }
/* Condensed row head: grip · dot · Qn · one-line preview · ✦ / ✕ icon actions.
   Standard rows get a hair-tinted canvas background. */
.iv-row .iv-row-head { gap: 9px; padding: 9px 11px; }
/* Compact grip in the interleaved list (the standalone Questions page keeps 15px). */
.iv-row .ivqrow-grip { font-size: 13px; }
.iv-row[data-cat="standard"] { background: var(--canvas); }
.iv-qn { min-width: 0; font-size: 12px; }
.iv-preview { flex: 1 1 auto; min-width: 0; font-size: 13px; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
/* The interview report's per-question EVIDENCE quote (owner ruling 2026-08-05).
   The candidate's own words, verbatim and never translated, so they are set
   apart from the model's prose in the Notes column beside them: italic, inked
   rather than muted, and free to wrap at any character because the span may be
   Chinese, where there are no spaces to break at. */
.iv-evidence { font-style: italic; color: var(--ink); overflow-wrap: anywhere; }
/* A follow-up's own row in the per-question table (Phase 34). It is nested under
   the question it probes: one step of indent on the QUESTION cell only, so the
   Score, Evidence and Notes columns stay on the table's own grid and the row is
   still readable straight down each column. The indent is a padding on the cell
   rather than a margin on the row -- a row cannot be inset without breaking the
   column alignment every other row shares. */
.iv-perq-fu > td:first-child { padding-left: var(--s5); }
/* The ask-language chip (owner ruling 2026-08-05: the interview is CONDUCTED in
   the role's interview language). It rides in the row HEAD, between the preview
   and the actions, so the effect of the language picker is visible on every
   question at a glance. Geometry is the shared `.chip` family's -- only the
   flex behaviour differs, because the preview beside it is the element allowed
   to absorb the free space. */
/* UX-115 MADE IT THE CONTROL AS WELL AS THE READOUT, so it is a <button> now and
   needs a pointer and a hover tone -- an affordance, not a geometry. Everything
   else still comes from `.chip`: no width, no padding, no font, no radius here.
   The hover is `.chip-check`'s idiom and MOVES NOTHING, because `.chip` already
   carries `border: 1px solid transparent` for exactly this. The focus ring is
   the global `button:focus-visible` rule and is not restated. */
.iv-lang-chip { flex: 0 0 auto; cursor: pointer; }
.iv-lang-chip:hover { border-color: var(--accent); color: var(--ink); }
.iv-row-actions { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 2px; }
.iv-ico { width: 24px; height: 24px; display: inline-flex; align-items: center;
  justify-content: center; border: 0; background: none; border-radius: 6px;
  cursor: pointer; color: var(--faint); font-size: 13px; line-height: 1; }
.iv-ico:hover { background: var(--hover); }
.iv-ico.iv-refine { color: var(--accent); }
.iv-ico:disabled { opacity: .5; cursor: default; }
.iv-row.iv-refining { opacity: .7; }
/* 3-way category pill group in the row editor */
.iv-cat-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin: 12px 0 0; }
.iv-cat-pills { display: inline-flex; flex-wrap: wrap; gap: 6px; }
.iv-cat-pill { padding: 4px 11px; border-radius: 999px; font-size: 12px;
  border: 1px solid var(--line); background: var(--panel); color: var(--muted);
  cursor: pointer; }
/* UX-61: the row's own language picker, sharing `.iv-cat-row`'s wrapping flex
   with the category pills — it is the other thing a question IS. Width and
   chrome come from `.input-narrow`; this rule only stops the select stretching
   to the row's free space, which is `.iv-preview`'s job one row up and would
   otherwise leave a 110px control rendered 300px wide. */
.iv-lang { flex: 0 0 auto; }
.iv-cat-pill:hover { border-color: var(--faint); }
.iv-cat-pill.cat-standard.is-sel  { border-color: var(--faint); background: var(--neutral-tint); color: var(--ink); }
.iv-cat-pill.cat-technical.is-sel { border-color: var(--accent); background: var(--accent-tint); color: var(--accent-d); }
.iv-cat-pill.cat-values.is-sel    { border-color: var(--warn); background: var(--warn-tint); color: var(--warn-ink); }
/* Persistent action row + the two-library Sets menu */
.iv-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line-soft); }
/* Once a role set exists, Generate downgrades from solid-accent primary to a
   lighter ghost-accent "Regenerate" (class toggled by the builder JS). */
.btn.ghost.iv-regen { color: var(--accent); border-color: var(--ring); }
.btn.ghost.iv-regen:hover { background: var(--accent-tint); border-color: var(--accent); }
.iv-sets-wrap { position: relative; display: inline-flex; margin-left: auto; }
.iv-sets-msg { display: block; }
.iv-sets-sec { display: flex; flex-direction: column; }
.iv-sec-h { display: flex; align-items: center; gap: 6px; }
.iv-sets-list { max-height: 116px; overflow-y: auto; }
.iv-set-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wz-pop-save { display: flex; align-items: center; gap: 6px; width: 100%; text-align: left;
  padding: 7px 10px; border: 0; background: none; border-radius: 7px; cursor: pointer;
  font-size: 12.5px; color: var(--accent); }
.wz-pop-save:hover { background: var(--hover); }
.iv-saveform { padding: 4px 2px 2px; }
.iv-savename { width: 100%; box-sizing: border-box; margin-top: 4px; }
.iv-saveform-act { display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px; }
/* Sets popover opens UPWARD (anchored to the action row) so it never overflows
   the dialog; widened for two scrollable library sections. */
.wz-pop.wz-pop-up { top: auto; bottom: calc(100% + 6px); min-width: 300px; max-height: 320px;
  overflow-y: auto; }
/* Styled wizard select: keeps the app's standard form-control geometry (padding,
   border, radius, focus ring inherited from the base `select` rule) but swaps the
   dated native OS arrow for the app's own chevron. Shared by the personality
   instrument picker and the per-row interview category select so every wizard
   select matches. The chevron SVG reuses the --faint stroke used by .pill-select. */
.wz-select { appearance: none; -webkit-appearance: none; cursor: pointer;
  padding-right: 34px; background-repeat: no-repeat; background-position: right 13px center;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path d='M1 1l4 4 4-4' fill='none' stroke='%238b8f98' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>"); }
.wt-proposed .wt-prop-item { display: flex; gap: 8px; align-items: flex-start;
  padding: 6px 0; border-top: 1px solid var(--line-soft); }
.wt-proposed .wt-prop-item:first-child { border-top: 0; }
.wt-proposed .wt-prop-item input { margin-top: 3px; }
/* Responsive: on narrow screens the wizard goes near-full-width with padding */
@media (max-width: 680px) {
  dialog.wizard { width: auto; max-width: none; margin: 12px; }
  dialog.wizard form { max-height: calc(100vh - 24px); }
  .wiz-steps .wiz-step span:not(.wiz-num) { display: none; }  /* dots only when tight */
  .rv-k { flex-basis: 96px; }
}
/* ===== Wizard Steps 3 & 4 refinement (Assessments / AI interview) =========
   Collapsible cards, hoisted paper-time bar, question-type + draft chips, a
   scope toolbar with a reuse-set popover, and collapsible category groups.
   All reuse existing tokens; no raw hex. Icons are inline stroke SVG matching
   the app's icon system. */
.wz-intro { color: var(--muted); max-width: 540px; font-size: 13.5px; margin: 0 0 14px; }
.wz-intro strong { color: var(--ink); font-weight: 600; }
.wz-ico { flex: 0 0 auto; }
/* Hoisted global paper time-limit bar */
.wz-timebar { display: flex; align-items: center; gap: 12px; background: var(--accent-tint);
  border: 1px solid var(--ring); border-radius: 12px; padding: 12px 16px; margin: 0 0 16px; }
.wz-timebar-l { display: flex; flex-direction: column; gap: 1px; }
.wz-timebar-l strong { font-size: 14px; font-weight: 600; color: var(--ink); }
.wz-timebar-l .wz-sub { color: var(--muted); font-size: 12.5px; }
.wz-timebar-r { margin-left: auto; display: inline-flex; align-items: center; gap: 7px; }
.wz-timebar-r input { width: 66px; text-align: center; }
.wz-timebar-r .wz-unit { color: var(--muted); font-size: 13px; }
/* Collapsible assessment cards */
.wz-card { border: 1px solid var(--line); border-radius: 12px; background: var(--panel); margin-top: 12px; }
/* Open state keeps the neutral hairline border (matching the other wizard steps'
   panels); the revealed body + rotated chevron signal open — no full-card accent
   frame. */
.wz-card.wz-off { background: var(--canvas); }
.wz-card-head { display: flex; align-items: center; gap: 11px; padding: 14px 16px; cursor: pointer; }
.wz-card-head:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 12px; }
.wz-card.wz-off .wz-card-head { cursor: default; }
.wz-card-title { font-size: 14.5px; font-weight: 600; color: var(--ink); }
.wz-pill { font-size: 11px; padding: 2px 8px; border-radius: 999px;
  background: var(--neutral-tint); color: var(--muted); white-space: nowrap; }
.wz-card-sum { margin-left: auto; font-size: 12.5px; color: var(--muted); }
.wz-chev { color: var(--faint); transition: transform .15s ease; }
.wz-card.wz-open > .wz-card-head .wz-chev { transform: rotate(180deg); }
.wz-card.wz-off .wz-chev { visibility: hidden; }
.wz-card-body { display: none; padding: 4px 16px 16px; border-top: 1px solid var(--line-soft); }
.wz-card.wz-open > .wz-card-body { display: block; }
.wz-card-body .panel { padding: 0; border: 0; background: none; }
/* On/off toggle switch (wraps the existing enablement checkbox) */
.wz-toggle { position: relative; display: inline-flex; flex: 0 0 auto;
  width: 38px; height: 22px; cursor: pointer; }
.wz-toggle input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; z-index: 1; }
.wz-toggle .wz-track { position: absolute; inset: 0; background: var(--line);
  border-radius: 999px; transition: background .15s ease; }
.wz-toggle .wz-knob { position: absolute; top: 3px; left: 3px; width: 16px; height: 16px;
  border-radius: 999px; background: var(--panel); box-shadow: var(--shadow-1);
  transition: transform .15s ease; }
.wz-toggle input:checked ~ .wz-track { background: var(--accent); }
.wz-toggle input:checked ~ .wz-knob { transform: translateX(16px); }
.wz-toggle input:focus-visible ~ .wz-track { box-shadow: 0 0 0 3px var(--accent-tint); }
/* Labelled pill toggle in a row (e.g. Step-5 CV auto-invite): centre the switch
   against its caption so the switch, caption and the controls below share a
   clean, consistent left edge. */
.check-inline.cv-invite-switch { align-items: center; }
/* CV auto-invite: the toggle (with its caption) and the threshold input share
   ONE row — the caption flexes, the threshold + % unit stay together on the
   right. Falls to a stacked layout on narrow screens. */
.cv-invite-row { align-items: flex-start; }
.cv-invite-row .cv-invite-switch { flex: 1 1 auto; min-width: 0; }
.cv-invite-thr { display: inline-flex; align-items: center; gap: 7px; flex: 0 0 auto; }
.cv-invite-thr .wz-unit { color: var(--muted); font-size: 13px; }
/* Question-type / source chips (restyle of the .wt-qtype checkboxes) */
.wz-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 6px; }
.wz-chip { position: relative; display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 11px; border: 1px solid var(--line); border-radius: 999px;
  background: var(--panel); color: var(--muted); font-size: 12.5px; cursor: pointer; }
.wz-chip input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.wz-chip::before { content: ""; width: 0; overflow: hidden; transition: width .1s ease; }
.wz-chip:has(input:checked) { background: var(--accent); border-color: var(--accent); color: #fff; }
.wz-chip:has(input:checked)::before { content: "\2713"; width: auto; margin-right: 1px; }
.wz-chip:has(input:focus-visible) { box-shadow: 0 0 0 3px var(--accent-tint); }
/* Review-drafts header + selectable draft cards (restyle of the proposal tray) */
.wz-drafts { border: 0; background: none; padding: 0; margin-top: 12px; }
.wz-drafts-head { display: flex; align-items: center; gap: 7px; margin-bottom: 8px; }
.wz-drafts-head strong { font-size: 13.5px; font-weight: 600; }
.wz-drafts-head .wz-muted { color: var(--faint); font-size: 12.5px; }
.wz-drafts-head .wz-regen { margin-left: auto; }
.wz-draft { display: flex; align-items: flex-start; gap: 10px; padding: 9px 11px;
  border: 1px solid var(--line); border-radius: 9px; background: var(--panel);
  color: var(--muted); font-size: 13px; cursor: pointer; margin-top: 7px; }
.wz-draft input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.wz-draft .wz-box { flex: 0 0 auto; width: 17px; height: 17px; border-radius: 5px;
  border: 1px solid var(--line); display: inline-flex; align-items: center;
  justify-content: center; color: transparent; font-size: 12px; margin-top: 1px; }
.wz-draft:has(input:checked) { border-color: var(--accent); background: var(--accent-tint); color: var(--ink); }
.wz-draft:has(input:checked) .wz-box { background: var(--accent); border-color: var(--accent); color: #fff; }
.wz-draft:has(input:focus-visible) .wz-box { box-shadow: 0 0 0 3px var(--accent-tint); }
.wz-commit { display: flex; justify-content: flex-end; margin-top: 10px; }
.wz-foot-note { color: var(--faint); font-size: 11.5px; margin: 8px 0 0; }
/* Step-3 intro flows to modal width (no fixed cap) */
.wz-intro.wz-intro-full { max-width: none; }
/* Collapsed summary is noise once the card is expanded (content is visible). */
.wz-card.wz-open > .wz-card-head .wz-card-sum { display: none; }
/* ---- Written builder: empty-state hero, condensed rows, per-row actions ----
   AI generation is the primary empty-state path; generated + hand-written
   questions share one condensed, editable row with icon-only Refine / delete. */
.wt-hero { border: 1px dashed var(--ring); background: var(--accent-tint);
  border-radius: 11px; padding: 16px; text-align: center; margin-top: 8px; }
.wt-hero-t { font-size: 13.5px; font-weight: 600; color: var(--ink); margin-bottom: 3px; }
.wt-hero-p { font-size: 12.5px; color: var(--muted); margin: 0 auto 12px; max-width: 640px; }
.wt-hero-alt { margin-top: 10px; }
.wt-link { background: none; border: 0; color: var(--accent); font-size: 12.5px;
  font-weight: 500; font-family: inherit; cursor: pointer; padding: 0; }
.wt-link:hover { text-decoration: underline; }
.wt-hero-lbl { display: block; font-size: 13px; font-weight: 500; color: var(--ink); margin: 16px 0 6px; }
/* Left-align the chip rows inside a centred, capped block so any count (5, 6,
   7+) wraps cleanly — no lone centred orphan chip on a second row. */
.wt-hero .wz-chips { justify-content: flex-start; max-width: none; margin: 0; }
/* Condensed question row (a compact variant of .q-block) */
#wtQuestions { counter-reset: wtq; }
.q-block.wt-row { counter-increment: wtq; margin-top: 8px; padding: 8px 10px;
  border: 1px solid var(--line); border-top: 1px solid var(--line); border-radius: 9px; }
.wt-row-head { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.wt-qn { font-size: 12px; font-weight: 600; color: var(--muted); }
.wt-qn::before { content: "Q" counter(wtq); }
.wt-ico { margin: 0; width: 24px; height: 24px; min-height: 24px; display: inline-flex;
  align-items: center; justify-content: center; border: 0; background: none; padding: 0;
  border-radius: 6px; cursor: pointer; font-family: inherit; }
.wt-ico:hover { background: var(--hover); }
.wt-ico:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }
.wt-ico[disabled] { opacity: .5; cursor: default; }
.wt-refine { margin-left: auto; color: var(--accent); }
.wt-refine .ai-spark { font-size: 14px; }
.wt-del { color: var(--faint); }
.wt-del:hover { color: var(--ink); }
.wt-ta { min-height: 52px; }
.wt-row-busy { opacity: .6; }
/* Persistent action row below the list (once >=1 question exists) */
.wt-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--line-soft); }
.btn.ghost.wt-accent { color: var(--accent); border-color: var(--ring); }
.btn.ghost.wt-accent:hover { background: var(--accent-tint); border-color: var(--accent); }
/* Step 4: intro row, scope toolbar + reuse-set popover */
.wz-introrow { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
/* Full-width lead (the "Always included" chip that used to sit at its right was
   removed — AI interview is mandatory and the surrounding copy already says so). */
.wz-introrow .wz-intro { margin: 0; flex: 1 1 auto; max-width: none; }
/* Step 4 nests the interview builder in a .panel whose own box already frames
   it; drop the panel's redundant top padding + the block's top margin so the
   intro-to-box gap matches the other steps' rhythm. */
/* P2-11: panels are keyed, not numbered — a wizard step is addressed by
   data-step-key so a step change is not also a stylesheet edit. Each selector
   below names the step it actually matched before the change, so rendering is
   unchanged. */
.wiz-panel[data-step-key="interview"] > .panel { padding-top: 0; }
.wiz-panel[data-step-key="interview"] > .panel > .assess-block { margin-top: 0; }
/* Review: same treatment — drop the panel's top padding + the first block's top
   margin so the intro-to-first-section gap matches the rhythm. */
.wiz-panel[data-step-key="review"] > .panel { padding-top: 0; }
.wiz-panel[data-step-key="review"] > .panel > .assess-block { margin-top: 0; }
/* THE TWO SCOPED SPINNER RULES THAT USED TO BE HERE ARE GONE (owner 2026-08-06).
   `.wiz-panel[data-step-key="review"] input[type=number].set-ctl` and
   `.wz-timebar-r input[type=number]` are now ONE unscoped rule beside the base
   `input[type=number]` declaration — see the comment there for why product-wide
   was chosen over this step. The review-scoped pair had matched nothing since
   P1-08 anyway, so half of what was here was already decoration. Do not add a
   third scoped copy: arm 8.14 of tests/check_tokens.py fails a spinner rule that
   is narrowed to one surface. */
/* (The old `.wiz-panel[data-step="6"]` review rule died with P1-08's Scoring
   step: Review became step 5, so it has matched nothing since. Its effect —
   dropping the review panel's top padding — is the data-step-key="review" rule
   above, which is the one that was really doing the work.) */
.wz-toolbar { display: flex; align-items: center; gap: 12px; background: var(--canvas);
  border: 1px solid var(--line-soft); border-radius: 10px; padding: 10px 14px; margin-bottom: 14px; }
.wz-toolbar-l { font-size: 13px; }
.wz-toolbar-l strong { font-weight: 600; color: var(--ink); }
.wz-toolbar-l .wz-muted { color: var(--faint); }
.wz-toolbar-r { margin-left: auto; position: relative; display: inline-flex; gap: 8px; }
.wz-pop { position: absolute; top: calc(100% + 6px); right: 0; z-index: 30;
  min-width: 262px; background: var(--panel); border: 1px solid var(--line);
  border-radius: 10px; box-shadow: var(--shadow-2); padding: 6px; }
.wz-pop[hidden] { display: none; }
.wz-pop-h { font-size: 11.5px; color: var(--faint); text-transform: uppercase;
  letter-spacing: .04em; padding: 6px 10px 4px; }
.wz-pop-item { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
  padding: 8px 10px; border: 0; background: none; border-radius: 7px; cursor: pointer;
  font-size: 13px; color: var(--ink); }
.wz-pop-item:hover { background: var(--hover); }
.wz-pop-item.sel { background: var(--accent-tint); color: var(--accent); }
.wz-pop-item .wz-count { margin-left: auto; font-size: 11px; padding: 1px 7px;
  border-radius: 999px; background: var(--neutral-tint); color: var(--muted); }
.wz-pop-div { height: 1px; background: var(--line-soft); margin: 5px 0; }
.wz-pop-link { display: block; padding: 7px 10px; font-size: 12.5px; color: var(--accent); text-decoration: none; }
.wz-pop-link:hover { text-decoration: underline; }
.wz-sr-select { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }
/* Step 4: collapsible category groups. (The read-only HR fold and the pinned
   "Always last" closer are retired in 1b — both are ordinary rows now.) */
.wz-grp { border: 1px solid var(--line); border-radius: 11px; background: var(--panel); margin-top: 8px; }
.wz-grp-head { display: flex; align-items: center; gap: 9px; padding: 11px 14px;
  cursor: pointer; list-style: none; font-size: 13px; }
.wz-grp-head::-webkit-details-marker { display: none; }
.wz-grp-head strong { font-weight: 600; }
.wz-dot { flex: 0 0 auto; width: 9px; height: 9px; border-radius: 999px; }
.wz-dot.tech { background: var(--pillar-tech); }
.wz-dot.pers { background: var(--pillar-pers); }
.wz-grp-head .wz-grp-r { margin-left: auto; display: inline-flex; align-items: center;
  gap: 8px; color: var(--faint); font-size: 12.5px; }
.wz-grp[open] .wz-grp-head .wz-chev { transform: rotate(180deg); }
.wz-grp-body { padding: 4px 12px 10px; border-top: 1px solid var(--line-soft); }
.wz-addq { display: inline-flex; align-items: center; gap: 6px; margin-top: 6px;
  padding: 7px 8px; border: 0; background: none; color: var(--accent);
  font-size: 13px; cursor: pointer; border-radius: 7px; }
.wz-addq:hover { background: var(--hover); }
.wz-grp-div { height: 1px; background: var(--line-soft); margin: 6px 0 0; }
@media (max-width: 680px) {
  .wz-timebar, .wz-toolbar { flex-wrap: wrap; }
  .wz-pop { right: auto; left: 0; }
}
/* ---- Screen-CV wizard: Step 2 (review & assign) + Step 3 (summary) -------- */
.wiz-page { max-width: 960px; }
.p-3 { padding: var(--s3); }
.chip.sm { font-size: 11px; padding: 2px 8px; }
/* Checkbox grid — bulk assign + per-row position multi-select */
.chk-grid { display: flex; flex-wrap: wrap; gap: 6px 16px; margin: 6px 0; }
.chk { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--ink); }
.chk input { margin: 0; }
.rev-pos { max-height: 116px; overflow-y: auto; min-width: 180px; }
/* Simple bordered table used by the review + summary steps */
.tbl-scroll { overflow-x: auto; border: 1px solid var(--line); border-radius: 10px; }
table.tbl { width: 100%; border-collapse: collapse; }
table.tbl th, table.tbl td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--line-soft);
  font-size: 13px; vertical-align: top; }
table.tbl th { color: var(--faint); font-size: 12px; font-weight: 600; background: var(--canvas); }
table.tbl tr:last-child td { border-bottom: 0; }
/* ---- UX-05(3) / UX-09a: ONE grid fix, not one per surface ----------------
   THE SHARED CAUSE, and it is genuinely shared: a `<th>`'s text is inset by the
   cell padding alone, while a `<td>` holding a full-width control insets that
   control by the cell padding and then the CONTROL insets its own text by
   `--field-pad-x` again. Every header therefore sat exactly one control-padding
   to the LEFT of the values under it -- which is what "the text box are not
   aligned with the column title" describes, and what the pipeline board had
   already patched privately with its own `.pos-table th` padding rule.

   `.tbl-fields` is the construction: put it on any table whose body cells hold
   full-width form controls and the headers move to sit over the control text.
   It is opt-in, because a table of plain text cells needs no offset and would
   be made wrong by one. */
.tbl-fields th { padding-left: var(--field-pad-x); }
/* Cells that hold no control opt back out, so a header over a bare icon column
   is not pushed off its own content. */
.tbl-fields th.rev-keep-h { padding-left: 0; }
/* Many candidates: keep column labels visible while the modal body scrolls. */
.review-tbl thead th { position: sticky; top: 0; z-index: 2; }
/* `:not(.chip-entry)` is load-bearing, not tidying. `.review-tbl input[type=text]`
   is (0,2,1) — an attribute selector counts as a class — so it OUT-SPECIFIES
   `.chip-input .chip-entry` (0,2,0) and landed the whole outer control geometry
   on the bare caret inside the Position(s) tag box: a 40px min-height stacked
   INSIDE the box's own padding + border, making that field 54px against the
   Source <select>'s 40px, plus width:100%/min-width:140px that shoved every
   chip onto its own line. The box owns the geometry; the caret inside it must
   not. Scoped here rather than in `.chip-input` because this is the only table
   in the product that blanket-sizes its text inputs over a chip field. */
.review-tbl input[type=text]:not(.chip-entry) { width: 100%; min-width: 140px; box-sizing: border-box; }
/* Single-line review controls (name/email/phone inputs + the Source select) share
   the app's standard input height so each row lines up; cells are top-aligned
   (table.tbl td) so the Position tag box still grows downward as chips wrap. */
.review-tbl input[type=text]:not(.chip-entry),
.review-tbl .pos-picker .pos-chip { min-height: 40px; box-sizing: border-box; }
/* Keep column: ONE control, the reversible ✕ — the checked checkbox beside it
   said the same thing twice, so the fragment's JS hides it (it stays the field
   the server reads, and stays visible without JS). Header text is dropped, so
   the column shrinks to the icon button's own 28px plus the cell padding. */
/* UX-05(2): the ✕ column "is taking too much column width". It is one 28px
   icon button, and with JS running the checkbox beside it is hidden -- so the
   width was almost entirely `table.tbl`'s 12px cell padding on both sides plus
   the nowrap keeping room for the hidden control. Halve the inset; `width: 1%`
   already asks for shrink-to-content and now gets closer to it. */
.rev-keep, .review-tbl th.rev-keep-h { white-space: nowrap; width: 1%;
  padding-left: var(--s2); padding-right: var(--s2); }
.rev-remove .rev-ico-undo { display: none; }
/* Excluded is a STATE, not a deletion: the row and its parsed fields stay put,
   dimmed, and the same button (aria-pressed) puts them back. The Keep cell is
   deliberately left at full contrast so the way back is never dimmed out. */
.rev-row.is-excluded > td:not(.rev-keep) { opacity: .45; }
.rev-row.is-excluded .rev-remove { color: var(--accent); }
.rev-row.is-excluded .rev-remove .rev-ico-x { display: none; }
.rev-row.is-excluded .rev-remove .rev-ico-undo { display: inline; }

/* Screen-CV modal hosts the WHOLE 3-step flow: the dialog is the flex column
   (persistent head + stepper + swappable #screenSwap), so each injected step's
   own wiz-body scrolls while its wiz-foot stays pinned. Scoped to #screenDialog
   so the single-form Edit-role wizard is untouched. */
/* This modal hosts a wide 6-column review table, so it is a wider variant than
   the ~820px new-role wizard. calc(100vw - 48px) keeps sensible margins and
   shrinks to near-full-width on narrow/tablet screens; the body still scrolls. */
#screenDialog { max-width: 1080px; width: min(1080px, calc(100vw - 48px)); }
#screenDialog[open] { display: flex; flex-direction: column;
  min-height: min(620px, calc(100vh - 80px)); max-height: calc(100vh - 80px); }
#screenDialog #screenSwap { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
#screenDialog #screenSwap > form { flex: 1 1 auto; min-height: 0; max-height: none; }
.drop-zone.is-error { border-color: var(--bad); }

/* Review step: source select + its "Other…" free-text escape hatch */
/* .rev-source* deleted with the Screen-CV Source column (UX-05). */

/* Review step: Position(s) type-to-search picker (chips + filter menu).
   Scoped under .pos-picker — a bare .pos-menu here collides with the
   positions-board row action menu (also .pos-menu, fixed-positioned below)
   and its left/right pinning stretched that menu across the viewport.

   The menu therefore carries its OWN class, `posq-menu`: sharing `.pos-menu`
   meant pool.html's document-wide __poolCloseMenus stamped `class="hidden"` on
   it on every click, and `.hidden { display: none }` left the typeahead
   permanently invisible. Because it no longer inherits the board menu's chrome,
   the panel styling is spelled out here. */
.pos-picker { position: relative; min-width: 190px; }
.pos-picker .pos-chip { min-height: 34px; }
.pos-picker .posq-menu { position: absolute; z-index: 5; left: 0; right: 0; margin-top: 4px;
  max-height: 190px; overflow-y: auto; min-width: 0; padding: 0;
  background: var(--panel); border: 1px solid var(--line); box-shadow: var(--shadow-2);
  border-radius: var(--r-md); }
/* Inert "why is this empty" line inside the menu — never an option. */
.posq-note { padding: 8px 11px; font-size: 12.5px; color: var(--muted); }
/* `.linky` is the ONE primitive for "a button that reads as a link", added at
   UX-51 when /positions/new was deleted and its two <a href> affordances became
   wizard triggers: the position picker's empty-state note (base.html) and the
   Screen-CV modal's "Add one" (_screen_cv_modal.html). A <button> because a
   hrefless <a> is not keyboard-focusable. ONE class rather than a rule per
   container (CLAUDE.md layout rule 1), and in the stylesheet rather than inline
   (check_tokens 8.12, hard zero). */
.linky { background: none; border: 0; padding: 0; font: inherit;
  color: var(--accent); cursor: pointer; text-decoration: underline; }
.posq-note a { color: var(--accent); }
.pos-opt { padding: 7px 11px; font-size: 13px; color: var(--ink); cursor: pointer; }
.pos-opt:hover, .pos-opt.is-active { background: var(--neutral-tint); }
/* The real <select>-like list is hidden and the options only appear on focus, so
   without this caret the control reads as an empty free-text box -- i.e. "there
   are no open roles". It is the affordance, not decoration. tabIndex=-1 in the
   enhancer keeps it out of the tab order; the input itself stays the focus stop. */
.pos-caret { margin-left: auto; padding: 0 2px; border: 0; background: none;
  color: var(--muted); font-size: 11px; line-height: 1; cursor: pointer; }
.pos-caret:hover { color: var(--ink); }

/* Candidate Pool — funnel stages are drill-down links; Screen-CV modal */
a.funnel-stage { display: block; text-decoration: none; color: inherit; border-radius: 8px; padding: 2px; margin: -2px; }
a.funnel-stage:hover { background: var(--hover); }

/* ---- Positions: dense table (Vanta) — radial + expander + fixed ⋯ menu ---- */
.pos-table td { vertical-align: middle; }
/* Symmetric horizontal cell padding (reusing the --s3 the global th/td rule
   already applies) so the column headers line up with the body content beneath
   them instead of sitting flush against the table edge. */
.pos-table th, .pos-table td { padding-left: var(--s3); padding-right: var(--s3); }
/* The actions column is content-width and never wraps. It is the LAST of seven
   columns on Interview history, so with no width of its own the six text
   columns took the space and "Report ->" clipped at common widths (observed at
   1568px). width:1% is the standard table idiom for "as narrow as the content";
   nowrap is what stops the link breaking across two lines inside it. */
.pos-table th.col-menu, .pos-table td.col-menu {
  width: 1%; white-space: nowrap; text-align: right;
}
/* Candidates count deep-links to the role's pipeline — reads as the number,
   with an underline affordance on hover/focus. */
.cand-count { color: var(--ink); text-decoration: none; }
.cand-count:hover, .cand-count:focus-visible { text-decoration: underline; }
/* G9 (owner, 2026-07-31): the two-letter square is deleted. It was the INITIALS
   OF A JOB TITLE ("SH" for Senior HR Manager) -- derived from the role, not from
   a person, so it identified nobody and collided between similar titles, while
   costing 26px + gap in a table that already clipped its actions column. The
   gap goes with it: there is nothing left to sit beside the name.
   UX-53: the `.rname` FLEX WRAPPER went too, and this is a container change
   rather than a deletion of the role name. It existed because the name sat in a
   `<td>` that needed a flex context; the board's role cell is now a grid cell
   and `.rname-block` is that context, so the wrapper had nothing left to do and
   the title's own rule is re-anchored to the line it actually lives on. */
.rname-l1 .rt { font-weight: 600; color: var(--ink); }
.pos-expand { display: inline-flex; align-items: center; gap: 8px; padding: 2px 6px;
  border: 0; background: none; cursor: pointer; font: inherit; color: var(--ink); border-radius: 6px; }
.pos-expand:hover { background: var(--hover); }
.rad { display: inline-flex; align-items: center; gap: 8px; }
.rad svg { width: 16px; height: 16px; transform: rotate(-90deg); }
.rad .track { stroke: var(--line-soft); }
.rad .fill { stroke: var(--accent); }
.rad .num { font-size: 12.5px; color: var(--ink); }
.pos-expand .exp-chev { color: var(--faint); transition: transform .15s ease; }
.pos-row.is-exp .exp-chev { transform: rotate(90deg); }

/* ---- UX-53: the positions BOARD is ONE GRID, header and rows together ------
   Owner design handoff 2026-08-05, the board half of UX-52's pass. This block
   REPLACES the v2 `<table>` board: `.pos-table-v2` and its `col-num` /
   `tr.pos-row` rules, the `.pipe-cell` / `.pipe-exp-l` / `.pipe-bar` /
   `.pipe-track` / `.pipe-seg-*` two-segment bar, and `.pos-ctrls` with its
   `.role-actions` override and the `.col-acts` width -- all of which existed to
   make four labelled buttons and a 230px cell fit inside a table row.

   `.pos-table` ITSELF DOES NOT RETIRE, and the handoff's claim that it does was
   the most expensive thing in it to have believed. It is load-bearing for three
   OTHER tables (interview history, the role screening rules, the weighting
   change history), `base.html`'s row-click delegate bails on it, and
   `tests/check_tokens.py` asserts `.pos-table th.col-menu` carries a
   `white-space: nowrap` written for Interview history's seventh column. Only the
   `-v2` half was ever this board's.

   WHY A GRID AT ALL, given that a table's columns agree for free. Because the
   cells are no longer text: a stacked name+subtitle, a bar with a legend under
   it, a chip and a menu do not want `vertical-align`, and every one of them was
   fighting the table. The cost is that hand-written divs only line up if ONE
   declaration lays out the header and the rows -- which is why `.posb-head` is a
   `.posb-row` and there is no header template of its own. Measured, not asserted:
   tests/test_ui_alignment.py, UX-53 block.

   EVERY VALUE HERE IS ON THE 4px SCALE. `check_tokens.py` 8.13 reported zero
   slack on all three ratchets when this was written -- UX-52 lowered the recorded
   numbers in the same commit as the live ones, so there is no headroom, and the
   roadmap's "~28 of headroom" was a misreading of that. The handoff's 14/10/7/6/3
   spacings are not here; nothing in this block asks the ratchet to bless a number
   nobody can explain, and retiring the rules above pays five back. */
/* KEPT, NOT REWRITTEN: the role name/subtitle stack is the same primitive it was
   under the table, and it is what the new role cell reuses. Rule 1 in practice --
   the cell changed container, the name did not change shape. */
.rname-block { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.rname-l1 { display: flex; align-items: center; gap: 8px; }
.rname-l1 a { text-decoration: none; }
.rt-meta { font-size: 12px; color: var(--faint); }
/* THE ACTIONS TRACK IS 136px, AND THE ARITHMETIC IS WRITTEN OUT BECAUSE IT HAS
   BEEN RE-DERIVED TWICE. It was 44px, then 80px when UX-59 pinned the copy icon
   beside the `...` trigger, and it is 136 since 2026-09-06, when the owner
   retired that trigger and asked for its three items as three icons on the row:
     26px copy button + its 4px margin                            = 30
     the P0-3 readiness marker, 14 + 5, when the public link is unset = 19
     three 28px `.icon-btn` controls, flush                       = 84
                                                                   --- 133
   rounded up to 136 for the 4px scale. It clears BOTH readiness states, because
   a track sized for the ready state would reflow every row on a workspace that
   has not set its public URL -- and that is the workspace the marker exists for.
   ONE token, so the closed board follows without a second declaration.

   A FIXED TRACK AND NOT `max-content`, and this is the third time that is worth
   saying in this sheet. `.posb-head` IS a `.posb-row`, so the header strip and
   every row are SEPARATE grid containers sharing one declaration -- an intrinsic
   track resolves PER CONTAINER, so the header's empty actions cell would measure
   near zero while a row's measured 133, and the `1fr` role column beside them
   would then divide two different remainders. That is exactly the defect
   `SCH TABLES` caught on the scheduling hub at 68px of spread. */
/* THREE TRACKS SINCE 2026-09-07 (owner): role, pipeline, actions. The 224px
   `Needs you` track went with its column. `.posb-table`'s `min-width` FALLS BY
   THE SAME 224 in the same edit -- leaving the floor at 996 would have handed
   every one of those pixels to the role NAME, which is `minmax(0, 1fr)`, and a
   1fr track absorbs slack in silence. That is UX-121's defect from the generous
   direction, and it is why the two numbers move together. */
/* THE DIVIDE MOVED LEFT (owner, 2026-09-07, from a screenshot with a redline
   on it: "The pipeline column is now far to right, can you move to
   approximately the redline position, also the column with of both Role and
   Pipeline can be wider").
   ROLE STOPS BEING THE GREEDY TRACK, AND THAT IS THE WHOLE CHANGE. It was
   `minmax(0, 1fr)` against a FIXED 320px pipeline, so every pixel a wide window
   added went to the role column -- 880 of a 1400px content width, against role
   content that measures about 330. The pipeline's bar, its legend chips and its
   "N of M decided" line had 320 and wrapped. The two tracks swap roles: the
   role column is BOUNDED and the pipeline takes the slack.
   AT 1400px OF CONTENT that is role 460, pipeline 728, actions 148 -- the
   pipeline column starting at ~500, which is the redline. Both columns are
   wider than the content they carry, which is what the owner asked for; what
   shrank is dead space, not a column.
   `minmax(260px, 460px)` AND NOT `460px`: below the floor the role name must be
   able to give ground before the pipeline does, because the name is the row's
   identity and `.table-scroll` is what catches the rest. The pipeline keeps its
   own 280px floor for the same reason in the other direction -- a bar with
   legend chips under it is unreadable narrower than that, and `minmax(0, 1fr)`
   would have crushed it to nothing rather than overflow (UX-121's defect).
   THE ACTIONS TRACK GOES 136 -> 148 for the icon spacing below: 3x28 icons +
   2x4 gaps + 4 right padding = 96, plus the 26px pinned copy icon, its 4px
   margin and the 19px readiness marker = 145, rounded up the 4px scale.

   AND 148 -> 220 ON 2026-09-09 (owner: "For the pipeline column, now it takes
   too long, reduce 10% of it's column width, the additional space goes to the
   icon bar part (right of the screen)"). The pipeline is the `1fr` track, so it
   has no width of its own to cut -- it takes whatever the other two leave. THE
   ONLY WAY TO NARROW IT IS TO WIDEN A NEIGHBOUR, and the owner named which one.

   THE 10% IS TAKEN AT THIS FILE'S OWN REFERENCE WIDTH, which is the 1400px of
   content the paragraph above works in: pipeline 728 there, 10% is 72.8, and 72
   lands on the 4px scale. 148 + 72 = 220, and the pipeline becomes 656. That is
   still 2.3x its own 280px floor and comfortably above the ~500px its bar,
   legend chips and "N of M decided" line actually measure, so the cut comes out
   of slack rather than out of content -- which is what "it takes too long"
   describes.

   ROLE IS UNTOUCHED AT `minmax(260px, 460px)`, deliberately. The owner asked for
   ONE column to give and ONE to receive; taking the difference out of the role
   name as well would be a second change nobody requested, and the 2026-09-07
   ruling that widened it is four days old.

   `.posb-table`'s `min-width` TAKES THE SAME +72 in this commit, for the reason
   the two entries below spell out in both directions: a floor left behind hands
   the difference to the greedy track, and a bounded track absorbs slack in
   silence exactly as `1fr` does until it hits its own ceiling. */
:root { --posb-cols: minmax(260px, 460px) minmax(280px, 1fr) 220px; }
/* The CLOSED board is three columns from the SAME row rule: the token is
   overridden on its container and inherits down to the header and every row. A
   second row class with its own `grid-template-columns` would be exactly the
   two-declarations mistake this block exists to avoid. */
/* The CLOSED board takes the same two content tracks and keeps its narrower
   actions cell -- a closed row has one control, not three. */
.posb-table.is-closed { --posb-cols: minmax(260px, 460px) minmax(280px, 1fr) 80px; }
/* THE FLOOR THE HANDOFF THOUGHT ALREADY EXISTED. `.table-scroll` (overflow-x:
   auto) is real and shipped; the "min-width inner div" it cites is not, anywhere
   in this sheet. It was never needed, because a `<table>` has an intrinsic
   minimum of its own. A grid has none -- `minmax(0, 1fr)` will crush the role
   title to nothing rather than overflow -- so the floor is genuinely new, and it
   goes on this element so no wrapper div is invented for it. */
/* RAISED 940 -> 996 WITH THE ACTIONS TRACK, and it is the same 56px. The floor
   is what hands the scroll to `.table-scroll` instead of letting `minmax(0, 1fr)`
   crush the role title to nothing; leaving it at 940 while the actions track grew
   would have taken those 56px out of the ROLE NAME and out of nothing else,
   silently, at every viewport below the floor -- which is UX-121's defect
   (`minmax(0, 1fr)`'s zero collapsing a name under its legible width) reached
   from the other direction. Raising it costs a horizontal scrollbar 56px earlier
   and keeps the name exactly the room it had.

   996 -> 772 ON 2026-09-07, the same 224 the `Needs you` track took with it when
   the owner deleted that column. The direction is the mirror of the paragraph
   above and the argument is identical: a floor left at 996 over three tracks
   hands 224px to `minmax(0, 1fr)`, which absorbs them in silence -- the name
   would simply have grown, which sounds harmless until the next column arrives
   and the floor is 224px of slack away from meaning anything. The floor's job is
   to be the width below which the columns crush, and that width moved. */
/* 772 -> 844 (2026-09-09): the actions track took +72 when the owner moved 10%
   of the pipeline column into it, and the floor follows it in the same edit.
   Same pairing rule as the two entries above, applied in the growing direction.
   The three floors still sum under it: 260 + 280 + 220 = 760, plus the row's
   own 2x32 gaps and 2x8 padding = 840, rounded up the 4px scale. */
.posb-table { min-width: 844px; }
.posb-row { display: grid; grid-template-columns: var(--posb-cols);
  align-items: center; gap: 32px; padding: 16px 8px;
  border-bottom: 1px solid var(--line-soft); }
/* `display: grid` BEATS the user agent's `[hidden] { display: none }`, so without
   this the two client-side filters would set `hidden` and change nothing. If rows
   stop hiding, this is the line. (Same trap as `.cand-row[hidden]`; it cost that
   block a debugging session, so it is stated here rather than rediscovered.) */
.posb-row[hidden] { display: none; }
.posb-row[data-role-url] { cursor: pointer; }
.posb-row[data-role-url]:hover { background: var(--hover); }
.posb-row[data-role-url]:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.posb-head { padding: 8px; cursor: default;
  border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
  font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--faint); }
/* The pipeline cell: bar over legend, as a link to the role drawer. Two rows in
   one grid cell, so it is a column flex and not another grid -- there is nothing
   here for a second set of tracks to align to. */
/* THE PIPELINE CELL: a progress LABEL, a COLOURED STAGE BAR, and the bar's KEY --
   three lines, and each one makes exactly ONE claim.

   THE BAR IS A PROPORTIONAL STAGE BAR AGAIN (UX-59, owner 2026-08-06: *"previously
   I had submitted a design change, where the pipeline bar would have different
   color showing the progress, and it's missed, can we get that back?"*), and the
   measurement that once refused it is NOT overturned -- that refusal was a
   CONDITION, and the condition is now met.

   WHAT THE MEASUREMENT SAID, unchanged and still true OF THE FUNNEL COUNTS: they
   are CUMULATIVE, not a partition. `screened` is every candidate with a CV score
   whatever their status now, so a scored candidate who has been hired is counted
   in `screened` AND in `decided`. Measured on a role of three candidates, all
   scored and all Hired: screened=3, decided=3, the segment widths sum to 6, and
   the bar rendered a FULLY decided role as 50% decided -- Phase 17's "the bar
   contradicts its own label" on this very surface. UX-53's own words were: do not
   restore the proportional stage bar WITHOUT A DISJOINT PARTITION to draw it from.

   WHAT CHANGED IS THE DATA, NOT THE READING OF IT. `db.positions_board` now also
   buckets candidates on `status` -- a SINGLE column, so every candidate lands in
   exactly one bucket by construction rather than by argument -- and
   `app._stage_mix` turns those counts into widths summing to 100. The cumulative
   funnel is untouched and still renders in this cell's TOOLTIP, where a funnel
   readout is exactly right. Two decompositions, two questions, one screen; what
   must never come back is a WIDTH computed from the cumulative set.

   THE LABEL'S DENOMINATOR MOVED WITH IT, which is the other half of the same
   defect. It read the SCREENED count, so "3 of 14 decided" meant three of fourteen
   scored CVs while looking like three of fourteen people -- the same contradiction
   in words instead of pixels. Both now read the candidate count.

   AN <svg> WITH x= / width= ATTRIBUTES, NOT AN INLINE style=. Those on a rect are
   SVG GEOMETRY, and they are how this bar keeps `_pool_main.html` at ZERO inline
   styles. That matters because SS8.6 in tests/test_layout_brand.py is a HARD ZERO
   on this file and says outright not to raise its allowance -- stricter than
   check_tokens 8.12, which would have permitted a computed `style="flex: N"`. Two
   guards, and the stricter one wins. */
/* THE TWO-LINE RESERVATION LIVES HERE, ON THE CELL, AND NOT ON THE LEGEND
   (owner report 2026-08-07). It was `height: 34px` on `.posb-legend` -- the LAST
   child -- which reserved the row's height correctly and then paid all of the
   slack out BELOW the visible ink. MEASURED on the board rather than reasoned
   about (layout rule 6): row box 108.7px centred at 54.4, `.posb-pipe` box 76px
   centred at 54 (so `align-items: center` on `.posb-row` was never the fault),
   and inside it a one-entry legend's ink ending at 73 with 19px of empty
   reserved box under it -- visible ink spanning 16..73, centre 44.5, riding
   9.9px above the role name and the attention chip beside it.
   ONE declaration owns the reservation and the centring, so the slack is split
   ABOVE and BELOW the ink instead of hanging off the bottom: 20 lead + 8 + 6 bar
   + 8 + 34 legend = 76, which is what `min-height` states. `min-height` and not
   `height`, because a clip is not wanted here -- the legend's own box is what
   clips (below), and a lead that ever wrapped should make the row taller rather
   than lose a line. Rule 2 in practice: the layout decision is on the container
   that owns the row height, never on one item inside it. */
.posb-pipe { display: flex; flex-direction: column; gap: 8px;
  min-height: 76px; justify-content: center;
  text-decoration: none; color: inherit; }
.posb-lead { font-size: 12.5px; color: var(--muted); }
/* `border-radius` on the SVG ELEMENT, never `rx` on the rects: `preserveAspect
   Ratio: none` stretches the 100-unit viewBox across the cell, so an `rx` would
   be stretched with it into an ellipse. The element's own radius is not.
   It is also why the segments are BUTTED rather than gapped: a 1-unit gap in a
   100-unit viewBox stretches to a different number of real pixels in every column
   width, so the ramp carries the boundaries and the geometry does not try to.
   6px rather than 4px -- at 4px a six-colour ramp reads as one muddy line. */
.posb-bar { display: block; width: 100%; height: 6px;
  border-radius: 999px; overflow: hidden; }
/* THE TRACK SURVIVES THE MOVE TO A PARTITION, and it is not decoration. It is
   drawn full-width UNDER the segments so that (a) a role with no candidates shows
   an empty rail rather than nothing, and (b) a partition that ever summed short of
   its population shows the shortfall as bare track instead of silently rescaling
   to look complete. Phase 17's lesson in its current form: an honest gap beats a
   full bar. `.posb-fill` went with the single-fill bar -- the fill is now one rule
   per stage below, so there is no colour left for a class named after the concept
   of "the filled part". */
.posb-track { fill: var(--line-soft); }
/* ONE RULE PER STAGE, reading the ramp declared at the top of this sheet. The SAME
   `.s-*` classes carry the legend's swatches below, so a segment and its key entry
   cannot end up different colours: there is one declaration serving both. */
.posb-seg.s-new { fill: var(--stage-new); }
.posb-seg.s-screened { fill: var(--stage-screened); }
.posb-seg.s-interview { fill: var(--stage-interview); }
.posb-seg.s-f2f { fill: var(--stage-f2f); }
.posb-seg.s-shortlist { fill: var(--stage-shortlist); }
.posb-seg.s-decided { fill: var(--stage-decided); }
.posb-seg.s-other { fill: var(--stage-other); }
/* A TWO-LINE CEILING, AND IT IS THE CLIP -- NOT THE ROW-HEIGHT FIX. That job moved
   up to `.posb-pipe`'s `min-height`, and the reasoning for the move is there; what
   stays here is the ceiling that makes the move safe. `max-height`, not `height`:
   the two are identical for a two-line legend and differ for a one-entry one,
   which is the whole defect -- a fixed height made every short legend reserve a
   line it did not use and then sat its ink at the top of the box.

   WHY A CEILING AT ALL, unchanged and still true. MEASURED (layout rule 6): a role
   with all six buckets filled renders a 49px key and a three-bucket role renders
   17px, so the rows came out 124px and 95px -- and "every board row is the same
   height, or a cell has wrapped" is exactly what UX-53's row-height arm asserts.
   The old five-count legend had the same latent fragility and only ever passed
   because the demo seed never produced four non-zero stages; the dots and the sixth
   segment made it reachable.

   TWO LINES IS PROVABLY ENOUGH, which is what makes a ceiling honest here rather
   than a clip waiting to happen. `_pool_stage_mix` returns a CLOSED SET of at
   most six segments -- it does not fan out through `registry.collect`, unlike the
   funnel builder -- and those six measure ~484px of content against 640px of
   capacity in two lines of the 320px track that `--posb-cols` pins. So no data can
   ask for a third line, and every row is the same height BY CONSTRUCTION instead of
   by luck. Guard: the six-bucket role in tests/test_ui_alignment.py's UX-53 block,
   which exists so this claim is measured rather than asserted here -- and which now
   also measures the ink's centre line, because row-height uniformity was green
   throughout the defect above and could not have caught it.

   `align-content: flex-start` STAYS, and it is not the bug. With the reservation
   gone from this box there is no slack left for it to push the ink to the top of;
   it is what keeps a two-line legend's two lines butted rather than spread.

   `gap: 4px 8px` -- both on the 4px scale, because ratchet 8.13's off-scale counter
   has zero slack and a 10px that nobody can explain is what it is there to refuse. */
.posb-legend { display: flex; flex-wrap: wrap; gap: 4px 8px;
  max-height: 34px; overflow: hidden; align-content: flex-start;
  font-size: 11px; line-height: 15px; color: var(--faint); }
/* A ROLE WITH NO CANDIDATES PRINTS NO KEY, and the empty box must leave the flow
   rather than sit in it at 0px: `.posb-pipe`'s 8px `gap` still applies BETWEEN the
   bar and a zero-height last child, so those 8px hung below the ink and pushed it
   4.5px above the row's centre -- the same defect as the reservation above, one
   eighth the size and on one row. MEASURED by UX-53's ink arm, which failed on
   exactly that row after the reservation moved and passed on every other.
   `:has()` and NOT `:empty`: the span always holds Jinja's own newline, so
   `:empty` never matches it. The box is nevertheless 0px tall, because flex layout
   discards a whitespace-only anonymous item -- which is why the row height was
   right and only the ink was wrong. 17 other rules in this sheet already use
   `:has()`. */
.posb-legend:not(:has(.posb-key)) { display: none; }
.posb-legend .tnum { font-variant-numeric: tabular-nums; }
.posb-pipe:hover .posb-legend { color: var(--muted); }
/* The legend is the bar's KEY: swatch, count, label. `align-items: center` on the
   ENTRY rather than a nudge on the dot -- the dot is 7px and the text 11px, and
   centring the row is what keeps them on one optical line at any font size. Layout
   rule 2 in miniature: the alignment lives on the container, not per item. */
.posb-key { display: inline-flex; align-items: center; gap: 4px; }
.posb-dot { flex: none; width: 7px; height: 7px; border-radius: 50%;
  background: var(--line-soft); }
.posb-dot.s-new { background: var(--stage-new); }
.posb-dot.s-screened { background: var(--stage-screened); }
.posb-dot.s-interview { background: var(--stage-interview); }
.posb-dot.s-f2f { background: var(--stage-f2f); }
.posb-dot.s-shortlist { background: var(--stage-shortlist); }
.posb-dot.s-decided { background: var(--stage-decided); }
.posb-dot.s-other { background: var(--stage-other); }
/* The chip is the only coloured thing on the page, and it is a LINK. `.chip` is
   the shared geometry and `.chip-warn` the shared tint; the only thing this adds
   is that an anchor does not underline itself inside one. */
/* `.posb-needs` IS DELETED with the column (owner, 2026-09-07). Its one rule
   undid the `text-decoration` an `<a class="chip">` inherits -- the two chips it
   styled were links into `?stage=awaiting_decision` / `?stage=awaiting_invite`.
   Nothing else on this board renders a chip as a link. */
/* Right-aligned by the GRID's own actions track, not by `margin-left: auto` or
   `space-between` -- those align a row to its own content width and are two of the
   three spellings the 8.13 drift ratchet counts down. UX-59 gave this cell a
   SECOND child (the pinned copy icon, plus its readiness marker when the public
   link is unset); the flex end-alignment already handled that, which is why the
   change was one number on the track above and nothing here. */
/* SPACING ON THE ICONS (owner, 2026-09-07: "Give some spacing to the icons on
   the far right"). Three 28px `.icon-btn` glyphs sat edge to edge and flush
   against the row's own 8px padding, so Delete read as part of Close. A `gap`
   between them and a little padding after the last one, both on the 4px scale;
   the actions TRACK above grew by the same 12px, because taking the room out of
   a fixed track would push the glyphs back under the row's edge. */
.posb-acts { display: flex; align-items: center; justify-content: flex-end;
  gap: 4px; padding-right: 4px; }
/* `.posb-menu { flex: none }` IS DELETED WITH THE `...` MENU IT SIZED (owner,
   2026-09-06). It had one call site and stopped that menu stretching in this flex
   cell; the three controls that replaced it are `.icon-btn`, which is a fixed
   28x28 box and cannot stretch. `.pcard-menu` / `.pcard-menu-pop` are NOT
   deleted -- the role drawer's own header menu (`.cand-rolemenu`) is still a
   caller, and it is now the last one.

   THE FORMS INSIDE THIS CELL NEED NO RULE OF THEIR OWN. Close/Restore and Delete
   are `<form class="m-0">` wrappers, and a form is a flex item whose only box is
   the 28px button inside it -- so the cell measures the same whether a control is
   wrapped or not. `.m-0` is what stops the user agent's own form margin
   reappearing between them. */
.icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px;
  border: 0; background: none; color: var(--faint); border-radius: 6px; cursor: pointer; }
.icon-btn:hover { background: var(--neutral-tint); color: var(--ink); }
/* THE DESTRUCTIVE TONE, RE-EXPRESSED FOR THIS CONTAINER (2026-09-06). Inside the
   retired popover it was `.pcard-menu-pop .danger`; on a row it is this. Same two
   tokens, same meaning -- `Delete role` is the only non-neutral control in the
   actions cell -- and it is a re-expression rather than a second definition for
   the reason UX-53 gave when the button moved the other way: `.btn.danger.sm`
   paints its own padding and border and would make the delete control a
   different size and shape from the two beside it. */
.icon-btn.danger { color: var(--bad-ink); }
.icon-btn.danger:hover { background: var(--bad-tint); color: var(--bad-ink); }
/* Grouped ⋯ menu: dividers only, danger item tinted on hover. */
/* Archived library questions render dimmed but fully readable. */
.row-muted td { opacity: .55; }

/* Voice interview: in-page left sub-nav (Interview questions / Templates /
   Scoring configuration / Settings / Default questions). */
.iv-layout { display: flex; gap: 28px; align-items: flex-start; }
.iv-subnav { flex: 0 0 200px; position: sticky; top: 16px; }
.iv-subnav a { display: block; padding: 7px 10px; border-radius: 8px; font-size: 13.5px;
  color: var(--muted); text-decoration: none; }
.iv-subnav a:hover { background: var(--hover); color: var(--ink); }
.iv-subnav a.active { background: var(--neutral-tint); color: var(--ink); font-weight: 600; }
.iv-subnav a.sub { margin-left: 14px; font-size: 13px; }
.iv-content { flex: 1 1 auto; min-width: 0; }

/* Module home (dashboard pattern): centered one-line subtitle under the mark. */
.home-modsub { text-align: center; margin-top: 6px; }
@media (max-width: 900px) {
  .iv-layout { display: block; }
  .iv-subnav { position: static; display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 14px; }
  .iv-subnav a.sub { margin-left: 0; }
}
/* G8: .pos-detail is deleted with the hidden mini-funnel row it styled. */
/* Inline status/team controls in the positions table */
.status-pill { font-weight: 600; }
.status-pill.ok  { background-color: var(--ok-tint);  color: var(--ok-ink);  border-color: transparent; }
.status-pill.bad { background-color: var(--bad-tint); color: var(--bad-ink); border-color: transparent; }
.team-form { display: inline-flex; align-items: center; gap: 6px; }
.team-select { max-width: 150px; }
/* ---- UX-01: a field PAIR, not a stack -----------------------------------
   A control and the conditional input that its own choice reveals belong on
   ONE row, side by side, with deliberate spacing -- not stacked with nothing
   tying them together. `align-items: flex-end` so the two share a baseline
   whatever their heights; `flex-wrap` so a narrow viewport wraps rather than
   overflowing the dialog (Phase 14). The pair is generic on purpose: it is the
   construction, and the wizard's project field is its first use. */
.field-pair { display: flex; align-items: flex-end; gap: var(--s4); flex-wrap: wrap; }
.field-pair > * { flex: 1 1 200px; min-width: 0; margin-top: 0; }
/* New-role wizard: widen the Project select so long project names fit, capped
   so it doesn't sprawl across the modal. Overrides the narrow `.team-select`. */
.field-pair .nr-team-select { flex: 0 1 320px; min-width: 200px; }
.field-pair input.team-new-input { flex: 1 1 200px; }
.nr-team-select { width: 30%; min-width: 200px; max-width: 35%; }
/* New-role wizard: the Job role box is HALF the row (owner, 2026-09-09: "Job
   role input textbox row width cut to 50% of that instead of full row"). A job
   title is a few words, and a full-width box invited the long descriptive
   titles this field is not for.

   A CLASS AND NOT AN INLINE STYLE, because `check_tokens.py` 8.12 is a hard
   zero on inline layout and a width typed at the call site is the drift the
   layout contract exists to prevent. It sits beside `.nr-team-select` above,
   which is the same idea applied to the Project select on the very next line
   of the same step -- and it takes that rule's `min-width` for the same reason:
   below it the placeholder truncates, and a title box narrower than its own
   placeholder reads as broken rather than compact.

   `input.nr-half` AND NOT `.nr-half`, AND THE FIRST DRAFT SHIPPED THE BARE
   CLASS AND DID NOTHING. The base control rule is
   `input[type="text"], … { width: 100% }` -- one type selector plus one
   attribute selector, specificity (0,1,1) -- and a bare class is (0,1,0), so it
   LOSES. The rule looked applied in the stylesheet and the element carried the
   class; the box measured 1039px of a 1039px parent.

   THIS IS THE SECOND TIME THIS EXACT CASCADE HAS BEEN PAID FOR, and the first
   one's note was deleted from this file EARLIER THE SAME DAY with the menu it
   described: `.pcard-menu-pop.cand-rolemenu-pop` was a double class for the
   same reason, against `.pcard-menu-pop { min-width: 200px }`, and its comment
   ended "Caught by measuring the popover, not by reading the cascade." Caught
   the same way again -- in a browser, on the deployed page. ANY RULE HERE THAT
   NARROWS A SHARED CONTROL PRIMITIVE NEEDS TO OUT-SPECIFY IT. Guard:
   `NR TITLE HALF` in tests/test_ui_alignment.py, which measures the box rather
   than reading the sheet. */
input.nr-half { width: 50%; min-width: 200px; }
.team-form input.team-new-input {
  width: 150px; max-width: 150px; box-sizing: border-box;
  padding: 6px 12px; border-radius: 999px; border: 1px solid var(--line);
  font-size: 12.5px; line-height: 1.4; background: var(--field); color: var(--ink);
}
.team-form input.team-new-input:focus { outline: none; border-color: var(--ring); box-shadow: 0 0 0 3px var(--accent-tint); }

/* THE TALENT-REVIEW RULES ARE DELETED (UX-105, owner 2026-08-10). `.review-toggle`,
   `.review-item` and `.review-text` styled the record card's review thread, which
   went with the construct. `.review-cell` went in the same edit: it had had no
   markup since the reserve tab was retired and was already dead, and leaving one
   orphan rule under a heading naming a deleted feature is the inert remainder this
   retirement exists to avoid. */

/* ---- Copy-to-clipboard control (copy_btn / copy_link macros) ------------- */
.copy-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; padding: 0; margin-left: 4px; vertical-align: middle;
  background: transparent; border: 1px solid var(--line); border-radius: 8px;
  color: var(--muted); cursor: pointer; transition: background .12s, color .12s, border-color .12s;
}
.copy-btn:hover { background: var(--bg); color: var(--ink); border-color: var(--muted); }
.copy-btn .copy-ok { display: none; }
.copy-btn.copied { color: var(--ok-ink); border-color: #b9e6cb; background: var(--ok-tint); }
.copy-btn.copied .copy-ico { display: none; }
.copy-btn.copied .copy-ok { display: inline; }
/* P0-3: any OTHER copy control — the Pipeline card's icon button, its menu
   entry, the Settings menu entry — had no .copied rule at all, so "Copy
   application link" confirmed nothing. Same success tint as .copy-btn; text
   buttons also get their label swapped by the handler in base.html. */
[data-copy].copied { color: var(--ok-ink); background: var(--ok-tint); }
.copy-ok-txt { color: var(--ok-ink); font-weight: 600; }

/* P0-3 readiness: the inline consequence block, and the marker that ties a
   control to it where there is no room for the block. */
.readiness-note { display: block; margin-top: 6px; }
.readiness-flag {
  display: inline-flex; align-items: center; justify-content: center;
  width: 14px; height: 14px; margin-left: 5px; border-radius: 50%;
  background: var(--bad-tint); color: var(--bad-ink);
  font-size: 10px; font-weight: 700; line-height: 1; vertical-align: middle;
}
.copy-link {
  display: inline-flex; align-items: center; gap: 2px; max-width: 100%;
  font-size: var(--text-sm); color: var(--muted);
}
.copy-link .link-ico { flex: none; opacity: .7; }
.copy-link-a {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  max-width: 30ch; color: var(--accent); text-decoration: none;
}
.copy-link-a:hover { text-decoration: underline; }

/* ---- Flash dismiss + auto-fade (E16) ------------------------------------- */
.flash { position: relative; padding-right: 38px; transition: opacity .5s ease; }
.flash.fade { opacity: 0; }
.flash-x {
  position: absolute; top: 50%; right: 10px; transform: translateY(-50%);
  width: 22px; height: 22px; line-height: 1; padding: 0;
  background: transparent; border: 0; border-radius: 6px;
  font-size: 18px; color: inherit; opacity: .55; cursor: pointer;
}
.flash-x:hover { opacity: 1; }
/* A PAGE-LEVEL CONFIRMATION MUST SURVIVE THE DRAWER OPENING OVER IT (owner
   ruling 2026-08-06). `/screening/<id>` now lands on the record's home WITH the
   drawer open, and the host page is what renders flashes — so "Saved." arrives
   on a page a 65vw panel is about to cover. MEASURED, not assumed: at 1440 the
   flash starts at x=350 (the sidebar's right edge) and `.drawer` starts at 504,
   leaving a 154px strip — narrower than the sentence.

   LIFTED ABOVE `.drawer-wrap` (z-index 60) RATHER THAN NARROWED. Constraining
   the flash to the visible column would wrap one sentence into a ~150px gutter,
   which is a worse read than being clipped. It is scoped to `body.drawer-open`
   so nothing changes on a page with no drawer, and it is on the SHARED `.flash`
   rule rather than on one surface's copy of it — the drawer is opened from
   twelve templates and a per-page fix would be the call-site mistake CLAUDE.md's
   layout contract is about. Guard: `deep-link drawer` in
   tests/test_ui_alignment.py, which measures OCCLUSION with elementFromPoint
   rather than trusting the stacking order. */
body.drawer-open .flash { z-index: 61; }

/* ---- Inline application-link row (positions) ----------------------------- */
.apply-link-row {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line-soft);
}
.apply-link-lbl { font-size: var(--text-xs); font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: .02em; }

/* ---- Screen-CV modal: locked-position note ------------------------------ */
.cv-locked-note {
  padding: 8px 12px; margin-bottom: 8px;
  background: var(--accent-tint); color: var(--accent);
  border-radius: 8px; font-size: var(--text-sm);
}

/* ---- Password field with show/hide toggle (settings) --------------------- */
.pw-field { display: inline-flex; align-items: stretch; gap: 6px; width: 100%; }
.pw-field input { flex: 1; }
.pw-toggle {
  flex: none; padding: 0 12px; font-size: var(--text-sm);
  background: var(--bg); color: var(--muted);
  border: 1px solid var(--line); border-radius: 8px; cursor: pointer;
}
.pw-toggle:hover { color: var(--ink); border-color: var(--muted); }

/* ---- Candidate quick-view drawer (B7) ----------------------------------- */
.drawer-wrap { position: fixed; inset: 0; z-index: 60; }
.drawer-wrap[hidden] { display: none; }
.drawer-backdrop { position: absolute; inset: 0; background: rgba(10,13,20,.35); }
.drawer {
  /* G7 (owner, 2026-07-31): 65% of the viewport, down from 80%. The floor below
     is what keeps this usable on a narrow window, and it matters MORE at 65vw
     than it did at 80vw -- on a 900px window 65vw is 585px, which is under the
     620px floor, so the floor is what actually applies there. Do not drop it
     while "simplifying" this rule. */
  position: absolute; top: 0; right: 0; height: 100%; width: 65vw;
  min-width: min(620px, 96vw); max-width: 96vw;
  background: var(--panel); box-shadow: var(--shadow-2);
  overflow-y: auto; padding: 24px 28px; animation: drawer-in .18s ease;
}
/* TWO CONTAINERS WERE EACH PAYING THE SAME TOP PADDING (UX-27, owner
   2026-08-03: "too much spacing on top"). MEASURED on the deployed build: the
   drawer's own `padding: 24px 28px` puts `.drawer-body` at y=24, and the
   `.panel.role-panel` inside it adds its own `padding: var(--s5) 0` -- so the
   role title started 48px down with an empty band above it.
   `.panel`'s vertical padding is the de-boxed page rhythm: on a PAGE it is the
   space between hairline-separated sections, and the first one needs it because
   nothing above supplies it. Inside the drawer something does. Only the FIRST
   panel is zeroed, so the rhythm BETWEEN stacked panels is untouched -- and only
   the top, so the panel keeps its bottom separation from what follows.
   Same shape as UX-26, UX-38 and UX-40: a rule correct in one container
   surviving into another. */
.drawer .drawer-body > .panel:first-child { padding-top: 0; }
@keyframes drawer-in { from { transform: translateX(24px); opacity: .5; } to { transform: none; opacity: 1; } }
/* B-5 (historical) + G15 (2026-07-31). This block used to describe the peek
   drawer's sticky close button: it had been absolutely positioned, scrolled away
   with the content, and was made sticky so one gesture down a long drawer could
   not leave the reader with no visible way out.
   That button is now DELETED from the peek drawer (base.html) -- Escape closes
   it from anywhere and the backdrop closes it by pointer, so the affordance was
   30px of header band on every open for a third route out. The `.drawer-close`
   rule below STAYS because assessment_kind.html's interview-preview modal still
   renders one. */
.drawer-close {
  position: sticky; top: 0; z-index: 2;
  display: flex; align-items: center; justify-content: center;
  margin-left: auto; width: 30px; height: 30px;
  background: var(--panel); border: 0; border-radius: 8px;
  font-size: 18px; color: var(--muted); cursor: pointer;
}
.drawer-close:hover { background: var(--bg); color: var(--ink); }
.drawer-loading { color: var(--muted); text-align: center; padding: 48px 12px; }
body.drawer-open { overflow: hidden; }

/* peek fragment */
.peek-back { margin-bottom: 12px; }
/* G15: the 28px right gutters both rows carried were reserved to clear the
   drawer's close button back when it was absolutely positioned. The button is
   gone, so there is nothing left to clear. */
.peek-crumbs { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
.peek-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; }
.peek-head h2 { font-size: var(--text-lg); }
.peek-status { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 16px 0; }
.peek-status form { display: flex; align-items: center; gap: 6px; margin: 0; }
.peek-status select { padding: 5px 8px; }
/* Vanta sectioned-checklist: each section is a stacked bordered card. */
.peek-sec { margin: 12px 0; padding: 14px 16px; border: 1px solid var(--line); border-radius: 10px; }
.peek-sec > .eyebrow:first-child { margin-bottom: 8px; }
.peek-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; margin: 10px 0 0; }
.peek-cols .eyebrow { margin-bottom: 6px; }
.peek-cols ul.clean { margin: 0; padding-left: 18px; }
.peek-cols ul.clean li { margin-bottom: 4px; font-size: var(--text-sm); }
.peek-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--line-soft); }
.peek-actions .btn:last-child { margin-left: auto; }
@media (max-width: 520px) { .peek-cols { grid-template-columns: 1fr; } }

/* Bulleted consent points (H30) — readable structure inside .consent-card. */
.consent-points { margin: 8px 0 12px; padding-left: 20px; list-style: disc; }
.consent-points li { margin-bottom: 6px; font-size: var(--text-sm); line-height: 1.5; }

/* ---- Styled confirmation dialog (E17) ----------------------------------- */
.confirm-dialog {
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 22px 24px; max-width: 440px; width: calc(100vw - 40px);
  box-shadow: var(--shadow-2); color: var(--ink); background: var(--panel);
}
.confirm-dialog::backdrop { background: rgba(10,13,20,.35); }
.confirm-msg { margin: 0 0 20px; font-size: var(--text-md); line-height: 1.5; white-space: pre-line; }
.confirm-actions { display: flex; justify-content: flex-end; gap: 10px; }
/* R-C typed confirmation. Shown only when the caller supplied
   [data-confirm-type]; the Confirm button stays disabled until the field
   matches exactly. */
.confirm-type { display: block; margin: 0 0 20px; }
.confirm-type-prompt { display: block; font-size: var(--text-sm); color: var(--muted); margin-bottom: 6px; }
.confirm-type-input { width: 100%; }
.confirm-actions .btn[disabled] { opacity: .45; cursor: not-allowed; }

/* ---- "Decide next step" evidence-in-view dialog (workshop R2) ------------ */
.decide-head { display: flex; flex-direction: column; gap: 2px; margin-bottom: 14px; }
.decide-rec { border: 1px solid var(--line); border-radius: var(--r-md); padding: 12px 14px; background: var(--bg); }
.decide-chips { flex-wrap: wrap; }
.decide-actions { flex-wrap: wrap; align-items: center; margin-top: 18px; }

/* ---- AI-generate microcopy (workshop R7, shared ai_note macro) ----------- */
.ai-note { display: block; margin-top: 6px; }
.ai-note-off { color: var(--faint); font-style: italic; }

/* ---- Library "start from a saved template" nudge (R5) -------------------- */
.lib-start {
  border: 1px solid var(--line); border-left: 3px solid var(--accent);
  border-radius: var(--r-md); padding: 10px 14px; margin: 0 0 14px;
  background: var(--accent-tint);
}
.lib-start strong { display: block; margin-bottom: 2px; }

/* The per-user customization-depth disclosure (`.depth-disclose`,
   `.depth-summary`, `.depth-summary-t`) was DELETED here on 2026-08-04. Its only
   markup lived in `templates/positions_new.html`, which UX-51 deleted, so the
   rules had zero consumers -- `check_tokens` catches a class with no rule and
   cannot see a rule with no class. The owner accepted the stepped wizard as
   superseding the behaviour ("Decision C / R6", a workshop label with no record
   in this repo): the 4-step flow IS the progressive disclosure now. Do not
   restore these without markup to hang them on. */

/* ---- List pagination (F20b) --------------------------------------------- */
.pager { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 14px; flex-wrap: wrap; }
.pager-btns { display: flex; align-items: center; gap: 10px; }
/* NUMBERED PAGE LINKS (owner, 2026-08-21). `.btn ghost sm` carries the geometry,
   so these two rules are only what a NUMBER needs that a word does not:
   `tabular-nums` so the boxes do not twitch between 1 and 8, and `min-width` so a
   one-digit page is the same size as a two-digit one -- otherwise the row of pages
   visibly reflows as you walk it, which is the same class of defect as a cell
   resizing per row. No padding override: the shared button owns that.

   THE CURRENT PAGE IS A `<span>`, NOT A LINK -- a link to where you already are is
   a control that does nothing. It borrows the same classes so the row stays even,
   and `aria-current="page"` is what actually names it for assistive tech; the
   visual weight below is the sighted half of that one fact. */
.pager-n { min-width: 32px; justify-content: center;
  font-variant-numeric: tabular-nums; }
.pager-n.is-current { font-weight: 600; color: var(--ink);
  border-color: var(--line); background: var(--hover); cursor: default; }
.pager-gap { padding: 0 4px; }
/* The direct-jump form, shown only when the numbered window cannot span the page
   count. `.input-narrow` is the shared 110px control width (layout rule 3 -- the
   width is a property of the class, not a number picked here), and it is paired
   with nothing because this input is not in a `.set-row`: `gap: 8px` on the flex
   row is what spaces it, so the child pushes nothing itself. The number spinner
   is left alone deliberately -- it is a real affordance on a page number, unlike
   the year inputs check_tokens 8.14 suppresses it for. */
.pager-jump { display: flex; align-items: center; gap: 8px; }
.pager-jump input[type=number] { width: 72px; text-align: center;
  font-variant-numeric: tabular-nums; }

/* ---- Mobile: off-canvas sidebar + scrollable tables (I/#31) -------------- */
/* -- #61: the 861-920px band was HALF converted, and 860 was the wrong number.
   MEASURED across the band on /library:

     clientWidth   sidebar        content box   .panel overflow-x   page scrolls X
       860         off-canvas          860          auto                 false
       861         DOCKED, 310px       551          visible              false
       880         docked,  310px      570          visible              false
       900         docked,  310px      590          visible              false

   At 861 the sidebar re-docks and takes 310px, while the panel clipping that
   lets a wide table scroll inside its own pane disengages at the same instant
   -- so content is squeezed to 551-590px with nothing absorbing the overflow.
   `<table class="alt">` measured a right edge of 985 in a 900px viewport.

   The cause is two numbers that were meant to be one: this block said 860, the
   `.content`/`.panel` tightening above says 920. Raising this to 920 makes them
   the same breakpoint, which is the fix the measurement points at -- the
   sidebar now undocks BEFORE the content can no longer afford it, rather than
   after.

   TWO CLAIMS ABOUT THIS BAND ARE WRONG AND ARE NOT THE REASON FOR THE CHANGE.
   Do not restore either.
     1. The original review said the page is pushed into horizontal scroll. It
        is not: documentElement.scrollWidth == clientWidth (900) at every width
        in the band.
     2. A previous re-measurement escalated that to "unreachable". Also wrong,
        and it is the one point the two passes contradict each other on. At 900
        the table's own pane still scrolls -- .table-scroll clientWidth 514,
        scrollWidth 637, overflow-x: auto -- and the count of genuinely clipped
        and unreachable elements at 861, 880 and 900 is ZERO. The band is
        CRAMPED, not broken. Severity: cosmetic-plus, not functional loss.
   (For completeness, because a stale count travels: this sheet has 12 distinct
   width breakpoints, not the 23 the review listed.)

   Guard: tests/test_layout_brand.py, marker `G27:`. */
.nav-toggle { display: none; }
.nav-backdrop { display: none; }
@media (max-width: 920px) {
  .nav-toggle {
    display: inline-flex; align-items: center; justify-content: center;
    position: fixed; top: 12px; left: 12px; z-index: 58;
    width: 40px; height: 40px; border-radius: 10px; font-size: 18px;
    background: var(--panel); border: 1px solid var(--line); color: var(--ink);
    box-shadow: var(--shadow-1); cursor: pointer;
  }
  .sidebar {
    position: fixed; left: 0; top: 0; z-index: 59;
    transform: translateX(-100%); transition: transform .2s ease;
    box-shadow: var(--shadow-2);
  }
  body.nav-open .sidebar { transform: none; }
  body.nav-open .nav-backdrop { display: block; position: fixed; inset: 0; z-index: 57; background: rgba(10,13,20,.35); }
  .content { padding: 64px 16px 24px; }
  /* Wide tables scroll horizontally within their panel instead of overflowing. */
  .panel { overflow-x: auto; }
  .panel table { min-width: 480px; }

  /* -- #26: the collapsed rail must NOT follow the recruiter onto a phone ---
     MEASURED at documentElement.clientWidth 365 with data-rail="collapsed":

       .sidebar width                        56px      (icon-only strip)
       .nav-sect.is-open > .nav-kids         display: none, rect 0 x 0
       the only route to those children      :root[data-rail=collapsed]
                                             .nav-sect:hover > .nav-kids

     `:hover` is the ONLY selector that reveals an explicitly-opened nav
     group's children on the collapsed rail. On a touch device there is no
     gesture that produces it, so those destinations are unreachable -- not
     cramped, not ugly, UNREACHABLE. That is functional loss.

     templates/base.html:9 restores data-rail from localStorage on every page
     at any width, with no width guard, so a recruiter who collapsed the rail
     on their desktop arrives on their phone with a 56px strip and a nav they
     cannot open. The collapse is a DESKTOP affordance: below this breakpoint
     the sidebar is already an off-canvas 310px drawer, where an icon-only
     strip buys nothing and costs the sub-navigation.

     Written as an append inside the existing mobile block rather than by
     wrapping the collapsed-rail block (style.css:684-720) in a min-width
     query: wrapping would re-indent forty lines and turn a clean merge into a
     conflict, which this repo has already paid for once. The declarations
     below restore the EXPANDED values from those same forty lines; they win on
     source order at equal specificity, and outside this media query the
     collapsed rail is untouched.

     Not undone here: the localStorage write itself, and the collapse toggle
     button. Both are correct -- the preference is real and belongs to the
     desktop it was set on. What changes is only whether it RENDERS at a width
     where it makes the product unusable.

     Guard: tests/test_layout_brand.py, marker `G27:`. */
  :root[data-rail="collapsed"] .sidebar { width: 310px; padding: 14px 10px; }
  :root[data-rail="collapsed"] .wordmark .wm-ver,
  :root[data-rail="collapsed"] .wordmark .wm-end { display: inline; }
  :root[data-rail="collapsed"] .wordmark { font-size: inherit; }
  :root[data-rail="collapsed"] .side-brand {
    justify-content: space-between; padding: 2px 10px 6px; flex-direction: row; gap: 8px; }
  :root[data-rail="collapsed"] .sidebar-nav { padding-top: 4px; overflow-y: auto; }
  :root[data-rail="collapsed"] .nav-label { display: block; }
  :root[data-rail="collapsed"] .sidebar .nav-txt { display: block; }
  :root[data-rail="collapsed"] .sidebar .nav-lock { display: inline-flex; }
  :root[data-rail="collapsed"] .sidebar .nav-item { justify-content: flex-start; padding: 7px 10px; }
  :root[data-rail="collapsed"] .nav-item.nav-sub2,
  :root[data-rail="collapsed"] .nav-item.nav-sub3 { display: flex; }
  :root[data-rail="collapsed"] .side-user-text { display: block; }
  :root[data-rail="collapsed"] .side-chev { display: inline-flex; }
  :root[data-rail="collapsed"] .side-user { justify-content: flex-start; }
  :root[data-rail="collapsed"] .nav-group-h { justify-content: flex-start; padding: 7px 10px; }
  :root[data-rail="collapsed"] .nav-group-h .nav-txt { display: block; }
  :root[data-rail="collapsed"] .nav-group-h .nav-chev { display: inline-flex; }
  /* The whole point: an open group's children are visible IN FLOW, and nothing
     about reaching them depends on a pointer that a touch device has not got. */
  :root[data-rail="collapsed"] .nav-sect.is-open > .nav-kids { display: flex; }
  :root[data-rail="collapsed"] .nav-sect:hover > .nav-kids {
    position: static; min-width: 0; padding: 0; background: none;
    border: 0; border-radius: 0; box-shadow: none; z-index: auto; }
  :root[data-rail="collapsed"] .nav-sect:not(.is-open):hover > .nav-kids { display: none; }
}

/* ---- P0-8: persistent settings save state -------------------------------- */
/* The autosave toast is transient by design; these two are the RECORD. Sticky so
   it reads anywhere in a 2,552px form, and a per-field marker so the evidence
   sits next to the thing that changed. */
.save-state {
  position: sticky; top: 0; z-index: 20;
  display: flex; align-items: center; gap: 8px;
  margin: 0 0 12px; padding: 7px 12px; border-radius: 8px;
  background: var(--panel); border: 1px solid var(--line);
  font-size: var(--text-sm); color: var(--muted);
}
.save-state-dot {
  width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto;
  background: var(--muted);
}
.save-state-txt { min-width: 0; }
/* `.save-state-inline` -- the strip ON THE TITLE ROW (owner ruling 2026-08-08).
   The sticky, full-width strip below the `h1` was written for a 2,552px form
   whose only evidence of saving was a 3.2-second toast; the redesign puts it
   beside the page title instead, where it is read in the same glance as the
   name of the thing being saved.

   STICKY IS DROPPED HERE AND KEPT ON THE BASE CLASS. The reason sticky existed
   -- "readable anywhere in a very long form" -- is what the redesign removes by
   making the sections short and tabbed; but the base class still has callers
   outside a title row, so this is a modifier rather than an edit to the rule.
   The MECHANISM is untouched: base.html still resolves `[data-save-state]` and
   still writes the same four states into it, so a 422 refusal still reads
   "Not saved" here exactly as it did below the heading. */
.save-state-inline { position: static; margin: 0; }
.save-state[data-state="saved"] { color: var(--ok-ink); border-color: #b9e6cb; background: var(--ok-tint); }
.save-state[data-state="saved"] .save-state-dot { background: var(--ok-ink); }
.save-state[data-state="dirty"], .save-state[data-state="saving"] { color: var(--ink); }
.save-state[data-state="dirty"] .save-state-dot,
.save-state[data-state="saving"] .save-state-dot { background: var(--warn); }
.save-state[data-state="error"] { color: var(--bad-ink); border-color: var(--bad); background: var(--bad-tint); }
.save-state[data-state="error"] .save-state-dot { background: var(--bad-ink); }

.field-save {
  display: inline-block; margin-left: 8px;
  font-size: 11.5px; font-weight: 500; color: var(--muted); white-space: nowrap;
}
.field-save[data-state="error"] { color: var(--bad-ink); }

/* ---- Settings autosave toasts (bottom-center of the viewport) ------------ */
.settings-toasts {
  position: fixed; left: 50%; transform: translateX(-50%); bottom: 16px; z-index: 80;
  display: flex; flex-direction: column; gap: 8px; align-items: center;
  pointer-events: none;
}
/* small dark pill toasts */
.settings-toast {
  padding: 7px 16px; border-radius: 999px; font-size: 12.5px; font-weight: 500;
  background: #17181c; color: #ffffff; border: 0; box-shadow: var(--shadow-2);
  transition: opacity .4s ease;
}
:root[data-theme="dark"] .settings-toast { background: #e7e8ea; color: #17181c; }
.settings-toast.error { background: var(--bad); color: #fff; }
.settings-toast.fade { opacity: 0; }

/* ---- Dashboard first-run get-started checklist -------------------------- */
/* `.getstarted` DELETED (offscale-headroom, 2026-08-09): the first-run checklist
   CONTAINER is unreferenced. Its `.gs-*` children are NOT deleted and must not
   be: `.gs-step` is live in templates/settings/_general.html and
   templates/_interview_settings.html, which is the same class name serving a
   different component. Deleting the family would have restyled Settings. */
.gs-head { margin-bottom: 16px; }
.gs-steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.gs-step { display: flex; align-items: flex-start; gap: 14px; padding: 14px; border: 1px solid var(--line-soft); border-radius: 12px; }
.gs-step .gs-mark { flex: none; width: 26px; height: 26px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center;
  background: var(--accent-tint); color: var(--accent); font-weight: 700; font-size: 13px; }
.gs-step.gs-done { border-color: #b9e6cb; background: var(--ok-tint); }
.gs-step.gs-done .gs-mark { background: var(--ok); color: #fff; }
.gs-body { flex: 1; min-width: 0; }
.gs-label { font-weight: 600; color: var(--ink); }
.gs-actions { display: flex; flex-wrap: wrap; gap: 8px; }

/* ---- Pool bulk triage (B3) + wide-table scroll (B2) --------------------- */
.table-scroll { overflow-x: auto; }
.col-check { width: 34px; }
.rowcheck, #poolAll { width: auto; cursor: pointer; }
/* CR-5 (2026-08-01): the pipeline role view's bulk-select controls. The same
   affordance as .rowcheck / #poolAll on the Talent bank, under their OWN class
   names on purpose: each surface's bulk script binds by class, and a shared name
   would let one page's script reach the other page's selection the day the two
   ever render together. */
.poolcheck, .poolselectall { width: auto; cursor: pointer; }

/* "How this is scored" disclosure on reports (B1) — formula tucked away, verdict stays visible. */
.score-help > summary { cursor: pointer; color: var(--muted); font-size: var(--text-sm); width: fit-content; }
.score-help > summary:hover { color: var(--ink); }
.score-help[open] > summary { margin-bottom: 6px; }

/* Pinned "Save Settings" bar — the <noscript> fallback on the settings page,
   kept in view at the bottom so no-JS users never have to scroll to save. */
.settings-save-bar {
  position: sticky; bottom: 0; z-index: 2;
  margin: 18px 0 0; padding: 14px 0;
  background: var(--panel); border-top: 1px solid var(--line);
  box-shadow: 0 -6px 16px rgba(24,39,75,.06);
}

/* ---- Assessment Library (reusable templates) ---------------------------- */
.chip.k-written { background: var(--accent-tint); color: var(--accent-d); }
.chip.k-interview { background: var(--warn-tint); color: var(--warn); }
th.ta-r, td.ta-r { text-align: right; }

/* ---- Hero metric panels + monitors -------------------------------------- */
.hero-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.hero-head .label { color: var(--faint); font-size: 13px; font-weight: 600; letter-spacing: .01em; }
.hero-value { font-size: 40px; font-weight: 650; letter-spacing: -.02em; margin-top: 6px; }
.hero-unit { font-size: 18px; font-weight: 500; color: var(--muted); }
.hero-inputs { display: flex; flex-wrap: wrap; gap: 14px; align-items: end; border-top: 1px solid var(--line-soft); padding-top: 12px; }
.hero-inputs label { display: flex; flex-direction: column; gap: 3px; }
.seg-toggle { display: inline-flex; border: 1px solid var(--line); border-radius: 999px; overflow: hidden; }
.seg-btn { border: 0; background: var(--panel); color: var(--muted); padding: 4px 12px; font-size: 12.5px; cursor: pointer; }
.seg-btn + .seg-btn { border-left: 1px solid var(--line); }
.seg-btn.active { background: var(--accent-tint); color: var(--accent-d); font-weight: 600; }
.attn-list { list-style: none; margin: 6px 0 0; padding: 0; }
.attn-list li { padding: 0; border-bottom: 1px solid var(--line-soft); font-size: 13.5px; }
.attn-list li:last-child { border-bottom: 0; }
/* Whole-row clickable attention item: bold role name + muted status, no
   link-blue, subtle hover — a ~44px target that keeps the :focus-visible ring. */
.attn-row { display: block; padding: 12px 10px; border-radius: var(--r-md);
  color: var(--ink); text-decoration: none; }
.attn-row:hover { background: var(--hover); }
.attn-name { font-weight: 600; }
.attn-meta { color: var(--muted); }
/* Completion rate vs target: filled bar + a target marker line (not colour-only:
   the numeric rate and target chip carry the signal too). */
.rate-bar-svg { display: block; width: 100%; max-width: 420px; height: 12px; }
.rb-track { fill: var(--line-soft); }
.rb-fill { fill: var(--accent); }
.rb-target { fill: var(--ink); }

/* ---- Floating "Ask AI" assistant (base.html #askAi) --------------------- */
/* Support-launcher pill, bottom-right, with a small chat panel. Colours are all
   design tokens, so light/dark follow the app theme automatically. */
.askai { position: fixed; right: 20px; bottom: 20px; z-index: 60; }
.askai-launch { display: inline-flex; align-items: center; gap: 8px; min-height: 44px;
  padding: 10px 16px; border: 1px solid var(--accent-d); border-radius: 999px;
  background: var(--accent); color: #fff; font-weight: 600; font-size: var(--text-sm);
  cursor: pointer; box-shadow: var(--shadow-2); }
.askai-launch:hover { background: var(--accent-d); }
.askai-launch:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
.askai-ico { flex: none; }
.askai-panel { position: absolute; right: 0; bottom: calc(100% + 12px);
  width: min(360px, calc(100vw - 40px)); max-height: min(70vh, 560px);
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg);
  box-shadow: var(--shadow-2); }
.askai-head { display: flex; align-items: center; justify-content: space-between;
  padding: 12px 14px; border-bottom: 1px solid var(--line-soft); }
.askai-title { font-weight: 650; color: var(--ink); font-size: var(--text-md); }
.askai-close { border: 0; background: none; color: var(--muted); font-size: 16px;
  line-height: 1; cursor: pointer; padding: 4px; border-radius: var(--r-sm); }
.askai-close:hover { color: var(--ink); background: var(--line-soft); }
.askai-log { flex: 1; overflow-y: auto; padding: 14px; display: flex;
  flex-direction: column; gap: 8px; }
.askai-hint { color: var(--muted); font-size: var(--text-sm); line-height: 1.5; }
.askai-msg { max-width: 85%; padding: 8px 12px; border-radius: var(--r-md);
  font-size: var(--text-sm); line-height: 1.5; white-space: pre-wrap; word-wrap: break-word; }
.askai-msg.user { align-self: flex-end; background: var(--accent-tint); color: var(--accent-d); }
.askai-msg.bot { align-self: flex-start; background: var(--line-soft); color: var(--ink); }
.askai-nav { background: none; padding: 0; }
.askai-go { display: inline-block; padding: 8px 12px; border: 1px solid var(--accent);
  border-radius: var(--r-md); color: var(--accent-d); font-weight: 600;
  font-size: var(--text-sm); text-decoration: none; }
.askai-go:hover { background: var(--accent-tint); }
.askai-options { display: flex; flex-wrap: wrap; gap: 8px; }
.askai-chip { padding: 6px 10px; }
.askai-form { display: flex; gap: 8px; padding: 12px 14px; border-top: 1px solid var(--line-soft); }
.askai-input { flex: 1; padding: 9px 12px; border: 1px solid var(--line);
  border-radius: var(--r-md); background: var(--bg); color: var(--ink); font-size: var(--text-sm); }
.askai-input:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; border-color: var(--accent); }
.askai-send { flex: none; width: 40px; border: 1px solid var(--accent-d);
  border-radius: var(--r-md); background: var(--accent); color: #fff; font-size: 16px;
  cursor: pointer; }
.askai-send:hover { background: var(--accent-d); }
.askai-send:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
@media (max-width: 640px) {
  .askai { right: 12px; bottom: 12px; }
  .askai-launch-txt { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
}

/* =====================================================================
   Disclosures, between-stage funnel, findings + action cards. The narrative
   callout and grouped trend charts here belonged to the insights module and
   went with it (E-00, 2026-07-31); what remains is used by core pages.
   All widths/heights are SVG geometry or flex; colours are tokens only.
   ===================================================================== */

/* "View data ▾" / "View evidence ▾" progressive-disclosure block. Works with
   no JS (native <details>); summary reads as a subtle link. */
details.view-data { margin-top: var(--s3); }
details.view-data > summary {
  list-style: none; cursor: pointer; display: inline-flex; align-items: center; gap: 4px;
  color: var(--accent); font-size: var(--text-sm); font-weight: 500;
}
details.view-data > summary::-webkit-details-marker { display: none; }
details.view-data > summary::after { content: "▾"; font-size: 10px; }
details.view-data[open] > summary::after { content: "▴"; }
details.view-data[open] > summary { color: var(--ink); }
details.view-data > .view-data-body { margin-top: var(--s3); }

/* Edit-assumptions popover row (hours-saved config). */
.assumptions-row { margin-top: var(--s3); }

/* Between-stage funnel: stacked rows, each (label · bar · count), with a
   conversion badge sitting in the gap before the next stage. Scoped to
   .funnel-conv so the redesign's grid rows do NOT clobber the pool/positions
   .funnel-stage (a separate horizontal-column funnel that shares the name). */
.funnel-conv { display: flex; flex-direction: column; gap: var(--s2); }
.funnel-conv .funnel-stage { display: grid; grid-template-columns: 150px 1fr 44px;
  align-items: center; gap: var(--s3); font-size: var(--text-sm); }
.funnel-conv .funnel-stage > .tnum { text-align: right; }
.funnel-conv .funnel-gap { display: flex; align-items: center; gap: var(--s2);
  padding: 2px 0 2px 162px; font-size: var(--text-xs); }
.conv-pct { font-weight: 600; font-variant-numeric: tabular-nums; }
.conv-pct.hi { color: var(--ok-ink); }
.conv-pct.lo { color: var(--bad-ink); }
.conv-drop { color: var(--faint); }

/* Grouped monthly trend chart (screened vs hired). Per-month SVG column with
   two rects — heights are SVG geometry, fills are token classes. */
.gchart { display: flex; align-items: flex-end; gap: var(--s3); overflow-x: auto;
  padding-bottom: var(--s2); }
.gcol { display: flex; flex-direction: column; align-items: center; gap: var(--s2);
  min-width: 34px; }
.gchart-svg { display: block; width: 34px; height: 120px; }
.gbar-screened { fill: var(--accent); opacity: .35; }
.gbar-hired { fill: var(--ok); }
.gchart-x { font-size: var(--text-xs); color: var(--faint); white-space: nowrap; }
/* `.gchart-legend*` DELETED (offscale-headroom, 2026-08-09): unreferenced. The
   `.gbar-*` and `.gchart-x` rules above it are a different, live component. */

/* "Worth fixing" ranked findings list (2e). */
.findings { display: flex; flex-direction: column; gap: var(--s3); }
.finding { border: 1px solid var(--line); border-radius: var(--r-md);
  padding: var(--s3) var(--s4); }
.finding-head { display: flex; align-items: baseline; gap: var(--s2); flex-wrap: wrap; }
.finding-sev { flex: none; }
.finding-text { font-size: var(--text-sm); }
.finding-action { font-size: var(--text-sm); font-weight: 500; }

/* Lead action cards (2f) — accent keyline + warn keyline. */
.action-cards { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s4);
  margin-bottom: var(--s5); }
@media (max-width: 820px) { .action-cards { grid-template-columns: 1fr; } }
.action-card { border: 1px solid var(--line); border-radius: var(--r-lg);
  padding: var(--s4) var(--s5); background: var(--panel); }
.action-card.accent { border-left: 3px solid var(--accent); }
.action-card.warn { border-left: 3px solid var(--warn); background: var(--warn-tint); }
.action-card h2 { margin-top: 0; }
.action-card .action-lead { font-size: var(--text-2xl); font-weight: 600;
  letter-spacing: -.02em; line-height: 1.1; }
.action-card .action-cta { display: inline-block; margin-top: var(--s3);
  font-weight: 500; }

/* Ranked channel bars (2f) — label + bar, with a sub-line of counts. */
.rank-list { display: flex; flex-direction: column; gap: var(--s3); }
.rank-row { display: flex; flex-direction: column; gap: 3px; }
.rank-top { display: grid; grid-template-columns: 1fr 44px; align-items: baseline; gap: var(--s2);
  font-size: var(--text-sm); }
.rank-sub { font-size: var(--text-xs); color: var(--faint); }
.mini-target { position: relative; margin-top: var(--s2); }

/* ===== Talent bank — re-match workspace (direction 1b) =====
   A 300px | 1fr split: a rail of open roles, and the assessed bank re-ranked by
   fit to the selected role. Every value maps to an existing token; quiet .15s. */
.v-tbsep { width: 1px; align-self: stretch; background: var(--line); margin: 2px 2px; }
.v-poolfilter { display: inline-flex; align-items: center; gap: 8px; margin: 0; }
.v-fbfilter { display: inline-flex; align-items: center; gap: 8px; margin: 0; flex-wrap: wrap; }

/* Fused header controls (1b merge): the pool-status pill, Position/Status/Source
   filters, Score/Recent sort and search live INSIDE the state header (.v-header on
   the .v-banner) rather than a separate toolbar bar. .v-bannertext (flex:1) keeps
   the control cluster pushed to the right on one baseline; the whole cluster wraps
   below the title on narrow widths. One shared control height. Both themes. */
.v-header { flex-wrap: wrap; row-gap: 12px; --v-ctrl-h: 34px; }
.v-hdctrls { display: inline-flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.v-hdctrls .segmented { background: var(--line-soft); align-self: center; }
/* Filter selects in the fused header. `padding: 0 12px` used to override
   .pill-select's `6px 30px 6px 13px`, which is what reserves room for the
   chevron the same rule paints at `right 12px center` — so the caret was drawn
   UNDER the last characters of the label and the control read as a chevron-less
   chip with its text jammed against the border. Keep the shared height and the
   header's squarer radius; put the right padding back.

   The widths are pinned to one min/max because these controls auto-sized to
   their longest option: four filters on three rows came out 169 / 227 / 223 /
   138px with no alignment logic at all. A long role title now truncates with an
   ellipsis instead of stretching its control past its neighbours. */
.v-poolfilter select, .v-fbfilter select { width: auto; height: var(--v-ctrl-h);
  padding: 0 30px 0 13px; font-size: var(--text-sm); border-radius: var(--r-md);
  min-width: 160px; max-width: 200px; text-overflow: ellipsis; }
.v-hdctrls .search { display: inline-flex; align-items: center; gap: 8px;
  height: var(--v-ctrl-h); padding: 0 12px; border: 1px solid var(--line);
  border-radius: var(--r-md); background: var(--field); color: var(--muted);
  transition: border-color .15s ease, box-shadow .15s ease; }
.v-hdctrls .search svg { flex: none; }
.v-hdctrls .search:focus-within { border-color: var(--ring); box-shadow: 0 0 0 3px var(--accent-tint); }
.v-hdctrls .search input[type=search] { width: 200px; max-width: 100%; height: 100%;
  border: 0; background: none; padding: 0; font-size: var(--text-sm); color: var(--ink); }
.v-hdctrls .search input[type=search]:focus { outline: none; box-shadow: none; }
.v-split { display: grid; grid-template-columns: 300px 1fr; gap: 0; border: 1px solid var(--line);
  border-radius: 14px; overflow: hidden; background: var(--panel); width: 100%; }
.v-rolecol { border-right: 1px solid var(--line); background: var(--canvas); display: flex; flex-direction: column; }
.v-rolecol-hd { padding: 14px 16px 10px; border-bottom: 1px solid var(--line); }
.v-rolecol-hd h2 { margin: 0 0 2px; font-size: 13px; font-weight: 600; }
.v-rolelist { display: flex; flex-direction: column; padding: 8px; gap: 4px;
  flex: 1; min-height: 0; overflow-y: auto; }
.v-rolecard { text-align: left; border: 1px solid transparent; background: none; border-radius: 10px;
  padding: 11px 12px; cursor: pointer; display: flex; flex-direction: column; gap: 5px;
  font-family: inherit; text-decoration: none; transition: background .15s ease, color .15s ease; }
.v-rolecard:hover { background: var(--hover); }
.v-rolecard.on { background: var(--panel); border-color: var(--accent); box-shadow: 0 1px 2px rgba(20,22,26,.06); }
.v-rolecard .rt { font-size: 13.5px; font-weight: 600; color: var(--ink); }
.v-rolecard .rm { font-size: 12px; color: var(--muted); display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.v-newdot { display: inline-flex; align-items: center; gap: 5px; color: var(--accent); font-weight: 600; font-size: 11px; }
.v-newdot::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
.v-matchcol { display: flex; flex-direction: column; min-width: 0; }
.v-banner { display: flex; align-items: center; gap: 14px; padding: 14px 18px; background: var(--accent-tint);
  border-bottom: 1px solid var(--line); }
.v-banner .bt { font-size: 14px; font-weight: 600; color: var(--ink); }
.v-banner .bs { font-size: 12.5px; color: var(--muted); }
/* NO TRACK LIST IN THE BASE RULE. It was `26px 1.4fr 1fr auto`, and every
   `.v-mrow` in the tree also carries `.v-fb`, which replaces the whole list at
   :4258 -- so those four tracks have never rendered. That matters more than dead
   code usually does: the trailing `auto` is exactly what caused the talent-bank
   header-vs-row misalignment fixed on 2026-08-03, because a content-sized track
   resolves differently in the header grid than in each row grid. Leaving it here
   keeps the defect available to whoever next writes a `.v-mrow` without `.v-fb`.
   A grid with no explicit columns falls back to one implicit column, which is a
   visibly broken row rather than a subtly wrong one -- the better failure. */
.v-mrow { display: grid; gap: 16px; align-items: center;
  padding: 14px 18px; border-bottom: 1px solid var(--line-soft); transition: background .15s ease; }
.v-mrow:hover { background: var(--hover); }
.v-mrow.v-mhead { background: var(--canvas); border-bottom: 1px solid var(--line); }
.v-mrow .nm { font-size: 13.5px; font-weight: 600; color: var(--ink); }
.v-mrow .subtle { color: var(--faint); font-size: 12px; }
.v-ev { font-size: 12.5px; color: var(--muted); line-height: 1.5; }
.v-ev b { color: var(--ink); font-weight: 600; }
.v-rank { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--faint); text-align: center; }
.v-mact { display: flex; align-items: center; gap: 10px; justify-content: flex-end; }
.v-bannertext { flex: 1; min-width: 0; }
/* Owner comment 14 (2026-08-01). `.v-bannertext` is `flex: 1 1 0%` with
   `min-width: 0`, so it yielded width all the way down while `.v-hdctrls`
   held a constant block of min-content and never shrank. The header's
   `flex-wrap: wrap` therefore never fired: there was always a text column
   left to squeeze.

   MEASURED on the role re-match banner (/talent?mode=rematch), sweeping the
   `.v-split` container from 1400px down, BEFORE the fix:

     .v-split   1400  1300  1200  1100  1000   900   850   800
     text col    586   486   386   286   186    71    21   426(wrapped at last)
     title lines   2     2     2     3     4    10    14     2
     sub lines     2     3     3     4     7    19    31     3
     .v-hdctrls  426   426   426   426   426   426   426   426

   The control cluster is 426px at EVERY width. That is the whole bug: the
   wrap could not fire until the text column had already been squeezed to
   21px and the description was running at one word per line.

   The fix is a FLOOR, not a width, and `flex: 1 1 0%` is deliberately KEPT.
   Setting `flex: 1 1 auto` here (or adding `flex: 1 1 auto` to `.v-hdctrls`)
   makes the flex base the item's max-content, which forces the cluster onto
   its own row at EVERY width including 1400 -- measured, and a regression:
   the fused single-line header is the design. With the base still 0, the
   floor only bites once the text column would go below it, and the deficit
   then has nowhere to go but the wrap.

   38ch (~348px at this element's 14.5px) is MEASURED, not chosen: at 22ch and
   30ch the wrap still fires late enough for the title to reach three lines at
   a 1100px container; 38ch is the first floor at which the title never
   exceeds two lines at any container width from 1400 down to 700. Below the
   floor `.v-hdctrls` wraps to its own row, which is what
   `.v-header { flex-wrap: wrap; row-gap: 12px; }` was written to do.

   NOT a media query: the trigger is the CONTAINER's width. This header also
   renders inside the 65vw peek drawer, where a viewport query would fire at a
   container 35% narrower than the number it names. */
.v-bannertext { min-width: 38ch; }
.v-mhead span:first-child { text-align: center; }
.v-empty { padding: 28px 18px; }
.v-rolenote { padding: 12px; }

/* ===== Talent bank — Full bank dense table (shares the re-match .v-* rows) =====
   Same row language as the re-match workspace: a full-width bordered panel of
   .v-mrow rows with the shared thin scrollbar, and no left rail (Full bank is not
   scoped to one role). The .v-fb modifier swaps in the Full-bank column set:
   select · candidate (+applied role/status subline) · score · source · other
   fits · actions. Every value maps to an existing token. */
.v-fbtable { width: 100%; border: 1px solid var(--line); border-radius: 14px;
  overflow-x: auto; background: var(--panel);
  scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
.v-fbtable::-webkit-scrollbar { height: 8px; }
.v-fbtable::-webkit-scrollbar-track { background: transparent; }
.v-fbtable::-webkit-scrollbar-thumb { background: var(--line); border-radius: 999px; }
/* FIVE TRACKS, NOT SIX -- "Other fits" gave up its column. THE OVERLAP WAS AN
   OVERFLOW, and the sticky rule below was only its messenger.

   Measured on the deployed build at 1443px: `.v-fbfits` was 72px wide at left
   1155 -- its declared MINIMUM, i.e. the row had already run out of width --
   and the Actions cell started at 1160, INSIDE it, rendering the fits text as
   the fragments `No open / r / c / e / match`. Reproduced here in a harness
   over this stylesheet: with the six tracks at their minimums the row's total
   exceeds the panel by a little, `position: sticky; right: 0` shifts the
   Actions cell left by exactly that overflow, and it lands on its neighbour.
   The overlap tracked the overflow one-for-one (742px panel: 22px over, 6px
   overlap), which is the proof it is the overflow and not the sticky rule.

   Widening the Actions track is therefore the WRONG fix and was measured to be:
   it raises the row's minimum, so the overflow grows and the overlap grows with
   it -- at a 236px Actions minimum the cell covered the fits column entirely.
   The only fix is for the row to FIT, and this table has one column more than
   it can pay for. So the "other fits" content moves onto its own line inside
   the candidate cell, exactly as the Applied cell already carries its
   provenance on a second line rather than as a seventh column, and the freed
   ~85px is what the remaining five tracks needed. It also gives that content
   the width to render a whole sentence, which 72px never could. */
/* ---- THE ACTIONS TRACK IS DEFINITE, AND THAT IS THE WHOLE ALIGNMENT FIX -----
   MEASURED ON THE DEPLOYED BUILD (Chrome, 2026-08-03), because this could not
   be read off the source. Header vs row, resolved:
       header  26px 639.6 290.7 261.7 200.0
       rows    26px 597.1 271.4 244.3 279.3   <- all 12 rows agreed exactly
   EVERY `.v-mrow` IS ITS OWN GRID CONTAINER -- the rows are siblings, not one
   grid -- so tracks resolve per row from that row's own content. The last track
   was `minmax(200px, auto)`, and `auto` is content-sized: the header's word
   "Actions" wants 200px while a row's Save-to + Save + menu wants 279px. That
   difference changed the free space, so the three `fr` tracks ALSO resolved
   differently, and the header drifted 43px / 62px / 79px right of its columns.
   Adding classes to the header spans could never have fixed this; the boxes
   were never the problem, the TRACKS were.
   A definite track removes the only content-dependent term, so header and rows
   resolve identically from the container width alone. If the cluster ever grows
   past this, it overflows VISIBLY -- which is honest -- instead of silently
   pulling every column out of line again. Equal column widths are still not
   imposed: the other four tracks keep their minmax()/fr behaviour.
   The width is written as a LITERAL, not a var(), on purpose. `TB ROW WIDTH` in
   tests/test_talent_bank.py anchors on `grid-template-columns` sitting on this
   rule's opening line and counts tracks by matching lengths -- a var() is
   neither, and both arms went red. The guard is right and the code moved.

   ---- SEVEN TRACKS (owner, 2026-08-21). THE ACTIONS TRACK IS GONE, AND WITH IT
   THE ONLY REASON THIS ROW EVER OVERFLOWED. -------------------------------------
   The owner deleted the whole `Actions` column -- the `Save to…` form, the
   `Screen CV` form and the `⋮` menu together -- so the table is the select cell
   plus six columns of FACT and nothing else. What went with the 288px track is
   the ENTIRE class of defect that track created, and that is worth stating rather
   than quietly enjoying: the cell was `position: sticky; right: 0`, which turns
   any overflow into an OVERLAP (the cell shifts left by exactly the overflow and
   lands on its neighbour). Nothing is pinned now, so an overflow at a narrow
   width scrolls -- which is recoverable -- instead of hiding a column under
   another one. The `.v-fbfits` incident of 2026-08-02 and the CR-1 stacking-
   context fight of 2026-07-31 were both that same sticky cell; neither can recur.

   THE ARITHMETIC, recomputed rather than carried over. Dropping the track drops
   a GAP with it, so available track space at 1440px is 916px (1048 container -
   36 padding - 6x16 gap), against a minimum sum of 590px. That is 326px of slack
   for the fr weights to distribute, where eight tracks had 22px. At 1024px the
   minimums no longer fit the 500px available and it scrolls by ~90px -- it was
   overflowing by ~394px with the Actions track, so this is a four-fold
   improvement and NOT a claim that every width now fits. Below ~920px the media
   query at the end of this file stacks the row and the question does not arise.

   THE SIX MINIMUMS AND THEIR fr WEIGHTS ARE UNCHANGED, deliberately. They were
   measured under Playwright on the seeded bank and rebalanced once after looking
   at the result: the first draft gave `Applied for` a 96px minimum and the
   smallest fr share, so every slack pixel went to Candidate and "Enterprise
   Account Executive" rendered over THREE lines -- which made every row three
   lines tall and cost 119px of vertical scroll. 36px moved out of the four fact
   columns (which show `—` on most rows and whose longest real values wrap
   acceptably) and into `Applied for`, whose fr was raised so it takes slack ahead
   of them. A cell wrapping is not free: it sets the row height for the whole
   table. With 326px of slack rather than 22px the wrapping pressure is off
   entirely, so re-tuning these numbers would be a change with no defect behind
   it. Lower any minimum and the header can still wrap at 1024px, where the row
   is already scrolling. */
/* COMPACTED (owner, 2026-08-21): *"Now we have plenty of space the each row, we
   can make it slightly more compact."* -- said after the Actions column and the
   `In <status>` subline both went, which is what freed the space.

   VERTICAL ONLY, AND SCOPED TO `.v-fb`. `.v-mrow`'s own `padding: 14px 18px` is
   shared with the interview-history table, and CLAUDE.md layout rule 5 is that six
   of the last nine defects were a correct rule surviving into a container it was
   never measured in -- so the compaction lands on the bank's row and nowhere else.
   `padding-block` alone, so the 18px horizontal inset is not redeclared and the
   header and the rows keep the shared left edge UX-50 measures.

   8px IS ON THE SPACING SCALE ({0,4,8,12,16,24,32,48,64}), deliberately: a new
   off-scale declaration would RAISE check_tokens 8.13's counter, which may only
   fall. 14 -> 8 is 12px off every row, and with the subline gone a typical row is
   one line of content instead of two. */
.v-mrow.v-fb { grid-template-columns: 26px minmax(120px, 1.5fr) minmax(88px, .9fr) minmax(76px, .7fr) minmax(64px, .5fr) minmax(84px, .8fr) minmax(132px, 1.4fr);
  align-items: center; }
.v-mrow.v-fb { padding-block: 8px; }
/* UX-09d: the select-all now sits in the HEADER's own select cell, so header
   and rows share one control column and one alignment by construction. `width:
   auto` because the base input rule stretches every control to 100%, which on a
   26px track produced a checkbox the size of the cell. */
.v-fbsel { display: flex; align-items: center; justify-content: center; }
.v-fbsel input[type=checkbox] { width: auto; margin: 0; }
.v-fbcand { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.v-fbname { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
.v-fbcand .subtle { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
/* THE SIX FACT CELLS (2026-08-06). `.v-fbscore` and `.v-fbsource` are DELETED with
   the Score and Applied columns; these five replace them and `.v-fbcand` above is
   unchanged. Each is `min-width: 0` so a long value wraps inside its own track
   instead of pushing the track past its minimum and overflowing the row -- which
   is the mechanism that put the Actions cell on top of its neighbour in 2026-08-02.
   Font size and colour come from the shared `.meta` / inherited pair, so a
   populated cell reads as data and an empty one reads as muted. */
.v-fbdomain, .v-fbsen, .v-fbyears, .v-fbauth, .v-fbrole {
  min-width: 0; overflow-wrap: anywhere; font-size: var(--text-xs); color: var(--ink); }
/* `Years` visibly, `Years of experience` as the accessible name: the full phrase
   needs ~112px and this track is 64px. The `<abbr>` carries the long form, and its
   default dotted underline is removed because a column header is not an
   annotation the reader should be invited to hover for meaning. */
.v-fbabbr { text-decoration: none; }
.v-fbvia { color: var(--faint); }
/* THE SIX-FACT FILTER DISCLOSURE IS GONE (UX-109, owner 2026-08-10), and with it
   `.tb-tagfilter` and its one rule. The four `.set-row.set-row-wide.filter-group`
   fieldsets it held are in the header bar now, inside `.tb-facet` popovers -- see
   the block appended at the END of this sheet.
   ITS NOTE IS KEPT, because it is about a trap and not about that disclosure: A
   SUMMARY MUST NOT BE GIVEN `display: flex`. A flex summary drops its `::marker`,
   so the disclosure triangle disappears and the control reads as a bold heading
   with no affordance -- caught by screenshotting it, not by reading it. That
   applies to every `<summary>` in the product, including the four new ones, which
   is why `.tb-facet`'s trigger paints its own chevron instead. */
/* `.tb-nosave` IS DELETED (owner, 2026-08-21). It coloured the sentence a row
   showed INSTEAD of a `Save to…` control -- "In every open role" / "No open
   role" -- so that the affordance's absence was stated rather than merely
   observed. There is no per-row control left to be absent, and the bulk bar's
   equivalent state is a disabled control with its reason, which is the pattern
   `interviews.followups_available()` established and needs no class here. */
/* No longer a column: a line inside the candidate cell (see the grid comment
   above). It keeps its class because that is what the row's own markup and this
   sheet already call it, and `min-width: 0` still matters -- it sits in a flex
   column and must be allowed to shrink and wrap rather than widen the track. */
.v-fits-body a { display: inline-block; margin-top: 4px; }
.v-fits summary { cursor: pointer; font-size: var(--text-xs); color: var(--accent-d); list-style: none; }
.v-fits summary::-webkit-details-marker { display: none; }
.v-fits-body { margin-top: 6px; font-size: var(--text-xs); display: flex; flex-wrap: wrap; gap: 4px; }
/* ---- `.v-fbact` AND EVERYTHING THAT PINNED IT ARE DELETED (owner, 2026-08-21).
   Five rules and three defect fixes went in one edit, and the reasoning is kept
   here BECAUSE it is the argument against rebuilding any of it. ---------------

   WHAT WAS HERE. `.v-fbact` carried `display: flex` + `justify-content:
   flex-end` + `flex-wrap` + `gap` (UX-50, the owner's THIRD report of this
   column: the header text's right edge sat 325.86px inside the row clusters'
   because header and cell derived their layout from DIFFERENT rules, with the
   TRACKS agreeing to 0.00px -- the grid was never the defect). Then
   `position: sticky; right: 0` on both the cell and the header's
   `span:last-child`, with an opaque background and a left-edge shadow, because
   at 1280px the pane scrolled 246px and put every row's controls 231px past the
   visible edge. Then `:has(details.tb-menu[open]) { z-index: 40 }`, because the
   sticky cell's own z-index created a STACKING CONTEXT in which the row menu
   could never rise above the next row's cell -- CR-1, which read as "the menu is
   transparent" and was actually the Save-to controls painting on top of an
   opaque menu. Then a <=640px block switching the pinning off again.

   THE `span:last-child` RULES WERE THE DANGEROUS HALF, and deleting them was not
   optional. They were written to catch the Actions HEADER by position. With the
   track gone, `span:last-child` resolves to `Applied for` -- so leaving them
   would have pinned a fact column to the right edge with an opaque background
   and a drop shadow, on a table where nothing else is sticky. That is a defect
   nothing in the markup would show and no diff would read as wrong; it is the
   same positional-selector trap the stacked-header block below already records
   surviving twice by luck.

   NOTHING REPLACES ANY OF IT, and nothing should. UX-50's shared edge, the
   pinning, and CR-1's stacking context were all properties OF a cell that no
   longer exists. The row's last cell is now an ordinary fact column that
   scrolls with its neighbours. If a control ever returns to this table, it does
   NOT come back as a pinned cell: re-read the SEVEN TRACKS note above, where the
   whole class of overlap defect that pinning caused is recorded. */
/* `.v-fbtable .tb-review` DELETED (UX-105) — the table-density override for a
   block that no longer renders. */

/* ===== Talent bank — unified shell (1b merge) =====
   ONE role rail + a per-state fused header (controls folded into the banner)
   across both states (all-roles record table vs a role's fit ranking). The
   segmented Re-match/Full-bank toggle is retired; the rail's pinned "All roles"
   item is the all-roles entry point. Every value maps to a token; both themes. */

/* The right pane is the single horizontal-scroll owner in both states, so the
   fit-state "Why they fit" (.v-ev) column degrades to a scroll rather than
   vanishing at narrow widths, and the nested full-bank table does NOT add a
   second scrollbar. The .v-split frame owns the border/radius/overflow — the
   nested .v-fbtable drops its own so borders and corners are not doubled. */
/* CR-1 CAUSE 2, LOCATED AND MEASURED BUT DELIBERATELY NOT FIXED (2026-08-01).
   `overflow-x: auto` computes `overflow-y` to `auto` as well -- the spec
   forbids one axis visible while the other is not -- so this pane is a scroll
   container on BOTH axes, and `.popover-body` (position: absolute) is clipped
   by it. Measured at a 1440px window: the pane is 668px tall with a 1149px
   scroll height, and an open row menu is cut 36px short at its bottom edge.
   That is Sarah's "menu items partly hidden and cut off mid-word".

   TWO CANDIDATE FIXES WERE TESTED AGAINST THE RENDERED PAGE AND BOTH FAILED.
   Neither is in this file; do not re-add either believing it works.

   1. `.tb-menu { position: relative }` plus an upward-opening rule for
      `:nth-last-child(-n+3)`. The first half is a NO-OP: `details.popover` is
      already `position: relative`, so `.tb-menu` IS the popover's containing
      block already -- the comment further down that says the menus "position
      against `.tb-menu`" is TRUE, not the bug it was taken for. The second
      half keys on DOM order, but this pane scrolls INDEPENDENTLY of the page,
      so the row that is visually at the bottom is not the last row in the DOM.
      Measured: the rule left every visible row's menu still overflowing, and
      only moved rows that were scrolled out of sight anyway.

   2. `.tb-menu > .popover-body { position: fixed }` at its static position.
      Placement survives (measured within 4px of the absolute original), but it
      does NOT escape the clip: hit-testing a point below the pane's edge still
      returns MAIN.content, not the menu.

   WHAT SHIPPED (2026-08-01, owner-sanctioned): the plan's own fallback --
   `position: fixed` PLUS JavaScript that positions the menu against its
   trigger. The rule below is the whole of the CSS half; the coordinates come
   from the tb-menu block INSIDE the existing popover IIFE in
   templates/base.html, which is reused rather than duplicated so the
   aria-expanded sync, the Escape path and the outside-click dismissal are
   still the one copy there has ever been. `.pos-menu` is NOT reused; its
   scoping comment records that exact collision.

   WHY CANDIDATE 2 ABOVE REALLY FAILED, measured 2026-08-01, because the
   difference is the entire fix: nothing traps a fixed descendant on this page
   (no ancestor of `.popover-body` computes a transform, filter, perspective,
   backdrop-filter, will-change or `contain: paint`), so `position: fixed` DOES
   escape the pane's clip. What it does not do is place the box: a fixed
   element has no useful static position, and the same menu measured
   825,1124 as `absolute` measured 904,1180 as bare `fixed` -- 79px down and
   56px right, off the pane's bottom edge entirely. The hit test that returned
   MAIN.content was probing where the menu used to be. Supplying top/left is
   what makes it work, and it is why the JavaScript is not optional.

   MEASURED, 1440x900 window, pane scrolled to its end, the Actions menu on the
   last fully visible row (5 items, 218px tall), probing the centre of the
   LOWEST item, `Delete...`:
     before  document.elementFromPoint(1254, 1008) -> null  (below the viewport)
             and at the pane's own edge, (1254, 879) -> MAIN.content
     after   document.elementFromPoint(1254, 746)  -> BUTTON.menu-item.danger
   The menu flips upward there because it does not fit below the trigger.

   ON SCROLL THE MENU CLOSES, it does not track. A fixed box does not move with
   the pane, and the trigger can leave the pane entirely -- a menu still
   anchored to a row nobody can see is a worse bug than the clip. Closing is
   also what pool.html's `.pos-menu`, the only other fixed-position row menu in
   the product, already does, and it routes through the shared closePopovers()
   so aria-expanded stays honest for free.

   Do NOT "fix" this by deleting the `position: relative` on
   `.v-talentbody .v-matchcol` below -- that is what clips the `.sr-only`
   labels and keeps this page to one scrollbar. */
/* `details.popover.tb-menu > .popover-body { position: fixed }` STOOD HERE and is
   DELETED (owner, 2026-08-21) with the talent bank's Actions column, which held
   its only subject. It was ALREADY REDUNDANT -- `details.popover.menu-end >
   .popover-body` (:1377) grants the same declaration to every row overflow menu
   in the product, and both `.tb-menu` usages carried `.menu-end` too -- and the
   note at :1367 said it was kept solely as the anchor for the history above,
   because "deleting it would strand the note".

   THE NOTE IS NOT STRANDED: everything above this line stays, and `.menu-end`'s
   rule now points at it directly. That was the whole reason the redundant rule
   was tolerated, so re-pointing the reference is what discharges it rather than
   keeping a selector that can never match. The measurements, the two candidate
   fixes that FAILED against a rendered page, and the reason bare `position:
   fixed` does not work without the JavaScript are all still here, and they still
   describe every `.menu-end` menu in the product. */
/* The live-filter swap region (owner, 2026-08-21). `display: contents` IS THE
   WHOLE RULE and it is load-bearing rather than cosmetic: `.v-talentbody` is a
   flex column and this node's children -- `.v-fbtable`, the pager, the empty-state
   div -- are its flex items. An ordinary wrapper would become the flex item
   instead and take their growth, their `min-height: 0` and the pane's scrolling
   with it, which is CLAUDE.md layout rule 5 exactly: a correct rule surviving into
   a container it was never measured in. Removing the wrapper's own box leaves the
   children in the same flex context they were in before it existed.

   CONSEQUENCE, stated because it is easy to trip over: this node HAS NO BOX. It
   cannot be given a background, a border, a padding or an `aria-busy` style, and
   `getBoundingClientRect()` on it is empty. static/talent_filter.js therefore
   hangs the busy state on `.v-fbtable` inside it. If this ever needs to paint
   something, it needs to stop being `display: contents` -- and then the flex
   properties above have to move onto it in the same edit. */
.tb-swap { display: contents; }
.v-matchcol { overflow-x: auto; }
.v-matchcol .v-fbtable { border: 0; border-radius: 0; overflow-x: visible; background: transparent; }
.v-matchcol .bulk-hint { padding: 14px 18px 0; margin: 0; }
.v-matchcol .pager { padding: 12px 18px; }

/* All-roles persistent header — mirrors the role .v-banner but neutral (canvas)
   so the two states read as distinct without the toggle. */
.v-banner-full { background: var(--canvas); }
.v-banner-full svg { color: var(--muted); }

/* Pinned spine header: the all-roles / full-bank rail item. Deliberately not a
   peer .v-rolecard — heavier weight, an icon, its own accent .on state. */
.v-railall { display: flex; flex-direction: column; gap: 3px; text-decoration: none;
  padding: 11px 12px; border-radius: 10px; border: 1px solid transparent;
  transition: background .15s ease, border-color .15s ease; }
.v-railall:hover { background: var(--hover); }
.v-railall.on { background: var(--accent-tint); border-color: var(--accent); }
.v-railall .rt { font-size: 13.5px; font-weight: 700; color: var(--ink);
  display: flex; align-items: center; gap: 8px; }
.v-railall .rt svg { flex: none; color: var(--accent); }
.v-railall .rm { font-size: 12px; color: var(--muted); padding-left: 23px; }
.v-raildiv { height: 1px; background: var(--line); margin: 8px 6px; }

/* Collapsible rail (desktop): the header carries a small toggle; collapsed, the
   rail shrinks to a strip and the record table reclaims the width. */
.v-rolecol-hd { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
.v-rolecol-title { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.v-railcollapse { flex: none; width: 26px; height: 26px; border: 1px solid var(--line);
  background: var(--panel); color: var(--muted); border-radius: var(--r-sm); cursor: pointer;
  font-size: 15px; line-height: 1; display: inline-flex; align-items: center; justify-content: center;
  transition: background .15s ease, color .15s ease, transform .15s ease; }
.v-railcollapse:hover { background: var(--hover); color: var(--ink); }
.v-split.v-rail-collapsed { grid-template-columns: 46px 1fr; }
.v-split.v-rail-collapsed .v-rolecol-title,
.v-split.v-rail-collapsed .v-rolelist { display: none; }
.v-split.v-rail-collapsed .v-rolecol-hd { padding: 10px 0; justify-content: center; }
.v-split.v-rail-collapsed .v-railcollapse { transform: rotate(180deg); }

/* ===== Talent bank — the shell fills the viewport =====
   .v-split sized to its content, so an empty bank ended the card ~250px down and
   left dead space under it. The fix is a flex chain, NOT a viewport height on the
   card: main.content -> .v-talentbody -> .v-split -> the two panes. A height/100dvh
   on .v-split would ignore the page heading and the fused header and overflow.

   SCOPED with :has(), deliberately. `.content` is the shell for every page in the
   product; turning it into a flex column globally stops margin collapsing and can
   shrink width:auto block children everywhere (dashboard, pool, positions,
   reports, settings, trust, candidate pages). Only the talent page needs the
   chain, so only the talent page gets it. A browser without :has() keeps today's
   content-height card — the current behaviour, not a break.

   Scrolling stays INSIDE the panes (.v-rolelist already scrolls; .v-matchcol owns
   its own overflow), so the document keeps exactly one scrollbar. */
main.content:has(.v-talentbody) { display: flex; flex-direction: column; height: 100dvh; min-height: 0; }
.v-talentbody { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.v-talentbody .v-split { flex: 1 1 auto; min-height: 0; grid-template-rows: minmax(0, 1fr); }
.v-talentbody .v-rolecol, .v-talentbody .v-matchcol { min-height: 0; }
/* NOT cosmetic — this is what keeps the promise of one scrollbar. Each record row
   carries a visually-hidden `.sr-only` label, and `.sr-only` is position:absolute
   with no positioned ancestor, so its containing block was the initial one: the
   pane's overflow could not clip it and its offset (up to the table's full height)
   propagated to the viewport as a phantom page scroll of blank space. Making the
   pane a containing block clips them. The row menus already position against
   `.tb-menu`, so nothing visible moves. */
.v-talentbody .v-matchcol { position: relative; }
/* The fused state header is fixed-height; the empty message takes the rest of the
   pane and centres in it instead of being pinned under the header. */
.v-talentbody .v-matchcol > .v-banner.v-header { flex: 0 0 auto; }
.v-talentbody .v-matchcol > .v-empty { flex: 1; display: grid; place-items: center;
  padding: var(--s6); }
/* .empty-state p carries a 16px bottom margin for the stacked layout; grid
   centring counts it, which would sit the text 8px above the true middle. */
.v-talentbody .v-matchcol > .v-empty p { margin: 0; }

/* Mobile rail toggle + drawer scrim — desktop-inert, shown at the breakpoint. */
.v-railtoggle-m { display: none; }
.v-railscrim { display: none; }

/* Responsive (<=640px): the rail becomes an off-canvas drawer opened from the
   toolbar — NOT a full role list stacked above the table. No body h-scroll. */
@media (max-width: 640px) {
  .v-split, .v-split.v-rail-collapsed { grid-template-columns: 1fr; }
  /* One column, and the forced full height is dropped: the rail is an off-canvas
     drawer, so the card is just the record list and should grow with it. Reverting
     main.content to block also restores normal block flow on small screens. */
  main.content:has(.v-talentbody) { display: block; height: auto; }
  .v-talentbody { display: block; flex: none; }
  .v-talentbody .v-split { flex: none; grid-template-rows: auto; }
  .v-talentbody .v-matchcol > .v-empty { display: block; padding: 28px 18px; }
  .v-rolecol { position: fixed; top: 0; left: 0; bottom: 0; width: 280px; max-width: 84vw;
    z-index: 70; transform: translateX(-100%); transition: transform .2s ease;
    box-shadow: var(--shadow-2); border-right: 1px solid var(--line); }
  .v-split.v-rail-open .v-rolecol { transform: translateX(0); }
  /* The desktop collapse control and its collapsed layout do not apply on mobile. */
  .v-railcollapse { display: none; }
  .v-split.v-rail-collapsed .v-rolecol-title,
  .v-split.v-rail-collapsed .v-rolelist { display: flex; }
  .v-railtoggle-m { display: inline-flex; align-items: center; gap: 6px;
    height: var(--v-ctrl-h); padding: 0 12px; border: 1px solid var(--line);
    border-radius: var(--r-md); background: var(--field); color: var(--ink);
    font-size: var(--text-sm); cursor: pointer; }
  .v-railscrim { display: block; position: fixed; inset: 0; z-index: 65;
    background: rgba(10,13,20,.35); }
  .v-railscrim[hidden] { display: none; }
  /* Full-bank rows collapse to a stacked card (F10): the 6-column grid's
     min column widths (~800px) forced whole-page horizontal panning at 375px.
     Checkbox keeps its slim first column; every other cell stacks beside it. */
  .v-mrow.v-fb { grid-template-columns: 26px minmax(0, 1fr); row-gap: 6px; }
  .v-mrow.v-fb > * { grid-column: 2; min-width: 0; }
  .v-mrow.v-fb .v-fbsel { grid-column: 1; grid-row: 1; align-self: start; }
  /* `.v-mrow.v-fb .v-fbact { justify-content: flex-start }` stood here, undoing
     the `flex-end` the wide layout wanted. Deleted with the cell (2026-08-21). */
  /* Stacked rows: only the Candidate header label remains meaningful.
     NAMED, NOT COUNTED. This was `span:nth-child(n+3)`, written when the grid
     had SIX columns (the comment above still said so). "Other fits" gave up its
     column on 2026-08-02 and `View report` moved into Actions on 2026-08-03, so
     the positional selector has already survived two changes to the thing it
     counts -- correct at HEAD by luck, and nothing renders CSS to notice when it
     stops being. Adding one header span would have silently hidden the wrong
     column. The headers gained these classes in the 2026-08-03 alignment fix,
     so naming them costs nothing and cannot drift.
     THAT WARNING CAME DUE ON 2026-08-06, and being named is what made it a
     one-line edit instead of a silent defect: `Score` and `Applied` went, five
     fact columns arrived, and `nth-child(n+3)` would have hidden the wrong three.
     IT CAME DUE A SECOND TIME ON 2026-08-21, and this is the case that proves the
     point rather than merely repeating it: the owner deleted `Actions`, which was
     the LAST header in the row. `nth-child(n+3)` would still have hidden exactly
     the right set here and looked fine -- but the sticky rules further up this
     file, which selected the same cell as `span:last-child`, would silently have
     started pinning `Applied for` instead. Naming these five cost nothing and
     drifted nowhere; the positional selector 200 lines away is the one that had
     to be hunted down. The list below is the five fact headers -- every header
     except Candidate, the only one still meaningful when the row is stacked. */
  .v-mrow.v-fb.v-mhead { grid-template-columns: 26px minmax(0, 1fr); }
  .v-mrow.v-fb.v-mhead .v-fbdomain,
  .v-mrow.v-fb.v-mhead .v-fbsen,
  .v-mrow.v-fb.v-mhead .v-fbyears,
  .v-mrow.v-fb.v-mhead .v-fbauth,
  .v-mrow.v-fb.v-mhead .v-fbrole { display: none; }
}


/* ---- background-work: the sidebar row (templates/_jobs_menu.html) ---------
   ONE LINK, NOT A POPOVER (owner ruling, 2026-08-07). It borrows `.side-user`
   wholesale for its geometry and hover, so it inherits the rail-collapsed
   behaviour with no new machinery; only what is genuinely its own is here.

   THE PANEL'S RULES ARE DELETED, NOT DISABLED. `.jobsq-panel`, `.jobsq-foot`,
   `.jobsq-item`, `.jobsq-item-bad`, `.jobsq-item-h`, `.jobsq-cancel`,
   `.jobsq-note`, `.jobsq-label`, `.jobsq-ack` and `.jobsq-cols` went with the
   markup they styled. A rule kept "in case" is how a deleted surface grows back.

   THE TRACK-VS-FILL LESSON SURVIVES ITS RULE, and it is why the page's outcome
   bar below is SEGMENTED. `.jobsq-item-bad .jobsq-bar > span` used to paint the
   fill `--bad`, over a fill drawn as `settled / total` -- which reaches 100%
   whether the batch scored everybody or nobody, because a scorer failure that
   must not be retried is recorded `done`. A batch of 800 that assessed none of
   them therefore drew a FULL, saturated bar inside the group headed "needs
   attention": the page exists to answer "did anything fail", and its loudest
   pixel said the opposite. This is the identical mis-read Phase 17 found on the
   pipeline funnel, where progress-COMPLETE and progress-GOOD were two facts
   carried by one colour. Both fixes now avoid it the same way -- one segment per
   outcome, so no single segment can fill the bar unless that outcome really is
   the whole batch. */
.jobsq-wrap[hidden] { display: none; }
.jobsq-link { text-decoration: none; color: inherit; }
.jobsq-link:hover { background: var(--hover); }
.jobsq-dot { width: 8px; height: 8px; border-radius: 50%;
  background: var(--line); flex: 0 0 auto; }
/* Two states, and they are not the same question. `is-busy` is "work is moving";
   `is-att` is "something wants you", and it wins -- a failure under a blue dot
   is the reading this whole refinement exists to stop. */
.jobsq-wrap.is-busy .jobsq-dot { background: var(--accent); }
.jobsq-wrap.is-att .jobsq-dot { background: var(--bad); }
.jobsq-wrap.is-att .jobsq-link .role { color: var(--bad-ink); }
.jobsq-link .role { font-variant-numeric: tabular-nums; }
/* THE BADGE COUNTS BATCHES NEEDING ATTENTION, not active jobs, so it is --bad
   rather than --accent. `#fff` on --bad is the same pairing the rest of the
   product uses for a count chip on a saturated ground. */
.jobsq-count { min-width: 20px; padding: 0 8px; border-radius: 10px;
  background: var(--bad); color: #fff; font-size: var(--text-xs);
  line-height: 18px; text-align: center; flex: 0 0 auto; }
.jobsq-count[hidden] { display: none; }
/* BADGE OR CHEVRON, NEVER BOTH: the trailing slot holds one affordance. Driven
   from `is-att` in CSS rather than by setting `.hidden` on the element in JS --
   `hidden` is an IDL property of HTMLElement and this is an <svg>, which
   implements HTMLOrSVGElement instead, so the assignment set a JS property,
   wrote no attribute, and the rail rendered both. */
.jobsq-wrap.is-att .jobsq-chev { display: none; }
/* A HAIRLINE, AND ONLY WHILE SOMETHING RUNS. Aggregate progress across the
   active batches, from the server's own `active_pct` -- a track with no work
   behind it is a control reporting on nothing. */
.jobsq-hair { height: 3px; border-radius: 2px; background: var(--line);
  overflow: hidden; margin: 4px 8px 0; }
.jobsq-hair[hidden] { display: none; }
.jobsq-hair > span { display: block; height: 100%; width: 0; background: var(--accent); }
/* Collapsed rail: `.side-user-text` hides as it does for the account row, and
   the dot and badge SURVIVE -- so a narrowed rail still says something needs
   you, which is the one thing this control exists for. */
:root[data-rail="collapsed"] .jobsq-hair { display: none; }

/* ---- background-work: the page (templates/jobs_batches.html) --------------
   TWO BORDERED TABLES, NOT A LIST OF CARDS (design handoff
   2026-09-03-background-work). It was three side-by-side state groups (UX-19c,
   owner 2026-08-03), then one full-width column of seven-band rows, and with 32
   batches needing attention neither could be SCANNED: a batch of two CVs
   occupied 200px, and six red paragraphs stood between the reader and the first
   row. The questions this page answers are "how much is broken / whose fault /
   can I press anything / which batch", so it is one line per FACT and one line
   per BATCH, numbers in columns, and every sentence on a `title`.

   THE COLUMN TEMPLATE IS DECLARED ONCE PER TABLE AND SHARED BY THE HEAD AND
   EVERY ROW. That is CLAUDE.md layout rule 2 -- the grid lives on the row, never
   per item -- and it is what makes the header labels sit over their cells by
   construction rather than by two numbers somebody kept in step. There is no
   `space-between` and no `margin-left: auto` in this block: every right edge is
   a grid track, and the two `.jobsq-spacer`s are inside flex rows that are not
   tables (the filter strip and the table footer).

   THE MEASUREMENTS ARE ON THE 4px SCALE (0/4/8/12/16/24/32/48/64), and that was
   a CONSTRAINT rather than a preference: `check_tokens` 8.13's off-scale counter
   has zero slack, so every value the handoff specified off the scale (6px and
   2px head/row padding, a 2px bar gap, a 1px stack gap, 3px 9px control padding)
   is rounded to the nearest scale step here. The 4px and 6px bar HEIGHTS, the
   38px and 32px row floors, the 320px bar cap and the fixed track widths are not
   spacing and are not counted.

   NOTHING BELOW 760px IS DESIGNED HERE, and the handoff says so in its own
   words. The tracks are fixed, so a narrow window scrolls the table rather than
   reflowing it; the batch table's own arithmetic closes at 890px of content
   column (fixed tracks 680 + 84 of gap), which is a 1280px window with the rail.
   A viewport rule for the phone case is a product decision nobody has taken.

   THE TRACK-VS-FILL LESSON SURVIVES AND IS WHY THE OUTCOME CELL IS SEGMENTED.
   The old bar drew `settled / total`, which reaches 100% whether the batch
   scored everybody or nobody -- a scorer failure that must not be retried is
   recorded 'done' -- so a batch of 800 that assessed none of them drew a FULL,
   saturated bar inside the group headed "needs attention". One segment per
   outcome means no single segment can fill the bar unless that outcome really is
   the whole batch. */
.jobsq-updated { display: flex; align-items: center; gap: var(--s2); }
.jobsq-age { font-size: 12.5px; color: var(--faint);
  font-variant-numeric: tabular-nums; }
.jobsq-refresh-i { flex: 0 0 auto; }
/* THE ONE SENTENCE ON THE PAGE, and it is in the head rather than under the h1.
   It is the only place the product says a batch survives navigation, which is
   the reason a recruiter does not sit and watch -- so it is kept and moved
   rather than retired with the rest of the prose. Faint and beside "Updated Ns
   ago", where it reads as a note about the page and not as a paragraph. */
.jobsq-persist { font-size: 12px; color: var(--faint); }

/* The state filters. Pills, and the ACTIVE one is announced by `aria-current`
   rather than by a second class -- one attribute, read by a screen reader and
   selected on by the stylesheet, so the two cannot drift. */
.jobsq-filters { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
  margin: var(--s5) 0; }
.jobsq-filter { display: inline-flex; align-items: center; gap: var(--s2);
  padding: var(--s1) var(--s3); border-radius: 999px; border: 1px solid var(--line);
  background: none; color: var(--muted); font-family: inherit;
  font-size: 12.5px; font-weight: 500; cursor: pointer; text-decoration: none; }
.jobsq-filter:hover { color: var(--ink); }
.jobsq-filter[aria-current="true"] { border-color: var(--accent);
  background: var(--accent-tint); color: var(--accent); font-weight: 600; }
.jobsq-filter[data-tone="bad"][aria-current="true"] { border-color: var(--bad);
  background: var(--bad-tint); color: var(--bad-ink); }
.jobsq-filter-n { font-variant-numeric: tabular-nums; }
.jobsq-scope { color: var(--faint); }
/* A flex SPACER, never `margin-left: auto` or `justify-content: space-between`
   on the row. Those are the two drift primitives CLAUDE.md's layout rule 2 names
   and `check_tokens` 8.13 counts: each aligns a row to its OWN content width, so
   two rows that look right alone disagree with each other. */
.jobsq-spacer { flex: 1; }

/* NO WIDTH CAP (UX-100, owner 2026-08-09: "it should take up full width instead
   of just 60-65% of the page"). This carried `max-width: 1000px`, which at the
   ~1600px the owner measured is 63% -- the number in the comment. It was a
   LAYOUT cap, and this file's own measure rule at :598 defends the opposite:
   "Cap the MEASURE, never the layout." A container of aligned tables is layout.

   IT IS NOW THE WRAPPER FOR BOTH TABLES rather than for four state groups, and
   `BW WIDTH` in tests/test_ui_alignment.py still measures it reaching its
   column's content box on both edges at three viewports. */
.jobsq-groups { display: flex; flex-direction: column;
  gap: var(--s4); }
.jobsq-group[hidden] { display: none; }
.jobsq-empty { padding: var(--s3); }

/* ---- TABLE 1: THE REASONS (`.triage`) ------------------------------------
   ONE ROW PER FAILURE CODE ACROSS THE WHOLE ATTENTION GROUP. This is
   `.jobsq-banner` with a count column: the banner said "this is one problem on
   our side, not N separate ones" in a red paragraph, six times, with N at the
   end of each sentence. The table makes no such claim -- the `side` column says
   whose fault a code is, in words, and a row whose `batches` cell reads 8 lets
   the reader draw the conclusion in one glance.

   THE BOX IS THE `.sch-flush` TREATMENT AND NOT A NEW CARD CLASS. `check_tokens`
   8.5 keeps a may-not-grow register of card-shaped names, and a bordered table
   is not a card: 1px of `--line`, radius 8, `overflow: hidden` so the rows'
   hairlines stop at the corner, on `--panel`. */
.triage, .jobsq-log { border: 1px solid var(--line); border-radius: 8px;
  background: var(--panel); font-variant-numeric: tabular-nums;
  /* THE TABLE SCROLLS BELOW ITS FLOOR, AND THE PAGE DOES NOT. Both track lists
     are FIXED plus one `1fr`, so the batch table's arithmetic closes at 890px of
     content column -- a 1280px window with the rail -- and the handoff designs
     nothing narrower. `overflow: hidden` was the first draft and it CLIPPED: at
     a 1080px window the actions column was 88px outside the box, invisible and
     unreachable, with the page reporting no horizontal scroll at all. Found by
     `BW LOG` in tests/test_ui_alignment.py, not by reading the cascade.
     `overflow-y` is pinned to `hidden` because a bare `overflow-x` would compute
     the other axis to `auto` and put a vertical scrollbar on a table that never
     needs one; the pair still clips, which is what the radius wants. */
  overflow-x: auto; overflow-y: hidden; }
/* ...AND THE ROW REFUSES TO SHRINK, which is what gives the box something to
   scroll. `min-content` on a grid container with fixed tracks IS the sum of the
   tracks plus the gaps -- DERIVED, so it cannot drift from the template above
   it the way a typed 764px would. */
.triage-row, .triage-head, .triage-foot,
.jobsq-row, .jobsq-log-head { min-width: min-content; }
/* THE ONE APPROVED NEW PRIMITIVE, and both selectors share one declaration so
   the head can never drift from the rows beneath it. */
.triage-row, .triage-head, .triage-foot { display: grid;
  grid-template-columns: 48px 200px 64px minmax(0, 1fr) 64px 176px;
  gap: var(--s3); align-items: center;
  padding: var(--s1) var(--s3); }
/* THE FOOTER IS THE SAME GRID WITH THREE TRACKS, not a flex row: the item total
   has to sit under the `items` column or it is a number beside a number it does
   not match. */
.triage-foot { grid-template-columns: 48px minmax(0, 1fr) max-content;
  padding: var(--s2) var(--s3); font-size: 11.5px; color: var(--faint); }
/* THE HEAD TREATMENT IS `.sch-roles-head`'s, deliberately: a canvas fill, small
   uppercase faint labels, hairlines top and bottom. Two tables in this product
   now read the same way. */
.triage-head, .jobsq-log-head { padding: var(--s2) var(--s3);
  background: var(--canvas); border-bottom: 1px solid var(--line-soft);
  font-size: 10.5px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--faint); }
.triage-row { min-height: 32px; border-bottom: 1px solid var(--line-soft); }
.triage-row:hover { background: var(--hover); }
/* THE ITEM COUNT IS THE LOUDEST THING IN THE ROW, and it is `--bad-ink` only
   when the fault is ours -- the one case whose number grows while nobody does
   anything. `--*-ink` for TEXT and `--bad`/`--warn` for fills and rules: `--bad`
   on `--panel` is measured marginal for text (note at `:root`). */
.triage-n { font-size: 12.5px; font-weight: 600; color: var(--ink); }
.triage-n[data-fault="ours"] { color: var(--bad-ink); }
/* THE CODE IS THE SUPPORT REFERENCE, which is why it is the mono face: it is a
   string somebody reads out or pastes into an email, not a word. */
.triage-code { font-family: var(--font-mono); font-size: 11.5px;
  color: var(--ink); overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
.triage-batches { font-size: 12px; color: var(--muted); }
/* ---- ALIGNMENT IS A PROPERTY OF THE COLUMN, NOT OF THE CELL --------------
   Declared ONCE for the header cell and the body cell together. `.jq-end` and
   `.jq-mid` are the two shared names the HEAD markup carries, so a column's
   alignment is one declaration covering the label and the cells under it rather
   than a decision taken twice.

   `justify-self` AND NOT `text-align`, WHICH IS THE WHOLE REASON THIS RULE IS
   SHAPED THIS WAY. Both render a number at the right of a 48px track and look
   identical; only one is VISIBLE TO A MEASUREMENT. `text-align` leaves the cell
   filling its track, so a right-aligned number under a left-aligned header label
   is two boxes with identical edges and disagreeing contents -- and every arm in
   this repo's browser suite reads boxes. `justify-self: end` shrinks the cell to
   its content and puts that content's edge ON the column edge, which is what
   `TRIAGE GRID` / `BW LOG` in tests/test_ui_alignment.py then compare down the
   column. The first draft used `text-align: right` here with bare `<span>`s in
   the head; it rendered a label at the left of the `items` track over numbers at
   its right, and no arm could see it. */
.triage-n, .triage-batches, .triage-act, .jq-end { justify-self: end; }
.jobsq-row > .jobsq-dot, .jq-mid { justify-self: center; }
.triage-foot-sum { color: var(--faint); }
/* THE SHARE BAR. `flex` weights come from the row's own counts, so the two
   segments sum to the whole attention group by construction -- the same
   arithmetic-free arrangement as the outcome bar below. Capped at 320px because
   past that the proportion is no easier to read and the `retry` column is what
   the eye wants next. */
/* NO `max-width` ON THE BAR, and the handoff asked for 320px. A cap on ONE
   CELL inside a shared grid track is the per-call-site layout decision this
   contract exists to refuse: the head's own cell fills the track, so the cap put
   92px between the column's header edge and its rows' at 1440px. `TRIAGE GRID`
   measured it. The bar fills its track instead, which is also the reading the
   column is for -- a proportion is easier to compare across rows the wider it
   is drawn. */
.triage-bar { display: flex; height: 4px; gap: var(--s1); }
.triage-bar-on { border-radius: 2px; background: var(--muted); }
.triage-bar-on[data-fault="ours"] { background: var(--bad); }
.triage-bar-on[data-fault="yours"] { background: var(--warn); }
.triage-bar-off { background: none; }

/* ---- TABLE 2: THE BATCHES (`.jobsq-log`) ---------------------------------
   EIGHT TRACKS, 38px, ONE LINE PER BATCH, and a RUNNING row is the same eight
   cells with different readings -- which is why the stage strip went. It was the
   only band a running row had that a finished one did not, and the step name it
   carried is in the reason column now.

   THE HEAD AND EVERY ROW SHARE ONE DECLARATION, exactly as the reasons table
   does. `BW LOG` in tests/test_ui_alignment.py measures that in the browser
   across attention AND running rows, because a grid that agrees in the
   stylesheet and not on screen is the defect class this whole contract exists
   for. */
.jobsq-row, .jobsq-log-head { display: grid;
  grid-template-columns: 16px 56px minmax(0, 1fr) 96px 96px 200px 96px 120px;
  gap: var(--s3); align-items: center; padding: var(--s1) var(--s3); }
.jobsq-row { min-height: 38px; border-bottom: 1px solid var(--line-soft); }
.jobsq-row:hover { background: var(--hover); }
/* THE DOT REPLACES THE 2px LEFT RULE, and it borrows `.jobsq-dot`'s geometry
   from the sidebar block above -- one 8px circle in the product, two consumers.
   The sidebar's dot carries no `data-state`, so these four rules cannot reach
   it. The state is an ATTRIBUTE and never half of a class name: check_tokens'
   class scanner strips Jinja, so a class assembled half in the template and half
   at render time is one this sheet can never be proved to define. */
.jobsq-dot[data-state="attention"] { background: var(--bad); }
.jobsq-dot[data-state="running"] { background: var(--accent); }
.jobsq-dot[data-state="done"] { background: var(--ok); }
.jobsq-dot[data-state="cancelled"] { background: var(--warn); }
/* THE BATCH, AS THE HEAD OF ITS OWN SUPPORT REFERENCE (`BW-158`). Mono for the
   same reason the code is: it is a string that gets quoted. */
.jobsq-id { font-family: var(--font-mono); font-size: 11.5px; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.jobsq-name { display: flex; align-items: baseline; gap: var(--s2);
  min-width: 0; overflow: hidden; }
/* THE KIND AS TWO LETTERS. `.jobsq-eyebrow` IS DELETED with the band it was:
   the kind, the creator and the timestamp shared one line above the title, and
   the creator and the timestamp are now the `when` cell and its `title`. */
.jobsq-kind { flex: 0 0 auto; font-size: 10.5px; font-weight: 600;
  letter-spacing: .05em; text-transform: uppercase; color: var(--faint); }
/* IT ELLIPSISES, REVERSING THE EARLIER NOTE HERE ("it wraps and is not
   clamped"). That was right while the row was 200px tall and a wrapped role name
   cost nothing; on a 38px line a wrap is what breaks the shared row height, and
   the full label is in the cell's `title`. */
.jobsq-title { font-size: 13px; font-weight: 600; letter-spacing: -.01em;
  color: var(--ink); min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
/* THE OUTCOME CELL: SEGMENTS for a settled batch, a TRACK AND FILL for a running
   one. See the track-vs-fill note at the head of this section for what the fill
   alone used to claim. `.jobsq-prog` / `.jobsq-progn` are DELETED -- the count
   they carried beside the bar is the `done / failed` cell now, in the same
   column as every other row's counts. */
.jobsq-outcome { display: flex; height: 6px; gap: var(--s1); }
.jobsq-seg { border-radius: 3px; }
.jobsq-seg[data-tone="ok"] { background: var(--ok); }
.jobsq-seg[data-tone="bad"] { background: var(--bad); }
.jobsq-seg[data-tone="warn"] { background: var(--warn); }
/* NOT YET SETTLED, and it is the track colour rather than a fourth outcome
   colour: an absence must not claim a state. It is what stops a still-running
   batch whose first three children failed from drawing a full red bar. */
.jobsq-seg[data-tone="pending"] { background: var(--line); }
/* The progress bar is a plain span pair rather than <progress>: <progress> is
   near-unstyleable across engines. */
.jobsq-track { height: 6px; border-radius: 3px; background: var(--line);
  overflow: hidden; }
.jobsq-track > span { display: block; height: 100%; background: var(--accent); }
/* THE COUNTS, AND THE THREE QUANTITIES STAY APART. A crash can be completed by a
   retry and a child that ran and declined cannot, which is the distinction this
   page exists to keep -- so `done / failed / not scored / planned` are four
   numbers in four tones and never their sum. */
.jobsq-counts { font-size: 12px; white-space: nowrap; }
.jobsq-num-ok { color: var(--ok-ink); }
.jobsq-num-bad { color: var(--bad-ink); }
.jobsq-num-warn { color: var(--warn-ink); }
.jobsq-num-run { color: var(--accent); }
.jobsq-num-n { color: var(--faint); }
/* THE REASON CELL IS A TWO-LINE STACK, both lines ellipsised inside their own
   track. `line-height` is tightened here and nowhere else on the page: two lines
   at the body's own 1.45 would be 33px of content inside a 38px row, and the
   shared row height is the thing this table is for. */
.jobsq-reason { display: flex; flex-direction: column; min-width: 0;
  overflow: hidden; line-height: 1.25; }
.jobsq-reason-l { display: flex; align-items: baseline; gap: var(--s2);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.jobsq-code { font-family: var(--font-mono); font-size: 11px; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; }
.jobsq-more { flex: 0 0 auto; font-size: 11px; color: var(--faint); }
.jobsq-who { font-size: 11.5px; color: var(--muted); }
.jobsq-stagenow { font-size: 11.5px; color: var(--muted); }
.jobsq-stalled { font-size: 11px; color: var(--bad-ink); }
/* WHOSE FAULT, AS A WORD AND NOT ONLY A COLOUR. A reader who cannot distinguish
   the tints gets the same answer from the text. The three rules are on the
   element's OWN `data-fault` rather than on an ancestor's, because the word
   appears in two tables now -- the reasons table's `side` column and the batch
   row's reason cell -- and a descendant selector would have needed one copy per
   container. */
.jobsq-fault { flex: 0 0 auto; font-size: 10px; font-weight: 600;
  letter-spacing: .04em; text-transform: uppercase; color: var(--faint); }
.jobsq-fault[data-fault="ours"] { color: var(--bad-ink); }
.jobsq-fault[data-fault="yours"] { color: var(--warn-ink); }
.triage-side { font-size: 10.5px; }
/* WHEN AND HOW LONG. `no estimate` is a WORD and not a blank, because the server
   withholds an ETA for every multi-stage pipeline on purpose and a reader cannot
   tell a withheld number from a missing one unless the page says which. */
.jobsq-when { font-size: 12px; color: var(--faint); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.jobsq-noeta { border-bottom: 1px dotted var(--line); }
.jobsq-acts { display: flex; justify-content: flex-end; align-items: center;
  gap: var(--s1); }
/* THE ONE-WORD STATE, where there is nothing to press. Its sentence is on the
   element's own `title` -- `jobs.retry_action` supplies both, so the word and
   the explanation cannot come from two different tables. */
.jobsq-act-none { font-size: 11px; color: var(--faint); white-space: nowrap; }
/* Borderless, and the SAME height as the ghost button beside it so their
   baselines agree -- `.btn.sm`'s own min-height does that, and the transparent
   border keeps the box identical rather than one pixel shorter. */
.jobsq-seen { background: none; border: 1px solid transparent;
  color: var(--faint); }
.jobsq-seen:hover { color: var(--ink); }

/* THE TABLE'S FOOTER LINES. Three of them: the Finished disclosure's summary,
   the Cancelled one's, and the table's own "N of M shown" with the support
   address. `.jobsq-fold-sum` is DELETED and this replaces it -- the fold's
   summary and the footer were two bands saying the same thing.

   NATIVE `<details>`, not a JS accordion: a page that needs no second
   collapsible mechanism should not ship one. The rule is on `details` rather
   than on a `.jobsq-fold` class of its own, because the group wrapper the filter
   binds to is the same element. */
.jobsq-log-foot { display: flex; align-items: center; gap: var(--s2);
  padding: var(--s2) var(--s3); font-size: 11.5px; color: var(--faint);
  border-top: 1px solid var(--line-soft); }
.jobsq-log-n { font-variant-numeric: tabular-nums; }
.jobsq-live { justify-content: flex-end; }
details.jobsq-group > summary { cursor: pointer; list-style: none; }
details.jobsq-group > summary::-webkit-details-marker { display: none; }
details.jobsq-group > summary:hover { background: var(--hover); }
.jobsq-caret { color: var(--faint); font-size: 11px; }
details.jobsq-group[open] .jobsq-caret { transform: rotate(90deg);
  display: inline-block; }
/* ---- SINGLE JOBS (owner, 2026-09-07) -----------------------------------
   The unbatched half of the queue. A `<table>` and not a grid: four columns of
   short cells with no bar, no stage breakdown and no ETA, so the intrinsic
   sizing a table gives for free is the right instrument -- and unlike the batch
   rows, the header and the rows are ONE element here, so the per-container
   resolution that forces a fixed track on `.posb-cols` and `.sch-bkt` cannot
   apply. Full width inside the group it sits in. */
.jobsq-singles { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.jobsq-singles th { text-align: left; padding: var(--s2) var(--s3);
  font-size: 10.5px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--faint);
  border-bottom: 1px solid var(--line-soft); }
.jobsq-singles td { padding: var(--s2) var(--s3); vertical-align: middle;
  border-bottom: 1px solid var(--line-soft); }
.jobsq-singles tr:last-child td { border-bottom: 0; }
/* THE ERROR SITS UNDER THE STATE CHIP rather than in a column of its own: it is
   empty on every row that did not fail, and a column blank for most rows makes
   the table harder to read. Ellipsised, with the full string on `title`. */
.jobsq-singles-why { display: block; max-width: 46ch; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
/* Right-aligned by the CELL, never by a margin on the control -- an auto margin
   is one of the three drift spellings 8.13 counts down. */
.jobsq-singles-act { text-align: right; white-space: nowrap; }

/* ---- WHAT WENT, SO IT DOES NOT GROW BACK --------------------------------
   `.jobsq-banner` -- six red paragraphs before the first row, retired with
   `jobs.shared_fault_codes`. The reasons table is the same fact as a column.
   `.jobsq-fails` / `.jobsq-fail` / `.jobsq-fail-n` / `.jobsq-fail-why` /
   `.jobsq-fail-who` / `.jobsq-fail-act` / `.jobsq-fail-none` /
   `.jobsq-fail-note` -- one tinted line per reason INSIDE every row, repeating
   the banner's sentence per batch. The reason CELL carries the code and the
   count; the sentence is the row's `title`.
   `.jobsq-fail-support` / `.jobsq-fail-ref` -- a whole line per batch for a
   reference the row already is (`.jobsq-id` is `BW-158` and its `title` is the
   full string). The address is one line in the table footer.
   `.jobsq-stages` / `.jobsq-stage` / `.jobsq-stage-name` / `.jobsq-stage-time`
   and their five state rules -- the stage strip. Its step NAME is the running
   row's reason cell; its per-stage averages were tooltips on cells that no
   longer exist, which is a recorded cost.
   `.jobsq-now` -- "Now: Karen Lau 14.2s", a per-worker line under every running
   batch. The elapsed clock survives in the `when` cell; the names do not.
   `.jobsq-prog` / `.jobsq-bar` / `.jobsq-progn` -- the running row's own band.
   The track and fill moved into the outcome cell and the count into `done /
   failed`, so a running row is the same eight cells as every other row.
   `.jobsq-meta` -- the prose outcome line.
   `.jobsq-eyebrow` -- the kind/creator/time band above the title.
   `.jobsq-ghead` / `.jobsq-gdot` / `.jobsq-gtitle` / `.jobsq-gcount` /
   `.jobsq-gnote` / `.jobsq-gact` -- the four group heads. The states are the
   filter pills' counts and the rows' own dots; a band repeating them above rows
   in the same table was the third place each number appeared.
   `.jobsq-fold` / `.jobsq-fold-sum` -- see `.jobsq-log-foot` above.
   `.jobsq-rows` / `.jobsq-rhead` / `.jobsq-rmain` -- the row's inner flex
   scaffolding, which a grid on the row itself does not need.
   `.sub` on this page -- its one sentence is `.jobsq-persist` in the page head.

   A RULE KEPT "IN CASE" IS HOW A DELETED SURFACE GROWS BACK, which is why these
   are named here rather than left commented out. `check_tokens` 8.4 watches a
   list of retired component names and this block is not on it: these are not
   component names, they are one surface's own vocabulary, and the guard that
   holds them absent is `BW TRIAGE` in tests/test_jobs_async.py. */

/* ---- Collapsible group (templates/_collapsible_group.html) ---------------
   Built in Phase 24 for the background-work page's ongoing / failed / finished
   grouping (UX-19c). UX-13e asks for the same interaction in Settings and
   Phase 22 builds that; PHASE 22 SHOULD ADOPT THIS MACRO AND THESE CLASSES
   rather than introduce a second pattern, and extend them here if the settings
   version needs more.

   A native <details>/<summary>: keyboard-operable and announced correctly
   without a line of JavaScript, which matters on a page whose whole job is to
   be readable when something has gone wrong. */
.cgroup { border: 1px solid var(--line-soft); border-radius: 8px; }
.cgroup + .cgroup { margin-top: 12px; }
.cgroup-head { display: flex; align-items: center; gap: 8px; cursor: pointer;
  padding: 9px 12px; font-weight: 600; font-size: 13px;
  list-style: none; user-select: none; }
/* The default disclosure triangle is replaced by our own so it sits on the
   left in every engine; Safari needs the -webkit- pseudo-element too. */
.cgroup-head::-webkit-details-marker { display: none; }
.cgroup-head::before { content: "\25B8"; color: var(--faint); font-size: 11px;
  transition: transform .12s ease; }
.cgroup[open] > .cgroup-head::before { transform: rotate(90deg); }
/* UX-13a: page head = capped prose column + the summary strip that fills the
   space the cap leaves. `minmax(0, …)` on both tracks so neither can push the
   page into a horizontal scroll; the aside is `max-content` so it takes only
   what its chips need and gives the rest back to the sentence. */
.page-head { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, max-content);
  align-items: start; gap: var(--s5); }
.page-head-copy { min-width: 0; }
.page-head-aside { justify-self: end; margin-top: 6px; }
@media (max-width: 760px) {
  .page-head { grid-template-columns: minmax(0, 1fr); }
  .page-head-aside { justify-self: start; }
}
.cgroup-title { flex: 1 1 auto; }
/* UX-13e (remainder-a, 2026-08-03): the STATUS CHIPS a settings panel heading
   carries -- `Key set`, `Token set`, `Configured` -- travel into the summary
   rather than being dropped to make the heading fit a plain string. They are
   the only at-a-glance signal that a credential is configured, and they must
   stay legible when the group is CLOSED, which is the state they matter in. */
.cgroup-chips { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px;
  flex: 0 0 auto; font-weight: 400; }
/* A settings panel inside a group body already has the group's padding around
   it; its own 22px band and hairline would double both. */
.settings-pane .cgroup-body > .panel { padding: 0; border-bottom: 0; }
/* THE MULTI-PANEL SIBLING RULE IS DELETED (UX-83, owner 2026-08-07). It read
   `.settings-pane .cgroup-body > .panel + .panel { border-top: 1px solid
   var(--line-soft); padding-top: 12px; }` and it existed for ONE call site: the
   AI-interview tab's groups held two and four panels, where the zeroed
   `border-bottom` above took away the only thing separating stacked settings
   sections and left them 12px of flex `gap` apart.

   THAT CALL SITE IS GONE. The owner removed the double-nested disclosure from that
   tab, so its panels are children of `.settings-pane` directly and get
   `.settings-pane .panel`'s own bottom hairline back -- which is the "plain
   sections separated by hairlines" this pane was designed as. Every other
   `.settings-pane` group holds exactly ONE panel (General's two groups, Retention's
   windows, Roles & permissions' capabilities and project access), verified by
   reading all four partials rather than inferred, so the rule now matches nothing.

   DELETED RATHER THAN KEPT "for a future multi-panel body": a rule no call site
   selects is the shape this repository keeps striking, and the guard that pinned it
   (`IV COLLAPSIBLE`, tests/test_settings_surface.py) is converted in the same
   commit to assert the panels get their hairline from the pane instead. The rule
   ABOVE stays -- it still has four one-panel consumers. */
.settings-pane .cgroup { margin-top: 12px; }
/* UX-28 / UX-37: the role wizard's sections are `.assess-block`, not `.panel`, so
   the `.settings-pane` rule above does not reach them -- and `.assess-block`
   carries its OWN border, radius and padding. Nested inside `.cgroup` (which is
   also bordered) that draws A BOX INSIDE A BOX with two paddings, the same
   doubling UX-27 fixed on the drawer. The group is the box now; the block inside
   it is just content. */
.cgroup-body > .assess-block { border: 0; border-radius: 0; padding: 0; margin: 0; }
/* AND THE TITLE MOVED TO THE SUMMARY, so the block's own title would render it
   twice. Hidden rather than deleted from the partial on purpose: all three
   partials are shared with `positions_new.html`, which does NOT use groups and
   still needs its headings. Scoping to `.cgroup-body` is what lets one partial
   serve both. `.assess-body` keeps its own spacing -- only the heading goes. */
.cgroup-body > .assess-block > .assess-title { display: none; }
.cgroup-body > .assess-block > .assess-body { margin-top: 0; }
.cgroup-count { flex: 0 0 auto; color: var(--faint); font-weight: 500;
  font-variant-numeric: tabular-nums; }
.cgroup-body { padding: 0 12px 12px; display: flex; flex-direction: column;
  gap: 12px; }
/* `bad` colours the HEADING only -- the rows inside keep their own tone, and a
   red wash over a whole group would make one failure look like twenty.
   `--bad-ink` rather than `--bad` for the text: --bad is the FILL colour and
   its contrast on --panel is measured marginal (see the note at :root). */
.cgroup-bad { border-color: var(--bad); }
.cgroup-ok { border-color: var(--ok, var(--line)); }
.cgroup-bad > .cgroup-head { color: var(--bad-ink); }

/* ---- Interview report shell (Project C) --------------------------------
   Measured at v0.1.1099 the report was 5,299px tall across 10 sections, with
   the recording at ~3,464px and not sticky: a reviewer scrolled past the whole
   assessment to reach the video, and any timestamp click would have scrolled it
   back out of view. The page is now a scrolling main column plus a sticky rail
   that holds the player and the download link.

   The rail is FIRST in the DOM and placed into column 2 explicitly. The plan's
   markup snippet appended it after the main column, but both its own CSS note
   ("on one column the rail simply leads") and its Task 3 test (the rail is
   emitted before the transcript) require DOM-first. Explicit grid placement is
   what keeps the visual order main-left / rail-right regardless.

   Two columns collapse to one under 1100px, where a 340px rail leaves the main
   column too narrow to read; on one column the rail simply leads, which is the
   same reading order the sticky layout gives. */
.report-shell { display: grid; grid-template-columns: minmax(0, 1fr) 340px;
                gap: 24px; align-items: start; }
.report-main { min-width: 0; grid-column: 1; grid-row: 1; }
.report-rail { position: sticky; top: 16px; align-self: start; grid-column: 2; grid-row: 1; }
@media (max-width: 1100px) {
  .report-shell { grid-template-columns: 1fr; }
  .report-main, .report-rail { grid-column: 1; grid-row: auto; }
  .report-rail { position: static; }
}
@media print { .report-shell { display: block; } .report-rail { position: static; } }

/* Jump nav at the head of the Assessment panel. Plain in-page anchors, so it
   works with JavaScript off and needs no active-state tracking. scroll-margin
   on the targets keeps a jumped-to heading clear of the sticky rail's top edge. */
.report-jump { display: flex; flex-wrap: wrap; gap: 6px 14px; margin: 0 0 16px; }
.report-jump a { font-size: var(--text-sm); color: var(--muted); text-decoration: none; }
.report-jump a:hover { color: var(--ink); text-decoration: underline; }
.report-jump a:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
.report-main [id^="rs-"] { scroll-margin-top: 20px; }

/* A timestamp reads as a time, not as a button, until you reach for it. */
.qa-seek { flex: none; padding: 1px 7px; border: 1px solid var(--line);
  border-radius: 999px; background: transparent; cursor: pointer;
  font: inherit; font-size: 12px; font-variant-numeric: tabular-nums;
  color: var(--muted); }
.qa-seek:hover { color: var(--ink); border-color: var(--ink); }
.qa-seek:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
@media print { .qa-seek { border: 0; padding-left: 0; } }

/* ── Reduced motion (WCAG 2.3.3 Animation from Interactions) ──────────────
   UI review (live, 2026-08-01) finding #8: this sheet had ZERO
   prefers-reduced-motion support across ~3,900 lines while carrying 36
   transition / animation declarations, so a user with a vestibular disorder
   had no escape from any of them.

   A blanket near-zero duration rather than an enumerated list, because the
   declarations are spread over the whole sheet and a list would go stale on
   the next component. 0.01ms rather than 0: transitionend / animationend
   still fire, so any script waiting on one does not hang.

   !important is deliberate and is the documented pattern for this block — the
   rules it overrides are ordinary specificity-winning component rules, and
   this must beat all of them. It is scoped to duration and iteration only, so
   it changes NO layout, colour or state; the end state of every transition is
   reached immediately rather than skipped.

   Keep in lock-step with static/candidate.css, which carries the same block.
   Guard: tests/test_layout_brand.py, marker `G20:`. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0s !important;
    animation-delay: 0s !important;
  }
  html { scroll-behavior: auto !important; }
}

/* --- V-4 comparison table + V-5 keyboard triage (roadmap Phase 6, slice 2) ---
   APPENDED, never interleaved: this sheet is edited by several lanes at once
   and a reflow turns a clean merge into a hundred conflicts.

   NO ANIMATION AND NO TRANSITION IN THIS BLOCK, deliberately. The row cursor is
   real focus, so the shared `:focus-visible` rule above (2px solid var(--ring),
   outline-offset 2px) IS the cursor -- nothing here paints a second one, and
   nothing here needs the prefers-reduced-motion block to rescue it. A guard in
   tests/test_compare.py asserts this block stays motionless. */
.compare-wrap {
  /* Wide by nature -- up to compare.MAX_COLUMNS dimension columns. Scroll the
     TABLE, never the page body. */
  overflow-x: auto;
}
.compare-tbl { min-width: 100%; }
.compare-tbl th[scope="col"] {
  /* The header must survive scrolling down a capped-but-long comparison;
     without it the columns stop being labelled halfway down and the table
     stops being a comparison. */
  position: sticky;
  top: 0;
  background: var(--bg);
  z-index: 1;
}
/* The focus ring is drawn by the shared :focus-visible rule. outline-offset is
   negative here ONLY because a table row's ring would otherwise be clipped by
   the scroll container; the width and the colour are untouched. */
.compare-row:focus-visible { outline-offset: -2px; }
.compare-row:focus-visible td,
.compare-row:focus-visible th { background: var(--accent-tint); }

/* ---- The cells carry visual encoding (2026-08-02) --------------------------
   Every score cell was a bare `<td class="ta-r tnum">4/5</td>`: no colour, no
   bar, nothing to read across a row or down a column without reading each
   number one at a time. A comparison table is the one surface where visual
   encoding earns its keep, and this one had none.

   A SEGMENTED METER, NOT A TINT AND NOT A COLOURED PILL. Two reasons, both
   from .planning/UI-CONTRACT.md. A per-dimension mark is an INDEX -- a raw
   number on a stated scale -- so it keeps its number and its `/5` (rule 3) and
   must not grow a `.score-pill`, which is the JUDGEMENT component. And rule 2
   says a judgement is never coloured on a template's own authority: there is no
   band function for a 1-5 dimension mark, so any red/amber/green here would be
   this stylesheet inventing a verdict. A meter restates the number that is
   already there and claims nothing new.

   One segment per point of `compare.SCALE_MAX`, filled up to the mark, so the
   scale is legible from the cell itself and nothing computes a percentage. The
   meter is aria-hidden: the number beside it is the accessible value, and a
   screen reader reading five spans would be noise. */
.cmp-meter { display: flex; gap: 2px; justify-content: flex-end; margin-top: 3px; }
.cmp-seg { width: 8px; height: 4px; border-radius: 1px; background: var(--line); }
.cmp-seg.on { background: var(--accent); }
/* THE IDENTITY COLUMNS STAY IN VIEW. Measured on the deployed build: 13
   columns, `.compare-wrap` clientWidth 1053 against scrollWidth 1657 -- 604px
   of columns off-screen with no affordance at all. Scrolling to the twelfth
   dimension used to take the candidate's name off the screen with it, which
   makes a comparison table unreadable exactly when it is doing its job.
   Opaque backgrounds are load-bearing: without them the scrolled columns show
   through underneath. */
.compare-tbl td:first-child,
.compare-tbl th:first-child { position: sticky; left: 0; background: var(--bg); z-index: 1; }
.compare-tbl th[scope="row"] { position: sticky; left: 46px; background: var(--bg); z-index: 1; }
.compare-tbl thead th:first-child,
.compare-tbl thead th:nth-child(2) { z-index: 2; }
.compare-row:focus-visible td:first-child,
.compare-row:focus-visible th[scope="row"] { background: var(--accent-tint); }
/* CR-23: the audit reference beside the name. It is a CROSS-REFERENCE, not an
   identity -- `match_audit` and the erasure evidence rows key on it -- so it
   rides at the muted weight `.blind-ref` already defines and takes one step of
   the spacing scale to sit off the name. No new colour and no new size: a
   second identity styled like the first is what would make the name harder to
   read, which is the whole complaint CR-23 came from. */
.compare-tbl .compare-ref { margin-left: 4px; }

/* `.cost-preview` AND ITS TWO STATE RULES STOOD HERE (Phase 23) and are deleted
   with `templates/_cost_preview.html` (owner ruling 2026-09-01).

   ITS BUG IS THE PART WORTH CARRYING FORWARD: the neutral state was declared
   `background: var(--panel-2)`, and `--panel-2` was never declared in either
   sheet -- so the fallback always fired, the box was #f6f7f9 in BOTH themes, and
   in dark theme its inherited `--ink` sat on it at 1.14:1. Invisible, and only
   on the NEUTRAL state, because `.warn` and `.error` set their own colour -- so
   the broken one was the normal case. `check_tokens.py` 8.11 now fails on any
   `var()` that cannot resolve, which is what stops the next one. */

/* CR-27: scorecard group section rows; sub-rows indent under their group. */
.score-group-row td { background: var(--neutral-tint); padding-top: 8px; }
.score-sub { padding-left: 16px; }

/* ---- UX-109: the Talent bank's four CV-fact facet controls ---------------
   Owner 2026-08-10. `All` / `Role` / `Status` / `Score ↓ Recent` / `Search` left
   the bar; Domain, Seniority, Years of experience and Work authorization took
   their place as `details.popover.menu-end` triggers.

   APPENDED, NOT SORTED IN. The `layout-heights` lane owns `.wt-*`, `.vocab-list`
   and `.cand-*` in this file concurrently, and a re-sort turns a clean merge into
   a hundred conflicts (CLAUDE.md, branches).

   THREE RULES, AND THE SMALLNESS IS THE POINT. Everything structural comes from
   primitives that were already measured: `.popover` supplies the body box, the
   elevation and the dismissal behaviour; `.menu-end` supplies the fixed,
   viewport-clamped placement; `.pill-select` supplies the trigger's fill, border,
   radius, weight and its chevron background-image. What is scoped here is only
   what the HEADER container needs differently, which is the same thing
   `.v-poolfilter select, .v-fbfilter select` above already does for the selects
   that used to sit in this row: the shared control height and the header's
   squarer radius.

   WHY THE SELECTOR IS THIS LONG. `details.popover > summary:not(.row-menu-btn)`
   is (0,2,2) and sets `display: inline-block`, `color: var(--accent)` and
   `font-size: var(--text-sm)` -- so a bare `.pill-select` (0,1,0) would lose the
   geometry to it and the trigger would render as an accent-coloured text link
   with a pill's padding. Scoped to the container rather than counter-ruled with
   `!important` (layout rule 5: ask which container a rule was written for).

   IT SPENDS NOTHING OFF THE 4px SCALE, AND THAT IS NOT WHAT IT FIRST DID. The
   obvious inset is `.v-fbfilter select`'s own `0 30px 0 13px`, reproduced so the
   chevron `.pill-select` paints at `right 12px center` is not drawn under the
   label -- the exact defect that rule above records. Both numbers are OFF the
   spacing scale, and 8.13's off-scale ratchet may only FALL, so copying them
   would have grown the debt by two to match a neighbour by 1px. `var(--s6)` /
   `var(--s3)` (32/12) clear the same 10px chevron with the same visual result;
   the trigger and the /pool filter selects differ by one pixel of left inset and
   by two of right, and no measurement can tell. No drift primitive either:
   nothing here is `space-between` or an auto margin. */
.v-hdctrls details.popover > summary.pill-select {
  display: inline-flex; align-items: center; height: var(--v-ctrl-h);
  padding: 0 var(--s6) 0 var(--s3); border-radius: var(--r-md);
  color: var(--ink); font-size: var(--text-sm); white-space: nowrap;
}
.v-hdctrls details.popover[open] > summary.pill-select { border-color: var(--accent); }
/* The body holds ONE `.set-row.set-row-wide.filter-group` and its Apply button.
   `.set-row`'s own `padding: var(--s3) 0` would put the group's title against the
   popover's top inset, which already pays `var(--s4)`; the block below spends the
   gap once, on the button, so the fieldset can keep the shared primitive
   untouched. IT MUST NOT SCROLL: the global popover script listens for `scroll`
   in the CAPTURE phase to close a fixed-position menu that would otherwise detach
   from its trigger, and a scrolling body would fire that on itself. Four short
   closed lists is the constraint that keeps that true. */
.tb-facet .set-row { padding-top: 0; }
.tb-facet-apply { margin-top: var(--s2); }


/* ── Account calendar: the availability PAINT GRID ────────────────────────
   Replaces 28 bare `<input type=time>` fields on `/account/calendar`. The
   owner's report of the form it replaces was "no one knows what's happening from
   the page": seven rows of four time fields state a week as 28 unrelated
   numbers, and a week is a SHAPE. Drawing it is the whole design.

   THE CELLS ARE REAL CHECKBOXES and this block never hides that fact behind a
   div. The input is taken out of flow the same way `.segmented` does it, the
   `<label>` beside it is the painted square, and `:checked + .cal-slot` is what
   colours it -- so the editor works with JavaScript off, every cell is focusable
   and toggleable from the keyboard, and the selected state is a real `checked`
   attribute rather than a class only a script understands. `calendar_grid.js`
   adds drag-painting on top and is pure enhancement.

   GRIDLINES ARE CELL BORDERS, NOT A `gap`. The handoff draws them as a 1px grid
   `gap` over a coloured container, which is a fine prototype and the wrong
   choice here for two reasons: a 1px gap is off the 4px spacing scale that arm
   8.13 ratchets, and fractional column widths make a gap-drawn line land on
   half-pixels at some viewport widths while a border never does. Same 1px line,
   same token, no ratchet cost.

   THE COLUMN COUNT IS PAIRED WITH `scheduling.GRID_DAYS` -- Mon-Fri, owner
   ruling. Two places state it and they must agree, so
   `tests/test_ui_alignment.py` asserts the pair rather than trusting the
   comment. */
.cal-head { align-items: center; padding-bottom: var(--s5);
  border-bottom: 1px solid var(--line-soft); }
.cal-head h1 { font-size: 21px; font-weight: 650; letter-spacing: -.01em; }
.cal-actions { display: inline-flex; align-items: center; gap: var(--s3); }
.cal-band { margin-top: var(--s5); }
/* A GRID, not `justify-content: space-between`: the figure aligns to a TRACK
   rather than to its own content width, which is the distinction CLAUDE.md's
   layout rule 2 is about and the reason arm 8.13 counts the alternative. */
.cal-band-head { display: grid; grid-template-columns: minmax(0, 1fr) max-content;
  align-items: end; gap: var(--s5); }
.cal-band-head h2 { margin: 0; font-size: 16px; font-weight: 600;
  letter-spacing: -.01em; }
/* The page's largest number, and the reason the redesign exists: one legible
   figure instead of a fact the reader has to add up from 28 fields. */
.cal-figure { text-align: right; line-height: 1.1; }
.cal-figure-value { display: block; font-size: var(--text-2xl); font-weight: 650;
  letter-spacing: -.025em; color: var(--ink); font-variant-numeric: tabular-nums; }
.cal-figure-caption { display: block; font-size: var(--text-xs);
  color: var(--faint); margin-top: var(--s1); }

.cal-grid-wrap { margin-top: var(--s4); }
.cal-grid-head, .cal-grid { display: grid;
  grid-template-columns: var(--cal-gutter) repeat(5, minmax(0, 1fr)); }
.cal-day { font-size: 11.5px; font-weight: 600; letter-spacing: .02em;
  color: var(--ink); text-align: center; padding-bottom: var(--s2); }
/* `user-select: none` on the GRID and not on the band: a drag that begins on a
   cell must not select the heading text above it, and the heading is still
   selectable text a reader may want to copy. */
.cal-grid { border: 1px solid var(--line); border-radius: var(--r-lg);
  overflow: hidden; background: var(--field);
  -webkit-user-select: none; user-select: none; }
.cal-time { height: var(--cal-row-h); line-height: var(--cal-row-h);
  background: var(--field); font-size: 10.5px; color: var(--faint);
  text-align: right; padding-right: var(--s2);
  font-variant-numeric: tabular-nums;
  border-right: 1px solid var(--line-soft);
  border-bottom: 1px solid var(--line-soft); }
/* Out of flow exactly as `.segmented`'s radios are, so an absolutely positioned
   input never claims a grid track. */
.cal-check { position: absolute; opacity: 0; width: 0; height: 0; }
.cal-slot { display: block; height: var(--cal-row-h); margin: 0;
  background: var(--field); cursor: pointer;
  border-right: 1px solid var(--line-soft);
  border-bottom: 1px solid var(--line-soft); }
.cal-check:checked + .cal-slot { background: var(--accent-tint); }
/* An INSET ring: `.cal-grid` clips its children, so an outward outline on a cell
   in the first or last column would be cut in half by `overflow: hidden`. */
.cal-check:focus-visible + .cal-slot { box-shadow: inset 0 0 0 2px var(--ring); }
.cal-grid .cal-time:last-of-type, .cal-grid .cal-slot:last-of-type { border-bottom: 0; }

.cal-foot { display: grid; align-items: center; gap: var(--s5);
  grid-template-columns: max-content max-content minmax(0, 1fr);
  margin-top: var(--s4); padding-top: var(--s4);
  border-top: 1px solid var(--line-soft); }
.cal-legend { display: inline-flex; align-items: center; gap: var(--s2);
  font-size: 12.5px; color: var(--muted); white-space: nowrap; }
.cal-swatch { width: 12px; height: 12px; border-radius: 3px;
  background: var(--accent-tint); border: 1px solid var(--accent-edge); }
.cal-interval { display: inline-flex; align-items: center; gap: var(--s2);
  font-size: 12.5px; color: var(--faint); white-space: nowrap; }
.cal-interval .segmented label { font-variant-numeric: tabular-nums; }
/* `justify-self`, not `margin-left: auto`. Same right edge, and it comes from the
   row's own track -- which is the property arm 8.13's drift counter exists to
   protect. */
.cal-save { justify-self: end; }
@media (max-width: 760px) {
  .cal-foot { grid-template-columns: minmax(0, 1fr); }
  .cal-save { justify-self: start; }
}
/* TOUCH IS A STATED PARTIAL, not a solved case. The handoff asks for a separate
   interval-list editor on touch viewports; this is not that. A 19px row is far
   under the 44px target minimum, so a coarse pointer gets a row tall enough to
   hit deliberately -- 32px, still short of 44 -- and the grid stays the same
   grid. An interviewer setting their week from a phone is the case this does not
   yet serve properly. */
@media (pointer: coarse) {
  .cal-grid, .cal-grid-head { --cal-row-h: 32px; }
}

/* The catch-all rows: no rail, because they belong to no heading. */
.evi-rows-loose { margin-top: 16px; }
.evi-rows-loose > .hint { margin-bottom: 8px; }

/* ===========================================================================
   THE SCHEDULING HUB (owner handoff `design_handoff_scheduling_hub`, 1a)
   ===========================================================================
   A1-A5's layout. Read CLAUDE.md's layout contract before editing any of it:
   every alignment defect this repo has had was a layout decision made at ONE
   CALL SITE instead of in a shared component, so the grids here live on the ROW
   and are declared ONCE per row family.

   SPACING IS `var(--sN)`, NEVER A LITERAL px. check_tokens 8.13's
   `offscale_spacing_max` is at 634 with ZERO slack and may only FALL, so the
   handoff's 13/18/26/34px margins would fail the commit that added them. Sizes,
   radii, font sizes and grid tracks are literal and exact -- the ratchet counts
   margin/padding/gap only, and those are the declarations that cause drift.
   Recorded in `.planning/briefs/scheduling-hub.md`, ruling 2.

   COLOURS ARE TOKENS, NEVER THE HANDOFF'S RAW HEX. Most of its palette already
   IS the token values; where its danger ramp differs from --bad/--bad-tint/
   --bad-ink the token wins, because a raw hex has no dark-mode value and this
   app has a dark theme. The four genuinely NEW colour roles below (the calendar
   fills) are declared as tokens in BOTH palettes at the head of this sheet.
   Ruling 3 of the same brief.

   NO `space-between`, NO `margin-left: auto`, NO `margin: ... auto`. Those are
   8.13's three drift primitives, at 52 with zero slack, and each aligns a row to
   its OWN content width instead of to a shared column -- the named cause of
   every drift defect here. Right edges come from a grid track. */

/* `.sch-journey` IS DELETED (owner, 2026-09-07): the journey strip is retired.
   THE SHAPE IS RECORDED BECAUSE THE NEXT MULTI-CELL READOUT NEEDS IT. The strip
   was `repeat(4, minmax(0, 1fr))` on the CONTAINER, so its four cells were four
   grid ITEMS of one grid and their widths agreed by construction rather than by
   four coincidences -- layout rule 2, and the per-call-site sizing this
   stylesheet exists to stop. `repeat(4, ...)` and not `auto-fit`, because four
   was the number of facts and a strip that reflowed to three-plus-one would put
   the verdict on a line of its own; the `minmax(0, ...)` mattered because
   without the 0 a long address refuses to shrink and pushes the fourth cell off
   the strip. The divider was `border-right` PER CELL with the last cleared,
   since a `gap` over the strip's own fill shows the background rather than a
   hairline. And the current cell was painted from `aria-current="step"` ITSELF
   -- never a companion class, so the paint and the announcement were one
   decision that could not drift. Its browser block was `SCH JOURNEY` in
   tests/test_ui_alignment.py.

   `.sch-lede`'S TOMBSTONE CAME WITH IT AND IS RESTATED, because it is a rule
   about copy rather than about the strip: a 76ch paragraph under the h1 on
   every visit restated what the page below already showed, and a page that says
   the same thing twice is one you read instead of using. That is why the lede
   is not coming back either -- and it is now the only surviving half of that
   pair, the strip having been the other. */

/* ---- A5: the first run ------------------------------------------------- */
/* THE FIRST-RUN CARD IS A `.card` WITH A NARROWER MEASURE, centred. The
   handoff draws it as a 660px column on white; the card supplies the surface and
   this supplies the column. `margin-inline: auto` is NOT one of 8.13's drift
   primitives -- those are about a row aligning to its own CONTENT width inside a
   shared grid, and this is a block centring itself in a fluid parent, which has
   no siblings to fall out of line with. */
/* A5 IS GONE WHOLE (owner, 2026-09-04). It shed `.sch-steps` and
   `.sch-firstrun-sub` on 2026-09-03, leaving a heading and a foot; the heading
   and the foot followed them, because the journey strip stated the same facts
   on every tab in every state and this card only ever appeared while the
   workspace was broken. The rules are deleted rather than orphaned -- an unused
   selector is an invitation to re-add the markup.

   THAT ARGUMENT IS SPENT: the strip is retired too (2026-09-07). The facts are
   stated by the Setup tab's roles TABLE now, per role, with `blocker_for`'s
   reason and fix in `.sch-verdict`. Kept in the past tense rather than
   corrected away, because the reason the card is not coming back is unchanged:
   it was the steady state's answer rendered a second time. */
/* `.sch-step*` IS DELETED WITH IT (2026-09-07) -- the row grid, the numeral
   pill, its three states (later outlined, current filled, done in `--ok-tint`),
   the copy column and the action slot. Nine rules, all of them scoped to
   `.sch-journey` as their only container, which is why they go together: the
   strip's own note said the track list and the padding came from the parent, so
   these could never have been reused anywhere else. */

/* ---- the shared table header ------------------------------------------ */
/* `.sch-banner*` IS RETIRED (2026-09-03 handoff) AND NOTHING REPLACES IT. It
   rendered every blocked role as a sentence with a fix button, and the table
   below rendered the same role again as `No` plus the same sentence -- the same
   fault stated twice, with the fix in the copy rather than in the row. A blocked
   role is now listed ONCE, in the roles table, with its fix in the row.

   ONE HEADER FOR THREE TABLES -- roles, types and bookings. It carries no track
   list of its own: each table's own class supplies the tracks and this element
   wears BOTH, so the header and the rows cannot drift apart. That is layout rule
   2 with the shared component being the header itself.

   THE HORIZONTAL PADDING MUST MATCH THE ROWS' (`var(--s4)`) or every column
   heading sits a few pixels off the cells beneath it. The vertical padding is
   deliberately smaller: a header is a label strip, not a row. */
.sch-thead { padding: var(--s2) var(--s4); background: var(--canvas);
  border-top: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft);
  font-size: 10.5px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--faint); }

/* ---- A1: the two cards ------------------------------------------------ */
/* `.card` WITH THE PADDING MOVED INSIDE. Not a new card shape -- the bands
   within it (head, header strip, rows) each own their own padding, because the
   header strip is full-bleed and a padded parent would inset it. Deliberately
   NOT named `*-card`: check_tokens 8.5 keeps a may-not-grow register of card
   CLASSES, and a padding modifier is not a new card. */
.sch-flush { padding: 0; overflow: hidden; }
/* `.sch-flush-head`, `.sch-flush-copy h2` AND `.sch-flush-link` ARE DELETED
   (owner, 2026-09-05) with the Setup tab's "Your open roles" band, which was
   their only caller -- the two-track head, its heading size and the small link
   at the right edge. A rule with no call site is a surface a later reader
   believes exists, which is why `.sch-types*` went the same way the day before.

   `.sch-flush` ITSELF STAYS and is still used by BOTH tables: it is the
   padding-zero card that lets a full-bleed `.sch-thead` reach the card's edges,
   and with the band gone that header strip is simply the first child. */

/* THE COLUMN TEMPLATE, DECLARED ONCE AND USED BY THE HEADER AND EVERY ROW.
   This is layout rule 2 -- the grid lives on the row, never per item -- and it is
   what makes the header labels line up with the cells beneath them by
   construction rather than by two numbers somebody kept in step. */
/* FIVE TRACKS FROM 2026-09-05 (owner), and the two new ones are NUMBERS so they
   are fixed rather than intrinsic. It was three: the 190px read-only interviewer
   column went on 2026-09-04 when the select started naming the person, and then
   the candidate count left the role cell for a column of its own and `Times
   offered` stopped doubling as the verdict's reason text.

   EVERY NEW TRACK IS A FIXED NUMBER, AND `max-content` WAS TRIED AND REFUSED BY
   A MEASUREMENT -- for the SECOND time in two days. The header strip and the
   rows are SEPARATE grid containers sharing this one declaration, so an
   intrinsic track resolves PER CONTAINER: `max-content` on the verdict column
   measured 140px under the heading "Can candidates book?" and 53px under a row's
   bare chip, and the two `1fr` tracks beside it then divided different
   remainders -- 92px of spread and thirty headings off their cells. `SCH TABLES`
   caught it, exactly as it caught the identical 68px defect on the bookings
   action track the day before.

   I WROTE THAT WARNING INTO THIS COMMENT AND THEN SHIPPED THE MISTAKE ANYWAY,
   which is worth recording: the first draft of this rule said `max-content` was
   "safe for the verdict only because both its values are a chip of
   near-identical width" -- true of the ROWS and irrelevant, because the widest
   thing in that column is the HEADING. A track is sized by the widest cell in
   its own container, and the header is a container.

   160px FITS "CAN CANDIDATES BOOK?" at `.sch-thead`'s 10.5px uppercase with its
   letter-spacing. A number column sized intrinsically has the same fault plus
   one of its own: it would be as wide as its widest value, so a workspace whose
   counts reach three digits would shift every column beside it. */
.sch-roles { display: grid;
  grid-template-columns: minmax(0, 1.15fr) 96px 250px 160px 128px;
  gap: var(--s4); align-items: center; }
/* A NUMBER COLUMN READS DOWN, WHICH IS THE ONLY REASON IT IS A COLUMN. Tabular
   numerals so digits align, and the heading is right-aligned with the cells so
   the label sits over the value rather than over the empty left side of the
   track. */
.sch-num, .sch-num-h { text-align: right; font-variant-numeric: tabular-nums; }
.sch-num { font-size: 13px; color: var(--ink); }
.sch-offered { display: flex; justify-content: flex-end; align-items: center;
  gap: var(--s2); min-width: 0; }
/* `.sch-roles-head` KEEPS ITS NAME AND LOST ITS STYLING to `.sch-thead`, which
   the element now also wears. The name survives because it is what marks THIS
   table's header among three that share one treatment; its only remaining rule
   is the stacking breakpoint at the foot of this block. */
/* ONE ROW CHROME FOR ALL THREE TABLES. The handoff gives types and bookings
   `var(--s2)` of vertical padding against the roles table's `var(--s3)`; they
   share one value instead, because two row paddings is two numbers to keep in
   step for 8px of density, and the horizontal value is load-bearing (it is what
   `.sch-thead` matches). */
.sch-row { margin: 0; padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--line-soft); }
.sch-row:last-child { border-bottom: 0; }
.sch-row:hover { background: var(--hover); }
.sch-role { display: flex; flex-direction: column; min-width: 0; }
.sch-role-t { font-size: 13.5px; font-weight: 600; color: var(--ink); }
.sch-role-d { font-size: 12px; color: var(--faint); }
.sch-type { display: flex; align-items: center; gap: var(--s2); min-width: 0; }
.sch-select { width: 100%; min-width: 0; font-size: 13px; }
/* AN EMPTY SELECT IS DRAWN AS UNFINISHED -- a stronger border and muted text --
   because "nothing attached yet" is the commonest reason a role is not bookable
   and a select that looks the same either way hides it. */
.sch-select.is-empty { border-color: var(--line-input-empty); color: var(--faint); }
/* THE NO-SCRIPT SUBMIT. Hidden here rather than in the markup, and only once the
   script has CLAIMED the form -- so with scripting off it is a visible, working
   button. It stays keyboard-reachable while hidden from pointer users would be a
   lie, so this really removes it: the script's own change handler is the
   replacement. */
.sch-row[data-sch-claimed] .sch-attach-go { display: none; }
.sch-who { display: flex; align-items: center; gap: var(--s2); min-width: 0; }
.sch-who-n { font-size: 13px; color: var(--ink); overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
/* THE AVATAR IS INITIALS ON A TINT, never an image: this product stores no
   photographs of anybody and a broken <img> is worse than two letters. */
.sch-avatar { display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 auto; width: 24px; height: 24px; border-radius: 999px;
  background: var(--accent-tint); color: var(--accent-d);
  font-size: 11px; font-weight: 600; }
/* ONE TRACK FROM 2026-09-05 (owner: the column is only a Yes/No label). It was
   three -- chip, short reason, fix -- and `.sch-verdict-d` / `.sch-verdict-fix`
   are deleted with the two that went: the reason is the chip's `title` now and
   the fix moved to the `Times offered` cell.

   IT STAYS A GRID rather than becoming a bare span, because `max-content` on one
   track is what pins the chip to the column's own left edge. A `justify-self` or
   an `inline-flex` would centre it on its own width, so Yes and No -- two chips
   of different widths -- would start in different places down the column, which
   is the per-item alignment layout rule 2 forbids.

   THE ELLIPSIS RULE WENT WITH THE REASON TRACK. It mattered because a wrapping
   sentence made rows different heights; a chip cannot wrap. */
.sch-verdict { display: grid; min-width: 0; align-items: center;
  grid-template-columns: max-content; }
/* THE NUDGE FORM IS A SIBLING OF THE ROW, not a child, because a form cannot
   contain a form -- the row IS the attach form. It holds only hidden inputs, so
   it paints nothing; this keeps it from contributing a row of margin between two
   table rows. */
.sch-nudge { margin: 0; }

/* ---- A1: THE INTERVIEW TYPES TABLE IS DELETED ------------------------- */
/* `.sch-types`, `.is-off`, `-h`, `-n`, `-d`, `-why` and `-act` went with
   `event_types` itself (owner ruling 2026-09-04, *"There is ONE interview"*) --
   the card, its four tracks and the quieter treatment for an inactive type. They
   outlived the table they styled by a few hours; a rule with no call site is a
   surface a later reader believes exists.

   THE TRACK LESSON IS THE ONE THAT GENERALISES and it survives on `.sch-bkt`
   above: `justify-content: flex-end` inside a `max-content` track agrees with
   the right edge of its own row's content and therefore with nothing, which is
   why an action track is either fixed or exactly its contents' width. */

/* ---- A2: the drawer --------------------------------------------------- */
/* A MODIFIER ON THE EXISTING `.drawer`, not a second drawer primitive. What it
   changes: a fixed 660px instead of 65vw (the handoff's width, and this drawer
   holds a two-column field grid that reflows badly on a fluid track), and a flex
   column so the head and foot stay put while the body scrolls. */
.drawer.sch-drawer { width: 660px; min-width: min(660px, 96vw); padding: 0;
  overflow: hidden; display: flex; flex-direction: column; }
.sch-drawer-form { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }
.sch-drawer-head { display: grid; grid-template-columns: minmax(0, 1fr) max-content;
  gap: var(--s3); align-items: start; padding: var(--s5) var(--s5) var(--s4);
  border-bottom: 1px solid var(--line-soft); }
.sch-drawer-titles { display: flex; flex-direction: column; min-width: 0; }
/* THE NAME EDITS IN PLACE. A borderless input that takes the title's own type
   scale, bordered only on hover and focus -- so it reads as the heading it is
   until you touch it. `width: 100%` inside a `minmax(0,1fr)` track is what keeps
   a long name from widening the drawer. */
.sch-drawer-title { width: 100%; border: 1px solid transparent; background: none;
  padding: var(--s1); border-radius: 8px;
  font-size: 17px; font-weight: 650; letter-spacing: -.012em; color: var(--ink); }
.sch-drawer-title:hover { border-color: var(--line); }
.sch-drawer-title:focus { border-color: var(--ring); background: var(--panel); }
/* THE STATUS PILL SITS IN THE HEAD'S `max-content` TRACK, so a long name in
   the track beside it shortens the name rather than pushing the pill out of the
   drawer. It carries no colour of its own: the script writes a `.chip` variant
   into it, which is the same three-tint vocabulary the rest of the page uses. */
.sch-drawer-pill { display: inline-flex; align-items: center; }
.sch-drawer-sub { font-size: 12.5px; color: var(--faint); padding-left: var(--s1); }
/* THE BODY IS THE ONLY SCROLLER. `min-height: 0` on a flex child is what actually
   permits it to shrink below its content -- without it the body grows and the
   footer leaves the viewport, which is the defect the handoff calls out
   ("children must not shrink"). */
.sch-drawer-body { flex: 1 1 auto; min-height: 0; overflow-y: auto;
  display: flex; flex-direction: column; gap: var(--s4);
  padding: var(--s4) var(--s5) var(--s5); }
.sch-drawer-foot { display: grid; grid-template-columns: minmax(0, 1fr) max-content;
  gap: var(--s3); align-items: center; padding: var(--s3) var(--s5);
  border-top: 1px solid var(--line-soft); background: var(--canvas); }
.sch-drawer-acts { display: inline-flex; align-items: center; gap: var(--s2); }
.sch-off { display: inline-flex; align-items: center; gap: var(--s2);
  font-size: 12.5px; color: var(--bad-ink); cursor: pointer; }

/* the three questions */
.sch-q { display: flex; flex-direction: column; gap: var(--s2); }
.sch-q2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s4); }
.sch-q-col { display: flex; flex-direction: column; gap: var(--s2); min-width: 0; }
.sch-q-l { font-size: 13px; font-weight: 600; color: var(--ink); }
.sch-seg { align-self: start; }
.sch-other { display: inline-flex; align-items: center; gap: var(--s2);
  font-size: 12.5px; color: var(--muted); }
.sch-person { display: grid;
  grid-template-columns: max-content minmax(0, 1fr) max-content;
  gap: var(--s2); align-items: center;
  border: 1px solid var(--line); border-radius: 9px; padding: var(--s2) var(--s3); }
.sch-person-copy { display: flex; flex-direction: column; min-width: 0; }
.sch-person-n { font-size: 13.5px; font-weight: 600; color: var(--ink); }
/* THE STATE LINE IS GREEN WHEN READY AND RED WHEN NOT, and both are the SAME
   computation with a different sign (`scheduling.interviewer_card`). A line that
   could only be green would be the reassurance this whole redesign removes. */
.sch-person-d { font-size: 12px; color: var(--ok-ink); line-height: 1.45; }
.sch-person[data-blocked] .sch-person-d { color: var(--bad-ink); }
.sch-person-sel { font-size: 12.5px; max-width: 200px; }

/* Advanced: `<details>`, the native disclosure. Permitted HERE because the
   no-disclosure ruling is about the SETTINGS PANE, not the product. */
.sch-adv { border: 1px solid var(--line); border-radius: 10px; }
.sch-adv-sum { display: grid; grid-template-columns: minmax(0, 1fr) max-content;
  gap: var(--s3); align-items: center; padding: var(--s3);
  background: var(--canvas); border-radius: 10px; cursor: pointer;
  list-style: none; }
.sch-adv[open] .sch-adv-sum { border-radius: 10px 10px 0 0; }
.sch-adv-sum::-webkit-details-marker { display: none; }
.sch-adv-copy { display: flex; flex-direction: column; min-width: 0; }
.sch-adv-t { font-size: 13px; font-weight: 600; color: var(--ink); }
.sch-adv-d { font-size: 12px; color: var(--faint); }
.sch-adv-hint { font-size: 12.5px; color: var(--muted); }
.sch-adv-body { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s3) var(--s4); padding: var(--s4);
  border-top: 1px solid var(--line-soft); }
/* SPACE-BETWEEN IS NOT USED HERE, deliberately -- it is one of 8.13's three
   drift primitives. The label/value pair is a two-track grid so every row in the
   block shares one value column instead of each aligning to its own text. */
.sch-adv-item { display: grid; grid-template-columns: minmax(0, 1fr) max-content;
  gap: var(--s2); align-items: center; font-size: 12.5px; color: var(--ink); }

/* the live candidate preview */
.sch-preview { border: 1px solid var(--accent-edge); background: var(--accent-tint);
  border-radius: 10px; padding: var(--s3) var(--s4);
  display: flex; flex-direction: column; gap: var(--s2); }
.sch-preview-head { display: grid; grid-template-columns: minmax(0, 1fr) max-content;
  gap: var(--s3); align-items: baseline; }
.sch-preview-t { font-size: 12.5px; font-weight: 600; color: var(--accent-d); }
.sch-preview-sum { font-size: 13px; color: var(--ink); line-height: 1.5; }
.sch-preview-why { font-size: 12.5px; color: var(--bad-ink); line-height: 1.5; }
.sch-preview-chips { display: flex; flex-wrap: wrap; gap: var(--s2); }
.sch-chip { border: 1px solid var(--accent-edge); background: var(--panel);
  border-radius: 7px; padding: var(--s1) var(--s2); font-size: 12.5px;
  color: var(--ink); font-variant-numeric: tabular-nums; }
.sch-chip.is-more { border-style: dashed; background: transparent; color: var(--accent-d); }
/* A QUIET SKELETON WHILE IN FLIGHT, never a spinner over the form -- the
   handoff's own instruction, and the difference between a preview and an
   interruption. */
.sch-preview.is-loading .sch-preview-chips { opacity: .45; }

/* ---- A3: the page-head actions --------------------------------------- */
/* `.sch-conn*` IS RETIRED (2026-09-03 handoff). A tinted band with two sentences
   on every load stated ONE of the four facts the journey strip now states as
   four; its three states are journey cell 1 and its FACTS are the `.sch-fact`
   rows in the rail.

   ITS BUTTONS COULD NOT GO IN THE OUTCOME CARD, and the reason is HTML rather
   than taste: the rail is inside the hours `<form>` and a form cannot contain a
   form -- the browser drops the inner one silently, which would have left
   Connect and Disconnect as dead controls. They are in the page head, which is
   where this product already puts a page-level action, is outside the form, and
   keeps both as real POSTs with scripting off. `Use the suggestion` joins them,
   having moved off the admin-gated Setup tab: it writes the reader's OWN hours,
   and an interviewer who is not a workspace admin could never reach it there. */
.sch-headacts { display: flex; align-items: center; gap: var(--s2); }

/* ---- A3: the week and its rail ---------------------------------------- */
/* TWO COLUMNS, AND THE RAIL IS FIXED. The week must be free to take whatever is
   left, because its own tracks are `repeat(5, minmax(0,1fr))` -- a fluid rail
   would make the day columns depend on the outcome number's digit count. */
.sch-hours { display: grid; grid-template-columns: minmax(0, 1fr) 300px;
  gap: var(--s5); align-items: start; margin-top: var(--s4); }
.sch-week { display: flex; flex-direction: column; gap: var(--s3); min-width: 0; }
/* `.sch-week-bar`, its `h2` and `.sch-week-nav` ARE DELETED (owner, 2026-09-04)
   with the week stepper they laid out. `.sch-week` itself STAYS: it is the column
   that holds the grid and the legend, and it is still doing that job. */
/* ONE WARNING CLASS, TWO CALL SITES, AND THE NAME NO LONGER CARRIES A PLACE.
   It was `.sch-week-warn` while both warnings sat over the grid; the
   busy-unknown one now renders in the RAIL, beside the Outlook fact it is
   about, and the far-week one stays over the week because it is a fact about
   the week somebody navigated to. A class named for a location it no longer
   only occupies is how the next reader is misled. */
.sch-warn { margin: 0; }
.sch-grid-wrap { min-width: 0; }
/* THE HEADER AND THE BODY SHARE ONE TEMPLATE, which is the only thing that keeps
   a day label above its own column. 58px is the handoff's gutter. */
/* ONE NAMED CLASS OWNS THE TRACKS, and the header and the body both carry it.
   This was two selectors sharing one declaration, which is two places a track
   list can drift -- and a drifted track list is a day label sitting above the
   wrong column. 58px is the handoff's gutter. */
.sch-grid-cols { display: grid;
  grid-template-columns: 58px repeat(5, minmax(0, 1fr)); }
.sch-grid-head { padding-bottom: var(--s1); }
.sch-day { font-size: 11.5px; font-weight: 600; text-align: center; color: var(--ink); }
.sch-day.is-past { color: var(--faint); }
.sch-grid { border: 1px solid var(--line); border-radius: 10px; overflow: hidden;
  user-select: none; }
.sch-time { height: 21px; line-height: 21px; text-align: right;
  padding-right: var(--s2); font-size: 10.5px; color: var(--faint);
  font-variant-numeric: tabular-nums;
  border-right: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); }
/* ONE CELL, TWO STATES, AND THE FILL IS THE LEGEND. `scheduling.hours_grid`
   decides which state a cell is in and this is the only place a state becomes a
   colour -- so the swatch below and the grid above cannot disagree.

   THERE WERE FIVE (owner ruling 2026-09-04). `past`, `busy_out` and the `busy_in`
   HATCH are deleted with the week and the busy layer they described. The hatch is
   the one worth a note: it was the only fill that was not a flat colour, and
   deliberately so, because it meant "you published this hour and you are not free
   in it" -- neither of its neighbours, and it must not read as a shade of either.
   If busy is ever drawn on a dated surface again, that is why it needs its own
   treatment rather than a third tint. */
.sch-slot { display: block; height: 21px; margin: 0; cursor: pointer;
  border-right: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); }
.sch-slot[data-cal-state="free"] { background: var(--panel); }
.sch-slot[data-cal-state="offered"] { background: var(--cal-offered); }
/* THE CHECKBOX'S OWN STATE STILL HAS TO PAINT, WITH TWO STATES AS WITH FIVE.
   The server computes `data-cal-state`, but a cell the USER just painted has no
   new attribute until the page reloads -- so `:checked` overrides `free` and
   `:not(:checked)` overrides `offered`, which is what makes drag-painting look
   immediate. `calendar_grid.js` is unchanged and never touches the attribute; it
   toggles the checkboxes and recounts, which is why this pair is the whole of
   the interaction's paint. */
.cal-check:checked + .sch-slot[data-cal-state="free"] { background: var(--cal-offered); }
.cal-check:not(:checked) + .sch-slot[data-cal-state="offered"] { background: var(--panel); }
.cal-check:focus-visible + .sch-slot { box-shadow: inset 0 0 0 2px var(--ring); }
.sch-grid .sch-time:last-of-type, .sch-grid .sch-slot:last-of-type { border-bottom: 0; }
.sch-legend { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s3);
  font-size: 12.5px; color: var(--muted); }
.sch-key { display: inline-flex; align-items: center; gap: var(--s2); }
.sch-sw { width: 13px; height: 13px; border-radius: 3px; border: 1px solid var(--line); }
/* ONE SWATCH. The other three named states the grid no longer draws, and a
   swatch for a fill that never appears is a legend entry for nothing. */
.sch-sw[data-cal-state="offered"] { background: var(--cal-offered); }

.sch-rail { display: flex; flex-direction: column; gap: var(--s3); }
.sch-outcome { display: flex; flex-direction: column; gap: var(--s1);
  padding: var(--s4); }
/* THE NUMBER THIS PAGE EXISTS TO MOVE. tabular-nums so it does not jump width as
   it changes, which it does on every save. */
.sch-outcome-n { font-size: 34px; font-weight: 650; letter-spacing: -.03em;
  line-height: 1.05; color: var(--ink); font-variant-numeric: tabular-nums; }
.sch-outcome-d { font-size: 12.5px; color: var(--muted); line-height: 1.5; }
/* `.sch-outcome-f` AND `.sch-outcome-why` ARE FOLDED INTO `.sch-fact`
   (2026-09-03 handoff): the one number that matters was followed by a sentence
   and then a second sentence, where the three things a reader needs are three
   labelled values.

   DELIBERATELY NOT `.set-row`, and the handoff's reason is right: that
   primitive's control track is `var(--set-ctl-track)` (320px) and this rail is
   300px wide, so its geometry has no meaning here. What it exists to ENFORCE
   does: this is a grid ON THE ROW with the value in the right-hand track, so the
   three values agree down the column instead of each ending at its own label.
   `set_row_min` is untouched -- no label/control row was hand-rolled out of the
   primitive, because none of these is a control. */
.sch-fact { display: grid; grid-template-columns: minmax(0, 1fr) max-content;
  gap: var(--s2); align-items: baseline; font-size: 12.5px; color: var(--muted); }
/* THE FIRST FACT CARRIES THE RULE that separates the facts from the number
   above them -- on the ROW rather than as a sibling `<hr>`, which would be a
   fourth child of a flex column and pay the column's gap twice. */
.sch-fact-top { margin-top: var(--s2); padding-top: var(--s2);
  border-top: 1px solid var(--line-soft); }
.sch-fact-v { color: var(--ink); font-weight: 600;
  font-variant-numeric: tabular-nums; }
.sch-fact[data-bad] .sch-fact-v { color: var(--bad-ink); }
/* THE PRIVACY LINE IS NOT A FACT ROW: it has no value in the right-hand track,
   so it would render as a label alone in a two-track grid and read as a fact
   whose value failed to load. */
.sch-fact-note { font-size: 12px; color: var(--faint); line-height: 1.45; }
.sch-interval { display: grid; grid-template-columns: minmax(0, 1fr) max-content;
  gap: var(--s2); align-items: center; padding: var(--s4); }
.sch-interval .segmented label { font-variant-numeric: tabular-nums; }
/* THE TIMEZONE NOTE SPANS BOTH TRACKS. It is a sentence under a row, not a
   third cell -- `grid-column: 1 / -1` rather than a wrapper div, because a
   wrapper would be a container nothing else needs. */
.sch-interval-note { grid-column: 1 / -1; }
.sch-save { min-height: 38px; }

/* ---- A4: the bookings, and the Move popover --------------------------- */
/* TWO TRACKS, SO THE ZONE TOKEN IS AT THE ROW'S RIGHT EDGE BECAUSE THE TRACK IS.
   `justify-content: space-between` and `margin-left: auto` are two of 8.13's
   three drift primitives and each aligns a row to its own content width; a grid
   on the row is the idiom this file uses everywhere else for the same job. */
.sch-filter { display: grid; grid-template-columns: minmax(0, 1fr) max-content;
  gap: var(--s3); align-items: center; margin-top: var(--s4); }
.sch-filter .segmented { justify-self: start; }
.sch-group { display: flex; flex-direction: column; gap: var(--s2);
  margin-top: var(--s3); }
/* THE FILTER HIDES THE OTHER TWO GROUPS ONLY ONCE THE SCRIPT HAS CLAIMED THE
   PAGE. With scripting off `[data-sch-filtered]` never appears, all three groups
   render with their headings, and nothing is unreachable. */
[data-sch-filtered] .sch-group { display: none; }
[data-sch-filtered] .sch-group.is-shown { display: flex; }
[data-sch-filtered] .sch-group-h { display: none; }
.sch-group-h { margin: var(--s2) 0 0; font-size: 15px; font-weight: 600;
  letter-spacing: -.008em; }
/* `.sch-bk*` IS RETIRED (2026-09-03 handoff) AND `.sch-bkt` IS A TABLE. The rows
   were cards because the reschedule picker was going to need the room, and the
   picker turned out to be a POPOVER -- so the cards bought nothing and cost what
   a card costs: `.sch-bk-act` was `justify-content: flex-end` inside a
   `max-content` track, which agrees with the right edge of its own row's content
   and therefore lines up with nothing. Tracks shared with `.sch-thead` are what
   make a diary scannable.

   FOUR TRACKS, DOWN FROM SIX (owner ruling 2026-09-04). The 8px state dot, the
   `Where` column and the 96px state chip went with Move and Cancel: every
   interview is Microsoft Teams now, so `Where` was one constant repeated down a
   column, and a chip on every row said "upcoming" about a row already filed
   under Upcoming. The header strip and every row share this ONE declaration --
   layout rule 2, the grid lives on the row -- so dropping a track drops it from
   both at once, which is why there is no header to update separately. */
.sch-bkt { display: grid; min-width: 0; gap: var(--s4); align-items: center;
  grid-template-columns: 180px minmax(0, 1.1fr) minmax(0, 1fr) 108px; }
/* `.sch-bkt-dot` AND `.sch-bkt-chip` ARE DELETED with the State column. What
   replaces them is a marker on the When cell, drawn for the two RESOLVED states
   only -- `cancelled` and `completed` -- because the absence of a marker is what
   "not yet resolved" means and a third tag saying so is noise on every row.
   The tone still travels as `data-tone` and never as an interpolated class: a
   class assembled from data cannot be checked against this stylesheet at all. */
.sch-bkt-mark { font-size: 11px; font-weight: 600; letter-spacing: .01em;
  margin-top: var(--s1); }
.sch-bkt-mark[data-tone="warn"] { color: var(--warn-ink); }
.sch-bkt-mark[data-tone="info"] { color: var(--muted); }
.sch-bkt-when { display: flex; flex-direction: column; }
.sch-bkt-rel { font-size: 13px; font-weight: 600; color: var(--ink); }
.sch-bkt-abs { font-size: 12px; color: var(--muted); }
/* `.sch-bkt-who` IS DELETED: candidate and role are two cells now, not one
   stacked pair, so there is nothing left to stack. */
.sch-bkt-cand { font-size: 13.5px; font-weight: 600; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sch-bkt-d { font-size: 12.5px; color: var(--muted); overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
/* THE ACTION TRACK IS FIXED AT 108px, AND `max-content` WAS TRIED AND REFUSED BY
   A MEASUREMENT. A fixed track is what makes `flex-end` inside it legitimate:
   the buttons end at the TRACK's right edge, the same on every row. 108px is
   three 30px icons plus two 8px gaps (106) with 2px of slack.

   `max-content` LOOKED LIKE THE TIDIER ANSWER AND IS THE DEFECT THIS FILE'S
   RULE 2 NAMES. The header strip and every row are SEPARATE grid containers
   sharing one template, so an intrinsic track resolves PER CONTAINER: the
   header's empty action cell measured 0 and a row's three icons measured 68, and
   the two 1fr tracks beside them then divided different remainders. `SCH TABLES`
   in tests/test_ui_alignment.py caught it -- 68px of spread and two column
   headings off their cells -- which is the browser doing what the cascade
   cannot. Any intrinsic sizing here has the same fault; only a fixed track or a
   single shared container fixes it.

   `wrap` stays for the narrow pane, where the icons are the only thing that can
   wrap. */
.sch-bkt-act { display: flex; flex-wrap: wrap; align-items: center;
  justify-content: flex-end; gap: var(--s2); }
/* THE THREE ROW ICONS (owner ruling 2026-09-04). A real `<button>` each -- two
   submit a form, one opens the peek drawer -- sized to a comfortable target
   rather than to the glyph, because a 16px icon is otherwise a 16px hit area.
   Each carries an `aria-label` naming the act AND the candidate in the template:
   G25's lesson is that an icon whose only label is its glyph reads as nothing,
   and a bare "Completed" in a list of rows does not say which one. */
.sch-ico { display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; padding: 0; border-radius: 8px;
  border: 1px solid var(--line); background: var(--panel); color: var(--muted);
  cursor: pointer; }
.sch-ico:hover { border-color: var(--ink); color: var(--ink); }
.sch-ico:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
/* THE DESTRUCTIVE ONE IS TINTED ON HOVER ONLY, so a row of icons does not read
   as an alert at rest. `--bad-ink` on `--bad-tint` is a themed pair rather than
   white on `--bad`, which is the accent-fill trap G18's inversion list exists
   for. */
.sch-ico-bad:hover { border-color: var(--bad-ink); color: var(--bad-ink);
  background: var(--bad-tint); }
/* THE SCRIPTLESS-SENTENCE RULE AND ITS `data-sch-moveable` OVERRIDE ARE DELETED
   with the Move popover. It was the sentence a scriptless reader got in place of
   a script-only control, and Move was the only script-only control this hub had
   -- both write icons are ordinary forms that work with scripting off. The one
   remaining hidden-then-revealed control in the product is the month calendar on
   `/book`, which degrades to the complete flat list instead of to a sentence.

   ITS CLASS NAME IS DELIBERATELY NOT SPELLED HERE. The guard that holds it
   absent reads this file for the SELECTOR, and a comment quoting the selector is
   indistinguishable from the rule to a substring scan -- which is exactly the
   distinction #28 in tests/test_first_boot.py had to draw. */

/* `.sch-pop*` IS DELETED (owner ruling 2026-09-04) with the A4 Move popover:
   the fixed wrapper, its `[hidden]`, its form, the title, the two copy lines and
   the refusal line, the three-column slot grid, the day headings, the off-flow
   radio, the chip, its `:checked` and `:focus-visible` treatments and the
   footer. Fifteen rules for one control that no longer exists.

   ONE IDEA IN IT IS WORTH KEEPING and is recorded rather than left in dead CSS:
   the coordinates were set INLINE as computed values, which check_tokens 8.12
   permits precisely because a measured rect is data and not a layout decision.
   Any future anchored popover may do the same. */

/* A TEXT-ONLY BUTTON THAT READS AS A LINK, for the inline "nudge Amara" inside a
   verdict sentence. It is a BUTTON and not an anchor because it performs an
   action rather than navigating -- an anchor there would be a POST behind a link,
   which is the shape this codebase refuses everywhere. */
.link-btn { border: 0; background: none; padding: 0; font: inherit;
  color: var(--accent); cursor: pointer; text-decoration: underline; }
.link-btn:hover { color: var(--accent-d); }
.link-btn:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }

/* ---- the sidebar count badge: DELETED AGAIN ---------------------------- */
/* `.nav-badge` HAS NO RENDER SITE (owner, 2026-09-04). It came back on
   2026-09-03 for the scheduling row and went again the next day; the class has
   now been deleted twice and restored once, which is why the rule is removed
   rather than left unused -- a styled class with no render site is how the
   third arrival skips the review the second one got.

   ITS ARGUMENT IS KEPT, because a future badge will make the same one and it is
   the argument that has to be beaten: this badge counted a FAULT rather than
   inventory (a healthy workspace showed nothing), and no other surface stated
   the fact. That is what distinguished it from the two pills the 2026-08-07
   ruling struck. What removed it is not that the argument failed but that the
   journey strip states the same four facts on every hub tab, so the badge was a
   quieter second copy on every page in the product.

   THE ACCESSIBLE-NAME RULE OUTLIVES THE CLASS, in G25 in
   tests/test_layout_brand.py, inverted back to an absence guard for the third
   time. UI review (live) #10: the Pipeline item computed to "Pipeline2" because
   label and digit were adjacent inline text with no separator and no name on the
   badge. Any badge that returns hides its digit from assistive tech and puts an
   `.sr-only` sentence beside it. */

/* ---- responsive: below 1100px the hub stacks -------------------------- */
/* DESKTOP-FIRST, which is the handoff's own scope for A1-A5 ("desktop-first
   (>=1100px); below that, tables become stacked cards"). A BREAKPOINT IS THE
   RIGHT INSTRUMENT HERE for the reason UX-121 records: these containers are a
   pure function of the viewport -- `<main>` is the only parent and it is fluid --
   which is not true of the role drawer and is why that one needed measuring
   instead. */
@media (max-width: 1100px) {
  .sch-hours { grid-template-columns: minmax(0, 1fr); }
  .sch-roles { grid-template-columns: minmax(0, 1fr); gap: var(--s2); }
  .sch-bkt { grid-template-columns: minmax(0, 1fr); gap: var(--s2); }
  /* EVERY TABLE STACKS, SO EVERY HEADER GOES. One rule for BOTH now -- the
     types table is deleted -- and still one selector, because one class carries
     the treatment. */
  .sch-thead { display: none; }
  .sch-row { padding: var(--s4); }
  .sch-bkt-act { justify-content: flex-start; }
  /* THE STRIP BECOMES A COLUMN. Four cells at 900px are 210px each, which
     ellipsises every second line to nothing; the divider flips from the right
     edge to the bottom, and the last cell clears the one it now has rather
     than the one it no longer does. */
  /* `.sch-journey`'s stacked rules went with the strip (2026-09-07). They
     flipped the divider from the right edge to the bottom and cleared it on the
     last cell rather than the one it no longer had -- the rule any future
     stacked strip needs, since a `:last-child` clearing the wrong edge is
     invisible until somebody looks at a phone. */
  .sch-q2, .sch-adv-body { grid-template-columns: minmax(0, 1fr); }
  /* THE DRAWER BECOMES A FULL-SCREEN SHEET, which is what the handoff asks for
     -- a 660px drawer on a 900px window leaves 240px of dimmed page that is
     neither usable nor informative. */
  .drawer.sch-drawer { width: 100vw; min-width: 0; max-width: 100vw; }
}

/* ---- People module, slice 1: the derived reporting line ------------------
   A plain nested list with a hairline rail, not a drawn chart. The tree is
   DERIVED from one spreadsheet column, and the preview's job is to let a human
   read back what that column said before anything is created. */
.people-tree { list-style: none; margin: var(--s3) 0 0; padding-left: 0; }
.people-tree .people-tree { margin: var(--s2) 0 0; padding-left: var(--s4);
  border-left: 1px solid var(--line); }
.people-tree li { padding: 4px 0; }
.people-tree li > b { font-weight: 600; }
.people-tree li > .mono { color: var(--muted); margin-left: var(--s2); }
.people-tree li > .sub { margin-left: var(--s2); }

/* ---- Tab strips with many tabs: one line, scrolling sideways ---------------
   Ruling N4 (owner, 2026-10-19): thirteen department names wrapped onto two
   lines in a strip built for three. A MODIFIER, not a change to `.tabs`, so
   every other strip keeps its exact geometry. The strip's rule moves from a
   border to an inset shadow because `overflow-x: auto` clips the active tab's
   2px underline where it used to overlap the 1px rule by `margin-bottom: -1px`;
   painted inside the box, the underline covers the rule as before. */
.tabs.tabs-scroll { flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden;
  border-bottom: 0; box-shadow: inset 0 -1px 0 var(--line);
  scrollbar-width: thin; }
.tabs.tabs-scroll a { flex: 0 0 auto; white-space: nowrap; margin-bottom: 0; }

/* ---- KPI sheet: the input/output pair and the period (rulings N1, N2) ---- */
/* A two-column grid -- the label in its own column, the goal wrapping in the
   other -- so "Run 40 onboarding calls" never wraps back under "Input". */
.kpi-pair { display: grid; grid-template-columns: max-content minmax(0, 1fr);
  column-gap: var(--s2); row-gap: 4px; min-width: 0; }
.kpi-pair > span { display: contents; }
.kpi-period { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--s3); }

/* ---- People / O-Chart (rulings O1-O7) ---------------------------------------
   The owner's sheet: one column per team, a band on top, the head, and every
   person's reports indented under them with an elbow line. Columns keep their
   width and the row scrolls sideways rather than squeezing names into two
   lines -- the same choice as the tab strips above. */
.ochart-scroll { overflow-x: auto; padding-bottom: var(--s3); }
.ochart { display: flex; gap: var(--s4); align-items: flex-start; min-width: min-content; }
.ochart-col { flex: 0 0 236px; min-width: 0; }
.ochart-head { margin: 0 0 var(--s3); padding: 8px 12px; border-radius: 8px;
  background: var(--accent); color: var(--bg); font-size: var(--text-sm);
  font-weight: 600; text-align: center; }
.ochart-tree, .ochart-tree ul { list-style: none; margin: 0; padding: 0; }
.ochart-tree li { margin: 0; }
.ochart-tree > li + li { margin-top: var(--s3); }
.ochart-tree ul { margin-left: 12px; }
.ochart-tree ul > li { position: relative; padding: 8px 0 0 16px;
  border-left: 1px solid var(--line); }
.ochart-tree ul > li:last-child { border-left-color: transparent; }
.ochart-tree ul > li::before { content: ""; position: absolute; left: -1px; top: 0;
  width: 14px; height: 28px; border-left: 1px solid var(--line);
  border-bottom: 1px solid var(--line); border-bottom-left-radius: 6px; }
.ochart-row { display: flex; align-items: flex-start; gap: 4px; }
.ochart-box { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column;
  gap: 0; padding: 8px 12px; border: 1px solid var(--line); border-radius: 8px;
  background: var(--panel); color: var(--ink); text-decoration: none; }
.ochart-box:hover { border-color: var(--accent); }
.ochart-box:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
.ochart-box.is-head { background: var(--accent-tint); border-color: var(--accent-edge); }
.ochart-box.is-head .ochart-name { font-weight: 600; }
.ochart-box.is-open { box-shadow: 0 0 0 2px var(--accent); }
.ochart-name { font-size: 13px; line-height: 1.3; overflow-wrap: anywhere; }
.ochart-title { font-size: 12px; color: var(--muted); line-height: 1.3; overflow-wrap: anywhere; }
.ochart-box .chip { align-self: flex-start; margin-top: 4px; }
.ochart-add { flex: 0 0 auto; width: 24px; height: 24px; margin-top: 8px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px dashed var(--line); border-radius: 6px; color: var(--muted);
  text-decoration: none; font-size: 14px; line-height: 1; }
.ochart-add:hover { color: var(--accent); border-color: var(--accent); }

/* The information box: the shared drawer, narrower -- nine fields, not a
   candidate report. */
.drawer.drawer-sm { width: 480px; min-width: min(480px, 96vw); }
.ochart-form label { display: block; margin: 12px 0 4px; }
.ochart-form input, .ochart-form select { width: 100%; }
.ochart-facts { display: grid; grid-template-columns: max-content 1fr; gap: 8px 16px;
  margin: var(--s3) 0; }
.ochart-facts dt { color: var(--muted); font-size: 13px; }
.ochart-facts dd { margin: 0; overflow-wrap: anywhere; }

/* Home (rulings P1-P6, 2026-10-19): one page, a section per thing the viewer
   is, stacked. Each section is a heading over its panels; two panels sit side
   by side where there is room and stack on a phone. The panels' own bottom
   margin is dropped inside a section so the column's gap is the one rhythm. */
.home-sec { display: flex; flex-direction: column; gap: var(--s4);
  margin-top: var(--s6); }
.home-sec > h2 { margin: 0; }
.home-sec .panel.card-keep { margin: 0; min-width: 0; }
.home-sec .panel h3 { margin-top: 0; }
.home-grid { display: grid; gap: var(--s4);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); }
.home-sec .attn-row .chip { margin-left: var(--s2); }
.home-sec .attn-row .attn-meta { display: block; }
.home-meter-col { width: 40%; }

/* Grading Structure as a rank list with a drawer (owner, 2026-10-20, ruling
   T1). One row per rank: its number, its title(s) as links that open the
   rank's drawer, its route, and an "Edit title & route" disclosure. Two
   roles at one rank (one per route) are two links to the same drawer. */
.grank-list { list-style: none; margin: var(--s3) 0 0; padding: 0;
  border-top: 1px solid var(--line); }
.grank { display: grid; grid-template-columns: 88px minmax(0, 1fr) auto;
  gap: var(--s2) var(--s4); align-items: baseline;
  padding: var(--s3) 0; border-bottom: 1px solid var(--line); }
.grank.is-open { background: var(--accent-tint); }
.grank-n { color: var(--muted); font-size: 13px; font-variant-numeric: tabular-nums;
  padding-left: var(--s2); }
.grank-titles { display: flex; flex-wrap: wrap; gap: var(--s1) var(--s4); min-width: 0; }
.grank-title { font-weight: 600; color: var(--ink); text-decoration: none;
  overflow-wrap: anywhere; }
.grank-title:hover, .grank-title:focus-visible { color: var(--accent); text-decoration: underline; }
.grank-title.is-empty { font-weight: 400; color: var(--muted); }
.grank-route { display: flex; gap: var(--s1); padding-right: var(--s2); }
.grank-edit { grid-column: 2 / -1; }
.grank-edit > summary { cursor: pointer; color: var(--muted); font-size: 13px; }
.grank-band { margin: var(--s5) 0 var(--s1); }
@media (max-width: 640px) {
  .grank { grid-template-columns: minmax(0, 1fr) auto; }
  .grank-n { grid-column: 1 / -1; padding-left: 0; }
  .grank-edit { grid-column: 1 / -1; }
}

/* ---- A person's record: its forms, one field per line (ruling U1) ---- */
/* `.form-row` is an INLINE row elsewhere -- a label beside a short input --
   and on the record that left text boxes half-width beside their labels
   while every dropdown ran full width under its own. A modifier, not a
   change to `.form-row`, so every other form keeps its geometry. */
.record-form .form-row { display: block; }
.record-form .form-row input { width: 100%; }
.record-form h3 { margin-top: var(--s5); }
.record-form > .btn { margin-top: var(--s4); }
/* "+ Add another" stacks a second picker under the first. */
.record-form .form-row select + select,
.record-form .form-row select + .btn { margin-top: var(--s2); }
