/* Last Bell dashboard theme.
 *
 * Design tokens extracted from the Purity UI Dashboard template —
 * Copyright (c) 2021 Creative Tim, MIT license (see the Credits section of
 * the README for the full notice reference) — via its Figma community file
 * and open-source Chakra theme:
 *   accent teal.300 #4FD1C5 · positive green.400 #48BB78 · negative red.400
 *   #F56565 · warn orange.400 #ED8936 · text gray.700 #1F2733 (Purity's
 *   override) · muted gray.400 #A0AEC0 · body gray.50 #F7FAFC · cards white,
 *   radius 15px, shadow 0 3.5px 5.5px rgba(0,0,0,.02), padding 22px ·
 *   font Roboto.
 * Fonts stay local (LAN box, no internet): Roboto if installed, else the
 * system stack. Every color routes through the custom properties below —
 * restyling means editing tokens, not rules.
 */

:root {
  color-scheme: light dark;

  /* palette */
  --accent:        #4fd1c5;
  --accent-strong: #38b2ac;
  --ok:            #48bb78;
  --warn:          #ed8936;
  --bad:           #f56565;
  --info:          #4fd1c5;

  /* WCAG-checked companions (a11y audit 2026-09-01). The pastel tokens
   * above are FILLS; anything that renders as text, icon stroke, or a
   * meaningful edge uses these instead. Light values are the ≥4.5:1
   * (text) / ≥3:1 (edges) versions; the dark theme swaps most back to
   * the pastels, which pass on dark surfaces. */
  --on-fill: #1f2733;          /* ink on the pastel fills — both themes */
  --link:    #17706a;          /* link text (5.9:1 white, 4.8:1 canvas) */
  --edge:    #17706a;          /* active borders, focus ring, sparklines */
  --control-border: #8494a9;   /* form-control borders (3.1:1) */
  --ok-ink:   #276749;
  --warn-ink: #9c4221;
  --bad-ink:  #c53030;
  --info-ink: #17706a;

  /* light surfaces — deliberately darker than Purity's gray.50 canvas: the
   * template's near-white bg gave white cards almost no separation, so the
   * canvas steps down to gray.200 and borders/shadow firm up to match. */
  --bg:         #e2e8f0;
  --surface:    #ffffff;
  --surface-2:  #e2e8f0;
  --text:       #1f2733;
  --text-muted: #56667b;
  --border:     #cbd5e0;

  /* shape + depth */
  --radius:      15px;
  --radius-sm:   12px;
  --card-shadow: 0 3.5px 5.5px rgba(0, 0, 0, 0.06);
  --card-pad:    22px;

  /* motion (tiers borrowed from jshq): fast = hover/press feedback,
   * base = enter/exit fades, slow = larger movement (row collapse),
   * linger = attention decay (toast fade-out). One symmetric curve
   * because the same tokens drive enters AND exits. */
  --dur-fast:   150ms;
  --dur-base:   300ms;
  --dur-slow:   400ms;
  --dur-linger: 750ms;
  --ease:       cubic-bezier(0.4, 0, 0.2, 1);
  --rise:       14px;
}

/* Dark palette. Three theme states: an explicit choice stamps
 * data-theme="dark"/"light" on <html> (the nav toggle, saved per browser);
 * with no choice, the system preference decides. The same token values
 * appear twice so the manual choice wins in both directions. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg:         #12161f;
    --surface:    #1f2733;
    --surface-2:  #2d3748;
    --text:       #f7fafc;
    --text-muted: #a0aec0;
    --border:     rgba(255, 255, 255, 0.08);
    --card-shadow: none;
    --link:    #38b2ac;
    --edge:    #4fd1c5;
    --control-border: rgba(255, 255, 255, 0.08);
    --ok-ink:   #48bb78;
    --warn-ink: #ed8936;
    --bad-ink:  #f56565;
    --info-ink: #4fd1c5;
  }
}
:root[data-theme="dark"] {
  --bg:         #12161f;
  --surface:    #1f2733;
  --surface-2:  #2d3748;
  --text:       #f7fafc;
  --text-muted: #a0aec0;
  --border:     rgba(255, 255, 255, 0.08);
  --card-shadow: none;
  --link:    #38b2ac;
  --edge:    #4fd1c5;
  --control-border: rgba(255, 255, 255, 0.08);
  --ok-ink:   #48bb78;
  --warn-ink: #ed8936;
  --bad-ink:  #f56565;
  --info-ink: #4fd1c5;
}

/* A forced theme must also force the UA's own colors (form controls, native
 * selection) — otherwise OS-dark + forced-light pages get the browser's dark
 * selection behind our dark text. */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"]  { color-scheme: dark; }

/* Explicit selection colors from the palette: airy teal in light mode so
 * dark text stays readable; translucent teal in dark mode. */
::selection { background: #b2f5ea; color: #1f2733; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) ::selection {
    background: rgba(79, 209, 197, 0.35);
    color: #f7fafc;
  }
}
:root[data-theme="dark"] ::selection {
  background: rgba(79, 209, 197, 0.35);
  color: #f7fafc;
}

* { box-sizing: border-box; }

body {
  font: 14px/1.55 Roboto, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
  background: var(--bg);
  color: var(--text);
  margin: 0 auto;
  max-width: 68rem;
  padding: 1.25rem 1.25rem 3rem;
}

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

/* Keyboard affordances: a skip link (hidden until focused) and one visible
 * focus ring, theme-correct via the tokens. */
.skip {
  position: absolute;
  left: -999px;
  top: 0.5rem;
  z-index: 40;
  background: var(--surface);
  color: var(--text);
  padding: 0.5rem 0.9rem;
  border-radius: var(--radius);
  box-shadow: var(--card-shadow);
  font-weight: 600;
}
.skip:focus { left: 0.5rem; }
:focus-visible { outline: 2px solid var(--edge); outline-offset: 2px; }
/* Visually hidden, still announced — screen-reader-only text. */
.vh {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── nav ────────────────────────────────────────────────────────────── */

nav {
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--card-shadow);
  padding: 0.8rem 1.2rem;
  margin-bottom: 1.4rem;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 1.1rem;
}
nav a { color: var(--text-muted); font-weight: 600; font-size: 0.8rem;
        display: inline-flex; align-items: center; gap: 0.35rem; }
nav a:hover { color: var(--text); text-decoration: none; }
/* The page you're on reads at full strength — the nav's current state
 * (aria-current='page' carries it programmatically too). */
nav a[aria-current='page'] { color: var(--text); }
nav a svg { display: none; width: 17px; height: 17px; flex: none; }
/* The brand mark stays at every width (the page-link icons above appear
 * only when the labels go). Slightly bigger than the icons; the source
 * bell is drawn tilted, so the box gets a hair of optical room. */
nav .brand svg.mark { display: inline-block; width: 20px; height: 20px; }
nav .brand {
  color: var(--text);
  font-weight: 800;
  font-size: 0.85rem;
  margin-right: 0.6rem;
}
/* Student links: direct name links at desktop width. On narrow they yield
 * to .smenu — the students icon fronting a <details> popover of names
 * (opens with no JS; app.js adds outside-click close). */
nav .navstudents { display: inline-flex; align-items: center; gap: 1.1rem; }
nav details.smenu { display: none; position: relative; }
nav details.smenu summary {
  list-style: none;
  cursor: pointer;
  line-height: 0;
  color: var(--text-muted);
}
nav details.smenu summary::-webkit-details-marker { display: none; }
nav details.smenu summary:hover,
nav details.smenu[open] summary { color: var(--text); }
nav details.smenu summary svg { width: 17px; height: 17px; }
nav .smenu-list {
  position: absolute;
  z-index: 15;
  top: calc(100% + 8px);
  left: 0;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.22);
  padding: 0.6rem 0.8rem;
  animation: msel-in var(--dur-fast) var(--ease);
}
nav .smenu-list a { white-space: nowrap; }
/* Settings is icon-only at every width. */
nav a.gear svg { display: inline-block; }
/* Icon-only, gear-sized: auto = half-filled circle, light = sun,
 * dark = moon (the state rides the tooltip/aria-label). */
#themetoggle {
  background: none;
  border: none;
  padding: 0;
  color: var(--text-muted);
  cursor: pointer;
  line-height: 0;
}
#themetoggle:hover { color: var(--text); }
/* 17px icons keep their look; padding+negative margin grows the
 * HIT AREA to >=24px without moving the layout (SC 2.5.8). */
nav a.gear, #themetoggle, nav details.smenu summary {
  padding: 0.35rem; margin: -0.35rem;
}
/* The gear+toggle pair is pushed right, apart from the page links. Must
 * stay AFTER the hit-area rule above: its `margin` shorthand would reset
 * this `margin-left: auto` at equal specificity. */
nav a.gear { margin-left: auto; }
nav .smenu-list a { padding: 0.25rem 0; }
#themetoggle svg { width: 17px; height: 17px; display: none; }
/* The button carries all three icons; the saved choice on <html> (set
 * before first paint) picks one, so nothing swaps after the page shows. */
#themetoggle .ic-auto { display: inline-block; }
:root[data-theme='light'] #themetoggle .ic-auto,
:root[data-theme='dark'] #themetoggle .ic-auto { display: none; }
:root[data-theme='light'] #themetoggle .ic-light { display: inline-block; }
:root[data-theme='dark'] #themetoggle .ic-dark { display: inline-block; }

/* ── type ───────────────────────────────────────────────────────────── */

h1 { font-size: 1.15rem; font-weight: 700; margin: 0.2rem 0 0.8rem; }
h2 { font-size: 0.95rem; font-weight: 700; margin: 1.6rem 0 0.4rem; }
.small { font-size: 0.8rem; color: var(--text-muted); }
code { background: var(--surface-2); padding: 0.1rem 0.35rem; border-radius: 6px; }

/* ── cards ──────────────────────────────────────────────────────────── */

.cards { display: flex; flex-wrap: wrap; gap: 1.1rem; }
.card {
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--card-shadow);
  padding: var(--card-pad);
  min-width: 17rem;
  flex: 1;
}
.card h2.cardname { margin: 0 0 0.15rem; font-size: 0.95rem; }
.card .cardname a { color: var(--text); }

/* Overview: student and course names are links and must read as links —
 * the same soft underline as the strip's filters. Course names deep-link
 * to the student page scoped to that course. History's assignment and
 * course names deep-link the same way, and wear the same underline. */
.card .cardname a,
table.courses td a,
table.history td a {
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--accent-strong) 40%, transparent);
  text-underline-offset: 3px;
}
.card .cardname a:hover,
table.courses td a:hover,
table.history td a:hover { text-decoration-color: currentColor; }
/* Breathing room between the school line and the status badges. */
.card .cardname + .small { margin-bottom: 0.55rem; }
/* The school name links out to the school's own site — the app's only outbound
 * link. It inherits .small's muted color so the name stays quiet; the soft
 * underline (same treatment as the in-app links above) is the affordance. */
.schoollink {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--accent-strong) 40%, transparent);
  text-underline-offset: 3px;
}
.schoollink:hover { text-decoration-color: currentColor; }

/* A table section as a card, Purity-style: the section title lives INSIDE
 * the card ("Authors Table"), not floating on the canvas above it. */
.tablecard { margin: 0 0 1.1rem; min-width: 0; }
.tablecard h2 { margin: 0 0 0.6rem; font-size: 1rem; }
.tablecard table { margin: 0; }
.tablecard > p.small { margin: 0.2rem 0 0; }

/* ── tables ─────────────────────────────────────────────────────────── */

table {
  border-collapse: collapse;
  width: 100%;
  margin: 0.5rem 0 1rem;
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--card-shadow);
  overflow: hidden;
}
.card table { box-shadow: none; border-radius: 0; }
th, td {
  text-align: left;
  padding: 0.55rem 0.8rem;
  border-bottom: 1px solid var(--border);
}
tr:last-child td { border-bottom: none; }
th {
  font-size: 0.65rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
}
td.num { text-align: right; font-variant-numeric: tabular-nums; }

/* Shared column grids so every instance of a table type lines up the same —
 * the per-course assignment tables on a student page must not each invent
 * their own column widths. First column (name/course) takes the remainder. */
table.assignments,
table.courses { table-layout: fixed; }
table.assignments td { overflow-wrap: break-word; }
table.assignments th:nth-child(2) { width: 22%; }   /* type   */
table.assignments th:nth-child(3) { width: 7.5rem; }/* due    */
table.assignments th:nth-child(4) { width: 5.5rem; }/* score  */
table.assignments th:nth-child(5) { width: 9rem; }  /* status */
table.courses th:nth-child(2) { width: 34%; }       /* teacher */
table.courses th:nth-child(3) { width: 3.6rem; }    /* percent */
table.courses th:nth-child(4) { width: 3.6rem; }    /* mark    */

/* ── status badges (solid pills, Purity-style) ──────────────────────── */

.badge {
  display: inline-block;
  padding: 0.14rem 0.65rem;
  border-radius: 8px;
  font-size: 0.72rem;
  font-weight: 700;
  white-space: nowrap;
  color: var(--on-fill);
}
.badge.ok    { background: var(--ok); }
.badge.info  { background: var(--info); }
.badge.warn  { background: var(--warn); }
.badge.bad   { background: var(--bad); }
.badge.muted { background: var(--surface-2); color: var(--text-muted); }

/* ── forms (settings) ───────────────────────────────────────────────── */

/* Add forms sit apart from the rows they feed: an inset panel on the canvas
 * color, so "create" and "manage" read as two zones. */
form.edit {
  display: flex; flex-wrap: wrap; gap: 0.45rem; align-items: center;
  margin: 0.15rem 0 1.1rem;
  padding: 0.7rem 0.8rem;
  background: var(--bg);
  border-radius: var(--radius-sm);
}
form.rowform { display: inline; margin: 0; }
form.edit select,
form.edit input,
td select,
td input {
  font: inherit;
  font-size: 0.78rem;
  border: 1px solid var(--control-border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  padding: 0.28rem 0.5rem;
}
form.edit input { max-width: 13rem; }
/* A settings field that is a value, not a row to add: label, control, unit
 * on one line, in the same inset panel as the add forms. */
.field {
  display: flex; flex-wrap: wrap; gap: 0.45rem; align-items: center;
  margin: 0.15rem 0 0.6rem;
  padding: 0.7rem 0.8rem;
  background: var(--bg);
  border-radius: var(--radius-sm);
}
.field label { font-size: 0.78rem; font-weight: 600; }
.field input {
  font: inherit;
  font-size: 0.78rem;
  border: 1px solid var(--control-border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  padding: 0.28rem 0.5rem;
  width: 5rem;
}
.field .unit { font-size: 0.78rem; color: var(--text-muted); }
.formtitle {
  font-size: 0.65rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
  min-width: 4.6rem;
}
form.edit button, form.rowform button, form.sectionform button {
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--on-fill);
  background: var(--accent);
  border: none;
  border-radius: var(--radius-sm);
  padding: 0.32rem 0.75rem;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}
form.edit button:hover,
form.rowform button:hover,
form.sectionform button:hover { background: var(--accent-strong); }
form.edit button:active,
form.rowform button:active,
form.sectionform button:active { transform: translateY(1px); }

/* Update buttons earn their place: absent until the row's fields differ
 * from what the server rendered — app.js toggles .dirty on the row form.
 * display:none (not visibility) so no phantom gap sits beside the
 * right-aligned remove button; the fade-in runs on appearance. */
@keyframes upd-in {
  from { opacity: 0; }
}
form.rowform button.upd { display: none; }
form.rowform.dirty button.upd {
  display: inline-block;
  animation: upd-in var(--dur-fast) var(--ease);
}

/* A manage table's one Save / Discard bar: the same rule at section
 * scale — absent until any bound field differs, then it fades in under
 * the table, hugging the right edge with the row action buttons. */
form.sectionform { display: none; }
form.sectionform.dirty {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 0.6rem;
  margin-top: 0.7rem;
  animation: upd-in var(--dur-fast) var(--ease);
}

/* Action buttons hug the right edge of their row so the two manage tables
 * (watchers, subscriptions) align with each other. */
table.manage th:last-child,
table.manage td:last-child { text-align: right; white-space: nowrap; }

/* Alert-types multiselect: a <details> popover that reads like a select.
 * The list floats over the rows below; the manage table must not clip it. */
table.manage { overflow: visible; }
details.msel { position: relative; display: inline-block; }
/* Closed, the absolute popover must not participate in layout —
 * its 12rem min-width was widening the phone viewport (SC 1.4.10). */
details.msel:not([open]) .msel-list { display: none; }
details.msel summary {
  list-style: none;
  cursor: pointer;
  font-size: 0.78rem;
  border: 1px solid var(--control-border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  padding: 0.28rem 1.5rem 0.28rem 0.5rem;
  white-space: nowrap;
  position: relative;
  transition: border-color var(--dur-fast) var(--ease);
}
details.msel summary::-webkit-details-marker { display: none; }
details.msel summary::after {
  content: "";
  position: absolute;
  right: 0.55rem;
  top: 50%;
  width: 6px;
  height: 6px;
  border-right: 1.5px solid var(--text-muted);
  border-bottom: 1.5px solid var(--text-muted);
  transform: translateY(-70%) rotate(45deg);
}
details.msel[open] summary { border-color: var(--edge); }
@keyframes msel-in {
  from { opacity: 0; transform: translateY(calc(-0.5 * var(--rise))); }
}
details.msel .msel-list {
  position: absolute;
  z-index: 15;
  top: calc(100% + 4px);
  left: 0;
  min-width: 12rem;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.22);
  padding: 0.55rem 0.7rem;
  animation: msel-in var(--dur-fast) var(--ease);
}
.msel-list label {
  font-size: 0.78rem;
  display: flex;
  gap: 0.45rem;
  align-items: center;
  white-space: nowrap;
  cursor: pointer;
}
.msel-list input { accent-color: var(--edge); margin: 0; }

/* The urgent-now checkbox rides in the Delivery cell. */
label.urgent {
  font-size: 0.72rem;
  color: var(--text-muted);
  white-space: nowrap;
  cursor: pointer;
}
label.urgent input { accent-color: var(--edge); vertical-align: -2px; }
/* A watcher's channels nest under it, one per row. */
td.chname { padding-left: 1.9rem; color: var(--text-muted); font-weight: 600; }
tr.chrow:not(:last-child) td { border-bottom: 1px dashed var(--border); }
/* Quiet secondary/destructive actions (row removes).
 * Declared after the accent rules so equal-specificity ties resolve here. */
form button.ghost,
.confirm button.ghost {
  font: inherit;
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  background: none;
  border: 1px solid var(--control-border);
  border-radius: var(--radius-sm);
  padding: 0.3rem 0.6rem;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}
form button.ghost:hover { color: var(--on-fill); background: var(--bad); border-color: var(--bad); }
form button.ghost:active { transform: translateY(1px); }
/* Confirm's Cancel is a quiet neutral — hover must NOT read destructive. */
.confirm button.ghost:hover { color: var(--text); border-color: var(--text-muted); background: none; }
.confirm button.ghost:active { transform: translateY(1px); }
#themetoggle { transition: color var(--dur-fast) var(--ease),
                           border-color var(--dur-fast) var(--ease); }

/* Validation feedback after a settings redirect (?err=…). */
.banner {
  border-radius: var(--radius);
  padding: 0.65rem 1.1rem;
  margin: 0 0 1.1rem;
  font-weight: 600;
  font-size: 0.85rem;
}
.banner.bad { background: var(--bad); color: var(--on-fill); }

/* ── toast (success feedback after a settings redirect ?ok=…) ───────── */

/* Enter: rises from below on the base tier. The from-frame must carry the
 * base rule's translateX(-50%) centering or the toast snaps sideways. */
@keyframes toast-in {
  from { opacity: 0; transform: translate(-50%, var(--rise)); }
}
.toast {
  position: fixed;
  bottom: 1.6rem;
  left: 50%;
  transform: translateX(-50%);
  animation: toast-in var(--dur-base) var(--ease);
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 4px solid var(--ok);
  border-radius: var(--radius);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
  padding: 0.6rem 1.1rem;
  font-size: 0.85rem;
  font-weight: 600;
  z-index: 20;
  max-width: 30rem;
}
/* Exit: a dismissal is attention decaying, not movement — opacity only,
 * on the linger tier. app.js adds the class and removes the node on a
 * timeout (never animationend — hidden documents don't run animations). */
@keyframes toast-out {
  to { opacity: 0; transform: translateX(-50%); }
}
.toast.toast-exit {
  animation: toast-out var(--dur-linger) var(--ease) forwards;
  pointer-events: none;
}

/* The settings footer credit — the app's one self-reference, kept quiet.
 * Links inherit the muted color so the line reads as a whisper, not nav. */
.credit {
  margin: 2rem 0 0.5rem;
  text-align: center;
  font-size: 0.7rem;
  color: var(--text-muted);
}
.credit a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.credit a:hover { color: var(--text); }
/* The home page's freshness line; stale means "older than two polls". */
.freshness code { font-size: inherit; }
.freshness.stale { color: var(--warn-ink); font-size: 0.78rem; }
/* The update check is a form (a click, never a page load) styled as a link. */
.credit form.inline { display: inline; }
.credit button.linkish {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.credit button.linkish:hover { color: var(--text); }

/* ── confirm dialog (destructive removals) ──────────────────────────── */

@keyframes scrim-in {
  from { opacity: 0; }
}
.scrim {
  position: fixed;
  inset: 0;
  z-index: 30;
  background: rgba(10, 13, 19, 0.55);
  animation: scrim-in var(--dur-base) var(--ease);
}
/* Enter rises like the toast; the from-frame carries the base transform. */
@keyframes confirm-in {
  from { opacity: 0; transform: translate(-50%, calc(-50% + var(--rise))); }
}
.confirm {
  position: fixed;
  z-index: 31;
  left: 50%;
  top: 38%;
  transform: translate(-50%, -50%);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.28);
  padding: 1.1rem 1.3rem;
  max-width: 24rem;
  width: calc(100% - 2rem);
  animation: confirm-in var(--dur-base) var(--ease);
}
.confirm p { margin: 0 0 0.9rem; font-size: 0.9rem; line-height: 1.5; }
.confirm-actions { display: flex; gap: 0.5rem; justify-content: flex-end; }
.confirm button.danger {
  font: inherit;
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--on-fill);
  background: var(--bad);
  border: none;
  border-radius: var(--radius-sm);
  padding: 0.32rem 0.75rem;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}
.confirm button.danger:hover { background: #e53e3e; }
.confirm button.danger:active { transform: translateY(1px); }

/* ── student page (C0): stat cards, course strip, four views ────────── */

/* The view switcher: four mini stat cards, each a plain link (?view=…).
 * The active view earns the accent border. */
.stats {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1.1rem;
  margin: 0 0 1.1rem;
}
a.stat {
  background: var(--surface);
  border: 1px solid transparent;
  border-radius: var(--radius);
  box-shadow: var(--card-shadow);
  padding: 0.9rem 1.1rem 0.8rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  color: var(--text);
  min-width: 0;
  transition: border-color var(--dur-fast) var(--ease);
}
a.stat:hover { text-decoration: none; border-color: var(--text-muted); }
a.stat.active { border-color: var(--edge); }
.stat .lbl { font-size: 0.65rem; font-weight: 700; text-transform: uppercase;
             letter-spacing: 0.05em; color: var(--text-muted); }
.stat .big { font-size: 1.35rem; font-weight: 700; line-height: 1.2;
             font-variant-numeric: tabular-nums; }
.stat .big .unit { font-size: 0.8rem; font-weight: 600; color: var(--text-muted); }
.stat .ctx { font-size: 0.74rem; color: var(--text-muted); }
.stat svg.spark { width: 100%; height: 34px; margin-top: 0.4rem; }
.stat .nextline { font-size: 0.76rem; margin-top: 0.15rem; overflow: hidden;
                  text-overflow: ellipsis; white-space: nowrap; }

/* Course summary strip: one compact row per current-term course. */
.striphead { display: flex; align-items: baseline; gap: 0.6rem;
             font-size: 0.95rem; font-weight: 700; }
.termtag { font-size: 0.7rem; font-weight: 600; color: var(--text-muted);
           text-transform: uppercase; letter-spacing: 0.06em; }
table.strip { table-layout: fixed; }
table.strip td { padding: 0.45rem 0.8rem; overflow-wrap: break-word; }
table.strip th:nth-child(2) { width: 6.5rem; }  /* grade       */
table.strip th:nth-child(3) { width: 6rem; }    /* 2-week delta */
table.strip th:nth-child(5) { width: 8rem; }    /* last graded  */
/* Course names are filters, and must read as clickable: underlined (softly
 * at rest, full strength on hover) with a funnel icon after the name. */
table.strip td:first-child a {
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--accent-strong) 40%, transparent);
  text-underline-offset: 3px;
}
table.strip td:first-child a:hover { text-decoration-color: currentColor; }
table.strip svg.filtericon {
  width: 11px; height: 11px; flex: none;
  vertical-align: -1px;
  margin-left: 0.35rem;
  opacity: 0.45;
  transition: opacity var(--dur-fast) var(--ease);
}
table.strip a:hover svg.filtericon { opacity: 1; }

/* The scoped course: an accent bar marks the active filter; the funnel
 * flips to an × (click again to clear). */
table.strip tr.scoped td:first-child { box-shadow: inset 3px 0 0 var(--edge); }
table.strip tr.scoped a { color: var(--text); font-weight: 700; }
table.strip tr.scoped svg.filtericon { opacity: 0.7; }

/* 2-week movement: triangle + points, colored by direction. */
.delta { font-size: 0.78rem; font-weight: 700; display: inline-flex;
         align-items: center; gap: 0.25rem; font-variant-numeric: tabular-nums; }
.delta svg { width: 11px; height: 11px; }
.delta.up { color: var(--ok-ink); }
.delta.down { color: var(--bad-ink); }
.delta.flat { color: var(--text-muted); }

/* Problems / Due-soon listings (Course column present only unscoped). */
table.openitems { table-layout: fixed; }
table.openitems td { overflow-wrap: break-word; }
table.openitems th:last-child { width: 10rem; }                    /* status */
table.openitems th:nth-last-child(2) { width: 7.5rem; }            /* due    */
table.openitems th:nth-child(2):nth-last-child(3) { width: 22%; }  /* course */

/* Recent grades: day-group separator rows instead of a header. */
table.recent { table-layout: fixed; }
table.recent td { overflow-wrap: break-word; }
table.recent col.c-course { width: 22%; }
table.recent col.c-score { width: 6rem; }
table.recent col.c-raw { width: 7rem; }
td.day { font-size: 0.65rem; font-weight: 700; text-transform: uppercase;
         letter-spacing: 0.05em; color: var(--text-muted); padding-top: 1rem; }
td.raw { font-size: 0.8rem; color: var(--text-muted);
         font-variant-numeric: tabular-nums; }

/* Everything: continuation tables (the graded-backlog expander) share the
 * assignment grid via <colgroup>, since they carry no header row. */
table.assignments col.c-type { width: 22%; }
table.assignments col.c-due { width: 7.5rem; }
table.assignments col.c-score { width: 5.5rem; }
table.assignments col.c-status { width: 9rem; }

/* The earned all-clear state (Problems with nothing to show). The bottom
 * margin keeps the card off the due-soon peek below it (same rhythm as
 * .tablecard). */
.allclear { text-align: center; padding: 2.6rem var(--card-pad); margin: 0 0 1.1rem; }
.allclear svg { width: 42px; height: 42px; margin-bottom: 0.7rem; }
.allclear h2 { margin: 0 0 0.3rem; font-size: 1rem; }
.allclear p { margin: 0; }
.allclear p a {
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--link) 50%, transparent);
  text-underline-offset: 3px;
}
.allclear p a:hover { text-decoration-color: currentColor; }

/* No-JS expanders: graded backlog ("Show all N graded"), closed terms, and
 * the All Courses strip. */
details.more summary,
details.closedterm summary,
details.allcourses summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
details.more summary::-webkit-details-marker,
details.closedterm summary::-webkit-details-marker,
details.allcourses summary::-webkit-details-marker { display: none; }
details.more summary svg,
details.closedterm summary svg,
details.allcourses summary svg {
  width: 12px; height: 12px; flex: none;
  transition: transform var(--dur-fast) var(--ease);
}
details.more summary {
  font-size: 0.78rem; font-weight: 600; color: var(--text-muted);
  padding: 0.6rem 0.8rem 0.1rem;
}
details.more summary:hover { color: var(--text); }
details.more[open] summary svg { transform: rotate(180deg); }
/* Open swaps the label: "Show all N" while closed, "Show less" once the
 * rows are out (the toggle sits at the table's end in both states). */
details.more summary .when-open { display: none; }
details.more[open] summary .when-closed { display: none; }
details.more[open] summary .when-open { display: inline; }

/* Overflow "Show all" (graded backlog, history): the overflow rows are a
 * hidden <tbody> of the same table (one header, aligned columns); the
 * <details> after the table is only the toggle. Opening it also restores
 * the border the preview tbody's last row sheds via tr:last-child — the
 * seam between the two tbodies. */
tbody.overflow { display: none; }
.tablecard:has(> details.more[open]) tbody.overflow { display: table-row-group; }
.tablecard:has(> details.more[open]) tbody:not(:last-child) tr:last-child td {
  border-bottom: 1px solid var(--border);
}

/* A closed term is a card whose face is its finals line. */
details.closedterm {
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--card-shadow);
  padding: var(--card-pad);
  margin: 0 0 1.1rem;
}
details.closedterm summary h2 { margin: 0; display: inline; }
details.closedterm summary svg { transform: rotate(-90deg); }
details.closedterm[open] summary svg { transform: rotate(0); }
details.closedterm .closedbody { margin-top: 1rem; }
details.closedterm .closedbody .card { background: var(--bg); box-shadow: none; }

/* The All Courses strip: a card whose face is its title — collapsed by
 * default (the stat cards are the page's front door), held open while a
 * course scope is active. */
details.allcourses {
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--card-shadow);
  padding: var(--card-pad);
  margin: 0 0 1.1rem;
}

details.allcourses summary > svg { transform: rotate(-90deg); }
details.allcourses[open] summary > svg { transform: rotate(0); }
details.allcourses table { margin: 0.6rem 0 0; box-shadow: none;
                           border-radius: 0; overflow: visible; }
/* Open/close eases instead of snapping (base tier). ::details-content +
 * interpolate-size are progressive enhancement — browsers without them
 * keep the instant toggle. */
details.allcourses { interpolate-size: allow-keywords; }
details.allcourses::details-content {
  opacity: 0;
  block-size: 0;
  overflow-y: clip;
  transition: content-visibility var(--dur-base) allow-discrete,
              opacity var(--dur-base) var(--ease),
              block-size var(--dur-base) var(--ease);
}
details.allcourses[open]::details-content { opacity: 1; block-size: auto; }
/* The active-filter tag on the summary: legible while collapsed, and the
 * clear control (funnel · course · small ×, the scoped row at tag scale).
 * Plain inline (not flex) so its TEXT baseline lines up with the
 * heading's — a flex tag would hand its baseline to the icon and ride
 * high. */
a.striptag {
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--link);
  white-space: nowrap;
  margin-left: 0.6rem;   /* doubles the heading's 0.6rem flex gap */
}
.striptag svg.filtericon { width: 11px; height: 11px;
                           vertical-align: -1.5px; margin-right: 0.3rem; }
.striptag svg.filtericon:last-child {
  margin-right: 0;
  margin-left: 0.25rem;
  opacity: 0.7;
  transition: opacity var(--dur-fast) var(--ease);
}
a.striptag:hover svg.filtericon:last-child { opacity: 1; }

h2.termhead { font-size: 0.8rem; color: var(--text-muted);
              text-transform: uppercase; letter-spacing: 0.06em;
              margin: 1.2rem 0 0.6rem; }

/* ── Phase C: status signal ─────────────────────────────────────────── */

/* Styled tooltips: any hover reveal renders as a design-system bubble
 * (surface, radius, shadow tokens) via data-tip — never the browser's
 * native title speck. CSS-only; visibility keeps the hidden bubble out of
 * the accessibility tree. Card tables must not clip the bubble (their
 * radius is 0, so overflow:hidden buys them nothing anyway). */
.card table { overflow: visible; }
[data-tip] { position: relative; }
span.tip { cursor: help; }
[data-tip]::after {
  content: attr(data-tip);
  position: absolute;
  bottom: calc(100% + 7px);
  left: 50%;
  transform: translateX(-50%);
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
  padding: 0.28rem 0.6rem;
  font-size: 0.72rem;
  font-weight: 400;
  line-height: 1.5;    /* the host's line-height must not leak in
                          (the icon-only theme button uses 0) */
  width: max-content;
  max-width: 18rem;
  white-space: normal;
  text-align: left;
  z-index: 25;
  /* display:none, not visibility — a merely-invisible bubble still occupies
   * SCROLL layout, and the right-edge ones were widening the phone viewport
   * to 447px (SC 1.4.10). @starting-style keeps the fade-in; hiding was
   * always immediate anyway. */
  display: none;
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease),
              display var(--dur-fast) allow-discrete;
}
/* Reveal after a beat (slow tier) — a tooltip that fires instantly on
 * every mouse-cross is noise. Hiding stays immediate. Keyboard focus
 * reveals the same bubble hover does (SC 1.4.13). */
[data-tip]:hover::after,
[data-tip]:focus-visible::after {
  display: block;
  opacity: 1;
  transition-delay: var(--dur-slow);
}
@starting-style {
  [data-tip]:hover::after,
  [data-tip]:focus-visible::after { opacity: 0; }
}
/* Placement variants: below (nav row — above would leave the viewport),
 * end-aligned (right-edge controls), start-aligned (first-column links). */
.tip-b::after { bottom: auto; top: calc(100% + 7px); }
.tip-e::after { left: auto; right: 0; transform: none; }
.tip-s::after { left: 0; transform: none; }

/* Row tint + leading icon per status — the escalation ladder reads by color
 * before it's read by text: due soon a light caution, ungraded past due a
 * stronger one, missing red. Same tokens as the badges. */
svg.rowicon { width: 13px; height: 13px; vertical-align: -2px;
              margin-right: 0.4rem; flex: none; }
tr.st-missing > td { background: color-mix(in srgb, var(--bad) 8%, transparent); }
tr.st-late    > td { background: color-mix(in srgb, var(--warn) 10%, transparent); }
tr.st-due     > td { background: color-mix(in srgb, var(--warn) 4%, transparent); }

/* Source mark: an assignment row that came from Canvas, not the gradebook. */
.src { display: inline-block; margin-left: 0.45rem; padding: 0 0.4rem;
       border: 1px solid var(--border); border-radius: 999px;
       font-size: 0.68rem; line-height: 1.45; letter-spacing: 0.02em;
       color: var(--text-muted); vertical-align: 1px; }
.src.twin { border-color: var(--warn); color: var(--warn-ink); }

/* A graded score below the household cutoff (Settings → Display). */
.low { color: var(--bad-ink); font-weight: 700; }

/* Badge click-through (?status=…#hit): matching rows pulse accent and decay
 * to their normal tint; the first match anchors the scroll. */
@keyframes hit-in {
  from { background: color-mix(in srgb, var(--accent) 28%, transparent); }
}
tr.hit > td { animation: hit-in calc(2 * var(--dur-linger)) var(--ease); }
#hit { scroll-margin-top: 1rem; }

/* ── alerts + history: type chips, paging ───────────────────────────── */

.chips { display: flex; flex-wrap: wrap; gap: 0.45rem; margin: 0.55rem 0 0.8rem; }

/* History filter bar: two labeled chip rows (Class / Change). The label
 * shares the strip/settings uppercase-tag treatment; the chips reuse the
 * alerts styling above. */
.histfilters { margin: 0.2rem 0 1.1rem; }
.filterrow { display: flex; align-items: baseline; gap: 0.7rem; flex-wrap: wrap;
             margin: 0.55rem 0; }
.filterrow .chips { margin: 0; }
.filterlabel {
  font-size: 0.65rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--text-muted);
  flex: none; min-width: 3.4rem; padding-top: 0.2rem;
}
a.chip {
  font-size: 0.74rem;
  font-weight: 600;
  color: var(--text-muted);
  border: 1px solid var(--control-border);
  border-radius: 999px;
  padding: 0.2rem 0.7rem;
  white-space: nowrap;
  transition: color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease);
}
a.chip:hover { color: var(--text); border-color: var(--text-muted);
               text-decoration: none; }
a.chip.active { color: var(--on-fill); background: var(--accent);
                border-color: var(--accent); }
a.chip b { font-weight: 700; opacity: 0.75; margin-left: 0.15rem; }

/* Alerts paging: "Showing 51–100 of 1,305" above a centered
 * Previous | 1 … 5 6 7 … 27 | Next row, everything on one baseline. The
 * active number fills like the active chip; a dead end is a muted span. */
.pager-range {
  margin: 0.9rem 0 0.35rem; text-align: center;
  font-size: 0.78rem; color: var(--text-muted);
}
.pager {
  display: flex; justify-content: center; align-items: center;
  flex-wrap: nowrap; gap: 0.6rem; font-size: 0.8rem; font-weight: 600;
  /* It's a <nav> for assistive tech, so the top bar's element rule lands
   * on it too — undo that chrome (and its wrap) here. */
  background: none; box-shadow: none; border-radius: 0;
  padding: 0; margin: 0;
}
.pager ol {
  list-style: none; margin: 0; padding: 0;
  display: flex; align-items: center; gap: 0.2rem;
}
.pager a, .pager .gap, .pager [aria-disabled] {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.9rem; min-height: 1.9rem; padding: 0 0.45rem;
  border-radius: var(--radius-sm); color: var(--text-muted);
}
.pager a:hover { color: var(--text); background: var(--surface-2);
                 text-decoration: none; }
.pager a[aria-current='page'] { color: var(--on-fill); background: var(--accent); }
.pager .gap { min-width: 1.2rem; }
.pager [aria-disabled] { opacity: 0.45; }
.pager .prev, .pager .next { gap: 0.3rem; }

/* Respect reduced-motion: collapse every duration so states land instantly;
 * app.js guards its scripted (inline-style) motion separately. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* ── responsive ─────────────────────────────────────────────────────── */

/* Student cards go single-column well before they'd crush their tables. */
@media (max-width: 56rem) {
  .cards { flex-direction: column; }
  .card { min-width: 0; }
}

/* Narrow nav: labels give way to icons (full labeled nav returns on wide),
 * and the student name links collapse into the .smenu icon menu. */
@media (max-width: 48rem) {
  .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.7rem; }
  nav { gap: 0.9rem; padding: 0.7rem 1rem; }
  nav a svg { display: inline-block; }
  nav a .lbl { display: none; }
  nav .brand { font-size: 0.75rem; }
  nav .navstudents { display: none; }
  nav details.smenu { display: inline-block; }
}

/* Phone width: tables stack. The header row hides and each row becomes a
 * block; labeled cells re-grow their column name from data-label. Cells
 * without a label (the row's identity — assignment/course name) read as the
 * block's title line. */
@media (max-width: 40rem) {
  /* Phone width: Previous/Next shrink to their arrows and the slots
   * tighten, so seven numbers plus both ends still sit on one row. */
  .pager .lbl { display: none; }
  .pager { gap: 0.35rem; }
  .pager a, .pager .gap, .pager [aria-disabled] {
    min-width: 1.6rem; padding: 0 0.3rem;
  }
  .pager .gap { min-width: 0.9rem; }
  table, tbody, tr, td { display: block; width: 100%; }
  tr.head { display: none; }
  table { table-layout: auto; }
  tr { padding: 0.5rem 0; border-bottom: 1px solid var(--border); }
  tr:last-child { border-bottom: none; }
  td { border: none; padding: 0.12rem 0; }
  td[data-label] { display: flex; justify-content: space-between;
                   align-items: baseline; gap: 1rem; }
  td[data-label]::before {
    content: attr(data-label);
    font-size: 0.65rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
    flex: none;
  }
  td:not([data-label]) { font-weight: 600; }
  td.num { text-align: right; }
  /* Stacked mode re-lays tbodies as blocks, and the row border moves from
   * td to tr — re-state the history overflow reveal + seam in those terms. */
  .tablecard:has(> details.more[open]) tbody.overflow { display: block; }
  .tablecard:has(> details.more[open]) tbody:not(:last-child) tr:last-child {
    border-bottom: 1px solid var(--border);
  }
  table.assignments th, table.courses th,
  table.strip th, table.openitems th { width: auto; }
  td.day { padding-top: 0.9rem; }
  /* Stacked cells lose their side padding, so the inset accent bar needs
   * the text pushed off it. */
  table.strip tr.scoped td:first-child { padding-left: 0.6rem; }
}
