/*
 * Shared stylesheet, driven entirely by design tokens (CSS variables).
 *
 * Every color, font, radius, and shadow below references a token — no raw
 * values in the rules. To reskin the whole site, change tokens, not rules:
 *
 *   - Pick a shipped theme in firebase-config.js:  siteConfig.theme
 *     ('paper' | 'dark' | 'playful' — defined in themes.css)
 *   - Or override any single token after the imports, e.g.
 *       :root { --accent: #0f766e; }
 *
 * The :root block below IS the 'paper' theme — a quiet academic-print look —
 * and doubles as the fallback when no theme is set.
 */

:root {
  /* Surfaces and lines */
  --bg: #f2f0eb;             /* page background — soft rag paper */
  --surface: #fdfcfa;        /* cards, header */
  --surface-2: #eceae3;      /* subtle fills: inactive tabs, secondary buttons */
  --border: #ddd8cc;         /* hairlines */
  --border-strong: #b5ad9c;  /* rules, ticket edges */
  --scrim: rgb(0 0 0 / 45%); /* behind a sheet — the page, dimmed but legible */

  /* Ink */
  --text: #26221b;           /* warm ink, not pure black */
  --muted: #6f6a5e;
  --danger: #a13333;

  /* Accent — one voice per theme */
  --accent: #7d2b34;         /* oxblood */
  --accent-hover: #63222a;
  --on-accent: #fdfcfa;      /* text placed on the accent */
  --accent-soft: #efe3dd;    /* tinted fill for callouts */

  /* Status */
  --ok-bg: #e2e8d5;   --ok-text: #3d5230;
  --warn-bg: #efe5cb; --warn-text: #6f5519;

  /* Shape */
  --line: 1px;               /* border width everywhere */
  --radius: 5px;
  --radius-sm: 3px;
  --shadow: none;            /* cards + tickets (print is flat) */
  --shadow-btn: none;
  --hero-rule: 4px double var(--border-strong);  /* the rule under the course title */

  /* Type */
  --font-display: 'Iowan Old Style', 'Palatino Linotype', Palatino, 'Book Antiqua', Georgia, serif;
  --font-body: 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, serif;
  --font-ui: system-ui, -apple-system, 'Segoe UI', sans-serif;

  /*
   * Motion — how the site moves. These are tokens like every other value
   * here, so a theme re-tunes the site's whole feel by changing six numbers,
   * and the reduced-motion block at the end of this file switches all of it
   * off in one place. 'paper' is the restrained one — it lifts and settles,
   * but it never bounces: the easing only decelerates, it doesn't overshoot.
   */
  --dur-quick: 110ms;                       /* hover, press, colour changes */
  --dur: 220ms;                             /* something opening or appearing */
  --dur-slow: 420ms;                        /* a section arriving on screen */
  --ease: cubic-bezier(0.2, 0.7, 0.2, 1);   /* quick start, soft landing */
  --lift: 1px;                              /* how far a thing rises on hover */
  --press: 1px;                             /* how far a button sinks when pressed */
  --rise: 12px;                             /* how far something travels as it enters */
}

* { box-sizing: border-box; }

/* In-page anchors glide instead of jumping (off under reduced motion). */
html { scroll-behavior: smooth; }

body {
  font-family: var(--font-ui);
  margin: 0; background: var(--bg); color: var(--text);
}

header {
  background: var(--surface); border-bottom: var(--line) solid var(--border);
  padding: 14px 24px; display: flex; align-items: center; gap: 16px;
}
header h1 { font-size: 17px; margin: 0; flex: 1; font-family: var(--font-display); }
header a { color: var(--accent); transition: color var(--dur-quick) var(--ease); }
/* A header's controls are a row of buttons, so the LINKS among them wear
   `a.button-link` too (Class, My grades, Instructor tools, Back to the
   course) — a row that mixes underlined links with buttons reads as two
   unrelated kinds of thing, and the links, which are pressed most, come out
   looking the least pressable. Nothing in a header should wrap mid-label. */
header a.button-link { white-space: nowrap; }

main { max-width: 900px; margin: 24px auto; padding: 0 16px; }

h1, h2, h3 { font-family: var(--font-display); }
a { color: var(--accent); transition: color var(--dur-quick) var(--ease); }

.card {
  background: var(--surface); border: var(--line) solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: 20px; margin-bottom: 16px;
}
.card h2 { margin: 0 0 12px; font-size: 17px; }
/* A block INSIDE a panel — "Add one person" under the roster it adds to. */
.card h3 { margin: 26px 0 8px; font-size: 15px; }

/* A folded-away extra: the class-list upload, the bulk XP file. Open it and
   the whole thing is there; closed, it stays out of the way of the daily job. */
.disclosure { margin-top: 22px; border-top: var(--line) solid var(--border); }
.disclosure > summary {
  cursor: pointer; padding: 12px 0 0;
  font-family: var(--font-ui); font-size: 14px; font-weight: 600; color: var(--accent);
}

button {
  background: var(--accent); color: var(--on-accent);
  border: var(--line) solid transparent; border-radius: var(--radius-sm);
  box-shadow: var(--shadow-btn);
  padding: 8px 14px; font-size: 14px; font-family: var(--font-ui); cursor: pointer;
  transition: background-color var(--dur-quick) var(--ease),
              color var(--dur-quick) var(--ease),
              border-color var(--dur-quick) var(--ease),
              box-shadow var(--dur-quick) var(--ease),
              transform var(--dur-quick) var(--ease);
}
/* Hover rises, press sinks — both are 0 in a theme that sets --lift/--press to 0. */
button:hover { background: var(--accent-hover); transform: translateY(calc(-1 * var(--lift))); }
button:active { transform: translateY(var(--press)); }
button.secondary {
  background: var(--surface-2); color: var(--text);
  border-color: var(--border);
}
button.secondary:hover { background: var(--border); }
button:disabled { background: var(--surface-2); color: var(--muted); box-shadow: none; cursor: default; transform: none; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

input, select {
  border: var(--line) solid var(--border-strong); border-radius: var(--radius-sm);
  padding: 8px 10px; font-size: 14px; font-family: var(--font-ui);
  background: var(--surface); color: var(--text);
  transition: border-color var(--dur-quick) var(--ease),
              background-color var(--dur-quick) var(--ease);
}
input:hover, select:hover { border-color: var(--accent); }

/*
 * File pickers (the roster CSV, the XP awards CSV). The browser draws the
 * "Choose File" button itself in the operating system's own look, so it is
 * the one control the tokens never reach — a grey OS button sitting inside a
 * themed page. ::file-selector-button hands it back: the field keeps the same
 * frame as every other input, and the button inside it is a secondary button
 * like the rest of the site. Only the button opens the picker, so only the
 * button takes the pointer cursor.
 */
input[type="file"] {
  padding: 6px 10px 6px 6px;   /* tighter on the left — the button sits there */
  color: var(--muted);         /* "No file chosen", then the chosen filename */
  max-width: 100%;
}
input[type="file"]::file-selector-button {
  margin-right: 10px;
  background: var(--surface-2); color: var(--text);
  border: var(--line) solid var(--border); border-radius: var(--radius-sm);
  box-shadow: var(--shadow-btn);
  padding: 6px 12px; font-size: 13px; font-family: var(--font-ui);
  cursor: pointer;
  transition: background-color var(--dur-quick) var(--ease),
              border-color var(--dur-quick) var(--ease),
              box-shadow var(--dur-quick) var(--ease),
              transform var(--dur-quick) var(--ease);
}
/* Hover tints and outlines in the accent, the same move the field itself
   makes — and unlike a fill of --border it stays readable on a theme whose
   ink and hairlines are the same colour (playful). */
input[type="file"]::file-selector-button:hover {
  background: var(--accent-soft); border-color: var(--accent);
  transform: translateY(calc(-1 * var(--lift)));
}
input[type="file"]::file-selector-button:active { transform: translateY(var(--press)); }

table { border-collapse: collapse; width: 100%; font-size: 14px; }
th, td { text-align: left; padding: 8px 10px; border-bottom: var(--line) solid var(--border); }
th { color: var(--muted); font-weight: 600; font-family: var(--font-ui); }

.muted { color: var(--muted); font-size: 13px; }
.error { color: var(--danger); }

.pill {
  display: inline-block; border-radius: 999px; padding: 2px 10px;
  font-size: 12px; font-weight: 600; font-family: var(--font-ui);
  transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.pill.ok { background: var(--ok-bg); color: var(--ok-text); }
.pill.warn { background: var(--warn-bg); color: var(--warn-text); }

/* ---- Instructor Tools: two levels, side by side --------------------------- */
/*
 * Instructor Tools is really two places — one course, and the whole site —
 * and this menu is what keeps them apart. Two groups, each under its own
 * heading, both on screen at all times, in a column beside the work. Every
 * LMS draws it this way (Canvas's course nav next to its account nav, Moodle's
 * course administration next to site administration) for one reason: a
 * teacher who cannot tell which level they are on eventually changes the
 * wrong thing. The row of tabs this replaced could not say it, and the
 * "Working in …" bar that tried had to appear and disappear to stay honest.
 */
.admin-shell {
  display: grid; grid-template-columns: 210px minmax(0, 1fr);
  gap: 28px; align-items: start;
}
.admin-nav { position: sticky; top: 16px; display: flex; flex-direction: column; gap: 20px; }
.nav-group { display: flex; flex-direction: column; gap: 8px; }
.nav-group-title {
  margin: 0; font-family: var(--font-ui); font-size: 11px; font-weight: 700;
  letter-spacing: 0.09em; text-transform: uppercase; color: var(--muted);
}
.nav-group select { width: 100%; }
/* The course's name where a picker would be, on an install with only one. */
.nav-one-course {
  font-family: var(--font-display); font-size: 15px; font-weight: 600;
  margin: 0; overflow-wrap: anywhere;
}
.nav-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.nav-item {
  display: block; width: 100%; text-align: left;
  background: transparent; color: var(--text); border-color: transparent;
  box-shadow: none; padding: 7px 10px;
}
.nav-item:hover { background: var(--surface-2); transform: none; }
.nav-item[aria-current="page"] {
  background: var(--accent); color: var(--on-accent); border-color: transparent;
}
.nav-item[aria-current="page"]:hover { background: var(--accent-hover); }

/*
 * The band above the work says, in words, which level you are on and what it
 * is about — so the answer is on the page and not only in the menu.
 */
.scope-band {
  padding: 2px 2px 14px; margin-bottom: 16px;
  border-bottom: var(--line) solid var(--border);
}
.scope-kicker {
  margin: 0 0 4px; font-family: var(--font-ui); font-size: 11px; font-weight: 700;
  letter-spacing: 0.09em; text-transform: uppercase; color: var(--muted);
}
.scope-band h2 { margin: 0; font-size: 21px; }
.scope-band h2 .scope-code { font-family: var(--font-ui); font-size: 13px; color: var(--muted); }
.scope-facts { margin: 6px 0 0; }
.scope-facts a { white-space: nowrap; }

@media (max-width: 860px) {
  /* One column: the two groups stack above the work, still labelled, and each
     group's items become a wrapping row rather than a tall list. */
  .admin-shell { grid-template-columns: minmax(0, 1fr); gap: 16px; }
  .admin-nav { position: static; gap: 14px; }
  .nav-list { flex-direction: row; flex-wrap: wrap; }
  .nav-item { width: auto; background: var(--surface-2); border-color: var(--border); }
}

/* ---- Branding (filled in by branding.js — see README "Branding") --------- */
/* Header wordmark: logo (or the built-in ticket mark) + site name. */
.brand {
  flex: 1; display: flex; align-items: center; gap: 10px; min-width: 0;
  font-family: var(--font-display); font-weight: 600; font-size: 15px;
}
[data-brand-logo] { display: flex; align-items: center; flex: none; }
.brand-mark { height: 26px; width: auto; display: block; }

/* ---- Sign-in card — the admission ticket --------------------------------- */
/*
 * The card is the same object <assessment-link> renders, scaled up for the
 * front door: a stub carrying the course identity, a dashed perforation, and
 * two notches punched out of the edges. The notches are page background
 * painted over the card border, which is exactly how a torn ticket reads.
 */
#signin { max-width: 366px; margin: 72px auto 40px; padding: 0; overflow: visible; }
#signin .signin-head { padding: 24px 24px 20px; }
#signin .signin-body { position: relative; padding: 22px 24px 24px; border-top: 1px dashed var(--border-strong); }
#signin .signin-body::before,
#signin .signin-body::after {
  content: ''; position: absolute; top: 0;
  width: 15px; height: 15px; border-radius: 50%; background: var(--bg);
}
#signin .signin-body::before { left: 0;  transform: translate(-50%, -50%); }
#signin .signin-body::after  { right: 0; transform: translate(50%, -50%); }

#signin .brand-lockup {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  margin: 0; text-align: center;
  font-family: var(--font-display); font-weight: 600; font-size: 19px;
}
#signin .brand-lockup .brand-mark { height: 44px; }
#signin h2 { margin: 0 0 4px; font-size: 19px; }
#signin .signin-body > .muted { margin: 0 0 16px; }
#signin input { width: 100%; margin-bottom: 10px; }

#signin-methods button.provider {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  width: 100%; margin-bottom: 8px; min-height: 42px;
  background: var(--surface); color: var(--text); font-weight: 500;
  border: var(--line) solid var(--border-strong); box-shadow: var(--shadow-btn);
}
#signin-methods button.provider:hover { background: var(--surface-2); }
/* Brand marks keep their official colors on every theme — see PROVIDER_MARKS. */
.provider-mark { width: 18px; height: 18px; flex: none; display: block; }

/* Two independent ways in, not a primary and a fallback. */
.signin-or {
  display: flex; align-items: center; gap: 12px;
  margin: 16px 0; color: var(--muted); font-size: 12px; font-family: var(--font-ui);
}
.signin-or::before, .signin-or::after {
  content: ''; flex: 1; height: var(--line); background: var(--border);
}

#signin-methods .emaillink button { width: 100%; }
/* No message, no space — an empty <p> would otherwise hold open the card. */
#signin-error { margin: 12px 0 0; font-size: 13px; }
#signin-error:empty { display: none; }

/* ---- Wrong-place notice -------------------------------------------------- */
/* Shown when someone lands on a page their role has no use for. It always
   carries a way onward, never just a refusal. */
.notice { max-width: 440px; margin: 72px auto; }
.notice h2 { margin: 0 0 10px; }
.notice p { margin: 0 0 10px; }
.notice-actions { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin: 18px 0 0; }
/*
 * A link that IS a button: same look, same hover lift, same press as `button`
 * above, because it is the same object doing a different job (it navigates
 * rather than acting). Themes style `a.button-link` alongside `button`, so a
 * new theme dresses both at once — see themes.css.
 */
a.button-link {
  display: inline-block; text-decoration: none;
  background: var(--accent); color: var(--on-accent);
  border: var(--line) solid transparent; border-radius: var(--radius-sm);
  box-shadow: var(--shadow-btn);
  padding: 8px 14px; font-size: 14px; font-family: var(--font-ui);
  transition: background-color var(--dur-quick) var(--ease),
              color var(--dur-quick) var(--ease),
              border-color var(--dur-quick) var(--ease),
              box-shadow var(--dur-quick) var(--ease),
              transform var(--dur-quick) var(--ease);
}
a.button-link:hover {
  background: var(--accent-hover); color: var(--on-accent);
  transform: translateY(calc(-1 * var(--lift)));
}
a.button-link:active { transform: translateY(var(--press)); }
/* `.secondary` is scoped to <button>, so an <a> asking for it used to come out
   looking primary — two accent buttons side by side, neither one the answer. */
a.button-link.secondary {
  background: var(--surface-2); color: var(--text); border-color: var(--border);
}
a.button-link.secondary:hover { background: var(--border); color: var(--text); }
.field-label {
  display: block; margin-bottom: 6px; font-size: 13px; font-weight: 600;
  font-family: var(--font-ui); color: var(--text);
}

/* Outcome panel under the "email me a link" button. A sent email is invisible
   until it lands, so the page has to report the send itself — silence reads
   as a broken button. One panel, three states. */
.signin-status {
  display: flex; flex-direction: column; gap: 7px;
  margin-top: 12px; padding: 12px 13px;
  border-radius: var(--radius-sm); border: var(--line) solid transparent;
  font-size: 13px; line-height: 1.45; font-family: var(--font-ui);
}
/* `hidden` has to win over the display rule above. An author `display` beats
   the browser's own [hidden] rule, so without this the empty panel sits on the
   page as a padded blank box before anything has been reported. */
.signin-status[hidden] { display: none; }
.signin-status.busy { background: var(--surface-2); color: var(--muted); }
.signin-status.ok   { background: var(--ok-bg);   color: var(--ok-text); }
.signin-status.err  { background: var(--warn-bg); color: var(--warn-text); }
/* "You already have an account here" — an instruction, not a failure, so it
   reads in the site's own accent rather than the warning colors. */
.signin-status.note { background: var(--accent-soft); color: var(--text); }
.signin-status .ss-title { font-size: 14px; }
/* The junk-folder line is the one people need most, so it gets a rule. */
.signin-status .ss-flag {
  border-left: 2px solid currentColor; padding-left: 9px;
  margin-top: 1px; opacity: 0.95;
}
.signin-status-note { opacity: 0.8; font-size: 12px; }

/* ---- Course-page building blocks (used by the authored homepage) --------- */

/* Small-caps kicker above a title: <p class="eyebrow">Winter 2026</p> */
.eyebrow {
  font-family: var(--font-ui); font-size: 12px; font-weight: 600;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted);
  margin: 0 0 4px;
}

.hero { padding: 16px 0 24px; border-bottom: var(--hero-rule); margin-bottom: 28px; }
.hero h1 { font-size: clamp(30px, 5vw, 44px); line-height: 1.1; margin: 0 0 12px; }
.hero .lede { font-family: var(--font-body); font-size: 17px; line-height: 1.55; max-width: 60ch; margin: 0 0 10px; }

/* Long-form text: serif, readable measure */
.prose { font-family: var(--font-body); font-size: 16.5px; line-height: 1.65; max-width: 68ch; }
.prose p { margin: 0 0 14px; }

/* Aside that interrupts the reading flow: <div class="callout">…</div> */
.callout {
  background: var(--accent-soft); border-left: 4px solid var(--accent);
  border-radius: var(--radius-sm); padding: 14px 18px; margin: 20px 0;
  font-family: var(--font-body);
}
.callout > :first-child { margin-top: 0; }
.callout > :last-child { margin-bottom: 0; }

.unit-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 16px; margin: 16px 0;
}
.unit-grid .card { margin-bottom: 0; }
.unit-grid h3 { margin: 0 0 8px; font-size: 17px; }
.unit-grid p { margin: 0 0 12px; font-family: var(--font-body); font-size: 14.5px; line-height: 1.5; }

/* ---- <assessment-link> — the launch ticket ------------------------------- */
/*
 * Renders as a small admission ticket: a stub with the title and your live
 * score, a dashed perforation, and the launch button. Drop it anywhere:
 * mid-paragraph, in a callout, in a card. See assessment-link.js.
 */
assessment-link {
  display: inline-flex; align-items: stretch; vertical-align: middle;
  background: var(--surface); border: var(--line) solid var(--border-strong);
  border-radius: var(--radius); box-shadow: var(--shadow);
  margin: 4px 0; max-width: 100%;
  transition: transform var(--dur-quick) var(--ease),
              border-color var(--dur-quick) var(--ease),
              box-shadow var(--dur-quick) var(--ease);
}
assessment-link:hover { transform: translateY(calc(-1 * var(--lift))); border-color: var(--accent); }
assessment-link .al-stub {
  display: flex; flex-direction: column; justify-content: center;
  padding: 8px 14px; border-right: 1px dashed var(--border-strong);
  min-width: 0;
}
assessment-link .al-title {
  font-family: var(--font-ui); font-weight: 600; font-size: 14px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
assessment-link .al-meta { font-family: var(--font-ui); font-size: 12px; color: var(--muted); margin-top: 1px; }
assessment-link .al-action { display: flex; align-items: center; padding: 8px 10px; }
assessment-link .al-meta .pill { font-size: 11px; padding: 1px 8px; }

/*
 * The ticket changed under the reader's feet — a grade landed, or a teacher
 * finished linking it. Wash it once in the accent tint and let the new stub
 * text rise into place, so the change is noticed instead of just happening.
 * assessment-link.js adds .al-changed only when the printed text differs.
 */
assessment-link.al-changed { animation: wash var(--dur-slow) var(--ease); }
assessment-link.al-changed .al-stub { animation: rise-in var(--dur) var(--ease); }

/* ---- Gradebook — a living ledger (gradebook.js) --------------------------- */
/*
 * Structure: summary strip → toolbar → the ledger grid → per-student drawer.
 * All color comes from the theme tokens; the only "chart junk" allowed is
 * red ink where a teacher's attention is needed, and a soft flash when a
 * score arrives live. Figures are tabular so columns add up visually.
 */

main.wide { max-width: 1200px; }
/* The class page: a ledger with a column per assessment or per level wants
   every pixel the window has. It was a dialog first, and it was cramped. */
main.widest { max-width: 1560px; }

.gb-summary {
  display: flex; flex-wrap: wrap; gap: 10px 36px; align-items: baseline;
  padding: 2px 2px 14px;
}
.gb-summary .gb-fig { display: flex; flex-direction: column; }
.gb-summary .gb-fig b {
  font-family: var(--font-display); font-size: 30px; font-weight: 600;
  line-height: 1.1; font-variant-numeric: tabular-nums;
}
.gb-summary .gb-fig b.gb-attn { color: var(--danger); }
.gb-summary .gb-fig span { font-size: 12px; color: var(--muted); }
.gb-summary .gb-live { margin-left: auto; align-self: end; font-size: 12px; color: var(--muted); }

.gb-toolbar {
  display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  padding-bottom: 12px; border-bottom: var(--line) solid var(--border);
  margin-bottom: 0;
}
.gb-chips { display: flex; gap: 6px; flex-wrap: wrap; }
.gb-chips button {
  background: var(--surface-2); color: var(--text); border-color: var(--border);
  border-radius: 999px; padding: 5px 12px; font-size: 13px;
}
.gb-chips button[aria-pressed="true"] { background: var(--accent); color: var(--on-accent); }
.gb-chips button .gb-count { opacity: 0.75; font-weight: 400; }
.gb-toolbar input[type="search"] { min-width: 170px; flex: 0 1 220px; }
.gb-toolbar .gb-right { margin-left: auto; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }

.gb-seg { display: inline-flex; border: var(--line) solid var(--border-strong); border-radius: var(--radius-sm); overflow: hidden; }
.gb-seg button {
  background: var(--surface); color: var(--text); border: 0; border-radius: 0;
  box-shadow: none; padding: 6px 12px; font-size: 13px;
}
.gb-seg button + button { border-left: var(--line) solid var(--border-strong); }
.gb-seg button[aria-pressed="true"] { background: var(--accent); color: var(--on-accent); }

.gb-options { position: relative; }
.gb-options > summary {
  list-style: none; cursor: pointer; user-select: none;
  background: var(--surface-2); color: var(--text);
  border: var(--line) solid var(--border); border-radius: var(--radius-sm);
  padding: 7px 12px; font-size: 13px;
}
.gb-options > summary::-webkit-details-marker { display: none; }
.gb-options[open] > summary { background: var(--border); }
.gb-options .gb-menu {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 30;
  background: var(--surface); border: var(--line) solid var(--border-strong);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: 12px 14px; min-width: 230px;
}
.gb-options[open] .gb-menu { animation: pop-in var(--dur) var(--ease); }
.gb-options .gb-menu label {
  display: flex; gap: 8px; align-items: baseline; font-size: 13.5px;
  padding: 5px 0; cursor: pointer;
}

.gb-scroll {
  overflow: auto;
  /* Fill what's left of the window under the summary/toolbar, so the whole
     ledger — sticky header to sticky median footer — is on screen at once. */
  max-height: clamp(300px, calc(100vh - 350px), 200vh);
  border-bottom: var(--line) solid var(--border);
}
table.gb-grid { border-collapse: separate; border-spacing: 0; width: 100%; }
/* Re-sorting or re-filtering rebuilds the rows — fade the new set in so the
   ledger settles instead of snapping. Only for choices the teacher made;
   arriving scores use the flash below instead. */
.gb-scroll.gb-swap table.gb-grid tbody { animation: fade-in var(--dur) var(--ease); }
.gb-grid tbody td, .gb-grid tbody th.gb-name-col,
.gb-grid td .gb-cell, .gb-grid tbody th.gb-name-col button, .gb-grid thead button {
  transition: background-color var(--dur-quick) var(--ease), color var(--dur-quick) var(--ease);
}
.gb-grid th, .gb-grid td {
  border-bottom: var(--line) solid var(--border);
  padding: 0; white-space: nowrap;
}
.gb-grid thead th {
  position: sticky; top: 0; z-index: 2;
  background: var(--surface); vertical-align: bottom;
  border-bottom: var(--line) solid var(--border-strong);
}
.gb-grid thead th.gb-name-col { left: 0; z-index: 4; }
.gb-grid tbody th.gb-name-col, .gb-grid tfoot th.gb-name-col {
  position: sticky; left: 0; z-index: 1; background: var(--surface);
  font-weight: 400; text-align: left;
}
.gb-grid tfoot th, .gb-grid tfoot td {
  position: sticky; bottom: 0; background: var(--surface);
  border-top: var(--line) solid var(--border-strong); border-bottom: 0;
  color: var(--muted); font-size: 12.5px; font-variant-numeric: tabular-nums;
  padding: 7px 10px; text-align: right;
}
.gb-grid tfoot th.gb-name-col { z-index: 3; text-align: left; }

/* Header sort buttons fill the cell */
.gb-grid thead button {
  background: none; border: 0; box-shadow: none; border-radius: 0;
  color: var(--muted); font: 600 12.5px var(--font-ui); cursor: pointer;
  padding: 8px 10px; width: 100%; text-align: right;
  display: flex; flex-direction: column; align-items: flex-end; gap: 1px;
}
.gb-grid thead th.gb-name-col button { text-align: left; align-items: flex-start; }
.gb-grid thead button:hover { color: var(--text); background: var(--surface-2); }
.gb-grid thead button .gb-h-title {
  color: var(--text); white-space: normal; text-align: inherit;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  max-width: 130px; line-height: 1.25;
}
.gb-grid thead button .gb-h-sub { font-weight: 400; font-size: 11.5px; }
.gb-grid thead th[aria-sort] button .gb-h-sub::after { content: " ▴"; }
.gb-grid thead th[aria-sort="descending"] button .gb-h-sub::after { content: " ▾"; }

/* Name cells: the student is a button that opens the drawer */
.gb-grid tbody th.gb-name-col button {
  background: none; border: 0; box-shadow: none; border-radius: 0;
  color: var(--text); font-family: var(--font-body); font-size: 14.5px;
  cursor: pointer; padding: 7px 12px 7px 10px; width: 100%; text-align: left;
  display: flex; align-items: baseline; gap: 8px;
}
.gb-grid tbody th.gb-name-col button:hover { background: var(--surface-2); }
.gb-grid tbody th.gb-name-col .gb-flags { font-family: var(--font-ui); font-size: 11px; color: var(--muted); }
.gb-grid tbody th.gb-name-col .gb-flags.gb-flags-attn { color: var(--danger); }
.gb-grid tbody th.gb-name-col .gb-flags .pill { font-size: 10.5px; padding: 1px 7px; }

/* Score cells */
.gb-grid td { text-align: right; font-variant-numeric: tabular-nums; font-size: 13.5px; }
.gb-grid td .gb-cell {
  background: none; border: 0; box-shadow: none; border-radius: 0;
  color: var(--text); font: inherit; cursor: pointer;
  display: block; width: 100%; text-align: right; padding: 7px 10px;
}
.gb-grid td .gb-cell:hover { background: var(--accent-soft); }
.gb-grid td .gb-cell small { color: var(--muted); font-size: 11px; margin-left: 4px; }
.gb-grid td.gb-static { padding: 7px 10px; color: var(--muted); }

/* Attention ink: only where a teacher should look */
.gb-grid .gb-low { color: var(--danger); }
.gb-grid td .gb-cell.gb-low { text-decoration: underline; text-decoration-color: var(--danger); text-underline-offset: 3px; }
.gb-grid td.gb-missing { text-align: center; }
.gb-grid td.gb-missing i {
  font-style: normal; color: var(--muted);
  border-bottom: 2px dotted var(--border-strong); padding: 0 10px;
}
.gb-grid .gb-inprog { color: var(--muted); font-style: italic; }

/* Custom items — cells the teacher marks in place (grading.js configs) */
.gb-grid td.gb-xp { padding: 7px 10px; }   /* the XP total: a value, not a button */
.gb-grid td.gb-markable { cursor: pointer; }
.gb-grid td.gb-markable:hover i { border-bottom-color: var(--accent); }
.gb-grid td .gb-cell.gb-own { font-style: normal; }
.gb-grid .gb-letter {
  font-weight: 700; font-size: 12px; margin-left: 4px;
  color: var(--accent);
}
.gb-editor { display: flex; gap: 4px; align-items: center; }
.gb-editor input { width: 4.6em; padding: 4px 6px; font-size: 0.88rem; }
.gb-editor button { padding: 4px 8px; font-size: 0.8rem; }
.gb-d-cats { margin: 14px 0 4px; }

/* The bookkeeper's double rule before the totals column — pinned right so
   the running total never scrolls away, like the name column on the left. */
.gb-grid .gb-total {
  border-left: 4px double var(--border-strong); font-weight: 600;
  position: sticky; right: 0; background: var(--surface);
}
.gb-grid thead th.gb-total { z-index: 4; }
.gb-grid tbody td.gb-total { z-index: 1; }
.gb-grid tfoot td.gb-total { z-index: 3; }
.gb-grid td.gb-total, .gb-grid tbody .gb-total { padding: 7px 12px 7px 10px; text-align: right; }
.gb-grid td.gb-total small { color: var(--muted); font-weight: 400; font-size: 11px; }

/* Rows */
.gb-grid tbody tr:hover td, .gb-grid tbody tr:hover th.gb-name-col { background: var(--surface-2); }
.gb-grid tbody tr.gb-drawer:hover td { background: var(--accent-soft); }
.gb-grid tbody tr:hover td .gb-cell:hover { background: var(--accent-soft); }
.gb-grid tr.gb-archived th.gb-name-col button, .gb-grid tr.gb-archived td { color: var(--muted); }

/* A score landing live */
@media (prefers-reduced-motion: no-preference) {
  .gb-grid td.gb-flash { animation: gb-land 1.6s ease-out; }
  @keyframes gb-land {
    0% { background: var(--accent-soft); }
    100% { background: transparent; }
  }
}

/* Student drawer (expanded detail row) */
.gb-grid tr.gb-drawer > td {
  white-space: normal; text-align: left; background: var(--accent-soft);
  border-bottom: var(--line) solid var(--border-strong); padding: 0;
}
/*
 * The drawer slides open. A table row can't animate its own height, so the
 * cell holds a one-row grid and the row track grows from 0fr to 1fr with the
 * padding clipped inside. gradebook.js adds .gb-opening only on the render
 * that opens it, so an arriving score doesn't replay the slide.
 */
.gb-drawer .gb-d-wrap { display: grid; grid-template-rows: 1fr; }
.gb-drawer .gb-d-wrap > div { overflow: hidden; min-height: 0; padding: 14px 18px; }
.gb-grid tr.gb-drawer.gb-opening .gb-d-wrap { animation: drawer-open var(--dur) var(--ease); }
.gb-drawer .gb-d-head { display: flex; flex-wrap: wrap; gap: 6px 18px; align-items: baseline; margin-bottom: 10px; }
.gb-drawer .gb-d-head strong { font-family: var(--font-display); font-size: 16.5px; }
.gb-drawer .gb-d-head a { font-size: 13px; }
.gb-drawer .gb-d-head .gb-d-overall { margin-left: auto; font-size: 13.5px; font-variant-numeric: tabular-nums; }
.gb-drawer table { width: auto; min-width: 60%; background: var(--surface); border: var(--line) solid var(--border); border-radius: var(--radius-sm); }
.gb-drawer table th, .gb-drawer table td { padding: 6px 12px; font-size: 13px; white-space: normal; }
.gb-drawer table td.gb-d-score { font-variant-numeric: tabular-nums; text-align: right; }
.gb-drawer table button { padding: 4px 10px; font-size: 12.5px; }
.gb-drawer .gb-d-note { color: var(--muted); font-size: 12.5px; margin: 8px 0 0; }

.gb-empty { color: var(--muted); padding: 18px 2px; font-size: 14px; }
.gb-empty a { color: var(--accent); }

@media (max-width: 640px) {
  .gb-grid thead button .gb-h-title { max-width: 88px; }
  .gb-summary .gb-fig b { font-size: 24px; }
  /* the grid manages its own scrolling; keep the generic table fix off it */
  table.gb-grid { display: table; }
  /* a phone has no room for two pinned columns — keep the name, free the total */
  .gb-grid .gb-total { position: static; }
}

/* ---- Assessment-site connection panel (connection.js) -------------------- */

/* A second section inside the Settings card needs air and a rule above it. */
.conn-heading {
  margin-top: 26px; padding-top: 20px;
  border-top: var(--line) solid var(--border);
}

.conn-block {
  margin: 18px 0 0; padding: 16px 16px 4px;
  background: var(--surface-2); border: var(--line) solid var(--border);
  border-radius: var(--radius);
}
.conn-block > h3 {
  margin: 0 0 6px; font-size: 15px; font-family: var(--font-ui);
}
.conn-block > p:last-child { margin-bottom: 12px; }

.conn-field { margin: 0 0 14px; max-width: 62ch; }
/* An address is long — let the box use the width it has. */
.conn-field input { width: 100%; box-sizing: border-box; }
.conn-field input.conn-readonly {
  background: var(--surface-2); color: var(--muted); border-style: dashed;
}
.conn-note { margin: 5px 0 0; line-height: 1.45; }
.conn-note code, .conn-manual code, .conn-values code {
  font-size: 0.92em; background: var(--surface); padding: 1px 5px;
  border: var(--line) solid var(--border); border-radius: var(--radius-sm);
}

/* The five values to hand over: value first, copy button hugging the right. */
.conn-values { width: auto; margin-bottom: 10px; }
.conn-values th, .conn-values td { border-bottom: none; padding: 4px 12px 4px 0; }
.conn-values th { white-space: nowrap; vertical-align: baseline; }
.conn-values code { word-break: break-all; }
.conn-values .conn-copy-cell { padding-right: 0; }
.conn-values button { padding: 4px 10px; font-size: 12.5px; white-space: nowrap; }

/* Two collapsed sections: the advanced fields, and the terminal route. */
.conn-advanced > summary, .conn-manual > summary {
  cursor: pointer; font-family: var(--font-ui); font-size: 14px; font-weight: 600;
  margin-bottom: 6px;
}
.conn-manual { background: transparent; }
.conn-manual p, .conn-manual li { font-size: 13.5px; line-height: 1.55; max-width: 68ch; }
.conn-manual ol { margin: 10px 0; padding-left: 22px; }
.conn-manual li { margin-bottom: 5px; }

.conn-actions {
  display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin: 18px 0 0;
}
.conn-actions .conn-prov { margin-left: auto; }
.conn-status { max-width: 62ch; }

@media (max-width: 640px) {
  .conn-values, .conn-values tbody, .conn-values tr, .conn-values th, .conn-values td {
    display: block; white-space: normal;
  }
  .conn-values tr { margin-bottom: 12px; }
  .conn-values .conn-copy-cell { margin-top: 4px; }
  .conn-actions .conn-prov { margin-left: 0; }
}

/* ---- Gradebook setup (Instructor Tools → This course) --------------------- */
/* The config editor is a document, so it gets a document's tools: a
   monospace box wide enough for real JSON, and warnings that read as advice
   rather than refusals (refusals use .error, like everywhere else). */
[data-gbs="editor"], .gbs-ai textarea {
  width: 100%; box-sizing: border-box; margin-top: 8px;
  font-family: var(--font-mono, ui-monospace, monospace); font-size: 13px;
  line-height: 1.5; color: var(--text);
  background: var(--surface-2); border: var(--line) solid var(--border);
  border-radius: var(--radius-sm); padding: 10px 12px;
}
.gbs-warnings { margin: 8px 0 0; padding-left: 20px; max-width: 68ch; }
.gbs-warnings li { color: var(--warn-text); margin-bottom: 4px; font-size: 13.5px; }
.gbs-ai {
  margin-top: 22px; padding-top: 16px;
  border-top: var(--line) solid var(--border);
}
.gbs-ai h3 { margin: 0 0 6px; font-size: 15px; font-family: var(--font-ui); }
.gbs-ai p { max-width: 68ch; }

/* The setup form: a mode picker, then small tables of plain fields. */
.gbs-h {
  margin: 26px 0 6px; padding-top: 20px; font-size: 15px;
  font-family: var(--font-ui); border-top: var(--line) solid var(--border);
}
.gbs-h4 { margin: 20px 0 4px; font-size: 14px; font-family: var(--font-ui); }
.gbs-modes { display: flex; flex-wrap: wrap; gap: 6px 18px; margin: 8px 0 4px; }
.gbs-mode { display: inline-flex; align-items: center; gap: 7px; cursor: pointer; }
.gbs-table { width: auto; }
.gbs-table th, .gbs-table td { padding: 6px 12px 6px 0; vertical-align: top; }
.gbs-table input:not([type="checkbox"]), .gbs-table select { max-width: 100%; }
.gbs-num { width: 5.5em; }
.gbs-num-cell { white-space: nowrap; }
.gbs-verdict { max-width: 68ch; }
.gbs-verdict p { margin: 8px 0 0; }
.gbs-weight-total { margin-left: 10px; }

/* ---- Motion — one small vocabulary, used everywhere ---------------------- */
/*
 * Five keyframes cover the whole site: rise-in (something arrives), fade-in
 * (something is replaced), pop-in (a menu opens), wash (something changed in
 * place), drawer-open (a row unfolds). Every value is a motion token, so a
 * theme retunes the feel and the reduced-motion block at the end of this file
 * cancels all of it in one place.
 */
@keyframes rise-in { from { opacity: 0; transform: translateY(var(--rise)); } to { opacity: 1; transform: none; } }
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes pop-in  { from { opacity: 0; transform: translateY(-6px) scale(0.98); } to { opacity: 1; transform: none; } }
@keyframes wash    { from { background-color: var(--accent-soft); } to { background-color: transparent; } }
@keyframes drawer-open { from { grid-template-rows: 0fr; opacity: 0; } to { grid-template-rows: 1fr; opacity: 1; } }
@keyframes spin    { to { transform: rotate(360deg); } }

/* A page, a card, or a tab panel arriving. */
.enter { animation: rise-in var(--dur-slow) var(--ease); }

/* A status line whose text just changed (flashIn in app.js). */
.flash-in { animation: fade-in var(--dur) var(--ease); }

/* "Checking your sign-in…" — held back half a second so a fast sign-in
   never flashes it on screen. */
.wait-note { animation: fade-in var(--dur-slow) var(--ease) 500ms both; }

/* A button waiting on the network (busy() in app.js): keeps its label,
   gains a turning ring, refuses a second click. */
button.is-busy { cursor: progress; }
button.is-busy::after {
  content: ''; display: inline-block; width: 0.7em; height: 0.7em;
  margin-left: 8px; vertical-align: -1px;
  border: 2px solid currentColor; border-right-color: transparent;
  border-radius: 50%; animation: spin 700ms linear infinite;
}

/*
 * Reveal on scroll (motion.js). Put .reveal on a section, or .reveal-group on
 * a wrapper whose children should arrive one after another. The hidden state
 * hangs off .motion, which motion.js sets only when it can definitely reveal
 * things again — no JS, no IntersectionObserver, or reduced motion all leave
 * the content plainly visible.
 */
.motion .reveal, .motion .reveal-group > * {
  opacity: 0; transform: translateY(var(--rise));
  transition: opacity var(--dur-slow) var(--ease), transform var(--dur-slow) var(--ease);
}
.motion .reveal.is-in, .motion .reveal-group.is-in > * { opacity: 1; transform: none; }
/* On paper there is no scrolling to do — print every section, revealed or not. */
@media print {
  .motion .reveal, .motion .reveal-group > * { opacity: 1 !important; transform: none !important; }
}
.motion .reveal-group.is-in > *:nth-child(2) { transition-delay: 70ms; }
.motion .reveal-group.is-in > *:nth-child(3) { transition-delay: 140ms; }
.motion .reveal-group.is-in > *:nth-child(4) { transition-delay: 210ms; }
.motion .reveal-group.is-in > *:nth-child(n+5) { transition-delay: 280ms; }

/*
 * Changing the theme swaps every colour at once; without this it lands like a
 * light switch. theme.js puts .theme-changing on <html> for one beat so the
 * page crossfades, then removes it — a blanket transition is fine for half a
 * second, not as a standing rule.
 */
.theme-changing, .theme-changing *, .theme-changing *::before, .theme-changing *::after {
  transition: background-color var(--dur-slow) var(--ease),
              border-color var(--dur-slow) var(--ease),
              color var(--dur-slow) var(--ease),
              box-shadow var(--dur-slow) var(--ease) !important;
}

/* ---- XP course elements ---------------------------------------------------
 * Styling for the four elements in course-components.js. Every value here is
 * a token, so an XP course inherits whatever theme the site is wearing — the
 * leaderboard on the zombie theme is the same markup as on paper.
 */

/* Mission acceptance — a quiet block until it is answered, then a receipt. */
.xp-accept {
  display: block; margin: 18px 0; padding: 16px 18px;
  background: var(--accent-soft); border: var(--line) solid var(--border);
  border-radius: var(--radius);
}
.xp-accept-ask { margin: 0 0 12px; }
.xp-accept-done { font-family: var(--font-ui); }

/* GamerID picker. */
.xp-gamerid {
  display: block; margin: 18px 0; padding: 16px 18px;
  background: var(--surface); border: var(--line) solid var(--border);
  border-radius: var(--radius);
}
.xp-gamerid-note { margin: 4px 0 10px; font-size: 0.9rem; }
.xp-gamerid-current { margin: 0 0 10px; }
.xp-gamerid input { width: 100%; max-width: 320px; display: block; margin-bottom: 12px; }
.xp-gamerid-actions { display: flex; gap: 8px; flex-wrap: wrap; }

/* The live standings table. */
.xp-board { display: block; margin: 18px 0; }
/* A five-column table has to be able to scroll on a phone rather than
   squeezing the GamerID column into one letter per line. */
.xp-board-scroll { overflow-x: auto; }
.xp-board table { width: 100%; }
.xp-board .xp-num { text-align: right; white-space: nowrap; }
.xp-board .xp-rank { width: 3em; text-align: center; font-family: var(--font-ui); }
/* The signed-in student's own row, so they can find themselves at a glance. */
.xp-board .xp-me > td {
  background: var(--accent-soft);
  border-top: var(--line) solid var(--border-strong);
  border-bottom: var(--line) solid var(--border-strong);
}

/* The student's own standing. */
.xp-summary { display: block; margin: 18px 0; }
.xp-summary-head {
  display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap;
}
.xp-total {
  font-family: var(--font-display); font-size: 2.6rem; line-height: 1;
  color: var(--accent);
}
.xp-total-unit {
  font-family: var(--font-ui); font-size: 0.9rem; margin-left: 6px;
  color: var(--muted); letter-spacing: 0.08em;
}
.xp-grade {
  font-family: var(--font-ui); font-weight: 700; font-size: 1.3rem;
  padding: 2px 12px; border-radius: var(--radius-sm);
  background: var(--accent); color: var(--on-accent);
}
.xp-rank-name { font-family: var(--font-ui); color: var(--muted); }
.xp-bar {
  height: 10px; margin: 14px 0 6px; border-radius: var(--radius-sm);
  background: var(--surface-2); border: var(--line) solid var(--border);
  overflow: hidden;
}
.xp-bar > span {
  display: block; height: 100%; background: var(--accent);
  transition: width var(--dur-slow) var(--ease);
}
.xp-facts { margin: 10px 0 0; padding-left: 20px; }
.xp-facts .xp-ok { color: var(--ok-text); }
/* Short of the sector minimum is the one state worth colouring: a student can
   be well clear on points and still not be passing. */
.xp-facts .xp-short { color: var(--danger); }

/* ---- The XP ledger (xp-gradebook.js) ------------------------------------- */

.xpg { width: 100%; border-collapse: collapse; font-size: 0.92rem; }
.xpg th, .xpg td {
  padding: 8px 10px; border-bottom: var(--line) solid var(--border);
  vertical-align: top;
}
/* The name column stays put while a term's worth of level columns scrolls. */
.xpg .xpg-name {
  position: sticky; left: 0; z-index: 1;
  background: var(--surface); min-width: 190px; text-align: left;
}
.xpg thead th { background: var(--surface-2); white-space: nowrap; }
.xpg .xp-num { text-align: right; white-space: nowrap; }
.xpg-max { color: var(--muted); font-size: 0.78rem; font-weight: 400; }

/* A markable cell. Three states: marked, accepted-but-unmarked, untouched. */
.xpg-cell { cursor: pointer; min-width: 3.6em; }
.xpg-cell:hover { background: var(--accent-soft); }
.xpg-none { color: var(--muted); }
/* The teacher's queue: the student took this level on and it is not marked. */
.xpg-awaiting { background: var(--warn-bg); color: var(--warn-text); font-weight: 600; }
.xpg-coins { display: block; font-size: 0.72rem; color: var(--muted); }
.xpg-short { color: var(--danger); font-weight: 600; }
.xpg-atrisk .xpg-name { border-left: 3px solid var(--danger); }
.xpg-grade {
  font-family: var(--font-ui); font-weight: 700;
  padding: 1px 8px; border-radius: var(--radius-sm);
  background: var(--surface-2); border: var(--line) solid var(--border);
}

.xpg-editing { background: var(--surface); }
.xpg-editor { display: flex; gap: 4px; align-items: center; }
.xpg-editor input { width: 4.2em; padding: 4px 6px; font-size: 0.88rem; }
.xpg-editor input[data-coin-input] { width: 3.2em; }
.xpg-editor button { padding: 4px 8px; font-size: 0.8rem; }
.gb-warn { color: var(--warn-text); }
.gb-sortbtn {
  background: none; border: 0; padding: 0; font: inherit; color: inherit;
  cursor: pointer; text-decoration: underline dotted;
}

/* ---- Student view --------------------------------------------------------- */
/*
 * The persistent banner while an instructor is in student view (view-mode.js).
 * It sits above the header and stays distinct on every theme: accent-tinted
 * fill, dashed accent rule — the same "temporary ticket" language as the
 * sign-in stub. It must never be mistakable for course content — and it is
 * STICKY: being in a borrowed identity must stay visible however far the
 * instructor scrolls (and survive the scroll position a reload restores).
 */
.studentview-bar {
  position: sticky; top: 0; z-index: 40;
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px 16px;
  padding: 10px 24px;
  background: var(--accent-soft); color: var(--text);
  border-bottom: var(--line) dashed var(--accent);
  font-size: 14px;
}
.studentview-bar strong { color: var(--accent); }
.studentview-bar-text { flex: 1; min-width: 220px; }
.studentview-bar button { flex-shrink: 0; }

/* ---- Sheet — an editor that comes to the front ---------------------------- */
/*
 * A <dialog> dressed as one of this site's cards. Used by the course editor on
 * Instructor Tools: a form that edits ONE named thing belongs in front of the
 * page, not below it, where a click three rows up moves the fields but not the
 * reader's eye.
 *
 * <dialog> is doing the load-bearing work — focus in and back out, Esc to
 * close, the page behind it inert — so there is no JavaScript here beyond
 * showModal()/close(). Everything below is looks: tokens only, and the
 * entrance is the site's own rise-in, which the reduced-motion block at the
 * end of this file switches off with every other animation.
 */
.sheet {
  width: min(560px, calc(100vw - 32px));
  max-height: min(88vh, 900px);
  padding: 0;
  background: var(--surface);
  color: var(--text);
  border: var(--line) solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.sheet::backdrop { background: var(--scrim); animation: fade-in var(--dur) var(--ease); }
.sheet[open] { animation: rise-in var(--dur) var(--ease); }
.sheet-inner { display: flex; flex-direction: column; max-height: inherit; }
.sheet-head {
  display: flex; align-items: center; gap: 12px;
  padding: 16px 20px; border-bottom: var(--line) solid var(--border);
}
.sheet-head h2 { margin: 0; font-size: 17px; flex: 1; }
.sheet-close {
  flex-shrink: 0; line-height: 1; font-size: 18px; padding: 4px 10px;
}
.sheet-body { padding: 16px 20px; overflow-y: auto; }
.sheet-foot {
  padding: 14px 20px; border-top: var(--line) solid var(--border);
  background: var(--surface-2);
  border-bottom-left-radius: var(--radius); border-bottom-right-radius: var(--radius);
}
.sheet-foot .muted:empty { display: none; }
.sheet-actions { display: flex; gap: 8px; margin: 0; flex-wrap: wrap; }

/* Nothing in a sheet may push it sideways: a flex item's default minimum is
   its content, so one long title makes the whole sheet wider than the phone
   it is on. Anything wide inside scrolls in its own box instead. */
.sheet-inner, .sheet-body, .sheet-head { min-width: 0; }

/*
 * A row of tabs inside a card — the class page (class.html). It reuses the
 * menu item from Instructor Tools, so "the thing I am looking at" is marked
 * the same way wherever a reader is.
 */
.tab-row {
  display: flex; gap: 4px; flex-wrap: wrap;
  margin: -4px 0 16px; padding-bottom: 12px;
  border-bottom: var(--line) solid var(--border);
}
.tab-row .nav-item { width: auto; }

/* A route through a sheet: the heading says what the route is for, and what
   follows is the real control, borrowed from the page underneath (the sign-in
   refusal lends its email-link form). */
.sheet-step { margin: 16px 0; }
.sheet-step-head { margin: 0 0 10px; font-size: 14px; }
.sheet-body > p:first-child { margin-top: 0; }

/* A labelled field, stacked. Placeholders alone stop being readable the moment
   someone types in the box; a label stays. */
.field { display: flex; flex-direction: column; gap: 4px; margin: 0 0 14px; }
.field label { font-family: var(--font-ui); font-size: 13px; font-weight: 600; }
.field-note { color: var(--muted); font-size: 12px; }
.field input:disabled, .field select:disabled { background: var(--surface-2); color: var(--muted); }

/* ---- Theme sample (Instructor Tools → Courses) ---------------------------- */
/*
 * A course's theme is previewed INSIDE this box, never by repainting the page.
 * Instructor Tools is site-wide: dressing the whole page in one course's theme
 * says the tools belong to that course, which is the confusion the Settings
 * tab already had to be rescued from. (The site theme picker on Settings does
 * repaint the page — there it is telling the truth.)
 *
 * The trick is that themes.css scopes its tokens with a bare
 * [data-theme="..."] selector, so stamping the attribute on this div
 * re-declares every colour, font, shape and motion token for its contents —
 * including the per-theme extras, like playful's hard shadows on buttons.
 * Nothing in here needs to know which theme it is showing.
 */
.theme-sample {
  margin-top: 12px;
  padding: 16px 18px;
  background: var(--bg);
  color: var(--text);
  border: var(--line) solid var(--border-strong);
  border-radius: var(--radius);
  font-family: var(--font-body);
  max-width: 460px;
}
.theme-sample[hidden] { display: none; }
.theme-sample-title {
  font-family: var(--font-display);
  font-size: 18px;
  margin: 0 0 4px;
}
.theme-sample-lede { margin: 0 0 12px; color: var(--muted); font-size: 14px; }
.theme-sample-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* The label rides OUTSIDE the sample, so it keeps the site's own look. */
.theme-sample-label { margin: 12px 0 0; }

/* ---- Small screens -------------------------------------------------------- */
@media (max-width: 640px) {
  header { flex-wrap: wrap; row-gap: 10px; }
  /* The site name owns the first row — a long one (or a long signed-in
     email) must never shrink below its words and paint over, or shove out,
     the row of controls beside it. */
  .brand { flex-basis: 100%; }
  #whoami { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* A <select> sizes itself to its widest OPTION, not to the space it has, so
     one course called "Operations Management — Zombie Survival Academy" pushed
     the whole page sideways on a phone. Cap it and let the text truncate; the
     dropdown itself still opens at full width. */
  #course-switch { max-width: 100%; min-width: 0; flex-shrink: 1; }
  /* Wide tables scroll sideways inside their card instead of crushing text. */
  table { display: block; overflow-x: auto; }
  th, td { white-space: nowrap; }
  td .muted { white-space: normal; display: inline-block; min-width: 100%; width: 0; }
}

/* ---- My grades (my-grades.js) --------------------------------------------- */
/*
 * A student's own record. The figures at the top borrow the ledger's shape
 * (big number, small label) so the two pages read as one family, and every
 * label says exactly what the number counts — a student reading their own
 * marks is the last person who should be shown a guess.
 */
.mg-figs {
  display: flex; flex-wrap: wrap; gap: 12px 36px; align-items: baseline;
  padding: 2px 2px 14px;
}
.mg-fig { display: flex; flex-direction: column; }
.mg-fig b {
  font-family: var(--font-display); font-size: 30px; font-weight: 600;
  line-height: 1.1; font-variant-numeric: tabular-nums;
}
.mg-fig .mg-of { font-size: 17px; color: var(--muted); font-weight: 400; }
.mg-fig span { font-size: 12px; color: var(--muted); }
.mg-fig.mg-short b { color: var(--danger); }
.mg-warn { color: var(--danger); font-size: 13px; }
.mg-num { text-align: right; font-variant-numeric: tabular-nums; }
#my-grades h3:first-child { margin-top: 8px; }

/* ---- Reduced motion ------------------------------------------------------- */
/*
 * A visitor whose system asks for less motion gets none of it. The tokens go
 * to zero, which collapses every distance and duration spelled in tokens, and
 * the blanket rule catches anything that isn't. `html[data-theme]` outranks
 * the per-theme tuning in themes.css, so this wins whatever theme is on.
 * Keep this block LAST.
 */
@media (prefers-reduced-motion: reduce) {
  :root, html[data-theme] {
    --dur-quick: 0ms; --dur: 0ms; --dur-slow: 0ms;
    --lift: 0px; --press: 0px; --rise: 0px;
  }
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    transition-delay: 0ms !important;
  }
}
