/* ═══════════════════════════════════════════════════════════════════════════
   Roots Academy — Light / Dark theme
   ═══════════════════════════════════════════════════════════════════════════

   HOW IT WORKS
   The portal's page CSS is already token-driven: student_portal.css and
   assignments.css each declare the same design-system palette (--paper,
   --surface, --ink, --line, --brand …), and the per-page sheets (fees.css,
   attendance.css, appointments.css, learning-process.css …) consume those
   tokens instead of re-declaring colours. Analytics re-declares the same
   palette under `.ra-db` with a --c-* prefix, and the academic pages under
   `body.acad` with an --acad-* prefix.

   So dark mode is mostly ONE job: re-point those tokens. Everything below the
   "TOKENS" section is clean-up for the places that hardcoded a colour instead
   of using a token (notifications panel, Bootstrap, a few page sheets).

   ACTIVATION
   `data-theme="dark"` on <html>, set before first paint by the inline snippet
   in partials/theme_boot.html and thereafter by static/js/theme.js.

   SPECIFICITY
   This file is included from student_portal_head.html, which several templates
   include BEFORE their own page CSS — so load order cannot be relied on. Every
   rule here is therefore prefixed with `html[data-theme="dark"]` (0,1,1),
   which outranks the `:root` (0,1,0) and `body.acad` (0,1,1 + later-in-file)
   declarations it replaces. Keep that prefix on anything added here.

   The toggle button styles at the top are NOT prefixed — the control has to
   render in both themes.
   ══════════════════════════════════════════════════════════════════════════ */


/* ─────────────────────────────────────────────────────────────────────────
   1. The toggle control
   Sized to match the notification bell it sits beside (42px circle, see
   notifications_panel.css .rn-bell) so the topbar reads as one row of
   controls. Also used standalone on the login page.

   The selector is `button.theme-toggle[data-roots-theme-toggle]` (0,2,1) and
   not the bare class, because assignments.css — loaded on nearly every student
   page — resets `body.assignments-page button { border: none; background:
   none }` at (0,1,1). A plain `.theme-toggle` (0,1,0) loses to it and the
   control renders as a naked icon beside a pill-shaped bell. The bell itself
   dodges this the same way, via .topbar-notif-wrap .notif-btn at (0,2,0).
   ───────────────────────────────────────────────────────────────────────── */
button.theme-toggle[data-roots-theme-toggle] {
  /* Deliberately no `position` — the bell needs relative for its badge, this
     does not, and setting it here (at 0,2,1) would outrank a page that wants
     the toggle taken out of flow, e.g. .theme-switch on the login page. */
  width: 42px;
  height: 42px;
  flex-shrink: 0;
  border-radius: 50%;
  border: 1px solid #E9E3D9;
  background: #fff;
  color: #5C5568;
  cursor: pointer;
  padding: 0;
  display: grid;
  place-items: center;
  transition: color .12s, border-color .12s, background .12s;
}
button.theme-toggle[data-roots-theme-toggle]:hover {
  color: #6E5E8A;
  border-color: #6E5E8A;
}
button.theme-toggle[data-roots-theme-toggle] svg {
  width: 19px;
  height: 19px;
  pointer-events: none;
  /* No `display` here. This selector is (0,2,2) and the icon swap below
     is (0,2,0), so a `display: block` on this rule outranked
     `.ic-sun { display: none }` and the button showed a moon AND a sun at
     once -- on the shared sign-in page and every student topbar. The
     button is `display: grid`, which blockifies its children anyway. */
}
button.theme-toggle[data-roots-theme-toggle]:focus-visible {
  outline: 2px solid #6E5E8A;
  outline-offset: 2px;
}

/* Show the mode you would switch TO: a moon while light, a sun while dark.
   Keyed off the hook attribute rather than .theme-toggle, because the document
   viewers mount their toggle with their OWN toolbar button class (.pv-icobtn /
   .cv-icobtn / .ta-ico) so it matches the compact toolbar it sits in — only the
   icon-swapping behaviour is shared, not the pill styling below. */
[data-roots-theme-toggle] .ic-sun { display: none; }
[data-roots-theme-toggle] .ic-moon { display: block; }
html[data-theme="dark"] [data-roots-theme-toggle] .ic-sun { display: block; }
html[data-theme="dark"] [data-roots-theme-toggle] .ic-moon { display: none; }

html[data-theme="dark"] button.theme-toggle[data-roots-theme-toggle] {
  background: var(--surface, #2b2f35);
  border-color: var(--line, #3d434b);
  color: var(--ink-2, #b2b9c4);
}
html[data-theme="dark"] button.theme-toggle[data-roots-theme-toggle]:hover {
  color: var(--brand-2, #b7a4e6);
  border-color: var(--brand-2, #b7a4e6);
}

/* The topbar is a tight flex row on phones — the toggle shrinks with the
   hamburger and bell rather than pushing the page title out. */
@media (max-width: 680px) {
  button.theme-toggle[data-roots-theme-toggle] { width: 38px; height: 38px; }
  button.theme-toggle[data-roots-theme-toggle] svg { width: 17px; height: 17px; }
}

@media (prefers-reduced-motion: reduce) {
  button.theme-toggle[data-roots-theme-toggle] { transition: none; }
}


/* ─────────────────────────────────────────────────────────────────────────
   2. TOKENS — the design-system palette, re-pointed for dark
   Page background is a neutral dark gray (#23262b) with cards one step
   lighter, so the card/page separation the light theme gets from a white
   card on cream paper survives.
   ───────────────────────────────────────────────────────────────────────── */
html[data-theme="dark"] {
  color-scheme: dark;

  --paper: #23262b;
  --surface: #2b2f35;
  --surface-2: #33373e;
  --ink: #e7eaee;
  --ink-2: #b2b9c4;
  /* --ink-3 is the muted/caption tone and is used far more widely than a
     "hint" colour usually is — KPI sublabels, empty states, table captions,
     card subtitles. A straight inversion of the light value landed at 4.3:1 on
     a card, just under AA, which is what made a whole class of small text look
     washed out. Lifted to 5.8:1 on --surface / 6.6:1 on --paper. */
  --ink-3: #a3abb7;
  --ink-soft: #b2b9c4;         /* used by the install modal in student_portal.css */
  --line: #3d434b;
  --line-2: #343941;

  /* --brand is used as TEXT about twice as often as it is used as a fill
     (117 vs 61 declarations across the portal's sheets), so it is tuned for
     text: #ad96e0 measures 5.3:1 on --surface and 4.6:1 on --surface-2. The
     earlier mid-tone (#9a83d4) tried to serve both roles and ended up at
     3.7-4.3:1 as text — under AA, which is what made small purple labels look
     washed out on Schedule, Study Materials and the flowchart.
     Every FILL that carries white text is redirected to --brand-fill instead;
     the list is in section 6 and is verified by the contrast audit. */
  --brand: #ad96e0;
  --brand-2: #b7a4e6;
  --brand-soft: #3a3155;

  /* The *-fill tokens exist because the semantic tokens flip role between
     themes. In light, --brand/--danger/--warn are dark enough to carry white
     text AND to read as text on white. Inverted for dark they are light, which
     is right for text on a card but leaves white labels floating on a pale
     fill (the Submit button, the "on" filter chips). Anything that fills a
     surface and puts #fff on top uses these instead — see section 6. */
  --brand-fill: #6d59a8;       /* white text 5.8:1 — measured, not estimated */
  --danger-fill: #b8453a;      /* white text 5.3:1 */
  --warn-fill: #8a6906;        /* white text 4.9:1 (yellow needs to go deep) */

  --success: #9a83d4;          /* the portal styles "success" purple, not green */
  --success-soft: #3a3155;
  --warn: #e8b45f;
  --warn-soft: #443619;
  --danger: #f0897e;
  --danger-soft: #4a2a26;
  --indigo: #8f92ff;
  --indigo-soft: #2e3060;
  --amber: #f0b45c;
  --amber-soft: #45351a;
  --slate: #9aa5b5;
  --slate-soft: #333a44;

  /* Shadows carry no weight over a dark page unless they are near-black. */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .30), 0 1px 3px rgba(0, 0, 0, .24);
  --shadow-md: 0 4px 14px rgba(0, 0, 0, .38), 0 2px 6px rgba(0, 0, 0, .26);
  --shadow-lg: 0 18px 40px rgba(0, 0, 0, .50), 0 6px 14px rgba(0, 0, 0, .32);
}

/* Analytics dashboard — same palette, --c-* prefix, scoped to .ra-db. */
html[data-theme="dark"] .ra-db {
  --c-brand: #9a83d4;
  --c-brand2: #b7a4e6;
  --c-bsoft: #3a3155;
  --c-surf: #2b2f35;
  --c-surf2: #33373e;
  --c-paper: #23262b;
  --c-ink: #e7eaee;
  --c-ink2: #b2b9c4;
  --c-ink3: #a3abb7;
  --c-line: #3d434b;
  --c-line2: #343941;
  --c-ok: #9a83d4;
  --c-oksoft: #3a3155;
  --c-warn: #e8b45f;
  --c-wsoft: #443619;
  --c-err: #f0897e;
  --c-esoft: #4a2a26;
  --c-ind: #8f92ff;
  --c-isoft: #2e3060;
  --c-amb: #f0b45c;
  --c-asoft: #45351a;
  --sh-sm: 0 1px 2px rgba(0, 0, 0, .30), 0 1px 3px rgba(0, 0, 0, .24);
  --sh-md: 0 4px 14px rgba(0, 0, 0, .38), 0 2px 6px rgba(0, 0, 0, .26);
  --sh-lg: 0 18px 40px rgba(0, 0, 0, .50), 0 6px 14px rgba(0, 0, 0, .32);
}

/* Academic pages (announcements) — --acad-* prefix, scoped to body.acad. */
html[data-theme="dark"] body.acad {
  --acad-paper: #23262b;
  --acad-paper-2: #1e2126;
  --acad-surface: #2b2f35;
  --acad-surface-2: #33373e;
  --acad-ink: #e7eaee;
  --acad-ink-2: #b2b9c4;
  --acad-ink-3: #a3abb7;
  --acad-line: #3d434b;
  --acad-line-2: #343941;
  --acad-brand: #9a83d4;
  --acad-brand-2: #b7a4e6;
  --acad-brand-dark: #6d59a8;
  --acad-brand-soft: #3a3155;
  --acad-success: #4cae7e;
  --acad-success-soft: #23402f;
  --acad-warn: #e8b45f;
  --acad-warn-soft: #443619;
  --acad-danger: #f0897e;
  --acad-danger-soft: #4a2a26;
  --acad-indigo: #8f92ff;
  --acad-indigo-soft: #2e3060;
  --acad-amber: #f0b45c;
  --acad-amber-soft: #45351a;
  --acad-shadow-sm: 0 1px 2px rgba(0, 0, 0, .30), 0 1px 3px rgba(0, 0, 0, .24);
  --acad-shadow-md: 0 4px 14px rgba(0, 0, 0, .38);
  --acad-shadow-lg: 0 18px 40px rgba(0, 0, 0, .50);
}

/* Per-page accent tokens. Only the *-soft tints need re-pointing — a pale
   wash designed to sit on white turns into a glare panel on a dark card. The
   saturated partners stay put; they read fine either way. */
/* NOTE THE SELECTOR: `html[data-theme="dark"] body`, not the root block above.
   These accents are declared by their pages on `body.fees-page`,
   `body.attendance-page`, `body.engagement-page` … — i.e. on BODY. A custom
   property set on body beats one inherited from html for everything inside
   body, so declaring them in the root block silently lost every time: the
   attendance calendar kept its pale mint day cells in dark mode. Adding `body`
   to the selector (0,1,2) outranks `body.attendance-page` (0,1,1) and fixes it. */
html[data-theme="dark"] body {
  --fees-paid-soft: #1e3d2b;       /* fees.css */
  --att-present-soft: #1e3d2b;     /* attendance.css */
  --att-present: #7fd3a3;
  --eng-excellent-soft: #1e3d2b;   /* engagement.css */
  --eng-excellent: #7fd3a3;
  --eng-needs-soft: #45311c;
  --eng-needs: #f0a56b;
  --video-soft: #4a2038;           /* learning-process.css */
  --video: #f090bd;
  --fb-good-soft: #1e3d2b;         /* feedback.css */
  --fb-good: #7fd3a3;
  --fb-warn-soft: #443619;

  /* quizzes.css reaches for `var(--acad-surface, #fff)` on the topic-quiz
     pages, which carry no body.acad — so the block below never applies and the
     #fff fallback wins. Defining the handful it falls back on at root level
     kills the fallback everywhere. */
  --acad-surface: #2b2f35;
  --acad-surface-2: #33373e;
  --acad-line: #3d434b;
  --acad-ink: #e7eaee;
  --acad-ink-2: #b2b9c4;
  --brand-50: #33373e;             /* study-materials.css hover fallback */
}


/* ─────────────────────────────────────────────────────────────────────────
   3. Page canvas
   ───────────────────────────────────────────────────────────────────────── */
html[data-theme="dark"] body {
  background: var(--paper);
  color: var(--ink);
}

/* The light theme's two purple radial page-glows are removed outright in
   section 11 (no gradients in dark mode), not dimmed here. */

html[data-theme="dark"] .portal-topbar {
  background: rgba(35, 38, 43, .84);
}

/* Scrollbars — hardcoded warm grays in student_portal.css / assignments.css. */
html[data-theme="dark"] .portal-main {
  scrollbar-color: #4c525b var(--paper);
}
html[data-theme="dark"] .portal-main::-webkit-scrollbar-track { background: var(--paper); }
html[data-theme="dark"] .portal-main::-webkit-scrollbar-thumb {
  background: #4c525b;
  border-color: var(--paper);
}
html[data-theme="dark"] .portal-main::-webkit-scrollbar-thumb:hover { background: #5c636e; }
html[data-theme="dark"] body.assignments-page ::-webkit-scrollbar-thumb {
  background: #4c525b;
  border-color: var(--paper);
}
html[data-theme="dark"] body.assignments-page ::-webkit-scrollbar-thumb:hover { background: #5c636e; }

/* The feedback gate card sets background:#fff inline, so it needs !important. */
html[data-theme="dark"] #feedback-gate-card {
  background: var(--surface) !important;
  color: var(--ink);
}


/* ─────────────────────────────────────────────────────────────────────────
   4. Notifications panel
   notifications_panel.css hardcodes the cream/white palette throughout, so
   every surface in it has to be named here. Several of its rules already use
   !important (they fight Bootstrap's dropdown), which is why some of these do.
   ───────────────────────────────────────────────────────────────────────── */
html[data-theme="dark"] .roots-notif-wrap .rn-bell,
html[data-theme="dark"] .topbar-notif-wrap .notif-btn {
  background: var(--surface);
  border-color: var(--line);
  color: var(--ink-2);
}
html[data-theme="dark"] .roots-notif-wrap .rn-bell:hover,
html[data-theme="dark"] .topbar-notif-wrap .notif-btn:hover {
  color: var(--brand-2);
  border-color: var(--brand-2);
}
html[data-theme="dark"] .roots-notif-wrap .rn-badge,
html[data-theme="dark"] .topbar-notif-wrap .rn-badge,
html[data-theme="dark"] .topbar-notif-wrap #notification-count,
html[data-theme="dark"] .topbar-notif-wrap .badge-danger { border-color: var(--paper); }

html[data-theme="dark"] .rn-panel,
html[data-theme="dark"] .notif-dropdown,
html[data-theme="dark"] .topbar-notif-wrap .dropdown-menu.dropdown-menu-lg {
  background: var(--surface);
  border-color: var(--line) !important;
  box-shadow: 0 4px 10px rgba(0, 0, 0, .34), 0 18px 50px rgba(0, 0, 0, .52) !important;
}
html[data-theme="dark"] .rn-head h2 { color: var(--ink); }
html[data-theme="dark"] .rn-head .rn-count { background: var(--brand-soft); color: var(--brand-2); }
html[data-theme="dark"] .rn-mark-read { color: var(--brand-2); }
html[data-theme="dark"] .rn-mark-read:hover { background: var(--brand-soft); }
html[data-theme="dark"] .rn-tabs { border-bottom-color: var(--line); }
html[data-theme="dark"] .rn-tab { color: var(--ink-3); }
html[data-theme="dark"] .rn-tab:hover { color: var(--ink); }
html[data-theme="dark"] .rn-tab[aria-selected="true"] { background: var(--brand-fill); color: #fff; }
html[data-theme="dark"] .rn-list::-webkit-scrollbar-thumb {
  background: #4c525b;
  border-color: var(--surface);
}
html[data-theme="dark"] .rn-group { color: var(--ink-3); }
html[data-theme="dark"] .rn-item { border-top-color: var(--line-2); }
html[data-theme="dark"] .rn-item:hover { background: var(--surface-2); }
html[data-theme="dark"] .rn-line1 { color: var(--ink-2); }
html[data-theme="dark"] .rn-line1 b,
html[data-theme="dark"] .rn-line1 strong { color: var(--ink); }
html[data-theme="dark"] .rn-item.read .rn-line1 b,
html[data-theme="dark"] .rn-item.read .rn-line1 strong { color: var(--ink-2); }
html[data-theme="dark"] .rn-meta { color: var(--ink-3); }
html[data-theme="dark"] .rn-chip {
  background: var(--surface-2);
  border-color: var(--line);
  color: var(--ink-2);
}
html[data-theme="dark"] .rn-unread-dot { background: var(--brand-2); }
html[data-theme="dark"] .rn-i-assign { background: #3a3155; color: #c3b1ee; }
html[data-theme="dark"] .rn-i-test { background: #443619; color: #edc274; }
html[data-theme="dark"] .rn-i-submit { background: #1e3d2b; color: #79cfa1; }
html[data-theme="dark"] .rn-i-system { background: #24374f; color: #8db4e2; }
html[data-theme="dark"] .rn-i-alert { background: #4a2a26; color: #f0897e; }
html[data-theme="dark"] .rn-mini {
  background: var(--surface-2);
  border-color: var(--line);
  color: var(--ink-2);
}
html[data-theme="dark"] .rn-mini:hover { border-color: var(--brand-2); color: var(--brand-2); }
html[data-theme="dark"] .rn-mini.primary {
  background: var(--brand-fill);
  border-color: var(--brand-fill);
  color: #fff;
}
html[data-theme="dark"] .rn-mini.primary:hover { background: var(--brand); border-color: var(--brand); }
html[data-theme="dark"] .rn-empty,
html[data-theme="dark"] .rn-state { color: var(--ink-3); }
html[data-theme="dark"] .rn-empty .rn-empty-ic { background: var(--surface-2); color: var(--brand-2); }
html[data-theme="dark"] .rn-empty p b { color: var(--ink); }
html[data-theme="dark"] .rn-foot {
  background: var(--surface);
  border-top-color: var(--line);
}
html[data-theme="dark"] .rn-foot a { color: var(--brand-2); }
html[data-theme="dark"] .rn-foot a:hover { background: var(--brand-soft); }
html[data-theme="dark"] .rn-foot a + a { border-left-color: var(--line); }
html[data-theme="dark"] .rn-push-optin {
  background: var(--surface-2);
  border-bottom-color: var(--line);
}
html[data-theme="dark"] .rn-push-optin-text strong { color: var(--ink); }
html[data-theme="dark"] .rn-push-optin-text span { color: var(--ink-2); }
html[data-theme="dark"] .rn-push-later {
  border-color: var(--line);
  color: var(--ink-2);
}
html[data-theme="dark"] .rn-push-later:hover { background: var(--surface-2); color: var(--ink); }
html[data-theme="dark"] .rn-push-enable {
  background: var(--brand-fill);
  border-color: var(--brand-fill);
}
html[data-theme="dark"] .rn-push-enable:hover {
  background: var(--brand);
  border-color: var(--brand);
}


/* ─────────────────────────────────────────────────────────────────────────
   5. Bootstrap 4 + legacy admin theme (app.min.css)
   Queries, Marks, Announcements and the OTP/confirm modals across the portal
   render straight Bootstrap, which ships a white-page palette.
   ───────────────────────────────────────────────────────────────────────── */
/* This project's bootstrap.min.css is a customised admin theme, not stock
   Bootstrap. It paints globals stock Bootstrap leaves alone:
     h1-h6      { color: #495057 }   ← every heading in the portal, dim on dark
     body       { border-top: 2px solid #eff2f7 }  ← a bright hairline up top
     a          { color: #174a83 }
   Those three have to be answered here or dark mode looks broken on pages that
   never hardcoded a colour of their own. */
html[data-theme="dark"] h1,
html[data-theme="dark"] h2,
html[data-theme="dark"] h3,
html[data-theme="dark"] h4,
html[data-theme="dark"] h5,
html[data-theme="dark"] h6,
html[data-theme="dark"] .h1,
html[data-theme="dark"] .h2,
html[data-theme="dark"] .h3,
html[data-theme="dark"] .h4,
html[data-theme="dark"] .h5,
html[data-theme="dark"] .h6 { color: var(--ink); }

/* …except the ones sitting on a filled brand panel, which stay white. The rule
   above ties with .upgrade h4 on specificity and would otherwise win on order. */
html[data-theme="dark"] .upgrade h4 { color: #fff; }

html[data-theme="dark"] body { border-top-color: var(--line); }

/* Only unclassed links: `a.btn-primary` and friends carry their own colour at a
   lower specificity than this selector and would be repainted by it. */
html[data-theme="dark"] a:not([class]) { color: var(--brand-2); }

html[data-theme="dark"] .card,
html[data-theme="dark"] .modal-content,
html[data-theme="dark"] .popover,
html[data-theme="dark"] .list-group-item,
html[data-theme="dark"] .dropdown-menu,
html[data-theme="dark"] .accordion > .card {
  background-color: var(--surface);
  border-color: var(--line);
  color: var(--ink);
}
html[data-theme="dark"] .card-header,
html[data-theme="dark"] .card-footer,
html[data-theme="dark"] .modal-header,
html[data-theme="dark"] .modal-footer {
  background-color: var(--surface-2);
  border-color: var(--line);
  color: var(--ink);
}
html[data-theme="dark"] .modal-title,
html[data-theme="dark"] .card-title { color: var(--ink); }

html[data-theme="dark"] .dropdown-item { color: var(--ink); }
html[data-theme="dark"] .dropdown-item:hover,
html[data-theme="dark"] .dropdown-item:focus {
  background-color: var(--surface-2);
  color: var(--ink);
}
html[data-theme="dark"] .dropdown-divider { border-top-color: var(--line); }

html[data-theme="dark"] .table,
html[data-theme="dark"] .table td,
html[data-theme="dark"] .table th {
  color: var(--ink);
  border-color: var(--line);
}
html[data-theme="dark"] .table thead th {
  color: var(--ink-2);
  border-bottom-color: var(--line);
}
html[data-theme="dark"] .table-striped tbody tr:nth-of-type(odd) {
  background-color: rgba(255, 255, 255, .03);
}
html[data-theme="dark"] .table-hover tbody tr:hover {
  background-color: rgba(255, 255, 255, .06);
  color: var(--ink);
}
html[data-theme="dark"] .table-bordered,
html[data-theme="dark"] .table-bordered td,
html[data-theme="dark"] .table-bordered th { border-color: var(--line); }

html[data-theme="dark"] .form-control,
html[data-theme="dark"] .custom-select,
html[data-theme="dark"] textarea,
html[data-theme="dark"] input[type="text"],
html[data-theme="dark"] input[type="email"],
html[data-theme="dark"] input[type="number"],
html[data-theme="dark"] input[type="search"],
html[data-theme="dark"] input[type="tel"],
html[data-theme="dark"] input[type="url"],
html[data-theme="dark"] input[type="date"],
html[data-theme="dark"] input[type="time"],
html[data-theme="dark"] input[type="password"],
html[data-theme="dark"] select {
  background-color: var(--surface-2);
  border-color: var(--line);
  color: var(--ink);
}
html[data-theme="dark"] .form-control:focus,
html[data-theme="dark"] .custom-select:focus {
  background-color: var(--surface-2);
  border-color: var(--brand);
  color: var(--ink);
  box-shadow: 0 0 0 3px rgba(154, 131, 212, .22);
}
html[data-theme="dark"] .form-control::placeholder,
html[data-theme="dark"] input::placeholder,
html[data-theme="dark"] textarea::placeholder { color: var(--ink-3); }
html[data-theme="dark"] .form-control:disabled,
html[data-theme="dark"] .form-control[readonly] {
  background-color: #262a2f;
  color: var(--ink-3);
}
html[data-theme="dark"] .input-group-text {
  background-color: var(--surface-2);
  border-color: var(--line);
  color: var(--ink-2);
}
html[data-theme="dark"] label,
html[data-theme="dark"] .col-form-label { color: var(--ink); }

html[data-theme="dark"] .nav-tabs { border-bottom-color: var(--line); }
html[data-theme="dark"] .nav-tabs .nav-link { color: var(--ink-2); }
html[data-theme="dark"] .nav-tabs .nav-link:hover { border-color: var(--line); }
html[data-theme="dark"] .nav-tabs .nav-link.active {
  background-color: var(--surface);
  border-color: var(--line) var(--line) var(--surface);
  color: var(--ink);
}

html[data-theme="dark"] .page-link {
  background-color: var(--surface);
  border-color: var(--line);
  color: var(--brand-2);
}
html[data-theme="dark"] .page-item.disabled .page-link {
  background-color: var(--surface-2);
  border-color: var(--line);
  color: var(--ink-3);
}
html[data-theme="dark"] .page-item.active .page-link {
  background-color: var(--brand-fill);
  border-color: var(--brand-fill);
  color: #fff;
}

html[data-theme="dark"] .close,
html[data-theme="dark"] .btn-close { color: var(--ink); text-shadow: none; opacity: .7; }
html[data-theme="dark"] .close:hover { color: var(--ink); opacity: 1; }

html[data-theme="dark"] .alert-success { background-color: #1e3d2b; border-color: #2c5a3f; color: #a3e0bf; }
html[data-theme="dark"] .alert-danger  { background-color: #4a2a26; border-color: #6d3b35; color: #f5b0a8; }
html[data-theme="dark"] .alert-warning { background-color: #443619; border-color: #63512a; color: #f0cd8e; }
html[data-theme="dark"] .alert-info,
html[data-theme="dark"] .alert-primary { background-color: #2e3060; border-color: #43468a; color: #b6b8ff; }
html[data-theme="dark"] .alert-secondary,
html[data-theme="dark"] .alert-light   { background-color: var(--surface-2); border-color: var(--line); color: var(--ink); }

html[data-theme="dark"] .badge-light { background-color: var(--surface-2); color: var(--ink); }
html[data-theme="dark"] .progress { background-color: var(--surface-2); }
html[data-theme="dark"] .border,
html[data-theme="dark"] .border-top,
html[data-theme="dark"] .border-right,
html[data-theme="dark"] .border-bottom,
html[data-theme="dark"] .border-left { border-color: var(--line) !important; }
html[data-theme="dark"] hr { border-top-color: var(--line); }

/* Bootstrap utilities that paint a light surface or dark ink outright. */
html[data-theme="dark"] .bg-white,
html[data-theme="dark"] .bg-light { background-color: var(--surface) !important; color: var(--ink); }
html[data-theme="dark"] .text-dark { color: var(--ink) !important; }
html[data-theme="dark"] .text-muted { color: var(--ink-3) !important; }
html[data-theme="dark"] .text-body { color: var(--ink) !important; }

/* Select2 (Queries, Marks) */
html[data-theme="dark"] .select2-container--default .select2-selection--single,
html[data-theme="dark"] .select2-container--default .select2-selection--multiple,
html[data-theme="dark"] .select2-dropdown,
html[data-theme="dark"] .select2-search__field {
  background-color: var(--surface-2) !important;
  border-color: var(--line) !important;
  color: var(--ink) !important;
}
html[data-theme="dark"] .select2-container--default .select2-selection--single .select2-selection__rendered {
  color: var(--ink);
}
html[data-theme="dark"] .select2-results__option { color: var(--ink); }
html[data-theme="dark"] .select2-container--default .select2-results__option[aria-selected="true"] {
  background-color: var(--surface-2);
}

/* DataTables (Announcements) */
html[data-theme="dark"] .dataTables_wrapper,
html[data-theme="dark"] .dataTables_info,
html[data-theme="dark"] .dataTables_length,
html[data-theme="dark"] .dataTables_filter { color: var(--ink-2); }
html[data-theme="dark"] table.dataTable tbody tr { background-color: transparent; }


/* ─────────────────────────────────────────────────────────────────────────
   6. Filled brand controls
   --brand has to double as a text colour on dark cards, so it is lighter than
   the light-theme purple. Controls that fill with it AND carry small white
   labels are moved to --brand-fill, which keeps them ~6:1. The light-theme
   rules for several of these already use !important.
   ───────────────────────────────────────────────────────────────────────── */
html[data-theme="dark"] .act-primary,
html[data-theme="dark"] .student-portal-page .btn-primary:not(.btn-link),
html[data-theme="dark"] .btn-primary:not(.btn-link) {
  background: var(--brand-fill) !important;
  border-color: var(--brand-fill) !important;
  color: #fff !important;
}
html[data-theme="dark"] .install-banner-cta,
html[data-theme="dark"] .update-banner-cta,
html[data-theme="dark"] .install-modal-cta {
  background: var(--brand-fill);
  border-color: var(--brand-fill);
}
html[data-theme="dark"] .ra-db .ra-btn,
html[data-theme="dark"] .ra-db .ra-print-btn,
html[data-theme="dark"] .ra-db .ra-you-tag { background: var(--brand-fill); }

/* "How to do your assignments" (assignments.css) is built from three stage
   colours, each in two roles: --ac is the LINE (card outlines, numbers,
   chevrons) and --acf is the FILL that carries #fff (the lane header band, the
   step icon tiles). In light both roles share one value. They cannot in dark:
   a fill still has to be deep enough for white text while a line has to be
   light enough to read on --surface, which is the split the *-fill tokens
   above exist for. Ratios measured, not estimated. */
html[data-theme="dark"] .asg-guide {
  --ag-s1: #98a2f5;  --ag-s1f: #4a57c9;   /* line 5.7:1 on card · white on fill 6.0:1 */
  --ag-s2: #b6a4e8;  --ag-s2f: #6d59a8;   /* line 6.1:1 · white on fill 5.8:1 */
  --ag-s3: #e2a755;  --ag-s3f: #96601a;   /* line 6.3:1 · white on fill 5.3:1 */
}

/* Submit is `background: var(--danger)` with white text. --danger is light in
   dark mode (it has to read as error text on a card), so the button came out
   pale coral with unreadable white on it. student_portal.css sets it
   !important, hence the !important here. */
html[data-theme="dark"] .act-submit {
  background: var(--danger-fill) !important;
}

/* ── The "ink as a fill" family ──────────────────────────────────────────
   The most persistent trap in this theme. Several components use the INK token
   as a background with white text on top — a near-black pill in light mode:

     assignments.css   .chip.on, .btn-asg
     attendance.css    .mbtn.on
     student_analytics .ra-rec-n        (the 1/2/3 badges on "next actions")
     canvas_viewer.css .cv-toast        (--cv-ink)
     annotator .ta-toast               (--ink)

   Inverted for dark, --ink becomes near-WHITE, so every one of them turned
   into white text on a white pill — invisible. They cannot be fixed by
   re-pointing the token (--ink has to be light; it is the body text colour),
   so each is redirected to a fill token instead. This is the same failure as
   --brand/--danger/--warn had; the fix list is kept together here so the next
   component that borrows --ink for a fill is easy to add.

   Chips/badges take the brand fill (that is what "selected" reads as on dark);
   toasts stay neutral-dark, since a toast is not a selection. */
html[data-theme="dark"] .chip.on,
html[data-theme="dark"] .btn-asg,
html[data-theme="dark"] body.attendance-page .mbtn.on,
html[data-theme="dark"] .ra-db .ra-rec-n {
  background: var(--brand-fill);
  border-color: var(--brand-fill);
  color: #fff;
}
html[data-theme="dark"] .cv-toast,
html[data-theme="dark"] .ta-toast {
  background: #14171b;
  color: var(--ink);
}
html[data-theme="dark"] .chip.on.danger  { background: var(--danger-fill); border-color: var(--danger-fill); }
html[data-theme="dark"] .chip.on.warn    { background: var(--warn-fill);   border-color: var(--warn-fill); }
html[data-theme="dark"] .chip.on.success { background: var(--brand-fill);  border-color: var(--brand-fill); }
html[data-theme="dark"] .chip:hover { border-color: var(--brand-2); }

/* ── Measured contrast fixes ──────────────────────────────────────────────
   Found by walking the rendered DOM of every student page in dark mode and
   measuring each visible text node against its effective background, rather
   than by reading CSS. Ratios below are WCAG (gamma-linearised).            */

/* The active sidebar item — --brand on --brand-soft — was the single most
   widespread failure in the portal: 3.75:1 on 26 pages. --brand cannot simply
   be lightened (it doubles as a fill), so the active label steps up to
   --brand-2 instead: 5.4:1. Same for sub-items and the topbar accent. */
html[data-theme="dark"] .nav-item.active,
html[data-theme="dark"] .nav-sub a.active,
html[data-theme="dark"] .avatar-dropdown a:hover,
html[data-theme="dark"] .portal-topbar h1 em { color: var(--brand-2); }
html[data-theme="dark"] .nav-item.active::before { background: var(--brand-2); }

/* Filled controls whose white label sat on --brand (3.2:1): the feedback
   wizard's Continue button and the selected annotation tool in both viewers. */
html[data-theme="dark"] .feedback-page .fb-btn-primary,
html[data-theme="dark"] .cv-tool.active,
html[data-theme="dark"] .ta-tool.active {
  background: var(--brand-fill);
  border-color: var(--brand-fill);
  color: #fff;
  box-shadow: none;
}

/* The two viewer bars that hardcode #fbfafd — the annotation toolbar and the
   thumbnail rail. They are chrome, and at 1.9:1 their labels were unreadable. */
html[data-theme="dark"] .cv-toolbar,
html[data-theme="dark"] .cv-thumbs {
  background: var(--cv-surface);
  border-color: var(--cv-line);
}

/* Viewer submit buttons: white label on --brand / --cv-brand (3.2:1). */
html[data-theme="dark"] .cv-submitbtn,
html[data-theme="dark"] .ta-submit,
html[data-theme="dark"] .ta-foot .ta-submit {
  background: var(--brand-fill);
  border-color: var(--brand-fill);
  color: #fff;
}
html[data-theme="dark"] .cv-submitbtn:hover:not(:disabled),
html[data-theme="dark"] .ta-submit:hover:not(:disabled) { background: #7d68bd; }

/* The autosave chip ("Saved") sat at --ink-3, 3.9:1 on the toolbar. The two
   viewers name it differently: .cv-savestate and .ta-chip. */
html[data-theme="dark"] .cv-savestate,
html[data-theme="dark"] .cv-savestate .txt,
html[data-theme="dark"] .ta-chip,
html[data-theme="dark"] .ta-chip .txt { color: var(--ink-2); }

/* Shaded table headers are a filled brand/blue bar carrying white labels. The
   generic `.table thead th { color: var(--ink-2) }` above is right for a
   transparent header but wrong here — it dropped these to 2.5:1. */
html[data-theme="dark"] tr.header-shaded th,
html[data-theme="dark"] .header-shaded th,
html[data-theme="dark"] .header-shaded { color: #fff; }
html[data-theme="dark"] tr.header-shaded { background-color: var(--brand-fill); }

/* ── Community (community.css) ──
   Posts and replies are pale blue/green cards with page-coloured text: 1.1:1
   once the page went dark. Each keeps its hue as a deep tint. */
html[data-theme="dark"] .post-item {
  background-color: var(--surface);
  box-shadow: var(--shadow-sm);
}
html[data-theme="dark"] .post-item .query {
  background-color: #1b3448;
  border-left-color: #3f7db3;
}
html[data-theme="dark"] .post-item .response-item {
  background-color: #1e3d2b;
  border-left-color: #35935a;
  box-shadow: var(--shadow-sm);
}
html[data-theme="dark"] .btn-outline-danger { color: var(--danger); border-color: var(--danger); }
html[data-theme="dark"] .btn-outline-danger:hover { background: var(--danger-fill); color: #fff; }

/* ── Take-a-quiz (inline <style> in start_quiz.html) ──
   Covered statically: this page boots quiz/proctoring JS that hangs headless
   Chrome, so it is the one student page the DOM audit could not measure. Its
   inline block is only six rules — the question card is a pale #fbfafd panel
   with page-coloured text, and the submit button is the light brand purple. */
html[data-theme="dark"] .quiz-question {
  background: var(--surface) !important;
  border-color: var(--line) !important;
  color: var(--ink);
}
html[data-theme="dark"] .quiz-timer { color: #f0897e; }
html[data-theme="dark"] .btn-quiz-submit { background: var(--brand-fill); color: #fff; }
html[data-theme="dark"] .btn-quiz-submit:hover { background: #7d68bd; color: #fff; }

/* ── --brand as small text on a dark surface ──
   --brand has to stay mid-toned (it doubles as a fill), which leaves it at
   3.7-4.2:1 as body text — under AA. Every place it labels something gets
   --brand-2 instead. These were found by measuring, not guessed. */
html[data-theme="dark"] .fc-box-start,
html[data-theme="dark"] .appt-person-role,
html[data-theme="dark"] .lp-legend,
html[data-theme="dark"] .ra-db .ra-vlbl,
html[data-theme="dark"] .tp-go,
html[data-theme="dark"] .sm-guide a,
html[data-theme="dark"] .brand-soft-text { color: var(--brand-2); }
/* Anything sitting on the soft brand wash (--brand-soft) — banners, notices,
   "you have already submitted" strips — measured 3.75:1 across 10 pages. */
html[data-theme="dark"] [class*="notice"],
html[data-theme="dark"] [class*="-info"],
html[data-theme="dark"] .fc-box-start *,
html[data-theme="dark"] .lp-insight,
html[data-theme="dark"] .eng-insight-box,
html[data-theme="dark"] .next-report { color: var(--ink); }

/* White labels on a --brand / --danger fill. Since --brand is tuned for text,
   every filled control that puts #fff on it is redirected here. */
/* !important here is deliberate and measured, not a shortcut. The owning rules
   outrank any reasonable selector this file could write: #smAccessGuide
   .sm-guide-copy carries an id (1,1,0); .profile-page .prof-section-head.rc-head
   .rc-save-all is (0,4,0); and body.learning-process-page / body.topic-quizzes-page
   variants tie on specificity but load AFTER theme.css on their pages. Each of
   these is a filled control whose white label measured 2.5:1 before the switch. */
html[data-theme="dark"] .rc-save-all,
html[data-theme="dark"] .appt-upc-join,
html[data-theme="dark"] .fc-box-goal,
html[data-theme="dark"] .prof-faq-toggle,
html[data-theme="dark"] .sm-guide-num,
html[data-theme="dark"] .sm-guide-copy,
html[data-theme="dark"] .rat-help-btn,
html[data-theme="dark"] .tmq-btn-primary,
html[data-theme="dark"] .feedback-page .s-dot,
html[data-theme="dark"] .ra-db .ra-cta-solid {
  background: var(--brand-fill) !important;
  color: #fff !important;
}
html[data-theme="dark"] .fc-box-danger {
  background: var(--danger-fill) !important;
  color: #fff !important;
}

/* student_flowchart: "Yes"/"No" tags are a saturated literal on a pale wash. */
html[data-theme="dark"] .fc-tag-yes { background: #1e3d2b !important; color: #7fd3a3 !important; }
html[data-theme="dark"] .fc-tag-no  { background: #4a2a26 !important; color: #f2988d !important; }

/* learning-process.css --video is a hot pink used as text on a card (3.4:1). */
html[data-theme="dark"] .stat-video { color: #f5a8ca; }

/* Community: the reply/query tints, matched without requiring .post-item —
   the list renders them standalone in some states. */
html[data-theme="dark"] .response-item { background-color: #1e3d2b; border-left-color: #35935a; }
html[data-theme="dark"] .query { background-color: #1b3448; border-left-color: #3f7db3; }
html[data-theme="dark"] .submit-button.btn-primary { background: var(--brand-fill); border-color: var(--brand-fill); color: #fff; }

/* ── Landing page: rules inside student_analytics.html's own <style> ──
   These are CSS rules (not style="" attributes), so the literal remapping
   above does not reach them; they need naming. Found by neutralising the
   scroll-reveal animation in the audit harness — until then these cards sat at
   opacity 0 in headless and were never measured. */
html[data-theme="dark"] .ra-goals-edit-btn { color: var(--brand-2); }   /* was #4D3E77, 1.29:1 */

/* Severity pills on the AI insight card: white on --c-err / --c-warn, which
   are light in dark mode (2.5:1). Same fill-token treatment as elsewhere. */
html[data-theme="dark"] .ra-sev.high { background: var(--danger-fill); color: #fff; }
html[data-theme="dark"] .ra-sev.med  { background: var(--warn-fill);   color: #fff; }
html[data-theme="dark"] .ra-sev.low  { background: var(--brand-fill);  color: #fff; }

/* The grade letter's colour comes from a Jinja variable written straight into
   style="color:…" — seven possible values, each a light-theme literal. */
html[data-theme="dark"] body.student-portal-page [style*="color:#dc2626" i],
html[data-theme="dark"] body.student-portal-page [style*="color: #dc2626" i] { color: #f2988d !important; }
html[data-theme="dark"] body.student-portal-page [style*="color:#ea580c" i],
html[data-theme="dark"] body.student-portal-page [style*="color: #ea580c" i] { color: #f0a56b !important; }
html[data-theme="dark"] body.student-portal-page [style*="color:#f59e0b" i],
html[data-theme="dark"] body.student-portal-page [style*="color: #f59e0b" i] { color: #f0c274 !important; }
html[data-theme="dark"] body.student-portal-page [style*="color:#2e7d32" i],
html[data-theme="dark"] body.student-portal-page [style*="color: #2e7d32" i] { color: #7fd3a3 !important; }
html[data-theme="dark"] body.student-portal-page [style*="color:#0569a8" i],
html[data-theme="dark"] body.student-portal-page [style*="color: #0569a8" i] { color: #7cc0e8 !important; }

/* ── Quiz results (inline <style> in view_quiz_results.html) ──
   The answer-review cards: a white feedback card and three choice tints. */
html[data-theme="dark"] .feedback-item {
  background: var(--surface) !important;
  border-color: var(--line) !important;
}
html[data-theme="dark"] .choice-correct { background: #1e3d2b !important; color: #a8e6c4; }
html[data-theme="dark"] .choice-wrong   { background: #4a2a26 !important; color: #f5b0a8; }
html[data-theme="dark"] .choice-neutral { background: var(--surface-2) !important; }

/* Viewer toolbar hints and labels — 4.2-4.3:1 on --surface. */
html[data-theme="dark"] .cv-hint,
html[data-theme="dark"] .ta-hint,
html[data-theme="dark"] #cvToolHint,
html[data-theme="dark"] #taHint { color: var(--ink-2); }
html[data-theme="dark"] .ta-zoomlabel,
html[data-theme="dark"] .cv-zoomlabel,
html[data-theme="dark"] #taZoomLabel { color: var(--brand-2); }

html[data-theme="dark"] .act-ghost {
  background: var(--surface) !important;
  color: var(--ink) !important;
  border-color: var(--line) !important;
}
html[data-theme="dark"] .act-icon {
  background: var(--surface-2);
  border-color: var(--line);
  color: var(--ink-2);
}
html[data-theme="dark"] .act-icon:hover { background: var(--brand-soft); color: var(--brand-2); }

/* The sidebar "Cambridge Assistant" card: its panel is flattened in section 11,
   here its white CTA settles onto the dark palette. */
html[data-theme="dark"] .upgrade a { background: var(--surface); color: var(--ink) !important; }
html[data-theme="dark"] .upgrade a:hover { background: var(--surface-2); color: var(--ink) !important; }

html[data-theme="dark"] #pwa-install-sidebar-link {
  background: var(--surface-2);
  border-color: var(--line);
  color: var(--brand-2);
}


/* ─────────────────────────────────────────────────────────────────────────
   7. Page sheets that hardcoded a light surface
   ───────────────────────────────────────────────────────────────────────── */

/* Active-test banners (student_portal.css) are pastel gradients; the fills are
   flattened in section 11, borders and titles handled here. */
html[data-theme="dark"] .student-active-test-banner.is-available { border-color: #5f4c1e; }
html[data-theme="dark"] .student-active-test-banner.is-available .satb-title { color: #f0cd8e; }
html[data-theme="dark"] .student-active-test-banner.is-in-progress { border-color: #4b4070; }
html[data-theme="dark"] .student-active-test-banner.is-in-progress .satb-title { color: var(--brand-2); }
html[data-theme="dark"] .satb-subject {
  background: rgba(255, 255, 255, .07);
  color: var(--brand-2);
  border-color: var(--line);
}

/* Analytics hero keeps its purple gradient — deepened so it does not glow. */
html[data-theme="dark"] .ra-db .ra-cta { background: var(--surface); color: var(--brand-2); }
html[data-theme="dark"] .ra-db .ra-cta:hover { color: var(--brand-2); }

/* "Submit your answers" chooser on the Assignments page (assignments.css
   .theory-panel). Both cards hardcode background:#fff while their <b> title
   inherits colour from the page — so in dark the titles were near-white text
   on a white card and read as blank. This is the whole fix: darken the card
   and the inherited light text becomes legible again. */
html[data-theme="dark"] .theory-panel .tp-choice,
html[data-theme="dark"] .theory-panel .tp-close {
  background: var(--surface);
  border-color: var(--line);
  color: var(--ink);
}
html[data-theme="dark"] .theory-panel .tp-choice:hover { border-color: var(--brand); }
html[data-theme="dark"] .theory-panel .tp-close:hover { background: var(--surface-2); }

/* Document preview modals. The *-inner / *-dialog elements are the modal
   SHELL — header bar, toolbar, then the viewer — not the page the document
   renders on. The PDF inside keeps its own white; only the chrome moves. */
html[data-theme="dark"] .preview-modal-dialog,
html[data-theme="dark"] .sm-preview-modal .sm-preview-inner,
html[data-theme="dark"] .pp-preview-modal .pp-preview-inner {
  background-color: var(--surface);
  border-color: var(--line);
}
html[data-theme="dark"] .preview-modal-header,
html[data-theme="dark"] .sm-preview-modal .sm-preview-head {
  background: var(--surface-2);
  border-bottom-color: var(--line);
  color: var(--ink);
}
html[data-theme="dark"] .sm-preview-modal .sm-preview-nav,
html[data-theme="dark"] .sm-preview-modal .sm-preview-btn {
  background: var(--surface-2);
  border-color: var(--line);
  color: var(--ink);
}
/* The scanned-page tray behind the thumbnails (#f8f9fa). */
html[data-theme="dark"] #previewPdfImages { background: var(--surface-2); }

/* Sticky bars and legacy page shells that were left cream. */
html[data-theme="dark"] .feedback-page .fb-nav-bar { background: rgba(35, 38, 43, .92); }
html[data-theme="dark"] body.student-portal-page .topbar { background: rgba(35, 38, 43, .82); }

/* Page sheets that painted a surface white with a literal. Scoped to
   .student-portal-page because these are generic class names (.section,
   .main-content) that other portals also use.
     community.css .section
     marks.css     .main-content, .welcome-message                          */
html[data-theme="dark"] body.student-portal-page .section,
html[data-theme="dark"] body.student-portal-page .main-content,
html[data-theme="dark"] body.student-portal-page .welcome-message {
  background-color: var(--surface);
  color: var(--ink);
}
html[data-theme="dark"] body.student-portal-page .section,
html[data-theme="dark"] body.student-portal-page .welcome-message {
  box-shadow: var(--shadow-sm);
}

/* DELIBERATELY LEFT LIGHT — do not "fix" these:
     past-papers.css / tests.css  #captureFlash      a white camera-flash frame
     assignments.css  .preview-modal-body iframe     the page the PDF renders on
     appointments.css .appt-calendar-iframe          third-party, white content
     feedback.css     .rng::-*-thumb                 a white knob on a dark track
     academic_redesign.css .acad-sw::after           a toggle knob
     resource-access-tour.css .rat-browser/-env      drawings of Google Drive's UI
   Inverting any of them makes the thing it represents wrong, not dark.
   Note the distinction from the block above: a preview modal's SHELL is app
   chrome and goes dark; only the document surface inside it stays white.      */

/* Anything still painting itself white through a literal, per page sheet.
   Scoped tightly so a genuinely white-on-purple element is left alone. */
html[data-theme="dark"] .student-portal-page .portal-legacy-content [style*="background:#fff"],
html[data-theme="dark"] .student-portal-page .portal-legacy-content [style*="background: #fff"],
html[data-theme="dark"] .student-portal-page .portal-legacy-content [style*="background-color:#fff"],
html[data-theme="dark"] .student-portal-page .portal-legacy-content [style*="background-color: #fff"] {
  background: var(--surface) !important;
  color: var(--ink);
}

/* Charts drawn as inline SVG inherit currentColor for their labels; canvas
   charts (ApexCharts/Chart.js) paint their own text and are handled in JS by
   the pages that own them. */
html[data-theme="dark"] .portal-legacy-content svg text { fill: currentColor; }

/* The onboarding tours need nothing here: student-tour.css (.st-tooltip,
   .st-skip) and resource-access-tour.css (.rat-card, .rat-browser-bar) all
   reach for their surfaces as `var(--surface, #fff)`, so re-pointing the token
   carries them. Their .rat-env / .rat-browser / .rat-profile insets stay white
   ON PURPOSE — they are little illustrations of Google Drive's own light UI,
   and darkening them would stop them looking like the thing being explained. */

/* ─────────────────────────────────────────────────────────────────────────
   8. Loading surfaces
   These are the worst offenders in a retrofitted dark mode: a full-screen
   near-opaque light veil that covers the whole viewport for the length of a
   navigation. Left alone, Schedule and Announcements strobe white on every
   page change even though the page underneath is already dark.
   ───────────────────────────────────────────────────────────────────────── */
html[data-theme="dark"] .sch-page-loading,      /* schedule.css   rgba(250,249,247,.92) */
html[data-theme="dark"] .acad-page-loading {    /* academic_redesign.css rgba(246,245,249,.9) */
  background: rgba(28, 31, 36, .92);
}
html[data-theme="dark"] .sch-page-loading-spinner,
html[data-theme="dark"] .acad-page-loading-spinner {
  border-color: var(--line);
  border-top-color: var(--brand-2);
}
html[data-theme="dark"] .sch-page-loading-text,
html[data-theme="dark"] .acad-page-loading-inner p { color: var(--ink-2); }

/* Inline "loading…" states. All of these inherit currentColor for their
   spinner glyph (Font Awesome fa-spin / Bootstrap .spinner-border), so setting
   the text colour fixes the mark with it. */
html[data-theme="dark"] .study-materials-page .sm-loading,
html[data-theme="dark"] .profile-page .prof-faq-loading,
html[data-theme="dark"] .faq-loading,
html[data-theme="dark"] .chart-loading { color: var(--ink-3); }
html[data-theme="dark"] .spinner-border,
html[data-theme="dark"] .spinner-grow { color: var(--brand-2); }
html[data-theme="dark"] body.acad .rel-pill.rel-loading {
  background: var(--surface-2);
  color: var(--ink-3);
}

/* A skeleton/placeholder shimmer built for a white page is a bright bar on a
   dark one. */
html[data-theme="dark"] .skeleton,
html[data-theme="dark"] .placeholder,
html[data-theme="dark"] .shimmer { background-color: var(--surface-2); }


/* ─────────────────────────────────────────────────────────────────────────
   9. Saturated fills
   Colours picked to sit on white are too hot on a dark canvas — #45cb85 and
   #eeb902 read as lit chips rather than labels. Badges move to the standard
   dark-UI treatment (a deep tint of the hue, with light text of the same hue),
   which keeps the semantic colour while dropping the glare. Buttons stay
   filled — they are actions — but are deepened, which also fixes the light
   theme's own poor white-on-#45cb85 contrast rather than inheriting it.
   ───────────────────────────────────────────────────────────────────────── */
html[data-theme="dark"] .badge-success   { background-color: #1e3d2b; color: #7fd3a3; }
html[data-theme="dark"] .badge-info      { background-color: #123c4d; color: #6fc7e8; }
html[data-theme="dark"] .badge-warning   { background-color: #443619; color: #e8c06a; }
html[data-theme="dark"] .badge-danger    { background-color: #4a2a26; color: #f2988d; }
html[data-theme="dark"] .badge-primary   { background-color: #3a3155; color: #c3b1ee; }
html[data-theme="dark"] .badge-secondary { background-color: #333a44; color: #aab3c0; }

/* Scoped off <button> on an .assignments-page. assignments.css resets those to
   plain text — `body.assignments-page button { border:none; background:none }`
   at (0,1,1) — so on Tests / Resources / Past Papers the scanner's
   .btn-success and friends already render unfilled in the LIGHT theme. These
   rules sit at (0,2,1) and would beat that reset, handing dark mode a filled
   button the light theme never shows. Dark mode restyles colour, not
   affordance, so those stay out of scope. `a.btn-*` is not caught by the reset
   (it targets `button`), so anchors stay in. */
html[data-theme="dark"] body:not(.assignments-page) .btn-success,
html[data-theme="dark"] body.assignments-page a.btn-success {
  background-color: #2a8f5c; border-color: #2a8f5c;
}
html[data-theme="dark"] body:not(.assignments-page) .btn-info,
html[data-theme="dark"] body.assignments-page a.btn-info {
  background-color: #0d7fa5; border-color: #0d7fa5;
}
html[data-theme="dark"] body:not(.assignments-page) .btn-warning,
html[data-theme="dark"] body.assignments-page a.btn-warning {
  background-color: var(--warn-fill); border-color: var(--warn-fill); color: #fff;
}
html[data-theme="dark"] body:not(.assignments-page) .btn-danger,
html[data-theme="dark"] body.assignments-page a.btn-danger {
  background-color: #cf5240; border-color: #cf5240;
}
html[data-theme="dark"] body:not(.assignments-page) .btn-secondary,
html[data-theme="dark"] body:not(.assignments-page) .btn-light,
html[data-theme="dark"] body.assignments-page a.btn-secondary,
html[data-theme="dark"] body.assignments-page a.btn-light {
  background-color: var(--surface-2);
  border-color: var(--line);
  color: var(--ink);
}
html[data-theme="dark"] .btn-outline-secondary { border-color: var(--line); color: var(--ink-2); }

/* The "test available" CTA is a pure #fcd208 block — the single brightest
   element in the portal. Deepened just enough to stop it glowing while staying
   unmistakably the yellow call to action. */
html[data-theme="dark"] .student-active-test-banner.is-available .satb-cta {
  background: #d8a91c;
  color: #241a05;
}

/* ── Inline style="" colour literals ────────────────────────────────────────
   The landing page and a few others write light-theme colours straight into
   markup (style="color:#4D3E77", style="color:#1b2130"). An inline style beats
   every stylesheet, so these survived dark mode untouched and were the reason
   text kept coming out invisible even with the CSS audit clean — #4D3E77 body
   text on a #2b2f35 card measures 1.6:1.

   Each literal is remapped to its dark counterpart. Generated, because the
   selectors have to cover "color:#x" and "color: #x", match case-insensitively,
   and exclude "background-color:#x" (of which "color:#x" is a substring — a
   naive rule would repaint the text of anything using the literal as a fill).
   ------------------------------------------------------------------------- */
html[data-theme="dark"] body.student-portal-page [style*="color:#4d3e77" i]:not([style*="background-color:#4d3e77" i]),
html[data-theme="dark"] body.student-portal-page [style*="color: #4d3e77" i]:not([style*="background-color: #4d3e77" i]) { color: #b7a4e6 !important; }   /* brand purple -> light brand (was ~1.6:1 on a dark card) */
html[data-theme="dark"] body.student-portal-page [style*="color:#6b52a8" i]:not([style*="background-color:#6b52a8" i]),
html[data-theme="dark"] body.student-portal-page [style*="color: #6b52a8" i]:not([style*="background-color: #6b52a8" i]) { color: #b7a4e6 !important; }   /* brand alt */
html[data-theme="dark"] body.student-portal-page [style*="color:#8b94a4" i]:not([style*="background-color:#8b94a4" i]),
html[data-theme="dark"] body.student-portal-page [style*="color: #8b94a4" i]:not([style*="background-color: #8b94a4" i]) { color: #a3abb7 !important; }   /* muted ink */
html[data-theme="dark"] body.student-portal-page [style*="color:#586173" i]:not([style*="background-color:#586173" i]),
html[data-theme="dark"] body.student-portal-page [style*="color: #586173" i]:not([style*="background-color: #586173" i]) { color: #b2b9c4 !important; }   /* secondary ink */
html[data-theme="dark"] body.student-portal-page [style*="color:#1b2130" i]:not([style*="background-color:#1b2130" i]),
html[data-theme="dark"] body.student-portal-page [style*="color: #1b2130" i]:not([style*="background-color: #1b2130" i]) { color: #e7eaee !important; }   /* primary ink — near-black, invisible in dark */
html[data-theme="dark"] body.student-portal-page [style*="color:#2e2a38" i]:not([style*="background-color:#2e2a38" i]),
html[data-theme="dark"] body.student-portal-page [style*="color: #2e2a38" i]:not([style*="background-color: #2e2a38" i]) { color: #e7eaee !important; }   /* primary ink (login palette) */
html[data-theme="dark"] body.student-portal-page [style*="color:#dc4a40" i]:not([style*="background-color:#dc4a40" i]),
html[data-theme="dark"] body.student-portal-page [style*="color: #dc4a40" i]:not([style*="background-color: #dc4a40" i]) { color: #f0897e !important; }   /* danger */
html[data-theme="dark"] body.student-portal-page [style*="color:#c0392b" i]:not([style*="background-color:#c0392b" i]),
html[data-theme="dark"] body.student-portal-page [style*="color: #c0392b" i]:not([style*="background-color: #c0392b" i]) { color: #f0897e !important; }   /* danger alt */
html[data-theme="dark"] body.student-portal-page [style*="color:#d9901a" i]:not([style*="background-color:#d9901a" i]),
html[data-theme="dark"] body.student-portal-page [style*="color: #d9901a" i]:not([style*="background-color: #d9901a" i]) { color: #e8c06a !important; }   /* warning */
html[data-theme="dark"] body.student-portal-page [style*="color:#2f6fed" i]:not([style*="background-color:#2f6fed" i]),
html[data-theme="dark"] body.student-portal-page [style*="color: #2f6fed" i]:not([style*="background-color: #2f6fed" i]) { color: #8db4e2 !important; }   /* info blue */
html[data-theme="dark"] body.student-portal-page [style*="color:#5b5ef0" i]:not([style*="background-color:#5b5ef0" i]),
html[data-theme="dark"] body.student-portal-page [style*="color: #5b5ef0" i]:not([style*="background-color: #5b5ef0" i]) { color: #a5a7ff !important; }   /* indigo */
html[data-theme="dark"] body.student-portal-page [style*="color:#159a57" i]:not([style*="background-color:#159a57" i]),
html[data-theme="dark"] body.student-portal-page [style*="color: #159a57" i]:not([style*="background-color: #159a57" i]) { color: #7fd3a3 !important; }   /* success green */
html[data-theme="dark"] body.student-portal-page [style*="color:#006400" i]:not([style*="background-color:#006400" i]),
html[data-theme="dark"] body.student-portal-page [style*="color: #006400" i]:not([style*="background-color: #006400" i]) { color: #7fd3a3 !important; }   /* dark green */
html[data-theme="dark"] body.student-portal-page [style*="color:#4caf50" i]:not([style*="background-color:#4caf50" i]),
html[data-theme="dark"] body.student-portal-page [style*="color: #4caf50" i]:not([style*="background-color: #4caf50" i]) { color: #7fd3a3 !important; }   /* green alt */
html[data-theme="dark"] body.student-portal-page [style*="color:#3c1348" i]:not([style*="background-color:#3c1348" i]),
html[data-theme="dark"] body.student-portal-page [style*="color: #3c1348" i]:not([style*="background-color: #3c1348" i]) { color: #d8c4ea !important; }   /* deep plum */

html[data-theme="dark"] body.student-portal-page [style*="background:#ede8f5" i],
html[data-theme="dark"] body.student-portal-page [style*="background: #ede8f5" i],
html[data-theme="dark"] body.student-portal-page [style*="background-color:#ede8f5" i],
html[data-theme="dark"] body.student-portal-page [style*="background-color: #ede8f5" i] { background-color: #3a3155 !important; }   /* brand-soft wash */
html[data-theme="dark"] body.student-portal-page [style*="background:#f0f0f0" i],
html[data-theme="dark"] body.student-portal-page [style*="background: #f0f0f0" i],
html[data-theme="dark"] body.student-portal-page [style*="background-color:#f0f0f0" i],
html[data-theme="dark"] body.student-portal-page [style*="background-color: #f0f0f0" i] { background-color: #33373e !important; }   /* grey panel */
html[data-theme="dark"] body.student-portal-page [style*="background:#f8f9fa" i],
html[data-theme="dark"] body.student-portal-page [style*="background: #f8f9fa" i],
html[data-theme="dark"] body.student-portal-page [style*="background-color:#f8f9fa" i],
html[data-theme="dark"] body.student-portal-page [style*="background-color: #f8f9fa" i] { background-color: #33373e !important; }   /* bootstrap light */

/* White fills written inline (Queries/Tests/marks toolbars, the feedback gate).
   Kept separate from the list above because #fff needs both spellings. */
html[data-theme="dark"] body.student-portal-page [style*="background:#fff" i],
html[data-theme="dark"] body.student-portal-page [style*="background: #fff" i],
html[data-theme="dark"] body.student-portal-page [style*="background-color:#fff" i],
html[data-theme="dark"] body.student-portal-page [style*="background-color: #fff" i],
html[data-theme="dark"] body.student-portal-page [style*="background:#ffffff" i],
html[data-theme="dark"] body.student-portal-page [style*="background: #ffffff" i],
html[data-theme="dark"] body.student-portal-page [style*="background-color:#ffffff" i],
html[data-theme="dark"] body.student-portal-page [style*="background-color: #ffffff" i] { background-color: var(--surface) !important; }

/* The design system's four "soft" tints, as INLINE styles.
   student_analytics.html — the page a student lands on after login — writes
   these literals directly into style="" attributes in several places: the
   performance heatmap cells and legend (raHeatColor()), the status pills on
   the Attendance/Assignments cards, and the progress-bar tracks. An inline
   style beats any stylesheet, so these are the one place !important is the
   correct tool rather than a shortcut.

   Matching the literal is complete rather than brittle here: the scale has
   exactly four stops and no interpolation, and the same four constants are
   reused for the pills. Colour encodes meaning (score, attendance band), so
   each stop keeps its hue and only swaps pale-tint-on-white for
   deep-tint-on-dark. Scoped to the portal body so nothing else is caught. */
html[data-theme="dark"] body.student-portal-page [style*="#f0eee9"] {
  background: #2f333a !important; color: #9aa3af !important;
}
html[data-theme="dark"] body.student-portal-page [style*="#e6f6ed"] {
  background: #1e3d2b !important; color: #7fd3a3 !important;
}
html[data-theme="dark"] body.student-portal-page [style*="#fbf1da"] {
  background: #443619 !important; color: #e8c06a !important;
}
html[data-theme="dark"] body.student-portal-page [style*="#fce9e7"] {
  background: #4a2a26 !important; color: #f2988d !important;
}
/* Same four as SVG presentation attributes (the attendance ring's track). */
html[data-theme="dark"] body.student-portal-page [stroke="#f0eee9"] { stroke: #3d434b; }

/* view_quiz_results.html declares .quiz-score-card in an inline <style> block
   with a pale lavender fill; its text measured 1.12:1 against it. */
html[data-theme="dark"] .quiz-score-card {
  background: var(--surface-2) !important;
  border-color: var(--line) !important;
  color: var(--ink);
}


/* ─────────────────────────────────────────────────────────────────────────
   10. Depth and finish
   The light theme gets its hierarchy from shadows on white. Shadows do almost
   nothing over a dark canvas, so dark mode has to get it from surface steps
   instead: sidebar (deepest) → page → card (lightest). Two levels read flat;
   three is what makes it look designed rather than inverted.
   ───────────────────────────────────────────────────────────────────────── */
html[data-theme="dark"] .portal-sidebar { background: #1f2227; }
html[data-theme="dark"] .side-foot { border-top-color: var(--line); }

/* Text selection, native controls and focus rings all default to a blue tuned
   for white backgrounds. */
html[data-theme="dark"] ::selection { background: rgba(154, 131, 212, .38); color: #fff; }
html[data-theme="dark"] input[type="checkbox"],
html[data-theme="dark"] input[type="radio"],
html[data-theme="dark"] input[type="range"],
html[data-theme="dark"] progress { accent-color: var(--brand); }
html[data-theme="dark"] :focus-visible { outline-color: var(--brand-2); }

/* Anything scrollable that is not the two containers already handled above. */
html[data-theme="dark"] { scrollbar-color: #4c525b var(--paper); }

/* Body text is --ink (#e7eaee), not #fff, on purpose — pure white on a near
   black surface is what makes long reading feel harsh. Inline color:#fff is
   left alone deliberately: in this codebase it nearly always sits on a filled
   purple hero, where white is the right answer. */

/* ─────────────────────────────────────────────────────────────────────────
   11. No purple gradients in dark mode
   The light theme leans on violet gradients everywhere — hero panels, avatar
   tiles, modal headers, progress bars, and two page-wide radial glows. Dark
   mode uses flat colour instead. Every rule here replaces a gradient with a
   single value, so `background` (the shorthand) is used deliberately: it
   clears background-image as well, which `background-color` would leave in
   place and let the gradient show through.

   Three flats do all the work:
     --brand-fill   small filled tiles that carry a white glyph
     #2d2650        the big deep-violet hero panels
     --surface-2    the pale surface→brand-soft "insight" tints
   ───────────────────────────────────────────────────────────────────────── */

/* -- Page-wide glows. Two purple radials wash every portal page; flat means
      flat, so they go entirely rather than getting dimmer. --------------- */
html[data-theme="dark"] body.student-portal-page::before,
html[data-theme="dark"] body.assignments-page::before,
html[data-theme="dark"] body.acad .page-content { background: none; }

/* -- Brand tiles: logo marks, avatars, icon chips. ---------------------- */
html[data-theme="dark"] .brand-mark,
html[data-theme="dark"] .topbar-brand,
html[data-theme="dark"] .avatar,
html[data-theme="dark"] .appt-person-av,
html[data-theme="dark"] .appt-stype-btn.on,
html[data-theme="dark"] .feedback-page .fhero .ic,
html[data-theme="dark"] .feedback-page .t-av,
html[data-theme="dark"] .feedback-page .done-screen .big,
html[data-theme="dark"] .ra-db .ra-av,
html[data-theme="dark"] .ra-db .ra-level-badge,
html[data-theme="dark"] .st-celebrate,
html[data-theme="dark"] body.acad .ch-stuhead .av,
html[data-theme="dark"] #resource-access-tour .rat-env::before,
html[data-theme="dark"] #resource-access-tour .rat-profile-good u,
html[data-theme="dark"] .student-active-test-banner.is-in-progress .satb-icon {
  background: var(--brand-fill);
}
/* The tiles' light-theme drop shadow is a violet glow; flat means no halo. */
html[data-theme="dark"] .brand-mark,
html[data-theme="dark"] .topbar-brand { box-shadow: none; }

/* -- Hero panels and filled modal headers. ------------------------------ */
html[data-theme="dark"] body.fees-page .fees-hero,
html[data-theme="dark"] body.fees-page .fees-receipt-modal .modal-header,
html[data-theme="dark"] .sch-hero,
html[data-theme="dark"] body.reports-page .rep-hero,
html[data-theme="dark"] .ra-db .ra-hero,
html[data-theme="dark"] .profile-page .prof-welcome,
html[data-theme="dark"] .upgrade,
html[data-theme="dark"] body.learning-process-page .fc-box-goal,
html[data-theme="dark"] body.learning-process-page .fc-m-card-goal,
html[data-theme="dark"] .prev-asg-modal .modal-header,
html[data-theme="dark"] .prev-tests-modal .modal-header,
html[data-theme="dark"] #previewPdfModal .modal-header {
  background: #2d2650;
}
/* Their decorative blur blobs and radial glows are the gradient by another
   name — a soft violet halo bleeding across the panel. */
html[data-theme="dark"] body.fees-page .fees-hero::before,
html[data-theme="dark"] body.fees-page .fees-hero::after,
html[data-theme="dark"] .sch-hero::after,
html[data-theme="dark"] .ra-db .ra-hero::before,
html[data-theme="dark"] .ra-db .ra-hero::after,
html[data-theme="dark"] .upgrade::after { display: none; }

/* -- Soft "insight" tints (surface-2 → brand-soft). --------------------- */
html[data-theme="dark"] body.engagement-page .eng-insight-box,
html[data-theme="dark"] body.learning-process-page .lp-insight,
html[data-theme="dark"] body.learning-process-page .flowchart-wrap,
html[data-theme="dark"] body.learning-process-page .fc-box-start,
html[data-theme="dark"] body.learning-process-page .fc-m-start .fc-m-card,
html[data-theme="dark"] body.reports-page .next-report,
html[data-theme="dark"] .ra-db .ra-ai-sum,
html[data-theme="dark"] .ra-db .ra-lb-you,
html[data-theme="dark"] .sch-today-item.is-live,
html[data-theme="dark"] .study-materials-page .sm-guide,
html[data-theme="dark"] .student-active-test-banner.is-in-progress {
  background: var(--surface-2);
}

/* -- Progress / accent bars. -------------------------------------------- */
html[data-theme="dark"] .feedback-page .pbar i,
html[data-theme="dark"] .study-materials-page .sm-guide::before,
html[data-theme="dark"] body.acad.exam-dashboard .exam-examiner-card::before,
html[data-theme="dark"] #releaseResultsModal .rel-bar > i {
  background: var(--brand);
}

/* -- Schedule subject cards. Pale pastel gradients built for a white page —
      the purple ones are in scope, and the rest are left flat too so one row
      of cards does not read as half-gradient. Each keeps its hue as a deep
      tint so the subject is still identifiable at a glance.
      Each subject also hardcodes near-black label colours (#14532d, #3b0764 …)
      chosen for those pale fills, so the text has to be re-pointed in the same
      breath — swapping only the background leaves the card unreadable. ---- */
html[data-theme="dark"] .sch-card.subj-biology   { background: #1e3d2b; border-color: #2c5a3f; }
html[data-theme="dark"] .sch-card.subj-chemistry { background: #443619; border-color: #63512a; }
html[data-theme="dark"] .sch-card.subj-physics   { background: #3a3155; border-color: #4b4070; }
html[data-theme="dark"] .sch-card.subj-support   { background: #2e3060; border-color: #43468a; }
html[data-theme="dark"] .sch-card.subj-default   { background: #372c52; border-color: #4a3d69; }
html[data-theme="dark"] .sch-card.subj-extra     { background: #45351a; border-color: #63512a; }

html[data-theme="dark"] .sch-card.subj-biology .sch-card-subj   { color: #a8e6c4; }
html[data-theme="dark"] .sch-card.subj-biology .sch-card-teacher,
html[data-theme="dark"] .sch-card.subj-biology .sch-card-time   { color: #7fd3a3; }

html[data-theme="dark"] .sch-card.subj-chemistry .sch-card-subj { color: #f3d69f; }
html[data-theme="dark"] .sch-card.subj-chemistry .sch-card-teacher,
html[data-theme="dark"] .sch-card.subj-chemistry .sch-card-time { color: #e8c06a; }

html[data-theme="dark"] .sch-card.subj-physics .sch-card-subj   { color: #d8cbf3; }
html[data-theme="dark"] .sch-card.subj-physics .sch-card-teacher,
html[data-theme="dark"] .sch-card.subj-physics .sch-card-time   { color: #c3b1ee; }

html[data-theme="dark"] .sch-card.subj-support .sch-card-subj   { color: #c8caff; }
html[data-theme="dark"] .sch-card.subj-support .sch-card-teacher,
html[data-theme="dark"] .sch-card.subj-support .sch-card-time   { color: #aeb1ff; }

html[data-theme="dark"] .sch-card.subj-default .sch-card-subj   { color: #e0cef8; }
html[data-theme="dark"] .sch-card.subj-default .sch-card-teacher,
html[data-theme="dark"] .sch-card.subj-default .sch-card-time   { color: #cbb0f0; }

html[data-theme="dark"] .sch-card.subj-extra .sch-card-subj     { color: #f7cba7; }
html[data-theme="dark"] .sch-card.subj-extra .sch-card-teacher,
html[data-theme="dark"] .sch-card.subj-extra .sch-card-time     { color: #eeb280; }

/* Past classes are dimmed to .55 opacity by a.sch-card.zoom-link.disabled.
   Over a dark card that lands under 3:1, so lift it slightly — still clearly
   "gone", still readable. */
html[data-theme="dark"] a.sch-card.zoom-link.disabled { opacity: .72; filter: none; }

/* The amber "test available" banner is the one non-purple gradient left in the
   shell; flattened too, so the two banner states match each other. */
html[data-theme="dark"] .student-active-test-banner.is-available {
  background: #3f3315;
}

/* ─────────────────────────────────────────────────────────────────────────
   12. Document viewers
   Three full-screen viewers live outside the portal shell and pull this file
   in themselves: paper_viewer.html (--pv-*), canvas_viewer.html (--cv-*) and
   assignment_theory_annotator.html (which reuses the portal's own token names,
   so the root block above already covers it).

   This section darkens the CHROME (toolbars, rails, workspace mat, inputs).
   The rendered document is handled separately in section 13, which inverts it
   so the paper reads dark too. Nothing in either section touches a canvas's
   pixels, the ink data, or any geometry — both are paint-only.
   ───────────────────────────────────────────────────────────────────────── */
html[data-theme="dark"] {
  /* paper_viewer.css */
  --pv-brand: #9a83d4;
  --pv-brand-2: #b7a4e6;
  --pv-ink: #e7eaee;
  --pv-ink-2: #b2b9c4;
  --pv-line: #3d434b;
  --pv-surface: #2b2f35;
  --pv-surface-2: #33373e;
  --pv-bg: #1c1f24;            /* the mat the page sits on — deepest surface */

  /* canvas_viewer.css */
  --cv-brand: #9a83d4;
  --cv-brand-soft: #3a3155;
  --cv-ink: #e7eaee;
  --cv-ink-2: #b2b9c4;
  --cv-ink-3: #a3abb7;
  --cv-line: #3d434b;
  --cv-line-2: #343941;
  --cv-surface: #2b2f35;
  --cv-workspace: #1c1f24;
  --cv-success: #4cae7e;
  --cv-danger: #f0897e;
  --cv-shadow: 0 1px 2px rgba(0, 0, 0, .30), 0 1px 3px rgba(0, 0, 0, .24);

  /* topic-mcq-practice.css — the MCQ answer sheet beside the question paper.
     The paper itself is an embedded paper_viewer, which inverts itself; before
     this the quiz chrome stayed light beside it, which is what made the two
     halves of the page disagree. --mcq-accent is a text tone here and
     --mcq-accent-fill carries white text, same split as --brand above. */
  --mcq-accent: #ad96e0;
  --mcq-accent-fill: #6d59a8;
  --mcq-accent-soft: #3a3155;
  --mcq-accent-line: #574a80;
  --mcq-brown: #ad96e0;
  --mcq-cream: #23262b;          /* the mat the two panes sit on */
  --mcq-surface: #2b2f35;
  --mcq-surface-2: #33373e;
  --mcq-border: #3d434b;
  --mcq-line-2: #343941;
  --mcq-green: #7fd3a3;
  --mcq-red: #f0897e;
  --mcq-amber: #e8b45f;
  --mcq-ink: #e7eaee;
  --mcq-ink-2: #b2b9c4;
  --mcq-ink-3: #a3abb7;
}

/* The two inputs in the paper viewer's toolbar hardcode a white fill. */
html[data-theme="dark"] .pv-pageinfo input,
html[data-theme="dark"] .pv-searchbox input {
  background: var(--pv-surface-2);
  border-color: var(--pv-line);
  color: var(--pv-ink);
}

/* Search hit highlights are drawn over the page's own white, so they keep
   their light-theme yellows — they have to stay legible against the document,
   not against the app. */

/* ─────────────────────────────────────────────────────────────────────────
   13. Dark documents
   In dark mode the rendered PDF inverts too, not just the chrome around it.

   HOW: invert(1) flips the paper to near-black and the print to near-white;
   hue-rotate(180deg) then puts coloured content back near its original hue, so
   a red diagram stays red rather than turning cyan. The two together are the
   standard dark-PDF recipe.

   WHY THE WHOLE PAGE CONTAINER, NOT JUST THE CANVAS: on the annotator and the
   proctored-test viewer the student's ink is an SVG overlay (.ta-ink /
   .inklayer) sitting ON TOP of the page canvas, inside the same .ta-page /
   .cv-page box. Filtering only the canvas would leave dark ink on a now-dark
   page — invisible. Filtering the container inverts ink and paper together, so
   handwriting stays readable.

   SAFE FOR SUBMISSIONS: a CSS filter is paint-only. Export does not screenshot
   the DOM — mergeAnnotatedPdf() in assignment_theory_annotator.js and
   canvas_viewer.js re-merges the stored vector annotations onto the PRISTINE
   original PDF bytes via PDFLib. What the student hands in is unaffected by
   what they are looking at. Filters also do not move layout or hit-testing, so
   pen coordinates are untouched.

   The explicit `background: #fff` matters: these boxes must be white BEFORE the
   filter so they invert to dark. Remove it and a transparent box inverts to
   nothing.
   ───────────────────────────────────────────────────────────────────────── */
html[data-theme="dark"] .pv-page,
html[data-theme="dark"] .cv-page,
html[data-theme="dark"] .ta-page,
html[data-theme="dark"] .pv-thumb,
html[data-theme="dark"] .cv-thumb .frame,
html[data-theme="dark"] .ta-thumb .frame {
  background: #fff;
  filter: invert(1) hue-rotate(180deg);
}

/* PDF previews shown in an <iframe> (assignments, exams, past papers, study
   materials). The iframe's document belongs to the browser's own PDF plugin and
   cannot be styled from here, but the filter applies to the rendered box. */
html[data-theme="dark"] .preview-modal-body iframe,
html[data-theme="dark"] .sm-preview-modal iframe,
html[data-theme="dark"] .pp-preview-modal iframe,
html[data-theme="dark"] iframe[title="Assignment PDF preview"] {
  background: #fff;
  filter: invert(1) hue-rotate(180deg);
}

/* The page keeps a rim rather than a shadow, and the rim must sit OUTSIDE the
   filtered box or it inverts with it. */
html[data-theme="dark"] .pv-page,
html[data-theme="dark"] .cv-page,
html[data-theme="dark"] .ta-page {
  box-shadow: 0 2px 6px rgba(0, 0, 0, .45), 0 10px 30px rgba(0, 0, 0, .35);
}

/* ── White ink is the one colour the inversion gets wrong ────────────────────
   Inverting the page box flips paper and ink together, which is right for every
   ink EXCEPT white: #FFFFFF becomes #000000 on a paper that is now #000000 too,
   so the student writes and nothing appears. That is the "white doesn't work in
   dark mode" report.

   invert(1) hue-rotate(180deg) is its own inverse on pure white and pure black,
   so applying it AGAIN to just these paths cancels the ancestor filter exactly
   and they paint their true white on the dark page. Only the paint changes —
   the stored colour is still #FFFFFF, so mergeAnnotatedPdf() still hands in
   white ink on the pristine original.

   Marked by canvas_viewer.js / assignment_theory_annotator.js when the stroke's
   colour is WHITE_INK. */
html[data-theme="dark"] .cv-page .inklayer path.ink-white,
html[data-theme="dark"] .ta-page .ta-ink path.ink-white {
  filter: invert(1) hue-rotate(180deg);
}

/* ── The white swatch belongs to dark mode ───────────────────────────────────
   On white paper in light mode, white ink is invisible by definition — it only
   ever made sense over a dark diagram, and students kept losing whole answers
   to it. In dark mode the paper is dark and white is the natural colour to
   write in, which is where it earns its place in the toolbar.

   Done in CSS rather than JS so it follows the theme toggle live, and so the
   engines that build their swatches from a JS array and the one that hardcodes
   them in the template behave the same way. The JS side only has to move the
   selection off white when the theme goes light — see resetWhiteInkOnLight(). */
.cv-color.is-white,
.ta-color.is-white {
  display: none;
}
html[data-theme="dark"] .cv-color.is-white,
html[data-theme="dark"] .ta-color.is-white {
  display: inline-block;
}

/* ── The note editor is a mark on the paper, not a form field ────────────────
   `html[data-theme="dark"] textarea` above is meant for the portal's forms, and
   it catches these two by accident: the open note editor is a <textarea>, but it
   sits INSIDE the inverted page box. So it was painted --surface-2 grey, that
   grey then inverted to a pale slab sitting on the dark paper, and the box-shadow
   token — already darkened for dark mode — inverted into a white halo around it.
   That is the "text box has a weird grey background in dark mode" report.

   Inside the filter, the right appearance is the LIGHT one: the editor should
   look exactly as it does in light mode and let the page inversion carry it to
   dark, the same way the paper and every other mark on it are carried. Hence
   literal values rather than tokens — a token that re-points for dark mode is
   precisely what must not be used inside an inverted box.

   The typed colour is not set here: canvas_viewer.js / assignment_theory_
   annotator.js put the student's ink colour on ta.style.color inline, which
   already beats the rule above. */
html[data-theme="dark"] .cv-textedit {
  background-color: rgba(255, 255, 255, .96);
  border-color: #4d3e77;
  box-shadow: 0 4px 16px rgba(29, 27, 46, .14);
}
html[data-theme="dark"] .ta-textedit {
  background-color: rgba(255, 255, 255, .97);
  border-color: #4D3E77;
  box-shadow: 0 4px 16px rgba(29, 27, 46, .14);
}
/* The committed note reads back from the same box, so it gets the same
   treatment — otherwise a note looks one way while it is open and another the
   moment it is committed. */
html[data-theme="dark"] .cv-textbox,
html[data-theme="dark"] .ta-textbox {
  background-color: transparent;
  color: inherit;
}

/* Printing must never carry the inversion — it would produce a black page. */
@media print {
  html[data-theme="dark"] .pv-page,
  html[data-theme="dark"] .cv-page,
  html[data-theme="dark"] .ta-page,
  html[data-theme="dark"] .pv-thumb,
  html[data-theme="dark"] .cv-thumb .frame,
  html[data-theme="dark"] .ta-thumb .frame,
  html[data-theme="dark"] .preview-modal-body iframe,
  html[data-theme="dark"] .sm-preview-modal iframe,
  html[data-theme="dark"] .pp-preview-modal iframe {
    filter: none !important;
  }
}

/* Print always goes back to the light palette: a dark page prints as a black
   slab and empties the toner. */
@media print {
  html[data-theme="dark"] { color-scheme: light; }
  html[data-theme="dark"] body { background: #fff !important; color: #000 !important; }
  .theme-toggle { display: none !important; }
}
