/* ScanGrade's own stylesheet — the tokens, the dark-mode remap and the
   legibility floor.

   This used to be a 36 KB <style> block inside base.html, which meant every
   page carried it *inside* its HTML: uncacheable, re-gzipped by nginx on every
   request, and re-sent to the same phone on every navigation. It is served from
   /static/ instead, where nginx caches it for a year — which is only safe
   because base.html asks for it through asset_v(), so the URL changes when
   these bytes do.

   It is deliberately plain CSS with no Jinja in it, so it can be a file at all.
   base.html must link it *after* tailwind.css: these rules override utility
   classes, and the reversed order fails silently, in dark mode only.

   The values here are measured, not chosen by eye — see
   tests/unit/test_dark_theme_contrast.py. */

        /* Light: --text-dim was #64748b, which is 4.34:1 on the page background
           (#f1f5f9) and therefore already below AA for the secondary text it
           paints; #5d6a7d is 5.01:1 there and 5.49:1 on a card. --text-muted was
           the same #64748b and failed for the same reason (it paints the small
           muted labels), so it became #606c80 — 4.85:1 in the worst case, still
           lighter than --text-dim so the three levels stay distinguishable.
           --input-border was #cbd5e1, which is 1.36:1 against the page: a field is
           identified by its border here, because its white fill is 1.06:1 on
           #f1f5f9, so 3:1 was required and not met. #7c8ca2 is the lightest
           slate tone that clears it against both surfaces (3.13:1 on the page,
           3.43:1 on a card) — the dark theme's own border is held to 3:1 too.
           Dark: every value below is measured against every surface in
           tests/unit/test_dark_theme_contrast.py, not chosen by eye.

           Four of them are *roles* rather than surfaces, and they are named here
           because the exam and correction pages used to spell them out by hand.
           A page-local `<style>` is not remapped by the utility rules below, so a
           `#fff` written in one was a decision nobody had recorded:

             · `--paper`      the sheet an answer is drawn on. Light is white
                              because a scanned page is white and the two are laid
                              on each other; dark steps it down the same ladder
                              every other surface uses, so the answer keeps its
                              dark ink instead of the sheet going black.
             · `--paper-ink`  what is written on that sheet — a tiered near-black
                              rather than `#000`, and the same in both themes
                              because the sheet is light in both.
             · `--on-accent`  a label on a saturated accent fill. This is the
                              app's convention (`bg-primary-600 text-white`) and
                              one value for both themes, like the fills are.
             · `--on-chrome`  a label on the chrome that is dark in both themes
                              (the canvas tool dots) — off-white, not pure white.

           tests/unit/test_theme_literals.py measures all four and holds the pages
           that read them to having no pure white or black left in their own CSS. */
        /* ── the layer scale ───────────────────────────────────────────────────
           One ordered list, and the only place in the whole app a stacking height
           is written. Every panel, drawer, modal and toast takes the nearest
           *name* it must sit above; a new one cannot pick a number by eye, and
           two cannot be handed the same height by accident — which is how the
           terms agreement, the submit gate and the anti-cheat watermark all came
           to read `9999`, leaving no height to place the exam's sit strip between
           the first two.

           Read each group top-down: each line paints over the line before it.
           The three groups are separate stacks that never compete —
             · `app`    the shell every page shares;
             · `paper`  a sitting, where the paper has its own overlays;
             · `canvas` inside a `.full-canvas-wrap`, a context of its own.
           The rules that paint read the token; the classes below let a template
           take a line by name. The values are re-declared identically in `.dark`
           so a theme cannot move a layer, and tests/unit/test_layer_scale.py
           holds the grouping, the order and that equality. */
        :root { color-scheme: light; --bg-body: #f1f5f9; --bg-card: #ffffff; --bg-subtle: #f8fafc; --bg-hover: #f1f5f9; --bg-card-translucent: rgba(255,255,255,0.92); --text: #1e293b; --text-dim: #5d6a7d; --text-muted: #606c80; --text-nav: #94a3b8; --border: #e2e8f0; --border-strong: #cbd5e1; --input: #ffffff; --input-border: #7c8ca2; --paper: #ffffff; --paper-ink: #111827; --on-accent: #ffffff; --on-chrome: #f1f5f9; --sb-size: 12px; --sb-track: #e8edf6; --sb-thumb: #6f819c; --sb-thumb-hover: #586a86; --sb-thumb-active: #2563eb; --shadow: 0 1px 2px rgba(0,0,0,0.03); --sg-layer-local: 1; --sg-layer-sticky: 20; --sg-layer-header: 30; --sg-layer-over: 40; --sg-layer-drawer: 50; --sg-layer-badge: 60; --sg-layer-dialog: 100; --sg-layer-notice: 200; --sg-layer-paper: 10; --sg-layer-bar: 20; --sg-layer-qstrip: 30; --sg-layer-float: 50; --sg-layer-rail: 85; --sg-layer-watermark: 88; --sg-layer-scrim: 90; --sg-layer-strip: 95; --sg-layer-media: 9998; --sg-layer-modal: 9999; --sg-layer-strip-over: 10000; --sg-layer-gate: 10001; --sg-layer-canvas-tool: 5; --sg-layer-canvas-grip: 6; --sg-layer-canvas-item: 10; --sg-layer-canvas-item-ctl: 20; --sg-layer-out: auto; }
        /* Every layer, so a template takes a line by name and never a number. */
        .sg-layer-local { z-index: var(--sg-layer-local); }
        .sg-layer-sticky { z-index: var(--sg-layer-sticky); }
        .sg-layer-header { z-index: var(--sg-layer-header); }
        .sg-layer-over { z-index: var(--sg-layer-over); }
        .sg-layer-drawer { z-index: var(--sg-layer-drawer); }
        .sg-layer-badge { z-index: var(--sg-layer-badge); }
        .sg-layer-dialog { z-index: var(--sg-layer-dialog); }
        .sg-layer-notice { z-index: var(--sg-layer-notice); }
        .sg-layer-paper { z-index: var(--sg-layer-paper); }
        .sg-layer-bar { z-index: var(--sg-layer-bar); }
        .sg-layer-qstrip { z-index: var(--sg-layer-qstrip); }
        .sg-layer-float { z-index: var(--sg-layer-float); }
        .sg-layer-rail { z-index: var(--sg-layer-rail); }
        .sg-layer-watermark { z-index: var(--sg-layer-watermark); }
        .sg-layer-scrim { z-index: var(--sg-layer-scrim); }
        .sg-layer-strip { z-index: var(--sg-layer-strip); }
        .sg-layer-media { z-index: var(--sg-layer-media); }
        .sg-layer-modal { z-index: var(--sg-layer-modal); }
        .sg-layer-strip-over { z-index: var(--sg-layer-strip-over); }
        .sg-layer-gate { z-index: var(--sg-layer-gate); }
        .sg-layer-canvas-tool { z-index: var(--sg-layer-canvas-tool); }
        .sg-layer-canvas-grip { z-index: var(--sg-layer-canvas-grip); }
        .sg-layer-canvas-item { z-index: var(--sg-layer-canvas-item); }
        .sg-layer-canvas-item-ctl { z-index: var(--sg-layer-canvas-item-ctl); }
        /* The one state that is not a height. `z-index` does not apply to a
           `position: static` element, so a region the shell lays out as a column
           is in no stack at all — and giving it a height would be a lie. It takes
           `out` by name instead of resetting `z-index` by hand, keyed to the same
           1024px breakpoint as the layout that needs it (the shell's drawer is
           `lg:static`, and Tailwind's `lg` is this width). */
        @media (min-width: 1024px) { .sg-layer-out-lg { z-index: var(--sg-layer-out); } }

        /* The dark tokens and every dark remap live in the `@media screen` block
           below — see "Dark theme — screen only" for why print is excluded. */
* { font-family: 'Inter', system-ui, sans-serif; }
        /* The text colour belongs on the root element too: templates set `text-*`
           on what they paint, and anything that does not would otherwise inherit
           the UA's black — unreadable on `.dark`. */
        body { background: var(--bg-body) !important; color: var(--text); }
html { transition: background-color .3s, color .3s; }
/* ── Legibility floor ────────────────────────────────────────────────────────
   The same correction the dark-theme block above applies to colour, applied to
   size. Templates reached for `text-[9px]`/`text-[10px]` to fit a dense card,
   and it spread: 546 uses of 8-11px across the templates, measured live at 8px
   on the login-tip captions. That is not a design choice anyone can read on a
   phone, and it was invisible on the 27-inch screen it was written on.

   Raising the floor here rather than editing 546 call sites keeps one decision
   in one place — the same reasoning as the colour remap. `!important` because a
   stray second stylesheet load is exactly the failure this has to survive.

   6-8px was used for toolbar tooltips that only appear on hover (`Pena`,
   `Hapus`) and for the icons beside them, so it is here too — a tooltip nobody
   can read is not a tooltip.

   NOT floored, deliberately: 4-5px is the OMR answer-sheet mockup
   (tools/generate_answer_sheet.html), where the sizes are part of a miniature of
   a printed sheet and `[QR]`/`Student Name:` are drawn at paper scale. Those are
   `text-[4px]`/`text-[5px]` only.

   Every rule here reaches **12px**, including the first three. They used to land
   at 10 / 11 / 11.5px — below the floor the block's own name promises, and the
   whole reason it was written. A measured phone audit found 14 elements under
   12px on one grading page, and the guard only asked whether a rule *existed*,
   not whether it reached the floor. */
.text-\[6px\]  { font-size: 12px !important; }
.text-\[7px\]  { font-size: 12px !important; }
.text-\[8px\]  { font-size: 12px !important; }
.text-\[9px\]  { font-size: 12px !important; }
.text-\[10px\] { font-size: 12px !important; }
.text-\[11px\] { font-size: 12.5px !important; }

/* Teacher page readability */
.teacher-content { font-size: 105%; }
.teacher-content input, .teacher-content select, .teacher-content textarea { font-size: 0.95rem; }
.teacher-content .text-xs { font-size: 0.8125rem; }
.teacher-content .text-sm { font-size: 0.925rem; }
.teacher-content .text-lg { font-size: 1.15rem; }
.teacher-content .text-xl { font-size: 1.35rem; }
.teacher-content .text-2xl { font-size: 1.65rem; }
.teacher-content .q-btn { width: 2.5rem; height: 2.5rem; font-size: 0.9rem; }
.teacher-content .p-4 { padding: 1.2rem; }
.teacher-content .p-5 { padding: 1.5rem; }
.teacher-content .px-4 { padding-left: 1.2rem; padding-right: 1.2rem; }
.teacher-content .gap-2 { gap: 0.55rem; }
.teacher-content .gap-3 { gap: 0.85rem; }
.teacher-content .w-10 { width: 3rem; }
.teacher-content .h-10 { height: 3rem; }
@media (max-width:640px) { .teacher-content { font-size: 110%; } }

/* ── The exam builder on a finger ─────────────────────────────────────────────
   The page a teacher writes a paper on, and the densest form in the app: a type
   select, a cognitive-level select and a media toggle per question, a key per PGK
   statement, and the mark-scheme rows. Measured on the running page as a demo
   teacher in headless Chrome with `pointer: coarse`, at 820x1180 (iPad Air 4
   portrait), 1180x820, 800x1280 (Galaxy Tab portrait), 1280x800, 1024x768 and
   768x1024: **42 controls under 40px** at every one of those widths, and **no
   horizontal overflow at any of them**. The layout was never the problem; the hit
   areas were. The smallest of the 42 were the mark-scheme rows at 16px, the type
   and level selects at 30px, the PGK category keys at 30px and the media toggle
   at 32px.

   Asked as `pointer: coarse` and not as a width, for the reason the chrome's own
   rule gives: a 768px tablet is as much a finger as a 375px phone, while a 768px
   window on a mouse desktop is not — and `max-width` gets both of them wrong in
   the same direction. `min-width` as well as `min-height`, because some of these
   are icon-only: that is the axis the sidebar's 12x24px close button needed.

   This does not shrink anything. Every control it touches is under 44px on a
   finger today, so a 44px floor can only grow it, and the measured widths at
   which that growth could overflow (820/768 portrait) have 700px of card to
   spend. The one place it changes a layout is inside the question header, whose
   row already wraps. Measured after: the count went from **42 to 0** at every
   width above — the last three were the action card's block links (670-926px
   wide, 36px tall), which `a[href]` catches and an inline link cannot, since
   `min-height` does not apply to an inline box. */
@media (pointer: coarse) {
    .sg-exam-builder select,
    .sg-exam-builder input[type="text"],
    .sg-exam-builder input[type="url"],
    .sg-exam-builder input[type="number"],
    .sg-exam-builder input[type="search"],
    .sg-exam-builder textarea,
    .sg-exam-builder button,
    .sg-exam-builder a[href] {
        min-height: 44px;
        min-width: 44px;
    }
}
        .nav-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.625rem 0.875rem; border-radius: 0.75rem; font-weight: 600; font-size: 0.875rem; transition: all .15s; color: #94a3b8; }
        /* The finger, not the width. The chrome's icon-only controls sized
           themselves to their glyph — the sidebar's close button measured 12x24
           px, and on a phone that button is the drawer's only exit. Asked as
           `pointer: coarse` because a breakpoint cannot decide it: a 768 px
           tablet is just as much a finger as a 375 px phone. */
        @media (pointer: coarse) {
            .tap-44 { min-width: 44px; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; }
            .nav-item { min-height: 44px; }
        }
        .nav-item:hover { background: rgba(255,255,255,0.08); color: #e2e8f0; }
        .nav-item.active { background: rgba(59,130,246,0.15); color: #93c5fd; }
        .nav-item i { width: 20px; text-align: center; font-size: .9rem; }
        /* .65rem is 10.4px — under the floor, and the sidebar's section labels
           are on every page in the app. The floor's bracket-utility rules cannot
           see a hand-written class, so these two were the hole in it. */
        .nav-section-title { padding: .5rem .875rem .25rem; font-size: .75rem; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; color: #475569; }
        .card { background: var(--bg-card); border-radius: 1rem; border: 1px solid var(--border); box-shadow: var(--shadow); }
        .card-hover:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.06); transform: translateY(-1px); transition: all .2s; }
        .stat-card { padding: 1.25rem 1.5rem; }
        .stat-icon { width: 44px; height: 44px; border-radius: 0.75rem; display: flex; align-items: center; justify-content: center; font-size: 1.15rem; }
        .badge { display: inline-flex; align-items: center; padding: .2rem .6rem; border-radius: 9999px; font-size: .75rem; font-weight: 700; letter-spacing: .02em; }
        [x-cloak] { display: none !important; }
        .btn { display: inline-flex; align-items: center; gap: .375rem; padding: .625rem 1.25rem; border-radius: .75rem; font-weight: 700; font-size: .875rem; transition: all .15s; cursor: pointer; border: none; }
        .btn-primary { background: #2563eb; color: #fff; }
        .btn-primary:hover { background: #1d4ed8; }
        .btn-ghost { background: transparent; color: var(--text-dim); border: 2px solid var(--border); }
        .btn-ghost:hover { background: var(--bg-hover); color: var(--text); }
        .input { width: 100%; padding: .75rem 1rem; border: 2px solid var(--input-border); border-radius: .75rem; font-size: .875rem; font-weight: 500; transition: all .15s; background: var(--input); color: var(--text); }
        .input:focus { outline: none; border-color: #3b82f6; }
        .toast-enter { animation: slideUp .25s ease-out forwards; }
        .toast-leave { animation: fadeIn .2s ease-in reverse forwards; }
        @media (max-width: 1023px) { .sidebar-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); z-index: var(--sg-layer-over); backdrop-filter: blur(4px); } }
        /* ── The scrollbar ────────────────────────────────────────────────────
           The control a reader touches most, and the only one nobody had
           measured: 5px wide, a `transparent` track, a slate thumb (#cbd5e1,
           #94a3b8 on hover) and no drag state. A stripe you cannot aim at, on
           a track you cannot see, that never answers the pointer.

           Three decisions, each measured rather than picked by eye:

           * **One scale, read by every bar in the app.** The size, the groove
             and the three thumb states are `--sb-*` tokens — declared here and
             remapped in `.dark` — and the rules below only read them. That is
             what makes a pane's bar the page's bar by construction rather than
             by two copies happening to agree: the 33 scrollable panes in the
             templates inherit an `overflow` and nothing else.
             The finger's raise is written as `:root:not(.dark), :root:is(.dark)`,
             and that is load-bearing rather than symmetrical-looking. `.dark` sits
             on the same element as `:root`, so both declare the size at the same
             specificity — and the `.dark` block is written *later* in this file,
             inside `@media screen` — which means a `pointer: coarse` rule named
             `:root, .dark` ties and **loses**, silently, leaving every dark touch
             device with the mouse-sized bar. One extra pseudo-class per carrier
             binds the raise above both, so it wins wherever it sits. Measured in a
             browser with that cascade shape: 16px with the stronger raise, 12px
             with an equal-specificity `.dark` size after a raise that only ties.
             tests/unit/test_scrollbar_usability.py asks the general form of it —
             the raise must bind more strongly than every rule that declares the
             size, whatever those rules are called.
           * **12px, and 16px for a finger.** 12 is the comfortable end of the
             platform range and 2.4x what shipped. The `pointer: coarse` block
             asks the same question `.tap-44` asks — a 768px tablet is just as
             much a finger as a 375px phone — and it raises `--sb-size` rather
             than writing a second `::-webkit-scrollbar` rule. The earlier shape
             (a resized rule inside the media block) depended on that block
             coming *after* the base rule, because at equal specificity an
             earlier one loses; a token on `:root` cannot lose to the rule that
             reads it, whatever the order. (iOS Safari draws its own overlay bar
             and ignores all of this; nothing here can reach it, and nothing
             needs to.)
           * **A groove, and a pill inside it.** The track is painted rather
             than transparent, and the thumb is inset 2px with
             `background-clip: padding-box`, so it reads as a handle sitting in
             a channel instead of a filled strip.
           * **The app's own blue, not the neutral slate.** A scrollbar is a
             control, so it is drawn in the accent the buttons use: #6f819c
             with #586a86 on hover, #2563eb while you are dragging it — and on
             a dark page #5f6e91 / #7285ad / #3b82f6. Every state clears 3:1
             against its own track in both themes, the same WCAG 1.4.11 bar
             `--input-border` answers to: light 3.38 / 4.68 / 4.40, dark
             3.21 / 4.42 / 4.45. Those numbers are held by
             tests/unit/test_scrollbar_usability.py, which is also what stops
             the two themes being handed the same bar — and by
             tests/unit/test_scrollbar_panes.py, which fails if any rule in the
             app or any template paints a bar without them.

           Not one selector here names `.dark`, and that is load-bearing. An
           earlier shape painted the dark bar with `.dark::-webkit-scrollbar-thumb,
           .dark ::-webkit-scrollbar-thumb` pairs, because `.dark ` with a space is
           a *descendant* selector and cannot paint the element that carries the
           class — `<html>`, whose bar is the viewport one the reader drags. It
           came to be written twice, by hand, in five states, so the page bar and
           an inner pane could not be given different values. A token has neither
           problem: an element resolves its own inheritance, and there is one rule
           to get right.

           Firefox has no ::-webkit-scrollbar, so it gets the standard pair —
           but *inside a Firefox-only @supports*, deliberately: a browser that
           honours `scrollbar-color` stops honouring ::-webkit-scrollbar
           (Chrome 121+), so an ungated pair would drop Chrome to the
           platform's own width and undo the sizing above in silence. It reads the
           same tokens (`var()` resolves in `scrollbar-color`), so the two engines
           cannot drift; nothing in a pane may set `scrollbar-width: thin` or
           `none`, which is how one pane used to narrow or hide its own bar. */
        ::-webkit-scrollbar { width: var(--sb-size); height: var(--sb-size); }
        ::-webkit-scrollbar-track { background: var(--sb-track); }
        ::-webkit-scrollbar-thumb { background: var(--sb-thumb); border-radius: 999px; border: 2px solid transparent; background-clip: padding-box; }
        ::-webkit-scrollbar-thumb:hover { background: var(--sb-thumb-hover); }
        ::-webkit-scrollbar-thumb:active { background: var(--sb-thumb-active); }
        ::-webkit-scrollbar-corner { background: var(--sb-track); }
        @media (pointer: coarse) { :root:not(.dark), :root:is(.dark) { --sb-size: 16px; } }
        @supports (-moz-appearance: none) { html { scrollbar-width: auto; scrollbar-color: var(--sb-thumb) var(--sb-track); } }
        dialog { max-height: 90vh; } dialog::backdrop { background: rgba(0,0,0,0.4); backdrop-filter: blur(2px); }
        @supports (height: 100dvh) { .h-full { height: 100dvh; } }
        .pb-safe { padding-bottom: env(safe-area-inset-bottom, 0px); }
        .pt-safe { padding-top: env(safe-area-inset-top, 0px); }
        @media (max-width: 1023px) { main { padding-bottom: 4rem; } }
        .dark-header { background: var(--bg-card); }

        /* ══ Light mode — the tint/accent pairs ═════════════════════════════════
           The dark remap further down answers "this light utility is wrong on a
           dark page". Light mode has the mirror problem, and it cannot be
           answered by remapping a utility on its own: `text-amber-600` is
           perfect on a white card and only fails on a `-50`/`-100` tint of its
           own hue — 2.86:1 on `bg-amber-100`, which is exactly what a warning
           badge is. Measured across the tree, 24 such pairs sat below AA.

           So each rule names *both* halves of the pair. The accent keeps its
           hue and steps down a shade or two; the tint is untouched, because the
           tint is the design. Only `color` moves, so a saturated fill such as
           `bg-emerald-600` on a button keeps its colour and its white label.

           Two details are load-bearing:

           * `:where()` around the background half holds each rule at one class
             of specificity — the same as `:where(.dark) .text-amber-600` in the
             remap below. Equal specificity means source order decides, and the
             remap is later, so dark mode still gets the dark value.
           * These sit *outside* `@media screen`, unlike the dark theme. Print
             drops the remap and keeps the light palette, so a badge has to stay
             readable on paper too.

           Out of scope, deliberately: a white label on a saturated fill
           (`bg-amber-500 text-white`, 2.15:1) is the app's button surface — the
           same one the dark theme keeps saturated — and a test holds those to a
           white or black label instead of to 4.5:1. Text that inherits its
           surface from an ancestor cannot be attributed statically at all;
           that is what the `--text*` tokens are for, not this block.
           ═════════════════════════════════════════════════════════════════════ */
        :where(.bg-gray-100).text-gray-400 { color: #4b5563; }      /* 2.31 → 6.87 */
        :where(.bg-stone-100).text-stone-400 { color: #57534e; }    /* 2.31 → 6.99 */
        :where(.bg-slate-100).text-slate-400 { color: #475569; }    /* 2.34 → 6.92 */
        :where(.bg-surface-100).text-surface-400 { color: #475569; }  /* 2.34 → 6.92 */
        :where(.bg-stone-50).text-stone-400 { color: #78716c; }     /* 2.41 → 4.59 */
        :where(.bg-surface-50).text-surface-400 { color: #64748b; }  /* 2.45 → 4.55 */
        :where(.bg-white).text-surface-400 { color: #64748b; }      /* 2.56 → 4.76 */
        :where(.bg-amber-100).text-amber-600 { color: #b45309; }    /* 2.86 → 4.51 */
        :where(.bg-amber-50).text-amber-600 { color: #b45309; }     /* 3.07 → 4.84 */
        :where(.bg-orange-50).text-orange-600 { color: #c2410c; }   /* 3.35 → 4.88 */
        :where(.bg-orange-200).text-orange-700 { color: #9a3412; }  /* 3.83 → 5.40 */
        :where(.bg-emerald-100).text-emerald-600 { color: #047857; }  /* 3.32 → 4.84 */
        :where(.bg-emerald-50).text-emerald-600 { color: #047857; }  /* 3.58 → 5.21 */
        :where(.bg-purple-50).text-purple-500 { color: #9333ea; }   /* 3.69 → 5.02 */
        :where(.bg-violet-100).text-violet-500 { color: #7c3aed; }  /* 3.57 → 4.80 */
        :where(.bg-primary-100).text-primary-600 { color: #1d4ed8; }  /* 4.24 → 5.49 */
        :where(.bg-red-50).text-red-500 { color: #b91c1c; }         /* 3.44 → 5.91 */
        :where(.bg-red-50).text-red-600 { color: #b91c1c; }         /* 4.41 → 5.91 */
        /* The drag-and-drop badge is the first rose pair in the tree: rose-600 on
           rose-50 measured 4.28, a hair under AA. */
        :where(.bg-rose-50).text-rose-600 { color: #be123c; }       /* 4.28 → 5.72 */
        :where(.bg-red-100).text-red-600 { color: #b91c1c; }        /* 3.95 → 5.30 */
        :where(.bg-red-200).text-red-700 { color: #991b1b; }        /* 4.47 → 5.74 */
        :where(.bg-green-200).text-green-700 { color: #166534; }    /* 4.14 → 5.88 */
        :where(.bg-yellow-200).text-yellow-700 { color: #854d0e; }  /* 4.23 → 5.89 */
        :where(.bg-surface-100).text-surface-500 { color: #475569; }  /* 4.34 → 6.92 */
        :where(.bg-surface-200).text-surface-500 { color: #475569; }  /* 3.86 → 6.15 */
        :where(.bg-slate-100).text-slate-500 { color: #475569; }    /* 4.34 → 6.92 */
        :where(.bg-slate-200).text-slate-500 { color: #475569; }    /* 3.86 → 6.15 */

        /* ══ Dark theme — screen only ══════════════════════════════════════════
           The tokens and every remap below live inside `@media screen`.

           Printing is paper, and browsers drop background colours while keeping
           text colours — so a dark-mode user printing a result sheet got
           near-white text on a white page, i.e. a blank sheet. Scoping the whole
           theme to `screen` makes print fall back to the light tokens in `:root`
           on its own, with no second set of values to keep in step.
           ═════════════════════════════════════════════════════════════════════ */
        @media screen {
        .dark { color-scheme: dark; --bg-body: #0f1117; --bg-card: #191c2b; --bg-subtle: #202436; --bg-hover: #262b41; --bg-card-translucent: rgba(25,28,43,0.92); --text: #e6e9f0; --text-dim: #a8b0c8; --text-muted: #949db5; --text-nav: #8892b0; --border: #2f3448; --border-strong: #3d4460; --input: #1e2233; --input-border: #66738f; --paper: #e8ecf2; --paper-ink: #111827; --on-accent: #ffffff; --on-chrome: #f1f5f9; --sb-size: 12px; --sb-track: #1a1f31; --sb-thumb: #5f6e91; --sb-thumb-hover: #7285ad; --sb-thumb-active: #3b82f6; --shadow: 0 1px 3px rgba(0,0,0,0.35); --sg-layer-local: 1; --sg-layer-sticky: 20; --sg-layer-header: 30; --sg-layer-over: 40; --sg-layer-drawer: 50; --sg-layer-badge: 60; --sg-layer-dialog: 100; --sg-layer-notice: 200; --sg-layer-paper: 10; --sg-layer-bar: 20; --sg-layer-qstrip: 30; --sg-layer-float: 50; --sg-layer-rail: 85; --sg-layer-watermark: 88; --sg-layer-scrim: 90; --sg-layer-strip: 95; --sg-layer-media: 9998; --sg-layer-modal: 9999; --sg-layer-strip-over: 10000; --sg-layer-gate: 10001; --sg-layer-canvas-tool: 5; --sg-layer-canvas-grip: 6; --sg-layer-canvas-item: 10; --sg-layer-canvas-item-ctl: 20; --sg-layer-out: auto; }

        /* ══ Dark theme ════════════════════════════════════════════════════════
           Templates carry light utilities (bg-white, text-slate-600,
           border-slate-200). Putting a `dark:` variant on every element of 114
           templates rots the moment somebody adds a card, so the light
           utilities are remapped here onto the same tokens `.card` and `.input`
           already use: a component readable in light mode stays readable.

           `:where(.dark)` holds specificity at one class, so a template's own
           `dark:` utility still wins — the whiteboard chrome asks for
           `dark:bg-gray-900` and keeps it.

           Measured (WCAG 2.1 AA: 4.5:1 text, 3:1 for a control's boundary):
             --text        on every surface  11.5 – 15.5
             --text-dim    on every surface   6.4 –  8.7
             --text-muted  on every surface   5.1 –  7.0
             accent text on its own tint      8.5 – 11.7
             --input-border vs the field      3.1
           Card borders stay near 1.4 on purpose — 1.4.11 covers a boundary
           needed to *identify a control*, and a 3:1 hairline on every card
           reads as a wireframe. The input is the boundary that carries
           meaning, so it is the one held to 3:1.
           ═════════════════════════════════════════════════════════════════════ */

        /* ── Neutral surfaces ── */
        :where(.dark) .bg-white { background: var(--bg-card); }
        :where(.dark) :is(.bg-surface-50,.bg-surface-100,.bg-slate-50,.bg-slate-100,.bg-stone-50,.bg-stone-100,.bg-gray-50,.bg-gray-100,.bg-zinc-50,.bg-zinc-100) { background: var(--bg-subtle); }
        :where(.dark) :is(.bg-surface-200,.bg-surface-300,.bg-slate-200,.bg-slate-300,.bg-stone-200,.bg-stone-300,.bg-gray-200,.bg-gray-300) { background: var(--bg-hover); }
        /* -300 is the same "track" shade as -200 here: a hairline divider between
           two toolbar buttons, the off state of a toggle, and the seat of a
           status badge. On a dark page any of those in grey is a bright scratch. */
        /* Floating chips that sit over page content (the auth back-button, the
           exam timer badge). The 5 – 25% whites are decorative shapes on a
           saturated gradient and are deliberately untouched. */
        :where(.dark) :is(.bg-white\/60,.bg-white\/70,.bg-white\/80,.bg-white\/90) { background: var(--bg-card-translucent); }

        /* ── Neutral text ── */
        :where(.dark) :is(.text-surface-600,.text-surface-700,.text-surface-800,.text-surface-900,.text-slate-600,.text-slate-700,.text-slate-800,.text-slate-900,.text-stone-600,.text-stone-700,.text-stone-800,.text-gray-600,.text-gray-700,.text-gray-800) { color: var(--text); }
        :where(.dark) :is(.text-surface-400,.text-surface-500,.text-slate-400,.text-slate-500,.text-stone-400,.text-stone-500,.text-gray-400,.text-gray-500) { color: var(--text-dim); }
        :where(.dark) :is(.text-surface-200,.text-surface-300,.text-slate-200,.text-slate-300,.text-stone-200,.text-stone-300) { color: var(--text-muted); }

        /* ── Form controls that carry no fill of their own ────────────────────
           140 of the 204 text/select/textarea elements in the templates have
           layout classes only (`flex-1 px-4 py-2 border border-slate-200`) and
           rely on the browser's default field. On a dark page that is a white
           slab with the UA's slate-400 placeholder on it — measured 2.54:1, the
           most visible defect in dark mode. Fixed once here rather than in 140
           places: the selector is element-level, so `.input` and any `bg-*`/
           `text-*` a template does apply are classes and still win. `color-scheme`
           above covers the rest of the native chrome (scrollbars, dropdowns,
           checkboxes) so it does not fall back to a light widget either.
           ═════════════════════════════════════════════════════════════════════ */
        :where(.dark) :is(input, select, textarea):not([type="checkbox"], [type="radio"], [type="range"], [type="file"], [type="color"], [type="submit"], [type="button"]) { background-color: var(--input); color: var(--text); }
        :where(.dark) :is(input, select, textarea)::placeholder { color: var(--text-muted); }
        :where(.dark) select option { background-color: var(--input); color: var(--text); }

        /* ── Neutral borders and dividers ── */
        :where(.dark) :is(.border-surface-50,.border-surface-100,.border-surface-200,.border-surface-300,.border-slate-50,.border-slate-100,.border-slate-200,.border-slate-300,.border-stone-100,.border-stone-200,.border-stone-300,.border-gray-100,.border-gray-200,.border-gray-300) { border-color: var(--border); }
        /* A divide- utility colours the *children*, so the override has to as well */
        :where(.dark) :is(.divide-surface-50,.divide-surface-100,.divide-surface-200,.divide-slate-50,.divide-slate-100,.divide-slate-200,.divide-stone-50,.divide-stone-100,.divide-stone-200,.divide-gray-100,.divide-gray-200) > :not([hidden]) ~ :not([hidden]) { border-color: var(--border); }

        /* ── Hover ────────────────────────────────────────────────────────────
           A hover state is its own class, so remapping `bg-white` does nothing
           for `hover:bg-white`: a light row would flash white under the cursor,
           and `hover:text-primary-700` (38 uses — the most common hover here)
           would snap to the raw light-mode navy on a dark card. `hover:` is the
           variant that lands on a surface in both themes, so it is remapped
           beside the base utility; `focus:` and `active:` accents are saturated
           hues that already read on dark, and are left alone.
           ═════════════════════════════════════════════════════════════════════ */
        :where(.dark) :is(.hover\:bg-surface-50,.hover\:bg-surface-100,.hover\:bg-surface-200,.hover\:bg-slate-50,.hover\:bg-slate-100,.hover\:bg-slate-200,.hover\:bg-slate-300,.hover\:bg-stone-50,.hover\:bg-stone-100,.hover\:bg-stone-300,.hover\:bg-gray-50,.hover\:bg-gray-100,.hover\:bg-gray-200,.hover\:bg-white):hover { background: var(--bg-hover); }
        :where(.dark) :is(.hover\:text-surface-600,.hover\:text-surface-700,.hover\:text-slate-600,.hover\:text-slate-700,.hover\:text-stone-600,.hover\:text-stone-700,.hover\:text-gray-600,.hover\:text-gray-700):hover { color: var(--text); }
        :where(.dark) :is(.hover\:text-surface-400,.hover\:text-surface-500,.hover\:text-slate-400,.hover\:text-slate-500,.hover\:text-stone-400,.hover\:text-stone-500,.hover\:text-gray-400,.hover\:text-gray-500):hover { color: var(--text-dim); }
        :where(.dark) :is(.hover\:border-surface-200,.hover\:border-surface-300,.hover\:border-slate-300):hover { border-color: var(--border); }

        /* ── Shadows ── */
        :where(.dark) .shadow-sm { box-shadow: var(--shadow); }
        :where(.dark) :is(.shadow-lg,.shadow-xl,.shadow-2xl) { box-shadow: 0 4px 12px rgba(0,0,0,0.2); }

        /* ══ Semantic hues ═════════════════════════════════════════════════════
           A tinted badge is `bg-<hue>-100` + `text-<hue>-700`. Remapping only the
           text — which is what the old list did — produced light text on a light
           tint, so both halves move together: 50/100/200 become the tint, and
           200 – 900 the accent that reads on it (the accent* text is what a
           badge uses; the tinted surface is what it sits on).

           Saturated 400 – 900 *backgrounds* are buttons carrying `text-white`
           and are deliberately left alone, so this is grouped by property, not
           by shade.
           ═════════════════════════════════════════════════════════════════════ */

        /* emerald — success, active, published */
        :where(.dark) :is(.bg-emerald-50,.bg-emerald-100,.bg-emerald-200) { background: #0d2b1e; }
        :where(.dark) :is(.hover\:bg-emerald-50,.hover\:bg-emerald-100,.hover\:bg-emerald-200):hover { background: #143a28; }
        :where(.dark) :is(.text-emerald-200,.text-emerald-300,.text-emerald-400,.text-emerald-500) { color: #34d399; }
        :where(.dark) :is(.text-emerald-600,.text-emerald-700,.text-emerald-800,.text-emerald-900) { color: #6ee7b7; }
        :where(.dark) :is(.border-emerald-100,.border-emerald-200,.border-emerald-300,.border-emerald-400) { border-color: #1f6b4a; }

        /* red — danger, wrong answer */
        :where(.dark) :is(.bg-red-50,.bg-red-100,.bg-red-200) { background: #2e1414; }
        :where(.dark) :is(.hover\:bg-red-50,.hover\:bg-red-100,.hover\:bg-red-200):hover { background: #3d1a1a; }
        :where(.dark) :is(.text-red-200,.text-red-300,.text-red-400,.text-red-500) { color: #f87171; }
        :where(.dark) :is(.text-red-600,.text-red-700,.text-red-800,.text-red-900) { color: #fca5a5; }
        :where(.dark) :is(.border-red-100,.border-red-200,.border-red-300,.border-red-400) { border-color: #7f2b2b; }

        /* amber — warning, pending */
        :where(.dark) :is(.bg-amber-50,.bg-amber-100,.bg-amber-200) { background: #2e2410; }
        :where(.dark) :is(.hover\:bg-amber-50,.hover\:bg-amber-100,.hover\:bg-amber-200):hover { background: #3d3016; }
        :where(.dark) :is(.text-amber-200,.text-amber-300,.text-amber-400,.text-amber-500) { color: #fbbf24; }
        :where(.dark) :is(.text-amber-600,.text-amber-700,.text-amber-800,.text-amber-900) { color: #fcd34d; }
        :where(.dark) :is(.border-amber-100,.border-amber-200,.border-amber-300,.border-amber-400) { border-color: #7c5a1a; }

        /* yellow — caution */
        :where(.dark) :is(.bg-yellow-50,.bg-yellow-100,.bg-yellow-200) { background: #2a2410; }
        :where(.dark) :is(.hover\:bg-yellow-50,.hover\:bg-yellow-100):hover { background: #383016; }
        :where(.dark) :is(.text-yellow-200,.text-yellow-300,.text-yellow-400,.text-yellow-500) { color: #facc15; }
        :where(.dark) :is(.text-yellow-600,.text-yellow-700,.text-yellow-800) { color: #fde047; }
        :where(.dark) :is(.border-yellow-100,.border-yellow-200,.border-yellow-300) { border-color: #77681c; }

        /* orange — the school's accent */
        :where(.dark) :is(.bg-orange-50,.bg-orange-100,.bg-orange-200) { background: #2e1c0e; }
        :where(.dark) :is(.hover\:bg-orange-50,.hover\:bg-orange-100,.hover\:bg-orange-200):hover { background: #3d2513; }
        :where(.dark) :is(.text-orange-200,.text-orange-300,.text-orange-400,.text-orange-500) { color: #fb923c; }
        :where(.dark) :is(.text-orange-600,.text-orange-700,.text-orange-800,.text-orange-900) { color: #fdba74; }
        :where(.dark) :is(.border-orange-100,.border-orange-200,.border-orange-300,.border-orange-400) { border-color: #7c4a1c; }

        /* green — correct answer */
        :where(.dark) :is(.bg-green-50,.bg-green-100,.bg-green-200) { background: #0f2a18; }
        :where(.dark) :is(.hover\:bg-green-50,.hover\:bg-green-100):hover { background: #153a21; }
        :where(.dark) :is(.text-green-200,.text-green-300,.text-green-400,.text-green-500) { color: #4ade80; }
        :where(.dark) :is(.text-green-600,.text-green-700,.text-green-800,.text-green-900) { color: #86efac; }
        :where(.dark) :is(.border-green-100,.border-green-200,.border-green-300,.border-green-400) { border-color: #1f6b39; }

        /* teal */
        :where(.dark) :is(.bg-teal-50,.bg-teal-100,.bg-teal-200) { background: #0d2a28; }
        :where(.dark) :is(.hover\:bg-teal-50,.hover\:bg-teal-100):hover { background: #123a37; }
        :where(.dark) :is(.text-teal-200,.text-teal-300,.text-teal-400,.text-teal-500) { color: #2dd4bf; }
        :where(.dark) :is(.text-teal-600,.text-teal-700,.text-teal-800,.text-teal-900) { color: #5eead4; }
        :where(.dark) :is(.border-teal-100,.border-teal-200,.border-teal-300) { border-color: #1a6b64; }

        /* sky */
        :where(.dark) :is(.bg-sky-50,.bg-sky-100,.bg-sky-200) { background: #0e2436; }
        :where(.dark) :is(.hover\:bg-sky-50,.hover\:bg-sky-100):hover { background: #133149; }
        :where(.dark) :is(.text-sky-200,.text-sky-300,.text-sky-400,.text-sky-500) { color: #38bdf8; }
        :where(.dark) :is(.text-sky-600,.text-sky-700,.text-sky-800,.text-sky-900) { color: #7dd3fc; }
        :where(.dark) :is(.border-sky-100,.border-sky-200,.border-sky-300,.border-sky-400) { border-color: #1c5a80; }

        /* cyan */
        :where(.dark) :is(.bg-cyan-50,.bg-cyan-100,.bg-cyan-200) { background: #0a2a30; }
        :where(.dark) :is(.hover\:bg-cyan-50,.hover\:bg-cyan-100):hover { background: #0e3a42; }
        :where(.dark) :is(.text-cyan-200,.text-cyan-300,.text-cyan-400,.text-cyan-500) { color: #22d3ee; }
        :where(.dark) :is(.text-cyan-600,.text-cyan-700,.text-cyan-800,.text-cyan-900) { color: #67e8f9; }
        :where(.dark) :is(.border-cyan-100,.border-cyan-200,.border-cyan-300) { border-color: #155e6b; }

        /* blue */
        :where(.dark) :is(.bg-blue-50,.bg-blue-100,.bg-blue-200) { background: #101f3a; }
        :where(.dark) :is(.hover\:bg-blue-50,.hover\:bg-blue-100):hover { background: #14294d; }
        :where(.dark) :is(.text-blue-200,.text-blue-300,.text-blue-400,.text-blue-500) { color: #60a5fa; }
        :where(.dark) :is(.text-blue-600,.text-blue-700,.text-blue-800,.text-blue-900) { color: #93c5fd; }
        :where(.dark) :is(.border-blue-100,.border-blue-200,.border-blue-300,.border-blue-400) { border-color: #1e4a86; }

        /* indigo */
        :where(.dark) :is(.bg-indigo-50,.bg-indigo-100,.bg-indigo-200) { background: #161a38; }
        :where(.dark) :is(.hover\:bg-indigo-50,.hover\:bg-indigo-100):hover { background: #1d234b; }
        :where(.dark) :is(.text-indigo-200,.text-indigo-300,.text-indigo-400,.text-indigo-500) { color: #818cf8; }
        :where(.dark) :is(.text-indigo-600,.text-indigo-700,.text-indigo-800,.text-indigo-900) { color: #a5b4fc; }
        :where(.dark) :is(.border-indigo-100,.border-indigo-200,.border-indigo-300) { border-color: #2f3a86; }

        /* violet */
        :where(.dark) :is(.bg-violet-50,.bg-violet-100,.bg-violet-200) { background: #1e1436; }
        :where(.dark) :is(.hover\:bg-violet-50,.hover\:bg-violet-100,.hover\:bg-violet-200):hover { background: #281a48; }
        :where(.dark) :is(.text-violet-200,.text-violet-300,.text-violet-400,.text-violet-500) { color: #a78bfa; }
        :where(.dark) :is(.text-violet-600,.text-violet-700,.text-violet-800,.text-violet-900) { color: #c4b5fd; }
        :where(.dark) :is(.border-violet-100,.border-violet-200,.border-violet-300,.border-violet-400) { border-color: #45309f; }

        /* purple */
        :where(.dark) :is(.bg-purple-50,.bg-purple-100,.bg-purple-200) { background: #1e1436; }
        :where(.dark) :is(.hover\:bg-purple-50,.hover\:bg-purple-100,.hover\:bg-purple-200):hover { background: #281a48; }
        :where(.dark) :is(.text-purple-200,.text-purple-300,.text-purple-400,.text-purple-500) { color: #a78bfa; }
        :where(.dark) :is(.text-purple-600,.text-purple-700,.text-purple-800,.text-purple-900) { color: #c4b5fd; }
        :where(.dark) :is(.border-purple-100,.border-purple-200,.border-purple-300) { border-color: #45309f; }

        /* rose */
        :where(.dark) :is(.bg-rose-50,.bg-rose-100,.bg-rose-200) { background: #2e1220; }
        :where(.dark) :is(.hover\:bg-rose-50,.hover\:bg-rose-100):hover { background: #3d182b; }
        :where(.dark) :is(.text-rose-200,.text-rose-300,.text-rose-400,.text-rose-500) { color: #fb7185; }
        :where(.dark) :is(.text-rose-600,.text-rose-700,.text-rose-800,.text-rose-900) { color: #fda4af; }
        :where(.dark) :is(.border-rose-100,.border-rose-200,.border-rose-300) { border-color: #86203a; }

        /* primary / brand — one palette, so one pair of rules */
        :where(.dark) :is(.bg-primary-50,.bg-primary-100,.bg-primary-200,.bg-brand-50,.bg-brand-100,.bg-brand-200) { background: #101f3a; }
        :where(.dark) :is(.hover\:bg-primary-50,.hover\:bg-primary-100,.hover\:bg-brand-50,.hover\:bg-brand-100):hover { background: #14294d; }
        :where(.dark) :is(.text-primary-200,.text-primary-300,.text-primary-400,.text-primary-500,.text-brand-200,.text-brand-300,.text-brand-400,.text-brand-500) { color: #60a5fa; }
        :where(.dark) :is(.text-primary-600,.text-primary-700,.text-primary-800,.text-primary-900,.text-brand-600,.text-brand-700,.text-brand-800,.text-brand-900) { color: #93c5fd; }
        :where(.dark) :is(.border-primary-100,.border-primary-200,.border-primary-300,.border-primary-400,.border-brand-100,.border-brand-200,.border-brand-300,.border-brand-400) { border-color: #1e4a86; }

        /* ── Semantic hover, same pairings as the tints above ── */
        :where(.dark) :is(.hover\:bg-blue-50,.hover\:bg-blue-100,.hover\:bg-blue-200):hover { background: #14294d; }
        :where(.dark) :is(.hover\:bg-indigo-50,.hover\:bg-indigo-100,.hover\:bg-indigo-200):hover { background: #1d234b; }
        :where(.dark) :is(.hover\:text-amber-200,.hover\:text-amber-300,.hover\:text-amber-400,.hover\:text-amber-500):hover { color: #fbbf24; }
        :where(.dark) :is(.hover\:text-amber-600,.hover\:text-amber-700,.hover\:text-amber-800,.hover\:text-amber-900):hover { color: #fcd34d; }
        :where(.dark) :is(.hover\:text-blue-200,.hover\:text-blue-300,.hover\:text-blue-400,.hover\:text-blue-500):hover { color: #60a5fa; }
        :where(.dark) :is(.hover\:text-blue-600,.hover\:text-blue-700,.hover\:text-blue-800,.hover\:text-blue-900):hover { color: #93c5fd; }
        :where(.dark) :is(.hover\:text-emerald-200,.hover\:text-emerald-300,.hover\:text-emerald-400,.hover\:text-emerald-500):hover { color: #34d399; }
        :where(.dark) :is(.hover\:text-emerald-600,.hover\:text-emerald-700,.hover\:text-emerald-800,.hover\:text-emerald-900):hover { color: #6ee7b7; }
        :where(.dark) :is(.hover\:text-orange-200,.hover\:text-orange-300,.hover\:text-orange-400,.hover\:text-orange-500):hover { color: #fb923c; }
        :where(.dark) :is(.hover\:text-orange-600,.hover\:text-orange-700,.hover\:text-orange-800,.hover\:text-orange-900):hover { color: #fdba74; }
        :where(.dark) :is(.hover\:text-purple-200,.hover\:text-purple-300,.hover\:text-purple-400,.hover\:text-purple-500):hover { color: #a78bfa; }
        :where(.dark) :is(.hover\:text-purple-600,.hover\:text-purple-700,.hover\:text-purple-800,.hover\:text-purple-900):hover { color: #c4b5fd; }
        :where(.dark) :is(.hover\:text-red-200,.hover\:text-red-300,.hover\:text-red-400,.hover\:text-red-500):hover { color: #f87171; }
        :where(.dark) :is(.hover\:text-red-600,.hover\:text-red-700,.hover\:text-red-800,.hover\:text-red-900):hover { color: #fca5a5; }
        :where(.dark) :is(.hover\:text-primary-200,.hover\:text-primary-300,.hover\:text-primary-400,.hover\:text-primary-500,.hover\:text-brand-200,.hover\:text-brand-300,.hover\:text-brand-400,.hover\:text-brand-500):hover { color: #60a5fa; }
        :where(.dark) :is(.hover\:text-primary-600,.hover\:text-primary-700,.hover\:text-primary-800,.hover\:text-brand-600,.hover\:text-brand-700,.hover\:text-brand-800):hover { color: #93c5fd; }
        :where(.dark) :is(.hover\:border-orange-100,.hover\:border-orange-200,.hover\:border-orange-300,.hover\:border-orange-400):hover { border-color: #7c4a1c; }
        :where(.dark) :is(.hover\:border-violet-100,.hover\:border-violet-200,.hover\:border-violet-300,.hover\:border-violet-400):hover { border-color: #45309f; }
        :where(.dark) :is(.hover\:border-primary-100,.hover\:border-primary-200,.hover\:border-primary-300,.hover\:border-primary-400,.hover\:border-brand-100,.hover\:border-brand-200,.hover\:border-brand-300,.hover\:border-brand-400):hover { border-color: #1e4a86; }
        /* The question-type controls added with the matching, true/false and
           drag-and-drop editors hover in rose, emerald and violet. A hover class
           is its own class, so without these the border snapped back to the pale
           light-mode hairline on a dark card — the same defect the base hues above
           exist to prevent. */
        :where(.dark) :is(.hover\:border-emerald-100,.hover\:border-emerald-200,.hover\:border-emerald-300,.hover\:border-emerald-400):hover { border-color: #1f6b4a; }
        :where(.dark) :is(.hover\:border-rose-100,.hover\:border-rose-200,.hover\:border-rose-300):hover { border-color: #86203a; }
        /* sky: the ordering question's chip bank hovers into it, the same way the
           drag & drop bank hovers into rose. */
        :where(.dark) :is(.hover\:border-sky-100,.hover\:border-sky-200,.hover\:border-sky-300):hover { border-color: #1c5a80; }
        /* teal: the complex multiple choice judgements hover into it, the same way
           the ordering bank hovers into sky. */
        :where(.dark) :is(.hover\:border-teal-100,.hover\:border-teal-200,.hover\:border-teal-300):hover { border-color: #1a6b64; }
        :where(.dark) :is(.hover\:text-rose-200,.hover\:text-rose-300,.hover\:text-rose-400,.hover\:text-rose-500):hover { color: #fb7185; }
        :where(.dark) :is(.hover\:text-rose-600,.hover\:text-rose-700,.hover\:text-rose-800,.hover\:text-rose-900):hover { color: #fda4af; }
        :where(.dark) :is(.hover\:border-violet-100,.hover\:border-violet-200,.hover\:border-violet-300):hover { border-color: #45309f; }

        /* ── Light gradient banners ───────────────────────────────────────────
           `bg-gradient-to-br from-emerald-50 to-white` is a subtle tint in light
           mode and a glowing band in dark mode. The stops compile to Tailwind's
           own --tw-gradient-* variables, which a stylesheet cannot set per
           element, so these are flattened to the tint's dark value instead —
           at 50→100 the gradient is nearly flat anyway. The neutral rule comes
           first so that `from-white to-primary-50` resolves to the hue.
           ═════════════════════════════════════════════════════════════════════ */
        :where(.dark) :is(.from-white,.to-white,.via-white,.from-surface-50,.from-slate-50,.to-slate-50,.from-stone-50,.to-stone-50) { background-image: none; background-color: var(--bg-subtle); }
        :where(.dark) :is(.from-card-50) { background-image: none; background-color: var(--bg-card); }
        :where(.dark) :is(.from-emerald-50,.from-emerald-100,.to-emerald-50,.to-emerald-100,.via-emerald-50) { background-image: none; background-color: #0d2b1e; }
        :where(.dark) :is(.from-red-50,.from-red-100,.to-red-50,.to-red-100,.via-red-50) { background-image: none; background-color: #2e1414; }
        :where(.dark) :is(.from-amber-50,.from-amber-100,.to-amber-50,.to-amber-100,.via-amber-50) { background-image: none; background-color: #2e2410; }
        :where(.dark) :is(.from-yellow-50,.from-yellow-100,.to-yellow-50,.to-yellow-100) { background-image: none; background-color: #2a2410; }
        :where(.dark) :is(.from-orange-50,.from-orange-100,.to-orange-50,.to-orange-100,.via-orange-50) { background-image: none; background-color: #2e1c0e; }
        :where(.dark) :is(.from-green-50,.from-green-100,.to-green-50,.to-green-100) { background-image: none; background-color: #0f2a18; }
        :where(.dark) :is(.from-teal-50,.from-teal-100,.to-teal-50,.to-teal-100) { background-image: none; background-color: #0d2a28; }
        :where(.dark) :is(.from-cyan-50,.from-cyan-100,.to-cyan-50,.to-cyan-100) { background-image: none; background-color: #0a2a30; }
        :where(.dark) :is(.from-sky-50,.from-sky-100,.to-sky-50,.to-sky-100) { background-image: none; background-color: #0e2436; }
        :where(.dark) :is(.from-blue-50,.from-blue-100,.to-blue-50,.to-blue-100,.via-blue-50) { background-image: none; background-color: #101f3a; }
        :where(.dark) :is(.from-indigo-50,.from-indigo-100,.to-indigo-50,.to-indigo-100) { background-image: none; background-color: #161a38; }
        :where(.dark) :is(.from-violet-50,.from-violet-100,.to-violet-50,.to-violet-100,.via-violet-50) { background-image: none; background-color: #1e1436; }
        :where(.dark) :is(.from-purple-50,.from-purple-100,.to-purple-50,.to-purple-100,.via-purple-50) { background-image: none; background-color: #1e1436; }
        :where(.dark) :is(.from-fuchsia-50,.from-fuchsia-100,.to-fuchsia-50,.to-fuchsia-100) { background-image: none; background-color: #2e1030; }
        :where(.dark) :is(.from-pink-50,.from-pink-100,.to-pink-50,.to-pink-100) { background-image: none; background-color: #2e1024; }
        :where(.dark) :is(.from-rose-50,.from-rose-100,.to-rose-50,.to-rose-100) { background-image: none; background-color: #2e1220; }
        :where(.dark) :is(.from-primary-50,.from-primary-100,.to-primary-50,.to-primary-100,.via-primary-50,.from-brand-50,.from-brand-100,.to-brand-50,.to-brand-100) { background-image: none; background-color: #101f3a; }

        /* ── Chrome that is dark in both modes ── */
        /* #475569 was 2.36:1 on the sidebar gradient, in light mode too. The
           sidebar is always dark, so one value serves both themes. */
        .nav-section-title { color: #8892b0; }
        /* The scrollbar's dark half is the `.dark` token block's `--sb-*` values
           (see "The scrollbar" above), which is why nothing here draws a bar:
           the five `.dark::-webkit-scrollbar-*` rules this replaced were the
           page-bar/inner-pane defect written out twice by hand. What is left is
           the one thing a token cannot do — the Firefox pair, which must stay
           inside the Firefox gate or Chrome 121+ drops ::-webkit-scrollbar. */
        @supports (-moz-appearance: none) { .dark { scrollbar-color: var(--sb-thumb) var(--sb-track); } }
        }

        /* ── The bottom tab bar on a 320px phone ──────────────────────────────
           Measured at 320px with the six items the super-admin bar carries:
           64 / 47 / 36 / 78 / 60 / 36. Six labels do not fit there, and because
           every item is `flex-1` with `min-width: auto`, the two shortest ones
           ("Files", "Dark") were crushed to 36px — below the 44px the app holds
           every other control to, and narrow enough that a thumb lands on the
           neighbour and navigates somewhere else.

           Under 360px the labels are hidden *visually* rather than removed: every
           item measures 53×44 there (60×44 at 359), and each still carries its
           name for a screen reader. The bar reads as an icon strip on the smallest
           phones and is untouched from 360px up, where the labels fit — measured
           at 375px, every item is already 58-78px wide. */
        @media (max-width: 359px) {
            .sg-app-bottomnav > a > span,
            .sg-app-bottomnav > button > span {
                position: absolute;
                width: 1px;
                height: 1px;
                overflow: hidden;
                clip-path: inset(50%);
                white-space: nowrap;
            }
            .sg-app-bottomnav > a,
            .sg-app-bottomnav > button {
                position: relative;
                min-width: 44px;
                /* The label was the item's height as much as its width: taking
                   it out of the flow dropped the bar to 36px tall, which is
                   under the floor in the other axis. */
                min-height: 44px;
            }
        }

        /* Print never carries a page wash. The token reset above already fixes the
           text colours; this stops a slate body being printed on the browsers that
           do print backgrounds. */
        @media print {
            body { background: #ffffff !important; color: #111827 !important; }
            .no-print { display: none !important; }
        }

        /* ── skeleton screens ─────────────────────────────────────────────────
           The page that waits for data used to say so with a spinner in the
           middle of nothing: the layout arrived only when the fetch settled, so
           the reader watched an empty column and then the whole page jumped.
           A skeleton is the other half of that trade — the shape of what is
           coming, drawn immediately, so the wait is about *content* rather than
           about whether anything is happening at all.

           The fill is Tailwind's (`bg-surface-100`), which the dark theme
           remaps, so a skeleton follows whichever theme is on; what this file
           adds is the sweep, which cannot be expressed as a utility, and the
           cell shapes below. The sheen is a gradient of the *slate* tone at low
           alpha rather than white, because a white sweep is invisible on the
           light card and a stripe of paint on the dark one; a translucent
           slate reads as a soft band on both.

           Motion is decoration here, so it is the first thing surrendered: a
           reader who asked the system for less movement gets the shape and the
           wait, without the sweep. */
        /* The band is the cell's *own* background, moved across it, and that is
           deliberate rather than incidental: a pseudo-element needs
           `position: absolute`, and an absolute element inside a box is a panel
           the layer scale has to account for (see `test_layer_scale.py`). A
           background is clipped to the cell by the box itself, so the sweep
           cannot cover anything else however the page is arranged — which is
           the property the layer rule is there to guarantee. Its colour and
           opacity are the only choices left, and both are about the two themes. */
        .sg-skel {
            background-image: linear-gradient(90deg,
                                              transparent,
                                              rgba(148, 163, 184, 0.28),
                                              transparent);
            background-repeat: no-repeat;
            background-size: 200% 100%;
            background-position: -100% 0;
            animation: sg-skel-sweep 1.4s ease-in-out infinite;
        }
        @keyframes sg-skel-sweep {
            0%   { background-position: -100% 0; }
            100% { background-position:  200% 0; }
        }
        @media (prefers-reduced-motion: reduce) {
            .sg-skel { animation: none; }
        }

        /* The cell shapes, kept here rather than as utilities on purpose. A
           skeleton is one pattern used in many places: if each page picked its
           own `h-3` and `rounded`, the same placeholder would be a different
           height on every screen, and the column it is holding open would move
           when the data landed — which is the jump the pattern exists to stop.
           The fill is deliberately *not* set here: pages use `bg-surface-100`,
           which the dark theme remaps. A height and a radius are the same in
           both themes; a colour is not. */
        .sg-skel-line,
        .sg-skel-title,
        .sg-skel-figure,
        .sg-skel-label {
            width: 100%;
        }
        .sg-skel-line   { height: .75rem;   border-radius: .375rem; }
        .sg-skel-title  { height: 1rem;     border-radius: .375rem; width: 11rem; }
        .sg-skel-figure { height: 2rem;     border-radius: .5rem;   width: 2.5rem; }
        .sg-skel-label  { height: .625rem;  border-radius: .25rem;  width: 6rem; }
        /* The ragged right edge of a paragraph. Three steps are enough to read
           as text and not as a table, and they are named rather than inline so
           the same three appear wherever a paragraph is being held open. */
        .sg-skel-line-90 { width: 90%; }
        .sg-skel-line-80 { width: 80%; }
