/**
 * Vanceli - Shared Components
 * ----------------------------------------------------------------------------
 * Every block below corresponds to one entry in /components.md - keep both
 * files in sync (instructions.md rule 4). Depends on tokens.css + base.css.
 */

/* =========================================================================
 * In-app type cap (design.md §2.2, components.md §23)
 * -------------------------------------------------------------------------
 * Authenticated dashboard text never exceeds text-h5 (20px). Rather than
 * chasing individual headings, each in-app ROOT re-points the above-cap type
 * steps at --text-h5. Everything inside then resolves to 20px automatically -
 * including bare <h1>-<h4> that pick up base.css's element defaults (the
 * empty-state headings, the Assessment Template drawer heading) and any rule
 * that reaches for --text-h4/h3/h2/h1/display by name.
 *
 * Why re-point here and not in tokens.css/base.css: the public marketing
 * pages and the auth screens are deliberately EXEMPT (landing hero keeps
 * text-display/text-h1; auth card title + OTP boxes keep text-h4). Editing
 * the tokens or the element defaults would shrink those too.
 *
 * Overlays are listed alongside .dashboard-shell because modals and drawers
 * mount on <body>, outside the shell - a descendant selector would miss them.
 *
 * Values stay in rem so browser zoom still works (design.md §2.2).
 * ========================================================================= */
.dashboard-shell, /* employer + candidate dashboard screens and top bar */
.dash-shell, /* post-login placeholder dashboard (pages/dashboard.css) */
.modal, /* incl. confirm dialogs + shortlist picker, which reuse .modal */
.candidate-drawer,
.template-drawer {
  --text-display: var(--text-h5);
  --text-h1: var(--text-h5);
  --text-h2: var(--text-h5);
  --text-h3: var(--text-h5);
  --text-h4: var(--text-h5);
}

/* -------------------------------------------------------------------------
 * In-app paragraph body copy = text-sm (14px) - design.md §2.2
 * -------------------------------------------------------------------------
 * Companion to the type cap above, and scoped to the SAME in-app roots. The
 * dashboard is dense and read at a glance, so running prose there sits at
 * 14px rather than the 16px design.md §2.1 specifies as the site-wide
 * default. Marketing and auth screens are deliberately untouched and keep
 * that 16px default - which is also why this lives here rather than on the
 * bare `p` rule in base.css: editing the element default would shrink the
 * landing hero and auth card copy too (same reasoning as the cap above).
 *
 * `:where()` is load-bearing. It zeroes the selector's specificity, so this
 * lands at element level (0,0,1) instead of (0,1,1). Without it, this rule
 * would outrank every single-class paragraph rule in the codebase and drag
 * card TITLES down with it - .candidate-card__name (text-h5),
 * .template-card__name / .shortlist-card__name (text-lg) are all marked up
 * as <p>. With it, any class that names a size still wins, so a paragraph
 * only opts up where a spec actually asks it to.
 *
 * --text-base is NOT remapped here: it must stay 16px for the interactive
 * chrome inside these roots (buttons §7, inputs and field labels §8).
 * Values stay in rem, so browser zoom still works (design.md §2.2).
 * ------------------------------------------------------------------------- */
:where(.dashboard-shell, .dash-shell, .modal, .candidate-drawer, .template-drawer) p {
  font-size: var(--text-sm);
  line-height: 1.5;
}

/* =========================================================================
 * Site Header (lightweight branding bar - not dashboard nav chrome,
 * used on every page so the theme toggle is always reachable, design.md §16.1)
 * ========================================================================= */
.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-4) var(--space-5);
}

.site-header__logo {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--text-lg);
  color: var(--text-primary);
  text-decoration: none;
  letter-spacing: -0.01em;
}

/* =========================================================================
 * Theme Toggle (design.md §1.4 note) - sun/moon icon button in header
 * ========================================================================= */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px; /* design.md §4.4 minimum touch target */
  border-radius: var(--radius-full);
  border: 1.5px solid var(--border-default);
  background: transparent;
  color: var(--icon-default);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out);
}

.theme-toggle:hover {
  background: var(--bg-surface);
}

.theme-toggle svg {
  width: 20px;
  height: 20px;
}

.theme-toggle .icon-moon {
  display: none;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle .icon-sun {
    display: none;
  }
  :root:not([data-theme="light"]) .theme-toggle .icon-moon {
    display: block;
  }
}

:root[data-theme="dark"] .theme-toggle .icon-sun {
  display: none;
}
:root[data-theme="dark"] .theme-toggle .icon-moon {
  display: block;
}

/* =========================================================================
 * Buttons (design.md §7)
 * ========================================================================= */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  height: 40px;
  padding: 0 var(--space-5);
  border-radius: var(--radius-full);
  font-family: var(--font-body);
  /* ALL button labels are text-sm/14px, at every size (design.md §7 "Sizes").
     This deliberately overrides §7's older 16px `md` / 18px `lg` font steps:
     button text is a label, not body copy, so it reads at one size everywhere
     and the size ladder now varies height/padding only. Set here on the base
     rule (which is the `md` default) and re-asserted on .btn-lg below; .btn-sm
     was already text-sm.
     NOT a token change: --text-base stays 16px because inputs and field labels
     still use it (design.md §2.2 "--text-base is not remapped", §8). The 14px
     lives on the button classes only.
     Heights/padding are untouched - typography only. */
  font-size: var(--text-sm);
  font-weight: 600;
  border: 1.5px solid transparent;
  cursor: pointer;
  text-decoration: none;
  transition: background-color var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    opacity var(--duration-fast) var(--ease-out);
  white-space: nowrap;
}

.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.4;
  cursor: not-allowed;
}

/* `.btn`'s `display: inline-flex` shares the browser default `[hidden]{display:
   none}`'s specificity and wins by cascade order, so a hidden button (e.g.
   Account Settings' "Verify Email" button once the email is verified,
   components.md §49; also `#create-template-btn` on the Assessments page)
   stays visible without this. Same fix already applied to .alert[hidden]
   and .dashboard-sidebar__badge[hidden]. */
.btn[hidden] {
  display: none;
}

/* Size variants change the BOX only - height and padding. The label stays
   text-sm/14px at all three sizes (design.md §7 "Sizes"); `lg` no longer steps
   the font up to 18px. */
.btn-lg {
  height: 48px;
  padding: 0 var(--space-6);
  font-size: var(--text-sm);
}

.btn-sm {
  height: 32px;
  padding: 0 var(--space-4);
  font-size: var(--text-sm);
}

.btn-primary {
  background: var(--accent);
  color: var(--accent-text-on);
  border-color: var(--accent);
}

.btn-primary:hover:not(:disabled):not([aria-disabled="true"]) {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
}

.btn-secondary {
  background: transparent;
  color: var(--accent);
  border-color: var(--accent);
}

.btn-secondary:hover:not(:disabled):not([aria-disabled="true"]) {
  background: var(--bg-surface);
}

.btn-ghost {
  background: transparent;
  color: var(--accent);
  border-color: transparent;
}

.btn-ghost:hover:not(:disabled):not([aria-disabled="true"]) {
  background: var(--bg-surface);
}

.btn-danger {
  background: var(--color-error);
  color: #ffffff;
  border-color: var(--color-error);
}

/* Danger, secondary weight (design.md §7 Danger addendum) - same shape as
   .btn-secondary above (transparent fill, 1.5px border, --bg-surface hover)
   but in the error palette instead of accent. For a STANDING destructive
   action that must read as secondary-rank in its card rather than the
   solid-fill .btn-danger above - e.g. Account Settings' Delete Account
   (components.md §49), which is already guarded by a confirm dialog and does
   not need the solid fill to signal danger. Distinct from the bulk pill's
   `.bulk-pill__btn--danger` (ghost-until-hover, §24/§37): that ambient-toolbar
   pattern flips to solid Error only on hover so it isn't a standing alarm;
   this one stays outlined at every state because the card itself is not
   ambient - the reader opened Security on purpose. No dark-mode override
   needed: --color-error already resolves per-theme. */
.btn-danger-outline {
  background: transparent;
  color: var(--color-error);
  border-color: var(--color-error);
}

.btn-danger-outline:hover:not(:disabled):not([aria-disabled="true"]) {
  background: var(--bg-surface);
}

/* -------------------------------------------------------------------------
 * Neutral (outline) - design.md §7
 * -------------------------------------------------------------------------
 * The only NON-ACCENT interactive button in the system. Every other variant is
 * accent-coloured or --color-error, which is normally right: a button in this
 * app is either the action you want or a destructive one.
 *
 * This exists for the case where a real, useful action must visibly NOT compete
 * with the primary in the same container. First use: the "Click Here to Reply"
 * button inside the Send Email preview (components.md §90) - it is a genuine
 * control the CANDIDATE will press, rendered inside a modal whose own primary is
 * "Send Email". In accent, the two read as rival CTAs and the employer's eye
 * goes to the wrong one; in ghost, it stops reading as a button at all, which is
 * wrong for something the preview is demonstrating.
 *
 * Tokens are the ones design.md already sanctions for neutral chrome:
 * --border-strong is the §8 filter-trigger's edge (the system's only other
 * neutral-bordered pill), and --hover-surface is named on §7's Tertiary row.
 * ------------------------------------------------------------------------- */
.btn-neutral {
  background: transparent;
  color: var(--text-primary);
  border-color: var(--border-strong);
}

.btn-neutral:hover:not(:disabled):not([aria-disabled="true"]) {
  background: var(--hover-surface);
}

.btn-block {
  width: 100%;
}

.btn-icon {
  width: 44px;
  height: 44px;
  padding: 0;
  border-radius: var(--radius-full);
  background: transparent;
  border-color: transparent;
  color: var(--icon-default);
}

.btn-icon:hover:not(:disabled) {
  background: var(--bg-surface);
}

.btn svg {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
}

/* ---- Overlay close (x) glyph - single source of truth (design.md §3.7) ----
   Every in-app overlay's close control renders the same `icon("x")` inside a
   `.btn-icon`, but each was styled per-component, and none of them set a glyph
   size: `.btn-icon` carries no `.btn` class, so the `.btn svg` 18px rule above
   never applied to them and the SVGs fell back to their intrinsic size. They
   are normalised here to `--icon-size-close` (20px, design.md §3.7's default)
   rather than given per-component sizes, so the close affordance reads
   identically across the drawer, modal, confirm dialog, picker and overlays.
   Listed as one rule instead of a shared class because the buttons are emitted
   by five different modules/templates and adding a class to each would be a
   markup change - this keeps one place to edit either way.
   GLYPH ONLY (design.md §4.4): `.btn-icon` keeps its fixed 44x44 box, so the
   hit area is untouched and the growth is absorbed by the button's invisible
   padding - never size the button down to the glyph.
   Covers: components.md §25 Candidate Detail Drawer, §31 Modal (and therefore
   §32 Confirmation Dialog + §35 Shortlist Picker, which both render into it),
   §41 Template Builder Drawer, §53 Resume viewer, §62 Assessment review overlay,
   §80 Advanced Search overlay.
   NOT close controls, deliberately excluded: chip removes (`.chip button`),
   the Advanced Search overlay's per-group `Clear` (§80 - a labelled action, not
   a close, and sized to sit beside a --text-sm heading), the Template Builder's
   remove-question/option/file buttons, and any in-field clear - those keep their
   own sizes.

   ⚠️ MISSING FROM THIS LIST = BROKEN, not merely inconsistent. `.btn-icon`
   deliberately does not size its own glyph, and there is no global `svg` rule in
   this stylesheet, so an overlay close whose selector is absent here has NO
   width/height from any source. The SVG then falls back to filling its 44x44
   button edge-to-edge. That is exactly what happened to the Advanced Search
   overlay before it was added above. Any new overlay close belongs here. */
.candidate-drawer__close svg,
.template-drawer__close svg,
.advanced-search-drawer__close svg,
.email-drawer__close svg,
.modal__close svg,
#assessment-overlay-close-btn svg,
#resume-viewer-close-btn svg,
#video-lightbox-close-btn svg,
#cv-viewer-close-btn svg {
  width: var(--icon-size-close);
  height: var(--icon-size-close);
}

.btn-spinner {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 2px solid currentColor;
  border-top-color: transparent;
  animation: btn-spin 700ms linear infinite;
}

@keyframes btn-spin {
  to {
    transform: rotate(360deg);
  }
}

/* Link button (design.md §7's "Link button" variant). It set NO font-size at
   all, so it took whatever its container gave it: 14px in the auth card footer,
   .otp-resend and .alert (fine by luck), but 16px for the standalone ones - the
   Shortlist Picker's "Remove from ..." quick actions (the drawer's reveal
   button was the other, and it is gone with the reveal gate). Pinned to text-sm/14px so every button label matches (§7).
   Safe for the inline-in-a-sentence call sites ("Already have an account? Log
   In"): those sentences are already 14px, so nothing desyncs from its copy. */
.link-btn {
  background: none;
  border: none;
  padding: 0;
  font-size: var(--text-sm);
  color: var(--accent);
  font-weight: 600;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* =========================================================================
 * Auth Card Shell (design.md §16.1 - centered single column, max-width 420px)
 * ========================================================================= */
.auth-shell {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

.auth-main {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-6) var(--space-4) var(--space-9);
}

.auth-card {
  width: 100%;
  max-width: var(--auth-card-width);
  background: var(--bg-surface);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  padding: var(--space-6) var(--space-5);
  /* Query container for the Stepper (components.md §9): its labels respond to
     THIS card's width, not the viewport, so they only appear once the ~420px
     card can actually seat all four beneath their nodes (see .stepper below). */
  container: auth-card / inline-size;
}

:root[data-theme="dark"] .auth-card {
  box-shadow: none;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .auth-card {
    box-shadow: none;
  }
}

.auth-card__eyebrow {
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-secondary);
  margin-bottom: var(--space-2);
}

/* Auth card title (design.md §2.1). --text-h4 / 24px is deliberate and is the
   documented auth-screen exemption from the 20px in-app cap (design.md §2.2) -
   that cap is scoped to .dashboard-shell and does not reach these screens
   (§16.1). Consumed by /login's <h1> and by all four of /get-started's step
   <h2>s; without this class a bare heading falls through to base.css's global
   element sizing (h2 -> --text-h2 / 36px), which is exactly what had happened
   on Get Started. */
.auth-card__title {
  font-size: var(--text-h4);
  margin-bottom: var(--space-2);
}

.auth-card__lead {
  color: var(--text-secondary);
  font-size: var(--text-sm);
  margin-bottom: var(--space-5);
}

.auth-card__footer {
  margin-top: var(--space-5);
  text-align: center;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

/* =========================================================================
 * Auth Split Layout + Promo Aside (design.md §16.1) - OPTIONAL two-panel
 * auth layout: the unchanged .auth-card in a left form column + a fixed-dark
 * promo aside on the right, as an alternative to the centered single-card
 * default above. Shell/panel styles live here (beside .auth-shell/.auth-main);
 * the promo-aside decorative content (the mock search bar) lives in
 * pages/auth.css. Consumed by: /login and /get-started. Never used on
 * reset-password (design.md §16.1 - kept distraction-free).
 * ========================================================================= */

/* Split wrapper - sits inside .auth-shell in place of a bare .auth-main,
   filling the shell height so BOTH columns run edge-to-edge from the top of
   the viewport. The header/logo is no longer a full-width band above the
   split; it moves INSIDE the left form column (.auth-split__form) so the
   promo aside is flush to the top - no white gap above it. Single column
   (form only) below Tablet; 50/50 row from Tablet up (design.md §3.2). */
.auth-split {
  flex: 1;
  display: flex;
  flex-direction: column;
}

/* Left form column: holds the in-column header (logo/toggle, top-left) + the
   centered .auth-main. A flex column so the header sits at the top and the
   card stays vertically centered in the remaining space. Fills the split on
   mobile via flex:1 (single-column, natural page scroll). */
.auth-split__form {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0; /* let the 50% column shrink instead of forcing horizontal scroll */
}

/* Below Tablet (< 600px): aside hidden, form column = the exact centered
   single-card default (.auth-main already centers its card). The in-column
   header/logo stays visible at the top on the form side. */
.auth-promo {
  display: none;
}

@media (min-width: 600px) {
  /* Fixed viewport height so the promo aside pins to the FULL viewport
     height (flush top & bottom) and the form column scrolls INTERNALLY when
     the form is taller than the viewport - the aside never scrolls away /
     leaves a gap. */
  .auth-split {
    flex-direction: row;
    height: 100vh;
    /* Cap at exactly 100vh - do NOT flex-grow to fill an over-tall shell, or
       a tall form would stretch the split (and the aside) past the viewport
       instead of scrolling the form column internally. */
    flex: none;
  }
  /* 50/50 split; the form column keeps its card centered (via .auth-main)
     and scrolls internally, while the aside stays pinned. */
  .auth-split__form {
    flex: 1 1 50%;
    overflow-y: auto;
  }
  /* Aside fills the other half at full viewport height (default align-items:
     stretch against the row's fixed 100vh height) - top-to-bottom, no gap. */
  .auth-promo {
    display: flex;
    flex: 1 1 50%;
  }
}

/* The promo aside itself - a fixed DARK surface in BOTH themes (design.md §6
   convention, same as footers/sidebars): painted with the raw, non-flipping
   --color-accent (NOT --accent, which becomes mint in dark mode). The theme
   tokens are re-scoped locally to their dark-mode values so that all
   foreground colors AND the reused Primary button (design.md §7) render
   dark-mode-correct regardless of the page theme, with zero restyling. */
.auth-promo {
  --text-primary: var(--color-white);
  --text-secondary: var(--neutral-300);
  --accent: var(--color-icon);
  --accent-text-on: var(--color-text-brand);
  --accent-hover: var(--color-icon-hover);
  --focus-ring: var(--color-icon);
  --border-default: var(--neutral-700);

  align-items: center;
  justify-content: center;
  padding: var(--space-9) var(--space-7);
  background: var(--color-accent);
}

/* Inner-edge hairline ONLY in dark mode, where the page canvas is also
   --color-accent and the panel would otherwise be indistinguishable from it.
   In light mode the dark panel separates from the light form side by
   contrast alone, so no border. */
:root[data-theme="dark"] .auth-promo {
  border-left: 1px solid var(--border-default);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .auth-promo {
    border-left: 1px solid var(--border-default);
  }
}

/* Centered vertical content stack: graphic slot (optional), headline,
   subhead, CTA. */
.auth-promo__inner {
  width: 100%;
  max-width: var(--auth-card-width); /* mirrors the form card's 420px width */
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-5);
}

.auth-promo__headline {
  margin: 0; /* rhythm comes from .auth-promo__inner's gap, not element margins */
  color: var(--text-primary); /* = --color-white via the local remap above */
}

/* The single emphasised word. mint-as-text is valid HERE ONLY because the
   surface is dark (color-icon on color-accent = 13.75:1); it would fail on
   any light surface and is forbidden there (design.md §1.1). */
.auth-promo__accent {
  color: var(--color-icon);
}

.auth-promo__subhead {
  margin: 0;
  font-size: var(--text-lg);
  color: var(--text-secondary); /* = --neutral-300 via the local remap (AAA) */
}

/* =========================================================================
 * Form Fields (design.md §8)
 * ========================================================================= */
.field {
  margin-bottom: var(--space-4);
}

/* A `.field` may be a <fieldset> when it wraps a set of related radios or
   checkboxes (Gender / Availability / Type on the Candidate Profile), so the
   group name can be a real <legend> and be announced with each option. The UA's
   default border/inset-legend chrome has to be stripped for it to look like
   every other field - the same reset `.advanced-search__group` already applies
   for the same reason. `min-width: 0` because fieldsets default to
   `min-content`, which would otherwise override the `minmax(0, 1fr)` columns of
   any grid they sit in and force horizontal overflow. */
fieldset.field {
  border: none;
  padding: 0;
  min-width: 0;
}

.field__label {
  display: block;
  font-size: var(--text-sm);
  font-weight: 600;
  margin-bottom: var(--space-2);
  color: var(--text-primary);
}

/* -------------------------------------------------------------------------
 * LABEL WITH AN INLINE HINT (components.md - "Inline field hint")
 *
 * A label row that carries its guidance text on the right instead of hiding it
 * behind a hover-only [data-tooltip] info icon (components.md §93). Introduced
 * for the Candidate Profile contact fields (Phone Number, Personal Email, Work
 * Email, Preferred Roles) so the guidance is readable on touch devices, where a
 * hover tooltip never appears at all.
 *
 * The LABEL itself becomes the flex container so the hint shares the label's
 * row rather than pushing the control down. Plain `.field__label` is untouched
 * and stays `display: block` everywhere else on the site.
 *
 * ⚠️ Requires the label text to be wrapped in `.field__label-text` - flex
 * `space-between` needs two children to push apart, and without the wrapper a
 * bare text node cannot be one of them.
 */
.field__label--with-hint {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
}

/* The label half keeps the label's own weight/colour (inherited); it only needs
   to stop shrinking so the hint never squeezes the field name. */
.field__label-text {
  flex: 0 0 auto;
}

/* The guidance half. BODY text, not label text: normal weight and secondary
   colour, matching `.field__hint` further down this file, which is the same
   copy in its block-below-the-control form. Right-aligned against the field's
   right edge as specified.

   `flex: 1 1 auto` + a min-width means that when the column is too narrow for
   both, the hint wraps onto its own line under the label (via the container's
   `flex-wrap`) instead of collapsing into a one-word-per-line ribbon. */
.field__hint-inline {
  flex: 1 1 auto;
  min-width: 220px;
  text-align: right;
  font-size: var(--text-xs);
  font-weight: 400;
  line-height: 1.4;
  color: var(--text-secondary);
}

/* Once wrapped onto its own line there is no label to align away from, so the
   right-alignment stops earning anything and reads as a ragged left edge. */
@media (max-width: 767px) {
  .field__hint-inline {
    text-align: left;
  }
}

/* Matching reset for the <legend> form of the label - removes the UA's default
   inline indent so a legend and a plain label start at the same x. */
legend.field__label {
  padding: 0;
}

.field__required {
  color: var(--color-error);
  margin-left: 2px;
}

.field__input-wrap {
  position: relative;
}

/* =========================================================================
 * FIELD AFFIX - a control or status pinned inside a text input's right edge
 * (design.md §8, components.md "Field Affix")
 *
 * First consumers: the candidate Profile's Email Address and Phone Number,
 * which show a "Verify" action while the value is unproven and a "Verified"
 * readout once it is (components.md §57). Generic on purpose - any input that
 * needs an inline action or state ("Show" on a password, a unit suffix) should
 * use this rather than inventing another one.
 *
 * ⚠️ THE AFFIX IS PLACED, THE INPUT IS PADDED. Both halves are required. The
 * affix is absolutely positioned inside .field__input-wrap, so it is OUT of the
 * input's flow and cannot push the value; the input then has to reserve the
 * space itself with .field__control--has-affix, or a long value runs underneath
 * the affix and the two overlap into an unreadable smear. There is no automatic
 * measurement - the reserve below is a fixed generous inset, tuned to the
 * longest affix these fields carry ("Verified" with its tick).
 * ========================================================================= */
.field__affix {
  position: absolute;
  top: 50%;
  right: var(--space-2);
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  /* The wrap is only a positioning context; clicks belong to the input except
     where an affix child actually wants them (the button re-enables its own).
     Without this, the affix's padding would steal clicks aimed at the end of
     the value and the caret would refuse to go there. */
  pointer-events: none;
}

.field__affix:empty {
  display: none;
}

/* The actionable variant - "Verify". A text button rather than a .btn: a filled
   or outlined control inside an input reads as a second field, and at 44px it
   would not fit a 44px input at all. Accent + 600 is the same "this is an
   action" language .alert__link and the searchable-select create row use. */
.field__affix-btn {
  pointer-events: auto;
  display: inline-flex;
  align-items: center;
  /* ⚠️ design.md §4.4: the VISIBLE text is short, so the hit area is bought
     with padding instead. --space-2 vertical takes the target past 32px inside
     a 44px input, and the input's own height carries the rest of the touch
     slop - the affix spans the full field height in practice. */
  padding: var(--space-2);
  margin: calc(var(--space-2) * -1) 0;
  background: none;
  border: 0;
  border-radius: var(--radius-xs);
  color: var(--accent);
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
  white-space: nowrap;
  transition: color var(--duration-fast) var(--ease-out);
}

.field__affix-btn:hover:not(:disabled) {
  color: var(--accent-hover);
  text-decoration: underline;
}

.field__affix-btn:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* Follows the disabled INPUT it sits in (profile lock, components.md §57): an
   enabled-looking action inside a disabled field is a broken promise. */
.field__affix-btn:disabled {
  color: var(--text-disabled);
  cursor: not-allowed;
}

/* The status variant - "Verified". Not a .badge: a filled pill inside an input
   competes with the value for attention, and this is a quiet confirmation, not
   a label on the data. Tick + word together, never colour alone (design.md
   §4.6) - the same rule the applied-filter state follows. */
.field__affix-status {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  color: var(--color-success);
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
}

.field__affix-status svg {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}

.field__control {
  width: 100%;
  height: 44px;
  padding: 0 var(--space-3);
  background: var(--bg-base);
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-xs);
  color: var(--text-primary);
  transition: border-color var(--duration-fast) var(--ease-out);
}

/* The other required half of the field affix above: room for it to sit in.
   ⚠️ MUST STAY BELOW `.field__control`, which sets `padding` as a SHORTHAND -
   authored above it, this modifier's padding-right is silently overwritten by
   an equal-specificity rule later in the file, and the affix lands on top of
   the value with nothing in the CSS to hint why.
   The inset is fixed and generous (tuned to "✓ Verified", the widest affix in
   use) rather than measured: a JS measurement would have to re-run on every
   font load, theme change and label swap, for a control whose whole job is to
   be unobtrusive. */
.field__control--has-affix {
  /* Measured, not guessed: the widest affix in use ("✓ Verified") renders at
     73px, plus the affix's own --space-2 right offset and a little breathing
     room before the value runs into it. */
  padding-right: 88px;
}

/* First textarea usage (design.md §8 "Text input / textarea") - the base
   rule's fixed 44px height suits single-line inputs only, so a textarea
   needs its own height/padding/resize behavior. */
textarea.field__control {
  height: auto;
  min-height: 88px;
  padding: var(--space-3);
  resize: vertical;
  font-family: var(--font-body);
}

textarea.field__control--extra-height {
  min-height: 160px;
}

/* First native <select> usage on a .field__control (design.md §8
   "Select / multi-select / combobox" - the 8px-radius toolbar select).
   Every other select-shaped control in this app is a custom combobox
   (country-select.js, searchable-select.js, multi-select.js) because it needs
   search, flags or chips; the Date of Birth field's Month/Day lists need none
   of that and a native select is keyboard- and mobile-native for free.
   The chevron is an inline data-URI so it inherits nothing from the icon sprite
   and needs no extra element inside the control. */
select.field__control {
  appearance: none;
  -webkit-appearance: none;
  border-radius: var(--radius-md);
  padding-right: var(--space-6);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256' fill='%23767676'%3E%3Cpath d='M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-3) center;
  background-size: 16px 16px;
  cursor: pointer;
}

/* Disabled is load-bearing here, not decorative: the Date of Birth field keeps
   Month locked until a Year is entered and Day locked until a Month is chosen
   (date-of-birth-field.js), so this state is the visible half of that rule. */
.field__control:disabled {
  background-color: var(--bg-surface);
  color: var(--text-disabled);
  cursor: not-allowed;
}

select.field__control:disabled {
  cursor: not-allowed;
}

.field__control::placeholder {
  color: var(--text-disabled);
}

/* =========================================================================
 * Date of Birth field (design.md §8 / §13, components.md §83)
 * -------------------------------------------------------------------------
 * Container for the three controls date-of-birth-field.js renders into
 * (Year input, Month select, Day select). Lives here rather than in
 * assets/css/pages/candidate-profile.css because the component is reusable -
 * the Candidate Profile is simply its first consumer.
 *
 * The columns are minmax()-based, not equal fractions: this field sits in the
 * RIGHT half of a two-up .profile-form__row-grid (beside Gender), so it has to
 * stay legible at roughly half the form's width. "September" is the widest
 * label by some margin, hence the wider middle column, and `minmax(0, ...)`
 * lets the selects shrink rather than forcing the row to overflow.
 * ========================================================================= */
.dob-field {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.3fr) minmax(0, 0.8fr);
  gap: var(--space-2);
}

/* Below the paired row's own 767px stacking point the field has the full form
   width, but on the narrowest phones three side-by-side controls still crowd;
   Year takes its own line and Month/Day share the next. */
@media (max-width: 400px) {
  .dob-field {
    grid-template-columns: 1fr 1fr;
  }

  .dob-field__part:first-child {
    grid-column: 1 / -1;
  }
}

.field__control:focus-visible {
  border-color: var(--focus-ring);
}

.field__control[aria-invalid="true"] {
  border-color: var(--color-error);
}

.field__control:disabled {
  background: var(--bg-surface);
  color: var(--text-disabled);
  cursor: not-allowed;
}

.field--with-action .field__control {
  padding-right: var(--space-9);
}

.field__action {
  position: absolute;
  right: 4px;
  top: 4px;
  width: 36px;
  height: 36px;
  border-radius: var(--radius-full);
  background: transparent;
  border: none;
  color: var(--icon-muted);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.field__action:hover {
  color: var(--icon-default);
  background: var(--bg-surface);
}

.field__action svg {
  width: 18px;
  height: 18px;
}

.field__hint {
  margin-top: var(--space-2);
  font-size: var(--text-xs);
  color: var(--text-secondary);
}

/* Two-up field row (components.md §4). Introduced for the mandatory
   First Name / Last Name pair, which is now collected on three screens
   (get-started, both Account Settings pages, Candidate Profile).
   Lives here rather than in a page stylesheet precisely because it is shared.

   Only the AUTH card actually needs this class: account-settings.css already
   has .account-settings__field-grid (collapses at 560px) and
   candidate-profile.css has .profile-form__row-grid (collapses at 767px), and
   the name pair reuses those on their own pages rather than adding a third
   near-identical grid. The 480px breakpoint here is tighter than either
   because the auth card is only ~420px wide to begin with, so the pair has to
   survive further down before stacking. */
.field-grid-2 {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-3);
}

@media (max-width: 479px) {
  .field-grid-2 {
    grid-template-columns: 1fr;
  }
}

/* Static, non-editable trailing affix sitting OUTSIDE a field
   (components.md §4). Built for the employer's locked "@domain" in Account
   Settings > Update Email, where only the username segment of the address may
   be edited (assets/js/lib/account-settings-actions.js#openUpdateEmailModal):

       [ Enter email username ]  @vanceli.com

   The affix is deliberately OUTSIDE the input's box, not seamed onto it. The
   input keeps its own complete border and all four radii, and the domain sits
   beside it as plain text with a gap. That separation is the whole message:
   what is inside the box is the reader's to change, what is outside is fixed.
   An earlier revision joined the two into one control, which read as a single
   editable field with the domain merely at its far end - the opposite of what
   the pattern needs to convey.

   NOT .field--with-action: that modifier reserves right padding for a 36px
   circular button absolutely positioned OVER the control, which assumes a
   fixed width. A domain string is variable-width, so this is a flex row.
   Closest existing precedent is .phone-field, but that one is specific to the
   country-select control.

   The affix is a <span>, never a disabled <input>: a disabled input is skipped
   by assistive tech and would contribute nothing. It is instead bound to the
   control via aria-describedby by the caller (design.md §4.5), so a
   screen-reader user hears the fixed domain as part of the field's
   description. */
.field--with-suffix {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* Takes the leftover width; min-width:0 stops the flex item refusing to shrink
   below its intrinsic size on narrow viewports. Border and radii are NOT
   overridden - the box stays visibly closed on all four sides. */
.field--with-suffix .field__control {
  flex: 1 1 auto;
  min-width: 0;
}

.field__suffix {
  display: inline-flex;
  align-items: center;
  min-height: 44px; /* aligns with .field__control's height, without a box */
  color: var(--text-secondary); /* muted: informational, not an input */
  font-size: var(--text-sm);
  white-space: nowrap; /* a domain must never wrap mid-string */
  user-select: none;
  flex: 0 0 auto;
}

/* No error rule is needed here: the base `.field__control[aria-invalid="true"]`
   above already reddens the input, and that is the only thing that should
   redden. There is no seam for an error border to carry across now, and
   tinting the domain would imply the FIXED part is what the reader got wrong. */

.field__error {
  margin-top: var(--space-2);
  font-size: var(--text-xs);
  color: var(--color-error);
  display: flex;
  align-items: center;
  gap: var(--space-1);
}

.field__error svg {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
}

.field__error:empty {
  display: none;
}

/*
 * ⚠️ REQUIRED GUARD - this one is worse than the usual [hidden] specificity tie
 * (.alert[hidden], .btn[hidden] etc.), and it has already bitten once.
 *
 * `display: flex` above is an AUTHOR rule, while `[hidden] { display: none }` is
 * a USER-AGENT rule, and author always beats UA regardless of specificity. So
 * `hidden` on a .field__error did nothing at all: an error paragraph that ships
 * its text in the markup renders from first paint. That is exactly what the Send
 * Email composer's "Enter a valid email address." did (components.md §90).
 *
 * `:empty` above cannot cover it either - the element is not empty, it is
 * hidden. Both rules are needed because both conventions exist in the codebase:
 * most call sites render an EMPTY <p class="field__error"> and set textContent
 * (account-settings-actions.js), which :empty handles; this covers the ones that
 * toggle `hidden` instead.
 */
.field__error[hidden] {
  display: none;
}

/* =========================================================================
 * Switch / Toggle (design.md §8 "Switch", components.md "Switch")
 * -------------------------------------------------------------------------
 * design.md §8 has specified a switch since the beginning (track fills
 * --accent when on, white thumb) but nothing implemented it - callers that
 * needed a boolean fell back to a bare accent-coloured checkbox (e.g. the
 * Template Builder Drawer's two sharing flags, components.md §41). Promoted
 * to a real component here on that drawer's restyle, deliberately OUTSIDE
 * the .template-drawer block so any other screen can use it.
 *
 * ⚠️ The <input> is a REAL, still-focusable checkbox - clipped out of sight
 * with the same technique as .visually-hidden rather than
 * `display:none`/`visibility:hidden`, both of which would drop it from the
 * tab order and kill Space-to-toggle. Keyboard operability (design.md §4.2)
 * therefore comes for free from the native control; the track/thumb are
 * purely decorative siblings driven by :checked. Nothing here needs JS, and
 * `change` fires exactly as it would on a plain checkbox.
 *
 * Hit area (design.md §4.4): the 44px minimum is met by the LABEL's padding,
 * not by inflating the 44x24 track - the whole row is the target, since a
 * <label> forwards clicks to its control.
 * ========================================================================= */
.switch {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 44px;
  padding: var(--space-2) 0;
  cursor: pointer;
}

.switch__input {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.switch__track {
  position: relative;
  flex-shrink: 0;
  width: 44px;
  height: 24px;
  border-radius: var(--radius-full);
  background: var(--border-default);
  transition: background-color var(--duration-fast) var(--ease-out);
}

.switch__thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 20px;
  height: 20px;
  border-radius: var(--radius-full);
  /* Literal white in both themes, per design.md §8 - the thumb reads against
     the --accent track, not against the page, so it must NOT follow the
     light/dark surface tokens. */
  background: #ffffff;
  box-shadow: var(--shadow-sm);
  transition: transform var(--duration-fast) var(--ease-out);
}

.switch__input:checked + .switch__track {
  background: var(--accent);
}

.switch__input:checked + .switch__track .switch__thumb {
  transform: translateX(20px);
}

/* The clipped input can still match :focus-visible, so the ring is drawn on
   the visible track instead - same 2px/2px geometry as the global rule in
   base.css §4.2. */
.switch__input:focus-visible + .switch__track {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

.switch__label {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-primary);
}

.switch__input:disabled + .switch__track {
  opacity: 0.4;
}

.switch:has(.switch__input:disabled) {
  cursor: not-allowed;
}

/* =========================================================================
 * Password Strength Checklist (design.md §8 validation states)
 * ========================================================================= */
.pw-strength {
  margin-top: var(--space-2);
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-1) var(--space-3);
}

.pw-strength__item {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--text-xs);
  color: var(--text-secondary);
}

.pw-strength__item svg {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  color: var(--text-disabled);
}

.pw-strength__item[data-met="true"] {
  color: var(--color-success);
}

.pw-strength__item[data-met="true"] svg {
  color: var(--color-success);
}

/* =========================================================================
 * Role Selector (Employer / Candidate) - composed from radio inputs styled
 * as a Tabs pill group (design.md §6 Tabs), no new tokens introduced.
 *
 * Compactness change: the visible pill was reduced from a 44px-tall control
 * to the ~36px small-control size, and the icon/text gap and the selector's
 * bottom margin were tightened - all via existing tokens. The 44x44px tap
 * target (design.md §4.4) is preserved by keeping the label a 44px box and
 * padding it with invisible space: `padding-block: var(--space-1)` pads 4px
 * top+bottom (44 - 8 = 36px content), and `background-clip: content-box`
 * keeps the checked fill inside that 36px content box so the *visible* pill
 * shrinks while the *tappable* label stays 44px.
 * ========================================================================= */
.role-select {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-2);
  background: var(--bg-base);
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-sm);
  padding: var(--space-1);
  /* Tightened from --space-5 (24px) to --space-4 (16px) for a more compact block */
  margin-bottom: var(--space-4);
}

.role-select__option {
  position: relative;
}

.role-select__input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}

.role-select__label {
  display: flex;
  align-items: center;
  /* Icon + text block centers together in the pill (title/desc no longer
     left-hugged) per components.md §5 hierarchy update. */
  justify-content: center;
  gap: var(--space-2);
  /* 48px tap target (grew from 44px) to fit two text lines + wider gap
     without clipping - design.md §4.4 minimum still exceeded. */
  min-height: 48px;
  padding-block: var(--space-2);
  padding-inline: var(--space-2);
  border-radius: var(--radius-xs);
  /* Checked fill (below) paints only the content box, not the invisible pad */
  background-clip: content-box;
  font-weight: 600;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}

.role-select__label svg {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
}

.role-select__text {
  display: flex;
  flex-direction: column;
  /* Title/desc left-aligned to each other; the cluster as a whole still
     centers in the pill via .role-select__label's justify-content: center. */
  align-items: flex-start;
  /* Gap between title and descriptor line, was relying on line-height alone */
  gap: 2px;
  line-height: 1.2;
}

.role-select__title {
  font-weight: 600;
  font-size: var(--text-sm);
}

.role-select__desc {
  font-weight: 500;
  font-size: var(--text-xs);
  color: var(--text-secondary);
  opacity: 0.8;
}

.role-select__input:checked + .role-select__label .role-select__desc {
  color: inherit;
}

.role-select__input:checked + .role-select__label {
  background: var(--accent);
  color: var(--accent-text-on);
}

.role-select__input:focus-visible + .role-select__label {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* =========================================================================
 * Phone Field / Country-Code Combobox (design.md §8, searchable ARIA combobox)
 * ========================================================================= */
.phone-field {
  display: flex;
  gap: var(--space-2);
}

/* ⚠️ When the number input carries a field affix (components.md "Field Affix"),
   it is wrapped in a `.field__input-wrap` and the WRAP becomes the flex item,
   not the input. Without these two declarations the wrap shrink-wraps its
   absolutely-positioned-only content and the number input collapses to nothing.
   `min-width: 0` is what lets it shrink inside a narrow column rather than
   forcing the row wider than its grid cell.
   The affix belongs to the NUMBER input only - never wrap `.country-select`,
   whose own panel is positioned against it. */
.phone-field > .field__input-wrap {
  flex: 1;
  min-width: 0;
}

.country-select {
  position: relative;
  flex-shrink: 0;
  width: 128px;
}

/* ---------------------------------------------------------------------------
   30/70 PHONE ROW (project spec) - candidate profile only.

   The selector carries flag + alpha-3 code + dial code, so it gets 30% of the
   row and the number input takes the rest.

   ⚠️ THE `min-width` FLOOR IS NOT OPTIONAL. The row width is not monotonic with
   viewport width: the profile builder's preview pane is a fixed
   minmax(360px, 452px) column that only stops consuming width at <=1080px, so
   the row is at its NARROWEST (~226px) at a 1081px viewport - narrower than at
   768px (~389px). 30% of 226px is 65px against a trigger that needs ~143px to
   show "(BGD) +880" without truncating. The floor holds the selector at a
   readable width wherever the percentage would starve it, and is inert on wide
   screens where 30% is the larger value. Measured, not estimated: 148px is the
   worst-case content width plus a few px of slack.

   ⚠️ SCOPED TO A MODIFIER, deliberately. The same component mounts in the
   Account Settings "Update Phone Number" modal (account-settings-actions.js,
   reached from BOTH employer and candidate settings), a narrow dialog that
   keeps the unmodified 128px selector.

   ⚠️ `width: auto` is load-bearing - it unsets the base `width: 128px`, which
   would otherwise resolve as the used width and defeat the flex basis. */
.phone-field--split > .country-select {
  flex: 0 0 30%;
  min-width: 148px;
  width: auto;
}

/* ⚠️ `flex: 1 1 0` rather than a `70%` basis. The row has an 8px `gap`, and two
   zero-shrink bases summing to 100% do NOT absorb it - the gap is added on top
   and the row overflows by 8px at every width (the bug this replaces). Letting
   the number input consume the remaining free space instead makes the row add
   up exactly, and is also what lets the selector's min-width floor take effect
   without pushing the row wider than its container.
   `min-width: 0` comes from the base `.phone-field > .field__input-wrap` rule
   above and is required for this to shrink below its intrinsic width. */
.phone-field--split > .field__input-wrap {
  flex: 1 1 0;
}

.country-select__trigger {
  width: 100%;
  height: 44px;
  display: flex;
  align-items: center;
  /* --space-1, not --space-2: the trigger now holds three content items (flag,
     alpha-3 code, dial code) plus the chevron, and they are one unit reading as
     a single chip. 8px gaps between them read as four separate controls. */
  gap: var(--space-1);
  padding: 0 var(--space-2);
  background: var(--bg-base);
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-xs);
  cursor: pointer;
  color: var(--text-primary);
}

.country-select__trigger:focus-visible {
  border-color: var(--focus-ring);
}

.country-select__flag {
  width: 22px;
  height: 16px;
  /* smallest scale token, no bespoke radius (design.md §3.3) */
  border-radius: var(--radius-xs);
  object-fit: cover;
  flex-shrink: 0;
  border: 1px solid var(--border-default);
}

/* ISO 3166-1 alpha-3 code on the trigger, e.g. "(AUS)". Same size and mono face
   as the dial code so the two read as one chip, but in the secondary colour -
   the code identifies, the dial code is the value, and the dial code should stay
   the more prominent of the pair. */
.country-select__code {
  font-size: var(--text-sm);
  font-family: var(--font-mono);
  color: var(--text-secondary);
}

.country-select__dial {
  font-size: var(--text-sm);
  font-family: var(--font-mono);
}

.country-select__chevron {
  width: 14px;
  height: 14px;
  margin-left: auto;
  color: var(--icon-muted);
  flex-shrink: 0;
}

.country-select__panel {
  position: absolute;
  top: calc(100% + var(--space-2));
  left: 0;
  width: 320px;
  max-width: 90vw;
  background: var(--bg-elevated);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  z-index: var(--z-drawer); /* above page chrome such as the bulk pill (--z-sticky) - see components.md §24 */
  padding: var(--space-2);
}

.country-select__panel[hidden] {
  display: none;
}

.country-select__search {
  width: 100%;
  height: 40px;
  padding: 0 var(--space-3);
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-xs);
  background: var(--bg-base);
  color: var(--text-primary);
  margin-bottom: var(--space-2);
}

.country-select__list {
  max-height: 260px;
  overflow-y: auto;
}

.country-select__option {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2);
  border-radius: var(--radius-xs);
  cursor: pointer;
  font-size: var(--text-sm);
}

.country-select__option:hover,
.country-select__option[aria-selected="true"] {
  background: var(--bg-surface);
}

.country-select__option-name {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.country-select__option-dial {
  color: var(--text-secondary);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
}

.country-select__empty {
  padding: var(--space-4);
  text-align: center;
  color: var(--text-secondary);
  font-size: var(--text-sm);
}

/* =========================================================================
 * OTP Input (design.md §8 - 6 segmented single-character boxes)
 * ========================================================================= */
.otp-input {
  display: flex;
  gap: var(--space-2);
  justify-content: center;
  margin-bottom: var(--space-4);
}

.otp-input__box {
  width: 48px;
  height: 56px;
  text-align: center;
  font-size: var(--text-h4);
  font-family: var(--font-mono);
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-xs);
  background: var(--bg-base);
  color: var(--text-primary);
}

.otp-input__box:focus-visible {
  border-color: var(--focus-ring);
}

.otp-input[data-invalid="true"] .otp-input__box {
  border-color: var(--color-error);
}

.otp-resend {
  text-align: center;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  margin-bottom: var(--space-4);
}

/* Inline action row for a step within a multi-step modal (Account Settings'
   Update Email / Reset Password flows, assets/js/lib/account-settings-actions.js)
   - same shape as .assessment-send__actions below, used when a single
   openModal() call renders more than one step and needs its own footer per
   step instead of openModal()'s single fixed actions footer. */
.acct-modal__actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border-default);
}

/* =========================================================================
 * Stepper / Wizard Progress (design.md §6)
 * -------------------------------------------------------------------------
 * Vertical step layout + container-driven labels (was: horizontal labels
 * beside nodes, gated on a @media (min-width:600px) VIEWPORT query with
 * fixed-width connectors). The stepper lives inside the ~420px .auth-card,
 * so a viewport gate showed labels on desktop that never fit the card - they
 * wrapped and squeezed the fixed connectors. Now:
 *   - labels sit BENEATH their numbered nodes (absolute, so the thin
 *     connectors still center on the node row with no offset math),
 *   - connectors FLEX to distribute evenly and fill the card width,
 *   - label visibility is gated on the CONTAINER width (.auth-card is a
 *     `container: auth-card / inline-size` query container) - labels appear
 *     only once the card is wide enough to seat all four without
 *     wrapping/overlap.
 * The stepper is aria-hidden; the "Step X of 4" eyebrow (design.md §6)
 * remains the textual step context for screen readers. States
 * (current/complete + the completed checkmark) are unchanged below.
 * ========================================================================= */
.stepper {
  display: flex;
  align-items: center; /* nodes + connectors share the node-height row; labels are absolute */
  gap: var(--space-2);
  margin-bottom: var(--space-6);
  padding: 0 var(--space-4); /* keep edge labels clear of the card padding */
}

.stepper__step {
  position: relative; /* anchor for the label placed beneath */
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
}

.stepper__node {
  width: 28px;
  height: 28px;
  border-radius: var(--radius-full);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-xs);
  font-weight: 700;
  background: var(--bg-base);
  border: 1.5px solid var(--border-default);
  color: var(--text-secondary);
}

.stepper__node svg {
  width: 14px;
  height: 14px;
}

.stepper__step[data-state="current"] .stepper__node {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-text-on);
}

.stepper__step[data-state="complete"] .stepper__node {
  background: var(--color-success-bg);
  border-color: var(--color-success);
  color: var(--color-success);
}

/* Label sits directly beneath its node, centered; never wraps (it is hidden
   instead when the container is too narrow - see the container query below). */
.stepper__label {
  position: absolute;
  top: calc(100% + var(--space-1));
  left: 50%;
  transform: translateX(-50%);
  font-size: var(--text-xs);
  color: var(--text-secondary);
  white-space: nowrap;
  text-align: center;
  display: none;
}

/* Connectors flex to fill the row so four steps span the card width evenly,
   instead of a fixed pixel width that compresses under the squeeze. */
.stepper__connector {
  flex: 1 1 auto;
  height: 1.5px;
  background: var(--border-default);
}

/* Container-width gate (NOT viewport): reveal the beneath-node labels only
   once the .auth-card is wide enough to seat all four without
   wrapping/overlap, and reserve the space beneath the nodes for them. Below
   this the stepper is a compact numbers-only row that fits any card width
   (down to 360px viewport). Container established on .auth-card
   (`container: auth-card / inline-size`). */
@container auth-card (min-width: 360px) {
  .stepper {
    padding-bottom: var(--space-5);
  }
  .stepper__label {
    display: block;
  }
}

/* =========================================================================
 * Inline Alert / Banner (design.md §10)
 * ========================================================================= */
.alert {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  margin-bottom: var(--space-4);
}

/* `.alert` and the browser's default `[hidden]{display:none}` share the same
   specificity (one class vs. one attribute selector) - without this, `.alert`
   wins by appearing later in the cascade and a hidden alert (e.g. Account
   Settings' Delete Account gate, components.md §49) stays visible. Same fix
   already applied to .dashboard-sidebar__badge[hidden] in dashboard-shell.css. */
.alert[hidden] {
  display: none;
}

.alert svg {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  margin-top: 1px;
}

.alert-error {
  background: var(--color-error-bg);
  color: var(--color-error);
}

.alert-success {
  background: var(--color-success-bg);
  color: var(--color-success);
}

.alert-info {
  background: var(--color-info-bg);
  color: var(--color-info);
}

.alert-warning {
  background: var(--color-warning-bg);
  color: var(--color-warning);
}

/* -------------------------------------------------------------------------
 * Dark mode. The four `--color-*-bg` tints are LIGHT and are deliberately not
 * overridden in tokens.css (only the `--color-*` foregrounds are), so in dark
 * mode an untreated alert pairs a bright foreground with a near-white tint -
 * e.g. Account Settings' Delete Account gate (§49) rendered #fbbf24 on
 * #fef3c7, ~1.5:1. Unreadable, and it fails design.md §1.3's contrast floor.
 *
 * Fixed with the same transparent-wash approach already used for the
 * Shortlist card's delete-hover state (pages/shortlists.css): drop to the
 * neutral elevated surface and let the semantic FOREGROUND plus the paired
 * icon carry the meaning (§1.3's "never colour alone" rule is satisfied by
 * the icon, which every .alert already includes).
 *
 * Applied to all four variants together rather than only the warning one the
 * Delete Account gate needs - fixing one and leaving three broken is the
 * actual inconsistency. Both dark selectors are spelled out because the
 * manual toggle and OS-preference paths are independent.
 * ------------------------------------------------------------------------- */
:root[data-theme="dark"] .alert-error,
:root[data-theme="dark"] .alert-success,
:root[data-theme="dark"] .alert-info,
:root[data-theme="dark"] .alert-warning {
  background: var(--bg-elevated);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .alert-error,
  :root:not([data-theme="light"]) .alert-success,
  :root:not([data-theme="light"]) .alert-info,
  :root:not([data-theme="light"]) .alert-warning {
    background: var(--bg-elevated);
  }
}

/* An action link inside an alert (design.md §10: "semantic light-tint
   background + matching icon + optional action link"). Inherits the alert's
   semantic colour so it works across all four variants and both themes
   without a per-variant rule; underlined so it is not signalled by colour
   alone (§4.6). No min-height - §4.4's 44px floor governs button-like
   controls, not a text link inside a running sentence. */
.alert__link {
  color: inherit;
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* A dismissible alert (components.md §20 - the employer group-email notice is
   the first consumer, but this is a plain `.alert` variant and any screen can
   use it). The only structural change the close button needs: the message
   claims the free space so the button is pushed to the trailing edge, and the
   row centres rather than top-aligning, because a one-to-two-line message with
   a control on the right reads as a bar, not as a hanging paragraph. */
.alert--dismissible > span {
  flex: 1;
}

/* Inherits the alert's semantic colour, so one rule covers all four variants
   and both themes - the same approach as .alert__link above, and the reason no
   dark-mode override is needed here (the `.alert-*` dark rules recolour the
   whole block). `.btn-icon` supplies the sizing, hit area and focus ring; this
   only strips the surface so the button sits ON the tinted alert rather than
   stacking a second chip on top of it. */
.alert__close {
  color: inherit;
  background: transparent;
  border: none;
  flex-shrink: 0;
  /* `.btn-icon` is a fixed 44x44 target (design.md §4.4) - larger than this
     row's text. Negative block margins stop that target from inflating the
     alert's height, and the inline pull tucks the oversized hit area back into
     the alert's own padding so the glyph reads as inset from the right edge.
     The 44px target itself is untouched; only its visual footprint is. */
  margin-block: calc(var(--space-2) * -1);
  margin-inline-end: calc(var(--space-2) * -1);
}

/* Overrides `.btn-icon:hover`'s opaque --bg-surface, which would punch a
   canvas-coloured disc through the alert's tint. */
.alert__close:hover:not(:disabled) {
  /* A wash of the inherited semantic colour, so the hover state works on all
     four variants and both themes without a per-variant rule. */
  background: color-mix(in srgb, currentColor 12%, transparent);
}

/* =========================================================================
 * Candidate Welcome Card (design.md §13.2, components.md §67)
 * -------------------------------------------------------------------------
 * A PERMANENT banner pinned to the top of the candidate Profile page - it is
 * never dismissed, only COLLAPSED to a single greeting line and expanded again.
 * THEME-INVARIANT fixed-brand surface: a flat saturated indigo fill
 * (--welcome-banner-surface) under white text, using the fixed
 * --welcome-banner-* tokens so it stays the same in both themes - the same
 * convention as the fixed-dark footer / auth-promo (§16.1), branded indigo
 * instead of navy. All colours are tokens (no hardcoded hex here). Copy states,
 * collapse behavior and persistence live in welcome-card.js / welcome-store.js.
 * Placed in components.css (not a page file) so it can be reused on other
 * candidate screens later.
 *
 * Layout: two columns - text content left, the 2x2 action grid right.
 * ========================================================================= */
.welcome-card {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-6);
  padding: var(--space-6) var(--space-7);
  margin-bottom: var(--space-5);
  border-radius: var(--radius-lg); /* 12px image container (design.md §3.3) */
  background: var(--welcome-banner-surface);
  color: var(--welcome-banner-ink);
}

/* Same specificity guard as .alert[hidden]: `.welcome-card` (one class) ties
   the browser default [hidden]{display:none} (one attribute), so without this
   an authored-hidden card would paint before welcome-card.js un-hides it. */
.welcome-card[hidden] {
  display: none;
}

.welcome-card__content {
  position: relative; /* over the __bg + scrim layers */
  z-index: 1;
  min-width: 0; /* grid child: let long copy wrap instead of widening the column */
}

/* The expanded body (eyebrow / title / subtext / bullets). Hidden wholesale in
   the collapsed state - see .welcome-card--collapsed below. */
.welcome-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* The single-line greeting shown ONLY while collapsed ("Welcome to Vanceli,
   Mark"). welcome-card.js fills it; it is empty and hidden while expanded. */
.welcome-card__collapsed-line {
  display: none;
  margin: 0;
  padding-right: var(--control-icon-btn); /* clear the toggle in the corner */
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--welcome-banner-ink);
}

.welcome-card__eyebrow {
  margin: 0;
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em; /* overline treatment (design.md §2.2) */
  color: var(--welcome-banner-ink-soft);
}

.welcome-card__title {
  margin: 0;
  /* text-h5 is the in-app type cap (design.md §2.2) - the largest heading step
     allowed on authenticated screens; weight carries the prominence. */
  font-size: var(--text-h5);
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--welcome-banner-ink);
}

.welcome-card__subtext {
  margin: 0;
  /* Both copy states must sit on ONE line at desktop widths (project spec); the
     old 48ch measure wrapped them at two. 64ch still stops the line from running
     the full banner width on very wide viewports. */
  max-width: 64ch;
  font-size: var(--text-sm);
  color: var(--welcome-banner-ink-soft);
}

/* Three benefit bullets in a row; wrap/stack on narrow banners. */
.welcome-card__bullets {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-5);
  margin: var(--space-1) 0 0;
  padding: 0;
  list-style: none;
}

.welcome-card__bullet {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--welcome-banner-ink);
}

/* Small circular chip holding the check glyph (design.md §13.2). */
.welcome-card__check {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  border-radius: var(--radius-full);
  background: var(--welcome-banner-chip);
  color: var(--welcome-banner-ink);
}

.welcome-card__check svg {
  width: 14px;
  height: 14px;
}

/* =========================================================================
 * Action buttons - the 2x2 grid on the banner's right side.
 * Navigation links (<a>), styled as buttons. LEGIBILITY over the artwork is the
 * whole job: the translucent-white fill + white-alpha border must carry the
 * contrast on their OWN (design.md §4.1); backdrop-filter is enhancement only
 * and browsers without it lose nothing that matters.
 * ========================================================================= */
.welcome-card__actions {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
  /* Capped so the left column keeps enough room for the three benefit bullets to
     stay on ONE row at desktop widths.
     DEFINITE width on purpose: this sits in the card's `auto` grid track, and a
     percentage here would resolve against that track - which itself grows to
     absorb margin-right, so the block widened instead of moving. Fixed width +
     margin = a real leftward shift. The <=767px block swaps this to 100% once
     the columns stack. */
  width: 580px;
  max-width: 100%;
  /* Nudged clear of the collapse caret (top-right, __toggle) so the button
     block doesn't run under it - wide enough that the caret keeps visible
     breathing room next to the longer labels. */
  margin-right: calc(var(--space-8) - var(--space-1)); /* 48 - 4 = 44px */
}

.welcome-card__action {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--control-icon-btn); /* 44px hit target (design.md §4.4) */
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--welcome-banner-action-border);
  border-radius: var(--radius-md);
  background: var(--welcome-banner-action-bg);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  color: var(--welcome-banner-ink);
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1.3;
  text-align: center;
  text-decoration: none;
  white-space: nowrap; /* force one-line labels now grid is wide enough */
  transition: background var(--duration-fast) var(--ease-out);
}

.welcome-card__action:hover {
  background: var(--welcome-banner-action-bg-hover);
}

/* Same white-ink ring substitution as the toggle below (design.md §4.2):
   --focus-ring (accent-600) is low-contrast on indigo. */
.welcome-card__action:focus-visible {
  outline: 2px solid var(--welcome-banner-ink);
  outline-offset: 2px;
}

/* Overlay-style collapse/expand toggle, design.md §3.7/§4.4: 20px Phosphor
   caret in a 44px hit area. */
.welcome-card__toggle {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  z-index: 2; /* above the content + actions columns */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--control-icon-btn);
  height: var(--control-icon-btn);
  padding: 0;
  border: 0;
  border-radius: var(--radius-full);
  background: transparent;
  color: var(--welcome-banner-ink-soft);
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}

.welcome-card__toggle:hover {
  background: var(--welcome-banner-chip);
  color: var(--welcome-banner-ink);
}

.welcome-card__caret {
  width: var(--icon-size-close);
  height: var(--icon-size-close);
}

/* Ring substitution on the brand surface (design.md §4.2): --focus-ring
   (accent-600) is low-contrast on indigo, so the ring uses the white ink. */
.welcome-card__toggle:focus-visible {
  outline: 2px solid var(--welcome-banner-ink);
  outline-offset: 2px;
}

/* Exactly one caret at a time: up = "collapse me" while expanded, down =
   "expand me" while collapsed. Both are authored in the markup so the swap is
   pure CSS (no innerHTML churn on every toggle). */
.welcome-card__caret--down,
.welcome-card--collapsed .welcome-card__caret--up {
  display: none;
}
.welcome-card--collapsed .welcome-card__caret--down {
  display: block;
}

/* =========================================================================
 * Collapsed state - one greeting line, nothing else. The whole card is the
 * expand target (welcome-card.js), hence the pointer cursor; clicks that land
 * on the toggle or on an action link are excluded there, not here.
 * ========================================================================= */
.welcome-card--collapsed {
  grid-template-columns: minmax(0, 1fr);
  padding: var(--space-4) var(--space-7);
  cursor: pointer;
}

.welcome-card--collapsed .welcome-card__body,
.welcome-card--collapsed .welcome-card__actions {
  display: none;
}

.welcome-card--collapsed .welcome-card__collapsed-line {
  display: block;
}

/* Entrance fade - motion only when the user hasn't asked to reduce it
   (design.md §3.5). Under reduced-motion the card simply appears. */
@media (prefers-reduced-motion: no-preference) {
  .welcome-card {
    animation: welcome-card-in var(--duration-base) var(--ease-out);
  }
  @keyframes welcome-card-in {
    from {
      opacity: 0;
      transform: translateY(-4px);
    }
    to {
      opacity: 1;
      transform: none;
    }
  }
}

/* Below Tablet (design.md §3.2): the two columns stack - the actions grid drops
   under the copy and spans the full width, still 2x2. */
@media (max-width: 767px) {
  .welcome-card {
    grid-template-columns: minmax(0, 1fr);
    padding: var(--space-5);
  }
  .welcome-card__actions {
    width: 100%;
    margin-right: 0;
  }
  .welcome-card--collapsed {
    padding: var(--space-4) var(--space-5);
  }
}

/* Below ~480px two columns of buttons squeeze the longer labels
   ("Be More Visible to Employers") into four lines - go single-column. */
@media (max-width: 479px) {
  .welcome-card__actions {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* =========================================================================
 * Toast / Snackbar (design.md §10)
 * ========================================================================= */
.toast-region {
  position: fixed;
  bottom: var(--space-5);
  right: var(--space-5);
  left: var(--space-5);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-2);
  pointer-events: none;
}

/* A right-anchored drawer's sticky footer (§25) occupies the same
   bottom-right corner this region normally anchors to - shift toasts clear
   of it while any drawer is open (class toggled alongside the scroll lock,
   see assets/js/lib/scroll-lock.js callers) so a toast can never render on
   top of - and block clicks on - the footer's action buttons. Desktop: the
   drawer is a fixed 480px right panel, so toasts move to sit just left of
   it, still bottom-right of the remaining visible content. Mobile: the
   drawer is full-width, so there's no "beside" option - toasts move above
   the footer's height instead. */
body.drawer-open .toast-region {
  right: calc(480px + var(--space-5));
}

@media (max-width: 599px) {
  body.drawer-open .toast-region {
    right: var(--space-5);
    bottom: calc(var(--space-5) + 140px);
  }
}

.toast {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  max-width: 380px;
  background: var(--bg-elevated);
  border: 1px solid var(--border-default);
  box-shadow: var(--shadow-lg);
  border-radius: var(--radius-sm);
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-sm);
  animation: toast-in var(--duration-slow) var(--ease-out);
}

.toast[data-variant="success"] svg {
  color: var(--color-success);
}
.toast[data-variant="error"] svg {
  color: var(--color-error);
}
.toast svg {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
}

@keyframes toast-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* =========================================================================
 * Success / Confirmation panel (used by get-started + reset-password Step 4)
 * ========================================================================= */
.confirm-panel {
  text-align: center;
  padding: var(--space-4) 0;
}

.confirm-panel__icon {
  width: 56px;
  height: 56px;
  border-radius: var(--radius-full);
  background: var(--color-success-bg);
  color: var(--color-success);
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 auto var(--space-4);
}

.confirm-panel__icon svg {
  width: 28px;
  height: 28px;
}

/* =========================================================================
 * Modal / Dialog (design.md §10 - "Focused task or confirmation requiring
 * full attention"; also covers the Confirmation dialog variant). First real
 * usage of --z-modal. See assets/js/lib/modal.js and components.md "Modal /
 * Dialog" / "Confirmation Dialog Helper" entries.
 * ========================================================================= */
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(6, 27, 49, 0.5);
  z-index: var(--z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-5);
}

@media (max-width: 599px) {
  .modal-backdrop {
    padding: var(--space-3);
  }
}

.modal {
  width: 100%;
  max-width: 560px;
  max-height: 90vh;
  overflow-y: auto;
  background: var(--bg-elevated);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-xl);
}

/*
 * Wide variant - openModal({ size: "wide" }). For COMPOSER-shaped content only
 * (today: the Send Email modal, components.md §90), where a rich text editor
 * plus four stacked fields need room to breathe. Confirmations and short forms
 * stay at 560px: a wide measure makes running prose harder to read, so this is
 * a container affordance, not an upgrade.
 */
.modal--wide {
  max-width: 720px;
}

/*
 * The wide variant PINS its header and footer and scrolls only the body, where
 * the default .modal scrolls as one block.
 *
 * That difference is deliberate and scoped: a short confirmation never
 * overflows, so scrolling the whole panel costs it nothing. A composer always
 * exceeds 90vh once the editor has a few paragraphs in it, and with the default
 * behaviour the primary action (Send Email) scrolls off the bottom - the
 * employer finishes typing and the button they need is gone. Pinning it also
 * keeps "Show Preview" reachable at the moment it is most useful.
 *
 * overflow: hidden on the panel is what hands scrolling to the body; without it
 * both would scroll and the pinning would not hold.
 */
.modal--wide {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.modal--wide .modal__header,
.modal--wide .modal__footer {
  flex-shrink: 0;
}

.modal--wide .modal__body {
  overflow-y: auto;
}

.modal__header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-5);
  border-bottom: 1px solid var(--border-default);
}

.modal__title {
  font-size: var(--text-h5);
  margin: 0;
}

.modal__body {
  padding: var(--space-5);
}

.modal__footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  flex-wrap: wrap;
  padding: var(--space-5);
  border-top: 1px solid var(--border-default);
}

/*
 * Split footer - openModal({ actions: [{ align: "start" }] }). Pushes one
 * action to the left end while the rest stay right-aligned (Send Email modal:
 * Show Preview left / Send Email right, components.md §90).
 *
 * margin-right:auto rather than justify-content:space-between so the RIGHT
 * cluster keeps its own gap and ordering no matter how many actions it holds -
 * space-between would spread three buttons across the whole width instead.
 * Note the footer wraps, at which point the auto margin collapses harmlessly
 * and the buttons stack in DOM order.
 */
.modal__footer-action--start {
  margin-right: auto;
}

/* =========================================================================
 * Badge (design.md §9 - status pill, used minimally in this build)
 * ========================================================================= */
.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-full);
  font-size: var(--text-xs);
  font-weight: 600;
}

.badge-employer {
  background: var(--color-info-bg);
  color: var(--color-info);
}

.badge-candidate {
  background: var(--color-success-bg);
  color: var(--color-success);
}

.badge-neutral {
  background: var(--bg-elevated);
  color: var(--text-secondary);
  border: 1px solid var(--border-default);
}

/* Generic semantic-color modifiers (distinct from the role-specific
   .badge-employer/.badge-candidate above) - first used by the candidate
   card's assessment-status badge (Pending/Started/Completed/Accepted/
   Rejected), see components.md "Candidate Card". */
.badge-info {
  background: var(--color-info-bg);
  color: var(--color-info);
}

.badge-warning {
  background: var(--color-warning-bg);
  color: var(--color-warning);
}

.badge-success {
  background: var(--color-success-bg);
  color: var(--color-success);
}

/* Same pattern as .badge-success/.badge-warning/.badge-info above. */
.badge-error {
  background: var(--color-error-bg);
  color: var(--color-error);
}

/* -------------------------------------------------------------------------
 * Dark mode - same light-tint problem, and the same fix, as the `.alert-*`
 * variants above (see that block's comment for the full rationale). The
 * earlier note here recorded the mismatch as "consistent with the existing
 * badges' dark-mode behavior, not a new inconsistency"; that was true of the
 * badges relative to each other, but the pairing still fails design.md §1.3
 * on its own terms - Account Settings' "Unverified" badge (§49) rendered
 * #f87171 on #fee2e2, ~2.3:1.
 *
 * All six semantic/role variants are corrected together. `.badge-neutral` is
 * excluded: it already uses --bg-elevated in both themes.
 *
 * ⚠️ --bg-base here, NOT the --bg-elevated the alerts use. Badge text is 12px
 * (--text-xs), so it needs the full 4.5:1; on --bg-elevated the error variant
 * measures only 4.26:1 and fails. Measured against --bg-base: error 6.08,
 * info 9.31, success 9.66, warning 10.08 - all AA. --bg-surface would pass
 * too but is the exact colour of the cards these badges sit on, so the pill
 * would vanish; --bg-base reads as a darker inset pill against the card.
 * ------------------------------------------------------------------------- */
:root[data-theme="dark"] .badge-employer,
:root[data-theme="dark"] .badge-candidate,
:root[data-theme="dark"] .badge-info,
:root[data-theme="dark"] .badge-warning,
:root[data-theme="dark"] .badge-success,
:root[data-theme="dark"] .badge-error {
  background: var(--bg-base);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .badge-employer,
  :root:not([data-theme="light"]) .badge-candidate,
  :root:not([data-theme="light"]) .badge-info,
  :root:not([data-theme="light"]) .badge-warning,
  :root:not([data-theme="light"]) .badge-success,
  :root:not([data-theme="light"]) .badge-error {
    background: var(--bg-base);
  }
}

/* =========================================================================
 * Chip (design.md §1.1 - color-icon fill-background pattern, §8 Tag/skills
 * input). Used for filter selections (Skills/Work Classification/Location) and
 * candidate skill tags.
 *
 * Light mode follows design.md §1.1 literally: color-icon fill + dark text.
 * Dark mode deliberately does NOT reuse color-icon here (that token is
 * already the primary-button fill in dark mode - reusing it for low-emphasis
 * chips would blur the button/chip visual hierarchy), so dark-mode chips use
 * a neutral elevated surface instead.
 * ========================================================================= */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-full);
  font-size: var(--text-xs);
  font-weight: 600;
  background: var(--color-icon);
  color: var(--color-text-brand);
}

:root[data-theme="dark"] .chip {
  background: var(--bg-elevated);
  color: var(--text-primary);
  border: 1px solid var(--border-default);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .chip {
    background: var(--bg-elevated);
    color: var(--text-primary);
    border: 1px solid var(--border-default);
  }
}

.chip button {
  display: inline-flex;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  color: inherit;
}

.chip button svg {
  width: 12px;
  height: 12px;
}

.chip-static {
  background: var(--bg-elevated);
  color: var(--text-secondary);
  border: 1px solid var(--border-default);
}

/* Same guard as .alert[hidden] / .btn[hidden]: one class ties the browser
   default [hidden]{display:none}, and the later `display: inline-flex` wins the
   tie. Added pre-emptively - nothing toggles a chip with `hidden` today, and the
   next thing that does should not have to rediscover this. */
.chip[hidden] {
  display: none;
}

/* =========================================================================
 * Multi-Select Combobox (design.md §8, components.md "Multi-Select Combobox")
 * Skills / Work Classification / Location (countries + cities) filters.
 * ========================================================================= */
.multi-select {
  position: relative;
}

/* Compact-control variant (design.md §8 "Search bar with filters", 40px;
   components.md §20/§21). The trigger is SPLIT into two boxes so the pill can
   shrink without shrinking the tap target - same intent as .role-select__label
   above, but done with an inset ::before because this control has a 1.5px
   border, which `background-clip: content-box` cannot inset:
     - the <button> box itself stays >= 44px (design.md §4.4 touch target) and
       is fully transparent/border-less;
     - ::before draws the 40px VISIBLE pill (fill + border + radius), inset and
       vertically centred inside that 44px box.
   Because the outer box is unchanged at 44px, nothing in the filters row
   reflows - only the painted pill gets shorter.

   ⚠️ GHOST TREATMENT (approved - design.md §8, components.md §20/§21): the pill
   is now INVISIBLE at rest - no fill, no border. Filters are a secondary,
   refining action and used to compete with the search field for attention as a
   row of five bordered boxes; quiet at rest means the eye lands on search first
   and on whatever is actually APPLIED second. The ::before is kept (rather than
   deleted) because it is still what paints the applied tint, the hover surface
   and the focus ring at the 40px visible height. Rest color is --text-secondary
   so an untouched filter reads as quiet without dropping below the 4.5:1 body
   floor (design.md §4.1). */
.multi-select__trigger {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  /* +1.5px matches the border the pill draws on ::before, so the text inset
     from the visible edge is identical to the old bordered-button version. */
  padding: 0 calc(var(--space-3) + 1.5px);
  background: transparent;
  border: none;
  color: var(--text-secondary);
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
  transition: color var(--duration-fast) var(--ease-out);
}

.multi-select__trigger::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  height: 40px; /* design.md §8 compact control / design.md §7 md button */
  /* Ghost at rest. Both declared (rather than omitted) so hover/active/focus
     below only swap COLORS - a border that appears on hover would shift the
     label by 1.5px and make the row twitch. */
  background: transparent;
  border: 1.5px solid transparent;
  border-radius: var(--radius-xs);
  pointer-events: none;
  transition: background-color var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out);
}

/* Hover surface. ⚠️ --bg-elevated, NOT the --bg-surface used by the canonical
   ghost pattern (.btn-ghost, §7): that pattern assumes it sits on --bg-base,
   but --bg-surface IS #fafbfb against a #ffffff canvas, so the hover would be
   invisible in light mode on this bar. --bg-elevated is an existing token
   (#f2f4f4 light / #213c3f dark) and reads in both. */
/* ⚠️ Both hover rules are guarded with :not(:disabled). A disabled trigger is a
   real disabled <button> (multi-select.js#setDisabled), and without the guard it
   would still light up its pill and darken its ink on hover - reading as
   interactive when it is not. Same guard .search-filter-bar__filters-btn
   already carries. */
.multi-select__trigger:hover:not(:disabled)::before {
  background: var(--bg-elevated);
}

.multi-select__trigger:hover:not(:disabled) {
  color: var(--text-primary);
}

/* Disabled treatment (design.md §14.3, components.md §20). Used by the
   Candidate Search bar for Skills / Experience before a search is initiated, and
   again afterwards for any filter whose dynamic facet came back empty.

   ⚠️ It must read as "NOT YET", not as "broken": the pill keeps its border and
   its place in the row, and only the ink drops to --text-disabled. Dimming the
   whole control toward invisibility would defeat the reason these filters are
   shown-but-disabled rather than hidden - the employer is meant to see what
   searching will unlock. Mirrors .search-filter-bar__filters-btn:disabled. */
.multi-select__trigger:disabled {
  color: var(--text-disabled);
  cursor: not-allowed;
}

.multi-select__trigger:disabled svg {
  opacity: 0.5;
}

/* The ::before is a child box and would otherwise paint over the label/badge/
   chevron. Lifting the real children into their own paint layer keeps them on
   top without a negative z-index (which would sink the pill behind the
   ancestor .search-filter-bar background, since .multi-select is only
   `position: relative` and forms no stacking context). */
.multi-select__trigger > * {
  position: relative;
}

.multi-select__trigger:focus-visible {
  outline: none;
}

.multi-select__trigger:focus-visible::before {
  border-color: var(--focus-ring);
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* Applied state (design.md §8, components.md §20/§21). This is the ONE state
   where a filter is allowed to be loud - it is the whole point of the ghost
   rest state above, and the only way to tell an applied filter from an idle one
   now that neither carries a border. Fill/ink/weight switch together, so
   "applied" is never signalled by color alone (design.md §4.6), and the count
   badge adds a non-color cue on top. Tokens are the mode-aware applied-filter
   set from tokens.css, NOT a literal accent tint - see the comment there for
   why. Measured 10.56:1 (light) / 7.01:1 (dark), both past the 4.5:1 floor. */
.multi-select__trigger.is-active {
  color: var(--accent-ink);
  font-weight: 700;
}

.multi-select__trigger.is-active::before {
  background: var(--accent-tint);
  border-color: var(--accent-tint-border);
}

.multi-select__trigger.is-active svg {
  color: var(--accent-ink);
}

/* The label is STATIC (components.md §20/§21): it never takes on the selected
   value, so a trigger's width does not depend on what is selected and the §20
   trigger row keeps fixed geometry. Selected values live as chips in the shared
   .search-filter-bar__active-filters band; the trigger only gains a count
   badge. The cap below is now just a defensive bound on a long *label*. */
.multi-select__trigger-label {
  max-width: 22ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.multi-select__trigger svg {
  /* 14px chevron, tuned to the 40px compact pill (design.md §8) */
  width: 14px;
  height: 14px;
  color: var(--icon-muted);
  flex-shrink: 0;
}

.multi-select__count-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  height: 20px;
  padding: 0 var(--space-1);
  border-radius: var(--radius-full);
  background: var(--accent);
  color: var(--accent-text-on);
  font-size: 11px;
  font-weight: 700;
}

.multi-select__count-badge[hidden] {
  display: none;
}

.multi-select__panel {
  position: absolute;
  top: calc(100% + var(--space-2));
  left: 0;
  width: 300px;
  max-width: 90vw;
  background: var(--bg-elevated);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  z-index: var(--z-drawer); /* above page chrome such as the bulk pill (--z-sticky) - see components.md §24 */
  padding: var(--space-3);
}

.multi-select__panel[hidden] {
  display: none;
}

/* Portalled popover (components.md §21, approved FLAG C). In popover mode the
   panel is appended to <body> and positioned with `fixed` coordinates by
   multi-select.js, so it can never be clipped by the sticky toolbar's own box
   or by any `overflow` on an ancestor - the reason the mockup's popover has to
   escape its wrapper. `fixed` (not `absolute`) is what keeps it locked to the
   trigger while the sticky bar stays put during page scroll; the controller
   re-measures on scroll/resize. Left/top are set inline by the controller. */
.multi-select__panel--portal {
  position: fixed;
  top: 0;
  left: 0;
  width: 280px;
}

/* Deferred Clear/Apply footer (approved FLAG C - multi-selects only; the
   Experience/Salary range sliders deliberately keep live-apply, see §22).
   Nothing the user checks reaches the result set until Apply is pressed, so
   the footer is the commit point and Esc/outside-click discards the draft. */
.multi-select__footer {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border-default);
}

.multi-select__footer .btn {
  flex: 1;
  justify-content: center;
}

/* Keyboard cursor for Arrow/Home/End/type-ahead navigation of the option list
   (design.md §4.3). Distinct from :hover so pointer and keyboard positions do
   not fight, and paired with aria-activedescendant on the search input. */
.multi-select__option.is-cursor {
  background: var(--bg-surface);
  box-shadow: inset 2px 0 0 var(--accent);
}

.multi-select__search-wrap {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0 var(--space-2);
  height: 40px;
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-xs);
  margin-bottom: var(--space-2);
}

.multi-select__search-wrap svg {
  width: 16px;
  height: 16px;
  color: var(--icon-muted);
  flex-shrink: 0;
}

/* Single focus treatment for the whole search field: the ring is drawn once on
   the WRAPPER whenever focus lands on the input inside it (mirrors
   .search-filter-bar__search:focus-within). Without this, the global
   `input:focus-visible` rule in base.css draws a second, smaller ring nested
   inside the wrapper's border - the doubled-stroke bug. */
.multi-select__search-wrap:focus-within {
  border-color: var(--border-strong);
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

.multi-select__search {
  flex: 1;
  border: none;
  background: transparent;
  outline: none;
  color: var(--text-primary);
  font-size: var(--text-sm);
  min-width: 0;
}

/* ...so the input must NOT draw its own ring on top of the wrapper's, or a
   keyboard user sees two nested rings. Class specificity (0,2,0) beats the
   global `input:focus-visible` (0,1,1) - same mechanism the main search field
   relies on. */
.multi-select__search:focus-visible {
  outline: none;
}

.multi-select__options {
  /* 300px, not the original 240px: a GROUPED list (components.md §21, the
     Candidates screen's Sub-Classification filter) spends rows on its headings, so
     the shorter box showed barely two entries per classification before scrolling.
     Ungrouped lists simply get a little more breathing room. */
  max-height: 300px;
  overflow-y: auto;
}

/* Group heading for `option.group` lists (components.md §21) - the small
   all-caps classification name above each run of sub-classifications in the Sub-Classification
   filter (design.md §14).

   Type recipe is lifted verbatim from .range-slider__number-label (the
   "Minimum years" / "Maximum years" labels in the Experience filter, §22
   below), because the spec asks for that exact size relationship: it is a
   quiet structural label, not content. Uppercase + tracking is the only
   addition, to separate it from the option rows without needing more weight
   or color contrast.

   The separator is a top border on every heading EXCEPT the first, so groups
   read as sections rather than as a boxed list. `aria-hidden` is set on the
   element in multi-select.js - it is a visual marker only. */
.multi-select__group-label {
  font-size: var(--text-xs);
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-weight: 700;
  padding: var(--space-2) var(--space-2) var(--space-1);
}

.multi-select__group-label:not(:first-child) {
  margin-top: var(--space-2);
  border-top: 1px solid var(--border-default);
}

.multi-select__option {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2);
  border-radius: var(--radius-xs);
  cursor: pointer;
  font-size: var(--text-sm);
}

.multi-select__count,
.facet-count {
  margin-left: auto;
  font-size: var(--text-xs);
  color: var(--text-muted);
  font-weight: var(--font-medium);
  font-variant-numeric: tabular-nums;
  padding-left: var(--space-2);
}

.multi-select__option:hover {
  background: var(--bg-surface);
}

/* ---------------------------------------------------------------------------
   Cap treatment for the `maxSelected` config (components.md §21, §86).
   Applied ONLY to the still-unchecked rows once the cap is reached - the
   checked ones stay fully live so the user can always swap one out. Follows
   design.md §14.3's rule for a disabled control: it keeps its shape and its
   place, and only its ink dims, so the row reads as "not yet" rather than
   "broken". The hover fill is cancelled too, or a locked row would still
   light up under the pointer and look actionable.
   --------------------------------------------------------------------------- */
.multi-select__option.is-disabled {
  color: var(--text-muted);
  cursor: not-allowed;
}

.multi-select__option.is-disabled:hover {
  background: transparent;
}

/* Both input types, so a SINGLE-SELECT group keeps the identical row metrics as
   a multi-select one. The Advanced Search overlay's Type group (components.md
   §80) renders radios rather than checkboxes; without `[type="radio"]` here it
   would fall back to the UA's default size and accent and sit visibly smaller
   and differently-coloured than the Gender/Availability rows above it. The
   round-vs-square glyph is the ONLY intended difference between them - that is
   what signals "pick one" versus "pick any". */
.multi-select__option input[type="checkbox"],
.multi-select__option input[type="radio"] {
  accent-color: var(--accent);
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}

/* Free-text "Use {typed}" row (allowCreate config, components.md §51/§57) */
.multi-select__option--create {
  color: var(--accent);
  font-weight: 600;
}

.multi-select__option--create svg {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}

.multi-select__empty {
  padding: var(--space-4);
  text-align: center;
  color: var(--text-secondary);
  font-size: var(--text-sm);
}

/* Default (inline mode, §57): the cluster is its own wrapping row under the
   trigger inside rootEl. When a caller passes a `chipsContainer` the cluster is
   moved there instead and this flex box is dissolved - see the
   .search-filter-bar__active-filters block below. */
.multi-select__chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-top: var(--space-2);
}

.multi-select__chips:empty {
  margin-top: 0;
}

/* =========================================================================
 * FIELD VARIANT - `.multi-select--field` (components.md §21)
 *
 * The same combobox rendered as a FORM CONTROL rather than a filter pill: a
 * full-width 44px bordered field matching `.field__control` and
 * `.searchable-select__trigger` exactly, with the selection still shown as
 * chips underneath. First consumers: the Candidate Profile's Career Sub Classification
 * (§87) and Work Experience > Skills (§57), which sit beside real form fields
 * - a bare-text ghost trigger next to a bordered select reads as a link, not
 * as a control the candidate is meant to open.
 *
 * ⚠️ OPT-IN, PER INSTANCE. The class goes on the mount element the caller
 * passes to initMultiSelect(); the widget itself knows nothing about it. That
 * is deliberate: the employer's search & filter bar depends on the ghost rest
 * state (see the GHOST TREATMENT note above), and a variant that had to be
 * opted OUT of would put that at the mercy of every future consumer.
 *
 * ⚠️ THE BORDER MOVES FROM ::before TO THE BUTTON. The base trigger is a
 * transparent, border-less 44px box that paints a 40px pill through ::before,
 * so a field variant cannot simply restyle that pseudo-element: 40px inset in
 * a 44px box is the wrong height, and the pill's geometry is tuned to the
 * compact filter row. `content: none` retires it, and every state that used to
 * ride on it - hover, focus, applied - is re-homed onto the button below.
 * ========================================================================= */
.multi-select--field {
  /* The base `.multi-select` is only `position: relative`, with no width, so a
     field-variant trigger would shrink-wrap its label inside a grid cell.
     `.searchable-select` already sets this for the same reason. */
  width: 100%;
}

.multi-select--field .multi-select__trigger {
  display: flex;
  width: 100%;
  /* `height`, not the base `min-height`: this must be the SAME 44px box as the
     .field__control and .searchable-select__trigger it sits beside, and a
     min-height would let a long label grow one control taller than the other. */
  height: 44px;
  /* No +1.5px correction here - unlike the base, the border is on this element,
     so --space-3 already measures from the visible edge. */
  padding: 0 var(--space-3);
  background: var(--bg-base);
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-xs);
  /* A control's own text, not a quiet filter label: --text-primary at the body
     size and weight, so "Add a Sub-classification" reads exactly like the
     "Choose your professional area of expertise" placeholder next to it. */
  color: var(--text-primary);
  font-size: var(--text-base);
  font-weight: 400;
  text-align: left;
  transition: border-color var(--duration-fast) var(--ease-out);
}

/* Retires the 40px pill. Everything below replaces the states it carried. */
.multi-select--field .multi-select__trigger::before {
  content: none;
}

.multi-select--field .multi-select__trigger:hover:not(:disabled) {
  border-color: var(--border-strong);
}

.multi-select--field .multi-select__trigger:focus-visible {
  border-color: var(--focus-ring);
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* ⚠️ Disabled is a FIRST-CLASS state here, not an edge case: Sub-classifications is
   disabled until its row has a classification, so this is what a candidate sees on
   an untouched form. It has to read as a real control that is not ready yet,
   which means the surface changes too - the base rule drops the ink only,
   which on a bordered field looks like a bug. Mirrors
   `.searchable-select__trigger:disabled`, the control beside it. */
.multi-select--field .multi-select__trigger:disabled {
  background: var(--bg-surface);
  color: var(--text-disabled);
}

/* ⚠️ THE APPLIED STATE IS DELIBERATELY QUIET IN A FIELD. `.is-active` turns a
   filter pill accent-inked and 700-weight, which is the whole point of the
   ghost rest state - but a form control must not change colour and weight just
   because it holds a value, and the chips below already show what was chosen.
   The count badge is kept: it is a non-color cue and it survives the chips
   being scrolled past. */
.multi-select--field .multi-select__trigger.is-active {
  color: var(--text-primary);
  font-weight: 400;
}

.multi-select--field .multi-select__trigger.is-active svg {
  color: var(--icon-muted);
}

/* Label takes the free space so the chevron can sit at the far edge, the way
   every other select on the form ends. The base's 22ch cap is a bound on a
   filter label in a fixed-geometry row; here the column width is the bound. */
.multi-select--field .multi-select__trigger-label {
  flex: 1;
  max-width: none;
}

.multi-select--field .multi-select__trigger svg {
  width: 16px;
  height: 16px;
  margin-left: auto;
}

/* Matches the FIELD's width instead of the base's fixed 300px, so the panel
   opens flush with the control that owns it - same as
   `.searchable-select__panel`. */
.multi-select--field .multi-select__panel {
  right: 0;
  width: auto;
}

/* =========================================================================
 * Filter Popover (trigger + panel wrapper for non-list filter controls, e.g.
 * a Range Slider) - shares the multi-select trigger's visual language for
 * consistency across the search & filter bar.
 * ========================================================================= */
.filter-popover {
  position: relative;
}

.filter-popover__panel {
  position: absolute;
  top: calc(100% + var(--space-2));
  left: 0;
  width: 300px;
  max-width: 90vw;
  background: var(--bg-elevated);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  z-index: var(--z-drawer); /* above page chrome such as the bulk pill (--z-sticky) - see components.md §24 */
  padding: var(--space-4);
}

.filter-popover__panel[hidden] {
  display: none;
}

.filter-popover__currency {
  margin-bottom: var(--space-3);
  height: 40px;
}

/* =========================================================================
 * Dual-Handle Range Slider (design.md §8, components.md "Range Slider")
 * Experience / Salary Expectation filters.
 * ========================================================================= */
.range-slider {
  position: relative;
  height: 24px;
  margin-bottom: var(--space-2);
}

.range-slider__track {
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  height: 4px;
  transform: translateY(-50%);
  background: var(--border-default);
  border-radius: var(--radius-full);
}

.range-slider__fill {
  position: absolute;
  top: 0;
  bottom: 0;
  background: var(--accent);
  border-radius: var(--radius-full);
}

.range-slider__input {
  position: absolute;
  inset: 0;
  width: 100%;
  margin: 0;
  background: transparent;
  pointer-events: none;
  -webkit-appearance: none;
  appearance: none;
}

.range-slider__input::-webkit-slider-thumb {
  pointer-events: auto;
  -webkit-appearance: none;
  appearance: none;
  width: 18px;
  height: 18px;
  border-radius: var(--radius-full);
  background: var(--accent);
  border: 2px solid var(--bg-base);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  margin-top: 3px;
}

.range-slider__input::-moz-range-thumb {
  pointer-events: auto;
  width: 18px;
  height: 18px;
  border-radius: var(--radius-full);
  background: var(--accent);
  border: 2px solid var(--bg-base);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
}

.range-slider__input::-webkit-slider-runnable-track,
.range-slider__input::-moz-range-track {
  -webkit-appearance: none;
  background: transparent;
  height: 4px;
}

.range-slider__input:focus-visible::-webkit-slider-thumb {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

.range-slider__numbers {
  display: flex;
  gap: var(--space-3);
  margin-bottom: var(--space-1);
}

.range-slider__number-label {
  flex: 1;
  font-size: var(--text-xs);
  color: var(--text-secondary);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.range-slider__number {
  height: 36px;
  padding: 0 var(--space-2);
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-xs);
  background: var(--bg-base);
  color: var(--text-primary);
  font-size: var(--text-sm);
}

.range-slider__display {
  margin: 0;
}

/* =========================================================================
 * Sticky Search & Filter Bar (design.md §8 "Search bar with filters",
 * components.md §20) - REDESIGNED to the connected two-row toolbar.
 *
 * ⚠️ CANDIDATES-ONLY. These rules are consumed by exactly one screen,
 * `dashboard/employer/index.html`. The bar used to be mounted on Assessments,
 * Shortlists and Updates as well, in a results-less "redirect" mode that
 * filtered nothing on those pages and merely bounced a submitted search here;
 * the search box and its filter triggers are now Candidates-only and that mode
 * has been deleted from candidate-search-filter-bar.js. The block is kept in
 * components.css rather than moved to a page stylesheet because it composes
 * with the shared .multi-select / .filter-popover / .range-slider components
 * that also live here. Do not re-add it to another screen - give that screen
 * its own filters, as Assessments (§44) does.
 *
 * Structure (top -> bottom), all inside ONE elevated surface so the search and
 * its filters read as a single instrument:
 *   Row 1  .search-filter-bar__search   unified field: magnifier + input +
 *                                       flush primary Search button, ONE focus
 *                                       treatment via :focus-within (§4.2)
 *   Row 2  .search-filter-bar__toolbar  filter triggers (left) + live summary
 *                                       and Clear all (right)
 *
 * ⚠️ REVERSAL (approved - design.md §8): the controls now sit DIRECTLY ON THE
 * PAGE CANVAS. There is no card, no fill of its own, no radius and no shadow.
 * This is the third surface decision for this bar (full-bleed toolbar ->
 * rounded --bg-elevated card -> canvas); the two earlier ones are recorded in
 * design.md §8 / components.md §20 so neither is "restored" as a bug.
 *
 * How a surface-less region stays legible while sticky: `background` is set to
 * --bg-base, which is the exact token base.css paints on <body>. At rest that
 * is indistinguishable from the canvas (the point of the redesign), but it is
 * fully OPAQUE, so cards scrolling underneath are covered rather than showing
 * through. A transparent sticky region would let card text collide with the
 * search field - that is the failure mode this line prevents, and it is why
 * --bg-base is load-bearing rather than cosmetic.
 *
 * The only edge is a hairline that appears ONLY once the bar is pinned (see
 * .is-stuck below), so the region reads as bare canvas while it is in flow and
 * grows a seam exactly when it needs one to separate itself from content
 * moving underneath it.
 *
 * Stays at the plain z-sticky tier (not higher) - it must NOT outrank
 * page-chrome elements like .dashboard-shell__menu-btn or the mobile
 * off-canvas sidebar/backdrop (both z-drawer), or it paints over them and
 * blocks interaction. Its filter popovers are portalled to <body> (§21) and
 * carry their own z-drawer, so they still float above the fixed .bulk-pill
 * every candidate grid now uses (z-sticky - components.md §24) without this
 * ancestor's z-index being raised.
 * ========================================================================= */
.search-filter-bar {
  position: sticky;
  /* Parks flush beneath the fixed global top bar (design.md §6.2) rather than
     at the viewport top. Derived from the token, not hardcoded - see
     --topbar-height in tokens.css. Nothing nests inside this bar any more:
     the Candidates page's bulk bar is the body-level fixed .bulk-pill
     (components.md §24), and the Assessments page - which used to nest its
     #active-bulk-bar here as a CHILD so the two would stick as one unit - no
     longer mounts this bar at all (components.md §44). */
  top: var(--topbar-height);
  z-index: var(--z-sticky);
  /* PADDING, not margin (design.md §8 content spine): the horizontal value
     matches .dashboard-main's own `padding: ... var(--space-6) ...` in
     dashboard-shell.css, so the search field and the candidate cards below it
     share one left and right edge. With the card surface gone that registration
     is visible - a margin would inset the background too and leave a canvas
     gutter for cards to scroll through. */
  padding: var(--space-5) var(--space-6) var(--space-3);
  /* Light-grey section surface (theme-aware): --bg-elevated is #f6f7f6 in light
     and a raised surface in dark, so the whole search+filter strip reads as a
     distinct light-grey panel against the white content canvas while the white
     search pill and the bordered filter chips sit on top of it. Opaque, so it
     still covers content scrolling under the pinned (sticky) bar. */
  background: var(--bg-elevated);
  /* Declared transparent at rest so the .is-stuck rule below only has to swap
     the COLOR - toggling border-width would shift layout by 1px on every pin. */
  border-bottom: 1px solid transparent;
  transition: border-color var(--duration-base) var(--ease-out);
}

/* Pinned state (design.md §8, components.md §20). `.is-stuck` is toggled by
   candidate-search-filter-bar.js from an IntersectionObserver on a zero-height
   sentinel placed just above the bar - the standard way to detect that a
   `position: sticky` element has left flow, since CSS alone cannot express it.
   --border-default rather than --border-strong: this is a quiet seam, not the
   edge of a raised surface. */
.search-filter-bar.is-stuck {
  border-bottom-color: var(--border-default);
}

/* The row stack (search / filters / chip band). Name kept from the previous
   build so no selector outside this block has to change. Deliberately NOT
   width-capped: the former `max-width: 960px; margin: 0 auto` centred the
   controls in their own column while the results grid ran full width, which
   the card surface used to disguise. On bare canvas that stagger is obvious,
   so the contents now run the full spine and register with the grid.
   8pt rhythm (design.md §3.1) between rows. */
.search-filter-bar__form {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  /* Center each row (search field / filter toolbar) in the spine so the compact
     search pill and the centered filter group read as one stacked, centered
     group (Airbnb-style reference). */
  align-items: center;
}

/* ---- Row 1: unified search field (design.md §8, components.md §20) --------
   One rounded container holds the magnifier, the input and the primary Search
   button, so the whole thing reads as a SINGLE control rather than an input
   sitting next to a button.
   Heights: the container is 44px - the standard minimum hit area (design.md
   §4.4). It holds the 40px `md` Search button with 2px of padding and a 1.5px
   border around it, and stands 4px taller than the 40px filter pills below. The
   input stretches to the container's full height, so the ENTIRE field is the
   hit area, not just the button.

   With the card gone this container is the only bordered box in the region, so
   its border is what makes the field findable against the canvas - hence
   --border-strong below rather than --border-default. */
.search-filter-bar__search {
  display: flex;
  align-items: center;
  /* Compact, centered pill (Airbnb-style) rather than the former full-width
     field. Centering is handled by `align-items: center` on the form above. */
  width: 100%;
  max-width: 520px;
  height: 44px;
  padding: 2px;
  background: var(--bg-base);
  /* --border-strong for the same reason as the card above, and because
     design.md §4.1 asks for >=3:1 on an input border against its own fill.
     ⚠️ KNOWN GAP: in dark the border-strong stop (primary-700 on the primary-950
     #061B31 canvas) still falls short of 3:1 against the field fill.
     That is a TOKEN-level limitation rather than one of this component - every
     .field__control in the app is the same or worse (an existing one measures
     1.0:1 in dark). Raising dark --border-strong to --neutral-500 would clear
     3:1 app-wide; flagged rather than changed here, since it reaches well
     beyond this bar. */
  border: 1.5px solid var(--border-strong);
  /* Fully-rounded pill (Airbnb-style): the container's corners are a full radius
     so the field reads as a rounded pill, concentric with the circular submit
     button nested inside it. */
  border-radius: var(--radius-full);
  transition: border-color var(--duration-fast) var(--ease-out);
}

/* SINGLE focus treatment for the whole control (design.md §4.2): the ring is
   drawn once on the container whenever focus is anywhere inside it - the
   input or the Search button - instead of each part ringing separately. */
.search-filter-bar__search:focus-within {
  border-color: var(--border-strong);
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* ...which means the inner controls must NOT draw their own ring, or a
   keyboard user sees two nested rings on the same control. */
.search-filter-bar__search :focus-visible {
  outline: none;
}

/* But focus must still be *locatable* when it moves input -> Search button,
   since the container ring does not move. An inset outline on the button, in
   the button's own on-accent text color, marks which part holds focus without
   adding a second outer ring. */
.search-filter-bar__submit:focus-visible {
  box-shadow: inset 0 0 0 2px var(--accent-text-on);
}

.search-filter-bar__search-icon {
  width: 16px;
  height: 16px;
  margin-left: var(--space-3);
  color: var(--icon-muted);
  flex-shrink: 0;
}

.search-filter-bar__input {
  flex: 1;
  min-width: 0;
  /* Stretch to the container's full 48px so the whole field is the hit area
     (design.md §4.4), even though the visible chrome is the container's. */
  align-self: stretch;
  padding: 0 var(--space-3);
  border: none;
  background: transparent;
  outline: none;
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: var(--text-sm);
}

.search-filter-bar__input::placeholder {
  color: var(--text-secondary);
}

/* input[type=search] paints a UA clear (x) and, on WebKit, decoration glyphs
   inside the field - both break the "one connected control" reading and
   duplicate the Clear all affordance. */
.search-filter-bar__input::-webkit-search-cancel-button,
.search-filter-bar__input::-webkit-search-decoration {
  -webkit-appearance: none;
  appearance: none;
}

.search-filter-bar__clear-query-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  margin-right: var(--space-2);
  padding: 0;
  border: none;
  border-radius: var(--radius-full);
  background: transparent;
  color: var(--icon-muted);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out);
}

.search-filter-bar__clear-query-btn[hidden] {
  display: none !important;
}

.search-filter-bar__clear-query-btn:hover {
  background: var(--hover-surface);
  color: var(--text-primary);
}

.search-filter-bar__clear-query-btn svg {
  width: 16px;
  height: 16px;
}

/* The primary Search button, flush inside the container (design.md §7
   Primary). Query-mandatory search keeps its explicit trigger - this button IS
   that trigger, just relocated inside the field. */
.search-filter-bar__submit {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0;
  flex-shrink: 0;
  /* Icon-only round button (Airbnb-style): a 36px circle inset inside the
     48px field. The "Search" label span is kept for the accessible name but is
     visually hidden below - so this shows the magnifier alone at every width. */
  width: 36px;
  height: 36px;
  padding: 0;
  border: none;
  border-radius: var(--radius-full);
  background: var(--accent);
  color: var(--accent-text-on);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out);
}

.search-filter-bar__submit:hover {
  background: var(--accent-hover);
}

.search-filter-bar__submit svg {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}

/* The label is kept in the DOM for the button's accessible name ("Search") but
   is visually hidden at every width, so the submit control is icon-only. */
.search-filter-bar__submit-label {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---- Row 2: filter toolbar (design.md §8, components.md §20) --------------
   Filters left, summary + Clear all right. Both groups wrap independently on
   the 8pt rhythm, so a long filter set reflows without the summary detaching
   from its row. */
.search-filter-bar__toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  /* Center the whole toolbar group (Airbnb-style reference) rather than letting
     the filters run to the left edge with the summary pinned right. */
  justify-content: center;
}

.search-filter-bar__filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
  justify-content: center;
  min-width: 0;
}

/* "Advanced Filters" button - a bordered rounded pill matching the 40px height
   of the filter triggers. Opens the Advanced Search overlay (components.md
   §46); it used to be an unwired "Filters" placeholder at the HEAD of the
   left-hand filter row, and now leads the right-hand summary cluster instead.

   ⚠️ It is `disabled`, not hidden, until a search is initiated (design.md §14):
   its four filters are dynamic and have nothing to describe until there are
   results, but hiding the only entry point to a whole filter surface would hide
   the feature. The disabled treatment below therefore has to read as "not yet",
   which is why it keeps its border and merely drops to --text-disabled ink
   rather than dimming the whole pill to near-invisibility. */
.search-filter-bar__filters-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  height: 40px;
  padding: 0 var(--space-4);
  flex-shrink: 0;
  border: 1.5px solid var(--border-strong);
  border-radius: var(--radius-full);
  background: transparent;
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out);
}

/* --bg-base (white in light / canvas in dark), not --bg-elevated, because the
   section itself is now --bg-elevated - a hover in the same colour would be
   invisible. Same reasoning applies to the filter triggers' hover below. */
.search-filter-bar__filters-btn:hover:not(:disabled) {
  background: var(--bg-base);
}

.search-filter-bar__filters-btn svg {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}

/* Pre-search state. Border and layout are untouched so the control keeps its
   place and its shape - only the ink and the cursor say "not available yet". */
.search-filter-bar__filters-btn:disabled {
  color: var(--text-disabled);
  border-color: var(--border-default);
  cursor: not-allowed;
}
.search-filter-bar__filters-btn:disabled svg {
  opacity: 0.5;
}

/* Applied state, mirroring an applied filter trigger (accent tint + accent ink
   + weight), so "this button is doing something" reads identically wherever it
   appears on the row. */
.search-filter-bar__filters-btn.is-active {
  background: var(--accent-tint);
  border-color: var(--accent-tint-border);
  color: var(--accent-ink);
  font-weight: 700;
}
.search-filter-bar__filters-btn.is-active:hover:not(:disabled) {
  background: var(--accent-tint-strong);
}

/* Each filter trigger gets a bordered, fully-rounded bounding box matching the
   Filters button (Airbnb-style reference). SCOPED to the search-filter-bar so
   the same .multi-select__trigger used elsewhere (e.g. the candidate profile
   work-experience skills) keeps its original ghost treatment. The rest border
   was already drawn transparent at 1.5px on ::before, so colouring it in shifts
   nothing. Full radius applies to every state; the visible rest border is
   :not(.is-active) so an APPLIED filter keeps its own accent-tint border from
   the .is-active::before rule above (equal specificity, so this must not clobber
   it). */
.search-filter-bar__filters .multi-select__trigger::before {
  border-radius: var(--radius-full);
}
.search-filter-bar__filters .multi-select__trigger:not(.is-active)::before {
  border-color: var(--border-strong);
}

/* Hover fills white (see the Filters-button note above) so it reads on the
   --bg-elevated section instead of the now-invisible same-colour fill.
   :not(:disabled) for the same reason as the base rule - this is the more
   specific of the two and would otherwise re-enable the hover fill that the
   base rule's guard just suppressed. */
.search-filter-bar__filters .multi-select__trigger:hover:not(:disabled)::before {
  background: var(--bg-base);
}

/* The rest border stays visible while disabled - see the :disabled note on
   .multi-select__trigger. Without this the bar-scoped :not(.is-active) rule
   above still supplies --border-strong, which is too assertive for a control
   that cannot be used; --border-default keeps the pill's shape without
   advertising it. */
.search-filter-bar__filters .multi-select__trigger:disabled::before {
  border-color: var(--border-default);
}

/* ---- Row 3: shared active-filters chip band (design.md §8, components.md
   §20 Line 3 / §21 "Chip placement") --------------------------------------
   ⚠️ FIX - ragged trigger row. Each popover filter used to render its selected
   values as a chip cluster inside its OWN `.multi-select`, which is a flex item
   of `.search-filter-bar__filters` above. Those chips reserved space in the
   trigger row, so selecting 3 Skills + 2 Classifications grew those two items and
   knocked Experience/Salary/Country onto uneven rows. The chips are hoisted
   here instead: ONE full-width band, a sibling of the toolbar and therefore
   outside the trigger row's flex context entirely, so nothing that happens to
   the chips can feed back into trigger sizing or position.

   `display: contents` on each hoisted `.multi-select__chips` dissolves the
   per-control wrapper, so all filters' chips wrap as ONE continuous flow in
   this band rather than as per-trigger clusters (the §21 rule) - while the
   wrapper element itself stays in the DOM, keeping each control's remove-(×)
   delegate bound exactly as before. */
.search-filter-bar__active-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  width: 100%;
}

.search-filter-bar__active-filters .multi-select__chips {
  display: contents;
}

/* Collapse with NO residual gap when nothing is selected. `:has(.chip)` rather
   than `:empty` because the band always holds one (possibly chip-less) wrapper
   per filter, so it is never literally empty - and `display: none` rather than
   a zero height so the parent flex column's `gap` is not spent on it either. */
.search-filter-bar__active-filters:not(:has(.chip)) {
  display: none;
}

/* Optional hairline separator between the filter triggers and the summary
   cluster (design.md §8, components.md §20). Purely decorative - aria-hidden in
   the markup - and it rides the same reveal as the summary, since a divider
   with nothing on its right is just a stray mark. */
.search-filter-bar__sep {
  width: 1px;
  height: 20px;
  margin: 0 var(--space-2);
  background: var(--border-default);
  flex-shrink: 0;
}

/* Right-aligned summary cluster: the "N active" readout sits next to Clear all
   so the count explains what the control would undo.

   ⚠️ REVEAL-ON-ACTIVE (approved - design.md §8, components.md §20): the cluster
   and its separator are ABSENT until at least one filter is applied, rather
   than permanently present with a "No filters active" readout and a disabled
   Clear all. An idle bar therefore carries no controls that do nothing. The
   controller toggles the `hidden` attribute (not opacity, which is what the
   reference mock used) so the cluster also leaves the tab order and the
   accessibility tree when there is nothing to clear - the same reason the
   button used to be genuinely `disabled` rather than styled-off. Because it is
   now absent rather than disabled, the old WCAG-1.4.3 note about --text-disabled
   contrast no longer applies and has been removed with it. */
/* ⚠️ REVEAL-ON-ACTIVE NOW APPLIES PER-ITEM, NOT TO THE WHOLE CLUSTER. The
   cluster itself is permanent, because the Advanced Filters button lives inside
   it and must always be reachable (see that button's note above). Only the
   three items that explain/undo an applied filter set - the separator, the
   count readout and Clear all Filters - carry `hidden`. */
.search-filter-bar__sep[hidden],
.search-filter-bar__summary-count[hidden],
.search-filter-bar__clear[hidden] {
  display: none;
}

.search-filter-bar__summary {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-shrink: 0;
}

.search-filter-bar__summary-count {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  white-space: nowrap;
}

/* The number itself is the only part that changes, so it carries the weight
   and the primary ink - the surrounding words stay secondary (design.md §2.2:
   hierarchy from weight/color, not size). */
.search-filter-bar__summary-count b {
  color: var(--text-primary);
  font-weight: 600;
}

/* "Clear all Filters" - a DESTRUCTIVE ghost control (design.md §2.1 error
   colour). It is the only control on this bar that throws work away, so it is
   the only one that leaves the neutral --text-primary ink and takes the error
   token, matching .btn-danger / .alert-error / .badge-error elsewhere in this
   file. Still ghost-shaped rather than a filled red button: it sits inside a
   quiet filter row, and a solid destructive button there would outweigh the
   primary Search action beside it.

   It needs no disabled state: it is hidden unless there is something to clear,
   so it only ever exists in its active form. */
.search-filter-bar__clear {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: 0 var(--space-3);
  border: none;
  border-radius: var(--radius-xs);
  background: transparent;
  color: var(--color-error);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 500;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out);
}

/* The error TINT, so the hover reads as part of the same destructive family
   rather than as the neutral --bg-elevated every other control on the row uses. */
.search-filter-bar__clear:hover {
  background: var(--color-error-bg);
}

/* ⚠️ DARK MODE NEEDS ITS OWN HOVER. --color-error flips to a lighter red in
   dark mode (tokens.css) but --color-error-bg does NOT - the tint is defined
   light-mode-only by design, and using it here would paint a near-white block
   behind light-red text. Falls back to the neutral elevated surface, which is
   exactly the escape hatch .badge-error already takes in this file. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .search-filter-bar__clear:hover {
    background: var(--bg-elevated);
  }
}
:root[data-theme="dark"] .search-filter-bar__clear:hover {
  background: var(--bg-elevated);
}

.search-filter-bar__clear svg {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
}

/* ---- Below the Tablet breakpoint (design.md §3.2: Mobile = 360-599px) -----
   The Search button is already an icon-only circle at every width (see
   .search-filter-bar__submit / .search-filter-bar__submit-label above), so no
   mobile-specific collapse is needed here any more. */
@media (max-width: 599px) {
  /* With the row wrapped, let the summary span the full width and push its two
     halves apart. */
  .search-filter-bar__summary {
    width: 100%;
    justify-content: space-between;
  }

  /* ...which makes a vertical divider between two stacked rows meaningless. */
  .search-filter-bar__sep {
    display: none;
  }

  /* Matches .dashboard-main's own mobile padding step in dashboard-shell.css.
     ⚠️ This breakpoint used to be 767px while .dashboard-main dropped at 599px,
     so between 600-767px the bar was inset 16px and the grid 32px. The old card
     surface disguised that 16px stagger; with the bar on bare canvas and its
     contents aligned to the grid (see .search-filter-bar__form), the two MUST
     step together or the misregistration is plainly visible. */
  .search-filter-bar {
    padding: var(--space-4) var(--space-4) var(--space-2);
  }
}

/* (The former 767px inset rule lived here. It was folded into the 599px block
   above so the bar steps in lockstep with .dashboard-main - see the comment
   there.) */

/* =========================================================================
 * Tabs (design.md §6/§8 - "pill style for filter-like tabs") - first
 * concrete implementation. Used to switch between the Assessments screen's
 * "Assessment Templates" and "Candidate Assessments" views, only one of
 * which is ever visible at a time. Real `role="tablist"`/`role="tab"` +
 * `aria-selected` (design.md §4.3), arrow-key navigation between tabs.
 * ========================================================================= */
.tabs__list {
  display: inline-flex;
  gap: var(--space-1);
  padding: var(--space-1);
  background: var(--bg-surface);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-full);
  margin-bottom: var(--space-5);
}

.tabs__tab {
  padding: var(--space-2) var(--space-5);
  border-radius: var(--radius-full);
  border: none;
  background: transparent;
  color: var(--text-secondary);
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out);
}

.tabs__tab:hover {
  color: var(--text-primary);
}

.tabs__tab[aria-selected="true"] {
  background: var(--accent);
  color: var(--accent-text-on);
}

@media (max-width: 599px) {
  .tabs__list {
    display: flex;
  }
  .tabs__tab {
    flex: 1;
    padding: var(--space-2) var(--space-3);
  }
}

/* -------------------------------------------------------------------------
 * `.tabs--segmented` - SEGMENTED VARIANT of the block above (components.md
 * §43, §44). Now used by every `.tabs` consumer - employer Assessments (§44),
 * employer + candidate Account Settings (§49/§60), candidate Profile (§57) and
 * candidate Assessments (§58); the base pill style above has no consumer left
 * but is kept as the block default and the glider's fallback paint. This
 * is a restyle by modifier, not a fork - the markup, the roving tabindex and
 * the `aria-selected` state machine are all identical, so a page opts in
 * with one extra class on the wrapper.
 *
 * Geometry (design.md §7 ladder): a 40px `md` tab inside a 4px-padded track
 * = 48px outer. The mock this came from used 38px in the same 4px track;
 * 38px is off Vanceli's 32/40/48 ladder, so it steps up to 40 rather than
 * introducing a fifth height for one screen.
 *
 * ⚠️ The active tab DELIBERATELY DIVERGES from the base block's `--accent`
 * fill. Inside a filled track a solid accent pill reads as a pressed
 * toggle-button rather than as the selected tab; the segmented idiom wants
 * the active tab to look like the page surface has been lifted THROUGH the
 * track. Hence a `--bg-elevated` pill on a `--bg-surface` track. Do not
 * "restore" the accent fill here to match `.tabs__tab[aria-selected]` above.
 * ------------------------------------------------------------------------- */
.tabs--segmented .tabs__list {
  gap: var(--space-1);
  padding: var(--space-1);
  background: var(--bg-surface);
  border: 1px solid var(--border-default);
  /* --radius-md, not the base block's --radius-full: a fully-round track
     around square-ish inner pills leaves visible crescent gaps at the ends. */
  border-radius: var(--radius-md);
  /* Containing block for `.tabs__glider` below. Set unconditionally (not only
     on glider consumers) so it is also the `offsetParent` the glider's JS
     measures against - see tabs-glider.js. Harmless for non-glider tracks. */
  position: relative;
}

.tabs--segmented .tabs__tab {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  height: 40px;
  padding: 0 var(--space-4);
  /* Inner radius minus the track's 4px padding + 1px border, so the pill's
     curve stays concentric with the track's rather than looking clipped. */
  border-radius: calc(var(--radius-md) - 3px);
  /* Keeps the label above the glider, which is painted underneath it. */
  position: relative;
  z-index: 1;
}

.tabs--segmented .tabs__tab[aria-selected="true"] {
  background: var(--bg-elevated);
  color: var(--text-primary);
  box-shadow: var(--shadow-sm);
}

/* Dark mode: no shadow (§3.4 - invisible on a dark canvas). The elevation
   already reads as the surface step bg-surface -> bg-elevated; a hairline is
   added to reinforce the pill's edge the way every other dark elevated layer
   does. Both dark selectors are spelled out because the manual toggle and the
   OS-preference paths are independent (see .candidate-card below). */
:root[data-theme="dark"] .tabs--segmented .tabs__tab[aria-selected="true"] {
  box-shadow: none;
  border: 1px solid var(--border-default);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .tabs--segmented .tabs__tab[aria-selected="true"] {
    box-shadow: none;
    border: 1px solid var(--border-default);
  }
}

/* -------------------------------------------------------------------------
 * `.tabs__glider` - the sliding selected-tab pill (components.md §43, §65).
 * OPT-IN: a page gets it by adding one `<span class="tabs__glider">` inside
 * `.tabs__list` and calling `initTabsGlider()` (assets/js/lib/tabs-glider.js).
 * Assessments (§44) has no glider span and is completely unaffected by this
 * block - which is why this is a restyle of `.tabs--segmented`, not a fork.
 *
 * The glider takes over the SAME paint the selected tab used to carry above
 * (--bg-elevated + --shadow-sm light, hairline + no shadow dark) and simply
 * moves it, so the two themes stay in parity by construction rather than by
 * a second set of colour rules.
 *
 * ⚠️ The tab's own paint is suppressed by `[data-glider="ready"]`, an
 * attribute tabs-glider.js sets only AFTER its first successful measure -
 * deliberately not `:has(.tabs__glider)`. If the module fails to load or
 * throws, the attribute is never written and the selected tab keeps painting
 * itself: the tabs degrade to the static segmented style instead of losing
 * their selected state entirely. Same reason the glider starts at width 0.
 * ------------------------------------------------------------------------- */
.tabs--segmented .tabs__glider {
  position: absolute;
  /* Offsets are from the track's PADDING box, so 0/0 is the inner edge of the
     1px border. JS writes the full transform (including the y offset) and the
     width/height, measured off the live tab - nothing here hard-codes the
     40px tab height, so the mobile padding step needs no companion rule. */
  top: 0;
  left: 0;
  width: 0;
  height: 0;
  z-index: 0;
  pointer-events: none;
  border: 1px solid transparent;
  border-radius: calc(var(--radius-md) - 3px);
  background: var(--bg-elevated);
  box-shadow: var(--shadow-sm);
  transition: transform var(--duration-base) var(--ease-out),
    width var(--duration-base) var(--ease-out);
}

/* Dark mode: mirrors the selected-tab rules above exactly - no shadow on a
   dark canvas (§3.4), hairline instead. Both selectors spelled out because
   the manual toggle and the OS-preference paths are independent. */
:root[data-theme="dark"] .tabs--segmented .tabs__glider {
  box-shadow: none;
  border-color: var(--border-default);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .tabs--segmented .tabs__glider {
    box-shadow: none;
    border-color: var(--border-default);
  }
}

/* Glider is live: hand the selected tab's paint over to it. Border-colour is
   reset (not `border: none`) so the dark-mode hairline disappears without the
   tab's box changing size and shifting its label by a pixel. */
.tabs--segmented .tabs__list[data-glider="ready"] .tabs__tab[aria-selected="true"] {
  background: transparent;
  box-shadow: none;
  border-color: transparent;
}

/* design.md §4.3 / the mock's own reduced-motion block. The glider still
   POSITIONS correctly - only the tweening is dropped, so it snaps to the
   selected tab. Never disable the transform itself. */
@media (prefers-reduced-motion: reduce) {
  .tabs--segmented .tabs__glider {
    transition: none;
  }
}

/* Count pill inside a segmented tab. Sizing/tabular-figure treatment is
   forked from `.updates-page__count`, which solves the same problem: the
   count must not reflow its label when it crosses 9 -> 10.
   ⚠️ Always `aria-hidden` in the markup - the number is repeated into the
   tab's `aria-label` ("Templates, 3 items") instead, because appending a bare
   digit to the accessible name makes a screen reader read "Templates 3". */
.tabs__count {
  display: inline-grid;
  place-items: center;
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  border-radius: var(--radius-full);
  background: var(--bg-elevated);
  color: var(--text-secondary);
  font-size: var(--text-xs);
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

/* On the active tab the pill sits ON --bg-elevated, so it needs its own
   surface to stay legible - the applied-filter tint pairing, which is
   contrast-checked in both themes (design.md §1.4). */
.tabs--segmented .tabs__tab[aria-selected="true"] .tabs__count {
  background: var(--accent-tint);
  color: var(--accent-ink);
}

@media (max-width: 599px) {
  .tabs--segmented .tabs__tab {
    /* Inherits `flex: 1` from the base block's mobile rule above; only the
       horizontal padding tightens. The 40px height is NOT reduced - it is the
       control's hit area, not decoration (§4.4). */
    padding: 0 var(--space-2);
  }
}

/* =========================================================================
 * Candidate Card (design.md §9 Candidate card, Avatar)
 * ========================================================================= */
/* Elevation + hover convention (design.md §9/§3.4): light mode rests at
   shadow-sm and lifts to shadow-md on hover, keeping its --border-default
   hairline throughout (the shadow is what deepens, not the border) - see the
   dark-mode override pair below for the shared card's surface-lightening
   counterpart (no shadow, §3.4). */
.candidate-card {
  /* The shared size of BOTH top-right control marks - the bookmark glyph and
     the select checkbox square (components.md §23, design.md §3.7's 20px
     default). Declared once so the pair cannot drift apart again; the two
     controls are built very differently (an icon button vs a bordered
     checkbox) and previously carried three unrelated sizes. */
  --candidate-control-glyph: 20px;
  /* ---- Minimal list ROW (design.md §9 Candidate row anatomy, components.md
     §23) ----
     A full-width list row: the avatar on the LEFT and the content column
     (.candidate-card__body) beside it, flat on the page canvas and separated
     from the next row by ONE hairline (border-bottom) - no box, no shadow. The
     layout is uniform at every width (the body column just narrows and its meta/
     skills wrap on mobile), so there is no separate desktop grid. Generous
     vertical padding keeps the row from reading congested. */
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  gap: var(--space-4);
  padding: var(--space-5) var(--space-4);
  /* ⚠️ LOAD-BEARING, not tidiness - this is what stops the row overflowing the
     page on a narrow viewport. All three `.candidate-grid` copies (employer-
     dashboard.css, shortlists.css, assessments.css) declare
     `grid-template-columns: 1fr`, and a `1fr` track's AUTO MINIMUM is its item's
     min-content width - so a card whose longest unshrinkable line exceeds the
     column simply widens the track past the viewport and the whole page scrolls
     sideways. Measured at a 420px viewport: the track resolved to 443px.
     `min-width: 0` overrides that auto minimum so the track can shrink and the
     card's own children wrap/ellipsize as designed.
     Fixed HERE rather than by changing each grid to `minmax(0, 1fr)`: one rule
     covers Candidates, Shortlists and both Assessments tabs, and it travels with
     the component to any future grid. */
  min-width: 0;
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--border-default);
  border-radius: 0;
  box-shadow: none;
  cursor: pointer;
  position: relative;
  transition: background-color var(--duration-fast) var(--ease-out);
}

/* Hover: a quiet neutral surface tint marks the row as the click target for
   selection (matches the whole-card click/keydown handling in
   candidate-card.js). Replaces the old shadow lift, which no longer suits a flat
   list. Dark mode lifts to --bg-elevated instead (below). */
.candidate-card:hover {
  background: var(--hover-surface);
}

/* Dark mode: no shadow (invisible on a dark canvas per §3.4); the flat base
   already sets box-shadow:none, so dark mode only needs the one-step surface
   lift on hover (bg-base -> bg-elevated), the same elevation cue every other
   dark layer uses. The --border-default hairline stays constant to hold the
   row edge. */
:root[data-theme="dark"] .candidate-card:hover {
  background: var(--bg-elevated);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .candidate-card:hover {
    background: var(--bg-elevated);
  }
}

.candidate-card:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* Selected row: an accent-tint wash + an accent hairline, in place of the old
   boxed accent border. aria-pressed on the root (candidate-card.js) is the
   non-color signal; the check inside the select indicator also fills (below). */
.candidate-card.is-selected {
  background: var(--accent-tint);
  border-bottom-color: var(--accent);
}

/* Brief attention pulse for a candidate card landed on via the Updates
   screen's notification click-through (components.md "Updates Page") - not
   a persistent state, removed via JS on `animationend`. Inherits the global
   `prefers-reduced-motion` clamp in base.css automatically. */
.candidate-card--highlight {
  animation: candidate-card-highlight-pulse 1.6s var(--ease-out);
}

@keyframes candidate-card-highlight-pulse {
  0% {
    box-shadow: 0 0 0 3px var(--accent);
  }
  100% {
    box-shadow: 0 0 0 3px transparent;
  }
}

/* Absolutely-positioned top-right cluster: assessment-status badge (if any)
   + the selection indicator, laid out with flexbox so the badge's variable
   text width never has to be hand-measured against the indicator's own
   position - see components.md "Candidate Card". */
/* §23 redesign: this cluster now holds the two icon CONTROLS (bookmark +
   select affordance); the assessment status badge moved out of it into the
   identity block. Offsets pulled in to --space-1 because the bookmark is a
   44px .btn-icon box (design.md §4.4 - never shrink it): at --space-3 its
   glyph centre would sit ~34px down and, since this is a centre-aligned
   flex row, it would drag the 22px select indicator down off the card's
   optical top edge with it. The tighter offset puts the GLYPHS where the
   old badge/indicator pair sat, with the button's invisible padding
   overhanging into the card's own padding. */
.candidate-card__top-right {
  position: absolute;
  top: var(--space-1);
  right: var(--space-1);
  display: flex;
  align-items: center;
  /* gap:0 is deliberate - the VISUAL gap is set on the indicator's
     margin-left below, because the bookmark button's 44px hit box extends
     12px past its own 20px glyph and a plain flex gap would be added on top
     of that invisible padding, reading as a lopsided ~16px space. */
  gap: 0;
  z-index: 1;
}

/* Select control (design.md §3.7, §9): a Phosphor `square` (unselected) that
   swaps to `check-square-fill` (selected), matched in SIZE + WEIGHT to the
   Phosphor bookmark glyph beside it so the two read as ONE pair. This replaces
   the old CSS-drawn bordered square, which mismatched the bookmark icon. Still
   the same <span>, no ARIA/markup-role change (the card carries
   role="button"/aria-pressed, see candidate-card.js). Card-only class -
   candidate-card.js is its sole emitter - so no .candidate-card scoping needed. */
.candidate-card__select-indicator {
  flex-shrink: 0;
  width: var(--candidate-control-glyph);
  height: var(--candidate-control-glyph);
  /* Gap + right-inset math kept from the matched-pair layout: pull the 20px mark
     back over the bookmark button's 12px of invisible right padding so the two
     sit an even --space-2 apart, and give it the same per-side breathing room
     the bookmark glyph has so both align inside the card's --space-4 padding.
     Derived from the two sizes, not hardcoded, so they stay correct if either
     changes. */
  margin-left: calc(var(--space-2) - (44px - var(--candidate-control-glyph)) / 2);
  margin-right: calc((44px - var(--candidate-control-glyph)) / 2);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Same rest tint as the bookmark (.btn-icon uses --icon-default), so the
     unselected pair is one colour + one size + one weight. */
  color: var(--icon-default);
  transition: color var(--duration-fast) var(--ease-out);
}

.candidate-card__select-indicator svg {
  width: var(--candidate-control-glyph);
  height: var(--candidate-control-glyph);
}
/* Two stacked glyphs: `square` (first) shown unselected, `check-square-fill`
   (second) hidden until selected. */
.candidate-card__select-indicator svg:last-child {
  display: none;
}

/* Hover cue: hovering anywhere on the card previews the accent tint on the
   square, signalling the whole card is the select target (matches the
   click/keydown handling in candidate-card.js, unchanged). */
.candidate-card:hover .candidate-card__select-indicator {
  color: var(--accent);
}

/* Selected: swap the outline square for the filled check-square, in accent. */
.candidate-card.is-selected .candidate-card__select-indicator {
  color: var(--accent);
}
.candidate-card.is-selected .candidate-card__select-indicator svg:first-child {
  display: none;
}
.candidate-card.is-selected .candidate-card__select-indicator svg:last-child {
  display: block;
}

/* Content column (design.md §9 Candidate row "Body"): everything beside the
   avatar. It now holds exactly TWO children - the two-column
   .candidate-card__main and the action footer beneath it (components.md §23).
   The two top lines inside the left column (__meta-row, __identity-line) carry a
   right gutter to clear the absolute top-right control cluster (below). */
.candidate-card__body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* ---- The two-column split (components.md §23) --------------------------------
   Left: identity + facts + classifications + skills. Right: the career summary
   aside. The FOOTER is deliberately not in here - it is a sibling below, so the
   buttons align to the card's left edge and are never pushed around by the
   summary card's height. */
.candidate-card__main {
  display: flex;
  flex-direction: row;
  /* flex-start, not center: the summary aside must top-align with the left
     column's first line (design.md §9 / components.md §23). It was `center`
     only because the aside carried a 44px top offset to duck under the
     absolute .candidate-card__top-right cluster; that clearance is now
     horizontal (margin-right on __summary), so both columns start at the
     same y. */
  align-items: flex-start;
  gap: var(--space-6);
}

/* `min-width: 0` is load-bearing, not defensive boilerplate: without it this
   flex child refuses to shrink below the intrinsic width of its longest
   unbreakable content (a long location or skill chip), which would let the left
   column push the summary aside off the card entirely. */
.candidate-card__col {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* ---- Career summary aside (components.md §23) --------------------------------
   The candidate's own one-line pitch, on a FAINT surface card at the
   right-middle of the row. It used to sit in-flow under the name as a
   full-width icon + text row, which pushed the facts, classifications and skills -
   the things an employer scans a result list for - progressively down the card.

   `flex: 0 1 clamp(...)` rather than a fixed width or a bare percentage: the
   basis tracks the card width so it stays proportionate on a wide viewport, the
   clamp stops it collapsing to an unreadable ribbon or ballooning past the data
   it supports, and `flex-shrink: 1` lets it give way first when the left column
   needs the room.

   Colours are TOKENS, never literals (design.md §1.5) - the "faint" reading has
   to survive the dark-mode swap, which a hardcoded near-white would not.
   --bg-surface is the right one: the card row itself is flat on the page canvas
   (--bg-base), so the surface step reads as a quiet inset panel against it, and
   in dark mode the pair resolves to #111111 / #191919 which holds the same
   relationship.

   ⚠️ line-height is a LITERAL 1.6, not a token, because the design system does
   not define line-height tokens (tokens.css has none - the previous inline
   `var(--line-height-relaxed)` on this text resolved to nothing and silently did
   no work). Do not "restore" that variable without adding it to tokens.css. */
.candidate-card__summary {
  /* +50% over the original clamp(200px, 28%, 320px) per user request. */
  flex: 0 1 clamp(300px, 42%, 480px);
  min-width: 0;
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  padding: var(--space-3);
  background: var(--bg-surface);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--text-secondary);
  /* Clears the absolutely-positioned .candidate-card__top-right cluster
     (~:4066), which is out of flow and reserves no space. The panel's right
     edge otherwise sits only 16px from the card edge (the card's own
     padding-right) while the cluster occupies the first 72px, so the panel's
     top-right corner would render UNDER the bookmark/select icons.
     Horizontal clearance, mirroring the 72px gutter the left column already
     puts on .candidate-card__identity-line / .candidate-card__meta-row -
     same cluster, same constant, both axes now consistent.
     margin, NOT padding: this is a bordered/tinted panel, so padding would
     leave the border box under the icons and only inset the text.
     Replaces a former `margin-top: 44px`, which cleared the same collision by
     pushing the whole aside down a row - the misalignment this fixes. */
  margin-right: 72px;
}

/* Pinned to the FIRST LINE of the summary, not vertically centred in the block:
   the text runs to several lines, and a centred glyph would float away from the
   sentence it introduces. The 2px nudge optically centres it against that first
   line rather than aligning its box to the cap height. */
.candidate-card__summary-icon {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  width: 16px;
  height: 16px;
  margin-top: 2px;
  color: var(--icon-muted);
}

.candidate-card__summary-icon svg {
  width: 16px;
  height: 16px;
}

/* careerSummary is rich text (sanitizeRichText in candidate-card.js), so it can
   contain <p>/<strong>/<em>. Flatten the block margins a sanitized <p> would
   otherwise bring in, which would push the text off-centre inside the padding
   box, and keep the whole thing shrinkable inside the flex row. */
.candidate-card__summary-text {
  min-width: 0;
}

.candidate-card__summary-text > *:first-child {
  margin-top: 0;
}

.candidate-card__summary-text > *:last-child {
  margin-bottom: 0;
}

/* Below the md breakpoint (design.md §3.2) the two columns STACK: at that width
   the aside would be squeezed to a few words per line while crushing the facts
   row beside it. Full width and below the data, which is the same reading order
   the source already has. */
@media (max-width: 767px) {
  .candidate-card__main {
    flex-direction: column;
    gap: var(--space-3);
    /* ⚠️ `stretch`, overriding the `flex-start` above, and it is REQUIRED - not
       a tidy-up. Once the direction flips to column, `align-items` governs the
       HORIZONTAL axis: with `flex-start` both children size to their max-content
       width, so the left column blew past the card (571px inside a 239px row)
       and `min-width: 0` could not stop it - that property constrains the MAIN
       axis, which is now vertical. `stretch` makes both children take the card's
       full width, which is what a stacked layout wants anyway. */
    align-items: stretch;
  }

  .candidate-card__summary {
    /* Cancels the clamp()ed basis from the base rule so the aside spans the full
       card rather than staying pinned near 200-320px under the data. */
    flex: 1 1 auto;
    width: 100%;
    /* Stacked layout puts the summary below the left column, nowhere near the
       top-right cluster - the base rule's 72px clearance would be a dead
       gutter shrinking the panel. */
    margin-right: 0;
  }
}

.candidate-card .candidate-card__facts {
  margin-top: var(--space-2);
}

.candidate-card .candidate-card__skills {
  margin-top: var(--space-2);
}

.candidate-card .candidate-card__footer {
  margin-top: var(--space-2);
  padding-top: 0;
}

.candidate-card__avatar,
.avatar {
  width: 56px;
  height: 56px;
  border-radius: var(--radius-full);
  object-fit: cover;
  flex-shrink: 0;
  background: var(--bg-elevated);
  border: 1px solid var(--border-default);
}

.avatar-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--icon-muted);
}

.avatar-placeholder svg {
  width: 28px;
  height: 28px;
}

/* Default avatar illustration (components.md §78, design.md §9) - the
   gender-derived silhouette shown for candidates who skipped the OPTIONAL
   profile photo. Sibling of .avatar-placeholder, not a modifier of it: the two
   are mutually exclusive states and .avatar-placeholder's 28px centred-glyph
   sizing is exactly what this must NOT inherit.

   Full-bleed is the whole point - the bust is drawn past the bottom edge of the
   SVG viewBox and clipped by the mount's own border-radius, which is what makes
   it read as a Facebook-style silhouette rather than a small icon in a circle.
   Hence overflow:hidden + 100% sizing.

   Colour comes from the two tokens (no raw hex here, per instructions.md), so
   light and dark are both correct with no second rule. Every mount already
   sets its own width/height/border-radius; this block only paints and fills.

   ⚠️ The doubled `.avatar-illustration.avatar-illustration` is DELIBERATE, not
   a typo. This one class is applied to five different mounts, four of which
   already set their own `background` and/or `svg { width: Npx }` in rules that
   land LATER in the cascade - either further down this file
   (.candidate-profile__avatar, :4640) or in a page stylesheet that loads after
   components.css (.shortlist-card__avatar, .pv-avatar,
   .profile-form__photo-placeholder). At equal (0,1,0)/(0,1,1) specificity the
   later rule wins, so a single class would silently lose on most mounts and
   the silhouette would render as a small centred glyph on the wrong surface.
   Repeating the class raises this to (0,2,0)/(0,2,1) so it wins everywhere
   without needing one bespoke override per mount - and, more importantly,
   without a future sixth mount needing to remember to add one. */
.avatar-illustration.avatar-illustration {
  overflow: hidden;
  background: var(--avatar-illustration-surface);
  color: var(--avatar-illustration-figure);
}

.avatar-illustration.avatar-illustration svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* Name, alone on its line (design.md §9 ".name"): the anchor of the whole card
   at text-h5/700. The title no longer shares this line - it moved to the role
   line above - so the name is now the single flex item and takes the truncation
   itself. The right gutter clears the top-right control cluster so a long name
   never runs under the bookmark/select. */
.candidate-card__identity-line {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  min-width: 0;
  margin: 0;
  padding-right: 72px;
}

.candidate-card__name {
  font-family: var(--font-display);
  font-size: var(--text-h5);
  font-weight: 700;
  color: var(--text-primary);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Role line (design.md §9): the quiet line at the TOP of the column, sharing
   its row with the assessment status badge. Same right gutter as the name line
   so it clears the top-right control cluster.

   BOTH children are optional now (the role because it is optional data, the
   badge because most candidates have no live assessment), so this row can be
   empty. `:empty` collapses it to zero height rather than leaving a phantom
   14px gap above the name - without this, a card with neither would sit
   visibly lower than one with a role. */
.candidate-card__meta-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  padding-right: 72px;
}

.candidate-card__meta-row:empty {
  display: none;
}

/* The role title, in the slot the "Gender, Age" demographics line used to hold
   (which is now a fact in the meta row below). Weight 500 lifts it just clear
   of plain metadata without competing with the name beneath it. */
.candidate-card__role {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
  margin: 0;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.candidate-card__status {
  flex-shrink: 0;
}

/* Meta row (design.md §9 ".meta"): the FOUR facts as an inline, wrapping
   icon + value row - Gender/Age / Location / Experience / Type,Availability -
   with NO uppercase micro-labels (removed for a minimal, uncongested card; the
   icon carries the meaning). Classifications used to be a fifth cell here and now
   have their own row (the --classifications modifier below). Generous column-gap
   gives the reference's airy rhythm. Card-only classes now (the drawer/Profile
   use the .candidate-profile__* namespace), so no scoping is needed. */
.candidate-card__facts {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  column-gap: var(--space-5);
  row-gap: var(--space-2);
}

.candidate-card__fact {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.candidate-card__fact svg {
  width: 15px;
  height: 15px;
  flex-shrink: 0;
  color: var(--icon-muted);
}

/* Value at the 14px body step, quiet (--text-secondary) so the meta row reads
   as a supporting line beneath the name.title anchor (matches the reference). */
.candidate-card__fact-value {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  white-space: nowrap;
}

/* Classifications row (components.md §23): the up-to-five classifications the candidate
   works in, on their OWN row directly below the facts. Not a sixth cell inside
   .candidate-card__facts - three full taxonomy names ("Human Resources and
   Recruitment . Banking and Financial Services . ...") are by far the longest
   value on the card, and letting them wrap inside the shared facts row would
   drag the short facts around as the viewport narrows. A row of its own wraps
   as a unit. It inherits every .candidate-card__facts rule above (same icon
   size, same gaps) and only adds the top spacing. */
.candidate-card__facts--classifications {
  margin-top: var(--space-2);
}

/* When the value wraps to a second line, the inherited `align-items: center`
   would float the icon into the vertical middle of the block, detached from
   the text it labels. Pin it to the first line instead. */
.candidate-card__facts--classifications .candidate-card__fact {
  align-items: flex-start;
}

.candidate-card__facts--classifications .candidate-card__fact svg {
  /* Optical centring against the 14px/1.5 first line (21px tall, 15px glyph). */
  margin-top: 3px;
}

/* The one fact value allowed to WRAP instead of ellipsizing. Nowrap is right
   for the short facts - a truncated "Austin, United..." still reads - but
   clipping the classifications line would hide entire classifications with no "+N"
   affordance to signal it, so it soft-wraps onto a second line instead.
   align-items on the parent .candidate-card__fact keeps the icon centred
   against a two-line value. */
.candidate-card__fact-value--wrap {
  white-space: normal;
}

/* Base layout stays UNSCOPED - the drawer also reuses .candidate-card__skills
   as-is for a work-experience entry's small inline chip row
   (candidate-drawer.js), which still needs this flex/wrap/gap layout with no
   divider. */
.candidate-card__skills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}

/* Skills row (design.md §9): 5 chips + a "+N" overflow chip, one wrapping row
   (VISIBLE_SKILL_COUNT in candidate-card.js owns the number). The old
   two-chip-row min-height reservation (a uniform-height device for the retired
   2-up grid) is gone - a list row sizes to its content. The "+N" chip is what
   prevents silent clipping (never `overflow:hidden`), and it is now essential
   rather than a nicety: a candidate may store up to 50 skills. */

/* Overflow chip ("+45"): same .chip-static pill as a real skill, but tabular-
   feeling and never truncated. Card-only class, no scoping needed. */
.candidate-card__skills-more {
  flex-shrink: 0;
  font-variant-numeric: tabular-nums;
}

/* Actions (design.md §7/§9): View Profile (secondary) then Send Email (PRIMARY),
   a LEFT-aligned pair at the foot of the card, sized to their labels (not
   stretched full-width) and wrapping rather than overflowing on a narrow
   viewport. A little top space separates them from the content above. The
   Shortlist icon lives in the top-right cluster, not here; Send/Manage
   Assessment stays absent (Candidate Detail Drawer only, via View Profile).

   This is a SIBLING of .candidate-card__main, not a child of the left column, so
   the pair aligns to the card's own left edge and the summary aside's height
   never moves it. */
.candidate-card__footer {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding-top: var(--space-1);
}

.candidate-card__footer .btn {
  flex: 0 0 auto;
}

/* "Send Email" carries `.candidate-card__email-btn` as a HOOK ONLY - it has no
   declarations of its own any more, so no rule is needed here.

   ⚠️ It used to be RIGHT-PINNED with `margin-left: auto`. That rule was REMOVED
   (components.md §23): the actions read as one ranked group in reading order per
   design.md §7. Do not reintroduce the auto margin, and do not switch the footer
   to `space-between` or `flex-end`; both would break the same grouping - which
   matters more now that the footer is a PAIR, where any spread reads as two
   unrelated controls rather than a secondary and its primary. */

.candidate-card__shortlist-btn {
  flex-shrink: 0;
}

/* Bookmark glyph 18px -> 20px (text-h5, design.md §2.1; components.md §23) so
   it holds its own against the name at the same size. GLYPH ONLY: `.btn-icon`
   keeps its fixed 44x44 box, so the extra 2px is absorbed by the button's
   invisible padding and the hit area stays at 44px (design.md §4.4) - never
   size the button down to the glyph. Carries the `.candidate-card` ancestor to
   out-specify the shared `.btn svg` 18px rule (0,2,1 vs 0,1,1) rather than
   relying on source order, and to leave the drawer's own shortlist button
   alone - candidate-drawer.js renders that one as a bare `.btn-icon` without
   this class, so it stays at 18px. */
/* CAUSE OF THE MISALIGNMENT THIS FIXES (components.md §23, design.md §3.7):
   `.btn-icon` sets width/height/padding but NO `display`, so the button stays
   a block box and its <svg> is an INLINE child sitting on the text baseline
   rather than being centred. Measured on the card, the two control boxes
   shared a centreline exactly (the cluster's `align-items: center` was always
   working) while the bookmark's visible GLYPH rode 3px high - so the pair read
   as misaligned even though the boxes were not. Centring the button's own
   content puts the glyph on the box centre, and therefore on the checkbox's
   centreline.
   Scoped to `.candidate-card` because `.btn-icon` is shared: the Candidate
   Detail Drawer renders its own shortlist button as a bare `.btn-icon` with no
   `.candidate-card` ancestor (candidate-drawer.js), as do the modal/overlay
   close buttons, and none of them should shift. The same latent baseline
   offset exists on those buttons - fixing it globally on `.btn-icon` would be
   the real repair, but it would nudge every icon button in the app and is out
   of scope here. */
.candidate-card .candidate-card__shortlist-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* Bookmark glyph at the pair's shared mark size (was --text-h5, the same 20px
   by a different name - now pointing at the token both controls share so they
   cannot drift apart). GLYPH ONLY: `.btn-icon` keeps its fixed 44x44 box, so
   the hit area stays 44x44 (design.md §4.4) with the difference absorbed as
   invisible padding - never size the button down to the glyph. The
   `.candidate-card` ancestor also out-specifies the shared `.btn svg` 18px
   rule (0,2,1 vs 0,1,1) rather than relying on source order. */
.candidate-card .candidate-card__shortlist-btn svg {
  width: var(--candidate-control-glyph);
  height: var(--candidate-control-glyph);
}

/* Bookmark hover, needed once the button moved onto the card surface
   (components.md §23). The shared `.btn-icon:hover` fills with --bg-surface,
   which is the SAME colour as a resting card - so hover feedback simply did
   not render at all, and on a selected card (--bg-elevated) it painted a
   lighter patch that read backwards.
   Specificity note: `.btn-icon:hover:not(:disabled)` is (0,3,0), NOT (0,2,0)
   - :not() contributes its argument's specificity - so this rule repeats the
   :not() to reach (0,4,0) and win outright rather than tying and depending
   on source order.
   Light/dark are split for the same reason the card's own hover is (see the
   shadow-vs-surface-lift rules above): there is no single token that reads as
   a subtle scrim in both. In light, --border-default darkens the near-white
   surface just enough; --border-strong would be a heavy grey disc. In dark
   the card itself lifts to --bg-elevated on hover, and --border-default is
   the SAME value as --bg-elevated - so it would be invisible again, and
   --border-strong is the one step that still reads. Verified against the
   hovered card background in both themes and on selected cards. */
.candidate-card .candidate-card__shortlist-btn:hover:not(:disabled) {
  background: var(--border-default);
}

:root[data-theme="dark"] .candidate-card .candidate-card__shortlist-btn:hover:not(:disabled) {
  background: var(--border-strong);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .candidate-card .candidate-card__shortlist-btn:hover:not(:disabled) {
    background: var(--border-strong);
  }
}

/* Accent tint once a candidate is in >=1 shortlist (assets/js/lib/
   shortlist-store.js's isCandidateShortlisted()). The SOLID vs outline
   distinction is now carried by an icon WEIGHT swap - candidate-card.js /
   candidate-drawer.js render `bookmark-fill` (solid) when shortlisted and
   `bookmark` (outline) otherwise - because Phosphor icons are always
   `fill=currentColor`, so the old `fill: none`->`currentColor` toggle no longer
   distinguishes states. This rule just tints whichever weight is rendered.
   Used on both the candidate card and the drawer's shortlist button - which
   are now the SAME control in the same place on both surfaces, pinned
   top-right (components.md §23/§25). The drawer selector was
   `.candidate-drawer__actions .btn-icon` while the bookmark lived in the
   action footer; it is a named class now that the button has its own. */
.candidate-card__shortlist-btn.is-shortlisted,
.candidate-drawer__shortlist-btn.is-shortlisted {
  color: var(--accent);
}

/* =========================================================================
 * Shortlist Picker (unified add/remove checklist, renders into the generic
 * Modal - assets/js/lib/shortlist-picker.js, components.md "Shortlist
 * Picker"). Row styling modeled on .multi-select__option for consistency.
 * ========================================================================= */
.shortlist-picker__list-label {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-secondary);
  margin: 0 0 var(--space-2);
}

.shortlist-picker__create-form {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  margin-bottom: var(--space-4);
  flex-wrap: wrap;
}

.shortlist-picker__create-form .field__control {
  flex: 1;
  min-width: 160px;
  height: 40px;
  border-radius: var(--radius-pill);
}

.shortlist-picker__create-actions {
  display: flex;
  gap: var(--space-2);
}

.shortlist-picker__create-form .shortlist-picker__create-actions .btn {
  height: 40px;
  border-radius: var(--radius-pill);
}

.shortlist-picker__list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  max-height: 320px;
  overflow-y: auto;
}

.shortlist-picker__row {
  padding: var(--space-2);
  border-radius: var(--radius-xs);
}

.shortlist-picker__row:hover {
  background: var(--bg-surface);
}

.shortlist-picker__row-label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  font-size: var(--text-sm);
}

.shortlist-picker__row-label input[type="checkbox"] {
  accent-color: var(--accent);
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}

.shortlist-picker__row-name {
  flex: 1;
  min-width: 0;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.shortlist-picker__row-count {
  font-size: var(--text-xs);
  flex-shrink: 0;
}

.shortlist-picker__row-note {
  margin: var(--space-1) 0 0 26px;
  font-size: var(--text-xs);
  color: var(--text-secondary);
}

.shortlist-picker__empty {
  padding: var(--space-4) 0;
  text-align: center;
  color: var(--text-secondary);
  font-size: var(--text-sm);
}

.shortlist-picker__quick-actions {
  display: flex;
  gap: var(--space-4);
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border-default);
  flex-wrap: wrap;
}

.shortlist-picker__quick-actions[hidden] {
  display: none;
}

.shortlist-picker__quick-actions .link-btn {
  color: var(--color-error);
}

/* =========================================================================
 * Assessment Send Modal + Template Preview (assets/js/lib/assessment-actions.js,
 * components.md "Assessment Actions"). Template picker rows mirror
 * .shortlist-picker__row for visual consistency across the app's Modal-based
 * pickers.
 * ========================================================================= */
.assessment-send__list-label {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-secondary);
  margin: 0 0 var(--space-2);
}

.assessment-send__template-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  max-height: 200px;
  overflow-y: auto;
  margin-bottom: var(--space-4);
}

.assessment-send__row {
  padding: var(--space-2);
  border-radius: var(--radius-xs);
}

.assessment-send__row:hover {
  background: var(--bg-surface);
}

.assessment-send__row-label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  font-size: var(--text-sm);
}

.assessment-send__row-label input[type="radio"] {
  accent-color: var(--accent);
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}

.assessment-send__row-name {
  flex: 1;
  min-width: 0;
  font-weight: 600;
}

.assessment-send__row-summary {
  font-size: var(--text-xs);
}

.assessment-send__empty {
  padding: var(--space-4) 0;
  text-align: center;
  color: var(--text-secondary);
  font-size: var(--text-sm);
}

.assessment-send__details {
  padding-top: var(--space-4);
  margin-top: var(--space-2);
  border-top: 1px solid var(--border-default);
}

.assessment-send__details[hidden] {
  display: none;
}

.assessment-send__preview {
  margin-bottom: var(--space-4);
}

.assessment-send__actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border-default);
}

.assessment-preview h3 {
  margin-bottom: 2px;
}

.assessment-preview__section {
  margin-top: var(--space-4);
}

.assessment-preview__section h4 {
  font-size: var(--text-sm);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
  margin-bottom: var(--space-2);
}

.assessment-preview__questions {
  padding-left: var(--space-5);
}

.assessment-preview__question {
  margin-bottom: var(--space-3);
}

.assessment-preview__question-prompt {
  font-weight: 600;
  margin: 0;
}

.assessment-preview__question-type {
  font-size: var(--text-xs);
  margin: 2px 0 var(--space-1);
}

.assessment-preview__options {
  list-style: disc;
  padding-left: var(--space-5);
  font-size: var(--text-sm);
}

/* Review Assessment (assets/js/lib/assessment-actions.js
   openReviewAssessmentModal()) - candidate's answer rendered alongside the
   template preview above. Single/multiple-choice: the full option list
   re-rendered with the candidate's picks marked, so the answer reads in
   context of what was offered rather than in isolation. */
.assessment-preview__options li.is-selected {
  font-weight: 600;
  color: var(--accent);
}

.assessment-preview__answer {
  margin: var(--space-1) 0 0;
  font-size: var(--text-sm);
}

.assessment-preview__answer-label {
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
  margin: 0 0 2px;
}

.assessment-preview__decision {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-weight: 600;
  margin-top: var(--space-1);
}

/* =========================================================================
 * Candidate Assessment Card (assets/js/lib/candidate-assessment-card.js) -
 * design.md §13 "Candidate Assessments". Same overall card shape as
 * .template-card above (surface/border/radius), but with its own name since
 * it renders a different domain object (an assessment record, not a
 * template) with different fields (deadline, elapsed time, status badge).
 * ========================================================================= */
.candidate-assessment-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4);
  background: var(--bg-surface);
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-md);
}

.candidate-assessment-card__top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
}

.candidate-assessment-card__name {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 700;
  margin: 0;
}

.candidate-assessment-card__details {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  margin: 0;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.candidate-assessment-card__deadline,
.candidate-assessment-card__elapsed {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  margin: 0;
}

.candidate-assessment-card__deadline svg,
.candidate-assessment-card__elapsed svg {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}

.candidate-assessment-card__footer {
  margin-top: var(--space-2);
}

/* =========================================================================
 * Assessment question inputs (assets/js/lib/candidate-assessment-actions.js
 * openTakeAssessmentOverlay()) - real answer controls, as opposed to
 * .assessment-preview__questions above's read-only rendering. Radio/
 * checkbox row visuals reuse the same pattern already established by
 * .assessment-send__row-label (single-select template picker) and
 * .shortlist-picker__row-label (multi-select checklist).
 * ========================================================================= */
.assessment-question {
  margin-bottom: var(--space-5);
}

.assessment-question__prompt {
  font-weight: 600;
  margin: 0;
}

.assessment-question__type {
  font-size: var(--text-xs);
  margin: 2px 0 var(--space-2);
}

.assessment-question__options {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.assessment-question__option-label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-xs);
  cursor: pointer;
}

.assessment-question__option-label:has(input:checked) {
  border-color: var(--accent);
  background: var(--bg-elevated);
}

/* =========================================================================
 * Assessment Attachments (assets/js/lib/candidate-assessment-actions.js) -
 * the take-assessment overlay's optional file/link submission section,
 * shown only when the employer's template allows it. File list reuses
 * .resume-dropzone (components.css, above) for the dropzone itself; the
 * rows below are new.
 * ========================================================================= */
.assessment-attachments {
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border-default);
}

.assessment-attachments h4 {
  font-size: var(--text-sm);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
  margin-bottom: var(--space-2);
}

/* ⚠️ TWO CONSUMERS, despite the name: the take-assessment overlay's attachment
 * dropzone (candidate-assessment-actions.js) AND the Send Email composer's
 * Attachments field, its preview, and the Email Details overlay
 * (email-compose.js / email-details-drawer.js, components.md §90). Both render
 * the same object - an attached file with a name and a size - so they share one
 * row rather than two that resemble each other. Renaming this block means
 * updating both. */
.assessment-attachments__file-list {
  margin-top: var(--space-2);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.assessment-attachments__file-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  background: var(--bg-surface);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-xs);
  font-size: var(--text-sm);
}

.assessment-attachments__file-row svg {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  color: var(--icon-muted);
}

.assessment-attachments__file-name {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.assessment-links {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.assessment-links__row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.assessment-links__row .field__control {
  flex: 1;
}

/* =========================================================================
 * Assessment Taking Overlay (assets/js/lib/candidate-assessment-actions.js
 * openTakeAssessmentOverlay()) - right-side overlay, same fixed-panel
 * construction as .resume-viewer-overlay above (design.md §10 "Drawer / side
 * panel"), with its own header layout (title + live elapsed-time timer) and
 * a sticky footer for the Submit Assessment action.
 * ========================================================================= */
.assessment-taking-overlay {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: 560px;
  max-width: 100vw;
  background: var(--bg-base);
  border-left: 1px solid var(--border-default);
  box-shadow: var(--shadow-xl);
  z-index: var(--z-drawer);
  display: flex;
  flex-direction: column;
}

.assessment-taking-overlay__header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4);
  border-bottom: 1px solid var(--border-default);
  flex-shrink: 0;
}

.assessment-taking-overlay__heading {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.assessment-taking-overlay__title {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.assessment-taking-overlay__timer {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--accent);
}

.assessment-taking-overlay__timer svg {
  width: 16px;
  height: 16px;
}

.assessment-taking-overlay__body {
  flex: 1;
  overflow-y: auto;
  padding: var(--space-5);
}

.assessment-taking-overlay__footer {
  display: flex;
  justify-content: flex-end;
  padding: var(--space-4);
  border-top: 1px solid var(--border-default);
  flex-shrink: 0;
}

@media (max-width: 599px) {
  .assessment-taking-overlay {
    width: 100%;
  }
}

/* =========================================================================
 * Action Menu (assets/js/lib/assessment-actions.js openManageAssessmentMenu()
 * - a small anchored popover of buttons, positioned via `position: fixed` +
 * JS-computed coordinates rather than .filter-popover's `position: absolute`,
 * since its trigger can live inside an already-scrolling ancestor. See
 * components.md "Assessment Actions" for the rationale.
 *
 * z-index bug fix: its only trigger today is the Candidate Detail Drawer's
 * "Manage Assessment" button (`.candidate-drawer`, z-index var(--z-drawer)) -
 * at `var(--z-dropdown)` this menu rendered BEHIND the drawer, so it was
 * visible but every click on a menu item was silently intercepted by the
 * drawer underneath instead. Raised above `--z-drawer` to fix.
 * ========================================================================= */
.action-menu {
  position: fixed;
  z-index: var(--z-modal);
  min-width: 200px;
  background: var(--bg-elevated);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lg);
  padding: var(--space-2);
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.action-menu__item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-xs);
  background: none;
  border: none;
  cursor: pointer;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-primary);
  text-align: left;
}

.action-menu__item:hover {
  background: var(--bg-surface);
}

.action-menu__item svg {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  color: var(--icon-muted);
}

.action-menu__item--danger {
  color: var(--color-error);
}

.action-menu__item--danger svg {
  color: var(--color-error);
}

/* =========================================================================
 * `.bulk-action-bar` - REMOVED (was: the original in-flow bulk toolbar)
 * -------------------------------------------------------------------------
 * DELETED, not deprecated: it has no consumers left. The three screens that
 * used it all now run the shared floating `.bulk-pill` below - Candidates was
 * forked to it first (components.md §24), the Assessments Active tab was
 * migrated next (§44), and the Shortlists detail view was the last holdout
 * (§37). Its markup gained the tally chip, clear button, divider and pill
 * button classes in that conversion, which is exactly what the old note here
 * warned this block did not supply.
 *
 * Kept as a tombstone rather than a silent deletion because the class name is
 * still referenced in the docs' history and in a handful of z-index comments
 * below. If you are looking for it: there is nothing to restore, and a new
 * bulk surface should use `.bulk-pill`, not a revival of this.
 * ========================================================================= */

/* =========================================================================
 * Bulk Selection Pill - `.bulk-pill` (components.md §24, §44)
 * -------------------------------------------------------------------------
 * The contextual bulk-action surface: a rounded floating pill fixed to the
 * bottom-centre of the content column, animating up on the first selection and
 * away when the selection empties. Shared by the Candidates screen and the
 * Assessments Active tab (§44) - both drive it the same way (toggle .is-visible,
 * write the count into .bulk-pill__count), so their bulk UI is identical. Only
 * the action buttons differ per screen; everything else is one component.
 *
 * ALL THREE CANDIDATE GRIDS now run this: Candidates (§24), the Assessments
 * Active tab (§44) and the Shortlists detail view (§37). It began as a FORK of
 * `.bulk-action-bar` rather than an edit of it, because that block was still
 * driving screens whose markup had no tally chip or divider - forking kept them
 * byte-identical while Candidates moved. Each has since been converted and
 * `.bulk-action-bar` is gone (see the tombstone above), so the fork is now
 * simply the component.
 *
 * WHY ITS OWN BUTTON CLASSES rather than `.btn.btn-secondary`: see the long
 * comment on `.bulk-pill__btn` below. In short, `.btn-secondary`'s shared
 * hover rule is a specificity fight this component kept losing, and not
 * matching it at all is a structural fix rather than another override.
 * ========================================================================= */
.bulk-pill {
  /* Fixed to the VIEWPORT, not to any scroll container (design.md §3.2).
     --z-sticky is deliberate and is the CEILING, not a floor: the pill is
     page chrome, so everything in the overlay tiers must cover it - the
     off-canvas nav rail and the candidate drawer (--z-drawer), the modals
     this pill itself opens (--z-modal), and toasts (--z-toast). At
     --z-drawer it tied with the rail and, being later in the DOM, painted
     OVER the open mobile nav drawer.
     It shares 200 with .search-filter-bar and wins that tie on DOM order,
     which is harmless: one is pinned to the top edge and the other to the
     bottom, so they never overlap. The search bar's own filter popovers sit
     at --z-drawer and so still open above the pill, which is correct. */
  position: fixed;
  bottom: 28px;
  z-index: var(--z-sticky);

  /* Centred on the CONTENT COLUMN, not the viewport. The icon rail is an
     in-flow flex sibling (dashboard-shell.css), not a fixed overlay, so a
     plain `left: 50%` would sit visibly left of the content it acts on.
     Offsetting by half the rail width re-centres it and keeps it clear of
     the rail. The rail is `fit-content` with --sidebar-rail-width as a
     floor, so this is a close approximation rather than pixel-exact - fine
     for a floating element, and it resets to a true centre below 600px
     where the rail goes off-canvas (see the media query at the end). */
  left: calc(50% + var(--sidebar-rail-width) / 2);

  /* Never touches the viewport edges.

     ⚠️ THE 1100px CEILING IS A FLOOR-FOR-CONTENT, NOT A TARGET. The pill is
     shrink-to-fit, so it is only ever as wide as its children - this number
     just has to be >= the WIDEST pill in the app, which is the Specific
     Shortlist one (tally + clear + divider + its action buttons). It was 900px,
     sized when the widest pill was the Candidates screen's three buttons; the
     extra action ("Remove from Shortlist") pushed the content past the cap and,
     because every child is `flex-shrink: 0` under `flex-wrap: nowrap`, the
     overflow was not clipped or wrapped - the last button simply painted OUTSIDE
     the accent bar. Raising the cap is the fix.

     KEPT AT 1100px deliberately, even though the Shortlist pill currently
     renders only three labelled buttons (~770px) because Send Assessment is
     hidden behind ASSESSMENTS_ENABLED. A ceiling above the content width costs
     nothing on a shrink-to-fit box, and this value is already correct for the
     four-button pill that returns if that flag flips. Do not "tighten" it.

     The point at which the offset centring runs out of room for the LABELS is a
     separate, flag-sensitive number - see the media query at the end of this
     component. */
  max-width: min(calc(100vw - var(--space-8)), 1100px);

  display: flex;
  align-items: center;
  gap: var(--space-3);
  /* No wrapping: the pill shrinks by dropping labels at 600px (below)
     rather than growing a second row, which would break the fixed
     bottom offset. */
  flex-wrap: nowrap;
  padding: var(--space-2) var(--space-3);
  /* Squared off to the 4px scale step (design.md §3.3 `--radius-xs`;
     components.md §24). Was --radius-full, which made the container a true
     pill. CONTAINER ONLY - the contents are deliberately unchanged: the count
     chip stays --radius-sm (also 4px, so it reads flush with the bar), and the
     clear button and the three action buttons stay --radius-full, so the
     component now reads as a squared bar holding round controls.
     No dark-mode counterpart is needed: the [data-theme="dark"] rules below
     override only box-shadow/border-color, so both themes inherit this. */
  border-radius: var(--radius-xs);
  background: var(--accent);
  color: var(--accent-text-on);

  /* Elevation (design.md §3.4). Light mode gets the largest shadow token -
     this is the most-elevated transient surface on the page. A faint light
     hairline defines the edge where the shadow is weakest. */
  box-shadow: var(--shadow-xl);
  border: 1px solid rgba(255, 255, 255, 0.14);

  /* Enter/exit (design.md §3.5). Transform + opacity only, so the animation
     stays off the layout/paint path. The 340ms spring easing overshoots
     slightly on entry - a deliberate one-off outside the standard
     120/200/320 scale, which --ease-out cannot express. Opacity uses the
     standard --duration-base so the pill is legible before it finishes
     settling. */
  transition:
    transform 0.34s cubic-bezier(0.3, 1.1, 0.4, 1),
    opacity var(--duration-base) var(--ease-out);

  /* Hidden resting state. The element is ALWAYS MOUNTED and never carries
     `hidden`/`display: none` - either would cancel the transition, and the
     JS toggles the .is-visible class precisely so it doesn't have to.
     pointer-events is what actually makes the invisible pill inert; without
     it the off-screen pill would still swallow clicks at the page bottom. */
  transform: translate(-50%, 140%);
  opacity: 0;
  pointer-events: none;
}

.bulk-pill.is-visible {
  transform: translate(-50%, 0);
  opacity: 1;
  pointer-events: auto;
}

/* design.md §3.5: honour prefers-reduced-motion. The pill fades only. The
   transform is pinned to its VISIBLE value in both states - not merely
   un-transitioned - otherwise the hidden state would leave it parked 140%
   below the viewport and it would fade in somewhere off-screen. */
@media (prefers-reduced-motion: reduce) {
  .bulk-pill,
  .bulk-pill.is-visible {
    transition: opacity var(--duration-base) var(--ease-out);
    transform: translate(-50%, 0);
  }
}

/* ---- Count tally (components.md §24) ---- */
.bulk-pill__tally {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding-left: var(--space-2);
  font-size: var(--text-sm);
  white-space: nowrap;
}

/* Small inset tile in the page's base surface, which reads as a lighter
   cut-out on the accent pill in BOTH themes (light: near-white on deep teal;
   dark: deep teal on mint - inverted, still a clear figure/ground break). */
.bulk-pill__count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 26px;
  height: 24px;
  padding: 0 var(--space-1);
  border-radius: var(--radius-sm);
  background: var(--bg-base);
  color: var(--accent);
  font-weight: 700;
  /* Tabular figures so the pill does not shift width as the count crosses
     9 -> 10 -> 100 while the user is still clicking cards. */
  font-variant-numeric: tabular-nums;
}

/* The muted half of the tally. Reduced opacity rather than a separate token:
   --text-secondary is tuned for page surfaces and has no on-accent partner,
   so stepping the on-accent ink down is the only token-pure way to mute it
   in both themes at once. Kept at .75 so it stays comfortably legible. */
.bulk-pill__label {
  opacity: 0.75;
}

/* ---- Clear selection (design.md §4.4) ----
   The glyph is 18px but the control is a full 44x44 target: the extra area
   is invisible padding, exactly as §4.4 prescribes for icon-only controls. */
.bulk-pill__clear {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  flex-shrink: 0;
  padding: 0;
  border: 0;
  border-radius: var(--radius-full);
  background: transparent;
  color: var(--accent-text-on);
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out);
}

.bulk-pill__clear svg {
  width: 18px;
  height: 18px;
}

.bulk-pill__clear:hover {
  /* --accent-hover is one step from --accent in both themes, so the fill
     stays on the same side of the contrast line as the pill and the
     --accent-text-on glyph keeps reading against it. */
  background: var(--accent-hover);
}

/* ---- Divider ----
   currentColor so it tracks --accent-text-on through both themes without a
   second token; decorative only, hidden from the a11y tree in the markup. */
.bulk-pill__divider {
  width: 1px;
  height: 24px;
  flex-shrink: 0;
  background: currentColor;
  opacity: 0.25;
}

/* ---- Action buttons (design.md §7) ----
 * STANDALONE CLASSES, NOT `.btn.btn-secondary`. This is load-bearing.
 *
 * HISTORY: the previous bulk bar used `.btn-secondary`, whose shared hover
 * rule fills the button with `--bg-surface`. That is correct on a normal
 * page surface (dark --accent label on a light fill) but catastrophic on an
 * accent-filled bar, where the label is inverted to --accent-text-on: the
 * fill collapsed the text into the background at ~1.04:1 light / ~1.09:1
 * dark - the labels effectively vanished on hover. The scoped override that
 * fixed it had to restate the shared rule's full
 * `:not(:disabled):not([aria-disabled="true"])` chain just to reach a
 * winning specificity, and any future edit that shortened the selector would
 * silently reintroduce the bug.
 *
 * The pill sidesteps the fight entirely: `.bulk-pill__btn` is not
 * `.btn-secondary`, so the shared rule never matches and there is no
 * specificity race to lose. DO NOT re-add `.btn` / `.btn-secondary` to these
 * buttons - it would resurrect the exact collapse described above. */
.bulk-pill__btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  flex-shrink: 0;
  height: 36px;
  padding: 0 var(--space-3);
  border-radius: var(--radius-full);
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  /* Ghost-on-accent: transparent fill, on-accent ink and hairline.
     1.5px matches design.md §7's secondary outline weight. */
  background: transparent;
  color: var(--accent-text-on);
  border: 1.5px solid var(--accent-text-on);
  transition:
    background var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out);
}

/* ⚠️ REQUIRED, not defensive. `display: inline-flex` above has the same
   specificity as the UA's `[hidden]{display:none}` and wins on cascade order, so
   a `hidden` pill button stays VISIBLE without this. Added when the Candidates
   bulk bar's "Send Assessment" was hidden (components.md §86) and turned out to
   still render. Exactly the same trap `.btn[hidden]`, `.alert[hidden]` and
   `.search-filter-bar__clear[hidden]` already work around in this file. */
.bulk-pill__btn[hidden] {
  display: none;
}

/* ---- Tooltip carrier (components.md §93) ----
   A transparent wrapper whose ONLY job is to hold [data-tooltip] for a button
   that is dimmed by the disabled state below.

   ⚠️ It exists because `opacity: 0.4` on the button also fades the button's own
   ::after, and opacity cannot be undone by a descendant - the tooltip rendered
   at 40% and was barely readable. Hanging the tooltip on a wrapper OUTSIDE the
   dimmed element is the only fix that keeps design.md §7's 40% disabled
   treatment intact. The keyboard path is carried by the
   [data-tooltip]:focus-within rule in dashboard-shell.css, since a span is not
   focusable itself.

   inline-flex so the wrapper is layout-invisible: the button remains the thing
   that sizes, and the pill's own flex gap still spaces it like every other
   direct child. */
.bulk-pill__tip {
  position: relative;
  display: inline-flex;
  flex-shrink: 0;
}

/* design.md §4.4: the visible pill is 36px tall to keep the bar compact, but
   the HIT AREA must still be >=44px. This pseudo-element extends the target
   4px above and below without affecting layout. */
.bulk-pill__btn::before {
  content: "";
  position: absolute;
  inset: -4px 0;
}

.bulk-pill__btn svg {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}

/* Hover restates BOTH background and colour, so the legible pairing is
   explicit at the point of use and can never be half-inherited. */
.bulk-pill__btn:hover:not(:disabled):not([aria-disabled="true"]) {
  background: var(--accent-hover);
  border-color: var(--accent-text-on);
  color: var(--accent-text-on);
}

/* Primary action (Add to Shortlist): filled, using the page's base surface
   as the fill and the pill's own accent as the ink - the inverse of the
   ghost buttons. In dark mode this resolves to a deep-teal chip on the mint
   pill; in light mode a white chip on the deep-teal pill. */
.bulk-pill__btn--primary {
  background: var(--bg-base);
  color: var(--accent);
  border-color: var(--bg-base);
  font-weight: 700;
}

.bulk-pill__btn--primary:hover:not(:disabled):not([aria-disabled="true"]) {
  /* --accent-tint is the muted partner of --accent in both themes, so it
     stays light enough for the --accent label to hold contrast. Colour is
     restated for the same reason as above. */
  background: var(--accent-tint);
  border-color: var(--accent-tint);
  color: var(--accent);
}

/* ---- Danger action (Remove from Shortlist, components.md §37) ----
   AT REST IT IS AN ORDINARY GHOST BUTTON. That is the decision, not an
   oversight: this bar appears whenever an employer selects a card, including
   when they are only browsing, and a permanently red button on an accent bar
   reads as an alarm on a screen where nothing is wrong. It commits to the
   meaning at the point of action instead - the red arrives on hover/focus, the
   moment the pointer is actually on the destructive control - which is the same
   escalation `.btn-danger` uses one step earlier. The confirm dialog behind it
   (design.md §10) is the real safety net; this is the warning colour, not the
   guard.

   Ink is a local custom property rather than a hardcoded white because
   --color-error flips from a deep red (#b91c1c) to a light one (#f87171) in
   dark mode, and white on #f87171 is 2.77:1 - a fail. Measured: light
   #b91c1c/white = 6.47:1, dark #f87171/--neutral-900 = 6.77:1.

   Source order matters: this rule and the generic .bulk-pill__btn:hover above
   have the SAME specificity (0,4,0), so it must stay below it to win. */
.bulk-pill__btn--danger {
  --bulk-danger-ink: var(--color-white);
}

.bulk-pill__btn--danger:hover:not(:disabled):not([aria-disabled="true"]),
.bulk-pill__btn--danger:focus-visible {
  background: var(--color-error);
  border-color: var(--color-error);
  color: var(--bulk-danger-ink);
}

/* Both selectors are needed to match how tokens.css pairs an explicit
   [data-theme] with the OS preference - same pattern as the .bulk-pill
   dark-mode elevation rules below. */
[data-theme="dark"] .bulk-pill__btn--danger {
  --bulk-danger-ink: var(--neutral-900);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .bulk-pill__btn--danger {
    --bulk-danger-ink: var(--neutral-900);
  }
}

/* design.md §7: 40% opacity + not-allowed, excluded from hover by the
   :not() chains above.

   ⚠️ ONE REAL CONSUMER NOW (components.md §24): the Candidates pill's
   #bulk-send-email-btn is inactive while any selected candidate is still
   hidden, since there is no address to send to. It uses `aria-disabled`, NOT
   the `disabled` property, and that choice is load-bearing rather than
   stylistic - a truly disabled button dispatches no pointer events and is not
   focusable, so its [data-tooltip] explanation could never be reached by
   hover OR by keyboard. The reason is duplicated into the button's aria-label
   for screen readers (design.md §7: disable "with an explanation where the
   reason isn't obvious"). The click handler carries its own guard, because an
   aria-disabled button still fires click. */
.bulk-pill__btn:disabled,
.bulk-pill__btn[aria-disabled="true"] {
  opacity: 0.4;
  cursor: not-allowed;
}

/* ---- Focus (design.md §4.2) ----
   2px, 2px offset, on EVERY interactive control in the pill. The ring colour
   is --accent-text-on rather than the usual --focus-ring: --focus-ring is
   accent-toned, and on an accent-filled surface it would be invisible. This
   is a deliberate, documented deviation (components.md §24). */
.bulk-pill__btn:focus-visible,
.bulk-pill__clear:focus-visible {
  outline: 2px solid var(--accent-text-on);
  outline-offset: 2px;
}

/* The primary button's fill IS --bg-base, so an --accent-text-on ring would
   disappear into it in light mode. Its ring takes the pill's accent instead,
   which reads against both the button fill and the pill behind it. */
.bulk-pill__btn--primary:focus-visible {
  outline-color: var(--accent);
}

/* ---- Elevation in dark mode (design.md §3.4) ----
   "Dark mode uses no shadows - elevation comes from surface lightening plus
   a --border-default hairline." Both selectors are needed to match how
   tokens.css pairs an explicit [data-theme] with the OS preference. */
[data-theme="dark"] .bulk-pill {
  box-shadow: none;
  border-color: var(--border-default);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .bulk-pill {
    box-shadow: none;
    border-color: var(--border-default);
  }
}

/* ---- Widest pill, medium viewports (components.md §24) ----
   SPECIFIC SHORTLIST ONLY. That screen's pill carries one action more than the
   Candidates set (§24 records why: it adds "Remove from Shortlist"), so it is
   the widest pill in the app and stops fitting before the shared 600px collapse
   point. The pill is centred on the CONTENT COLUMN (`left: 50% + rail/2`), so
   its right edge sits at `50vw + 38px + w/2`; keeping that inside the viewport
   gutter needs `w <= 100vw - 124px`. Below that the buttons overflow the accent
   bar instead of wrapping, since the pill is `nowrap` with non-shrinking
   children.

   ⚠️ THIS THRESHOLD TRACKS THE NUMBER OF *VISIBLE* BUTTONS, AND IT IS EASY TO
   GET WRONG - it already was once, in both directions.

   History, because the arithmetic is the only way to re-derive this number:
     - 1099px, for FOUR labelled buttons (~960px content, ~1084px viewport).
     - Corrected to 899px once it was noticed that
       `#detail-bulk-assessment-btn` is `hidden` behind ASSESSMENTS_ENABLED
       (assets/js/lib/feature-flags.js), leaving only THREE visible (~770px
       content, ~894px viewport). The old number had been stripping labels off a
       pill with ~200px to spare.
     - BACK TO 1099px NOW: "Download CVs" (components.md §106) adds a fourth
       VISIBLE button to this pill, so the four-button budget above applies
       again - Remove from Shortlist -> Add to another Shortlist -> Download CVs
       -> Send Email. At 899px those four would paint outside the accent bar,
       which is the exact failure this query exists to prevent.

   ⚠️ IF ASSESSMENTS_ENABLED IS FLIPPED ON, a FIFTH visible button appears here
   and this must rise again (~1150px of content, so ~1299px) - re-run the
   `w <= 100vw - 124px` budget above rather than guessing.

   The treatment is the SAME ONE the 600px query applies, just triggered
   earlier for this pill: every action label drops to its icon + aria-label,
   and the tally keeps its number. ALL of them collapse together - leaving the
   short labels in would make those buttons read as a different kind of
   control from their icon-only neighbours (same reasoning as the note in the
   600px query below). The tally's "selected" is KEPT here: there is room for
   it at this width, and it is what makes the count legible as a count.

   ⚠️ Every id below must have an aria-label in the markup - the label span is
   its only visible name once collapsed. */
@media (max-width: 1099px) {
  #shortlist-detail-bulk-bar .bulk-pill__btn-text {
    display: none;
  }

  /* design.md §4.4's 44px floor, for the same reason as the 600px query: the
     ::before hit area only stretches vertically, so width is held here. */
  #shortlist-detail-bulk-bar .bulk-pill__btn {
    min-width: 44px;
    padding: 0 var(--space-2);
  }
}

/* ---- Candidates pill, medium viewports (components.md §24 + §106) ----
   NEW WITH DOWNLOAD CVs, and the same budget as the shortlist query above.
   The Candidates pill used to hold two labelled actions (Add to Shortlist ->
   Send Email), which fit comfortably down to the shared 600px collapse. Adding
   "Download CVs" makes three (~770px of content, needing ~894px of viewport by
   the `w <= 100vw - 124px` budget derived above), so it now needs its own
   earlier collapse for exactly the reason the shortlist pill has one.

   Separate query rather than a shared selector: the two pills hold a different
   number of buttons and therefore collapse at different widths. Merging them
   would drag one of the two to the wrong threshold.

   ⚠️ IF ASSESSMENTS_ENABLED IS FLIPPED ON, `#bulk-assessment-btn` becomes
   visible here too and this must rise to 1099px to match the shortlist pill. */
@media (max-width: 899px) {
  #bulk-action-bar .bulk-pill__btn-text {
    display: none;
  }

  #bulk-action-bar .bulk-pill__btn {
    min-width: 44px;
    padding: 0 var(--space-2);
  }
}

/* ---- Narrow viewports (components.md §24) ----
   600px is the same breakpoint at which the icon rail goes off-canvas
   (dashboard-shell.css), so the pill returns to a true viewport centre at
   exactly the point the rail stops occupying space. Labels drop out to keep
   the pill inside the viewport: the word "selected" (the number alone still
   reads, and the live region still announces the full sentence) and EVERY
   action label, each falling back to its icon plus the aria-label already on
   the button. Every control keeps its >=44px target - only text is removed,
   never padding. */
@media (max-width: 599px) {
  .bulk-pill {
    left: 50%;
    gap: var(--space-2);
    max-width: calc(100vw - var(--space-4));
  }

  .bulk-pill__label,
  .bulk-pill__btn-text {
    display: none;
  }

  /* Icon-only now, so the horizontal padding squares up around the glyph.
     min-width is load-bearing, not cosmetic: with the label gone the button
     collapsed to 35px wide, under design.md §4.4's 44px floor. The ::before
     on .bulk-pill__btn only extends the target VERTICALLY, so the width has
     to be held here.
     BOTH Candidates actions collapse, and all three of the Shortlist detail
     screen's. Leaving one label would make that button read as a different kind
     of control from its neighbours, and the labels plus the tally do not fit a
     320px viewport anyway. (The reveal buttons that used to head this list are
     gone with the reveal gate, components.md §24.) The
     Assessments Active pill (components.md §44) collapses BOTH its action
     labels, so its two buttons need the same floor here - same rule, not a
     fork.

     ⚠️ Every id listed here must have an aria-label in the markup, since the
     label span is its only visible name. */
  #bulk-send-email-btn,
  #bulk-shortlist-btn,
  #detail-bulk-send-email-btn,
  #detail-bulk-existing-btn,
  #detail-bulk-remove-btn,
  #bulk-update-deadlines-btn,
  #bulk-cancel-all-btn {
    min-width: 44px;
    padding: 0 var(--space-2);
  }
}

/* =========================================================================
 * Drawer (design.md §10 - generic right-side panel). Used by the Candidate
 * Detail Drawer (components.md "Candidate Detail Drawer" / "Resume View").
 * ========================================================================= */
.drawer-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(6, 27, 49, 0.5);
  z-index: var(--z-drawer);
}

/* Fades in alongside the drawer's slide. Written as a keyframe animation on
   the visible state rather than a transition between two static states, for
   the fail-safe reason documented on .candidate-drawer below. */
.drawer-backdrop:not([hidden]):not(.is-closing) {
  animation: drawer-backdrop-in var(--duration-base) var(--ease-out);
}

.drawer-backdrop.is-closing {
  animation: drawer-backdrop-out var(--duration-base) var(--ease-in) forwards;
}

.drawer-backdrop[hidden] {
  display: none;
}

@keyframes drawer-backdrop-in {
  from {
    opacity: 0;
  }
}

@keyframes drawer-backdrop-out {
  to {
    opacity: 0;
  }
}

.candidate-drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: 480px;
  max-width: 100vw;
  background: var(--bg-base);
  border-left: 1px solid var(--border-default);
  box-shadow: var(--shadow-xl);
  z-index: var(--z-drawer);
  overflow-y: auto;
  overflow-x: hidden; /* guards against the sticky footer's full-bleed negative margins (below) ever poking out sideways */
  /* No bottom padding here on purpose: a scroll container's own padding
     always reserves empty space after its last child, regardless of that
     child's margins - a negative-margin/sticky-offset trick can't "delete"
     that reserved space (verified empirically). .candidate-drawer__actions
     supplies its own bottom padding instead, so the sticky footer sits
     flush with this container's true edge with no gap below it. */
  padding: var(--space-6) var(--space-5) 0;
}

/* Slide-in from the right edge.
   ---------------------------------------------------------------------------
   ⚠️ Deliberately a keyframe animation ON THE VISIBLE STATE, not a transition
   between an off-screen default and an .is-open class. With the transition
   approach the drawer's *resting* style is translateX(100%), so anything that
   stops the animation from running - a throttled or backgrounded renderer,
   where neither rAF nor CSS transitions tick - leaves it un-hidden but parked
   fully off-screen: an open dialog with a trapped focus ring and nothing
   visible. That was observed in practice, not theorised.

   Written this way the resting style IS the visible one. The animation only
   plays the entrance; if it never runs, the drawer simply appears. Same for
   the exit: .is-closing plays it out, and candidate-drawer.js hides the
   element on a timer that does not depend on the animation finishing. */
.candidate-drawer:not([hidden]):not(.is-closing) {
  animation: drawer-in var(--duration-slow) var(--ease-out);
}

.candidate-drawer.is-closing {
  animation: drawer-out var(--duration-slow) var(--ease-in) forwards;
}

@keyframes drawer-in {
  from {
    transform: translateX(100%);
  }
}

@keyframes drawer-out {
  to {
    transform: translateX(100%);
  }
}

/* design.md §4.5 - honour "reduce motion": the drawer still appears and
   disappears, just with no slide or fade. Suppressing the animations is
   enough precisely because the resting state is the visible one (above);
   candidate-drawer.js reads the same media query so it also skips the
   exit delay and hides immediately. */
@media (prefers-reduced-motion: reduce) {
  .candidate-drawer:not([hidden]):not(.is-closing),
  .candidate-drawer.is-closing,
  .template-drawer:not([hidden]),
  .advanced-search-drawer:not([hidden]):not(.is-closing),
  .advanced-search-drawer.is-closing,
  .email-drawer:not([hidden]):not(.is-closing),
  .email-drawer.is-closing,
  .drawer-backdrop:not([hidden]):not(.is-closing),
  .drawer-backdrop.is-closing {
    animation: none;
  }
}

.candidate-drawer[hidden] {
  display: none;
}

@media (max-width: 599px) {
  .candidate-drawer {
    width: 100%;
  }
}

/* =========================================================================
 * Video Lightbox (components.md §102, design.md §14.8)
 * -------------------------------------------------------------------------
 * A centred overlay that plays one clip. Opened by the Employer dashboard's
 * search-steps card 05; markup in dashboard/employer/index.html, behaviour in
 * assets/js/lib/video-lightbox.js.
 *
 * REUSES .drawer-backdrop above rather than defining its own dimmer - same
 * tint, same fade, same [hidden] handling, and it inherits the reduced-motion
 * block for free. The only thing this component adds is the centred panel.
 *
 * CENTRED, NOT A SIDE PANEL, so it is deliberately NOT .candidate-drawer:
 * video is the content, and a 480px right-hand rail would letterbox it to
 * nothing. That is the whole reason this is a separate component and not a
 * drawer variant.
 *
 * NOT built on modal.js either: that component owns a header bar with a title
 * and a padded body, both of which would frame a video that should be
 * full-bleed to its own rounded corners.
 * ========================================================================= */
.video-lightbox {
  position: fixed;
  inset: 0;
  /* Above --z-drawer: this opens from a page that can also have drawers, and
     the dimmer it shares is at --z-drawer, so the panel must clear both. */
  z-index: var(--z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  /* Keeps the panel off the viewport edges on small screens, and gives the
     close button somewhere to sit without overhanging the screen. */
  padding: var(--space-4);
  /* ⚠️ LOAD-BEARING, AND NOT AN OPTIMISATION. This element is inset:0 and sits
     ABOVE the shared .drawer-backdrop (--z-modal over --z-drawer), so as a
     normal hit-target it covers the entire viewport and swallows every click
     aimed at the dimmer - click-outside-to-close silently never fires. Letting
     pointer events fall through here, and re-enabling them on the panel below,
     is what makes the backdrop reachable. Verified: without this pair, clicking
     the dimmed area does nothing. */
  pointer-events: none;
}

.video-lightbox[hidden] {
  display: none;
}

/* 16/9 with a max-width, rather than a fixed size: the panel is whatever the
   viewport allows up to 1100px, and `aspect-ratio` keeps the shape without
   needing the clip's real dimensions in CSS. min() so it also fits a phone in
   landscape, where 92vw is the binding constraint rather than 1100px. */
.video-lightbox__panel {
  position: relative;
  /* Restores hit-testing for the video and its close button - the container
     above disables it so the backdrop stays clickable. Both halves of that pair
     are required; removing either one breaks a different interaction. */
  pointer-events: auto;
  width: min(1100px, 92vw);
  max-height: 92vh;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-lg);
  /* Clips the video's own square corners to the panel's radius. */
  overflow: hidden;
  /* Black, not a surface token, in BOTH themes: this is the letterbox behind
     the frame, and a themed grey would show as mismatched bars against the
     clip. One of the few deliberate hard-coded colours - see design.md §14.8. */
  background: #000;
  box-shadow: var(--shadow-xl);
}

/* Same fail-safe pattern as .candidate-drawer above: the animation plays on
   the VISIBLE resting state, so a renderer that never runs it leaves the panel
   simply present rather than stuck at scale(0.96) and invisible. */
.video-lightbox:not([hidden]):not(.is-closing) .video-lightbox__panel {
  animation: video-lightbox-in var(--duration-base) var(--ease-out);
}

.video-lightbox.is-closing .video-lightbox__panel {
  animation: video-lightbox-out var(--duration-base) var(--ease-in) forwards;
}

@keyframes video-lightbox-in {
  from {
    opacity: 0;
    transform: scale(0.96);
  }
}

@keyframes video-lightbox-out {
  to {
    opacity: 0;
    transform: scale(0.96);
  }
}

/* object-fit: contain, NOT cover: a clip whose aspect ratio is not 16/9 must be
   letterboxed against the black panel, never cropped. */
.video-lightbox__video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* Pinned to the panel's top-right, mirroring .candidate-drawer__top-controls.
   ⚠️ Unlike every other close button in the app, this one sits on VIDEO, not on
   a themed surface - so it cannot rely on --text-primary against --bg-base and
   carries its own scrim and light ink in both themes. Without this it is
   invisible on any pale frame. The 44x44 hit area comes from .btn-icon. */
.video-lightbox__close {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  z-index: 2;
  color: var(--color-white);
  background: rgba(6, 27, 49, 0.55);
  border-radius: var(--radius-full);
}

/* ⚠️ `:not(:disabled)` is REQUIRED here, not decoration. `.btn-icon:hover:not(
   :disabled)` (0,3,0) sets `background: var(--bg-surface)`, which out-specifies
   a plain `.video-lightbox__close:hover` (0,2,0) - the scrim would be replaced
   by a pale surface fill on hover and the white glyph would disappear against
   it. Matching the selector's specificity is what keeps the scrim. */
.video-lightbox__close:hover:not(:disabled) {
  background: rgba(6, 27, 49, 0.8);
  color: var(--color-white);
}

/* design.md §4.5. The panel still appears and disappears, just without the
   scale/fade - the resting state is the visible one, and video-lightbox.js
   reads the same query so it also skips the exit delay. The shared
   .drawer-backdrop is already covered by the reduced-motion block above. */
@media (prefers-reduced-motion: reduce) {
  .video-lightbox:not([hidden]):not(.is-closing) .video-lightbox__panel,
  .video-lightbox.is-closing .video-lightbox__panel {
    animation: none;
  }
}

/* =========================================================================
 * CV Viewer (assets/js/lib/cv-viewer.js, components.md §104, design.md §10)
 * -------------------------------------------------------------------------
 * Displays the candidate's ACTUAL CV file - PDF in the browser's native viewer,
 * DOCX rendered by docx-preview - above the Candidate Detail Drawer. Opened by
 * the drawer's View CV button; replaces the old in-drawer resume panel.
 *
 * Built and torn down in JS (like .resume-viewer-overlay), so there is no
 * authored markup and no [hidden] state to style here.
 *
 * ⚠️ CLOSES ON THE × ONLY - a documented exception to design.md §10, where
 * every other overlay dismisses on backdrop click and Escape. The dimmer is
 * therefore purely visual here; cv-viewer.js binds no listener to it.
 * ========================================================================= */
.cv-viewer {
  position: fixed;
  inset: 0;
  /* Above --z-drawer: this opens FROM the candidate drawer and must sit over
     it, matching the video lightbox's use of the modal layer. */
  z-index: var(--z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
  /* Unlike the video lightbox, hit-testing is NOT disabled here: there is no
     click-through-to-dismiss behaviour to preserve, and leaving the container
     live means a stray click outside the panel lands harmlessly on it instead
     of reaching the drawer underneath. */
}

/* Tall and portrait-ish: a CV is a paged A4 document, so the panel is shaped
   to show a page rather than a video frame. */
.cv-viewer__panel {
  display: flex;
  flex-direction: column;
  width: min(900px, 94vw);
  height: min(92vh, 1100px);
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: var(--bg-base);
  box-shadow: var(--shadow-xl);
}

/* Same shape as .resume-viewer-overlay__header - heading left, close right. */
.cv-viewer__header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
  flex-shrink: 0;
  padding: var(--space-3) var(--space-3) var(--space-3) var(--space-5);
  border-bottom: 1px solid var(--border-default);
}

/* Groups the name + provenance sub-line so both sit left of the close button
   as one flex item (components.md §104). */
.cv-viewer__heading {
  min-width: 0;
}

.cv-viewer__title {
  font-weight: 600;
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Provenance sub-line (components.md §103/§104) - "uploaded by the employee" vs
   "built using Vanceli's built-in CV tool", same copy cvSourceMessage() feeds
   the drawer's sticky message above the CV buttons. */
.cv-viewer__subtitle {
  margin: var(--space-1) 0 0;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

/* THE scroll box. `min-height: 0` is load-bearing, not habit: a flex child's
   automatic minimum size is its content, so without it a long document would
   push the panel taller than the viewport instead of scrolling inside it. */
.cv-viewer__body {
  flex: 1;
  min-height: 0;
  overflow: auto;
  /* Neutral grey gutter around the white page, the convention every PDF reader
     uses. Fixed rather than tokenised: it frames DOCUMENT content (below), not
     app chrome. */
  background: #525659;
}

/* The PDF path: the browser's own viewer, with its own scrolling, zoom and
   search. `border: 0` because an iframe ships a default border. */
.cv-viewer__frame {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

/* The DOCX path. docx-preview injects its own page elements and stylesheet;
   this only centres them and stops a wide document forcing horizontal scroll on
   the whole panel. */
.cv-viewer__docx {
  padding: var(--space-4) 0;
}

.cv-viewer__docx .docx-wrapper {
  background: transparent;
  padding: 0;
}

/* ⚠️ The rendered page must stay WHITE IN BOTH THEMES. It is a document, not
   app surface - an employer downloading this CV gets a white page, and a
   dark-mode preview that disagreed with the file would be actively misleading.
   Same reason the PDF iframe is left entirely alone. */
.cv-viewer__docx .docx {
  margin: 0 auto var(--space-4);
  background: #fff;
  color: #000;
  box-shadow: var(--shadow-lg);
  max-width: 100%;
}

/* Loading and error states, centred in the grey gutter. */
.cv-viewer__status {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
  height: 100%;
  padding: var(--space-6);
  text-align: center;
  color: var(--color-white);
}

@media (max-width: 599px) {
  .cv-viewer {
    padding: 0;
  }

  .cv-viewer__panel {
    width: 100%;
    height: 100%;
    border-radius: 0;
  }
}

/* Close (x) - pinned to the drawer's top-right at every scroll depth.
   ---------------------------------------------------------------------------
   This REVERSES the earlier "plain position: static, floated, scrolls away
   with the content" treatment (see components.md §25). The redesigned header
   is sticky, so a close control that scrolled out from under it read as an
   inconsistency; Esc/backdrop remain available regardless, but the visible
   affordance now matches them.

   `position: fixed` rather than `sticky` because this button is a DOM sibling
   of #drawer-profile/#drawer-resume (one button serves both views), so it has
   no sticky-positioning parent that spans the scroll box. .candidate-drawer
   sets a `transform`, which makes it the containing block for fixed
   descendants - so these offsets resolve against the drawer, not the
   viewport, and hold at both the 480px and the full-width mobile size.
   .candidate-drawer__header reserves padding-right so nothing runs under it.
   Glyph stays 20px via the shared --icon-size-close rule; the 44x44 hit area
   is unchanged (design.md §3.7/§4.4). */
/* ---- Top-right controls: bookmark + close (components.md §25) ----
   The FIXED positioning lives on this wrapper now, not on .candidate-drawer__close
   itself. The bookmark moved out of the action footer to sit immediately left of
   the ×, matching the Candidate Card, where the same pair is already pinned
   top-right (§23).

   The wrapper is built in JS (candidate-drawer.js) rather than authored in the
   three host pages - see the note there. `gap: 0` with the buttons' own padding
   supplying the separation is the same treatment .candidate-card__top-right
   uses, so the two clusters read identically. */
.candidate-drawer__top-controls {
  position: fixed;
  top: var(--space-4);
  right: var(--space-4);
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 0;
}

/* Both glyphs at 20px. The close × already gets that from the shared
   overlay-close rule near the top of this file; the bookmark needs its own,
   or it would inherit .btn-icon's default and sit visibly smaller beside it.
   The 44x44 hit areas are untouched (design.md §3.7/§4.4). */
.candidate-drawer__shortlist-btn svg {
  width: var(--icon-size-close);
  height: var(--icon-size-close);
}

/* Its `.is-shortlisted` accent tint is NOT declared here - it shares one rule
   with the card's bookmark further up this file, so the two can never drift. */

/* Needed because the button is toggled with the `hidden` attribute in the CV
   view: .btn-icon sets `display: inline-flex`, which outranks the UA's
   [hidden] { display: none } and would leave it visible. Same trap
   .advanced-search__group-clear documents (§80). */
.candidate-drawer__shortlist-btn[hidden] {
  display: none;
}

/* Sticky header (components.md §25) - identity block + contact row stay
   pinned while the sections scroll under them.
   ---------------------------------------------------------------------------
   Full-bleed via the same negative-margin technique as the sticky footer
   below: cancel .candidate-drawer's side padding with negative left/right
   margin, then restore it as this element's own padding, so the background
   spans edge to edge. .candidate-drawer's `overflow-x: hidden` keeps the
   bleed from producing a horizontal scrollbar. The background must be opaque
   or scrolled content shows through. padding-right clears the fixed top-right
   cluster, which is now TWO 44px targets (bookmark + close) plus a gap, not
   one - widen this if a third control ever joins them, or the identity block's
   name will run underneath. */
.candidate-drawer__header {
  position: sticky;
  /* NOT `top: 0`. A sticky offset resolves against the scroll container's
     PADDING edge, and .candidate-drawer has var(--space-6) of top padding -
     so `top: 0` pins the header 32px below the drawer's true top edge and
     leaves a gap that scrolled content shows through. Cancelling the
     padding here pins it flush to the border edge instead, which is also
     exactly where its negative top margin already places it at rest. */
  top: calc(-1 * var(--space-6));
  z-index: 1;
  margin: calc(-1 * var(--space-6)) calc(-1 * var(--space-5)) var(--space-5);
  padding: var(--space-5) var(--space-5) var(--space-4);
  padding-right: calc(88px + var(--space-4));
  background: var(--bg-base);
  border-bottom: 1px solid var(--border-default);
}

/* Identity block. Also used, unsticky, by the candidate's own Profile screen
   preview (assets/js/pages/candidate-profile.js, components.md §57) - hence
   the `.candidate-profile__` namespace rather than a drawer-only one. */
.candidate-profile__header {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}

.candidate-profile__avatar {
  width: 64px;
  height: 64px;
  flex-shrink: 0;
  border-radius: var(--radius-full);
  object-fit: cover;
  background: var(--bg-surface);
}

.candidate-profile__identity {
  min-width: 0; /* lets long names ellipsize instead of forcing the flex row wider */
}

/* text-h5/20px - the design.md §2.2 in-app ceiling. The drawer already remaps
   --text-h1..h4 to --text-h5 at its root, so this is belt-and-braces for the
   Profile-screen copy, which sits inside .dashboard-shell instead. */
.candidate-profile__name {
  font-size: var(--text-h5);
  line-height: 1.3;
  margin: 0;
  overflow-wrap: anywhere;
}

.candidate-profile__sub {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  margin: 2px 0 0;
}

/* Role as a quiet pill. Deliberately its own class, not the card's bare
   `.candidate-card__role` (which this header used to borrow) - see the CLASS
   OWNERSHIP note in candidate-profile-view.js. */
.candidate-profile__role-pill {
  display: inline-block;
  margin: var(--space-2) 0 0;
  padding: 2px var(--space-2);
  border: 1px solid var(--accent-tint-border);
  border-radius: var(--radius-full);
  background: var(--accent-tint);
  color: var(--accent-ink);
  font-size: var(--text-xs);
  font-weight: 600;
}

/* Contact row (components.md §15): the candidate's email and phone in the
   drawer's sticky header. Now UNCONDITIONAL - it used to render only once the
   employer had revealed the candidate, with a
   .candidate-drawer__contact-hidden line standing in otherwise. That gate and
   that rule are both gone. */
.candidate-profile__contact {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  margin-top: var(--space-3);
}

.candidate-profile__contact-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.candidate-profile__contact-item span {
  overflow-wrap: anywhere;
}

.candidate-profile__contact-item svg {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  color: var(--icon-muted);
}

/* ---- Send Email, in the sticky header (components.md §25) ----
   Moved out of the bottom action bar by product request so it sits directly
   under the email/phone row it acts on, and stays reachable at every scroll
   depth rather than only at the end of a long profile.

   FULL BLEED IS DELIBERATE, AND `width: 100%` IS THE WHOLE MECHANISM.
   `.candidate-drawer__header` is a plain BLOCK (not flex - see that rule), so
   this button stretches to the header's content box on that one declaration:
   no flex container, no `display` override, no alignment rule needed. `.btn`'s
   own `display: inline-flex` keeps the glyph and label centred inside the
   stretched box.

   This reverses the earlier shrink-wrapped `.btn-sm` treatment, which existed
   so a full-height primary would not compete with the candidate's name above
   it. The drawer now has exactly ONE primary in total - the footer's two CV
   actions are both secondary at equal width - so the full bleed is the signal
   carrying that hierarchy, and the name holds its own through the type scale.
   See the matching note in candidate-drawer.js.

   ⚠️ IT MUST END UP EXACTLY AS WIDE AS THE FOOTER'S TWO CV BUTTONS COMBINED -
   that is the stated requirement, and it is what the arithmetic below targets.

   ⚠️ THE NEGATIVE MARGIN CANCELS A DIFFERENCE, NOT THE WHOLE PADDING. Two
   earlier attempts shipped visibly wrong from the running app:

     1. `width: 100%` alone - inset on the right. `.candidate-drawer__header`
        reserves `padding-right: calc(88px + var(--space-4))` (104px) to clear
        the FIXED top-right cluster (bookmark + close), so its content box stops
        104px short while the footer's stops only var(--space-5) (24px) short.
        `width: 100%` faithfully filled the SHORTER box.
     2. Cancelling the full 104px - the button came out NARROWER still. It
        overshot the drawer's own content edge by 24px, and `.candidate-drawer`
        carries `overflow-x: hidden`, so the excess was clipped rather than
        drawn.

   The footer bar is the reference: it bleeds to `calc(-1 * var(--space-5))` and
   re-pads by var(--space-5), so its content spans the drawer minus 24px a side.
   This button starts 104px short on the right, so it needs back exactly the
   DIFFERENCE - 104px minus that same 24px - to land on the identical edge. At
   480px drawer width both then measure 432px. Written in terms of the two
   tokens rather than as `80px` so it tracks any change to either.

   The header's padding itself must NOT be reduced to fix this: it is what keeps
   the name and contact rows from running underneath the close button. Only this
   button opts out, and only on the right - it clears the cluster by sitting
   BELOW it, not beside it. */
.candidate-drawer__email-btn {
  /* `flex` not `inline-flex` (.btn's own value): an inline-level box shrink-wraps
     to its label, so the negative margin below would just drag the shrink-wrapped
     pill rightwards instead of widening it. As a block-level flex container it
     fills the line box, which is what makes the bleed work - and `.btn`'s own
     `justify-content: center` still centres the glyph + label inside it. */
  display: flex;
  /* ⚠️ THE WIDTH AND THE MARGIN MUST BOTH BE STATED, AND THE WIDTH MUST INCLUDE
     THE RECLAIMED SPACE. Three earlier attempts each failed in a different way,
     all confirmed by measuring the running app in a headless browser:
       1. `width: 100%` alone            -> 352px: filled the header's SHORT
                                            content box, inset ~80px on the right.
       2. negative margin, no width      -> 151px: `.btn` sets no width and a
                                            flex box still sizes to its content,
                                            so the margin only SHIFTED the
                                            shrink-wrapped pill rightwards.
       3. `width: 100%` + margin         -> 351px: `100%` fixes the used width
                                            outright, so the margin could not
                                            widen it - it moved the right edge
                                            and left the same 80px shortfall.
     `100%` plus the reclaimed difference is what actually spans the footer's
     width: the header's content box (drawer - 24px - 104px) plus the 80px the
     margin pulls back = the footer's box (drawer - 24px - 24px). Measured 431px
     against the footer's 431px at 480px drawer width, and 331px vs 331px at the
     380px breakpoint. */
  width: calc(100% + (88px + var(--space-4) - var(--space-5)));
  margin-top: var(--space-3);
  margin-right: calc(-1 * (88px + var(--space-4) - var(--space-5)));
}

/* ---- Classifications section (components.md §56/§87, design.md §9) ----
   Sits between the facts box and Skills in the Candidate Detail Drawer. One
   group per classification: a bold name over a quiet middot-joined line of that
   classification's sub-classifications.

   Deliberately NOT chips. design.md §9 already specifies middot-joined text for
   classifications on the candidate row, and a chip treatment here would read as
   skills - which sit directly below this section. The name/sub-line pair is
   what carries the parent/child relationship the flat namespaced values encode.

   The sub-line is the one value in this panel allowed to soft-wrap rather than
   ellipsize (same rule as the card's classifications row): taxonomy names are long,
   and truncating one would hide which specialisation a candidate actually
   chose. */
.candidate-profile__classification-group {
  margin-bottom: var(--space-3);
}

.candidate-profile__classification-group:last-child {
  margin-bottom: 0;
}

.candidate-profile__classification-name {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-primary);
}

.candidate-profile__sub-classifications {
  margin: 2px 0 0;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  overflow-wrap: anywhere;
}

/* "Assessment Status: <status>" line - shown above the facts panel (see
   candidate-profile-view.js, components.md §25). */
.candidate-profile__assessment-status {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  font-weight: 600;
  margin: 0 0 var(--space-5);
}

/* Facts panel: location+timezone / work mode * employment type / cumulative
   experience. Boxed so it reads as one unit of "hard facts" separate from the
   narrative sections below. */
.candidate-profile__facts {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-bottom: var(--space-6);
  padding: var(--space-4);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  background: var(--bg-surface);
}

.candidate-profile__fact {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.candidate-profile__fact svg {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  margin-top: 2px;
  color: var(--icon-muted);
}

.candidate-profile__section {
  margin-bottom: var(--space-6);
}

/* Uppercase overline section header (text-xs per the redesign spec). Replaces
   the previous text-base + underline treatment. */
.candidate-profile__overline {
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-secondary);
  margin: 0 0 var(--space-3);
}

/* ---- Skills ----
   ⚠️ THE TWO-COLUMN "name + N yrs" LIST IS GONE, and with it
   .candidate-profile__skills / __skill-row / __skill-name / __skill-years.

   It was a hairline-separated row per skill with a right-aligned, tabular-nums
   year count. There is no year count any more: skills are no longer
   collected as name+years pairs at all. A candidate attaches skills to each
   individual JOB, and the set an employer sees is the derived union of those
   (assets/js/lib/candidate-derived.js#cumulativeSkills) - names only.

   The drawer now renders them through .candidate-profile__entry-skills below
   (the same `.chip.chip-static` pills the per-job entries use), so the
   cumulative set and the per-job sets read as one vocabulary.

   The old comment here explained why there was deliberately NO proficiency bar
   - that reasoning outlived the rules it guarded and is now moot: there is no
   `years` field left to be tempted into drawing one from. If a real proficiency
   field is ever added, design.md §4.6 still applies - pair any bar with its
   numeric value, colour/length is never the only signal. ---- */

/* ---- Work experience timeline. The rail and node markers are pure
   decoration drawn with a border and a ::before dot - the entries stay plain
   semantic elements, so the history reads identically with CSS off or in a
   screen reader. list-style is cleared explicitly because this is an <ol>
   (chronological history is genuinely ordered) and base.css styles lists. ---- */
.candidate-profile__timeline {
  list-style: none;
  margin: 0;
  padding: 0;
}

.candidate-profile__timeline-item {
  position: relative;
  margin: 0;
  padding: 0 0 var(--space-5) var(--space-5);
  border-left: 1px solid var(--border-default);
}

/* Last entry: stop the rail at the final node instead of running past it. */
.candidate-profile__timeline-item:last-child {
  padding-bottom: 0;
  border-left-color: transparent;
}

.candidate-profile__timeline-item::before {
  content: "";
  position: absolute;
  left: -4.5px;
  top: 6px;
  width: 8px;
  height: 8px;
  border-radius: var(--radius-full);
  background: var(--accent);
  /* Ring in the drawer's own background so the dot reads as sitting ON the
     rail rather than being crossed by it. */
  box-shadow: 0 0 0 3px var(--bg-base);
}

.candidate-profile__entry {
  margin-bottom: var(--space-4);
}

.candidate-profile__entry:last-child {
  margin-bottom: 0;
}

.candidate-profile__entry-title {
  font-weight: 700;
  font-size: var(--text-sm);
}

.candidate-profile__entry-meta {
  font-size: var(--text-xs);
  color: var(--text-secondary);
  margin: 2px 0 var(--space-2);
}

.candidate-profile__entry-body {
  font-size: var(--text-sm);
}

/* Per-entry skill chips (work experience). Own namespace rather than the
   card's `.candidate-card__skills`. */
.candidate-profile__entry-skills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-top: var(--space-2);
}

.candidate-drawer__actions {
  /* Sticky footer: stays pinned to the bottom of the drawer's own scroll box
     while the content scrolls underneath it. .candidate-drawer has no bottom
     padding of its own (see that rule) specifically so this can sit flush with
     the true bottom edge via a plain `bottom: 0` - this bar supplies its own
     bottom padding (var(--space-6)) instead, so there's still comfortable
     breathing room below the buttons. Horizontal bleed (full width) uses
     negative left/right margin to cancel the drawer's side padding.

     ⚠️ SERVES BOTH DRAWER VIEWS. It was the profile view's footer only; the CV
     view now renders the same class for its "Download CV" bar (components.md
     §26), so any change here lands on both.

     ⚠️ THE BACKGROUND MUST STAY FULLY OPAQUE. `--bg-base` is a solid colour in
     both themes and that is load-bearing, not incidental: content scrolls
     directly beneath this bar, and any alpha, tint or backdrop-filter here
     means text visibly runs through the buttons. */
  position: sticky;
  bottom: 0;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin: var(--space-5) calc(-1 * var(--space-5)) 0;
  padding: var(--space-4) var(--space-5) var(--space-6);
  background: var(--bg-base);
  border-top: 1px solid var(--border-default);
}

/* ---- Full-bleed 50/50 split (components.md §25) ----
   The profile view's bar spans edge-to-edge, divided equally between its two
   buttons - by id, not by class, since the split is a two-specific-buttons
   layout, not a reusable pattern. If the third (assessment) action is ever
   un-hidden the footer's shape needs a fresh decision; these rules do not
   attempt to guess it.

   The split was 40/60 when this bar held one primary and one secondary. Both
   buttons are secondary now (the drawer's one primary is Send Email, up in the
   sticky header), so there is no hierarchy left for unequal widths to express.
   THE RATIO IS THE ONLY THING THAT CHANGED - every word below about the
   flex-basis still holds exactly as written.

   ⚠️ THE `0` FLEX-BASIS IS WHAT STOPS THE ROW WRAPPING. Do not "restore" a
   percentage basis here. CSS Flexbox decides line-wrapping (spec §9.3) BEFORE
   flex-shrink is resolved, using each item's un-shrunk hypothetical size - so
   with any percentage basis the two hypothetical sizes plus the row's own
   `gap: var(--space-2)` must fit the container, and 100% of any split + 8px does
   not. The wrap algorithm then drops the second button onto its own line where,
   alone with `flex-grow`, it stretches to full width: one button stacked above a
   full-width other. That was the reported bug. (It was first hit with the 40/60
   split this bar used to carry, but nothing about it is specific to that ratio -
   an even 50/50 in percentages overflows by the same 8px.)

   A previous fix reserved the gap inside each basis (`calc(40% - 4px)` /
   `calc(60% - 4px)`) so the two sums landed on exactly the content width. It
   worked, but only with ZERO rounding slack, and it was verified in Chromium
   only - an engine that rounds percentage bases differently can tip it back
   over. It also silently broke if `--space-2` changed, since the `4px` was a
   hardcoded half of it.

   A basis of `0` removes the arithmetic entirely: each item's hypothetical size
   is 0, so `0 + 0 + gap` can never exceed the container and the wrap check
   cannot fail - at any width, zoom, device pixel ratio or engine. The free
   space (content width minus the gap) is then divided by the GROW RATIO, which
   is where the split actually lives - derived, instead of hardcoded as a width.

   `min-width: 0` on both is load-bearing and unrelated to the above: a
   `flex-basis: 0` does NOT override an item's automatic minimum size, so
   without it each button still refuses to shrink below its own min-content and
   would overflow sideways in a narrow drawer. Same reasoning as the 50% column
   split at :490 in this file. */
/* ⚠️ THE IDS CHANGED WITH THE §26 REWORK, THE FLEX MATH DID NOT.
   The bar used to hold [View CV | Send Email] (#drawer-resume-btn /
   #drawer-email-btn). Send Email moved into the sticky header and the CV view
   became its own overlay, so the bar now holds the two CV actions:
   [Download CV | View CV], now at an even split since both are secondary. Every
   word of the wrap reasoning above still applies - only the selectors were
   repointed and the grow ratio evened out. #drawer-resume-btn no longer exists
   anywhere; do not resurrect it.

   Both buttons are always active now - every employee has a CV, so there is
   no disabled-state wrapper span to size any more (the old
   .candidate-drawer__cv-tip:has() rules were removed with it). */
#drawer-download-cv-btn,
#drawer-view-cv-btn {
  flex: 1 1 0;
  min-width: 0;
}

/* Sticky provenance message (components.md §25/§103) - the FIRST child of
   .candidate-drawer__actions, above the two buttons. flex-basis: 100% on a
   flex-wrap row is what forces it onto its own line without a fixed height;
   the buttons then wrap below it exactly as they would on a narrow drawer.
   Deliberately NOT an .alert - a tinted alert background inside this bar's
   opaque .candidate-drawer__actions surface would read as a second stacked
   surface. This is a quiet caption instead, same weight as .sample-notice. */
.candidate-drawer__cv-source {
  flex-basis: 100%;
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  margin: 0 0 var(--space-1);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.candidate-drawer__cv-source svg {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  margin-top: 1px;
}

/* `.candidate-drawer__primary-action` was REMOVED here (components.md §25). It
   pushed the footer bar's one primary to the far right with `margin-left: auto`,
   back when that bar held a primary at all. It doesn't: the drawer's single
   primary is Send Email in the sticky header, and both CV buttons in the footer
   are secondary at equal flex. The last element carrying the class was View CV,
   which no longer does, so the rule styled nothing. Do not re-add it looking for
   a "one primary" marker in this bar - the marker for that is the header button.

   The footer's shortlist bookmark rule that used to sit here is GONE - the
   bookmark moved to the fixed top-right cluster (.candidate-drawer__top-controls
   above), which carries its own 20px glyph sizing. The footer holds only real
   `.btn`s now, so the shared `.btn svg` rule covers everything in it. */

/* `.candidate-drawer__contact-hidden` was REMOVED here. It styled the drawer's
   'Hidden - select "Show Contact Details" to reveal' line, which no longer
   renders: the reveal gate over name/photo/email/phone is retired and the
   contact row is unconditional (components.md §15, candidate-drawer.js). */

/* =========================================================================
 * Template Builder Drawer (assets/js/lib/template-drawer.js, components.md
 * "Template Builder Drawer") - the second concrete Drawer implementation,
 * following the Candidate Detail Drawer's conventions above (non-sticky
 * close icon, always-visible bottom action bar) but not sharing code with
 * it, since the content models are unrelated. Wider than .candidate-drawer
 * (560px vs 480px) to comfortably fit the builder's form fields/question
 * blocks.
 *
 * Layout note: unlike .candidate-drawer's "position: sticky" footer (safe
 * there only because a candidate profile's content reliably exceeds the
 * viewport height), a freshly-created template can be SHORT (one question,
 * or none yet) - a sticky-within-scroll-container footer would then overlap
 * the "Add Question" buttons underneath it (confirmed empirically:
 * elementFromPoint() at the button's coordinates resolved to the footer,
 * silently swallowing clicks). Fixed with a more robust flex layout instead:
 * .template-drawer is a column flex container, .template-drawer__scroll is
 * the ONLY scrolling region (flex:1), and .template-drawer__actions is a
 * plain flex sibling OUTSIDE that scroll region - structurally incapable of
 * overlapping content regardless of how little/much of it exists.
 * .template-drawer__header is a third sibling on the same principle, so the
 * eyebrow/title/close row stays put without position:sticky.
 * ========================================================================= */
.template-drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: 560px;
  max-width: 100vw;
  background: var(--bg-base);
  border-left: 1px solid var(--border-default);
  box-shadow: var(--shadow-xl);
  z-index: var(--z-drawer);
  display: flex;
  flex-direction: column;
}

/* Slide-in, reusing the @keyframes drawer-in/out defined for .candidate-drawer
   above - including the reason they are keyframes on the VISIBLE state rather
   than a transition out of an off-screen default (see that block's note: a
   stalled animation must never leave an open, focus-trapped drawer parked off
   screen). Entrance only: unlike candidate-drawer.js, this controller hides the
   element synchronously in actuallyClose(), and routing that through an exit
   animation would mean delaying the close behind the §32 discard confirmation.
   Reduced motion is handled by the shared @media block above. */
.template-drawer:not([hidden]) {
  animation: drawer-in var(--duration-slow) var(--ease-out);
}

.template-drawer[hidden] {
  display: none;
}

@media (max-width: 699px) {
  .template-drawer {
    width: 100%;
  }
}

.template-drawer__header {
  flex-shrink: 0;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-6) var(--space-5) var(--space-4);
  border-bottom: 1px solid var(--border-default);
}

.template-drawer__heading-group {
  min-width: 0; /* lets a long template name wrap instead of pushing the close button off-edge */
}

.template-drawer__eyebrow {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
  margin-bottom: var(--space-1);
}

/* No font-size here on purpose: .template-drawer is an in-app type-cap root
   (see the remap near the top of this file), so h2 already resolves to
   --text-h5 / 20px. Hardcoding a larger value would break design.md §2.2. */
.template-drawer__header h2 {
  margin-bottom: 0;
}

.template-drawer__close {
  flex-shrink: 0;
  margin-right: calc(var(--space-2) * -1); /* optically aligns the 44px hit box's glyph with the content edge */
}

.template-drawer__scroll {
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  padding: var(--space-5);
}

.template-drawer__section {
  margin-bottom: var(--space-6);
}

.template-drawer__section h3 {
  font-size: var(--text-base);
  margin-bottom: var(--space-3);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--border-default);
}

.template-drawer__add-question-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

/* Questions empty state - structural, rendered by renderQuestionsList() only
   while draft.questions is empty. */
.template-drawer__empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-6) var(--space-4);
  border: 1.5px dashed var(--border-default);
  border-radius: var(--radius-md);
  text-align: center;
}

.template-drawer__empty svg {
  width: 24px;
  height: 24px;
  color: var(--icon-muted);
  margin-bottom: var(--space-1);
}

.template-drawer__empty-title {
  font-weight: 600;
  color: var(--text-primary);
}

.template-drawer__question-block {
  padding: var(--space-4);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  background: var(--bg-surface);
  margin-bottom: var(--space-3);
}

.template-drawer__question-header {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

.template-drawer__question-type {
  min-width: 0;
  display: inline-flex;
  align-items: center;
  padding: 2px var(--space-2);
  border-radius: var(--radius-full);
  background: var(--bg-elevated);
  border: 1px solid var(--border-default);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-secondary);
}

.template-drawer__question-number {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: var(--radius-full);
  background: var(--accent);
  color: var(--accent-text-on);
  font-size: var(--text-xs);
  font-weight: 600;
}

/* margin-left:auto pushes the delete button to the far edge - the pill stays
   its natural width, which `flex:1` on the pill would have stretched. */
.template-drawer__question-header .btn-icon {
  margin-left: auto;
  flex-shrink: 0;
}

.template-drawer__options {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.template-drawer__option-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.template-drawer__option-row .field__control {
  flex: 1;
  min-width: 0;
  height: 40px;
}

/* Decorative type cue only (radio for single-choice, checkbox for multiple) -
   disabled and aria-hidden in the markup, so this is styling for something the
   author reads but never operates. Kept at full opacity despite :disabled, as
   it is a legend rather than an unavailable control. */
.template-drawer__option-affordance {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  accent-color: var(--accent);
  opacity: 1;
}

.template-drawer__add-option-btn {
  margin-top: var(--space-2);
}

.template-drawer__file-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-xs);
  margin-bottom: var(--space-2);
  font-size: var(--text-sm);
}

.template-drawer__file-row span {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.template-drawer__file-row svg {
  /* Size (20px) comes from the shared builder-icon rule below (design.md §3.7
     / components.md §41 - was 16px); this keeps only the muted color + no
     shrink so the file glyph and the remove-x row stay aligned. */
  color: var(--icon-muted);
  flex-shrink: 0;
}

/* Reference-files dropzone. Modelled on .resume-dropzone (further down this
   file) rather than sharing it: that one owns upload progress/success/error
   states this prototype has no use for, since reference files here are
   metadata-only (see template-drawer.js's backend note). */
.template-drawer__dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-6) var(--space-4);
  border: 1.5px dashed var(--border-default);
  border-radius: var(--radius-md);
  background: var(--bg-surface);
  text-align: center;
  transition: border-color var(--duration-fast) var(--ease-out),
    background-color var(--duration-fast) var(--ease-out);
}

.template-drawer__dropzone svg {
  /* Size (20px) comes from the shared builder-icon rule below (design.md §3.7
     / components.md §41 - was 24px). */
  color: var(--icon-muted);
}

/* ---- Builder icon glyphs -> 20px, single source (design.md §3.7,
   components.md §41) ----
   Every FUNCTIONAL icon in the scrolling body - the question-card trash, the
   option / file remove x, the three Add-question pluses, the inline Add Option
   plus, the dropzone upload glyph, and the file-row file glyph - resolves to
   the shared 20px icon size through this ONE rule, so no per-icon size can
   drift out of step again. --icon-size-close IS design.md §3.7's 20px icon
   default (the doc frames it as the one size that must stay consistent across
   overlays), reused here as that canonical 20px token; it is theme-independent,
   so light + dark stay in parity automatically. The old per-context sizes on
   .template-drawer__file-row svg (16px) and .template-drawer__dropzone svg
   (24px) were dropped above so this is the sole sizer.
   The :where() list scopes the rule to the button / dropzone / file-row glyphs
   and deliberately EXCLUDES the empty-state clipboard illustration (kept at
   24px as a decorative placeholder) and the option-row radio/checkbox cues and
   sharing switches (form controls, not svgs - untouched).
   GLYPH ONLY (design.md §4.4): the icon buttons keep their fixed 44x44
   .btn-icon hit box and the add-buttons keep their layout - only the visible
   svg grows, the clickable box is unchanged. The header close x is sized by the
   separate shared --icon-size-close rule (§31) and is already 20px. */
.template-drawer__scroll
  :where(.btn, .btn-icon, .template-drawer__dropzone, .template-drawer__file-row)
  svg {
  width: var(--icon-size-close);
  height: var(--icon-size-close);
}

.template-drawer__dropzone.is-dragover {
  border-color: var(--accent);
  background: var(--bg-elevated);
}

.template-drawer__dropzone-text {
  color: var(--text-secondary);
}

/* The two sharing flags now use the shared .switch component (design.md §8);
   only the row rhythm is drawer-local. */
.template-drawer__section .switch + .switch {
  margin-top: var(--space-2);
}

.template-drawer__actions {
  /* A plain flex sibling of .template-drawer__scroll (not inside it, not
     position:sticky) - see the .template-drawer layout note above for why
     this is more robust than the sticky-within-scroll-container technique. */
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  flex-wrap: wrap;
  padding: var(--space-4) var(--space-5) var(--space-6);
  background: var(--bg-base);
  border-top: 1px solid var(--border-default);
}

/* ---- Resume view (plain, ATS-friendly, single column) ---- */
.resume-view {
  /* Clearance for the CV view's OWN sticky footer (the "Download CV" bar,
     components.md §26). candidate-drawer.js renders that bar as a SIBLING of
     this element, not a child, precisely so this padding can push the last CV
     section clear of it - a footer nested inside its own spacer would just be
     pushed down by it. The value matches .candidate-drawer__actions' bottom
     padding.

     (It previously existed for the opposite reason: the resume view had no
     footer at all, and this stood in for the bottom padding .candidate-drawer
     deliberately does not carry.) */
  padding-bottom: var(--space-6);
}

.resume-view h2 {
  margin-bottom: 2px;
}

.resume-view__subtitle {
  color: var(--text-secondary);
  margin-bottom: var(--space-5);
}

.resume-view section {
  margin-bottom: var(--space-5);
}

.resume-view h3 {
  font-size: var(--text-sm);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
  margin-bottom: var(--space-2);
}

.resume-view ul {
  list-style: disc;
  padding-left: var(--space-5);
}

.resume-view li {
  margin-bottom: var(--space-1);
  font-size: var(--text-sm);
}

/* =========================================================================
 * Pagination (design.md §6, components.md §27)
 * -------------------------------------------------------------------------
 * A centred two-part block: a "Showing X-Y of Z" status line above a single
 * rounded --bg-surface pill holding prev arrow / page buttons / ellipses /
 * next arrow. Rendered by assets/js/lib/pagination.js.
 *
 * Sizing (design.md §7 / §4.4): the visible controls are a compact 38px - one
 * notch under the `sm` 32px..`md` 40px band - deliberately smaller than the
 * 44px touch-target floor so a 7-page run stays a single readable pill rather
 * than a 320px-wide slab. The §4.4 floor is met with the invisible-padding
 * pattern instead: each button paints at 38px but carries a transparent 44x44
 * ::after hit overlay. The 6px gap between controls is not decorative - it is
 * exactly what keeps two adjacent 44px overlays from colliding (38 + 6 = 44).
 * ========================================================================= */
.pagination {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-6);
}

.pagination[hidden] {
  display: none;
}

/* Status line. Numbers are emphasised (--text-primary / 600) against quiet
   surrounding text, and tabular figures stop the digits jittering as the page
   changes (design.md §2.1). */
.pagination__summary {
  font-size: var(--text-xs);
  color: var(--text-secondary);
  margin: 0;
  font-variant-numeric: tabular-nums;
}

.pagination__summary strong {
  color: var(--text-primary);
  font-weight: 600;
}

/* The controls pill. */
.pagination__controls {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  justify-content: center;
  padding: var(--space-1);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-full);
  background: var(--bg-surface);
  box-shadow: var(--shadow-sm);
}

.pagination__page,
.pagination__arrow {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 38px;
  height: 38px;
  padding: 0 var(--space-2);
  border-radius: var(--radius-full);
  border: 1px solid transparent;
  background: transparent;
  color: var(--text-secondary);
  font-size: var(--text-sm);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}

/* §4.4 invisible hit area - centred on the visible 38px box, extends 3px past
   each edge to 44x44. Purely a hit target: no paint, no layout effect. */
.pagination__page::after,
.pagination__arrow::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 100%;
  height: 44px;
  min-width: 44px;
  transform: translate(-50%, -50%);
}

.pagination__page:hover,
.pagination__arrow:hover:not([aria-disabled="true"]) {
  background: var(--accent-tint);
  color: var(--accent-ink);
}

/* Current page - a solid filled button. --accent / --accent-text-on is the
   §7 Primary pairing and clears 4.5:1 in both modes (13.75:1 in dark). */
.pagination__page.is-current,
.pagination__page.is-current:hover {
  background: var(--accent);
  color: var(--accent-text-on);
  border-color: var(--accent);
}

/* Arrows - bordered icon buttons, so they read as a distinct control class
   from the quiet page numbers. */
.pagination__arrow {
  min-width: 38px;
  padding: 0;
  border-color: var(--border-default);
  color: var(--text-primary);
}

.pagination__arrow svg {
  width: 18px;
  height: 18px;
}

/* Bounds state. aria-disabled, not `disabled`, per design.md §7 - the control
   stays in the tab order; pagination.js guards the click. */
.pagination__arrow[aria-disabled="true"] {
  opacity: 0.4;
  cursor: not-allowed;
}

.pagination__ellipsis {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  height: 38px;
  color: var(--text-secondary);
  font-size: var(--text-sm);
  user-select: none;
}

/* §4.2 - "radius matched to the element's own radius". base.css's global focus
   rule sets radius-xs, which would square off these round controls mid-focus;
   restore radius-full. z-index keeps the ring above the neighbouring hit
   overlays, which paint later and would otherwise clip it. */
.pagination__page:focus-visible,
.pagination__arrow:focus-visible {
  border-radius: var(--radius-full);
  z-index: 1;
}

/* =========================================================================
 * Empty State (design.md §9)
 * ========================================================================= */
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: var(--space-10) var(--space-4);
  color: var(--text-secondary);
}

.empty-state[hidden] {
  display: none;
}

.empty-state svg {
  width: 40px;
  height: 40px;
  color: var(--icon-muted);
  margin-bottom: var(--space-4);
}

.empty-state h2 {
  color: var(--text-primary);
  margin-bottom: var(--space-2);
}

.empty-state p {
  max-width: 48ch;
  margin: 0;
}

/* ---- Optional icon TILE (components.md §28, §44) --------------------------
   The bare-`<svg>` form above (a 40px muted glyph) stays the default and is
   what the employer dashboard and Shortlists still use. This variant puts the
   glyph in a tinted rounded tile instead, giving the zero-state a focal point
   when it sits inside a framed panel (§44's `.assessments-panel`), where a
   loose muted glyph reads as an accident rather than a deliberate mark.
   ⚠️ The `.empty-state svg` rule above is a DESCENDANT selector, so it also
   matches the glyph nested in here. `.empty-state__icon svg` at (0,2,1)
   out-specifies it at (0,1,1) - both forms therefore keep working, and the
   two existing consumers are untouched. */
.empty-state__icon {
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  border-radius: var(--radius-md);
  background: var(--accent-tint);
  color: var(--accent-ink);
  margin-bottom: var(--space-4);
}

.empty-state__icon svg {
  width: 28px;
  height: 28px;
  /* currentColor, not --icon-muted: the glyph must read against the tinted
     tile, and --accent-ink is the token contrast-paired with --accent-tint in
     both themes (design.md §1.4). */
  color: currentColor;
  margin-bottom: 0;
}

/* ---- CTA row (components.md §28) -----------------------------------------
   Promoted here from `assets/css/pages/shortlists.css`, where it was first
   written, now that a second and third screen need it. Wraps 1-2 buttons so a
   zero state can offer a primary action (and optionally a secondary) without
   each page re-inventing a one-off `style="margin-top: ..."`. */
.empty-state__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-3);
  margin-top: var(--space-4);
}

/* =========================================================================
 * Searchable Select Combobox (design.md §8, components.md §51) - a plain
 * visible-label single-select combobox, distinct from .country-select (flag+
 * dial-code trigger chip) and .multi-select (chips + persistent panel).
 * Structurally close to .country-select's own classes/panel/list/option
 * pattern, minus anything dial-code-specific, plus an optional "create" row.
 * ========================================================================= */
.searchable-select {
  position: relative;
  width: 100%;
}

.searchable-select__trigger {
  width: 100%;
  height: 44px;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0 var(--space-3);
  background: var(--bg-base);
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-xs);
  cursor: pointer;
  color: var(--text-primary);
  font-size: var(--text-base);
  text-align: left;
}

.searchable-select__trigger:focus-visible {
  border-color: var(--focus-ring);
}

.searchable-select__trigger:disabled {
  background: var(--bg-surface);
  color: var(--text-disabled);
  cursor: not-allowed;
}

.searchable-select__trigger-label {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.searchable-select__trigger svg {
  width: 16px;
  height: 16px;
  margin-left: auto;
  color: var(--icon-muted);
  flex-shrink: 0;
}

.searchable-select__panel {
  position: absolute;
  top: calc(100% + var(--space-2));
  left: 0;
  right: 0;
  max-width: 100%;
  background: var(--bg-elevated);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  z-index: var(--z-drawer);
  padding: var(--space-2);
}

.searchable-select__panel[hidden] {
  display: none;
}

.searchable-select__search-wrap {
  position: relative;
  margin-bottom: var(--space-2);
}

.searchable-select__search-wrap svg {
  position: absolute;
  left: var(--space-3);
  top: 50%;
  transform: translateY(-50%);
  width: 16px;
  height: 16px;
  color: var(--icon-muted);
}

.searchable-select__search {
  width: 100%;
  height: 40px;
  padding: 0 var(--space-3) 0 var(--space-8);
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-xs);
  background: var(--bg-base);
  color: var(--text-primary);
}

.searchable-select__options {
  max-height: 240px;
  overflow-y: auto;
}

.searchable-select__option {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2);
  border-radius: var(--radius-xs);
  cursor: pointer;
  font-size: var(--text-sm);
}

.searchable-select__option:hover,
.searchable-select__option.is-selected {
  background: var(--bg-surface);
}

.searchable-select__option--create {
  color: var(--accent);
  font-weight: 600;
}

.searchable-select__option--create svg {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}

.searchable-select__empty {
  padding: var(--space-4);
  text-align: center;
  color: var(--text-secondary);
  font-size: var(--text-sm);
}

/* Inline typeahead variant (Country/City, components.md §57 & §51): the field
   itself is the text input, so the in-panel search box is hidden and the
   trigger button is swapped for a plain bordered `.field__control`-style
   input. Suggestions still render in the same `.searchable-select__panel`. */
.searchable-select--inline .searchable-select__input {
  width: 100%;
  height: 44px;
  padding: 0 var(--space-3);
  background: var(--bg-base);
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-xs);
  color: var(--text-primary);
  font-size: var(--text-base);
}

.searchable-select--inline .searchable-select__input:focus-visible {
  border-color: var(--focus-ring);
}

.searchable-select--inline .searchable-select__input:disabled {
  background: var(--bg-surface);
  color: var(--text-disabled);
  cursor: not-allowed;
}

.searchable-select--inline .searchable-select__input:disabled::placeholder {
  color: var(--text-disabled);
}

.searchable-select--inline .searchable-select__search-wrap {
  display: none;
}

.searchable-select__option.is-active {
  background: var(--bg-surface);
}

/* =========================================================================
 * Single-Value Year Slider (design.md §8, components.md §52) - a single
 * native range input variant of .range-slider (dual-handle). Same track/
 * thumb visual language, one handle, one fill anchored at 0.
 * ========================================================================= */
.year-slider {
  position: relative;
  height: 24px;
  margin-bottom: var(--space-2);
}

.year-slider__track {
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  height: 4px;
  transform: translateY(-50%);
  background: var(--border-default);
  border-radius: var(--radius-full);
}

.year-slider__fill {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  background: var(--accent);
  border-radius: var(--radius-full);
}

.year-slider__input {
  position: absolute;
  inset: 0;
  width: 100%;
  margin: 0;
  background: transparent;
  -webkit-appearance: none;
  appearance: none;
}

.year-slider__input::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 18px;
  height: 18px;
  border-radius: var(--radius-full);
  background: var(--accent);
  border: 2px solid var(--bg-base);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  margin-top: 3px;
}

.year-slider__input::-moz-range-thumb {
  width: 18px;
  height: 18px;
  border-radius: var(--radius-full);
  background: var(--accent);
  border: 2px solid var(--bg-base);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
}

.year-slider__input::-webkit-slider-runnable-track,
.year-slider__input::-moz-range-track {
  -webkit-appearance: none;
  background: transparent;
  height: 4px;
}

.year-slider__input:focus-visible::-webkit-slider-thumb {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

.year-slider__row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-1);
}

.year-slider__number {
  width: 88px;
  flex-shrink: 0;
  height: 36px;
  padding: 0 var(--space-2);
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-xs);
  background: var(--bg-base);
  color: var(--text-primary);
  font-size: var(--text-sm);
}

.year-slider__display {
  margin: 0;
  color: var(--text-secondary);
  font-size: var(--text-xs);
}

.year-slider__input:disabled,
.year-slider__number:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* =========================================================================
 * View CV overlay + Upload CV modal (design.md §13, components.md §53)
 * -------------------------------------------------------------------------
 * ⚠️ CURRENTLY DORMANT - NOT DEAD. These rules style assets/js/lib/
 * resume-upload.js, which no longer has an importer: the Candidate Profile's
 * CV tab that mounted it was removed, and the module was deliberately KEPT
 * because it is the flow the inert "Upload Your CV" button
 * (#pf-upload-cv-btn, components.md §85) is intended to open. Do not delete
 * either the module or these rules as unused.
 *
 * Scope note for anyone tidying below: `.resume-tab__actions` and
 * `.resume-viewer-overlay*` are exclusive to that dormant module, but
 * `.resume-dropzone*` further down is SHARED and LIVE - the candidate
 * Assessments screen renders it for attachment uploads
 * (candidate-assessment-actions.js). The `.resume-view*` family above is
 * likewise live, via the employer's View CV drawer (§26).
 * ========================================================================= */
.resume-tab__actions {
  display: flex;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-top: var(--space-4);
}

/* ---- View Resume: right-side overlay (same construction pattern as the
   Candidate Detail Drawer - a fresh DOM node per open/close, since this is
   unrelated chrome, not a persistent candidate-data drawer). ---- */
.resume-viewer-overlay {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: 480px;
  max-width: 100vw;
  background: var(--bg-base);
  border-left: 1px solid var(--border-default);
  box-shadow: var(--shadow-xl);
  z-index: var(--z-drawer);
  display: flex;
  flex-direction: column;
}

.resume-viewer-overlay__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4);
  border-bottom: 1px solid var(--border-default);
  flex-shrink: 0;
}

.resume-viewer-overlay__title {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.resume-viewer-overlay__body {
  flex: 1;
  overflow-y: auto;
  padding: var(--space-5);
}

@media (max-width: 599px) {
  .resume-viewer-overlay {
    width: 100%;
  }
}

/* ---- Upload Resume modal: drag-and-drop zone (design.md §8's documented
   idle/drag-over/uploading/success/error states - first concrete
   drag-and-drop implementation in this codebase; the Assessment Template
   Builder's "Add File", components.md §41, is button-only). ---- */
.resume-dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-8) var(--space-4);
  border: 1.5px dashed var(--border-default);
  border-radius: var(--radius-md);
  background: var(--bg-surface);
  text-align: center;
  transition: border-color var(--duration-fast) var(--ease-out), background-color var(--duration-fast) var(--ease-out);
}

.resume-dropzone svg {
  width: 32px;
  height: 32px;
  color: var(--icon-muted);
}

.resume-dropzone.is-dragover {
  border-color: var(--accent);
  background: var(--bg-elevated);
}

.resume-dropzone--success {
  border-style: solid;
  border-color: var(--color-success);
}

.resume-dropzone--success svg {
  color: var(--color-success);
}

/* ---- Notification Feed (Updates screen) ----------------------------------
   design.md §10 "Notification center": grouped by day, unread indicator
   dot, "Mark all as read" action. Originally page-local to the employer
   Updates screen (assets/css/pages/updates.css); promoted here once the
   candidate dashboard's Updates screen needed the identical pattern - see
   components.md "Notification Row / Date-Group Header" entry. Used on both
   dashboard/employer/updates/ and dashboard/candidate/updates/. All values
   are design tokens, so light/dark mode and the row's compact-width layout
   below are automatic. */
/* NOTE: there was a `.dashboard-main--reading > * { max-width: 720px }` rule
   here that capped the whole Updates content column. REMOVED - it was wrong at
   the container level. design.md §3.2 specifies dashboards as "fixed sidebar +
   FLUID content", and `.dashboard-main` (dashboard-shell.css) is `flex: 1` with
   no max-width on every other employer screen; capping only this one left a
   wide dead band on the right with the rows hugging the left and the timestamps
   stranded mid-row. The feed now fills the spine like every other screen, and
   the measure is constrained where it actually belongs - on the message text
   itself (see .update-row__message below). components.md §46/§47. */

.updates-page__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-5);
}

.updates-page__header .dashboard-main__title {
  margin-bottom: 0;
}

/* Title + unread tally travel together on the left, so they stay paired when
   the header wraps and the Mark-all button drops to its own line. */
.updates-page__title-group {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* Live unread count (components.md §47). Accent pill with tabular figures so
   it doesn't reflow the title as the count crosses 9 -> 10. Hidden at zero by
   the `hidden` attribute, which the page script drives off getUnreadCount(). */
.updates-page__count {
  display: inline-grid;
  place-items: center;
  min-width: 22px;
  height: 22px;
  padding: 0 var(--space-2);
  border-radius: var(--radius-full);
  background: var(--accent);
  color: var(--accent-text-on);
  font-size: var(--text-xs);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.updates-page__count[hidden] {
  display: none;
}

/* Shared "this is placeholder data" disclosure (components.md §47, §94).
   ---------------------------------------------------------------------------
   PROMOTED from .updates-page__sample-notice when the Sent Emails screen became
   its second consumer - the house rule (components.md §1) is that a block moves
   up here the moment more than one screen needs it, and two near-identical
   muted-caption rules were exactly the drift that rule exists to prevent.

   Deliberately quiet: it is a standing caveat, not an alert, so it takes muted
   body text rather than a warning callout. Carries NO margin of its own -
   placement differs per screen, so each supplies it via its own modifier
   below. */
.sample-notice {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

/* §47 placement: pulled up under the page title, cancelling
   .dashboard-main__title's own bottom margin so the caveat reads as part of the
   heading block rather than as the first row of the feed. */
.updates-page__sample-notice {
  margin: calc(var(--space-5) * -1) 0 var(--space-5);
}

/* ---- Date-group sections ---- */
.update-group {
  margin-bottom: var(--space-6);
}

/* design.md §2.2's overline style: text-xs + 0.04em + uppercase via CSS, not
   hardcoded capitals, so the accessible name keeps its natural casing. */
.update-group__heading {
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-secondary);
  margin: 0 0 var(--space-2);
}

/* No inter-row gap - rows are a continuous scannable run, separated by their
   own hover fill rather than by whitespace or borders. */
.update-group__list {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* ---- Notification row (components.md §46) ----
 * FLAT, not carded. This replaced a bordered `--bg-surface` card per row: at
 * feed density the borders read as a stack of separate objects competing for
 * attention, where the point of the screen is one continuous list. The row
 * now has no chrome at rest and materialises a `--bg-elevated` fill on hover.
 *
 * The row is a plain <li> holding TWO SIBLING BUTTONS - the mark dot and the
 * row body. It is deliberately NOT `role="button"` with a nested control,
 * which would be an invalid a11y tree. */
/* Horizontal inset lives HERE, on the row, so the hover fill and the row's
   content share the same left/right insets (--space-4 both sides). It used to
   live on .update-row__main's padding, which only inset the right side - the
   left was set by the mark button's 44px width, leaving the avatar ~52px in
   against a 12px right inset. The gap is --space-4 because the mark button's
   hit area overhangs into it by exactly that much; see below. */
.update-row {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: 0 var(--space-4);
  border-radius: var(--radius-lg);
}

.update-row:hover {
  background: var(--bg-elevated);
}

/* ---- Static (non-interactive) row - components.md §46/§59 ----
   The candidate Notifications feed renders informational rows: no read state
   to toggle, nowhere to click through to. The renderer emits them with no
   buttons at all (notification-feed.js, `rowConfig.interactive: false`), so
   this modifier only has to withdraw the two affordances the base row
   advertises - the hover fill and the pointer cursor. A row that lights up
   under the cursor is promising a click it cannot honour; design.md §7's rule
   that an unavailable control must still "read as not-yet, not broken" is
   about controls that EXIST, and there is no control here at all.

   Nothing else needs overriding: .update-row__main's own declarations (flex,
   min-width, gap, padding, border:0, background:transparent) all apply
   unchanged to the <div> that replaces the <button>. */
.update-row--static,
.update-row--static .update-row__main {
  cursor: default;
}

.update-row--static:hover {
  background: transparent;
}

/* ---- Mark-as-read button (the unread dot) ----
   44x44 target around an 8px dot - the extra area is invisible padding,
   exactly as design.md §4.4 prescribes for icon-only controls. Stays MOUNTED
   once read (disabled, not removed) so the dot's fade can play and the row's
   left edge never jumps; `disabled` also drops it from the tab order once
   there is nothing left to mark. */
/* CAUSE OF THE OLD DEEP INDENT: this button was a literal 44x44 box around an
   8px dot, so the avatar could never start closer than 44px from the row's
   left edge while the right inset was only 12px - badly lopsided, and it read
   as the hover bar having a huge left padding (components.md §46).
   The VISIBLE box is now --space-3 wide and the 44px target is restored by the
   ::before overlay below, the same "visible pill smaller than its hit box"
   technique §20's filter triggers use. */
.update-row__mark {
  position: relative;
  flex-shrink: 0;
  display: grid;
  place-items: center;
  width: var(--space-3);
  height: 44px;
  padding: 0;
  border: 0;
  background: transparent;
  border-radius: var(--radius-full);
  cursor: pointer;
}

/* Restores the full 44x44 target (design.md §4.4) without widening the visible
   box: 12px + 16px on each side = 44px. The overhang is sized to land exactly
   in dead space - the left 16px falls in the row's own padding, the right 16px
   in the row's gap - so it never overlaps .update-row__main. That matters:
   .update-row__main is a later sibling, so any overlapping region would be
   captured by it and the extra hit area would be dead. */
.update-row__mark::before {
  content: "";
  position: absolute;
  inset: 0 calc(var(--space-4) * -1);
}

.update-row__mark:disabled {
  cursor: default;
}

.update-row__dot {
  width: 8px;
  height: 8px;
  border-radius: var(--radius-full);
  background: var(--accent);
  transition:
    opacity var(--duration-base) var(--ease-out),
    transform var(--duration-base) var(--ease-out);
}

/* Read rows retract the dot rather than removing it - see the mount note
   above. This is the one place the design leans on opacity, and it is safe
   because the dot carries no text (design.md §4.1 governs text contrast). */
.update-row:not(.is-unread) .update-row__dot {
  opacity: 0;
  transform: scale(0.4);
}

@media (prefers-reduced-motion: reduce) {
  .update-row__dot {
    transition: opacity var(--duration-base) var(--ease-out);
    transform: none;
  }
  .update-row:not(.is-unread) .update-row__dot {
    transform: none;
  }
}

/* ---- Row body: the click-through target ---- */
.update-row__main {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  /* Vertical only - the horizontal inset moved up to .update-row so the hover
     fill and the content share it symmetrically (components.md §46). */
  padding: var(--space-2) 0;
  border: 0;
  background: transparent;
  border-radius: var(--radius-lg);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}

/* ---- Avatar tile + corner event badge ----
   The tile answers "what is this about", the badge "what happened". Employer
   rows show the candidate's silhouette; candidate rows, which name nobody,
   show the assessment. Never initials - the subject's name, where there is
   one, is already in the message beside it. */
.update-row__tile {
  position: relative;
  flex-shrink: 0;
  display: block;
}

.update-row__avatar {
  width: 40px;
  height: 40px;
}

.update-row__avatar svg {
  width: 20px;
  height: 20px;
}

/* Solid semantic fill + an on-colour glyph, ringed in the page background so
   it reads as a separate object sitting on the avatar rather than a hole in
   it. The ring follows the row's hover fill so the seam doesn't reappear.

   These use the FOREGROUND semantic tokens only. The matching `--color-*-bg`
   tokens are deliberately avoided: they are not overridden for dark mode, so
   e.g. `.badge-success` there is light mint on pale mint - survivable at card
   size, illegible at 18px. Colour is always paired with a glyph, never the
   sole signal (design.md §1.3). */
.update-row__badge {
  position: absolute;
  right: -3px;
  bottom: -3px;
  display: grid;
  place-items: center;
  width: 18px;
  height: 18px;
  border-radius: var(--radius-full);
  box-shadow: 0 0 0 2px var(--bg-base);
}

.update-row:hover .update-row__badge {
  box-shadow: 0 0 0 2px var(--bg-elevated);
}

.update-row__badge svg {
  width: 11px;
  height: 11px;
}

/* Employer events */
.update-row__badge--completed {
  background: var(--color-success);
  color: var(--bg-base);
}

.update-row__badge--accepted {
  background: var(--color-info);
  color: var(--bg-base);
}

/* Candidate events */
.update-row__badge--available {
  background: var(--color-info);
  color: var(--bg-base);
}

.update-row__badge--deadline {
  background: var(--color-warning);
  color: var(--bg-base);
}

/* ---- Message ----
   Running prose = body copy, so text-sm per design.md §2.2's in-app paragraph
   rule. Only the subject's name is bolded, via <b> from the renderer. */
/* The measure cap lives here, not on the content column. Now that the feed
   fills the fluid spine (design.md §3.2), an uncapped message would run
   edge-to-edge on a wide monitor; 68ch keeps it readable while the row itself
   still spans the full width so the timestamp can pin to the true right edge.
   components.md §46/§47. */
.update-row__message {
  flex: 1;
  min-width: 0;
  max-width: 68ch;
  font-size: var(--text-sm);
  line-height: 1.4;
  font-weight: 400;
  color: var(--text-primary);
}

.update-row__message b {
  font-weight: 600;
}

/* ---- Read state ----
   Dimming is done with TOKENS, not a blanket row opacity. The reference mock
   used `opacity: .62` on the whole row, which composites the message text to
   roughly 3.1:1 in light and 3.4:1 in dark - under design.md §4.1's 4.5:1
   floor for body text. Shifting the message to `--text-secondary` at full
   opacity reads as the same recede while staying measurably legible; only
   the dot and the avatar, which carry no text, actually fade.

   ⚠️ THE `:not(.update-row--static)` GUARDS ARE LOAD-BEARING. These selectors
   key off the ABSENCE of `.is-unread`, which was safe while every feed had a
   read state - a row was either unread or read. A static row is neither: it
   carries no read state at all and so never gets `.is-unread`, and without
   these guards all ten rows of the candidate feed would render permanently in
   the "already read" treatment - greyed message, 65%-opacity tile - as though
   the whole screen were stale. Guarded, static rows sit at --text-primary and
   full opacity, and the employer feed's behaviour is untouched. */
.update-row:not(.is-unread):not(.update-row--static) .update-row__message {
  color: var(--text-secondary);
}

.update-row.is-unread .update-row__message {
  font-weight: 500;
}

.update-row:not(.is-unread):not(.update-row--static) .update-row__avatar,
.update-row:not(.is-unread):not(.update-row--static) .update-row__badge {
  opacity: 0.65;
}

/* ---- Accent-tinted tile ----
   design.md §9's framed-icon treatment (--accent-tint fill + --accent-ink
   glyph), the same pairing the empty state's 56px tile uses. Opt-in via
   `rowConfig.avatarClass`, currently only from the candidate Notifications
   feed: with the unread dot gone, this tile is the sole colour left in the
   row, and it is what makes the feed scannable by event class (viewed vs.
   shortlisted) without reading every sentence.

   Both tokens flip for dark mode on their own (--accent-tint -> primary-800,
   --accent-ink -> accent-300), so no dark-mode rule is needed here. Colour is
   never the only signal - the glyph inside carries the distinction, and the
   message states it in words (design.md §4.6). */
.update-row__avatar--accent {
  background: var(--accent-tint);
  /* .avatar's base rule paints a --border-default hairline, which reads as a
     grey seam around a tinted disc. --accent-tint-border is the token pairing
     designated for exactly this (accent-200 light / primary-700 dark), so the
     ring recedes into the fill instead of outlining it. */
  border-color: var(--accent-tint-border);
  color: var(--accent-ink);
}

/* ---- Timestamp ----
   `margin-left: auto` is load-bearing: the message is capped at 68ch, so once
   the row is wider than that the message stops growing and the time would
   otherwise sit right beside it, stranded mid-row. This pins it to the row's
   right inset instead (components.md §46). */
.update-row__time {
  flex-shrink: 0;
  margin-left: auto;
  padding-left: var(--space-4);
  font-size: var(--text-xs);
  color: var(--text-secondary);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* ---- Focus (design.md §4.2) ---- */
.update-row__mark:focus-visible,
.update-row__main:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

@media (max-width: 599px) {
  /* Only the INTERNAL spacing tightens. The row's own padding and gap stay at
     --space-4 deliberately: .update-row__mark::before's overhang is sized to
     exactly fill them, so shrinking either would push its hit area over
     .update-row__main and silently kill part of the 44px target (§4.4). */
  .update-row__main {
    gap: var(--space-2);
  }

  .update-row__time {
    padding-left: var(--space-2);
  }
}

/* ==========================================================================
   §80 ADVANCED SEARCH OVERLAY (employer Candidate Search)
   design.md §14. Markup: dashboard/employer/index.html.
   Controller: assets/js/lib/advanced-search-drawer.js.
   ==========================================================================
   Holds the Gender / Availability / Type / Age filters that are too many to sit
   inline on the filter row. Opened by .search-filter-bar__filters-btn.

   Shares .candidate-drawer's fixed right-hand geometry, --z-drawer, .drawer-backdrop
   and @keyframes drawer-in/out on the VISIBLE state (see that block's note on why
   the animation is not a transition out of an off-screen default). Two right-hand
   overlays on the same screen that behaved differently would read as a bug.

   ⚠️ LAYOUT IS A THREE-SIBLING FLEX COLUMN, NOT STICKY-IN-A-SCROLL-CONTAINER.
   This is the .template-drawer pattern (see its own layout note), adopted here
   after the sticky version shipped with a real defect:

     `position: sticky; bottom: 0` only pins while the CONTAINER OVERFLOWS. The
     group list is dynamic - narrow the search and two of the four groups hide
     themselves - so on a short filter set the drawer stopped overflowing and the
     footer dropped to its natural flow position with dead space beneath it. It
     LOOKED pinned only because the default result set happened to be long.

   The flex column makes it structural instead: __body takes `flex: 1` (= 1 1 0%)
   and absorbs every leftover pixel, so __actions is at the true bottom edge with
   one group or ten, and __header at the true top.

   It also DELETES the whole no-show-through apparatus the sticky version needed -
   the opaque backgrounds, the negative full-bleed margins cancelling the drawer's
   side padding, and the `top: calc(-1 * var(--space-6))` offset that existed
   because a sticky offset resolves against the scroll container's padding edge.
   Header and footer are now SIBLINGS of the scroll region, so content physically
   cannot pass under them. Do not reintroduce any of it. */
.advanced-search-drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: 420px;
  max-width: 100vw;
  background: var(--bg-base);
  border-left: 1px solid var(--border-default);
  box-shadow: var(--shadow-xl);
  z-index: var(--z-drawer);
  display: flex;
  flex-direction: column;
  /* No padding here on purpose: the drawer is now a layout box, not a scroll
     box. Each of the three regions owns its own padding, which is what lets the
     header and footer draw their full-bleed borders edge to edge. */
}

.advanced-search-drawer[hidden] {
  display: none;
}

.advanced-search-drawer:not([hidden]):not(.is-closing) {
  animation: drawer-in var(--duration-slow) var(--ease-out);
}
.advanced-search-drawer.is-closing {
  animation: drawer-out var(--duration-slow) var(--ease-in) forwards;
}

@media (max-width: 599px) {
  .advanced-search-drawer {
    width: 100%;
  }
}

/* ---- Header: title left, close right - a fixed-height flex sibling ------ */
.advanced-search-drawer__header {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-5) var(--space-5) var(--space-4);
  border-bottom: 1px solid var(--border-default);
}

.advanced-search-drawer__title {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: 700;
  color: var(--text-primary);
}

/* In-header, NOT position:fixed. The Candidate Drawer's equivalent cluster
   (.candidate-drawer__top-controls) has to be fixed because it sits outside two
   swappable view panels and serves both; this overlay has a single body, so the
   close button can live in the flex header beside the title and can never drift
   out of alignment with it.
   Its 20px glyph comes from the shared overlay-close rule near the top of this
   file - see the warning there about what happens when a close is left out. */
.advanced-search-drawer__close {
  flex-shrink: 0;
  /* Optically aligns the 44px hit box's glyph with the content edge, matching
     .template-drawer__close. */
  margin-right: calc(var(--space-2) * -1);
}

/* ---- Scrolling body - THE ONLY scroll container in this overlay --------- */
.advanced-search-drawer__body {
  /* `flex: 1` is 1 1 0%: the body absorbs all leftover height, which is what
     holds __actions against the true bottom edge no matter how few groups are
     rendered. Needs no `min-height: 0` - a flex-basis of 0 plus overflow-y
     already establishes the scroll context (auto min-size only bites when the
     basis is `auto`/content). */
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

/* A `<div role="group" aria-labelledby>`, NOT a <fieldset>/<legend>.
   ⚠️ The fieldset was correct until each group gained a `Clear` beside its
   title: a <legend>'s content model does not admit a flow-content wrapper, and
   legend layout is too quirky to reliably flex a heading and a button onto one
   row. `role="group"` + `aria-labelledby` maps to the identical accessibility
   semantics the fieldset provided, so nothing was lost by the swap. */
.advanced-search__group {
  min-width: 0;
}

.advanced-search__group[hidden] {
  display: none;
}

/* The title row: heading left, per-group Clear right. Owns the gap below itself
   so .advanced-search__group-title does not need a margin of its own. */
.advanced-search__group-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}

.advanced-search__group-title {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--text-primary);
}

/* Per-group Clear (design.md §14.4). QUIET on purpose: up to four of these are
   on screen at once, so the destructive --color-error treatment is reserved for
   the bar-level "Clear all Filters" that wipes every filter. This one clears a
   single group's DRAFT and is undoable by dismissing the overlay, which is a far
   smaller act than the red control implies.
   Deliberately excluded from the shared overlay-close svg rule near the top of
   this file - it is a labelled action, not a close, and its 12px glyph is sized
   to sit beside a --text-sm heading rather than to be a 20px hit target. */
.advanced-search__group-clear {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-2);
  border: none;
  border-radius: var(--radius-xs);
  background: transparent;
  color: var(--text-secondary);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 600;
  cursor: pointer;
  transition: color var(--duration-fast) var(--ease-out),
    background-color var(--duration-fast) var(--ease-out);
}

.advanced-search__group-clear:hover {
  color: var(--text-primary);
  background: var(--bg-elevated);
}

.advanced-search__group-clear svg {
  width: 12px;
  height: 12px;
  flex-shrink: 0;
}

/* ⚠️ Required: `display: inline-flex` above beats the UA's `[hidden]{display:none}`,
   so without this the button would stay visible when the controller hides it.
   Same trap `.search-filter-bar__clear[hidden]` already has to work around. */
.advanced-search__group-clear[hidden] {
  display: none;
}

/* The rows themselves reuse .multi-select__option verbatim (see the controller)
   so every checkbox row in the filter system looks identical. This wrapper only
   stacks them - deliberately WITHOUT .multi-select__options' max-height/scroll,
   since these lists are short and the drawer body is already the scroll region.
   Nesting a second scroller here would create the classic scroll-trap. */
.advanced-search__options {
  display: flex;
  flex-direction: column;
}

/* ---- Footer: Clear All / Apply - a flex sibling, NOT position:sticky -----
   ⚠️ This used to be `position: sticky; bottom: 0` inside the scrolling drawer,
   which pinned only while the drawer OVERFLOWED. The group list is dynamic, so a
   narrow search that rendered two of the four groups left the content short, the
   drawer no longer overflowed, and the footer dropped to its natural flow
   position with dead space under it. As a plain flex sibling of the scroll
   region it is at the true bottom edge unconditionally.

   Padding is symmetric (space-4) top and bottom. The old rule ended in space-6
   purely to supply the bottom padding the scroll container deliberately lacked;
   with the flex column that extra depth is just a gap under the buttons. */
.advanced-search-drawer__actions {
  flex-shrink: 0;
  display: flex;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-top: 1px solid var(--border-default);
}

/* Equal-width buttons: neither action is secondary enough to shrink, and a
   full-width pair reads as the drawer's committed decision point. */
.advanced-search-drawer__actions .btn {
  flex: 1;
  justify-content: center;
}

/* A disabled Age slider (every matched candidate is the same age - see the
   controller's setDisabled call) still shows its value, just inert. */
.range-slider-field--disabled {
  opacity: 0.6;
}
.range-slider-field--disabled .range-slider__input,
.range-slider-field--disabled .range-slider__number {
  cursor: not-allowed;
}

/* =========================================================================
 * Rich Text Editor (design.md §8 "Rich text editor")
 * -------------------------------------------------------------------------
 * The only implementation of that §8 row. Controller:
 * assets/js/lib/rich-text-editor.js; catalog entry components.md §89.
 *
 * Composed from documented primitives rather than inventing a look: the shell
 * borrows the `.field__control` edge (1.5px --border-default, --radius-xs) so a
 * rich text field sits in a form beside plain inputs without reading as a
 * foreign widget, and the toolbar buttons reuse the --accent-tint/--accent-ink
 * active pattern that tags and selected states use elsewhere (§9).
 * ========================================================================= */
.rte {
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-xs);
  background: var(--bg-base);
}

/* Focus is shown on the SHELL, not on the contenteditable, because the toolbar
   and the text area are one control to the reader even though only one of them
   can hold the caret. :focus-within is what makes that true visually, and it
   matches `.field__control:focus-visible`'s own --focus-ring border. */
.rte:focus-within {
  border-color: var(--focus-ring);
}

/*
 * STICKY, and the shell above deliberately has NO `overflow: hidden` so it can
 * be: the editor is the tallest thing in the composer, and the modal body is
 * the single scroll container (see .modal--wide). Capping the editor's own
 * height instead would give the composer two nested scrollbars, which is worse
 * than either problem it solves. Sticky keeps Bold within reach at paragraph
 * twelve. Corner radii are declared explicitly here because dropping
 * `overflow: hidden` means the shell no longer clips its children.
 */
.rte__toolbar {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-1);
  padding: var(--space-2);
  background: var(--bg-surface);
  border-bottom: 1px solid var(--border-default);
  border-top-left-radius: var(--radius-xs);
  border-top-right-radius: var(--radius-xs);
}

.rte__divider {
  width: 1px;
  align-self: stretch;
  margin: 0 var(--space-1);
  background: var(--border-default);
}

/* 32px square, matching .btn-sm's box so the toolbar lines up with any small
   button placed near it. Deliberately NOT .btn-icon (a 44px circle) - a dense
   formatting toolbar at 44px would be wider than the field it edits. */
.rte__tool {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 1.5px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-secondary);
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}

.rte__tool svg {
  width: 18px;
  height: 18px;
}

.rte__tool:hover {
  background: var(--hover-surface);
  color: var(--text-primary);
}

/*
 * Active carries TWO meanings now that no selection is required (§89): "the
 * selected text has this mark" and "what you type next will have this mark".
 * That second reading is why the state has to be this legible - it is the only
 * signal that Bold is armed before a single character exists to show it.
 * aria-pressed is set in step, so the state is never colour-only (§4.6).
 *
 * ⚠️ NO `:disabled` RULE, deliberately. Toolbar buttons are never disabled -
 * the earlier "select some text first" gating was removed (§89), and a dimmed
 * half-toolbar was the single thing that made this editor look broken on open.
 */
.rte__tool.is-active {
  background: var(--accent-tint);
  border-color: var(--accent-tint-border);
  color: var(--accent-ink);
}

/* --radius-md, per the design.md §8 Select row. Sized to the 32px tool box so
   the bar keeps one horizontal rhythm; --text-xs because it sits inside a
   control strip, not in the form's own label hierarchy. */
.rte__size-select {
  height: 32px;
  padding: 0 var(--space-2);
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-md);
  background: var(--bg-base);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 500;
  cursor: pointer;
}

.rte__size-select:focus-visible {
  border-color: var(--focus-ring);
}

/*
 * ⚠️ --text-sm is the RESTING size, and it is a data decision, not a visual
 * preference: rich-text-editor.js defaults its size select to "Small" and only
 * emits a `.rte-text--*` span when the employer picks a size explicitly. So
 * unwrapped text MUST render at the same size .rte-text--sm does, here and in
 * .email-preview__body, or the editor shows one size and sends another.
 * Changing DEFAULT_SIZE in that module means changing both rules.
 */
.rte__content {
  min-height: 200px;
  padding: var(--space-3) var(--space-4);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  line-height: 1.6;
}

/* A contenteditable cannot use :placeholder-shown, and an "empty" one often
   still holds a browser-inserted <br>. Emptiness is decided in JS and
   published as .is-empty on the shell (rich-text-editor.js). */
.rte.is-empty .rte__content::before {
  content: attr(data-placeholder);
  color: var(--text-muted);
  pointer-events: none;
}

.rte__content:focus {
  outline: none; /* the shell's :focus-within border carries the focus signal */
}

/* base.css strips list markers app-wide (nav/menu lists are the common case),
   so a rich text list has to opt BACK IN - otherwise the employer's bulleted
   list renders as indented text with nothing marking it as a list. Applied here
   and again on .email-preview__body, since a stored body is rendered outside
   the editor too. */
.rte__content ul,
.rte__content ol {
  margin: 0 0 var(--space-3);
  padding-left: var(--space-5);
}

.rte__content ul {
  list-style: disc;
}

.rte__content ol {
  list-style: decimal;
}

.rte__content a {
  color: var(--accent-text);
  text-decoration: underline;
}

/* The anchor currently being typed into (§89's one-click link, "draft" mode).
   Tinted so the employer can see where the link ends while they are still
   inside it - without this a half-typed URL is indistinguishable from a
   finished one, and the only cue that clicking the button again will close it
   is the toolbar highlight. The class never survives getHtml(). */
.rte__content a.rte-link-draft {
  background: var(--accent-tint);
  border-radius: var(--radius-xs);
}

/* -------------------------------------------------------------------------
 * Rich text OUTPUT classes.
 * -------------------------------------------------------------------------
 * These four are the only classes rich-text-editor.js emits (it rewrites
 * execCommand's legacy <font size> into them) and the only ones its sanitizer
 * keeps on a <span>. They are therefore part of the STORED DATA FORMAT, not
 * merely presentation: renaming one silently reflows every message already
 * saved. Any surface that renders a stored body - the preview below, a future
 * Sent list, the backend's own email template - must ship these rules. Mapped
 * to the design.md §2 type scale rather than raw px so they follow the tokens.
 * ------------------------------------------------------------------------- */
.rte-text--sm {
  font-size: var(--text-sm);
}
.rte-text--md {
  font-size: var(--text-base);
}
.rte-text--lg {
  font-size: var(--text-lg);
}
.rte-text--xl {
  font-size: var(--text-h5);
}

/* =========================================================================
 * Recipient Chips (design.md §9 "Recipient chip")
 * -------------------------------------------------------------------------
 * Controller: assets/js/lib/recipient-chips.js; catalog entry components.md
 * §92. First use: the Send Email composer's Recipient field (§90).
 *
 * ⚠️ NOT `.chip` (§36), and not a variant of it. That class is a --text-xs pill
 * sized for a one-word tag; this token is 32px tall and carries an avatar plus
 * two strings, so inheriting it would mean overriding its font-size, padding,
 * height and gap - every declaration it has. It reuses the PATTERN on purpose
 * though - pill radius, inline-flex, the same nested remove button - so the two
 * read as siblings.
 * ========================================================================= */
.recipient-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.recipient-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  max-width: 100%;
  min-height: 32px;
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-pill);
  background: var(--bg-surface);
  font-size: var(--text-sm);
  color: var(--text-primary);
}

/* Sizing lives here, not in avatar-illustration.js - that module emits no
   dimensions by design and every mount supplies its own (see .candidate-card__
   avatar). Both branches need these: the <img> branch does NOT get the
   .avatar-illustration class, so border-radius/object-fit cannot be inherited
   from it. */
.recipient-chip__avatar {
  display: block;
  flex-shrink: 0;
  width: 24px;
  height: 24px;
  border-radius: var(--radius-full);
  object-fit: cover;
}

/* min-width:0 is what lets the ellipsis below actually engage - a flex child
   defaults to min-content width and would otherwise push the chip wider than
   the modal rather than truncate. */
.recipient-chip__label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.recipient-chip__name {
  font-weight: 600;
}

/* Quieter than the name: both are needed to identify the person, but the name
   is what the employer reads and the address is what they verify. */
.recipient-chip__email {
  color: var(--text-secondary);
}

/* 20px box for a 12px glyph - matching `.chip button svg`, so the remove
   affordance is the same size everywhere it appears. Only rendered when there
   is more than one recipient (recipient-chips.js). */
.recipient-chip__remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  padding: 0;
  border: none;
  border-radius: var(--radius-full);
  background: none;
  color: var(--icon-muted);
  cursor: pointer;
}

.recipient-chip__remove svg {
  width: 12px;
  height: 12px;
}

.recipient-chip__remove:hover {
  background: var(--hover-surface);
  color: var(--text-primary);
}

/* =========================================================================
 * Send Email modal - composer + preview (design.md §9 "Revealing contact
 * information" - Send Email is a reveal-gated affordance; §10 "Modal / dialog")
 * -------------------------------------------------------------------------
 * Controller: assets/js/lib/email-compose.js; catalog entry components.md §90.
 * Mounted inside .modal--wide. The two views are SIBLINGS swapped by `hidden`
 * and never re-rendered - see the module header for why.
 * ========================================================================= */

/*
 * ⚠️ NO `gap` HERE, and that is the fix for a real inconsistency: `.field`
 * already carries `margin-bottom: var(--space-4)`, so a flex gap on the
 * container ADDED to it and left the composer's fields 32px apart while every
 * other modal form in the app (change password, send assessment) sits at 16px.
 * The house convention is that `.field` owns its own spacing and the container
 * owns none. Do not reintroduce a gap.
 */
.email-compose {
  display: flex;
  flex-direction: column;
}

/* The last field's margin would otherwise stack on top of .modal__body's own
   padding, leaving a deeper gutter under Reply-to than above Recipient. */
.email-compose .field:last-child {
  margin-bottom: 0;
}

/* Same specificity guard as .alert[hidden] / .btn[hidden] above: one class ties
   the browser default [hidden]{display:none} (one attribute), and `display:
   flex` declared later in the sheet wins the tie - so the view swap would show
   BOTH views at once without this. Applies to both sibling views. */
.email-compose[hidden],
.email-preview[hidden] {
  display: none;
}

/* -------------------------------------------------------------------------
 * Preview. Read-only by construction - it is rendered markup with no controls
 * in it besides the reply link.
 * ------------------------------------------------------------------------- */
.email-preview {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

/* A <dl> because these are genuinely term/definition pairs: a label column and
   a value column, collapsing to stacked rows when narrow. The label column is
   FIXED at 64px rather than `auto` so "To" and "Subject" share one left edge -
   an auto column sizes to the longer word and indents the shorter one, which
   reads as accidental in a two-row header. */
.email-preview__meta {
  display: grid;
  grid-template-columns: 64px 1fr;
  gap: var(--space-2) var(--space-3);
  margin: 0;
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--border-default);
}

.email-preview__meta dt {
  color: var(--text-secondary);
  font-size: var(--text-sm);
  font-weight: 600;
}

.email-preview__meta dd {
  margin: 0;
  color: var(--text-primary);
  font-size: var(--text-sm);
  overflow-wrap: anywhere; /* a long address must not widen the modal */
}

@media (max-width: 479px) {
  .email-preview__meta {
    grid-template-columns: 1fr;
    gap: var(--space-1);
  }
  .email-preview__meta dd {
    margin-bottom: var(--space-2);
  }
}

/*
 * ⚠️ The body sets its OWN sizes and is deliberately exempt from the in-app
 * `p -> --text-sm` cap at the top of this file. It happens to resolve to the
 * same 14px today because Small is the editor's default size (see
 * .rte__content) - but it must be stated here rather than inherited, because
 * the employer can size any run UP and the cap would drag those <p>s back down.
 */
.email-preview__body {
  color: var(--text-primary);
  font-size: var(--text-sm);
  line-height: 1.6;
  overflow-wrap: anywhere;
}

.email-preview__body p {
  font-size: inherit;
  line-height: inherit;
}

/* Opts back in to list markers - see the .rte__content note above. */
.email-preview__body ul,
.email-preview__body ol {
  margin: 0 0 var(--space-3);
  padding-left: var(--space-5);
}

.email-preview__body ul {
  list-style: disc;
}

.email-preview__body ol {
  list-style: decimal;
}

.email-preview__body a {
  color: var(--accent-text);
  text-decoration: underline;
}

/* -------------------------------------------------------------------------
 * The standing platform footer + reply button, and its twin on the success
 * confirmation.
 * -------------------------------------------------------------------------
 * Both use the house INSET PANEL recipe - `--space-4` padding, 1px
 * --border-default, --radius-md, --bg-surface - the same one the Candidate
 * Detail Drawer's facts panel (.candidate-profile__facts) and the Template
 * Builder's question blocks use. Centred, because unlike the message above it
 * this block is not correspondence: it is Vanceli speaking, and centring is
 * what separates a system notice from the employer's own words.
 *
 * The two are declared together so they cannot drift. The employer checks the
 * reply-to address in the preview and then sees it again after sending; if the
 * two boxes looked different, the second would read as a new fact rather than
 * a confirmation of the one they already approved.
 * ------------------------------------------------------------------------- */
/* Attachment list inside the composer preview and the Email Details overlay
 * (components.md §90 "Attachments"). The ROWS themselves are
 * .assessment-attachments__file-* above - deliberately reused, not re-declared,
 * since it is the same object rendered in the same treatment. Only the heading
 * and the block's own spacing are new here. */
.email-preview__attachments {
  margin-bottom: var(--space-5);
}

.email-preview__attachments-title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-sm);
  font-weight: var(--font-semibold);
  color: var(--text-secondary);
}

.email-preview__reply,
.email-sent__reply {
  padding: var(--space-4);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  background: var(--bg-surface);
  text-align: center;
}

.email-preview__reply-note {
  margin: 0 0 var(--space-3);
  color: var(--text-secondary);
  font-size: var(--text-sm);
}

/* -------------------------------------------------------------------------
 * Post-send confirmation. Its body reuses .confirm-panel / .confirm-panel__icon
 * (components.md §12) inside a modal; only these two blocks are new.
 * ------------------------------------------------------------------------- */
.email-sent__lead {
  margin: 0 0 var(--space-4);
  color: var(--text-primary);
}

.email-sent__reply {
  margin: 0;
  color: var(--text-secondary);
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
}

.email-sent__reply strong {
  display: inline-block;
  margin-top: var(--space-1);
  color: var(--text-primary);
}

/* ==========================================================================
   EMAIL DETAILS OVERLAY - components.md §96
   design.md §10 (right-side overlay with pinned header + footer), §11.1.
   Markup: dashboard/employer/sent-emails/index.html.
   Controller: assets/js/lib/email-details-drawer.js.
   ==========================================================================
   Shares .candidate-drawer's fixed right-hand geometry, --z-drawer, the
   .drawer-backdrop, and @keyframes drawer-in/out declared on the VISIBLE state
   (see the long note on .candidate-drawer above for why the animation is
   written that way rather than as a transition off an off-screen default).

   ⚠️ THREE-SIBLING FLEX COLUMN, NOT STICKY-IN-A-SCROLL-CONTAINER - the same
   structure .advanced-search-drawer uses, adopted for the same reason that
   block records: `position: sticky; bottom: 0` pins a footer only while the
   container OVERFLOWS. This overlay's content is a single email body, which is
   routinely SHORTER than the drawer, so a sticky delivery bar would drop to
   mid-panel with dead space beneath it on the common case rather than the rare
   one.

   The flex column also satisfies "nothing scrolls through the pinned items"
   STRUCTURALLY: __header and __footer are siblings of the scroll region, not
   overlays on top of it, so content physically cannot pass behind them. That
   deletes the entire no-show-through apparatus .candidate-drawer__header needs
   - the opaque background, the negative full-bleed margins cancelling the
   container's side padding, and the `top: calc(-1 * var(--space-6))` offset
   that exists because a sticky offset resolves against the scroll container's
   PADDING edge. Do not reintroduce any of it here.

   This block lives in components.css rather than in pages/sent-emails.css,
   despite having exactly one consumer, because it must sit beside the shared
   @keyframes it animates with and its selectors are listed in the shared
   prefers-reduced-motion group above. Splitting it would put an animation and
   its reduced-motion suppression in two different stylesheets.
   ========================================================================== */
.email-drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  /* Wider than .candidate-drawer's 480px: this holds prose at a readable
     measure, not a column of short facts. */
  width: 520px;
  max-width: 100vw;
  background: var(--bg-base);
  border-left: 1px solid var(--border-default);
  box-shadow: var(--shadow-xl);
  z-index: var(--z-drawer);
  display: flex;
  flex-direction: column;
  /* No padding on the container on purpose - it is a layout box, not a scroll
     box. Each of the three regions owns its own padding, which is what lets the
     header and footer draw hairlines that span the full width. */
}

/* Required, not belt-and-braces: the rule above sets an AUTHOR `display: flex`,
   which beats the user-agent [hidden]{display:none} regardless of order, so
   without this the overlay would never hide. Same trap .email-preview[hidden]
   and .candidate-drawer[hidden] document. */
.email-drawer[hidden] {
  display: none;
}

.email-drawer:not([hidden]):not(.is-closing) {
  animation: drawer-in var(--duration-slow) var(--ease-out);
}

.email-drawer.is-closing {
  animation: drawer-out var(--duration-slow) var(--ease-in) forwards;
}

@media (max-width: 599px) {
  .email-drawer {
    width: 100%;
  }
}

/* ---- Pinned header: title left, close right ---------------------------- */
/* flex-shrink: 0 is load-bearing - without it a long body would compress the
   header instead of scrolling, since flex items shrink before they overflow. */
.email-drawer__header {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-5);
  border-bottom: 1px solid var(--border-default);
}

/* text-h5/20px - the design.md §2.2 in-app type ceiling, matching every other
   overlay title. */
.email-drawer__title {
  margin: 0;
  font-size: var(--text-h5);
  font-weight: 600;
  line-height: 1.3;
  color: var(--text-primary);
}

/* IN the header, not `position: fixed` like .candidate-drawer__close. That one
   is fixed because it is a sibling of two swappable view panels and has no
   sticky parent spanning the scroll box; here there is a single body and a real
   header to sit in, so the simpler treatment is also the more robust one - the
   × can never drift out of alignment with the title it sits beside. The 20px
   glyph comes from the shared overlay-close rule near the top of this file and
   the 44x44 hit area from .btn-icon (design.md §3.7/§4.4). The negative margin
   pulls the button's own padding back so its GLYPH, not its hit area, lines up
   with the header's right padding. */
.email-drawer__close {
  flex-shrink: 0;
  margin-right: calc(var(--space-2) * -1);
}

/* ---- The only scroll container in this overlay ------------------------- */
.email-drawer__body {
  /* `flex: 1` is 1 1 0% - it absorbs every leftover pixel, which is precisely
     what holds the footer against the true bottom edge on a one-line email and
     lets it scroll on a long one. A `flex-basis: auto` here would reintroduce
     the sticky version's problem. */
  flex: 1;
  overflow-y: auto;
  /* A long unbroken address or URL must ellipsize or wrap, never scroll the
     panel sideways. */
  overflow-x: hidden;
  padding: var(--space-5);
}

/* ---- Pinned delivery receipt ------------------------------------------- */
.email-drawer__footer {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-4) var(--space-5);
  border-top: 1px solid var(--border-default);
  background: var(--bg-base);
  color: var(--text-secondary);
  font-size: var(--text-sm);
}

/* The delivered double-tick (icons.js "checks"). --color-success is the only
   green in the system and it flips to a lighter value in dark mode, so the
   glyph stays legible on both surfaces without a second declaration.
   ⚠️ The word "Delivered" beside it carries the meaning - the colour and the
   glyph are reinforcement, never the signal on their own (design.md §4.6). */
.email-drawer__delivered-icon {
  display: flex;
  flex-shrink: 0;
  color: var(--color-success);
}

/* 20px, a touch larger than the 16px this line's type would suggest: the double
   tick is two overlapping strokes rather than one, so it needs the extra size to
   read as a double tick rather than as a smudge. See the ⚠️ on `checks` in
   icons.js - the glyph and this size were tuned together. */
.email-drawer__delivered-icon svg {
  width: 20px;
  height: 20px;
}

/* The chip list sits above the subject, mirroring the composer, so it needs the
   same gap the rest of .email-preview's column uses. .email-preview supplies
   that gap already; this only removes the list's own default padding, which
   base.css strips app-wide but which a nested mount can reintroduce. */
.email-drawer__recipients {
  min-width: 0;
}
