    /*
     * Roboto, served from this application.
     *
     * **Nothing is fetched from Google.** A `fonts.googleapis.com` link is two
     * round trips before a single character can be painted — one for the CSS,
     * one for the font it names — on connections this product's schools do not
     * have. It is also an availability dependency for a system that has to keep
     * working when the office line is down, and it tells a third party which
     * school opened which screen and when.
     *
     * Roboto ships as a **variable** font, so one 43 KB file carries every
     * weight from 400 to 900 — the whole scale for less than a single static
     * weight would have cost. `latin-ext` is a second declaration with its own
     * `unicode-range`, which means the browser fetches it only if a character
     * in that range actually appears on the page; an English or Roman-Urdu
     * screen never downloads it at all.
     *
     * `font-display: swap` — text is readable in the fallback immediately and
     * re-renders in Roboto when it arrives, rather than a blank screen while a
     * font loads. The fallback stack below starts with the system UI font, so
     * the swap is a small weight shift and not a change of typeface.
     */
    @font-face {
        font-family: "Roboto";
        font-style: normal;
        font-weight: 100 900;
        font-stretch: 100%;
        font-display: swap;
        src: url("/fonts/roboto/roboto-latin.woff2") format("woff2");
        unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
            U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
            U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
    }
    @font-face {
        font-family: "Roboto";
        font-style: normal;
        font-weight: 100 900;
        font-stretch: 100%;
        font-display: swap;
        src: url("/fonts/roboto/roboto-latin-ext.woff2") format("woff2");
        unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
            U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
            U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
            U+A720-A7FF;
    }

    :root {
        /* ---- brand: the one place a school's colour enters (FR-041) ---- */
        --accent: #2563eb;
        --accent-strong: #1d4ed8;
        --accent-soft: #eaf2ff;
        --accent-ink: #ffffff;
        /* Link and label text, separate from the accent a button is filled
           with: a colour chosen to carry white text is by definition too dark
           to *be* text on a dark ground. */
        --accent-text: var(--accent);

        /* ---- neutrals: Tailwind's own grey ramp ----
           The reference template is built entirely out of `gray-100` through
           `gray-900`, so the ramp *is* those values. Naming them here rather
           than reaching for `bg-gray-800` in markup keeps the one-token rule:
           the dark theme below still only has to move eleven lines. */
        --n-0:  #ffffff;
        --n-25: #f9fafb;   /* gray-50  */
        --n-50: #f9fafb;   /* gray-50  */
        --n-100:#f3f4f6;   /* gray-100 — the page itself */
        --n-200:#e5e7eb;   /* gray-200 */
        --n-300:#d1d5db;   /* gray-300 */
        --n-400:#9ca3af;   /* gray-400 */
        --n-500:#6b7280;   /* gray-500 */
        --n-600:#4b5563;   /* gray-600 — a card header's label */
        --n-700:#374151;   /* gray-700 — table body text */
        --n-800:#1f2937;   /* gray-800 — the frame: sidebar and header */
        --n-900:#111827;   /* gray-900 */

        /* ---- the frame ----
           The template puts every control surface on one dark grey and reserves
           a blue-to-grey gradient for the band that names the page. Named here
           because four components read them and none of them should be holding
           its own copy of #1f2937. */
        /* Literals, not ramp references: the dark theme inverts the ramp, and
           the frame is gray-800 in both themes — the template's rail is dark
           because it is the frame, not because the page is light. */
        --chrome:      #1f2937;
        --chrome-deep: #111827;
        --chrome-ink:  #9ca3af;
        /* A card header's gradient: gray-300 down to gray-100. Ramp
           references, so a dark theme's headers darken with its cards. */
        --head-from: var(--n-300);
        --head-to:   var(--n-100);
        /* The page-title band: blue-900 across to gray-800. */
        --title-from:#1e3a8a;
        --title-to:  #1f2937;
        /* A table heading. Blue-900 on light; lifted on dark, where it would
           otherwise be navy text on near-black. */
        --th-ink: #1e3a8a;

        /* The page itself, named separately from the ramp so the dark theme
           can move it without moving every surface that sits on it. The
           template's content column is `bg-gray-100`, a step darker than the
           white cards on it — which is what makes the cards read as cards
           without needing a border. */
        --page: var(--n-100);
        --ink: var(--n-800);

        /* ---- semantic ---- */
        --ok-bg:#e9f8ef;   --ok-ink:#15803d;  --ok-line:#b7e6c8;
        --warn-bg:#fff8e8; --warn-ink:#b45309;--warn-line:#fadfa4;
        --bad-bg:#fef2f2;  --bad-ink:#b91c1c; --bad-line:#f6c9c9;
        --info-bg:#eaf2ff; --info-ink:#1d4ed8;--info-line:#c5dbfb;

        /* ---- type ---- */
        /* Tailwind's own `font-sans`, which is what the template sets on
           `body`. The system face leads and the locally served Roboto stays in
           the chain behind it, so a machine with no system UI face still gets
           the metrics every screen was laid out against. */
        --font: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
                "Segoe UI", "Roboto", "Helvetica Neue", Arial, "Noto Sans",
                sans-serif;
        --t-xs: .6875rem;   /* 11px — micro labels */
        --t-sm: .75rem;     /* 12px — meta */
        --t-cell: .8125rem; /* 13px — table body, kept dense on purpose */
        --t-md: .875rem;    /* 14px — default UI text */
        --t-lg: 1rem;       /* 16px — card titles */
        --t-xl: 1.5rem;     /* 24px — page title (the template's text-2xl) */
        --t-2xl:1.875rem;   /* 30px — metrics */

        /* ---- space: a 4px scale, no arbitrary values ---- */
        --s-1:.25rem; --s-2:.5rem; --s-3:.75rem; --s-4:1rem;
        --s-5:1.5rem; --s-6:2rem; --s-7:3rem;

        /* ---- shape: Tailwind's radii, which is all the template uses ----
           `rounded-lg` on every card, `rounded-full` on the icon discs, and
           one `rounded-tl-3xl` on the page-title band. The large radii this
           file used to carry are gone: a 24px corner is a different product
           from the reference. */
        --r-sm:.25rem; --r-md:.375rem; --r-lg:.5rem; --r-xl:.5rem;
        --r-panel:.5rem; --r-full:9999px;
        --r-band:1.5rem;    /* rounded-3xl, on the title band's top-left only */

        /* Tailwind's `shadow` and `shadow-xl`. The template puts `shadow-xl`
           on every card, so the heavier one is the default here rather than
           the exception it was. */
        --shadow-1: 0 1px 3px 0 rgba(0,0,0,.1), 0 1px 2px 0 rgba(0,0,0,.06);
        --shadow-2: 0 20px 25px -5px rgba(0,0,0,.1), 0 10px 10px -5px rgba(0,0,0,.04);
        --shadow-accent: 0 10px 15px -3px rgba(0,0,0,.1), 0 4px 6px -2px rgba(0,0,0,.05);

        /* The template's rail is `md:w-48` (12rem). That cuts "Teacher
           assignments" in half here, and a menu entry a reader has to guess at
           is worse than a slightly wider rail — so 15rem, which is the
           narrowest that still holds the longest entry. */
        --nav-w: 15rem;

        color-scheme: light;
    }

    /*
     * Dark, opt-in. Only the ramp and the semantic tints move; not one
     * component rule below is duplicated here, which is the whole argument for
     * having had a token layer in the first place.
     *
     * The tints are rebuilt rather than darkened algorithmically: a `warn`
     * panel that is merely a dimmer yellow reads as a disabled control, and the
     * one thing a warning must not look like is something switched off.
     */
    :root[data-theme="dark"] {
        --n-0:  #111827;
        --n-25: #131c2c;
        --n-50: #16202f;
        --n-100:#1b2637;
        --n-200:#243044;
        --n-300:#31405a;
        --n-400:#64748b;
        --n-500:#94a3b8;
        --n-600:#b6c2d3;
        --n-700:#d3dcea;
        --n-800:#e8eef8;
        --n-900:#ffffff;

        --page: #0b1220;
        --ink: var(--n-800);

        --accent-soft: #14264a;
        --accent-ink: #ffffff;
        /* The school's own hue, lifted until it reads on a dark ground —
           rather than a fixed light blue, which would throw away FR-041's
           branding the moment anybody switched themes. */
        --accent-text: color-mix(in srgb, var(--accent) 45%, #ffffff);

        --ok-bg:#0f2a1c;   --ok-ink:#6ee7a8;  --ok-line:#1d5137;
        --warn-bg:#2e2208; --warn-ink:#fbbf24;--warn-line:#5b4312;
        --bad-bg:#2c1416;  --bad-ink:#fca5a5; --bad-line:#5c2326;
        --info-bg:#14264a; --info-ink:#93c5fd;--info-line:#25406e;

        /* Blue-900 is unreadable on near-black, so the heading ink lifts to
           the shade the links already use. */
        --th-ink: #93c5fd;

        --shadow-1: 0 1px 2px rgba(0,0,0,.30), 0 8px 20px rgba(0,0,0,.28);
        --shadow-2: 0 18px 44px rgba(0,0,0,.45);
        --shadow-accent: 0 10px 24px rgba(37,99,235,.35);

        color-scheme: dark;
    }

    *, *::before, *::after { box-sizing: border-box; }

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

    body {
        margin: 0;
        font-family: var(--font);
        font-size: var(--t-md);
        line-height: 1.6;
        color: var(--ink);
        background: var(--page);
        -webkit-font-smoothing: antialiased;
    }

    a { color: var(--accent-text); text-decoration: none; }
    a:hover { text-decoration: underline; }

    h1, h2, h3 { margin: 0; font-weight: 800; letter-spacing: -.015em; line-height: 1.25; }
    h1 { font-size: var(--t-xl); }
    h2 { font-size: var(--t-lg); }

    /* ================= app shell ================= */
    .shell { display: flex; min-height: 100vh; }
    /* The page never scrolls sideways. Anything that genuinely needs more
       width than the screen — a wide table — carries its own scroller, and a
       document that scrolls as well as the table inside it is a document
       nobody can operate with a thumb. */
    body { overflow-x: hidden; }

    /*
     * A floating panel rather than a flush one: inset from the window on all
     * four sides with the page showing around it, so the navigation reads as
     * a card the screens sit beside rather than as the left edge of the
     * browser.
     *
     * Sticky inside the flex row, not fixed. Fixed would take the panel out
     * of the flow and every screen would need a left offset that has to be
     * kept in step with the width — and there are two widths now, because it
     * collapses.
     */
    .sidebar {
        width: var(--nav-w);
        flex: 0 0 var(--nav-w);
        /* One flat grey, flush to the window. The template's rail is not a
           floating card: it runs the full height against the left edge with
           the content column butted up to it, and the inset panel this used to
           be is the single biggest reason the two did not look alike. */
        background: var(--chrome);
        color: var(--chrome-ink);
        display: flex;
        flex-direction: column;
        position: sticky;
        top: 0;
        height: 100vh;
        margin: 0;
        border-radius: 0;
        box-shadow: var(--shadow-2);
        /* Deliberately not `overflow: hidden`, tempting as it is for the
           corners: the collapsed panel's tooltips are drawn outside this box
           and clipping is what would make them invisible. */
        transition: width 260ms ease, flex-basis 260ms ease;
    }

    /*
     * Collapsed: icons only, and only where there is a panel to collapse.
     *
     * Every rule here is inside the desktop query on purpose. Below 52rem the
     * panel is a drawer you open and close, and a drawer that also has to be
     * widened is two gestures for one menu — so the state simply does not
     * exist there, rather than existing and being undone rule by rule.
     *
     * The groups are hidden rather than shrunk — a `<details>` summary with
     * no room for its label is a control that says nothing — and the script
     * opens every one of them on the way in, so the icons behind them stay
     * reachable. Each row then names itself on hover.
     */
    @media (min-width: 52.0625rem) {
        :root.nav-collapsed .sidebar {
            width: 4.75rem;
            flex-basis: 4.75rem;
        }
        :root.nav-collapsed .sidebar .brand-text,
        :root.nav-collapsed .sidebar .brand-name,
        :root.nav-collapsed .sidebar .brand-sub,
        :root.nav-collapsed .sidebar .sidebar-foot,
        :root.nav-collapsed .sidebar .nav-group > summary,
        /* The console's menu is one flat list under plain headings rather
           than `<details>` groups; those headings go the same way. */
        :root.nav-collapsed .sidebar .nav > .nav-label { display: none; }
        /* The mark stays — it is the school — and the toggle drops under it,
           because side by side in 4.75rem neither of them fits. */
        :root.nav-collapsed .sidebar .sidebar-brand {
            flex-direction: column; align-items: center; justify-content: center;
            gap: var(--s-2); padding: var(--s-4) var(--s-2);
        }
        :root.nav-collapsed .sidebar .nav { padding-left: var(--s-2); padding-right: var(--s-2); }
        :root.nav-collapsed .sidebar .nav a { position: relative; justify-content: center; padding: .85rem 0; }
        /* The icon is an `<svg>`; the label is the only span on the row. */
        :root.nav-collapsed .sidebar .nav a > span { display: none; }

        /* The label, as a tooltip, only while there is no room for the real one. */
        :root.nav-collapsed .sidebar .nav a::after {
            content: attr(data-label);
            position: absolute; left: calc(100% + .75rem); top: 50%;
            transform: translateY(-50%);
            z-index: 70;
            padding: .3rem .6rem;
            border-radius: var(--r-md);
            background: var(--n-900); color: #fff;
            font-size: var(--t-sm); font-weight: 600;
            white-space: nowrap;
            box-shadow: var(--shadow-2);
            opacity: 0; pointer-events: none;
            transition: opacity 140ms ease;
        }
        :root.nav-collapsed .sidebar .nav a:hover::after,
        :root.nav-collapsed .sidebar .nav a:focus-visible::after { opacity: 1; }
    }

    .sidebar-brand {
        display: flex; align-items: center; gap: var(--s-3);
        padding: var(--s-4);
        border-bottom: 1px solid rgba(255,255,255,.08);
    }

    /* The collapse control. White on the dark panel, because it is the one
       thing on the header that is a button rather than a label. */
    .nav-collapse {
        margin-left: auto;
        flex: 0 0 1.85rem;
        width: 1.85rem; height: 1.85rem;
        display: flex; align-items: center; justify-content: center;
        border: 0; border-radius: var(--r-md);
        background: rgba(255,255,255,.1); color: #fff;
        cursor: pointer;
        transition: background 160ms ease, transform 260ms ease;
    }
    .nav-collapse:hover { background: #fff; color: #151a2d; }
    :root.nav-collapsed .sidebar .nav-collapse { margin-left: 0; transform: rotate(180deg); }
    .nav-collapse svg { width: 1.1rem; height: 1.1rem; display: block; }
    .brand-badge {
        width: 2.75rem; height: 2.75rem; flex: 0 0 2.75rem;
        border-radius: var(--r-lg);
        background: var(--accent); color: #fff;
        display: flex; align-items: center; justify-content: center;
        font-weight: 800; font-size: var(--t-md); letter-spacing: .02em;
        box-shadow: var(--shadow-accent);
    }
    .brand-text { flex: 1 1 auto; min-width: 0; }
    .brand-name {
        display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3;
        color: #fff; font-weight: 700; font-size: var(--t-lg);
        line-height: 1.25;
        overflow: hidden; overflow-wrap: anywhere;
    }
    .brand-sub {
        display: block; font-size: var(--t-sm); color: #94a3b8;
        overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }

    .nav { padding: var(--s-3) var(--s-3) var(--s-4); flex: 1; overflow-y: auto; }
    /* A sixty-entry list scrolls. A scrollbar the colour of the dark panel is
       a scrollbar nobody can find. */
    .nav { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.22) transparent; }
    .nav::-webkit-scrollbar { width: 8px; }
    .nav::-webkit-scrollbar-thumb { background: rgba(255,255,255,.18); border-radius: 999px; }
    .nav::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,.3); }

    /* The band that opens a section. One step down from a row rather than
       three: at 11px it read as a caption on the rows under it instead of as
       the heading over them. */
    .nav-label {
        font-size: var(--t-xs); text-transform: uppercase; letter-spacing: .08em;
        color: var(--n-500); font-weight: 700;
        padding: var(--s-4) var(--s-2) var(--s-2);
    }

    /*
     * FR-325a — the menu is a set of tabs, not one list of sixty links.
     *
     * A school's menu has ten sections in it. Shown all at once it is a wall a
     * person has to read past every time they want the one screen they came
     * for — and the section they want is usually below the fold. Each section
     * is now a heading that opens; the one holding the page you are on opens
     * itself, and the rest stay shut until you ask.
     *
     * `<details>`, not script: it works on the first paint, and it still works
     * for somebody whose browser blocks JavaScript.
     */
    .nav-group > summary {
        display: flex; align-items: center; gap: var(--s-2);
        cursor: pointer; list-style: none;
        border-radius: var(--r-md);
        user-select: none;
    }
    .nav-group > summary::-webkit-details-marker { display: none; }
    .nav-group > summary:hover { color: #fff; background: transparent; }
    .nav-group > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
    .nav-group > summary .nav-caret {
        margin-left: auto; flex: 0 0 auto;
        transition: transform 160ms ease;
        opacity: .7;
    }
    .nav-group[open] > summary .nav-caret { transform: rotate(90deg); }
    /* A folder inside a folder: Settings opens to a few headings, and each
       heading opens to its own links. */
    .nav-group.nav-sub { margin-left: var(--s-3); border-left: 1px solid rgba(255,255,255,.12); }
    .nav-group.nav-sub > summary { padding: var(--s-2) var(--s-2); font-size: .8125rem; }
    .nav-group.nav-sub > a { padding-left: var(--s-3); font-size: .875rem; }
    /* A section holding the page you are on is marked, so a shut menu still
       says where you are. */
    .nav-group.is-here > summary { color: #fff; }

    /*
     * An icon on every row, on one grid and one stroke weight. Sixty labels in
     * a column are sixty words to read; sixty icons are a shape you learn
     * once and then aim at — which only works if the icon is large enough to
     * be a shape rather than a smudge.
     *
     * **Each row is its own chip.** A flat list of sixty rows separated by
     * nothing is a wall of text you re-read every time; a tinted, rounded
     * block per row with air between them is a row you aim at from memory
     * after a week. 16px of vertical padding, 20px icons and 16px labels are
     * the sizes that stopped it reading as a dense index.
     */
    /*
     * A row is a line with a rule under it, not a filled chip.
     *
     * The template's rails are `border-b-2 border-gray-800` — the same grey as
     * the panel, so invisible — turning the section's own colour on hover and
     * on the row you are on. That is the whole mechanic: nothing is filled,
     * and the colour arrives on one edge. Filled pills were this file's own
     * idea and they are what made the menu read as a different product.
     */
    .nav a {
        display: flex; align-items: center; gap: var(--s-3);
        padding: .7rem var(--s-2);
        margin-bottom: 0;
        border-radius: 0;
        border-bottom: 2px solid transparent;
        background: none;
        color: var(--chrome-ink);
        font-size: var(--t-md);
        line-height: 1.3;
        transition: color 160ms ease, border-color 160ms ease;
    }
    .nav a > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .nav a .icon { flex: 0 0 1.125rem; width: 1.125rem; height: 1.125rem; opacity: 1; }

    .nav a:hover { background: none; color: #fff; text-decoration: none; }

    /* The current page is stated, not implied by a hover colour. */
    .nav a[aria-current="page"] {
        background: none; color: #fff; font-weight: 600;
        border-bottom-color: var(--accent);
        box-shadow: none;
    }

    .icon { display: block; width: 1.125rem; height: 1.125rem; }

    .sidebar-foot {
        padding: var(--s-3);
        border-top: 1px solid rgba(255,255,255,.08);
        font-size: var(--t-sm);
    }
    .sidebar-foot .who { color: #fff; font-weight: 600; }
    .sidebar-foot .role { color: var(--n-500); font-size: var(--t-xs); }
    .sidebar-foot .btn-secondary {
        background: rgba(255,255,255,.08); color: #e2e8f0;
        border-color: rgba(255,255,255,.16);
    }
    .sidebar-foot .btn-secondary:hover { background: rgba(255,255,255,.14); }
    .sidebar { --chrome-ink: #e5e7eb; }
    .sidebar .nav a, .sidebar .nav-group > summary { font-size: .9375rem; }
    .sidebar .nav-label { font-size: .8125rem; color: #cbd5e1; }
    .sidebar .brand-sub, .sidebar-foot { font-size: .875rem; color: #cbd5e1; }

    /* Reference theme's own stack and body size (10.5pt/11.5pt, Roboto
       first), scoped to the frame rather than moved globally. */
    .sidebar, .topbar {
        font-family: 'Roboto', sans-serif, system-ui, -apple-system, "Segoe UI",
            "Helvetica Neue", Arial, "Noto Sans", sans-serif;
        font-size: .875rem;
    }

    .sidebar-search { padding: 0 var(--s-3) var(--s-2); }
    .sidebar-search .omnibox { display: block; flex: none; width: 100%; min-width: 0; }
    .sidebar-search .omnibox-results { max-height: 16rem; }
    .sidebar-search .omnibox input[type="search"] {
        border-radius: 30px; border: 1px solid var(--n-300); padding: .4rem 1rem;
    }

    .sidebar-session {
        display: flex; align-items: center; justify-content: space-between; gap: var(--s-2);
        margin: 0 var(--s-3) var(--s-2); padding: var(--s-2) var(--s-3);
        background: rgba(255,255,255,.06); border-radius: var(--r-md);
        color: #fff; font-size: var(--t-sm); font-weight: 600;
    }
    .sidebar-session:hover { background: rgba(255,255,255,.12); text-decoration: none; color: #fff; }
    .sidebar-session .icon { flex: 0 0 1rem; width: 1rem; height: 1rem; opacity: .8; }

    :root.nav-collapsed .sidebar-search,
    :root.nav-collapsed .sidebar-session { display: none; }
    .sidebar-foot .role { font-size: .8125rem; color: #cbd5e1; }

    .main { flex: 1; min-width: 0; display: flex; flex-direction: column; }

    /*
     * The header is the second half of the frame, on the same grey as the rail.
     *
     * The template's header holds the search box and the account menu and
     * nothing else — the page's name lives on the band below it, inside the
     * content column, where `.page-head` draws it. Keeping the title up here as
     * well would state it twice.
     */
    .topbar {
        background: var(--chrome);
        border-bottom: 0;
        padding: var(--s-2) var(--s-4);
        display: flex; align-items: center; gap: var(--s-4);
        position: sticky; top: 0; z-index: 20;
    }
    .topbar .grow { flex: 1; min-width: 0; }
    .topbar h1 { font-size: var(--t-md); color: #fff; font-weight: 600; }
    .topbar .crumb {
        font-size: var(--t-md); color: var(--chrome-ink);
        font-weight: 700; text-transform: uppercase; letter-spacing: .09em;
    }

    /*
     * The band that names the page: `rounded-tl-3xl bg-gradient-to-r
     * from-blue-900 to-gray-800`, sitting on a strip of the header's grey so
     * the big top-left corner has something to be cut out of.
     */
    .page-head-wrap { background: var(--chrome); padding-top: var(--s-3); }
    .page-head {
        border-top-left-radius: var(--r-band);
        background: linear-gradient(90deg, var(--title-from) 0%, var(--title-to) 100%);
        padding: var(--s-4);
        box-shadow: var(--shadow-1);
        color: #fff;
    }
    .page-head h1 {
        font-size: var(--t-xl); font-weight: 700; color: #fff;
        padding-left: var(--s-2); letter-spacing: -.01em;
    }

    /* ================= theme toggle =================
       Two buttons rather than one that changes meaning: a single control
       labelled with the theme you are *not* in is a coin-flip every time. */
    .theme-toggle {
        display: inline-flex; gap: 2px; padding: 2px; width: 100%;
        background: var(--n-100);
        border: 1px solid var(--n-200);
        border-radius: var(--r-full);
    }
    .theme-toggle button {
        border: 0; background: none; cursor: pointer;
        flex: 1;
        font: inherit; font-size: var(--t-xs); font-weight: 700;
        color: var(--n-600);
        padding: .25rem .6rem; border-radius: var(--r-full);
        line-height: 1.6;
    }
    .theme-toggle button[aria-pressed="true"] { background: var(--accent); color: #fff; }

    /* ================= omnibox (FR-086) =================
       One search box in the header, on every screen. Anchored so its results
       overlay the page rather than pushing it down — a list that jumps as you
       type is one you stop trusting. */
    .omnibox { position: relative; flex: 0 1 26rem; min-width: 10rem; }
    /*
     * On the dark header, as the template draws it:
     * `bg-gray-900 text-white border border-transparent
     *  focus:border-gray-400 rounded`.
     */
    .omnibox input[type="search"] {
        width: 100%;
        padding: .5rem .9rem;
        font: inherit; font-size: var(--t-md);
        color: #fff;
        background: var(--chrome-deep);
        border: 1px solid transparent; border-radius: var(--r-md);
    }
    .omnibox input[type="search"]::placeholder { color: var(--chrome-ink); }
    .omnibox input[type="search"]:focus {
        outline: none;
        background: var(--chrome-deep);
        border-color: #9ca3af;
        box-shadow: none;
    }
    .omnibox-results {
        position: absolute; top: calc(100% + .4rem); left: 0; right: 0; z-index: 40;
        max-height: 22rem; overflow-y: auto;
        background: var(--n-0);
        border: 1px solid var(--n-200); border-radius: var(--r-lg);
        box-shadow: var(--shadow-2);
    }
    .omnibox-hit {
        display: flex; flex-direction: column; gap: .1rem;
        padding: var(--s-2) var(--s-3);
        font-size: var(--t-md);
        border-bottom: 1px solid var(--n-100);
        text-decoration: none; color: var(--n-800);
    }
    .omnibox-hit:last-child { border-bottom: 0; }
    .omnibox-hit:hover { background: var(--n-50); text-decoration: none; }
    .omnibox-hit .muted { font-size: var(--t-xs); }
    .omnibox-empty { padding: var(--s-3); font-size: var(--t-sm); color: var(--n-500); }
    /* The box answers about several kinds of thing at once, so each kind is
       labelled: a bare list mixing a child, a challan number and a screen name
       makes the reader work out which is which from the text alone. */
    .omnibox-group {
        padding: var(--s-2) var(--s-3) .2rem;
        font-size: var(--t-xs); font-weight: 600; text-transform: uppercase;
        letter-spacing: .04em; color: var(--n-500);
        background: var(--n-50);
        border-bottom: 1px solid var(--n-100);
    }
    @media (max-width: 52rem) { .omnibox { display: none; } }

    .content { padding: var(--s-5); max-width: 84rem; width: 100%; }

    /* ================= surfaces ================= */
    /* `bg-white border-transparent rounded-lg shadow-xl` — the card carries no
       border at all in the template. It reads as a card because the page under
       it is gray-100 and the shadow is deep. */
    .card {
        background: var(--n-0);
        border: 1px solid transparent;
        border-radius: var(--r-lg);
        box-shadow: var(--shadow-2);
        /* Clips a flush table's square corners to the card's radius. Nothing
           in the application positions anything outside a card — the one
           overlay, the omnibox, lives in the topbar. */
        overflow: hidden;
    }
    .card + .card { margin-top: var(--s-4); }
    /*
     * `bg-gradient-to-b from-gray-300 to-gray-100 uppercase text-gray-800
     * border-b-2 border-gray-300 rounded-tl-lg rounded-tr-lg p-2`.
     *
     * The filled grey band is the template's signature on every panel, and it
     * is the opposite of the call this file made before — a hairline along the
     * top edge, chosen so thirty cards would not read as a paint chart. With
     * the band in grey rather than the section's colour, thirty of them do not.
     */
    .card-head {
        padding: var(--s-2) var(--s-3);
        background: linear-gradient(180deg, var(--head-from) 0%, var(--head-to) 100%);
        border-bottom: 2px solid var(--n-300);
        border-top-left-radius: var(--r-lg);
        border-top-right-radius: var(--r-lg);
        display: flex; align-items: center; gap: var(--s-3);
    }
    .card-head h2 { flex: 1; }
    .card-head .strong,
    .card-head h2 {
        font-size: var(--t-md); font-weight: 700;
        text-transform: uppercase; letter-spacing: .04em;
        color: var(--n-600);
    }
    .card-body { padding: var(--s-5); }

    /* Above the labels in the hierarchy and unmistakably a different kind of
       thing: still upper case and tracked out, but 12px and darker, so it
       reads as the band that opens a group rather than as another label. */
    .section-title {
        font-size: var(--t-md); text-transform: uppercase; letter-spacing: .05em;
        color: var(--n-600); font-weight: 700;
        margin: var(--s-5) 0 var(--s-3);
    }

    /* ================= metrics ================= */
    .metrics {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
        gap: var(--s-3);
        margin-bottom: var(--s-4);
    }
    /* The same card as the dashboard's headline tiles, at the size a profile
       header wants: the pastel wash, the saturated bottom edge, `shadow-xl`.
       See the note on `.tile`. */
    .metric {
        background: linear-gradient(180deg, var(--tone-line, var(--n-100)) 0%, var(--tone-bg, var(--n-50)) 100%);
        border: 1px solid transparent;
        border-bottom: 4px solid var(--tone-b, var(--n-400));
        border-radius: var(--r-lg);
        padding: var(--s-4) var(--s-5);
        box-shadow: var(--shadow-2);
        /* A grid item's default `min-width: auto` refuses to go below its
           content, which is how a long figure pushes a card wider than its
           track and out past the card beside it. */
        min-width: 0;
        overflow: hidden;
        /* The card is the reference for the figure inside it — see the note
           below. Without this the `cqi` units fall back to the viewport,
           which is the one measurement that says nothing about how much room
           the number actually has (C-71). */
        container-type: inline-size;
    }
    /*
     * **No amount leaves its box, and no amount is ever broken in half.**
     *
     * The first attempt sized these against the *viewport* and let long
     * figures wrap. On a wide screen the viewport is large, so the clamp
     * stayed at its maximum and `1,875,987.50` wrapped to a second line as
     * "1,875,987." and "50" — which is not a smaller number, it is two wrong
     * ones. A reader takes the first line as the figure.
     *
     * The size is now taken from the **card**, with container query units:
     * 1cqi is 1% of this card's own width, so a narrow card gets small digits
     * and a wide one gets large, with no reference to the window at all. And
     * numbers do not wrap — if a figure will not fit, it shrinks.
     */
    .metric .n {
        font-size: 1.55rem;
        font-weight: 700; line-height: 1.2;
        color: var(--n-900); font-variant-numeric: tabular-nums;
        letter-spacing: -.02em;
        min-width: 0;
        overflow-wrap: normal;
        word-break: keep-all;
    }
    @supports (font-size: 1cqi) {
        .metric .n { font-size: clamp(1.05rem, 8.6cqi, var(--t-2xl)); }
    }
    .metric .l {
        font-size: var(--t-sm); text-transform: uppercase; letter-spacing: .04em;
        color: var(--n-600); font-weight: 700; margin-top: var(--s-1);
    }
    .metric .hint { font-size: var(--t-sm); color: var(--n-600); margin-top: var(--s-1); line-height: 1.5; }
    /* FR-342 — a deficit is marked, not left as a minus sign somebody has to
       notice in a row of similar-looking numbers. */
    .metric .n.is-deficit { color: var(--bad-ink); }
    .metric .l .badge { margin-left: var(--s-2); }

    /* ================= headline cards =================
       The four that lead the page. Colour is doing real work here: these are
       the figures somebody checks at a glance from across a desk, and a wall
       of identical white cards makes every one of them equally easy to miss.
       The rest of the page stays white, so this stays a signal. */
    .stat-row {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
        gap: var(--s-3);
        margin-bottom: var(--s-4);
    }
    /* The same metric card again, at the width the money screens lay out — a
       pastel wash, a saturated bottom edge and dark text, rather than the solid
       coloured block with white text this used to be. One card in the product,
       drawn three sizes. */
    .stat {
        display: flex; align-items: center; gap: var(--s-4);
        min-width: 0;
        padding: var(--s-5);
        border-radius: var(--r-lg);
        background: linear-gradient(180deg, var(--tone-line, var(--n-100)) 0%, var(--tone-bg, var(--n-50)) 100%);
        border-bottom: 4px solid var(--tone-b, var(--n-400));
        color: var(--n-800);
        box-shadow: var(--shadow-2);
    }
    .stat .stat-l { color: var(--n-600); }
    /*
     * A named colour supplies the same three steps the tiles use: the two ends
     * of the wash and the saturated one for the bottom edge and the disc. A
     * card that names none takes the section's, so the next one written without
     * a colour is still legible.
     */
    .stat-emerald { --tone-line:#a7f3d0; --tone-bg:#d1fae5; --tone-b:#059669; }
    .stat-blue    { --tone-line:#bfdbfe; --tone-bg:#dbeafe; --tone-b:#2563eb; }
    .stat-rose    { --tone-line:#fecdd3; --tone-bg:#ffe4e6; --tone-b:#e11d48; }
    .stat-amber   { --tone-line:#fde68a; --tone-bg:#fef3c7; --tone-b:#d97706; }
    .stat-slate   { --tone-line:#e2e8f0; --tone-bg:#f1f5f9; --tone-b:#475569; }

    /* `rounded-full` with the saturated step behind a white glyph — the
       template's disc, which is the same one the tiles carry. */
    .stat-mark {
        flex: 0 0 3.25rem;
        width: 3.25rem; height: 3.25rem;
        display: flex; align-items: center; justify-content: center;
        border-radius: var(--r-full);
        background: var(--tone-b, var(--n-500));
        color: #fff;
    }
    .stat-mark .icon { width: 1.6rem; height: 1.6rem; color: #fff; }

    /*
     * `flex: 1` is doing real work here, not tidying.
     *
     * `container-type: inline-size` applies inline containment, which means
     * this element no longer offers its content's width to the flex layout —
     * so without a flex basis it collapses to its minimum and every line in it
     * wraps. "Enrolled now, 2026" came out as three lines inside a card
     * with room for one.
     */
    .stat-body { flex: 1 1 auto; min-width: 0; container-type: inline-size; }
    .stat-sub { overflow-wrap: break-word; }

    /* A figure somebody wants to check is a figure they will try to click.
       The whole card is the target — not a link buried in it — because the
       number is what they are aiming at. */
    a.stat, a.metric {
        text-decoration: none;
        transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease;
    }
    a.stat:hover, a.metric:hover {
        text-decoration: none;
        transform: translateY(-2px);
        box-shadow: var(--shadow-2);
    }
    a.metric:hover { border-color: var(--accent); }
    a.stat:focus-visible, a.metric:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

    /* The arrow only appears on the ones that go somewhere, so a card without
       it is not a link somebody is failing to click. */
    a.metric .l::after,
    a.stat .stat-l::after {
        content: ' →';
        opacity: 0;
        transition: opacity 160ms ease;
    }
    a.metric:hover .l::after,
    a.stat:hover .stat-l::after { opacity: .75; }
    /* `display:block` so a card built from spans stacks exactly like one built
       from divs. Two screens drew these three lines with different elements
       and only one of them looked right. */
    .stat-l {
        display: block;
        font-size: var(--t-xs); font-weight: 800;
        letter-spacing: .07em; text-transform: uppercase;
        opacity: .92;
        overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    .stat-n {
        display: block;
        font-size: 1.4rem;
        font-weight: 900; line-height: 1.2;
        font-variant-numeric: tabular-nums; letter-spacing: -.02em;
        margin-top: .1rem;
        overflow-wrap: normal;
        word-break: keep-all;
    }
    @supports (font-size: 1cqi) {
        /* Measured against the text column, which is the card less its icon. */
        .stat-n { font-size: clamp(1rem, 11cqi, 1.65rem); }
    }
    .stat-sub { display: block; font-size: var(--t-xs); opacity: .9; margin-top: .15rem; }

    /* ================= tables ================= */
    .table-wrap { overflow-x: auto; }
    /* `text-gray-700` on the body, `text-left text-blue-900` on the headings —
       the template's whole table, which is a plain one with the colour spent
       entirely on telling the heading row apart. */
    table.grid {
        width: 100%; border-collapse: collapse;
        background: var(--n-0); color: var(--n-700);
    }
    table.grid th {
        font-size: var(--t-sm); text-transform: none; letter-spacing: 0;
        color: var(--th-ink); font-weight: 700; text-align: left;
        padding: var(--s-3);
        background: var(--n-50);
        border-bottom: 1px solid var(--n-200);
        white-space: nowrap;
    }
    /* A sortable heading reads as one: the whole cell is the target, the
       arrow sits beside the word rather than wrapping under it, and it takes
       the section's colour on hover so it is visibly a control. */
    table.grid th a {
        color: inherit; text-decoration: none;
        display: inline-flex; align-items: center; gap: .25rem;
        white-space: nowrap;
    }
    table.grid th a:hover { color: var(--tone-ink); text-decoration: none; }
    table.grid td {
        /* A row of 13px text in .6rem of padding is 2.1rem tall, and two
           hundred of them are a wall. The extra two millimetres each side is
           what lets somebody read across one row without losing it. */
        padding: .7rem var(--s-3);
        border-bottom: 1px solid var(--n-200);
        font-size: var(--t-cell);
        line-height: 1.5;
        vertical-align: middle;
    }
    /* Content does not sit flush against the card's edge. */
    table.grid th:first-child, table.grid td:first-child { padding-left: var(--s-4); }
    table.grid th:last-child, table.grid td:last-child { padding-right: var(--s-4); }
    table.grid tbody tr:last-child td { border-bottom: 0; }
    table.grid tbody tr:hover { background: var(--n-25); }
    /* FR-327 — amounts right-aligned, and lined up digit for digit. */
    .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
    /* For a short label that means one thing — "School-wide", "Per campus" —
       and reads as two when a narrow column breaks it. */
    .nowrap { white-space: nowrap; }
    .muted { color: var(--n-500); }
    .strong { font-weight: 700; color: var(--n-900); }

    .empty {
        padding: var(--s-7) var(--s-4);
        text-align: center;
        color: var(--n-500);
        background: var(--n-0);
        border: 1px dashed var(--n-300);
        border-radius: var(--r-xl);
    }

    /* ================= forms ================= */
    /* A field's own label, and the largest of the three label sizes on a
       form: 13px at weight 700 in near-black. It was 11px at weight 800 in
       slate, which reads as a caption — smaller and fainter than the value
       under it, on the one word that says what the value is. */
    label {
        display: block; font-size: var(--t-cell); color: var(--n-700);
        font-weight: 700; letter-spacing: .01em; margin-bottom: .35rem;
    }
    input, select, textarea {
        font: inherit; font-size: var(--t-md);
        color: var(--n-900);
        padding: .45rem .7rem;
        border: 1px solid var(--n-300);
        border-radius: var(--r-md);
        background: var(--n-0);
        max-width: 100%;
    }
    input:focus, select:focus, textarea:focus {
        outline: none;
        border-color: var(--accent);
        box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
    }
    button:focus-visible, a:focus-visible, summary:focus-visible {
        outline: 2px solid var(--accent);
        outline-offset: 2px;
    }
    .field { margin-bottom: var(--s-3); }
    .field input, .field select, .field textarea { width: 100%; }

    /*
     * A field that has something in it is ticked.
     *
     * The tick hangs off the **label**, not the control. Inside the box was
     * the first attempt and it does not survive contact with this product's
     * own fields: a `<select>` draws its arrow where the tick would go, the
     * date field has a picker button there, the password field has the eye,
     * and a number input has its spinners. One of those overlaps on every
     * form. The label is one element, every field has one, and nothing else
     * is ever drawn on it — so the mark lands in the same place on a text
     * box, a dropdown, a date and a checkbox alike.
     *
     * Set by the script at the end of this file, which also runs once on load
     * so a form that arrives filled in — an edit screen, or anything handed
     * back with `old()` after a validation error — is ticked before it is
     * touched.
     */
    [data-filled] > label::after,
    label[data-filled]::after {
        content: '';
        display: inline-block;
        width: .8rem; height: .8rem;
        margin-left: .35rem;
        vertical-align: -.1em;
        background: no-repeat center / contain
            url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Ccircle cx='10' cy='10' r='10' fill='%2316a34a'/%3E%3Cpath d='M5.8 10.4l2.7 2.7 5.7-5.9' fill='none' stroke='%23fff' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    }
    /* One step below the label it explains, and lighter: the three sizes on
       a form are heading, label, note, and they have to be told apart at a
       glance rather than by reading them. */
    .hint { font-size: var(--t-cell); color: var(--n-500); margin-top: .3rem; line-height: 1.5; }

    .form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-4); }
    .form-grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    @media (max-width: 60rem) { .form-grid-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

    /* ================= dates =================
       A date is typed far more often than it is picked — a clerk entering
       two hundred admissions knows the birthday and does not want a calendar
       for 2011. And `<input type="date">` alone is not good enough for either
       job: it renders in the *browser's* locale, so the same field reads
       `04/06/2019` to one member of staff and `06/04/2019` to the next, with
       nothing on screen saying which. In a student information system that is
       not a rough edge; it is a wrong date of birth on a B-Form.

       So the field shows an explicit `DD/MM/YYYY` text box and a calendar
       button beside it. The native input is still there, still holds the value,
       still carries the name the form posts — nothing behind this changed,
       and the server receives exactly the ISO date it always did. */
    .date-field {
        display: inline-flex;
        align-items: stretch;
        position: relative;
        max-width: 100%;
    }
    .field .date-field { width: 100%; }

    /* Rendered, not hidden: `showPicker()` refuses on an element that is not
       being rendered, so it sits under the button at zero opacity. */
    .date-field > input[type="date"] {
        position: absolute;
        right: 0; top: 0; bottom: 0;
        width: 2.5rem;
        opacity: 0;
        border: 0; padding: 0; margin: 0;
        pointer-events: none;
    }

    .date-field > .date-text {
        flex: 1 1 auto;
        min-width: 6.5rem;
        border-top-right-radius: 0;
        border-bottom-right-radius: 0;
        border-right-color: transparent;
        font-variant-numeric: tabular-nums;
        letter-spacing: .02em;
    }
    .date-field > .date-text:focus { border-right-color: var(--accent); }

    .date-field > .date-open {
        flex: 0 0 auto;
        display: inline-flex; align-items: center; justify-content: center;
        width: 2.25rem;
        padding: 0;
        border: 1px solid var(--n-300);
        border-left: 0;
        border-radius: 0 var(--r-md) var(--r-md) 0;
        background: var(--n-50);
        color: var(--n-600);
        cursor: pointer;
    }
    .date-field > .date-open:hover { background: var(--n-100); color: var(--accent-text); }
    .date-field > .date-open svg { width: 1rem; height: 1rem; display: block; }

    /* Eight digits that are not a real date. Said on the field rather than at
       submit: 31/02 is a typo somebody can fix in the second it takes to
       notice, and a page reload later it is a mystery. */
    .date-field > .date-text.is-invalid,
    .date-field > .date-text.is-invalid:focus {
        border-color: var(--bad-ink);
        box-shadow: 0 0 0 3px color-mix(in srgb, var(--bad-ink) 15%, transparent);
    }
    .date-field > .date-text.is-invalid + .date-open { border-color: var(--bad-ink); }

    /* ================= passwords =================
       FR-051a — a password box you can read back.

       Built exactly like `.date-field` above: the input keeps the name the
       form posts and the button sits against it sharing one border, so the two
       composite fields in the product are visibly the same idea.

       The button is `tabindex="-1"` on purpose. Tabbing out of a password
       field should reach the submit button, the way it always has; somebody
       who wants to reveal the text is reaching for it with a mouse, and a
       keyboard user should not have to pass through it on every sign-in. */
    .password-field {
        display: inline-flex;
        align-items: stretch;
        position: relative;
        max-width: 100%;
    }
    .field .password-field { width: 100%; }

    .password-field > input {
        flex: 1 1 auto;
        min-width: 0;
        border-top-right-radius: 0;
        border-bottom-right-radius: 0;
        border-right-color: transparent;
    }
    .password-field > input:focus { border-right-color: var(--accent); }

    .password-field > .password-reveal {
        flex: 0 0 auto;
        display: inline-flex; align-items: center; justify-content: center;
        width: 2.25rem;
        padding: 0;
        border: 1px solid var(--n-300);
        border-left: 0;
        border-radius: 0 var(--r-md) var(--r-md) 0;
        background: var(--n-50);
        color: var(--n-600);
        cursor: pointer;
    }
    .password-field > .password-reveal:hover { background: var(--n-100); color: var(--accent-text); }
    .password-field > .password-reveal:focus-visible {
        outline: 2px solid var(--accent);
        outline-offset: -2px;
    }
    .password-field > .password-reveal svg { width: 1rem; height: 1rem; display: block; }

    /* Edge and IE put their own reveal control in the field. Two eyes side by
       side, one of which is the browser's and behaves differently, is worse
       than either alone. */
    .password-field > input::-ms-reveal,
    .password-field > input::-ms-clear { display: none; }

    /* ================= buttons ================= */
    .btn {
        display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
        font: inherit; font-size: var(--t-md); font-weight: 700;
        min-height: 2.125rem;
        padding: .35rem 1rem;
        border: 1px solid transparent;
        /* `rounded` — the pill was this file's own signature and the reference
           has no pills on it. */
        border-radius: var(--r-md);
        background: var(--accent); color: var(--accent-ink);
        cursor: pointer; text-decoration: none;
        white-space: nowrap;
        transition: background 180ms ease, box-shadow 180ms ease, border-color 180ms ease;
    }
    /* A darker step rather than a brightness filter: `filter` re-rasterises the
       button on every hover and washes the label out with the background. */
    .btn:hover { background: var(--accent-strong); box-shadow: var(--shadow-accent); text-decoration: none; }
    .btn-secondary { background: var(--n-0); color: var(--n-700); border-color: var(--n-300); }
    .btn-secondary:hover { background: var(--n-50); border-color: var(--n-400); box-shadow: none; }
    .btn-ghost { background: transparent; color: var(--accent-text); border-color: transparent; }
    .btn-ghost:hover { background: var(--accent-soft); box-shadow: none; }
    .btn-danger { background: #b91c1c; }
    .btn-danger:hover { background: #991b1b; box-shadow: none; }
    .btn-sm { font-size: var(--t-sm); min-height: 1.75rem; padding: .2rem .7rem; }
    .btn-link {
        background: none; border: 0; padding: 0; color: var(--accent-text);
        cursor: pointer; font: inherit; font-size: var(--t-md); min-height: 0;
    }
    .btn-link:hover { text-decoration: underline; background: none; box-shadow: none; }
    .btn[disabled], .btn:disabled { opacity: .5; cursor: not-allowed; box-shadow: none; }
    input:disabled, select:disabled, textarea:disabled { background: var(--n-100); color: var(--n-600); cursor: not-allowed; }
    .actions { display: flex; gap: var(--s-2); align-items: center; flex-wrap: wrap; }

    /* ================= menus =================
       A `<details>` styled as a dropdown: it opens, closes on Escape and is
       reachable by keyboard with no script at all. */
    .menu { position: relative; display: inline-block; }
    .menu > summary { list-style: none; cursor: pointer; }
    .menu > summary::-webkit-details-marker { display: none; }
    .menu[open] > summary { background: var(--accent-strong); }

    .menu-panel {
        position: absolute; top: calc(100% + .4rem); right: 0; z-index: 40;
        min-width: 15rem;
        padding: var(--s-2);
        background: var(--n-0);
        border: 1px solid var(--n-200);
        border-radius: var(--r-lg);
        box-shadow: var(--shadow-2);
    }
    .menu-item {
        display: block; width: 100%; text-align: left;
        padding: .4rem var(--s-3);
        border: 0; border-radius: var(--r-md);
        background: none; color: var(--n-700);
        font: inherit; font-size: var(--t-md);
        cursor: pointer; text-decoration: none;
    }
    .menu-item:hover { background: var(--accent-soft); color: var(--accent-text); text-decoration: none; }
    .menu-item.is-disabled { color: var(--n-400); cursor: default; }
    .menu-item.is-disabled:hover { background: none; color: var(--n-400); }
    .menu-label {
        padding: var(--s-3) var(--s-3) var(--s-1);
        font-size: var(--t-xs); text-transform: uppercase; letter-spacing: .08em;
        color: var(--n-600); font-weight: 800;
    }

    /* ================= the account menu =================
       Who is signed in lives in the header rather than at the foot of the
       navigation, because the navigation collapses on a phone and "sign out"
       is the one control somebody needs on a shared office machine. */
    .avatar-mark {
        display: inline-flex; align-items: center; justify-content: center;
        width: 2rem; height: 2rem; flex: 0 0 2rem;
        border-radius: var(--r-full);
        background: var(--accent); color: #fff;
        font-size: var(--t-xs); font-weight: 800; letter-spacing: .02em;
    }
    .avatar-mark-lg { width: 3.25rem; height: 3.25rem; flex: 0 0 3.25rem; font-size: var(--t-lg); }

    /* `text-white focus:outline-none` on the header's grey — the template's
       "Hi, User" control is plain white text with a caret, not a bordered
       pill on a white bar. */
    .user-menu > .user-summary {
        display: inline-flex; align-items: center; gap: var(--s-2);
        padding: .3rem .5rem .3rem .3rem;
        border: 1px solid transparent;
        border-radius: var(--r-md);
        background: none;
        cursor: pointer;
        list-style: none;
        max-width: 15rem;
    }
    .user-menu > .user-summary::-webkit-details-marker { display: none; }
    .user-menu > .user-summary:hover { border-color: rgba(255,255,255,.18); background: rgba(255,255,255,.06); }
    .user-menu[open] > .user-summary { border-color: rgba(255,255,255,.28); background: rgba(255,255,255,.1); }

    .user-who { display: flex; flex-direction: column; min-width: 0; line-height: 1.2; }
    .user-hello {
        font-size: var(--t-sm); font-weight: 700; color: #fff;
        overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    .user-where {
        font-size: var(--t-xs); color: var(--chrome-ink);
        overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    .user-caret { color: var(--chrome-ink); font-size: .65rem; }

    .menu-head {
        padding: var(--s-2) var(--s-3) var(--s-3);
        border-bottom: 1px solid var(--n-200);
        margin-bottom: var(--s-2);
    }
    .menu-form { margin: var(--s-2) 0 0; border-top: 1px solid var(--n-200); padding-top: var(--s-2); }
    .menu-item-danger { color: var(--bad-ink); font-weight: 700; }
    .menu-item-danger:hover { background: var(--bad-bg); color: var(--bad-ink); }

    /* The account screen's own header. */
    .account-head {
        display: flex; align-items: center; gap: var(--s-4); flex-wrap: wrap;
        background: var(--n-0);
        border: 1px solid var(--n-200);
        border-radius: var(--r-xl);
        box-shadow: var(--shadow-1);
        padding: var(--s-5);
        margin-bottom: var(--s-4);
    }
    /* A value that is shown rather than edited. Given the height and the
       alignment of an input so a read-only grid does not jump about beside a
       form that has both. */
    .readout {
        min-height: 2.1rem;
        display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap;
        font-size: var(--t-md); font-weight: 600; color: var(--n-900);
    }

    /* ================= dialog ================= */
    dialog.sheet {
        width: min(34rem, calc(100vw - 2rem));
        max-height: calc(100vh - 4rem);
        padding: 0;
        border: 1px solid var(--n-200);
        border-radius: var(--r-panel);
        background: var(--n-0);
        color: var(--ink);
        box-shadow: var(--shadow-2);
        overflow: hidden;
    }
    dialog.sheet::backdrop { background: rgba(15,23,42,.45); }
    /* The shell, not `form`. A popup that cannot be a form — one inside
       another form is not nested, it closes the outer one — was left without
       the column layout, so its body never scrolled and the buttons at the
       foot were pushed off the bottom of the screen. */
    dialog.sheet > form,
    dialog.sheet > .sheet-shell { display: flex; flex-direction: column; max-height: calc(100vh - 4rem); }
    /* A popup that asks for eight fields needs the room for three of them
       across; the default 34rem is for a popup that asks two questions. */
    dialog.sheet-wide { width: min(58rem, calc(100vw - 2rem)); }
    .sheet-title {
        display: flex; align-items: center; justify-content: space-between; gap: var(--s-3);
        padding: var(--s-4) var(--s-5);
        background: var(--accent); color: var(--accent-ink);
        font-size: var(--t-lg);
    }
    .sheet-title .btn-link { color: var(--accent-ink); font-size: var(--t-xl); line-height: 1; }
    .sheet-body { padding: var(--s-4) var(--s-5); overflow-y: auto; }
    .sheet-body .section-title:first-of-type { margin-top: var(--s-4); }
    .sheet-foot {
        display: flex; gap: var(--s-2); justify-content: flex-end;
        padding: var(--s-4) var(--s-5);
        border-top: 1px solid var(--n-200);
        background: var(--n-25);
    }
    .check {
        display: flex; align-items: center; gap: var(--s-2);
        margin: 0 0 .3rem; padding: .15rem 0;
        font-size: var(--t-md); font-weight: 400; color: var(--n-700);
        text-transform: none; letter-spacing: 0;
        cursor: pointer;
    }
    .check input { width: auto; margin: 0; }

    /*
     * A `<fieldset>` is a card, and it has to be told it may be narrow.
     *
     * Browsers give a fieldset `min-inline-size: min-content`, which is the
     * one box in CSS that will not shrink below its content. A single long
     * `<option>` — "Per campus — holder works at one campus at a time" — was
     * enough to push the whole page sideways on a phone (C-74). Everything
     * else here is the same surface every other panel uses, so a form on an
     * older screen no longer reads as a different application.
     */
    fieldset {
        min-width: 0;
        margin: 0 0 var(--s-4);
        padding: var(--s-5);
        background: var(--n-0);
        border: 1px solid var(--n-200);
        border-radius: var(--r-xl);
        box-shadow: var(--shadow-1);
    }
    fieldset > legend {
        padding: 0 var(--s-2);
        margin-left: calc(-1 * var(--s-2));
        font-size: var(--t-xs); text-transform: uppercase; letter-spacing: .09em;
        color: var(--n-600); font-weight: 800;
    }
    fieldset > legend a { color: var(--accent-text); }
    fieldset > label { margin-top: var(--s-3); }
    fieldset > label:first-of-type { margin-top: 0; }
    fieldset > input, fieldset > select, fieldset > textarea { width: 100%; }

    /* The permission list: many short rows, so they read as columns rather
       than as one long strip somebody has to scroll past. */
    .perm {
        display: flex; align-items: center; gap: var(--s-2);
        margin: 0; padding: .2rem 0;
        font-size: var(--t-md); font-weight: 400; color: var(--n-700);
        text-transform: none; letter-spacing: 0;
        cursor: pointer;
        overflow-wrap: anywhere;
    }
    .perm input { width: auto; margin: 0; flex: 0 0 auto; }
    .perm-grid {
        display: grid;
        /* Wide enough for the longest permission name there is
           (`academic.teacher_assignment.manage`), because a name broken across
           two lines is a name somebody has to reassemble before they can
           decide whether to tick it. */
        grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
        gap: 0 var(--s-4);
    }

    /* FR-290c — a password shown once has to be readable character for
       character, including the ones that look alike. */
    .one-time-secret {
        font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
        font-size: var(--t-lg); letter-spacing: .06em;
        margin: var(--s-2) 0;
        padding: var(--s-2) var(--s-3);
        background: var(--n-0);
        border: 1px dashed currentColor;
        border-radius: var(--r-md);
        overflow-wrap: anywhere;
    }

    /*
     * A row of headline figures on a report page.
     *
     * The same job as `.metrics` on the dashboard and the same rule: the digits
     * are measured against the box, never against the window, so no amount can
     * be pushed out of its own panel by being large (C-71). `.form-grid` used
     * to do this work with a `font-size` typed into a style attribute, which
     * disagreed page by page and knew nothing about the width it had.
     */
    .figures {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
        gap: var(--s-4) var(--s-5);
    }
    .figures > * { min-width: 0; container-type: inline-size; }
    .figure {
        font-size: 1.25rem;
        font-weight: 800; line-height: 1.25;
        color: var(--n-900);
        font-variant-numeric: tabular-nums; letter-spacing: -.015em;
        overflow-wrap: normal; word-break: keep-all;
    }
    @supports (font-size: 1cqi) {
        .figure { font-size: clamp(.95rem, 10cqi, 1.4rem); }
    }
    .figure.is-deficit { color: var(--bad-ink); }

    /*
     * The home screen: big buttons for the few things a person actually does.
     *
     * A teacher who signs in wants to mark the register. A clerk wants to take
     * a fee. Neither wants to read a menu of sixty links first, so the home
     * screen is a short row of large targets and nothing else.
     */
    /*
     * Wider tracks and a bigger gap than the grid started with.
     *
     * At 14rem the descriptions were breaking to three lines — "Take today's
     * register for your class" over three — and 0.75rem of gap between rows
     * of cards that tall read as one block rather than twelve targets. The
     * rows now have as much air between them as the cards have inside them,
     * which is what stops a grid looking like a table.
     */
    .tiles {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
        gap: var(--s-4);
        margin-bottom: var(--s-5);
    }
    /*
     * Every rule here is written under `.tiles`, and that is load-bearing.
     *
     * The dashboard has tiles of its own further down this file, and being
     * further down is all it takes: `.tile` there sets `display: block` and a
     * 2rem mark, and it was winning on the home screen for no better reason
     * than source order. The icon sat on top of its own label instead of
     * beside it. Scoping these to the grid they belong to settles it without
     * touching the dashboard, which wants a stacked tile.
     */
    .tiles .tile {
        display: flex; align-items: center; gap: var(--s-4);
        padding: var(--s-5);
        min-width: 0;
        /* A hue named on the mark (`tile-rose` …) wins; otherwise the tone. */
        background: linear-gradient(180deg, var(--tile-200, var(--tone-line, var(--n-100))) 0%, var(--tile-100, var(--tone-bg, var(--n-50))) 100%);
        border: 1px solid transparent;
        border-bottom: 4px solid var(--tile-600, var(--tone-b, var(--n-400)));
        border-radius: var(--r-lg);
        box-shadow: var(--shadow-2);
        color: var(--n-900); text-decoration: none;
        transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease;
    }
    .tiles .tile:hover {
        text-decoration: none;
        transform: translateY(-2px);
        box-shadow: var(--shadow-2);
    }
    .tiles .tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
    /* Large enough to aim at from across a desk — this screen is a row of
       targets, and the icon is what somebody recognises before they have read
       the label under their finger. */
    /* The chip grows with the glyph: a 2.75rem icon in a 3.25rem chip has
       barely a hair of padding and reads as a mistake rather than a mark. */
    .tiles .tile-mark {
        flex: 0 0 4rem; width: 4rem; height: 4rem;
        display: flex; align-items: center; justify-content: center;
        border-radius: var(--r-full);
        background: var(--tile-600, var(--tone-b, var(--accent))); color: #fff;
    }
    .tiles .tile-mark .icon { width: 2rem; height: 2rem; color: #fff; }
    .tile-body { min-width: 0; }
    .tile-t { display: block; font-weight: 700; font-size: var(--t-lg); line-height: 1.3; }
    .tile-d {
        display: block;
        font-size: var(--t-sm); color: var(--n-600); margin-top: .15rem;
        line-height: 1.45; overflow-wrap: anywhere;
    }

    /* ================= badges ================= */
    .badge {
        display: inline-block;
        font-size: var(--t-xs); font-weight: 700; letter-spacing: .01em;
        padding: .15rem .55rem; border-radius: var(--r-full);
        border: 1px solid transparent;
        white-space: nowrap;
    }
    /*
     * Status is read across a room, so the tint carries it rather than the
     * word alone.
     *
     * These were the same near-white washes the notices use, which is right
     * for a panel filling half a screen and wrong for a pill the size of a
     * thumbnail: "Paid" and "Overdue" in a column of twenty rows were told
     * apart only by reading them. The band is the section's own line colour
     * with the ink kept dark, so the contrast is real without the pill
     * turning into a solid block the text has to fight.
     */
    .badge-ok   { background: var(--ok-line);   color: var(--ok-ink);   border-color: var(--ok-ink); }
    .badge-warn { background: var(--warn-line); color: var(--warn-ink); border-color: var(--warn-ink); }
    .badge-bad  { background: var(--bad-line);  color: var(--bad-ink);  border-color: var(--bad-ink); }
    .badge-info { background: var(--info-line); color: var(--info-ink); border-color: var(--info-ink); }

    /*
     * A row that needs acting on, tinted end to end.
     *
     * A badge marks one cell; this marks the line, which is what somebody
     * scanning a list actually sees first. Kept light enough that the text
     * on it reads normally — the point is "this one", not a highlighter.
     */
    tr.row-bad > td { background: var(--bad-bg); }
    tr.row-bad:hover > td { background: color-mix(in srgb, var(--bad-bg) 82%, var(--bad-line)); }
    .bad-text { color: var(--bad-ink); }
    .badge-neutral { background: var(--n-100); color: var(--n-600); border-color: var(--n-200); }

    /* ================= timeline (FR-089) =================
       A record read top to bottom: the date in a fixed column so the eye can
       scan dates without reading the entries, and the entry beside it. The
       columns stack below 40rem, because an 8.5rem date column on a phone
       leaves nothing for the entry itself. */
    .timeline-row {
        display: flex; gap: var(--s-4);
        padding: var(--s-3) 0;
        border-top: 1px solid var(--n-200);
    }
    .timeline-row:first-child { border-top: 0; padding-top: 0; }
    .timeline-row:last-child { padding-bottom: 0; }
    .timeline-date { flex: 0 0 8.5rem; font-size: var(--t-sm); line-height: 1.4; }
    .timeline-body { min-width: 0; font-size: var(--t-sm); }
    .timeline-body .muted { margin-top: var(--s-1); }
    @media (max-width: 40rem) {
        .timeline-row { flex-direction: column; gap: var(--s-1); }
        .timeline-date { flex: none; }
    }

    /* A student's photograph (FR-080), served through a permission check. */
    .avatar {
        width: 5rem; height: 5rem;
        border-radius: var(--r-lg);
        object-fit: cover;
        border: 1px solid var(--n-200);
        background: var(--n-50);
        box-shadow: var(--shadow-1);
    }

    /* ================= tabs =================
       The lower half of a profile: guardians, education, documents, the
       timeline, the status form and the notes. Six panels stacked made a page
       two metres long in which the last three were found by scrolling past the
       first three, so they are six tabs instead.

       **Without JavaScript every panel is visible, stacked, exactly as before.**
       The script hides the inactive ones and only then paints the strip as
       tabs; a page that arrives with no script is a long page, not an empty
       one. That is why `hidden` is applied by the script rather than written
       into the markup, and why each panel keeps its own heading.

       The strip scrolls sideways on a phone rather than wrapping: a wrapped
       tab strip pushes the panel down the screen and reads as two rows of
       unrelated buttons. */
    .tab-list {
        display: flex;
        gap: var(--s-1);
        border-bottom: 1px solid var(--n-200);
        overflow-x: auto;
        margin: var(--s-5) 0 var(--s-4);
        /* Still scrolls if a strip of tabs overflows — the bar itself is
           just never drawn. */
        scrollbar-width: none;
    }
    .tab-list::-webkit-scrollbar { display: none; }
    .tab-list button {
        flex: 0 0 auto;
        appearance: none;
        border: 0;
        border-bottom: 2px solid transparent;
        background: none;
        padding: var(--s-3) var(--s-4);
        font: inherit;
        /* 12px on a strip of eight labels was a row nobody could read at a
           glance, which is the one job a tab strip has. */
        font-size: var(--t-lg);
        font-weight: 700;
        color: var(--n-600);
        cursor: pointer;
        white-space: nowrap;
        /* The border sits on the container, so the tab's own line has to cover
           it rather than sit above it. */
        margin-bottom: -1px;
    }
    /* The strip belongs to the screen it is on, so it wears that screen's
       colour (see the colour system below): a student's profile in the
       Students hue, a staff record in the Staff one. */
    .tab-list button:hover { color: var(--tone-ink); }
    .tab-list button[aria-selected="true"] {
        color: var(--tone-ink);
        border-bottom-color: var(--tone-a);
    }
    .tab-list a {
        flex: 0 0 auto;
        border-bottom: 2px solid transparent;
        padding: var(--s-3) var(--s-4);
        font-size: var(--t-lg);
        font-weight: 700;
        color: var(--n-600);
        text-decoration: none;
        white-space: nowrap;
    }
    .tab-list a:hover { color: var(--tone-ink); }
    .tab-list a[aria-current="page"] {
        color: var(--tone-ink);
        border-bottom-color: var(--tone-a);
    }
    .tab-list button:focus-visible {
        outline: 2px solid var(--accent);
        outline-offset: -2px;
        border-radius: var(--r-sm);
    }
    /* A count beside a tab, so "Education" says whether there is any. */
    .tab-list .tab-count {
        display: inline-block;
        margin-left: var(--s-2);
        padding: 0 .4rem;
        border-radius: var(--r-full);
        background: var(--n-100);
        color: var(--n-600);
        font-size: var(--t-sm);
        font-weight: 800;
    }
    /* `--accent-bg` is not a token this design system has, so the selected
       tab's count had been falling through to the grey fallback since it was
       written — the one pill on the strip that was meant to be picked out. */
    .tab-list button[aria-selected="true"] .tab-count { background: var(--tone-bg); color: var(--tone-ink); }

    /* The panel's own heading is redundant once the strip names it, so the
       script removes it — but only the script, because without one the heading
       is the only thing separating six stacked panels. */
    .tabs-live .tab-panel > .section-title:first-child { display: none; }

    /* ================= a person's record =================
       FR-089 / FR-031a — one layout for every profile in the product: the
       photograph on the left, the record itself beside it as label-then-value
       rows.

       **Why the labels sit beside the fields rather than above them.** A
       profile is thirty-odd short facts about one person, and stacked labels
       make thirty-odd two-line blocks: the page becomes a column of text a
       metre long that somebody scrolls past looking for a phone number. Beside
       them, the labels form a single reading edge and the eye runs down it.

       **Why the field column is capped.** An input for a blood group has no
       business being 900px wide on a desktop: a text box tells you how much to
       type, and one the width of the window says "an essay". The cap is the
       same measure the print sheet uses for the same reason.

       The school console and the school's own screens use this together, so
       that a support call and the clerk being supported are looking at the
       same page in the same order. */
    .record { display: flex; gap: var(--s-5); align-items: flex-start; }
    .record-photo { flex: 0 0 11.5rem; width: 11.5rem; }
    .record-fields { flex: 1 1 auto; min-width: 0; max-width: 46rem; }

    /* 4:5, the ratio of every passport and B-Form photograph a school holds,
       so an uploaded portrait fills the frame instead of being letterboxed. */
    .record-shot {
        width: 11.5rem; height: 14.4rem;
        display: flex; align-items: center; justify-content: center;
        border: 1px solid var(--n-200);
        border-radius: var(--r-lg);
        background: var(--n-50);
        box-shadow: var(--shadow-1);
        overflow: hidden;
    }
    .record-shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
    /* FR-044 — no photograph is initials, never a broken image. */
    .record-shot .initials {
        font-size: 2.5rem; font-weight: 900; letter-spacing: .04em;
        color: var(--accent); opacity: .55;
    }
    .record-photo-tools { margin-top: var(--s-3); }
    .record-photo-tools .btn { width: 100%; justify-content: center; }
    .record-photo-tools .hint { text-align: center; }

    /* The file input hides inside its own label, so the control looks like the
       button it is and still works with no JavaScript at all: pick a file,
       then press Upload. */
    .file-pick { position: relative; overflow: hidden; }
    .file-pick input[type=file] {
        position: absolute; inset: 0;
        width: 100%; height: 100%;
        opacity: 0; cursor: pointer;
    }

    /* ---- profile completion, under the photograph ----
       One ring, one figure. The circle's radius is 15.915 so its
       circumference is exactly 100 and a percentage can be used as the dash
       length unchanged. */
    .record-ring {
        position: relative;
        width: 7.5rem; height: 7.5rem;
        margin: var(--s-4) auto 0;
        display: flex; align-items: center; justify-content: center;
    }
    .record-ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
    .record-ring circle { fill: none; stroke-width: 3.2; }
    .record-ring .ring-track { stroke: var(--n-200); }
    .record-ring .ring-arc { stroke: #4A6741; stroke-linecap: round; }
    .record-ring .ring-face {
        position: absolute; inset: 0;
        display: flex; flex-direction: column; align-items: center; justify-content: center;
        text-align: center; line-height: 1.2;
    }
    .record-ring .ring-n { font-size: var(--t-lg); font-weight: 900; color: #4A6741; }
    .record-ring .ring-l { font-size: var(--t-xs); color: var(--n-600); padding: 0 .3rem; }

    /* ---- the record as it reads ----
       Labels above values in short columns, the way the profile template
       lays a record out: a 14px grey label over a 16px value, grouped under
       a 24px olive heading. The editable form keeps the label-beside-field
       rows below, because inputs need the width. */
    .record-read { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s-5) var(--s-4); }
    .record-read .row-field { display: block; margin: 0; }
    .record-read .row-field > label {
        padding: 0 0 .15rem;
        font-size: var(--t-md); font-weight: 400; color: #62748E;
    }
    .record-read .row-field .val { padding: 0; font-size: var(--t-lg); color: #314158; }
    .record-read .row-field .hint { margin-top: .15rem; }
    /* A heading inside the grid — "school fields" — spans it and opens a
       group, exactly as the template's section rules do. */
    .record-read .section-title {
        grid-column: 1 / -1;
        margin: var(--s-4) 0 0;
        padding-bottom: var(--s-2);
        border-bottom: 2px solid #8FA985;
        font-size: 1.5rem; font-weight: 600; letter-spacing: 0;
        text-transform: none; color: #4A6741;
    }
    @media (max-width: 68rem) { .record-read { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
    @media (max-width: 48rem) { .record-read { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

    /* ---- attachments, as cards ---- */
    .attach-head { display: inline-flex; align-items: center; gap: var(--s-3); }
    .attach-head .icon { width: 1.35rem; height: 1.35rem; flex: 0 0 auto; }
    .attach-grid {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
        gap: var(--s-4);
    }
    .attach-card {
        padding: var(--s-4);
        background: #F8FAFC;
        border: 1px solid #E2E8F0;
        border-radius: var(--r-lg);
    }
    .attach-card h4 { margin: 0 0 var(--s-2); font-size: var(--t-md); font-weight: 400; color: #62748E; }
    .attach-shot { margin-bottom: var(--s-2); max-width: 100%; }
    .attach-shot img {
        display: block;
        max-height: 100px; max-width: 100%;
        border: 1px solid var(--n-200);
        border-radius: var(--r-sm);
    }
    .attach-card a {
        display: flex; align-items: center; gap: var(--s-2);
        color: #4A6741; font-weight: 600;
    }
    .attach-card a .icon { width: 1.1rem; height: 1.1rem; flex: 0 0 auto; }
    .attach-card a:hover { text-decoration: underline; }

    /* A section band on a profile, in the same hand as the groups above. It
       is a flex row so a control — Edit profile — can sit on the band
       without breaking the rule under it. */
    .profile-title {
        display: flex; align-items: center; justify-content: space-between; gap: var(--s-3);
        margin: var(--s-6) 0 var(--s-4);
        padding-bottom: var(--s-2);
        border-bottom: 2px solid #8FA985;
        font-size: 1.5rem; font-weight: 600;
        color: #4A6741;
    }

    .row-field {
        display: grid;
        grid-template-columns: 10rem minmax(0, 1fr);
        gap: var(--s-3);
        align-items: start;
        margin-bottom: var(--s-3);
    }
    .row-field > label {
        padding-top: .5rem;
        font-size: var(--t-md); font-weight: 700; color: var(--n-700);
        line-height: 1.35;
    }
    .row-field input, .row-field select, .row-field textarea { width: 100%; }
    /* A read-only row: the value sits on the same baseline a field would, so
       the console view and the editable view line up row for row. */
    .row-field .val { padding-top: .5rem; font-size: var(--t-md); color: var(--n-900); }
    .row-field .val.muted { color: var(--n-600); }
    .row-field .hint { margin-top: .3rem; }

    @media (max-width: 54rem) {
        .record { flex-direction: column; }
        .record-photo { flex: none; width: 100%; max-width: 11.5rem; }
        .record-fields { max-width: none; }
        .row-field { grid-template-columns: minmax(0, 1fr); gap: var(--s-1); }
        .row-field > label { padding-top: 0; }
        .row-field .val { padding-top: 0; }
    }

    /* ================= notices ================= */
    .notice {
        padding: var(--s-4) var(--s-5);
        border-radius: var(--r-lg);
        border: 1px solid var(--info-line);
        background: var(--info-bg); color: var(--info-ink);
        font-size: var(--t-md);
        margin-bottom: var(--s-4);
    }
    .notice-ok  { background: var(--ok-bg);  color: var(--ok-ink);  border-color: var(--ok-line); }
    .notice-bad { background: var(--bad-bg); color: var(--bad-ink); border-color: var(--bad-line); }
    .notice-warn{ background: var(--warn-bg);color: var(--warn-ink);border-color: var(--warn-line); }
    /* The neutral one: an instruction rather than a result. Badges already had
       an info tone; notices did not, so a purely informational panel had to
       borrow `warn` and read as a problem. */
    .notice-info{ background: var(--info-bg);color: var(--info-ink);border-color: var(--info-line); }
    .notice ul { margin: var(--s-1) 0 0; padding-left: var(--s-4); }
    .notice .hint { color: inherit; opacity: .85; }

    /* ================= list chrome (SRS 4.23) ================= */
    .filters {
        display: flex; flex-wrap: wrap; gap: var(--s-3); align-items: flex-end;
        background: var(--n-0);
        border: 1px solid var(--n-200);
        border-radius: var(--r-xl);
        padding: var(--s-4) var(--s-5);
        box-shadow: var(--shadow-1);
        margin-bottom: var(--s-4);
    }
    .list-meta {
        display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap;
        margin: var(--s-3) 0;
        font-size: var(--t-sm);
    }
    .list-meta .count { color: var(--n-600); font-weight: 700; }
    .list-meta .spacer { flex: 1; }
    .chip {
        display: inline-flex; align-items: center; gap: .35rem;
        background: var(--n-0); color: var(--n-700);
        border: 1px solid var(--n-300);
        padding: .2rem .7rem; border-radius: var(--r-full);
        font-size: var(--t-sm); font-weight: 600;
    }
    .chip:hover { border-color: var(--n-400); background: var(--n-50); text-decoration: none; }
    .chip .x { color: var(--n-400); font-weight: 800; }
    .chip-clear { color: var(--bad-ink); border-color: var(--bad-line); background: var(--bad-bg); }
    .chip-saved { color: var(--info-ink); border-color: var(--info-line); background: var(--info-bg); }
    .save-view { display: flex; gap: var(--s-1); align-items: center; }
    .save-view input { font-size: var(--t-sm); padding: .25rem .6rem; }
    .save-view .pin { font-size: var(--t-xs); color: var(--n-600); display: flex; gap: .25rem; align-items: center; margin: 0; }

    .pager { margin-top: var(--s-4); font-size: var(--t-sm); }
    .pager svg { width: 1rem; height: 1rem; }

    /*
     * FR-326 — paging.
     *
     * Laravel's stock paginator view is written for Tailwind and carries its
     * chevrons as inline SVG sized by utility classes. With no Tailwind in
     * the page those classes do nothing, an SVG with no width fills its line
     * box, and the next/previous arrows came out several times the height of
     * the numbers beside them. The view is ours now (`vendor/pagination`) and
     * the arrows are text, which cannot outgrow the type around it.
     */
    .pagination {
        display: flex; flex-wrap: wrap; align-items: center;
        justify-content: space-between; gap: var(--s-3);
        margin-top: var(--s-4);
    }
    .pagination-count { margin: 0; font-size: var(--t-sm); color: var(--n-600); }
    .pagination-count strong { color: var(--n-800); font-weight: 700; }
    .pagination-list {
        display: flex; flex-wrap: wrap; align-items: center; gap: .25rem;
        list-style: none; margin: 0; padding: 0;
    }
    .pg {
        display: inline-flex; align-items: center; justify-content: center;
        min-width: 2rem; height: 2rem; padding: 0 .5rem;
        border: 1px solid var(--n-200); border-radius: var(--r-md);
        background: var(--n-0); color: var(--n-700);
        font-size: var(--t-sm); font-weight: 700; line-height: 1;
        text-decoration: none; white-space: nowrap;
    }
    a.pg:hover {
        background: var(--n-50); border-color: var(--accent);
        color: var(--n-900); text-decoration: none;
    }
    a.pg:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
    .pg.is-current {
        background: var(--accent); border-color: var(--accent);
        color: var(--accent-ink);
    }
    .pg.is-disabled { opacity: .45; cursor: default; }
    .pg.is-gap { border-color: transparent; background: transparent; }

    /* ================= campus switcher (FR-374) ================= */
    .campus-switcher { display: flex; align-items: center; gap: var(--s-2); margin: 0; }
    .campus-switcher label { margin: 0; color: #fff; }
    /* On the dark header: the same `bg-gray-900 text-white` the search box
       wears, so the two controls beside each other are one idea. */
    .campus-switcher select {
        background: var(--chrome-deep); color: #fff;
        border-color: transparent;
    }

    /*
     * The bell and the other ghost controls in the header.
     *
     * `.btn-ghost` is written for a white page — accent-coloured text going to
     * a pale tint on hover — and on the header's grey that is a dark label on a
     * dark ground. Scoped to the header only, so the same class on a card is
     * untouched.
     */
    .topbar .btn-ghost { color: #fff; }
    .topbar .btn-ghost:hover { background: rgba(255,255,255,.1); color: #fff; }

    /* ================= light chrome (reference theme reskin) =================
       The frame — sidebar, header and the title band's backing strip — on a
       white rail with dark ink, matching the reference admin theme's light
       skin. Only colour moves here; everything above (collapse, groups,
       tones, layout) is untouched. Built on the `--n-*` ramp rather than
       literals so it inverts for free under `data-theme="dark"`. */
    .sidebar, .topbar, .page-head-wrap { background: var(--n-0); }
    .sidebar { color: var(--n-600); border-right: 1px solid var(--n-200); box-shadow: none; }
    .sidebar-brand { border-bottom-color: var(--n-200); }
    .brand-name { color: var(--n-800); }
    .brand-sub { color: var(--n-500); }
    .nav-collapse { background: var(--n-100); color: var(--n-700); }
    .nav-collapse:hover { background: var(--n-200); color: var(--n-900); }
    .nav { scrollbar-color: var(--n-300) transparent; }
    .nav::-webkit-scrollbar-thumb { background: var(--n-300); }
    .nav::-webkit-scrollbar-thumb:hover { background: var(--n-400); }
    .nav-group > summary:hover { color: var(--n-900); }
    .nav-group.is-here > summary { color: var(--n-900); }
    .nav a { color: var(--n-600); }
    .nav a:hover { color: var(--n-900); }
    .nav a[aria-current="page"] { color: var(--n-900); }
    .sidebar { --chrome-ink: var(--n-600); }
    .sidebar .nav-label { color: var(--n-500); }
    .sidebar .brand-sub, .sidebar-foot { color: var(--n-500); }
    .sidebar-foot { border-top-color: var(--n-200); }
    .sidebar-foot .who { color: var(--n-800); }
    .sidebar-foot .btn-secondary { background: var(--n-100); color: var(--n-700); border-color: var(--n-200); }
    .sidebar-foot .btn-secondary:hover { background: var(--n-200); }
    .sidebar-session { background: var(--n-100); color: var(--n-700); }
    .sidebar-session:hover { background: var(--n-200); color: var(--n-900); }
    :root.nav-collapsed .sidebar .nav a::after { background: var(--n-800); color: var(--n-0); }

    .topbar { color: var(--n-700); }
    .topbar h1 { color: var(--n-900); }
    .topbar .btn-ghost { color: var(--n-600); }
    .topbar .btn-ghost:hover { background: var(--n-100); color: var(--n-900); }
    .nav-toggle { color: var(--n-700); }
    .nav-toggle:hover { background: var(--n-100); }
    .campus-switcher label { color: var(--n-600); }
    .campus-switcher select { background: var(--n-100); color: var(--n-800); border-color: var(--n-200); }
    .user-menu > .user-summary:hover { border-color: var(--n-200); background: var(--n-100); }
    .user-menu[open] > .user-summary { border-color: var(--n-300); background: var(--n-100); }
    .user-hello { color: var(--n-900); }
    .user-where, .user-caret { color: var(--n-500); }
    .omnibox input[type="search"] { background: var(--n-100); color: var(--n-800); border-color: var(--n-200); }
    .omnibox input[type="search"]::placeholder { color: var(--n-500); }
    .omnibox input[type="search"]:focus { background: var(--n-0); border-color: var(--accent); }

    /* ================= impersonation (FR-033) ================= */
    .impersonation-bar {
        position: sticky; top: 0; z-index: 100;
        background: #7f1d1d; color: #fff;
        padding: var(--s-2) var(--s-4);
        font-size: var(--t-sm);
        display: flex; align-items: center; justify-content: space-between; gap: var(--s-4);
    }
    .impersonation-bar .btn { background: #fff; color: #7f1d1d; }
    .impersonation-bar .btn:hover { background: #fee2e2; box-shadow: none; }

    /* ================= auth ================= */
    .auth-page {
        min-height: 100vh;
        display: flex; align-items: center; justify-content: center;
        padding: var(--s-5);
        background:
            radial-gradient(1100px 560px at 50% -12%, color-mix(in srgb, var(--accent) 16%, transparent), transparent 62%),
            var(--page);
    }
    .auth-card {
        width: 100%; max-width: 24rem;
        background: var(--n-0);
        border: 1px solid var(--n-200);
        border-radius: var(--r-panel);
        box-shadow: var(--shadow-2);
        padding: var(--s-6);
    }
    .auth-brand { display: flex; align-items: center; gap: var(--s-3); margin-bottom: var(--s-5); }
    .auth-brand .brand-badge { width: 2.5rem; height: 2.5rem; flex: 0 0 2.5rem; }
    .auth-brand .name { font-weight: 800; color: var(--n-900); line-height: 1.2; }
    .auth-brand .sub { font-size: var(--t-xs); color: var(--n-500); }
    /* The one landing-page-sized control in the product. */
    .auth-card .btn { width: 100%; min-height: 3rem; font-size: var(--t-md); }
    .auth-links { margin-top: var(--s-4); font-size: var(--t-sm); display: flex; justify-content: space-between; gap: var(--s-3); }

    /* ================= small screens =================
       The navigation becomes a drawer rather than a wrapped list of pills. A
       school's own list is sixty entries long; wrapped into pills it filled
       two screens of a phone before the page began, which is why nobody
       scrolled past it.

       Driven by a checkbox, not by script: it works on the first paint, and on
       a phone with JavaScript blocked it still opens. */
    .nav-state { position: absolute; opacity: 0; pointer-events: none; }

    .nav-toggle {
        display: none;
        align-items: center; justify-content: center;
        width: 2.25rem; height: 2.25rem;
        flex: 0 0 2.25rem;
        margin: 0;
        border: 1px solid transparent;
        border-radius: var(--r-md);
        background: none;
        color: #fff;
        font-size: 1.05rem;
        cursor: pointer;
    }
    .nav-toggle:hover { background: rgba(255,255,255,.1); }

    .nav-scrim { display: none; }

    @media (max-width: 64rem) {
        .content { padding: var(--s-4); max-width: 100%; }
        .topbar { padding: var(--s-3) var(--s-4); }
    }

    @media (max-width: 52rem) {
        .nav-toggle { display: inline-flex; }

        /* A drawer, not a floating card: it slides in against the edge, so
           the inset and the rounded corners the panel has on a desktop are
           dropped here, and the collapsed state with it — a drawer you have
           to open and then widen is two gestures for one menu. */
        .sidebar, :root.nav-collapsed .sidebar {
            position: fixed;
            top: 0; bottom: 0; left: 0;
            z-index: 60;
            height: 100dvh;
            width: min(17rem, 84vw);
            flex-basis: auto;
            margin: 0;
            border-radius: 0;
            /* Off-canvas by default. `transform` rather than `left` so the
               drawer slides on the compositor instead of relaying out the
               page behind it. */
            transform: translateX(-100%);
            transition: transform 200ms ease;
            box-shadow: var(--shadow-2);
        }
        .nav-state:checked ~ .sidebar { transform: translateX(0); }
        .nav-collapse { display: none; }

        /* Tap anywhere off the drawer to close it. The label covers the page;
           it is only in the layer when the drawer is open. */
        .nav-state:checked ~ .nav-scrim {
            display: block;
            position: fixed; inset: 0; z-index: 50;
            background: rgba(15, 23, 42, .45);
        }

        /* The header wraps instead of pushing the page wider than the
           phone. A flex row whose children will not shrink is the usual way a
           page ends up scrolling sideways, and it is the header rather than
           the table that does it — the table already scrolls inside its own
           box. */
        .topbar { position: sticky; top: 0; flex-wrap: wrap; row-gap: var(--s-2); }
        .topbar > * { min-width: 0; max-width: 100%; }
        .campus-switcher select { max-width: 9rem; }

        .user-menu > .user-summary {
            max-width: none;
            padding: .3rem;
            border-radius: var(--r-full);
        }
        .user-who, .user-caret { display: none; }

        /* The menu is anchored to the right of a button that is now at the
           right edge; without this it opens off the screen. */
        .menu-panel { right: 0; max-width: calc(100vw - 1.5rem); }
    }

    @media (max-width: 40rem) {
        .form-grid { grid-template-columns: 1fr; }
        .filters { padding: var(--s-4); }
        .filters > * { width: 100%; }
        .filters .btn, .filters .actions { width: 100%; }
        .filters .actions .btn { flex: 1; }

        .content { padding: var(--s-3); }
        .topbar { padding: var(--s-2) var(--s-3); gap: var(--s-2); }
        .topbar h1 { font-size: var(--t-lg); }
        .topbar .crumb { display: none; }

        .metrics { grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: var(--s-2); }
        .metric { padding: var(--s-3) var(--s-4); }
        .metric .n { font-size: 1.5rem; }

        .card { border-radius: var(--r-lg); }
        .card-head, .card-body { padding: var(--s-4); }
        .section-title { margin: var(--s-5) 0 var(--s-2); }

        .account-head { padding: var(--s-4); gap: var(--s-3); }
        .account-head .actions { width: 100%; }
        .account-head .actions .btn { width: 100%; }

        /* A table wider than the phone scrolls sideways — and says so, with
           a shadow at the edge that is there only while there is more to
           see. Without it people assume the last column they can see is the
           last column there is. */
        .table-wrap {
            background:
                linear-gradient(to right, var(--n-0) 30%, rgba(255,255,255,0)),
                linear-gradient(to right, rgba(255,255,255,0), var(--n-0) 70%) 100% 0,
                radial-gradient(farthest-side at 0 50%, rgba(15,23,42,.16), rgba(0,0,0,0)),
                radial-gradient(farthest-side at 100% 50%, rgba(15,23,42,.16), rgba(0,0,0,0)) 100% 0;
            background-repeat: no-repeat;
            background-size: 2.5rem 100%, 2.5rem 100%, .75rem 100%, .75rem 100%;
            background-attachment: local, local, scroll, scroll;
        }

        dialog.sheet { width: calc(100vw - 1.5rem); border-radius: var(--r-lg); }
        .sheet-title, .sheet-body, .sheet-foot { padding-left: var(--s-4); padding-right: var(--s-4); }
        .sheet-foot { flex-wrap: wrap; }
        .sheet-foot .btn { flex: 1 1 8rem; }

        .menu-panel { min-width: 13rem; }
        .date-field > .date-text { min-width: 0; }

        /* The list toolbar: a row of buttons and a save-view box that between
           them were wider than the screen. */
        .list-meta { gap: var(--s-2); }
        .list-meta .actions { width: 100%; }
        .list-meta .actions .btn { flex: 1 1 auto; }
        .save-view { width: 100%; }
        .save-view input { flex: 1 1 auto; min-width: 0; }

        .actions > form { min-width: 0; }
        .chip { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
    }

    /* ================= charts (FR-332/335/336) =================
       The SVG carries its own colours as presentation attributes so that it
       renders identically in a browser, in dompdf and on paper — a chart whose
       appearance depends on a stylesheet is one that comes out of the PDF
       pipeline blank. That is also why the plate stays light in the dark
       theme: FR-335's greyscale-safe palette was chosen against white, and
       re-tinting it per theme would mean two palettes to keep honest and one
       of them never looked at. */
    .chart-grid {
        display: grid;
        /* Two across, not three. At three the cards are about 420px and the
           chart inside scales to 58% — the numbers on the axis stop being
           numbers. `min()` lets the track fall below 26rem on a phone, where
           a third column was never going to happen anyway. */
        grid-template-columns: repeat(auto-fit, minmax(min(26rem, 100%), 1fr));
        gap: var(--s-4);
    }
    /* The chart gets the room; the card does not pad it away. */
    .chart-card .card-body { padding: var(--s-4) var(--s-4) var(--s-2); }
    .chart-card { margin-bottom: 0; }
    .chart { width: 100%; height: auto; display: block; }
    .chart-empty {
        margin: 0; padding: var(--s-5) 0; text-align: center;
        font-size: var(--t-sm); color: var(--n-500);
    }
    /* The plate, not the card: a white band edge to edge inside a dark card
       reads as a rendering fault, whereas an inset white panel reads as what
       it is — a printed chart laid on the page. */
    :root[data-theme="dark"] .chart {
        background: #ffffff;
        border-radius: var(--r-lg);
        padding: var(--s-3);
    }
    .chart-figures { border-top: 1px solid var(--n-200); }
    .chart-figures > summary {
        cursor: pointer; padding: var(--s-3) var(--s-5);
        font-size: var(--t-xs); text-transform: uppercase; letter-spacing: .08em;
        font-weight: 800;
        color: var(--n-600);
    }
    .chart-figures > summary:hover { color: var(--accent-text); }
    .dashboard-filters { align-items: flex-end; }

    /* ================= the dashboard =================
       The screen that is open when the school day starts. One column, full
       width: it carried a 20rem rail of lists down the right, which took a
       fifth of the width away from every chart beside it for three panels that
       each belong to a screen of their own. */
    .dash-main { min-width: 0; }

    /* ---- the eight figures ----
       White, not coloured. Eight saturated cards in two rows is a paint chart:
       at four the colour separated the numbers from the page, at eight it
       stops separating anything. The colour moved to the icon, which is
       enough to tell one card from its neighbour at a glance and leaves the
       figure itself the darkest thing in the card. */
    /* Four across, then four again — not `auto-fit`. Eight cards flowing
       freely land five-and-three on a wide screen, and the orphan row reads as
       a card somebody forgot to finish. Two rows of four is also the shape the
       figures come in: four about money, four about who turned up. */
    .tile-row {
        display: grid;
        grid-template-columns: repeat(4, minmax(0, 1fr));
        gap: var(--s-3);
        margin-bottom: var(--s-4);
    }
    @media (max-width: 62rem) { .tile-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
    @media (max-width: 28rem) { .tile-row { grid-template-columns: 1fr; } }
    /*
     * The template's Metric Card, exactly:
     *
     *   bg-gradient-to-b from-{c}-200 to-{c}-100
     *   border-b-4 border-{c}-600 rounded-lg shadow-xl p-5
     *     └ rounded-full p-5 bg-{c}-600  +  a white icon inside it
     *     └ h5 font-bold uppercase text-gray-600   (the label)
     *     └ h3 font-bold text-3xl                  (the figure)
     *
     * The colour is a pastel wash behind the whole card with the saturated step
     * on the bottom edge and in the icon disc. The `tile-{hue}` classes below
     * supply the pair; the card reads them, so a card whose markup names no hue
     * still gets the section's.
     */
    .tile {
        display: block;
        background: linear-gradient(180deg, var(--tile-200, var(--n-100)) 0%, var(--tile-100, var(--n-50)) 100%);
        border: 1px solid transparent;
        border-bottom: 4px solid var(--tile-600, var(--n-400));
        border-radius: var(--r-lg);
        box-shadow: var(--shadow-2);
        padding: var(--s-5);
        color: var(--ink);
        text-decoration: none;
        min-width: 0;
        transition: transform 160ms ease, box-shadow 160ms ease;
    }
    a.tile:hover {
        text-decoration: none;
        transform: translateY(-2px);
        box-shadow: var(--shadow-2);
    }
    a.tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
    /* The disc leads and the words sit to the right of it, which is the
       template's row: `flex-shrink pr-4` then `flex-1 text-right md:text-center`. */
    .tile-top { display: flex; align-items: center; gap: var(--s-4); flex-direction: row-reverse; }
    .tile-l {
        flex: 1 1 auto; min-width: 0;
        font-size: var(--t-sm); font-weight: 700; line-height: 1.35;
        text-transform: uppercase; letter-spacing: .04em;
        color: var(--n-600);
    }
    /* `rounded-full p-5 bg-{c}-600` with a white glyph in it. */
    .tile-mark {
        flex: 0 0 3.25rem;
        width: 3.25rem; height: 3.25rem;
        display: flex; align-items: center; justify-content: center;
        border-radius: var(--r-full);
        background: var(--tile-600, var(--n-500));
        color: #fff;
    }
    .tile-mark .icon { width: 1.6rem; height: 1.6rem; color: #fff; }
    .tile-n {
        margin-top: var(--s-2);
        font-size: var(--t-2xl); font-weight: 700; line-height: 1.2;
        font-variant-numeric: tabular-nums; letter-spacing: -.02em;
        overflow-wrap: anywhere;
        text-align: right;
    }
    .tile-note, .tile-trend { text-align: right; }
    /* The denominator, lighter than the figure it qualifies. It is the thing
       that makes the number mean something, and the thing nobody should read
       first. */
    .tile-of { color: var(--n-400); font-weight: 800; }
    /* The sentence under a figure that needs one. Quiet: it is what the
       number means, not a second number. */
    .tile-note {
        margin-top: .3rem;
        font-size: var(--t-xs); color: var(--n-500); line-height: 1.4;
    }

    /* FR-337/345 — this period against the one before it. Small and beside
       the figure rather than another line under it: a headline tile reads
       one number first, and the trend is context for that number, not a
       second figure competing with it. */
    .tile-trend {
        display: inline-flex; align-items: center; gap: .2rem;
        margin-top: .3rem;
        font-size: var(--t-xs); font-weight: 800;
        font-variant-numeric: tabular-nums;
    }
    .tile-trend.is-up   { color: var(--ok-ink); }
    .tile-trend.is-down { color: var(--bad-ink); }

    /*
     * One hue = Tailwind's `{c}-200`, `{c}-100` and `{c}-600`, which is all a
     * metric card needs: the two ends of the wash and the saturated step for
     * the bottom edge and the disc.
     *
     * The class is on `.tile-mark`, and the variables have to reach the `.tile`
     * around it — so each pair is declared on the tile that *contains* the
     * marked element. `:has` is what makes that possible without the hue being
     * written twice into the markup.
     */
    /* No hue named at all (declared before the hues, so a hue on the mark
       wins over a tone the row around it was given): the section's own, so nothing is ever the grey of
       a card that failed to be given a colour. */
    .tile[data-tone], [data-tone] .tile {
        --tile-200: var(--tone-line);
        --tile-100: var(--tone-bg);
        --tile-600: var(--tone-b);
    }

    .tile:has(.tile-rose)    { --tile-200:#fecdd3; --tile-100:#ffe4e6; --tile-600:#e11d48; }
    .tile:has(.tile-emerald) { --tile-200:#a7f3d0; --tile-100:#d1fae5; --tile-600:#059669; }
    .tile:has(.tile-amber)   { --tile-200:#fde68a; --tile-100:#fef3c7; --tile-600:#d97706; }
    .tile:has(.tile-violet)  { --tile-200:#ddd6fe; --tile-100:#ede9fe; --tile-600:#7c3aed; }
    .tile:has(.tile-indigo)  { --tile-200:#c7d2fe; --tile-100:#e0e7ff; --tile-600:#4f46e5; }
    .tile:has(.tile-sky)     { --tile-200:#bae6fd; --tile-100:#e0f2fe; --tile-600:#0284c7; }
    .tile:has(.tile-teal)    { --tile-200:#99f6e4; --tile-100:#ccfbf1; --tile-600:#0d9488; }
    .tile:has(.tile-orange)  { --tile-200:#fed7aa; --tile-100:#ffedd5; --tile-600:#ea580c; }
    .tile:has(.tile-slate)   { --tile-200:#e2e8f0; --tile-100:#f1f5f9; --tile-600:#475569; }

    /* The same hues on a dark ground: the pastel wash under light ink was
       unreadable, so each hue drops to its dark tone. */
    :root[data-theme="dark"] .tile:has(.tile-rose)    { --tile-200:#5c2326; --tile-100:#2c1416; }
    :root[data-theme="dark"] .tile:has(.tile-emerald) { --tile-200:#1d5137; --tile-100:#0f2a1c; }
    :root[data-theme="dark"] .tile:has(.tile-amber)   { --tile-200:#5b4312; --tile-100:#2e2208; }
    :root[data-theme="dark"] .tile:has(.tile-violet)  { --tile-200:#3f2d75; --tile-100:#241a45; }
    :root[data-theme="dark"] .tile:has(.tile-indigo)  { --tile-200:#2f3a80; --tile-100:#1b2150; }
    :root[data-theme="dark"] .tile:has(.tile-sky)     { --tile-200:#155e75; --tile-100:#0b2a33; }
    :root[data-theme="dark"] .tile:has(.tile-teal)    { --tile-200:#155e56; --tile-100:#0c2b29; }
    :root[data-theme="dark"] .tile:has(.tile-orange)  { --tile-200:#6b3410; --tile-100:#2f1a0a; }
    :root[data-theme="dark"] .tile:has(.tile-slate)   { --tile-200:#334155; --tile-100:#1e293b; }

    /* The disc itself takes the saturated step and a white glyph, whichever
       hue it was given — these classes used to set a pale background and dark
       ink, which is the opposite of the template's disc. */
    .tile-mark.tile-rose, .tile-mark.tile-emerald, .tile-mark.tile-amber,
    .tile-mark.tile-violet, .tile-mark.tile-indigo, .tile-mark.tile-sky,
    .tile-mark.tile-teal, .tile-mark.tile-orange, .tile-mark.tile-slate {
        background: var(--tile-600); color: #fff;
    }


    /* ---- the year panel ---- */
    .panel { margin-bottom: var(--s-4); }
    /* The same grey gradient band the card headers wear — see `.card-head`. */
    .panel-head {
        padding: var(--s-2) var(--s-3);
        background: linear-gradient(180deg, var(--head-from) 0%, var(--head-to) 100%);
        border-bottom: 2px solid var(--n-300);
        border-top-left-radius: var(--r-lg);
        border-top-right-radius: var(--r-lg);
        display: flex; align-items: flex-start; gap: var(--s-3); flex-wrap: wrap;
    }
    .panel-head > div:first-child { flex: 1 1 14rem; min-width: 0; }
    .panel-head .strong {
        font-size: var(--t-md); font-weight: 700;
        text-transform: uppercase; letter-spacing: .04em;
        color: var(--n-600);
    }
    .panel-controls { display: flex; gap: var(--s-2); flex-wrap: wrap; align-items: center; }
    .panel-controls select {
        font-size: var(--t-sm);
        padding: .45rem 1.9rem .45rem .7rem;
    }

    /*
     * The dashboard's own controls, a size up.
     *
     * Apply and Print Report are the things anybody does
     * on this screen, and at `btn-sm` they were the smallest targets on the
     * page — a 1.75rem pill carrying the only actions it offers. Scoped to
     * the two bars they sit in rather than changed on `.btn-sm`, which is
     * worn by every table row action in the product and is the right size
     * there.
     */
    .panel-controls .btn,
    .dashboard-filters .btn {
        font-size: var(--t-md);
        min-height: 2.25rem;
        padding: .4rem 1rem;
        gap: .45rem;
    }
    .panel-controls .btn .icon,
    .dashboard-filters .btn .icon { width: 1.15rem; height: 1.15rem; }

    /* A key and its colour, for a chart whose legend is HTML rather than ink
       on the plate. Screen-only: the printed charts draw their own (FR-335). */
    .legend { display: flex; gap: var(--s-3); flex-wrap: wrap; margin-top: var(--s-2); }
    .legend-key {
        display: inline-flex; align-items: center; gap: .4rem;
        font-size: var(--t-sm); font-weight: 600; color: var(--n-600);
        min-width: 0;
    }
    .legend-key i {
        width: .7rem; height: .7rem; border-radius: 3px; flex: 0 0 .7rem;
    }
    .legend-key b { color: var(--ink); font-variant-numeric: tabular-nums; }
    .legend-wrap { margin: 0 0 var(--s-4); gap: var(--s-2) var(--s-4); }

    /* ---- the two splits ---- */
    .split-row {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(min(24rem, 100%), 1fr));
        gap: var(--s-4);
        margin-bottom: var(--s-4);
    }
    .split-card { margin-bottom: 0; }
    .split-total {
        display: flex; justify-content: space-between; align-items: baseline;
        margin-top: var(--s-4); padding-top: var(--s-3);
        border-top: 1px solid var(--n-200);
        font-size: var(--t-sm); color: var(--n-600);
    }
    .split-total strong {
        font-size: var(--t-lg); color: var(--ink);
        font-variant-numeric: tabular-nums;
    }

    /* ---- a count against its whole ----
       Student attendance, staff attendance, fees, enquiries: four panels, one
       shape, one partial. The figures are the primary reading and the bar is
       the secondary one — which is what keeps the panel honest in greyscale,
       where the tone of the bar carries nothing (FR-335). */
    .meter-card { margin-bottom: 0; }
    .meter + .meter { margin-top: var(--s-4); }
    .meter-head {
        display: flex; align-items: baseline; justify-content: space-between;
        gap: var(--s-3);
    }
    .meter-l {
        font-size: var(--t-xs); font-weight: 800;
        letter-spacing: .08em; text-transform: uppercase;
        color: var(--n-700);
        overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    .meter-p {
        font-size: var(--t-md); font-weight: 800;
        font-variant-numeric: tabular-nums;
        color: var(--ink);
    }
    .meter-sub { font-size: var(--t-xs); color: var(--n-500); margin-top: .1rem; }
    .meter-bar {
        height: .35rem; margin-top: var(--s-2);
        border-radius: var(--r-full);
        background: var(--n-100);
        overflow: hidden;
    }
    .meter-fill { display: block; height: 100%; border-radius: var(--r-full); }
    /* A zero-width bar still shows the sliver of its own colour, so a row
       reading 0% is visibly a row rather than a gap in the panel. */
    .meter-fill { min-width: 2px; }
    .meter-ok   { background: #22c55e; }
    .meter-warn { background: #f59e0b; }
    .meter-bad  { background: #ef4444; }
    .meter-info { background: #3b82f6; }

    /* ================= the colour system =================
       Twelve tones, and the one rule that makes them worth having: a tone is
       never written into a template. A block is told which tone it is —
       `data-tone="5"` — and every surface inside it takes its heading tint,
       its border and its table head from the same three tokens. Retinting a
       module is one number, not a sweep through eighty files.

       Why twelve. The menu has twelve groups, and the point of the exercise is
       that Fees does not look like Attendance: one hue per group means the
       screen says which part of the school you are in before you have read the
       title. Within a page the tones step by three so two sections stacked on
       top of each other are never neighbouring hues.

       These are the same twelve the tiles have always used, so nothing that was
       already coloured has to change to match. */
    [data-tone="0"]  { --tone-ink:#1d4ed8; --tone-bg:#eaf2ff; --tone-line:#c5dbfb; --tone-a:#3b82f6; --tone-b:#2563eb; }
    [data-tone="1"]  { --tone-ink:#047857; --tone-bg:#d1fae5; --tone-line:#a7f3d0; --tone-a:#10b981; --tone-b:#059669; }
    [data-tone="2"]  { --tone-ink:#6d28d9; --tone-bg:#ede9fe; --tone-line:#ddd6fe; --tone-a:#8b5cf6; --tone-b:#6d28d9; }
    [data-tone="3"]  { --tone-ink:#b45309; --tone-bg:#fef3c7; --tone-line:#fde68a; --tone-a:#f59e0b; --tone-b:#d97706; }
    [data-tone="4"]  { --tone-ink:#be123c; --tone-bg:#ffe4e6; --tone-line:#fecdd3; --tone-a:#f43f5e; --tone-b:#e11d48; }
    [data-tone="5"]  { --tone-ink:#0f766e; --tone-bg:#ccfbf1; --tone-line:#99f6e4; --tone-a:#14b8a6; --tone-b:#0f766e; }
    [data-tone="6"]  { --tone-ink:#4338ca; --tone-bg:#e0e7ff; --tone-line:#c7d2fe; --tone-a:#6366f1; --tone-b:#4338ca; }
    [data-tone="7"]  { --tone-ink:#c2410c; --tone-bg:#ffedd5; --tone-line:#fed7aa; --tone-a:#f97316; --tone-b:#c2410c; }
    [data-tone="8"]  { --tone-ink:#0e7490; --tone-bg:#cffafe; --tone-line:#a5f3fc; --tone-a:#06b6d4; --tone-b:#0e7490; }
    [data-tone="9"]  { --tone-ink:#be185d; --tone-bg:#fce7f3; --tone-line:#fbcfe8; --tone-a:#ec4899; --tone-b:#be185d; }
    [data-tone="10"] { --tone-ink:#4d7c0f; --tone-bg:#ecfccb; --tone-line:#d9f99d; --tone-a:#84cc16; --tone-b:#4d7c0f; }
    [data-tone="11"] { --tone-ink:#334155; --tone-bg:#e2e8f0; --tone-line:#cbd5e1; --tone-a:#64748b; --tone-b:#334155; }

    /* Dark rebuilds the tints rather than dimming them, for the same reason
       the semantic panels above are rebuilt: a tint that is only a darker
       version of itself reads as a surface that has been switched off. The
       gradient stops are left alone — they carry white text on both themes. */
    :root[data-theme="dark"] [data-tone="0"]  { --tone-ink:#93c5fd; --tone-bg:#14264a; --tone-line:#25406e; }
    :root[data-theme="dark"] [data-tone="1"]  { --tone-ink:#6ee7a8; --tone-bg:#0f2a1c; --tone-line:#1d5137; }
    :root[data-theme="dark"] [data-tone="2"]  { --tone-ink:#c4b5fd; --tone-bg:#241a45; --tone-line:#3f2d75; }
    :root[data-theme="dark"] [data-tone="3"]  { --tone-ink:#fbbf24; --tone-bg:#2e2208; --tone-line:#5b4312; }
    :root[data-theme="dark"] [data-tone="4"]  { --tone-ink:#fda4af; --tone-bg:#2c1416; --tone-line:#5c2326; }
    :root[data-theme="dark"] [data-tone="5"]  { --tone-ink:#5eead4; --tone-bg:#0c2b29; --tone-line:#155e56; }
    :root[data-theme="dark"] [data-tone="6"]  { --tone-ink:#a5b4fc; --tone-bg:#1b2150; --tone-line:#2f3a80; }
    :root[data-theme="dark"] [data-tone="7"]  { --tone-ink:#fdba74; --tone-bg:#2f1a0a; --tone-line:#6b3410; }
    :root[data-theme="dark"] [data-tone="8"]  { --tone-ink:#67e8f9; --tone-bg:#0b2a33; --tone-line:#155e75; }
    :root[data-theme="dark"] [data-tone="9"]  { --tone-ink:#f9a8d4; --tone-bg:#33101f; --tone-line:#6d1a3c; }
    :root[data-theme="dark"] [data-tone="10"] { --tone-ink:#bef264; --tone-bg:#1e2a0b; --tone-line:#3f5314; }
    :root[data-theme="dark"] [data-tone="11"] { --tone-ink:#cbd5e1; --tone-bg:#1e293b; --tone-line:#334155; }

    /* A screen with no tone at all still has one: the school's own accent.
       Nothing below has to test whether the tokens were set. */
    body {
        --tone-ink: var(--accent-text);
        --tone-bg: var(--accent-soft);
        --tone-line: var(--info-line);
        --tone-a: var(--accent);
        --tone-b: var(--accent-strong);
    }

    /* ---- the band that opens a group ----
       A filled strip, in the section's own tint — the same treatment a table
       head gets, for the same reason: it is the line that says a new block
       starts here, and a bare coloured word does that far less well than a
       band does. The bar on its left keeps the hue readable in greyscale and
       for anybody who cannot tell the tints apart. */
    .section-title {
        display: flex; align-items: center; gap: var(--s-2);
        color: var(--tone-ink);
        background: var(--tone-bg);
        border: 1px solid var(--tone-line);
        border-radius: var(--r-md);
        padding: .4rem var(--s-3);
    }
    .section-title::before {
        content: '';
        flex: 0 0 auto;
        width: .25rem; height: 1em;
        border-radius: var(--r-full);
        background: linear-gradient(180deg, var(--tone-a), var(--tone-b));
    }

    /* ---- cards ----
       The header band stays grey, as the template draws it, and the section's
       colour goes on one edge instead. A card that wore its module's tint in
       the band was the paint chart the headline tiles are careful not to be,
       and the grey band is what stops thirty of them being one. */
    .card[data-tone], .panel[data-tone], [data-tone] > .card, [data-tone] > .panel {
        border-bottom: 4px solid var(--tone-a);
    }

    /* ---- tables ----
       The head is a grey bar, and deliberately the one place on the page the
       section's colour does not reach.

       It was tinted with the rest at first and that was the wrong call: a
       table is the densest thing on any of these screens — two hundred rows
       of names and amounts — and a coloured band over it competes with the
       figures for the eye instead of framing them. Grey separates the
       headings from the data and then gets out of the way, which is the whole
       job of a table head. The colour stays on the band above the table,
       where it says which section this is. */
    table.grid th {
        background: var(--n-100);
        /* `text-left text-blue-900` — the template's one flash of colour inside
           a panel, and the thing that tells a heading row from a data row
           without a rule heavy enough to compete with the figures. */
        color: var(--th-ink);
        border-bottom: 1px solid var(--n-300);
    }
    /* Every other row in the faintest grey, so the eye tracks a wide row from
       the name on the left to the amount on the right without losing its
       place. The hover is a step darker and stays the stronger signal. */
    table.grid tbody tr:nth-child(even) td { background: var(--n-25); }
    table.grid tbody tr:hover td { background: var(--n-100); }

    /* ---- the toolbar above a list ---- */
    .filters {
        border-left: 3px solid var(--tone-line);
        background: linear-gradient(180deg, var(--tone-bg) 0%, var(--n-0) 68%);
    }

    /* ---- an empty result ----
       Tinted rather than grey. "Nothing here" is an outcome of the screen you
       are on, and it reads as a page that failed to load when it is the only
       colourless thing on it. */
    .empty { border-color: var(--tone-line); background: color-mix(in srgb, var(--tone-bg) 45%, var(--n-0)); }

    /* ---- the figures ----
       The number stays near-black — it is the thing being read. The colour
       goes on the edge, the label and the icon, which is enough to tell one
       card from the next without competing with the figure. */
    /* The saturated step goes on the bottom edge, as the template draws it —
       this used to be a 3px rule along the top. */
    .metric[data-tone] { border-bottom-color: var(--tone-b); }
    .metric[data-tone] .l { color: var(--n-600); }
    /*
     * Only the marks nobody has already coloured: the dashboard's eight tiles
     * carry their own and must keep it.
     *
     * Named one by one rather than `:not([class*="tile-"])`, which cannot
     * work — the element's own class is `tile-mark`, so a substring match on
     * `tile-` excludes every mark on the site including the plain ones this
     * rule exists for.
     */
    .tile-mark:not(.tile-rose):not(.tile-emerald):not(.tile-amber):not(.tile-violet):not(.tile-indigo):not(.tile-sky):not(.tile-teal):not(.tile-orange):not(.tile-slate) {
        background: var(--tone-bg);
        color: var(--tone-ink);
    }

    /* ---- the module's own colour, in the frame ----
       A strip under the header and a wash behind the page title: the two
       places a reader is already looking when a screen opens. */
    .topbar { border-bottom: 2px solid var(--tone-a); }
    .topbar .crumb { color: var(--tone-a); }

    /* ---- the menu ----
       Each group wears its module's colour, so the strip under the header and
       the row that was clicked to get here are the same hue. The heading's
       own icon carries the colour — a shape to aim at from the closed menu,
       the way the reference template puts one icon on every category. */
    .nav-group[data-tone] > summary .icon { color: var(--tone-a); opacity: 1; }
    .nav-group[data-tone].is-here > .nav-label { color: var(--tone-a); }
    /* Hover turns the rule the section's own colour — the template's
       `hover:border-pink-500`, one hue per section instead of a hard-coded
       five — and the row you are actually on keeps it lit. Nothing is filled:
       see the note on `.nav a`. */
    .nav-group[data-tone] a:hover { border-bottom-color: var(--tone-a); }
    .nav-group[data-tone] a[aria-current="page"] {
        background: none;
        border-bottom-color: var(--tone-a);
        box-shadow: none;
    }

    /* ---- and the icon itself ----
       One grey for sixty icons made the column read as a single texture the
       eye slides down. Each takes its own module's hue, so Fees is green and
       Exams is pink wherever you meet them — the same hue the dot beside the
       group, the strip under the header and that section's bands already
       wear. No palette is invented here; it is `--tone-a`, which every one
       of those reads too.

       The `.icon` selector is deliberately narrow: `.nav a` also carries the
       caret on a group summary, which must stay the label's colour. */
    .nav-group[data-tone] a .icon { color: var(--tone-a); opacity: 1; }
    /* The row you are on is no longer filled, so its icon keeps the hue
       rather than going white — which is the template's active row exactly:
       a coloured icon, white text, coloured rule. */
    .nav-group[data-tone] a[aria-current="page"] .icon { color: var(--tone-a); }

    /* ---- badges the stylesheet never had ----
       `badge-success`, `badge-danger` and `badge-muted` are worn by the export
       list, the backup log and the messaging spend screen, and none of the
       three was ever defined here — so a finished export and a failed one were
       the same grey pill. Aliases rather than new colours: a fourth green in a
       product that already has one is how two screens come to disagree about
       what "done" looks like. */
    .badge-success { background: var(--ok-bg);   color: var(--ok-ink);   border-color: var(--ok-line); }
    .badge-danger  { background: var(--bad-bg);  color: var(--bad-ink);  border-color: var(--bad-line); }
    .badge-muted   { background: var(--n-100);   color: var(--n-600);    border-color: var(--n-200); }

    /* A plain `.badge` with nothing after it — a class, a period, a template
       name — takes the section's tint rather than staying the one colourless
       thing in a coloured row. */
    .badge:not([class*="badge-"]) {
        background: var(--tone-bg);
        color: var(--tone-ink);
        border-color: var(--tone-line);
    }

    /* A label that only the screen reader reads. The two selects on the year
       panel say what they are in their own options — "Session: 2026",
       "All months" — so a visible label beside each would be the same word
       twice. */
    .sr-only {
        position: absolute; width: 1px; height: 1px;
        padding: 0; margin: -1px; overflow: hidden;
        clip: rect(0 0 0 0); white-space: nowrap; border: 0;
    }

    @media print {
        .sidebar, .topbar, .filters, .list-meta, .pager, .theme-toggle, .no-print { display: none !important; }
        /* The page-title band is chrome too, and a dark gradient across the
           top of an A4 sheet is most of a toner cartridge. The screen name is
           already in the printed header. */
        .page-head-wrap { display: none !important; }
        .content { padding: 0; }
        .card { border: 0; box-shadow: none; }

        /*
         * FR-332 — a printed chart carries its figures. The table is collapsed
         * on screen and forced open on paper, because a chart on a sheet of A4
         * with no numbers under it cannot be checked against anything, which is
         * the whole reason a school prints one.
         */
        .chart-figures > summary { display: none; }
        /* A closed <details> hides its children in the browser regardless of
           CSS on the element itself, so the children are the ones overridden. */
        .chart-figures:not([open]) > *:not(summary) { display: block !important; }
        .panel-controls { display: none !important; }
        .tile-row { grid-template-columns: repeat(4, 1fr); }
        .tile, .split-card, .meter-card, .panel { break-inside: avoid; }
        .chart-grid { grid-template-columns: 1fr; gap: var(--s-3); }
        .chart-card { page-break-inside: avoid; }

        /*
         * The tints come off on paper. They are there to tell one section of
         * the screen from another while somebody moves around the app; a
         * printed sheet is one section by definition, and a school printing a
         * 200-row defaulter list should not be buying ink for a wash behind
         * every other row. The section bands and the coloured text stay —
         * they are thin, and they are what makes a printed page identifiable
         * as coming from the screen it was printed from.
         */
        .card-head, .panel-head, table.grid th,
        table.grid tbody tr td, .filters, .empty {
            background: #fff !important;
        }
    }

    /*
     * The setup wizard (FR-011a).
     *
     * Three surfaces that have to read as one thing: the list, the strip along
     * the top of each step's own screen, and the quiet reminder everywhere
     * else. They share the ring, the bar and the tick, so a school recognises
     * the second and third from having seen the first.
     */
    .setup-hero {
        display: flex; align-items: center; gap: var(--s-6);
        padding: var(--s-6);
        margin-bottom: var(--s-4);
    }
    .setup-hero-text { min-width: 0; flex: 1 1 auto; }
    .setup-kicker {
        margin: 0 0 .25rem;
        font-size: var(--t-xs); font-weight: 800; letter-spacing: .08em;
        text-transform: uppercase; color: var(--tone-ink, var(--n-500));
    }
    .setup-title { margin: 0 0 .5rem; font-size: var(--t-2xl); font-weight: 900; color: var(--n-900); }
    .setup-lede { margin: 0; font-size: var(--t-md); line-height: 1.6; color: var(--n-700); max-width: 60ch; }

    /* The ring is the figure a school looks for first, so it is drawn from
       the same number the bar is and never from a second one. */
    .setup-ring {
        flex: 0 0 auto;
        width: 7.5rem; height: 7.5rem;
        border-radius: var(--r-full);
        display: flex; flex-direction: column; align-items: center; justify-content: center;
        background: conic-gradient(var(--tone-a, var(--accent)) calc(var(--pct) * 1%), var(--n-200) 0);
        position: relative;
    }
    .setup-ring::after {
        content: ''; position: absolute; inset: .6rem;
        background: var(--n-0); border-radius: var(--r-full);
    }
    .setup-ring-num {
        position: relative; z-index: 1;
        font-size: var(--t-2xl); font-weight: 900; line-height: 1; color: var(--n-900);
    }
    .setup-ring-num span { font-size: var(--t-sm); font-weight: 700; }
    .setup-ring-sub { position: relative; z-index: 1; font-size: var(--t-xs); color: var(--n-600); margin-top: .2rem; }

    .setup-bar { height: .5rem; border-radius: var(--r-full); background: var(--n-200); overflow: hidden; }
    .setup-bar-fill {
        height: 100%; border-radius: var(--r-full);
        background: var(--tone-a, var(--accent));
        transition: width 300ms ease;
    }

    .setup-actions { margin: var(--s-4) 0 var(--s-6); flex-wrap: wrap; }
    .setup-left { font-size: var(--t-sm); color: var(--warn-ink); font-weight: 700; }
    .setup-left-ok { color: var(--ok-ink); }

    .setup-list { display: flex; flex-direction: column; gap: var(--s-2); margin-bottom: var(--s-5); }
    .setup-step {
        display: flex; align-items: center; gap: var(--s-4);
        padding: var(--s-4);
        background: var(--n-0);
        border: 1px solid var(--n-200);
        border-radius: var(--r-panel);
    }
    .setup-step.is-done { background: var(--ok-bg); border-color: var(--ok-line); }
    .setup-step-mark {
        flex: 0 0 auto;
        width: 2rem; height: 2rem; border-radius: var(--r-full);
        display: flex; align-items: center; justify-content: center;
        background: var(--n-100); color: var(--n-600);
        font-weight: 800; font-size: var(--t-sm);
    }
    .setup-step.is-done .setup-step-mark { background: var(--ok-ink); color: #fff; }
    .setup-step.is-done .setup-step-mark svg { width: 1rem; height: 1rem; }
    .setup-step-body { flex: 1 1 auto; min-width: 0; }
    .setup-step-head { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
    .setup-step-title { font-size: var(--t-md); font-weight: 800; color: var(--n-900); }
    .setup-step-why { margin: .2rem 0 0; font-size: var(--t-sm); color: var(--n-600); line-height: 1.5; }
    .setup-step-do { flex: 0 0 auto; }

    /* The strip: the wizard's own chrome on somebody else's screen. */
    .setup-strip {
        position: relative;
        display: flex; align-items: center; gap: var(--s-4); flex-wrap: wrap;
        padding: var(--s-4) var(--s-5) calc(var(--s-4) + .4rem);
        margin-bottom: var(--s-4);
        background: var(--tone-bg, var(--n-50));
        border: 1px solid var(--tone-line, var(--n-200));
        border-radius: var(--r-panel);
        overflow: hidden;
    }
    .setup-strip-mark {
        flex: 0 0 auto;
        width: 2.25rem; height: 2.25rem; border-radius: var(--r-full);
        display: flex; align-items: center; justify-content: center;
        background: var(--tone-a, var(--accent)); color: #fff;
        font-weight: 900;
    }
    .setup-strip-body { flex: 1 1 18rem; min-width: 0; }
    .setup-strip-kicker {
        margin: 0; font-size: var(--t-xs); font-weight: 800; letter-spacing: .06em;
        text-transform: uppercase; color: var(--tone-ink, var(--n-600));
    }
    .setup-strip-title { margin: .1rem 0 0; font-size: var(--t-lg); font-weight: 800; color: var(--n-900); }
    .setup-strip-why { margin: .15rem 0 0; font-size: var(--t-sm); color: var(--n-700); line-height: 1.5; }
    .setup-strip-do { flex: 0 0 auto; }
    .setup-strip-bar { position: absolute; left: 0; right: 0; bottom: 0; height: .4rem; background: var(--n-200); }
    .setup-strip-bar span { display: block; height: 100%; background: var(--tone-a, var(--accent)); transition: width 300ms ease; }

    /* The reminder everywhere else. One line, and skippable by the eye. */
    .setup-nudge {
        display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap;
        padding: var(--s-3) var(--s-4);
        margin-bottom: var(--s-4);
        background: var(--n-0);
        border: 1px solid var(--n-200);
        border-left: 4px solid var(--accent);
        border-radius: var(--r-panel);
    }
    .setup-nudge-bar { flex: 0 0 6rem; height: .4rem; border-radius: var(--r-full); background: var(--n-200); overflow: hidden; }
    .setup-nudge-bar span { display: block; height: 100%; background: var(--accent); }
    .setup-nudge-text { flex: 1 1 auto; font-size: var(--t-sm); font-weight: 700; color: var(--n-800); }

    @media (max-width: 52rem) {
        .setup-hero { flex-direction: column; align-items: flex-start; }
        .setup-step { flex-wrap: wrap; }
        .setup-step-do { width: 100%; }
    }

    @media print {
        .setup-strip, .setup-nudge { display: none !important; }
    }

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