/* Design tokens live in zixan/static/zixan/tokens.css, loaded before this
   sheet by every base template. They were declared here and, in a
   near-identical copy, in portal.css -- which is how the two drifted: the
   portal never declared --warn at all, so a chip meaning "needs attention"
   rendered amber to staff and red to a candidate. One language, one file. */

/* ZIXAN candidate portal — mobile-first, theme-aware, no external assets. */

/* 2026-09-07. Re-pointed to the visual language of the public workforce
   catalogue, which the owner approved: white and light cool-gray surfaces,
   cobalt actions, and structure carried by a 1px border rather than by a
   shadow. A candidate arriving from /workforce/ used to cross a visible seam
   into a different-looking product; the tokens were already shared, so the
   seam was entirely in this sheet's own recipes. Nothing here is a new class
   — the templates are untouched. */


* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  font-family: var(--font-ui);
  font-size: var(--fs-lead);
  /* Looser than --lh-body (1.5): this surface is read, not scanned. A console
     row is a datum; a vacancy description is a paragraph. */
  line-height: 1.55;
  color: var(--text);
  background: var(--bg);
}

.container { width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: 1rem; }

a { color: var(--brand-ink); }
a:hover { color: var(--brand-hover); }

img, svg { max-width: 100%; height: auto; }

/* Focus visibility for keyboard users. 2px, the catalogue's ring: 3px on a
   10px-radius control reads as a second border rather than as focus. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.skip-link {
  position: absolute;
  left: -999px;
  top: 0;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  padding: 0.5rem 1rem;
  border-radius: 0 0 var(--r-md) 0;
  z-index: var(--z-topbar);
}
.skip-link:focus { left: 0; }

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

/* Header — the mobile W1 contract: brand · nav · language · theme, and the
   header's height is DETERMINISTIC. The old header wrapped freely, which
   measured 61px on a desktop, 152px at 375px and 254px at 320px — a third
   of a phone before any content. Now exactly one thing may wrap: the nav
   takes its own full row below 720px and scrolls horizontally instead of
   stacking, so the header is one row on desktop, two fixed rows on a phone,
   and never a function of how many links exist. */
.site-header { background: var(--surface); border-bottom: 1px solid var(--border); }
.header-inner {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 1.25rem;
  /* 64px, the catalogue's bar. Stated as a minimum rather than a height so
     the phone's second row still has somewhere to go. */
  min-height: 4rem; padding-block: 0.5rem;
}
/* The mark used to be drawn here as generated content, because this sheet
   could not change the template that rendered a bare word. The template now
   includes partials/brand.html and the real mark arrives with it, so the
   pseudo-element is gone rather than doubled — chrome.css carries the layout
   for .zx-brand and this class is only the header's hook. */
.brand { white-space: nowrap; }
/* The language selector opens the trailing control cluster; on the phone's
   two-row layout nothing else occupies row one's right side. */
.header-inner > .langsel, .header-inner > .langquick { margin-inline-start: auto; }
.primary-nav {
  display: flex; flex-wrap: nowrap; gap: 0.125rem; flex: 1; min-width: 0;
  overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none;
}
.primary-nav::-webkit-scrollbar { display: none; }
.primary-nav a {
  display: inline-flex; align-items: center;
  text-decoration: none; font-weight: 600; font-size: var(--fs-sm);
  white-space: nowrap;
  padding: 0.5rem 0.75rem; border-radius: var(--r-sm); color: var(--muted);
}
.primary-nav a:hover { background: var(--surface-2); color: var(--text); }
/* The current door fills, it does not merely darken. The portal's base
   template does not mark its top-row links yet — on a phone the bottom bar
   carries "you are here" instead — so this waits for the attribute rather
   than inventing a second mechanism. */
.primary-nav a[aria-current="page"] { background: var(--brand-soft); color: var(--brand-ink); }
@media (pointer: coarse) {
  /* The token floor reaches buttons and .button, not a bare nav anchor, and
     these sit shoulder to shoulder in a scrolling row. */
  .primary-nav a { min-height: var(--tap-min); }
}
@media (max-width: 719px) {
  /* The nav is the only child allowed onto a second row; the row scrolls. */
  .primary-nav { order: 10; flex-basis: 100%; }
  /* A signed-in candidate navigates with the bottom bar instead. */
  body.has-bottomnav .primary-nav { display: none; }
}
/* The 273px three-pill language row lived here, and after it the theme
   toggle that dressed as a fourth language pill. Both controls now come
   from zixan/chrome.css: the compact <details> selector and the icon
   theme toggle every surface shares. */

/* Layout blocks */
main { padding-block: 1.5rem 3rem; }
main:focus { outline: none; }
/* The border is the panel. The shadow that used to sit here made every
   section of a candidate screen read as an equally-important floating card;
   the edge was already being drawn by the border underneath it. */
.panel {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md);
  padding: 1.125rem 1.25rem; margin-block: 1.25rem;
}

/* The display step is the page's one big title; below it the ramp, not a
   free clamp. The old clamps peaked at 2.4rem/38px, which on a phone was a
   headline competing with the content under it. */
h1 { font-size: var(--fs-display); line-height: var(--lh-tight); letter-spacing: -0.01em; }
h2 { font-size: var(--fs-h1); line-height: var(--lh-tight); letter-spacing: -0.01em; }
h3 { font-size: var(--fs-h2); line-height: var(--lh-tight); }
.lead { font-size: var(--fs-h3); color: var(--muted); }
.muted { color: var(--muted); }
.note { color: var(--muted); font-size: var(--fs-body); font-style: italic; }
.hint { color: var(--muted); }

/* Hero.

   A public page has one job before the reader decides whether to keep going,
   and the measured premium pages do it with SIZE CONTRAST and negative
   tracking rather than with weight or a decorative face. The UI scale stops
   at 28px on purpose — that is the right ceiling inside a screen a person
   works in — so the two marketing steps live in tokens.css and are spent
   here and on the catalogue stage, nowhere else. */
.hero {
  display: grid; gap: var(--s-5); align-items: center;
  padding-block: var(--s-6) var(--s-5);
}
@media (min-width: 720px) {
  .hero { grid-template-columns: 1.25fr 0.75fr; gap: var(--s-7); padding-block: var(--s-8) var(--s-7); }
}
.hero h1 {
  font-size: var(--fs-display-2); line-height: var(--lh-display);
  letter-spacing: var(--track-display); text-wrap: balance; max-width: 20ch;
  margin-block: 0 var(--s-3);
}
@media (min-width: 720px) { .hero h1 { font-size: var(--fs-display-3); } }
.hero .lead { max-width: 46ch; margin-block: 0; }
/* An eyebrow says who the page is for before the headline says what it offers.
   It is the only uppercase text on the page, which is what lets it read as a
   label rather than as shouting. */
.eyebrow {
  font-size: var(--fs-caption); font-weight: 700; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--brand-ink); margin-block: 0 var(--s-3);
}
.cta-row { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-top: var(--s-5); }
/* Three facts a person can hold us to, not three adjectives. Each carries a
   cobalt tick so the row reads as a set at a glance and as sentences when
   actually read. */
.trust-row {
  list-style: none; margin: var(--s-6) 0 0; padding: var(--s-5) 0 0;
  border-top: 1px solid var(--border);
  display: grid; gap: var(--s-3) var(--s-5);
}
@media (min-width: 720px) {
  /* Spans both hero columns: three facts on one line read as a set, where a
     two-then-one wrap reads as an accident. */
  .trust-row { grid-column: 1 / -1; grid-template-columns: repeat(3, 1fr); }
}
.trust-row li {
  display: flex; align-items: center; gap: var(--s-2);
  color: var(--muted); font-size: var(--fs-body);
}
.trust-row li::before {
  content: ""; flex: none; width: 1.15rem; height: 1.15rem; border-radius: 50%;
  background:
    linear-gradient(45deg, transparent 44%, var(--brand-contrast) 44%, var(--brand-contrast) 56%, transparent 56%) center / 0.55rem 0.12rem no-repeat,
    var(--brand);
}
.hero-visual { display: flex; justify-content: center; }
@media (max-width: 719px) {
  /* On a phone the object follows the words and the buttons instead of
     standing between them and the first vacancy. */
  .hero { grid-template-columns: 1fr; }
  .hero-visual { --mark-size: 7.5rem; }
}
@media (min-width: 720px) { .hero-visual { --mark-size: 13rem; } }

/* Section rhythm: a heading and, where the section continues elsewhere, one
   quiet link to where it continues. */
.section-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s-4); flex-wrap: wrap; margin-bottom: var(--s-4);
}
.section-head h2 { margin: 0; }
.section-more {
  font-size: var(--fs-body); font-weight: 600; color: var(--brand-ink);
  text-decoration: none; white-space: nowrap;
}
.section-more:hover { text-decoration: underline; }
.section-featured, .section-how { padding: var(--s-5) var(--s-5) var(--s-4); }

/* Buttons — three recipes, one set of metrics: 40px tall, 16px of side
   padding, a 10px radius. The pill went because a pill and a 10px card in
   the same panel read as two different products. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 0.375rem;
  min-height: 2.5rem; padding: 0.5rem 1rem; border-radius: var(--r-md);
  border: 1px solid var(--border); background: var(--surface); color: var(--text);
  font-weight: 700; font-size: var(--fs-sm); line-height: var(--lh-tight);
  text-align: center; text-decoration: none; cursor: pointer;
}
@media (pointer: coarse) {
  /* tokens.css floors `button`, `.button` and `a.button` — not `.btn`, which
     is this surface's own name and is worn by anchors as often as by buttons.
     Without this, every primary action on the phone is 40px: four pixels
     under the floor, on the one surface that is phone-first. */
  .btn { min-height: var(--tap-min); }
}
/* The committing action, uppercase with the catalogue's tracking. It wraps
   where the catalogue's does not: these labels are whole Russian and Uzbek
   sentences ("Подтвердить предложение"), not the catalogue's two-word verbs,
   and nowrap would push them off a 320px screen. */
.btn-primary {
  background: var(--brand); color: var(--brand-contrast); border-color: var(--brand);
  text-transform: uppercase; letter-spacing: 0.02em;
}
.btn-primary:hover { background: var(--brand-hover); border-color: var(--brand-hover); color: var(--brand-contrast); }
/* The considered alternative: an outlined action, 1.5px so it holds its own
   next to a filled one without shouting. */
.btn-secondary {
  background: var(--surface); color: var(--brand-ink);
  border: 1.5px solid var(--brand);
}
.btn-secondary:hover { background: var(--brand-soft); color: var(--brand-ink); }
/* The main website's two additions (portal/site.css refines both): the one
   amber action per screen, and an outline button for a dark band. */
.btn-accent { background: var(--accent-amber, var(--brand)); color: var(--accent-ink, var(--brand-contrast)); }
.btn-on-dark { background: transparent; color: #fff; border: 1px solid rgba(255, 255, 255, 0.35); }
/* The quiet exit — withdraw, log out, disconnect. Ink stays the body ink, because this is
   a real verb a person has to be able to read, not a caption. */
.btn-ghost { background: var(--surface); color: var(--text); border-color: var(--border); }
.btn-ghost:hover { background: var(--surface-2); border-color: var(--border-strong); }
.btn[aria-disabled="true"], .btn:disabled { opacity: 0.55; cursor: not-allowed; }
/* The hero's pair. Uppercase is dropped at this size: all-caps Cyrillic loses
   the ascender and descender profile that makes a word recognisable at a
   glance, and «СМОТРЕТЬ ВАКАНСИИ» is a whole phrase rather than a two-word
   verb. The two buttons keep an obvious difference in weight so the eye is
   never asked to choose between equals. */
.btn-lg {
  min-height: 3rem; padding: 0.75rem 1.5rem;
  font-size: var(--fs-lead); text-transform: none; letter-spacing: 0;
}
.btn-quiet {
  background: transparent; color: var(--text); border-color: var(--border-strong);
}
.btn-quiet:hover { background: var(--surface-2); border-color: var(--text); }
@media (max-width: 559px) {
  /* One thumb, one column: side by side at 320px each button is narrower than
     the words inside it. */
  .cta-row { flex-direction: column; align-items: stretch; }
  .cta-row .btn { width: 100%; }
}

/* Highlight card — the featured offer's whole tile is the link, so it takes
   the card treatment itself rather than sitting inside one. */
.highlight-card {
  display: flex; flex-direction: column; gap: 0.25rem;
  text-decoration: none; color: var(--text);
}
.highlight-card:hover .featured-role { color: var(--brand-ink); }
/* The number a person came for. Kept in --success ink, not brand: money is a
   fact on this card, not the action. */
.salary { font-weight: 800; font-size: var(--fs-h2); color: var(--success); }
.meta { color: var(--muted); }
.empty { color: var(--muted); }

.steps { padding-left: 1.2rem; }
.steps li { margin-block: 0.4rem; }
/* The numbers are the point of this list — it is a real sequence — so they are
   drawn rather than left to the browser's marker, and the list loses its
   indent because the numeral is now the indent. */
.steps--numbered {
  list-style: none; padding: 0; margin: 0;
  display: grid; gap: var(--s-4);
}
@media (min-width: 720px) { .steps--numbered { grid-template-columns: repeat(3, 1fr); gap: var(--s-5); } }
.steps--numbered li {
  display: grid; grid-template-columns: auto 1fr; gap: var(--s-3);
  align-items: start; margin: 0;
}
.step-n {
  display: grid; place-items: center; flex: none;
  width: 2rem; height: 2rem; border-radius: 50%;
  background: var(--brand-soft); color: var(--brand-ink);
  font-weight: 800; font-size: var(--fs-body); font-variant-numeric: tabular-nums;
}
.step-t { color: var(--text); }

/* Vacancies — the W3 discovery chrome. The search input is always visible;
   the three selects fold behind a native <details> whose summary carries an
   applied count. Everything is one GET form; the disclosure is layout, not
   a second form. */
.search-row { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin-block: 1rem; }
/* The catalogue's universal input: taller than a button, 1.5px so the field
   has a findable edge of its own, 16px text so iOS does not zoom the page
   the moment a candidate taps it. */
.search-row input[type="search"] {
  flex: 1 1 12rem; min-width: 0; height: 3.25rem; padding: 0 1rem;
  border-radius: var(--r-md); border: 1.5px solid var(--border-strong);
  background: var(--surface); color: var(--text); font-size: var(--fs-lead);
}
.search-go { flex: none; }
.filter-sheet { flex-basis: 100%; }
.filter-sheet > summary {
  display: inline-flex; align-items: center; min-height: var(--tap-min);
  padding: 0.35rem 0.9rem; cursor: pointer; list-style: none;
  border: 1px solid var(--border); border-radius: var(--r-md);
  font-weight: 700; font-size: var(--fs-sm); color: var(--text); background: var(--surface);
}
.filter-sheet > summary::-webkit-details-marker { display: none; }
.filter-sheet > summary:hover { background: var(--surface-2); }
/* Open is a state, so it reads as selected rather than merely hovered. */
.filter-sheet[open] > summary { background: var(--brand-soft); border-color: var(--brand-soft-2); color: var(--brand-ink); }
.filter-fields {
  display: grid; gap: 0.75rem; margin-top: 0.75rem;
  padding: 1rem; border: 1px solid var(--border); border-radius: var(--r-md);
  background: var(--surface);
}
@media (min-width: 640px) { .filter-fields { grid-template-columns: repeat(3, 1fr) auto; align-items: end; } }
.filter-field { display: grid; gap: 0.25rem; }
.filter-field > label { font-size: var(--fs-caption); font-weight: 700; color: var(--muted); }
.filter-field select {
  min-height: var(--tap-min); padding: 0.5rem 0.75rem;
  border-radius: var(--r-sm); border: 1.5px solid var(--border-strong);
  background: var(--surface); color: var(--text); font-size: var(--fs-lead);
  width: 100%;
}
.applied-filters { list-style: none; display: flex; flex-wrap: wrap; gap: 0.4rem; padding: 0; margin-block: 0.75rem; align-items: center; }
/* An applied filter is a blue chip: it is a choice the reader made and can
   take back, not a status the system is reporting. Qualified by the list
   because the base .chip recipe is declared further down this file and would
   otherwise win on source order alone. */
.chip-remove { text-decoration: none; }
.applied-filters .chip-remove { background: var(--brand-soft); color: var(--brand-ink); border-color: var(--brand-soft-2); }
.applied-filters .chip-remove:hover { background: var(--brand-soft-2); color: var(--brand-ink); }
.clear-all { font-size: var(--fs-sm); font-weight: 700; }
@media (pointer: coarse) {
  /* Audit fix: the removable filter chips and clear-all are interactive
     anchors the token floor does not reach; a mis-tap here re-runs a query. */
  .applied-filters .chip-remove, .applied-filters .clear-all {
    min-height: var(--tap-min); display: inline-flex; align-items: center;
  }
}
.pagination { display: flex; align-items: center; justify-content: center; gap: 0.5rem; margin-block: 1.5rem; flex-wrap: wrap; }
/* The prev/next links already carry .btn.btn-ghost, so they are styled once,
   as buttons, and nothing here overrides them. Only the "3 / 12" counter
   between them needs a rule: it is the one thing in the row that is text
   rather than a control, and it should read as text. */
.pagination span {
  display: inline-flex; align-items: center;
  color: var(--muted); font-size: var(--fs-sm); font-variant-numeric: tabular-nums;
}

/* the pre-W3 two-select strip, kept for any template still using it */
.filter { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: end; margin-block: 1rem; }
.filter select {
  min-height: var(--tap-min); padding: 0.5rem 0.75rem; border-radius: var(--r-sm);
  border: 1.5px solid var(--border-strong); background: var(--surface);
  color: var(--text); font-size: var(--fs-lead);
}
.vacancy-grid { list-style: none; padding: 0; margin: 0; display: grid; gap: 1rem; }
@media (min-width: 640px) { .vacancy-grid { grid-template-columns: repeat(2, 1fr); } }
.vacancy-card {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md);
  padding: 0;
}
/* The whole card is a target on a phone, so the border is what answers the
   hover — a shadow appearing under one card in a grid re-introduces exactly
   the floating quilt the borders replaced. */
.vacancy-card:hover { border-color: var(--border-strong); }
.vacancy-card:focus-within { border-color: var(--brand); }
/* The link IS the card: it carries the padding, so there is no dead border of
   card around a live centre. Grid rather than block, because the four parts
   have a fixed order and the money needs its own row. */
.vacancy-card-link {
  display: grid; gap: var(--s-1); padding: var(--s-4) var(--s-4) var(--s-3);
  text-decoration: none; color: var(--text); border-radius: inherit;
}
.vacancy-role {
  font-weight: 700; font-size: var(--fs-h3); line-height: var(--lh-tight);
  letter-spacing: var(--track-tight); text-wrap: balance;
}
.vacancy-card:hover .vacancy-role { color: var(--brand-ink); }
/* The figure a person came for, on its own line, with the qualifiers beneath
   it rather than trailing off the same green sentence. */
.vacancy-card .salary { margin-top: var(--s-1); font-variant-numeric: tabular-nums; }
.salary-note { color: var(--muted); font-size: var(--fs-caption); }
/* An unknown amount is stated, not styled as money. */
.salary-unknown { color: var(--muted); font-size: var(--fs-body); font-style: italic; }
.vacancy-place {
  display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap;
  margin-top: var(--s-2); color: var(--text); font-size: var(--fs-body); font-weight: 600;
}
/* The country is a place, not one more word in a metadata sentence: a small
   cobalt marker gives it presence without an image the CSP would have to
   allow and a low-end phone would have to fetch. */
.place-country { display: inline-flex; align-items: center; gap: var(--s-2); }
.place-country::before {
  content: ""; flex: none; width: 0.5rem; height: 0.5rem; border-radius: 50%;
  background: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft);
}
.place-city { color: var(--muted); font-weight: 400; }
.place-city::before { content: "· "; }
.vacancy-tags { display: flex; flex-wrap: wrap; gap: var(--s-2); margin-top: var(--s-2); }
.chip--quiet { background: var(--surface-2); color: var(--muted); border-color: var(--border); }
.vacancy-note { margin: 0 var(--s-4) var(--s-3); }
.vacancy-grid--three { }
@media (min-width: 720px) { .vacancy-grid--three { grid-template-columns: repeat(3, 1fr); } }
.results-actions { display: flex; flex-wrap: wrap; gap: var(--s-3); }
/* The empty-state pattern (mobile W1): a centred panel with the message and
   at most one action. On a brand-new account every list is empty at once, so
   these panels ARE the first-run experience — a grey sentence in a corner
   reads as something broken. */
.empty-state { text-align: center; padding-block: 2.25rem; }
.empty-state .empty { margin-block: 0 1rem; font-size: var(--fs-lead); }

.vacancy-detail .facts { display: grid; gap: 0.5rem; margin-block: 1rem; }
.vacancy-detail dt { font-weight: 700; }
.vacancy-detail dd { margin: 0; color: var(--muted); }
.back-link { text-decoration: none; }
.benefits { padding-left: 1.2rem; }

/* Questionnaire */
.questionnaire { max-width: 640px; }
/* The track is the divider step, not the border step: a progress bar sits
   inside a panel that already has a border, and reusing --border made the
   empty half of the bar read as an edge. */
.progress {
  background: var(--n-100); border-radius: var(--r-pill); height: 8px;
  overflow: hidden; margin-bottom: 1rem;
}
.progress-bar { background: var(--brand); height: 100%; transition: width 0.25s ease; }
.progress-label { font-weight: 700; font-size: var(--fs-caption); letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted); margin-bottom: 0.35rem; }
.q-form fieldset { border: 1px solid var(--border); border-radius: var(--r-md); padding: 1rem 1.125rem; background: var(--surface); }
.q-form legend { font-weight: 700; padding-inline: 0.4rem; }
.field { display: flex; flex-direction: column; gap: 0.4rem; margin-block: 0.75rem; font-weight: 600; }
/* Textarea was missing from this list, so the concierge message box and the
   application note rendered as raw browser controls beside styled fields. */
.field input, .field select, .field textarea {
  min-height: var(--tap-min); padding: 0.6rem 0.75rem;
  border-radius: var(--r-md); border: 1.5px solid var(--border-strong);
  background: var(--surface); color: var(--text); font-size: var(--fs-lead); font-weight: 400;
  font-family: inherit;
}
.field input:focus, .field select:focus, .field textarea:focus { border-color: var(--brand); }
.field input[type="checkbox"], .field input[type="radio"] { min-height: 0; }
.choice {
  display: flex; align-items: center; gap: 0.6rem; padding: 0.6rem 0.5rem;
  min-height: var(--tap-min);
  border: 1px solid transparent; border-radius: var(--r-sm);
  cursor: pointer; font-weight: 500;
}
.choice:hover { background: var(--brand-soft); border-color: var(--brand-soft-2); }
.choice input { width: 1.15rem; height: 1.15rem; flex: none; }
/* A suggestion is an offer, not a verdict: a soft brand fill with its own
   border, rather than the outline that used to read as a focus ring. */
.choice--suggested { background: var(--brand-soft); border-color: var(--brand); }
.choice--consent { align-items: flex-start; }
.inline-fields { display: flex; gap: 0.75rem; flex-wrap: wrap; }
.suggestions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem; }
.q-actions { display: flex; justify-content: space-between; gap: 0.75rem; margin-top: 1.25rem; }
.form-error { color: var(--danger); font-weight: 700; font-size: var(--fs-sm); }

/* Honeypot: visually and semantically removed, still in the DOM for bots. */
.hp-field { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* The living ZIXAN mark.

   It replaced a geometric mascot whose twelve literal hexes — a skin tone, a
   hard-hat yellow, a hi-vis green — were a second palette living inside this
   sheet, in neither theme and in no token. The object has one colour and takes
   it from the cascade, so it follows all four theme states for free.

   The static SVG is what a reader sees with scripting off or under a reduced
   motion preference; mark.js sets data-mark-live only once it holds a working
   2D context, and only then does the canvas take over. So a browser that
   cannot animate shows a finished mark rather than an empty box. */
.mark { display: flex; flex-direction: column; align-items: center; gap: var(--s-3); margin: 0; }
.mark-stage {
  position: relative; width: var(--mark-size, 9.5rem); aspect-ratio: 1;
  display: grid; place-items: center;
  color: var(--brand);
}
.mark--ai .mark-stage { color: var(--ai); }
/* The wash reads as light coming off the object rather than a plate behind it,
   so it has no edge of its own to line up with the layout. */
.mark-stage::before {
  content: ""; position: absolute; inset: -12%; border-radius: 50%;
  background: radial-gradient(circle at 50% 45%,
    color-mix(in srgb, currentColor 20%, transparent) 0%,
    transparent 68%);
  pointer-events: none;
}
.mark-static, .mark-canvas { position: relative; width: 100%; height: auto; display: block; }
.mark-canvas { display: none; }
[data-mark-live] .mark-static { display: none; }
[data-mark-live] .mark-canvas { display: block; }
.mark-caption {
  font-weight: 600; color: var(--muted); text-align: center;
  font-size: var(--fs-body); max-width: 22ch; text-wrap: balance;
}


/* Featured verified offers (a curated list, never a ranking). */
.featured-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.75rem; }
@media (min-width: 640px) { .featured-list { grid-template-columns: repeat(3, 1fr); } }
.featured-item {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-md); padding: 0.875rem 1rem;
}
.featured-item:hover { border-color: var(--border-strong); }
.featured-role { font-weight: 700; font-size: var(--fs-h3); }

/* Legal pages */
.legal { max-width: 720px; }
.legal .facts { display: grid; gap: 0.5rem; margin-block: 1rem; }
.legal dt { font-weight: 700; }
.legal dd { margin: 0; color: var(--muted); }

/* Footer */
.site-footer { border-top: 1px solid var(--border); background: var(--surface); padding-block: 1.5rem; }
.site-footer p { margin-block: 0.35rem; font-size: var(--fs-body); color: var(--muted); }
.ai-note { font-weight: 500; }

/* ---------------------------------------------------------------------------
 * Master Build surfaces: the assistant and the candidate account.
 *
 * Same tokens, same components, no new dependency. Everything below degrades to
 * readable stacked content when the stylesheet fails to load, because the markup
 * underneath it is ordinary headings, lists and forms.
 * ------------------------------------------------------------------------- */

.stack { display: grid; gap: 0.75rem; }
.actions { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin-block: 0.75rem; }
.help { color: var(--muted); font-size: var(--fs-sm); margin-block: 0.25rem; }

.form-errors {
  margin: 0.4rem 0;
  padding: 0.5rem 0.75rem 0.5rem 1.5rem;
  border: 1px solid var(--danger);
  border-radius: var(--r-md);
  color: var(--danger);
  font-size: var(--fs-sm);
}

.chips { list-style: none; display: flex; flex-wrap: wrap; gap: 0.4rem; padding: 0; margin: 0.4rem 0; }
/* Tinted fill with matching ink, the catalogue's chip, rather than the old
   outline-only pill. An outlined chip in a bordered card is a third border;
   a filled one is a label, which is what a chip actually is. The word stays
   — colour is never the only carrier of the meaning. */
.chip {
  display: inline-flex; align-items: center; gap: 0.25rem;
  padding: 0.15rem 0.5rem;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  background: var(--surface-2); color: var(--muted);
  font-size: var(--fs-caption); font-weight: 700; line-height: 1.4;
}
.chip--good { background: var(--success-soft); color: var(--success); }
/* `--warn`, not `--danger`. This chip carries "your document expires soon",
   "your phone is not verified yet", "this document should be replaced" -- work
   the candidate still has time to do. Painting it the same red as a refusal
   told every candidate with a renewal due that something had gone wrong, and
   left nothing louder for the cases where something actually had. The portal
   had no `--warn` of its own until the shared token file arrived; it does
   now. Warn is the one chip that also keeps a border: amber on cream is the
   quietest of the three fills, and this is the one a candidate must not
   scroll past. */
.chip--warn { background: var(--warn-soft); border-color: var(--warn-line); color: var(--warn); }

/* Assistant transcript. The question is the candidate's own words echoed back
 * from the session; nothing here is persisted to the database. */
.thread { display: grid; gap: 0.75rem; margin-block: 1rem; }
.turn {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  padding: 0.85rem 1rem;
}
.turn-question { margin-block: 0 0.4rem; }
.turn-answer { margin-block: 0; }

/* Candidate account */
.data-list { display: grid; gap: 0.5rem; margin-block: 1rem; }
.data-list dt { font-weight: 700; }
.data-list dd { margin: 0; color: var(--muted); }

.timeline { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.4rem; }
.timeline li { border-inline-start: 3px solid var(--border); padding-inline-start: 0.75rem; }
.timeline li:first-child { border-inline-start-color: var(--brand); }

.apply-form { margin-block: 1rem; display: grid; gap: 0.5rem; }

.record-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.6rem; }
.record-list > li {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 0.75rem 0.9rem;
  background: var(--surface);
}

.token-code {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: var(--fs-h3);
  letter-spacing: 0.06em;
  padding: 0.4rem 0.7rem;
  border: 1px dashed var(--border);
  border-radius: var(--r-md);
  word-break: break-all;
}

/* ---------------------------------------------------------------------------
 * Candidate dashboard. Same tokens, mobile-first: one column of cards, large
 * tap targets, and a five-step ladder that reads top-to-bottom on a phone.
 * ------------------------------------------------------------------------- */

/* Same law as .panel: the dashboard is a column of cards, and a shadow on
   each of them turned "here are your five things" into five things shouting
   at once. */
.card {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md);
  padding: 1.125rem 1.25rem; margin-block: 1rem;
}
.card h2 { margin-top: 0; font-size: var(--fs-h2); }

.messages { list-style: none; padding: 0; margin: 0.75rem 0; display: grid; gap: 0.5rem; }
.messages li {
  border: 1px solid var(--border); border-inline-start: 4px solid var(--brand);
  border-radius: var(--r-md); background: var(--surface); padding: 0.6rem 0.9rem;
}
.messages li.error, .messages li.warning { border-inline-start-color: var(--danger); }
.messages li.success { border-inline-start-color: var(--accent); }

.process { border-top: 1px solid var(--border); padding-top: 0.75rem; }
.process:first-child { border-top: 0; padding-top: 0; }
.process h3 { margin-block: 0 0.2rem; }
.process--closed { opacity: 0.8; }

.ladder { list-style: none; padding: 0; margin: 0.75rem 0; display: grid; gap: 0.35rem; }
.ladder-step { display: flex; align-items: center; gap: 0.55rem; color: var(--muted); }
.ladder-dot {
  width: 0.8rem; height: 0.8rem; border-radius: 50%; flex: none;
  border: 2px solid var(--border); background: var(--surface);
}
.ladder-step.is-reached { color: var(--text); }
.ladder-step.is-reached .ladder-dot { border-color: var(--accent); background: var(--accent); }
.ladder-step.is-current .ladder-label { font-weight: 700; }
.ladder-step.is-current .ladder-dot { border-color: var(--brand); background: var(--brand); }
@media (min-width: 640px) {
  .ladder { display: flex; flex-wrap: wrap; gap: 0.4rem 1.1rem; }
}

.action-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.6rem; }
.action-list li {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md);
  padding: 0.7rem 0.9rem; display: flex; flex-wrap: wrap; gap: 0.3rem 0.75rem;
  align-items: baseline;
}
.action-list li:hover { border-color: var(--border-strong); }
.action-list a { font-weight: 700; text-decoration: none; }

.mono { font-family: var(--font-mono); }

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
}

/* --- safe areas ----------------------------------------------------------
   `viewport-fit=cover` is now on the portal base too, so these resolve. The
   candidate portal is the surface most likely to be opened on a notched
   phone. */
.site-header { padding-top: max(0px, env(safe-area-inset-top)); }
main { padding-bottom: max(3rem, env(safe-area-inset-bottom)); }

/* The front door's two-card choice (mobile W2): stacked on a phone, side by
   side from 640px. Each card is one complete path — a heading, one short
   paragraph, one action — because a person at this screen is deciding which
   door they are at, not reading. */
.choice-grid { display: grid; gap: 1rem; margin-block: 1rem; }
@media (min-width: 640px) { .choice-grid { grid-template-columns: 1fr 1fr; align-items: start; } }
.choice-card h2 { margin-top: 0; }
.choice-card .btn { display: inline-flex; align-items: center; min-height: var(--tap-min); }

/* Audit fix: a general .facts definition-list. The W4 offer card and
   offer-confirm page use <dl class="facts"> but portal.css only scoped
   `.vacancy-detail .facts`, so the labels and values ran together. */
.facts { display: grid; gap: 0.4rem 1rem; margin-block: 1rem; }
@media (min-width: 560px) { .facts { grid-template-columns: auto 1fr; } }
.facts > div { display: contents; }
.facts dt { font-weight: 700; }
.facts dd { margin: 0; }

/* Mobile W4: the offer surfaces. The waiting card leads the dashboard and
   is the one card allowed the warn accent — a person is waiting on it. */
.offer-waiting { border-inline-start: 4px solid var(--warn); }
.offer-card .offer-accept { margin-block: 0.75rem 0.5rem; }
.offer-decline > summary { cursor: pointer; color: var(--muted); font-weight: 600; min-height: var(--tap-min); display: inline-flex; align-items: center; }
.offer-decline[open] > summary { color: var(--text); }
.radio-row { display: flex; align-items: center; gap: 0.5rem; min-height: var(--tap-min); }

/* The activity feed (candidate programme): a dated list of plain-language
   facts. New items carry a quiet left accent; action-required the warn one.
   The banner card on home is the "something changed" doorway. */
.activity-list { list-style: none; margin: 1rem 0 0; padding: 0; display: grid; gap: 0.5rem; }
.activity-item {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md);
  padding: 0.7rem 0.9rem; display: flex; flex-wrap: wrap; gap: 0.25rem 0.75rem; align-items: baseline;
}
.activity-item.is-new { border-inline-start: 3px solid var(--brand); }
.activity-item.needs-action { border-inline-start: 3px solid var(--warn); }
.activity-when { font-size: var(--fs-sm); color: var(--muted); font-variant-numeric: tabular-nums; flex: none; }
.activity-label { flex: 1 1 14rem; min-width: 0; }
/* Soft, not solid. "New" is one word among the plain-language facts in the
   feed, and a filled cobalt badge beside every fresh row read as an alarm
   for what is only a timestamp. */
.chip--new { background: var(--brand-soft); color: var(--brand-ink); }
.activity-banner { border-inline-start: 4px solid var(--brand); }

/* The selfie request (candidate programme): the one request that is a
   camera moment, not a file field. The label IS the button; the input
   inside it is visually hidden but still focusable and still works with
   scripting off via the send button beside it. */
.selfie-card {
  background: var(--warn-soft); border: 1px solid var(--warn-line); border-radius: var(--r-md);
  padding: 1rem; display: grid; gap: 0.5rem; justify-items: start;
}
.selfie-card h3 { margin: 0; font-size: var(--fs-h3); }
.selfie-card .selfie-capture { cursor: pointer; }
.selfie-card .selfie-capture input:focus-visible ~ *,
.selfie-card .selfie-capture:focus-within { outline: 2px solid var(--focus); outline-offset: 2px; }

/* M3: the floating contact door. Native <details>; the summary is the round
   button, the open state adds the menu above it. Bottom offset clears the
   bottom bar (its height + safe area) so neither is ever covered.

   This and its menu are two of the few things on the portal that genuinely
   float over the page, so they are two of the few things that keep a shadow
   — and it is now the shared elevation, tinted with the ink, rather than a
   neutral black that read as dirt on a cool-gray page. */
.contact-fab {
  position: fixed; right: 1rem;
  bottom: calc(4.4rem + env(safe-area-inset-bottom, 0px));
  z-index: var(--z-dialog);
}
@media (min-width: 720px) { .contact-fab { bottom: 1.25rem; } }
.contact-fab summary {
  list-style: none; cursor: pointer;
  display: grid; place-items: center;
  width: 3.2rem; height: 3.2rem; border-radius: 50%;
  background: var(--brand); color: var(--brand-contrast);
  box-shadow: var(--elev-2);
}
.contact-fab summary:hover { background: var(--brand-hover); }
.contact-fab summary::-webkit-details-marker { display: none; }
.contact-fab summary svg { width: 1.5rem; height: 1.5rem; }
.contact-fab summary .contact-fab-close { display: none; font-size: var(--fs-h2); line-height: 1; }
.contact-fab[open] summary svg { display: none; }
.contact-fab[open] summary .contact-fab-close { display: block; }
.contact-fab summary:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.contact-fab-menu {
  position: absolute; right: 0; bottom: calc(100% + 0.6rem);
  min-width: 13rem; display: grid; gap: 0.25rem;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: 0.5rem;
  box-shadow: var(--elev-2);
}
.contact-fab-menu a {
  display: flex; align-items: center; min-height: var(--tap-min);
  padding: 0.6rem 0.75rem; border-radius: var(--r-sm);
  text-decoration: none; color: var(--text); font-weight: 600; font-size: var(--fs-body);
}
.contact-fab-menu a:hover, .contact-fab-menu a:focus-visible { background: var(--brand-soft); color: var(--brand-ink); }
@media (prefers-reduced-motion: no-preference) {
  /* 250ms, the one motion duration in the language. The old 160ms snap was
     the fastest thing on the surface and read as a glitch beside it. */
  .contact-fab-menu { transform-origin: bottom right; animation: fabPop 0.25s ease-out; }
  @keyframes fabPop { from { transform: scale(0.92); opacity: 0; } to { transform: scale(1); opacity: 1; } }
}

/* M3: living section headings. The underline is a directional cue — a soft
   gradient that keeps drifting toward the reading direction, saying "this
   section continues, explore it" without a single extra element. Sections
   stay visually separated by their panels; the motion lives only in the
   4px ribbon. */
.live-heading { position: relative; padding-bottom: 0.55rem; }
.live-heading::after {
  content: ""; position: absolute; left: 0; bottom: 0;
  width: 5.5rem; height: 4px; border-radius: 2px;
  background: linear-gradient(90deg,
    var(--brand) 0%, var(--brand) 35%,
    var(--brand-soft) 50%,
    var(--brand) 65%, var(--brand) 100%);
  background-size: 200% 100%;
}
/* The ribbon used to run `headingFlow 2.8s linear infinite`, and
   `background-position` is not a compositor property: every heading on the
   page repainted forty times a second for as long as the page was open, on
   the surface whose readers hold the weakest phones, to signal nothing. It
   now moves ONCE as the section arrives, which is the moment the cue is
   actually about, and then holds still. */
@media (prefers-reduced-motion: no-preference) {
  .live-heading::after { animation: headingFlow 900ms var(--ease-out) 1 both; }
  @keyframes headingFlow { from { background-position: 200% 0; } to { background-position: 0% 0; } }
}

/* The hero breathes: two soft brand washes drifting very slowly behind the
   text. Pure decoration, aria-invisible, compositor-only. */
.hero { position: relative; overflow: hidden; }
/* Two 48px-blurred discs drifted here for ever on 14s and 18s loops. A blur
   that large over a 22rem layer is expensive on a low-end Android even when
   the transform itself is composited, and the movement said nothing. The wash
   stays — it is what stops the hero reading as a bare rectangle — but it is
   now a gradient that costs one paint and never moves. */
.hero::before {
  content: ""; position: absolute; z-index: -1; pointer-events: none;
  inset-inline-end: -14rem; inset-block-start: -12rem;
  width: 34rem; height: 34rem; border-radius: 50%;
  background: radial-gradient(circle,
    color-mix(in srgb, var(--brand) 13%, transparent) 0%,
    transparent 62%);
}

/* ==========================================================================
   The assistant.

   This page had no visual object at all — a heading, a disclaimer, then a
   transcript — although it is the one screen where the machine is actually in
   front of the reader. The object arrives here in violet, and violet appears
   nowhere else on this surface, which is the only reason it can be read as
   "a machine is doing something" rather than as decoration.
   ========================================================================== */
.assistant { max-width: 46rem; }
.assistant-head {
  display: grid; gap: var(--s-4); align-items: center;
  padding-block: var(--s-3) var(--s-5);
  --mark-size: 6rem;
}
@media (min-width: 640px) {
  .assistant-head { grid-template-columns: auto 1fr; gap: var(--s-5); --mark-size: 8rem; }
}
.assistant-head h1 { margin-block: 0 var(--s-3); }
/* The disclaimer is not clutter: it is the page telling the truth about
   itself. It reads as a note rather than a warning, because nothing has gone
   wrong. */
.ai-note {
  margin: 0; padding: var(--s-3) var(--s-4);
  border-inline-start: 3px solid var(--ai-line); border-radius: var(--r-sm);
  background: var(--ai-soft); color: var(--text); font-size: var(--fs-body);
}
.assistant-empty {
  border: 1px solid var(--border); border-radius: var(--r-md);
  background: var(--surface); padding: var(--s-5); margin-bottom: var(--s-4);
}
.assistant-empty h2 { margin-block: 0 var(--s-2); font-size: var(--fs-h2); }
.assistant-empty .muted { margin-block: 0 var(--s-4); }

/* Starter prompts and clarification chips are the same object, because they
   do the same job: one tap that becomes the next message. They are real submit
   buttons, so the page keeps working with scripting off. */
.prompt-row { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.prompt-chip {
  font: inherit; font-size: var(--fs-body); font-weight: 600; cursor: pointer;
  min-height: var(--tap-min); padding: 0.5rem 0.875rem; border-radius: var(--r-pill);
  border: 1px solid var(--border-strong); background: var(--surface); color: var(--text);
  text-decoration: none; display: inline-flex; align-items: center;
}
.prompt-chip:hover { border-color: var(--ai); color: var(--ai-ink); background: var(--ai-soft); }
.prompt-chip--go { border-color: var(--brand); color: var(--brand-ink); }
.prompt-chip--go:hover { background: var(--brand-soft); border-color: var(--brand); color: var(--brand-ink); }
.prompt-chip--flat { border-style: dashed; color: var(--muted); cursor: default; }

/* The transcript. It had styles declared for it and never adopted them, so a
   question and its answer rendered as one undifferentiated block. */
.thread { list-style: none; margin: 0 0 var(--s-5); padding: 0; display: grid; gap: var(--s-4); }
.turn { display: grid; gap: var(--s-3); }
.turn-question {
  margin: 0; justify-self: end; max-width: 85%;
  padding: var(--s-3) var(--s-4); border-radius: var(--r-lg) var(--r-lg) var(--r-sm) var(--r-lg);
  background: var(--brand-soft); color: var(--text); font-weight: 600;
}
.turn-who {
  display: block; font-size: var(--fs-caption); font-weight: 700; color: var(--brand-ink);
  letter-spacing: 0.04em; text-transform: uppercase; margin-bottom: var(--s-1);
}
.turn-answer {
  display: grid; gap: var(--s-3);
  padding: var(--s-4); border: 1px solid var(--border); border-radius: var(--r-lg);
  background: var(--surface);
}
.turn-text { margin: 0; }
.turn-label { margin: 0; color: var(--muted); font-size: var(--fs-sm); font-weight: 600; }
/* Machine-written text is the one place violet is spent on type, and it is
   spent there because the reader has to be able to discount the right words. */
.ai-written {
  margin: 0; padding: var(--s-3); border-radius: var(--r-sm);
  border-inline-start: 3px solid var(--ai); background: var(--ai-soft);
  font-size: var(--fs-body);
}
.ai-written-label {
  display: block; font-size: var(--fs-caption); font-weight: 700;
  color: var(--ai-ink); letter-spacing: 0.03em; text-transform: uppercase;
  margin-bottom: var(--s-1);
}
.assistant-cards { gap: var(--s-3); }
@media (min-width: 640px) { .assistant-cards { grid-template-columns: 1fr 1fr; } }
.why-fits { margin: 0 var(--s-4) var(--s-3); color: var(--muted); font-size: var(--fs-caption); }
.ask { display: grid; gap: var(--s-3); }
.ask-actions { display: flex; flex-wrap: wrap; gap: var(--s-3); }
.assistant-foot { margin-top: var(--s-5); font-size: var(--fs-caption); }

/* The assistant's floating door. It replaces the contact bubble, which offered
   the assistant as its third item and, on the assistant page itself, linked to
   the page it was on. The object inside it is the same mark the assistant page
   opens with, so tapping a violet diamond and arriving at a violet diamond is
   one continuous gesture rather than two unrelated screens. */
.assistant-fab {
  position: fixed; z-index: var(--z-dialog);
  inset-inline-end: 1rem;
  inset-block-end: calc(4.4rem + env(safe-area-inset-bottom));
  display: grid; place-items: center;
  width: 3.5rem; height: 3.5rem; border-radius: 50%;
  background: var(--surface); border: 1px solid var(--ai-line);
  box-shadow: var(--elev-2);
  --mark-size: 2.6rem;
}
.assistant-fab .mark { gap: 0; }
.assistant-fab .mark-stage::before { inset: -25%; }
.assistant-fab:hover { border-color: var(--ai); }
.assistant-fab:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }
@media (min-width: 720px) { .assistant-fab { inset-block-end: 1.25rem; } }
@media (prefers-reduced-motion: no-preference) {
  .assistant-fab { transition: border-color var(--dur-1) var(--ease-out); }
}

.human-row {
  margin-top: var(--s-5); padding-top: var(--s-4);
  border-top: 1px solid var(--border);
}
.human-row-label {
  margin: 0 0 var(--s-3); font-size: var(--fs-sm); font-weight: 600; color: var(--muted);
}

/* The candidate's own screen. The dashboard led with a display-size greeting
   and a privacy sentence; a worker opening the app wants to know what to do,
   and everything above that answer is a cost paid on every visit. */
.greeting {
  margin-block: 0 var(--s-4); color: var(--muted); font-size: var(--fs-lead);
}
.greeting strong { color: var(--text); font-weight: 700; }
/* Screen readers still get the page's name; the eye does not need it twice
   when the tab bar already says which room this is. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}
/* One process, one row: the five-step ladder stays on the detail page, where
   there is room for it and where a person goes to read the detail. Here it is
   the step you are on and what happens next. */
.process-step {
  display: inline-flex; align-items: center; gap: var(--s-2);
  font-size: var(--fs-caption); font-weight: 700; color: var(--brand-ink);
  background: var(--brand-soft); border-radius: var(--r-pill);
  padding: 0.2rem 0.6rem;
}
.process-bar {
  height: 4px; border-radius: 2px; background: var(--n-100);
  margin-block: var(--s-3) var(--s-2); overflow: hidden;
}
.process-bar span { display: block; height: 100%; background: var(--brand); border-radius: 2px; }
/* The whole process row is the link, like the vacancy card: the button that
   used to sit under each one was a second target for a destination the row
   already named. */
.process { border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface); }
.process:hover { border-color: var(--border-strong); }
.process-link { display: block; padding: var(--s-4); text-decoration: none; color: var(--text); }
.process-link h3 { margin-block: 0 var(--s-1); font-size: var(--fs-h3); }
.process-link .muted { margin: 0; font-size: var(--fs-body); }
.process:hover .process-link h3 { color: var(--brand-ink); }
.process-line { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); margin: 0; }
.process--closed { opacity: 0.85; }
.process-count { font-size: var(--fs-caption); color: var(--muted); font-variant-numeric: tabular-nums; }
.process-next { margin: var(--s-2) 0 0; font-size: var(--fs-body); }

/* The generic thirteen-category uploader, folded away. It contradicted the
   page's own first sentence — "upload only what was requested" — by sitting
   between the requests and the list. It stays reachable and stays the no-JS
   path; it opens by itself when there is nothing requested, because then it is
   the only thing on the page that does anything. */
.other-upload { margin-block: var(--s-5); }
.other-upload > summary {
  cursor: pointer; list-style: none; display: inline-flex; align-items: center;
  gap: var(--s-2); min-height: var(--tap-min);
  font-weight: 600; color: var(--brand-ink);
}
.other-upload > summary::-webkit-details-marker { display: none; }
.other-upload > summary::before {
  content: "+"; display: grid; place-items: center;
  width: 1.5rem; height: 1.5rem; border-radius: 50%;
  background: var(--brand-soft); color: var(--brand-ink); font-weight: 700;
}
.other-upload[open] > summary::before { content: "−"; }

/* On the questionnaire the mark accompanies the question rather than
   announcing itself: a centred object with a three-line caption pushed the
   step the person is answering below the fold on a phone. Side by side, it is
   presence without cost. */
.questionnaire .mark {
  flex-direction: row; align-items: center; gap: var(--s-3);
  --mark-size: 3.25rem; margin-bottom: var(--s-4);
}
.questionnaire .mark-caption { text-align: start; max-width: 32ch; font-size: var(--fs-sm); }

/* The mark's caption is a question now; this is how it gets asked. Centred
   under the figure so the two read as one invitation rather than a picture
   with a button beside it. */
.hero-ask { display: inline-flex; margin-top: 0.75rem; }
.hero-visual { display: flex; flex-direction: column; align-items: center; }

/* --- MyID verification -------------------------------------------------- */
/*
 * The frame MyID's Web SDK loads into. Sized here rather than in the template
 * because the one thing that must not happen is a fixed pixel height on a
 * phone: the SDK draws a camera viewfinder and a pair of buttons, and a frame
 * shorter than the viewport hides the buttons below the fold on exactly the
 * devices most candidates use.
 *
 * `min()` keeps it tall on a phone (most of the viewport) without growing
 * absurdly on a desktop monitor.
 */
.myid-frame-holder {
  width: 100%;
  height: min(78vh, 720px);
  min-height: 420px;
  border-radius: var(--radius, 8px);
  overflow: hidden;
  background: var(--surface-2, #f4f5f7);
}

.myid-frame-holder iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}
