/* ══════════════════════════════════════════════════════════════════════════
   Legacy un-namespaced palette. Every component in this file still consumes
   THESE names, so they and the --ax-* foundation below must always hold the
   same values — a one-sided edit splits the dashboard's palette in half.

   Re-valued 2026-08-15 into the Semester Calendar language (Stage 4). Source
   of every value: widgets/academic-calendar.js:428-452, the CSS behind
   instructor/calendar.html. Cool slate on white became warm neutral under
   white cards; sky #0ea5e9 became the wordmark teal #0E8C7A, which is the
   value docs/design-lab/index.html has called "the proposed dashboard
   direction" since 2026-08-05.

   --accent2..5 survive as NAMES only because ~40 rules still reference them.
   They are no longer "more brand colours" (dashboard-design.md §3 allows
   exactly one accent) — they now hold the calendar's warm status marks, so
   .btn-success / -warn / -danger read as status rather than as palette.
   ══════════════════════════════════════════════════════════════════════════ */
:root {
  --bg: #F7F7F4;      /* acal-bg        — the page ground, no longer white */
  --bg2: #FBFBF9;     /* acal-surface-2 — insets, field fills, row hover   */
  --bg3: #F1F0EC;     /* derived        — quiet fills one step down        */
  --card: #FFFFFF;    /* acal-surface   — the raised surface, unchanged    */
  --border: #E9E9E4;  /* acal-border                                       */
  --accent: #0E8C7A;  /* acal-accent                                       */
  --accent2: #5449B5; /* acal-cons-bd   — the one non-status chromatic use */
  --accent3: #5F9420; /* acal-study-bd  — success                          */
  --accent4: #B5730F; /* acal-holi-bd   — warning                          */
  --accent5: #A32D2D; /* acal-mid-bd    — danger                           */
  --text: #191C24;    /* acal-ink                                          */
  --text2: #585E6E;   /* acal-ink-2                                        */
  --text3: #9195A3;   /* acal-ink-3                                        */
  /* Shell palette — neutral charcoal since 2026-08-14 (was cool navy-slate:
     bg #0f172a, fg #cbd5e1, muted #64748b, hover #1e293b). One grey ramp, four
     rungs, ~16-19 points apart, so band → record → hover reads as one ladder
     rather than three unrelated fills. The accent is the ONLY chromatic thing
     left in the shell, which is what makes the active row findable at a glance.
     --sidebar-row / -line / -rule / -ink replace rgba() literals that used to be
     written at each consumer; see the Menu anatomy block below. */
  --sidebar-bg: #333333;     /* group band + the base under everything */
  --sidebar-row: #464646;    /* the lifted record fill */
  --sidebar-line: #333333;   /* record separator — the band showing through */
  --sidebar-rule: #8a8f93;   /* the icon-rail / label divider */
  --sidebar-fg: #e8e8e8;     /* label ink            — 7.8:1 on --sidebar-row */
  --sidebar-ink: #f7f7f7;    /* band + hovered label — 11.5:1 on --sidebar-bg */
  --sidebar-muted: #9a9a9a;  /* rail icons, footer   —  4.5:1 on --sidebar-bg */
  --sidebar-hover: #565656;
  --sidebar-width: 236px;
  /* Unified dashboard content widths (admin + instructor).
     1400 since 2026-08-14, down from 1600. The pair is the point: 1080 is the
     reading measure for single-column forms, 1400 is the data measure for
     tables and tool panes, and the ~320px between them is what keeps the two
     tokens meaning different things. The value is not new — the Roster pane had
     capped itself at 1400 page-locally since 2026-08-13 with the note that its
     stat tiles and table "read as one measure at 1400 and as scattered
     fragments at 1600"; this promotes that finding and deletes the override.
     Panes are flush to the inline-start (no auto margins anywhere), so the cap
     only binds above ~1690px of viewport — laptops render identically. */
  --content-max: 1400px; /* data / table / tool sections */
  --form-max: 1080px;    /* course-setup single-column form pages */
}

/* ══════════════════════════════════════════════════════════════════════════
   AX TOKEN FOUNDATION  (Stage 1a 2026-08-05; RE-VALUED Stage 4 2026-08-15)
   ──────────────────────────────────────────────────────────────────────────
   docs/dashboard-design.md §2 — the dashboard's token contract, namespaced
   --ax-*. It landed inert: every value was a copy of what the rule above it
   already rendered, so the block provably could not change a pixel. That was
   the point — foundation first, consumers second, re-valuation third.

   THIS IS THE RE-VALUATION. Every value below now comes from
   widgets/academic-calendar.js:428-452 — the CSS behind
   instructor/calendar.html ("Semester Calendar & Course Dates"), the one
   dashboard surface with a finished visual identity. It stays Tier B and
   exempt (§1b); it is now also the reference the rest of the file describes.
   The legacy un-namespaced block above holds the same values and must be
   edited in the same breath as this one.

   WHY THE NAMESPACE. shared/styles.css and courses/_shared/_chapter.css each
   declare 13 of the same variable names and disagree on 10 of them (--accent
   is #0ea5e9 here, #5b47e0 there). admin/appearance.html loads BOTH, so a
   saved learning theme currently recolours that page's own buttons while you
   edit colours in it. Namespacing is what ends that permanently — see
   docs/design-system.md §2b and docs/visual-debt.md VD-3.

   DELIBERATELY ABSENT — these live in the .ax-* form layer's own :root
   further down this file, and were re-valued there in the same pass:
       --ax-r (10px) · --ax-r-sm (8px) · --ax-bw (1px)
       --ax-fs-label (.75) / --ax-fs-ctrl (.875) / --ax-fs-hint (.6875rem)
   Do not duplicate them into this block — one declaration site each.
   ══════════════════════════════════════════════════════════════════════════ */
:root {
  /* ── Surface ramp ─────────────────────────────────────────────────────── */
  --ax-bg:            #F7F7F4;
  --ax-bg2:           #FBFBF9;
  --ax-bg3:           #F1F0EC;
  --ax-card:          #FFFFFF;
  --ax-border:        #E9E9E4;
  --ax-border-strong: #D8D6CE;  /* dividers that must be seen */

  /* ── Ink ──────────────────────────────────────────────────────────────── */
  --ax-text:          #191C24;
  --ax-text2:         #585E6E;
  --ax-text3:         #9195A3;
  --ax-text4:         #B0B3BD;  /* faint — timestamps, disabled, hints */

  /* ── Accent (ONE interactive accent; -ink is the text-safe variant) ───── */
  --ax-accent:        #0E8C7A;
  --ax-accent-ink:    #0A5147;  /* acal-accent-ink — hover fills + ink on -bg */
  --ax-accent-bg:     #E5F5F2;  /* acal-accent-bg — the soft fill behind icon
                                   tiles, selected rows and quiet badges.
                                   Added 2026-08-15: pages hand-wrote
                                   rgba(14,165,233,.10) because no name existed. */
  --ax-accent-rgb:    14,140,122;

  /* ── Type stacks — one declaration for both faces (dashboard-design.md §2).
        The Segoe stack was re-declared per page and `Consolas` appeared in
        three spellings; these two names end that. The pair is the calendar's:
        IBM Plex Sans Arabic carries Arabic, which the platform already
        stores, and Space Grotesk's figures are what make a mark sheet read.
        The <link> that fetches them is per page — see dashboard-design.md §6. */
  --ax-font-body:     "IBM Plex Sans Arabic", system-ui, -apple-system, "Segoe UI", sans-serif;
  --ax-font-mono:     "Space Grotesk", "Segoe UI", monospace;

  /* ── Semantic status — THE one declaration site (dashboard-design.md §2 ·
        visual-debt VD-6). Three states, each a triple: the line/mark colour,
        the soft fill, and the text colour legible on that fill. Write the
        semantic name, never the literal pair. Values are byte-identical to
        what pages already hardcode, so adoption never changes rendering.

        Consumers today: `.status-msg.success/.error` (this file, Stage 1b)
        and instructor/content-map.html, where the triples ARE the
        published/draft/late vocabulary — re-valuing them changes what that
        page means, not just how it looks (dashboard-design.md §1b, Tier C).

        RE-VALUED 2026-08-15 (Stage 4), and this family is the one that needed
        a decision rather than a swap. The calendar has no status vocabulary —
        it has a DATA palette for study weeks, holidays and exams. So these
        nine are derived from it (study green, midterm red, holiday amber),
        lightened for message fills, rather than kept at the old cool literals:
        #dcfce7 is a cool mint that reads as a colour cast next to #F7F7F4,
        not as a status.

        ⚠ TIER C (dashboard-design.md §1b): instructor/content-map.html reads
        all nine, and there the triples ARE the published / draft / late
        vocabulary — this changes what that matrix means, not just how it
        looks. Confirmed wanted there before landing, per §1b option 2.

        Still hardcoded in this file, pending a later pass: .stub-pill.soon,
        .game-opt[data-c="2"], .gm-p1, .bx-half, .bx-reset, .tag-badge. */
  --ax-ok:            #5F9420;   /* acal-study-bd  */
  --ax-ok-bg:         #E4F0D4;   /* acal-study-bg, lightened for a fill */
  --ax-ok-fg:         #2C4A12;   /* acal-study-ink, lifted off pure black */
  --ax-err:           #A32D2D;   /* acal-mid-bd    */
  --ax-err-bg:        #F9DEDE;   /* acal-mid-bg, lightened */
  --ax-err-fg:        #6B1E1E;   /* acal-mid-ink, lifted */
  --ax-warn:          #B5730F;   /* acal-holi-bd   */
  --ax-warn-bg:       #FBEBD0;   /* acal-holi-bg, lightened */
  --ax-warn-fg:       #5C3A08;   /* acal-holi-ink, lifted */

  /* ── Type scale — 7 steps replacing 44 ad-hoc sizes.
        Anchor is --ax-fs-4: the real working size of a dense academic
        dashboard, which is why 17 values had accreted between .62 and .95rem. */
  --ax-fs-1:          0.6875rem;  /* 11px — micro labels, table column heads  */
  --ax-fs-2:          0.75rem;    /* 12px — hints, meta, timestamps           */
  --ax-fs-3:          0.8125rem;  /* 13px — dense data, table cells           */
  --ax-fs-4:          0.875rem;   /* 14px — body, controls          ← anchor  */
  --ax-fs-5:          0.9375rem;  /* 15px — sub-headings      ← 1rem, 08-15   */
  --ax-fs-6:          1.25rem;    /* 20px — headline stats                    */
  --ax-fs-7:          1.375rem;   /* 22px — page titles     ← 1.75rem, 08-15  */

  /* ── Space — 4px base, replacing every integer 1..14px in use ─────────── */
  --ax-sp-1:          4px;
  --ax-sp-2:          8px;
  --ax-sp-3:          12px;
  --ax-sp-4:          16px;
  --ax-sp-5:          24px;
  --ax-sp-6:          32px;
  --ax-sp-7:          48px;

  /* ── Radius — still four values. --ax-r / --ax-r-sm live in the .ax-*
        layer (see header); the ladder across both blocks is 8 / 10 / 16 /
        pill since 2026-08-15, matching --acal-r-card at the top end. ─────── */
  --ax-r-lg:          16px;    /* acal-r-card — cards, panels, table shells */
  --ax-r-pill:        999px;

  /* ── Border — --ax-bw lives in the .ax-* layer (see header) ───────────── */
  --ax-bw-strong:     1.5px;   /* focus repaint only */

  /* ── Elevation. This is the one place the calendar contradicts what this
        file used to say, and the contradiction was adopted deliberately:
        shadows are no longer "near-invisible", they are the card language.
        --ax-sh-1 IS --acal-shadow, verbatim — a 1px contact shadow plus a
        wide, heavily-negative-spread ambient one, which is what lets a white
        card sit ON the warm ground instead of being cut out of it. The
        hairline border stays underneath. See dashboard-design.md §3. ─────── */
  --ax-sh-1:          0 1px 2px rgba(20,24,40,.04), 0 8px 24px -12px rgba(20,24,40,.10);
  --ax-sh-2:          0 2px 6px rgba(20,24,40,.06), 0 12px 32px -14px rgba(20,24,40,.18);
  --ax-sh-3:          0 4px 12px rgba(20,24,40,.08), 0 24px 56px -20px rgba(20,24,40,.30);

  /* ── Z-index — names for the ad-hoc 100 / 999 / 3000 / 9999 ladder ────── */
  --ax-z-sticky:      100;
  --ax-z-overlay:     1000;
  --ax-z-modal:       2000;
  --ax-z-toast:       3000;

  /* ── Shell (dashboard-only; no learning-surface equivalent) ───────────── */
  --ax-sidebar-bg:    #333333;
  --ax-sidebar-row:   #464646;
  --ax-sidebar-line:  #333333;
  --ax-sidebar-rule:  #8a8f93;
  --ax-sidebar-fg:    #e8e8e8;
  --ax-sidebar-ink:   #f7f7f7;
  --ax-sidebar-muted: #9a9a9a;
  --ax-sidebar-hover: #565656;
  --ax-sidebar-width: 236px;
  --ax-content-max:   1400px;   /* re-valued 2026-08-14 with --content-max above */
  --ax-form-max:      1080px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
  background: var(--bg); color: var(--text);
  font-family: var(--ax-font-body);
  font-size: 16px; min-height: 100vh; line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}
/* body::before is GONE (2026-08-15). It drew a fixed 40px grid of
   rgba(14,165,233,0.05) hairlines across all 33 panes. It encoded nothing —
   dashboard-design.md §3 has said "no decorative backdrop" since the standard
   was written — and it was the last place the sky accent would have survived
   the teal pass. Deleted rather than recoloured: a warm grid would be the same
   mistake in a nicer colour. If you are here because a pane looks bare, the
   answer is the card elevation in --ax-sh-1, not texture behind it. */

/* Auth gate — the CARD now lives in shared/auth-card.css (moved 2026-08-16).
   It is shared with the student login modal (shared/student-auth.js), and none
   of the nine pages that load that kernel link THIS file, so the rules could
   not stay here. instructor/index.html <link>s both stylesheets; the markup
   carries .authcard / .auth-btn / .auth-msg-err / .auth-msg-ok alongside its
   original ids, which _auth.js still drives.
   What stays here is only where the instructor page PUTS the card. */
#auth-gate { margin: 60px auto 40px; }

/* Status screens */
.status-screen {
  display: none; position: relative; z-index: 1;
  max-width: 420px; margin: 120px auto;
  background: var(--card); border-radius: 12px;
  padding: 40px 36px; text-align: center;
  box-shadow: 0 4px 20px rgba(0,0,0,0.08);
}
#pending-approval { border: 2px solid var(--accent4); }
#access-rejected  { border: 2px solid var(--accent5); }
#access-denied {
  display: none; position: relative; z-index: 1;
  max-width: 420px; margin: 120px auto;
  background: var(--card); border: 2px solid var(--accent5);
  border-radius: 12px; padding: 36px; text-align: center;
  box-shadow: 0 4px 20px rgba(0,0,0,0.08);
}
.status-screen .st-icon { font-size: 2.8rem; margin-bottom: 14px; }
#pending-approval h2 { color: var(--accent4); margin-bottom: 10px; }
#access-rejected  h2 { color: var(--accent5); margin-bottom: 10px; }
#access-denied    h2 { color: var(--accent5); margin-bottom: 12px; }
.status-screen p { color: var(--text2); font-size: 0.92rem; margin-bottom: 8px; }
.status-screen .btn-back, #access-denied button {
  display: inline-block; padding: 10px 28px;
  border: none; border-radius: 8px; font-size: 0.95rem;
  font-weight: 600; cursor: pointer; margin-top: 8px;
}
#pending-approval .btn-back { background: var(--accent4); color: #fff; }
#access-rejected  .btn-back { background: var(--accent5); color: #fff; }
#access-denied    button   { background: var(--accent);  color: #fff; }

/* Dashboard shell */
#dashboard { display: none; position: relative; z-index: 1; }
.sidebar {
  position: fixed; top: 0; bottom: 0; inset-inline-start: 0;
  width: var(--sidebar-width);
  background: var(--sidebar-bg); color: var(--sidebar-fg);
  overflow-y: auto; z-index: 100;
  display: flex; flex-direction: column;
  transition: transform 200ms ease;
}
/* transform has no logical form, so the mirror is spelled out. This is the one
   place in the shell that needs a [dir] selector rather than a logical property. */
#dashboard.sidebar-collapsed .sidebar          { transform: translateX(-100%); }
[dir="rtl"] #dashboard.sidebar-collapsed .sidebar { transform: translateX(100%); }
#dashboard.sidebar-collapsed .main-content     { margin-inline-start: 0; }

/* Drag-to-resize handle. Lives on #dashboard (not inside .sidebar, whose
   overflow-y:auto would clip it) and tracks the sidebar's inline-end edge via
   the same --sidebar-width the sidebar itself reads. Logic in instructor/_mount.js.
   ⚠ RTL: this rule mirrors, but the drag ARITHMETIC does not — _mount.js reads
   e.clientX straight as the new width, which is measured from the viewport's
   left edge in either direction. Mirroring that is a JS change, not a CSS one. */
.sidebar-resizer {
  position: fixed; top: 0; bottom: 0;
  inset-inline-start: var(--sidebar-width); width: 8px; margin-inline-start: -4px;
  cursor: col-resize; z-index: 101;
  background: transparent; transition: background .15s;
}
.sidebar-resizer:hover,
#dashboard.resizing .sidebar-resizer { background: var(--accent); opacity: .5; }
#dashboard.resizing { user-select: none; cursor: col-resize; }
#dashboard.resizing .main-content { transition: none; }          /* no lag while dragging */
#dashboard.resizing iframe { pointer-events: none; }             /* let drag events pass through content iframes */
#dashboard.sidebar-collapsed .sidebar-resizer { display: none; } /* hidden when sidebar is hidden */

.sidebar-toggle {
  background: none; border: 1px solid #5a5a5a; color: var(--sidebar-fg);
  padding-block: 4px; padding-inline: 10px; border-radius: 4px; cursor: pointer;
  font-size: 1rem; line-height: 1; margin-inline-end: 10px;
  font-family: inherit;
}
.sidebar-toggle:hover { background: var(--sidebar-hover); color: #fff; }
.sidebar-brand {
  padding-block: 18px 16px; padding-inline: 20px;
  border-block-end: 1px solid rgba(255,255,255,0.10);
  display: flex; align-items: center; gap: 10px;
}
/* The sidebar brand mark is the canonical lockup (shared/logo.js,
   [data-axlogo="lockup inverse"]); no page-local logo styling here. */

.sidebar-nav { flex: 1; padding-block: 6px 20px; }

/* ══ Menu anatomy (2026-08-14) ═══════════════════════════════════════════════
   Three parts, and each one encodes something: the group header is a solid
   BAND (a section of the tool, not a whisper above it), items are hairline-
   separated RECORDS on a lifted fill, and the icon sits in a fixed rail split
   from the label by a vertical rule so every label starts at the same x.

   Scaled down from the reference on purpose: 40px rows, not ~52px. This menu
   carries 32 items across 8 groups — at reference proportions it runs ~2,200px
   and scrolls on every screen. The reference's leading NUMBERS were dropped for
   the same kind of reason: chapters are a sequence, dashboard tools are not, and
   a number here would assert an order that doesn't exist (and would renumber
   itself the moment the Admin group is hidden for a non-admin).

   ⚠ THE SEPARATOR RUNS DARK, and that inversion is the whole effect. Everywhere
   else on this dashboard a divider is a hairline LIGHTER than the surface it sits
   on — the light-pane convention. Here it is --sidebar-line, the band colour, so a
   row is not "a surface with a line under it" but a slab lifted off the band with
   the band visible between slabs. Swap it back to a light rgba() and the menu
   flattens into a list. For the same reason .nav-category draws NO bottom border:
   the band→record colour step IS the boundary, and a line there would read as a
   fifth rung that doesn't exist. The last row of a group and the next band are
   both --sidebar-bg, so they merge — deliberately.

   Logical properties throughout — a future dir="rtl" mirrors with no second
   stylesheet. Still on the --sidebar-* / --accent names rather than --ax-*: the
   drag-resizer in instructor/_mount.js reads and writes --sidebar-width by name,
   so renaming this family is a JS contract change, not a CSS one (VD-3). */
.nav-category {
  display: flex; align-items: center; justify-content: space-between;
  min-height: 34px;
  padding-block: 8px; padding-inline: 18px 14px;
  font-size: 0.875rem; font-weight: 700; letter-spacing: 0.01em;
  color: var(--sidebar-ink);
  cursor: pointer; user-select: none;
}
.nav-category:hover { color: #fff; }
.nav-cat-caret {
  font-size: 0.6rem; color: rgba(255,255,255,0.45);
  transition: transform .2s ease, color .15s;
}
.nav-category:hover .nav-cat-caret { color: var(--sidebar-fg); }
.nav-group.collapsed .nav-cat-caret { transform: rotate(-90deg); }
.nav-group-items {
  overflow: hidden; max-height: 600px;
  transition: max-height .25s ease;
}
.nav-group.collapsed .nav-group-items { max-height: 0; }
.nav-item {
  display: flex; align-items: center;
  min-height: 40px;
  padding-block: 0; padding-inline: 0 14px;
  font-size: 0.875rem; font-weight: 500;
  color: var(--sidebar-fg); cursor: pointer;
  background: var(--sidebar-row);
  border-block-end: 1px solid var(--sidebar-line);
  transition: background .15s, color .15s;
  user-select: none;
}
.nav-item:hover { background: var(--sidebar-hover); color: var(--sidebar-ink); }
.nav-item.active {
  background: var(--accent); color: #fff; font-weight: 600;
  border-block-end-color: transparent;
}
.nav-icon { flex: 0 0 40px; text-align: center; color: var(--sidebar-muted); }
.nav-item.active .nav-icon { color: #fff; }
/* The rule lives on the LABEL, not on an element of its own: a border-inline-start
   there is drawn around the label's line box — ~26px of a 40px row, the proportion
   the reference has — and grows to two lines by itself when a long label wraps at
   a narrow drag width. Hiding the label on the icon rail hides the rule with it. */
.nav-label {
  flex: 1 1 auto; min-width: 0;
  padding-block: 4px; padding-inline-start: 12px;
  border-inline-start: 1px solid var(--sidebar-rule);
}
.nav-item.active .nav-label { border-inline-start-color: rgba(255,255,255,0.45); }
.nav-badge {
  flex: 0 0 auto; margin-inline-start: 8px;
  font-size: 0.62rem; font-weight: 700;
  padding-block: 1px; padding-inline: 6px; border-radius: 10px;
  background: rgba(45,187,166,0.18); color: #2DBBA6;
  letter-spacing: 0.3px;
}
/* The ink is a LIFTED accent, not --accent itself: on the charcoal band the
   base teal #0E8C7A measures 3.0:1, under the floor for 0.62rem text, while
   #2DBBA6 restores ~5.3:1 and keeps the pill unmistakably the accent hue.
   That value is not invented — it is the calendar's own dark-mode accent
   (widgets/academic-calendar.js:448), so the lift already exists in the
   system. Same reasoning as the #7dd3fc it replaces, one hue over.
   .legacy's amber was already a light tint and needed no change. */
.nav-badge.legacy { background: rgba(245,158,11,0.18); color: #fbbf24; }
/* Both pills vanish into the accent fill, so the active row washes them white. */
.nav-item.active .nav-badge,
.nav-item.active .nav-badge.legacy { background: rgba(255,255,255,0.22); color: #fff; }

.sidebar-footer {
  padding-block: 12px; padding-inline: 20px;
  border-block-start: 1px solid rgba(255,255,255,0.10);
  font-size: 0.75rem; color: var(--sidebar-muted);
}
.sidebar-footer a { color: #2DBBA6; text-decoration: none; }  /* lifted accent, as .nav-badge */
.sidebar-footer a:hover { text-decoration: underline; }

.main-content {
  margin-inline-start: var(--sidebar-width);
  min-height: 100vh; display: flex; flex-direction: column;
  transition: margin-inline-start 200ms ease;
}
/* The top bar takes the sidebar's band colour, not a shade of its own: the two
   meet along the full top edge and are read as one L of chrome wrapping the pane.
   A second dark value there registers as a seam, not as hierarchy. */
.top-bar {
  position: sticky; top: 0;
  background: var(--sidebar-bg); color: var(--sidebar-fg);
  padding: 10px 24px;
  display: flex; align-items: center; justify-content: space-between;
  font-family: var(--ax-font-mono); font-size: 13px;
  z-index: 50; border-bottom: 1px solid rgba(255,255,255,0.10);
}
.top-bar .tb-crumb { color: var(--sidebar-muted); }
.top-bar button {
  background: none; border: 1px solid #5a5a5a;
  color: #b5b5b5; padding: 4px 12px; border-radius: 4px;
  cursor: pointer; font-family: inherit; font-size: 12px;
}
.top-bar button:hover { background: var(--sidebar-hover); color: var(--sidebar-ink); }
/* ── Pills: ONE shape, many colour pairs ─────────────────────────────────
   The calendar states a badge exactly once (.acal-xrow .tag): 11px / 600,
   4px 10px, fully round, colour carried by a token pair. Every badge family
   in this file now takes that geometry, so the five shapes this file used to
   ship (12px radius here, 20px there, 999px elsewhere; weights 700 and 600;
   four padding pairs) collapse to one. Only the colour pair distinguishes
   them — which is what a badge is for. */
.role-badge, .bonus-badge, .tag-badge, .stub-card .stub-pill, .gm-recent-card .rc-status {
  display: inline-block; padding: 4px 10px; border-radius: var(--ax-r-pill);
  font-size: var(--ax-fs-1); font-weight: 600; line-height: 1.35;
  white-space: nowrap; letter-spacing: 0;
}
.badge-admin      { background: var(--ax-accent-bg); color: var(--ax-accent-ink); }
.badge-instructor { background: var(--ax-ok-bg);     color: var(--ax-ok-fg); }

/* Header and body share one measure and one inline gutter, so the title, the
   rule under it and the content below all start and end on the same two lines.
   The rule stops at the cap rather than running full-bleed: it marks where the
   content actually is, instead of pointing into the empty space a flush-start
   column leaves on a wide monitor. */
.section-header {
  padding-block: 24px 8px; padding-inline: 28px;
  max-width: var(--content-max);
  border-block-end: 1px solid var(--border);
}
/* Type from the calendar's `header.pg`: 22px / 600 with -.4px tracking. The
   old 1.4rem/700 was the largest thing on most panes; at 22px the title stops
   competing with the data under it and the tracking does the work instead. */
.section-header h1 {
  font-size: var(--ax-fs-7); font-weight: 600;
  letter-spacing: -0.4px; margin-bottom: 2px;
}
.section-header .crumb {
  font-size: var(--ax-fs-1); color: var(--text3);
  font-family: var(--ax-font-mono); font-weight: 600;
  text-transform: uppercase; letter-spacing: 1.2px;
}
.section-header p { color: var(--text2); font-size: var(--ax-fs-3); margin-bottom: 14px; }
.section-body { padding-block: 22px 60px; padding-inline: 28px; max-width: var(--content-max); }
.section-pane { display: none; }
.section-pane.active { display: block; }

/* ── The icon tile ───────────────────────────────────────────────────────
   The calendar's one recurring device (.acal-icard .ic, .acal-xrow .ic): a
   rounded square filled with a soft status tint, holding a currentColor
   glyph. It is the reason a calendar card reads at a glance without a rule
   or a heading. Declared once here for its two consumers in this file.
   Deliberately NOT a token — 46px/12px is component geometry, and there is
   no third consumer to generalise from (Rule of Three). */
.summary-card .ic, .stub-card .stub-icon {
  width: 46px; height: 46px; border-radius: 12px;
  display: grid; place-items: center; flex-shrink: 0;
  background: var(--ax-accent-bg); color: var(--ax-accent-ink);
  font-size: 1.4rem;                         /* legacy emoji call sites */
}
.summary-card .ic > svg, .stub-card .stub-icon > svg { width: 23px; height: 23px; }
.summary-card.is-ok   .ic, .stub-card.is-ok   .stub-icon { background: var(--ax-ok-bg);   color: var(--ax-ok-fg); }
.summary-card.is-warn .ic, .stub-card.is-warn .stub-icon { background: var(--ax-warn-bg); color: var(--ax-warn-fg); }
.summary-card.is-err  .ic, .stub-card.is-err  .stub-icon { background: var(--ax-err-bg);  color: var(--ax-err-fg); }

/* Stub cards — an empty state, so it stays centred; auto on BOTH inline sides
   is centring, not a physical property, and needs no RTL conversion (§3). */
.stub-card {
  background: var(--card); border: var(--ax-bw) solid var(--border);
  border-radius: var(--ax-r-lg); box-shadow: var(--ax-sh-1);
  padding: 34px 30px; text-align: center; max-width: 640px; margin: 20px auto;
}
.stub-card .stub-icon { margin: 0 auto 12px; }
.stub-card h3 { margin: 8px 0 4px; font-size: var(--ax-fs-5); font-weight: 600; }
.stub-card p  { color: var(--text2); font-size: var(--ax-fs-3); margin-bottom: 14px; }
.stub-card .stub-pill { margin-bottom: 8px; }
.stub-pill.soon   { background: var(--ax-warn-bg);   color: var(--ax-warn-fg); }
.stub-pill.legacy { background: var(--ax-accent-bg); color: var(--ax-accent-ink); }
.stub-card ul {
  text-align: left; max-width: 420px;
  margin: 10px auto 16px; color: var(--text2); font-size: 0.88rem;
}
.stub-card ul li { margin-bottom: 4px; }
.stub-card .btn-redirect {
  display: inline-block; padding: 10px 22px;
  background: var(--accent);
  color: #fff; border: none; border-radius: var(--ax-r);
  font-weight: 600; font-size: 0.92rem;
  text-decoration: none; cursor: pointer;
}
.stub-card .btn-redirect:hover { opacity: 0.92; }

/* Filters / controls / tables.
   Geometry here was written as literals (2px borders, 8px radii, 0.9rem) and
   is now read from the token layer — which is the half of Stage 4 that makes
   the NEXT re-valuation an edit to :root instead of another sweep. */
.filters { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 18px; }
.filters select, .filters input {
  padding: 8px 12px; border: var(--ax-bw) solid var(--border);
  border-radius: var(--ax-r); font-size: var(--ax-fs-4);
  background-color: var(--bg2); color: var(--text); font-family: inherit;
}
.filters select { min-width: 180px; }
.filters select:focus, .filters input:focus { outline: none; border-color: var(--accent); background-color: var(--card); }
.filters select:focus-visible, .filters input:focus-visible { box-shadow: var(--ax-ring); }
.btn {
  padding: 8px 16px; border: var(--ax-bw) solid transparent; border-radius: var(--ax-r);
  font-size: var(--ax-fs-4); cursor: pointer; font-weight: 600; font-family: inherit;
  transition: background-color .15s, color .15s, border-color .15s;
}
.btn-primary   { background: var(--accent); color: #fff; }
.btn-primary:hover:not(:disabled) { background: var(--ax-accent-ink); }
.btn-success   { background: var(--accent3); color: #fff; }
.btn-warn      { background: var(--accent4); color: #fff; }
.btn-danger    { background: var(--accent5); color: #fff; }
/* Secondary takes .acal-dimbtn's shape: a white surface sitting ON the warm
   ground, not a grey block cut out of it. On #F7F7F4 the old --bg3 fill was
   nearly invisible — the button had no edge at all. */
.btn-secondary {
  background: var(--card); color: var(--text2);
  border-color: var(--border); box-shadow: var(--ax-sh-1);
}
.btn-secondary:hover:not(:disabled) { background: var(--bg2); }
.btn:hover { opacity: 0.9; }
.btn-primary:hover, .btn-secondary:hover { opacity: 1; }   /* these recolour instead */
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
.inline-form { display: none; align-items: center; gap: 8px; margin-bottom: 12px; }
.inline-form.visible { display: flex; }
.inline-form input {
  padding: 8px 12px; border: var(--ax-bw) solid var(--border);
  border-radius: var(--ax-r); font-size: var(--ax-fs-4);
  background-color: var(--bg2); font-family: inherit;
}

/* ── Table ───────────────────────────────────────────────────────────────
   The table is now a card: the shell carries the radius, hairline and
   elevation, and clips the header's corners. Wrap the <table> in
   .data-table-card where the markup allows; the bare table still renders
   correctly without it, so no call site is required to change.

   tabular-nums is not a nicety — dashboard-design.md §2 has mandated it since
   the standard was written and it was never implemented. This is a gradebook;
   a column of marks that doesn't align on the decimal is a defect. */
.data-table-card {
  background: var(--card); border: var(--ax-bw) solid var(--border);
  border-radius: var(--ax-r-lg); box-shadow: var(--ax-sh-1); overflow: hidden;
}
.data-table {
  width: 100%; border-collapse: collapse;
  font-size: var(--ax-fs-3); background: var(--card);
  font-variant-numeric: tabular-nums;
}
.data-table th {
  text-align: left; padding: 11px 16px;
  background: var(--bg2); border-bottom: var(--ax-bw) solid var(--border);
  font-size: var(--ax-fs-1); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--text3); white-space: nowrap;
}
.data-table td { padding: 12px 16px; border-bottom: var(--ax-bw) solid var(--border); }
.data-table-card .data-table tbody tr:last-child td { border-bottom: none; }
.data-table tr:hover { background: var(--bg2); }
/* The margin rule (dashboard-design.md §3): a selected row is marked at its
   inline-start edge, never filled. Inset box-shadow rather than a border so
   the row's height and the cell grid are untouched. */
.data-table tbody tr.selected { background: var(--ax-accent-bg); }
.data-table tbody tr.selected td:first-child { box-shadow: inset 3px 0 0 var(--accent); }
.bulk-area { display: none; margin-bottom: 16px; }
.bulk-area.visible { display: block; }
.bulk-area textarea {
  width: 100%; height: 120px; padding: 12px;
  border: var(--ax-bw) solid var(--border); border-radius: var(--ax-r);
  font-family: var(--ax-font-mono); font-size: var(--ax-fs-3);
  margin-bottom: 8px; resize: vertical; background-color: var(--bg2);
}
/* The accent as a 3px inline-start edge — §3 names .status-msg as one of the
   places the margin rule applies, and it never carried it. Each state colours
   the edge with its own mark, so the strip reads before the text does. */
.status-msg {
  padding: 9px 14px; border-radius: var(--ax-r);
  border-inline-start: 3px solid transparent;
  font-size: var(--ax-fs-3); margin-bottom: 12px; display: none;
}
/* Stage 1b (2026-08-05): the four literals below moved onto the status tokens.
   No fallback — the tokens are declared in this same file, so they cannot be
   missing wherever this rule matches. Values are unchanged, so this renders
   byte-identically; it makes the tokens real rather than aspirational. */
.status-msg.success { display: block; background: var(--ax-ok-bg);  color: var(--ax-ok-fg);  border-inline-start-color: var(--ax-ok); }
.status-msg.error   { display: block; background: var(--ax-err-bg); color: var(--ax-err-fg); border-inline-start-color: var(--ax-err); }
/* VD-15 (2026-08-06): the THIRD state, and the reason this bug existed.
   `.status-msg` alone was doing two opposite jobs — "hide me" (auto-dismiss
   timeouts) and "show a neutral message" ("Saving…", "Unsaved changes"). The
   base rule is display:none, so the second job silently never rendered on any
   page linking this file: accreditation, gradebook, question-bank and
   live-exams showed NO feedback between click and result. One class cannot
   mean both things, so the neutral case gets its own name.
   Quiet on purpose — in-progress is not a result. No new token: bg3 + text2
   are the existing muted pair. */
.status-msg.info    { display: block; background: var(--ax-bg3);    color: var(--ax-text2); border-inline-start-color: var(--ax-border-strong); }
#loading {
  position: relative; z-index: 1; text-align: center;
  padding: 120px 20px; color: var(--text2); font-size: var(--ax-fs-4);
}

@media (max-width: 900px) {
  :root { --sidebar-width: 56px; }
  .sidebar-resizer { display: none; }   /* no manual resize on the icon rail */
  /* The rail keeps its hairlines (structure survives) but loses the label — and
     with it the vertical rule, which is drawn on the label's own border. */
  .nav-item { padding-inline: 0; justify-content: center; }
  .nav-icon { flex: 1 1 auto; }
  .nav-label, .nav-badge, .nav-category, .sidebar-footer { display: none; }
  /* Icon rail: collapse the brand lockup to just its "A" badge, centered. */
  .sidebar-brand .axlogo { display: none; }
  .sidebar-brand { justify-content: center; padding-inline: 0; }
  /* In the icon rail the category headers (and their carets) are hidden, so a
     collapsed group would hide its icons too — force every group open here. */
  .nav-group.collapsed .nav-group-items { max-height: none; }
}
@media (max-width: 768px) {
  .filters { flex-direction: column; align-items: stretch; }
  .data-table { font-size: 0.8rem; }
  .section-body { padding-block: 18px 40px; padding-inline: 16px; }
  .section-header { padding-block: 18px 6px; padding-inline: 16px; }
}

/* ── Game Center ── */
.game-room-code { font-family:var(--ax-font-mono); font-size:3rem; font-weight:700;
  letter-spacing:0.12em; color:var(--accent2); }
.game-timer-bar { height:10px; border-radius:6px; background:var(--bg3);
  overflow:hidden; margin:12px 0 4px; }
/* The timer is a countdown, so green→amber→red IS the information. Re-valued
   onto the status marks rather than the old sky-era literals. */
.game-timer-fill { height:100%; border-radius:6px;
  background:linear-gradient(90deg,var(--ax-ok),var(--ax-warn),var(--ax-err)); transition:width .1s linear; }
.game-q-text { font-size:1.25rem; font-weight:600; padding:18px 20px;
  background:var(--card); border:var(--ax-bw) solid var(--border); border-radius:var(--ax-r-lg);
  box-shadow:var(--ax-sh-1); margin:14px 0; line-height:1.5; }
.game-opts { display:grid; grid-template-columns:1fr 1fr; gap:10px; margin:12px 0; }
.game-opt { padding:16px 14px; border:none; border-radius:var(--ax-r);
  font-size:1rem; font-weight:600; cursor:default; }
.game-opt.correct { outline:4px solid var(--ax-ok); }
/* A four-way DATA palette, not chrome — the four answers must be told apart at
   a glance from the back of a lecture hall, which is the one job no single
   accent can do (§3 sends data colour to a chart palette, not to the accent).
   Re-drawn from the calendar's own day colours so it belongs to this system:
   midterm red, term-2 blue, holiday amber, study green. */
.game-opt[data-c="0"] { background:#F6C2C2; color:#4E1313; }
.game-opt[data-c="1"] { background:#B5D4F4; color:#0C447C; }
.game-opt[data-c="2"] { background:#FBCB7C; color:#3E2202; }
.game-opt[data-c="3"] { background:#C8E0A0; color:#20420A; }
.game-players-list { max-height:240px; overflow-y:auto;
  border:var(--ax-bw) solid var(--border); border-radius:var(--ax-r); background:var(--card); }
.game-player-row { display:flex; justify-content:space-between; align-items:center;
  padding:9px 14px; border-bottom:var(--ax-bw) solid var(--border); font-size:0.88rem; }
.game-player-row:last-child { border-bottom:none; }
.gm-recent-card { background:var(--card);
  border:var(--ax-bw) solid var(--border); border-radius:var(--ax-r-lg);
  box-shadow:var(--ax-sh-1);
  padding:15px 18px; margin-bottom:10px; display:flex; align-items:center; gap:16px; }
.gm-recent-card .rc-code { font-family:var(--ax-font-mono); font-size:1.4rem; font-weight:700; color:var(--accent2); }
.gm-recent-card .rc-meta { flex:1; font-size:var(--ax-fs-3); color:var(--text2); }
.rc-lobby   { background:var(--ax-accent-bg); color:var(--ax-accent-ink); }
.rc-active  { background:var(--ax-ok-bg);     color:var(--ax-ok-fg); }
.rc-done    { background:var(--bg3);          color:var(--text3); }
.gm-podium  { display:flex; justify-content:center; align-items:flex-end; gap:10px; margin:20px 0; }
.gm-podium-place { text-align:center; border-radius:10px 10px 0 0; padding:14px 12px;
  min-width:110px; max-width:140px;
  display:flex; flex-direction:column; align-items:center; justify-content:center; overflow:hidden; }
/* The podium keeps its gradients — §3's "no gradient fills" is about chrome
   pretending to be interesting, and a winners' podium is a celebration, not a
   control. Re-valued into the warm family so it doesn't sit cold on #F7F7F4:
   gold from the holiday amber, silver from the surface ramp, bronze already
   warm. Recorded so the exception reads as a decision, not an oversight. */
.gm-p1 { background:linear-gradient(180deg,#FBEBD0,#FBCB7C); min-height:160px; order:2; }
.gm-p2 { background:linear-gradient(180deg,#F1F0EC,#E9E9E4); min-height:130px; order:1; }
.gm-p3 { background:linear-gradient(180deg,#FBF0E4,#F6C7B7); min-height:115px; order:3; }
.gm-podium-place .pm-icon { font-size:1.6rem; flex-shrink:0; }
.gm-podium-place .pm-name { font-weight:700; font-size:0.78rem; margin:4px 0 2px; line-height:1.3; word-break:break-word; }
.gm-podium-place .pm-score { font-family:var(--ax-font-mono); font-size:0.85rem; color:var(--text2); flex-shrink:0; }

/* ── Instructors Admin ── */
.instructor-section-title {
  font-size: var(--ax-fs-5); font-weight: 600; margin: 24px 0 12px;
  padding-block-end: 6px; border-block-end: var(--ax-bw) solid var(--border);
  color: var(--text);
}
.instructor-section-title:first-child { margin-top: 0; }
.course-checkbox-list { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 14px; }
.course-checkbox-list label {
  display: flex; align-items: center; gap: 6px;
  padding: 6px 12px; border: var(--ax-bw) solid var(--border);
  border-radius: var(--ax-r); cursor: pointer; font-size: var(--ax-fs-4);
  background: var(--card);
  text-transform: none; letter-spacing: 0;
  color: var(--text); font-weight: 400;
  transition: border-color .15s, background-color .15s;
}
.course-checkbox-list label input { accent-color: var(--accent); }
.course-checkbox-list label:has(input:checked) {
  border-color: var(--accent); background: var(--ax-accent-bg);
}

/* ── Summary cards — the calendar's .acal-icard ──────────────────────────
   Two real changes beyond chrome, both deliberate:

   LEFT-ALIGNED, not centred. Centred figures in a grid of six give every card
   a different optical left edge, so the row reads as scattered fragments. The
   calendar aligns them and the row becomes a column of facts.

   THE FIGURE IS INK, NOT ACCENT. §3 reserves the accent for what you can act
   on and where you are; a stat is neither. Teal numbers everywhere would spend
   the accent on the one thing that is never interactive. Status overrides
   (.val.is-warn / .is-over on instructor/index.html) still colour it.

   The icon tile is OPT-IN and needs one wrapper, because today's markup is a
   flat .val + .lbl with nothing to group:
     <div class="summary-card">
       <div class="ic">…svg…</div>
       <div><div class="lbl">Ungraded</div><div class="val">8</div></div>
     </div>
   Without a .ic the card is plain left-aligned text — which is exactly what
   every existing call site renders, so no page is required to change. */
.summary-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 14px;
  margin-bottom: 24px;
}
.summary-card {
  background: var(--card);
  border: var(--ax-bw) solid var(--border);
  border-radius: var(--ax-r-lg);
  padding: 18px 20px;
  box-shadow: var(--ax-sh-1);
}
.summary-card:has(> .ic) { display: flex; align-items: center; gap: 15px; }
.summary-card:has(> .ic) > :not(.ic) { min-width: 0; }
.summary-card .val {
  font-family: var(--ax-font-mono); font-size: var(--ax-fs-6);
  font-weight: 600; line-height: 1.2; letter-spacing: -0.3px;
  color: var(--text); font-variant-numeric: tabular-nums;
}
.summary-card .lbl { font-size: var(--ax-fs-2); color: var(--text3); }
.summary-card .sub { font-size: var(--ax-fs-2); color: var(--text2); margin-top: 1px; }

/* ── Bonus / Tags ── */
/* Shape comes from the shared pill rule above; only the colour pair is here.
   The three literals that used to live in this block (#d97706, #1d4ed8 and
   #dc2626 — a THIRD red no token covered, flagged in the foundation header)
   are gone: every pair is now a status token. */
.bonus-badge { font-family:var(--ax-font-mono); font-variant-numeric:tabular-nums;
  background:var(--ax-warn-bg); color:var(--ax-warn-fg); }
.bonus-badge.zero { background:var(--bg3); color:var(--text3); }
.bx-half  { background:var(--ax-accent-bg); color:var(--ax-accent-ink); }
.bx-full  { background:var(--ax-ok-bg);     color:var(--ax-ok-fg); }
.bx-reset { background:var(--ax-err-bg);    color:var(--ax-err-fg); }
.tag-pip { display:inline-block; font-size:0.75rem; vertical-align:middle; margin-inline-start:3px; line-height:1; }
/* The margin rule again: a flagged row is marked at its inline-start edge.
   Logical property, so it mirrors under dir="rtl" — the physical border-left
   here was the exact shape of bug recorded in dashboard-design.md §3. */
.has-tags-row { background:var(--ax-err-bg) !important; box-shadow: inset 3px 0 0 var(--ax-err); }
.tag-badge { background:var(--ax-err-bg); color:var(--ax-err-fg);
  font-family:var(--ax-font-mono); font-variant-numeric:tabular-nums; }
.tag-badge.zero { background:var(--bg3); color:var(--text3); }

/* ── Random Picker animations ── */
@keyframes orbFloat1 {
  0%,100% { transform:translate(0,0) scale(1); opacity:.6; }
  33%     { transform:translate(60px,-40px) scale(1.2); opacity:.8; }
  66%     { transform:translate(-30px,50px) scale(.85); opacity:.5; }
}
@keyframes orbFloat2 {
  0%,100% { transform:translate(0,0) scale(1); opacity:.5; }
  40%     { transform:translate(-50px,30px) scale(1.15); opacity:.7; }
  70%     { transform:translate(40px,-60px) scale(.9); opacity:.4; }
}
@keyframes borderGlow {
  0%,100% { box-shadow:0 0 20px rgba(124,58,237,.4),0 0 60px rgba(124,58,237,.15),inset 0 0 20px rgba(124,58,237,.05); }
  50%     { box-shadow:0 0 40px rgba(124,58,237,.75),0 0 110px rgba(124,58,237,.3),inset 0 0 30px rgba(124,58,237,.12); }
}
@keyframes scanSweep {
  0%   { top:0%;   opacity:0; }
  8%   { opacity:1; }
  92%  { opacity:1; }
  100% { top:100%; opacity:0; }
}
@keyframes resultReveal {
  0%   { transform:scale(.72) rotateX(18deg); opacity:0; filter:blur(10px); }
  55%  { transform:scale(1.05) rotateX(-4deg); opacity:1; filter:blur(0); }
  78%  { transform:scale(.98) rotateX(1deg); }
  100% { transform:scale(1) rotateX(0deg); opacity:1; }
}
@keyframes nameReveal {
  0%   { opacity:0; filter:blur(14px); letter-spacing:.6em; transform:scale(1.35); }
  65%  { opacity:1; filter:blur(0);    letter-spacing:.04em; transform:scale(1.02); }
  100% { opacity:1; filter:blur(0);    letter-spacing:normal; transform:scale(1); }
}
@keyframes spotlightExpand {
  from { opacity:0; transform:scale(0); }
  to   { opacity:1; transform:scale(1); }
}
@keyframes sparklePop {
  0%   { transform:rotate(var(--r)) translateY(0) scale(0); opacity:0; }
  30%  { opacity:1; transform:rotate(var(--r)) translateY(var(--d)) scale(1); }
  100% { transform:rotate(var(--r)) translateY(calc(var(--d)*2)) scale(0); opacity:0; }
}
#pickerResultCard { animation:resultReveal .65s cubic-bezier(.34,1.4,.64,1) forwards; }
#pickerResultName { animation:nameReveal .75s cubic-bezier(.22,1,.36,1) .25s both; }
@keyframes confetti3D {
  from { transform:translate3d(0,0,0) rotateX(0deg) rotateY(0deg) rotateZ(0deg); opacity:1; }
  to   { transform:translate3d(var(--tx),var(--ty),var(--tz)) rotateX(var(--rx)) rotateY(var(--ry)) rotateZ(var(--rz)) scale(.12); opacity:0; }
}
@keyframes shatter3D {
  0%   { transform:translate3d(0,0,0) rotateX(0deg) rotateY(0deg) rotateZ(0deg) scale(1); opacity:1; }
  55%  { opacity:.7; }
  100% { transform:translate3d(var(--tx),var(--ty),var(--tz)) rotateX(var(--rx)) rotateY(var(--ry)) rotateZ(var(--rz)) scale(0); opacity:0; }
}
@keyframes shockRing3D {
  0%   { transform:perspective(500px) rotateX(68deg) scale(0);   opacity:.95; }
  70%  { opacity:.35; }
  100% { transform:perspective(500px) rotateX(68deg) scale(4.8); opacity:0; }
}
@keyframes exLabelWin {
  0%   { opacity:0; transform:translate(-50%,-50%) perspective(600px) rotateX(55deg) scale(.28); filter:blur(10px); }
  32%  { opacity:1; transform:translate(-50%,-50%) perspective(600px) rotateX(-6deg) scale(1.28); filter:blur(0); }
  58%  { opacity:1; transform:translate(-50%,-50%) perspective(600px) rotateX(2deg)  scale(1); }
  100% { opacity:0; transform:translate(-50%,-50%) perspective(600px) rotateX(-12deg) scale(1.45); }
}
@keyframes exLabelWrong {
  0%   { opacity:0; transform:translate(-50%,-50%) perspective(600px) rotateZ(-20deg) scale(.22); filter:blur(12px); }
  28%  { opacity:1; transform:translate(-50%,-50%) perspective(600px) rotateZ(5deg)   scale(1.24); filter:blur(0); }
  52%  { opacity:1; transform:translate(-50%,-50%) perspective(600px) rotateZ(-2deg)  scale(1); }
  100% { opacity:0; transform:translate(-50%,-50%) perspective(600px) rotateZ(8deg)   scale(.6); }
}
@keyframes overlayFlashWin   { 0%,100%{background:transparent;} 12%{background:rgba(16,185,129,.2);} }
@keyframes overlayFlashWrong { 0%,100%{background:transparent;} 12%{background:rgba(245,158,11,.18);} }

/* Shared centered dialog styles live inside shared/dialog.js so they apply on
   every page, including ones that don't load this stylesheet. */

/* ══════════════════════════════════════════════════════════════════════════
   AX FORM-CONTROL LAYER  (added 2026-08-05)
   ──────────────────────────────────────────────────────────────────────────
   Opt-in classes for dashboard forms. Before this, the only form rules in
   this file were #auth-gate input, .filters, .inline-form, .bulk-area and
   .course-checkbox-list — so every other control on all 33 pages that link
   this stylesheet rendered as a raw browser default. Ten-plus pages had each
   reinvented the same field rule under their own prefix (.ce-field, .sc-field,
   .cf-field, .ci-field, .lf-field, .ls-field, .jr-field, .fw-input,
   .inst-input — the last two byte-identical).

   Promoted from instructor/course-editor.html .ce-field: the only complete
   label -> control -> hint -> focus set in the repo.

   ── FOUR INVARIANTS. Do not break these. ──
   1. NO bare input/select/textarea/button selector may EVER be added here.
      The control rules enumerate input types on purpose so that
      input[type=color] and input[type=range] are never caught:
      admin/appearance.html ships a 40x30 colour swatch and .tok-range
      sliders that any bare height/appearance rule would destroy. The same
      class of failure is on record for chapter CSS — see
      docs/design-system.md §6 (scopeChapterCss()). shared/styles.test.js
      enforces this; if you are here to add `input { }`, the answer is no.
   2. background-color longhand, never the `background` shorthand — the
      select arrow below is a background-image and a shorthand erases it.
   3. :focus repaints the border; :focus-visible adds the ring. Keeps the
      ring reading as a keyboard affordance.
   4. RESERVED .ax-* NAMES — the namespace already holds the chapter/slide
      canvas: .ax-block, .ax-slide*, .ax-deck*, .ax-selected, .ax-sbs-child
      and --ax-canvas-* / --ax-deck-* / --ax-screen-* / --ax-slide-*
      (instructor/chapter-editor.js, courses/_shared/_dynamicChapterRender.js,
      admin/appearance-preview.html — and chapter-editor.html links THIS
      file). Everything below was grep-verified disjoint from those.
   ══════════════════════════════════════════════════════════════════════════ */
:root {
  /* One height and one radius for the layer, so a select, a text box and a
     .btn-sm line up on the same row. */
  --ax-h:        38px;
  --ax-h-sm:     30px;
  /* Re-valued 2026-08-15 with the rest of Stage 4. The radius ladder across
     this block and the foundation above is 8 / 10 / 16 / pill; the border
     drops 1.5px → 1px, because the calendar's chrome reads as a hairline
     under an elevated card rather than as an outline around a flat one. */
  --ax-r:        10px;
  --ax-r-sm:     8px;
  --ax-bw:       1px;
  /* Type scale: label / control / hint — collapsed onto --ax-fs-2 / -4 / -1
     so the layer stops carrying three sizes the 7-step scale already names. */
  --ax-fs-label: 0.75rem;
  --ax-fs-ctrl:  0.875rem;
  --ax-fs-hint:  0.6875rem;
  --ax-gap:      4px;    /* label -> control -> hint */
  --ax-gap-row:  14px;   /* field -> field */
  --ax-field-bg: var(--bg2, #FBFBF9);
  /* Dashboards define no --accent-rgb, so the fallback IS the platform accent.
     Same idiom as admin/appearance.html:95 — correct today, theme-ready later. */
  --ax-ring:     0 0 0 3px rgba(var(--accent-rgb, 14,140,122), 0.16);

  /* Semantic status tokens are NOT declared here — they are platform semantics,
     not form-control geometry, and they live with the rest of the foundation at
     the top of this file. This block owns only the .ax-* layer's own geometry. */
}

/* ── Field: label -> control -> hint ─────────────────────────────────────── */
.ax-field { display: flex; flex-direction: column; gap: var(--ax-gap); min-width: 0; }
.ax-field > label, .ax-label {
  font-size: var(--ax-fs-label); font-weight: 600; letter-spacing: 0.02em;
  line-height: 1.35; color: var(--text2, #585E6E);
}
.ax-hint  { font-size: var(--ax-fs-hint); line-height: 1.45; color: var(--text3, #9195A3); }
.ax-error { font-size: var(--ax-fs-hint); font-weight: 600; color: var(--accent5, #A32D2D); }

/* Layout helpers. .ax-fieldset also kills <fieldset>'s min-width:min-content
   default, which otherwise blows out any flex/grid parent. */
.ax-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
           gap: var(--ax-gap-row) 18px; align-items: start; }
.ax-grid.ax-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.ax-fieldset { border: 0; margin: 0; padding: 0; min-width: 0; }
.ax-legend {
  font-size: var(--ax-fs-5); font-weight: 600; color: var(--text, #191C24);
  margin: 20px 0 4px; padding: 0;
}
.ax-scroll-x { overflow-x: auto; }

/* ── Controls (types enumerated — colour/range/file/checkbox excluded) ───── */
.ax-field input[type="text"],     .ax-field input[type="number"],
.ax-field input[type="url"],      .ax-field input[type="email"],
.ax-field input[type="password"], .ax-field input[type="search"],
.ax-field input[type="tel"],      .ax-field input[type="date"],
.ax-field input[type="time"],     .ax-field select, .ax-field textarea,
.ax-input, .ax-select, .ax-textarea {
  width: 100%; min-height: var(--ax-h); padding: 8px 12px;
  border: var(--ax-bw) solid var(--border, #E9E9E4); border-radius: var(--ax-r);
  font-size: var(--ax-fs-ctrl); font-family: inherit; line-height: 1.4;
  color: var(--text, #191C24);
  background-color: var(--ax-field-bg);   /* longhand — see invariant 2 */
  outline: none;
  transition: border-color .15s, box-shadow .15s, background-color .15s;
}
.ax-field textarea, .ax-textarea { min-height: 72px; line-height: 1.55; resize: vertical; }
.ax-field textarea.ax-mono, .ax-textarea.ax-mono {
  font-family: var(--ax-font-mono); font-size: var(--ax-fs-3);
}
.ax-field input::placeholder, .ax-field textarea::placeholder,
.ax-input::placeholder, .ax-textarea::placeholder { color: var(--text3, #9195A3); opacity: .9; }

.ax-field input:focus, .ax-field select:focus, .ax-field textarea:focus,
.ax-input:focus, .ax-select:focus, .ax-textarea:focus {
  border-color: var(--accent, #0E8C7A); background-color: #fff;
}
.ax-field input:focus-visible, .ax-field select:focus-visible, .ax-field textarea:focus-visible,
.ax-input:focus-visible, .ax-select:focus-visible, .ax-textarea:focus-visible {
  box-shadow: var(--ax-ring);
}
.ax-field input:disabled, .ax-field select:disabled, .ax-field textarea:disabled,
.ax-input:disabled, .ax-select:disabled, .ax-textarea:disabled {
  opacity: .55; cursor: not-allowed; background-color: var(--bg3, #F1F0EC);
}

/* ── Select: one arrow on every platform ─────────────────────────────────── */
/* The glyph is shared/icons.js's canonical `chevron-down` re-expressed as a
   data URI — a CSS background cannot call icons.js, and a data URI cannot read
   var(), so the stroke is --text3's literal #9195A3 (re-valued 2026-08-15 with the
   token; a data URI is the one place a stale literal cannot be avoided). */
.ax-field select, .ax-select {
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  padding-right: 34px; cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%239195A3' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 10px center; background-size: 16px 16px;
}
.ax-field select::-ms-expand, .ax-select::-ms-expand { display: none; }

/* ── Money / numerics ────────────────────────────────────────────────────── */
/* ::before cannot render on an <input>, so the prefix needs a wrapper. USD is
   the only currency in this codebase (usdPerMinute, monthlyCostUsd,
   anomalyCallUsd), so the glyph is literal rather than a data attribute. */
.ax-money { position: relative; display: block; min-width: 0; }
.ax-money::before {
  content: '$'; position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
  font-size: var(--ax-fs-ctrl); line-height: 1; color: var(--text3, #9195A3);
  pointer-events: none;
}
.ax-money > input { padding-left: 25px; }
.ax-money > input, .ax-num { font-variant-numeric: tabular-nums; }

/* ── Checkbox: bold title line + muted note line ─────────────────────────── */
.ax-check {
  display: grid; grid-template-columns: auto 1fr; gap: 2px 10px; align-items: start;
  padding: 10px 12px; border: var(--ax-bw) solid var(--border, #E9E9E4);
  border-radius: var(--ax-r); background: var(--card, #fff); cursor: pointer;
  transition: border-color .15s, background-color .15s;
}
.ax-check + .ax-check { margin-top: 8px; }
.ax-check:hover { border-color: var(--accent, #0E8C7A); }
.ax-check > input[type="checkbox"] {
  grid-row: 1 / span 2; width: 17px; height: 17px; margin: 2px 0 0;
  accent-color: var(--accent, #0E8C7A); cursor: pointer;
}
.ax-check > input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--accent, #0E8C7A); outline-offset: 2px;
}
.ax-check-title { font-size: var(--ax-fs-4); font-weight: 600; line-height: 1.35; color: var(--text, #191C24); }
.ax-check-note  { font-size: var(--ax-fs-2); line-height: 1.45; color: var(--text3, #9195A3); }
/* :has() precedent already in this file at .course-checkbox-list. */
.ax-check:has(> input:checked) { border-color: var(--accent, #0E8C7A); background: var(--ax-accent-bg); }

/* ── Matrix: subject x setting grid of inputs ────────────────────────────── */
/* For settings that are genuinely two-dimensional (role x limit). Listing them
   flat forces the subject name into every label; a matrix says it once. */
.ax-matrix { width: 100%; border-collapse: collapse; font-size: var(--ax-fs-3);
  font-variant-numeric: tabular-nums; }
.ax-matrix th, .ax-matrix td {
  padding: 8px 10px; text-align: left; vertical-align: middle;
  border-bottom: var(--ax-bw) solid var(--border, #E9E9E4);
}
.ax-matrix thead th {
  font-size: var(--ax-fs-1); font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--text3, #9195A3); white-space: nowrap;
  border-bottom: var(--ax-bw) solid var(--border, #E9E9E4);
}
.ax-matrix tbody th { font-size: var(--ax-fs-4); font-weight: 600; color: var(--text, #191C24); white-space: nowrap; }
.ax-matrix tbody tr:last-child th, .ax-matrix tbody tr:last-child td { border-bottom: none; }
.ax-matrix td .ax-money, .ax-matrix td > input { max-width: 190px; }

/* ── Toggle switch ───────────────────────────────────────────────────────── */
/* Promoted verbatim from instructor/tools.html (byte-identical twin in
   instructor/code-lab.html). NOTE: neither page links this stylesheet, so their
   local copies must STAY — this is a new consumer, not a de-duplication. */
.ax-switch {
  appearance: none; -webkit-appearance: none;
  width: 44px; height: 24px; flex: none; background: var(--bg3, #F1F0EC);
  border-radius: 999px; position: relative; cursor: pointer;
  transition: background 0.18s;
}
/* The on-state is the accent, not green: a switch says "this is on", which is
   an interactive state. Green would claim it is a GOOD state. .btn-success
   keeps --accent3 because there the green IS the semantic. */
.ax-switch:checked  { background: var(--accent, #0E8C7A); }
.ax-switch:disabled { cursor: not-allowed; opacity: 0.5; }
.ax-switch::after {
  content: ''; position: absolute; left: 3px; top: 3px;
  width: 18px; height: 18px; border-radius: 50%;
  background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.2);
  transition: left 0.18s;
}
.ax-switch:checked::after { left: 23px; }
.ax-switch:focus-visible  { outline: 2px solid var(--accent, #0E8C7A); outline-offset: 2px; }
.ax-switch.ax-switch-danger:checked { background: var(--accent5, #A32D2D); }

/* ── Invalid state (idiom from admin/appearance.html .tok-hex.invalid) ───── */
/* Same specificity as the base control rules but declared later, so it wins on
   source order — no !important needed. */
.ax-field input.ax-invalid, .ax-field select.ax-invalid, .ax-field textarea.ax-invalid,
.ax-input.ax-invalid, .ax-select.ax-invalid, .ax-textarea.ax-invalid {
  border-color: var(--accent5, #A32D2D); background-color: rgba(163,45,45,0.05);
}

/* ── Danger zone ─────────────────────────────────────────────────────────── */
/* Irreversible or service-affecting controls get quarantined here so they can
   never be mistaken for a preference. */
.ax-danger-zone {
  margin-top: 22px; padding: 14px 16px; border-radius: var(--ax-r);
  border: var(--ax-bw) solid rgba(163,45,45,0.40); background: rgba(163,45,45,0.04);
}
.ax-danger-zone > h4 {
  display: flex; align-items: center; gap: 8px; margin: 0 0 4px;
  font-size: var(--ax-fs-4); font-weight: 600; color: var(--accent5, #A32D2D);
}
.ax-danger-zone > h4 svg { display: block; }
.ax-danger-zone > .ax-hint { margin-bottom: 10px; }
.ax-danger-row {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding: 12px 0; border-top: var(--ax-bw) solid rgba(163,45,45,0.20);
}
.ax-danger-row:first-of-type { border-top: none; }
.ax-danger-body { flex: 1 1 260px; min-width: 0; }
.ax-danger-body .t { display: block; font-size: var(--ax-fs-4); font-weight: 600; color: var(--text, #191C24); }
.ax-danger-body .d { display: block; font-size: var(--ax-fs-2); line-height: 1.45; color: var(--text3, #9195A3); margin-top: 2px; }

/* ── Buttons: the missing sizes and states ───────────────────────────────── */
/* .btn declares no :focus, no :active and no size modifier, which is why pages
   inline-override padding+font-size (11 times in admin/ai-settings.html alone).
   Declared after .btn, so equal-specificity modifiers win on source order. */
.btn-sm { padding: 5px 12px; font-size: var(--ax-fs-3);  border-radius: var(--ax-r-sm); }
.btn-xs { padding: 3px 8px;  font-size: var(--ax-fs-1); border-radius: var(--ax-r-sm); }
.btn:focus-visible { outline: 2px solid var(--accent, #0E8C7A); outline-offset: 2px; }
.btn:active:not(:disabled) { transform: translateY(1px); }
/* NO .btn-ic here on purpose: instructor/index.html:16 and admin/ai-settings.html
   already define it locally (as an icon-alignment helper with its own
   vertical-align/margin). A shared copy declaring `gap` would land unopposed on
   every button using it in the instructor shell and shift their spacing —
   a silent cross-page change, which is precisely what this layer must not do. */

/* ── Motion + responsive ─────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  .ax-field input, .ax-field select, .ax-field textarea,
  .ax-input, .ax-select, .ax-textarea, .ax-check, .ax-switch, .ax-switch::after,
  /* Added 2026-08-15 with the transitions Stage 4 introduced on these. */
  .btn, .filters select, .filters input, .course-checkbox-list label {
    transition: none !important;
  }
  .btn:active:not(:disabled) { transform: none; }
}
@media (max-width: 720px) {
  .ax-grid, .ax-grid.ax-cols-2 { grid-template-columns: 1fr; }
  .ax-matrix th, .ax-matrix td { padding: 7px 6px; }
  .ax-matrix thead th { font-size: 0.66rem; letter-spacing: 0.3px; }
  .ax-matrix td .ax-money, .ax-matrix td > input { max-width: none; }
  .ax-danger-row { align-items: flex-start; }
}
