/*  ============================================================================
    Starting Point — design modernisation: the component layer
    ============================================================================

    The clean-sheet vocabulary.  Every rule here references tier 3 semantic tokens
    and nothing else — no ramp step, no literal hex, no brand or mode class.  That
    is what makes one rule correct in four palettes.

    Geometry is the geometry of the chosen explorations: 1c for the shell, 3b for
    the form controls, 5b for mobile, 6a for dense tables and 6b for the users
    list.  Where a value came from a measurement rather than a choice it says so.

    THE SEAM IS GONE.  Every rule here used to be scoped under `.sp-next` so that a
    migrated screen could opt in while `theme.css` still governed the ones that had not
    moved — seven class names collided between the two vocabularies, and two of them
    destructively: `.btn` was on every existing button, and legacy's `.tile` was a centred
    flex column with a shadow where this one is a quiet panel.

    Every screen has moved, `theme.css` is deleted, and the prefix came off in one commit.
    What that commit had to carry across first is worth knowing, because none of it was
    obvious from the file: the `@import` of Nunito (the only request for the typeface the
    whole system is built on), `.sp-root`, `.sp-reset`'s own bare selector, the bare `a`
    rules, and eight utility shims the transcribed guide content depends on.

    Removing the prefix dropped every rule's specificity by exactly one class, uniformly,
    so relative precedence held — including the three places written deliberately to
    out-specify something: `.field-tight` over `.field-row > .field`, `.trow-unread` over
    the 6a body cells, and `.tile-skill` over `.tile`.  Uniform is the safe direction, and
    all three were measured in a browser afterwards rather than trusted to the arithmetic.

    AUDIT SELECTORS BY PARSING, NOT BY READING LINES.  The original scoping pass rewrote
    only selectors that ended a line with `{` or `,`, so it left every single-line rule —
    `.navpill-on`, `.btn-p`, `.avatar`, `.modal` — unscoped: 119 of 181.  The check written
    to confirm the work shared the bug, looked at the same line-endings, and reported
    clean.  What found it was measuring the active nav pill in the running app and getting
    `rgba(0, 0, 0, 0)`.  A verification that shares the bug it is checking for is not a
    verification — and the removal pass was written to track comment state for the same
    reason, so prose mentioning the prefix was left for a human to rewrite.

    This file is the draft of the stylesheet `crates/ui-core`'s components carry.
    ============================================================================  */
/*  ----------------------------------------------------------------------------
    The base reset.

    The rules that reach the document rather than a component, and they
    are unscoped because they are about the DOCUMENT rather than about a screen: no
    selector reaches inside the app, so nothing here can restyle an unmigrated screen.

    They also close a defect older than this work.  Neither `theme.css` nor the design
    capture contained a single `html` or `body` rule, so the user agent's
    `body { margin: 8px }` was in force on every screen the application has ever
    rendered — an 8px frame around the whole app, which is why the shell could never
    reach the edge of the window.  Found by the user looking at it, not by any of the
    gates: the 68px bar measures 68px whether or not it starts 8px in from the left.

    `height: 100%` on both is what makes a percentage height mean anything further down:
    a percentage resolves against the parent's height, so a chain that starts at an
    `auto`-height `<html>` resolves to `auto` all the way.

    Deliberately no `background` on `body`.  The palette tokens are declared on the
    wrapper `<div>` that carries `brand-sp light`, and custom properties inherit
    downward only — `var(--surface-canvas)` on `body` would resolve to nothing at all.
    `.shell-next` is `min-height: 100vh` and paints the canvas itself, so the body's own
    surface is never visible.
    ----------------------------------------------------------------------------  */

html {
	height: 100%;
	/*  Stops mobile Safari inflating text in landscape, which no other browser does and
	    which silently breaks any measured type scale.  */
	-webkit-text-size-adjust: 100%;
}

body {
	height: 100%;
	margin: 0;
	padding: 0;
}


/*  ----------------------------------------------------------------------------
    Root
    ----------------------------------------------------------------------------  */
/*  `.sp-reset` ITSELF as well as its descendants.  `theme.css` had the element in this
    selector list and this file did not, so deleting that sheet would have left the one
    element carrying the class — the application root — at `content-box`.  Inert today,
    because that wrapper sets no width, padding or border, but a delta is a delta and this
    deletion is meant to be exactly neutral.  */
.sp-reset, .sp-reset *, .sp-reset *::before, .sp-reset *::after { box-sizing: border-box; }

/*  The document root, carried across from `theme.css`, which was the only sheet that ever
    declared it.  It owns the surface, the base colour and the base type for the whole page —
    which is what the guest screens and the not-found view render straight onto, having no
    frame of their own.

    The values are the new tokens rather than legacy's `--font-sans` / `--gray-800` /
    `--leading-base`, all three of which went with that sheet.  `min-height: 100%` belongs
    here because this class sits on ONE element and that element is the page; a duplicate of
    this rule lived on `.sp-next` while the seam existed, deliberately without the height,
    because that class was on three elements at once.  */
.sp-root {
	/*  The pinned mobile tab bar's height, declared here because THREE rules depend on it
	    and none of them is a descendant of the others: the bar itself, the sheet that rises
	    above it, and the content padding that clears it. Declaring it on `.tabbar` would put
	    it out of reach of the other two — which is the duplication this exists to remove.

	    The home-indicator inset is part of the measurement rather than added at each site,
	    so a phone that has one moves all three together.  */
	--tabbar-h: calc(74px + env(safe-area-inset-bottom, 0px));
	min-height: 100%;
	background: var(--surface-canvas);
	color: var(--text-2);
	font: var(--t-body);
	-webkit-font-smoothing: antialiased;
}

/*  A bare anchor, also carried from `theme.css`.  Every link the application renders
    carries a class — `.link`, `.link-plain`, `.btn`, `.lr`, `.tab`, `.cell-who` — with one
    exception: the four anchors inside the transcribed guide content, which
    `.guide-content a` styles.  This is the floor under anything that gets missed, and
    without it a classless link reverts to the user agent's blue underline.

    `--brand-text` rather than legacy's `--brand`: the latter is a FILL at 2.41:1 on paper,
    and this is type.  */
/*  `:not([class])` on the HOVER, and it is a fix rather than a refinement.

    The rest state is a floor for a classless link, which is what the comment above is
    about. The hover was the same rule without the same reasoning, and it LEAKED: a bare
    `a:hover` scores (0,1,1), so it beats every component class at (0,1,0) — meaning any
    anchor whose own rule set `text-decoration: none` at rest got an underline back on
    hover anyway.

    Audited rather than guessed, and the audit had to include the classes built in a
    closure — `.navpill` and `.tab` are `attr:class=move || …`, so a grep for a literal
    `class="…"` misses the whole navigation, which is half of what this is about.

    Of the twenty-two anchor classes this frontend emits, THREE declare their own hover
    decoration: `.link`, `.link-plain` and `.cell-who`, all of which want one. The other
    nineteen were underlining by accident — every `.btn`, `.navpill`, `.tab`, `.mark`,
    `.menu-item`, `.tile-action`, `.org-card`, `.card`, `.lr`, `.panel-open`.

    The symptom that found it: the dashboard draws `.tile tile-action` as a `<button>` for
    the two actions that open a dialog and as an `<A>` for the rest. Same class, same
    appearance, and only the anchors underlined — so the row of cards was visibly
    inconsistent with itself for a reason that had nothing to do with cards.

    A classless anchor still underlines, which is what the transcribed guide content wants
    and what the floor exists for. A classed anchor now governs its own hover, which is
    where that decision belongs.  */
a { color: var(--brand-text); text-decoration: none; }
a:not([class]):hover { color: var(--brand-text-hover); text-decoration: underline; }
/*  The colour shift stays for every anchor, classed or not — it is the hover affordance
    that survives the underline going, and a control with no hover response at all reads as
    disabled. Components that set their own `color` still win it, since this scores no
    higher than a class.  */
a:hover { color: var(--brand-text-hover); }

.sp-root h1, .sp-root h2, .sp-root h3, .sp-root p { margin: 0; }

.h-display { font: var(--t-display); color: var(--text-1); letter-spacing: -.01em; }
.h-1 { font: var(--t-h1); color: var(--text-1); letter-spacing: -.01em; }
.h-2 { font: var(--t-h2); color: var(--text-1); }
.h-3 { font: var(--t-h3); color: var(--text-1); }
.t-small { font: var(--t-small); }
.t-hint { font: var(--t-hint); color: var(--text-3); }
.t-eyebrow {
	font: var(--t-eyebrow);
	color: var(--text-3);
	letter-spacing: .09em;
	text-transform: uppercase;
}

.link {
	color: var(--brand-text);
	font-weight: 600;
	text-decoration: none;
	border-radius: var(--s-1);
}
.link:hover { color: var(--brand-text-hover); text-decoration: underline; }
.link:focus-visible { outline: 0; box-shadow: var(--focus-ring); }

/*  ----------------------------------------------------------------------------
    Button — one component.  46px, or 38px as `.btn-sm`; the type token is the
    same in both, because the height is `height` and not a line box.
    ----------------------------------------------------------------------------  */
.btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: var(--s-2);
	height: 46px;
	padding: 0 22px;
	border: 0;
	border-radius: var(--r-control);
	font: var(--t-button);
	white-space: nowrap;
	text-decoration: none;
	cursor: pointer;
	transition: background var(--m-fast), border-color var(--m-fast), box-shadow var(--m-fast);
}
.btn:focus-visible { outline: 0; box-shadow: var(--focus-ring); }
.btn:disabled, .btn[aria-disabled="true"] {
	background: var(--disabled-fill);
	color: var(--disabled-text);
	border-color: transparent;
	cursor: not-allowed;
	box-shadow: none;
}

.btn-sm { height: 38px; padding: 0 16px; border-radius: var(--r-pill); }
.btn-block { width: 100%; }
.btn-icon { width: 46px; padding: 0; }
.btn-icon.btn-sm { width: 38px; }

/*  Primary.  The fill and its type are ONE token pair — `--action-fill` with
    `--action-ink` — because the polarity differs per palette: Starting Point puts
    ink on orange, EmployabilityUK puts white on a deepened magenta.  A component
    that reached for `--brand` and assumed ink would be unreadable in one of the
    four.  */
.btn-p { background: var(--action-fill); color: var(--action-ink); }
.btn-p:hover { background: var(--action-fill-hover); }

.btn-s {
	background: transparent;
	color: var(--text-1);
	border: 1.5px solid var(--bound);
}
.btn-s:hover { border-color: var(--bound-hover); background: var(--fill); }
.btn-s:active { background: var(--fill-hover); }

.btn-g { background: transparent; color: var(--brand-text); }
.btn-g:hover { background: var(--fill); color: var(--brand-text-hover); }

.btn-d { background: var(--danger-fill); color: var(--danger-ink); }
.btn-d:hover { filter: brightness(.92); }

.btn-row { display: flex; gap: var(--s-3); flex-wrap: wrap; align-items: center; }
/*  A form's own actions sit at its trailing edge, where the eye leaves the last field.  */
.btn-row-end { justify-content: flex-end; }

/*  ----------------------------------------------------------------------------
    Fields — exploration 3b, outlined.

    White fill, a 1.5px boundary that clears 3:1 on its own surface, 48px at radius 12.

    Every rule here USED to be mirrored onto legacy's `.input` and `.label`, because the
    shared components — `reference::choice_select`, the modal chrome — were rendered by
    migrated and unmigrated screens alike and so could not be renamed.  Without the mirror
    the filter dialog rendered its Country select at legacy's 42px beside its siblings' 48.
    The mirror came off when `reference.rs`'s three selects took `.fld`, which was the last
    markup naming a legacy field class anywhere in the application.
    ----------------------------------------------------------------------------  */
.field { display: flex; flex-direction: column; gap: 6px; }
/*  A dialog body, or any run of fields.  The gap is what the inline `padding-top:12px`
    on every legacy dialog was standing in for, and it applies between every pair rather
    than only above the first.  */
.form-stack { display: flex; flex-direction: column; gap: var(--s-4); }
/*  A file input keeps the browser's own control — there is no styling a native file
    picker into the 3b vocabulary without replacing it, and a replacement is a component
    with its own keyboard behaviour.  It gets the field's type so the button beside the
    filename is not 13px Times.  */
input[type="file"] { font: var(--t-small); color: var(--text-2); }
.field-row { display: flex; gap: var(--s-4); flex-wrap: wrap; }
.field-row > .field { flex: 1 1 14rem; min-width: 0; }
/*  Narrower inside a dialog.  14rem twice plus the gap is 472px, and a medium modal's
    content box is 468 — so the name pair wrapped to two rows in the invite dialog, one
    field per line, which is the layout the row exists to avoid.  8rem still forces a
    single column on a phone, where two 8rem fields do not fit either.  */
.modal .field-row > .field { flex-basis: 8rem; }

.lbl { font: var(--t-label); color: var(--text-1); }
/*  Required is a mark AND a word to the assistive layer — never colour alone,
    which is the sixteenth legacy finding.  */
.lbl .req { color: var(--state-bad-text); margin-left: 3px; }
.hint { font: var(--t-hint); color: var(--text-3); }
.err { font: var(--t-hint); color: var(--state-bad-text); font-weight: 600; }

.fld {
	height: 48px;
	width: 100%;
	padding: 0 15px;
	border: 1.5px solid var(--bound);
	border-radius: var(--r-control);
	background: var(--surface-raised);
	color: var(--text-1);
	font: var(--t-control);
	transition: border-color var(--m-fast), box-shadow var(--m-fast);
}
.fld::placeholder { color: var(--text-placeholder); }
.fld:hover { border-color: var(--bound-hover); }
.fld:focus {
	outline: 0;
	border-color: var(--focus-line);
	box-shadow: var(--focus-ring);
}
.fld:disabled {
	background: var(--disabled-fill);
	border-color: var(--surface-hair);
	color: var(--disabled-text);
	cursor: not-allowed;
}
/*  The error boundary is `--state-bad-mark`, which clears 3:1 in all four
    palettes; the message under it is `--state-bad-text`, which clears 4.5:1.
    The two are different tokens because they have different jobs.  */
.fld[aria-invalid="true"] { border-color: var(--state-bad-mark); }
.fld[aria-invalid="true"]:focus { box-shadow: 0 0 0 3px color-mix(in srgb, var(--state-bad-mark) 26%, transparent); }

textarea.fld, textarea.input { height: auto; min-height: 108px; padding: 12px 15px; line-height: 24px; resize: vertical; }

/*  A multiple select sizes itself from its `size` attribute, and the 48px control height
    overrides that: the filter dialog's three multi-selects each showed one row of five
    and scrolled, where legacy shows all five.  Measured — `height: 48px` on a
    `size="5"` select is not a smaller box, it is a broken one.  */
select[multiple].fld, select[multiple].input {
	height: auto;
	padding: 8px 10px;
	background-image: none;
}

/*  A select carries its own chevron: the UA one cannot be recoloured per
    palette, and on dark it renders near-invisible.  */
select.fld, select.input {
	appearance: none;
	padding-right: 40px;
	background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
		linear-gradient(135deg, currentColor 50%, transparent 50%);
	background-position: calc(100% - 21px) 21px, calc(100% - 15px) 21px;
	background-size: 6px 6px, 6px 6px;
	background-repeat: no-repeat;
}

.check { display: flex; align-items: flex-start; gap: 10px; cursor: pointer; }
.check input {
	appearance: none;
	flex: none;
	width: 20px;
	height: 20px;
	margin: 2px 0 0;
	border: 1.5px solid var(--bound);
	border-radius: 6px;
	background: var(--surface-raised);
	cursor: pointer;
	transition: background var(--m-fast), border-color var(--m-fast);
}
.check input[type="radio"] { border-radius: var(--r-round); }
.check input:hover { border-color: var(--bound-hover); }
.check input:checked { background: var(--action-fill); border-color: var(--action-fill); }
.check input:checked::after {
	content: "";
	display: block;
	width: 100%;
	height: 100%;
	background: var(--action-ink);
	clip-path: polygon(16% 52%, 38% 74%, 84% 26%, 92% 36%, 38% 90%, 8% 60%);
}
.check input[type="radio"]:checked::after {
	clip-path: none;
	border-radius: var(--r-round);
	transform: scale(.42);
}
.check input:focus-visible { outline: 0; box-shadow: var(--focus-ring); }
.check span { font: var(--t-small); color: var(--text-1); }
/*  A set of checkboxes read as one answer — Open To's job types, its locations — wraps
    rather than stacking, because eight one-word options down a column is eight rows of
    mostly empty card. `--s-3` across and `--s-2` down: tighter vertically so a wrapped
    line still reads as part of the same set rather than as a new one.  */
.check-row { display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-3); }
/*  216 administrative sub-areas do not belong at full height inside a card: the set
    scrolls in place instead. The height is 220px, as the panel had inline, which is about
    six wrapped lines — enough to look scrollable rather than cut off.  */
.check-scroll { max-height: 220px; overflow-y: auto; }

.switch { display: inline-flex; align-items: center; gap: 10px; cursor: pointer; }
.switch input {
	appearance: none;
	position: relative;
	width: 44px;
	height: 26px;
	margin: 0;
	border: 1.5px solid var(--bound);
	border-radius: var(--r-round);
	background: var(--fill);
	cursor: pointer;
	transition: background var(--m-fast), border-color var(--m-fast);
}
/*  The knob, and its colour is per STATE rather than fixed.

    It was `--surface-raised` — white — in both states, which measured 2.52:1 against the
    checked orange and 1.22:1 against the unchecked sand.  SC 1.4.11 wants 3:1 for what
    identifies a control's state, and 2.52:1 is the very brand-orange-on-white figure this
    modernisation exists to remove.  Found by rendering the component for the first time on
    `/integrations`; it had been in this file unused since the foundations.

    A single token cannot fix both states, because the two tracks are different KINDS of
    token: the checked track is `--action-fill`, whose matching ink is `--action-ink` by
    definition, and the unchecked track is `--fill`, a SURFACE, whose matching ink is
    `--text-1`.  Using `--action-ink` for both leaves EUK light at 1.22:1 (its
    `--action-ink` is white and its `--fill` is near-white pink) and both dark palettes at
    ~1.3:1 (near-black ink on a dark track).

    Measured across all four palettes, both states, worst pair 5.66:1.  */
.switch input::after {
	content: "";
	position: absolute;
	top: 2px;
	left: 2px;
	width: 19px;
	height: 19px;
	border-radius: var(--r-round);
	background: var(--text-1);
	box-shadow: var(--lift-1);
	transition: transform var(--m-fast), background var(--m-fast);
}
.switch input:checked { background: var(--action-fill); border-color: var(--action-fill); }
.switch input:checked::after { transform: translateX(18px); background: var(--action-ink); }
.switch input:focus-visible { outline: 0; box-shadow: var(--focus-ring); }

/*  ----------------------------------------------------------------------------
    Surfaces
    ----------------------------------------------------------------------------  */
.card {
	background: var(--surface-raised);
	border-radius: var(--r-card);
	box-shadow: var(--lift-1);
	padding: 22px 24px;
}
.card-head {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--s-3);
	margin-bottom: var(--s-4);
}

/*  A sunken tile is how a card holds a group without a second boundary.  */
.tile {
	background: var(--surface-sunken);
	border-radius: var(--r-tile);
	padding: 18px;
}
/*  An interactive tile — a link or a button that IS the whole tile.  The design file
    carried these four declarations as inline styles on each specimen; a screen gets
    them as a class.  `text-align` and `border` are only needed because a <button> is
    one of the two shapes this takes.  */
.tile-action {
	display: block;
	width: 100%;
	border: 0;
	text-align: left;
	text-decoration: none;
	cursor: pointer;
	transition: box-shadow var(--m-fast), background var(--m-fast);
}
.tile-action:hover { background: var(--fill); }
.tile-action:focus-visible { outline: 0; box-shadow: var(--focus-ring); }
.tile-action .h-2 { margin-top: 6px; }

.tile-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: var(--s-3); }

/*  A tile that is one skill in the passport strip.  `14 Screens` drew these as 176px tiles
    with the status pill above the name; the width is what makes four fit across a 1064px
    column and two across a phone.

    This rule used to sit at the foot of the file, below a seam reset that gave `.tile`
    `display: block` to undo a legacy leak — written above it first, and the passport's pill,
    name and evidence line all ran together on one line.  With `theme.css` gone there is no
    leak to undo and `.tile` declares no `display` at all, so order no longer matters and the
    rule lives beside the component it modifies.  */
.tile-skill {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--s-2);
	width: 176px;
	padding: 14px;
	/*  `.chip-row` centres its children, which is right for chips of one line and wrong
	    for tiles whose names run to two: the row staggered, with the taller tiles floating
	    between the shorter ones.  Stretch makes every tile in a row the same height.  */
	align-self: stretch;
}

/*  A tile grid whose labels are sentences rather than words — the reporting screen's eight
    figures, where "Work experience applications" at a 9rem floor wrapped to FOUR lines and
    stretched every tile in the row to 140px. 15rem gives five columns at 1360 and at most
    two lines. DELIBERATELY AFTER `.tile-grid`: both selectors are (0,2,0) and the element
    carries both classes, so file order is what decides this one.  */
.tile-grid-wide { grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); }
.stat-n { font: var(--t-display); color: var(--text-1); display: block; }
.stat-l { font: var(--t-eyebrow); color: var(--text-3); letter-spacing: .09em; text-transform: uppercase; }

/*  The hero: the brand concentrated into one card rather than washed across a
    panel.  White type is safe anywhere on the ramp in all four palettes — the
    EmployabilityUK stop is the deepened magenta for exactly that reason.  */
.hero {
	background: linear-gradient(118deg, var(--grad-a), var(--grad-b));
	border-radius: var(--r-card);
	border-bottom-right-radius: var(--r-silhouette);
	color: var(--hero-ink);
	padding: 26px 30px 28px;
	box-shadow: var(--lift-2);
}
.hero .h-display, .hero .h-1 { color: var(--hero-ink); }
.hero .sub {
	font: var(--t-body);
	color: color-mix(in srgb, var(--hero-ink) 84%, transparent);
	margin-top: 6px;
}

.flash {
	display: flex;
	align-items: flex-start;
	gap: 10px;
	border-radius: var(--r-tile);
	padding: 13px 18px;
	font: var(--t-small);
	font-weight: 600;
}
.flash-ok { background: var(--state-ok-fill);   color: var(--state-ok-text); }
.flash-wait { background: var(--state-wait-fill); color: var(--state-wait-text); }
.flash-bad { background: var(--state-off-fill);  color: var(--state-bad-text); }
.flash-note { background: var(--surface-sunken);  color: var(--text-1); }

/*  The dim behind a dialog, and the box that centres it.
    ============================================================================
    THIS RULE WAS DEAD AND THE CLASS THAT NEEDED IT HAD NONE.  `modal.rs` emitted
    legacy's `.modal-backdrop`, which `theme.css` declared and this file never did, while
    `.scrim` sat here matching nothing.  Deleting that sheet took the only rule the
    emitted class had, so every dialog in the application rendered INLINE in the page
    flow — no fixed position, no centring, no dim — from a card halfway down the content
    column.  It rendered, so nothing erred: not clippy, not 30 test binaries, not 180
    browser specs, all of which assert content INSIDE `.modal` and never where the dialog
    sits.  Found by opening one.

    The same mismatch as the dialog widths one block down, one layer up: a class the CSS
    defines, a different class the Rust emits, and no gate comparing the two.  The fix is
    the same too — the emitter now uses this file's name.

    `z-index` ABOVE the menus, which take 40.  Legacy's backdrop was also 40, so a
    dropdown left open behind a dialog would tie with it and win on document order.  A
    dialog is modal; nothing may paint over it.  */
.scrim {
	position: fixed;
	inset: 0;
	z-index: 50;
	background: var(--scrim);
	display: flex;
	align-items: center;
	justify-content: center;
	padding: var(--s-6);
}
/*  `max-height` and its OWN scroll, which is the third thing the legacy sheet's modal
    rules carried and this file did not.  It only became visible once the scrim was fixed:
    while the backdrop was rendering static, a tall dialog simply made the PAGE longer and
    everything in it stayed reachable by scrolling.  Fixed, the dialog overflows the
    viewport and its footer becomes unclickable — which is how the browser suite reported
    it, five specs failing with "element is outside of the viewport" on an Apply button.

    `100%` of the scrim's content box rather than a viewport unit, so the scrim's own
    padding is subtracted once and the dialog never touches the screen edge.  */
.modal {
	background: var(--surface-raised);
	border-radius: var(--r-tile);
	box-shadow: var(--lift-3);
	width: 100%;
	max-width: 520px;
	max-height: 100%;
	overflow-y: auto;
	padding: 24px 26px 22px;
}
/*  The four steps from `06 Explorations - Dialog Sizing`, in pixels rather than legacy's
    percentages of the viewport — 25/50/75/91.67%, which made the same dialog a different
    shape on every screen and took none of its numbers from the form inside it.  Each is a
    `max-width` over the `width: 100%` above, so a dialog is never wider than its step and
    never wider than the screen.

    NAMED BY PURPOSE, and that is deliberate.  These were `-s`, `-l` and `-x`, which are
    size-shaped names that invite picking by feel — and which nothing ever emitted, because
    `ModalWidth::class()` was emitting legacy's `modal-small`/`-medium`/`-large`/`-xl`
    instead.  The mismatch was silent: `.modal`'s own `max-width: 520px` applied to
    everything, so EVERY dialog in the application rendered at 520 whatever it asked for,
    measured on a Small filter and a Medium invite alike.  A purpose name states the rule.

    The rule, from the exploration: **a tall control forces the wide step, even in a short
    form** — two fields and a 156px multiple select do not fit a 520 column, because it is
    the control's height that breaks the dialog rather than the number of rows.  */
.modal-confirm { max-width: 400px; }
.modal-wide    { max-width: 720px; }
.modal-canvas  { max-width: 960px; }
.modal-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s-4); margin-bottom: var(--s-4); }
.modal-foot { display: flex; justify-content: flex-end; gap: var(--s-3); margin-top: var(--s-5); }

/*  The bulk-invite job report.  Three counts rather than one total, so each needs to be
    readable on its own line — and the list keeps a marker, because it IS a list.  */
.job-status { color: var(--text-1); font-weight: 600; }
.job-counts { margin: 0; padding-left: 18px; color: var(--text-2); }
.job-running { margin-top: var(--s-3); }

/*  ----------------------------------------------------------------------------
    Status pill — dot AND word, on a fill/type token pair.  This is the answer to
    "status is signalled by colour alone, in the brand's own hue".  The 1px
    boundary is the pill's type colour: the fill is only 1.1:1 against the surface
    it sits on, so without it the pill has no discernible shape.
    ----------------------------------------------------------------------------  */
.pill {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	height: 24px;
	padding: 0 10px;
	border-radius: var(--r-round);
	font: var(--t-pill);
	white-space: nowrap;
	box-shadow: inset 0 0 0 1px currentColor;
}
.pill::before {
	content: "";
	width: 7px;
	height: 7px;
	border-radius: var(--r-round);
	background: currentColor;
	flex: none;
}
.p-ok { background: var(--state-ok-fill);   color: var(--state-ok-text); }
.p-wait { background: var(--state-wait-fill); color: var(--state-wait-text); }
.p-off { background: var(--state-off-fill);  color: var(--state-off-text); }

/*  ----------------------------------------------------------------------------
    Meter — the employability passport's progress, and any other proportion.
    ----------------------------------------------------------------------------  */
/*  This is the one component that uses `--brand` directly: a filled proportion is
    a mark rather than type, so the 3:1 rule applies and the brand hue is allowed
    to be itself.  The figure beside it is `--text-1`, never the brand — orange as
    type is `--brand-text` and magenta as type is a different hex again.  */
.meter {
	height: 10px;
	border-radius: var(--r-round);
	background: var(--fill);
	overflow: hidden;
}
.meter > i {
	display: block;
	height: 100%;
	border-radius: var(--r-round);
	background: var(--brand);
	transition: width var(--m-base);
}
.meter-row { display: flex; align-items: center; gap: var(--s-3); }
.meter-row .meter { flex: 1; }
.meter-fig { font: var(--t-body-strong); color: var(--text-1); font-variant-numeric: tabular-nums; }

/*  ----------------------------------------------------------------------------
    Dense table — exploration 6a.  42px head, 56px rows, checkbox selection with
    an ink bulk bar, filters as chips.
    ----------------------------------------------------------------------------  */
.tbl-wrap {
	background: var(--surface-raised);
	border-radius: var(--r-card);
	box-shadow: var(--lift-1);
	overflow: hidden;
}
/*  A table wider than its frame scrolls inside its own wrapper rather than pushing
    the page sideways.  `overscroll-behavior-x: contain` stops a trackpad swipe from
    navigating the browser back, and the element takes `tabindex="0"` in the markup
    so the keyboard can reach the columns a mouse can.  */
.tbl-scroll { overflow-x: auto; overscroll-behavior-x: contain; }
.tbl-scroll:focus-visible { outline: 0; box-shadow: inset 0 0 0 2px var(--focus-line); }

.tbl-6a { width: 100%; border-collapse: collapse; font: var(--t-small); }
.tbl-6a thead th {
	height: 42px;
	padding: 0 16px;
	background: var(--surface-sunken);
	border-bottom: 1px solid var(--surface-hair);
	text-align: left;
	font: var(--t-eyebrow);
	color: var(--text-3);
	letter-spacing: .09em;
	text-transform: uppercase;
	white-space: nowrap;
}
/*  `:is(td, th)` because the reporting table makes each candidate's name a
    `<th scope="row">` — the correct element for a row header, and one the browser draws
    bold and centred by default, which would put that column out of step with the eight
    beside it.  Every body-cell rule takes both for the same reason: a hover background on
    eight cells of nine reads as a rendering fault.  */
.tbl-6a tbody :is(td, th) {
	height: 56px;
	padding: 0 16px;
	border-bottom: 1px solid var(--surface-hair);
	color: var(--text-2);
	vertical-align: middle;
	text-align: left;
	font-weight: inherit;
}
.tbl-6a tbody tr:last-child :is(td, th) { border-bottom: 0; }
.tbl-6a tbody tr:hover :is(td, th) { background: var(--surface-sunken); }
.tbl-6a tbody tr[aria-selected="true"] td { background: var(--fill); }
.cell-strong { font: var(--t-body-strong); color: var(--text-1); }
.cell-num { text-align: right; font-variant-numeric: tabular-nums; }
/*  A reference slug, which `13 Screens` drew as a `<code>`.  A class rather than the tag,
    because the tag would need the same rule anyway and the legacy sheet styled neither —
    and the meaning is what earns the second family: this string is a LITERAL somebody copies
    into a configuration file, not prose.  Legacy showed it as a grey second line under the
    name, where nothing distinguished it from a subtitle.  */
.slug { font: var(--t-mono); color: var(--text-3); }

.avatar {
	width: 32px;
	height: 32px;
	border-radius: var(--r-round);
	background: var(--fill);
	color: var(--text-2);
	display: inline-flex;
	align-items: center;
	justify-content: center;
	font: var(--t-pill);
	flex: none;
}
/*  A person's name does not wrap.  In a 56px row a two-line name is cramped, and
    the table already scrolls, so the column taking the width it needs costs
    nothing.  Seen in the reporting specimen before it was fixed.  */
.cell-who { display: flex; align-items: center; gap: 10px; white-space: nowrap; }
/*  A name that is a link, in a table where nothing says so. Legacy links it too and gives no
    indication either, which is the same defect as the sixteen nav links that went nowhere,
    inverted: the affordance is missing rather than the destination.

    Measured before this: the anchor arrived carrying `theme.css`'s brand orange, which is
    2.52:1 on white — invisible only because `.cell-strong` inside it sets a colour of its
    own. A cell that ever holds bare text inside the link would have rendered it.  */
a.cell-who { color: inherit; text-decoration: none; border-radius: var(--s-1); }
a.cell-who:hover .cell-strong { text-decoration: underline; }
a.cell-who:focus-visible { outline: 0; box-shadow: var(--focus-ring); }

.bulkbar {
	display: flex;
	align-items: center;
	gap: var(--s-4);
	height: 52px;
	padding: 0 16px;
	background: var(--bar-fill);
	color: var(--bar-ink);
	font: var(--t-small);
	font-weight: 600;
}
.bulkbar .btn-s { color: var(--bar-ink); border-color: color-mix(in srgb, var(--bar-ink) 46%, transparent); }
.bulkbar .btn-s:hover { background: color-mix(in srgb, var(--bar-ink) 12%, transparent); border-color: var(--bar-ink); }
.bulkbar .spacer { flex: 1; }

.chip {
	display: inline-flex;
	align-items: center;
	gap: 7px;
	height: 32px;
	padding: 0 13px;
	border-radius: var(--r-round);
	background: transparent;
	color: var(--text-2);
	font: var(--t-small);
	font-weight: 600;
	cursor: pointer;
	box-shadow: inset 0 0 0 1px var(--bound);
	transition: background var(--m-fast), box-shadow var(--m-fast);
}
.chip:hover { background: var(--fill); }
.chip:focus-visible { outline: 0; box-shadow: inset 0 0 0 1px var(--bound), var(--focus-ring); }
.chip-on { background: var(--fill); color: var(--text-1); box-shadow: inset 0 0 0 1.5px var(--bound-hover); }
.chip-on:hover { background: var(--fill-hover); }

/*  A chip that is a real `<button>`, for the mailbox switcher. `.chip` was drawn for the applied-filter
    pills on the users index, which are buttons too — this only resets what a `<button>` brings with it
    and `.chip` does not already set, which is the lesson from reusing `.lr` for a different affordance:
    borrowing a component means resetting every property the new context inherits.  */
.chip-btn {
	border: 0;
	font-family: inherit;
	appearance: none;
}

/*  A chip's name, apart from the count beside it.  */
.chip-label { white-space: nowrap; }

/*  The count beside a mailbox's name. This is the whole reason the one-table redesign was available:
    `19 Screens` deferred it because a mentor would lose "is there anything in the approval queue" at a
    glance, and a number on the chip is that glance.  */
.chip-count {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-width: 20px;
	height: 20px;
	padding: 0 6px;
	border-radius: var(--r-round);
	background: var(--fill);
	color: var(--text-2);
	font: var(--t-hint);
	font-weight: 700;
}
/*  On the selected chip the count sits on `--fill` already, so it needs its own ground to stay legible
    as a separate thing rather than merging into the chip.  */
.chip-on .chip-count { background: var(--surface-raised); color: var(--text-1); }

/*  A dot on a button that says a filter is applied while the dialog that set it is closed. Without it a
    reader can be looking at a narrowed list with nothing on screen saying so — and then read the count
    beside it as the whole truth.  */
.btn-dot::after {
	content: "";
	width: 6px;
	height: 6px;
	margin-left: 7px;
	border-radius: var(--r-round);
	background: var(--action-fill);
	flex: none;
}

/*  The mailbox switcher's own row, so it can wrap at 390 without the search box wrapping with it.  */
.msg-boxes { flex-wrap: wrap; gap: var(--s-2); }
.chip-row { display: flex; gap: var(--s-2); flex-wrap: wrap; align-items: center; }

/*  A tag is a chip that is NOT a control, and it sits here so the difference is impossible
    to miss when reaching for one of them.

    `.chip` above carries `cursor: pointer`, a hover and a focus ring because it is a
    control — a filter you click. Open To's chosen job types and locations are read back,
    not clicked, and reusing `.chip` for them promises an interaction that does not exist:
    the pointer changes, the background lifts, nothing happens. That is the same lie the
    sixteen nav links that went nowhere told, in miniature.

    Same silhouette, none of the affordances. Recorded as an open question in
    `16 Adoption - Batch 3` and settled the way it recommended.  */
.tag {
	display: inline-flex;
	align-items: center;
	height: 28px;
	padding: 0 11px;
	border-radius: var(--r-round);
	background: var(--surface-sunken);
	color: var(--text-2);
	font: var(--t-small);
	font-weight: 600;
}
.tag-row { display: flex; gap: var(--s-2); flex-wrap: wrap; }

/*  Row actions never wrap.  `.btn-row` wraps by design, but inside a 56px row a
    wrapped second line makes that one row 89px and breaks the rhythm of every
    other one — measured in the catalogue specimen.  The column takes the width it
    needs instead, which the scroller absorbs.  */
.tbl-6a .btn-row { flex-wrap: nowrap; }

.pager { display: flex; align-items: center; gap: var(--s-3); padding: 14px 16px; }
.pager .count { font: var(--t-hint); color: var(--text-3); margin-right: auto; }

/*  ----------------------------------------------------------------------------
    List and panel — exploration 6b, for the users screen.  76px rows beside a
    358px detail panel, so choosing a person does not cost a navigation.
    ----------------------------------------------------------------------------  */
.split { display: flex; gap: 18px; align-items: flex-start; }
.split > .tbl-wrap, .split > .lr-wrap { flex: 1; min-width: 0; }
/*  The content column beside a rail, where that column is a STACK rather than one table
    or row list. `14 Screens` drew the profile's column with an inline
    `flex:1;min-width:0;…;gap:18px`, which is this rule — named because eleven panels in
    one column is the layout, and an inline style cannot be reviewed against the token
    scale. `min-width: 0` is load-bearing: without it a wide child (a chip row that does
    not wrap, a table) pushes the flex item past its share and the rail loses its 358.  */
.split-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 18px; }
/*  And the same on every child, which is not belt-and-braces.  A flex item defaults to
    `min-width: auto` — it refuses to shrink below its own min-content — so ONE wide
    descendant makes the item, the column and the page wider than the viewport.  Measured
    at 390 with the profile's unmigrated panels still in the column: a legacy table row
    whose min-content is 1397px took the whole page to a 1429px scroll, where the same
    row on the unmigrated screen overflows by 18. Wide content still has to scroll — that
    is `.tbl-wrap`'s job — but it scrolls inside its own panel rather than dragging the
    page with it.  */
.split-main > * { min-width: 0; }


.lr-wrap {
	background: var(--surface-raised);
	border-radius: var(--r-card);
	box-shadow: var(--lift-1);
	overflow: hidden;
}
.lr {
	display: flex;
	align-items: center;
	gap: var(--s-4);
	width: 100%;
	min-height: 76px;
	padding: 0 20px;
	border: 0;
	border-bottom: 1px solid var(--surface-hair);
	background: transparent;
	text-align: left;
	cursor: pointer;
	transition: background var(--m-fast);
}
/*  `.lr` was written for a `<button>`, and a row that navigates is an `<a>`.  A button
    carries no underline and inherits its colour; an anchor gets both from the UA, so a
    row-as-link arrives underlined in link blue with its `.lr-name` and `.lr-meta` colours
    overridden.  On the component, not on each call site — the jobs batch has three.  */
a.lr { text-decoration: none; color: inherit; }
.lr:last-child { border-bottom: 0; }
.lr:hover { background: var(--surface-sunken); }
.lr:focus-visible { outline: 0; box-shadow: inset 0 0 0 2px var(--focus-line); }
.lr-on { background: var(--fill); }
.lr .avatar { width: 40px; height: 40px; font: var(--t-small); font-weight: 700; }
/*  A row list nested INSIDE a card, and rows that are list items rather than controls.
    `14 Screens` drew the profile's history panels with three inline overrides on every
    row — `min-height:64px;padding:0 16px;cursor:default` — plus `box-shadow:none` on the
    wrapper. These are those, named.

    `.lr` is built for the users list, where a row IS the click target: 76px, its own
    padding, a pointer. Inside a card at 64px it is a line in a list, and a pointer cursor
    on something that does not respond to a click is the same lie the top bar's dead links
    told. The last row already loses its rule — `.lr:last-child` above — so nothing here
    needs to repeat that.  */
.lr-wrap-flush { box-shadow: none; }
.lr-static { min-height: 64px; padding: 0 16px; cursor: default; }
/*  And the hover goes with the cursor. `cursor: default` alone left `.lr:hover`'s sand
    background in place, so a row that does nothing when clicked still lit up under the
    pointer — the same promise the cursor was changed to stop making, in a different
    property. Found in a screenshot taken with the pointer resting on a row, three panels
    after the cursor was fixed: a property-by-property reset is what a shared component
    needs, not one property and an assumption about the rest.  */
.lr-static:hover { background: transparent; }
/*  A tighter row for a list whose rows carry no avatar: the applications lists and the
    advert's applicants panel.  60px, which is what `13 Screens` drew inline for all three
    and `17 Screens` names.  */
.lr-compact { min-height: 60px; padding: 0 16px; }

/*  A block of the advert's own prose — a role description, a testimonial.  `white-space:
    pre-wrap` because the copy arrives with the line breaks whoever wrote it typed, and
    collapsing them turns three paragraphs into one.

    `:not(:last-child)` on the gap: the description is followed by a `.kv` and needs air
    between them — measured flush, the role description sat directly on the first label —
    while the same class is the LAST thing in About The Company and a trailing margin there
    is dead space inside the card.  */
.prose { white-space: pre-wrap; }
.prose:not(:last-child) { margin-bottom: var(--s-4); }

/*  A label-and-value list inside a `.card`.  `.panel dl` already does this for a
    panel; `13 Screens` drew the same grid inline twice for cards, which is what this names.
    The label column takes its own width so the values line up with each other.  */
.kv { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: 8px 14px; }
.kv dt { font: var(--t-hint); color: var(--text-3); }
.kv dd { margin: 0; font: var(--t-small); color: var(--text-1); }

/*  One group of an advert's required skills: an eyebrow and its tags.  Only the gap between
    groups, because the first sits directly under the card's head.  */
.skill-group-block + .skill-group-block { margin-top: var(--s-4); }

/*  ----------------------------------------------------------------------------
    Skill evidence: the two panels a skills row opens onto, inside `.lr-open`.
    ----------------------------------------------------------------------------  */

/*  One uploaded file: a 64px thumbnail, its details, and the remove action.  The rule
    beneath each item rather than a `.lr-wrap` — this list sits INSIDE the sunken
    `.lr-open`, and a white row-list surface nested in that reads as a second panel.  */
.media-item {
	display: flex;
	gap: var(--s-3);
	padding: var(--s-2) 0;
	border-bottom: 1px solid var(--surface-hair);
}
.media-item:last-of-type { border-bottom: 0; }
.media-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
/*  A file name can be one unbroken 80-character string, and a flex item will not break it
    on its own — `min-width: 0` above lets the box shrink, and this lets the WORD break.  */
.media-name { font: var(--t-body-strong); font-size: 14px; word-break: break-word; }
.media-audio { margin-top: var(--s-2); max-width: 100%; }
.media-group { margin-top: var(--s-3); }
.media-upload { margin-top: var(--s-4); }

/*  The thumbnail, and the type badge that stands in for a file that has none — a PDF, an
    audio clip. `flex: none` so neither ever gives up its 64px to the text beside it.  */
.media-thumb { flex: none; display: block; }
.media-thumb img { width: 64px; height: 64px; object-fit: cover; border-radius: var(--s-2); display: block; }
.media-badge {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 64px;
	height: 64px;
	border-radius: var(--s-2);
	background: var(--fill);
	font: var(--t-hint);
	font-weight: 700;
	/*  `--text-2`, not `--text-3`.  This badge is the only thing identifying a file with no
	    preview, and `text-3 on fill` measures 4.45:1 in the Starting Point light palette —
	    under the 4.5 floor.  Nothing caught it, because that pair was in neither the required
	    nor the exempt list; `crates/palette` now records the measurement.  */
	color: var(--text-2);
}

/*  ----------------------------------------------------------------------------
    The multi-select — exploration 4c, adopted.

    The filter's three `<select multiple>` controls were reported as too short. MEASURED
    first, and the obvious suspicion was wrong: `appearance: none` is not collapsing the
    sizing, and `size` is honoured exactly — six rows of 24px plus 8px of padding is the
    156px the skills control measures.

    The complaint is the RATIO. That box is a six-row window onto a hundred and eleven
    grouped skills: 2638px of content in 156px, six per cent visible. And the dialog holding
    it already measures 852px of a 900px viewport, so raising `size` enough to matter pushes
    the dialog off the screen. No value of `size` fixes a hundred and eleven options —
    filtering them down to a handful is the only thing that fits, which is what a type-ahead
    is and why this replaces the element rather than restyling it.

    4c's own values are exploration-era literals; restated here against tier 3, as 4b's were.
    ----------------------------------------------------------------------------  */

/*  The field. `height: auto` with a control's `min-height`, so it starts the size of a text
    input and grows as chips wrap — which is the whole reason a chip list beats a fixed
    window onto a long list.  */
.msel {
	box-sizing: border-box;
	width: 100%;
	min-height: 48px;
	height: auto;
	padding: 7px 12px;
	/*  `--r-control`, the same radius `.fld` uses: this IS a field, and a different corner
	    would make it read as a different family of control.  */
	border-radius: var(--r-control);
	border: 1.5px solid var(--bound);
	background: var(--surface-raised);
	display: flex;
	flex-wrap: wrap;
	gap: 7px;
	align-items: center;
	/*  A text cursor, because the whole field is a click target for its input — the chips
	    sit in it rather than in front of it.  */
	cursor: text;
	transition: border-color var(--m-fast), box-shadow var(--m-fast);
}
.msel:hover { border-color: var(--bound-hover); }
/*  `:focus-within` rather than `:focus`: the focus belongs to the input inside, and a field
    that shows no ring while its own input is focused reads as inert. The same reason the
    dropzone uses it.  */
.msel:focus-within {
	outline: 0;
	border-color: var(--focus-line);
	box-shadow: var(--focus-ring);
}

/*  One chosen value. A tint of the brand rather than the brand itself: these can be a dozen
    across a dialog, and a dozen saturated pills is a filter that shouts.  */
.schip {
	display: inline-flex;
	align-items: center;
	gap: 7px;
	height: 32px;
	padding: 0 6px 0 12px;
	border-radius: var(--r-round);
	background: color-mix(in srgb, var(--brand) 16%, transparent);
	/*  `--text-1`, NOT `--brand-text`. Measured in the browser, dark mode: `--brand-text` on this
	    tint is 4.07:1, under the 4.5:1 floor — because `--brand-text` is defined to be readable on
	    a SURFACE, and mixing the brand into that surface moves the ground toward the very colour
	    the text is made of. The pill keeps the tint, which is the brand signature; the label reads.  */
	color: var(--text-1);
	font: var(--t-small);
	font-weight: 700;
}
/*  The remove control, inside the pill. A real `<button>`, so it is reachable by keyboard on
    its own — a chip whose only removal is a mouse is a filter a keyboard user cannot undo.  */
.schip button {
	width: 20px;
	height: 20px;
	border: 0;
	border-radius: var(--r-round);
	/*  Same finding as the label above: this measured 2.84:1, which fails the 4.5:1 it owes as
	    text and the 3:1 it would owe as a bare control.  */
	background: color-mix(in srgb, var(--text-1) 14%, transparent);
	color: var(--text-1);
	font: var(--t-hint);
	font-weight: 700;
	line-height: 20px;
	text-align: center;
	cursor: pointer;
	padding: 0;
}
.schip button:hover { background: color-mix(in srgb, var(--text-1) 28%, transparent); }
.schip button:focus-visible { outline: 0; box-shadow: var(--focus-ring); }

/*  The type-ahead, sharing the field with the chips. Borderless and transparent because the
    FIELD is the control; a second boundary inside one would read as two.  */
.mtype {
	flex: 1;
	min-width: 90px;
	border: 0;
	background: transparent;
	outline: none;
	font: var(--t-body);
	line-height: 32px;
	color: var(--text-1);
}
.mtype::placeholder { color: var(--text-placeholder); }

/*  Inert. The region control has no options until a country is chosen, and it used to render as
    an empty 133px box that read as broken rather than waiting — so this form carries a sentence
    saying what it waits for, in place of the input.  */
.msel-off {
	background: var(--fill);
	border-color: var(--surface-hair);
	cursor: not-allowed;
}
.msel-off:hover { border-color: var(--surface-hair); }
.mtype-off {
	flex: 1;
	font: var(--t-body);
	line-height: 32px;
	color: var(--disabled-text);
}

/*  The single-value form. It replaces a `<select>`, so it carries the same chevron: a bare text box
    gives a reader no sign that a list is behind it, which is the one affordance a select never has
    to explain. Drawn the same way `select.fld` draws its own — two gradients in `currentColor`,
    because the UA arrow cannot be recoloured per palette and goes near-invisible on dark.  */
.msel-one {
	padding-right: 40px;
	background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
		linear-gradient(135deg, currentColor 50%, transparent 50%);
	background-position: calc(100% - 21px) 21px, calc(100% - 15px) 21px;
	background-size: 6px 6px, 6px 6px;
	background-repeat: no-repeat;
}

/*  The menu. `position: absolute` over the dialog rather than in flow: a filter dialog is
    already 852px of a 900px viewport, so a menu that pushed content down would move the
    field out from under the pointer that opened it.  */
.msel-wrap { position: relative; }
.listbox {
	position: absolute;
	left: 0;
	right: 0;
	top: calc(100% + var(--s-2));
	z-index: 20;
	background: var(--surface-raised);
	border: 1px solid var(--surface-hair);
	border-radius: var(--r-tile);
	box-shadow: var(--lift-3);
	overflow-y: auto;
	/*  Ten-ish rows. The point of the type-ahead is that the reader narrows rather than
	    scrolls, so this is a ceiling for the unfiltered case rather than the working size.  */
	max-height: 320px;
}
/*  A group heading, which is what keeps the skills list legible: 111 options in one flat run
    is a different defect from the one being fixed. `<optgroup>`'s replacement.  */
.lb-group {
	font: var(--t-eyebrow);
	letter-spacing: .12em;
	text-transform: uppercase;
	color: var(--text-2);
	padding: 12px 16px 6px;
}
.lb-opt {
	display: flex;
	align-items: center;
	gap: 10px;
	width: 100%;
	padding: 9px 16px;
	border: 0;
	background: transparent;
	font: var(--t-body);
	font-weight: 600;
	color: var(--text-1);
	text-align: left;
	cursor: pointer;
}
/*  Hover and KEYBOARD position share one appearance, deliberately: they mean the same thing —
    "this is the one that would be taken" — and giving them two looks would say there are two
    kinds of nextness.  */
.lb-opt:hover, .lb-opt-active { background: var(--fill); }
.lb-opt:focus-visible { outline: 0; box-shadow: inset var(--focus-ring); }
/*  A box per option, so a chosen value is visible IN THE LIST as well as as a chip. Without
    it, re-opening the menu says nothing about what is already taken.  */
.lb-box {
	flex: none;
	width: 18px;
	height: 18px;
	border-radius: 5px;
	border: 1.5px solid var(--bound);
	position: relative;
}
.lb-box-on { background: var(--action-fill); border-color: var(--action-fill); }
/*  A tick rather than a box, for the single-value menu. Same 18px slot so the rows of both controls
    line up, but no border: a checkbox on every row of a single-value list says "several", which is
    the opposite of what that control does.  */
.lb-tick { border-color: transparent; background: transparent; }
.lb-tick-on::after {
	content: "";
	position: absolute;
	left: 4px;
	top: 2px;
	width: 6px;
	height: 11px;
	border-right: 2px solid var(--brand-text);
	border-bottom: 2px solid var(--brand-text);
	transform: rotate(40deg);
}
.lb-box-on::after {
	content: "";
	position: absolute;
	left: 5px;
	top: 2px;
	width: 5px;
	height: 9px;
	border-right: 2px solid var(--action-ink);
	border-bottom: 2px solid var(--action-ink);
	transform: rotate(40deg);
}
/*  Nothing matched. A menu that renders empty looks broken; one that says so does not.  */
.lb-empty { padding: 14px 16px; font: var(--t-hint); color: var(--text-2); }
/*  The footer, which names the keys. 4c draws `.kbd` caps here, so the design intends the
    control to ADVERTISE its keyboard rather than hide it — which is the half a hand-rolled
    multi-select usually leaves out.  */
/*  Sticky, because the footer is INSIDE the scrolling box: with a hundred and eleven options it
    sat below the fold and the control documented its keyboard where nobody could read it. Seen in
    a browser, not by any gate.  */
.lb-foot {
	position: sticky;
	bottom: 0;
	border-top: 1px solid var(--surface-hair);
	padding: 10px 16px;
	background: var(--surface-sunken);
}
.lb-foot-c { font: var(--t-hint); color: var(--text-2); margin: 0; }
.kbd {
	display: inline-block;
	font: var(--t-hint);
	font-family: ui-monospace, Menlo, monospace;
	font-weight: 700;
	padding: 2px 5px;
	border-radius: 4px;
	background: var(--fill);
	color: var(--text-2);
}

/*  ----------------------------------------------------------------------------
    The upload control — exploration 4b, adopted.

    Every file input in the application was a bare `<input type="file">` until this: the
    browser's own "Choose File / No file chosen", inside screens that had all been migrated
    to the new vocabulary.  The design for it has existed since the form-controls
    exploration and was simply never brought across.

    Exploration 4b's own values were exploration-era literals — `#FFF6EC` for the drag
    tint, `rgba(255,128,1,.22)` for its ring.  Restated here against tier 3, which is what
    makes one rule correct in five brands, four of them derived from a single hue.

    TWO SUBSTITUTIONS ARE DELIBERATE, and both are contrast rather than taste:

    - `.dz-c` takes `--text-2`, where 4b used its `--ink-3` (this system's `--text-3`).
      `text-3 on fill` measures 4.45:1 in the Starting Point light palette and 4.47 in
      EmployabilityUK's — under the 4.5 floor.  `crates/palette` records the measurement;
      `.media-badge` and the evidence tile made the same move for the same reason.
    - The drag tint and ring are `color-mix` against `--brand` rather than the drawn hex,
      because a partner's dropzone must warm towards THEIR colour.  A literal peach would
      make every brand's drag state orange.
    ----------------------------------------------------------------------------  */

/*  A `<label>`, not a div with a click handler.  A label pointing at the input opens the
    file dialog natively, so keyboard, pointer and screen reader all work with no
    JavaScript — the `div` + `onClick` shape loses the keyboard, which is the finding the
    design audit already raised against the entity cards.  */
.dz {
	display: block;
	border: 1.5px dashed var(--bound);
	border-radius: var(--r-tile);
	background: var(--fill);
	padding: var(--s-5) var(--s-4);
	text-align: center;
	cursor: pointer;
	transition: background var(--m-fast), border-color var(--m-fast), box-shadow var(--m-fast);
}
.dz:hover { background: var(--fill-hover); }
/*  The focus ring lands here rather than on the input, because the input is the thing being
    hidden — `:focus-within` is what carries a keyboard user's position onto the visible
    box.  Without it the control is operable and invisible, which is worse than unstyled.  */
.dz:focus-within {
	outline: 0;
	border-color: var(--focus-line);
	border-style: solid;
	box-shadow: var(--focus-ring);
}
/*  Dragging: the boundary goes SOLID as well as changing colour.  A dashed-to-dashed colour
    change is the whole signal in 4b's light palette and nearly none of it in a dark one, so
    the style change is what carries the state for anyone who cannot see the hue shift.  */
.dz-drag {
	border-color: var(--brand);
	border-style: solid;
	background: color-mix(in srgb, var(--brand) 10%, var(--fill));
	box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 22%, transparent);
}
.dz-t { font: var(--t-body-strong); color: var(--text-1); margin: 0 0 4px; }
/*  `<em>` on the browse word, with the italic removed: it is the actionable part of the
    sentence and must look it, but it is NOT a nested control — the whole box is the
    control, and a button inside a label is a second hit target that steals the first's
    click.  */
.dz-t em { color: var(--brand-text); font-style: normal; text-decoration: underline; }
.dz-c { font: var(--t-hint); color: var(--text-2); margin: 0; }
/*  Hidden but OPERABLE.  `display: none` and `visibility: hidden` remove an input from the
    accessibility tree in some browsers, and a label cannot activate what is not there — so
    this is the clip-rect pattern instead, which keeps it focusable and clickable while
    taking it out of the layout.  */
.dz input[type="file"] {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
	border: 0;
}

/*  One picked or uploaded file.  A raised row inside whatever surface holds the control, so
    it reads as a thing that now exists rather than as more of the dropzone.  */
.dz-list { margin: var(--s-3) 0 0; }
.frow {
	display: flex;
	align-items: center;
	gap: var(--s-3);
	padding: 12px 14px;
	border: 1px solid var(--surface-hair);
	border-radius: var(--r-tile);
	background: var(--surface-raised);
	margin: 0 0 var(--s-2);
}
.frow:last-child { margin-bottom: 0; }
.frow-body { flex: 1; min-width: 0; }
/*  The type, from the MIME the server sniffed rather than from the extension.  `flex: none`
    so it never gives up its square to a long name.  */
.ftype {
	flex: none;
	width: 40px;
	height: 40px;
	border-radius: var(--s-2);
	background: var(--fill);
	color: var(--text-2);
	font: var(--t-hint);
	font-weight: 800;
	line-height: 40px;
	text-align: center;
	letter-spacing: .04em;
}
/*  A file name can be one unbroken 80-character string, which no flex item breaks on its
    own: `min-width: 0` above lets the box shrink and this lets the WORD break.  The same
    pair `.media-name` needs.  */
.fname { font: var(--t-body-strong); font-size: 14px; color: var(--text-1); margin: 0; word-break: break-word; }
.fmeta { font: var(--t-hint); color: var(--text-3); margin: 2px 0 0; }

/*  The progress track.  INDETERMINATE, and that is a decision rather than a gap: uploads go
    through `fetch`, which has no bytes-sent event at all, so a percentage here would be a
    number the client does not have.  A bar animated to 90% and parked is the version worth
    refusing — it states a fact nobody knows, and a reader who has watched it stall learns
    to distrust every other indicator in the product.

    Where a count IS known it is said in words instead: message attachments upload one at a
    time, so `.fmeta` carries "Uploading 2 of 5".  */
.prog { height: 6px; border-radius: var(--r-round); background: var(--fill); overflow: hidden; margin: 7px 0 0; }
.prog span { display: block; height: 100%; width: 40%; background: var(--brand); border-radius: var(--r-round); }
.prog-wait span { animation: dz-slide 1.15s ease-in-out infinite; }

@keyframes dz-slide {
	from { transform: translateX(-110%); }
	to   { transform: translateX(260%); }
}

/*  The first animation in this sheet, so it is also the first that has to answer for
    itself: an indefinite motion is exactly what `prefers-reduced-motion` exists for, and
    someone with a vestibular disorder should not be made to watch a bar slide for as long
    as an upload takes.  The bar stays — it still says "in flight" — it just stops moving.  */
@media (prefers-reduced-motion: reduce) {
	.prog-wait span { animation: none; width: 100%; }
}

/*  Done.  A tick rather than a colour change, because a green bar and a grey bar are the
    same bar to a reader who cannot tell them apart.  */
.tickr { flex: none; width: 22px; height: 22px; border-radius: var(--r-round); background: var(--state-ok-fill); position: relative; }
.tickr::after {
	content: "";
	position: absolute;
	left: 7px;
	top: 5px;
	width: 5px;
	height: 9px;
	border-right: 2px solid var(--state-ok-text);
	border-bottom: 2px solid var(--state-ok-text);
	transform: rotate(40deg);
}

/*  ----------------------------------------------------------------------------
    A candidate's evidence, as a grid.

    NOT `.card-grid`.  That one is three cards of prose at a 280px floor; this is thirty
    files at a glance, and the whole complaint being answered is that the dialog it replaces
    "does not utilise space correctly".  A 280px floor gives 390px one column and 1440px
    four; a 150px floor gives two and eight, which is what a wall of evidence should be.

    `auto-fill` rather than a column count, for the same reason `.card-grid` uses it: this
    grid sits in the content column at every width.
    ----------------------------------------------------------------------------  */
.evidence-filters { margin-bottom: var(--s-4); }

.evidence-grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
	gap: var(--s-3);
}

/*  A `<figure>`, because that is what an image with a caption is — and the user agent gives
    it `margin: 1em 40px`, which nothing in this sheet resets.  `.sp-root`'s reset covers
    h1-h3 and p only, so a bare element brought in for the first time carries its UA
    geometry into the grid: 40px of side margin per tile, inside a 150px track.  Reset here
    rather than added to that list, so the reason travels with the component that needs it.  */
.evidence-tile {
	margin: 0;
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: var(--s-2);
}

/*  The square.  `aspect-ratio` with `object-fit: cover` so files of every proportion —
    a portrait phone video, a landscape screenshot, an A4 page — produce one tidy grid
    rather than a ragged one, the same choice `.article-img` makes for illustrations.

    An anchor, so it is keyboard reachable and openable in a new tab.  That means undoing
    what the bare `a` rules give it: `color` (it wraps type, which must stay `--text-3`)
    and the hover underline, which needs `:hover` to out-specify `a:hover`.  */
.evidence-open {
	display: block;
	width: 100%;
	aspect-ratio: 1;
	overflow: hidden;
	border-radius: var(--r-tile);
	background: var(--fill);
	box-shadow: inset 0 0 0 1px var(--bound);
	/*  A button's own reset, and every property here is one the user agent supplies. `font`
	    is the load-bearing one: a button does NOT inherit type, so the file name inside would
	    render in the browser's 13.33px system font while every caption beside it uses
	    Nunito.  */
	border: 0;
	padding: 0;
	font: inherit;
	color: inherit;
	text-align: inherit;
	cursor: pointer;
	transition: box-shadow var(--m-fast);
}
.evidence-open:hover { box-shadow: inset 0 0 0 1px var(--bound-hover), var(--lift-2); }
.evidence-open:focus-visible { outline: 0; box-shadow: var(--focus-ring); }
.evidence-thumb { width: 100%; height: 100%; object-fit: cover; display: block; }

/*  The tile for a file that has no preview, which is a DESIGNED state and not an error:
    audio and a CV produce no thumbnail at all, and in development no seeded file is in the
    object store, so this is what a reviewer sees most.  It has to read as a deliberate
    placeholder — centred, typed, named — rather than as a broken image.  */
.evidence-typed {
	width: 100%;
	height: 100%;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: var(--s-2);
	padding: var(--s-3);
	text-align: center;
}
/*  `--text-2` for both, for the reason `.media-badge` above now records: `text-3 on fill`
    does not clear 4.5:1 in either light palette, and these two spans are the whole of what
    identifies the file.  */
.evidence-badge { font: var(--t-body-strong); color: var(--text-2); letter-spacing: .06em; }
/*  A file name can be one unbroken 80-character string, which no flex box breaks on its
    own — the same pair `.media-name` needs, plus a clamp, because here there is a fixed
    square to stay inside rather than a row that can grow.  */
.evidence-file {
	font: var(--t-hint);
	color: var(--text-2);
	word-break: break-word;
	display: -webkit-box;
	-webkit-box-orient: vertical;
	-webkit-line-clamp: 3;
	overflow: hidden;
}

/*  What the file is evidence FOR, and its verified state.  `figcaption` carries no UA
    margin, but it is reset for the same reason as the figure: so a browser that disagrees
    cannot move the grid.  */
.evidence-caption {
	margin: 0;
	min-width: 0;
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--s-1);
}
.evidence-belongs {
	font: var(--t-small);
	font-weight: 600;
	color: var(--text-1);
	word-break: break-word;
	display: -webkit-box;
	-webkit-box-orient: vertical;
	-webkit-line-clamp: 2;
	overflow: hidden;
}

/*  The lightbox's body: the item, what it is, and the reviewer's one write.

    `.evidence-open` is a BUTTON now rather than an anchor, because it opens a dialog rather
    than navigating — so the properties being reset above are a button's, not a link's. A
    user-agent button brings its own `background`, `border`, `padding`, `font` and
    `text-align: center`, none of which a tile wants, and `font: inherit` matters because a
    button does NOT inherit type by default: the file name inside would otherwise render in
    the browser's 13.33px system font while every other caption on the page uses Nunito.  */
.evidence-detail { display: flex; flex-direction: column; gap: var(--s-3); }

/*  A bounded stage rather than a natural-size one. `70vh` so a tall portrait photograph
    cannot make the dialog taller than the window — a lightbox that has to be scrolled to
    reach its own controls is the space problem this page exists to fix, reappearing one
    layer in.  */
.evidence-stage {
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: var(--s-3);
	min-height: 180px;
	max-height: 70vh;
	padding: var(--s-3);
	border-radius: var(--r-tile);
	background: var(--surface-sunken);
}
/*  `contain` rather than `cover`: a tile is a thumbnail and may crop, but this is the item
    itself and cropping it would hide the evidence.  */
.evidence-full { max-width: 100%; max-height: calc(70vh - var(--s-6)); object-fit: contain; display: block; }
.evidence-audio { width: 100%; max-width: 420px; }
/*  The typed block is shared with the tile, so it must not carry the tile's fixed square
    here — the stage owns the height.  */
.evidence-stage .evidence-typed { height: auto; }
.evidence-facts { display: flex; flex-direction: column; gap: 2px; min-width: 0; }

/*  Previous, position, next.  NOT `.pager`: that one pushes its count left with
    `margin-right: auto` and carries a list footer's 14px/16px padding, so borrowing it would
    mean overriding two of its three properties — cheaper and clearer to say this outright.
    `space-between` puts the two controls at the ends with the position between them, which is
    what a lightbox's stepper looks like everywhere.  */
.evidence-step { display: flex; align-items: center; justify-content: space-between; gap: var(--s-3); }

/*  ----------------------------------------------------------------------------
    The Goald video picker.
    ----------------------------------------------------------------------------  */
.goald-picker { margin-top: var(--s-4); }
.goald-prompt { margin-bottom: var(--s-3); }
.goald-search { margin-bottom: var(--s-3); }
/*  One attached video: a line of type and its remove action.  */
.goald-link { display: flex; align-items: center; gap: var(--s-3); padding: var(--s-1) 0; }
.goald-link-label { flex: 1; min-width: 0; word-break: break-word; }

/*  The browse grid.  `auto-fill` with a floor rather than a fixed column count, because
    this grid renders inside a sunken block inside a card inside a column — every one of
    which has its own width — and a count that fits at 1440 overflows at 390.  */
.vid-grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
	gap: var(--s-3);
}
.vid-card {
	display: flex;
	flex-direction: column;
	gap: var(--s-2);
	padding: var(--s-3);
	border-radius: var(--r-tile);
	background: var(--surface-raised);
}
/*  A 16:9 frame on a black ground: a portrait clip letterboxes rather than cropping to a
    square, and the ground is what makes the letterbox deliberate.  */
.vid-thumb {
	width: 100%;
	aspect-ratio: 16 / 9;
	object-fit: cover;
	border-radius: var(--s-2);
	background: #000;
	display: block;
}
.vid-action { margin-top: auto; }

/*  ----------------------------------------------------------------------------
    The user record: one person as somebody else sees them.
    ----------------------------------------------------------------------------  */

/*  The action row under the identity card.  It is a row of buttons, so `.btn-row` does the
    work; this is only the space above it.  */
.record-actions { margin-top: var(--s-4); }
.public-toggle { margin-top: var(--s-4); }

/*  A field that should take only the width its control needs, inside a `.field-row` whose
    other member takes the rest — the enrolment year against the form group.

    BOTH declarations are load-bearing.  `flex: none` stops the field growing, and
    `width: auto` on the control stops `.fld`'s `width: 100%` resolving against the row
    instead of the content: with only the first, the year select measured 560px for four
    digits.  */
/*  BOTH selectors, and the second is the one that works. `.field-row > .field` is
    THREE classes (0,3,0) and this was two (0,2,0), so the row's rule won on specificity
    whatever the file order — measured: the postcode kept a 224px basis and came out the
    same width as an address line. Specificity, not position, and counting it beats
    assuming it.  */
.field-tight, .field-row > .field-tight { flex: none; }
.field-tight .fld { width: auto; }
.enrolment-add { align-items: flex-end; margin-top: var(--s-4); }

/*  The identity card's own head is `.panel-head`, which carries a bottom margin for a panel
    that continues below it.  Here it IS the card, so that margin is dead space.  */
.card > .panel-head:only-child { margin-bottom: 0; }

/*  ----------------------------------------------------------------------------
    The organisation record.
    ----------------------------------------------------------------------------  */

/*  The logo beside its picker.  `align-items: center` because the two are different heights
    and neither should decide the other's baseline.  */
.logo-row { display: flex; align-items: center; gap: var(--s-4); flex-wrap: wrap; }
.logo-current {
	width: 96px;
	height: 96px;
	border-radius: var(--r-tile);
	object-fit: cover;
	display: block;
	flex: none;
}

/*  ----------------------------------------------------------------------------
    The organisations index: 6b, and the brief beside it.
    ----------------------------------------------------------------------------  */

/*  The toolbar sits above the split rather than in the page head, because it has three
    controls and the head has the page's name in it.  */
.org-toolbar { margin-bottom: var(--s-4); }
.org-search { flex: 0 0 384px; max-width: 100%; }

/*  The brief's own blocks: an address, a phone number, a paragraph. Each is a group of
    lines that needs air beneath it and nothing else.  */
.brief-block { margin-bottom: var(--s-3); }
.brief-strong { font-weight: 700; color: var(--text-1); }
/*  Every eyebrow after the first needs the space the panel's own rhythm would give it.  */
.brief-head { margin-top: var(--s-4); }
.brief-actions { margin-top: var(--s-4); }

/*  A grid of cards, for a list short enough that cards are still the right shape — the
    memberships screen, where there are one to three and each carries a paragraph. The two
    big indexes became 6b lists because ninety-one cards behind eight filters is the worst
    shape for finding one; three cards of prose is not that problem.

    `auto-fill` with a 280px floor rather than a column count: this grid sits in the content
    column at every width, and a count that fits 1440 does not fit 390.  */
.card-grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
	gap: 18px;
}

/*  A card that is itself a link. `.card` says nothing about type or decoration, because
    nothing had needed it to until a whole card became an anchor.  */
.org-card { color: inherit; text-decoration: none; display: block; }
.org-card:hover { box-shadow: var(--lift-2); }
.org-card:focus-visible { outline: 0; box-shadow: var(--focus-ring); }

/*  Three lines of a description, then an ellipsis. `-webkit-line-clamp` is the only thing
    that does this and it is prefixed in every engine that supports it, including Firefox —
    which is why the prefixed property is correct here rather than a mistake.  */
.clamp-3 {
	display: -webkit-box;
	-webkit-line-clamp: 3;
	-webkit-box-orient: vertical;
	overflow: hidden;
}

/*  A heading over a GROUP of fields rather than a label for one — the add-organisation
    dialog's "first administrator" block. Spaced like a field, so the group reads as part of
    the same form rather than as a second one.  */
.form-section { margin-top: var(--s-3); }

/*  ----------------------------------------------------------------------------
    Messages: the first screen to render exploration 6a's dense table.
    ----------------------------------------------------------------------------  */

/*  A table INSIDE a card. `.tbl-wrap` carries its own surface, radius and shadow, so nested
    in a card it draws a second boundary — the same thing `.lr-wrap-flush` exists for.  */
.tbl-wrap-flush { box-shadow: none; border-radius: 0; }

/*  A sortable column head. 6a drew the head as plain text because reporting and the
    catalogues do not sort; a mailbox does, on three of its four columns.

    A BUTTON filling the cell rather than a click handler on the `<th>`: a cell that responds
    to a click with no affordance is the pattern this redesign keeps removing, and a button is
    what a keyboard reaches. It inherits the head's own type so the row still reads as a head
    rather than as four controls.  */
.th-sort {
	display: inline-flex;
	align-items: center;
	gap: var(--s-2);
	padding: 0;
	border: 0;
	background: none;
	font: inherit;
	color: inherit;
	letter-spacing: inherit;
	text-transform: inherit;
	cursor: pointer;
}
.th-sort:hover { color: var(--text-2); }
.th-sort:focus-visible { outline: 0; box-shadow: var(--focus-ring); border-radius: var(--s-1); }

/*  Unread is WEIGHT, which is legacy's own reasoning kept: it survives both brands and does
    not depend on perceiving a hue. Deliberately not a pill — a pill is for a state somebody
    can act on, and a column of identical badges down an inbox says nothing.

    `font-weight`, NOT the `--t-body-strong` shorthand: that token is 16px where the table's
    own type is 14.5px, so it made unread rows both bolder and BIGGER — measured 16/600
    against 14.5/400, which reads as two tables interleaved rather than one with emphasis in
    it.

    And the selector has to out-specify `.tbl-6a tbody :is(td, th)` (0,1,2), which sets the
    cell colour. Written as `.trow-unread td` (0,1,1) the colour silently did not
    apply — the third time in two batches that a modifier lost to its own component's cell
    rule. Measure the computed value; file order is not the answer.  */
.tbl-6a tbody tr.trow-unread td { font-weight: 700; color: var(--text-1); }

/*  The empty and loading states sit in a single cell that spans the row: `text-align` rather
    than a colspan, because the cell count differs per mailbox and a wrong colspan is a
    layout bug that only shows on one of seven.  */
.tbl-empty { color: var(--text-3); font: var(--t-hint); }

/*  One recipient of a sent message, and its delivery pill.  */
.msg-recipient { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }
.msg-search { margin-bottom: var(--s-4); }
.msg-search > .field { flex: 0 0 384px; max-width: 100%; }

/*  The reader's body, under a rule. Legacy draws a DOTTED `--gray-400` line, which belongs to
    no palette here — a hairline in a token colour says the same thing.  */
.msg-body { margin-top: var(--s-4); padding-top: var(--s-4); border-top: 1px solid var(--surface-hair); }
.msg-attachments { margin-top: var(--s-4); }
.msg-attachment { margin-top: var(--s-2); }

/*  The add-an-address and add-a-phone rows: fields on one baseline with their action.

    NO rule on the children. `.field-row > .field` already gives every field its 14rem basis,
    and a rule here would have to out-specify it to say anything different — which is a
    reason to leave the widths to one place rather than two. The children are all `.field`s
    now, which is what makes that enough: they were wrapped in legacy `<div style="flex:1">`,
    and an inline flex declaration beats every class rule, so each wrapper collapsed to 19px
    and the labels overlapped each other.  */
.org-add { align-items: flex-end; margin-top: var(--s-4); }

/*  An advert from another platform, on a row list of this one's.  Legacy signals these
    with a blue ground, a 4px blue left border and a blue badge, and THERE IS NO BLUE in
    this palette — four palettes on semantic tokens, none of them carrying a fifth hue for
    "from somewhere else".  So it is the sunken surface and a leading edge in a token
    colour, with the word itself carried by a `.tag`: provenance is not a status, so not a
    pill, and it is not a control, so not a chip.

    The edge is `box-shadow` rather than `border-left`, which would add 4px to the row's
    content box and step its text out of line with every other row's.  */
.lr-external {
	background: var(--surface-sunken);
	box-shadow: inset 4px 0 0 var(--bound);
}

/*  A link that is a piece of a row rather than a link in prose: it takes the row's own
    colour and weight and adds an underline only on hover, because `.lr-name` has already
    said this is the row's title and `--brand-text` on every title in a list is noise.  */
.link-plain { color: inherit; text-decoration: none; }
.link-plain:hover { text-decoration: underline; }
.link-plain:focus-visible { outline: 0; box-shadow: var(--focus-ring); border-radius: var(--s-1); }

/*  An add-or-edit form that opens INSIDE a card, above or in place of a row.  It needs to
    read as part of the list rather than as a second card, so it takes the row's own
    horizontal padding and a rule beneath it — which is what the legacy panels drew with an
    inline `padding:12px 0;border-bottom:1px solid var(--gray-200)`.  */
.form-inset {
	padding: var(--s-4) 16px;
	border-bottom: 1px solid var(--surface-hair);
}

.lr-who { min-width: 0; flex: 1; }
/*  A row whose name is one line WITH something in front of it — the skills list's evidence
    bulb. `.lr-who` is a block stack, which put the bulb on its own line above the name.  */
.lr-inline { display: flex; align-items: center; gap: 10px; }

/*  The block a row expands into: one skill's evidence, opened under it. Sunken and inset
    from the row's own 16px so it reads as belonging to the row above rather than as the
    next item in the list — which is what legacy drew with a 2px left border.  */
.lr-open {
	margin: 0 16px var(--s-3);
	padding: var(--s-3) var(--s-4);
	border-radius: var(--r-tile);
	background: var(--surface-sunken);
}
.evidence-tabs { margin-bottom: var(--s-3); }

/*  A control and the action that commits it, on one line: the skills picker's Add, the
    off-catalogue box's. The field takes the width and the button takes none of it —
    `flex: none` on the button is what stops a two-word label wrapping to two lines.  */
.fld-action { display: flex; align-items: flex-start; gap: var(--s-3); }
.fld-action > .fld { flex: 1; min-width: 0; }
.fld-action > .btn { flex: none; }
/*  The group headings in the skills list sit over a row list rather than over a field, so
    they need the space a `.form-stack` gap would otherwise have given them.  */
.skill-group { margin-top: var(--s-4); margin-bottom: var(--s-2); }
.skill-add { margin-bottom: var(--s-4); }

/*  A candidate introducing themselves, capped at 32rem: full card width is a 1100px-wide
    player for a phone-shot clip. `--r-tile` rather than the control radius — it is a
    surface holding content, not something you type into.  */
.intro-video { width: 100%; max-width: 32rem; border-radius: var(--r-tile); display: block; }
.intro-video-upload { margin-top: var(--s-4); }
.lr-name { font: var(--t-body-strong); color: var(--text-1); display: block; }
/*  A pill inside a row's name — "Home" then Main, "+44 07700 900344" then Preferred — is a
    second thing on the same line, and `.lr-name` is a block of text rather than a flex row,
    so nothing separated them: measured flush, both edges at 500px, reading "Home• Main".  */
.lr-name .pill { margin-left: var(--s-2); }
.lr-meta { font: var(--t-hint); color: var(--text-3); }
/*  The users index has one meta span per row, so the separator between its facts lives
    inside that span's own text. A profile row has several — employer AND period,
    institution AND grade AND period — and those spans are inline, so they concatenate:
    "Previous Employer Ltd14 August 2023", measured on the seeded candidate. The separator
    therefore belongs BETWEEN the spans.

    Which makes an empty meta span paint a stray " · ", so a row with no dates renders no
    period span at all rather than an empty one — see `employment.rs`. Generated content is
    invisible to Playwright's `toContainText`, so the spec guarding it counts empty spans
    instead of looking for the character.  */
.lr-meta + .lr-meta::before { content: " · "; }
/*  An address is several lines that have to STAY several lines, which is the one thing a
    run of `.lr-meta` spans cannot do: they are inline, so they take the separator above
    and read as "Address Line 1 · HU9 0AA Kingston-upon-Hull". A block of its own instead.  */
.lr-lines { display: block; font: var(--t-hint); color: var(--text-3); }
.lr-lines > span { display: block; }

/*  A card that holds two lists needs a head for each of them. Contact Details is the only
    one so far: `14 Screens` merges addresses and phone numbers under one heading, where two
    `.card`s would have drawn two boundaries and a second gap between them.

    The rule is a top border on the second part rather than a bottom border on the first, so
    a card with only one part — which is what someone with no phone number on file gets —
    draws no stray line under it.  */
.card-part + .card-part {
	margin-top: var(--s-4);
	padding-top: var(--s-4);
	border-top: 1px solid var(--surface-hair);
}
.card-part-head {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--s-3);
	margin-bottom: var(--s-3);
}

.panel {
	flex: none;
	width: 358px;
	background: var(--surface-raised);
	border-radius: var(--r-tile);
	box-shadow: var(--lift-2);
	padding: 22px 22px 20px;
	transition: opacity var(--m-base);
}
.panel-head { display: flex; align-items: flex-start; gap: var(--s-3); margin-bottom: var(--s-4); }
/*  The name takes the slack so the close button sits hard against the right edge.  */
.panel-who { flex: 1; min-width: 0; }
.avatar-lg { width: 56px; height: 56px; font: 700 17px/1 var(--font); }
/*  96px, for the one page whose job is to introduce a person to a stranger who has never
    met them — the public profile.  Legacy's own size, and 56 is too small for it: on every
    other screen the avatar identifies a row in a list, and here it IS the page.

    Legacy draws it as a 12px-cornered SQUARE. This keeps the circle, because every avatar
    in the new vocabulary is one and a single rounded square on the one page an outsider
    sees would read as a different application's component. Recorded as a deviation.  */
.avatar-xl { width: 96px; height: 96px; font: 800 28px/1 var(--font); }
/*  An avatar that is a photograph rather than initials.  `.avatar` centres text in a
    circle; an `<img>` given the same box needs telling how to fill it, or a portrait
    photo arrives stretched into the square.  */
img.avatar { object-fit: cover; }
/*  A rail that stays with the reader.  `14 Screens` makes the profile's rail sticky
    because its jump list is the way around a nine-panel column — a list that scrolls out
    of sight is a list that is only useful at the top of the page.  The offset matches the
    canvas padding, so the panel stops level with where the content started.  */
.panel-sticky { position: sticky; top: 18px; }
/*  The panel's own vertical rhythm.  A `dl` and a full-width action need space either
    side of them, and the pill between the head and the list needs to sit on its own
    line — it is an inline-flex, so without this it would share one with the `dl`.  */
.panel > .pill { margin-bottom: var(--s-4); }
.panel-open { margin-top: var(--s-5); }

/*  A heading beside its actions.  `.card-head` already does that; this only stops the
    18px gap the content column puts between its children being doubled here, because
    the chip row below belongs visually to the heading rather than to the list.  */
.list-head { margin-bottom: 0; }
/*  The search field sits in the chip row rather than in the toolbar, so it needs a
    width of its own: it is a filter like the chips beside it, not an action.  */
.search-field { flex: 0 0 280px; }
/*  A control in a `.card-head`, beside the page title: the reporting screen's organisation
    picker, which says WHICH cohort every figure below is about.  300px because the widest
    seeded organisation name is longer than the 280 a search box gets, and a name truncated
    in the control that identifies the report is the one place truncation is not acceptable.  */
.head-field { flex: 0 0 300px; }
.lr-empty { padding: 26px 20px; color: var(--text-3); font: var(--t-small); }
.panel dl { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: 8px 14px; }
.panel dt { font: var(--t-hint); color: var(--text-3); }
.panel dd { margin: 0; font: var(--t-small); color: var(--text-1); }

/*  ----------------------------------------------------------------------------
    Navigation — exploration 1c at desktop, 5b at mobile.  The same two lists
    (five primary entries, the rest behind More) render as a top bar or as a tab
    bar; nothing else changes between the two.
    ----------------------------------------------------------------------------  */
.topbar {
	display: flex;
	align-items: center;
	gap: var(--s-7);
	height: 68px;
	padding: 0 var(--s-7);
	background: var(--surface-raised);
	border-bottom: 1px solid var(--surface-hair);
}
.mark { display: flex; align-items: center; gap: 10px; flex: none; }
.mark i {
	width: 30px;
	height: 30px;
	border-radius: 9px;
	background: linear-gradient(140deg, var(--grad-a), var(--grad-b));
	flex: none;
}
/*  The brand's icon, cropped out of the full wordmark by its `viewBox`.  Square, because the
    icon is: measured at 105.1 x 105.1 in the artwork's own units, so a 30px box holds it with
    no letterboxing and no `object-fit` to reason about.

    `.mark i` above stays as the fallback for a brand whose mark is a supplied raster or which
    supplied none — see the shell for why a raster cannot take this slot.  */
.mark-icon { width: 30px; height: 30px; flex: none; display: block; }
.mark b { font: 800 16px/20px var(--font); color: var(--text-1); letter-spacing: -.01em; }

.nav { display: flex; align-items: center; gap: 4px; }
.navpill {
	display: inline-flex;
	align-items: center;
	gap: 7px;
	height: 36px;
	padding: 0 15px;
	border: 0;
	border-radius: var(--r-pill);
	background: transparent;
	color: var(--text-2);
	font: var(--t-nav);
	font-weight: 600;
	text-decoration: none;
	cursor: pointer;
	transition: background var(--m-fast), color var(--m-fast);
}
.navpill:hover { background: var(--surface-sunken); color: var(--text-1); }
.navpill:focus-visible { outline: 0; box-shadow: var(--focus-ring); }

/*  The active entry, and a correction to what the design file drew.

    Measured in the running app: `--surface-sunken` on `--surface-raised` is 1.12:1 in
    the light palettes and 1.11:1 in the dark ones.  That is the token layer working as
    intended — every surface tint is quiet, and none of them clears 3:1 against another
    (`--fill` 1.22, `--surface-hair` 1.27, `--fill-hover` 1.31) — but it means the chip
    alone is not a perceivable indicator of where you are, which is exactly the class of
    defect this modernisation exists to close.  The design file's specimen looked right
    because it was judged by eye against a card, which is the trap.

    So the chip stays as the shape and a 2px mark carries the state.  `--brand-text` is
    the only brand token that clears 3:1 as a non-text mark on a raised surface in all
    four palettes (5.89:1 light; `--action-fill` is 2.52:1 and would fail).  The state is
    then signalled three ways — mark, weight and text colour — and none of them alone.  */
.navpill-on {
	background: var(--surface-sunken);
	color: var(--text-1);
	font-weight: 700;
	position: relative;
}
.navpill-on::after {
	content: "";
	position: absolute;
	left: 15px;
	right: 15px;
	bottom: -8px;
	height: 2px;
	border-radius: 1px;
	background: var(--brand-text);
}

.topbar .spacer { flex: 1; }
.topbar-tools { display: flex; align-items: center; gap: var(--s-2); }

/*  The count is a number as well as a dot, so it survives a palette that a
    colour-blind reader cannot separate.  */
.badge {
	position: absolute;
	top: -3px;
	right: -3px;
	min-width: 18px;
	height: 18px;
	padding: 0 5px;
	border-radius: var(--r-round);
	background: var(--danger-fill);
	color: var(--danger-ink);
	font: 700 11px/18px var(--font);
	text-align: center;
}
.has-badge { position: relative; }

.menu {
	min-width: 232px;
	background: var(--surface-raised);
	border-radius: var(--r-tile);
	box-shadow: var(--lift-3);
	padding: 8px;
	transition: opacity var(--m-base), transform var(--m-base);
}
.menu-item {
	display: flex;
	align-items: center;
	gap: 10px;
	width: 100%;
	height: 40px;
	padding: 0 12px;
	border: 0;
	border-radius: var(--r-pill);
	background: transparent;
	color: var(--text-1);
	font: var(--t-small);
	font-weight: 600;
	text-align: left;
	text-decoration: none;
	cursor: pointer;
}
.menu-item:hover { background: var(--surface-sunken); }
.menu-item:focus-visible { outline: 0; box-shadow: var(--focus-ring); }
/*  The chosen radio item.  Colour AND weight AND a mark, for the third time in this
    file: `aria-checked` already tells the assistive layer, but a sighted reader
    picking Light out of System / Light / Dark should not have to be able to see orange
    to know which one is in force.  The mark is `::after` so it needs no markup.  */
.menu-item-on { color: var(--brand-text); font-weight: 700; }
.menu-item-on::after {
	content: "";
	width: 6px;
	height: 10px;
	margin-left: auto;
	border-right: 2px solid currentColor;
	border-bottom: 2px solid currentColor;
	transform: rotate(45deg) translate(-1px, -2px);
	flex: none;
}
.menu-sep { height: 1px; background: var(--surface-hair); margin: 6px 4px; }
.menu-head { font: var(--t-eyebrow); color: var(--text-3); letter-spacing: .09em; text-transform: uppercase; padding: 8px 12px 4px; }

/*  Mobile: the band is present at every width — the Direction's own fix for "the
    mobile breakpoint deletes the brand" — and collapses to a title bar on scroll
    rather than disappearing.  */
.band {
	background: linear-gradient(118deg, var(--grad-a), var(--grad-b));
	color: var(--hero-ink);
	padding: 18px 20px 22px;
	border-bottom-left-radius: var(--r-silhouette);
	transition: padding var(--m-slow);
}
.band-tight { padding: 10px 20px; }
.band-tight .sub { display: none; }

/*  PINNED, which it was always meant to be.  Two things in this sheet already assumed it:
    `.tabsheet` sits at `bottom: 74px`, which is only a sensible place for a sheet if the
    bar occupies the bottom 74px of the VIEWPORT, and `.content` reserves 90px of bottom
    padding on mobile, which is only needed if something is covering it.  Neither made
    sense while the bar was in the document flow and scrolled away with the page — and
    scrolling away is what it did, because `position` was never written.

    `--tabbar-h` is declared on `.sp-root` because three rules depend on it and they were
    three separate literals: this bar, the sheet above it, and the content padding below
    it.  It also has to grow by the home-indicator inset on a phone that has one, and
    growing a literal in three places is how two of them get missed.  */
.tabbar {
	position: fixed;
	left: 0;
	right: 0;
	bottom: 0;
	/*  Below the sheet and the menus at 40, and well below the dialog scrim at 50: a sheet
	    rises OVER this bar and a modal covers everything, which is what both are for.  */
	z-index: 30;
	box-sizing: border-box;
	display: flex;
	align-items: stretch;
	height: var(--tabbar-h);
	/*  The inset as bottom padding, so the row of tabs sits above the home indicator
	    rather than half under it.  `box-sizing: border-box` above is what keeps that inside
	    the declared height instead of adding to it.  */
	padding: 8px 4px env(safe-area-inset-bottom, 0px);
	background: var(--surface-raised);
	border-top: 1px solid var(--surface-hair);
}
.tab {
	flex: 1;
	/*  Both are load-bearing.  A <button> carries a UA `padding: 1px 6px` that an
	    <a> does not, and `flex: 1 1 0%` grows from the content basis — so the More
	    tab measured 86px against its four anchor siblings' 74 and the bar was
	    visibly uneven.  With these, all five measure 76.4.  */
	padding: 0;
	min-width: 0;
	min-height: 52px;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 5px;
	border: 0;
	border-radius: var(--r-pill);
	background: transparent;
	color: var(--text-3);
	font: var(--t-tab);
	text-decoration: none;
	cursor: pointer;
}
.tab i { width: 22px; height: 22px; border-radius: 7px; background: currentColor; display: block; }

/*  The label never wraps, and never spills.  Six tabs share 390px, so a slot is 58.5px
    — measured, and measured again after "Organisations" and "Opportunities" were found
    needing 63 of it.  Short forms are the fix (see `Key::NavHome`); this is the net
    under them, for the next long word or the first locale that has no short one.  */
.tab-label {
	max-width: 100%;
	overflow: hidden;
	white-space: nowrap;
	text-overflow: ellipsis;
}
.tab:focus-visible { outline: 0; box-shadow: var(--focus-ring); }
/*  Weight as well as colour, for the same reason `.navpill-on` grew a mark: colour
    alone is the first of the sixteen legacy findings, and a tab bar is the one place
    in the app where knowing which of five you are on is the whole point.  */
.tab-on { color: var(--brand-text); font-weight: 700; }

/*  ----------------------------------------------------------------------------
    Layout
    ----------------------------------------------------------------------------  */
.content { padding: var(--s-7) var(--s-8); display: flex; flex-direction: column; gap: 18px; }
.cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr)); gap: 18px; align-items: start; }

/*  The guest screens: one card, centred, on the canvas.
    ----------------------------------------------------------------------------
    These screens render OUTSIDE the shell — there is no frame — so they sit straight on
    `.sp-root`, which is where the canvas and the base type come from.  Legacy grounds
    them on a full-page brand gradient (`.backdrop-auth`); `14 Screens` drops it, because
    the gradient's whole job in 1c is to concentrate into one card and a page that IS one
    card does not need a second brand statement behind it.

    `min-height: 100dvh` rather than `100vh`: on a phone the dynamic unit accounts for the
    browser's own chrome, and a sign-in form pushed half a bar below the fold is the one
    place that matters.  */
.auth-ground {
	min-height: 100dvh;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: var(--s-5);
}
.auth-card { width: 100%; max-width: 420px; }
/*  One article in the index grid: an illustration, a title, a clamped excerpt, and the
    way in.  A column so the action can be pushed to the bottom.  */
.article-card { display: flex; flex-direction: column; gap: var(--s-2); }
/*  Full width of the card, and the tile radius rather than the card's — a picture inside a
    surface steps down, or the two curves fight.  `aspect-ratio` with `cover` so eleven
    illustrations of different proportions produce one tidy grid instead of eleven card
    heights.  */
.article-img {
	width: 100%;
	aspect-ratio: 16 / 9;
	object-fit: cover;
	border-radius: var(--r-tile);
	display: block;
}
/*  `auto` so every card's button sits on one baseline whatever its excerpt runs to — the
    same reason the Goald video cards carry it.  */
.article-action { margin-top: auto; }

/*  The per-skill evidence bulb, on the profile and the user record.  Carried off
    `theme.css`, which coloured it with `--keyword-red` #FF0000 and `--keyword-green`
    #008000 — raw hues that exist nowhere else in this system.  The state pair says the
    same thing and is the one the pills use, so a lit bulb and an Active pill agree.

    The colour is NOT the only signal: `evidence_bulb` gives the icon a name either way,
    because a red-green distinction is unreadable to a screen reader and to anyone who
    cannot tell the two apart.  */
.skill-bulb { width: 1rem; height: 1rem; flex: none; margin-right: var(--s-2); }
.skill-bulb-evidence { color: var(--state-ok-text); }
.skill-bulb-none { color: var(--state-off-text); }

/*  ----------------------------------------------------------------------------
    Injected content: the guide pages and the articles.
    ----------------------------------------------------------------------------

    These are the only screens whose body is HTML the application does not write — a
    compile-time constant transcribed from the legacy templates, set with `inner_html`.
    So the container styles ELEMENTS rather than classes, which is the one place in this
    stylesheet that is the right thing to do.

    THE CLASS NAME IS DELIBERATELY LEGACY'S.  The legacy sheet scoped ten utility shims
    under `.guide-content` — `mb-2`, `pl-2`, `ml-4`, `flex`, `justify-center`,
    `text-center`, `mr-2`, `items-center`, `subtitle-euk`, `text-magenta-euk` — and those
    ten are EXACTLY the ten the transcribed markup uses, counted: 393 `mb-2` alone.  They
    were never legacy chrome to delete with the sheet; they are the content's own layout,
    and the content cannot be restyled without editing a transcription nobody should edit.
    So they were CARRIED ACROSS below rather than dropped, which is the one thing the
    deletion of that sheet could have got silently and unrecoverably wrong.

    These rules used to out-specify the legacy sheet's own `.guide-content` typography,
    which is why they exist at all; that sheet is gone and they are simply the typography
    now.  */
.guide-content { color: var(--text-2); font: var(--t-body); }
/*  `--t-h2` at 17px, NOT `--t-h3`: `--t-h3` is 15px and the body here is `--t-body`'s 16,
    so h3 would render every heading SMALLER than the paragraphs under it. Legacy's own
    figure was 18px, larger than its 16px body, and that relationship is the point.  */
.guide-content h2 {
	font: var(--t-h2);
	color: var(--text-1);
	margin: var(--s-5) 0 var(--s-2);
}
/*  The first heading sits directly under the card's own head, where that margin is a
    second gap nobody asked for.  */
.guide-content > h2:first-child { margin-top: 0; }
.guide-content p, .guide-content li { font: var(--t-body); color: var(--text-2); }
/*  NO paragraph margin here.  The content's own `mb-2` is on 393 elements and legacy sets no
    `p` margin at all — the rhythm IS the utility, and an earlier draft of this rule silently
    replaced the content's 8px with 12px everywhere.

    THE ARITHMETIC INVERTED WHEN THE SEAM CAME OFF, and the reason is worth reading before
    trusting the old note.  Scoped, `.sp-next .guide-content p` was (0,2,1) against the
    shim's (0,2,0), so the element rule won and a margin here overrode the content.
    Unscoped, `.guide-content p` is (0,1,1) against the shim's unchanged (0,2,0) — two
    classes beat one — so the shim now wins and a margin here would lose.

    The rule stands either way: this selector declares no spacing, because the content owns
    its own.  What changed is that the hazard is no longer the one the comment described.  */
/*  ONLY the list style, as legacy sets only `list-style-type` here.  A `padding-left` on
    this selector used to out-specify the content's own `pl-2` and broke the list indent the
    same way the paragraph rhythm broke, before the spec caught it.  Since the seam came off
    the shim wins instead — see the note above — but the rule is unchanged either way,
    because the content indents itself with `pl-2 ml-4`.  */
.guide-content ul { list-style: disc; }
/*  Legacy's own 0.25rem, and it does set this one — the content has no per-item utility.  */
.guide-content li { margin-bottom: var(--s-1); }
.guide-content a { color: var(--brand-text); font-weight: 600; }
.guide-content a:hover { color: var(--brand-text-hover); text-decoration: underline; }
.guide-content a:focus-visible { outline: 0; box-shadow: var(--focus-ring); border-radius: var(--s-1); }
/*  A clip in the resource pages, and an illustration in an article.  `max-width` on the
    video rather than a fixed width: legacy caps it at 600px, and a 600px player in a
    358px column is the overflow this whole migration keeps finding.  */
.guide-content video { width: 100%; max-width: 600px; border-radius: var(--r-tile); display: block; }
.guide-content img { max-width: 100%; height: auto; border-radius: var(--r-tile); }
/*  `subtitle-euk` is a HEADING in the transcription, not a subtitle — it carries the same
    weight the legacy sheet gave `h2`. Mapped to the same token as `h2` above so they agree,
    because the content uses them interchangeably.  */
.guide-content .subtitle-euk { font: var(--t-h2); color: var(--text-1); }
/*  Legacy's `text-magenta-euk` already resolves to `--brand` rather than hard-coded
    magenta (design-audit row 2). `--brand` is a FILL token at 2.41:1 on paper, so as type
    it becomes `--brand-text`, which is the 5.89:1 pair `10 Foundations` established for
    exactly this.  */
.guide-content .text-magenta-euk { color: var(--brand-text); }

/*  THE UTILITY SHIMS, carried across from the legacy sheet — the last thing it supplied to
    a running screen, and the reason its `.guide-content` block could not simply be deleted.

    These eight class names are in the TRANSCRIBED CONTENT, not in any markup this
    application writes: `mb-2` alone appears 393 times across `guide_content.rs` and
    `articles_content.rs`. They are the content's own layout. Restyling it would mean
    editing a transcription nobody should edit, so the names are honoured instead.

    Legacy declared them in rem; these are the spacing tokens whose values match exactly —
    0.5rem is `--s-2`'s 8px and 1rem is `--s-4`'s 16px. `e2e/tests/guide.spec.ts` asserts
    four of them by computed effect (8px, 8px, 16px, and `display: flex`), so a silent loss
    fails a test rather than reflowing every guide page and article at once.  */
.guide-content .mb-2 { margin-bottom: var(--s-2); }
.guide-content .mr-2 { margin-right: var(--s-2); }
.guide-content .pl-2 { padding-left: var(--s-2); }
.guide-content .ml-4 { margin-left: var(--s-4); }
.guide-content .flex { display: flex; }
.guide-content .justify-center { justify-content: center; }
.guide-content .items-center { align-items: center; }
.guide-content .text-center { text-align: center; }

/*  The other shell-less screen, and it wants the opposite alignment.  A guest card centres
    in the viewport; the public profile is a DOCUMENT — it starts at the top and scrolls, so
    this reuses `.content` for the column (30/40 padding, 18px between cards) and adds only
    what a page with no frame needs: a width to centre in, and a ground tall enough that a
    short profile does not sit on a stripe of canvas.

    900px is legacy's own measurement, from the inline `max-width:900px` this replaces.  */
.doc-page { max-width: 900px; margin: 0 auto; min-height: 100dvh; }
/*  Register is two columns of fields in legacy and wants the wide step, for the same
    reason a dialog does — see `06 Explorations`.  */
.auth-card-wide { max-width: 720px; }
.auth-mark { display: block; margin: 0 auto var(--s-5); }
/*  A mark has three forms and they need three rules.  The height is shared, which is what
    keeps every brand's card the same height rather than differing by the accident of a
    logo's proportions; the width follows from each mark's own ratio.  */
.auth-mark-vector,
.auth-mark-raster { height: 38px; width: auto; }
/*  THE FIX, and the reason the platform mark is drawn as elements rather than fetched as an
    image.  The word "Starting" is `currentColor` in the inlined vector, so it takes this and
    is legible on either ground.  It used to be the drawn `#1A1B4B`, which measures 1.03:1 on
    a dark card: the word vanished and only "point" and the dot survived — a thing a
    screenshot shows and no other gate can.  An `<img>` cannot reach the fills inside an SVG,
    so the previous answer was a hard-coded `#FFFFFF` plate behind the whole mark on dark.  */
.auth-mark-vector { color: var(--text-1); }
/*  A raster cannot be tokenised, so the plate survives for supplied images — scoped to them
    rather than applied to every mark.  A partner's logo is drawn for their own light ground
    and this is the only way to give it one.  */
.dark .auth-mark-raster {
	box-sizing: content-box;
	padding: 10px 14px;
	border-radius: var(--r-control);
	background: #FFFFFF;
}
/*  A brand that has supplied nothing draws its NAME, in the top bar's treatment: the
    gradient tile and the brand name.  Centred on the card rather than laid out in a row, so
    it reads as a mark and not as a heading.  Both parts are tokens, so this form is the one
    that needs no plate and no exception in either mode.  */
.auth-mark-name { display: flex; align-items: center; justify-content: center; gap: 10px; height: 38px; }
.auth-mark-name i {
	width: 30px;
	height: 30px;
	border-radius: 9px;
	background: linear-gradient(140deg, var(--grad-a), var(--grad-b));
	flex: none;
}
.auth-mark-name b { font: 800 20px/24px var(--font); color: var(--text-1); letter-spacing: -.01em; }
/*  The row under the button: one centred link, or two with a rule between them.  */
.auth-foot { margin-top: var(--s-4); text-align: center; font: var(--t-small); }
/*  The line under the mark saying what the screen is for. `--text-2`, because it is
    orientation and the label above the first field is what the eye should land on. The
    class name is legacy's and is kept: `e2e/tests/auth_recovery.spec.ts` reads it, and
    renaming it would cost a spec change for nothing.  */
.auth-lede { font: var(--t-body); color: var(--text-2); text-align: center; }

/*  The reveal control sits inside the field's own box, so the field needs a positioning
    context and the input needs room for the button not to overlap its text.  */
.field-reveal { position: relative; }
.field-reveal .fld { padding-right: 46px; }
.reveal {
	position: absolute;
	top: 0;
	right: 0;
	height: 48px;
	width: 44px;
	display: grid;
	place-items: center;
	border: 0;
	background: none;
	color: var(--text-3);
	cursor: pointer;
	border-radius: var(--r-control);
}
.reveal:hover { color: var(--text-1); }
.reveal:focus-visible { outline: 0; box-shadow: var(--focus-ring); }

@media (max-width: 767px) {
	.topbar { display: none; }
	/*	The bottom padding clears the pinned bar, from the bar's own measurement plus a
	    gap — it was a 90px literal, which is 74 plus a guess with no way to know about a
	    home indicator.  */
	.content { padding: var(--s-4) var(--s-4) calc(var(--tabbar-h) + var(--s-4)); }
	/*	The profile's rail is sticky BESIDE the content and must not be sticky ABOVE it.
	    `.split` turns into a column here, which puts that card at the top of the page — and
	    `position: sticky` then pinned it to the viewport while every panel below scrolled
	    underneath it. A rail that stays with the reader is the point of it on a desktop,
	    where it sits next to what it is a rail FOR; stacked, it is just a card in the way.  */
	.panel-sticky { position: static; }
	.split { flex-direction: column; }
	/*  6a, transposed: a card per row of label-and-value pairs.  `13 Screens` drew this for
	    the reporting table and the reason is arithmetic — nine columns need 1481px, and no
	    amount of sideways scrolling makes a 390px window a useful way to read them.  Three
	    columns DO scroll usefully, which is why the catalogues keep their table and only a
	    table asking for `.tbl-cards` transposes.

	    The labels come out of `data-label`, set from the same list the head is built from,
	    so no column is dropped — which is the failure this replaces rather than accepts.  */
	.tbl-cards thead { display: none; }
	.tbl-cards, .tbl-cards tbody, .tbl-cards tr { display: block; }
	.tbl-cards tbody tr { padding: 14px 16px; border-bottom: 1px solid var(--surface-hair); }
	.tbl-cards tbody tr:last-child { border-bottom: 0; }
	.tbl-cards tbody tr:hover :is(td, th) { background: transparent; }
	/*  `height: auto` overrides the 56px rhythm, which is a row's height and not a line's.  */
	.tbl-cards tbody :is(td, th) {
		display: grid;
		grid-template-columns: 10rem 1fr;
		gap: var(--s-3);
		align-items: baseline;
		height: auto;
		padding: 3px 0;
		border: 0;
	}
	.tbl-cards tbody :is(td, th)::before {
		content: attr(data-label);
		font: var(--t-hint);
		color: var(--text-3);
	}
	/*  The name cell has no label and is the card's heading, so it spans the pair.  */
	.tbl-cards tbody :is(td, th):not([data-label]) { display: block; padding-bottom: var(--s-2); }
	.tbl-cards tbody :is(td, th):not([data-label])::before { content: none; }
	/*  Numbers align left once they are values in a list rather than a column to compare
	    down.  `tabular-nums` stays: it is what keeps a pair of figures the same width.  */
	.tbl-cards tbody td.cell-num { text-align: left; }
	/*  Both children, and for the same reason `.panel` was already here: `.split` sets
	    `align-items: flex-start`, so a column-direction item is sized to its CONTENT
	    rather than stretched to the container. Without this the profile's column took its
	    width from the widest thing in it — measured at 390 as a 1429px page scroll, from a
	    legacy table row — and `min-width: 0` cannot help, because the item is not being
	    squeezed, it is being asked how wide it would like to be.  */
	.panel, .split-main { width: 100%; }
}
@media (min-width: 768px) {
	.tabbar { display: none; }
}


/*  ----------------------------------------------------------------------------
    The frame.

    `.shell-next` is the column the whole application sits in; `.shell-body` is the region a
    screen draws into, and every screen lays itself out at 1c's 30/40 with a `.content` of
    its own.  While the migration ran, `shell.rs` chose between this and legacy's centred
    64rem `.shell-content` per route, from a `MIGRATED` list; that list became every route
    and both it and the branch are gone.
    ----------------------------------------------------------------------------  */
.shell-next {
	display: flex;
	flex-direction: column;
	min-height: 100vh;
	background: var(--surface-canvas);
}
.shell-body { flex: 1; min-width: 0; }


/*  A pill that owns a menu needs a positioning context, and the menu hangs from the
    bar's own bottom edge rather than from the pill — 62px is the 68px bar less the
    6px the design file measured the gap at.  */
.navpill-wrapper { position: relative; display: inline-flex; }
.menu-under { position: absolute; top: 62px; left: 0; z-index: 40; }
.menu-right { left: auto; right: 0; }

.caret {
	width: 7px;
	height: 7px;
	border-right: 1.5px solid currentColor;
	border-bottom: 1.5px solid currentColor;
	transform: rotate(45deg) translateY(-2px);
	display: block;
	flex: none;
}

/*  The mobile More sheet: a menu that rises from the tab bar rather than hanging
    from a pill, because at 390 a dropdown would open off the bottom of the screen.  */
.tabsheet {
	position: fixed;
	left: 0;
	right: 0;
	/*  Directly on top of the bar, from the bar's own measurement — this was a second
	    literal 74px, which is how it would have ended up overlapping the bar on exactly the
	    devices the home-indicator inset applies to.  */
	bottom: var(--tabbar-h);
	z-index: 40;
	background: var(--surface-raised);
	box-shadow: var(--lift-3);
	border-top-left-radius: var(--r-tile);
	border-top-right-radius: var(--r-tile);
	padding: 14px 12px 20px;
}

/*  The frame's own responsive switch, and the only place the two halves of the nav
    model are chosen between.  */
@media (max-width: 767px) {
	.topbar { display: none; }
	.shell-body { padding-bottom: 90px; }
}
@media (min-width: 768px) {
	.tabbar { display: none; }
	.tabsheet { display: none; }
}
