/* =====================================================================
   candidate.css — PRD v2.1 §6.4: shared sheet for the six candidate-
   facing pages (interview_landing/run/done, test_landing/run/done).

   Slice D-RESTYLE: linked from all six pages (after style.css, which
   still provides the shared components: .btn, .flash, .consent-card,
   .warnbar, ul.clean, .meta …). This sheet owns the candidate-journey
   layout (.candidate-page / .candidate-wrap / .candidate-card).
   Token VALUES follow the Claude/Apple reference language — keep in
   lock-step with style.css (check_tokens.py asserts equality).
   ===================================================================== */

@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
:root {
  /* color — Vanta-style cool neutrals; IDENTICAL values to style.css (the
     token check asserts cross-sheet equality — keep in lock-step). */
  --bg: #ffffff;
  --panel: #ffffff;
  --ink: #17181c;  --muted: #62666e;  --faint: #6b6f77;
  --line: #e5e6ea; --line-soft: #eef0f2;
  /* THE BOOKABLE-DAY TINT, restated here because this sheet declares its
     own light palette rather than inheriting style.css's -- 8.11 asserts
     every var() resolves within the sheet that uses it, and check_tokens
     asserts the two sheets agree on the VALUE. Same hex as style.css. */
  --cal-offered: #dfe6ff;
  --accent: #3b5bdb; --accent-d: #2f49b0; --accent-tint: #eef1fd;
  --ring: #5f7fdc;
  --ok: #12844a;   --ok-tint: #e4f3ea;   --ok-ink: #0c6b3b;
  --warn: #c07f18; --warn-tint: #fbf0da; --warn-ink: #8a5b00;
  --bad: #d33f37;  --bad-tint: #fbe9e7;  --bad-ink: #b3261e;
  --neutral: #62666e; --neutral-tint: #f1f2f4;
  --field: #ffffff; --canvas: #fafafa; --hover: #f5f6f7;
  /* type — sans everywhere; serif token kept (unused) for lock-step parity. */
  --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
  --font-serif: Georgia, 'Times New Roman', serif;
  --font-mono: ui-monospace, "SF Mono", "Cascadia Code", Consolas, monospace;
  --text-xs: 12px; --text-sm: 13px; --text-base: 14px; --text-md: 15px;
  --text-lg: 17px; --text-xl: 22px; --text-2xl: 30px;
  /* space (4px scale) */
  --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 24px; --s6: 32px; --s7: 48px;
  /* shape */
  --r-sm: 6px; --r-md: 8px; --r-lg: 10px; --r-xl: 14px;
  --shadow-1: none;
  --shadow-2: 0 8px 24px rgba(20,22,26,.10);
}

/* Vanta-style: sans headlines everywhere (serif dropped) */
.candidate-page h1 { font-family: var(--font-sans); font-weight: 650; letter-spacing: -.01em; }
/* candidate-facing targets stay 44px (WCAG AAA) even though the workspace
   buttons are 38px */
.candidate-page .btn { min-height: 44px; }
/* CV drop-zone: dashed frame around the real (kept) file input */
.drop-zone { display: block; margin-top: 6px; padding: 26px 18px; text-align: center;
  border: 1.5px dashed var(--line); border-radius: var(--r-lg); background: var(--panel);
  cursor: pointer; transition: border-color .15s ease, background .15s ease; font-weight: 400; }
.drop-zone:hover, .drop-zone.is-over { border-color: var(--accent); background: var(--accent-tint); }
.drop-zone input[type=file] { position: absolute; opacity: 0; width: 1px; height: 1px;
  margin: 0; padding: 0; border: 0; clip: rect(0,0,0,0); }
/* THE FOCUS INDICATOR HAS TO LIVE ON THE FRAME, because the real input is the
   1x1 clipped box above. `input:focus` in style.css sets `outline: none` and
   then paints a border and a box-shadow ON THAT 1x1 BOX, so neither renders;
   `a/button/summary:focus-visible` does not match an `<input>`. The result was
   a keyboard user reaching the apply form's ONE REQUIRED CONTROL with no
   indication at all (WCAG 2.4.7). `:focus-within` on the visible wrapper is
   the idiom this product already uses for the same reason on `.check-chip`,
   `.chip-input` and the talent-bank search. `--ring` is 3.78:1 and was chosen
   against the 1.4.11 non-text floor; `--accent-tint` is 1.13:1 on white and
   would have contributed nothing here. */
.drop-zone:focus-within { outline: 2px solid var(--ring); outline-offset: 2px;
  border-color: var(--accent); }
.drop-zone .dz-text { color: var(--muted); font-size: var(--text-base); }
.drop-zone .dz-text strong { color: var(--ink); }
/* CV-first lead: an accent-tinted drop zone with a stronger headline label so it
   reads as the primary starting point of the apply form. */
.dz-lead { display: block; font-weight: 600; font-size: var(--text-md); margin-top: 4px; }
.drop-zone.dz-accent { border-color: var(--accent); background: var(--accent-tint);
  padding: 32px 18px; }
.drop-zone.dz-accent .dz-text strong { color: var(--accent-d); }
/* Collapsible job description + data-preferences disclosures on the apply page. */
.jd-details > summary { cursor: pointer; font-weight: 600; color: var(--ink);
  padding: 6px 0; }
.jd-details > summary:hover { color: var(--accent); }

/* ---- centered-card pattern (§6.4) ----
   .candidate-page on <body>; .candidate-wrap centres the column on the
   canvas (default 680px for landing pages; .wide 820px for run pages;
   .narrow 560px for done pages); .candidate-card is the hero card. */
.candidate-page {
  min-height: 100vh; margin: 0; display: block;
  background: var(--bg); color: var(--ink);
  font-family: var(--font-sans); font-size: var(--text-md); line-height: 1.55;
  -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
}
.candidate-wrap { max-width: 620px; margin: 48px auto 80px; padding: 0 24px; }
.candidate-wrap.wide { max-width: 820px; margin-top: var(--s5); }
.candidate-wrap.narrow { max-width: 560px; margin-top: 60px; }
/* v2: one continuous reading column on paper — cards flatten into
   hairline-separated sections. */
.candidate-card {
  margin: 0; padding: 28px 0;
  background: transparent; border: 0; border-radius: 0; box-shadow: none;
  border-bottom: 1px solid var(--line);
}
.candidate-card:last-child { border-bottom: 0; }
.candidate-brandline { text-align: center; color: var(--muted); font-size: var(--text-sm); margin: 0 0 var(--s4); }
/* Language switcher line on the narrow completion pages (right-aligned). */
.candidate-langline { text-align: right; margin: 0 0 var(--s2); }
.candidate-brandline .langswitch, .candidate-brand .langswitch { margin-left: var(--s2); }
.candidate-brand { display: flex; align-items: center; justify-content: center; gap: var(--s2);
  margin: 0 0 var(--s4); font-weight: 600; }
/* `.apply-brand`, `.apply-company` and `.brand-wordmark` were DELETED here on
   2026-09-07. The first two lost their markup at the 2026-09-06 two-column
   rewrite (`.ap-brand` / `.ap-company` replaced them) and the third lost its
   only `<img>` when the Verqa mark became the `.wordmark` COMPONENT -- see the
   note in templates/apply.html for why that `<img>` could never render right.
   Nothing in templates/ emits any of the three; `check_tokens` 8.9's dead-rule
   scan is what would have found them eventually, and this is finding them at
   the commit that orphaned them instead. */

/* numbers / refs / timers: tabular figures; mono is reserved for refs */
.candidate-mono {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
}

/* run-page header row: H1 + candidate/position meta on one line */
.candidate-header { display: flex; justify-content: space-between;
                    align-items: center; gap: 14px; flex-wrap: wrap; }
.candidate-header h1 { margin: 0; }

/* ---- interview_run.html — voice-only stage (no on-screen question, no
   editable answer). Big mic orb + speaking/listening state only. ---- */
.voice-stage { min-height: 100vh; display: flex; flex-direction: column; background: var(--bg); }
.voice-top { display: flex; align-items: center; justify-content: space-between; gap: 14px;
             padding: 16px 28px; background: var(--panel); border-bottom: 1px solid var(--line); }
/* THE TOP BAR'S LEFT SIDE NAMES THE SESSION: the mark, the ROLE as this page's
   `h1`, a hairline middot, and the candidate. It named the brand, which a
   candidate on this page already knows. The `h1` is drawn at the bar's own size
   because a heading's LEVEL and its SIZE are different questions -- the level is
   the outline and is not negotiable, the size is the design. */
.voice-role-h { margin: 0; font-size: 14px; font-weight: 600;
  letter-spacing: -.01em; color: var(--ink); }
.voice-sep { color: var(--line); }
.voice-who { color: var(--muted); font-weight: 400; }
.voice-brand { display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 14px;
               letter-spacing: -.01em; color: var(--ink); }
.voice-brand .brand-mark { width: 28px; height: 28px; flex-shrink: 0; display: flex;
               align-items: center; justify-content: center; border-radius: 8px;
               background: var(--accent); color: #fff; font-size: 13px; font-weight: 700; }
.voice-metaright { display: flex; align-items: center; gap: 16px; font-size: 13px; color: var(--muted); }
.rec-pill { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); }
.rec-pill .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--faint); }
.rec-pill.live .dot { background: var(--bad); animation: recblink 1.4s ease-in-out infinite; }
@keyframes recblink { 0%,100%{opacity:1} 50%{opacity:.25} }

.voice-body { flex: 1; display: flex; flex-direction: column; align-items: center;
              justify-content: center; text-align: center; padding: 40px 24px; }
/* `.voice-title` AND `.voice-role` ARE DELETED (owner handoff, 2026-09-06). They
   were an "AI interview" heading and a role subtitle above the camera; the mock
   makes the camera the first thing, and the heading MOVED into the top bar as
   `.voice-role-h` rather than being dropped -- a page with no `h1` is a heading
   outline with nothing in it. */

.orb-wrap { position: relative; width: 260px; height: 260px; display: flex;
            align-items: center; justify-content: center; margin: 26px 0 4px; }
.orb { position: relative; z-index: 2; width: 152px; height: 152px; border-radius: 50%;
       background: var(--accent-tint); border: 1px solid #d7e1ff; color: var(--accent);
       display: flex; align-items: center; justify-content: center;
       transition: transform .3s ease, background .3s ease, color .3s ease, border-color .3s ease; }
.orb .orb-icon { width: 56px; height: 56px; }
.orb-ring { position: absolute; top: 50%; left: 50%; width: 152px; height: 152px;
            margin: -76px 0 0 -76px; border-radius: 50%; border: 2px solid var(--accent);
            opacity: 0; z-index: 1; }
.orb-spin { display: none; width: 48px; height: 48px; border: 4px solid var(--line);
            border-top-color: var(--accent); border-radius: 50%; animation: spin .9s linear infinite; }

.voice-stage[data-state="speaking"] .orb { background: #dfe8ff; transform: scale(1.04); }
.voice-stage[data-state="speaking"] .orb-ring { animation: ring 2.2s ease-out infinite; }
.voice-stage[data-state="speaking"] .orb-ring.r2 { animation-delay: 1.1s; }
.voice-stage[data-state="listening"] .orb { background: var(--accent); color: #fff; border-color: var(--accent); }
.voice-stage[data-state="listening"] .orb-ring { animation: ring 1.8s ease-out infinite; }
.voice-stage[data-state="listening"] .orb-ring.r2 { animation-delay: .9s; }
.voice-stage[data-state="thinking"] .orb { background: var(--panel); }
.voice-stage[data-state="thinking"] .orb .orb-icon { display: none; }
.voice-stage[data-state="thinking"] .orb-spin { display: block; }
@keyframes ring { 0% { opacity: .45; transform: scale(1); } 100% { opacity: 0; transform: scale(1.78); } }
@keyframes spin { to { transform: rotate(360deg); } }

.voice-question { margin: 22px auto 0; max-width: 620px; background: var(--panel);
  border: 1px solid var(--line); border-radius: var(--r-lg); padding: 18px 22px;
  font-size: 20px; line-height: 1.45; font-weight: 500; color: var(--ink);
  text-align: center; white-space: pre-wrap; box-shadow: var(--shadow-1); }
.voice-stage[data-state="speaking"] .voice-question { border-color: var(--accent); }
/* Bilingual question: the Chinese line sits under the English, slightly larger. */
.voice-question .q-zh { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line);
  font-size: 22px; }
.voice-state { font-size: 18px; font-weight: 500; color: var(--ink); min-height: 26px; margin-top: 14px; }
.voice-hint { font-size: 13.5px; color: var(--muted); max-width: 420px; margin: var(--s2) auto 0; line-height: 1.5; }
/* THE END CONTROL IS A GHOST AT THE STAGE'S OWN SIZE (owner handoff,
   2026-09-06). `.btn.ghost` is the shared secondary -- transparent, hairline,
   500 weight -- and this adds only the height, because a 44px control in the
   middle of a 640px stage reads as an afterthought while a primary-weight one
   reads as an invitation to end. `interview.js` sets only `textContent` and
   `hidden` on this element, which is what makes the class safe to change here. */
.candidate-page .btn.ghost.iv-endbtn { min-height: 48px; padding: var(--s3) var(--s5);
  border-radius: var(--r-lg); font-size: 14px; }

.voice-eq { display: none; align-items: flex-end; gap: 4px; height: 26px; margin-top: 18px; }
.voice-stage[data-state="listening"] .voice-eq { display: flex; }
.voice-eq i { width: 4px; height: 7px; background: var(--accent); border-radius: 2px; animation: eq 1s ease-in-out infinite; }
.voice-eq i:nth-child(2){ animation-delay:.15s } .voice-eq i:nth-child(3){ animation-delay:.3s }
.voice-eq i:nth-child(4){ animation-delay:.45s } .voice-eq i:nth-child(5){ animation-delay:.6s }
@keyframes eq { 0%,100%{ height:7px } 50%{ height:24px } }

.voice-actions { margin-top: 30px; display: flex; gap: 12px; align-items: center; justify-content: center; }
.btn.lg { padding: 13px 30px; font-size: 15px; border-radius: 10px; }
.voice-foot { display: flex; align-items: center; gap: 9px; justify-content: center;
              padding: 16px 24px 30px; color: var(--faint); font-size: 12px; }
.voice-foot svg { flex-shrink: 0; }

.voice-cam { position: fixed; right: 20px; bottom: 20px; width: 172px; border-radius: 12px;
             overflow: hidden; border: 1px solid var(--line); background: #0e121b;
             box-shadow: var(--shadow-2); }
.voice-cam video { display: block; width: 100%; aspect-ratio: 4/3; object-fit: cover; }
.voice-cam .cam-tag { position: absolute; top: 8px; left: 8px; display: inline-flex; align-items: center;
                      gap: 5px; background: rgba(14,18,27,.72); color: #fff; font-size: 10.5px;
                      font-weight: 500; padding: 2px 8px; border-radius: 999px; }
.voice-cam .cam-tag .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--bad);
                      animation: recblink 1.4s ease-in-out infinite; }

/* Camera-forward interview: the candidate's camera is the centre stage; the AI
   voice is a small orb badge (their positions were swapped). */
.cam-stage { position: relative; width: min(520px, 82vw); aspect-ratio: 4 / 3;
  margin: 20px auto 4px; border-radius: var(--r-lg); overflow: hidden;
  background: #0e121b; border: 1px solid var(--line); box-shadow: var(--shadow-2); }
.cam-stage video { display: block; width: 100%; height: 100%; object-fit: cover; }
.cam-stage .cam-tag { position: absolute; top: 10px; left: 10px; display: inline-flex;
  align-items: center; gap: 5px; background: rgba(14,18,27,.72); color: #fff; font-size: 11px;
  font-weight: 500; padding: 3px 9px; border-radius: 999px; }
.cam-stage .cam-tag .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--bad);
  animation: recblink 1.4s ease-in-out infinite; }
.cam-stage .ai-orb { position: absolute; right: 12px; bottom: 12px; width: 46px; height: 46px;
  border-radius: 50%; background: var(--accent); color: #fff; display: flex; align-items: center;
  justify-content: center; box-shadow: var(--shadow-1); }
.cam-stage .ai-orb svg { width: 22px; height: 22px; }
.voice-stage[data-state="speaking"] .cam-stage .ai-orb { animation: aiorb 1.1s ease-in-out infinite; }
@keyframes aiorb { 0%,100% { transform: scale(1); } 50% { transform: scale(1.14); } }

@media (prefers-reduced-motion: reduce) {
  .orb-ring, .voice-eq i, .rec-pill.live .dot, .voice-cam .cam-tag .dot, .orb-spin,
  .cam-stage .cam-tag .dot, .cam-stage .ai-orb { animation: none !important; }
}

/* ---- test_run.html ---- */
.timerbar { position: sticky; top: 0; z-index: 10; background: var(--panel);
            border-bottom: 1px solid var(--line); padding: 10px 20px;
            display: flex; justify-content: space-between; align-items: center; }
.timerbar-right { display: flex; align-items: center; gap: 16px; }
#answered { font-variant-numeric: tabular-nums; }
#clock { font-variant-numeric: tabular-nums;
         font-weight: 600; font-size: 20px; }
#clock.warn { color: var(--bad); }
.wt-likert { display: flex; gap: 6px; flex-wrap: wrap; margin-top: var(--s2); }
.wt-likert label { flex: 1; min-width: 120px; min-height: 44px; box-sizing: border-box;
                   display: flex; gap: 6px;
                   align-items: center; border: 1px solid var(--line);
                   border-radius: 8px; padding: 8px 10px; cursor: pointer;
                   font-weight: 400; margin: 0; font-size: var(--text-sm);
                   transition: border-color .15s ease, background .15s ease; }
.wt-likert label:hover { background: var(--bg); }
.wt-likert input { width: auto; }
textarea.wt-answer { min-height: 200px; }

/* Aptitude multiple-choice: stacked, clickable A–D options. */
.wt-mcq { display: flex; flex-direction: column; gap: 8px; margin-top: var(--s2); }
.wt-mcq .mcq-option { display: flex; gap: 10px; align-items: flex-start;
                      min-height: 44px; box-sizing: border-box;
                      border: 1px solid var(--line); border-radius: 8px;
                      padding: 12px; cursor: pointer; margin: 0;
                      font-weight: 400; font-size: var(--text-sm);
                      transition: border-color .15s ease, background .15s ease; }
.wt-mcq .mcq-option:hover { background: var(--bg); }
.wt-mcq .mcq-option input { width: auto; margin-top: 3px; }
/* SJT scenarios: Best / Worst pick per scenario (Written Test Framework). */
.wt-sjt { display: flex; gap: var(--s3); flex-wrap: wrap; margin-top: var(--s2); }
.wt-sjt-pick { flex: 1; min-width: 180px; display: flex; flex-direction: column;
               gap: 6px; margin: 0; font-size: var(--text-sm); font-weight: 600; }
.wt-sjt-pick select { border: 1px solid var(--line); border-radius: 8px;
                      min-height: 44px; padding: 8px 10px; font-size: var(--text-base);
                      background: var(--panel); color: var(--ink); }

/* New Design port — candidate-facing flow (apply · consent · interview · test)
   Token-only, additive; layout patterns lifted from the mockups. */

/* eyebrow accent variant (mockup "Now hiring" label above the role title) */
.eyebrow.accent { color: var(--accent); }

/* lead / intro paragraph under an H1 on a candidate card (mockup: 14.5px muted) */
.candidate-lead { font-size: var(--text-base); color: var(--muted);
                  line-height: 1.6; margin: 0 0 var(--s5); }
.candidate-card h1 + .candidate-lead { margin-top: var(--s1); }
.candidate-lead strong { color: var(--ink); }

/* bulleted "how it works / before you start" list (mockup ul) */
.candidate-steps { margin: 0 0 var(--s5); padding-left: 18px;
                   font-size: var(--text-sm); color: var(--muted); line-height: 1.8; }
.candidate-steps strong { color: var(--ink); }

/* status circle above the H1 on outcome pages (done / expired / invalid) */
.status-badge { width: 56px; height: 56px; border-radius: 999px;
                display: inline-flex; align-items: center; justify-content: center;
                font-size: var(--text-2xl); margin-bottom: var(--s4); }
.status-badge.ok  { background: var(--ok-tint);   color: var(--ok-ink); }
.status-badge.warn { background: var(--warn-tint); color: var(--warn-ink); }
.status-badge.bad { background: var(--bad-tint);  color: var(--bad-ink); }

/* "what happens next" panel on the application-received page */
.next-steps { text-align: left; background: var(--bg);
              border: 1px solid var(--line-soft); border-radius: var(--r-lg);
              padding: var(--s4) var(--s5); margin-top: var(--s5); }
.next-steps ol { margin: 0; padding-left: 18px; font-size: var(--text-sm);
                 color: var(--muted); line-height: 2; }

/* consent_confirm: each required box as a bordered, clickable row.
   Styling only — the legal copy and inputs are unchanged. */
.consent-check { display: flex; align-items: flex-start; gap: var(--s3);
                 font-weight: 400; border: 1px solid var(--line);
                 border-radius: var(--r-md); padding: 14px 16px;
                 margin: 0 0 var(--s3); cursor: pointer; font-size: var(--text-sm); }
.consent-check input { width: auto; margin-top: 1px; accent-color: var(--accent);
                       flex-shrink: 0; }
.consent-check span { color: var(--muted); line-height: 1.55; }
.consent-check strong { color: var(--ink); }

/* run-page meta line (candidate · role · time-left) sized to the mockup */
.candidate-header .candidate-metaline { font-size: var(--text-sm); color: var(--muted); }
.candidate-header .candidate-metaline strong { color: var(--ink); }

/* hands-free answer-window countdown on the interview run page */
.iv-timer { font-variant-numeric: tabular-nums; font-weight: 600; }

/* Client-side file-validation feedback on the apply form (H27). */
.hint-bad { color: var(--bad); font-weight: 600; }

/* Persistent interview progress indicator (H28). */
.iv-progress { font-size: var(--text-sm); color: var(--muted); font-variant-numeric: tabular-nums; }
.iv-progress[hidden] { display: none; }

/* CV upload progress bar (apply form). Width is set at runtime by apply.html. */
.upload-bar { margin-top: var(--s3); height: 8px; border-radius: 999px; background: var(--line-soft); overflow: hidden; }
.upload-bar[hidden] { display: none; }
.upload-fill { display: block; height: 100%; width: 0; background: var(--accent); transition: width .15s ease; }

/* Interview pre-flight device check status (C9). */
.devicecheck { margin-top: var(--s4); }
.devok { color: var(--ok-ink); font-weight: 600; }
.devbad { color: var(--bad); font-weight: 600; }

/* ── Reduced motion (WCAG 2.3.3) ──────────────────────────────────────────
   Lock-step copy of the block at the foot of static/style.css; see the
   reasoning there. This sheet is loaded on the six candidate-facing pages and
   carries 18 transition / animation declarations of its own, including the
   `recblink` recording pulse on .rec-pill.live and the .voice-eq bars, which
   run for the whole length of an interview. A candidate cannot opt out of
   sitting the interview, so this is the surface where the escape hatch matters
   most. Guard: tests/test_layout_brand.py, marker `G20:`. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0s !important;
    animation-delay: 0s !important;
  }
  html { scroll-behavior: auto !important; }
}

/* -- Mobile: tap targets and the iOS input-zoom floor (Phase 14) -----------
   MEASURED at documentElement.clientWidth 365 on the live /apply/<token>, and
   the rules that produce those numbers re-read in this tree at HEAD:

     consent checkbox   13 x 13   (UA default; nothing in either sheet sized it)
     text/email/tel     14px      static/style.css `input[type=text], ...`
     input[type=file]   13px      static/style.css `input[type=file]`
     summary            317 x 35  .jd-details summary
     submit button      317 x 44  the ONLY compliant target of the 18

   16 of 18 interactive elements on the public application form were under the
   44px tap-target floor, and the three worst were the CONSENT ticks -- the
   controls carrying the candidate's legal consent to AI-assisted processing,
   at under 9% of the 44 x 44 minimum area.

   TWO SEPARATE floors, and they are not the same rule:
     * 44px is the TAP target (WCAG 2.5.5). The target for a checkbox is its
       <label>, not the box: clicking anywhere in the label toggles it. So the
       box grows to 24px for visibility and the LABEL carries the 44px, which
       is what a thumb actually hits.
     * 16px is the iOS Safari font-size floor. Safari force-zooms the viewport
       when a focused field is under 16px, and the zoom does not undo itself.
       What is asserted here is the CSS PRECONDITION, measured in Chrome. The
       zoom itself is user-agent behaviour, needs real iOS Safari, and is NOT
       closable from a browser-connector session.

   Scoped to 860px -- the app's own mobile breakpoint -- and to .candidate-page
   so nothing in the recruiter product moves. This is also the first width media
   query this sheet has ever carried (#45).

   NOT changed, deliberately: the inline links inside the consent copy
   (96 x 17) and the four field labels (317 x 20). WCAG 2.5.5 exempts a target
   in a sentence of text, and padding those to 44px would break the paragraph
   they sit in. The field labels are not targets in their own right -- they
   focus an input that is itself 44px after this block.

   Guard: tests/test_layout_brand.py, marker `G27:`. */
@media (max-width: 860px) {
  .candidate-page input[type=text], .candidate-page input[type=email],
  .candidate-page input[type=password], .candidate-page input[type=number],
  .candidate-page input[type=search], .candidate-page input[type=date],
  .candidate-page input[type=tel], .candidate-page select,
  .candidate-page textarea, .candidate-page input[type=file] {
    font-size: 16px; min-height: 44px; box-sizing: border-box;
  }
  .candidate-page textarea { min-height: 120px; }
  /* The label IS the tap target. Centred rather than flex-start so the box
     sits against the first line of a two-line consent sentence. */
  .candidate-page label.check, .candidate-page .consent-check {
    min-height: 44px; padding: var(--s2) 0; align-items: center;
  }
  .candidate-page label.check input[type=checkbox],
  .candidate-page .consent-check input[type=checkbox] {
    width: 24px; height: 24px; flex: 0 0 auto; margin: 0; accent-color: var(--accent);
  }
  .candidate-page .jd-details > summary {
    min-height: 44px; display: flex; align-items: center;
  }
}

/* ── `/i/<token>` — the four numbered steps (owner design handoff, 2026-08-05) ─
   The candidate interview landing rendered as ONE undifferentiated column:
   seven "how it works" bullets, two consent blocks and the eligibility form all
   styled as prose, with duration / browser / recording buried mid-list. This
   block is the four-step version -- facts strip, numbered sections, form chrome
   on the declaration panel, device check last, one blue full-width CTA.

   FOUR THINGS ABOUT THIS BLOCK ARE DELIBERATE AND ARE NOT STYLE PREFERENCES:

   1. EVERY RULE IS `ivl-`-PREFIXED OR `body[data-page]`-SCOPED. `iv-q` was the
      first name reached for, and it is ALREADY IN USE, unstyled, by three
      recruiter-side templates (`_assess_interview.html`,
      `_interview_settings.html`, `_new_role_modal.html`) -- defining it here
      would have silently put a bordered panel and 24px margins on the interview
      template editor. That is exactly CLAUDE.md layout rule 5: a correct rule
      surviving into a container it was never measured in, which is what six of
      the last nine alignment defects actually were. `ivl-` is unused anywhere.
   2. NO NEW OFF-SCALE SPACING AND NO NEW DRIFT PRIMITIVE. `check_tokens.py`
      8.13 was at 668/668 and 61/61 -- ZERO headroom on both -- when this was
      written, so every margin/padding/gap below is on the 4px scale
      (0/4/8/12/16/24/32/48/64) and nothing here uses `space-between`,
      `margin-left: auto` or `margin: … auto`. The handoff specified 40px card
      padding, 14px facts padding, 10px gaps, 6px select offsets, 3px label
      offsets and a 20px panel gap; NONE of those numbers is here, for the same
      reason the UX-53 board block spends nothing (see tests/layout_ratchets.txt).
      The card is centred by `.candidate-wrap`'s EXISTING `margin: … auto`, which
      is already counted -- this block adds no second one.
   3. NO DARK THEME. `candidate.css` carries zero `data-theme` rules: the
      candidate pages are light-only today, the handoff is light-only, and
      inventing a dark variant here would be a product decision nobody made.
      Checked before writing, not assumed.
   4. ONE COLOUR IN THE HANDOFF HAS NO EXACT TOKEN and it is the desk behind the
      card: `#f2f3f5`. `--neutral-tint` is `#f1f2f4`, one value away in two
      channels and imperceptible, so it is used rather than minting a token that
      would then have to be kept in lock-step across both sheets for a difference
      no eye can see. Every other value in the handoff's token table IS an exact
      existing token (`--canvas` #fafafa, `--line` #e5e6ea, `--line-soft`
      #eef0f2, `--ink`, `--muted`, `--faint`, `--accent`, `--accent-d`,
      `--ok-ink`, `--bad-ink`, `--hover`). No hex appears below. */
body[data-page="iv-landing"] { background: var(--neutral-tint); }
/* `.candidate-wrap.ivl-land` IS DELETED (owner handoff, 2026-09-06). It widened
   this page's column to 760px for a three-cell facts strip and 44px radio rows,
   both of which are gone -- the invitation is `.narrow`, the same 560px column
   every other candidate dead end and confirmation uses. One width for the whole
   journey is what the handoff's shared card is for. */
/* The card becomes a real bordered surface again. `.candidate-card` above is the
   FLAT continuous-column variant (transparent, hairline `border-bottom` only),
   which is right for the run/done pages and wrong for a page the candidate has
   to read, fill in and commit from. The `border` shorthand resets that
   `border-bottom`, and this selector out-specifies `.candidate-card:last-child`. */
body[data-page="iv-landing"] .candidate-card {
  padding: 32px 32px 48px;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-lg);
}
.ivl-eyebrow { margin: 0 0 4px; }
.ivl-h1 { margin: 0 0 4px; font-size: 21px; }
/* `text-wrap: pretty` so the greeting does not leave one word on its own line. */
.ivl-lead { margin: 4px 0 16px; text-wrap: pretty; }

/* `.ivl-facts` / `.ivl-fact*` ARE DELETED (owner handoff, 2026-09-06). The
   strip carried duration, per-answer window and browser requirement in three
   bordered cells. The two facts a candidate DECIDES on -- how many questions and
   how long -- are one sentence in the lead now; the per-answer window went
   because it is a countdown once an answer starts and a candidate who has not
   started cannot act on it. Both surviving numbers are still DERIVED, which was
   this block's own rule and the reason it existed. */

/* THE SECTIONS, AND THERE ARE TWO. It was a numbered sequence -- `.ivl-step`
   owning the rhythm so no step could drift -- with headings counting the reader
   through four blocks. The headings are gone with the numbering (owner handoff),
   and what is left is a hairline between the consent and the start, which is the
   only division the page still makes.

   `.ivl-step` KEEPS ITS NAME AND ITS RHYTHM, renamed only in what it is used
   for: `.ivl-sect` is the same 24px-above-a-hairline rule. `.ivl-orient`,
   `.ivl-step-h`, `.ivl-step-cta` and `.ivl-quote` are DELETED with the blocks
   they laid out -- the what-happens list, the step headings, the CTA's own
   section and the hiring-team message. */
.ivl-sect { padding: var(--s5) 0 0; border-top: 1px solid var(--line); }
/* THE CONSENT NEEDS AIR ON BOTH SIDES (owner, 2026-09-09: the spacing "looks
   off ... for 'I agree to Priv....' and the blue box, and the statement and
   the line below"). Two separate faults, one cause -- the tick had padding
   ABOVE it and none below, so it sat 24px under the panel and 0px above the
   next section's hairline, which then ran directly under the sentence.

   THE RULE BELOW THE STATEMENT IS DELETED rather than spaced. The start button
   is a full-width filled control; a hairline above it divides nothing the
   button has not already divided, and it was the thing touching the sentence.
   Spacing does the work now, which is this page's own idiom above. */
.ivl-consent-sect { padding-bottom: var(--s5); }
.ivl-sect-soft { border-top: 0; padding-top: var(--s4); }
/* THE SOFTER HAIRLINE for the second division, which is the mock's: the consent
   is separated from the page by a full rule, the start from the consent by a
   quieter one. */
.ivl-sect-soft { border-top-color: var(--line-soft); }
/* THE DEVICE CHECK AND ITS ANSWER ON ONE ROW, which is what makes the status
   read as a reply to the button rather than as a new paragraph. It wraps rather
   than truncating: the failure sentence names a browser and is longer than the
   row. */
.ivl-devrow { display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--s3); margin-bottom: var(--s3); }
/* THE DEVICE-CHECK PANEL (owner, layout option B, 2026-09-09). The check
   moved above the consent tick because it is the one step that cannot be taken
   later -- the interview is resume-only, so a dead mic discovered after
   starting is a failed attempt and not a retry.

   `--accent-tint` RATHER THAN A NEW COLOUR. It is the page's own informational
   tint and is already used for the CV drop zone on the sibling apply page, so
   this introduces no token. The heading takes `--accent-d` rather than
   `--accent`: the tint is 1.13:1 on white, and the darker ink is what carries
   the pair over the contrast floor the note beside `--accent-tint` records.

   EVERY VALUE IS ON THE 4px SCALE (`--s3` / `--s4`), because 8.13's off-scale
   spacing counter has zero headroom and may only fall. `--r-sm` is the shared
   control radius; a typed px here would be the per-call-site decision rule 4
   forbids.

   `margin-bottom` AND NOT A SECTION HAIRLINE: `.ivl-sect` draws a rule above
   itself, and the tick's rule immediately below this panel is the division the
   page still makes. A second hairline would have boxed the panel twice. */
.ivl-devbox { background: var(--accent-tint); border-radius: var(--r-sm);
  padding: var(--s4); margin: var(--s4) 0 0; }
.ivl-devbox-h { margin: 0 0 var(--s1); font-size: var(--text-sm);
  font-weight: 600; color: var(--accent-d); }
.ivl-devbox-n { margin: 0 0 var(--s3); font-size: var(--text-sm);
  line-height: 1.5; color: var(--ink); }
/* The row inside the panel carries no bottom margin -- the panel's own padding
   is the gap, and `.ivl-devrow`'s 12px would have doubled it. */
.ivl-devbox .ivl-devrow { margin-bottom: 0; }
/* NO HAIRLINE DIRECTLY UNDER THE PANEL, found by measuring the deployed page:
   `.ivl-sect`'s `border-top` landed at exactly the panel's bottom edge (both at
   y=364), so a 1px rule sat flush against a tinted block and read as a stray
   second border rather than as a division.

   THE PANEL IS THE DIVISION. It is a filled surface; a rule immediately beneath
   it divides nothing that the fill has not already separated, and the section's
   own 24px padding still gives the tick its air.

   SCOPED TO THE ADJACENCY rather than added as a counter-rule -- CLAUDE.md's
   layout rule 5: "The fix is nearly always to scope the rule, not to add a
   counter-rule." Every other `.ivl-sect` keeps its hairline. */
.ivl-devbox + .ivl-sect { border-top: 0; }
/* THE BUTTON ON THE TINT. `.btn.ghost` is transparent with a hairline border,
   which is correct on white and nearly invisible on `--accent-tint` -- the
   owner read it as "hardly readable". It takes the page's own surface colour
   here so it reads as a raised control on the tinted ground, which is what a
   ghost button is doing on white in the first place.

   SCOPED TO THE PANEL, not a new button class: `.btn.ghost` is right
   everywhere else on this page, and a `.btn-on-tint` would be a second
   vocabulary for one placement. */
.ivl-devbox .btn.ghost { background: var(--panel); border-color: var(--line);
  color: var(--ink); font-weight: 600; }
.ivl-devbox .btn.ghost:hover { border-color: var(--accent);
  color: var(--accent-d); }
.ivl-foot { margin: var(--s2) 0 0; text-align: center;
  font-size: var(--text-sm); color: var(--faint); }

/* The ONE required consent tick, placed against the message it agrees to. */
.ivl-consent { display: flex; align-items: flex-start; gap: 12px;
  margin: 16px 0 0; font-weight: 400; font-size: 13.5px; line-height: 1.55;
  color: var(--ink); cursor: pointer; }
.ivl-consent input { width: auto; margin: 4px 0 0; flex-shrink: 0;
  accent-color: var(--accent); }
/* The red asterisk is the ONLY required-affordance on the page -- the "* Required"
   legend was removed during review, so this cannot become decorative. */
.ivl-req { color: var(--bad-ink); font-weight: 600; }

/* `.ivl-what` IS DELETED with the what-happens list (owner handoff). Four
   bullets explaining the flow, on a page whose flow is now: tick, check, start. */

/* THE DECLARATION PANEL IS DELETED WITH THE GATE BEHIND IT (owner ruling,
   2026-09-06): `.ivl-panel`, `.ivl-panel-err`, `.ivl-q`, `.ivl-q-label`,
   `.ivl-q-note`, `.ivl-radios`, `.ivl-radio`, `.ivl-langs`, `.ivl-lang` and
   `.ivl-select`. They laid out the work-authorisation radios and the per-language
   selects, which `app.iv_accept` no longer asks for -- see the record there for
   why the UI and the server half had to go together.

   THE DEVICE CHECK KEPT ITS OWN RULE and did not live in that panel's frame,
   which is why it survives below intact. */

.ivl-devstatus { margin: 8px 0 0; font-size: var(--text-sm); font-weight: 600; }
/* `.devok` / `.devbad` are defined ABOVE this block, so a bare `.ivl-devstatus`
   colour would out-order them. Paired selectors instead. `--bad-ink` rather than
   `.devbad`'s `--bad`: the handoff's error colour, and the darker of the two. */
.ivl-devstatus.devok { color: var(--ok-ink); }
.ivl-devstatus.devbad { color: var(--bad-ink); }

/* THE CTA. The only primary action on the page, so it is the brand accent and
   48px rather than the candidate 44px floor. `.candidate-page .btn` sets that
   floor at (0,2,0), so this pair has to out-specify it. */
.candidate-page .btn.ivl-cta { min-height: 48px; font-size: var(--text-md);
  font-weight: 600; }

/* Mobile. Same two floors as the block above -- 44px tap targets and the 16px
   iOS input-zoom precondition (which the shared `select` arm above already
   covers). The card loses its side padding to the gutters. */
@media (max-width: 860px) {
  body[data-page="iv-landing"] .candidate-card { padding: 24px 16px 32px; }
  .ivl-consent { min-height: 44px; }
  .ivl-consent input[type=checkbox] { width: 24px; height: 24px; flex: 0 0 auto;
    margin: 0; }
  .ivl-select { max-width: 100%; }
}

/* THE WIDE-DESKTOP GRID IS DELETED (owner handoff, 2026-09-06), and the
   `.ivl-land` width it opened up with it. It put a reading column beside a 340px
   orientation rail at 1280px and above, so the 424px that was empty beside the
   text carried the facts strip and the what-happens list instead. Both are gone,
   the card is one column at every width, and a grid with one thing in it is not
   a grid.

   `UX-56 WIDE` MEASURED THIS BLOCK at 1279 / 1280 / 1920 and its subject is
   gone. It is converted rather than deleted, because the property underneath it
   -- the CTA is this page's only primary action and spans its column -- is still
   true and still worth measuring.

   WHAT THE OLD COMMENT RECORDED AND IS WORTH KEEPING: the grid lived on the CARD
   rather than on each item, which is rule 2 of the layout contract, and the only
   spacing it added was a 48px column gap on the 4px scale. Any future two-column
   pass on a candidate page should start from that shape. */


/* ===========================================================================
   S1 -- THE CANDIDATE BOOKING PAGE (owner handoff `design_handoff_scheduling_hub`)
   ===========================================================================
   MOBILE-FIRST, and this is the only surface in the product that is: 360px is a
   real width for a candidate opening an email on a phone, and the handoff
   requires it. The slot grid is four day columns, dropping to two and then one.

   SPACING IS `var(--sN)`. check_tokens 8.13 counts BOTH stylesheets and its
   off-scale ratchet may only FALL, so the handoff's 30/32/20/9px margins round
   to the 4px scale. Sizes, radii and font sizes are exact.

   WHAT IT REPLACES: a `.grid2` of `.check` radio labels whose visible text was
   the raw UTC instant until a script rewrote it, with no statement anywhere of
   what the interview WAS. */

/* ---- APPLY: read on the left, do on the right ----------------------------
   Owner handoff `design_handoff_candidate_pages`, 2026-09-06. Three stacked
   cards in a 620px column became one card with two columns: the role and its
   description on the left, the form as a panel on the right.

   THE COLUMNS ARE A WRAPPING FLEX ROW, not a grid, and that is the one place
   this sheet departs from layout rule 2 on purpose. Both columns need to be
   fluid AND to have a preferred width they wrap at, which is `flex-basis`'s
   whole job; a grid would need a breakpoint to say the same thing, and the
   handoff's own rule is that the row wraps "below ~1000px" without one. No
   `space-between` and no `margin: auto` -- the gap does the spacing, which is
   what the drift ratchet is about.

   THE FORM COLUMN IS STICKY, and it is inert once the row has wrapped: a
   full-width column has nothing to stick beside. That is why there is no query
   turning it off.

   THE LANGUAGE SWITCHER IS DELETED with the brand line that carried it. It is
   the only candidate surface that offered one, and `set_language` is still a
   route -- what went is this page's control, not the capability. */
/* WIDER ON A DESKTOP, 1120 -> 1320 (owner, 2026-09-07: the page took about
   half the screen). The whole 200px goes to the READING column, because
   `.ap-formcol` keeps its 440px cap -- a form panel that grows with the window
   just puts more air around the same four fields, while a job description is
   the thing there is more of. On a 2133px monitor that is the card going 52% ->
   62% of the screen with the description gaining ~9% of it, which is what was
   asked for. The measure below had to move WITH it or the 200px would have
   landed as white space. */
.candidate-wrap.ap-wrap { max-width: 1320px; }
/* NO BESPOKE CARD CLASS. `check_tokens` 8.5 keeps a may-not-grow register of
   card classes and `.candidate-card` is already in it, so this scopes on the
   PAGE -- the same idiom `body[data-page="iv-landing"]` uses one block down. */
body[data-page="apply"] .candidate-card { display: flex; flex-direction: column;
  gap: var(--s6); padding: var(--s7); }
.ap-brand { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }
.ap-brand-div { width: 1px; height: 20px; background: var(--line); display: block; }
/* The `.wordmark` component is 18px in the product rail; this page is a
   candidate's first sight of the brand, so it is set a little larger here.
   Sizing is the ONLY thing overridden -- the letterforms, the blue `q` and the
   checkmark come from style.css, so the two surfaces cannot drift. */
.ap-brand .wordmark { font-size: 22px; }
/* The company's own mark, optically matched to the wordmark's cap height
   rather than to its font-size. It is a fixed asset in Fano Blue: candidate.css
   has NO dark theme (see the note at the palette), so there is no second
   pairing to get right. */
.ap-cologo { height: 19px; width: auto; display: block; flex: 0 0 auto; }
.ap-company { font-size: 17px; font-weight: 600; letter-spacing: -.01em; }
.ap-cols { display: flex; flex-wrap: wrap; gap: var(--s7); align-items: flex-start; }
/* `min-width: 0` ON BOTH, or a long unbroken word in the job description makes
   the reading column refuse to shrink and pushes the form off the card. */
.ap-read { flex: 1 1 420px; min-width: 0; display: flex; flex-direction: column;
  gap: var(--s5); }
.ap-formcol { flex: 1 1 400px; max-width: 440px; min-width: 0;
  position: sticky; top: var(--s5); }
/* `text-wrap: pretty` so a long role title does not leave one word on its own
   line, which is what the h1 on this page does more than any other. */
.ap-h1 { margin: 0; font-size: 32px; font-weight: 650; letter-spacing: -.018em;
  text-wrap: pretty; }
.ap-meta { margin: var(--s2) 0 0; display: flex; flex-wrap: wrap;
  align-items: baseline; gap: var(--s3); font-size: 16px; color: var(--muted);
  line-height: 1.5; }
.ap-sep { color: var(--line); }
.ap-jd { border-top: 1px solid var(--line); padding-top: var(--s5); }
/* THE JOB DESCRIPTION IS OPEN PROSE AND IT DOES NOT SCROLL. It was
   `.prose-scroll` inside a `<details>`; a description behind a disclosure AND
   inside its own scroller is two things between a candidate and the thing they
   are deciding about. `65ch` is a reading measure, not a pixel target. */
/* THE MEASURE MOVES WITH THE COLUMN, and this is the trap the width change
   would otherwise have fallen into: `max-width: 65ch` caps the text at about
   490px, so widening the column to 690px would have added 200px of WHITE SPACE
   and left the description exactly as it was. 16px/78ch reads at roughly 620px
   -- larger type as well as a wider column, which is the owner's word
   ("larger"), and still inside a comfortable reading measure. */
.ap-prose { font-size: 16px; line-height: 1.7; color: var(--muted);
  white-space: pre-wrap; max-width: 78ch; }
/* THE MICRO-CAP, one rule for every label on this page -- the section headings
   and the field labels are the same thing at the same size, which is what makes
   the form panel read as one list rather than as a stack of blocks. */
.ap-cap { margin: 0 0 var(--s2); font-size: 12px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .08em; color: var(--faint); }
.ap-cap-2 { margin-top: var(--s5); }
.ap-cap-sub { font-weight: 400; text-transform: none; letter-spacing: 0; }
.ap-lbl { display: block; margin-bottom: var(--s2); }
.ap-opt { font-weight: 400; text-transform: none; letter-spacing: 0; }
.ap-req { color: var(--bad-ink); font-weight: 600; }

/* THE FORM AS A PANEL, on `--canvas` so it reads as the thing to fill in rather
   than as more page. A MODIFIER, not a new card class: `check_tokens` 8.5 keeps
   a may-not-grow register and `.candidate-card` is already in it. */
.ap-panel { background: var(--canvas); border: 1px solid var(--line);
  border-radius: var(--r-lg); padding: var(--s6);
  display: flex; flex-direction: column; gap: var(--s5); }
.ap-panel-t { margin: 0; font-size: 17px; font-weight: 650; letter-spacing: -.012em; }
/* `auto-fit` + `minmax`, so the four identity fields are ONE column in the
   panel and TWO the moment the panel is wider -- no query, and the same rule
   serves the language selects below. */
.ap-fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--s4); }
.ap-fields-2 { margin-top: var(--s4); }
.ap-sect { border-top: 1px solid var(--line-soft); padding-top: var(--s5); }
.ap-q { margin: 0 0 var(--s3); font-size: 14px; font-weight: 600; color: var(--ink); }
.ap-radios { display: flex; flex-wrap: wrap; gap: var(--s2); }
/* THE ROWS ARE THE HIT TARGET, 48px, and the real radio stays visible -- so
   `:focus-within` is a SECOND indicator on the row rather than the only one. */
.ap-radio { flex: 1 1 180px; display: flex; align-items: center; gap: var(--s3);
  min-height: 48px; padding: var(--s2) var(--s4); border: 1px solid var(--line);
  border-radius: var(--r-md); font-size: 14px; cursor: pointer;
  transition: border-color .15s ease, background .15s ease; }
.ap-radio:hover { background: var(--panel); }
.ap-radio:focus-within { border-color: var(--ring); }
.ap-radio input { width: auto; margin: 0; accent-color: var(--accent); }
/* The help line under a field group -- today the fluency gloss, which is the
   half of the one-word ladder that DEFINES the words. See the template. */
.ap-hint { margin: var(--s3) 0 0; font-size: 12px; line-height: 1.5;
  color: var(--faint); }
.ap-points { margin: 0 0 var(--s3); padding-left: var(--s5); list-style: disc;
  color: var(--muted); font-size: 13px; line-height: 1.55;
  display: flex; flex-direction: column; gap: var(--s1); }
/* 44px MINIMUM on a consent row, because it is a tap target on a phone and the
   two on this page are the most legally significant controls it has. */
.ap-check { display: flex; gap: var(--s3); align-items: flex-start;
  min-height: 44px; font-size: 13px; line-height: 1.55; cursor: pointer;
  color: var(--muted); }
.ap-check input { width: 22px; height: 22px; flex: 0 0 auto; margin: 0;
  accent-color: var(--accent); }
.ap-check + .ap-check { margin-top: var(--s2); }
/* `.ap-note` and `.ap-check-sep` were DELETED on 2026-09-07 with the markup
   that called them, one day after both arrived. The optional-and-never-scored
   promise went because the section stopped being optional -- both Yes/No
   questions are mandatory now -- and the hairline went with the required TICK
   it separated, which is a Yes/No question today. Neither is orphaned; the
   sentence's guarantee is still stated on `/privacy`, which is the document
   that carries it. */
/* The second and subsequent question groups in a section. `.ap-cap-2`'s twin,
   at question weight rather than caption weight. */
.ap-q-2 { margin-top: var(--s5); }
.candidate-page .btn.ap-submit { min-height: 60px; border-radius: var(--r-lg);
  font-size: 16px; font-weight: 600; }

/* ---- THE DEAD END: one shape for all eleven ------------------------------
   Owner handoff `design_handoff_candidate_pages`, 2026-09-06. Every invalid,
   expired, closed, throttled and error page was its own arrangement of a status
   badge, a headline, one or two paragraphs and a button. They are ONE card now:
   headline, ONE line, at most one action.

   A MODIFIER ON THE SANCTIONED CARD, not a new one -- `check_tokens` 8.5 keeps a
   may-not-grow register of card CLASSES and `.candidate-card` is already in it,
   so a `*-card` of its own for a padding and a gap would be the wrong fix. Same
   reasoning `.bk-shell` records one block up.

   THE BADGE CIRCLES ARE GONE FROM THESE PAGES and the class is NOT deleted:
   `interest_done` and `interview_done` still use it, and both are out of this
   handoff's scope by the owner's own cut.

   THE HEADING STAYS AN `<h1>` while the mock draws an `<h2>`. The mock TILES the
   eleven states side by side for review, where they are sections of one page; in
   production each is the whole page and its heading is that page's first-level
   one. The SIZE is the mock's -- 19px -- which is what the design is actually
   about. */
.candidate-card.cend { display: flex; flex-direction: column;
  align-items: flex-start; gap: var(--s3); padding: var(--s6); }
.cend h1 { margin: 0; font-size: 19px; font-weight: 650; letter-spacing: -.012em; }
/* ONE LINE, and the rule says so by styling `p` rather than a class: a second
   paragraph added here inherits the same treatment and is visibly a second
   paragraph, which is the thing the handoff cut from 413 and 500. */
.cend p { margin: 0; font-size: 14px; color: var(--muted); line-height: 1.6; }
.cend .cend-ref { font-size: 13px; color: var(--faint); }
.cend .cend-ref code { font-family: var(--font-mono); }

/* ---- APPLICATION RECEIVED: the same card, one size up --------------------
   40px of padding against the dead ends' 32, because this is the end of a
   journey somebody chose rather than a wall they hit. The success badge SHRANK
   (56px with a 30px glyph -> 40px with a 19px one): it was the loudest thing on
   a page whose news is one sentence. */
/* `var(--s7)` AND NOT THE MOCK'S 40px. `check_tokens` 8.13 counts BOTH sheets
   and its off-scale ratchet has ZERO slack and may only FALL -- a bare 40 raised
   it by one and the guard refused the commit. Rounding to the nearest token is
   the precedent this sheet already set for the booking block's 30/32/20/9px
   margins, and it keeps the "one size up from a dead end" intent: 48 against the
   error card's 32. */
.candidate-card.cdone { display: flex; flex-direction: column;
  align-items: flex-start; gap: var(--s4); padding: var(--s7); }
.cdone-badge { width: 40px; height: 40px; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 19px; background: var(--ok-tint); color: var(--ok-ink); }
.cdone h1 { margin: 0; font-size: 26px; font-weight: 650; letter-spacing: -.018em; }
.cdone-lead { margin: 0; font-size: 16px; color: var(--muted); line-height: 1.6; }
.cdone-lead strong { color: var(--ink); }
/* THE TIMING LIVES IN THE LEAD SENTENCE NOW, so the `.next-steps` panel that
   carried it is deleted from this page. The hairline below is what separates the
   news from the way to ask about it. */
.cdone-foot { margin: 0; font-size: 13px; color: var(--faint);
  border-top: 1px solid var(--line-soft); padding-top: var(--s4);
  align-self: stretch; }

/* A MODIFIER ON THE SANCTIONED , not a new card. Deliberately
   NOT named `*-card`: check_tokens 8.5 keeps a may-not-grow register of card
   CLASSES, and `.candidate-card` is already in it -- widening that set for a
   width and a gap would be the wrong fix, exactly as it was for `.sch-flush`. */
.bk-shell { max-width: 560px; border-radius: 14px; padding: var(--s6) var(--s6) var(--s6);
  display: flex; flex-direction: column; gap: var(--s4); }
/* THE ORG NAME AS AN EYEBROW, because a candidate opening a link from an email
   needs to know who it is from before anything else on the page. */
.bk-eyebrow { font-size: 12px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .06em; color: var(--faint); }
.bk-h { margin: 0; font-size: 22px; font-weight: 650; letter-spacing: -.018em;
  color: var(--ink); }

/* THE CONTEXT LINE (owner mockup, 2026-09-06). `.bk-facts` -- three labelled
   cells in a bordered box -- is DELETED, and it had itself replaced a run-on
   line at the 2026-09-03 handoff on the argument that a candidate scanning for
   the DURATION had to read the whole sentence. The owner has asked for the line
   back, so the answer to that argument moves into the TYPE rather than into
   labels: the role carries the weight, the rest is muted, and each fact is its
   own element with a middot between, so the line scans as four things instead of
   as prose. `.bk-slots-head` / `.bk-slots-t` / `.bk-slots-tz` go with it -- the
   instruction is what the layout now says by being a calendar beside a list of
   times, and the zone moved under the Confirm button as `.bk-tznote`.

   IT WRAPS RATHER THAN TRUNCATING. A role title is as long as somebody typed
   it, and the middots are separate elements so a wrap breaks between facts. */
.bk-meta { margin: 0; display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--s2); font-size: 15px; color: var(--muted); line-height: 1.5; }
.bk-meta-role { font-weight: 600; color: var(--ink); overflow-wrap: anywhere; }
.bk-meta-sep { color: var(--line); }

/* ---- the SCRIPTLESS flat list ------------------------------------------
   FOUR COLUMNS, and it is the fallback rather than the design now: with
   scripting off every offered instant is a chip here, labelled with its UTC
   time. `static/book_slots.js` hides this block once it has built the calendar
   below, and moves these same cells into it. */
.bk-slots { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--s2); }
.bk-slots[hidden] { display: none; }
/* `.bk-day` was the four-column grid's per-column heading. The calendar names
   the chosen day once, in `.bk-times-head`, so a heading per column has nothing
   left to label -- but the class is KEPT because the flat fallback is still
   rendered and a future grouping of it would want it back. */
.bk-day { font-size: 11.5px; font-weight: 600; color: var(--faint);
  text-align: center; }

/* ---- the CALENDAR: a month, then that day's times ----------------------
   TWO COLUMNS ON A 560px CARD: the month is `max-content` so its seven squares
   size themselves, and the times column takes the rest. A candidate picks a DAY
   before a time, which is why the day control comes first in both the visual
   order and the DOM.

   IT SHIPS `hidden` AND THE SCRIPT REVEALS IT -- see `templates/book.html`.
   Only the browser knows the reader's zone, so only the browser can say which
   local day a UTC instant falls on, so only the browser can draw this. */
/* THE CALENDAR IS THE FLUID COLUMN NOW AND THE TIMES ARE THE FIXED ONE, which
   is the reverse of what it was and is what the mockup shows. A month of seven
   fluid squares spends whatever the card has; a column of time chips does not
   get better past about 220px a chip, so pinning it is what stops two columns of
   "9:00 AM" stretching into buttons that read as bars. */
/* THE TWO COLUMNS ARE THE SAME HEIGHT, and that is what anchors the Confirm
   button (owner, 2026-09-06: *"I want this piece to align with the bottom of the
   calendar, no matter how many slots are offered, so it won't move up and down
   with number of slots"*). It was `align-items: start`, so the times column was
   only as tall as its content and the button rode up and down with the day's
   offer count -- a control that moves under the reader's cursor between one day
   and the next.

   `stretch` ALONE IS NOT THE FIX; the slack has to have somewhere to go. The
   times LIST takes it (`flex: 1` below), so the button is pushed to the bottom
   by a real box rather than by `margin-top: auto` -- which would be the fourth
   spelling of the drift primitive the 8.13 ratchet counts down, and would leave
   the list free to overflow the column instead of scrolling inside it. */
.bk-picker { display: grid; grid-template-columns: minmax(0, 1fr) var(--bk-times-col);
  gap: var(--s6); align-items: stretch; }
.bk-picker[hidden] { display: none; }
.bk-cal-wrap { display: flex; flex-direction: column; gap: var(--s2);
  min-width: 0; }
/* THE MONTH BAR IS A THREE-TRACK GRID, not `space-between`: the label sits in a
   fluid middle track so the two arrows hold their positions as the month name
   changes width, which it does on every step. */
/* THE MONTH READS FIRST AND THE STEPPER SITS AT THE FAR END. It was
   arrow / label / arrow, which put the month's name in a fluid middle track that
   re-centred as the name changed width on every step; a heading at the start of
   its own row does not move at all. The two arrows are adjacent because they are
   one control, and `1fr` between them is what pushes the pair to the end without
   `space-between` -- which is one of the three spellings the drift ratchet
   counts down. */
.bk-cal-bar { display: grid;
  grid-template-columns: max-content minmax(0, 1fr) max-content;
  gap: var(--s2); align-items: center; }
.bk-cal-navs { grid-column: 3; display: flex; gap: var(--s2); }
.bk-cal-month { font-size: 20px; font-weight: 650; color: var(--ink);
  letter-spacing: -.012em; }
.bk-cal-nav { border: 1px solid var(--line); border-radius: 10px;
  background: var(--panel); color: var(--ink); cursor: pointer;
  width: 40px; height: 40px; font-size: 16px; line-height: 1; padding: 0; }
.bk-cal-nav:hover:not(:disabled) { border-color: var(--ink); }
/* DISABLED, NOT HIDDEN, at the ends of the range -- a stepper that vanishes
   moves the other control under the reader's cursor. */
.bk-cal-nav:disabled { opacity: .4; cursor: default; }
.bk-cal-nav:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
/* SEVEN FLUID COLUMNS. It was `repeat(7, 34px)`, which sized the month to the
   squares and left the rest of the column empty however wide the card got --
   the defect the owner reported as "it does not utilize the screen space". Now
   the squares size to the month. */
.bk-cal { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: var(--s2); }
.bk-cal-dow { font-size: 12px; font-weight: 600; color: var(--faint);
  text-align: center; padding-bottom: var(--s1); }
.bk-cal-pad { display: block; }
/* EVERY SQUARE IS A BUTTON, including the days with nothing to offer -- they are
   `disabled` rather than omitted, so the month keeps its shape and assistive
   tech is told they are not choices. */
/* A ROUNDED RECT, NOT A CIRCLE, and it fills its track. A circle has to be as
   wide as it is tall, so a fluid seven-column month would have grown 80px-tall
   rows of empty space; a rect takes the width and keeps its own height. */
.bk-cal-day { display: flex; align-items: center; justify-content: center;
  width: 100%; height: 56px; padding: 0; border-radius: 12px;
  border: 1px solid transparent; background: none; cursor: pointer;
  font: inherit; font-size: 15px; color: var(--ink);
  font-variant-numeric: tabular-nums; }
.bk-cal-day:hover:not(:disabled) { border-color: var(--ink); }
/* A BOOKABLE DAY IS TINTED AND AN EMPTY ONE IS FAINT. The tint is the signal and
   the `aria-label` carries the count, because a colour is not something a screen
   reader has -- see the script. */
.bk-cal-day:not(:disabled) { background: var(--cal-offered); font-weight: 600; }
.bk-cal-day:disabled { color: var(--faint); cursor: default; }
.bk-cal-day[aria-current="date"] { background: var(--ink); color: var(--panel);
  border-color: var(--ink); }
.bk-cal-day:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
/* `min-height: 0` IS LOAD-BEARING, not defensive. A flex item's default
   `min-height: auto` refuses to shrink below its content, so without it the
   times list would push this column taller than the calendar the moment a day
   had more offers than the space allows -- which is the exact case the anchor is
   for. */
.bk-times-wrap { display: flex; flex-direction: column; gap: var(--s2);
  min-width: 0; min-height: 0; }
.bk-times-head { font-size: 17px; font-weight: 650; color: var(--ink);
  letter-spacing: -.01em; }
/* HOW MANY, UNDER THE DAY. Its own line rather than a parenthetical on the
   heading, so the heading stays one short phrase at any count. It sits directly
   under the heading, which is why the wrap's own gap is closed here. */
.bk-times-sub { margin-top: calc(var(--s2) * -1); font-size: 13px;
  color: var(--faint); }
/* ONE COLUMN OF TIMES, and it SCROLLS rather than growing the card: a day with
   twenty offers must not push the CTA off the screen on a phone. */
/* TWO COLUMNS IN A PINNED COLUMN. The chips are the mockup's size -- big enough
   to read a time at a glance and to hit on a touch screen -- and the cap is what
   keeps the Confirm button under them on screen rather than below the fold on a
   day with many offers. `BOOK FACTS` COUNTS how many fit and prints it; do not
   write that number into this comment, which is how the last one rotted. */
/* THE LIST TAKES THE COLUMN'S SLACK, which is what puts the Confirm button on
   the calendar's bottom edge whatever the day offers. `flex: 1` with
   `min-height: 0` means: grow into the space the calendar defines, and scroll
   inside it rather than growing the column. A day with three offers leaves the
   space empty above the button; a day with twenty scrolls. Either way the button
   does not move.

   THE `max-height` IS GONE AND IS NOT MISSED: the calendar's own height is the
   cap now, which is a better one because it is the thing the column is being
   aligned to. `BOOK FACTS` counts how many chips fit and prints it; do not write
   that number into this comment, which is how the last one rotted. */
.bk-times { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s2); flex: 1 1 auto; min-height: 0; align-content: start;
  overflow-y: auto; }
.bk-cell { display: block; min-width: 0; }
.bk-cell[hidden] { display: none; }
/* A REAL RADIO, off-flow: the value posts, the keyboard reaches it, and a screen
   reader is told it is one choice among many. The chip is its label. */
.bk-radio { position: absolute; opacity: 0; width: 0; height: 0; }
.bk-chip { display: block; text-align: center; cursor: pointer;
  border: 1px solid var(--line); border-radius: 12px; padding: var(--s3) 0;
  font-size: 15px; font-weight: 500; color: var(--ink);
  font-variant-numeric: tabular-nums; }
.bk-chip:hover { border-color: var(--ink); }
.bk-radio:checked + .bk-chip { background: var(--ink); color: var(--panel);
  border-color: var(--ink); font-weight: 600; }
/* THE RING IS NOT REMOVED AND NOT NARROWED. This is a public page a candidate may
   be operating entirely from the keyboard. */
.bk-radio:focus-visible + .bk-chip { outline: 2px solid var(--ring);
  outline-offset: 2px; }
/* AN EMPTY CELL IS A DASHED DASH, so a short day reads as "nothing here" rather
   than letting the next day's chip slide up into its place. `aria-hidden` on the
   element, because it is not a choice. */
.bk-blank { border: 1px dashed var(--line); border-radius: 8px;
  padding: var(--s2) 0; text-align: center; font-size: 13px; color: var(--faint); }
/* `.bk-more` IS DELETED with `Show more times`. It revealed the tail of the
   four-column grid, which showed three chips of four days; the calendar shows
   every offered day in the month, so there is no tail. */

.bk-label { font-size: 12.5px; font-weight: 600; color: var(--ink); }
/* `.bk-note` IS DELETED WITH THE FIELD IT ROUNDED (owner, 2026-09-06). It had
   exactly one call site, the accommodation textarea, and that field was rendered
   and read by nothing -- `book_submit` takes `start` and the zone, so whatever a
   candidate typed was discarded on submit. `.bk-label` STAYS: the slot fieldset's
   own legend uses it. */

/* THE CONFIRM BLOCK: the button and the zone note, one unit, because the script
   MOVES them together into the times column and a lone button would leave the
   note stranded above the flat list. `margin-top: auto` is deliberately NOT here
   -- the times list has its own cap, so the button follows it rather than being
   pushed to the bottom of a column whose height the calendar decides. */
.bk-confirm { display: flex; flex-direction: column; gap: var(--s2);
  padding-top: var(--s2); }
/* THE CTA IS 44px MINIMUM, which is the touch-target floor and the reason this
   is the one full-width button in the product. */
.bk-cta { width: 100%; min-height: 52px; border-radius: 12px;
  font-size: 15px; font-weight: 600; }
/* THE ZONE IS NAMED, and it wraps rather than truncating: "Asia/Ho_Chi_Minh" is
   longer than the column and a clipped zone name is worse than a second line. It
   is centred under the button because it is a footnote to that choice. */
.bk-tznote { text-align: center; font-size: 13px; color: var(--faint);
  overflow-wrap: anywhere; }
.bk-foot { margin: 0; text-align: center; font-size: 12px; color: var(--faint);
  line-height: 1.5; }
/* `.bk-next` / `.bk-step` / `.bk-step-n` ARE DELETED (owner, 2026-09-06). They
   were three numbered facts under a hairline saying what happens after Confirm.
   Every claim was true at HEAD and the mechanisms behind them are untouched --
   the Graph event, `scheduling.confirmation_message` and the day-before sweep in
   `maybe_enqueue_reminders` all still run. What went is the page saying so
   before the candidate has booked; `book_done.html` is where they land next and
   is the surface that tells them. `.bk-foot` still stays: it is the support
   address on the BLOCKED branch, where there is no booking for anything to
   happen after. */
/* THE BLOCKER SENTENCE. `blocker.candidate` from the shared S2 map -- never the
   code, and never a grid beside it. Left-aligned, unlike `.bk-foot`: it is a
   paragraph the candidate has to read and act on, not a footnote. */
.bk-why { margin: 0; font-size: 13.5px; color: var(--ink); line-height: 1.6; }

/* ---- DESKTOP: the card widens and the picker spends the room -------------
   Owner, 2026-09-06: *"now in a desktop setting, it does not utilize the screen
   space properly"*. Measured before: a 560px card in a 1920px viewport, holding
   a 262px calendar of 34px squares beside a 170px column of times capped at
   320px of scroll -- so a candidate on a laptop read a phone layout with 1360px
   of empty page beside it.

   THE UPWARD BREAKPOINT IS THE SAME INSTRUMENT THE DOWNWARD ONES ARE, for the
   reason stated below them: this card's width is a pure function of the viewport
   -- `max-width` inside a fluid wrap with no other parent -- which is exactly
   the condition UX-121 records for when a breakpoint is right and measuring is
   not needed instead.

   SCOPED TO `[data-page="book"]`, WHICH IS NOT DECORATION. `.candidate-wrap.narrow`
   is shared with 404, 413, 500, apply_done, apply_invalid, book_done and
   book_invalid -- all of them single short paragraphs that a 560px column is
   CORRECT for. Widening the shared class would have stretched seven pages of
   prose to 880px to fix one calendar.

   THE TIMES GO TO THREE COLUMNS AND THE CAP RISES, and that pair is the answer
   to "what if there are ten offers rather than three": MEASURED on the
   deployment, eighteen chips are readable before anything scrolls, against four
   in the old single column at 320px. Do not trust that figure from this comment -- it is
   derived by `BOOK FACTS`, which fills the list until it scrolls and prints what
   it found; the first draft of this block computed eighteen from a guessed chip
   height and was wrong by half. The cap is not REMOVED, because the scroll is
   what keeps the CTA on screen -- the rule below `.bk-times` states that, and it
   is as true on a laptop as on a phone; it is raised so the scroll is a rare
   case instead of the normal one. */
@media (min-width: 900px) {
  /* BOTH CAPS, AND MISSING THE SECOND ONE IS WHAT SHIPPED FIRST. `.bk-shell` --
     the card the reader actually sees -- carries a `max-width` of its own, so
     widening only the WRAP moved the padding box around an unchanged 560px card
     and the page looked exactly as it had. The guard passed because it measured
     the WRAP. Two elements, two caps, one media query, and `BOOK FACTS` now
     measures the card and the picker inside it. */
  .candidate-page[data-page="book"] .candidate-wrap.narrow { max-width: 1120px; }
  .bk-shell { max-width: 1120px; padding: var(--s7) var(--s7); gap: var(--s6); }

  /* THE TIMES COLUMN IS SIZED BY THE CALENDAR, NOT BY ITS OWN CONTENT, and this
     pair of declarations is the whole mechanism. `align-items: stretch` alone was
     NOT enough and the guard caught it: a grid row is as tall as its TALLEST
     item, so a day with twenty offers grew the row, stretched the calendar to
     match, and moved the button 938px down the page -- the exact complaint,
     unfixed, with the alignment arm still passing because both columns ended
     together at the new height.

     `height: 0` MAKES THIS COLUMN CONTRIBUTE NOTHING to the row's intrinsic
     size, so the calendar alone decides it; `min-height: 100%` then fills that
     row. The times list inside takes the slack with `flex: 1` and scrolls when
     there is more than fits. MEASURED both ways: three offers and twenty-seven,
     button top 659 in both, both columns ending at 736.

     DESKTOP ONLY. Below the stacking query there is no calendar beside this
     column to be sized by, and `height: 0` there would collapse it. */
  .bk-times-wrap { height: 0; min-height: 100%; }

  /* THE TYPE SCALES WITH THE CARD, AND SHIPPING IT WITHOUT THIS WAS THE SECOND
     THING THAT "STILL LOOKED OFF". The first attempt widened the LAYOUT and left
     every size at the value it had in a 560px phone column, so a 1120px card
     carried a 22px heading and 44px time chips -- correct proportions at the
     wrong scale, which reads as a page that has been stretched rather than
     designed. The owner's mockup is the reference and these are its ratios
     against a 1120px card. Everything is on the 4px scale except the font
     sizes, which are exact by the sheet's own rule at the top of this block. */
  .bk-h { font-size: 32px; }
  .bk-meta { font-size: 16px; gap: var(--s3); }
  .bk-cal-month { font-size: 24px; }
  .bk-cal-nav { width: 48px; height: 48px; font-size: 18px; }
  .bk-cal-dow { font-size: 13px; }
  .bk-cal-day { height: 60px; font-size: 16px; border-radius: 14px; }
  .bk-cal { gap: var(--s3); }
  .bk-times-head { font-size: 20px; }
  .bk-times-sub { font-size: 14px; }
  /* THE CHIP IS A TOUCH TARGET AND A LABEL AT ONCE, so it gets a height rather
     than padding alone -- `min-height` keeps a wrapped label from making one
     chip taller than its neighbour, which is what a padded box would do. */
  .bk-times { gap: var(--s3); }
  .bk-chip { min-height: 56px; display: flex; align-items: center;
    justify-content: center; font-size: 16px; }
  .bk-confirm { padding-top: var(--s3); gap: var(--s3); }
  .bk-cta { min-height: 60px; font-size: 16px; }
  .bk-tznote { font-size: 14px; }
}

/* Two columns at 520px, one at 400px. A BREAKPOINT IS THE RIGHT INSTRUMENT here
   -- the card's width is a pure function of the viewport (`max-width: 560px`
   inside a fluid wrap, with no other parent) which is the condition UX-121
   records for when measuring is needed instead. */
/* THE TIMES COLUMN IS PINNED AND THE CALENDAR TAKES THE REST, so the token is
   the one number the two-column layout turns on. Below the stacking query it is
   irrelevant -- the picker is one column there -- and it is a token rather than
   a literal because `.bk-picker` is the only rule that may spend it. */
:root { --bk-times-col: 400px; }
@media (max-width: 900px) {
  /* BETWEEN 520 AND 900 the card is the phone column in a wider window, so a
     400px times track would leave the month under 200px. The calendar keeps the
     width and the times take what is left, which is the shape this page had
     before the redesign. */
  :root { --bk-times-col: minmax(0, 1fr); }
  .bk-cal-day { height: 44px; font-size: 14px; }
  .bk-cal-month { font-size: 16px; }
  .bk-times-head { font-size: 14px; }
}
@media (max-width: 520px) {
  .bk-shell { padding: var(--s5) var(--s4); }
  .bk-slots { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* THE PICKER STACKS: the month, then that day's times beneath it. Side by side
     the month leaves ~180px for the time column on a 375px phone, which is
     narrower than a time chip wants. Reusing this existing query rather than
     bringing a breakpoint of its own, exactly as the block below does. */
  .bk-picker { grid-template-columns: minmax(0, 1fr); gap: var(--s4); }
  /* AND THE TIME LIST KEEPS ITS TWO COLUMNS once it has the full width, with the
     scroller lifted: on a phone the page scrolls, so a day with eight offers does
     not need a scroller inside a scroller -- and with the picker STACKED there is
     no calendar beside it to align the button to, so there is no slack to absorb
     either. */
  .bk-times { flex: 0 0 auto; overflow-y: visible; }
}
@media (max-width: 400px) {
  .bk-slots { grid-template-columns: minmax(0, 1fr); }
  /* `.bk-facts` / `.bk-next` ARE DELETED and their fallbacks with them (owner,
     2026-09-06). `.bk-meta` needs none: it is a wrapping flex line, so it falls
     to two lines on a narrow phone without a query. */
  .bk-times { grid-template-columns: minmax(0, 1fr); }
}

/* ---- THE JURISDICTIONAL TERMS STEP (owner decisions 2026-09-16) -----------
   `/i/<token>/terms`: counsel's four documents, displayed in full and accepted
   here rather than linked from a tick. Scoped to its own `data-page` on the
   same rule as `iv-landing` above, so none of it can reach the other five
   candidate pages.

   EVERY LENGTH IS A SPACING TOKEN. `check_tokens` 8.13 ratchets off-scale
   spacing DOWNWARDS ONLY and has zero slack, so a value picked by eye here
   fails the commit that adds it. */
body[data-page="iv-terms"] { background: var(--neutral-tint); }

/* THE DOCUMENT'S OWN SCROLL PANE, so the acceptance controls stay on screen
   while the terms are read. The height is what makes the scroll gate mean
   something: a pane tall enough to show the whole document would open the gate
   on load, which is why this is capped rather than allowed to grow.
   `overscroll-behavior` stops a flick at the end of the terms carrying on into
   the page behind and moving the button out from under the reader's thumb. */
.ivt-doc { max-height: 60vh; overflow-y: auto; overscroll-behavior: contain;
  margin: var(--s4) 0 0; padding: var(--s4);
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--panel); color: var(--ink);
  font-size: var(--text-sm); line-height: 1.6; }
.ivt-doc p { margin: 0 0 var(--s3); }
.ivt-doc ul, .ivt-doc ol { margin: 0 0 var(--s3); padding-left: var(--s5); }
.ivt-doc li { margin: 0 0 var(--s1); }
/* The documents carry no headings of their own -- counsel numbers the sections
   in bold body text -- so `strong` is what has to read as structure. */
.ivt-doc strong { color: var(--ink); }
.ivt-doc:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.ivt-dl { margin: var(--s3) 0 0; font-size: var(--text-sm); }

/* Each separate consent is a tap target on a phone and is the most legally
   significant control on the page, so it takes `.ap-check`'s 44px floor
   rather than `.ivl-consent`'s tighter single-tick geometry. */
.ivt-consents { margin: var(--s5) 0 0; display: flex; flex-direction: column;
  gap: var(--s4); }
.ivt-consent { display: flex; gap: var(--s3); align-items: flex-start;
  min-height: 44px; font-size: var(--text-sm); line-height: 1.55;
  color: var(--ink); cursor: pointer; }
.ivt-consent input { width: 22px; height: 22px; flex: 0 0 auto; margin: 0;
  accent-color: var(--accent); }

.ivt-hint { margin: var(--s4) 0 var(--s2); font-size: var(--text-sm);
  color: var(--faint); text-align: center; }

/* The location question on the landing page. `.ivl-q` and `.ivl-q-label` were
   DELETED with the declaration panel (owner ruling 2026-09-06) and are not
   being resurrected under their old names -- this is a different control with
   a different job, and reusing a retired class is how a deletion quietly
   un-happens. */
.ivl-loc-q { margin: 0 0 var(--s2); font-size: var(--text-sm);
  font-weight: 600; color: var(--ink); }
.ivl-loc-help { margin: 0 0 var(--s3); font-size: var(--text-xs);
  line-height: 1.5; color: var(--faint); }


/* THE TERMS DIALOG, on the apply page AND the interview landing (owner,
   2026-09-17: "exactly like what we used in the application stage"). Scoped
   `body[data-page]` rather than `body[data-page="apply"]`: the prefix exists
   only to out-specify `body[data-page="apply"] .candidate-card`, and an
   unvalued attribute selector has the same specificity while covering both. It holds the same `.candidate-card` the
   standalone step renders, so every `.ivt-*` rule above applies unchanged --
   what this adds is only the dialog's own box. */
.ap-terms-dlg { width: min(880px, 96vw); max-width: 96vw; padding: 0;
  border: 0; border-radius: var(--r-md); background: transparent;
  /* THE DIALOG SCROLLS AS ONE, AND FLEXING IT WAS THE WRONG ANSWER. Capping
     the dialog and letting the terms pane absorb the difference squeezed that
     pane to 32px, because mainland China's five consent labels are long enough
     to claim the whole box -- the document became a slot two lines high and the
     accept button still sat below the fold. Measured, not reasoned about.

     So the card keeps its natural height, the DIALOG scrolls, and the pane
     keeps a height of its own. The scroll gate still measures the pane, which
     is what it has always measured. */
  max-height: 92vh; overflow-y: auto; }
.ap-terms-dlg::backdrop { background: rgba(0, 0, 0, 0.45); }
/* THE CARD'S OWN PADDING IS `28px 0` -- vertical only, because on the page it
   sits inside a wrapper that supplies the horizontal gutter. In a dialog there
   is no wrapper, so the content ran to the edges. This restores the gutter and
   gives the block some air; the dialog is a box of legal text and reads badly
   tight. */
/* THE CARD'S `gap` IS WHAT WAS ACTUALLY DOING IT, and the per-element margins
   I reached for first were the wrong instrument -- they applied (12/8/16) while
   the measured gaps stayed 44/40/48. `body[data-page="apply"] .candidate-card`
   is a flex column with `gap: var(--s6)`, so 32px was being added between every
   child on top of whatever each one asked for. Correct for the page, where the
   card IS the layout; wrong inside a dialog, where these four elements are a
   header above a scrolling box. Set the gap, then let the margins do the rest.

   MEASURED, NOT REASONED: the cascade is a weaker instrument than the browser,
   and this is the sixth defect in this repo of the same shape -- a correct rule
   surviving into a container it was never measured in. */
body[data-page] .ap-terms-dlg .candidate-card { margin: 0; width: 100%;
  padding: var(--s5); border-bottom: 0; gap: 0;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-md); }
/* THE HEAD OF THE DIALOG IS TIGHTER THAN THE PAGE'S (owner, 2026-09-16,
   measured off a screenshot: 62px eyebrow-to-title, 61px title-to-lead and
   76px lead-to-document). Those margins are `ivl-*`'s, written for a standalone
   page where the card IS the viewport and the air is the design. Here the same
   four elements are a HEADER above a scrolling box, and the reader wants the
   document -- so this is the one place they are overridden, on the 4px scale
   like everything else. */
body[data-page] .ap-terms-dlg .ivl-eyebrow { margin: 0 0 var(--s3); }
body[data-page] .ap-terms-dlg .ivl-h1 { margin: 0 0 var(--s2); }
body[data-page] .ap-terms-dlg .ivl-lead { margin: 0 0 var(--s4); }
body[data-page] .ap-terms-dlg .ivt-doc { margin-top: 0; }
/* TIGHTER THAN THE STANDALONE STEP (owner, 2026-09-16: "the spacing looks very
   wide"). That page has a whole viewport and one column; the dialog is a box
   the reader is scrolling through, and 24px between five consent rows pushed
   the button a screen away from the last of them. The rows are still their own
   blocks -- this narrows the gap, it does not run them together. */
body[data-page] .ap-terms-dlg .ivt-consents { gap: var(--s3);
  margin-top: var(--s4); margin-bottom: var(--s5); }
body[data-page] .ap-terms-dlg .ivt-consent { min-height: 0; }
body[data-page] .ap-terms-dlg .ivt-hint { margin: var(--s3) 0 var(--s2); }
/* Shorter than the standalone page's 60vh: here it shares the viewport with
   the acceptance controls inside one scrolling box. */
body[data-page] .ap-terms-dlg .ivt-doc { max-height: 40vh; }

/* THE CLOSE CONTROL, injected by the apply page's script rather than rendered
   by the template: it belongs to the DIALOG, not to the document. The
   standalone no-script page has nothing to close and keeps its `Back` link
   instead, which is the route out that works there. */
.ap-terms-x { position: absolute; top: var(--s3); right: var(--s3);
  width: 32px; height: 32px; display: flex; align-items: center;
  justify-content: center; border: 0; border-radius: var(--r-sm);
  background: transparent; color: var(--faint); cursor: pointer;
  font-size: 20px; line-height: 1; }
.ap-terms-x:hover { background: var(--hover); color: var(--ink); }
body[data-page] .ap-terms-dlg .candidate-card { position: relative; }
