/* =====================================================================
   SudoExam design system — the tool-screen stylesheet.

   Plain CSS under public/, NOT in the Vite bundle, for the same
   reason page-loader.css is: the Filament panel cannot read Vite's
   manifest, and the workbook editor is now a panel page. Both the
   custom app layouts and the panel load this identical file, so a
   card, a chip and a worksheet cell look the same on either side.
   Cache-stamped by App\Support\Assets::url().

   ANYTHING A TOOL SCREEN USES BELONGS HERE, not in app.css — a rule
   left behind there simply does not exist inside the panel, and the
   symptom is silent: a dialog rendered as inline text with no overlay.

   THEME: "Focus Flow" (client's chosen mockup, 2026-08-16). Ported
   from theme-c-focus-flow.html. The palette is "Instrument": cool
   neutrals biased toward ONE kingfisher accent, with the semantics
   held deliberately apart from it —

       accent  = where you are, what you may press, what is answered
       good    = "this is safe"          (secure mode, published)
       flag    = "you must act / come back"  (answer boxes, flags, ECF)
       crit    = "this will cost you marks" (unanswered at review, wrong)
       calc    = derived, not typed          (computed worksheet cells)

   The accent does no semantic work of its own. `calc` is the one
   family the mockup does not carry: the worksheet grid has FIVE cell
   roles and they are marking semantics, not decoration, so purple had
   to be re-tuned into the palette rather than dropped (see §GRID).

   Originally ported from the pitch prototype (design/prototype.html);
   every class name from that port is kept.
   ===================================================================== */
:root {
    /* ---- ground & ink ---- */
    --ff-ground: #f2f5f6;
    --ff-ground-2: #e8eeef;
    --ff-surface: #ffffff;
    --ff-sunken: #f7f9fa;
    --ff-ink: #0b1a1e;
    --ff-ink-2: #3b4e54;
    --ff-muted: #5f7278;
    --ff-line: #dce5e7;
    --ff-line-2: #c3d2d5;

    /* ---- the one accent ---- */
    --ff-accent: #0a6e80;
    --ff-accent-hi: #08596a;
    --ff-accent-soft: #e4f1f3;
    --ff-accent-edge: #b6d9de;
    --ff-on-accent: #ffffff;

    /* ---- semantics, kept apart from the accent ---- */
    --ff-good: #0e6b4f;  --ff-good-soft: #e3f3ec;  --ff-good-edge: #b4ddcb;
    --ff-flag: #9a5b00;  --ff-flag-soft: #fbefdc;  --ff-flag-edge: #e7cb9b;
    --ff-crit: #a31b12;  --ff-crit-soft: #fbeae8;  --ff-crit-edge: #efc0bb;
    /* a worksheet figure the candidate did not type — derived */
    --ff-calc: #5b21b6;  --ff-calc-soft: #f1edfb;  --ff-calc-edge: #d9cdf3;

    /* A solid amber to sit BEHIND near-black text (nav badges). The
       flag ink itself is far too dark to be a fill. */
    --ff-flag-fill: #c2820c;
    --ff-on-flag: #0b1a1e;
    /* the candidate's own highlighter — never a marking colour */
    --ff-hl: #fbf1c4;

    /* A blurred bar has to know what it is blurring; color-mix is not
       safe on the machines this paints on, so the veil is a token. */
    --ff-veil: rgba(255, 255, 255, .84);
    --ff-scrim: rgba(11, 26, 30, .62);

    /* ---- depth ---- */
    --ff-sh-1: 0 1px 2px rgba(11,26,30,.06), 0 1px 1px rgba(11,26,30,.04);
    --ff-sh-2: 0 10px 24px -14px rgba(11,26,30,.34), 0 2px 5px rgba(11,26,30,.05);
    --ff-sh-3: 0 34px 64px -34px rgba(11,26,30,.45), 0 6px 16px rgba(11,26,30,.07);
    --ff-inset: inset 0 1px 0 rgba(255,255,255,.16);

    /* ---- shape & motion ---- */
    --ff-r-lg: 16px; --ff-r-md: 11px; --ff-r-sm: 8px;
    --ff-t: 180ms cubic-bezier(.22,.61,.36,1);

    /* ---------------------------------------------------------------
       The names the product already writes. They are ALIASES now, not
       values: one edit above re-themes every screen, and no selector
       anywhere had to be renamed. resources/css/app.css consumes these
       too, which is why the token block lives in the file both shells
       and the panel can read.
       --------------------------------------------------------------- */
    --ink: var(--ff-ink);
    --muted: var(--ff-muted);
    --slate: var(--ff-ink-2);
    --line: var(--ff-line);
    --soft: var(--ff-sunken);
    --brand: var(--ff-accent);
    --brand-soft: var(--ff-accent-soft);
    --brand-deep: var(--ff-accent-hi);
    --amber: var(--ff-flag-fill);
    --amber-deep: var(--ff-flag);
    --red: var(--ff-crit);
    --red-deep: var(--ff-crit);
    --ok: var(--ff-good);
    --ok-soft: var(--ff-good-soft);

    --sxfont: "Segoe UI Variable Text", "Segoe UI", -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Noto Sans", Arial, system-ui, sans-serif;
    --mono: ui-monospace, "Cascadia Mono", "SF Mono", "JetBrains Mono", Consolas, "Liberation Mono", monospace;
    /* height of the product topbar: everything sticky has to clear it */
    --sxtop: 57px;
    /* ---------------------------------------------------------------
       THE DESK (the candidate's sitting screen). The two rail heights
       are declared here and RE-MEASURED in JS from the real
       getBoundingClientRect(), so the panes and the floating-window
       clamp read one authority rather than a literal that a browser
       zoom or a translated string can falsify. --sxbot-rail is 0 by
       design: the agreed structure carries no bottom bar, and Batch D's
       clamp still subtracts it.
       --------------------------------------------------------------- */
    --sxtop-rail: 56px;
    --sxbot-rail: 0px;
    --sxnav-w: 264px;
    --sxsrc-w: 380px;
    --sxedge: 40px;
    --sxpaper: 16px;
    /* Stacking, as tokens so the ordering is assertable: a floating
       window can never reach the rail that carries the clock. */
    --sx-z-win: 85;
    --sx-z-rail: 110;
}

/* =====================================================================
   DARK.

   Three declarations, as the theming contract asks: the system
   preference, an explicit data-theme, and Filament's own .dark class
   on the panel. All three carry the SAME values.

   The system-preference block is deliberately narrowed:
     · body.sx.auth      — the admission slip is printed stationery
     · body.sx:has(.landing)  — landing, /verify, the certificate, the
                            installer and the guide are one light
                            registry paper, drawn in app.css
   Those pages have their own light design and no dark variant, so
   flipping the tokens under them would half-paint them. Everything
   else on the app shell — the runner, the gate, results, payment,
   self-marking, marking — is token-driven and follows the system.
   ===================================================================== */
@media (prefers-color-scheme: dark) {
    body.sx:not(.auth):not([data-theme="light"]):not(:has(.landing)) {
        color-scheme: dark;

        --ff-ground: #080f12;
        --ff-ground-2: #0c1519;
        --ff-surface: #101a1e;
        --ff-sunken: #0c1519;
        --ff-ink: #eaf2f3;
        --ff-ink-2: #b3c4c8;
        --ff-muted: #8aa0a5;
        --ff-line: #1e2f34;
        --ff-line-2: #2c4148;

        --ff-accent: #4fc3d2;
        --ff-accent-hi: #7bd6e1;
        --ff-accent-soft: #0e2e35;
        --ff-accent-edge: #1e4c56;
        --ff-on-accent: #04191f;

        --ff-good: #57c79c;  --ff-good-soft: #0c271f;  --ff-good-edge: #1c4437;
        --ff-flag: #e9a94a;  --ff-flag-soft: #2a2011;  --ff-flag-edge: #4a3a1c;
        --ff-crit: #ff9086;  --ff-crit-soft: #2e1512;  --ff-crit-edge: #512622;
        --ff-calc: #c4b5fd;  --ff-calc-soft: #1b1530;  --ff-calc-edge: #3a2f5c;

        --ff-flag-fill: #e9a94a;
        --ff-hl: #3b3418;

        --ff-veil: rgba(16, 26, 30, .88);
        --ff-scrim: rgba(2, 8, 10, .68);

        --ff-sh-1: 0 1px 2px rgba(0,0,0,.5);
        --ff-sh-2: 0 10px 24px -14px rgba(0,0,0,.8), 0 2px 5px rgba(0,0,0,.35);
        --ff-sh-3: 0 34px 64px -34px rgba(0,0,0,.9), 0 6px 16px rgba(0,0,0,.4);
        --ff-inset: inset 0 1px 0 rgba(255,255,255,.06);
    }
}

:root[data-theme="dark"], body.sx[data-theme="dark"], :root.dark, body.dark {
    color-scheme: dark;

    --ff-ground: #080f12;
    --ff-ground-2: #0c1519;
    --ff-surface: #101a1e;
    --ff-sunken: #0c1519;
    --ff-ink: #eaf2f3;
    --ff-ink-2: #b3c4c8;
    --ff-muted: #8aa0a5;
    --ff-line: #1e2f34;
    --ff-line-2: #2c4148;

    --ff-accent: #4fc3d2;
    --ff-accent-hi: #7bd6e1;
    --ff-accent-soft: #0e2e35;
    --ff-accent-edge: #1e4c56;
    --ff-on-accent: #04191f;

    --ff-good: #57c79c;  --ff-good-soft: #0c271f;  --ff-good-edge: #1c4437;
    --ff-flag: #e9a94a;  --ff-flag-soft: #2a2011;  --ff-flag-edge: #4a3a1c;
    --ff-crit: #ff9086;  --ff-crit-soft: #2e1512;  --ff-crit-edge: #512622;
    --ff-calc: #c4b5fd;  --ff-calc-soft: #1b1530;  --ff-calc-edge: #3a2f5c;

    --ff-flag-fill: #e9a94a;
    --ff-hl: #3b3418;

    --ff-veil: rgba(16, 26, 30, .88);
    --ff-scrim: rgba(2, 8, 10, .68);

    --ff-sh-1: 0 1px 2px rgba(0,0,0,.5);
    --ff-sh-2: 0 10px 24px -14px rgba(0,0,0,.8), 0 2px 5px rgba(0,0,0,.35);
    --ff-sh-3: 0 34px 64px -34px rgba(0,0,0,.9), 0 6px 16px rgba(0,0,0,.4);
    --ff-inset: inset 0 1px 0 rgba(255,255,255,.06);
}

/* The workbook editor is embedded in the panel and is deliberately a
   sheet of paper laid on the desk — AdminPanelProvider::toolStyle()
   pins color-scheme:light on it. Pin the tokens to match, or a dark
   panel would hand it dark ink on that paper. */
.sx-tools {
    --ff-ground: #f2f5f6;
    --ff-ground-2: #e8eeef;
    --ff-surface: #ffffff;
    --ff-sunken: #f7f9fa;
    --ff-ink: #0b1a1e;
    --ff-ink-2: #3b4e54;
    --ff-muted: #5f7278;
    --ff-line: #dce5e7;
    --ff-line-2: #c3d2d5;

    --ff-accent: #0a6e80;
    --ff-accent-hi: #08596a;
    --ff-accent-soft: #e4f1f3;
    --ff-accent-edge: #b6d9de;
    --ff-on-accent: #ffffff;

    --ff-good: #0e6b4f;  --ff-good-soft: #e3f3ec;  --ff-good-edge: #b4ddcb;
    --ff-flag: #9a5b00;  --ff-flag-soft: #fbefdc;  --ff-flag-edge: #e7cb9b;
    --ff-crit: #a31b12;  --ff-crit-soft: #fbeae8;  --ff-crit-edge: #efc0bb;
    --ff-calc: #5b21b6;  --ff-calc-soft: #f1edfb;  --ff-calc-edge: #d9cdf3;

    --ff-flag-fill: #c2820c;
    --ff-hl: #fbf1c4;
    --ff-veil: rgba(255, 255, 255, .84);
    --ff-scrim: rgba(11, 26, 30, .62);

    --ff-sh-1: 0 1px 2px rgba(11,26,30,.06), 0 1px 1px rgba(11,26,30,.04);
    --ff-sh-2: 0 10px 24px -14px rgba(11,26,30,.34), 0 2px 5px rgba(11,26,30,.05);
    --ff-sh-3: 0 34px 64px -34px rgba(11,26,30,.45), 0 6px 16px rgba(11,26,30,.07);
    --ff-inset: inset 0 1px 0 rgba(255,255,255,.16);
}

body.sx { font-family: var(--sxfont); background: var(--ff-ground); color: var(--ff-ink); font-size: 14px;
    -webkit-font-smoothing: antialiased; }
.sx a { color: var(--ff-accent); text-decoration: none; }
/* One focus ring for the whole app shell: a real outline, so it
   survives Windows forced-colours mode. Never a box-shadow. */
body.sx :focus-visible { outline: 2px solid var(--ff-accent); outline-offset: 2px; }

/* topbar — one header for the whole product: translucent, blurred,
   aligned to the 1180px page grid (the navy bar is retired everywhere) */
.topbar { position: sticky; top: 0; z-index: 100; background: var(--ff-veil);
    -webkit-backdrop-filter: saturate(1.5) blur(12px); backdrop-filter: saturate(1.5) blur(12px);
    color: var(--ff-ink); padding: 13px max(18px, calc((100% - 1180px) / 2));
    display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    border-bottom: 1px solid var(--ff-line); }
.topbar .brand { font-weight: 700; font-size: 17px; letter-spacing: -.018em; margin-right: 6px; color: var(--ff-ink); }
.topbar .brand span { color: var(--ff-accent); }
.navbtn { font: 600 13px var(--sxfont); cursor: pointer; padding: 7px 13px; border-radius: 999px;
    border: 1px solid var(--ff-line-2); background: var(--ff-surface); color: var(--ff-ink-2) !important;
    transition: color var(--ff-t), background var(--ff-t), border-color var(--ff-t); }
.navbtn:hover { background: var(--ff-sunken); border-color: var(--ff-muted); color: var(--ff-ink) !important; }
/* The identity end of the bar: who you are and the way out, as ONE flex
   item so the two can never be split across a wrap. */
.topbar .sp { margin-left: auto; display: flex; align-items: center; gap: 10px;
    font-size: 12px; color: var(--ff-muted); }
.topbar .sp .who { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.topbar form { margin: 0; }
.navbtn.on { background: var(--ff-accent-soft) !important; border-color: var(--ff-accent-edge) !important;
    color: var(--ff-accent) !important; }
.navbtn.cta { background: var(--ff-accent); border-color: var(--ff-accent); color: var(--ff-on-accent) !important;
    box-shadow: var(--ff-inset), var(--ff-sh-2); }
.navbtn.cta:hover { background: var(--ff-accent-hi); border-color: var(--ff-accent-hi); color: var(--ff-on-accent) !important; }
.navbtn .navbadge { display: inline-flex; align-items: center; justify-content: center; min-width: 17px;
    height: 17px; padding: 0 5px; margin-left: 7px; border-radius: 999px; background: var(--ff-flag-fill);
    color: var(--ff-on-flag); font: 700 10.5px var(--mono); vertical-align: 1px; }
/* Phone widths. The candidate's header used to wrap: brand + My exams +
   name filled the line and "Sign out" dropped onto a second row of its
   own, doubling the bar's height and reading as an accident. It was a
   plain width problem — 353px of content in a 339px line — so the fix is
   to make the row fit: tighter grid padding and pills, and a name that
   truncates instead of claiming whatever it needs. Deliberately NOT a
   menu button: a Sign out somebody has to go looking for is worse than
   one that wrapped. Roles carrying four nav items (examiner, marker) can
   still wrap, but they now wrap onto a shorter bar. */
@media (max-width: 480px) {
    .topbar { gap: 7px; padding: 9px 14px; }
    .topbar .brand { font-size: 15.5px; margin-right: 0; }
    .navbtn { padding: 6px 10px; font-size: 12.5px; }
    .navbtn .navbadge { margin-left: 5px; }
    .topbar .sp { gap: 7px; }
    .topbar .sp .who { max-width: 10ch; }
}

/* =====================================================================
   Loading. The loader itself (.loadbar, .sxload, .sxveil) lives in
   public/css/page-loader.css — plain CSS, because the Filament panel
   loads the very same file and shows the very same loader.
   ===================================================================== */

/* the inline spinner inside a busy button */
.spin { display: inline-block; width: 14px; height: 14px; border-radius: 50%; margin-right: 7px;
    border: 2px solid rgba(255,255,255,.4); border-top-color: #fff; vertical-align: -2px;
    animation: sx-spin .7s linear infinite; }
.spin.dark { border-color: rgba(11,26,30,.2); border-top-color: var(--ff-ink-2); }

@keyframes sx-spin { to { transform: rotate(360deg); } }

/* visually hidden, still announced by screen readers */
.visually-hidden { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* page & headings */
.page { max-width: 1180px; margin: 0 auto; padding: 20px 18px 60px; }
.page.wide { max-width: 1460px; }
.page.narrow { max-width: 440px; }
h1.t { margin: 0 0 3px; font-size: 20px; letter-spacing: -.018em; font-weight: 650; text-wrap: balance; }
.sub { color: var(--ff-muted); font-size: 13px; margin-bottom: 18px; }
.head { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px; flex-wrap: wrap; margin-bottom: 18px; }

/* breadcrumbs — one "where am I, how do I get back" pattern for every
   tool screen (marking, previews, builders), rendered by <x-crumbs> */
.crumbs { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    font: 600 11px var(--sxfont); letter-spacing: .09em; text-transform: uppercase;
    color: var(--ff-muted); margin: 0 0 6px; }
.crumbs a { color: var(--ff-muted); }
.crumbs a:hover { color: var(--ff-accent); text-decoration: underline; }
.crumbs .sep { color: var(--ff-line-2); }
.crumbs [aria-current] { color: var(--ff-accent); font-weight: 700; }

/* cards */
.card { background: var(--ff-surface); border: 1px solid var(--ff-line); border-radius: var(--ff-r-md);
    margin-bottom: 16px; overflow: hidden; box-shadow: var(--ff-sh-1); }
.card > h2 { margin: 0; padding: 14px 17px; font-size: 11px; letter-spacing: .13em; text-transform: uppercase;
    color: var(--ff-muted); border-bottom: 1px solid var(--ff-line); display: flex; align-items: center;
    justify-content: space-between; gap: 10px; flex-wrap: wrap; font-weight: 700; font-family: var(--sxfont); }
.card .body { padding: 17px; }
/* A card that folds. The SUMMARY is the card's own header — never an
   <h2> nested inside it, which loses the header's padding and border
   and leaves a bare strip of text where a card ought to be. The
   disclosure arrow is drawn here because the native marker cannot be
   positioned and looks like a bullet on Windows. */
.cardfold > summary { margin: 0; padding: 14px 40px 14px 17px; position: relative; cursor: pointer;
    list-style: none; font: 700 11px var(--sxfont); letter-spacing: .13em; text-transform: uppercase;
    color: var(--ff-muted); border-bottom: 1px solid var(--ff-line);
    display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.cardfold > summary::-webkit-details-marker { display: none; }
.cardfold > summary::after { content: ''; position: absolute; right: 17px; top: 50%;
    width: 7px; height: 7px; margin-top: -5px; border-right: 1.5px solid var(--ff-muted);
    border-bottom: 1.5px solid var(--ff-muted); transform: rotate(45deg);
    transition: transform var(--ff-t); }
.cardfold[open] > summary::after { transform: rotate(225deg); margin-top: -2px; }
.cardfold > summary:hover { color: var(--ff-accent); background: var(--ff-sunken); }
.cardfold > summary:focus-visible { outline: 2px solid var(--ff-accent); outline-offset: -2px; }
.cardfold:not([open]) > summary { border-bottom: 0; }
.hint { color: var(--ff-muted); font-size: 12.5px; line-height: 1.6; margin: 0; }
.hint.tight { margin-top: 8px; }

/* buttons */
.btn { font: 600 13px var(--sxfont); cursor: pointer; border-radius: var(--ff-r-sm); padding: 8px 14px;
    border: 1px solid var(--ff-line-2); background: var(--ff-surface); color: var(--ff-ink-2);
    display: inline-block; box-shadow: var(--ff-sh-1); transition: color var(--ff-t), background var(--ff-t),
    border-color var(--ff-t), transform var(--ff-t); }
.btn:hover { color: var(--ff-ink); border-color: var(--ff-muted); background: var(--ff-surface); }
.btn:active { transform: none; }
.btn.primary { background: var(--ff-accent); border-color: var(--ff-accent); color: var(--ff-on-accent);
    box-shadow: var(--ff-inset), var(--ff-sh-2); }
.btn.primary:hover { background: var(--ff-accent-hi); border-color: var(--ff-accent-hi); color: var(--ff-on-accent); }
.btn.ok { background: var(--ff-good); border-color: var(--ff-good); color: #fff;
    box-shadow: var(--ff-inset), var(--ff-sh-2); }
.btn.ok:hover { background: var(--ff-good); border-color: var(--ff-good); filter: brightness(1.1); }
.btn.danger { color: var(--ff-crit); border-color: var(--ff-crit-edge); background: var(--ff-crit-soft); box-shadow: none; }
.btn.danger:hover { color: var(--ff-crit); border-color: var(--ff-crit); background: var(--ff-crit-soft); }
.btn.sm { padding: 6px 11px; font-size: 12px; }
/* The Save button's morph (workbook builder): "Saving…" rides
   wire:loading; "✓ Saved" is an Alpine-local state armed by the
   sx-notify save variant for ~2.5s. Green here is the same statement
   as the toast's edge — the save is DONE — never a resting style. */
.btn.primary.is-saved,
.btn.primary.is-saved:hover { background: var(--ff-good); border-color: var(--ff-good); color: #fff; }
.btn:disabled { opacity: .45; cursor: not-allowed; }
.btn.disabled { opacity: .45; pointer-events: none; }
.btnrow { display: flex; gap: 9px; flex-wrap: wrap; align-items: center; }

/* fields */
label.f { display: block; font: 700 10px var(--sxfont); letter-spacing: .13em; text-transform: uppercase;
    color: var(--ff-muted); margin-bottom: 6px; }
.in { box-sizing: border-box; width: 100%; font: 400 14px var(--sxfont); padding: 9px 11px; border: 1px solid var(--ff-line-2);
    border-radius: var(--ff-r-sm); color: var(--ff-ink); background: var(--ff-surface); }
.in::placeholder { color: var(--ff-muted); opacity: .9; }
.in:hover { border-color: var(--ff-muted); }
.in:focus { outline: 2px solid var(--ff-accent); outline-offset: -1px; border-color: var(--ff-accent); }
/* a stacked field: label, control, then its hint or error */
.fld { margin-bottom: 15px; }
.fld:last-child { margin-bottom: 0; }
.fld .hint, .fld .err { margin: 6px 0 0; }
.fld .err { color: var(--ff-crit); font-size: 12.5px; }

.r2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.r3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; }
.r4 { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 12px; }
@media (max-width: 760px) { .r2, .r3, .r4 { grid-template-columns: 1fr; } }
.field { margin-bottom: 14px; }

/* chips & tags */
.chips { display: flex; gap: 7px; flex-wrap: wrap; align-items: center; }
.chip { font: 600 12.5px var(--sxfont); padding: 7px 12px; border-radius: 999px; border: 1px solid var(--ff-line);
    color: var(--ff-ink-2); background: var(--ff-surface); cursor: pointer; white-space: nowrap;
    transition: color var(--ff-t), background var(--ff-t), border-color var(--ff-t); }
.chip:hover { border-color: var(--ff-line-2); background: var(--ff-sunken); color: var(--ff-ink); }
.chip.acct { border-color: var(--ff-accent-edge); background: var(--ff-accent-soft); color: var(--ff-accent); }
.chip.on { background: var(--ff-accent) !important; border-color: var(--ff-accent) !important; color: var(--ff-on-accent) !important; }
/* =====================================================================
   workbook navigation (the examiner's editor): PAGES over TABS.
   The two rows share a label column so the hierarchy is visible at a
   glance instead of reading as two piles of chips; "add" controls stay
   folded away behind a + until they are wanted.
   ===================================================================== */
.wbnav { border: 1px solid var(--ff-line); border-radius: var(--ff-r-md); background: var(--ff-surface);
    margin-bottom: 14px; box-shadow: var(--ff-sh-1); overflow: hidden;
    /* It is the first cell of .wbcols now. Narrow, it spans the whole
       grid and reads exactly as it always did — a band above the sheet. */
    grid-column: 1 / -1; }
.wbnav-row { display: grid; grid-template-columns: 96px minmax(0, 1fr); align-items: start;
    gap: 12px; padding: 10px 12px; }
.wbnav-row + .wbnav-row { border-top: 1px solid var(--ff-line); background: var(--ff-sunken); }
.wbnav-label { font: 700 10px var(--sxfont); letter-spacing: .13em; text-transform: uppercase;
    color: var(--ff-muted); padding-top: 8px; }
.wbnav-label small { display: block; font: 600 10.5px var(--sxfont); letter-spacing: .02em;
    text-transform: none; color: var(--ff-ink-2); margin-top: 2px; overflow-wrap: anywhere; }
.wbnav-items { display: flex; gap: 7px; flex-wrap: wrap; align-items: center; min-width: 0; }
/* One element per chip, so Livewire has something stable to key and morph
   against — and so the chip and its rename box occupy the same slot. */
.wbnav-slot, .wbchip-slot { display: inline-flex; align-items: center; }
.wbnav-edit { display: inline-flex; gap: 7px; align-items: center; flex-wrap: wrap; }
.wbnav-sep { width: 1px; height: 20px; background: var(--ff-line); margin: 0 3px; }
.wbnav-add { display: inline-flex; align-items: center; gap: 6px;
    font: 600 12.5px var(--sxfont); padding: 7px 12px; border-radius: 999px; cursor: pointer;
    border: 1px dashed var(--ff-line-2); background: var(--ff-surface); color: var(--ff-ink-2); white-space: nowrap; }
.wbnav-add:hover { border-color: var(--ff-accent); color: var(--ff-accent); background: var(--ff-accent-soft); }

/* THE ADD MENU. Six pill buttons in a wrapping row read fine across a
   wide band and fell apart in a 268px rail — a ragged pile, with the one
   option carrying an icon stacking its label underneath it. A menu
   instead: one row per thing, and the sentence saying what the thing IS
   moved out of a title attribute (which nobody hovers, and which a
   keyboard or touch user never sees at all) onto the row itself. */
.wbadd { display: block; width: 100%; max-width: 460px; margin-top: 4px;
    border: 1px solid var(--ff-line); border-radius: var(--ff-r-md);
    background: var(--ff-surface); box-shadow: var(--ff-sh-2); overflow: hidden; }
.wbadd-head { padding: 9px 13px; font: 700 10px var(--sxfont); letter-spacing: .13em;
    text-transform: uppercase; color: var(--ff-muted); background: var(--ff-sunken);
    border-bottom: 1px solid var(--ff-line); }
.wbadd-item { display: flex; align-items: flex-start; gap: 10px; width: 100%; text-align: left;
    padding: 10px 13px; border: 0; background: none; cursor: pointer; color: var(--ff-ink-2);
    font: inherit; border-bottom: 1px solid var(--ff-line); }
.wbadd-item:last-of-type { border-bottom: 0; }
.wbadd-item:hover { background: var(--ff-accent-soft); color: var(--ff-accent); }
.wbadd-item:focus-visible { outline: 2px solid var(--ff-accent); outline-offset: -2px; }
.wbadd-item .ico-sheet { margin-top: 2px; flex: none; color: var(--ff-muted); }
.wbadd-item:hover .ico-sheet { color: var(--ff-accent); }
.wbadd-item > span { min-width: 0; display: block; }
.wbadd-item b { display: block; font: 650 13px var(--sxfont); color: var(--ff-ink); }
.wbadd-item:hover b { color: var(--ff-accent); }
.wbadd-item small { display: block; margin-top: 1px; font: 400 11.5px/1.45 var(--sxfont);
    color: var(--ff-muted); }
.wbadd-foot { padding: 9px 13px; border-top: 1px solid var(--ff-line); background: var(--ff-sunken); }
.ico-sheet { flex: 0 0 auto; }

/* a page or tab chip: a name, plus the actions that belong to it */
.wbchip { display: inline-flex; align-items: center; gap: 3px; padding: 0 6px 0 0;
    border: 1px solid var(--ff-line); border-radius: 999px; background: var(--ff-surface); color: var(--ff-ink-2);
    font: 600 12.5px var(--sxfont); max-width: 100%; }
.wbchip:hover { border-color: var(--ff-line-2); background: var(--ff-sunken); }
.wbchip.tab { border-color: var(--ff-accent-edge); background: var(--ff-accent-soft); color: var(--ff-accent); }
.wbchip.on { background: var(--ff-accent); border-color: var(--ff-accent); color: var(--ff-on-accent); }
.wbchip-name { display: inline-block; padding: 7px 4px 7px 12px; border: 0; background: none;
    color: inherit; font: inherit; cursor: pointer; max-width: 260px; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; }
.wbchip-count { font: 700 10.5px var(--mono); padding: 2px 6px; border-radius: 999px;
    background: var(--ff-ground-2); color: var(--ff-muted); margin-right: 4px; }
.wbchip.on .wbchip-count { background: rgba(255,255,255,.25); color: var(--ff-on-accent); }
.wbchip-kind { font: 700 9.5px var(--sxfont); letter-spacing: .1em; text-transform: uppercase;
    padding: 2px 6px; border-radius: 4px; background: var(--ff-ground-2); color: var(--ff-muted); margin-right: 2px; }
.wbchip.on .wbchip-kind { background: rgba(255,255,255,.22); color: var(--ff-on-accent); }
.wbchip-act { border: 0; background: none; cursor: pointer; color: inherit; opacity: .55;
    font-size: 13px; line-height: 1; padding: 6px 5px; border-radius: 999px; }
.wbchip-act:hover { opacity: 1; background: var(--ff-ground-2); }
.wbchip.on .wbchip-act:hover { background: rgba(255,255,255,.22); }
/* Pages drag into reading order: the handle carries the grab affordance,
   the slot paints the insertion cue as a 2px accent rule on the side the
   dragged page would land. Alt+arrow does the same from the keyboard. */
.wbchip-drag { cursor: grab; user-select: none; touch-action: none; opacity: .45;
    font: 700 11px var(--sxfont); line-height: 1; padding: 8px 5px 8px 11px; }
.wbchip-drag:active { cursor: grabbing; }
.wbchip-drag:hover { opacity: .9; }
.wbchip-drag + .wbchip-name { padding-left: 6px; }
.wbchip-slot.wbdragging { opacity: .45; }
.wbchip-slot.wbdrop-before { box-shadow: -3px 0 0 0 var(--ff-accent); border-radius: 2px; }
.wbchip-slot.wbdrop-after { box-shadow: 3px 0 0 0 var(--ff-accent); border-radius: 2px; }

/* the tab's own settings, as labelled fields rather than a run of
   controls crammed into the card heading */
.wbset { display: flex; flex-wrap: wrap; gap: 12px; padding: 12px 16px;
    border-bottom: 1px solid var(--ff-line); background: var(--ff-sunken); }
.wbset-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.wbset-field > span { font: 700 10px var(--sxfont); letter-spacing: .13em; text-transform: uppercase;
    color: var(--ff-muted); }
.wbset-field .in { width: 230px; padding: 7px 10px; }
.wbset-field.narrow .in { width: 88px; }

/* what can be done to the current selection */
.wbtools { display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: center;
    padding: 10px 16px; border-bottom: 1px solid var(--ff-line); }
.wbtools-label { font: 600 12px var(--sxfont); color: var(--ff-muted);
    display: inline-flex; align-items: center; gap: 6px; }

/* =====================================================================
   THE BUILDER, MADE CALM (client, 2026-09-09: "still as messy as it is
   before ... not that user friendly").

   Three things were wrong and none of them was paint. The spreadsheet —
   the only reason this screen exists — was the smallest object on the
   page. The same three facts about cells were taught in three places
   ~900px apart. And the rail was three sibling cards of identical
   weight carrying TWO buttons both labelled "Apply", which acted on
   different things and could never both be meaningful, because
   syncSelection() sets either a cell or a range and never both.
   ===================================================================== */

/* ① the sheet frame. The tab's settings fold onto its own name instead
      of standing as a band above the grid on every render. INLINE, not
      a popover: `.card` is overflow:hidden, so an absolutely positioned
      dropdown in here is clipped with no error at all. */
.wbsheet > h2 { gap: 8px; }
.wbsheet-set { margin-inline-start: auto; }
.wbsheet-set > summary { list-style: none; cursor: pointer; display: inline-flex;
    align-items: center; gap: 7px; padding: 2px 4px; border-radius: var(--ff-r-sm); }
.wbsheet-set > summary::-webkit-details-marker { display: none; }
.wbsheet-set > summary:hover { background: var(--ff-sunken); }
.wbsheet-set > summary:focus-visible { outline: 2px solid var(--ff-accent); outline-offset: 1px; }
.wbsheet-set-cog { font-size: 13px; color: var(--ff-muted); line-height: 1; }
.wbsheet-set[open] .wbsheet-set-cog { color: var(--ff-accent); }
/* The band is a sibling of the <h2> in flow terms — it must span the
   card, not sit inside the header's flex row. */
.wbsheet-set > .wbset { width: 100%; margin: 12px -17px -14px; box-sizing: content-box;
    padding-inline: 17px; border-top: 1px solid var(--ff-line); border-bottom: 0; }

/* ② the sheet bar: ONE row where there were three bands, a tip and a
      legend. The colour key sits on the same line as the buttons that
      produce those colours. */
.wbsheet-bar { gap: 8px 12px; }
.wbsheet-bar .wbkey { margin: 0 0 0 auto; gap: 6px 12px; font-size: 11px; }
.wbsheet-bar .wbkey i { width: 13px; height: 11px; margin-right: 4px; border-radius: 3px; }
.wbsheet-bar .tiny { max-width: 44ch; }
/* An answer box is amber in the grid, in the mark badge and on the
   candidate's own sheet. The button that MAKES one should not be the
   same teal as Save — that spent the page's one accent on the wrong
   thing and put two filled accents in the same eyeline. */
.btn.amber { background: var(--ff-flag-soft); border-color: var(--ff-flag-edge); color: var(--ff-flag);
    box-shadow: none; font-weight: 700; }
.btn.amber:hover { background: var(--ff-flag-soft); border-color: var(--ff-flag); color: var(--ff-flag); }

/* ③ the inspector: one panel, one job, one button that NAMES its target */
.wbinsp > h2 b { font-family: var(--mono); font-weight: 700; letter-spacing: 0;
    text-transform: none; font-size: 12.5px; color: var(--ff-ink); }
.wbinsp.is-empty > h2 { color: var(--ff-muted); }
.wbinsp-go { width: 100%; margin-top: 13px; text-align: center; }
.wbinsp-range.is-fold { margin: 14px -17px -17px; }
.wbinsp-range.is-fold > .cardfold { border-top: 1px solid var(--ff-line); }
.cardfold.plain { border: 0; border-radius: 0; background: none; box-shadow: none; margin: 0; }
.cardfold.plain > summary { border-bottom: 0; }
.cardfold.plain[open] > summary { border-bottom: 1px solid var(--ff-line); }

/* ④ readiness, beside Save. Progress is not a property of the selection:
      it answers "can this go into a paper yet", which is a question
      about the button next to it. */
.wbready { position: relative; }
.wbready > summary { list-style: none; cursor: pointer; display: inline-flex; align-items: center;
    gap: 6px; white-space: nowrap; font: 700 11.5px var(--sxfont); padding: 7px 12px;
    border-radius: 999px; border: 1px solid var(--ff-line-2); background: var(--ff-surface);
    color: var(--ff-ink-2); }
.wbready > summary::-webkit-details-marker { display: none; }
.wbready.is-ok > summary { background: var(--ff-good-soft); border-color: var(--ff-good-edge); color: var(--ff-good); }
.wbready.is-warn > summary { background: var(--ff-flag-soft); border-color: var(--ff-flag-edge); color: var(--ff-flag); }
.wbready > summary:focus-visible { outline: 2px solid var(--ff-accent); outline-offset: 1px; }
.wbready-body { position: absolute; right: 0; top: calc(100% + 6px); z-index: 30; width: 320px;
    padding: 12px 15px; border: 1px solid var(--ff-line); border-radius: var(--ff-r-md);
    background: var(--ff-surface); box-shadow: var(--ff-sh-3); }
.wbready-step { display: flex; gap: 9px; align-items: flex-start; padding: 7px 0; }
.wbready-step + .wbready-step { border-top: 1px solid var(--ff-line); }
.wbready-tick { flex: none; width: 19px; height: 19px; border-radius: 50%; display: flex;
    align-items: center; justify-content: center; font: 800 11px var(--sxfont); margin-top: 1px;
    background: var(--ff-ground-2); color: var(--ff-muted); }
.wbready-step.is-done .wbready-tick { background: var(--ff-good-soft); color: var(--ff-good); }
.wbready-what { font: 700 12.5px var(--sxfont); color: var(--ff-ink); }
.wbready-step.is-done .wbready-what { color: var(--ff-muted); }

/* FOCUS MODE — the examiner asks the menu to step aside so the sheet can
   have its width (client, 2026-09-09). Opened in a NEW TAB from the
   builder's header, so the tab they came from is untouched.

   `.fi-layout` is display:flex and the sidebar is a flex sibling of the
   main container, so display:none hands its ~290px straight to the
   content — there is no padding offset to unwind and no empty gutter.
   The TOPBAR deliberately stays: it is the way back, and every sticky
   element on this screen is positioned against --sx-panel-top (4rem),
   which is the topbar's own height. */
body:has(.wbfocus) .fi-sidebar,
body:has(.wbfocus) .fi-layout-sidebar-toggle-btn,
body:has(.wbfocus) .fi-sidebar-close-overlay { display: none !important; }
/* One breath less padding, since the whole point is width. */
body:has(.wbfocus) .fi-main { padding-inline: 1.25rem; }

/* ⑤ the command row travels with the examiner. Save was two screens
      below the cell being edited on a long workbook — which is the whole
      reason the save toast had to be invented. */
.wbedit > .head { position: sticky; top: var(--sx-panel-top, 0px); z-index: 20;
    margin: 0 0 14px; padding: 8px 0; background: var(--ff-ground);
    border-bottom: 1px solid var(--ff-line); }
.wbedit > .head::before { content: ''; position: absolute; inset: 0 -20px; z-index: -1;
    background: var(--ff-ground); border-bottom: 1px solid var(--ff-line); }
/* The stage scrolls itself into view when a block is picked; it has to
   clear the bar now, or it lands underneath it. */
.wbedit .wbstage { scroll-margin-top: calc(var(--sx-panel-top, 0px) + 74px); }

/* the page bar: the parts of an answer workbook, above their tabs. It
   is deliberately a different object from the tab strip - a ruled band
   with its own label - so two rows of chips can never be mistaken for
   one long row. */
.pagebar { display: flex; gap: 7px; flex-wrap: wrap; align-items: center;
    padding: 8px 10px; margin-bottom: 8px; border: 1px solid var(--ff-line);
    border-radius: var(--ff-r-md); background: var(--ff-sunken); }
.pagebar-label { font: 700 10px var(--sxfont); letter-spacing: .13em; text-transform: uppercase;
    color: var(--ff-muted); margin-right: 3px; }
.pagebar-item { display: inline-flex; align-items: center; gap: 2px; }
.pagebar .chip { border-color: var(--ff-line-2); }
.tag { font: 650 11px var(--sxfont); letter-spacing: .04em; text-transform: uppercase; padding: 4px 9px;
    border-radius: 6px; background: var(--ff-sunken); border: 1px solid var(--ff-line);
    color: var(--ff-ink-2); }
.tag.acct { background: var(--ff-accent-soft); border-color: var(--ff-accent-edge); color: var(--ff-accent); }

/* The simulator's honesty tag. Worn by every real screen — the exam
   page, the rules gate, the marking script, the result — whenever the
   attempt underneath is a simulation, so a practice paper can never be
   mistaken for a real one. Amber on purpose: not an error, a caution. */
.simtag { display: inline-flex; align-items: center; flex: 0 0 auto; font: 700 10.5px var(--sxfont);
    letter-spacing: .09em; text-transform: uppercase; padding: 3px 9px; border-radius: 999px;
    background: #fffbeb; border: 1px solid #fde68a; color: #92400e; white-space: nowrap; }

/* Payments on the candidate's screens (2026-09-08): one line per fee on
   the exam card, and the rows of the Payments page. The badge carries
   the status in the product's three verdict colours — waiting is amber,
   accepted is green, refused or in doubt is rose — and the reference is
   set in the mono face because it is a thing to be quoted. */
.payline { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 10px;
    font: 500 12.5px var(--sxfont); color: var(--ff-muted); }
.payref { font: 600 11.5px var(--mono); color: var(--ff-ink-2); letter-spacing: .02em; white-space: nowrap; }
.paybadge { display: inline-flex; align-items: center; padding: 3px 9px; border-radius: 999px;
    border: 1px solid var(--ff-line); background: var(--ff-sunken); font: 700 10.5px var(--sxfont);
    letter-spacing: .06em; text-transform: uppercase; color: var(--ff-ink-2); white-space: nowrap; }
.paybadge.ok { background: #ecfdf5; border-color: #a7f3d0; color: #047857; }
.paybadge.wait { background: #fffbeb; border-color: #fde68a; color: #92400e; }
.paybadge.bad { background: #fff1f2; border-color: #fecdd3; color: #be123c; }
/* What the fee desk wrote back when it did not accept a payment — set
   apart as the institution's own words, not folded into the status. */
.payremark { display: flex; gap: 8px; align-items: flex-start; margin: 8px 0 10px; padding: 9px 12px;
    border-radius: 8px; background: #fff1f2; border: 1px solid #fecdd3; color: #9f1239;
    font: 500 12.5px/1.55 var(--sxfont); max-width: 60ch; }
.payremark b { font-weight: 700; white-space: nowrap; }
/* The Payments page's search: one box, a count while it filters, Clear. */
.paysearch { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 0 0 12px; }
.paysearch .in { max-width: 440px; flex: 1 1 260px; }
.paysearch .hint { margin: 0; white-space: nowrap; }
/* Earlier tries at the same fee — a refused transfer, a cancelled card
   session — listed under the row that carries the fee's present state. */
.paytries { margin: 8px 0 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 4px; }
.paytries li { font: 500 12px/1.6 var(--sxfont); color: var(--ff-muted); display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.paytries .paybadge { padding: 1px 7px; font-size: 10px; }
.paytries .rem { color: #9f1239; }
.payrows { display: flex; flex-direction: column; }
.payrow { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px 18px; align-items: start;
    padding: 14px 0; border-top: 1px solid var(--ff-line); }
.payrow:first-child { border-top: 0; padding-top: 0; }
.payrow .nm { font: 650 14.5px var(--sxfont); color: var(--ff-ink); }
.payrow .why { margin-top: 3px; font: 500 12.5px/1.6 var(--sxfont); color: var(--ff-muted); }
.payrow .amt { font: 700 16px var(--mono); color: var(--ff-ink); text-align: right; white-space: nowrap; }
.payrow .acts { display: flex; gap: 8px; justify-content: flex-end; flex-wrap: wrap; }
@media (max-width: 640px) {
    .payrow { grid-template-columns: 1fr; }
    .payrow .amt { text-align: left; }
    .payrow .acts { justify-content: flex-start; }
}

/* segmented control */
.seg { display: flex; background: var(--ff-ground-2); border-radius: var(--ff-r-md); padding: 3px; gap: 3px; }
.seg button { flex: 1; font: 600 12.5px var(--sxfont); cursor: pointer; border: 0; background: none;
    color: var(--ff-muted); padding: 8px 6px; border-radius: var(--ff-r-sm); }
.seg button.on { background: var(--ff-surface); color: var(--ff-ink); box-shadow: var(--ff-sh-1); }

/* callouts. Amber is the default because a callout is nearly always
   "you must act"; accent is the neutral aside; crit is reserved. */
.callout { background: var(--ff-flag-soft); border: 1px solid var(--ff-flag-edge); color: var(--ff-flag);
    border-radius: var(--ff-r-md); padding: 11px 13px; font-size: 12.5px; line-height: 1.6; font-weight: 600; }
.callout.blue { background: var(--ff-accent-soft); border-color: var(--ff-accent-edge); color: var(--ff-accent); }
.callout.rose { background: var(--ff-crit-soft); border-color: var(--ff-crit-edge); color: var(--ff-crit); }
.callout.green { background: var(--ff-good-soft); border-color: var(--ff-good-edge); color: var(--ff-good); }

/* stem & misc */
.stem { font-size: 14.5px; line-height: 1.68; color: var(--ff-ink-2); }
.stem ul { list-style: disc; padding-left: 20px; }
.stem ol { list-style: decimal; padding-left: 20px; }
.stem h2 { font-size: 16px; font-weight: 650; margin: 10px 0 4px; color: var(--ff-ink); }
.stem h3 { font-size: 14.5px; font-weight: 650; margin: 8px 0 3px; color: var(--ff-ink); }
.stem b, .stem strong { color: var(--ff-ink); font-weight: 650; }
.stem p { margin: 0 0 8px; }
.stem p:last-child { margin-bottom: 0; }
.stem blockquote { border-left: 3px solid var(--ff-line-2); padding-left: 12px; color: var(--ff-muted); margin: 8px 0; }
/* A table authored in the question editor - or pasted from Excel -
   must read as a set of accounts, not as loose text. A wide schedule
   scrolls inside the stem rather than pushing the paper sideways. */
.stem { overflow-x: auto; }
.stem table {
    border-collapse: collapse; margin: 10px 0; min-width: 100%;
    font-size: 13px; font-variant-numeric: tabular-nums;
}
.stem :is(th, td) {
    border: 1px solid var(--ff-line); padding: 7px 10px;
    text-align: left; vertical-align: top;
}
.stem th { background: var(--ff-sunken); font: 700 10px var(--sxfont); letter-spacing: .1em;
    text-transform: uppercase; color: var(--ff-muted); white-space: nowrap; }
.stem :is(th, td) > p { margin: 0; }
.stem caption { caption-side: top; text-align: left; font-weight: 650; color: var(--ff-ink); padding-bottom: 4px; }
.tiny { font-size: 12px; color: var(--ff-muted); }
.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
/* every figure in an accounting product is monospaced and tabular */
.num { font-family: var(--mono); font-variant-numeric: tabular-nums; }

/* =====================================================================
   §GRID — the worksheet, the heart of the product.

   FIVE cell roles, and they are MARKING semantics, not decoration.
   Each one is told apart by three channels, not one, so the grid is
   still readable to a candidate who cannot separate the hues:

     role      fill            ink        shape cue
     ------------------------------------------------------------------
     label     surface         ink-2      none — it is the paper
     given     sunken          ink-2      none, and nothing to type in
     input     flag-soft       flag       hairline ring + a HEAVIER
                                          bottom rule: something to
                                          write on (from the mockup)
     computed  calc-soft       calc       a 3px bar down the left
                                          margin: derived, not typed
     free      surface         ink        nothing — it is working, and
                                          working carries no marks

   Contrast, measured (light / dark):
     input     #9a5b00 on #fbefdc = 4.78:1  ·  #e9a94a on #2a2011 = 7.80:1
     computed  #5b21b6 on #f1edfb = 7.91:1  ·  #c4b5fd on #1b1530 = 9.51:1
     given     #3b4e54 on #f7f9fa = 8.27:1  ·  #b3c4c8 on #0c1519 = 10.24:1
     label     #3b4e54 on #ffffff = 8.74:1  ·  #b3c4c8 on #101a1e = 9.80:1
     marks .mk #0a6e80 on #e4f1f3 = 5.11:1  ·  #4fc3d2 on #0e2e35 = 6.89:1

   Separation (OKLab dE, light) against the palette this replaces:
     input vs computed FILL  0.0448 (was 0.0356)
     input vs computed INK   0.3195 (was 0.3179)
     given vs surface FILL   0.0363 (was 0.0162)
   — every distinction is at least as wide as before, and the shape
   cues are new. Amber still means "a mark is available here"; purple
   still means "the sheet worked this out".
   ===================================================================== */
.gridwrap { overflow: auto; border: 1px solid var(--ff-line); border-radius: var(--ff-r-md);
    background: var(--ff-surface); max-height: 66vh; box-shadow: var(--ff-sh-1); }
table.sheet { border-collapse: separate; border-spacing: 0; font-size: 13px; width: auto; }
table.sheet th, table.sheet td { border-right: 1px solid var(--ff-line); border-bottom: 1px solid var(--ff-line);
    padding: 0; background: var(--ff-surface); white-space: nowrap; }
table.sheet th.hdr { background: var(--ff-sunken); color: var(--ff-muted); font: 700 10px var(--mono);
    letter-spacing: .1em; text-align: center; padding: 6px 7px; position: sticky; top: 0; z-index: 3;
    border-bottom-color: var(--ff-line-2); }
table.sheet td.rn { background: var(--ff-sunken); color: var(--ff-muted); font: 700 10.5px var(--mono);
    text-align: center; padding: 4px 7px; position: sticky; left: 0; z-index: 2; min-width: 34px;
    border-right-color: var(--ff-line-2); }
table.sheet th.corner { position: sticky; left: 0; top: 0; z-index: 4; }
.gc { padding: 6px 9px; min-width: 84px; max-width: 240px; overflow: hidden; text-overflow: ellipsis;
    min-height: 27px; position: relative; }
.gc.label { color: var(--ff-ink-2); font-weight: 600; background: var(--ff-surface); }
.gc.given { background: var(--ff-sunken); color: var(--ff-ink-2); text-align: right;
    font-family: var(--mono); font-variant-numeric: tabular-nums; }
/* An answer box. The heavier bottom rule is the mockup's "something to
   write on"; the ring is what says a mark lives here. */
.gc.input { background: var(--ff-flag-soft); text-align: right; font-family: var(--mono);
    font-variant-numeric: tabular-nums; color: var(--ff-flag);
    box-shadow: inset 0 0 0 1px var(--ff-flag-edge), inset 0 -2px 0 var(--ff-flag-edge); }
/* Derived. The bar in the left margin is the non-colour channel — the
   figure was worked out, not written. */
.gc.computed, .gc.live { background: var(--ff-calc-soft); text-align: right; font-family: var(--mono);
    font-variant-numeric: tabular-nums; color: var(--ff-calc);
    box-shadow: inset 3px 0 0 var(--ff-calc-edge); }
.gc.live { font-weight: 600; }
.gc.spacer { background: var(--ff-ground-2); }
.gc.sel { outline: 2px solid var(--ff-accent); outline-offset: -2px; z-index: 1; }
/* A cell clips its own content so a long label ellipses — which also
   clipped the focus ring of the box inside it, on the one screen where
   knowing which box has the caret matters most. Let the ring out while
   the cell holds focus; the ellipsis is only wanted at rest. */
.gc:focus-within { overflow: visible; z-index: 2; }
/* Marks available here. A bordered chip rather than a solid blob, so
   it never fights the figure underneath it. */
.gc .mk { position: absolute; top: 2px; right: 2px; font: 700 9px var(--mono); line-height: 1.35;
    background: var(--ff-accent-soft); color: var(--ff-accent); border: 1px solid var(--ff-accent-edge);
    border-radius: 4px; padding: 0 4px; pointer-events: none; }
/* The chip goes OPPOSITE the figure. Every numeric role is
   right-aligned, so a top-right badge sat on top of the candidate's own
   answer — "48,200" read as "48,2". On those roles it moves to the top
   left, clear of the computed cell's margin bar, and the box is padded
   so a long figure cannot run under it either. */
.gc.input .mk, .gc.given .mk, .gc.computed .mk, .gc.live .mk { right: auto; left: 3px; }
.gc.computed .mk, .gc.live .mk { left: 7px; }
.gc.input:has(.mk) input.gi { padding-left: 26px; }
/* editable cells (builder): the cell supplies the look, the input is transparent */
.gc.edit { padding: 0; cursor: text; }
.gc.edit input { box-sizing: border-box; width: 100%; min-width: 84px; border: 0; background: transparent; padding: 6px 9px;
    font: inherit; color: inherit; text-align: inherit; outline: none; }
.gc.pick { cursor: pointer; }
/* student inputs */
.gc input.gi { box-sizing: border-box; width: 100%; min-width: 84px; border: 1px solid transparent; border-radius: 5px; padding: 4px 6px;
    font: 400 13px var(--mono); font-variant-numeric: tabular-nums; text-align: right; background: transparent;
    color: var(--ff-flag); outline: none; }
.gc input.gi:focus { border-color: var(--ff-accent); background: var(--ff-surface); color: var(--ff-ink);
    box-shadow: 0 0 0 3px var(--ff-accent-soft), 0 0 0 4px var(--ff-accent-edge); }

/* The candidate answered with a formula: the box shows what it works out
   to until they click back into it, the way a spreadsheet does. Painted
   over the input rather than replacing it, so the caret, the tab order
   and Livewire's binding are all untouched. */
.gc.input { position: relative; }
.gcalc { position: absolute; inset: 1px; display: flex; align-items: center; justify-content: flex-end;
    padding: 0 7px; border-radius: 5px; background: var(--ff-flag-soft); pointer-events: none;
    font-family: var(--mono); font-variant-numeric: tabular-nums; color: var(--ff-flag); }
.gcalc::before { content: 'fx'; position: absolute; left: 5px; font: italic 700 9px var(--mono);
    letter-spacing: .04em; color: var(--ff-flag); opacity: .75; }

/* A cell the template left empty, which the candidate may write in.
   Quieter than an answer box: it is working, not an answer. */
.gc.free { position: relative; background: var(--ff-surface); text-align: left; box-shadow: none; }
.gc.free input.gi { text-align: left; font-family: var(--sxfont); font-weight: 400; color: var(--ff-ink); min-width: 70px; }
.gc.free .gcalc { background: var(--ff-surface); color: var(--ff-ink-2); justify-content: flex-start; padding-left: 20px; }
.gc.free .gcalc::before { color: var(--ff-muted); }

/* The candidate's own mark-up. Presentation only — no mark depends on it. */
.gc.st-b input.gi, .gc.st-b .gcalc { font-weight: 700; }
.gc.st-u input.gi, .gc.st-u .gcalc { text-decoration: underline; }
.gc.st-h, .gc.st-h input.gi, .gc.st-h .gcalc { background: var(--ff-hl); }

/* The working bar above a sheet the candidate may write freely in. */
.wbbar { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; margin-bottom: 8px;
    padding: 7px 10px; border: 1px solid var(--ff-line); border-radius: var(--ff-r-md);
    background: var(--ff-surface); box-shadow: var(--ff-sh-1); }
.wbbar-cell { font: 700 11px var(--mono); letter-spacing: .04em; color: var(--ff-muted);
    min-width: 62px; }
.wbbar-btn { font: 600 12.5px var(--sxfont); cursor: pointer; padding: 5px 10px; border-radius: 7px;
    border: 1px solid var(--ff-line); background: var(--ff-surface); color: var(--ff-ink-2); }
.wbbar-btn:hover:not(:disabled) { background: var(--ff-sunken); color: var(--ff-ink); }
.wbbar-btn:disabled { opacity: .4; cursor: not-allowed; }
.wbbar-btn.hl { background: var(--ff-hl); border-color: var(--ff-flag-edge); color: var(--ff-ink); }
.wbbar-sep { width: 1px; height: 18px; background: var(--ff-line); }
.wbbar-hint { font-size: 11.5px; color: var(--ff-muted); margin-left: auto; }

/* THE CHROME BUDGET ABOVE A CANDIDATE'S GRID.
   Measured on the runner: 197px of a 705px pane spent before row 1 — an
   18px hint paragraph, a chip strip, a 44px bar carrying a second hint,
   then Univer's own ribbon and formula bar — on the screen size where
   ten rows were visible. Everything that is not the grid folds into one
   28px line, closed. A candidate who wants the explanation opens it;
   nobody pays for it twice. */
.qmain.fill .wbbar { padding: 3px 8px; margin-bottom: 6px; box-shadow: none; }
/* Over the Univer surface the bar is the SHEET'S OWN top strip — one
   object over the grid, not a white card floating above a bordered
   card with a grey band inside it (client, 2026-09-08). Scoped to the
   Univer stack; the legacy grid keeps its card. Hovers are restated for
   a sunken ground, where the old sunken hover disappeared. The gain is
   ~6px and one fewer frame; the deliverable is the finish. */
.qmain.fill .wbbar:has(+ .sx-univer-host) { margin-bottom: 0; padding: 4px 8px; gap: 6px;
    border-bottom: 0; border-radius: var(--ff-r-md) var(--ff-r-md) 0 0; background: var(--ff-sunken);
    box-shadow: none; }
.qmain.fill .wbbar:has(+ .sx-univer-host) + .sx-univer-host { border-top-left-radius: 0; border-top-right-radius: 0; }
.qmain.fill .wbbar:has(+ .sx-univer-host) .wbbar-btn { height: 26px; padding: 0 10px; border-radius: 6px; }
.qmain.fill .wbbar:has(+ .sx-univer-host) .wbbar-btn:hover:not(:disabled) { background: var(--ff-surface);
    border-color: var(--ff-line-2); color: var(--ff-ink); }
.qmain.fill .wbbar:has(+ .sx-univer-host) .chip { padding: 4px 10px; }
.qmain.fill .wbbar:has(+ .sx-univer-host) .chip:hover { background: var(--ff-surface); }
.qmain.fill .wbbar:has(+ .sx-univer-host) .wbfold > summary { height: 26px; padding: 0 6px; }
.qmain.fill .wbbar:has(+ .sx-univer-host) .wbfold > summary:hover { background: var(--ff-surface); color: var(--ff-ink); }
.wbfold { min-width: 0; }
.wbfold > summary { display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
    list-style: none; padding: 3px 4px; border-radius: 6px;
    font: 600 12px var(--sxfont); color: var(--ff-muted); }
.wbfold > summary::-webkit-details-marker { display: none; }
.wbfold > summary::before { content: '▸'; font-size: 10px; color: var(--ff-muted); }
.wbfold[open] > summary::before { content: '▾'; }
.wbfold > summary:hover { background: var(--ff-sunken); color: var(--ff-ink); }
/* Open, the explanation is a POPOVER under its trigger, over the grid —
   the host never changes size, so Univer never re-measures for a help
   click, and a candidate who opens it and forgets pays no rows for it
   (client, 2026-09-08; it used to drop onto a line of its own and push
   the grid down ~150px). The trigger sits at the bar's right edge so
   `right: 0` hangs the sheet under it at every width; z 30 clears
   Univer's own header (z 10) and stays under the floating windows (85). */
.wbbar { position: relative; }
.wbbar .wbfold { margin-left: auto; }
.wbfold-body { position: absolute; right: 0; top: calc(100% + 6px); z-index: 30;
    width: min(78ch, calc(100% - 16px)); padding: 10px 12px; background: var(--ff-surface);
    border: 1px solid var(--ff-line); border-radius: var(--ff-r-md); box-shadow: var(--ff-sh-2); }
.wbfold-body p { margin: 0 0 6px; font-size: 12.5px; line-height: 1.55; color: var(--ff-ink-2); }
.wbfold-body p:last-child { margin-bottom: 0; }

/* EXCEL'S OWN TAB STRIP, AND THE CANDIDATE MAY ONLY SWITCH TABS.
   The paper's tabs belong to the examiner: adding, renaming, deleting,
   copying, recolouring, hiding and reordering are removed from every
   menu AND cancelled before they run (REFUSED / HIDDEN_MENU in
   univer-bridge.js). The one control the menu config cannot reach is
   the "+" button, which is markup rather than a menu item — so it goes
   on the pinned vendor hook, exactly as the builder's own buttons are
   dressed above. Hidden AND refused, never one of the two. */
.sx-univer-host [data-u-comp="sheet-bar-append-button"] { display: none !important; }
.sx-univer-host [data-u-comp="slide-tab-item"] { cursor: pointer; }
/* On the screen size most exam halls run the response pane is 663px
   wide and the requirement band is open by default, so a 300px floor
   pushed the tab strip below the fold and made the whole column scroll
   to reach it — the strip the client asked for, hidden again. The floor
   drops on a short screen: the grid still grows into every pixel the
   question's own chrome can spare, and folding the requirements gives
   it another 200. */
@media (max-height: 820px) {
    .sxdesk .qmain.fill .sx-univer-host { min-height: 232px; }

    /* Measured in Chrome at 1366x768, the resolution the client's
       complaint came from: of the 712px between the rails, the question's
       own chrome took 83, the requirement band 146, and Univer's own
       ribbon, toolbar, formula bar and tab strip 144 — leaving the grid
       ITSELF 168px, about six rows. The band stays (it is the marked
       task, and folding it is the candidate's own choice — it is
       remembered, and folding it here really does hand the grid another
       105px, measured). The two lines that only repeated the screen
       are gone at every height now (see .qmain.fill above); what this
       block keeps is the metric trade a short screen still needs —
       tighter gaps, and buttons set to the row they sit in: Flag and
       Report were 30px tall on a 22px title row, and they set the row's
       height. With the simple ribbon (Univer's 144 → ~100) and the
       strip joined to the sheet, the grid gets roughly three more rows
       than the first measurement. */
    .sxdesk .qmain.fill > .qbody { gap: 8px; padding: 8px 20px 6px; }
    .sxdesk .qmain.fill .qhead { padding-bottom: 0; }
    .sxdesk .qmain.fill .qctx { margin-bottom: 4px; }
    .sxdesk .qact { height: 26px; padding: 0 9px; }
    /* sheet → 6px → hairline → 6px → Back: the host's border and the
       move bar's border never sit 4px apart as a false double rule. */
    .sxdesk .qmain.fill .qmove { padding-top: 6px; padding-bottom: 6px; }
}

/* The Univer spreadsheet host (candidate runner / editable previews).
   Univer paints its own light chrome inside; the frame matches the
   design system's cards. Pinned light for the same reason the panel's
   .sx-tools is — the workbook is a sheet of paper on the desk. */
.sx-univer-host { height: 65vh; min-height: 380px; border: 1px solid var(--ff-line);
    border-radius: var(--ff-r-md); overflow: hidden; background: #ffffff;
    box-shadow: var(--ff-sh-1); color-scheme: light; }

/* Univer's own chrome, dressed as the product. Univer ships its ribbon
   tabs and toolbar buttons as bare borderless text (client: "glued
   borderless text") — here they take the product's real button
   treatment: bordered pills with breathing room, the active ribbon tab
   in the accent tint. The `univer-h-9` / `univer-h-10` hooks are the
   ribbon strip and the toolbar strip of the PINNED vendor build under
   public/vendor/univer — if that build is ever upgraded, re-check these
   two hooks first. Both hosts named so the builder and the runner agree. */
.sx-univer-host header, .wbu-host header,
.sx-univer-host header [class*="univer-h-10"], .wbu-host header [class*="univer-h-10"] {
    background: var(--ff-sunken); }
.sx-univer-host header [class*="univer-h-9"] button,
.wbu-host header [class*="univer-h-9"] button {
    border: 1px solid var(--ff-line); background: var(--ff-surface);
    border-radius: 7px; padding: 4px 12px; margin: 0 2px;
    font-weight: 600; color: var(--ff-ink-2); }
.sx-univer-host header [class*="univer-h-9"] button.univer-bg-primary-50,
.wbu-host header [class*="univer-h-9"] button.univer-bg-primary-50 {
    border-color: var(--ff-accent-edge); background: var(--ff-accent-soft);
    color: var(--ff-accent); }
.sx-univer-host header [class*="univer-h-10"] button,
.wbu-host header [class*="univer-h-10"] button {
    border: 1px solid var(--ff-line); background: var(--ff-surface);
    border-radius: 6px; margin: 0 1px; min-width: 26px; height: 26px; }
/* The runner's ribbon is `simple` (univer-bridge.js): one toolbar row,
   no Start / Formulas / Data tab row above it. 34px around the 26px
   buttons — measured, the number-format select and the highlight
   dropdown fit. The h-9 rules above are for a classic ribbon, which
   the runner no longer draws. (The selector also matches Univer's
   off-screen measuring toolbar; harmless.) */
.sxdesk .sx-univer-host header [class*="univer-h-10"] { height: 34px; }
/* drag selection */
.noselect, .noselect * { user-select: none; }
.gc.drag { outline: 2px dashed var(--ff-accent); outline-offset: -2px; background: var(--ff-accent-soft) !important; }

/* Marking paint. Once a cell is marked, the verdict is its whole job —
   so the verdict fill REPLACES the role fill and the ring is 2px, well
   clear of the 1px hairline a role uses. ok/ecf/bad are the three most
   separated inks in the palette (OKLab dE 0.12–0.25 apart). */
.gc.mark-ok { box-shadow: inset 0 0 0 2px var(--ff-good) !important; background: var(--ff-good-soft) !important;
    color: var(--ff-good) !important; }
.gc.mark-ecf { box-shadow: inset 0 0 0 2px var(--ff-flag) !important; background: var(--ff-flag-soft) !important;
    color: var(--ff-flag) !important; }
.gc.mark-bad { box-shadow: inset 0 0 0 2px var(--ff-crit) !important; background: var(--ff-crit-soft) !important;
    color: var(--ff-crit) !important; }

/* The legend prints the ACTUAL cell fills, so it cannot drift from
   what the grid does. */
.legend { display: flex; gap: 8px 18px; flex-wrap: wrap; margin-bottom: 11px; font-size: 11.5px; color: var(--ff-muted); }
.legend i { display: inline-block; width: 18px; height: 15px; border-radius: 5px; margin-right: 6px;
    vertical-align: -2px; border: 1px solid var(--ff-line-2); }
.legend i.lg-label { background: var(--ff-surface); }
.legend i.lg-given { background: var(--ff-sunken); }
/* the swatches carry the grid's own shape cues too, so the key is
   readable without colour: the answer box has its heavier bottom rule,
   the computed cell its bar in the margin */
.legend i.lg-input { background: var(--ff-flag-soft); border-color: var(--ff-flag-edge);
    border-bottom-width: 2px; }
.legend i.lg-computed { background: var(--ff-calc-soft); border-color: var(--ff-calc-edge);
    box-shadow: inset 3px 0 0 var(--ff-calc-edge); }

/* results */
.score { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-bottom: 18px; }
.sc { background: var(--ff-surface); border: 1px solid var(--ff-line); border-radius: var(--ff-r-md);
    padding: 15px; text-align: center; box-shadow: var(--ff-sh-1); }
.sc .l { font: 700 9.5px var(--sxfont); letter-spacing: .1em; text-transform: uppercase; color: var(--ff-muted); }
.sc .v { font: 600 27px var(--mono); letter-spacing: -.02em; margin-top: 5px;
    font-variant-numeric: tabular-nums; color: var(--ff-ink); }
.sc.pass { background: var(--ff-good-soft); border-color: var(--ff-good-edge); } .sc.pass .v { color: var(--ff-good); }
.sc.ecf { background: var(--ff-flag-soft); border-color: var(--ff-flag-edge); } .sc.ecf .v { color: var(--ff-flag); }
.vd { border-radius: var(--ff-r-md); padding: 11px 13px; margin-bottom: 7px; border: 1px solid var(--ff-line);
    display: flex; justify-content: space-between; gap: 12px; align-items: flex-start; flex-wrap: wrap;
    background: var(--ff-surface); }
.vd .nm { font: 650 13.5px var(--sxfont); letter-spacing: -.008em; color: var(--ff-ink); }
.vd .why { font-size: 11.5px; color: var(--ff-muted); line-height: 1.55; margin-top: 3px; font-family: var(--mono); }
.vd .mk2 { font: 700 13px var(--mono); font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--ff-ink); }
.vd.ok { background: var(--ff-good-soft); border-color: var(--ff-good-edge); } .vd.ok .nm, .vd.ok .mk2 { color: var(--ff-good); }
.vd.ecf { background: var(--ff-flag-soft); border-color: var(--ff-flag-edge); } .vd.ecf .nm, .vd.ecf .mk2 { color: var(--ff-flag); }
.vd.bad { background: var(--ff-crit-soft); border-color: var(--ff-crit-edge); } .vd.bad .nm, .vd.bad .mk2 { color: var(--ff-crit); }
.badge { font: 700 9.5px var(--sxfont); letter-spacing: .1em; text-transform: uppercase; padding: 2px 6px;
    border-radius: 4px; background: var(--ff-ground-2); color: var(--ff-muted); }
.x { color: var(--ff-muted); cursor: pointer; font-size: 17px; line-height: 1; padding: 2px 5px; background: none; border: 0; }
.x:hover { color: var(--ff-crit); }
[x-cloak] { display: none !important; }

/* =====================================================================
   THE EXAM RUNNER — the candidate's screen.

   Ported from the Focus Flow mockup: a quiet top bar that carries only
   trust chrome (who, secure, saving, time left), the ground behind the
   paper, one question on a surface card, and a fixed foot for the
   actions. Nothing in here shouts except the two things that are
   allowed to: the clock when it is running out, and a failed save.
   ===================================================================== */
.qnav-strip { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; width: 100%; padding-bottom: 10px; }
/* The question palette — the mockup's review grid. Answered is the
   ACCENT (a solid tile), not green: green is reserved for "this is
   safe" (secure mode), and the palette needs its own voice. Not
   answered is dashed and hollow, so the three states differ in shape
   as well as in colour. Flag beats answered, as it does in the mockup:
   a flagged question is one you asked to come back to. */
.qnav { min-width: 30px; height: 30px; padding: 0 7px; display: inline-flex; align-items: center;
    justify-content: center; border-radius: var(--ff-r-sm); border: 1px dashed var(--ff-line-2);
    background: var(--ff-surface); color: var(--ff-muted);
    font: 600 12.5px var(--mono); font-variant-numeric: tabular-nums;
    transition: color var(--ff-t), background var(--ff-t), border-color var(--ff-t); }
.qnav:hover { border-style: solid; border-color: var(--ff-accent); background: var(--ff-accent-soft);
    color: var(--ff-accent); }
.qnav.done { border-style: solid; background: var(--ff-accent); border-color: var(--ff-accent);
    color: var(--ff-on-accent); }
.qnav.done:hover { background: var(--ff-accent-hi); border-color: var(--ff-accent-hi); color: var(--ff-on-accent); }
/* marking palette: this answer still needs a person */
.qnav.pending { border-style: solid; background: var(--ff-flag-soft); border-color: var(--ff-flag);
    color: var(--ff-flag); }
.qcard { scroll-margin-top: 128px; }
/* The clock is a pill, not loose text: it is the one number a candidate
   looks for without reading. Amber under ten minutes, red under five —
   the only red on the screen. */
.clock { display: inline-flex; align-items: center; gap: 8px; height: 30px; padding: 0 12px;
    border-radius: 999px; border: 1px solid var(--ff-line-2); background: var(--ff-sunken);
    font: 600 15px var(--mono); font-variant-numeric: tabular-nums; letter-spacing: .01em; color: var(--ff-ink); }
.clock.warn { border-color: var(--ff-flag-edge); background: var(--ff-flag-soft); color: var(--ff-flag); }
.clock.low { border-color: var(--ff-crit-edge); background: var(--ff-crit-soft); color: var(--ff-crit); }
/* self-marking: your work beside the model answer */
.marksplit { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 18px; align-items: start; }
.marksplit .splith { margin: 0 0 9px; font: 700 10px var(--sxfont); letter-spacing: .13em;
    text-transform: uppercase; color: var(--ff-muted); }
.modelside { border-left: 3px solid var(--ff-accent-edge); padding-left: 16px; }
.selfmark { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 16px;
    padding-top: 14px; border-top: 1px solid var(--ff-line); }
.selfmark label.f { margin: 0; }
@media (max-width: 900px) {
    .marksplit { grid-template-columns: 1fr; }
    .modelside { border-left: 0; border-top: 3px solid var(--ff-accent-edge); padding: 14px 0 0; }
}

/* =====================================================================
   THE DESK — the candidate's sitting screen (Focus Flow structure,
   client-approved 2026-08-21, docs/plans/exam-screen-STRUCTURE.md).

   The exam stopped being a document page and became a desk. Three
   columns between a fixed top rail and the foot of the viewport:

     · left   — WHERE AM I: the paper, the section tree, Review & submit
     · centre — THE TASK AND THE ANSWER, always together
     · right  — WHAT AM I READING: the scenario, the exhibits, notes

   Both side rails fold to their own edge and the choice is remembered
   on the device. THE PAGE ITSELF NEVER SCROLLS: .sxdesk is fixed to the
   viewport and each column scrolls inside itself, so a long exhibit can
   never push the answer box off the bottom and the clock can never
   scroll away.

   Nothing may cover the top rail. Every dialog this screen opens starts
   BELOW it (.ovl.sxbelow), and the one exception is deliberate: the
   secure-mode warning, which is meant to stop everything.

   Measured defects this replaces (shots in C:/tmp/sx-runner-redesign):
   18-62% dead grey ground on seven screens out of eight; a spreadsheet
   that overflowed the eighth by 133-210px; the marked task hidden
   behind a rail toggle; and all four tools painting a scrim over the
   countdown, the save state and the hand-in button.
   ===================================================================== */
.sxdesk { position: fixed; inset: 0; display: flex; flex-direction: column;
    background: var(--ff-ground); color: var(--ff-ink); overflow: hidden; }
/* Belt and braces: .sxdesk is out of flow, so the document has nothing
   left to scroll — but a stray margin on <body> must not be able to
   produce a second scrollbar behind a fixed screen. */
body:has(.sxdesk) { overflow: hidden; }

/* ---------------------------------------------------------------------
   The top rail. Trust chrome only, and never covered by anything.
   Declared here, RE-MEASURED in JS (examRunner.measureRails) so the
   panes and the floating-window clamp read one authority even after a
   browser zoom or a translated string changes the real height.
   --------------------------------------------------------------------- */
.runbar { position: relative; z-index: var(--sx-z-rail); flex: 0 0 auto;
    height: var(--sxtop-rail); background: var(--ff-surface);
    border-bottom: 1px solid var(--ff-line); box-shadow: 0 1px 2px rgba(11,26,30,.05);
    padding: 0 14px; display: flex; align-items: center; }
/* Three zones that must NOT wrap: a wrapped clock is a clock that moved. */
.runbar .runrow { display: flex; align-items: center; gap: 10px; flex-wrap: nowrap;
    width: 100%; min-width: 0; height: 100%; }
.runbar .runbrand { flex: 0 0 auto; font: 650 13px var(--sxfont); letter-spacing: -.01em;
    color: var(--ff-ink); padding-right: 12px; border-right: 1px solid var(--ff-line); }
.runbar .runbrand span { color: var(--ff-accent); }
/* The institution, not the candidate: whose examination this is. */
.rb-inst { flex: 0 1 auto; min-width: 0; font: 600 13px var(--sxfont); color: var(--ff-ink-2);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rb-gap { flex: 1 1 auto; min-width: 8px; }
/* Autosave state. A pill, so it reads as chrome and not as prose —
   until it has bad news, when it grows into a sentence. */
.runbar .saver { flex: 0 0 auto; font: 600 12px var(--mono); color: var(--ff-muted);
    background: var(--ff-sunken); border-radius: 999px; padding: 5px 11px; white-space: nowrap; }
.runbar .saver .bad { color: var(--ff-crit); font-family: var(--sxfont); font-weight: 700;
    white-space: normal; }
body.sx-save-failed .runbar .saver, .sx-unsaved .runbar .saver { background: var(--ff-crit-soft); }
/* The candidate's own number. Never a name: this screen is photographed,
   invigilated and shoulder-read, and marking is blind by the same rule. */
.rb-cand { flex: 0 0 auto; font: 600 12.5px var(--mono); font-variant-numeric: tabular-nums;
    color: var(--ff-muted); padding-left: 12px; border-left: 1px solid var(--ff-line); }
/* The clock, at the size the one number a candidate hunts for deserves. */
.sxdesk .clock { flex: 0 0 auto; height: 32px; font-size: 20px; font-weight: 600; padding: 0 13px; }
.rb-eye { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
    width: 30px; height: 30px; border-radius: var(--ff-r-sm); border: 1px solid var(--ff-line);
    background: var(--ff-surface); color: var(--ff-muted); cursor: pointer; font-size: 14px;
    transition: color var(--ff-t), border-color var(--ff-t); }
.rb-eye:hover { color: var(--ff-accent); border-color: var(--ff-accent-edge); }
/* Hiding the clock is the candidate's own choice — the only reason the
   countdown may ever be invisible. It says so, and it says it plainly. */
.clock-off { flex: 0 0 auto; display: inline-flex; align-items: center; height: 32px; padding: 0 13px;
    border-radius: 999px; border: 1px dashed var(--ff-line-2); background: var(--ff-sunken);
    color: var(--ff-muted); font: 600 12.5px var(--sxfont); white-space: nowrap; }

/* A control that is invisible until it is focused: the skip link and
   the on-demand time announcement. A keyboard user must be able to SEE
   where they are; a permanently hidden focus stop is a trap. */
.sxskip { position: absolute; left: 12px; top: 8px; z-index: 600; width: 1px; height: 1px;
    padding: 0; margin: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
    border: 1px solid var(--ff-accent); border-radius: var(--ff-r-sm);
    background: var(--ff-surface); color: var(--ff-accent); font: 600 13px var(--sxfont); cursor: pointer; }
.sxskip:focus { width: auto; height: auto; padding: 7px 12px; clip-path: none; overflow: visible; }

/* ---------------------------------------------------------------------
   The work area: three columns, each its own scroller.
   --------------------------------------------------------------------- */
/* grid-template-rows is LOAD-BEARING, and its absence was measured: an
   implicit `auto` row is sized to the tallest item's max-content, and
   overflow does not cap max-content — so the columns grew to 749px
   inside a 712px band and the navigator's pinned "Review & submit" sat
   37px below the bottom of a 768px screen, which is the resolution most
   exam halls run. minmax(0, 1fr) pins the row to the band and lets each
   column scroll inside itself, which is the whole design. */
.sxwork { flex: 1 1 auto; min-height: 0; display: grid;
    grid-template-columns: var(--sxnav-w) minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr); }
.sxwork.nav-shut { grid-template-columns: var(--sxedge) minmax(0, 1fr); }

/* ---- left: where you are in the paper ---- */
.sxnav { min-width: 0; display: flex; flex-direction: column; overflow: hidden;
    background: var(--ff-sunken); border-right: 1px solid var(--ff-line); }
.sxwork.nav-shut .navbody { display: none; }
.sxwork:not(.nav-shut) .navedge { display: none; }
/* The folded rail is a WORD, not a mystery glyph — this is the one
   screen where a candidate must never have to work anything out. */
.navedge { flex: 1 1 auto; display: flex; align-items: center; justify-content: center;
    gap: 10px; padding: 14px 0; border: 0; border-right: 1px solid var(--ff-line);
    background: var(--ff-sunken); color: var(--ff-muted); cursor: pointer;
    writing-mode: vertical-rl; font: 700 11px var(--sxfont); letter-spacing: .14em;
    text-transform: uppercase; }
.navedge:hover { color: var(--ff-accent); background: var(--ff-accent-soft); }
.navbody { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }
.navhead { position: relative; flex: 0 0 auto; padding: 14px 14px 12px;
    border-bottom: 1px solid var(--ff-line); }
.navkicker { font: 700 10px var(--sxfont); letter-spacing: .14em; text-transform: uppercase;
    color: var(--ff-muted); margin-bottom: 5px; }
.navtitle { font: 650 14.5px var(--sxfont); letter-spacing: -.012em; color: var(--ff-ink);
    line-height: 1.35; margin-bottom: 5px; padding-right: 28px; }
.navmeta { font: 600 12px var(--sxfont); color: var(--ff-muted); }
/* The honest statement of progress. It is a count of what the candidate
   has answered, not a percentage of anything the product cannot know. */
.navprog { margin-top: 9px; padding-top: 9px; border-top: 1px dashed var(--ff-line);
    font: 600 12px var(--sxfont); color: var(--ff-ink-2); }
.navprog b { color: var(--ff-accent); font-weight: 700; }
.navprog .fl { color: var(--ff-flag); font-weight: 700; }
.navfold { flex: 0 0 auto; width: 26px; height: 26px;
    display: inline-flex; align-items: center; justify-content: center; padding: 0;
    border: 1px solid var(--ff-line); border-radius: var(--ff-r-sm); cursor: pointer;
    background: var(--ff-surface); color: var(--ff-muted); font-size: 13px; }
.navfold:hover { color: var(--ff-accent); border-color: var(--ff-accent-edge); }
/* Only the navigator's own fold is pinned into its heading block. The
   reading rail's sits in a flex row and must NOT be taken out of flow —
   absolutely positioned with no positioned ancestor it flew to the top
   right of the desk, where nothing on this screen may ever go. */
.navhead .navfold { position: absolute; top: 11px; right: 10px; }
.navtree { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
    padding: 10px 8px 16px; }
.navsec + .navsec { margin-top: 6px; }
.navsec-h { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
    margin: 10px 0 6px; padding: 4px 6px; border: 0; border-radius: var(--ff-r-sm);
    background: none; cursor: pointer; color: var(--ff-muted);
    font: 700 10px var(--sxfont); letter-spacing: .13em; text-transform: uppercase; }
.navsec-h:hover { background: var(--ff-sunken); color: var(--ff-accent); }
.navsec-h::after { content: ''; flex: 1 1 auto; height: 1px; background: var(--ff-line); }
.navsec-h span { flex: 0 0 auto; }
/* One row per case study, one row per BATCH of objective questions. */
.navrow { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: center;
    width: 100%; text-align: left; margin-bottom: 2px; padding: 8px 9px;
    border: 0; border-left: 3px solid transparent; border-radius: var(--ff-r-sm);
    background: none; color: var(--ff-ink-2); cursor: pointer;
    transition: background var(--ff-t), color var(--ff-t); }
.navrow:hover { background: var(--ff-sunken); color: var(--ff-ink); }
.navrow.here { background: var(--ff-accent-soft); border-left-color: var(--ff-accent); color: var(--ff-accent); }
.navrow-body { min-width: 0; display: block; }
.navrow-title { font: 600 13px var(--sxfont); line-height: 1.4; overflow: hidden;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.navrow-meta { display: block; margin-top: 3px; font: 500 11.5px var(--sxfont); color: var(--ff-muted); }
.navrow.here .navrow-meta { color: var(--ff-accent); opacity: .85; }
/* The four states differ in SHAPE as well as colour: answered is solid,
   flagged is amber, current is ringed, untouched is a dashed hollow. */
.navstate { flex: 0 0 auto; width: 20px; height: 20px; border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    border: 1px dashed var(--ff-line-2); background: var(--ff-surface);
    font: 700 11px var(--sxfont); color: var(--ff-muted); }
.navstate.done { border: 1px solid var(--ff-accent); background: var(--ff-accent); color: var(--ff-on-accent); }
.navstate.flagged { border: 1px solid var(--ff-flag); background: var(--ff-flag-soft); color: var(--ff-flag); }
.navbatch { padding: 4px 9px 10px; border-left: 3px solid transparent; }
.navbatch.here { border-left-color: var(--ff-accent); }
.navbatch-h { font: 500 11.5px var(--sxfont); color: var(--ff-muted); margin-bottom: 6px; }
.navdots { display: flex; flex-wrap: wrap; gap: 5px; }
.navdot { width: 26px; height: 26px; display: inline-flex; align-items: center; justify-content: center;
    border-radius: var(--ff-r-sm); border: 1px dashed var(--ff-line-2); background: var(--ff-surface);
    color: var(--ff-muted); cursor: pointer;
    font: 600 11.5px var(--mono); font-variant-numeric: tabular-nums;
    transition: background var(--ff-t), border-color var(--ff-t), color var(--ff-t); }
.navdot:hover { border-style: solid; border-color: var(--ff-accent); color: var(--ff-accent); }
.navdot.done { border-style: solid; border-color: var(--ff-accent); background: var(--ff-accent); color: var(--ff-on-accent); }
.navdot.flagged { border-style: solid; border-color: var(--ff-flag); background: var(--ff-flag-soft); color: var(--ff-flag); }
.navdot.here { outline: 2px solid var(--ff-accent); outline-offset: 2px; }
/* Pinned: the way out is never scrolled away from. */
.navfoot { flex: 0 0 auto; padding: 11px 12px; border-top: 1px solid var(--ff-line);
    background: var(--ff-sunken); }
.navfoot .btn { width: 100%; justify-content: center; }
.navfoot-note { display: block; margin-top: 7px; text-align: center;
    font: 600 11.5px var(--sxfont); color: var(--ff-muted); }
.navfoot-note.todo { color: var(--ff-flag); }

/* ---- centre + right ----
   ONE grid does both states, so nothing is remounted when the reading
   pane opens: that matters most here, because the centre can hold a
   live Univer spreadsheet and tearing it down mid-exam costs the
   candidate their selection, scroll and undo history. */
.qwork { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) var(--sxedge);
    grid-template-rows: minmax(0, 1fr); }
.qwork.is-split { grid-template-columns: minmax(0, 1fr) var(--sxsrc-w); }
/* A briefing screen has nothing to read alongside it. */
.qwork.solo { grid-template-columns: minmax(0, 1fr); }

/* ---- centre: the task and the answer, always together ---- */
/* The column holds exactly two things: the work, which scrolls, and the
   move bar, which does not. Putting the scroller on .qbody rather than
   on the column is what keeps Back and Next pinned to the foot on a
   long question — and, on a spreadsheet, what stops the grid painting
   underneath them (measured at 1366x768: the grid's last 25px sat
   behind the bar). */
.qmain { min-width: 0; display: flex; flex-direction: column; overflow: hidden;
    background: var(--ff-surface); }
/* A short question and a briefing screen are capped at a readable
   measure rather than stretched across a hall monitor: 135-158
   characters a line was measured on the old runner, twice the readable
   maximum and worse on a bigger screen. */
.qmain.narrow > .qbody { width: 100%; max-width: 880px; }
/* A spreadsheet answer FILLS the column instead of scrolling it: the
   measured defect was a grid that ran 133-210px off the bottom of the
   page on the screen size most exam halls run. */
/* A spreadsheet answer takes every pixel the question's own chrome can
   spare. When the chrome genuinely needs more room than the screen has,
   .qbody scrolls rather than the grid being clipped — losing the bottom
   of a spreadsheet silently is the failure this screen exists to
   prevent. With room to spare the flex chain hands every spare pixel to
   the grid and no scrollbar appears at all. */
.qmain > .qbody { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column;
    gap: 14px; padding: 18px 22px 26px; overflow-y: auto; overscroll-behavior: contain; }
.qmain.fill > .qbody { gap: 10px; padding: 14px 22px 12px; }
.qhead { flex: 0 0 auto; }
.qctx { font: 700 11px var(--sxfont); letter-spacing: .12em; text-transform: uppercase;
    color: var(--ff-muted); margin-bottom: 7px; }
.qctx b { color: var(--ff-accent); font-weight: 700; }
.qtitlerow { display: flex; align-items: flex-start; gap: 14px; }
.qtitle { flex: 1 1 auto; min-width: 0; margin: 0; font: 650 20px var(--sxfont);
    letter-spacing: -.02em; line-height: 1.3; color: var(--ff-ink); }
.qsub { margin: 5px 0 0; font: 500 12.5px var(--sxfont); color: var(--ff-muted); }
/* On a spreadsheet question every line above the grid is a row of the
   grid the candidate cannot see. */
.qmain.fill .qtitle { font-size: 17px; }
/* …so two lines that only repeat what the screen already says go, at
   EVERY height (they used to go only under 820px, and the same sheet
   wore two designs on two monitors):
   · "Spreadsheet · everything you were given is on the right" — the
     reading rail is open beside it and headed WHAT YOU ARE READING,
     and under 900px the rail stacks below, where "on the right" is
     false;
   · the YOUR ANSWER rule above a spreadsheet. The heading itself stays
     in the page for the skip link and the heading outline; it is only
     taken off the paint. */
.sxdesk .qmain.fill .qsub { display: none; }
.sxdesk .qmain.fill .qans-h { position: absolute; width: 1px; height: 1px; padding: 0;
    margin: -1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
.qacts { flex: 0 0 auto; display: flex; align-items: center; gap: 7px; }
/* Quiet by design: Flag and Report are for the candidate's own use and
   must never compete with the task or the answer. */
.qact { display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 11px;
    border: 1px solid var(--ff-line); border-radius: var(--ff-r-sm); background: var(--ff-surface);
    color: var(--ff-muted); cursor: pointer; font: 600 12.5px var(--sxfont); white-space: nowrap;
    transition: color var(--ff-t), border-color var(--ff-t), background var(--ff-t); }
.qact:hover { color: var(--ff-ink); border-color: var(--ff-line-2); }
.qact.on { color: var(--ff-flag); border-color: var(--ff-flag-edge); background: var(--ff-flag-soft); }

/* Part tabs: one per part of a question that HAS parts, marks on every
   one. Rendered from the frozen paper — see the note in _question. They
   choose which REQUIREMENT is on screen; the answer below never moves. */
.parttabs { flex: 0 0 auto; display: flex; gap: 7px; flex-wrap: wrap; }
.parttab { display: inline-flex; align-items: baseline; gap: 7px; padding: 7px 12px;
    border: 1px solid var(--ff-line); border-radius: 999px; background: var(--ff-surface);
    color: var(--ff-ink-2); cursor: pointer; font: 600 12.5px var(--sxfont); }
.parttab em { font-style: normal; font-weight: 700; color: var(--ff-accent); }
.parttab i { font-style: normal; font-size: 11.5px; color: var(--ff-muted); }
.parttab.on { border-color: var(--ff-accent); background: var(--ff-accent-soft); color: var(--ff-accent); }
.parttab.on i { color: var(--ff-accent); }

/* THE REQUIREMENT BAND — the task is always on screen, never behind a
   button. This is the heart of the client's item 4: on a 10-mark case
   study the candidate used to see a scenario, an empty box, and the
   numbered tasks hidden behind a rail row nothing pointed at. */
.reqband { flex: 0 0 auto; border: 1px solid var(--ff-accent-edge); border-radius: var(--ff-r-md);
    background: var(--ff-accent-soft); overflow: hidden; }
.reqband > summary { display: flex; align-items: center; gap: 9px; cursor: pointer;
    padding: 7px 14px; list-style: none;
    font: 700 10.5px var(--sxfont); letter-spacing: .13em; text-transform: uppercase;
    color: var(--ff-accent); }
.reqband > summary::-webkit-details-marker { display: none; }
.reqband > summary::after { content: '⌃'; margin-left: auto; font-size: 13px; transform: rotate(180deg);
    transition: transform var(--ff-t); }
.reqband[open] > summary::after { transform: rotate(0deg); }
/* The band's own metrics are the marked task's, and one density on
   every question of the paper (2026-09-08): the words never shrink —
   A+ / A- still scale them — only the furniture around them does. */
.reqband .reqbody { padding: 0 14px 9px; background: var(--ff-surface); }
.reqband .reqbody .stem { padding-top: 8px; color: var(--ff-ink); }
.sxdesk .reqband .stem p:not(:last-child) { margin-bottom: 5px; }
.reqmarks { flex: 0 0 auto; padding: 2px 8px; border-radius: 999px; background: var(--ff-surface);
    border: 1px solid var(--ff-accent-edge); color: var(--ff-accent);
    font: 700 11px var(--mono); font-variant-numeric: tabular-nums; letter-spacing: 0; }
.reqrow { flex: 1 1 auto; display: flex; align-items: baseline; gap: 10px; }
/* The requirement band is the one thing that must not eat the answer on
   a short screen: it keeps its own scroller instead of growing. */
.qmain.fill .reqband .reqbody { max-height: 26vh; overflow-y: auto; }

/* ---- the response ---- */
.qans-h { flex: 0 0 auto; display: flex; align-items: baseline; gap: 10px; margin: 2px 0 0;
    font: 700 10.5px var(--sxfont); letter-spacing: .13em; text-transform: uppercase;
    color: var(--ff-muted); }
.qans-h::after { content: ''; flex: 1 1 auto; height: 1px; background: var(--ff-line); }
.qans { min-width: 0; }
.qmain.fill .qans { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
/* The chain that lets a spreadsheet fill the pane: the workbook partial
   is one wrapper holding a toolbar and the Univer host. */
.qmain.fill .qans > * { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.qmain.fill .qans div:has(> .sx-univer-host) { flex: 1 1 auto; min-height: 0;
    display: flex; flex-direction: column; }
.sxdesk .sx-univer-host { flex: 1 1 auto; height: auto; min-height: 300px; }
/* the legacy .gc player, kept working under the same roof */
.qmain.fill .qans .gridwrap { flex: 1 1 auto; min-height: 0; max-height: none; }

/* THE ANSWER AT FULL SIZE (client, 2026-08-27: "use up all the height
   and width available on the browser"). One class on the work area —
   .sxwork.ansmax, toggled by examRunner.toggleAnswerMax() — and
   .qansfull (the answer heading AND the pane, one wrapper) takes
   everything BELOW THE TOP RAIL. The rail (clock · saving · secure
   pill · End exam) is trust chrome and is never covered. The wrapper —
   never .qans alone — is what maximizes, because the wrapper contains
   its own exit: the heading's button for every plain type, the
   workbook bar's for spreadsheets. Absolute against .sxwork rather
   than fixed against the viewport, so "below the rail" holds by
   construction and no pixel height is ever measured; the abspos box
   escapes the overflow of .qmain/.qbody because neither is positioned
   (its containing block is .sxwork). The z-index sits UNDER the
   floating calculator and note pad (--sx-z-win: 85) and far under
   warnings and dialogs (.ovl at 500): nothing that must reach the
   candidate can ever be covered by their own answer. */
.sxwork { position: relative; }
/* The wrapper joins the existing flex chains in normal mode: it flexes
   exactly where .qans used to, hands the height on, and inherits the
   column's own gap so the heading keeps its spacing. */
.qansfull { display: flex; flex-direction: column; min-width: 0; min-height: 0; gap: inherit; }
.qansfull > .qans { min-height: 0; }
.qmain.fill .qansfull { flex: 1 1 auto; }
.qansfull:has(.ansbox) { flex: 1 1 auto; }
.qansfull:has(.ansbox) > .qans { flex: 1 1 auto; }
.qmain.fill .qansfull > .qans { flex: 1 1 auto; }

.sxwork.ansmax .qmain .qansfull { position: absolute; inset: 0; z-index: 70;
    background: var(--ff-ground); padding: 10px 14px; overflow: hidden; }
.sxwork.ansmax .qmain .qansfull > .qans { flex: 1 1 auto; overflow: auto;
    display: flex; flex-direction: column; }

/* While maximized, everything the answer covers leaves the keyboard
   and the accessibility tree WITH it: a painted-over control is still
   Tab-reachable, so without this a candidate could press an invisible
   Back, Next — or Finish — through their own sheet (adversarial
   review, 2026-08-27). visibility keeps every box, so nothing reflows
   and the grid never re-measures against the wrong layout. */
.sxwork.ansmax .sxnav, .sxwork.ansmax .qsrc, .sxwork.ansmax .qmove,
.sxwork.ansmax .qmain > .qbody > :not(.qansfull) { visibility: hidden; }

/* The heading and its toggle share a row — the button is NEVER inside
   the h2, which would fold its text into the heading's accessible
   name and into the essay box labelled by #sxanswer. */
.qansrow { flex: 0 0 auto; display: flex; align-items: baseline; gap: 10px; }
.qansrow .qans-h { flex: 1 1 auto; }
/* On a spreadsheet question the workbook bar carries the toggle, so the
   row's button goes. display:contents so the empty row costs no gap;
   the h2 stays in the tree — the skip link and the textarea label both
   point at it. BUT display:contents makes the h2 itself an item of the
   COLUMN flex above, where the row's `flex: 1 1 auto` (meant to spread
   it along a row) made it grow DOWNWARD and soak up every spare pixel —
   the client's "big gap between Your answer and the answer template"
   (2026-09-08). The heading is one line, never a spring. */
.qmain.fill .qansrow { display: contents; }
.qmain.fill .qansrow .qans-h { flex: 0 0 auto; }
.qmain.fill .qansrow .qmaxbtn { display: none; }

/* The door in and out: quiet, like .qact — it must never compete with
   the answer it enlarges. */
.qmaxbtn { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 6px;
    font: 600 11.5px var(--sxfont); letter-spacing: 0; text-transform: none;
    cursor: pointer; padding: 4px 10px; border-radius: 999px;
    border: 1px solid var(--ff-line); background: var(--ff-surface); color: var(--ff-ink-2); }
.qmaxbtn:hover { background: var(--ff-sunken); color: var(--ff-ink); }
.qmaxbtn[aria-pressed="true"] { border-color: var(--ff-accent); color: var(--ff-accent);
    background: var(--ff-accent-soft); }
/* The paper the candidate writes on. A real box with a bottom rule, not
   a hairline rectangle — it must look like somewhere to write. */
/* The box takes the room the question left it — an answer sheet is as
   long as the paper allows, and a band of empty ground under a 3-line
   box is the measured defect this screen exists to fix. */
.qans:has(> div > .ansbox) { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.qans:has(> div > .ansbox) > div { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.qans:has(> div > .ansbox) .ansbox { flex: 1 1 auto; }
.ansbox { box-sizing: border-box; width: 100%; min-height: 220px; resize: vertical;
    font: 400 var(--sxpaper)/1.65 var(--sxfont); color: var(--ff-ink);
    padding: 14px 15px; border: 1px solid var(--ff-line-2); border-bottom-width: 2px;
    border-radius: var(--ff-r-md); background: var(--ff-surface); }
.ansbox:hover { border-color: var(--ff-muted); }
.ansbox:focus { outline: 2px solid var(--ff-accent); outline-offset: -1px; border-color: var(--ff-accent); }
.ansfoot { display: flex; align-items: center; justify-content: space-between; gap: 12px;
    margin-top: 7px; }
.wordcount { font: 600 12px var(--mono); font-variant-numeric: tabular-nums; color: var(--ff-muted); }
.wordcount.over { color: var(--ff-crit); }
/* The empty state. It says the two things a frightened candidate needs:
   your work saves itself, and you can come back to this. */
.ansempty { display: flex; flex-direction: column; align-items: flex-start; gap: 10px;
    padding: 22px; border: 1px dashed var(--ff-line-2); border-radius: var(--ff-r-md);
    background: var(--ff-sunken); }
.ansempty b { font: 650 14.5px var(--sxfont); color: var(--ff-ink); }
.ansempty p { margin: 0; font-size: 13px; line-height: 1.6; color: var(--ff-muted); max-width: 56ch; }

/* the foot of the centre column: how you move, pinned so it is never
   hunted for at the bottom of a long scroll */
.qmove { flex: 0 0 auto; display: flex; align-items: center; gap: 10px;
    padding: 10px 22px; border-top: 1px solid var(--ff-line); background: var(--ff-surface); }
.qmove .sp { flex: 1 1 auto; }

/* ---- right: what you are reading ---- */
.qsrc { min-width: 0; display: flex; flex-direction: column; overflow: hidden;
    background: var(--ff-surface); border-left: 1px solid var(--ff-line); }
.qwork:not(.is-split) .srcpane { display: none; }
.qwork.is-split .srcedge { display: none; }
.srcedge { flex: 1 1 auto; display: flex; align-items: center; justify-content: center; gap: 10px;
    padding: 14px 0; border: 0; border-left: 1px solid var(--ff-line);
    background: var(--ff-sunken); color: var(--ff-muted); cursor: pointer;
    writing-mode: vertical-rl; font: 700 11px var(--sxfont); letter-spacing: .14em;
    text-transform: uppercase; }
.srcedge:hover { color: var(--ff-accent); background: var(--ff-accent-soft); }
.srcpane { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }
.srchead { flex: 0 0 auto; padding: 10px 12px; border-bottom: 1px solid var(--ff-line); }
.srchead-top { display: flex; align-items: center; gap: 8px; margin-bottom: 9px; }
.srchead-top h2 { flex: 1 1 auto; margin: 0; font: 700 10px var(--sxfont); letter-spacing: .14em;
    text-transform: uppercase; color: var(--ff-muted); }
/* Tabs, not a stack: one item at a time, and which one is open is
   visible without reading. */
.srctabs { display: flex; flex-wrap: wrap; gap: 6px; }
.srcgroup { width: 100%; margin: 4px 0 1px; font: 700 9.5px var(--sxfont); letter-spacing: .14em;
    text-transform: uppercase; color: var(--ff-muted); }
.srctab { display: inline-flex; align-items: center; gap: 6px; max-width: 100%;
    padding: 6px 11px; border: 1px solid var(--ff-line); border-radius: 999px;
    background: var(--ff-surface); color: var(--ff-ink-2); cursor: pointer;
    font: 600 12.5px var(--sxfont); }
.srctab em { font-style: normal; font-weight: 700; font-family: var(--mono); font-size: 11px;
    color: var(--ff-muted); }
.srctab span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.srctab:hover { border-color: var(--ff-line-2); background: var(--ff-sunken); color: var(--ff-ink); }
.srctab.on { border-color: var(--ff-accent); background: var(--ff-accent-soft); color: var(--ff-accent); }
.srctab.on em { color: var(--ff-accent); }
/* Batch D hangs the pop-out control here; it renders only once the
   window manager is on the page, so it is never a control with no
   effect. */
.srctab-pop { display: inline-flex; align-items: center; justify-content: center;
    width: 20px; height: 20px; margin: -4px -6px -4px 0; padding: 0; border: 0; border-radius: 50%;
    background: none; color: inherit; cursor: pointer; font-size: 11px; opacity: .6; }
.srctab-pop:hover { opacity: 1; background: var(--ff-surface); }
.srcbody { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
    padding: 14px 14px 22px; display: flex; flex-direction: column; }
.srcbody .stem { color: var(--ff-ink-2); }
.srcitem-h { margin: 0 0 9px; font: 650 14px var(--sxfont); letter-spacing: -.012em; color: var(--ff-ink); }
/* One slot per item, and only one is ever displayed — so the shown slot
   is the pane's only flex child. A written page keeps its natural
   height and lets the pane scroll; a document and the note pad take
   every remaining pixel instead, which is what stops two nested
   scrollers making a viewer feel broken. */
.srcslot { flex: 0 0 auto; min-width: 0; }
.srcslot.doc, .srcslot.tall { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
/* A document DOCKS here. It used to open a full-screen sheet over the
   countdown, the save state and End exam — measured, and against the
   product's own promise that the clock is always visible. */
.qsrc-file { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto;
    border: 1px solid var(--ff-line); border-radius: var(--ff-r-md); overflow: hidden; }
.qsrc-file .docframe { flex: 1 1 auto; width: 100%; min-height: 0; border: 0; background: var(--ff-sunken); }
.qsrc-file .docnote { flex: 0 0 auto; padding: 8px 12px; border-top: 1px solid var(--ff-line);
    background: var(--ff-sunken); font-size: 11.5px; color: var(--ff-muted); }
.srcnote { display: block; margin-top: 10px; font-size: 11.5px; color: var(--ff-muted); }
/* ---- the whole-paper tools, in the paper navigator ----
   The briefing video, "Download all materials" and the offline door.
   All three are about the PAPER, so they sit in the column that answers
   "what is this paper" and are on screen from the first screen to the
   last. They used to be split between the per-question reading rail
   (which folds, and is absent on the summary and the section briefings)
   and the summary itself, so on a paper whose questions carry no
   exhibits the download was reachable only through an edge tab reading
   "Notes & calculator" — install walk, 2026-08-23. */
.srcget { margin: 10px 0 0; }
.railget-note { margin: 6px 0 0; font-size: 11.5px; color: var(--ff-muted); }
/* One shape for all three rows, so the navigator's tool block reads as
   one object rather than three unrelated buttons. */
.navtool,
.railget { display: flex; align-items: center; gap: 9px; width: 100%; padding: 9px 12px;
    border: 1px solid var(--ff-line); border-radius: var(--ff-r-md);
    background: var(--ff-surface); color: var(--ff-ink-2); cursor: pointer;
    font: 600 12.5px var(--sxfont); text-decoration: none; text-align: left; }
.navtool + .navtool { margin-top: 8px; }
.navtool:hover { border-color: var(--ff-accent); background: var(--ff-accent-soft); color: var(--ff-accent); }
.navtool:focus-visible { outline: 2px solid var(--ff-accent); outline-offset: 2px; }
.navtool-i { flex: 0 0 auto; font-size: 13px; line-height: 1; color: var(--ff-muted); }
.navtool:hover .navtool-i { color: var(--ff-accent); }
/* The offline door once a pack has been taken: still quiet, but no
   longer the same row it was — this candidate is holding the paper and
   the thing they need from it now is the way to send it back. */
.navtool.on { border-color: var(--ff-accent-edge); background: var(--ff-accent-soft); color: var(--ff-accent); }
.navtool.on .navtool-i { color: var(--ff-accent); }
.railget:hover { border-color: var(--ff-accent); background: var(--ff-accent-soft); color: var(--ff-accent); }
.railget:focus-visible { outline: 2px solid var(--ff-accent); outline-offset: 2px; }
.railget-i { flex: 0 0 auto; font-size: 14px; line-height: 1; color: var(--ff-muted); }
.railget:hover .railget-i { color: var(--ff-accent); }
/* Busy is a STATE, never a spinner that keeps turning after the answer
   has arrived: the label says what is happening and the poll that set
   it has a stated end. */
.railget.is-busy { border-color: var(--ff-accent); background: var(--ff-accent-soft);
    color: var(--ff-accent); cursor: progress; }
.railget.is-busy .railget-i { color: var(--ff-accent); }

/* ---------------------------------------------------------------------
   Paper text size. Three steps, chrome unchanged — the cheapest
   possible defence against "the system made me fail".
   --------------------------------------------------------------------- */
.sxdesk[data-textsize="1"] { --sxpaper: 16px; }
.sxdesk[data-textsize="2"] { --sxpaper: 18px; }
.sxdesk[data-textsize="3"] { --sxpaper: 20px; }
/* The paper reads at its own size and at a readable measure. Interface
   text does not move: four sizes on this screen, not eight. */
.sxdesk .stem { font-size: var(--sxpaper); line-height: 1.55; }
.qmain .stem, .qmain .opt span, .reqband .stem { max-width: 74ch; }
.srcbody .stem { max-width: 72ch; }
.sxdesk .opt { font-size: var(--sxpaper); padding: 12px 14px; }
.sxdesk .tiny { font-size: 12px; }
.sxsize { display: inline-flex; align-items: center; gap: 2px; }
.sxsize button { width: 28px; height: 30px; border: 1px solid var(--ff-line); background: var(--ff-surface);
    color: var(--ff-muted); cursor: pointer; font: 700 12px var(--sxfont); }
.sxsize button:first-child { border-radius: var(--ff-r-sm) 0 0 var(--ff-r-sm); }
.sxsize button:last-child { border-radius: 0 var(--ff-r-sm) var(--ff-r-sm) 0; border-left: 0; }
.sxsize button:hover { color: var(--ff-accent); border-color: var(--ff-accent-edge); }

/* ---------------------------------------------------------------------
   Dialogs on the desk start BELOW the top rail.

   The measured defect was that opening a PDF, the calculator, the note
   pad or the navigation put a scrim over the countdown, the save state
   and the hand-in button — elementFromPoint over all three returned
   `ovl`. A review list is a mode and may cover the paper; it may not
   cover the clock.

   The secure-mode warning is the deliberate exception: it is meant to
   stop everything, and it prints the live clock inside itself.
   --------------------------------------------------------------------- */
.ovl.sxbelow { top: var(--sxtop-rail); align-items: flex-start; padding-top: 28px; }

/* The SHARED dialog (public/js/sx-dialog.js) obeys the same rule, and
   it has to be told geometrically rather than by z-index.

   sx-dialog.js appends `.sxdlg` to <body>, while `.runbar` sits inside
   `.sxdesk` — which is position: fixed and therefore ITS OWN STACKING
   CONTEXT. So the rail's z-index is local: it orders things inside the
   desk and can never out-rank a body-level layer, whatever number
   either side carries. Measured before this rule: the hand-in confirm
   put its 62% scrim over the whole rail, the clock's contrast fell from
   17.79:1 to 3.59:1 — below AA at the exact moment a candidate is
   deciding whether to hand in early — and `End exam` lost the hit test.

   Moving its top edge is the fix, exactly as .ovl.sxbelow does it, and
   from the SAME measured variable: examRunner.measureRails rewrites
   --sxtop-rail from the rail's real getBoundingClientRect, so a browser
   zoom or a translated string cannot open a gap a literal would miss.

   Scoped to `body:has(.sxdesk)`, because sx-dialog.js is product-wide:
   the workbook editor inside Filament, /update, /install and the
   marking screens keep the full-screen dialog they have always had. */
body:has(.sxdesk) .sxdlg { top: var(--sxtop-rail); }

.reviewgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; }
.reviewq { display: grid; grid-template-columns: 26px minmax(0, 1fr); gap: 9px; align-items: center;
    text-align: left; padding: 8px 10px; border: 1px solid var(--ff-line); border-radius: var(--ff-r-sm);
    background: var(--ff-surface); color: var(--ff-ink-2); cursor: pointer; font: 600 12.5px var(--sxfont); }
.reviewq:hover { border-color: var(--ff-accent); background: var(--ff-accent-soft); color: var(--ff-accent); }
.reviewq.here { outline: 2px solid var(--ff-accent); outline-offset: 1px; }
.reviewq small { display: block; font: 500 11px var(--sxfont); color: var(--ff-muted); }
.reviewstats { display: flex; gap: 0; margin-bottom: 14px; border: 1px solid var(--ff-line);
    border-radius: var(--ff-r-md); overflow: hidden; background: var(--ff-surface); }
.reviewstat { flex: 1 1 0; padding: 11px 13px; border-right: 1px solid var(--ff-line); }
.reviewstat:last-child { border-right: 0; }
.reviewstat b { display: block; font: 700 21px var(--sxfont); letter-spacing: -.02em; color: var(--ff-ink); }
.reviewstat span { font: 600 11px var(--sxfont); letter-spacing: .1em; text-transform: uppercase;
    color: var(--ff-muted); }
.reviewstat.todo b { color: var(--ff-flag); }

/* ---------------------------------------------------------------------
   The secure-mode warning. The one thing allowed to cover the rails.

   Every rule here is a CLASS. The markup used to carry a static
   style="…" AND an Alpine string :style on the same elements, and
   Alpine's string form calls setAttribute('style', …) — replacing the
   whole attribute. Measured from the live DOM: the 56px circle rendered
   422 x 21px with border-radius 0. It was also the only screen in the
   product painted in hard-coded hexes, including a slate-900 that
   exists nowhere else; it is on the palette now.
   --------------------------------------------------------------------- */
.warndlg { max-width: 480px; }
.warnrule { height: 6px; background: var(--ff-flag-fill); }
.warndlg.is-final .warnrule { background: var(--ff-crit); }
.warnbody { padding: 26px 28px; text-align: center; }
.warnglyph { width: 56px; height: 56px; border-radius: 50%; margin: 0 auto 14px;
    display: flex; align-items: center; justify-content: center;
    background: var(--ff-flag-soft); color: var(--ff-flag);
    font: 800 26px var(--sxfont); }
.warndlg.is-final .warnglyph { background: var(--ff-crit-soft); color: var(--ff-crit); }
.warnkick { margin: 0 0 8px; font: 800 11.5px var(--sxfont); letter-spacing: .14em;
    text-transform: uppercase; color: var(--ff-flag); }
.warndlg.is-final .warnkick { color: var(--ff-crit); }
.warnhead { margin: 0 0 10px; font: 650 21px var(--sxfont); letter-spacing: -.02em; color: var(--ff-ink); }
.warntext { margin: 0 0 14px; font-size: 14px; line-height: 1.6; color: var(--ff-ink-2); }
.warncount { margin-bottom: 14px; text-align: left; }
/* The live clock, printed inside the one dialog that covers the rail:
   the countdown is never actually out of sight. */
.warnclock { display: flex; align-items: center; justify-content: space-between; gap: 14px;
    text-align: left; padding: 12px 16px; margin-bottom: 16px;
    border: 1px solid var(--ff-line-2); border-radius: var(--ff-r-md); background: var(--ff-sunken); }
.warnclock .wc-k { font: 700 10.5px var(--sxfont); letter-spacing: .11em; text-transform: uppercase;
    color: var(--ff-muted); }
.warnclock .wc-s { font: 600 12px var(--sxfont); color: var(--ff-crit); margin-top: 2px; }
.warnclock .wc-t { font: 700 24px var(--mono); font-variant-numeric: tabular-nums; color: var(--ff-ink); }
.warnback { width: 100%; padding: 13px; font-size: 15px; }
.warnbody .hint { text-align: center; }

/* ---------------------------------------------------------------------
   Floating windows (Batch D). The classes live here, with C, because
   tools.css has one owner and a rule left in app.css would silently not
   exist on this screen.

   THE STACKING IS A GUARANTEE, not a hope: a window sits BELOW the top
   rail's z-index, so a clamping bug, a browser-zoom edge case or a
   future rail that grows cannot put one over the countdown or over
   hand-in. Both numbers are tokens so the ordering can be asserted.
   --------------------------------------------------------------------- */
.sxwins { position: fixed; inset: 0; z-index: var(--sx-z-win); pointer-events: none; }
.sxwin { position: fixed; pointer-events: auto; display: flex; flex-direction: column;
    min-width: 280px; min-height: 180px; background: var(--ff-surface);
    border: 1px solid var(--ff-line-2); border-radius: var(--ff-r-md);
    box-shadow: var(--ff-sh-3); overflow: hidden; }
.sxwin-bar { flex: 0 0 auto; display: flex; align-items: center; gap: 9px; padding: 8px 10px;
    border-bottom: 1px solid var(--ff-line); background: var(--ff-sunken); cursor: move;
    touch-action: none; }
.sxwin-cat { flex: 0 0 auto; padding: 2px 7px; border-radius: 999px; background: var(--ff-accent-soft);
    color: var(--ff-accent); font: 700 9.5px var(--sxfont); letter-spacing: .12em; text-transform: uppercase; }
.sxwin-title { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; font: 650 13px var(--sxfont); color: var(--ff-ink); }
.sxwin-acts { flex: 0 0 auto; display: flex; align-items: center; gap: 4px; }
.sxwin-body { flex: 1 1 auto; min-height: 0; overflow: auto; display: flex; flex-direction: column; }
.sxwin-grip { position: absolute; right: 0; bottom: 0; width: 20px; height: 20px; padding: 0;
    border: 0; background: none; cursor: nwse-resize; color: var(--ff-line-2); touch-action: none; }
.sxwin-edge { position: absolute; background: none; }
.sxwin-edge.e { top: 0; right: -3px; width: 8px; height: 100%; cursor: ew-resize; touch-action: none; }
.sxwin-edge.s { left: 0; bottom: -3px; height: 8px; width: 100%; cursor: ns-resize; touch-action: none; }
.sxwin.is-min { height: auto !important; min-height: 0; }
.sxwin.is-min .sxwin-body, .sxwin.is-min .sxwin-grip, .sxwin.is-min .sxwin-edge { display: none; }
/* The bar's own buttons are buttons: a pointerdown on one of them must
   not start a drag, which is why exam-windows.js refuses .sxwin-act. */
.sxwin-move { flex: 0 0 auto; width: 22px; height: 24px; padding: 0; border: 0; border-radius: var(--ff-r-sm);
    background: none; color: var(--ff-muted); cursor: grab; font-size: 11px; letter-spacing: -2px; }
.sxwin-move:hover { background: var(--ff-surface); color: var(--ff-ink); }
.sxwin-act { display: inline-flex; align-items: center; gap: 4px; height: 24px; padding: 0 7px;
    border: 1px solid transparent; border-radius: var(--ff-r-sm); background: none;
    color: var(--ff-muted); cursor: pointer; font: 600 12px var(--sxfont); }
.sxwin-act:hover { border-color: var(--ff-line); background: var(--ff-surface); color: var(--ff-ink); }
.sxwin-act[aria-pressed="true"] { border-color: var(--ff-accent-edge); background: var(--ff-accent-soft);
    color: var(--ff-accent); }
.sxwin-act.dock { color: var(--ff-ink-2); }
/* A real outline, never a box-shadow: box-shadow vanishes in Windows
   forced-colours mode, and this screen runs on hall machines. */
.sxwin-move:focus-visible, .sxwin-act:focus-visible, .sxwin-grip:focus-visible,
.sxwin-closeall:focus-visible { outline: 2px solid var(--ff-accent); outline-offset: 1px; }
.sxwin-grip:focus-visible { color: var(--ff-accent); }
.sxwin-body { padding: 13px 14px 16px; }
/* A document and the note pad take every remaining pixel; a written
   page keeps its natural height and lets the window scroll. Two nested
   scrollers on one pane is what makes a viewer feel broken. */
.sxwin-body.tall { padding-bottom: 13px; }
.sxwin-body.tall > .sxwin-fill, .sxwin-body.tall > .qsrc-file { flex: 1 1 auto; min-height: 0;
    display: flex; flex-direction: column; }
.sxwin-body.tall > .sxwin-notes { flex: 1 1 auto; min-height: 120px; }
.sxwin-body > .srcitem-h { flex: 0 0 auto; }
/* THE CLASSIC TRAP that makes a home-made window manager feel broken:
   the moment the cursor crosses an iframe, the frame swallows the
   pointer and the drag stops dead. Every body goes inert for the
   duration of a drag or a resize. */
.sxwins.is-dragging .sxwin-body { pointer-events: none; }
.sxwins.is-dragging { user-select: none; }
/* The one honest control for a desk that has got away from the
   candidate. It renders only when there is more than one window to
   close — with a single one open, its own close button is shorter.

   Three placements were wrong and all three were found by driving it in
   Chrome: bottom LEFT sits on the navigator's pinned "Review & submit";
   bottom RIGHT sits on "Next" whenever the reading rail is folded; and
   any z-index below the windows lets the very things it closes cover
   it. So it is CENTRED, on the move bar's own empty middle, above every
   window and below the rail — and the raise counter in exam-windows.js
   renumbers rather than climbing, so nothing can ever reach this. */
.sxwin-closeall { position: fixed; left: 50%; transform: translateX(-50%);
    bottom: calc(var(--sxbot-rail) + 13px);
    z-index: 1000; pointer-events: auto; padding: 7px 13px; border: 1px solid var(--ff-line-2);
    border-radius: 999px; background: var(--ff-surface); color: var(--ff-ink-2);
    box-shadow: var(--ff-sh-2); cursor: pointer; font: 600 12.5px var(--sxfont); }
.sxwin-closeall:hover { border-color: var(--ff-accent-edge); color: var(--ff-accent); }

/* ---------------------------------------------------------------------
   The guide video (Batch A). A facade until it is pressed: nothing is
   fetched from a third party until the candidate asks for it, which is
   at once the privacy answer, the performance answer and the answer for
   a lab machine with no internet.
   --------------------------------------------------------------------- */
.vidstrip { display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
    padding: 12px 14px; border: 1px solid var(--ff-line); border-radius: var(--ff-r-md);
    background: var(--ff-sunken); }
.vidstrip .vt { flex: 1 1 220px; min-width: 0; }
.vidstrip .vt b { display: block; font: 650 13.5px var(--sxfont); color: var(--ff-ink); }
.vidstrip .vt span { font-size: 12px; color: var(--ff-muted); }
/* The player and the blocked note are inserted as SIBLINGS of the
   caption inside a wrapping flex row, so they have to claim a row of
   their own or a 16:9 frame tries to share one with the text. */
.vidwrap { flex: 1 1 100%; min-width: 0; }
.vidhint { font: 400 12px/1.6 var(--sxfont); color: var(--ff-muted); margin: 7px 0 0; }
.vidurl { font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; font-size: 11.5px;
    background: var(--ff-sunken); border: 1px solid var(--ff-line); border-radius: 5px;
    padding: 1px 5px; word-break: break-all; }
.vidwait { font: 500 12.5px var(--sxfont); color: var(--ff-muted); }
.vidglyph { color: var(--ff-accent); font-size: 15px; line-height: 1; }

/* The strip lives in the READING RAIL now, not in a dialog (client,
   2026-08-22), so it has to survive a 300-380px column: the caption and
   the button stop sitting side by side and stack instead. */
.srcslot .vidstrip { flex-direction: column; align-items: stretch; gap: 9px; }
.srcslot .vidstrip .vt { flex: 0 0 auto; }
.srcslot .vidstrip .vidglyph { display: none; }
.srcslot .vidwrap { width: 100%; }
.vidframe { display: block; width: 100%; aspect-ratio: 16 / 9; border: 0;
    border-radius: var(--ff-r-sm); background: #000; }
/* The crop (client, 2026-09-08: "hide the youtube logo and the title").
   No embed parameter removes the provider's title strip, so the frame
   is made 72px taller than 16:9 at both ends and the box clips it. The
   provider letterboxes a too-tall frame, so those bands are black bars
   carrying nothing but its title strip and, on pause, its logo — the
   picture keeps its full 16:9. Keyboard play/pause still reaches the
   frame; the caption says to click the picture. */
.vidcrop { position: relative; aspect-ratio: 16 / 9; overflow: hidden; background: #000;
    border-radius: var(--ff-r-sm); }
.vidcrop .vidframe { position: absolute; left: 0; top: -72px; width: 100%; height: calc(100% + 144px);
    aspect-ratio: auto; border-radius: 0; }
.vidlight .vidcrop { border-radius: 0; }

/* ---------------------------------------------------------------------
   THE TYPE SCALE. Four text sizes on this screen, and one display size.

   Measured on the old runner (probe-a11y.json): EIGHT sizes inside 5px,
   and 53% of the text below 13px, with the PAPER itself set at 14.5 —
   1.5px above the furniture around it. Nothing on the screen said "this
   is the exam and that is the chrome". The desk added a navigator, a
   reading rail and a tab strip, each of which arrived with micro-labels
   of its own, so a re-measure in Chrome on 2026-08-22 found TWELVE
   sizes and 47 of 69 elements below 13px. This block closes the set.

     --sx-meta    12     marks, counts, timestamps, hints, and every
                         tracked uppercase label. ONE size: the labels
                         are told apart by case and colour, not by
                         shaving half a pixel off each other.
     --sx-ui      13.5   anything you press or aim at — buttons, rail
                         rows, tabs, the brand, the fold controls.
     --sx-head    15     the heading of a pane or a card.
     --sx-display 20     the two things that anchor the screen: the
                         countdown and the question's own title. The
                         warning dialog's clock is the same object and
                         takes the same size.
     the paper    var(--sxpaper)  16 / 18 / 20, the candidate's own
                         choice, and the ONLY size on this screen that
                         a candidate can change.

   It is written as one block on purpose. Sizes set here override the
   `font:` shorthands above (later, equal specificity, longhand after
   shorthand), so weight, family and letter-spacing stay where they were
   authored and only the size is settled in one reviewable place. Add a
   rule above and it will be pulled into the scale here; do not answer a
   layout problem with a thirteenth size.
   --------------------------------------------------------------------- */
.sxdesk { --sx-meta: 12px; --sx-ui: 13.5px; --sx-head: 15px; --sx-display: 20px; }

/* meta — said once, quietly */
.sxdesk .navkicker,
.sxdesk .navmeta,
.sxdesk .navprog,
.sxdesk .navbatch-h,
.sxdesk .navrow-meta,
.sxdesk .navfoot-note,
.sxdesk .navsec-h,
.sxdesk .navedge,
.sxdesk .srcedge,
.sxdesk .srchead-top h2,
.sxdesk .srcgroup,
.sxdesk .srctab em,
.sxdesk .srcnote,
.sxdesk .docnote,
.sxdesk .railget-note,
.sxdesk .qctx,
.sxdesk .qsub,
.sxdesk .reqrow,
.sxdesk .reqmarks,
.sxdesk .qans-h,
.sxdesk .parttab i,
.sxdesk .wordcount,
.sxdesk .tiny,
.sxdesk .reviewq small,
.sxdesk .reviewstat span,
.sxdesk .warnkick,
.sxdesk .warnclock .wc-k,
.sxdesk .warnclock .wc-s,
.sxdesk .why,
.sxdesk .mk2 { font-size: var(--sx-meta); }

/* interface — anything you press or aim at */
.sxdesk .btn,
.sxdesk .runbrand,
.sxdesk .rb-inst,
.sxdesk .rb-cand,
.sxdesk .pill,
.sxdesk .saver,
.sxdesk .sxskip,
.sxdesk .sxsize button,
.sxdesk .navfold,
.sxdesk .navrow-title,
.sxdesk .srctab,
.sxdesk .railget,
.sxdesk .navtool,
.sxdesk .qact,
.sxdesk .parttab,
.sxdesk .reviewq,
.sxdesk .ansempty p,
.sxdesk .callout,
.sxdesk .hint,
.sxdesk .wbbar-btn { font-size: var(--sx-ui); }

/* the heading of a pane or a card */
.sxdesk .navtitle,
.sxdesk .srcitem-h,
.sxdesk .ansempty b,
.sxdesk .qmain.fill .qtitle,
.sxdesk .warnback { font-size: var(--sx-head); }

/* No backdrop-filter anywhere on the desk. The product bans it for the
   page loader "because this paints on college lab machines", and the
   runner is the one screen guaranteed to run on those machines: a
   full-viewport GPU blur, raised at the exact moment a frightened
   candidate needs the dialog to appear instantly, is the worst possible
   place to spend it. The scrim alone does the same job. `.exambar` lost
   its blur when the desk retired it; these are the last three. */
.sxdesk .ovl,
body:has(.sxdesk) .sxdlg { -webkit-backdrop-filter: none; backdrop-filter: none; }

/* the two things that anchor the screen */
.sxdesk .clock,
.sxdesk .qtitle,
.sxdesk .warnhead,
.sxdesk .warnclock .wc-t,
.sxdesk .reviewstat b,
.sxdesk .calcdisplay { font-size: var(--sx-display); }

/* the paper: what the candidate reads and writes, at their own size.
   The warning dialog's own sentences are the paper too — it is the one
   thing on this screen a frightened person has to read carefully. */
.sxdesk .warntext,
.sxdesk .warncount,
.sxdesk .calcpad .btn,
.sxdesk .qans .in,
.sxdesk .qans select,
.sxdesk .qans textarea { font-size: var(--sxpaper); }

/* ---------------------------------------------------------------------
   Narrow screens. The desk is built for a hall machine (1366x768 and
   up); below that the columns lie down rather than shrinking into three
   unusable slivers. Two panes of ~300px are worse than one of 700.
   --------------------------------------------------------------------- */
@media (max-width: 1100px) {
    .qwork.is-split { grid-template-columns: minmax(0, 1fr) 300px; }
    .sxwork { grid-template-columns: 220px minmax(0, 1fr); }
}
@media (max-width: 900px) {
    .sxdesk { position: static; height: auto; overflow: visible; }
    body:has(.sxdesk) { overflow: auto; }
    .runbar { position: sticky; top: 0; height: auto; }
    .runbar .runrow { flex-wrap: wrap; height: auto; padding: 8px 0; }
    .sxwork, .sxwork.nav-shut { grid-template-columns: minmax(0, 1fr); }
    .qwork, .qwork.is-split, .qwork.solo { grid-template-columns: minmax(0, 1fr); }
    .sxnav, .qsrc { border: 0; border-bottom: 1px solid var(--ff-line); }
    .navedge, .srcedge { writing-mode: horizontal-tb; padding: 10px; }
    .qmain, .qmain.fill { overflow: visible; }
    .navtree, .srcbody { overflow: visible; }
    .sxdesk .sx-univer-host { height: 60vh; }
    /* Floating is switched OFF here, not degraded: the desk itself has
       lain down and the page scrolls, so a position:fixed window would
       hover over content moving underneath it. exam-windows.js agrees
       (it refuses to open below 900 and docks what was open, keeping
       the remembered geometry), and the rail's pop-out control goes
       with it rather than being left as a control with no effect. */
    .sxwins { display: none; }
    .srctab-pop { display: none; }
    /* The answer-at-full-size mode belongs to the fixed desk above:
       down here the desk has lain down and the page scrolls, so an
       absolute overlay would be document-tall with its exit somewhere
       above the viewport (adversarial review, 2026-08-27). Same
       decision as the floating windows two rules up, for the same
       reason — switched OFF, not degraded. The neutralisers matter on
       their own: a window resized below 900 mid-mode must land back
       in the normal layout, not in an invisible one. */
    .sx-maxdoor { display: none; }
    .sxwork.ansmax .qmain .qansfull { position: static; padding: 0; overflow: visible; }
    .sxwork.ansmax .qmain .qansfull > .qans { overflow: visible; }
    .sxwork.ansmax .sxnav, .sxwork.ansmax .qsrc, .sxwork.ansmax .qmove,
    .sxwork.ansmax .qmain > .qbody > :not(.qansfull) { visibility: visible; }
}

/* the question palette, shared with the review dialog */
.qnav.flagged { border-style: solid; border-color: var(--ff-flag); background: var(--ff-flag-soft);
    color: var(--ff-flag); }
.qnav.flagged:hover { border-color: var(--ff-flag); background: var(--ff-flag-soft); color: var(--ff-flag); }
.qnav.here { outline: 2px solid var(--ff-accent); outline-offset: 2px; }
.calcpad { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
.calcpad .btn { padding: 11px 0; font: 600 14px var(--mono); }
.calcdisplay { text-align: right; font: 600 21px var(--mono); font-variant-numeric: tabular-nums;
    padding: 12px 13px; margin-bottom: 10px; border: 1px solid var(--ff-line-2);
    border-radius: var(--ff-r-sm); background: var(--ff-sunken); overflow-x: auto; }

/* Dialogs. Used by the workbook editor guide, the Excel import and the
   in-page document viewer below — all of which now render inside the
   Filament panel, which cannot see the Vite bundle these rules used to
   live in. Without them a dialog rendered as plain inline content: no
   overlay, no card, no centring. */
/* dialog (guide) */
.ovl { position: fixed; inset: 0; z-index: 500; display: flex; align-items: center; justify-content: center;
    padding: 22px; background: var(--ff-scrim);
    -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px); }
.ovl .dlg { width: 100%; max-width: 700px; background: var(--ff-surface); color: var(--ff-ink);
    border: 1px solid var(--ff-line); border-radius: var(--ff-r-lg); overflow: hidden;
    box-shadow: var(--ff-sh-3); max-height: 92vh; overflow-y: auto; }
.dlg .dh { display: flex; justify-content: space-between; align-items: center; padding: 15px 20px;
    border-bottom: 1px solid var(--ff-line); }
.dlg .dh b { font-size: 15px; font-weight: 650; letter-spacing: -.018em; }
.dlg .db { padding: 20px; min-height: 230px; }
.dlg .df { display: flex; align-items: center; gap: 10px; padding: 13px 20px;
    border-top: 1px solid var(--ff-line); background: var(--ff-sunken); }
/* A dialog that is working: the way out is withheld rather than removed,
   so the sheet does not jump, and it is visibly not clickable. */
.dlg.is-busy .x { pointer-events: none; opacity: .35; }

/* Upload progress. A real percentage — the browser reports one, so this is
   the one place in the product a filled bar is honest. Everywhere else the
   loader deliberately refuses to imply a figure it does not have. */
.upload { margin-top: 12px; }
.upload-track { height: 4px; border-radius: 999px; background: var(--ff-ground-2); overflow: hidden; }
.upload-fill { display: block; height: 100%; border-radius: 999px; background: var(--ff-accent);
    box-shadow: inset 0 0 0 1px var(--ff-accent-edge); transition: width .18s linear; }

/* Parsing. No percentage exists for this, so it is a spinner and words. */
.importing { display: flex; gap: 11px; align-items: flex-start; margin-top: 14px;
    padding: 12px 14px; border: 1px solid var(--ff-accent-edge); border-radius: var(--ff-r-md);
    background: var(--ff-accent-soft); color: var(--ff-accent); font: 600 13px var(--sxfont); }
.importing .spin { margin-top: 2px; flex: 0 0 auto; }
.importing .hint { font-weight: 400; margin-top: 3px; }

.dots { display: flex; gap: 6px; margin-right: auto; }
.dots i { width: 8px; height: 8px; border-radius: 50%; background: var(--ff-line-2); display: block; }
.dots i.on { background: var(--ff-accent); }
.demo { border: 1px solid var(--ff-line); border-radius: var(--ff-r-md); padding: 16px; background: var(--ff-sunken);
    margin-bottom: 14px; display: flex; align-items: center; justify-content: center; min-height: 120px;
    overflow-x: auto; }
.gstep { font-size: 13.5px; line-height: 1.65; color: var(--ff-ink-2); margin: 0; }
.gstep b { color: var(--ff-ink); font-weight: 650; }

/* in-page source document viewer */
.ovl .dlg.docdlg { max-width: 980px; height: 88vh; display: flex; flex-direction: column; overflow: hidden; }
.docframe { flex: 1; width: 100%; border: 0; background: var(--ff-sunken); }
/* "this is safe" — the one place green is right */
.pill { display: inline-flex; align-items: center; gap: 7px; height: 26px; padding: 0 11px;
    border: 1px solid var(--ff-good-edge); background: var(--ff-good-soft); color: var(--ff-good);
    border-radius: 999px; font: 600 12px var(--sxfont); }
.pill .d { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.pill.off { background: var(--ff-sunken); border-color: var(--ff-line); color: var(--ff-muted); }
/* the question card: one thing at a time, on its own sheet */
.qcard { background: var(--ff-surface); border: 1px solid var(--ff-line); border-radius: var(--ff-r-lg);
    margin-bottom: 16px; overflow: hidden; box-shadow: var(--ff-sh-1); }
.qcard .qh { padding: 15px 18px; border-bottom: 1px solid var(--ff-line); display: flex;
    justify-content: space-between; gap: 12px; flex-wrap: wrap; align-items: center;
    background: var(--ff-sunken); }
.qcard .qh .qn { font: 700 11px var(--sxfont); letter-spacing: .09em; text-transform: uppercase; color: var(--ff-muted); }
.qcard .qb { padding: 18px; }
.opt { display: flex; align-items: flex-start; gap: 11px; padding: 11px 13px; border: 1px solid var(--ff-line);
    border-radius: var(--ff-r-md); margin-bottom: 8px; cursor: pointer; background: var(--ff-surface);
    transition: background var(--ff-t), border-color var(--ff-t); }
.opt:hover { border-color: var(--ff-accent-edge); background: var(--ff-sunken); }
.opt:has(input:checked) { border-color: var(--ff-accent); background: var(--ff-accent-soft); }
.opt input { margin-top: 2px; accent-color: var(--ff-accent); }
/* the mockup's answer box: a field with something to write on */
.blank { box-sizing: border-box; display: inline-block; width: min(150px, 100%); border: 1px solid var(--ff-accent-edge);
    border-bottom-width: 2px; background: var(--ff-surface); padding: 4px 9px;
    font: 600 14px var(--mono); font-variant-numeric: tabular-nums; color: var(--ff-ink);
    border-radius: 6px; transition: border-color var(--ff-t), box-shadow var(--ff-t); }
.blank:hover { border-color: var(--ff-accent); }
.blank:focus { outline: none; border-color: var(--ff-accent);
    box-shadow: 0 0 0 3px var(--ff-accent-soft), 0 0 0 4px var(--ff-accent-edge); }
.shield { filter: blur(13px) saturate(.6); pointer-events: none; user-select: none; }

/* =====================================================================
   THE MARKING WORKSPACE

   A marker is deliberately not a member of the admin panel, so marking
   cannot be a panel page. It gets the panel's SHAPE instead: a
   persistent rail, a header band, a stat strip and a work list, in the
   product's own colours. The three marking screens all render through
   <x-marking-shell>, so they read as one place.

   Everything sticky in here clears the product topbar via --sxtop.
   ===================================================================== */
.mkshell { display: grid; grid-template-columns: 226px minmax(0, 1fr);
    gap: 26px; align-items: start; }
.mkmain { min-width: 0; }

/* the rail */
.mkside { position: sticky; top: calc(var(--sxtop) + 14px); display: flex; flex-direction: column; gap: 2px; }
.mkside-brand { display: flex; align-items: center; gap: 10px; padding: 2px 4px 12px;
    border-bottom: 1px solid var(--ff-line); margin-bottom: 10px; }
.mkside-ico { flex: 0 0 auto; width: 32px; height: 32px; border-radius: var(--ff-r-sm); display: grid;
    place-items: center; background: var(--ff-accent-soft); color: var(--ff-accent); }
.mkside-ico svg { width: 17px; height: 17px; }
.mkside-name { display: flex; flex-direction: column; min-width: 0; }
.mkside-name b { font-size: 14px; font-weight: 650; letter-spacing: -.012em; }
.mkside-name small { font-size: 11.5px; color: var(--ff-muted); margin-top: 1px; }
.mkside-group { font: 700 9.5px var(--sxfont); letter-spacing: .13em; text-transform: uppercase;
    color: var(--ff-muted); padding: 14px 10px 5px; }
.mkside-group:first-of-type { padding-top: 0; }

.mknav { display: flex; align-items: center; gap: 10px; padding: 7px 10px; border-radius: var(--ff-r-sm);
    font: 550 13px var(--sxfont); color: var(--ff-ink-2) !important; border: 1px solid transparent;
    transition: background var(--ff-t), color var(--ff-t), border-color var(--ff-t); }
.mknav svg { width: 16px; height: 16px; flex: 0 0 auto; opacity: .62; }
.mknav > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mknav:hover { background: var(--ff-surface); border-color: var(--ff-line); color: var(--ff-ink) !important; }
.mknav.on { background: var(--ff-accent-soft); border-color: transparent; color: var(--ff-accent) !important; font-weight: 650; }
.mknav.on svg { opacity: 1; }
.mknav.off { color: var(--ff-muted) !important; opacity: .7; cursor: default; }
.mknav.off:hover { background: none; border-color: transparent; color: var(--ff-muted) !important; }
.mkbadge { margin-left: auto; font: 700 10.5px var(--mono); font-style: normal; min-width: 19px;
    height: 19px; padding: 0 6px; border-radius: 999px; background: var(--ff-flag-fill); color: var(--ff-on-flag);
    display: inline-flex; align-items: center; justify-content: center; }
/* Both ways of marking carry the SAME badge. There used to be a `quiet`
   variant on "By question", which read as a ranking — the rail said
   scripts were the real work while the queue recommended the other way.
   They are one backlog counted twice, so they weigh the same. */

.mkside-day { display: flex; align-items: baseline; gap: 7px; padding: 10px 12px; border-radius: var(--ff-r-md);
    background: var(--ff-surface); border: 1px solid var(--ff-line); }
.mkside-day b { font: 600 19px var(--mono); font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
.mkside-day span { font-size: 11.5px; color: var(--ff-muted); line-height: 1.35; }
.mkside-foot { margin: 16px 10px 0; padding-top: 12px; border-top: 1px solid var(--ff-line);
    font-size: 11px; line-height: 1.55; color: var(--ff-muted); }

/* a card that folds away — the question in one-question mode. The
   default disclosure triangle is dropped in both engines, because the
   card's own header is the affordance. */
.card > summary { list-style: none; cursor: pointer; }
.card > summary::-webkit-details-marker { display: none; }
.card > summary:focus-visible { outline: 2px solid var(--ff-accent); outline-offset: -2px; }
.card > summary > h2 { border-bottom: 0; }
/* Pinned: the mark scheme stays with the marker down the whole cohort.
   Applying one scheme answer after answer is the entire point of
   one-question mode, and scrolling back up to re-read it defeats it. */
.mkpin { position: sticky; top: calc(var(--sxtop) + 8px); z-index: 70; }
.mkpin[open] > .body { border-top: 1px solid var(--ff-line); max-height: 38vh; overflow: auto; }

/* the header band, ruled off from the work below it */
.mkhead { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px;
    flex-wrap: wrap; padding-bottom: 15px; margin-bottom: 18px; border-bottom: 1px solid var(--ff-line); }
.mkhead-t { min-width: 0; }

/* the stat strip: one surface, hairline-divided, no tile borders — the
   same object the staff dashboard uses, so the two agree by sight */
.mkstats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); background: var(--ff-surface);
    border: 1px solid var(--ff-line); border-radius: var(--ff-r-md); overflow: hidden; margin-bottom: 18px;
    box-shadow: var(--ff-sh-1); }
.mkstat { padding: 14px 17px; }
.mkstat + .mkstat { border-left: 1px solid var(--ff-line); }
.mkstat .l { display: block; font: 700 9.5px var(--sxfont); letter-spacing: .1em;
    text-transform: uppercase; color: var(--ff-muted); }
.mkstat .v { display: block; font: 600 26px var(--mono); letter-spacing: -.02em; margin-top: 5px;
    font-variant-numeric: tabular-nums; line-height: 1.1; color: var(--ff-ink); }
.mkstat .d { display: block; font-size: 11.5px; color: var(--ff-muted); margin-top: 3px; }
.mkstat.warn .v { color: var(--ff-flag); }
.mkstat.ok .v { color: var(--ff-accent); }
.mkstat.calm .v { color: var(--ff-muted); }
@media (max-width: 700px) {
    .mkstats { grid-template-columns: 1fr; }
    .mkstat + .mkstat { border-left: 0; border-top: 1px solid var(--ff-line); }
}

/* THE CHOICE: two ways to work through one backlog.

   Both ways of marking already existed and both worked; the screen
   simply never said so, and read as two unrelated lists — which is how
   a marker (the client included) ends up doing the slower one forever
   without knowing there was a decision to make. The two are therefore
   stated side by side, in the same shape, each with what it is for, its
   own count, and a reason when it is unavailable.

   The tiles share the card's surface and are divided by a hairline
   rather than floated as two sub-cards: they are two answers to one
   question, not two things. */
.mkways { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
    border-top: 1px solid var(--ff-line); }
.mkway { position: relative; display: flex; flex-direction: column; gap: 11px; padding: 16px 17px 17px;
    background: var(--ff-surface); transition: background var(--ff-t); }
.mkway + .mkway { border-left: 1px solid var(--ff-line); }
.mkway:not(.off):hover { background: var(--ff-sunken); }
.mkway-top { display: flex; align-items: flex-start; gap: 13px; }
/* The figure carries the idea faster than the sentence does: three
   scripts, marked either right down one of them or straight across all
   three at the same question. */
.mkway-fig { flex: 0 0 auto; width: 74px; height: 46px; margin-top: 1px; }
.mkway-fig svg { display: block; width: 100%; height: 100%; }
.mkfig-sheet { fill: var(--ff-surface); stroke: var(--ff-line-2); stroke-width: 1; }
.mkfig-sheet.on { fill: var(--ff-accent-soft); stroke: var(--ff-accent-edge); }
.mkfig-rule { stroke: var(--ff-line-2); stroke-width: 2; stroke-linecap: round; }
.mkfig-rule.on { stroke: var(--ff-accent); stroke-width: 2.4; }
.mkfig-band { fill: var(--ff-accent-soft); stroke: var(--ff-accent-edge); stroke-width: 1; }
.mkway-t { min-width: 0; }
.mkway-name { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    font: 650 14.5px var(--sxfont); letter-spacing: -.015em; color: var(--ff-ink); }
.mkway-name .tag { font-style: normal; }
.mkway-what { display: block; font-size: 12.5px; line-height: 1.55; color: var(--ff-ink-2); margin-top: 4px; }
.mkway-when { margin: 0; font-size: 12.5px; line-height: 1.65; color: var(--ff-muted); }
.mkway-when b { color: var(--ff-ink-2); font-weight: 650; }
/* pinned to the bottom, so the two tiles' actions line up whatever the
   length of the sentences above them */
.mkway-foot { margin-top: auto; display: flex; align-items: center; gap: 10px 12px; flex-wrap: wrap; }
.mkway-count { font: 600 12px var(--mono); font-variant-numeric: tabular-nums; color: var(--ff-ink-2);
    white-space: nowrap; }
/* the whole tile is the target; the button is only where it is
   announced — the same trick the work rows use */
.mkway-go { position: static; }
.mkway-go::after { content: ''; position: absolute; inset: 0; }
.mkway-go:focus-visible::after { outline: 2px solid var(--ff-accent); outline-offset: -3px;
    border-radius: var(--ff-r-md); }
/* the reason takes its own line rather than squeezing in beside the
   dead button — a sentence explaining why you cannot do something has
   to be readable, not a column three words wide */
.mkway-none { flex: 1 1 100%; font-size: 12px; line-height: 1.55; color: var(--ff-muted); margin: 0; }
.mkway.off { background: var(--ff-sunken); }
.mkway.off .mkway-fig { opacity: .4; }
.mkway.off .mkway-name, .mkway.off .mkway-what { color: var(--ff-muted); }
@media (max-width: 880px) {
    .mkways { grid-template-columns: 1fr; }
    .mkway + .mkway { border-left: 0; border-top: 1px solid var(--ff-line); }
}

/* the work list: dense rows, not a stack of cards. A marker scans this
   list dozens of times a day, so it is ruled and aligned rather than
   spaced out. */
.mklist { display: flex; flex-direction: column; }
.mkrow { position: relative; display: flex; align-items: center; gap: 14px; padding: 13px 17px;
    flex-wrap: wrap; background: var(--ff-surface); transition: background var(--ff-t); }
.mkrow + .mkrow { border-top: 1px solid var(--ff-line); }
.mkrow:hover { background: var(--ff-sunken); }
.mkno { flex: 0 0 auto; font: 600 11.5px var(--mono); letter-spacing: .02em; color: var(--ff-ink-2);
    background: var(--ff-ground-2); border-radius: 6px; padding: 7px 9px; white-space: nowrap; }
.mkrow-main { flex: 1 1 240px; min-width: 0; }
.mkrow-title { display: block; font: 600 13.5px var(--sxfont); color: var(--ff-ink); letter-spacing: -.008em; }
a.mkrow-title { position: relative; z-index: 1; color: var(--ff-ink); }
a.mkrow-title:hover { color: var(--ff-accent); text-decoration: underline; }
.mkrow-meta { display: block; font-size: 11.5px; color: var(--ff-muted); margin-top: 2px; line-height: 1.55; }
.mkrow-tags { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
/* The row is the target; the button is only where it is announced. The
   overlay sits under .mkrow-title, which keeps its own z-index. */
.mkrow-go { position: static; }
.mkrow-go::after { content: ''; position: absolute; inset: 0; }
.mkrow-go:focus-visible::after { outline: 2px solid var(--ff-accent); outline-offset: -3px; border-radius: 12px; }
.mkrow.sm { padding: 11px 17px; }
.mkcount { font: 650 11px var(--sxfont); letter-spacing: .04em; text-transform: uppercase;
    padding: 4px 9px; border-radius: 6px; background: var(--ff-flag-soft); border: 1px solid var(--ff-flag-edge);
    color: var(--ff-flag); white-space: nowrap; }
.mkcount.calm { background: var(--ff-sunken); border-color: var(--ff-line); color: var(--ff-ink-2); }

/* a real empty state: the queue being clear is good news, and the
   screen should say so instead of leaving a grey expanse */
.mkempty { padding: 46px 24px 44px; text-align: center; max-width: 430px; margin: 0 auto; }
.mkempty-ico { width: 46px; height: 46px; border-radius: 50%; display: grid; place-items: center;
    background: var(--ff-good-soft); color: var(--ff-good); margin: 0 auto 14px; }
.mkempty-ico svg { width: 22px; height: 22px; }
.mkempty h3 { margin: 0 0 6px; font-size: 16px; font-weight: 650; letter-spacing: -.018em; }
.mkempty p { margin: 0 0 16px; font-size: 13px; line-height: 1.65; color: var(--ff-muted); }
.mkempty .btnrow { justify-content: center; }

/* a script in progress: how far through it this marker is, kept in
   view while they scroll a long paper */
.mkprog { position: sticky; top: calc(var(--sxtop) + 8px); z-index: 80; display: flex;
    align-items: center; gap: 8px 14px; flex-wrap: wrap; margin-bottom: 16px; padding: 10px 14px;
    border: 1px solid var(--ff-line); border-radius: var(--ff-r-md); background: var(--ff-veil);
    -webkit-backdrop-filter: saturate(1.4) blur(10px); backdrop-filter: saturate(1.4) blur(10px);
    box-shadow: var(--ff-sh-1); }
.mkprog-n { font: 600 12px var(--sxfont); color: var(--ff-ink-2); white-space: nowrap; }
/* The work still owed, in the unit the QUEUE counts in. A marker
   promised "3 answers to mark" and then shown "1 of 4 marked" cannot
   tell which of the two figures is the job. */
.mkprog-left { font: 700 11px var(--sxfont); letter-spacing: .04em; text-transform: uppercase;
    padding: 4px 9px; border-radius: 6px; white-space: nowrap;
    background: var(--ff-flag-soft); border: 1px solid var(--ff-flag-edge); color: var(--ff-flag); }
.mkprog-left.done { background: var(--ff-good-soft); border-color: var(--ff-good-edge); color: var(--ff-good); }
.mkprog-track { flex: 1 1 110px; min-width: 90px; height: 4px; border-radius: 999px;
    background: var(--ff-ground-2); overflow: hidden; }
.mkprog-fill { display: block; height: 100%; border-radius: 999px; background: var(--ff-accent);
    box-shadow: inset 0 0 0 1px var(--ff-accent-edge); transition: width .3s ease; }
.mkprog .qnav-strip { width: auto; padding-bottom: 0; }

/* one-question mode: every OTHER question still waiting, one click
   away. Moving on used to mean going back to the queue and finding it */
.mkqjump { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; width: 100%;
    padding-top: 9px; margin-top: 2px; border-top: 1px solid var(--ff-line); }
.mkqjump-l { font: 700 9.5px var(--sxfont); letter-spacing: .12em; text-transform: uppercase;
    color: var(--ff-muted); margin-right: 2px; }
.mkqjump .qnav { gap: 7px; padding: 0 5px 0 9px; }
/* the count is a separate fact from the question number: "Q1 5" read as
   one string */
.mkqjump .qnav em { font: 700 10px var(--mono); font-style: normal; padding: 2px 5px;
    border-radius: 4px; background: var(--ff-flag-fill); color: var(--ff-on-flag); }
/* a question in a DIFFERENT paper: the marker is about to change mark
   scheme, so it must not look like the one next door */
.mkqjump .qnav.other { border-style: dashed; background: var(--ff-surface);
    border-color: var(--ff-line-2); color: var(--ff-muted); }
.mkqjump .qnav.other em { background: var(--ff-ground-2); color: var(--ff-muted); }

/* =====================================================================
   THE ONE BAND A MARKER TYPES INTO

   Everything else on these screens is read-only: the paper, the script,
   the mark scheme, the engine's working. This is the only interactive
   part, so it is tinted, ruled off, and captioned — never hunted for.

   Where the engine has already produced a mark, ACCEPTING it is one
   action and reads as one: a single primary button carrying the figure,
   with the manual boxes folded behind "Give a different mark". Two
   equally-weighted routes was the puzzle.
   ===================================================================== */
.mkcontrols { margin-top: 14px; padding: 13px 14px; border-radius: var(--ff-r-md);
    border: 1px solid var(--ff-accent-edge); background: var(--ff-accent-soft);
    box-shadow: inset 3px 0 0 var(--ff-accent); }
.mkc-said { min-width: 0; }
.mkc-cap { display: block; font: 700 9.5px var(--sxfont); letter-spacing: .13em;
    text-transform: uppercase; color: var(--ff-accent); }
.mkc-said p { margin: 3px 0 0; font-size: 13px; line-height: 1.55; color: var(--ff-ink-2); }
.mkc-said p b { color: var(--ff-ink); font-variant-numeric: tabular-nums; }
.mkc-accept { display: flex; align-items: center; gap: 12px 16px; flex-wrap: wrap; }
/* Only in the ROW layout. A flex-basis on a child of the column below
   would be read as a height, and stretched the caption to 220px of
   empty tint above the boxes. */
.mkc-accept > .mkc-said { flex: 1 1 220px; }
.mkc-accept .btnrow { margin-left: auto; }
.mkc-manual { display: flex; flex-direction: column; gap: 11px; }
.mkc-fields { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; }
.mkfield { min-width: 0; }
.mkfield.grow { flex: 1 1 220px; }
.mkfield.full { flex: 1 0 100%; }
.mkc-manual > .btnrow { display: flex; gap: 8px; flex-wrap: wrap; }
/* the maximum sits BESIDE the box, not only in a label: it is the one
   number the marker is working to, and it is what makes a silent clamp
   unnecessary */
.mkscore { display: inline-flex; align-items: stretch; border-radius: var(--ff-r-sm);
    border: 1px solid var(--ff-line-2); background: var(--ff-surface); overflow: hidden; }
.mkscore .in { width: 92px; border: 0; border-radius: 0; text-align: right;
    font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 15px; font-weight: 600; }
.mkscore .in:focus { outline: 2px solid var(--ff-accent); outline-offset: -2px; }
/* no spinners: they are 11px targets that change a grade */
.mkscore .in::-webkit-outer-spin-button,
.mkscore .in::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.mkscore .in[type="number"] { -moz-appearance: textfield; appearance: textfield; }
/* A refused save. Pressing "Save mark" with an empty box read as a
   silent no-op on the one action this screen exists for, because the
   only sign was a quiet callout further down a long page. The box that
   has to change is now the box that looks wrong. */
.mkscore:has(.in.is-bad) { border-color: var(--ff-crit); box-shadow: 0 0 0 3px var(--ff-crit-soft); }
.mkscore .in.is-bad { background: var(--ff-crit-soft); }
.mkscore .in.is-bad:focus { outline-color: var(--ff-crit); }
.mkscore-of { display: flex; align-items: center; padding: 0 11px; white-space: nowrap;
    background: var(--ff-sunken); border-left: 1px solid var(--ff-line);
    font: 600 12.5px var(--mono); color: var(--ff-muted); }
.mkrubric { margin: 12px 0; }

/* a settled answer, still on screen. In one-question mode a mistyped
   mark used to make the whole card VANISH — the marker could not see
   what they had awarded, to whom, or amend it. */
.mkmarked { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin-top: 12px;
    padding: 11px 13px; border-radius: var(--ff-r-md); border: 1px solid var(--ff-line);
    background: var(--ff-sunken); }
.mkmarked-v { font: 600 15px var(--mono); font-variant-numeric: tabular-nums; color: var(--ff-ink); }
.mkmarked .btn { margin-left: auto; }

/* the end state: finishing has to be unmistakable, and it has to say
   what "settled" means and what happens next — the word appeared once
   anywhere in marking and was never defined */
.mkdone { display: flex; gap: 14px; align-items: flex-start; flex-wrap: wrap; margin-bottom: 16px;
    padding: 17px 18px; border-radius: var(--ff-r-lg); border: 1px solid var(--ff-good-edge);
    background: var(--ff-good-soft); box-shadow: var(--ff-sh-1); }
.mkdone-ico { flex: 0 0 auto; width: 34px; height: 34px; border-radius: 50%; display: grid;
    place-items: center; background: var(--ff-surface); color: var(--ff-good);
    border: 1px solid var(--ff-good-edge); }
.mkdone-ico svg { width: 18px; height: 18px; }
.mkdone-t { flex: 1 1 300px; min-width: 0; }
.mkdone-t h3 { margin: 0 0 5px; font-size: 15.5px; font-weight: 650; letter-spacing: -.016em;
    color: var(--ff-good); }
.mkdone-t p { margin: 0; font-size: 13px; line-height: 1.65; color: var(--ff-ink-2); }
.mkdone-next { margin-top: 7px !important; padding-top: 7px; border-top: 1px solid var(--ff-good-edge); }
.mkdone .btnrow { flex: 0 0 auto; display: flex; gap: 8px; flex-wrap: wrap; }

/* "the candidate wrote nothing" — deliberately slate and dashed, not
   amber: nothing here is wrong and nothing here is urgent. It used to
   render as an empty box, indistinguishable from a page that failed. */
.mknone { display: flex; gap: 12px; align-items: flex-start; margin-bottom: 12px;
    padding: 13px 15px; border-radius: var(--ff-r-md); border: 1px dashed var(--ff-line-2);
    background: var(--ff-sunken); }
.mknone-ico { flex: 0 0 auto; width: 26px; height: 26px; color: var(--ff-muted); }
.mknone-ico svg { width: 20px; height: 20px; }
.mknone b { display: block; font-size: 13.5px; font-weight: 650; color: var(--ff-ink); }
.mknone span { display: block; font-size: 12.5px; line-height: 1.55; color: var(--ff-muted); margin-top: 2px; }

/* the auto-marker's working, for a person: what was marked, what they
   wrote, what was expected — never the engine's own cell address alone */
.mkwork { margin: 12px 0; }
.mkwork-h { display: block; font: 700 9.5px var(--sxfont); letter-spacing: .13em;
    text-transform: uppercase; color: var(--ff-muted); margin-bottom: 7px; }
.vd-what { flex: 1 1 220px; min-width: 0; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.vd-where { font: 500 11px var(--mono); color: var(--ff-muted); }
.vd-facts { flex: 1 0 100%; display: flex; gap: 6px 14px; flex-wrap: wrap; margin-top: 2px; }
.fact { font: 600 12.5px var(--mono); font-variant-numeric: tabular-nums; color: var(--ff-ink); }
.fact i { font: 600 9.5px var(--sxfont); font-style: normal; letter-spacing: .1em;
    text-transform: uppercase; color: var(--ff-muted); margin-right: 5px; }
.fact.none { color: var(--ff-muted); font-family: var(--sxfont); font-weight: 500; font-size: 12px; }

/* a fold that looks like a control. The model answer is the single most
   important thing on an essay screen and it sat behind a 10px triangle */
.mkfold { margin: 12px 0; }
.mkfold > summary { cursor: pointer; list-style: none; display: inline-flex; align-items: center; gap: 7px;
    padding: 5px 11px 5px 9px; border-radius: 999px; border: 1px solid var(--ff-line-2);
    background: var(--ff-surface); font: 650 11px var(--sxfont); letter-spacing: .06em;
    text-transform: uppercase; color: var(--ff-ink-2); }
.mkfold > summary::-webkit-details-marker { display: none; }
.mkfold > summary::before { content: '▸'; font-size: 10px; color: var(--ff-muted); }
.mkfold[open] > summary::before { content: '▾'; }
.mkfold > summary:hover { border-color: var(--ff-accent); color: var(--ff-accent); }
.mkfold > summary:focus-visible { outline: 2px solid var(--ff-accent); outline-offset: 2px; }

/* ---------------------------------------------------------------------
   THE MATERIALS OF ONE QUESTION, on every screen that shows somebody
   else's paper — the marker's script, mark-by-question, the examiner's
   paper preview, the single-question preview. Deliberately the same
   object as the candidate's own .srctab: a monospace code, the
   examiner's title, and the heading THEY chose above the group. A
   marker and a candidate discussing "E2" must be looking at the same
   document, and the surest way to make that true is to draw it the
   same.
   --------------------------------------------------------------------- */
/* the same list on the examiner's paper preview, where it sits between
   the requirements and the answer surface */
.ppmats { margin: 0 0 14px; }

.mats { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0 0; }
.mats-h { width: 100%; margin: 6px 0 1px; font: 700 9.5px var(--sxfont); letter-spacing: .14em;
    text-transform: uppercase; color: var(--ff-muted); }
.mats-h:first-child { margin-top: 0; }
.mats-row { max-width: 100%; }
.mats-row.doc,
.mats-row > summary { display: inline-flex; align-items: center; gap: 6px; max-width: 100%;
    padding: 6px 11px; border: 1px solid var(--ff-line); border-radius: 999px;
    background: var(--ff-surface); color: var(--ff-ink-2); cursor: pointer;
    font: 600 12.5px var(--sxfont); text-decoration: none; list-style: none; }
.mats-row > summary::-webkit-details-marker { display: none; }
.mats-row.doc em,
.mats-row > summary em { font-style: normal; font-weight: 700; font-family: var(--mono);
    font-size: 11px; color: var(--ff-muted); }
.mats-row.doc span,
.mats-row > summary span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mats-row.doc:hover,
.mats-row > summary:hover { border-color: var(--ff-line-2); background: var(--ff-sunken); color: var(--ff-ink); }
.mats-row.doc:focus-visible,
.mats-row > summary:focus-visible { outline: 2px solid var(--ff-accent); outline-offset: 2px; }
.mats-row[open] > summary { border-color: var(--ff-accent); background: var(--ff-accent-soft); color: var(--ff-accent); }
.mats-row[open] > summary em { color: var(--ff-accent); }
.mats-go { font-style: normal; font-size: 11px; color: var(--ff-muted); }
.mats-caret { font-style: normal; font-size: 10px; color: var(--ff-muted); }
.mats-caret::before { content: '▸'; }
.mats-row[open] .mats-caret { color: var(--ff-accent); }
.mats-row[open] .mats-caret::before { content: '▾'; }
/* An opened written page takes the whole row: a paragraph of a case
   study reads as a paragraph, never as an oversized chip. */
.mats-row[open] { width: 100%; }
.mats-body { margin: 10px 0 4px; padding: 12px 15px; border: 1px solid var(--ff-line);
    border-radius: var(--ff-r-md); background: var(--ff-surface); color: var(--ff-ink-2); }

/* the pinned mark scheme in one-question mode. Its header used to be a
   .card > summary > h2, which no rule in this file ever matched, so it
   printed as a stray line overlapping the card's top border. */
.mkpin-h { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 14px 17px;
    background: var(--ff-sunken); border-bottom: 1px solid var(--ff-line); }
.mkpin:not([open]) > .mkpin-h { border-bottom: 0; }
.mkpin-t { font: 700 11px var(--sxfont); letter-spacing: .13em; text-transform: uppercase;
    color: var(--ff-ink-2); }
.mkpin-fold { margin-left: auto; font: 600 10.5px var(--sxfont); letter-spacing: .06em;
    text-transform: uppercase; color: var(--ff-muted); }
.mkpin-h:hover .mkpin-fold { color: var(--ff-accent); }
.mkscheme-b { margin-top: 14px; padding-top: 13px; border-top: 1px solid var(--ff-line); }
.mkscheme-h { display: block; font: 700 9.5px var(--sxfont); letter-spacing: .13em;
    text-transform: uppercase; color: var(--ff-muted); margin-bottom: 7px; }
/* reference material a marker reads — the model answer, the guide. A
   bare .callout is AMBER, and amber means "you must act": a model
   answer is the opposite of that. Quoted stock, ruled in the accent. */
.mkref { border-left: 3px solid var(--ff-accent-edge); background: var(--ff-surface);
    border-radius: 0 var(--ff-r-md) var(--ff-r-md) 0; padding: 12px 15px;
    font-size: 13.5px; line-height: 1.7; color: var(--ff-ink-2); white-space: pre-line; }

@media (max-width: 1000px) {
    /* The rail becomes a strip above the work rather than a column
       squeezed beside it. */
    .mkshell { grid-template-columns: 1fr; gap: 16px; }
    .mkside { position: static; flex-direction: row; align-items: center; flex-wrap: wrap; gap: 7px;
        padding-bottom: 14px; border-bottom: 1px solid var(--ff-line); }
    .mkside-brand { border: 0; margin: 0; padding: 0 10px 0 0; }
    .mkside-group, .mkside-foot { display: none; }
    .mknav { border-color: var(--ff-line); background: var(--ff-surface); }
    .mkside-day { padding: 7px 11px; }
    .mkside-day b { font-size: 15px; }
}


/* =====================================================================
   THE PAPER PREVIEW, PAGED

   A candidate meets the paper one question at a time, so the examiner's
   preview shows it that way too (client request 2026-08-15). Turning a
   page is Alpine toggling x-show over screens that are all already
   rendered — no round trip, and "Whole paper" simply shows them all.

   Printing ALWAYS gives the whole paper, one question per sheet,
   whichever mode the screen is in: a printed exam paper that stopped
   after question 1 would be worse than useless.
   ===================================================================== */
.ppseg { width: 236px; }
.ppseg button { white-space: nowrap; }

.ppnav { position: sticky; top: calc(var(--sxtop) + 8px); z-index: 80; margin-bottom: 16px;
    padding: 10px 14px; border: 1px solid var(--ff-line); border-radius: var(--ff-r-md);
    background: var(--ff-veil);
    -webkit-backdrop-filter: saturate(1.4) blur(10px); backdrop-filter: saturate(1.4) blur(10px);
    box-shadow: var(--ff-sh-1); }
.ppnav-row { display: flex; align-items: center; gap: 12px; }
.ppnav-where { flex: 1; min-width: 0; display: flex; flex-direction: column; text-align: center; }
.ppnav-where b { font-size: 13.5px; font-weight: 650; letter-spacing: -.012em; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; }
.ppnav-where small { font-size: 11.5px; color: var(--ff-muted); margin-top: 1px; }
.ppnav-strip { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 10px;
    padding-top: 10px; border-top: 1px solid var(--ff-line); }
.ppnav-sum { font-size: 10px; }
/* the section a run of questions belongs to, in the palette itself —
   an exam paper's numbering means little without it */
.ppnav-sec { font: 700 10px var(--sxfont); letter-spacing: .1em; text-transform: uppercase;
    color: var(--ff-muted); background: var(--ff-ground-2); padding: 5px 9px; border-radius: 6px;
    cursor: pointer; margin-left: 5px; white-space: nowrap; }
.ppnav-sec:first-child { margin-left: 0; }
.ppnav-sec:hover, .ppnav-sec.here { color: var(--ff-accent); background: var(--ff-accent-soft); }
.ppnav-sec.here { box-shadow: inset 0 0 0 1px var(--ff-accent-edge); }
.ppnav-sec:focus-visible { outline: 2px solid var(--ff-accent); outline-offset: 1px; }

/* which section this question sits in, on the question's own header */
.ppsec { font: 700 10px var(--sxfont); letter-spacing: .09em; text-transform: uppercase;
    color: var(--ff-accent); background: var(--ff-accent-soft); padding: 3px 8px; border-radius: 5px;
    margin-left: 8px; }

.ppfoot { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 4px; }

@media print {
    .topbar, .ppnav, .ppfoot, .ppseg, .head .btnrow, .runbar { display: none !important; }
    .ppscreens, .ppscreen { display: block !important; }
    .ppscreen { page-break-after: always; break-after: page; }
    .ppscreen:last-child { page-break-after: auto; break-after: auto; }
    .card, .qcard { box-shadow: none; break-inside: avoid; }
}

/* =====================================================================
   The product's dialog (public/js/sx-dialog.js).

   Replaces every native alert/confirm in the product: Livewire's
   wire:confirm, and Filament's unsaved-changes warning — which without
   SPA mode is the browser's own unstylable "Leave site?" prompt.

   It lives HERE rather than in app.css or in AdminPanelProvider because
   it is needed by both shells and by the panel: the exam runner's "End
   exam", the workbook editor inside Filament, the marking screens, the
   updater.

   Local variables kept for compatibility, but they now read from the
   theme tokens — which are themselves re-declared for the panel's dark
   mode above, so this needs no .dark block of its own any more.
   ===================================================================== */
.sxdlg {
    --dlg-card: var(--ff-surface); --dlg-line: var(--ff-line); --dlg-ink: var(--ff-ink);
    --dlg-muted: var(--ff-muted); --dlg-tint: var(--ff-sunken);
    position: fixed; inset: 0; z-index: 90; display: none;
    align-items: center; justify-content: center; padding: 24px;
    background: var(--ff-scrim);
    -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
}
.sxdlg.on { display: flex; }
.sxdlg-sheet {
    width: 100%; max-width: 30rem; overflow: hidden;
    border-radius: var(--ff-r-lg); background: var(--dlg-card);
    border: 1px solid var(--dlg-line); color: var(--dlg-ink);
    box-shadow: var(--ff-sh-3);
    font-family: var(--sxfont);
}
.sxdlg-ribbon { height: 3px; background: var(--ff-accent); }
.sxdlg.is-danger .sxdlg-ribbon { background: var(--ff-crit); }
.sxdlg-head { display: flex; gap: 14px; align-items: flex-start; padding: 20px 22px 18px; }
.sxdlg-mark {
    flex: none; display: grid; place-items: center; width: 36px; height: 36px;
    border-radius: 999px; color: var(--ff-flag); background: var(--ff-flag-soft);
    border: 1px solid var(--ff-flag-edge);
}
.sxdlg.is-danger .sxdlg-mark { color: var(--ff-crit); background: var(--ff-crit-soft); border-color: var(--ff-crit-edge); }
.sxdlg-mark svg { width: 20px; height: 20px; }
.sxdlg-head h2 { font-size: 16px; font-weight: 650; letter-spacing: -.018em; margin: 0; }
.sxdlg-head p { margin: 5px 0 0; font-size: 13.5px; line-height: 1.55; color: var(--dlg-muted); }
.sxdlg-foot {
    display: flex; flex-wrap: wrap; gap: 10px; justify-content: flex-end;
    padding: 14px 22px 18px; border-top: 1px solid var(--dlg-line); background: var(--dlg-tint);
}
.sxdlg-btn {
    padding: 8px 15px; border-radius: var(--ff-r-sm); font-size: 13px; font-weight: 600;
    cursor: pointer; background: var(--ff-accent); color: var(--ff-on-accent); border: 1px solid transparent;
    font-family: inherit; box-shadow: var(--ff-inset), var(--ff-sh-2);
}
.sxdlg-btn:hover { background: var(--ff-accent-hi); }
/* The destructive answer is the quiet one and never holds focus, so
   Enter can never be the key that throws somebody's work away. */
.sxdlg-btn.is-go {
    background: transparent; color: var(--ff-crit);
    border-color: var(--dlg-line); font-weight: 500; box-shadow: none;
}
.sxdlg-btn.is-go:hover { background: var(--ff-crit-soft); }
.sxdlg-btn:focus-visible { outline: 2px solid var(--ff-accent); outline-offset: 2px; }
@media (prefers-reduced-motion: no-preference) {
    .sxdlg.on .sxdlg-sheet { animation: sxdlg-in .16s ease-out both; }
    @keyframes sxdlg-in { from { opacity: 0; transform: translateY(6px) scale(.99); } }
}
@media print { .sxdlg { display: none !important; } }

/* =====================================================================
   Toasts for the non-panel half of the product (public/js/sx-notify.js).

   Filament gives the panel a toast stack and a global failure handler;
   the exam runner, marking, the workbook editor and the student pages
   had neither, so a 500 or a dropped connection produced a button that
   stopped spinning and nothing else.

   Bottom-right, above everything except the loader veil (z 90) and the
   dialog. Tones follow the house rule: amber is "you must act", red is
   reserved for what is genuinely wrong, green is quiet.
   ===================================================================== */
.sxtoasts {
    position: fixed; right: 16px; bottom: 16px; z-index: 80;
    display: flex; flex-direction: column; gap: 10px;
    width: min(390px, calc(100vw - 32px)); pointer-events: none;
}
.sxtoast {
    pointer-events: auto;
    display: flex; gap: 11px; align-items: flex-start;
    padding: 12px 13px; border-radius: var(--ff-r-md);
    background: var(--ff-surface); border: 1px solid var(--ff-line);
    border-inline-start: 3px solid var(--ff-muted);
    box-shadow: var(--ff-sh-2);
    font-family: var(--sxfont);
}
.sxtoast svg { flex: none; width: 18px; height: 18px; margin-top: 1px; color: var(--ff-muted); }
.sxtoast-body { flex: 1 1 auto; min-width: 0; }
.sxtoast-title { font-size: 13.5px; font-weight: 650; color: var(--ff-ink); letter-spacing: -.012em; }
.sxtoast-text { margin-top: 3px; font-size: 12.5px; line-height: 1.55; color: var(--ff-ink-2); }
.sxtoast-x {
    flex: none; width: 22px; height: 22px; border: 0; border-radius: 6px;
    background: none; color: var(--ff-muted); font-size: 17px; line-height: 1; cursor: pointer;
}
.sxtoast-x:hover { background: var(--ff-ground-2); color: var(--ff-ink); }
.sxtoast.is-ok { border-inline-start-color: var(--ff-good); }
.sxtoast.is-ok svg { color: var(--ff-good); }
.sxtoast.is-warn { border-inline-start-color: var(--ff-flag); }
.sxtoast.is-warn svg { color: var(--ff-flag); }
.sxtoast.is-bad { border-inline-start-color: var(--ff-crit); }
.sxtoast.is-bad svg { color: var(--ff-crit); }
/* The emphasized SAVE confirmation (sx-notify `variant: 'save'`, raised
   by the workbook builder's clean save — the one green allowed to speak
   up, because the quiet corner toast went unseen from a long workbook).
   Larger title, the whole card edged in the success colour, a deeper
   shadow, and the caller holds it ~7s. Same surface, same glyph, no
   extra motion — obvious, not carnival. */
.sxtoast.is-save {
    padding: 15px 16px;
    border-color: var(--ff-good-edge);
    border-inline-start: 5px solid var(--ff-good);
    box-shadow: var(--ff-sh-3);
}
.sxtoast.is-save svg { width: 21px; height: 21px; margin-top: 0; }
.sxtoast.is-save .sxtoast-title { font-size: 15px; letter-spacing: -.014em; }
.sxtoast.is-save .sxtoast-text { font-size: 13px; }
@media (prefers-reduced-motion: no-preference) {
    .sxtoast { animation: sxtoast-in .18s ease-out both; }
    .sxtoast.is-going { animation: sxtoast-out .2s ease-in both; }
    @keyframes sxtoast-in { from { opacity: 0; transform: translateY(8px); } }
    @keyframes sxtoast-out { to { opacity: 0; transform: translateY(6px); } }
}
@media print { .sxtoasts { display: none !important; } }

/* The runbar's client-side failure state. A request that 500s or never
   arrives leaves NO server state to read, so this one has to be driven
   from the browser — see sx-notify.js. */
body.sx-save-failed .saver .sx-saveguard { display: inline !important; }
body.sx-save-failed .saver > span:not(.sx-saveguard) { display: none !important; }
.saver .sx-saveguard { display: none; }

/* Same contract for the secure-mode warning overlay, which sits ON TOP
   of the runbar: it must never assert "Your answers are saved." while
   the last write failed. Both sentences are in the DOM; whichever half
   of the guard knows — .sx-unsaved from PHP, body.sx-save-failed from
   the browser — swaps them. */
.ovl-unsaved { display: none; }
.sx-unsaved .ovl-saved,
body.sx-save-failed .ovl-saved { display: none; }
.sx-unsaved .ovl-unsaved,
body.sx-save-failed .ovl-unsaved { display: block; color: var(--ff-crit); font-weight: 600; }

/* The workbook editor's status chip. Tone follows the house rule —
   green is done, amber is "you must act", red is genuinely wrong — and
   it must live in tools.css because that editor is a PANEL page. */
.wbstatus.is-ok { background: var(--ff-good-soft); color: var(--ff-good); }
.wbstatus.is-warn { background: var(--ff-flag-soft); color: var(--ff-flag); }
.wbstatus.is-bad { background: var(--ff-crit-soft); color: var(--ff-crit); }

/* NOTE on motion. The mockup ships a blanket
   `prefers-reduced-motion: reduce { * { animation-duration:.001ms !important } }`.
   That rule is deliberately NOT ported: `*` under body.sx reaches the
   page loader, whose reduce state is a designed thing (a dotted
   printer's leader, not a stopped bar), and an !important here would
   overwrite it — the exact regression documented in CLAUDE.md. Motion
   in this file is confined to `--ff-t` hover transitions and to the two
   keyframes that already sit inside `prefers-reduced-motion:
   no-preference`; nothing loops, nothing moves on its own. */

/* ---- The Univer workbook surface (the builder's grid when
   sudoexam.workbook_ui = univer). In tools.css for the standing reason:
   the workbook editor is a PANEL page and the panel cannot read Vite's
   manifest — a rule left in the bundle simply does not exist there. */
/* The workbook editor declares itself a desktop screen (audit
   2026-08-20). Below 900px the grid was squeezed to about two pixels and
   the rail's panels edited a spreadsheet nobody could see. Rather than
   half-build a mobile spreadsheet, the editor says what it needs, says
   nothing is lost, and offers the way back.

   A media query, not JavaScript: the broken layout must not be painted
   first and then swapped. */
.wbsmall { display: none; }
/* the editor's two columns — grid on the left, rail on the right */
.wbcols { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 18px; align-items: start; }

/* THREE COLUMNS WHEN THERE IS ROOM — navigation, document, properties
   (client, 2026-09-10: "why not this question section be move to the
   right side where the nav menu are so that in a single screen without
   scrolling I can see more?").

   The block list is NAVIGATION — it is what the panel's own sidebar
   would be if this screen had one — so it belongs beside the sheet, not
   stacked on top of it. As a band it cost ~300px of height on every
   question and pushed the spreadsheet off the first screen; as a column
   it costs none, and the layout lands on the arrangement every editor
   uses: what you are working ON at the left, the document in the middle,
   what you have SELECTED at the right.

   The threshold is the sheet's own width, not the window's. Three
   columns need ~1560px of CONTENT to leave the grid a workable middle,
   and the panel's sidebar takes ~290 of the window — so the plain page
   waits for 1860px while focus mode, which has already given that back,
   switches at 1180. Below either, .wbnav's `grid-column: 1 / -1` puts it
   back on top and nothing has changed.

   As a column it becomes a rail: it stays put while the sheet scrolls
   beside it, it scrolls itself when the question is long, and its
   label/items row stacks rather than spending 96px of 268 on a label.
   The two blocks below are deliberately duplicated rather than merged
   with a selector list — a media query cannot be OR'd, and the sticky
   behaviour must never switch on while the list is still a full-width
   band, which is what a shared rule outside them would do. */
@media (min-width: 1860px) {
    .wbcols { grid-template-columns: 268px minmax(0, 1fr) 320px; }
    .wbcols > .wbnav {
        grid-column: auto; margin-bottom: 0; min-width: 0;
        position: sticky; top: calc(var(--sx-panel-top, 0px) + 70px);
        max-height: calc(100vh - var(--sx-panel-top, 0px) - 90px); overflow: auto;
    }
    .wbcols > .wbnav .wbnav-row { grid-template-columns: minmax(0, 1fr); gap: 7px; }
    .wbcols > .wbnav .wbnav-label { padding-top: 0; }
    /* The rail scrolls as a whole now, so the inner cap would only
       produce a second scrollbar inside the first. */
    .wbcols > .wbnav .wbblocks { max-height: none; overflow: visible; }
}

/* A block row was built for a wide band: badge, name, a "1 tab · 3 marks
   from cells" note and three action buttons, ALL ON ONE LINE. At 268px
   that line collides — measured in Chrome, the name ran underneath the
   icons. The row becomes two lines instead: badge + name + actions, then
   the note.

   A CONTAINER query, not a media query, because the thing that decides
   this is the RAIL'S width and nothing else — the same rail is 268px in
   focus mode and 1116px as a band in the same window. It is safe here:
   `container-type: inline-size` makes the element a containing block for
   absolutely-positioned descendants, and the block list has none (the
   drag cues are box-shadows, the rename boxes are inline). Where
   container queries are unsupported the rules simply do not match and
   the row keeps the one-line layout it has always had. */
.wbnav { container-type: inline-size; container-name: wbnav; }
@container wbnav (max-width: 420px) {
    /* The OUTER row keeps its single line — handle | head | actions —
       or the handle and the buttons each claim a line of their own and a
       five-block question is taller than the band ever was. Only the
       head wraps, dropping the note under the name. */
    .wbblock { flex-wrap: nowrap; align-items: flex-start; padding: 0 6px 0 0; }
    .wbblock-head { flex-wrap: wrap; row-gap: 2px; padding: 7px 4px 7px 2px; }
    .wbblock-name { flex: 1 1 auto; }
    .wbblock-sub { flex: 1 1 100%; line-height: 1.4; white-space: normal; }
    .wbblock-acts { align-self: center; }
    .wbchip-drag, .wbblock > .wbchip-drag { padding-top: 10px; }
    /* The lane note explaining that one answer sheet serves every task
       is three lines of a 268px rail. It stays — it is the sentence that
       stops an examiner hunting for per-task tabs — but at rail size. */
    .wbgroup-note { font-size: 10.5px; line-height: 1.45; }
    .wbnav-items { gap: 6px; }
}
@media (min-width: 1180px) {
    body:has(.wbfocus) .wbcols { grid-template-columns: 268px minmax(0, 1fr) 320px; }
    body:has(.wbfocus) .wbcols > .wbnav {
        grid-column: auto; margin-bottom: 0; min-width: 0;
        position: sticky; top: calc(var(--sx-panel-top, 0px) + 70px);
        max-height: calc(100vh - var(--sx-panel-top, 0px) - 90px); overflow: auto;
    }
    body:has(.wbfocus) .wbcols > .wbnav .wbnav-row { grid-template-columns: minmax(0, 1fr); gap: 7px; }
    body:has(.wbfocus) .wbcols > .wbnav .wbnav-label { padding-top: 0; }
    body:has(.wbfocus) .wbcols > .wbnav .wbblocks { max-height: none; overflow: visible; }
}

@media (max-width: 899px) {
    .wbedit > *:not(.wbsmall):not(.wbcols) { display: none; }
    /* The spreadsheet's column keeps a REAL BOX, off screen. A canvas
       engine handed a 0x0 container lays its page out at zero width and
       Univer logs "The column width is less than 0…" — an error in the
       console of a product that has none. `visibility: hidden` is the
       load-bearing half: it takes the whole subtree out of the tab order
       and out of the accessibility tree (which `left: -20000px` alone
       does not), while leaving the box the engine needs to measure. */
    .wbedit > .wbcols { position: absolute; left: -20000px; top: 0; width: 1080px;
        visibility: hidden; pointer-events: none; }
    .wbsmall { display: block; max-width: 560px; margin: 8px auto; padding: 22px 24px;
        background: var(--ff-surface); border: 1px solid var(--ff-line);
        border-radius: var(--ff-r-lg, 14px); box-shadow: var(--ff-sh-1); }
    .wbsmall h2 { margin: 0 0 10px; font-size: 17px; font-weight: 650; letter-spacing: -.02em;
        color: var(--ff-ink); }
    .wbsmall p { margin: 0 0 10px; font-size: 13.5px; line-height: 1.6; color: var(--ff-ink-2); }
    .wbsmall .btn { margin-top: 4px; }
}

.wbu .legend { margin-bottom: 10px; }
/* THE SHEET IS THE DOCUMENT, so it takes the room the chrome gave back.
   It used to be a flat 620px — the smallest important object on an
   1861px page — with a media query that SHRANK it to 480px on exactly
   the 768px laptops that could least afford it. One clamp instead, so
   the grid grows with the window at every size and is never smaller
   than it was: 940px tall screen -> 680, 1080p -> 820, 768 -> 508. */
.wbu-host {
    height: clamp(420px, calc(100vh - 260px), 980px); min-height: 380px;
    border: 1px solid var(--ff-line); border-radius: var(--ff-r-md, 10px);
    overflow: hidden; background: #fff;
    color-scheme: light;
}
/* The short-screen override is GONE on purpose: it outranked any clamp
   and silently undid the geometry above on the machines that needed it
   most. Do not put it back. */

/* The question, folded beside the grid. Building an answer template
   means constantly checking what was asked; this keeps the real stem
   (tables included) one click away instead of one screen away. */
.wbq { border: 1px solid var(--ff-line); border-radius: var(--ff-r-md, 10px);
    background: var(--ff-surface); box-shadow: var(--ff-sh-1); }
.wbq summary { display: flex; align-items: center; gap: 10px; padding: 11px 16px;
    cursor: pointer; list-style: none; }
.wbq summary::-webkit-details-marker { display: none; }
.wbq-head { flex: none; font: 700 10.5px var(--sxfont); letter-spacing: .12em;
    text-transform: uppercase; color: var(--ff-muted); }
.wbq-peek { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font: 400 12.5px var(--sxfont); color: var(--ff-ink-2); }
.wbq[open] .wbq-peek { visibility: hidden; }
.wbq-caret { flex: none; width: 8px; height: 8px; margin-right: 2px;
    border-right: 2px solid var(--ff-muted); border-bottom: 2px solid var(--ff-muted);
    transform: rotate(45deg); transition: transform .12s; }
.wbq[open] .wbq-caret { transform: rotate(-135deg); }
.wbq summary:hover .wbq-head { color: var(--ff-ink); }
.wbq-body { padding: 4px 16px 14px; border-top: 1px solid var(--ff-line); }
.wbq-body .stem { padding-top: 10px; }
.wbq-req { margin-top: 12px; padding-top: 10px; border-top: 1px dashed var(--ff-line-2); }
.wbq-label { display: block; margin-bottom: 4px; font: 700 10.5px var(--sxfont);
    letter-spacing: .12em; text-transform: uppercase; color: var(--ff-muted); }

/* A quiet tip: guidance that sits beside a control without shouting.
   Muted, small, marked only by a bordered lead-in badge. As a flex
   child it takes a real basis so its sentence wraps as a paragraph,
   never word-by-word between its neighbours. */
.sx-tip { font: 400 11.5px var(--sxfont); color: var(--ff-muted); line-height: 1.5; }
.sx-tip::before { content: 'Tip'; margin-right: 6px; padding: 1px 5px;
    border: 1px solid var(--ff-line); border-radius: 5px;
    font: 700 9px var(--sxfont); letter-spacing: .08em; text-transform: uppercase;
    color: var(--ff-muted); }
.sx-tip b { font-weight: 600; color: var(--ff-ink-2); }
p.sx-tip { display: block; }
.wbtools .sx-tip { flex: 1 1 240px; max-width: 460px; }

/* The guide's miniature of the native sheet bar. */
.gtabs { display: flex; align-items: flex-end; gap: 2px; padding: 8px 10px 0;
    border: 1px solid var(--ff-line); border-radius: 8px; background: var(--ff-sunken); }
.gtab { padding: 6px 12px 7px; border: 1px solid var(--ff-line); border-bottom: 0;
    border-radius: 7px 7px 0 0; background: var(--ff-surface); color: var(--ff-muted);
    font: 600 11.5px var(--sxfont); }
.gtab.on { color: var(--ff-accent); border-color: var(--ff-accent-edge);
    box-shadow: inset 0 -2px 0 var(--ff-accent); }
.gtab.add { border-radius: 7px; margin-bottom: 6px; padding: 3px 9px; font-weight: 700; }

/* ---- Panel menu scrollbar (client, 2026-08-20: "thinner or hidden") ----
   In tools.css rather than AdminPanelProvider::houseStyle() because the
   provider was mid-edit by a running workflow when this landed; the panel
   links this file on every page (HEAD_END hook), so it reaches the
   sidebar the same way. The bar is invisible until the pointer is over
   the menu and thin when it shows — the menu itself stays scrollable.
   Chrome 121+ honours scrollbar-width/color and then IGNORES the
   ::-webkit-scrollbar rules; older engines read only the webkit ones.
   Both are needed. */
.fi-sidebar-nav { scrollbar-width: thin; scrollbar-color: transparent transparent; }
.fi-sidebar-nav:hover { scrollbar-color: rgb(100 116 139 / .35) transparent; }
.fi-sidebar-nav::-webkit-scrollbar { width: 5px; }
.fi-sidebar-nav::-webkit-scrollbar-track { background: transparent; }
.fi-sidebar-nav::-webkit-scrollbar-thumb { background: transparent; border-radius: 999px; }
.fi-sidebar-nav:hover::-webkit-scrollbar-thumb { background: rgb(100 116 139 / .35); }
.dark .fi-sidebar-nav:hover { scrollbar-color: rgb(148 163 184 / .3) transparent; }
.dark .fi-sidebar-nav:hover::-webkit-scrollbar-thumb { background: rgb(148 163 184 / .3); }

/* =====================================================================
   THE QUESTION BUILDER'S BLOCK LIST (2026-08-23)

   One question is one flat ordered list of blocks — the client's own
   recap: "lets treat all these as 1 question. requirement is also the
   same as a block". Written page, PDF, requirement, answer sheet: one
   palette, everything draggable.

   Every class here is in tools.css and NOT in app.css, and that is not
   a preference. The Filament panel cannot read Vite's manifest, so a
   rule left in the bundle simply does not exist on this screen — and
   the symptom is silent rather than loud.

   The pieces panel replaces the old PAGES row inside .wbnav, so it
   inherits that band's chrome and adds only what a GROUP needs: a rule
   down the left edge, a header naming the tab the candidate will see,
   and a quiet line saying so.
   ===================================================================== */
/* THE LIST SCROLLS INSTEAD OF GROWING. A question with five blocks and a
   task pushed the spreadsheet ~400px down the page and off the first
   screen entirely — which is why the client, looking at exactly that
   question, still called the screen messy after the first pass. Capped,
   the spine stays fully visible and costs the same whether the question
   has two blocks or twenty. `overscroll-behavior` stops a flick inside
   the list from carrying on and scrolling the page underneath it. */
.wbblocks { display: flex; flex-direction: column; gap: 6px; min-width: 0; width: 100%;
    max-height: 232px; overflow-y: auto; overscroll-behavior: contain; padding-right: 2px; }
.wbblocks::-webkit-scrollbar { width: 9px; }
.wbblocks::-webkit-scrollbar-thumb { background: var(--ff-line-2); border-radius: 999px;
    border: 3px solid var(--ff-surface); }
.wbblocks::-webkit-scrollbar-track { background: transparent; }
/* On a tall screen there is room to show more of it before scrolling. */
@media (min-height: 1000px) { .wbblocks { max-height: 340px; } }

/* one block, as one keyed root element of the SAME shape whether or not
   it is selected — an unkeyed list is morphed positionally, and a
   differently-structured "current" row leaves the highlight behind on
   whichever row happened to occupy the old slot */
.wbblock { display: flex; align-items: center; gap: 6px; min-width: 0;
    border: 1px solid var(--ff-line); border-radius: var(--ff-r-md, 10px);
    background: var(--ff-surface); color: var(--ff-ink-2);
    font: 600 12.5px var(--sxfont); padding: 0 8px 0 0; }
.wbblock:hover { border-color: var(--ff-line-2); background: var(--ff-sunken); }
.wbblock.on { border-color: var(--ff-accent); background: var(--ff-accent-soft);
    color: var(--ff-accent); box-shadow: inset 2px 0 0 var(--ff-accent); }
.wbblock.is-sheet { border-style: dashed; }

.wbblock-head { display: flex; align-items: center; gap: 8px; min-width: 0; flex: 1 1 auto;
    padding: 9px 4px 9px 2px; border: 0; background: none; color: inherit; font: inherit;
    cursor: pointer; text-align: left; }
.wbblock-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wbblock-sub { flex: none; font: 400 11px var(--sxfont); color: var(--ff-muted); }
.wbblock.on .wbblock-sub { color: var(--ff-accent); opacity: .85; }

.wbblock-badge { flex: none; font: 700 9.5px var(--sxfont); letter-spacing: .1em;
    text-transform: uppercase; padding: 2px 6px; border-radius: 4px;
    background: var(--ff-ground-2); color: var(--ff-muted); }
.wbblock-badge.is-req { background: var(--ff-accent-soft); color: var(--ff-accent); }
.wbblock-badge.is-sheet { background: var(--ff-flag-soft); color: var(--ff-flag); }

.wbblock-acts { flex: none; display: inline-flex; align-items: center; gap: 1px; }

/* a task and everything filed under it, drawn as one object so the
   consequence of dragging the task ("it takes its group with it") is
   visible BEFORE the drop rather than discovered after it */
.wbgroup { border: 1px solid var(--ff-line); border-radius: var(--ff-r-md, 10px);
    background: var(--ff-sunken); padding: 8px 8px 8px 0; margin: 2px 0;
    display: flex; align-items: stretch; gap: 8px; min-width: 0; }
.wbgroup-rule { flex: 0 0 3px; border-radius: 999px; background: var(--ff-accent);
    opacity: .55; margin-left: 8px; }
.wbgroup-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.wbgroup-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
    font: 700 10px var(--sxfont); letter-spacing: .13em; text-transform: uppercase;
    color: var(--ff-muted); padding: 0 2px; }
.wbgroup-head b { font: 600 12.5px var(--sxfont); letter-spacing: 0; text-transform: none;
    color: var(--ff-ink); }
.wbgroup-note { margin-left: auto; font: 400 11px var(--sxfont); letter-spacing: 0;
    text-transform: none; color: var(--ff-muted); }
.wbgroup-marks { font: 400 11px var(--sxfont); letter-spacing: 0; text-transform: none;
    color: var(--ff-muted); }

/* the insertion cue, painted on the slot the block would land on. The
   list is vertical, so the rule is horizontal — the pages row's cue is
   the same object turned ninety degrees. */
.wbblock-slot { display: block; min-width: 0; }
.wbblock-slot.wbdragging { opacity: .45; }
.wbblock-slot.wbdrop-before { box-shadow: 0 -3px 0 0 var(--ff-accent); border-radius: 2px; }
.wbblock-slot.wbdrop-after { box-shadow: 0 3px 0 0 var(--ff-accent); border-radius: 2px; }
/* a drop the projection cannot represent: an answer sheet sits at the
   foot of its task, so it can move BETWEEN tasks and not within one */
.wbblock-slot.wbdrop-refused { box-shadow: 0 0 0 2px var(--ff-bad); border-radius: 8px; }
.wbblock-slot.wbdrop-refused .wbblock { border-color: var(--ff-bad); }

/* the two pinned slots — the question's own Overview and the shared
   instruction. Pinned means un-draggable and un-deletable, and it has
   to LOOK it or somebody will try. */
.wbpinned { display: inline-flex; align-items: center; gap: 8px;
    font: 700 10px var(--sxfont); letter-spacing: .13em; text-transform: uppercase;
    color: var(--ff-muted); padding: 7px 12px; border-radius: 999px;
    border: 1px solid var(--ff-line); background: var(--ff-ground-2); }
.wbpinned.on { border-color: var(--ff-accent); background: var(--ff-accent-soft); color: var(--ff-accent); }
.wbpinned .ico-pin { font-size: 11px; opacity: .6; letter-spacing: 0; }

/* the stage, while a READING block is open.

   Univer is parked, never unmounted: the host keeps a real box off
   screen at a real width. `visibility: hidden` is the load-bearing
   half — it takes the subtree out of the tab order and the
   accessibility tree, which `left: -20000px` alone does not — while
   leaving the engine something to measure. NEVER `display: none`:
   a canvas engine handed a 0x0 container logs "The column width is
   less than 0" and lays its page out at zero width. */
.wbstage-parked { position: absolute; left: -20000px; top: 0; width: 1080px;
    visibility: hidden; pointer-events: none; }

/* The hosted Filament fields. The panel's own annotation styling has to
   win here: tools.css ships a chip-style .sx-tip for tool screens, and
   this is the first screen where Filament fields and that chip share a
   page. The panel variant is already scoped to .fi-in-text /
   .fi-sc-component in AdminPanelProvider — this only stops the tool
   chip reaching INTO a Filament wrapper. */
.wbstage .fi-in-text .sx-tip::before,
.wbstage .fi-sc-component .sx-tip::before { content: none; }
/* The editor for the block the examiner just clicked. It is the far end
   of a chip at the top of the page, so it says which chip it is (the
   same badge, the same name) and carries the accent edge the selected
   chip carries — one object, not two screens sharing a page. */
.wbstage { min-width: 0; scroll-margin-top: 74px; }
.wbstage-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    margin: 0 0 12px; padding: 9px 12px; border-left: 3px solid var(--ff-accent);
    border-radius: 0 var(--ff-r-sm) var(--ff-r-sm) 0; background: var(--ff-accent-soft); }
/* The block's own NAME, so the head answers "which one am I editing?" —
   it used to read "This block" whichever block that was. */
.wbstage-head h3 { margin: 0; font: 650 14.5px var(--sxfont); letter-spacing: -.01em;
    text-transform: none; color: var(--ff-ink); }
.wbstage-head .wbstage-what { flex: 1 1 200px; font: 400 12.5px var(--sxfont); color: var(--ff-ink-2); }
.wbstage-head .wbblock-badge { background: var(--ff-surface); color: var(--ff-accent); }

/* The question's own spine — the blocks that belong to the question
   rather than to any one task, and which every tab therefore shows. It
   is a lane like the others so the chip markup is written once, but it
   is not a GROUP: no rule, no border, nothing implying a tab. */
.wbgroup.is-spine { border: 0; background: transparent; padding: 0; margin: 0; }
.wbgroup.is-spine > .wbgroup-rule { display: none; }

/* ============================================================
   THE CONNECTION CHECK — the "Before you begin" gate
   (client decision, 2026-08-22)

   It has to look like a MEASUREMENT and not like an alarm. Four
   verdicts, four tones, and the neutral one while it is running is
   deliberately the calmest thing on the strip: a candidate must not
   be frightened by a check that has not finished.

   Tones come from the product's own semantic variables, so the strip
   agrees with the callouts, the runbar and the marking verdicts by
   construction and in dark mode without a second block.

   Nothing here is a button that decides anything: the only control is
   "Check again", and the offline pointer scrolls to a real control
   further down the page rather than being one itself.
   ============================================================ */
.conncheck { border: 1px solid var(--line); border-left: 4px solid var(--line);
    background: var(--soft); border-radius: 10px; padding: 12px 14px; margin: 0 0 14px; }
/* The traffic light (client, 2026-09-08): good is green, usable-but-
   slow is amber, barely usable or absent is red. Four verdicts, three
   colours — "poor" and "bad" are both the red the candidate should
   act on. */
.conncheck.is-good { border-color: var(--ff-good-edge); border-left-color: var(--ff-good);
    background: var(--ff-good-soft); }
.conncheck.is-fair { border-color: var(--ff-flag-edge); border-left-color: var(--ff-flag);
    background: var(--ff-flag-soft); }
.conncheck.is-poor,
.conncheck.is-bad  { border-color: var(--ff-crit-edge); border-left-color: var(--ff-crit);
    background: var(--ff-crit-soft); }

.conncheck-h { display: flex; align-items: center; gap: 9px; }
.conncheck-h b { font: 700 13.5px var(--sxfont); color: var(--ink); }
.conncheck.is-good .conncheck-h b { color: var(--ff-good); }
.conncheck.is-fair .conncheck-h b { color: var(--ff-flag); }
.conncheck.is-poor .conncheck-h b,
.conncheck.is-bad  .conncheck-h b { color: var(--ff-crit); }
.conncheck-again { margin-left: auto; }

/* The one moving thing, and it is the SAME object as the page loader's
   hairline: a dot that breathes while a measurement is being taken, and
   stops dead the moment there is an answer. Never a spinner and never a
   tick — see the loader's note about a success glyph animating during a
   state that has not succeeded. */
.conncheck-dot { width: 9px; height: 9px; border-radius: 50%; flex: none;
    background: var(--slate); }
.conncheck.is-good .conncheck-dot { background: var(--ff-good); }
.conncheck.is-fair .conncheck-dot { background: var(--ff-flag); }
.conncheck.is-poor .conncheck-dot,
.conncheck.is-bad  .conncheck-dot { background: var(--ff-crit); }

@media (prefers-reduced-motion: no-preference) {
    .conncheck:not([class*="is-"]) .conncheck-dot,
    .gate-conn:not([class*="is-"]) .conncheck-dot { animation: sxconnpulse 1.5s ease-in-out infinite; }
}
@keyframes sxconnpulse { 0%, 100% { opacity: .32; } 50% { opacity: 1; } }

.conncheck-say { font: 400 13px/1.6 var(--sxfont); color: var(--slate); margin: 6px 0 0; }
.conncheck-num { font: 600 12px/1.6 var(--sxfont); color: var(--slate); margin: 5px 0 0;
    font-variant-numeric: tabular-nums; }
.conncheck-foot { font: 400 11.5px/1.55 var(--sxfont); color: var(--slate); opacity: .82;
    margin: 8px 0 0; }
.conncheck-do { margin: 10px 0 0; padding: 10px 12px; border-radius: 8px;
    background: var(--ff-surface); border: 1px solid var(--line); }
.conncheck-do p { font: 400 13px/1.65 var(--sxfont); color: var(--slate); margin: 0 0 9px; }

/* Where the poor verdict sends them. A ring, not a colour change: the
   block already has its own meaning and repainting it would say
   something about the offline route that is not true. */
#sx-offline-route.is-pointed { outline: 2px solid var(--ff-flag); outline-offset: 6px;
    border-radius: 8px; }

/* ============================================================
   THE OFFLINE READ-BACK'S FIGURES

   What the uploaded answer book actually gave up, printed under
   each question of the read-back — not a count of it. A worksheet
   candidate's whole anxiety is that ONE number was misread, and
   "15 cells filled" cannot answer that; the read-back exists so it
   can be answered before anything is written to the paper.

   It sits inside the .vd verdict row as a full-width third child,
   which is why .vd carries flex-wrap. The list is a responsive grid
   and is capped in HEIGHT rather than in length, so a fifteen-cell
   worksheet is read at a glance and a two-hundred-cell one scrolls
   instead of burying the Apply button below three screens of
   figures. The count of what could not be listed is printed under
   it — a read-back that quietly hides part of itself is the same
   failure as one that only counts.

   Quiet on purpose: this is evidence to be checked, not a verdict.
   No tone colours, because a tone here would be read as a mark, and
   nothing on this screen may ever say what an answer was worth.
   ============================================================ */
.obfigs { flex: 1 0 100%; margin: 9px 0 0; padding: 9px 10px 8px; border-radius: 8px;
    border: 1px solid var(--line); background: var(--ff-surface); }
.obfigs-h { font: 700 9.5px var(--sxfont); letter-spacing: .1em; text-transform: uppercase;
    color: var(--ff-muted); margin: 0 0 6px; }
.obfigs-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
    gap: 3px 14px; max-height: 196px; overflow: auto; overscroll-behavior: contain; }
.obfig { display: flex; gap: 8px; align-items: baseline; justify-content: space-between;
    padding: 2px 0; border-bottom: 1px dotted var(--line); min-width: 0; }
.obfig-k { font: 500 11px var(--sxfont); color: var(--ff-muted); white-space: nowrap;
    overflow: hidden; text-overflow: ellipsis; }
.obfig-v { font: 600 12px var(--mono); color: var(--ff-ink); font-variant-numeric: tabular-nums;
    text-align: right; overflow-wrap: anywhere; min-width: 0; }
.obfigs-more { font: 400 11px/1.55 var(--sxfont); color: var(--ff-muted); margin: 7px 0 0; }

/* A written answer is prose, not a figure: it takes the whole width,
   the reading face and a left edge, because a paragraph squeezed into a
   190px ledger column and right-aligned in a monospace face is exactly
   the unreadable thing this block was added to prevent. */
.obfig.is-long { grid-column: 1 / -1; display: block; }
.obfig.is-long .obfig-k { display: block; white-space: normal; margin-bottom: 2px; }
.obfig.is-long .obfig-v { display: block; text-align: left; font: 400 12.5px/1.65 var(--sxfont);
    color: var(--ff-ink-2); overflow-wrap: break-word; }

/* =====================================================================
   THE EXAM DOCKET — the "Before you begin" gate (2026-09-08, client:
   "make use of the free space available"; judged 4-concept panel).

   A sticky docket of the paper's facts on the left, the reading sheet
   on the right: rules → the examiner's video → this computer → confirm
   and start. Tokens only, so dark mode falls out of the token blocks;
   nothing animates but the connection dot, which is the same object
   the report uses. The docket's status foot is accent, never green —
   consent is not success, and no tick exists anywhere on the page.
   ===================================================================== */
.gate-head { padding: 6px 0 16px; margin-bottom: 20px; border-bottom: 1px solid var(--ff-line); }
.gate-kick { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.gate-head .qtitle { font-size: 22px; margin-top: 4px; }
.gate-head .qsub { max-width: 72ch; }
.gate-simnote { font: 500 12.5px/1.5 var(--sxfont); color: var(--ff-muted); margin: 6px 0 0; max-width: 72ch; }

.gate { display: grid; grid-template-columns: minmax(300px, 360px) minmax(0, 1fr); gap: 28px; align-items: start; }
.gate-read { min-width: 0; }

/* The docket IS the sticky element (its .card is overflow:hidden, which
   is harmless there); its middle scrolls when a paper has many sections,
   so the foot — the pointer to consent — never leaves a 768px viewport. */
.gate-docket { position: sticky; top: calc(var(--sxtop) + 14px);
    max-height: calc(100vh - var(--sxtop) - 28px); display: flex; flex-direction: column; margin-bottom: 0; }
.gate-docket > h2 { flex: 0 0 auto; }
.gate-scroll { min-height: 0; overflow: auto; overscroll-behavior: contain; }
.gate-scroll:focus-visible { outline: 2px solid var(--ff-accent); outline-offset: -2px; }

/* Figure tiles: a <dl>, hairline-divided like the dashboard's strip. */
.gate-figs, .rs-figs { display: grid; grid-template-columns: 1fr 1fr; margin: 0; }
.gate-fig, .rs-fig { padding: 13px 17px; border-bottom: 1px solid var(--ff-line); min-width: 0; }
.gate-fig:nth-child(odd):not(.span), .rs-fig:nth-child(odd):not(.span) { border-right: 1px solid var(--ff-line); }
.gate-fig .l, .rs-fig .l { display: block; font: 700 9.5px var(--sxfont); letter-spacing: .1em; text-transform: uppercase;
    color: var(--ff-muted); margin: 0; }
.gate-fig .v, .rs-fig .v { display: block; font: 600 22px var(--mono); letter-spacing: -.02em; font-variant-numeric: tabular-nums;
    line-height: 1.15; margin: 5px 0 0; color: var(--ff-ink); overflow-wrap: anywhere; }
.gate-fig .d, .rs-fig .d { display: block; font: 400 11.5px/1.45 var(--sxfont); color: var(--ff-muted); margin: 3px 0 0; }
.gate-fig.good .v, .rs-fig.good .v { color: var(--ff-good); }
.gate-fig.calm .v, .rs-fig.calm .v { color: var(--ff-muted); }
.gate-fig.acct .v, .rs-fig.acct .v { color: var(--ff-accent); }
.gate-fig.crit .d, .rs-fig.crit .d { color: var(--ff-crit); font-weight: 600; }
.gate-fig.span, .rs-fig.span { grid-column: 1 / -1; border-right: 0; }

.gate-sections { padding: 13px 17px 8px; }
.gate-sub { font: 700 9.5px var(--sxfont); letter-spacing: .1em; text-transform: uppercase; color: var(--ff-muted);
    margin: 0 0 8px; }
.gate-sections .vd { margin-bottom: 6px; padding: 9px 12px; }
.gate-sections .vd .why { font-family: var(--sxfont); }

/* The "this computer" tile: a verdict is words, not a figure. */
.gate-conn .v { font: 700 13.5px/1.35 var(--sxfont); letter-spacing: 0; display: flex; align-items: center; gap: 8px; }
.gate-conn .d { font-variant-numeric: tabular-nums; }
.gate-conn.is-good .v { color: var(--ff-good); }
.gate-conn.is-fair .v { color: var(--ff-flag); }
.gate-conn.is-poor .v,
.gate-conn.is-bad  .v { color: var(--ff-crit); }
.gate-conn .conncheck-dot { background: var(--slate); }
.gate-conn.is-good .conncheck-dot { background: var(--ff-good); }
.gate-conn.is-fair .conncheck-dot { background: var(--ff-flag); }
.gate-conn.is-poor .conncheck-dot,
.gate-conn.is-bad  .conncheck-dot { background: var(--ff-crit); }
/* …and the folded computer card's header state wears the same light. */
.gate-hs.is-good { color: var(--ff-good); font-weight: 600; }
.gate-hs.is-fair { color: var(--ff-flag); font-weight: 600; }
.gate-hs.is-poor, .gate-hs.is-bad { color: var(--ff-crit); font-weight: 600; }

.gate-status { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 10px;
    flex-wrap: wrap; padding: 12px 17px; background: var(--ff-sunken); border-top: 1px solid var(--ff-line);
    font: 600 12.5px var(--sxfont); color: var(--ff-muted); }
.gate-status.ready { color: var(--ff-accent); }
.gate-status a { color: var(--ff-accent); font-weight: 650; white-space: nowrap; text-decoration: none; }
.gate-status a:hover { text-decoration: underline; }

/* The reading sheet. .card > h2 is flex space-between; with a number in
   front the title would fly to the right edge. */
.gate-read > .card > h2 { justify-content: flex-start; gap: 9px; }

/* One tint per card (client, 2026-09-08: "each section to have diff
   highlight or color"). A label colour for finding your place, never a
   verdict — the computer card stays amber whatever the line measures,
   and the start card is green whether or not consent has been given.
   Each tint is a family the token file already carries, so dark mode
   follows by construction. */
.gate-c1 { --gt: var(--ff-accent); --gt-soft: var(--ff-accent-soft); --gt-edge: var(--ff-accent-edge); }
.gate-c2 { --gt: var(--ff-calc);   --gt-soft: var(--ff-calc-soft);   --gt-edge: var(--ff-calc-edge); }
.gate-c3 { --gt: var(--ff-flag);   --gt-soft: var(--ff-flag-soft);   --gt-edge: var(--ff-flag-edge); }
.gate-c4 { --gt: var(--ff-good);   --gt-soft: var(--ff-good-soft);   --gt-edge: var(--ff-good-edge); }
.gate-read > .card[class*="gate-c"] { border-color: var(--gt-edge); border-left: 4px solid var(--gt); }
.gate-read > .card[class*="gate-c"] .gate-sub { color: var(--gt); }
.gate-read > .card[class*="gate-c"] .gate-rule::before { background: var(--gt); }
.gate-read > .card.gate-c1 .stem { border-left-color: var(--gt-edge); }

/* THE ACCORDION (client, 2026-09-08: "each section open will cause
   other section to collapse"). The header is a real button carrying
   the number, the title, a one-line state while folded, and a chevron.
   The OPEN card wears its tint on the header and a solid number; a
   folded card is calm — white header, outlined number — so the eye
   lands on the one thing to do. Bodies are x-show, never removed. */
.gate-read > .card > h2 { padding: 0; scroll-margin-top: calc(var(--sxtop) + 12px); }
.gate-hb { display: flex; align-items: center; gap: 10px; width: 100%; padding: 13px 17px; margin: 0;
    border: 0; background: none; cursor: pointer; text-align: left; color: inherit;
    font: inherit; letter-spacing: inherit; text-transform: inherit; }
.gate-hb:focus-visible { outline: 2px solid var(--gt, var(--ff-accent)); outline-offset: -2px; }
.gate-ht { flex: 0 1 auto; white-space: nowrap; }
.gate-hs { flex: 1 1 auto; min-width: 0; text-transform: none; letter-spacing: 0;
    font: 500 12.5px var(--sxfont); color: var(--ff-muted); overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; }
.gate-hs::before { content: '·'; margin-right: 8px; color: var(--ff-line-2); }
.gate-chev { flex: 0 0 auto; margin-left: auto; width: 8px; height: 8px; margin-right: 4px;
    border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
    transform: translateY(-2px) rotate(45deg); opacity: .55; }
.gate-chev.on { transform: translateY(2px) rotate(-135deg); opacity: .9; }
.gate-read > .card[class*="gate-c"] > h2 { background: var(--ff-surface); color: var(--ff-ink-2); border-bottom: 0; }
.gate-read > .card[class*="gate-c"] > h2:hover { background: var(--gt-soft); }
.gate-read > .card[class*="gate-c"] .gate-n { background: var(--ff-surface); color: var(--gt);
    box-shadow: inset 0 0 0 1.5px var(--gt); }
.gate-read > .card[class*="gate-c"].is-open > h2 { background: var(--gt-soft); color: var(--gt);
    border-bottom: 1px solid var(--gt-edge); }
.gate-read > .card[class*="gate-c"].is-open .gate-n { background: var(--gt); color: var(--ff-on-accent); box-shadow: none; }
.gate-read > .card[class*="gate-c"].is-open .gate-hs { color: var(--gt); }
.gate-read > .card[class*="gate-c"]:not(.is-open) { margin-bottom: 10px; }
/* The fold itself: a grid track that goes 0fr → 1fr, which the browser
   can animate without knowing the height; visibility flips with it, so
   a folded card's controls leave the tab order and the reading order.
   Never display:none — every body stays in the page for the tests and
   for the connection report inside the folded computer card. */
.gate-fold { display: grid; grid-template-rows: 0fr; visibility: hidden; }
.gate-inner { min-height: 0; overflow: hidden; }
.gate-read > .card.is-open > .gate-fold { grid-template-rows: 1fr; visibility: visible; }
.gate-read > .card > h2 { transition: none; }
.gate-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 16px;
    padding-top: 14px; border-top: 1px solid var(--ff-line); }
.gate-next { color: var(--gt); border-color: var(--gt-edge); background: var(--gt-soft); font-weight: 650; }
.gate-next:hover { color: var(--gt); border-color: var(--gt); background: var(--gt-soft); }
.gate-prev { color: var(--ff-muted); }
.gate-offline:focus { outline: none; }
.gate-read > .card > h2:focus { outline: none; }
.gate-read > .card > h2:focus-visible { outline: 2px solid var(--ff-accent); outline-offset: -2px; }
.gate-n { display: inline-grid; place-items: center; width: 20px; height: 20px; border-radius: 999px;
    background: var(--ff-accent-soft); color: var(--ff-accent); font: 700 10.5px var(--mono); letter-spacing: 0; }
.gate-leadin { font: 400 13.5px/1.6 var(--sxfont); color: var(--ff-muted); margin: 0 0 12px; }
/* The examiner's words visibly quoted; the engine's lines plain. */
.gate-read .stem { padding: 12px 14px; margin: 0 0 16px; border-left: 3px solid var(--ff-accent-edge);
    background: var(--ff-sunken); border-radius: 0 var(--ff-r-sm) var(--ff-r-sm) 0; }
.gate-rules { list-style: none; margin: 0; padding: 0; }
.gate-rule { display: grid; grid-template-columns: 22px 150px minmax(0, 1fr); gap: 0 12px; padding: 10px 0;
    border-top: 1px solid var(--ff-line); font: 400 14px/1.6 var(--sxfont); color: var(--ff-ink-2); }
.gate-rule:first-child { border-top: 0; padding-top: 2px; }
.gate-rule::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: var(--ff-accent);
    justify-self: center; margin-top: 7px; }
.gate-lead { font-weight: 650; color: var(--ff-ink); }
.gate-fullscreen { margin-bottom: 0; }
.gate-fullscreen .why { font-family: var(--sxfont); }
.gate-note { margin-bottom: 12px; font-weight: 500; }

.gate-consent { display: flex; gap: 12px; align-items: flex-start; padding: 14px 16px; margin: 0 0 8px;
    border: 1px solid var(--ff-line); border-radius: var(--ff-r-md); background: var(--ff-sunken); cursor: pointer;
    font: 600 13.5px/1.5 var(--sxfont); color: var(--ff-ink-2); }
.gate-consent input { width: 18px; height: 18px; flex: none; margin: 2px 0 0; accent-color: var(--ff-accent); }
.gate-consent:has(input:checked) { border-color: var(--ff-accent-edge); background: var(--ff-accent-soft); color: var(--ff-ink); }
.gate-consent:has(input:focus-visible) { outline: 2px solid var(--ff-accent); outline-offset: 2px; }
.gate-consent:focus { outline: none; }
.gate-reread { margin: 0 0 16px; font: 500 12.5px var(--sxfont); }
.gate-reread a { color: var(--ff-muted); }
.gate-reread a:hover { color: var(--ff-accent); }
.gate-go { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.gate-start { padding: 12px 22px; font-size: 15px; min-width: 240px; }
.gate-go .hint { margin: 0; }
.gate-why { margin-top: 8px; }
.gate-offline { margin-top: 22px; padding-top: 16px; border-top: 1px dashed var(--ff-line); }
.gate-offline .hint { font-size: 13px; margin-bottom: 10px; }
.gate-offline ul { color: var(--ff-ink-2); font-size: 13px; line-height: 1.85; padding-left: 19px; margin: 0 0 12px; }

.gate-blocked { max-width: 560px; }
.gate-blockedh { font: 650 16px var(--sxfont); color: var(--ff-ink); margin: 0 0 10px; }
.gate-blocked .callout { font-weight: 500; }
.gate-back { display: inline-block; margin-top: 14px; }

/* THE VIDEO, ON TOP (client, 2026-09-08: "once I play overlay it on
   top"). The player leaves the strip and sits over the page in a
   dialog: scrim, a 16:9 frame as wide as the viewport allows, the
   examiner's caption, and Close. Escape and the scrim both close it.
   Under the product's own dialog and toasts, over everything else. */
.vidlight { position: fixed; inset: 0; z-index: 420; display: grid; place-items: center; padding: 24px;
    background: var(--ff-scrim); }
.vidlight-box { width: min(100%, 1100px); background: var(--ff-surface); border: 1px solid var(--ff-line);
    border-radius: var(--ff-r-lg); box-shadow: var(--ff-sh-3); overflow: hidden; }
.vidlight-h { display: flex; align-items: center; gap: 12px; padding: 12px 14px 12px 18px;
    border-bottom: 1px solid var(--ff-line); }
.vidlight-h b { flex: 1 1 auto; min-width: 0; font: 650 14px var(--sxfont); color: var(--ff-ink);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vidlight .vidframe { border-radius: 0; }
.vidlight-f { padding: 10px 18px 12px; }
.vidlight-f .vidhint { margin: 0; }
@media (prefers-reduced-motion: no-preference) {
    .gate-consent { transition: background var(--ff-t), border-color var(--ff-t); }
    .vidlight { animation: sxvidin 160ms ease-out; }
    .gate-read > .card.is-open .gate-body { animation: sxgatein 260ms ease-out; }
    .gate-chev { transition: transform var(--ff-t), opacity var(--ff-t); }
    .gate-read > .card > h2 { transition: background 220ms cubic-bezier(.22,.61,.36,1), color 220ms cubic-bezier(.22,.61,.36,1); }
    .gate-read > .card[class*="gate-c"] .gate-n { transition: background 220ms cubic-bezier(.22,.61,.36,1), color 220ms cubic-bezier(.22,.61,.36,1), box-shadow 220ms cubic-bezier(.22,.61,.36,1); }
    /* Folding: the track shrinks over 280ms and visibility flips at the
       end; opening: visibility flips at once so the content is there
       while the track grows. */
    .gate-fold { transition: grid-template-rows 280ms cubic-bezier(.22,.61,.36,1), visibility 0s linear 280ms; }
    .gate-read > .card.is-open > .gate-fold { transition: grid-template-rows 280ms cubic-bezier(.22,.61,.36,1), visibility 0s; }
}
@keyframes sxvidin { from { opacity: 0; } to { opacity: 1; } }
@keyframes sxgatein { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
@media (max-width: 900px) {
    .gate { grid-template-columns: 1fr; gap: 16px; }
    .gate-docket { position: static; max-height: none; display: block; margin-bottom: 16px; }
    .gate-scroll { overflow: visible; }
    .gate-figs, .rs-figs { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .gate-fig, .rs-fig { border-right: 1px solid var(--ff-line); }
    .gate-fig:nth-child(3n), .gate-fig.span, .rs-fig:nth-child(3n), .rs-fig.span { border-right: 0; }
    .gate-status { display: none; }
}
@media (max-width: 600px) {
    .gate-figs, .rs-figs { grid-template-columns: 1fr 1fr; }
    .gate-fig, .rs-fig { border-right: 0; }
    .gate-fig:nth-child(odd):not(.span), .rs-fig:nth-child(odd):not(.span) { border-right: 1px solid var(--ff-line); }
    .gate-rule { grid-template-columns: 22px minmax(0, 1fr); }
    .gate-lead { grid-column: 2; margin-bottom: 2px; }
    .gate-rule > span:last-child { grid-column: 2; }
    .gate-start { width: 100%; }
}
@media print {
    .gate-docket { position: static; max-height: none; }
    .gate-scroll { overflow: visible; }
    .gate-status { display: none; }
}

/* =====================================================================
   THE MARK SHEET — the candidate's result page (2026-09-09, client:
   "further design this page UI"; judged 4-concept panel, unanimous).

   The verdict first and at size — one band — then the exam gate's two
   columns in mirror: the sticky column holds the marks the way the
   docket held the paper's facts (the .gate-fig tiles are ALIASED, not
   copied, so one retune lands on both screens), and the ledger beside
   it is the marked script. Tokens only; dark mode falls out. FAIL is
   slate on sunken ground — an outcome, not an error — never red.
   Nothing animates: a figure of record does not "arrive".
   ===================================================================== */
.rs-head .qtitle { font-size: 24px; }

/* The band. Tone is three custom props, the gate-c* mechanism: one
   rule set draws four states. */
.rs-band { display: grid; grid-template-columns: minmax(0, 1fr) minmax(280px, 420px); gap: 20px 40px;
    align-items: center; margin: 0 0 22px; padding: 20px 24px 22px; border: 1px solid var(--rt-edge);
    border-left: 5px solid var(--rt); border-radius: var(--ff-r-md); background: var(--rt-soft); }
.rs-band.pass { --rt: var(--ff-good); --rt-soft: var(--ff-good-soft); --rt-edge: var(--ff-good-edge); }
.rs-band.fail { --rt: var(--ff-ink-2); --rt-soft: var(--ff-sunken); --rt-edge: var(--ff-line-2); }
.rs-band.wait { --rt: var(--ff-flag); --rt-soft: var(--ff-flag-soft); --rt-edge: var(--ff-flag-edge); }
.rs-band.held { --rt: var(--ff-accent); --rt-soft: var(--ff-accent-soft); --rt-edge: var(--ff-accent-edge); }
.rs-verdict { min-width: 0; }
.rs-kick { font: 700 10px var(--sxfont); letter-spacing: .13em; text-transform: uppercase; color: var(--rt); }
.rs-word { font: 800 clamp(34px, 4.6vw, 52px)/1 var(--sxfont); letter-spacing: .04em; text-transform: uppercase;
    color: var(--rt); margin: 8px 0 10px; overflow-wrap: anywhere; }
.rs-band.held .rs-word, .rs-band.wait .rs-word { text-transform: none; letter-spacing: -.01em; }
.rs-say { margin: 0; font: 400 14px/1.6 var(--sxfont); color: var(--ff-ink-2); max-width: 52ch; }

/* The meter: the score against the pass mark on one ruled track. The
   fill keeps a border so it survives forced colours; the pass-mark tick
   is ink, clamped inside the track. No transition, ever. */
.rs-meter { min-width: 0; padding: 4px 0 26px; }
.rs-figline { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin: 0 0 10px; }
.rs-figure { font: 600 26px var(--mono); font-variant-numeric: tabular-nums; letter-spacing: -.02em; color: var(--rt); }
.rs-pct { font: 600 15px var(--mono); color: var(--ff-muted); }
.rs-track { position: relative; height: 10px; border-radius: 999px; background: var(--ff-ground-2);
    border: 1px solid var(--ff-line); }
.rs-fill { height: 100%; width: var(--rs-pct, 0%); max-width: 100%; border-radius: inherit; background: var(--rt);
    box-shadow: inset 0 0 0 1px var(--rt); }
.rs-passmark { position: absolute; top: -7px; left: clamp(12px, var(--rs-pass, 50%), calc(100% - 12px));
    width: 2px; height: 22px; margin-left: -1px; background: var(--ff-ink); }
.rs-passcap { position: absolute; top: 20px; left: clamp(12px, var(--rs-pass, 50%), calc(100% - 12px));
    transform: translateX(-50%); font: 600 11px var(--sxfont); color: var(--ff-muted); white-space: nowrap; }

/* The held state's three stages: filled for what is true, hollow for
   what is not. Static. */
.rs-steps { list-style: none; margin: 0; padding: 0; display: flex; gap: 0; }
.rs-steps li { position: relative; flex: 1 1 0; min-width: 0; padding: 18px 8px 0 0; font: 600 12px/1.4 var(--sxfont);
    color: var(--ff-ink); }
.rs-steps li b { display: block; font-weight: 650; }
.rs-steps li > span:not(.rs-dot):not(.visually-hidden) { display: block; margin-top: 2px; font: 500 11.5px var(--mono);
    color: var(--ff-muted); }
.rs-steps li::before { content: ''; position: absolute; top: 5px; left: 10px; right: 0; height: 1px;
    background: var(--ff-line-2); }
.rs-steps li:last-child::before { display: none; }
.rs-dot { position: absolute; top: 0; left: 0; width: 11px; height: 11px; border-radius: 50%;
    border: 2px solid var(--ff-line-2); background: var(--ff-surface); }
.rs-steps li.done .rs-dot { border-color: var(--rt); background: var(--rt); }
.rs-steps li.done { color: var(--ff-ink); }
.rs-steps li:not(.done) { color: var(--ff-muted); }

/* The two columns: the gate grid verbatim. The column is the sticky
   element (the .card itself; its middle scrolls, its foot is pinned). */
.rs { display: grid; grid-template-columns: minmax(300px, 360px) minmax(0, 1fr); gap: 28px; align-items: start; }
.rs-ledger { min-width: 0; }
.rs-side { position: sticky; top: calc(var(--sxtop) + 14px); max-height: calc(100vh - var(--sxtop) - 28px);
    display: flex; flex-direction: column; margin-bottom: 0; }
.rs-side > h2 { flex: 0 0 auto; }
.rs-scroll { min-height: 0; overflow: auto; overscroll-behavior: contain; }
.rs-scroll:focus-visible { outline: 2px solid var(--ff-accent); outline-offset: -2px; }
.rs-fig.flag .v { color: var(--ff-flag); }
.rs-fig.flag .d { color: var(--ff-flag); font-weight: 600; }
.rs-fig .v.rs-v-word { font: 650 16px/1.3 var(--sxfont); letter-spacing: 0; }

/* The question index: one row per question, the figure in mono, the
   outcome as a dot AND in the accessible name. */
.rs-qhead { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; flex-wrap: wrap;
    padding: 13px 17px 4px; }
.rs-qhead .gate-sub { margin: 0; }
.rs-jump { font: 600 11.5px var(--sxfont); color: var(--ff-accent); text-decoration: none; white-space: nowrap; }
.rs-jump:hover { text-decoration: underline; }
.rs-qlist { list-style: none; margin: 0; padding: 2px 0 6px; }
.rs-qrow { display: grid; grid-template-columns: 12px 32px minmax(0, 1fr) auto; gap: 8px; align-items: center;
    padding: 8px 17px; font: 500 12.5px var(--sxfont); color: var(--ff-ink-2); text-decoration: none;
    border-top: 1px solid var(--ff-line); }
.rs-qrow:hover { background: var(--ff-sunken); color: var(--ff-ink); }
.rs-qrow:focus-visible { outline: 2px solid var(--ff-accent); outline-offset: -2px; }
.rs-qn { font: 700 11px var(--mono); color: var(--ff-muted); }
.rs-qx { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rs-qm { font: 700 12px var(--mono); font-variant-numeric: tabular-nums; color: var(--ff-ink); white-space: nowrap; }
.rs-qdot { width: 10px; height: 10px; border-radius: 50%; border: 2px solid var(--ff-line-2); background: var(--ff-surface); }
.rs-qdot.full { border-color: var(--ff-good); background: var(--ff-good); }
.rs-qdot.part { border-color: var(--ff-flag); background: linear-gradient(90deg, var(--ff-flag) 50%, transparent 50%); }
.rs-qdot.none { border-color: var(--ff-crit); }
.rs-qdot.pending { border-style: dashed; }

/* The foot: the one action per state, pinned to the column's bottom. */
.rs-next { flex: 0 0 auto; padding: 12px 17px 14px; background: var(--ff-sunken); border-top: 1px solid var(--ff-line); }
.rs-next p { margin: 0 0 8px; font: 500 12.5px/1.5 var(--sxfont); color: var(--ff-ink-2); }
.rs-next .rs-next-date { color: var(--ff-accent); font-weight: 650; }
.rs-next .btn { width: 100%; text-align: center; }

/* The ledger. */
.rs-incident { margin-bottom: 14px; font-weight: 500; }
.rs-note { margin-bottom: 14px; font-weight: 400; }
.rs-chips { margin-top: 10px; }
.rs-q { scroll-margin-top: calc(var(--sxtop) + 12px); }
.rs-q:target { outline: 2px solid var(--ff-accent); outline-offset: 2px; }
.rs-q:focus { outline: none; }
.rs-outcome { display: inline-flex; align-items: center; gap: 9px; }
.rs-outcome-word { font: 600 12px var(--sxfont); letter-spacing: 0; text-transform: none; color: var(--ff-muted); }
.rs-q > h2 .tag.ok { background: var(--ff-good-soft); border-color: var(--ff-good-edge); color: var(--ff-good); }
.rs-q > h2 .tag.part { background: var(--ff-flag-soft); border-color: var(--ff-flag-edge); color: var(--ff-flag); }
.rs-q > h2 .tag.bad { background: var(--ff-crit-soft); border-color: var(--ff-crit-edge); color: var(--ff-crit); }
.rs-appeal > h2 .tag.ok { background: var(--ff-good-soft); border-color: var(--ff-good-edge); color: var(--ff-good); }
.rs-stem { margin-bottom: 14px; }
.rs-split { margin-top: 2px; }
/* The candidate's own words, visibly quoted — the gate's quoting device
   turned around. Blue is the aside colour and this is not an aside. */
.rs-ans { padding: 12px 14px; border-left: 3px solid var(--ff-accent-edge); background: var(--ff-sunken);
    border-radius: 0 var(--ff-r-sm) var(--ff-r-sm) 0; white-space: pre-line; color: var(--ff-ink);
    font: 400 14px/1.6 var(--sxfont); overflow-wrap: anywhere; }
.rs-ans-none { color: var(--ff-muted); font-style: italic; }
.rs-wb { margin-bottom: 14px; }
.rs-grid { max-height: 42vh; }
.rs-widenote { display: none; }
.rs-pending { margin-top: 10px; }
.rs-model { margin-top: 12px; padding: 12px 14px; border: 1px solid var(--ff-accent-edge);
    background: var(--ff-accent-soft); border-radius: var(--ff-r-md); }
.rs-model > b { display: block; font: 700 10.5px var(--sxfont); letter-spacing: .13em; text-transform: uppercase;
    color: var(--ff-accent); margin-bottom: 8px; }
.rs-appeal { border-left: 4px solid var(--ff-flag); border-color: var(--ff-flag-edge); }
.rs-appeal .rs-appeal-hint { margin-bottom: 12px; }
.rs-appeal .f { margin-bottom: 6px; }
.rs-appeal-btn { margin-top: 10px; }
.rs-actions { margin-top: 18px; }
.rs-printhead, .rs-printfoot { display: none; }

/* Tracked caps are a Latin device. */
[lang^="zh"] .rs-kick, [lang^="zh"] .rs-word, [lang^="zh"] .rs-fig .l, [lang^="zh"] .rs-model > b,
[lang^="zh"] .hm-kick, [lang^="zh"] .hm-group, [lang^="zh"] .hm-fig .l {
    letter-spacing: normal; text-transform: none; }

@media (max-width: 900px) {
    .rs { grid-template-columns: 1fr; gap: 16px; }
    .rs-side { position: static; max-height: none; display: block; margin-bottom: 16px; }
    .rs-scroll { overflow: visible; }
    .rs-qlist { display: flex; flex-wrap: wrap; gap: 7px; padding: 0 17px 12px; }
    .rs-qrow { display: inline-flex; gap: 6px; padding: 7px 11px; border: 1px solid var(--ff-line);
        border-radius: 999px; min-height: 36px; }
    .rs-qx { display: none; }
    .rs-split { grid-template-columns: 1fr; }
}
@media (max-width: 600px) {
    .rs-band { grid-template-columns: 1fr; gap: 16px; padding: 16px 16px 20px; }
    .rs-steps { flex-direction: column; gap: 10px; }
    .rs-steps li { padding: 0 0 0 22px; }
    .rs-steps li::before { display: none; }
    .rs-dot { top: 3px; }
}

/* Print: the candidate's own record of marks, in monochrome if need be
   — the word, the bordered fill, the ink tick and the figure on every
   row carry it without colour. The active workbook tab prints in full;
   the sticky column goes static; controls go. */
@media print {
    .topbar, #sx-loadbar, .rs-next, .rs-qhead, .rs-qlist, .rs-actions, .rs-appeal-btn, .rs-appeal textarea,
    .rs-appeal .f, .rs-chips { display: none !important; }
    .rs-printhead { display: flex; justify-content: space-between; gap: 16px; padding: 0 0 8px;
        border-bottom: 1px solid #9aa3ad; margin-bottom: 14px; font: 700 10px var(--sxfont);
        letter-spacing: .12em; text-transform: uppercase; color: #475569; }
    .rs-printfoot { display: flex; flex-direction: column; gap: 4px; margin-top: 18px; padding-top: 8px;
        border-top: 1px solid #9aa3ad; font: 500 10.5px/1.5 var(--sxfont); color: #475569; }
    .rs-band, .vd, .gc, .rs-fig, .rs-ans, .rs-model { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
    .rs { display: block; }
    .rs-side { position: static; max-height: none; display: block; break-inside: avoid; margin-bottom: 14px; }
    .rs-scroll { overflow: visible; }
    .rs-figs { grid-template-columns: repeat(4, minmax(0, 1fr)); }
    .rs-q { break-inside: avoid; box-shadow: none; }
    .rs-grid { max-height: none; overflow: visible; border-color: #9aa3ad; }
    .rs-grid .sheet { font-size: 10.5px; }
    .rs-widenote { display: block; margin: 6px 0 0; font: 500 11px var(--sxfont); color: #475569; }
    .rs-page .card { box-shadow: none; }
}

/* =====================================================================
   THE CALL BOARD — the candidate's "My exams" (2026-09-09, client:
   "further improve this page UI"; judged 4-concept panel).

   One exam is CALLED — drawn at size with its facts, its money and its
   attempts — and the rest fall into a register under headings the
   ranking already implies. Two rules here are structural, not
   stylistic: the action column holds nothing but a link, and the
   reason there is no action is prose in the other column. That is what
   stops a grey pill reading as a button, which is what the client saw.

   The figure tiles are the exam gate's, aliased above; the step track
   is the result page's, reused verbatim (it reads --rt from here).
   ===================================================================== */
.hm-top { margin-bottom: 18px; }
.hm-page .sub { margin-bottom: 0; }

/* ---- the called card ---- */
.hm-call { border-color: var(--rt-edge); border-left: 5px solid var(--rt); box-shadow: var(--ff-sh-2); }
.hm-call.go { --rt: var(--ff-accent); --rt-soft: var(--ff-accent-soft); --rt-edge: var(--ff-accent-edge); }
.hm-call.wait { --rt: var(--ff-flag); --rt-soft: var(--ff-flag-soft); --rt-edge: var(--ff-flag-edge); }
.hm-call.done { --rt: var(--ff-ink-2); --rt-soft: var(--ff-sunken); --rt-edge: var(--ff-line-2); }
/* The head is tinted; the body never is. The body holds money badges,
   mono figures and PASS/FAIL ink, all drawn against surface. */
.hm-call > .hm-head { background: var(--rt-soft); border-bottom: 1px solid var(--ff-line); }

.hm-head { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, auto); gap: 14px 28px;
    align-items: center; padding: 16px 20px; }
.hm-head.is-noact { grid-template-columns: minmax(0, 1fr); }
.hm-headmain { min-width: 0; }
.hm-kick { font: 700 10px var(--sxfont); letter-spacing: .13em; text-transform: uppercase;
    color: var(--rt, var(--ff-muted)); margin-bottom: 5px; }
.hm-titlerow { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; min-width: 0; }
.hm-name { margin: 0; font: 650 clamp(19px, 2.2vw, 24px)/1.25 var(--sxfont); letter-spacing: -.02em;
    color: var(--ff-ink); text-wrap: balance; }
.hm-card .hm-name { font-size: 15.5px; line-height: 1.35; }

/* THE REASON. Prose with a margin rule: no border, no radius, no fill,
   no hover, and never in the column where controls live. */
.hm-why { position: relative; margin: 9px 0 0; padding-left: 13px;
    font: 500 13.5px/1.6 var(--sxfont); color: var(--ff-ink-2); max-width: 56ch; }
.hm-why::before { content: ''; position: absolute; left: 0; top: 3px; bottom: 3px; width: 2px;
    background: var(--rt, var(--ff-line-2)); border-radius: 2px; }
.hm-card .hm-why { font-size: 12.5px; }
.hm-meta { margin: 7px 0 0; font: 500 12px/1.6 var(--sxfont); color: var(--ff-muted); }

/* THE ONE ACT. */
.hm-act { justify-self: end; display: flex; }
.hm-go { min-height: 46px; padding: 12px 22px; font-size: 15px; min-width: 190px;
    justify-content: center; text-align: center; text-wrap: balance; overflow-wrap: normal; }
.hm-card .hm-go { min-height: 38px; padding: 9px 16px; font-size: 13.5px; min-width: 150px; }
.hm-track { flex: 0 0 auto; min-width: 320px; }

/* ---- the facts, the gate's own tiles ---- */
.hm-figs { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr);
    border-bottom: 1px solid var(--ff-line); margin: 0; }
.hm-fig { padding: 12px 20px; min-width: 0; border-right: 1px solid var(--ff-line); }
.hm-fig:last-child { border-right: 0; }
.hm-fig .v { font-size: 20px; }
.hm-fig.flag .v { color: var(--ff-flag); }

/* ---- the money shelf ---- */
.hm-shelf { padding: 12px 20px; border-bottom: 1px solid var(--ff-line); }
.hm-shelf .payline, .hm-shelf .payremark { margin-bottom: 0; }
.hm-shelf .hint { margin-top: 8px; }
/* statusLabel() can be "Waiting for the institution to confirm" — 38
   characters the base badge would shout on one nowrap line. Scoped
   here; the Payments page and the receipt keep the base rule. */
.hm-shelf .paybadge { text-transform: none; letter-spacing: .01em; white-space: normal; font-size: 11px; }

/* ---- the attempts, each with its own way back ---- */
.hm-atts { padding: 6px 20px 14px; }
.hm-atts .gate-sub { margin: 8px 0 2px; }
.hm-att { display: grid; grid-template-columns: 10px minmax(0, 1fr) auto auto; gap: 4px 14px;
    align-items: center; padding: 11px 0; }
.hm-att + .hm-att, .hm-more .hm-att { border-top: 1px solid var(--ff-line); }
.hm-att .dot { width: 9px; height: 9px; border-radius: 50%; border: 2px solid var(--ff-line-2);
    background: var(--ff-surface); }
.hm-att .nm { min-width: 0; font: 650 13.5px var(--sxfont); color: var(--ff-ink); }
.hm-att .sub { display: block; margin-top: 2px; font: 500 11.5px var(--mono); color: var(--ff-muted);
    letter-spacing: -.01em; }
.hm-att .mk { font: 700 13px var(--mono); font-variant-numeric: tabular-nums; white-space: nowrap;
    color: var(--ff-muted); }
.hm-att .acts { display: flex; gap: 7px; flex-wrap: wrap; justify-content: flex-end; }
.hm-att.pass .dot { border-color: var(--ff-good); background: var(--ff-good); }
.hm-att.pass .mk { color: var(--ff-good); }
/* Slate, never red: not passing is an outcome, not an error — the rule
   the result page's own band follows. */
.hm-att.fail .dot { border-color: var(--ff-ink-2); background: var(--ff-ink-2); }
.hm-att.fail .mk { color: var(--ff-ink-2); }
.hm-att.wait .mk { font: 600 12px var(--sxfont); }
.btn.sm.doc { border-color: var(--ff-good-edge); color: var(--ff-good); background: var(--ff-good-soft); }
.btn.sm.doc:hover { border-color: var(--ff-good); color: var(--ff-good); }
.hm-more { margin-top: 2px; }
.hm-more > summary { cursor: pointer; list-style: none; padding: 9px 0 4px;
    font: 600 12px var(--sxfont); color: var(--ff-muted); }
.hm-more > summary::-webkit-details-marker { display: none; }
.hm-more > summary::before { content: '▸ '; }
.hm-more[open] > summary::before { content: '▾ '; }
.hm-more > summary:hover { color: var(--ff-accent); }

/* ---- the register ---- */
.hm-group { display: flex; align-items: center; gap: 10px; margin: 26px 0 10px;
    font: 700 11px var(--sxfont); letter-spacing: .13em; text-transform: uppercase; color: var(--ff-muted); }
.hm-group::after { content: ''; order: 2; flex: 1 1 auto; height: 1px; background: var(--ff-line); }
.hm-count { order: 3; font: 700 11px var(--mono); letter-spacing: 0; color: var(--ff-muted); }
.hm-card { margin-bottom: 10px; }
.hm-card .hm-head { padding: 13px 17px; }
.hm-card .hm-shelf, .hm-card .hm-atts { padding-left: 17px; padding-right: 17px; }
.hm-card .hm-atts { padding-top: 0; padding-bottom: 10px; }

/* ---- the empty state ---- */
.hm-empty { max-width: 560px; margin: 0 auto; text-align: center; }
.hm-empty .body { padding: 34px 26px 30px; }
.hm-empty h2 { margin: 0 0 8px; font: 650 17px var(--sxfont); color: var(--ff-ink); }
.hm-empty p { margin: 0 auto 18px; max-width: 46ch; font: 400 13.5px/1.65 var(--sxfont); color: var(--ff-muted); }
/* The letterhead device the loader and the login slip use — two
   engraved rules, never a tick over a state that has not succeeded. */
.hm-rule { display: flex; flex-direction: column; align-items: center; gap: 4px; margin-bottom: 18px; }
.hm-rule i { display: block; height: 1px; background: var(--ff-line-2); }
.hm-rule i:first-child { width: 140px; }
.hm-rule i:last-child { width: 52px; }

.hm-foot { margin: 22px 0 0; padding-top: 14px; border-top: 1px solid var(--ff-line);
    font: 500 12.5px var(--sxfont); color: var(--ff-muted); }
.hm-foot a { color: var(--ff-accent); font-weight: 650; }

@media (max-width: 900px) {
    .hm-head, .hm-head.is-noact { grid-template-columns: 1fr; }
    .hm-act { justify-self: stretch; }
    .hm-go, .hm-card .hm-go { width: 100%; }
    .hm-track { min-width: 0; }
    .hm-figs { grid-auto-flow: row; }
    .hm-fig { border-right: 0; border-bottom: 1px solid var(--ff-line); }
    .hm-fig:last-child { border-bottom: 0; }
}
@media (max-width: 640px) {
    .hm-att { grid-template-columns: 10px minmax(0, 1fr); }
    .hm-att .mk, .hm-att .acts { grid-column: 2; }
    .hm-att .acts { justify-content: flex-start; }
    .hm-att .acts .btn.sm { flex: 1 1 auto; min-height: 40px; justify-content: center; text-align: center; }
}
