/* ============================================================================================
   app-dark.css - premium dark theme for the LOGGED-IN app.

   Scoped entirely behind `body.app-dark`, so pages convert ONE AT A TIME: a page joins the new
   theme by adding that class and nothing else changes for the other authed pages. Drop the
   scoping only once all 8 (dashboard, exams, exam, history, history-detail, bookmarks,
   study-plan, profile, admin) have been converted and signed off.

   Ported from the reference app at "D:\UI Redesign\Dashboard" (React + Tailwind v4, default
   scale so 1 unit = 4px). Values below are decoded from that source, not eyeballed.

   Loaded AFTER styles.min.css so these rules win on equal specificity. Every selector carries
   the body.app-dark prefix, which also outranks the base single-class rules in styles.css.

   Related: [[app-content-rail]] - the shared rail lives in styles.css and is overridden here,
   never per page. [[csp-no-inline-handlers]] - no inline scripts; fonts are self-hosted because
   the CSP is font-src 'self' and Google Fonts would be refused.
   ============================================================================================ */

/* ---- 1. Typography ------------------------------------------------------------------------
   The reference pulls Instrument Sans + JetBrains Mono from Google Fonts. Our CSP is
   `font-src 'self' https://*.razorpay.com` and style-src has no fonts.googleapis.com, so both
   the stylesheet and the font files would be blocked. Self-hosted latin variable subsets
   (~30KB each) from the same Google build instead. */
@font-face {
  font-family: 'Instrument Sans';
  font-style: normal;
  font-weight: 400 700;                 /* variable: one file covers the whole range */
  font-display: swap;
  src: url('/fonts/instrument-sans-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
                 U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
                 U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url('/fonts/jetbrains-mono-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
                 U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
                 U+FEFF, U+FFFD;
}

body.app-dark {
  /* Palette. The reference is cyan -> indigo -> violet, a close cousin of the landing's
     brand/aqua/violet, so the two themes read as one product. */
  --ad-bg: #070A12;
  --ad-panel: #0A0E1A;
  --ad-cyan: #22d3ee;
  --ad-cyan-soft: #67e8f9;
  --ad-indigo: #6366f1;
  --ad-violet: #a855f7;
  --ad-violet-soft: #a78bfa;
  --ad-emerald: #34d399;
  --ad-amber: #fbbf24;
  --ad-rose: #f43f5e;

  --ad-text: #ffffff;
  --ad-text-2: rgba(255, 255, 255, .70);
  --ad-text-3: rgba(255, 255, 255, .45);
  --ad-text-4: rgba(255, 255, 255, .30);

  --ad-line: rgba(255, 255, 255, .08);
  --ad-line-soft: rgba(255, 255, 255, .06);

  /* .glass / .glass-soft from the reference's inline <style>. */
  --ad-glass: linear-gradient(180deg, rgba(255,255,255,.055), rgba(255,255,255,.025));
  --ad-glass-shadow: inset 0 0 0 1px rgba(0,0,0,.2), 0 20px 40px -24px rgba(0,0,0,.6);
  --ad-glass-soft: rgba(255, 255, 255, .03);

  /* Shell geometry. styles.css hardcodes 240/56 rather than using its own :root tokens, so
     both the literals and the tokens are overridden here to keep them from drifting apart. */
  --app-sidebar-width: 248px;
  --app-header-height: 64px;

  background: var(--ad-bg);
  color: var(--ad-text);
  font-family: 'Instrument Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;
  -webkit-font-smoothing: antialiased;
  overflow-x: clip;
}

/* Admin has wide operational tables, dense filter rows and chart cards. On tablets those surfaces
   pan as one workspace instead of being squeezed; normal user pages reflow instead and never scroll
   sideways.

   Gated to the tablet band deliberately. Ungated, the 1120px floor also applied on desktop, where
   `.app-main` is only (viewport - 248px) wide: measured 88px of horizontal scroll at a 1280px
   laptop and 2px at 1366px, on a page that had none before. Below 769px it is left off too - a
   phone is better served by today's reflow plus the internally scrolling `.admin-*-scroll` tables
   than by panning a 1120px layout. */
@media (min-width: 769px) and (max-width: 1024px) {
  html.admin-page-root,
  html.admin-page-root body.app-dark.app-admin {
    overflow-x: auto;
  }
  body.app-dark.app-admin .app-main {
    min-width: 1120px;
  }
}

/* The reference marks numeric/label text with a .mono class. We have no such hook in the
   existing markup, so the mono face is applied to the elements that are numeric by nature.
   .readiness-sub is NOT one of them and used to be listed here: it holds a sentence of coaching
   prose ("Getting closer - keep practicing"), which a monospace face renders as terminal output.
   The numbers that line used to sit beside now live in .readiness-stats, which belongs here. */
body.app-dark .sidebar-section-label,
body.app-dark .sidebar-user-role,
body.app-dark .stat-card .stat-sub,
body.app-dark .readiness-stats,
body.app-dark .domain-bar-pct,
body.app-dark .recent-attempt-date-row,
body.app-dark .recent-attempt-status,
body.app-dark .rec-label,
body.app-dark .exam-code-chip,
body.app-dark .tp-axis text {
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

body.app-dark ::selection { background: rgba(168, 85, 247, .40); }
body.app-dark :focus-visible { outline: 2px solid var(--ad-cyan); outline-offset: 2px; }

/* Thin scrollbars (reference .scroll-thin). */
body.app-dark .sidebar::-webkit-scrollbar,
body.app-dark .sidebar-nav::-webkit-scrollbar { width: 6px; height: 6px; }
body.app-dark .sidebar::-webkit-scrollbar-thumb,
body.app-dark .sidebar-nav::-webkit-scrollbar-thumb {
  background: rgba(255,255,255,.12); border-radius: 99px;
}
body.app-dark .sidebar::-webkit-scrollbar-track,
body.app-dark .sidebar-nav::-webkit-scrollbar-track { background: transparent; }

/* ---- 2. Ambient background ----------------------------------------------------------------
   Reference paints a fixed -z-10 layer: flat ink, a 48px masked grid, and two blurred colour
   blooms. Done with body::before/::after so no markup is needed. */
/* Grid lines at .020, not .035, and a near-flat mask instead of a top-anchored ellipse.
   The old `radial-gradient(ellipse at top, #000 20%, transparent 75%)` held the grid at FULL
   strength for the top third and faded it to literally nothing by the bottom of the viewport -
   measured contrast ran 5.0 / 5.0 / 5.0 / 4 / 3 / 2 / 1 / 0 down the screen. Because this layer
   is position:fixed the fade is anchored to the SCREEN, not the document, so cards slid through
   a static gradient as you scrolled and the texture read as inconsistent rather than ambient.
   Flat mask + lower alpha = the same texture everywhere, roughly 2.0 contrast, which is the
   "reads as texture, not as a pattern" target. Do not restore a steep mask here. */
body.app-dark::before {
  content: ''; position: fixed; inset: 0; z-index: -2; pointer-events: none;
  background-image:
    linear-gradient(to right, rgba(255,255,255,.020) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(255,255,255,.020) 1px, transparent 1px);
  background-size: 48px 48px;
  -webkit-mask-image: linear-gradient(180deg, #000, rgba(0,0,0,.85));
          mask-image: linear-gradient(180deg, #000, rgba(0,0,0,.85));
}
/* The exam page is the one focus surface in the app, and BOTH ambient layers come off for the whole
   of it - setup, question, review and results alike.
   The GRID is ambient depth behind card-dense product screens and earns nothing here. Worse, it is
   position:fixed, so on a long question the text scrolls over a STATIC texture, and it is loudest in
   exactly the empty regions this page leaves below the content. display:none rather than
   background-image:none so the fixed layer is not composited at all.
   The COLOUR BLOOMS are the two radials in ::after - indigo at 33% -160px and cyan off the right
   edge. On a page whose content is one centred column they land on empty canvas and read as patches
   rather than depth: a blue-purple smudge behind the header and a green-cyan glow on the far right.
   Only the flat veil survives, lighter than the shared one because there is no longer a grid
   underneath for it to knock back. For a little depth back, a single centred glow behind the card
   would go here - never one at an edge.
   Semantic colour is untouched: mode-card selection, correct/incorrect tints, progress fills, and
   pass/fail badges are all element-level and have nothing to do with these two layers. */
body.app-dark.app-exam::before { display: none; }
body.app-dark.app-exam::after {
  background: linear-gradient(180deg, rgba(6,8,15,.12), rgba(6,8,15,.12));
}

/* Plans gets the same treatment, for the same reason stated differently. The grid is ambient depth
   behind card-dense screens; Plans is four cards and then a long tail of quiet rows, so most of the
   viewport is empty canvas and the layer is position:fixed - the texture sits in exactly the regions
   that should be reading as calm. It also makes the page look like it belongs to the auth shell,
   which runs a 40px grid behind the sign-in panel, rather than to the app.
   The blooms go for the same reason as on the exam page: one centred column of content leaves the
   indigo radial behind the header and the cyan one off the right edge landing on nothing, which
   reads as a smudge and a green glow rather than as depth. The flat veil stays.
   Cards, colours, sidebar and layout are untouched - this is two ambient layers, nothing else. */
body.app-dark.app-plans::before { display: none; }
body.app-dark.app-plans::after {
  background: linear-gradient(180deg, rgba(6,8,15,.12), rgba(6,8,15,.12));
}

/* This layer sits at z-index -1, above the grid at -2 and below all content, so its last
   background layer doubles as the dark veil that knocks the grid back. Layer order matters:
   backgrounds paint first-listed on top, so the blooms stay at full strength and only the grid
   beneath is dimmed. The veil is FLAT (.30, not the .72 -> .90 ramp first tried): a graded veil
   is itself an unevenness, so it would have re-created the exact top-heavy problem upside down,
   and at .72+ combined with the lower line alpha the grid measured under 1.0 contrast - gone,
   not subtle. Flat .30 lands the grid at ~2.0 everywhere. */
body.app-dark::after {
  content: ''; position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background:
    radial-gradient(700px 500px at 33% -160px, rgba(99,102,241,.30), transparent 70%),
    radial-gradient(500px 500px at calc(100% + 160px) 50%, rgba(34,211,238,.20), transparent 70%),
    linear-gradient(180deg, rgba(6,8,15,.30), rgba(6,8,15,.30));
  filter: blur(0);   /* the gradients are already soft; blur() here would cost a full-page layer */
}

/* ---- 3. Sidebar ---------------------------------------------------------------------------
   Reference: fixed, 248px, #0A0E1A/90 + backdrop-blur-xl, 1px right border at white/7%. */
body.app-dark .sidebar {
  width: var(--app-sidebar-width);
  background: rgba(10, 14, 26, .90);
  -webkit-backdrop-filter: blur(24px);
          backdrop-filter: blur(24px);
  border-right: 1px solid rgba(255,255,255,.07);
  overflow: hidden;                       /* nav scrolls, not the whole rail */
  display: flex; flex-direction: column;
}
body.app-dark .sidebar-head,
body.app-dark .sidebar-brand {
  height: var(--app-header-height);       /* h-16, aligns exactly with the topbar rule */
  box-sizing: border-box;
  padding: 0 20px; display: flex; align-items: center; gap: 12px;
  border-bottom: 1px solid var(--ad-line-soft);
}
body.app-dark .sidebar-brand-text {
  font-size: 16px; font-weight: 600; letter-spacing: -.02em; color: #fff;
}
/* LEAVE THE WORDMARK ALONE. The reference tints "Grid" with a cyan->violet gradient, but the
   brand accent is #2dd4bf everywhere else on the site (styles.css .cg-grid-accent, and #34e0d0
   on the auth pages). Restyling it here made the sidebar logo disagree with every other page.
   Standing rule: never change the logo or the name. */
body.app-dark .sidebar-brand-text .cg-grid-accent {
  color: #2dd4bf; background: none; -webkit-text-fill-color: currentColor;
}
body.app-dark .sidebar-collapse-btn {
  margin-left: auto; width: 28px; height: 28px; border-radius: 8px;
  display: grid; place-items: center; background: transparent; border: 0;
  color: var(--ad-text-3); transition: color .2s, background .2s;
}
body.app-dark .sidebar-collapse-btn:hover { color: #fff; background: rgba(255,255,255,.05); }

/* nav.js emits a FLAT list - section labels and links are all siblings of .sidebar-nav, not
   wrapped per group. A single `gap` therefore spaces EVERY row equally, which is why a 20px group
   gap also pushed the individual menu items 20px apart and the rail read as mostly empty space.
   Item rhythm comes from the gap; group separation comes from the label's own margins.
   Measured on the reference: 4px item-to-item, and 43px across a group boundary, which is
   4 + label margin-top 12 + label height 15 + label margin-bottom 8 + 4. */
body.app-dark .sidebar-nav {
  flex: 1; overflow-y: auto; padding: 16px 12px; display: flex;
  flex-direction: column; gap: 4px;
}
body.app-dark .sidebar-section-label {
  font-size: 10px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--ad-text-4); padding: 0 12px; margin: 12px 0 8px;
}
body.app-dark .sidebar-nav > .sidebar-section-label:first-child { margin-top: 0; }
body.app-dark .sidebar-link {
  /* styles.css sets `margin: 2px 8px`; the 2px top+bottom stacked on top of the nav's gap and
     made every item sit 8px apart instead of 4. Rhythm is owned by the gap alone. */
  margin: 0;
  height: 40px; padding: 0 12px; border-radius: 12px; gap: 12px;
  display: flex; align-items: center;
  font-size: 13.5px; font-weight: 500; letter-spacing: -.01em;
  color: rgba(255,255,255,.55);
  border: 1px solid transparent;
  transition: color .2s, background .2s, border-color .2s, box-shadow .2s;
}
body.app-dark .sidebar-link:hover {
  color: #fff; background: rgba(255,255,255,.04); text-decoration: none;
}
/* TWO signals for one state, not four. This carried a cyan-to-violet gradient fill, a lit cyan
   border, an outer glow, a glowing dot at the row's end AND a cyan icon - five ways of saying
   "you are here", on the one row that needs the least persuading. What is left: a flat darker
   surface and a cyan left edge, plus the cyan icon that was already there. The gradient also put
   violet in the nav, which appears nowhere else on the dashboard any more. */
body.app-dark .sidebar-link.active {
  color: #fff;
  background: rgba(255,255,255,.05);
  border-color: transparent;
  box-shadow: inset 2px 0 0 var(--ad-cyan);
}
body.app-dark .sidebar-link svg { opacity: 1; color: rgba(255,255,255,.45); }
body.app-dark .sidebar-link:hover svg { color: rgba(255,255,255,.80); }
body.app-dark .sidebar-link.active svg { color: var(--ad-cyan-soft); }

body.app-dark .sidebar-card {
  margin: 0 12px 12px; padding: 14px; border-radius: 16px;
  background: var(--ad-glass); border: 1px solid var(--ad-line);
  box-shadow: var(--ad-glass-shadow);
  position: relative; overflow: hidden;
}
body.app-dark .sidebar-card.is-pro::before {
  content: ''; position: absolute; top: -32px; right: -32px; width: 96px; height: 96px;
  border-radius: 50%; background: rgba(52,211,153,.20); filter: blur(32px);
}
body.app-dark .sidebar-card-title { font-size: 13px; font-weight: 600; color: #fff; }
body.app-dark .sidebar-card-text { font-size: 11px; color: var(--ad-text-3); margin-top: 4px; }
body.app-dark .sidebar-pro-dot {
  width: 8px; height: 8px; border-radius: 50%; background: var(--ad-emerald);
  animation: ad-pulse-dot 2s ease infinite;
}
@keyframes ad-pulse-dot { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }

body.app-dark .sidebar-foot,
body.app-dark .sidebar-user {
  border-top: 1px solid var(--ad-line-soft); padding: 12px;
  display: flex; align-items: center; gap: 12px;
}
/* Initials, not a badge. This was a cyan-to-violet gradient disc with a white halo ring, which made
   the quietest corner of the sidebar its loudest element - and violet no longer appears anywhere
   else on the dashboard. A flat surface with a soft cyan edge keeps the initials legible while
   letting the nav above it lead. `border` is restated because styles.css sets a 1.5px white one. */
body.app-dark .sidebar-user-avatar {
  width: 36px; height: 36px; border-radius: 50%;
  background: rgba(34, 211, 238, .06);
  border: 1px solid rgba(34, 211, 238, .20);
  box-shadow: none;
  font-size: 11.5px; font-weight: 600; letter-spacing: .02em;
  color: var(--ad-cyan-soft);
  display: grid; place-items: center; flex-shrink: 0;
}
/* The name identifies the session; it is not a heading. Weight and colour both come down a step so
   it sits under the nav links rather than competing with them. */
body.app-dark .sidebar-user-name { font-size: 12.5px; font-weight: 500; color: var(--ad-text-2); }
body.app-dark .sidebar-user-role { font-size: 10px; color: var(--ad-text-4); }
body.app-dark .sidebar-logout {
  width: 32px; height: 32px; border-radius: 8px; display: grid; place-items: center;
  color: var(--ad-text-4); background: transparent;
}
/* Reachable on hover without being bright at rest, and no longer jumping to pure white. */
body.app-dark .sidebar-logout:hover { color: var(--ad-text-2); background: rgba(255,255,255,.05); }

body.app-dark .sidebar-overlay { background: rgba(0,0,0,.6); -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); }

/* ---- 4. Top bar ---------------------------------------------------------------------------
   Reference: sticky, h-16, #070A12/75 + blur, 1px bottom border at white/6%. */
body.app-dark .topbar {
  height: var(--app-header-height);
  background: rgba(7, 10, 18, .75);
  -webkit-backdrop-filter: blur(24px);
          backdrop-filter: blur(24px);
  border-bottom: 1px solid var(--ad-line-soft);
  box-shadow: none;
  left: var(--app-sidebar-width);
  display: flex; align-items: center; gap: 16px; padding: 0 32px;
}
/* Do NOT set `display` here. The base rule hides the hamburger on desktop and a media query
   turns it back on for mobile; declaring display:grid unconditionally overrode both and left it
   showing at 1440px next to an already-visible sidebar. Style only, centre via line-height. */
body.app-dark .topbar-hamburger {
  width: 36px; height: 36px; border-radius: 12px; padding: 0;
  background: var(--ad-glass-soft); border: 1px solid rgba(255,255,255,.07);
  color: var(--ad-text-2); text-align: center; line-height: 34px;
}
body.app-dark .topbar-hamburger:hover { background: rgba(255,255,255,.07); color: #fff; }

/* Topbar titles. The base sets #1a1a2e, which is all but invisible on the dark bar. */
body.app-dark .topbar-title {
  font-size: 15px; font-weight: 600; letter-spacing: -.01em; color: #fff;
}
body.app-dark .topbar-subtitle { font-size: 11px; color: rgba(255,255,255,.35); }

/* Search. nav.js sets `padding: 0 12px 0 32px` to clear its absolutely-positioned icon, so the
   left padding must be preserved - a flat `padding: 0 14px` put the placeholder under the icon.
   Reference geometry: h-10, rounded-full, pl-10. */
body.app-dark .topbar-search input[type="search"] {
  height: 40px; width: 256px; border-radius: 999px;
  padding: 0 16px 0 40px;
  background: var(--ad-glass-soft); border: 1px solid rgba(255,255,255,.07); color: #fff;
}
body.app-dark .topbar-search input[type="search"]::placeholder { color: rgba(255,255,255,.30); }
body.app-dark .topbar-search input[type="search"]:focus {
  background: rgba(255,255,255,.05); border-color: rgba(255,255,255,.12);
  box-shadow: 0 0 0 2px rgba(34,211,238,.30);
}
body.app-dark .topbar-search-ico { left: 15px; color: rgba(255,255,255,.35); }
body.app-dark .topbar-right, body.app-dark .topbar-actions {
  margin-left: auto; display: flex; align-items: center; gap: 10px;
}
body.app-dark .app-main {
  margin-left: var(--app-sidebar-width);
  padding-top: var(--app-header-height);
  background: transparent;
}

/* ---- 5. Shared surfaces -------------------------------------------------------------------
   Every white card in the light app becomes the same glass panel. Radius 16px (rounded-2xl). */
/* The glass gradient sits on an OPAQUE --ad-panel base rather than straight on the page. Panels
   used to be genuinely see-through, so the ambient grid ran underneath body copy wherever a card
   sat - the reason dense card areas read as noisy. The glass look is unchanged (same gradient,
   same border, same shadow); it just no longer transmits what is behind it. Only the last layer
   in a `background` shorthand may carry a colour, which is why --ad-panel goes last. */
body.app-dark .stat-card,
body.app-dark .section-card,
body.app-dark .recommended-card,
body.app-dark .exam-card,
body.app-dark .chart-container,
body.app-dark .readiness-widget,
body.app-dark .empty-state {
  background: var(--ad-glass), var(--ad-panel);
  border: 1px solid var(--ad-line);
  box-shadow: var(--ad-glass-shadow);
  border-radius: 16px;
  color: var(--ad-text-2);
}
body.app-dark .stat-card:hover,
body.app-dark .exam-card:hover { border-color: rgba(255,255,255,.15); }

body.app-dark h1, body.app-dark h2, body.app-dark h3, body.app-dark h4 { color: #fff; }
body.app-dark .page-header h1 { letter-spacing: -.03em; }
body.app-dark .section-label {
  font-size: 10px; letter-spacing: .16em; text-transform: uppercase; color: var(--ad-text-4);
}

/* Stat cards: 32px gradient numeral, uppercase label, mono sub, and a 3px accent rail on the
   left inset 16px top and bottom. The accent colour per card is set by the tone classes below. */
body.app-dark .stat-card {
  padding: 20px; position: relative; overflow: hidden; border-left: 1px solid var(--ad-line);
}
body.app-dark .stat-card::before {
  content: ''; position: absolute; left: 0; top: 16px; bottom: 16px; width: 3px;
  border-radius: 999px; background: linear-gradient(180deg, var(--ad-cyan), #0ea5e9);
}
body.app-dark .stat-card .stat-value,
body.app-dark .stat-card .stat-number {
  font-size: 32px; font-weight: 600; letter-spacing: -.04em;
  background: linear-gradient(135deg, var(--ad-cyan), #0ea5e9);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
body.app-dark .stat-card .stat-label {
  margin-top: 4px; font-size: 12px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--ad-text-2);
}
body.app-dark .stat-card .stat-sub { margin-top: 4px; font-size: 11px; color: var(--ad-text-4); }
/* Per-card accents by POSITION - dashboard only, where cards carry no accent class.
   Guarded with :not([class*="accent-"]) because history/admin set .accent-blue/-green/etc
   explicitly, and a bare :nth-child selector (0,4,1) outranks .accent-green (0,3,1), which
   silently painted a green card violet. */
body.app-dark .stat-grid > .stat-card:not([class*="accent-"]):nth-child(2)::before { background: linear-gradient(180deg, var(--ad-violet-soft), var(--ad-violet)); }
body.app-dark .stat-grid > .stat-card:not([class*="accent-"]):nth-child(2) .stat-value,
body.app-dark .stat-grid > .stat-card:not([class*="accent-"]):nth-child(2) .stat-number { background: linear-gradient(135deg, var(--ad-violet-soft), var(--ad-violet)); -webkit-background-clip: text; background-clip: text; }
body.app-dark .stat-grid > .stat-card:not([class*="accent-"]):nth-child(3)::before { background: linear-gradient(180deg, var(--ad-amber), #fb923c); }
body.app-dark .stat-grid > .stat-card:not([class*="accent-"]):nth-child(3) .stat-value,
body.app-dark .stat-grid > .stat-card:not([class*="accent-"]):nth-child(3) .stat-number { background: linear-gradient(135deg, var(--ad-amber), #fb923c); -webkit-background-clip: text; background-clip: text; }
body.app-dark .stat-grid > .stat-card:not([class*="accent-"]):nth-child(4)::before { background: linear-gradient(180deg, var(--ad-emerald), #14b8a6); }
body.app-dark .stat-grid > .stat-card:not([class*="accent-"]):nth-child(4) .stat-value,
body.app-dark .stat-grid > .stat-card:not([class*="accent-"]):nth-child(4) .stat-number { background: linear-gradient(135deg, var(--ad-emerald), #14b8a6); -webkit-background-clip: text; background-clip: text; }

/* ---- 6. Dashboard components --------------------------------------------------------------- */
body.app-dark .section-card { padding: 24px; }

/* Recommended next step */
body.app-dark .rec-icon {
  width: 48px; height: 48px; border-radius: 16px;
  background: linear-gradient(135deg, rgba(34,211,238,.20), rgba(168,85,247,.20));
  border: 1px solid rgba(255,255,255,.10);
  color: var(--ad-cyan-soft); display: grid; place-items: center; flex-shrink: 0;
}
body.app-dark .rec-label { font-size: 10px; letter-spacing: .12em; text-transform: uppercase; color: var(--ad-text-3); }
body.app-dark .rec-title { margin-top: 4px; font-size: 16px; font-weight: 600; letter-spacing: -.02em; color: #fff; }
body.app-dark .rec-desc { margin-top: 4px; font-size: 13px; color: var(--ad-text-3); }

/* Readiness by domain */
body.app-dark .domain-bar-label { color: var(--ad-text-2); font-size: 13px; font-weight: 400; }
body.app-dark .domain-bar-pct { color: rgba(255,255,255,.50); font-size: 12px; }
body.app-dark .domain-bar {
  height: 8px; border-radius: 999px; background: rgba(255,255,255,.06); overflow: hidden;
}
body.app-dark .domain-bar-fill {
  height: 100%; border-radius: 999px;
  background: linear-gradient(90deg, var(--ad-rose), #f87171);   /* weak (default) */
  transition: width .7s cubic-bezier(.16,1,.3,1);
}
body.app-dark .domain-bar-fill.mid,
body.app-dark .domain-bar-fill.medium { background: linear-gradient(90deg, var(--ad-amber), #fb923c); }
body.app-dark .domain-bar-fill.strong,
body.app-dark .domain-bar-fill.good { background: linear-gradient(90deg, var(--ad-emerald), #2dd4bf); }

/* Recent attempts */
body.app-dark .recent-attempt-row {
  padding: 14px 0; border-radius: 0; font-size: 14px;
  border-bottom: 1px solid rgba(255,255,255,.05);
}
body.app-dark .recent-attempt-row:last-child { border-bottom: 0; }
body.app-dark .recent-attempt-row:hover { background: transparent; }
body.app-dark .recent-attempt-name { color: var(--ad-cyan-soft); font-weight: 600; }
body.app-dark .recent-attempt-score { color: rgba(255,255,255,.80); font-weight: 500; }
body.app-dark .recent-attempt-date-row { font-size: 11px; color: var(--ad-text-4); }
body.app-dark .recent-attempt-status {
  font-size: 11px; padding: 4px 10px; border-radius: 999px;
  background: rgba(244,63,94,.15); color: #fda4af; border: 1px solid rgba(251,113,133,.20);
}
body.app-dark .recent-attempt-status.pass,
body.app-dark .recent-attempt-status.passed {
  background: rgba(52,211,153,.15); color: #6ee7b7; border-color: rgba(52,211,153,.25);
}

/* Readiness ring */
body.app-dark .readiness-ring .ring-bg { stroke: rgba(255,255,255,.08); }
/* Solid cyan, NOT url(#gradient): the ring SVG is emitted by dashboard.js and carries no <defs>,
   so a paint-server reference would resolve to nothing and the progress arc would not render. */
body.app-dark .readiness-ring .ring-fill { stroke: var(--ad-cyan); }
body.app-dark .readiness-ring .ring-text { fill: #fff; font-weight: 600; letter-spacing: -.03em; }
/* Three sizes for three jobs: the gap headline is the fact, the sub reads it, the trend row holds
   the raw numbers. The sub is a step brighter than it was - it is prose now that it no longer sits
   under a duplicate heading, and .30 white was too faint to read as a sentence. */
body.app-dark .readiness-label { color: #fff; font-size: 13.5px; }
body.app-dark .readiness-label.is-clear { color: #6ee7b7; }
body.app-dark .readiness-label.is-empty { color: var(--ad-text-2); font-weight: 500; }
body.app-dark .readiness-sub { color: var(--ad-text-3); font-size: 11.5px; }
/* Latest toward target. Cyan at low opacity, the same accent as the ring above it rather than a
   second colour, and no tick marks - the three figures underneath are the labels. */
body.app-dark .readiness-bar { background: rgba(255,255,255,.06); }
body.app-dark .readiness-bar-fill { background: rgba(34,211,238,.55); }
body.app-dark .readiness-bar-fill.is-clear { background: rgba(52,211,153,.55); }
/* The label is the quiet half and the number is the half being read, so they sit two steps apart
   rather than both landing in the middle. */
body.app-dark .rd-stat-lbl { color: var(--ad-text-4); font-size: 9px; letter-spacing: .1em; }
body.app-dark .rd-stat-val { color: rgba(255,255,255,.88); font-size: 12.5px; font-weight: 500; }

/* Exam cards */
body.app-dark .exam-card { padding: 20px; }
body.app-dark .exam-code-chip {
  background: rgba(255,255,255,.06); border: 1px solid var(--ad-line);
  color: var(--ad-cyan-soft); border-radius: 6px; font-size: 11px;
}
body.app-dark .exam-desc { color: var(--ad-text-3); }
body.app-dark .exam-metric-grid { color: var(--ad-text-2); }
body.app-dark .exam-progress-label, body.app-dark .exam-progress-empty { color: var(--ad-text-4); }

/* Trend chart */
body.app-dark .tp-line { stroke: var(--ad-cyan); stroke-width: 2.5; }   /* solid, same reason as .ring-fill */
body.app-dark .tp-axis text, body.app-dark .chart-container text { fill: rgba(255,255,255,.25); font-size: 9px; }
body.app-dark .chart-container line { stroke: rgba(255,255,255,.06); }

/* ---- 7. Controls --------------------------------------------------------------------------- */
/* Cyan/teal, not violet. .btn-primary is the PRODUCT action - Start Practice, Start Exam, Save
   Plan - and it appears on nearly every page, so violet everywhere made every button read as
   equally urgent and stopped signalling anything. Violet is now reserved for account/admin
   surfaces (see .pf-btn-primary and the admin rules), which is what makes it mean something.
   Ink is dark rather than white: on a #22d3ee -> #2dd4bf gradient white text fails contrast. */
body.app-dark .btn-primary {
  background: linear-gradient(90deg, var(--ad-cyan), #2dd4bf);
  border: 0; color: #052b33; border-radius: 999px; font-weight: 700;
  box-shadow: 0 0 24px -8px rgba(34,211,238,.55);
}
body.app-dark .btn-primary:hover { filter: brightness(1.06); }
/* Quiet primary: a form's Save/Update is the only meaningful action in its panel, so it needs to
   be findable, not loud. Outlined dark with cyan text - same family, a step down in weight. */
body.app-dark .btn-quiet {
  background: var(--ad-glass-soft); border: 1px solid rgba(34,211,238,.35);
  color: var(--ad-cyan-soft); border-radius: 999px; font-weight: 600; box-shadow: none;
}
body.app-dark .btn-quiet:hover {
  background: rgba(34,211,238,.10); border-color: rgba(34,211,238,.55); color: #a5f3fc;
  filter: none; transform: none;
}
body.app-dark .btn-secondary {
  background: var(--ad-glass-soft); border: 1px solid rgba(255,255,255,.07);
  color: var(--ad-text-2); border-radius: 999px;
}
body.app-dark .btn-secondary:hover { color: #fff; background: rgba(255,255,255,.07); }
body.app-dark select, body.app-dark input[type="text"], body.app-dark input[type="search"] {
  background: var(--ad-glass-soft); border: 1px solid rgba(255,255,255,.07);
  color: #fff; border-radius: 999px; height: 36px; padding: 0 14px; font-size: 13px;
}
body.app-dark select:focus, body.app-dark input:focus {
  outline: none; box-shadow: 0 0 0 2px rgba(34,211,238,.30);
}
body.app-dark select option { background: var(--ad-panel); color: #fff; }
body.app-dark .loading { color: var(--ad-text-3); }

/* ---- 8. Responsive ------------------------------------------------------------------------- */
/* The shell offsets may only be zeroed where the sidebar is actually off-canvas, and styles.css
   takes it off-canvas at 768px. This block used to run at 1024px, so across 769-1024 - every
   tablet portrait width - the 248px rail stayed fully on screen while the content was laid out as
   if it had gone, and the sidebar painted over the first column of every app page. Measured at
   810/834/1024: sidebar right edge 248, `.app-main` margin-left 0, cards starting at x=32. */
@media (max-width: 768px) {
  body.app-dark .app-main { margin-left: 0; }
  body.app-dark .topbar { left: 0; padding: 0 16px; }
}

/* Tablet (769-1024). A 248px rail costs a third of an 810px viewport, so the shell defaults to the
   72px icon rail: nav.js puts `.app-tablet` on the body and forces `.sidebar-collapsed`, and the
   content is offset by the RAIL rather than by the full sidebar.

   Both offsets are pinned for the whole band, in the collapsed state AND with the drawer open, so
   expanding the sidebar floats it over the page instead of reflowing everything underneath it.
   The doubled class carries this to (0,3,1) so it outranks the unconditional
   `body.app-dark .app-main { margin-left: var(--app-sidebar-width) }` in section 6 on specificity
   rather than on source order - see the cascade traps this file has already been bitten by. */
@media (min-width: 769px) and (max-width: 1024px) {
  body.app-dark.app-tablet .app-main { margin-left: 72px; }
  body.app-dark.app-tablet .topbar { left: 72px; padding: 0 20px; }
  /* Open, the rail is a drawer standing above the page, so it needs an edge the flat rail does
     not: the backdrop dims the content and the shadow lifts the panel off it. */
  body.app-dark.app-tablet.sidebar-drawer-open .sidebar {
    box-shadow: 0 24px 60px -18px rgba(0, 0, 0, .75);
  }
}
@media (max-width: 640px) {
  body.app-dark .stat-card { padding: 16px; }
  body.app-dark .section-card { padding: 18px; }
}

/* ---- 9. Exams / catalog page ----------------------------------------------------------------
   Ported from "D:\UI Redesign\Exams". Shared surfaces (.exam-card, .empty-state, buttons, inputs)
   already come from section 5; this covers only what is unique to the catalog. */
body.app-dark .catalog-page-head h1 { font-size: 36px; letter-spacing: -.03em; line-height: 1.15; }
/* Matched to the dashboard's .section-label, which stopped shouting in the same pass: sentence
   case, 11.5px, near-zero tracking.
   `!important` on font-size and colour is not decoration - the base rule in styles.css declares
   both with !important, so the values this rule previously set (10px, --ad-text-4) were being
   discarded and the eyebrow was rendering at the light theme's 11px and --label-color the whole
   time. An !important in a base stylesheet cannot be beaten by specificity, only matched. */
body.app-dark .catalog-eyebrow {
  font-size: 11.5px !important; font-weight: 600; letter-spacing: .01em;
  text-transform: none; color: var(--ad-text-3) !important;
}

/* The three KPI pills are out of the markup, and with them a glass panel, a gradient numeral and a
   mono caption each - nine ingredients for three numbers, two of which were the same number (total
   Exams and Showing are identical until you filter). The count that mattered is the "Showing 1-12
   of N exams" line under the filters.

   The filter panel keeps a surface but stops being the heaviest block on the page: no glass
   gradient, no drop shadow, a softer border, less padding. It was built from the same
   `var(--ad-glass), var(--ad-panel)` + --ad-glass-shadow recipe as an exam card, which put the
   controls at the same visual weight as the twelve results they filter. */
body.app-dark .catalog-toolbar {
  background: rgba(255,255,255,.02);
  border: 1px solid var(--ad-line-soft);
  box-shadow: none; border-radius: 14px;
  padding: 11px 14px;
}
body.app-dark .catalog-toolbar .category-filter { margin-bottom: 0; }
/* The search row and the chip row are two bands of a single control, so the gap between them is
   tighter than the panel's own padding. styles.css sets 12px here for the light theme. */
body.app-dark .catalog-search-row { margin-bottom: 10px; }
/* The result count. A plain muted row between the filters and the grid, with no surface of its own -
   it used to be a bordered bar nested inside the bordered panel. */
body.app-dark .catalog-status {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; margin: 11px 2px 16px;
}

/* The catalogue's scale, under the sub-head. Muted body text, not a statistic: this is the pair of
   figures worth keeping from the three KPI pills the page used to lead with, and putting them in a
   sentence is the whole point of having removed the pills. */
body.app-dark .catalog-count-line {
  margin: 6px 0 0;
  font-size: 12.5px; letter-spacing: .01em; color: var(--ad-text-4);
}
body.app-dark .exam-search, body.app-dark .exam-search-wrap input {
  background: var(--ad-glass-soft); border: 1px solid rgba(255,255,255,.07);
  color: #fff; border-radius: 999px; height: 40px;
}
body.app-dark .exam-search::placeholder { color: rgba(255,255,255,.30); }

/* Category chips. ONE is cyan; the rest are quiet outlines. At fifteen chips wrapping to two rows,
   a filled background on every one of them was most of what made this panel read as heavy - so
   inactive is a bare outline on the page surface rather than a filled pill.
   Active drops the cyan-to-teal gradient and the outer glow. A flat cyan fill already says
   "selected" unambiguously, and it is the only cyan in the row. */
body.app-dark .category-chip {
  height: 30px; padding: 0 13px; border-radius: 999px;
  font-size: 12.5px; font-weight: 500; line-height: 28px;
  background: none; border: 1px solid var(--ad-line);
  color: var(--ad-text-3); transition: color .2s, background .2s, border-color .2s;
}
body.app-dark .category-chip:hover {
  color: #fff; background: rgba(255,255,255,.05); border-color: rgba(255,255,255,.14);
}
body.app-dark .category-chip.active {
  background: var(--ad-cyan); color: #071018;
  border-color: transparent; font-weight: 600; box-shadow: none;
}
body.app-dark .category-chip.active .chip-count { color: rgba(7,16,24,.70); }

body.app-dark .catalog-showing, body.app-dark .catalog-footer,
body.app-dark .catalog-refreshing { color: var(--ad-text-4); font-size: 12px; }
body.app-dark .catalog-refreshing-dot { background: var(--ad-cyan); }
body.app-dark .domain-name { color: var(--ad-text-2); }
body.app-dark .domain-q-count { color: var(--ad-text-4); }
body.app-dark .header-filter, body.app-dark .category-filter { color: var(--ad-text-3); }

/* Case-study modal - it renders over the catalog, so it needs the dark treatment too. */
body.app-dark .cs-modal, body.app-dark .casestudy-panel, body.app-dark .modal {
  background: var(--ad-panel); border: 1px solid var(--ad-line);
  border-radius: 16px; color: var(--ad-text-2);
}
body.app-dark .cs-modal-head, body.app-dark .casestudy-head {
  border-bottom: 1px solid var(--ad-line-soft); color: #fff;
}
body.app-dark .cs-q-text, body.app-dark .casestudy-h { color: #fff; }
body.app-dark .cs-opts li, body.app-dark .cs-block {
  background: rgba(255,255,255,.03); border: 1px solid var(--ad-line); border-radius: 10px;
}
body.app-dark .cs-tick { color: var(--ad-emerald); }
body.app-dark .casestudy-tag, body.app-dark .cs-key {
  background: rgba(255,255,255,.06); color: var(--ad-cyan-soft); border-radius: 6px;
}
body.app-dark .cs-modal-close { color: var(--ad-text-3); background: transparent; }
body.app-dark .cs-modal-close:hover { color: #fff; }

/* ---- 10. Review feedback (2026-08-17) --------------------------------------------------------
   From a review of the deployed dashboard. Each item here is a real defect, not a preference. */

/* 10a. The white header bars. .section-card h3 keeps `background:#f8f9fc; color:#1a1a2e` from the
   light theme - section 5 restyled the panel but not its header, so "Readiness by Domain" and
   "Recent Attempts" rendered as bright blocks that broke the dark UI on sight.
   Checked against the live reference: the title is PLAIN TEXT inside the padded glass panel -
   no header bar, no divider rule, no separate fill. An earlier pass here gave it a full-bleed
   header with a border-bottom, which the reference does not have. */
body.app-dark .section-card h3,
body.app-dark .section-card h3.section-card-header,
body.app-dark .admin-container .section-card h3 {
  background: transparent;
  color: #fff;
  border: 0; border-radius: 0;
  padding: 0 0 16px;
  min-height: 0;
  font-size: 16px; font-weight: 600; letter-spacing: -.01em;
}
body.app-dark .section-card { padding: 24px; }

/* 10b. Narrower rail. 1600px left the content stretched on wide screens; the reference sits far
   tighter. Overriding the shared tokens keeps [[app-content-rail]] intact - still one rail, still
   centred, just narrower for the redesigned pages. */
body.app-dark {
  --app-content-max: 1360px;
  --app-content-wide-max: 1360px;
  --app-max: 1360px;
  --app-max-wide: 1360px;
  --app-max-narrow: 1360px;
}

/* 10c. Domain bar colours. dashboard.js emits .bar-red/.bar-orange/.bar-green - NOT the .mid/
   .strong names section 6 guessed at - so every bar fell through to the red default and the whole
   panel read as "everything is broken". Thresholds moved to red <40 / amber 40-69 / green 70+ in
   dashboard.js to match. */
body.app-dark .domain-bar-fill.bar-red {
  background: linear-gradient(90deg, var(--ad-rose), #fb7185);
}
body.app-dark .domain-bar-fill.bar-orange {
  background: linear-gradient(90deg, var(--ad-amber), #fb923c);
}
body.app-dark .domain-bar-fill.bar-green {
  background: linear-gradient(90deg, var(--ad-emerald), #2dd4bf);
}

/* 10d. Recent attempts: clearer rows, no bright rules between them. */
body.app-dark .recent-attempt-row {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 0; border-bottom: 1px solid rgba(255,255,255,.045);
}
body.app-dark .recent-attempt-row:last-of-type { border-bottom: 0; }
body.app-dark .recent-attempt-date-row {
  background: transparent; border: 0; padding: 0; margin-top: 2px;
  font-size: 11px; color: var(--ad-text-4);
}
body.app-dark .recent-attempts-mini { border: 0; background: transparent; }

/* 10e. Sidebar density. The Pro card and the profile row were the heaviest things in the rail. */
body.app-dark .sidebar-card { padding: 11px 12px; border-radius: 14px; }
body.app-dark .sidebar-card-title { font-size: 12.5px; }
body.app-dark .sidebar-card-text { font-size: 10.5px; margin-top: 2px; }
body.app-dark .sidebar-foot, body.app-dark .sidebar-user { padding: 10px 12px; }
body.app-dark .sidebar-user-avatar { width: 32px; height: 32px; font-size: 11px; }
body.app-dark .sidebar-user-name { font-size: 12.5px; }

/* ---- 11. Shared: data tables, badges, pagination, row actions --------------------------------
   Used by history AND admin (and parts of profile), so these live in a shared section rather than
   being duplicated per page. Ported from "D:\UI Redesign\Attempt": the table sits inside a
   `rounded-2xl glass overflow-hidden` panel with hairline row rules, not a white sheet. */
body.app-dark .data-table { border-collapse: collapse; }
body.app-dark .data-table th {
  background: rgba(255,255,255,.035);
  color: var(--ad-text-4);
  border-bottom: 1px solid var(--ad-line);
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 10px; letter-spacing: .12em; text-transform: uppercase;
  padding: 12px 16px;
}
/* A STICKY header has to be opaque: a translucent wash lets the rows passing underneath read
   through the column labels. The tint is the panel's glass (~5.5%) plus the header's own 3.5%,
   flattened onto the opaque panel base so it matches the surrounding surface. */
body.app-dark .data-table-sticky thead th {
  background: linear-gradient(rgba(255,255,255,.085), rgba(255,255,255,.085)), var(--ad-panel);
}
body.app-dark .data-table td {
  border-bottom: 1px solid rgba(255,255,255,.045);
  color: var(--ad-text-2); padding: 13px 16px;
}
body.app-dark .data-table tr:last-child td { border-bottom: 0; }
body.app-dark .data-table tr:hover td { background: rgba(255,255,255,.03); }
body.app-dark .data-table .cell-primary { color: #fff; font-weight: 600; }
body.app-dark .data-table .cell-secondary { color: var(--ad-text-4); }
body.app-dark .data-table .cell-dot { color: rgba(255,255,255,.25); }
body.app-dark .table-meta, body.app-dark .stat-note, body.app-dark .stat-help,
body.app-dark .filter-summary { color: var(--ad-text-4); }
/* The table lives in a .section-card, which pads itself; a table wants to reach the panel edge. */
body.app-dark .section-card > .table-scroll-wrap { padding-left: 0; padding-right: 0; }

/* Badges. The light theme uses pale tints on dark ink; inverted here to a tinted wash with a
   matching border, which is how every status pill in the reference is built. */
body.app-dark .badge {
  border-radius: 999px; font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 11px; padding: 4px 10px; border: 1px solid transparent;
}
body.app-dark .badge-pass, body.app-dark .badge-correct {
  background: rgba(52,211,153,.15); color: #6ee7b7; border-color: rgba(52,211,153,.25);
}
body.app-dark .badge-fail, body.app-dark .badge-incorrect {
  background: rgba(244,63,94,.15); color: #fda4af; border-color: rgba(251,113,133,.20);
}
body.app-dark .badge-info {
  background: rgba(34,211,238,.13); color: var(--ad-cyan-soft); border-color: rgba(34,211,238,.22);
}
body.app-dark .badge-warning {
  background: rgba(251,191,36,.14); color: #fcd34d; border-color: rgba(251,191,36,.24);
}
body.app-dark .badge-neutral {
  background: rgba(255,255,255,.06); color: var(--ad-text-3); border-color: var(--ad-line);
}

/* Stat-card accents. history/admin set an explicit .accent-* class instead of relying on order,
   so the dashboard's nth-child accents do not reach them. */
body.app-dark .stat-card.accent-blue::before   { background: linear-gradient(180deg, var(--ad-cyan), #0ea5e9); }
body.app-dark .stat-card.accent-purple::before { background: linear-gradient(180deg, var(--ad-violet-soft), var(--ad-violet)); }
body.app-dark .stat-card.accent-orange::before { background: linear-gradient(180deg, var(--ad-amber), #fb923c); }
body.app-dark .stat-card.accent-green::before  { background: linear-gradient(180deg, var(--ad-emerald), #14b8a6); }
body.app-dark .stat-card.accent-blue   .stat-value { background: linear-gradient(135deg, var(--ad-cyan), #0ea5e9); -webkit-background-clip: text; background-clip: text; color: transparent; }
body.app-dark .stat-card.accent-purple .stat-value { background: linear-gradient(135deg, var(--ad-violet-soft), var(--ad-violet)); -webkit-background-clip: text; background-clip: text; color: transparent; }
body.app-dark .stat-card.accent-orange .stat-value { background: linear-gradient(135deg, var(--ad-amber), #fb923c); -webkit-background-clip: text; background-clip: text; color: transparent; }
body.app-dark .stat-card.accent-green  .stat-value { background: linear-gradient(135deg, var(--ad-emerald), #14b8a6); -webkit-background-clip: text; background-clip: text; color: transparent; }
body.app-dark .stat-icon { color: var(--ad-text-3); }

/* Row action menus (the per-row "..." popovers). */
body.app-dark .row-action-trigger { color: var(--ad-text-3); background: transparent; border-radius: 8px; }
body.app-dark .row-action-trigger:hover { color: #fff; background: rgba(255,255,255,.06); }
body.app-dark .row-action-menu {
  background: var(--ad-panel); border: 1px solid var(--ad-line);
  border-radius: 12px; box-shadow: 0 24px 48px -20px rgba(0,0,0,.8);
  color: var(--ad-text-2); overflow: hidden;
}
body.app-dark .row-action-menu button, body.app-dark .row-action-menu a { color: var(--ad-text-2); }
body.app-dark .row-action-menu button:hover, body.app-dark .row-action-menu a:hover {
  background: rgba(255,255,255,.05); color: #fff;
}
body.app-dark .row-action-sep { background: var(--ad-line-soft); }
body.app-dark .action-danger, body.app-dark .btn-danger { color: #fda4af; }
body.app-dark .btn-danger {
  background: rgba(244,63,94,.14); border: 1px solid rgba(251,113,133,.22); border-radius: 999px;
}
body.app-dark .btn-danger:hover { background: rgba(244,63,94,.22); color: #fff; }

/* Pagination */
body.app-dark .history-footer, body.app-dark .catalog-footer {
  border-top: 1px solid var(--ad-line-soft); color: var(--ad-text-4);
}
body.app-dark .page-active {
  background: linear-gradient(90deg, #22d3ee, #5eead4); color: #071018;
  border-radius: 8px; font-weight: 600;
}
body.app-dark .page-ellipsis { color: var(--ad-text-4); }
body.app-dark .footer-sep { color: rgba(255,255,255,.2); }

/* Modals - shared by history, admin, study-plan and profile. */
body.app-dark .modal-overlay { background: rgba(0,0,0,.65); -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); }
body.app-dark .modal {
  background: var(--ad-panel); border: 1px solid var(--ad-line);
  border-radius: 16px; color: var(--ad-text-2);
  box-shadow: 0 40px 80px -32px rgba(0,0,0,.9);
}
body.app-dark .modal h2, body.app-dark .modal h3 { color: #fff; }
body.app-dark .modal-desc { color: var(--ad-text-3); }
body.app-dark .modal-actions { border-top: 1px solid var(--ad-line-soft); }

/* First-run exam-goal modal.
   The shared overlay dims to 65% black behind a 4px blur, which on this one hides the app almost
   completely at the moment a new user is deciding whether to engage with it. Lighter dim and a
   softer blur keep the product visible behind the choice, and the panel is lifted off --ad-panel so
   the modal reads as raised rather than as part of the background. */
body.app-dark .modal-overlay.onb-overlay,
body.app-dark .modal-overlay:has(.modal.is-onboarding) {
  background: rgba(3,6,14,.52); -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
}
body.app-dark .modal.is-onboarding {
  background: #10162a; border: 1px solid rgba(255,255,255,.14);
  box-shadow: 0 40px 90px -30px rgba(0,0,0,.85);
}
body.app-dark .onb-sub { color: var(--ad-text-2); }
body.app-dark .onb-search {
  background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.18); color: #fff;
}
body.app-dark .onb-search::placeholder { color: var(--ad-text-3); }
body.app-dark .onb-search:focus-visible, body.app-dark .onb-search:focus {
  border-color: var(--ad-cyan); box-shadow: 0 0 0 3px rgba(34,211,238,.22);
}
/* The sticky label needs a solid ground of its own, or rows scroll through it. */
/* .58 rather than --ad-text-3 (.45): this heads the list a new user is reading, so it should
   register as a label rather than fade into the panel. */
body.app-dark .onb-group-label { color: rgba(255,255,255,.58); background: #10162a; }
body.app-dark .onb-results { scrollbar-color: rgba(255,255,255,.22) transparent; }
body.app-dark .onb-results::-webkit-scrollbar-thumb { background: rgba(255,255,255,.22); background-clip: content-box; }
body.app-dark .onb-results::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,.34); background-clip: content-box; }
body.app-dark .onb-empty { color: var(--ad-text-3); }
body.app-dark .onb-pick {
  background: rgba(255,255,255,.045); border: 1px solid rgba(255,255,255,.12); color: #fff;
}
body.app-dark .onb-pick:hover {
  background: rgba(34,211,238,.09); border-color: rgba(34,211,238,.45);
}
body.app-dark .onb-pick:focus-visible { outline: 2px solid var(--ad-cyan); outline-offset: 2px; }
body.app-dark .onb-pick-sub { color: var(--ad-text-2); }
/* .52 rather than --ad-text-3 (.45): quiet enough to stay secondary, but clear of the 4.5:1 line
   that .45 lands a hair under on this panel. */
body.app-dark .onb-count { color: rgba(255,255,255,.52); }
body.app-dark .onb-badge { color: #0b1220; background: var(--ad-cyan-soft); }
/* Teal-700 resting, teal-500 on hover: one hue getting brighter, and both clear AA with their own
   label (5.47:1 white on #0f766e, 6.6:1 dark on #14b8a6). #0d9488 with white was 3.74:1, which a
   12px bold label does not get a large-text exemption for. */
body.app-dark .onb-start { background: #0f766e; color: #fff; }
body.app-dark .onb-pick:hover .onb-start { background: #14b8a6; color: #04231f; }
body.app-dark .onb-pick.is-busy .onb-start { background: rgba(255,255,255,.16); color: var(--ad-text-2); }
body.app-dark .onb-foot { border-top: 1px solid var(--ad-line); }
/* The global dark .btn-secondary sits at a .07-alpha border, which is near-invisible on this panel.
   Lift it here only: this is the escape hatch for anyone whose exam is not in the top five. */
body.app-dark .onb-foot .btn-secondary {
  background: rgba(255,255,255,.08); border-color: rgba(255,255,255,.28); color: #fff; font-weight: 600;
}
body.app-dark .onb-foot .btn-secondary:hover {
  background: rgba(34,211,238,.14); border-color: rgba(34,211,238,.5); color: var(--ad-cyan-soft);
}
body.app-dark .onb-skip { color: rgba(255,255,255,.52); }
body.app-dark .onb-skip:hover { color: var(--ad-text-2); }

/* ---- 12. History / Attempts page ------------------------------------------------------------ */
body.app-dark .hist-score { color: #fff; font-weight: 600; }
body.app-dark .hist-score-max { color: var(--ad-text-4); }
body.app-dark .history-toolbar, body.app-dark .header-filter { color: var(--ad-text-3); }
/* The History zero state is .saved-empty-shell now, so .history-preview / .history-empty-panel /
   .hp-* / .history-empty-ico are no longer emitted and their dark rules went with the markup.
   .es-rows below is what dresses that panel instead. */
body.app-dark .history-footer-left,
body.app-dark .history-footer-right { color: var(--ad-text-4); }

/* ---- 13. Saved / bookmarks ------------------------------------------------------------------
   NOTE on the reference: "D:\UI Redesign\Saved" is a different, older mock (its own nav, white
   logo tile, zinc palette) rather than a page of the Dashboard/Exams/Attempt system. Importing it
   verbatim would put a second visual language inside the same app, so this page uses the
   established app-dark tokens - glass panels, 16px radius, the same badges and controls. */
body.app-dark .bookmarks-filterbar,
body.app-dark .bookmarks-controls,
body.app-dark .bookmark-card,
body.app-dark .empty-state-card {
  background: var(--ad-glass);
  border: 1px solid var(--ad-line);
  box-shadow: var(--ad-glass-shadow);
  border-radius: 16px;
  color: var(--ad-text-2);
}
body.app-dark .bookmark-card-header { border-bottom: 1px solid var(--ad-line-soft); color: #fff; }
body.app-dark .bookmark-q-num {
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 11px; letter-spacing: .1em; color: var(--ad-cyan-soft);
}
body.app-dark .bookmark-q-domain {
  background: rgba(255,255,255,.06); border: 1px solid var(--ad-line);
  border-radius: 999px; color: var(--ad-text-3); font-size: 11px;
}
/* Inner blocks were all #f8fafc; they become a faint wash so the card still reads as one panel. */
body.app-dark .bookmark-q-text,
body.app-dark .bookmark-options,
body.app-dark .bookmark-explanation,
body.app-dark .bookmark-limit-info,
body.app-dark .bookmarks-exam-label,
body.app-dark .bookmark-statements {
  background: rgba(255,255,255,.03);
  border: 1px solid var(--ad-line);
  border-radius: 12px;
  color: var(--ad-text-2);
}
body.app-dark .bookmark-q-text { color: #fff; }
body.app-dark .bookmark-explanation-wrap { color: var(--ad-text-2); }
body.app-dark .bookmark-statement { border-bottom: 1px solid rgba(255,255,255,.045); }
body.app-dark .opt-indicator {
  border: 1px solid rgba(255,255,255,.18); background: transparent; color: var(--ad-text-3);
}
/* The ordering block that used to sit here was dead: every property was re-declared in the
   `/* Ordering */` block further down, which is the one that actually paints. Removed rather
   than left as a decoy - it cost time this session. See section 63. */
body.app-dark .btn-remove-bookmark { color: var(--ad-text-3); background: transparent; }
body.app-dark .btn-remove-bookmark:hover { color: #fda4af; background: rgba(244,63,94,.12); }
body.app-dark .bookmarks-filter-count { color: var(--ad-text-4); }
body.app-dark .es-icon { color: var(--ad-text-4); }
/* Saved Questions zero state: helper bullets + action row. */
body.app-dark .es-points li { color: var(--ad-text-3); }
body.app-dark .es-points li::before { background: var(--ad-cyan); opacity: .55; }

/* Form controls used across bookmarks / study-plan / profile / admin. */
body.app-dark .form-input, body.app-dark select.form-input, body.app-dark textarea.form-input {
  background: var(--ad-glass-soft); border: 1px solid rgba(255,255,255,.07);
  color: #fff; border-radius: 10px;
}
body.app-dark .form-input::placeholder { color: rgba(255,255,255,.30); }
body.app-dark .form-input:focus {
  outline: none; border-color: rgba(255,255,255,.14);
  box-shadow: 0 0 0 2px rgba(34,211,238,.30);
}
body.app-dark .form-group label, body.app-dark .form-row label { color: var(--ad-text-3); }
body.app-dark .form-message { color: var(--ad-text-3); }
/* Pill radius is right for the topbar search but wrong for a stacked form field. */
body.app-dark .form-input.bookmarks-search,
body.app-dark .form-input.bookmarks-exam-select,
body.app-dark .form-input.bookmarks-domain-select { border-radius: 999px; height: 38px; }

/* 13a. Two rules that outrank the section-13 defaults and kept their light fills:
   `.order-item.correct-position .order-num` (0,3,0) and the shared
   `.bookmark-option, .bookmark-statement` block. Matched at higher specificity here. */
body.app-dark .bookmark-option,
body.app-dark .bookmark-statement {
  background: rgba(255,255,255,.03);
  border: 1px solid var(--ad-line);
  border-radius: 10px;
  color: var(--ad-text-2);
}
body.app-dark .order-item.correct-position .order-num {
  background: rgba(52,211,153,.18); color: #6ee7b7; border-color: rgba(52,211,153,.30);
}
body.app-dark .order-item.wrong-position .order-num {
  background: rgba(244,63,94,.16); color: #fda4af; border-color: rgba(251,113,133,.26);
}
body.app-dark .order-review-col .order-num {
  background: rgba(255,255,255,.06); color: var(--ad-text-2); border-color: var(--ad-line);
}

/* ---- 14. Study Plan --------------------------------------------------------------------------
   Ported from "D:\UI Redesign\Study Planner". The .sp-* rules live in an inline <style> block in
   study-plan.html (60 of them), not styles.css - they are single-class, so the body.app-dark
   prefix outranks them regardless of source order. */
body.app-dark .sp-stat, body.app-dark .sp-summary,
body.app-dark .sp-insight-panel, body.app-dark .sp-calendar-layout {
  background: var(--ad-glass); border: 1px solid var(--ad-line);
  box-shadow: var(--ad-glass-shadow); border-radius: 16px; color: var(--ad-text-2);
}
body.app-dark .sp-section-title, body.app-dark .sp-stat-title,
body.app-dark .sp-insight-title, body.app-dark .sp-stat-value, body.app-dark .sp-tile-val {
  color: #fff;
}
body.app-dark .sp-section-subtitle, body.app-dark .sp-stat-label, body.app-dark .sp-stat-meta,
body.app-dark .sp-insight-label, body.app-dark .sp-insight-note, body.app-dark .sp-tile-lbl,
body.app-dark .sp-legend, body.app-dark .sp-track-sub, body.app-dark .sp-val-muted,
body.app-dark .sp-pill-sub, body.app-dark .sp-insight-kicker { color: var(--ad-text-4); }

/* Status tints: pale-on-ink becomes a wash with a matching border, as elsewhere in the theme. */
body.app-dark .sp-stat-badge.good, body.app-dark .sp-track-status.good {
  background: rgba(52,211,153,.15); color: #6ee7b7; border-color: rgba(52,211,153,.28);
}
body.app-dark .sp-stat-badge.warn, body.app-dark .sp-track-status.warn {
  background: rgba(251,191,36,.15); color: #fcd34d; border-color: rgba(251,191,36,.26);
}
body.app-dark .sp-stat-badge.danger, body.app-dark .sp-track-status.danger {
  background: rgba(244,63,94,.15); color: #fda4af; border-color: rgba(251,113,133,.26);
}
body.app-dark .sp-stat-badge.neutral, body.app-dark .sp-track-status.neutral {
  background: rgba(255,255,255,.06); color: var(--ad-text-2); border-color: var(--ad-line);
}
body.app-dark .sp-val-good { color: #6ee7b7; }

/* Progress + domain bars share the dashboard's track/fill language. */
body.app-dark .sp-progress, body.app-dark .sp-domain-bar, body.app-dark .sp-mini-bar {
  background: rgba(255,255,255,.06); border-radius: 999px;
}
body.app-dark .sp-progress-fill, body.app-dark .sp-domain-fill, body.app-dark .sp-mini-bar-top {
  background: linear-gradient(90deg, var(--ad-cyan), var(--ad-violet-soft)); border-radius: 999px;
}

/* Exam switcher pills */
body.app-dark .sp-pill {
  background: var(--ad-glass-soft); border: 1px solid rgba(255,255,255,.07);
  border-radius: 12px; color: var(--ad-text-2);
}
body.app-dark .sp-pill:hover { background: rgba(255,255,255,.06); color: #fff; }
body.app-dark .sp-pill.active {
  background: linear-gradient(90deg, rgba(34,211,238,.15), rgba(168,85,247,.15));
  border-color: rgba(34,211,238,.30); box-shadow: 0 0 24px -8px rgba(34,211,238,.4); color: #fff;
}
body.app-dark .sp-pill-code { color: var(--ad-cyan-soft); font-weight: 600; }
body.app-dark .sp-pill-add { border-style: dashed; color: var(--ad-text-3); }

/* Calendar */
body.app-dark .sp-cal-dow { color: var(--ad-text-4); }
body.app-dark .sp-cal-day {
  background: rgba(255,255,255,.04); border: 1px solid transparent; color: var(--ad-text-3);
}
body.app-dark .sp-cal-day.met     { background: rgba(52,211,153,.18); color: #6ee7b7; }
body.app-dark .sp-cal-day.partial { background: rgba(251,191,36,.18); color: #fcd34d; }
body.app-dark .sp-cal-day.none    { background: rgba(255,255,255,.03); color: var(--ad-text-4); }
body.app-dark .sp-cal-day.today {
  border-color: rgba(34,211,238,.45); box-shadow: inset 0 0 0 1px rgba(34,211,238,.45);
  background: rgba(34,211,238,.12); color: var(--ad-cyan-soft);
}
body.app-dark .sp-cal-num { color: inherit; }
body.app-dark .sp-cal-q { color: var(--ad-text-4); }

/* Tiles, insights, tracking */
body.app-dark .sp-tile, body.app-dark .sp-insight-micro, body.app-dark .sp-track-body {
  background: rgba(255,255,255,.03); border: 1px solid var(--ad-line);
  border-radius: 10px; color: var(--ad-text-2);
}
body.app-dark .sp-insight-row { border-bottom: 1px solid rgba(255,255,255,.045); }
body.app-dark .sp-insight-value { color: #fff; font-weight: 600; }
body.app-dark .sp-insight-ico, body.app-dark .sp-track-ico { color: var(--ad-cyan-soft); }
body.app-dark .sp-stat-icon { border-radius: 10px; }
body.app-dark .sp-stat-icon.ind   { background: rgba(99,102,241,.18);  color: #a5b4fc; }
body.app-dark .sp-stat-icon.amber { background: rgba(251,191,36,.16);  color: #fcd34d; }
body.app-dark .sp-stat-icon.teal  { background: rgba(45,212,191,.16);  color: #5eead4; }
body.app-dark .sp-exam-locked { color: var(--ad-text-4); }
body.app-dark .sp-stat-action, body.app-dark .sp-track-cta { color: var(--ad-cyan-soft); }
body.app-dark .sp-domain-top, body.app-dark .sp-domain-row { color: var(--ad-text-2); }

/* 14a. The study-plan tab strip is declared with an ID (`#sp-tabs.tabs`, specificity 1,1,0), so a
   `body.app-dark .tabs` rule (0,2,0) loses to it and the strip stayed light grey with a white
   active pill. Matched at ID strength here. The generic .tabs/.tab pair (used by admin) is
   covered too. */
body.app-dark #sp-tabs.tabs {
  background: var(--ad-glass-soft); border: 1px solid rgba(255,255,255,.07); border-radius: 11px;
}
body.app-dark #sp-tabs .tab { color: var(--ad-text-3); background: transparent; }
body.app-dark #sp-tabs .tab:hover { color: #fff; background: rgba(255,255,255,.05); }
body.app-dark #sp-tabs .tab.active {
  background: linear-gradient(90deg, rgba(34,211,238,.18), rgba(168,85,247,.18));
  color: #fff; box-shadow: 0 0 20px -8px rgba(34,211,238,.5);
}
body.app-dark .tabs { border-bottom-color: var(--ad-line); }
body.app-dark .tab { color: var(--ad-text-3); }
body.app-dark .tab:hover { color: #fff; }
body.app-dark .tab.active { color: var(--ad-cyan-soft); border-bottom-color: var(--ad-cyan); }

/* ---- 15. Profile ------------------------------------------------------------------------------
   Ported from "D:\UI Redesign\Profile". Like study-plan, the acct-, profile- and security-
   prefixed rules live in an inline <style> block in profile.html. The compound
   `.profile-page .section-card > h3` selector is (0,2,1), which the theme's (0,2,2) outranks.
   NOTE: never write a star-glob immediately followed by a slash inside these comments. That
   character pair closes the comment early and silently eats the rule after it - it happened here
   and dropped the entire panel rule below, which is why the profile cards stayed white. */
body.app-dark .acct-summary, body.app-dark .acct-plan, body.app-dark .acct-advanced,
body.app-dark .profile-grid, body.app-dark .acct-health {
  background: var(--ad-glass); border: 1px solid var(--ad-line);
  box-shadow: var(--ad-glass-shadow); border-radius: 16px; color: var(--ad-text-2);
}
body.app-dark .acct-summary-hero, body.app-dark .acct-summary-id h1,
body.app-dark .acct-stat-value, body.app-dark .profile-access-title strong,
body.app-dark .security-label, body.app-dark .danger-title { color: #fff; }
body.app-dark .acct-stat-label, body.app-dark .acct-health-label,
body.app-dark .profile-access-desc, body.app-dark .profile-access-meta-label,
body.app-dark .danger-desc, body.app-dark .profile-hero-email { color: var(--ad-text-4); }
body.app-dark .acct-stat, body.app-dark .profile-access-meta-item {
  background: rgba(255,255,255,.03); border: 1px solid var(--ad-line); border-radius: 12px;
}
body.app-dark .acct-summary-body, body.app-dark .acct-advanced-body,
body.app-dark .profile-access-meta-value { color: var(--ad-text-2); }
body.app-dark .acct-advanced > summary { color: #fff; }
body.app-dark .acct-advanced > summary:hover { background: rgba(255,255,255,.04); }
body.app-dark .adv-chevron { color: var(--ad-text-3); }

/* Avatar keeps the sidebar's identity gradient so the two agree. */
body.app-dark .profile-avatar {
  background: linear-gradient(135deg, var(--ad-cyan), var(--ad-violet));
  color: #fff; box-shadow: 0 0 0 2px rgba(255,255,255,.10);
}
body.app-dark .profile-hero-badges, body.app-dark .profile-plan-detail {
  background: rgba(255,255,255,.06); border: 1px solid var(--ad-line);
  border-radius: 999px; color: var(--ad-text-2);
}

/* Health pills - pale tints inverted to washes, same pattern as the badges. */
body.app-dark .health-pill.ok {
  background: rgba(52,211,153,.15); color: #6ee7b7; border-color: rgba(52,211,153,.28);
}
body.app-dark .health-pill.warn {
  background: rgba(251,191,36,.15); color: #fcd34d; border-color: rgba(251,191,36,.26);
}
body.app-dark .health-pill.off {
  background: rgba(255,255,255,.05); color: var(--ad-text-3); border-color: var(--ad-line);
}

/* Security + danger zone */
body.app-dark .security-row { border-bottom: 1px solid rgba(255,255,255,.045); }
body.app-dark .security-icon, body.app-dark .sec-ico {
  background: rgba(255,255,255,.06); color: var(--ad-cyan-soft); border-radius: 10px;
}
body.app-dark .sec-ico.sec-ico-danger { background: rgba(244,63,94,.14); color: #fda4af; }
/* The red is spent on the BUTTON. A tinted panel behind a red button says the same thing twice,
   and the panel is the half the reader cannot act on - it just colours the section permanently.
   What is left is the page's ordinary surface with the faintest warm cast, revealed only on open. */
body.app-dark .danger-row {
  background: rgba(244,63,94,.035); border: 1px solid rgba(255,255,255,.10); border-radius: 12px;
  padding: 12px 14px;
}
body.app-dark .adv-sub { color: var(--ad-text-4); font-weight: 500; }
body.app-dark .mfa-enable-btn { color: var(--ad-cyan-soft); }

/* ---- Modal controls on a light card --------------------------------------------------------
   show2faModal() paints a WHITE card over the dark page, so every app-dark control rule lands
   inverted inside it. Cancel rendered near-white on white and effectively vanished - on the 2FA
   dialogs that is an annoyance, on the delete-account dialog it removes the way out of a
   destructive flow, leaving the backdrop as the only escape. Password inputs came out dark grey
   beside white text inputs in the same form for the same reason.

   Scoped to #tfa-modal so it repaints only what sits on that card. */
body.app-dark #tfa-modal input[type="text"],
body.app-dark #tfa-modal input[type="password"],
body.app-dark #tfa-modal input[type="email"] {
  background: #fff; color: #0f172a; border: 1.5px solid #d7dde7;
}
body.app-dark #tfa-modal input::placeholder { color: #94a3b8; }
body.app-dark #tfa-modal .btn-secondary {
  background: #f1f5f9; color: #334155; border: 1px solid #d7dde7;
}
body.app-dark #tfa-modal .btn-secondary:hover { background: #e2e8f0; color: #0f172a; }
body.app-dark #tfa-modal .btn-danger {
  background: #c62828; color: #fff; border: 1px solid #c62828;
}
body.app-dark #tfa-modal .btn-danger:hover { background: #b71c1c; color: #fff; }
/* Disabled reads as "not yet", not as broken: the button keeps its shape and loses its weight. */
body.app-dark #tfa-modal .btn-danger:disabled {
  background: #fee2e2; color: #b91c1c; border-color: #fecaca; opacity: 1; cursor: not-allowed;
}
body.app-dark .profile-access-icon {
  background: rgba(52,211,153,.14); color: #6ee7b7; border-radius: 11px;
}
body.app-dark .acct-plan-info { color: var(--ad-text-2); }

/* badge-default was the one badge variant section 11 missed (light grey #e0e0e0). */
body.app-dark .badge-default {
  background: rgba(255,255,255,.06); color: var(--ad-text-3); border-color: var(--ad-line);
}

/* ---- 16. Admin ---------------------------------------------------------------------------------
   Ported from "D:\UI Redesign\Admin". Admin reuses the shared components (section-card, data-table,
   badge, tabs, modal, stat-card, form controls) so most of it is already covered. What is left is
   the .admin-container-scoped overrides, which sit at (0,2,0)-(0,3,0) and outrank parts of the
   shared layer, plus a handful of admin-only cells. */
body.app-dark .admin-container .section-card {
  border: 1px solid var(--ad-line);
  box-shadow: var(--ad-glass-shadow);
  border-radius: 16px;
}
body.app-dark .admin-container .section-card h3 {
  border-radius: 0; min-height: 0; padding: 0 0 16px;
}
/* Category chip in the Admin > Exams table. Deliberately neutral: the public catalog's `.cat-*`
   badges are a hard-coded per-brand list, so any category added after it was written matched no rule
   and rendered as a bare badge beside pale brand pills. One scannable chip, same for every category,
   and new categories can never drift out of it. Only this column - status pills, the filter chips
   above the table and the public exam cards are untouched. */
body.app-dark .admin-exam-category {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 20px; padding: 3px 9px; border-radius: 999px;
  background: rgba(255,255,255,.055); border: 1px solid var(--ad-line);
  color: var(--ad-text-2);
  font-size: 10px; font-weight: 750; letter-spacing: .045em; line-height: 1.1;
  text-transform: uppercase; white-space: nowrap;
}
body.app-dark .admin-exam-code-cell strong {
  color: var(--ad-cyan-soft);
  text-shadow: 0 0 16px rgba(34,211,238,.12);
}
body.app-dark .admin-exam-code-cell span {
  color: var(--ad-rose);
}
body.app-dark .admin-exam-name-cell strong {
  color: #fff;
  font-weight: 650;
}
body.app-dark .admin-exam-name-cell span {
  color: var(--ad-text-4);
}
body.app-dark .admin-metric-number {
  color: var(--ad-text-2);
  font-weight: 650;
}
body.app-dark .admin-score-pill {
  background: rgba(34,211,238,.10);
  border: 1px solid rgba(34,211,238,.20);
  color: var(--ad-cyan-soft);
}
body.app-dark .admin-empty-dash {
  color: var(--ad-text-4);
}
body.app-dark .admin-exam-row-inactive {
  opacity: .78;
}
body.app-dark .admin-exam-row-inactive .admin-exam-name-cell strong {
  color: var(--ad-text-2);
}
body.app-dark .voucher-code-input {
  color: var(--ad-cyan-soft);
}
body.app-dark .voucher-generate-btn {
  color: var(--ad-cyan-soft);
  text-decoration-color: rgba(34,211,238,.45);
}
body.app-dark .voucher-form label {
  color: var(--ad-text-3);
}
body.app-dark .voucher-form-error {
  color: var(--ad-rose);
}
body.app-dark .voucher-form-success {
  color: var(--ad-emerald);
}
body.app-dark .voucher-code-cell strong {
  color: var(--ad-cyan-soft);
  text-shadow: 0 0 16px rgba(34,211,238,.12);
}
body.app-dark .voucher-row-muted .voucher-code-cell strong {
  color: rgba(103, 232, 249, .78);
}
body.app-dark .voucher-code-cell span,
body.app-dark .voucher-type-meta,
body.app-dark .voucher-description {
  color: var(--ad-text-4);
}
body.app-dark .voucher-row-muted .voucher-type-meta,
body.app-dark .voucher-row-muted .voucher-description,
body.app-dark .voucher-row-muted .voucher-date {
  color: rgba(148,163,184,.64);
}
body.app-dark .voucher-missing-exam {
  color: var(--ad-rose);
}
body.app-dark .voucher-uses,
body.app-dark .voucher-date {
  color: var(--ad-text-2);
  font-weight: 650;
}
body.app-dark .voucher-redemptions-head span {
  color: var(--ad-cyan-soft);
}
body.app-dark .voucher-redemptions-empty {
  color: var(--ad-text-4);
}
body.app-dark .voucher-actions-cell .btn-secondary {
  background: rgba(255,255,255,.045);
  border-color: rgba(255,255,255,.09);
  color: var(--ad-text-2);
  box-shadow: none;
}
body.app-dark .voucher-actions-cell .btn-secondary:hover {
  background: rgba(34,211,238,.10);
  border-color: rgba(34,211,238,.22);
  color: #fff;
}
body.app-dark .voucher-actions-cell .btn-danger {
  background: rgba(244,63,94,.12);
  border-color: rgba(244,63,94,.24);
  color: #fda4af;
  box-shadow: none;
}
body.app-dark .voucher-actions-cell .btn-danger:hover {
  background: rgba(244,63,94,.20);
  border-color: rgba(244,63,94,.38);
  color: #fff;
}

/* Admin > Billing "Pro Access by Source" tiles, to match the surrounding dark stat cards. */
body.app-dark .billing-source-head { color: var(--ad-text-3); }
body.app-dark .billing-source-card {
  background: rgba(255,255,255,.035); border: 1px solid var(--ad-line); box-shadow: none;
}
body.app-dark .billing-source-value { color: #fff; font-weight: 750; }
body.app-dark .billing-source-label { color: var(--ad-text-4); }
body.app-dark .billing-dist-empty,
body.app-dark .billing-note,
body.app-dark .billing-scale-note,
body.app-dark .billing-revenue-month-label,
body.app-dark .billing-user-email,
body.app-dark .billing-event-detail { color: var(--ad-text-4); }
body.app-dark .billing-plan-name,
body.app-dark .billing-user-name { color: #fff; font-weight: 650; }
body.app-dark .billing-revenue-pair { border-bottom-color: var(--ad-line); }
body.app-dark .billing-legend-usd { background: var(--ad-emerald); }
body.app-dark .billing-legend-inr { background: var(--ad-cyan); }
body.app-dark .billing-usd .billing-revenue-label,
body.app-dark .billing-money-usd { color: var(--ad-emerald); }
body.app-dark .billing-inr .billing-revenue-label,
body.app-dark .billing-money-inr { color: var(--ad-cyan-soft); }
body.app-dark .billing-usd .billing-revenue-fill {
  background: linear-gradient(180deg, var(--ad-emerald), #0f8f72);
  box-shadow: 0 8px 22px -14px rgba(52,211,153,.75);
}
body.app-dark .billing-inr .billing-revenue-fill {
  background: linear-gradient(180deg, var(--ad-cyan), #3578e5);
  box-shadow: 0 8px 22px -14px rgba(34,211,238,.75);
}
body.app-dark .billing-status-success {
  background: rgba(52,211,153,.13); border-color: rgba(52,211,153,.28); color: var(--ad-emerald);
}
body.app-dark .billing-status-started {
  background: rgba(34,211,238,.11); border-color: rgba(34,211,238,.24); color: var(--ad-cyan-soft);
}
body.app-dark .billing-status-failed {
  background: rgba(244,63,94,.13); border-color: rgba(244,63,94,.28); color: var(--ad-rose);
}
body.app-dark .billing-status-canceled {
  background: rgba(251,191,36,.13); border-color: rgba(251,191,36,.28); color: var(--ad-amber);
}

/* .admin-container .tab.active is (0,3,0) and beats the shared (0,2,1) rule. */
body.app-dark .admin-container .tab { color: var(--ad-text-3); background: transparent; }
body.app-dark .admin-container .tab:hover { color: #fff; background: rgba(255,255,255,.05); }
body.app-dark .admin-container .tab.active {
  color: #fff; background: linear-gradient(90deg, rgba(34,211,238,.18), rgba(168,85,247,.18));
  border-color: rgba(34,211,238,.30);
}
body.app-dark .admin-container .badge-default {
  background: rgba(255,255,255,.06); color: var(--ad-text-3);
}
/* Admin marks stat cards with a coloured top border rather than the theme's left accent rail. */
body.app-dark .admin-container .stat-card { border-top-color: var(--ad-line); }
body.app-dark .admin-container .stat-card.accent-blue   { border-top-color: rgba(34,211,238,.45); }
body.app-dark .admin-container .stat-card.accent-green  { border-top-color: rgba(52,211,153,.45); }
body.app-dark .admin-container .stat-card.accent-orange { border-top-color: rgba(251,191,36,.45); }
body.app-dark .admin-container .stat-card.accent-purple { border-top-color: rgba(168,85,247,.45); }

/* Scrollable table regions */
body.app-dark .admin-users-scroll, body.app-dark .admin-activity-scroll,
body.app-dark .admin-event-scroll, body.app-dark .admin-exams-scroll,
body.app-dark .table-scroll-wrap { scrollbar-color: rgba(255,255,255,.18) transparent; }
body.app-dark .admin-users-scroll::-webkit-scrollbar-thumb,
body.app-dark .table-scroll-wrap::-webkit-scrollbar-thumb {
  background: rgba(255,255,255,.14); border-radius: 99px;
}

/* User cells */
body.app-dark .admin-user-avatar {
  background: linear-gradient(135deg, var(--ad-cyan), var(--ad-violet));
  color: #fff; box-shadow: 0 0 0 1px rgba(255,255,255,.10);
}
body.app-dark .admin-user-name { color: #fff; font-weight: 600; }
body.app-dark .admin-user-email, body.app-dark .admin-user-meta,
body.app-dark .admin-joined-cell, body.app-dark .admin-location-cell,
body.app-dark .admin-insight-label { color: var(--ad-text-4); }
body.app-dark .admin-user-row-muted { opacity: .55; }
body.app-dark .admin-insight-value { color: #fff; font-weight: 600; }
body.app-dark .admin-insight-detail { color: var(--ad-text-3); }
body.app-dark .admin-learning-score { color: var(--ad-cyan-soft); font-weight: 600; }
body.app-dark .admin-learning-counts, body.app-dark .admin-engagement-cell { color: var(--ad-text-2); }
body.app-dark .admin-activity-exams-cell strong { color: #fff; font-weight: 600; }
body.app-dark .admin-last-attempt em,
body.app-dark .admin-activity-exams-cell > span { color: var(--ad-text-3); }
body.app-dark .admin-activity-note,
body.app-dark .admin-activity-user-email,
body.app-dark .admin-event-action-word {
  color: var(--ad-text-4);
}
body.app-dark .admin-activity-user-name,
body.app-dark .admin-time-cell {
  color: var(--ad-text-2);
}
body.app-dark .admin-event-count {
  color: var(--ad-text-4);
}
body.app-dark .audit-date-cell {
  color: var(--ad-text-2);
}
body.app-dark .audit-admin-cell {
  color: #fff;
  font-weight: 650;
}
body.app-dark .audit-target-cell {
  color: var(--ad-cyan-soft);
}
body.app-dark .audit-detail-cell {
  color: var(--ad-text-4);
}
body.app-dark .admin-session-chip {
  background: rgba(34,211,238,.12); border: 1px solid rgba(34,211,238,.24);
  color: var(--ad-cyan-soft); border-radius: 999px;
}
body.app-dark .admin-mini-badge {
  background: rgba(255,255,255,.06); border: 1px solid var(--ad-line);
  border-radius: 999px; color: var(--ad-text-3); font-size: 10px;
}
body.app-dark .admin-self-action { color: var(--ad-text-4); }
body.app-dark .admin-select-label, body.app-dark .admin-toolbar-actions { color: var(--ad-text-3); }
body.app-dark .admin-attention-chips { color: var(--ad-text-3); }

/* Health meter keeps its traffic-light meaning, brightened for a dark track. */
body.app-dark .admin-health-meter { background: rgba(255,255,255,.06); border-radius: 999px; }
body.app-dark .admin-health-meter span.good { background: var(--ad-emerald); }
body.app-dark .admin-health-meter span.warn { background: var(--ad-amber); }
body.app-dark .admin-health-meter span.risk { background: var(--ad-rose); }

/* 16a. Two more that outranked or were missed by the sections above.
   - .header-filter (the admin/history filter selects) only had a colour override in section 9, so
     it kept the light control fill.
   - .rec-icon is claimed by `#rec-fallback .rec-icon` at ID strength (1,1,0), which beats the
     section 6 rule (0,2,1); the .recommended-card variant is matched here too. */
body.app-dark .header-filter, body.app-dark select.header-filter {
  background: var(--ad-glass-soft); border: 1px solid rgba(255,255,255,.07);
  color: #fff; border-radius: 999px;
}
body.app-dark .header-filter:focus { outline: none; box-shadow: 0 0 0 2px rgba(34,211,238,.30); }
body.app-dark #rec-fallback .rec-icon,
body.app-dark .recommended-card .rec-icon {
  background: linear-gradient(135deg, rgba(34,211,238,.20), rgba(168,85,247,.20));
  border: 1px solid rgba(255,255,255,.10); color: var(--ad-cyan-soft);
}

/* ---- 17. Dashboard reference parity -------------------------------------------------------
   The three blocks called out against "D:\UI Redesign\Dashboard": the Readiness Center drill
   button, the weak-domain Tip card, and the Recommended Practice header + cards.

   Also fixes a light surface that had been missed everywhere: `.exam-metric-grid div` is
   `background:#f8fafc; border:1px solid #edf1f7` in styles.css and section 6 only ever set the
   grid's colour, so the POOL/TIME/PASS boxes stayed light grey on the dark theme. The badges had
   no dark rule at all. Those paint fixes apply to every app-dark exam card (dashboard, exams and
   admin all emit this markup); the layout changes below are scoped to .dash-exam-card so the
   already-reviewed exams page keeps its own header treatment. */

/* Readiness Center: pill CTA under the sub line. margin-top is 10px, not 20px - the widget is
   a two-column grid now (ring beside text, styles.css) and the taller original gap was enough
   to push the card back past the Recommended card it has to match in row 1. */
body.app-dark .readiness-drill {
  display: inline-flex; align-items: center; justify-content: center;
  height: 32px; padding: 0 14px; margin-top: 10px;
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line);
  border-radius: 999px; color: var(--ad-text-2);
  font-size: 12.5px; font-weight: 500; text-decoration: none;
  transition: background .15s, color .15s, border-color .15s;
}
body.app-dark .readiness-drill:hover {
  background: rgba(255,255,255,.10); color: #fff; border-color: rgba(255,255,255,.15);
}

/* Tip card. Violet blur bloom in the top-right corner, clipped by the panel. */
/* An explicit height, deliberately. The card is a recommendation about one next action, so its
   size should be a decision - not a number inherited from whichever card happens to sit opposite
   it in the layout. Everything above the list and the CTA below it are fixed; the list absorbs the
   remainder and scrolls.
   The card fills its grid row (see .dash-area > .dash-tip in styles.css) so its top and bottom
   edges match Recent Attempts beside it, and it never grows with the domain count - the list is
   capped at four rows.
   The body copy is line-clamped so the block above the rows is a constant height: it embeds the
   domain name, and unclamped its wrapped height moves with whichever exam is selected, which is
   exactly the drift this card exists to avoid. */
/* Same neutral surface as every other panel - `var(--ad-glass), var(--ad-panel)` is exactly what
   .section-card uses, so the Tip card sits at the same value as Readiness Snapshot beside it.
   It previously painted the glass layer with no solid base and added a blurred violet orb in the
   top-right corner, which made a secondary suggestion the most decorative thing on the page.
   Violet is still here, but only where it carries meaning: the TIP eyebrow and the row bars.
   overflow:hidden stays - it clips the rows to the border radius, independent of the old orb. */
body.app-dark .dash-tip {
  position: relative; overflow: hidden;
  background: var(--ad-glass), var(--ad-panel); border: 1px solid var(--ad-line);
  box-shadow: var(--ad-glass-shadow); border-radius: 16px;
  padding: 20px;   /* no margin: the dash-grid row gap is the only vertical spacing */
}
body.app-dark .dash-tip-eyebrow {
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 10px; letter-spacing: .14em; color: rgba(196,181,253,.70);
}
body.app-dark .dash-tip-title {
  margin-top: 8px; font-size: 14px; font-weight: 600; letter-spacing: -.01em; color: #fff;
}
/* Clamped to three lines so the block ABOVE the list is a constant height. The sentence embeds the
   domain name, so its wrapped height varies by exam - left unclamped it changes how many rows fit
   and the card gains a different rhythm on every exam, which is the whole thing this card is being
   fixed to stop. */
body.app-dark .dash-tip-body {
  margin: 6px 0 0; font-size: 13px; line-height: 1.55; color: var(--ad-text-3);
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
  min-height: calc(3 * 1.55em);
}
/* auto, not a fixed margin: the card stretches to fill row 3 (styles.css .dash-area-tip), so the
   CTA is pushed to the bottom edge instead of sitting under the rows with the leftover height
   dangling below it. That dangling gap is exactly what a fixed margin here produces.
   align-self keeps the link its own width inside the column flex. */
body.app-dark .dash-tip-cta {
  display: inline-flex; align-items: center; gap: 6px;
  margin-top: auto; padding-top: 16px; align-self: flex-start;
  font-size: 13px; font-weight: 600; color: var(--ad-cyan-soft); text-decoration: none;
}
body.app-dark .dash-tip-cta:hover { color: #a5f3fc; }

/* Recommended Practice header row. */
/* The practice section starts below the whole two-column area, separated by a full-width hairline
   and a consistent 32px above it. */
body.app-dark .practice-head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 16px; margin: 32px 0 16px; padding-top: 32px;
  border-top: 1px solid var(--ad-line-soft);
}
body.app-dark .practice-head .section-label { margin-bottom: 8px; }
body.app-dark .practice-title {
  margin: 0; font-size: 20px; font-weight: 600; letter-spacing: -.02em; color: #fff;
}
body.app-dark .practice-browse {
  flex: none; font-size: 13px; font-weight: 500; color: var(--ad-text-3);
  text-decoration: none; transition: color .15s;
}
body.app-dark .practice-browse:hover { color: #fff; }
@media (max-width: 640px) { body.app-dark .practice-browse { display: none; } }

/* POOL / TIME / PASS boxes - the missed light surface, on every app-dark exam card. */
body.app-dark .exam-metric-grid div {
  background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.05);
  border-radius: 12px; padding: 8px; text-align: center;
}
body.app-dark .exam-metric-grid span {
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 9px; font-weight: 500; letter-spacing: .1em; color: var(--ad-text-4);
}
body.app-dark .exam-metric-grid strong { font-size: 12px; font-weight: 600; color: rgba(255,255,255,.80); }

/* Difficulty pill keeps its traffic-light meaning against a dark fill. The light-theme
   .diff-* rules are single-class (0,1,0), so these prefixed ones outrank them. */
body.app-dark .exam-diff-badge {
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 10px; font-weight: 500; letter-spacing: 0;
}
body.app-dark .diff-foundation {
  background: rgba(52,211,153,.15); border: 1px solid rgba(52,211,153,.20); color: #6ee7b7;
}
body.app-dark .diff-intermediate {
  background: rgba(251,191,36,.15); border: 1px solid rgba(251,191,36,.20); color: #fcd34d;
}
body.app-dark .diff-advanced {
  background: rgba(244,63,94,.15); border: 1px solid rgba(244,63,94,.20); color: #fda4af;
}

/* Dashboard cards only: the whole card is the link, the vendor label drops its pill, and the
   header no longer reserves a second badge line. */
body.app-dark .dash-exam-card { text-decoration: none; min-height: 0; }
body.app-dark .dash-exam-card .exam-card-header { min-height: 0; margin-bottom: 12px; }
body.app-dark .dash-exam-card .exam-category-badge {
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  background: none; padding: 0; border-radius: 0;
  color: rgba(103,232,249,.90); font-size: 10px; font-weight: 600; letter-spacing: .08em;
}
body.app-dark .dash-exam-card:hover {
  border-color: rgba(34,211,238,.25); transform: translateY(-4px);
  box-shadow: 0 20px 50px -20px rgba(34,211,238,.25);
}
body.app-dark .dash-exam-card:hover h3 { color: #cffafe; }

/* The 2px platform accent bar is a catalog-card device; the reference dashboard card has none,
   and its color-mix with #fff resolves to a light periwinkle (rgb 129,134,184) on the dark panel.
   Hidden on dashboard cards only - exams.html keeps it. */
body.app-dark .dash-exam-card::before { display: none; }

/* "View all attempts" - de-inlined in dashboard.js so it can take the rail's cyan, with the
   reference's divider above it. */
body.app-dark .recent-attempts-all {
  display: block; margin-top: 16px; padding-top: 12px;
  border-top: 1px solid rgba(255,255,255,.05);
  color: rgba(103,232,249,.80); font-size: 13px; font-weight: 500;
  text-align: center; text-decoration: none;
}
body.app-dark .recent-attempts-all:hover { color: #a5f3fc; }
/* Review signal strip - see .review-strip in styles.css. Numbers take the theme's mono face like
   every other counted figure on the dashboard; recovered is emerald because it is the only one of
   the three that is good news. */
body.app-dark .review-strip { border-top: 1px solid rgba(255,255,255,.06); }
body.app-dark .rs-val {
  color: #fff; font-size: 18px; letter-spacing: -.01em;
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
body.app-dark .rs-item.rs-good .rs-val { color: #6ee7b7; }
body.app-dark .rs-lbl { color: rgba(255,255,255,.55); }
body.app-dark a.rs-item:hover .rs-val { color: var(--ad-cyan-soft); }
body.app-dark a.rs-item:hover .rs-lbl { color: #fff; }
body.app-dark .review-strip + .recent-attempts-all {
  border-top: 0; margin-top: 12px; padding-top: 0;
}

/* ---- 18. Plan (pricing.html, logged-in) ---------------------------------------------------
   Ported from "D:\UI Redesign\Plan". pricing.html is a hybrid: a logged-out visitor gets the
   blog-dark public shell and public-mode.js strips app-dark from the body, so nothing here can
   reach them. Everything below is the authenticated billing UI only.

   The page carries its own inline <style> block in the head. That block is same-specificity with
   styles.css, so these body.app-dark rules outrank both. The About / Compare / FAQ sections used
   to be styled with inline style= attributes, which no stylesheet can beat - they were de-inlined
   to classes first (see the head block) rather than reaching for !important. */

/* Page head + account card */
body.app-dark .pricing-page-head h1 { color: #fff; font-size: 34px; letter-spacing: -.03em; }
body.app-dark .pricing-eyebrow { color: var(--ad-cyan-soft); letter-spacing: .16em; }
body.app-dark .pricing-account-card,
body.app-dark .pricing-proof-grid div,
body.app-dark .voucher-redeem-section {
  background: var(--ad-glass); border: 1px solid var(--ad-line);
  box-shadow: var(--ad-glass-shadow); border-radius: 16px;
}
body.app-dark .pricing-account-card span,
body.app-dark .pricing-proof-grid span { color: var(--ad-text-4); }
body.app-dark .pricing-account-card strong,
body.app-dark .pricing-proof-grid strong,
body.app-dark .voucher-redeem-section h2 { color: #fff; }
body.app-dark .pricing-account-card p { color: var(--ad-text-3); }
body.app-dark .pricing-current-context, body.app-dark .pricing-trust-line { color: var(--ad-text-3); }

/* Assurance pills */
body.app-dark .pricing-assurance-row span {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); color: var(--ad-text-2);
}
body.app-dark .pricing-assurance-row span::before { background: var(--ad-emerald); }

/* Launch-offer band. Already dark in the light theme; re-tuned to the rail's glass + cyan. */
body.app-dark .pricing-offer-band {
  background: linear-gradient(135deg, rgba(34,211,238,.10), rgba(168,85,247,.08));
  border: 1px solid rgba(34,211,238,.25); border-radius: 16px; box-shadow: none; color: #fff;
}
body.app-dark .pricing-offer-band > div > span { color: var(--ad-cyan-soft); }
body.app-dark .pricing-offer-band p { color: var(--ad-text-2); }
body.app-dark .pricing-offer-code {
  background: rgba(255,255,255,.10); border: 1px solid rgba(255,255,255,.18); color: #fff;
}
body.app-dark .pricing-offer-band .btn {
  background: linear-gradient(90deg, var(--ad-cyan), #5eead4); color: #071018; box-shadow: none;
}
body.app-dark .pricing-offer-band .btn:hover { filter: brightness(1.1); background: linear-gradient(90deg, var(--ad-cyan), #5eead4); color: #071018; }

/* Plan cards */
body.app-dark .pricing-card {
  background: var(--ad-glass); border: 1px solid var(--ad-line);
  box-shadow: var(--ad-glass-shadow); border-radius: 16px;
}
body.app-dark .pricing-card:hover { border-color: rgba(34,211,238,.25); }
body.app-dark .pricing-card.popular, body.app-dark .pricing-card.current {
  border-color: rgba(34,211,238,.35);
}
/* The top bar of a plan card is a ::before, NOT the element's border-top. Section 47 below
   neutralised border-top per plan and the cards still came out violet/cyan striped, because the
   bar the eye actually sees is this pseudo-element. Colour is neutral on every plan and cyan on
   exactly one thing - the plan you are on - so the palette stops encoding plan identity. */
body.app-dark .pricing-card::before,
body.app-dark .pricing-card-free::before,
body.app-dark .pricing-card-single-exam::before,
body.app-dark .pricing-card-pro-monthly::before,
body.app-dark .pricing-card-pro-annual::before { background: rgba(255,255,255,.14); }
body.app-dark .pricing-card.current::before { background: var(--ad-cyan); }
body.app-dark .pricing-card h3, body.app-dark .pricing-price,
body.app-dark .pricing-new-price { color: #fff; }
body.app-dark .pricing-old-price { color: var(--ad-text-4); }
body.app-dark .pricing-savings { color: var(--ad-emerald); }
body.app-dark .pricing-popular-tag {
  background: linear-gradient(90deg, var(--ad-cyan), #5eead4); color: #071018;
}
body.app-dark .pricing-current-tag { background: rgba(34,211,238,.18); color: var(--ad-cyan-soft); }

/* Kicker / fit chips */
body.app-dark .pricing-plan-kicker {
  background: rgba(99,102,241,.18); border: 1px solid rgba(99,102,241,.25); color: #c7d2fe;
}
body.app-dark .pricing-plan-fit {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); color: var(--ad-text-3);
}
body.app-dark .pricing-card-pro-annual .pricing-plan-kicker {
  background: rgba(52,211,153,.18); border-color: rgba(52,211,153,.25); color: #6ee7b7;
}
body.app-dark .pricing-card-pro-annual .pricing-plan-fit {
  background: rgba(52,211,153,.08); border-color: rgba(52,211,153,.20); color: #6ee7b7;
}

/* Notes and feature lists */
body.app-dark .pricing-card-note {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); color: var(--ad-text-3);
}
body.app-dark .pricing-discount-note,
body.app-dark .pricing-current-access,
body.app-dark .pricing-grandfathered {
  background: rgba(52,211,153,.10); border: 1px solid rgba(52,211,153,.20); color: #6ee7b7;
}
body.app-dark .pricing-features li { color: var(--ad-text-2); }
body.app-dark .pricing-features li::before { color: var(--ad-cyan); }
body.app-dark .pricing-action .btn-secondary {
  background: var(--ad-glass-soft); border-color: var(--ad-line); color: var(--ad-text-2);
}
body.app-dark .pricing-action .btn-secondary:hover { background: rgba(255,255,255,.10); color: #fff; }

/* Voucher redemption */
body.app-dark #voucher-error {
  background: rgba(244,63,94,.10); border: 1px solid rgba(244,63,94,.22); color: #fda4af;
}
body.app-dark #voucher-success {
  background: rgba(52,211,153,.10); border: 1px solid rgba(52,211,153,.22); color: #6ee7b7;
}

/* About / Compare / FAQ - de-inlined above so these can land. */
body.app-dark .pricing-about h2, body.app-dark .pricing-faq h2 {
  color: #fff; font-weight: 600; letter-spacing: -.02em;
}
body.app-dark .pricing-about-lead { color: var(--ad-text-2); }
body.app-dark .pricing-about-fine { color: var(--ad-text-4); }
body.app-dark .pricing-compare-label { color: var(--ad-text-3); }
body.app-dark .pricing-compare-links a {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); color: var(--ad-text-2);
}
body.app-dark .pricing-compare-links a:hover { background: rgba(255,255,255,.10); color: #fff; }
body.app-dark .pricing-faq-list h3 { color: #fff; font-weight: 600; }
body.app-dark .pricing-faq-list p { color: var(--ad-text-3); }
body.app-dark .pricing-faq-list a { color: var(--ad-cyan-soft); }

/* ---- 19. Results and review ---------------------------------------------------------------
   Shared by exam.html (the results screen) and history-detail.html, which renders the same
   markup on its own. No reference folder exists for either, so this follows the established
   tokens rather than inventing a new language.

   Semantics: emerald = correct, rose = wrong, amber = flagged, cyan = the answer you should
   have picked. Those meanings are load-bearing here, so the bands stay saturated enough to read
   at a glance instead of being flattened into glass. */

body.app-dark .results-container, body.app-dark .review-container { color: var(--ad-text-2); }

/* Result banner. The pass/fail/practice variants each carried their own light gradient. */
body.app-dark .result-banner {
  background: var(--ad-glass); border: 1px solid var(--ad-line);
  box-shadow: var(--ad-glass-shadow); border-radius: 16px; color: var(--ad-text-2);
}
body.app-dark .result-banner h2 { color: #fff; }
body.app-dark .result-banner .score { color: #fff; font-weight: 600; letter-spacing: -.03em; }
body.app-dark .result-banner .score-sub { color: var(--ad-text-3); }
body.app-dark .result-banner.passed {
  background: linear-gradient(135deg, rgba(52,211,153,.14), rgba(34,211,238,.06));
  border-color: rgba(52,211,153,.28);
}
body.app-dark .result-banner.passed h2, body.app-dark .result-banner.passed .score { color: #6ee7b7; }
body.app-dark .result-banner.failed {
  background: linear-gradient(135deg, rgba(244,63,94,.14), rgba(168,85,247,.06));
  border-color: rgba(244,63,94,.28);
}
body.app-dark .result-banner.failed h2, body.app-dark .result-banner.failed .score { color: #fda4af; }
body.app-dark .result-banner.practice, body.app-dark .result-banner.diagnostic {
  background: linear-gradient(135deg, rgba(34,211,238,.12), rgba(99,102,241,.08));
  border-color: rgba(34,211,238,.26);
}
body.app-dark .result-banner.practice h2, body.app-dark .result-banner.practice .score,
body.app-dark .result-banner.diagnostic h2, body.app-dark .result-banner.diagnostic .score {
  color: var(--ad-cyan-soft);
}
/* De-inlined in exam.js - it was a hardcoded white card. */
body.app-dark .diag-next-step {
  margin-top: 14px; padding: 12px 16px; text-align: left; font-size: 14px;
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line);
  border-radius: 12px; color: var(--ad-text-2);
}
body.app-dark .diag-next-step strong { color: #fff; }

/* Result stat tiles */
body.app-dark .result-stats .stat-card { padding: 16px; }
body.app-dark .result-note { color: var(--ad-text-3); }

/* ---- Score report (results screen) --------------------------------------------------------------
   Calm and analytical rather than a verdict: the panel keeps the normal glass surface, the outcome
   is a 3px accent rail plus coloured ink, and cyan is spent ONLY on the primary action. A full red
   wash made a near miss look like a system error rather than a score. */
body.app-dark .rres-head {
  background: var(--ad-glass), var(--ad-panel);
  border: 1px solid var(--ad-line); box-shadow: var(--ad-glass-shadow);
}
body.app-dark .rres-head::before { background: rgba(255,255,255,.14); }
body.app-dark .rres-head.is-pass::before { background: linear-gradient(90deg, var(--ad-emerald), #10b981); }
body.app-dark .rres-head.is-fail::before { background: linear-gradient(90deg, #f43f5e, #fb7185); }
body.app-dark .rres-eyebrow { color: var(--ad-text-4); }
body.app-dark .rres-verdict { color: #fff; }
body.app-dark .rres-head.is-pass .rres-verdict { color: #6ee7b7; }
body.app-dark .rres-head.is-fail .rres-verdict { color: #fda4af; }
body.app-dark .rres-score-val {
  color: #fff;
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
body.app-dark .rres-score-max { color: var(--ad-text-4); }
body.app-dark .rres-gap { color: var(--ad-text-2); }
/* Rose on a miss - see the light-theme note. Cyan here read as "in progress", and this page has
   exactly one cyan element: the primary action. */
body.app-dark .rres-head.is-fail .rsnap-fill {
  background: linear-gradient(90deg, rgba(244,63,94,.55), #fb7185);
  box-shadow: 0 0 12px -2px rgba(244,63,94,.45);
}

body.app-dark .rres-metric {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line);
}
body.app-dark .rres-ico { color: var(--ad-text-4); }
body.app-dark .rres-metric-lbl { color: var(--ad-text-4); }
body.app-dark .rres-metric-val {
  color: #fff;
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  letter-spacing: -.03em;
}
body.app-dark .rres-metric-note { color: var(--ad-text-4); }

/* Quieter table: no boxed header band, softer rules, taller rows. */
body.app-dark .rres-table th {
  background: transparent; color: var(--ad-text-4); border-bottom: 1px solid var(--ad-line);
}
body.app-dark .rres-table td { border-bottom: 1px solid rgba(255,255,255,.045); }
body.app-dark .rres-table tbody tr:last-child td { border-bottom: 0; }
body.app-dark .rres-table .rres-dom { color: rgba(255,255,255,.88); }
body.app-dark .rres-pct { color: var(--ad-text-3); }
body.app-dark .rres-pill.is-pass { background: rgba(52,211,153,.14); color: #6ee7b7; }
body.app-dark .rres-pill.is-mid { background: rgba(251,191,36,.14); color: #fcd34d; }
body.app-dark .rres-pill.is-weak { background: rgba(244,63,94,.14); color: #fda4af; }

body.app-dark .rres-next {
  background: var(--ad-glass), var(--ad-panel);
  border: 1px solid var(--ad-line); box-shadow: var(--ad-glass-shadow);
}
body.app-dark .rres-next-head { color: #fff; }
body.app-dark .rres-next-head .rres-ico { color: var(--ad-cyan-soft); }
body.app-dark .rres-next-lbl { color: var(--ad-text-4); }
body.app-dark .rres-next-val { color: rgba(255,255,255,.88); }
/* .rres-next-miss removed with the comma-joined sentence it styled - see .rres-focus. */
body.app-dark .rres-next-cta { color: var(--ad-cyan-soft); }
body.app-dark .rres-next-cta:hover { color: #a5f3fc; }

body.app-dark .rres-foot { border-top: 1px solid var(--ad-line); }
body.app-dark .rres-btn.is-primary {
  background: linear-gradient(135deg, var(--ad-cyan), #0ea5e9); color: #04121a;
  box-shadow: 0 10px 24px -12px rgba(34,211,238,.7);
}
body.app-dark .rres-btn.is-primary:hover { filter: brightness(1.06); }
body.app-dark .rres-btn.is-secondary {
  background: rgba(255,255,255,.05); border: 1px solid var(--ad-line); color: var(--ad-text-2);
}
body.app-dark .rres-btn.is-secondary:hover { background: rgba(255,255,255,.10); color: #fff; }
body.app-dark .rres-foot-quiet a { color: var(--ad-text-3); }
body.app-dark .rres-foot-quiet a:hover { color: #fff; }

/* Score by domain */
body.app-dark .domain-table {
  background: var(--ad-glass); border: 1px solid var(--ad-line);
  box-shadow: var(--ad-glass-shadow); border-radius: 16px;
}
body.app-dark .domain-table h3 { background: none; color: #fff; border-bottom: 1px solid var(--ad-line); }
body.app-dark .domain-table th {
  background: rgba(255,255,255,.03); color: var(--ad-text-4);
  border-bottom: 1px solid var(--ad-line); text-transform: uppercase; letter-spacing: .08em;
}
body.app-dark .domain-table td { color: var(--ad-text-2); border-bottom: 1px solid var(--ad-line-soft); }

/* Per-question review */
body.app-dark .review-detail {
  background: var(--ad-glass); border: 1px solid var(--ad-line);
  box-shadow: var(--ad-glass-shadow); border-radius: 16px;
}
body.app-dark .review-detail h3 { background: none; color: #fff; border-bottom: 1px solid var(--ad-line); }
body.app-dark .review-detail h3:hover { background: rgba(255,255,255,.03); }
body.app-dark .review-detail h3 .toggle-icon { color: var(--ad-text-4); }
body.app-dark .review-q-item { border-bottom: 1px solid var(--ad-line-soft); background: transparent; }
/* The question number labels the collapsed summary row, so it is readable text rather than a
   30%-white whisper. */
body.app-dark .review-q-num { color: var(--ad-text-2); }
body.app-dark .review-q-text { color: #fff; }
/* .review-q-badge is deliberately NOT in this rule. It sits after .badge-correct/.badge-incorrect
   at equal specificity, so including it overrode their semantic colours with --ad-text-4 and both
   verdicts rendered as the same 30%-white text. */
body.app-dark .review-q-domain { color: var(--ad-text-4); }

/* Answer options in review. The indicator dot carries the same colour as its band. */
body.app-dark .review-option {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); color: var(--ad-text-2);
}
body.app-dark .review-option.neutral { background: var(--ad-glass-soft); color: var(--ad-text-3); }
body.app-dark .review-option.neutral .opt-indicator { background: rgba(255,255,255,.08); color: var(--ad-text-4); }
body.app-dark .review-option.correct-chosen {
  background: rgba(52,211,153,.12); border-color: rgba(52,211,153,.30); color: #a7f3d0;
}
body.app-dark .review-option.correct-chosen .opt-indicator { background: var(--ad-emerald); color: #06251b; }
body.app-dark .review-option.wrong-chosen {
  background: rgba(244,63,94,.12); border-color: rgba(244,63,94,.30); color: #fecdd3;
}
body.app-dark .review-option.wrong-chosen .opt-indicator { background: var(--ad-rose); color: #2a0710; }
body.app-dark .review-option.missed {
  background: rgba(34,211,238,.10); border-color: rgba(34,211,238,.30); color: #a5f3fc;
}
body.app-dark .review-option.missed .opt-indicator { background: var(--ad-cyan); color: #071018; }
body.app-dark .review-explanation {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line);
  border-left: 3px solid rgba(99,102,241,.5); color: var(--ad-text-2);
}
body.app-dark .explanation-label { color: var(--ad-cyan-soft); }
body.app-dark .explanation-text, body.app-dark .explanation-note { color: var(--ad-text-2); }

/* ---- 20. Exam (exam.html) -----------------------------------------------------------------
   The test-taking screen: setup, question area, every question type, the review screen, and the
   fixed chrome. No reference folder exists for this page either. It is still a RE-SKIN - no
   markup or behaviour changed beyond de-inlining the diagnostic banner in section 19. */

/* Fixed chrome */
body.app-dark .exam-header {
  background: rgba(10,14,26,.88); backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--ad-line); color: #fff; box-shadow: none;
}
body.app-dark .exam-header-title { color: #fff; }
body.app-dark .exam-header-info { color: var(--ad-text-3); }
body.app-dark .exam-header-timer {
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  color: var(--ad-cyan-soft);
}
/* The progress bar is built with inline style= on the element, so these need !important - the
   one place in this file where nothing else can win. */
body.app-dark #exam-progress-bar { background: rgba(255,255,255,.06) !important; }
body.app-dark #exam-progress-fill {
  background: linear-gradient(90deg, var(--ad-cyan), var(--ad-indigo)) !important;
}
body.app-dark .bottom-nav {
  background: rgba(10,14,26,.92); backdrop-filter: blur(12px);
  border-top: 1px solid var(--ad-line); box-shadow: none;
}
body.app-dark .time-toast {
  background: rgba(10,14,26,.95); border: 1px solid rgba(251,191,36,.35);
  color: #fcd34d; box-shadow: var(--ad-glass-shadow);
}

/* Setup screen. The bar carries a logo and two controls, so it should recede: a near-transparent
   wash on the page's own colour and a hairline rule rather than a solid slab with a visible edge. */
body.app-dark .exam-setup-topbar {
  background: rgba(10,14,26,.55); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--ad-line-soft);
}
body.app-dark .exam-setup-brand { color: #fff; }
body.app-dark .exam-setup-brand .esb-accent { color: #2dd4bf; }
body.app-dark .exam-setup-back { color: var(--ad-text-3); }
body.app-dark .exam-setup-back:hover { color: #fff; background: rgba(255,255,255,.06); }
/* The flat glass panel treatment - a soft shadow, no glow. The surface only.
   No margin-top here any more. This rule used to pull the card up to 10px against a light theme it
   described as 28px (it was 40px), which left the two themes sitting at different heights; worse,
   being unconditional it also outranked the 640px rule in styles.css, so the mobile offset never
   applied to the dark theme at all. Both themes now take their offset from .welcome-card. */
body.app-dark .welcome-card {
  background: linear-gradient(180deg, rgba(255,255,255,.045), rgba(255,255,255,.022)), #0a0e1a;
  border: 1px solid rgba(255,255,255,.09);
  box-shadow: 0 28px 60px -34px rgba(0,0,0,.85), inset 0 1px 0 rgba(255,255,255,.05);
  border-radius: 16px;
}
body.app-dark .welcome-eyebrow { color: var(--ad-cyan-soft); letter-spacing: .16em; }
body.app-dark .welcome-header h1 { color: #fff; letter-spacing: -.03em; }
/* Body copy and the mode helper were at --ad-text-3 (45% white). That is label contrast, not
   reading contrast - these are the two sentences on the page a user actually reads. */
body.app-dark .welcome-header p { color: rgba(255,255,255,.62); }
body.app-dark .welcome-provider-badge {
  background: rgba(99,102,241,.18); border: 1px solid rgba(99,102,241,.25); color: #c7d2fe;
}
body.app-dark .welcome-code-badge {
  background: rgba(255,255,255,.06); border: 1px solid var(--ad-line); color: var(--ad-cyan-soft);
}
body.app-dark .welcome-stat, body.app-dark .welcome-fact, body.app-dark .welcome-facts {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); border-radius: 12px;
}
/* Equal height regardless of whether a value wraps to two lines, and the label sized down so the
   number carries the tile on its own. */
body.app-dark .welcome-stat {
  min-height: 74px; display: flex; flex-direction: column; justify-content: center;
  box-shadow: none;
}
body.app-dark .welcome-stat-val { color: #fff; }
/* Colour only. The SIZE of this label is set once, further down in section 54 - it was stated in
   two dark rules and in styles.css, and the last dark one silently decided it, so a bump in the
   light theme did nothing here. */
body.app-dark .welcome-stat-lbl { color: var(--ad-text-4); }
body.app-dark .welcome-fact-txt { color: var(--ad-text-3); }
body.app-dark .welcome-fact-dot { background: var(--ad-cyan); }
body.app-dark .welcome-section-label { color: var(--ad-text-4); letter-spacing: .16em; }
body.app-dark .welcome-progress-strip {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); color: var(--ad-text-3);
}
body.app-dark .welcome-progress-strip strong { color: #fff; }
body.app-dark .welcome-domain-row { border-bottom: 1px solid var(--ad-line-soft); }
body.app-dark .welcome-domain-top, body.app-dark .welcome-domain-name { color: var(--ad-text-2); }
body.app-dark .welcome-domain-count { color: rgba(255,255,255,.34); }
/* Thinner and darker: at full-card width these bars were the brightest thing on the setup screen,
   and they only convey relative pool size. */
body.app-dark .welcome-domain-bar { background: rgba(255,255,255,.045); height: 4px; }
/* exam.js emits a <span> here, not a <div> - matching the wrong element left the bar unfilled. */
/* Flat, not a gradient. A cyan-to-indigo sweep put a second hue on the one element whose whole job
   is to compare four lengths against each other - and a bar that changes colour along its length is
   harder to compare than one that does not. Indigo also appears nowhere else on this screen any
   more. One accent, held at the alpha the old gradient started on. */
body.app-dark .welcome-domain-bar span {
  background: rgba(34,211,238,.72); opacity: 1;
}
body.app-dark .welcome-select {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); color: #fff; border-radius: 10px;
}
/* The light theme grew a focus ring where outline:none used to leave nothing. Dark needs its own
   colour for it, or the indigo ring lands on a near-black control. */
body.app-dark .welcome-select:hover { border-color: rgba(255,255,255,.18); }
body.app-dark .welcome-select:focus,
body.app-dark .welcome-select:focus-visible {
  border-color: rgba(34,211,238,.55); box-shadow: 0 0 0 3px rgba(34,211,238,.15);
}
body.app-dark .welcome-actions { border-top: 1px solid var(--ad-line); }
body.app-dark .welcome-how, body.app-dark #welcome-trust { color: var(--ad-text-4); }
/* The shared .btn-primary carries a 24px cyan bloom, which on this card is the loudest thing on an
   otherwise calm page. A short drop shadow keeps it the obvious action without the halo, and the
   secondary drops to a quiet outline so the pair reads as primary + escape hatch. */
body.app-dark .welcome-actions .btn-primary {
  box-shadow: 0 6px 16px -10px rgba(34,211,238,.65);
}
body.app-dark .welcome-actions .btn-secondary {
  background: transparent; border: 1px solid var(--ad-line); color: var(--ad-text-3);
  box-shadow: none;
}
body.app-dark .welcome-actions .btn-secondary:hover {
  border-color: rgba(255,255,255,.18); color: #fff; background: rgba(255,255,255,.04);
}

/* Mode cards */
body.app-dark .mode-card {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); border-radius: 14px;
}
body.app-dark .mode-card:hover { border-color: rgba(255,255,255,.16); background: rgba(255,255,255,.05); }
body.app-dark .mode-card.selected {
  background: rgba(34,211,238,.07); border-color: rgba(34,211,238,.45);
}
/* The light rule used to put an INSET 1px ring in var(--cat-color) here, and app-dark never
   overrode box-shadow - so the dark selected card has been carrying a 1px amber line inside its
   cyan border on every AWS exam since --cat-color landed. Light is a soft outer ring now; this
   is the same ring in cyan, which both removes the amber and keeps the two themes parallel. */
body.app-dark .mode-card.selected { box-shadow: 0 0 0 3px rgba(34,211,238,.10); }
body.app-dark .mode-card.selected::after { background: var(--ad-cyan); color: #071018; }
/* The "decorative grey bar" across the top of every mode card was this: .mode-card-icon is a DIV,
   the light theme leaves it as bare coloured glyph with no box, and adding a background here made it
   a full-width block with the icon at its left end. It needs its own tile. */
body.app-dark .mode-card-icon {
  background: rgba(255,255,255,.06); color: var(--ad-cyan-soft);
  width: 34px; height: 34px; border-radius: 9px;
  display: inline-flex; align-items: center; justify-content: center;
}
body.app-dark .mode-card.selected .mode-card-icon { background: rgba(34,211,238,.16); color: var(--ad-cyan); }
body.app-dark .mode-card-title { color: #fff; }
body.app-dark .mode-card-desc { color: var(--ad-text-3); }

/* Question area */
body.app-dark .exam-main { background: transparent; }
body.app-dark .question-domain { color: var(--ad-cyan-soft); }
body.app-dark .question-text { color: #fff; }
body.app-dark .multi-hint {
  color: var(--ad-text-3); background: var(--ad-glass-soft); border: 1px solid var(--ad-line);
}
body.app-dark .option-item {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); color: var(--ad-text-2);
}
body.app-dark .option-item:hover { background: rgba(255,255,255,.06); border-color: rgba(255,255,255,.16); }
body.app-dark .option-item.selected {
  background: rgba(34,211,238,.10); border-color: rgba(34,211,238,.40); color: #fff;
}
/* The dark theme turns the letter into a badge, but inherited only min-width:20px from the light
   theme - where it is bare text and needs no padding. With a fill and a border on it, the glyph sat
   hard against the badge edge and read as clipped. */
body.app-dark .option-key {
  background: rgba(255,255,255,.08); border: 1px solid var(--ad-line); color: var(--ad-text-2);
  min-width: 28px; padding: 2px 7px; border-radius: 7px; text-align: center; line-height: 1.35;
}
body.app-dark .option-item.selected .option-key {
  background: var(--ad-cyan); color: #071018; border-color: transparent;
}
body.app-dark .option-text { color: inherit; }
/* The light theme's accent-color is #1a237e - navy on a near-black row, so a checked radio read as
   an unlit disc. Cyan while the question is live; once answered the input is disabled and the
   browser greys it regardless, which is what .your-answer-chip is for. */
/* The input recedes so the row tint and the letter badge carry the state. Two competing indicators
   at equal weight is what made the left edge look noisy - a native control cannot be styled to match
   this design language, so it becomes the quietest of the three rather than the loudest. Still a real
   input: full size for hit-testing is the label's job, and the whole row is the label. */
body.app-dark .option-item input[type="radio"],
body.app-dark .option-item input[type="checkbox"] {
  accent-color: var(--ad-cyan);
  width: 15px; height: 15px; flex-shrink: 0; opacity: .72;
  transition: opacity .15s;
}
body.app-dark .option-item.selected input[type="radio"],
body.app-dark .option-item.selected input[type="checkbox"] { opacity: 1; }
/* Revealed rows: the disabled grey disc drops back further still, because it is no longer carrying
   any meaning - .your-answer-chip and the badge do that. */
body.app-dark .option-item input:disabled { opacity: .45; }
body.app-dark .your-answer-chip {
  background: rgba(255,255,255,.10); border: 1px solid var(--ad-line); color: rgba(255,255,255,.78);
}
body.app-dark .option-eliminate { color: var(--ad-text-4); }
body.app-dark .option-eliminate:hover { color: #fff; background: rgba(255,255,255,.08); }
body.app-dark .option-eliminate.active { color: var(--ad-rose); }
/* `.option-row.eliminated .option-item` is (0,3,0) and outranks the (0,2,1) rule above, so the
   struck-out option kept its #fafbfc fill. Matched at its own strength rather than !important. */
body.app-dark .option-row.eliminated .option-item {
  background: var(--ad-glass-soft); border-color: var(--ad-line); opacity: .40;
}
body.app-dark .option-row.eliminated .option-item:hover { background: var(--ad-glass-soft); }
body.app-dark .option-item.correct-reveal {
  background: rgba(52,211,153,.12); border-color: rgba(52,211,153,.35); color: #a7f3d0;
}
body.app-dark .option-item.wrong-reveal {
  background: rgba(244,63,94,.12); border-color: rgba(244,63,94,.35); color: #fecdd3;
}
body.app-dark .answer-warning {
  background: rgba(251,191,36,.10); border: 1px solid rgba(251,191,36,.28); color: #fcd34d;
}

/* Practice feedback */
body.app-dark .practice-feedback.correct, body.app-dark .practice-feedback.incorrect {
  background: var(--ad-glass); border: 1px solid var(--ad-line); border-radius: 14px;
}
body.app-dark .practice-feedback.correct .feedback-banner { background: rgba(52,211,153,.14); color: #6ee7b7; }
body.app-dark .practice-feedback.incorrect .feedback-banner { background: rgba(244,63,94,.14); color: #fda4af; }
body.app-dark .practice-feedback .feedback-body { background: transparent; color: var(--ad-text-2); }
body.app-dark .practice-feedback .explanation-text { color: var(--ad-text-2); }
body.app-dark .correct-hint { color: var(--ad-cyan-soft); }

/* Practice insight rail */
body.app-dark .pp-card {
  background: var(--ad-glass); border: 1px solid var(--ad-line);
  box-shadow: var(--ad-glass-shadow); border-radius: 16px;
}
body.app-dark .pp-title, body.app-dark .pp-exam, body.app-dark .pp-strong { color: #fff; }
body.app-dark .pp-sub, body.app-dark .pp-k, body.app-dark .pp-lead { color: var(--ad-text-3); }
body.app-dark .pp-lead strong { color: #fff; }
/* The base rule uses border-TOP (#f1f2f8) and sets colour:#3a3f52 - overriding border-bottom
   left a light rule line and dark-on-dark text in the insight rail. */
body.app-dark .pp-row {
  border-top: 1px solid var(--ad-line-soft); border-bottom: none; color: var(--ad-text-2);
}
body.app-dark .pp-row.pp-row-first { border-top: none; }
body.app-dark .pp-row .pp-v { color: #fff; }
body.app-dark .pp-insight {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); color: var(--ad-text-2);
}
body.app-dark .pp-bar { background: rgba(255,255,255,.06); }
body.app-dark .pp-weak { color: var(--ad-rose); }
body.app-dark .pp-btn {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); color: var(--ad-text-2);
}
body.app-dark .pp-btn:hover { background: rgba(255,255,255,.10); color: #fff; }
body.app-dark .pp-upsell a { color: var(--ad-cyan-soft); }

/* Marked-for-review flags */
body.app-dark .q-marked-flag {
  background: rgba(251,191,36,.12); border: 1px solid rgba(251,191,36,.28); color: #fcd34d;
}
body.app-dark .q-marked-next {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); color: var(--ad-text-2);
}
body.app-dark .q-marked-next:hover { background: rgba(255,255,255,.10); color: #fff; }
/* Backtrack pill: glass, not the light theme's pale wash. Quiet enough to read as context. */
body.app-dark .q-backtrack {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); color: var(--ad-text-3);
}
body.app-dark .q-backtrack-sep { color: var(--ad-text-4); }
body.app-dark .q-backtrack-resume { color: var(--ad-cyan-soft); }
body.app-dark .q-backtrack-resume:hover { background: rgba(34,211,238,.14); color: #fff; }

/* Review screen */
body.app-dark .review-container h2 { color: #fff; }
body.app-dark .review-summary { color: var(--ad-text-3); }
body.app-dark .review-filter-btn {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); color: var(--ad-text-2);
}
body.app-dark .review-filter-btn:hover { background: rgba(255,255,255,.10); color: #fff; }
body.app-dark .review-filter-btn.active {
  background: rgba(34,211,238,.14); border-color: rgba(34,211,238,.40); color: var(--ad-cyan-soft);
}
body.app-dark .review-filter-count { background: rgba(255,255,255,.08); color: var(--ad-text-3); }
body.app-dark .review-filter-btn.active .review-filter-count { background: rgba(34,211,238,.25); color: #fff; }
body.app-dark .review-stat.answered { color: var(--ad-emerald); }
body.app-dark .review-stat.unanswered { color: var(--ad-text-4); }
body.app-dark .review-stat.marked { color: var(--ad-amber); }
body.app-dark .review-table th {
  background: rgba(255,255,255,.03); color: var(--ad-text-4); border-bottom: 1px solid var(--ad-line);
}
body.app-dark .review-table td { color: var(--ad-text-2); border-bottom: 1px solid var(--ad-line-soft); }
body.app-dark .review-table tr.clickable:hover td { background: rgba(255,255,255,.04); }
body.app-dark .status-answered { color: var(--ad-emerald); }
body.app-dark .status-unanswered { color: var(--ad-text-4); }
body.app-dark .status-marked { color: var(--ad-amber); }

/* ---- 21. Question types -------------------------------------------------------------------
   Every non-multiple-choice format: statement grids, yes/no, hotspot, drag-and-drop, ordering,
   case studies and scenario tables. Correct/wrong keep their emerald/rose meaning throughout. */

/* Statement + scenario tables */
body.app-dark .statement-table, body.app-dark .scenario-table, body.app-dark .review-stmt-table {
  background: var(--ad-glass), var(--ad-panel);
  border: 1px solid rgba(255,255,255,.12); border-radius: 8px; overflow: hidden;
}
body.app-dark .statement-table thead th, body.app-dark .scenario-table thead th {
  background: rgba(255,255,255,.06); color: rgba(255,255,255,.62);
  border-bottom: 1px solid rgba(255,255,255,.12);
}
/* `background` here is LOAD-BEARING, not decoration. The base rule is
   `.scenario-table tbody td { color:#333; background:#fff }`, and this override used to set only
   colour and border - so every scenario table rendered light body text on WHITE cells, which read
   as a blank white block under a correctly-styled header. That was the "table body is empty" bug:
   the rows were always in the DOM, they were just invisible. Never drop the background here. */
body.app-dark .statement-row td, body.app-dark .scenario-table tbody td {
  background: transparent;
  color: rgba(255,255,255,.86);
  border-bottom: none; border-top: 1px solid rgba(255,255,255,.08);
}
body.app-dark .scenario-table tbody tr:first-child td { border-top: none; }
body.app-dark .scenario-table tbody tr:hover td { background: rgba(255,255,255,.04); }
body.app-dark .stmt-text-col { color: var(--ad-text-2); }
body.app-dark .stmt-select {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); color: #fff; border-radius: 8px;
}
body.app-dark .statement-row.correct { background: rgba(52,211,153,.08); }
body.app-dark .statement-row.incorrect { background: rgba(244,63,94,.08); }
body.app-dark .stmt-icon.correct { color: var(--ad-emerald); }
body.app-dark .stmt-icon.incorrect { color: var(--ad-rose); }
body.app-dark .stmt-answer-display { color: var(--ad-text-2); }
body.app-dark .stmt-correct-answer { color: var(--ad-cyan-soft); }

/* Yes / No grids */
body.app-dark .yn-circle { border: 2px solid rgba(255,255,255,.20); background: transparent; }
body.app-dark .yn-circle.checked { border-color: var(--ad-cyan); background: var(--ad-cyan); }

/* Hotspot */
body.app-dark .hs-row { background: var(--ad-glass-soft); border: 1px solid var(--ad-line); }
body.app-dark .hs-stmt { color: var(--ad-text-2); }
body.app-dark .hs-choices { background: transparent; }
body.app-dark .hs-choice {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); color: var(--ad-text-2);
}
body.app-dark label.hs-choice:hover { background: rgba(255,255,255,.08); color: #fff; }
body.app-dark .hs-choice-dot { border: 2px solid rgba(255,255,255,.20); background: transparent; }
body.app-dark .hs-choice.selected {
  background: rgba(34,211,238,.12); border-color: rgba(34,211,238,.40); color: #fff;
}
body.app-dark .hs-choice.selected .hs-choice-dot { border-color: var(--ad-cyan); background: var(--ad-cyan); }
body.app-dark .hs-choice.correct { background: rgba(52,211,153,.12); border-color: rgba(52,211,153,.35); color: #a7f3d0; }
body.app-dark .hs-choice.incorrect { background: rgba(244,63,94,.12); border-color: rgba(244,63,94,.35); color: #fecdd3; }
body.app-dark .hs-mark.ok { color: var(--ad-emerald); }
body.app-dark .hs-mark.no { color: var(--ad-rose); }

/* Drag and drop */
body.app-dark .dd-col-label { color: var(--ad-text-4); }
body.app-dark .dd-count { background: rgba(255,255,255,.08); color: var(--ad-text-3); }
body.app-dark .dd-count.done { background: rgba(52,211,153,.18); color: #6ee7b7; }
body.app-dark .dd-zone, body.app-dark .dd-source, body.app-dark .dd-answer {
  background: var(--ad-glass-soft); border: 1px dashed rgba(255,255,255,.14); border-radius: 12px;
}
body.app-dark .dd-zone.dd-zone-over { background: rgba(34,211,238,.10); border-color: rgba(34,211,238,.45); }
body.app-dark .dd-item {
  background: rgba(255,255,255,.06); border: 1px solid var(--ad-line); color: var(--ad-text-2);
}
body.app-dark .dd-item[draggable="true"]:hover { background: rgba(255,255,255,.10); color: #fff; }
body.app-dark .dd-item.placed { background: rgba(34,211,238,.12); border-color: rgba(34,211,238,.35); color: #fff; }
body.app-dark .dd-num { background: rgba(255,255,255,.10); color: var(--ad-text-3); }
body.app-dark .dd-slot-empty { background: transparent; border: 1px dashed rgba(255,255,255,.14); }
body.app-dark .dd-slot-empty .dd-num { background: rgba(255,255,255,.06); color: var(--ad-text-4); }
body.app-dark .dd-empty, body.app-dark .dd-text { color: var(--ad-text-4); }
body.app-dark .dd-item.correct { background: rgba(52,211,153,.12); border-color: rgba(52,211,153,.35); color: #a7f3d0; }
body.app-dark .dd-item.incorrect { background: rgba(244,63,94,.12); border-color: rgba(244,63,94,.35); color: #fecdd3; }

/* Ordering */
body.app-dark .order-item {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); color: var(--ad-text-2);
}
body.app-dark .order-item:hover { background: rgba(255,255,255,.07); border-color: rgba(255,255,255,.16); }
body.app-dark .order-grip { color: var(--ad-text-4); }
body.app-dark .order-num { background: rgba(255,255,255,.10); color: var(--ad-text-3); }
body.app-dark .order-text { color: inherit; }
body.app-dark .order-btn {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); color: var(--ad-text-3);
}
body.app-dark .order-btn:hover:not([disabled]) { background: rgba(255,255,255,.12); color: #fff; }
body.app-dark .order-icon.correct { color: var(--ad-emerald); }
body.app-dark .order-icon.wrong { color: var(--ad-rose); }
body.app-dark .order-item.correct-position {
  background: rgba(52,211,153,.12); border-color: rgba(52,211,153,.35); color: #a7f3d0;
}
body.app-dark .order-item.correct-position .order-num { background: var(--ad-emerald); color: #06251b; }
body.app-dark .order-item.wrong-position {
  background: rgba(244,63,94,.12); border-color: rgba(244,63,94,.35); color: #fecdd3;
}
body.app-dark .order-item.wrong-position .order-num { background: var(--ad-rose); color: #2a0710; }
body.app-dark .order-correct-wrap { background: var(--ad-glass-soft); border: 1px solid var(--ad-line); }
body.app-dark .order-correct-label, body.app-dark .order-review-col-title { color: var(--ad-text-4); }
body.app-dark .order-correct-wrap .order-item { background: rgba(34,211,238,.10); border-color: rgba(34,211,238,.30); }

/* Case studies */
body.app-dark .casestudy-panel {
  background: var(--ad-glass); border: 1px solid var(--ad-line); border-radius: 16px;
}
body.app-dark .casestudy-head { background: rgba(255,255,255,.03); border-bottom: 1px solid var(--ad-line); }
body.app-dark .casestudy-tag {
  background: rgba(99,102,241,.18); border: 1px solid rgba(99,102,241,.25); color: #c7d2fe;
}
body.app-dark .casestudy-pos { color: var(--ad-text-4); }
body.app-dark .casestudy-intro, body.app-dark .casestudy-body { color: var(--ad-text-2); }
body.app-dark .casestudy-section + .casestudy-section { border-top: 1px solid var(--ad-line-soft); }
body.app-dark .casestudy-h { color: #fff; }
body.app-dark .cs-preview-head h1 { color: #fff; }
/* Now an inline marker on the Domain focus row rather than a full-width bar - see styles.css. */
body.app-dark .exam-cs-review {
  background: none; border: 0; color: rgba(251,191,36,.75);
}
body.app-dark .exam-cs-review:hover { background: none; color: #fcd34d; }

/* Report + bookmark controls.
   These were --ad-glass-soft on a --ad-line border with --ad-text-3 text: a 3%-alpha fill, an
   8%-alpha border and 45%-white text, which on the results panel read as a disabled label rather
   than a control. The fill and border are lifted to where the edge is actually visible and the
   label to --ad-text-2, which is the level the rest of this theme uses for readable secondary text. */
body.app-dark .btn-report-question, body.app-dark .btn-bookmark-question {
  background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.18); color: var(--ad-text-2);
}
body.app-dark .btn-bookmark-question:hover {
  background: rgba(34,211,238,.12); border-color: rgba(34,211,238,.45); color: var(--ad-cyan-soft);
}
/* Report is destructive-adjacent, so its hover is the rose accent rather than the cyan one. */
body.app-dark .btn-report-question:hover {
  background: rgba(244,63,94,.12); border-color: rgba(244,63,94,.45); color: #fda4af;
}
body.app-dark .btn-bookmark-question.bookmarked {
  background: rgba(251,191,36,.16); border-color: rgba(251,191,36,.45); color: #fcd34d;
}
body.app-dark .btn-bookmark-question:focus-visible,
body.app-dark .btn-report-question:focus-visible,
body.app-dark .rq-filter:focus-visible,
body.app-dark .rq-bulk-btn:focus-visible,
body.app-dark .rq-summary:focus-visible { outline: 2px solid var(--ad-cyan); outline-offset: 2px; }
body.app-dark .btn-bookmark-question:disabled, body.app-dark .btn-report-question:disabled,
body.app-dark .btn-bookmark-question[aria-disabled="true"],
body.app-dark .btn-report-question[aria-disabled="true"] {
  background: rgba(255,255,255,.04); border-color: var(--ad-line); color: var(--ad-text-3);
}
body.app-dark .review-q-actions { border-top: 1px solid var(--ad-line); }

/* Results review: filters, bulk controls and the collapsed summary row */
body.app-dark .rq-controls { border-bottom: 1px solid var(--ad-line); }
body.app-dark .rq-filter, body.app-dark .rq-bulk-btn {
  background: rgba(255,255,255,.06); border-color: rgba(255,255,255,.18); color: var(--ad-text-2);
}
body.app-dark .rq-filter:hover, body.app-dark .rq-bulk-btn:hover {
  background: rgba(255,255,255,.10); border-color: rgba(255,255,255,.28); color: #fff;
}
body.app-dark .rq-filter.is-on {
  background: rgba(34,211,238,.16); border-color: rgba(34,211,238,.55); color: var(--ad-cyan-soft);
}
body.app-dark .rq-summary-dom { color: var(--ad-text-3); }
body.app-dark .rq-stem-preview { color: var(--ad-text-2); }
body.app-dark .rq-pick-lbl { color: var(--ad-text-3); }
body.app-dark .rq-pick-val.is-ok { color: var(--ad-emerald); }
body.app-dark .rq-pick-val.is-bad { color: #fda4af; }
body.app-dark .rq-chevron { color: var(--ad-text-3); }
body.app-dark .rq-empty { color: var(--ad-text-3); }
body.app-dark .rres-toprow-quiet { color: var(--ad-text-2); }
body.app-dark .rres-toprow-quiet:hover { color: var(--ad-cyan-soft); }
body.app-dark .report-options { color: var(--ad-text-2); }
body.app-dark .report-validation { color: var(--ad-rose); }

/* 20a. Elements that had to be de-inlined from exam.html before any of this could reach them,
   plus the welcome wrapper itself, which paints #eceff5 behind the whole setup screen. */
body.app-dark #screen-welcome { background: transparent; }
body.app-dark .welcome-mode-lead,
body.app-dark .welcome-mode-hint,
body.app-dark .welcome-field-hint,
body.app-dark #welcome-trust { color: var(--ad-text-3); }
body.app-dark .welcome-field-label { color: #fff; }
body.app-dark .mode-card-rec {
  background: rgba(52,211,153,.16); border: 1px solid rgba(52,211,153,.25); color: #6ee7b7;
}

/* ---- 22. Exams catalog card parity --------------------------------------------------------
   Ported from "D:\UI Redesign\Exams" (arena ref 01a00d92-8f28-7abe-a8a2-66be3595984a).

   Section 9 themed the catalog toolbar and KPIs but never reached inside the card, which left
   six light surfaces. The worst was invisible content, not just a light patch: the Domain focus
   panel keeps `background:#fff` from styles.css while `.exam-card` sets colour to
   rgba(255,255,255,.70), so every domain name and question count was WHITE TEXT ON WHITE.

   The reference also drops the per-vendor accent. styles.css threads `--exam-accent` (AWS is
   #e65100) through the CTA, the category pill, the code chip, the domain bullets and the card's
   top bar, so an AWS card came out orange against a cyan theme. Here the vendor is plain mono
   text and everything else is the shared cyan. The accent variable is left in place - it still
   drives the light theme - it is simply not painted from here. */

/* Domain focus panel */
body.app-dark .exam-domains-section {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); border-radius: 12px;
}
body.app-dark .exam-domains-head strong, body.app-dark .exam-domains-title {
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  color: var(--ad-text-4); font-size: 10px; font-weight: 500; letter-spacing: .1em;
}
body.app-dark .exam-domains-head span {
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  color: var(--ad-text-4); font-weight: 500;
}
body.app-dark .exam-domains-list { color: var(--ad-text-2); }
body.app-dark .exam-domains-list li::before { background: var(--ad-cyan); }
body.app-dark .domain-name { color: var(--ad-text-2); }
body.app-dark .domain-q-count {
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  color: var(--ad-text-4); font-weight: 500;
}
body.app-dark .exam-domains-toggle { color: var(--ad-amber); font-weight: 600; }
body.app-dark .exam-domains-toggle:hover { color: #fcd34d; }

/* Start Practice - was a white button with an orange rule around it. */
body.app-dark .exam-start-btn {
  background: transparent; border: 1px solid rgba(34,211,238,.35);
  color: var(--ad-cyan-soft); border-radius: 999px; font-weight: 600;
}
body.app-dark .exam-start-btn:hover {
  background: rgba(34,211,238,.14); border-color: rgba(34,211,238,.55); color: #a5f3fc;
}

/* Vendor label loses its pill; difficulty keeps a coloured one (set in section 17). */
body.app-dark .exam-card .exam-category-badge {
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  background: none; border: none; padding: 0; border-radius: 0;
  color: var(--ad-text-3); font-size: 10px; font-weight: 600; letter-spacing: .08em;
}

/* Card top bar resolves to a light orange (rgb 241,159,115) via color-mix with #fff. The
   reference has no such bar - the vendor is carried by the label instead. */
body.app-dark .exam-card::before { display: none; }

/* Trust row */
body.app-dark .exam-trust { color: var(--ad-text-4); }
body.app-dark .exam-updated { color: var(--ad-text-3); }
body.app-dark .exam-trust-dot { background: rgba(255,255,255,.25); }

/* The catalog search sets padding-left:42px in styles.css to clear its absolutely positioned
   magnifier. The section 9 rule reset the shorthand and the placeholder ran under the icon. */
body.app-dark .exam-search, body.app-dark .exam-search-wrap input {
  padding: 11px 16px 11px 42px;
}
body.app-dark .exam-search-wrap svg { color: var(--ad-text-4); }

/* 22a. The catalog headline was "Exam" in white plus "Catalog" in a cyan-through-violet gradient,
   and .diff-advanced painted "Professional" violet. Both are gone. The title is plain white, and
   section 26 already made every level badge a neutral label - this override existed only to add
   violet back for one tier. Violet now appears nowhere in the signed-in app.
   .catalog-title-accent has no rule left because the span it styled is out of the markup. */

/* 20b. Setup-screen simplification (see exam.html): collapsed Exam details, one per-mode helper
   line, and the "Show all domains" disclosure. */
/* "Exam details" is a CONTROL, and it was grouped with the section labels at --ad-text-4 (30%
   white, ~2.6:1), which is the value a disabled control gets - so it read as one. It is the only
   way to reach the real-exam format notes, so it now sits at the weight of the other disclosures
   on this card. The section label keeps the quiet value: it is a label, and labels are meant to
   recede. Sharing one rule between a control and a label is what caused this. */
body.app-dark .welcome-details > summary { color: rgba(255,255,255,.74); }
body.app-dark .welcome-details > summary:hover { color: #fff; }
body.app-dark .welcome-details > summary:focus-visible { outline: 2px solid var(--ad-cyan); outline-offset: 2px; border-radius: 4px; }
/* Reading contrast, not label contrast - see the note beside .welcome-header p. Same selector as
   the earlier rule in section 21, and this one is later, so the value lives here. */
body.app-dark .welcome-mode-label { color: var(--ad-text-4); }

/* ---- 23. Practice contrast + theme toggle -------------------------------------------------
   Answered options were washing out on the dark theme. The cause is that styles.css declares the
   reveal states with !important:

     .option-item.correct-reveal { border-color:#2e7d32 !important; background:#e8f5e9 !important }
     .option-item.wrong-reveal   { border-color:#c62828 !important; background:#ffebee !important }
     .option-item.neutral-reveal { opacity:.45 !important }

   so no amount of specificity in section 20 could reach them - a light green/pink fill stayed under
   near-white text. This is the one place in this file where !important is the correct answer rather
   than a shortcut: the declaration being overridden is itself !important. Everything else in
   app-dark.css still wins on specificity alone.

   Unselected options also went to .45 opacity, which read as disabled rather than simply not
   chosen; .72 keeps them legible while still receding. */
body.app-dark .option-item.correct-reveal {
  background: rgba(52,211,153,.16) !important;
  border-color: rgba(52,211,153,.55) !important;
  color: #d1fae5 !important;
}
body.app-dark .option-item.wrong-reveal {
  background: rgba(244,63,94,.16) !important;
  border-color: rgba(244,63,94,.55) !important;
  color: #ffe4e6 !important;
}
body.app-dark .option-item.neutral-reveal { opacity: .72 !important; }
body.app-dark .option-item.correct-reveal .option-key {
  background: var(--ad-emerald); border-color: transparent; color: #06251b;
}
body.app-dark .option-item.wrong-reveal .option-key {
  background: var(--ad-rose); border-color: transparent; color: #2a0710;
}
/* The tick/cross beside a revealed option: #2e7d32 / #c62828 are too dark to read here. */
body.app-dark .reveal-icon.correct { color: #6ee7b7; }
body.app-dark .reveal-icon.wrong { color: #fda4af; }

/* "Correct answer: D) ..." was bright cyan and bold, competing with the explanation below it.
   Quieter surface, emerald to match the correct-option language, normal weight. */
/* `.practice-feedback.incorrect .correct-hint` is (0,3,0) and carries an ORANGE tint
   (rgba(230,81,0,.08)) - it beat the (0,2,1) rule this replaces, so the line reading out the
   CORRECT answer was tinted orange on both themes. Matched at its own strength, in emerald to
   agree with the correct-option language. */
body.app-dark .correct-hint,
body.app-dark .practice-feedback.incorrect .correct-hint,
body.app-dark .practice-feedback.correct .correct-hint {
  background: rgba(52,211,153,.08); border: 1px solid rgba(52,211,153,.20);
  border-radius: 10px; color: #a7f3d0; font-weight: 500;
}

/* Long-form reading: the explanation body carries the most text on the page. */
body.app-dark .practice-feedback .explanation-text,
body.app-dark .review-explanation .explanation-text { color: rgba(255,255,255,.78); line-height: 1.7; }
body.app-dark .practice-feedback .feedback-body { line-height: 1.65; }

/* Theme toggle, dark side. Dark shows the sun - the theme the click moves to. */
body.app-dark .exam-theme-btn {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); color: var(--ad-text-3);
}
body.app-dark .exam-theme-btn:hover {
  background: rgba(255,255,255,.10); border-color: rgba(255,255,255,.16); color: #fff;
}
body.app-dark .exam-theme-btn .etb-moon { display: none; }
body.app-dark .exam-theme-btn .etb-sun { display: block; }

/* Mobile: the header packs progress, timer and the toggle into one row. */
@media (max-width: 640px) {
  .exam-header-info .exam-theme-btn { height: 28px; width: 28px; }
  .exam-header-info .exam-theme-btn svg { height: 14px; width: 14px; }
}

/* 22b. Catalog domain chips (replaces the in-card domain list and its "Show less" toggle). */
body.app-dark .exam-domain-chip {
  background: rgba(255,255,255,.04); border: 1px solid var(--ad-line);
  color: var(--ad-text-2); font-weight: 500;
}
body.app-dark .exam-domain-chip-more {
  background: rgba(251,191,36,.12); border-color: rgba(251,191,36,.22);
  color: var(--ad-amber); font-weight: 600;
}
body.app-dark .exam-domain-empty { color: var(--ad-text-4); }

/* ---- 24. Admin surfaces that section 16 never reached --------------------------------------
   admin.html has never been rendered - its role gate calls the API and redirects to the dashboard
   locally - so these went unnoticed. A static coverage diff of every light-painting rule in
   styles.css against what this file overrides turned up the gap, and rendering the markup in a
   throwaway harness confirmed 60 light surfaces: white insight cards, white toolbar buttons, a
   white table-showing bar, white chart panels, and every status pill still on its pastel fill.

   Tone semantics are preserved throughout: good = emerald, info = cyan, warn = amber,
   risk/danger = rose, neutral = plain glass. */

/* Insight cards (the quick-filter tiles above the users table) */
body.app-dark .admin-insight-card {
  background: var(--ad-glass); border: 1px solid var(--ad-line);
  border-left: 3px solid rgba(255,255,255,.20); border-radius: 14px; color: var(--ad-text-2);
}
body.app-dark .admin-insight-card:hover {
  background: rgba(255,255,255,.06); border-color: rgba(255,255,255,.16);
}
body.app-dark .admin-insight-card.good { border-left-color: var(--ad-emerald); }
body.app-dark .admin-insight-card.info { border-left-color: var(--ad-cyan); }
body.app-dark .admin-insight-card.warn { border-left-color: var(--ad-amber); }
body.app-dark .admin-insight-card.risk { border-left-color: var(--ad-rose); }
body.app-dark .admin-insight-card.neutral { border-left-color: rgba(255,255,255,.20); }

/* Toolbar, quick filters and row-action buttons */
body.app-dark .admin-toolbar-actions {
  border-right: 1px solid var(--ad-line); border-bottom: 1px solid var(--ad-line);
}
body.app-dark .admin-toolbar-btn,
body.app-dark .admin-container .action-menu-btn,
body.app-dark .action-menu-btn,
body.app-dark .quick-filter,
body.app-dark .admin-container .quick-filter,
body.app-dark .row-action-trigger {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line);
  color: var(--ad-text-2); border-radius: 999px;
}
body.app-dark .admin-toolbar-btn:hover,
body.app-dark .admin-container .action-menu-btn:hover,
body.app-dark .action-menu-btn:hover,
body.app-dark .quick-filter:hover,
body.app-dark .row-action-trigger:hover {
  background: rgba(255,255,255,.10); border-color: rgba(255,255,255,.18); color: #fff;
}
body.app-dark .quick-filter.active,
body.app-dark .admin-container .quick-filter.active,
body.app-dark .row-action-trigger.active {
  background: rgba(34,211,238,.16); border-color: rgba(34,211,238,.40); color: var(--ad-cyan-soft);
}
body.app-dark .table-showing,
body.app-dark .admin-users-section .table-showing {
  background: transparent; border-bottom: 1px solid var(--ad-line); color: var(--ad-text-4);
}
body.app-dark .admin-exams-table .sort-indicator, body.app-dark .sort-indicator { color: var(--ad-text-4); }

/* Row action menus */
body.app-dark .action-menu {
  background: var(--ad-panel); border: 1px solid var(--ad-line); box-shadow: var(--ad-glass-shadow);
}
body.app-dark .action-menu-item { background: transparent; color: var(--ad-text-2); }
body.app-dark .action-menu-item:hover { background: rgba(255,255,255,.08); color: #fff; }
body.app-dark .action-menu-item.action-danger, body.app-dark .row-action-menu .action-danger { color: #fda4af; }
body.app-dark .action-menu-item.action-danger:hover,
body.app-dark .row-action-menu .action-danger:hover { background: rgba(244,63,94,.14); color: #fecdd3; }
body.app-dark .action-menu-item[disabled] { color: var(--ad-text-4); }

/* Status pills: chips, activity, presence, event badges */
body.app-dark .admin-chip.good, body.app-dark .admin-activity-pill.good,
body.app-dark .online-badge.online, body.app-dark .badge-evt-green {
  background: rgba(52,211,153,.14); border: 1px solid rgba(52,211,153,.24); color: #6ee7b7;
}
body.app-dark .admin-chip.info, body.app-dark .badge-evt-blue {
  background: rgba(34,211,238,.14); border: 1px solid rgba(34,211,238,.24); color: var(--ad-cyan-soft);
}
body.app-dark .admin-chip.warn, body.app-dark .admin-activity-pill.warn,
body.app-dark .online-badge.away, body.app-dark .badge-evt-orange {
  background: rgba(251,191,36,.14); border: 1px solid rgba(251,191,36,.24); color: #fcd34d;
}
body.app-dark .admin-chip.risk, body.app-dark .admin-activity-pill.risk {
  background: rgba(244,63,94,.14); border: 1px solid rgba(244,63,94,.24); color: #fda4af;
}
body.app-dark .admin-activity-pill.neutral, body.app-dark .online-badge.offline {
  background: rgba(255,255,255,.06); border: 1px solid var(--ad-line); color: var(--ad-text-3);
}
body.app-dark .badge-evt-indigo {
  background: rgba(99,102,241,.16); border: 1px solid rgba(99,102,241,.26); color: #c7d2fe;
}
body.app-dark .badge-evt-purple {
  background: rgba(168,85,247,.16); border: 1px solid rgba(168,85,247,.26); color: #d8b4fe;
}
body.app-dark .online-badge.online::before { background: var(--ad-emerald); }
body.app-dark .online-badge.away::before { background: var(--ad-amber); }
body.app-dark .online-badge.offline::before { background: rgba(255,255,255,.30); }

/* Generic card wrapper and the signup / billing charts */
body.app-dark .card,
body.app-dark .signup-trend-stat,
body.app-dark .signup-chart,
body.app-dark .billing-trend-stat {
  background: linear-gradient(180deg, rgba(255,255,255,.055), rgba(255,255,255,.028));
  border: 1px solid var(--ad-line);
  box-shadow: var(--ad-glass-shadow); border-radius: 16px; color: var(--ad-text-2);
}
body.app-dark .signup-col-bar { border-bottom: 2px solid var(--ad-line); }
body.app-dark .signup-col-fill {
  background: linear-gradient(180deg, var(--ad-cyan), var(--ad-emerald));
  box-shadow: 0 8px 22px -14px rgba(34,211,238,.85), inset 0 1px 0 rgba(255,255,255,.22);
}
body.app-dark .signup-col.is-zero .signup-col-fill { background: rgba(255,255,255,.10); box-shadow: none; }
body.app-dark .signup-col-value, body.app-dark .billing-trend-stat .bt-value { color: #fff; }
body.app-dark .signup-trend-stat .bt-value { color: var(--ad-cyan-soft); }
body.app-dark .signup-col.is-zero .signup-col-value,
body.app-dark .signup-col-month,
body.app-dark .signup-trend-stat .bt-trend { color: var(--ad-text-4); }
body.app-dark .signup-trend-stat .trend-up { color: var(--ad-emerald); }
body.app-dark .signup-trend-stat .trend-down { color: var(--ad-rose); }

/* Leaderboard */
body.app-dark .leaderboard-item { border-bottom: 1px solid var(--ad-line-soft); }
body.app-dark .leaderboard-rank {
  background: rgba(255,255,255,.08); color: var(--ad-text-2);
}
/* The rank badge was the only part the dark theme re-skinned, so the name and value kept their
   light-theme #333 and #1a237e from styles.css and were near-invisible on the dark glass. The
   name stays a soft white rather than pure white so the values and card headings keep the
   emphasis, and each column gets its own accent so the three cards read apart at a glance. */
body.app-dark .admin-container .leaderboard-item {
  padding: 9px 0;
  gap: 10px;
}
body.app-dark .admin-container .leaderboard-name {
  color: rgba(255, 255, 255, .76);
  line-height: 1.35;
}
/* One neutral treatment for every leaderboard value. Each list used to have its own colour -
   cyan for starts, violet for study time, emerald for growth - so the palette encoded WHICH LIST
   you were looking at, which the heading above it already says. Green is applied per row instead,
   by .is-growth, and only when the number actually went up. */
body.app-dark .admin-container .leaderboard-value {
  color: var(--ad-text-2);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
body.app-dark .admin-container .leaderboard-value.is-growth { color: #6ee7b7; }
/* Neutral ranks. Gold, silver and violet podium colours on an internal analytics list restate the
   ordering the numbers 1, 2, 3 already carry. */
body.app-dark .leaderboard-item .leaderboard-rank,
body.app-dark .leaderboard-item:nth-child(1) .leaderboard-rank,
body.app-dark .leaderboard-item:nth-child(2) .leaderboard-rank,
body.app-dark .leaderboard-item:nth-child(3) .leaderboard-rank {
  background: rgba(255,255,255,.06); color: var(--ad-text-3);
  border: 1px solid var(--ad-line); font-weight: 600;
}

/* Alerts, form messages and small shared bits reachable from admin and profile */
body.app-dark .alert-success, body.app-dark .form-message-success {
  background: rgba(52,211,153,.10); border: 1px solid rgba(52,211,153,.22); color: #6ee7b7;
}
body.app-dark .alert-warning {
  background: rgba(251,191,36,.10); border: 1px solid rgba(251,191,36,.22); color: #fcd34d;
}
body.app-dark .form-message-error {
  background: rgba(244,63,94,.10); border: 1px solid rgba(244,63,94,.22); color: #fda4af;
}
body.app-dark .modal-check {
  background: rgba(244,63,94,.10); border: 1px solid rgba(244,63,94,.22); color: #fecdd3;
}
body.app-dark .empty-state-card .es-icon {
  background: rgba(255,255,255,.06); color: var(--ad-cyan-soft);
}
body.app-dark .form-hint, body.app-dark .trend-placeholder-note,
body.app-dark .exam-domain-empty, body.app-dark .wrong-notes-header,
body.app-dark .report-expand-icon, body.app-dark .pw-toggle { color: var(--ad-text-4); }
body.app-dark .history-footer-left .footer-sep { background: rgba(255,255,255,.18); }
body.app-dark .loading::after { border: 3px solid rgba(255,255,255,.12); border-top-color: var(--ad-cyan); }

/* Tabs + table cells: `.tabs` kept a light 2px rule, and admin table cells their #0f172a text. */
body.app-dark .tabs { border-bottom: 2px solid var(--ad-line); }
body.app-dark .admin-table td, body.app-dark .admin-exams-table td { color: var(--ad-text-2); }

/* Bookmarks + exam-page leftovers from the same sweep */
body.app-dark .btn-remove-bookmark:hover { border-color: rgba(244,63,94,.35); color: #fda4af; }
body.app-dark .bookmark-option.correct {
  background: rgba(52,211,153,.12); border-color: rgba(52,211,153,.30); color: #a7f3d0;
}
body.app-dark .pf-btn {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); color: var(--ad-text-2);
}
body.app-dark .pf-btn:hover { background: rgba(255,255,255,.10); color: #fff; }
body.app-dark .pf-btn.saved {
  background: rgba(52,211,153,.14); border-color: rgba(52,211,153,.28); color: #6ee7b7;
}
body.app-dark .pf-btn-primary {
  background: linear-gradient(90deg, var(--ad-cyan), #5eead4); border-color: transparent; color: #071018;
}
/* The rule-out button, unboxed. app-dark set its colour, its border and a HOVER background, and
   never a resting one - so styles.css's `background: #fff` survived, and every option in exam mode
   carried a bright white block on its right that only went dark when the pointer was over it. The
   same omission applied to .active, where the light theme paints a solid #e53935 fill: rose text on
   a red fill, which is both loud and unreadable.

   A property left unset does not inherit the dark theme, it keeps whatever the light theme gave it.
   Both states are stated here rather than trusting the earlier block to have covered them. */
body.app-dark .option-eliminate {
  border: 2px solid var(--ad-line);
  background: rgba(255,255,255,.03);
}
body.app-dark .option-eliminate.active {
  background: rgba(244,63,94,.14); border-color: rgba(244,63,94,.42); color: var(--ad-rose);
}
body.app-dark .review-container { background: transparent; }
body.app-dark .btn.mark-active {
  background: rgba(251,191,36,.14); border-color: rgba(251,191,36,.40); color: #fcd34d;
}
body.app-dark .btn.mark-active:hover { background: rgba(251,191,36,.22); border-color: rgba(251,191,36,.55); }
body.app-dark .review-stat.answered, body.app-dark .status-answered {
  background: rgba(52,211,153,.14); color: #6ee7b7;
}
body.app-dark .review-stat.unanswered, body.app-dark .status-unanswered {
  background: rgba(255,255,255,.06); color: var(--ad-text-3);
}
body.app-dark .review-stat.marked, body.app-dark .status-marked {
  background: rgba(251,191,36,.14); color: #fcd34d;
}
body.app-dark .domain-bar-fill.pass { background: linear-gradient(90deg, var(--ad-emerald), #10b981); }
body.app-dark .domain-bar-fill.fail { background: linear-gradient(90deg, var(--ad-amber), #f59e0b); }
body.app-dark .recent-attempt-status.mini-pass {
  background: rgba(52,211,153,.14); border: 1px solid rgba(52,211,153,.24); color: #6ee7b7;
}
body.app-dark .recent-attempt-status.mini-fail {
  background: rgba(244,63,94,.14); border: 1px solid rgba(244,63,94,.24); color: #fda4af;
}
body.app-dark .plan-banner, body.app-dark .plan-banner-single {
  background: linear-gradient(135deg, rgba(99,102,241,.14), rgba(34,211,238,.08));
  border: 1px solid rgba(99,102,241,.26); color: var(--ad-text-2);
}
body.app-dark .plan-banner-text strong { color: #fff; }
body.app-dark .exam-card-footer { border-top: 1px solid var(--ad-line); }
body.app-dark .catalog-section-label { border-bottom-color: var(--ad-line); color: var(--ad-text-4); }

/* Case-study and report panels (super-admin review flows) */
body.app-dark .cs-q { border-top: 1px dashed var(--ad-line); }
body.app-dark .cs-opts li.cs-correct {
  background: rgba(52,211,153,.12); border-color: rgba(52,211,153,.32); color: #a7f3d0;
}
body.app-dark .cs-exp, body.app-dark .cs-modal-head {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); color: var(--ad-text-2);
}
body.app-dark .stmt-icon.correct { background: rgba(52,211,153,.22); color: #6ee7b7; }
body.app-dark .stmt-icon.incorrect { background: rgba(244,63,94,.22); color: #fda4af; }
body.app-dark .dd-count { border: 1px solid var(--ad-line); }
body.app-dark .dd-count.done { border-color: rgba(52,211,153,.30); }
body.app-dark .report-row-clickable:hover { background: rgba(255,255,255,.04); }
body.app-dark .admin-empty-panel {
  background: linear-gradient(180deg, rgba(255,255,255,.045), rgba(255,255,255,.024));
  border: 1px solid var(--ad-line);
  box-shadow: var(--ad-glass-shadow);
}
body.app-dark .admin-empty-panel strong {
  color: #fff;
}
body.app-dark .admin-empty-panel span,
body.app-dark .report-user-email,
body.app-dark .report-notes-cell {
  color: var(--ad-text-4);
}
body.app-dark .report-empty-error {
  border-color: rgba(244,63,94,.26);
}
body.app-dark .report-empty-error strong {
  color: var(--ad-rose);
}
body.app-dark .report-date-cell,
body.app-dark .report-question-id {
  color: var(--ad-text-2);
}
body.app-dark .report-exam-code {
  color: var(--ad-cyan-soft);
}
body.app-dark .report-actions-cell .btn-secondary {
  background: rgba(255,255,255,.045);
  border-color: rgba(255,255,255,.09);
  color: var(--ad-text-2);
}
body.app-dark .report-actions-cell .btn-secondary:hover {
  background: rgba(34,211,238,.10);
  border-color: rgba(34,211,238,.22);
  color: #fff;
}
body.app-dark .report-question-preview {
  background: var(--ad-glass-soft); border-top: 1px solid var(--ad-line); color: var(--ad-text-2);
}
body.app-dark .rqp-option {
  background: rgba(255,255,255,.04); border: 1px solid var(--ad-line); color: var(--ad-text-2);
}
body.app-dark .rqp-option.rqp-correct {
  background: rgba(52,211,153,.12); border-color: rgba(52,211,153,.30); color: #a7f3d0;
}
body.app-dark .rqp-explanation {
  background: rgba(251,191,36,.08); border: 1px solid rgba(251,191,36,.20); color: var(--ad-text-2);
}
body.app-dark .legal-modal { background: var(--ad-panel); border: 1px solid var(--ad-line); }
body.app-dark .legal-modal-footer { border-top: 1px solid var(--ad-line); }
body.app-dark .legal-modal-close, body.app-dark .legal-modal-meta { color: var(--ad-text-4); }

/* 24a. The row-action dropdown panel itself. Section 24 styled `.action-menu` and the items, but
   the container admin.js actually renders is `.action-menu-items` - a white panel with a light
   rule, so the menu stayed a bright card floating over the dark table. */
body.app-dark .action-menu-items {
  background: var(--ad-panel); border: 1px solid var(--ad-line);
  box-shadow: var(--ad-glass-shadow); border-radius: 12px;
}

/* 25. Dashboard practice cards: personal progress footer + CTA.
   The section is no longer a generic recommendation grid, so the cards carry the context that
   makes them personal - status, last score, last activity - and an explicit CTA whose wording
   depends on prior activity. The card is a <div> again rather than one big <a>: a real button
   inside an anchor is invalid, and the CTA is what the brief asks for. */
body.app-dark .dash-exam-card { display: flex; flex-direction: column; }
body.app-dark .dash-card-progress {
  align-items: center; display: flex; flex-wrap: wrap; gap: 8px;
  margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--ad-line-soft);
}
body.app-dark .dash-card-status {
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  border-radius: 999px; font-size: 10px; font-weight: 600; letter-spacing: .06em;
  padding: 3px 8px; text-transform: uppercase;
}
body.app-dark .dash-card-status.is-continue {
  background: rgba(34,211,238,.14); border: 1px solid rgba(34,211,238,.26); color: var(--ad-cyan-soft);
}
body.app-dark .dash-card-status.is-retake {
  background: rgba(251,191,36,.14); border: 1px solid rgba(251,191,36,.26); color: #fcd34d;
}
body.app-dark .dash-card-status.is-review {
  background: rgba(52,211,153,.14); border: 1px solid rgba(52,211,153,.26); color: #6ee7b7;
}
body.app-dark .dash-card-score { color: var(--ad-text-2); font-size: 12px; font-weight: 600; }
body.app-dark .dash-card-when {
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  color: var(--ad-text-4); font-size: 11px; margin-left: auto;
}
body.app-dark .dash-card-cta {
  display: block; margin-top: 14px; text-align: center; width: 100%;
}
/* The grid sizes columns to the count when the section is personal, so one started exam shows one
   card at card width instead of stretching across the row. */
body.app-dark .exam-grid.exam-grid-personal {
  grid-template-columns: repeat(auto-fill, minmax(260px, 320px));
}

/* 25a. Readiness percentage band. De-inlined in dashboard.js - it used to set style.color to the
   light-theme traffic-light values, which an inline style made unbeatable, so the number rendered
   dark navy/green/red on the dark ring. `.ring-text` is a div, not SVG text, so it needs `color`;
   the earlier rule set `fill` and never applied. */
body.app-dark .readiness-ring .ring-text { color: #fff; fill: #fff; }
body.app-dark .ring-text.ring-none { color: var(--ad-text-4); }
body.app-dark .ring-text.ring-weak { color: #fda4af; }
body.app-dark .ring-text.ring-mid { color: #fcd34d; }
body.app-dark .ring-text.ring-strong { color: #6ee7b7; }

/* ---- 26. Dashboard top rhythm + primary-action emphasis ------------------------------------
   .stat-note is de-inlined now (dashboard.html kept only display:none, which dashboard.js
   toggles), so the muted colour finally comes from the theme instead of a hard-coded #94a3b8. */
body.app-dark .stat-note { color: var(--ad-text-4); }

/* Recommended Next Step is the one action the page exists to push, so it keeps a cyan edge the
   other panels do not have - but a fainter one, and no outer bloom. It already leads with more
   room, a 20px line, a 52px icon and the page's only filled button; the glow was a fifth way of
   saying the same thing, and it was the one that read as a different material. Must sit AFTER the
   shared-surfaces rule in section 5 - same specificity (0,2,0), so source order decides. */
body.app-dark .recommended-card {
  border-color: rgba(34,211,238,.16);
  box-shadow: var(--ad-glass-shadow);
}

/* ---- 27. Exam History (Attempts) --------------------------------------------------------------
   Dark counterparts for the page rules just moved into styles.css. Several are ID-scoped there
   (#history-body ... = specificity 1,0,3), which no plain `body.app-dark .class` rule can beat -
   so those are matched here with the same ID plus the body class (1,1,3) and win by one class.
   The rest only need the body-class prefix to outrank their light originals. */

/* The white block. Header band becomes a muted, uppercase label row on the panel surface. */
body.app-dark #history-body table thead th {
  background: rgba(255,255,255,.03);
  color: var(--ad-text-4);
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 10px; font-weight: 500; letter-spacing: .12em; text-transform: uppercase;
  border-bottom: 1px solid var(--ad-line);
}
body.app-dark #history-card {
  border: 1px solid var(--ad-line);
  box-shadow: var(--ad-glass-shadow);
}
body.app-dark .history-row-clickable:hover td { background: rgba(255,255,255,.04); }

/* Status badges. Calmer, one hue per state: green pass, red fail, blue in-progress, grey
   abandoned. Same soft-pill treatment as the rest of the dark app. */
body.app-dark #history-body .badge-pass {
  background: rgba(52,211,153,.15); color: #6ee7b7; border: 1px solid rgba(52,211,153,.25);
}
body.app-dark #history-body .badge-fail {
  background: rgba(244,63,94,.15); color: #fda4af; border: 1px solid rgba(244,63,94,.25);
}
body.app-dark #history-body .badge-info {
  background: rgba(56,189,248,.14); color: #7dd3fc; border: 1px solid rgba(56,189,248,.24);
}
body.app-dark #history-body .badge-abandoned,
body.app-dark #history-body .badge-muted {
  background: rgba(255,255,255,.05); color: var(--ad-text-4); border: 1px solid var(--ad-line);
}

/* Score column + helper text. */
body.app-dark .hist-score strong { color: #fff; }
body.app-dark .hist-score strong.sc-pass { color: #6ee7b7; }
body.app-dark .hist-score strong.sc-fail { color: var(--ad-text-3); }
body.app-dark .hist-score-max { color: var(--ad-text-4); }
body.app-dark .page-header p { color: var(--ad-text-3); }
body.app-dark .stat-help { color: var(--ad-text-4); }
body.app-dark .stat-card .stat-label { color: var(--ad-text-4); }
/* The light rule carries !important, so this one has to as well. */
body.app-dark .stat-card .stat-value.stat-empty { color: var(--ad-text-4) !important; }
body.app-dark .stat-card.accent-orange .stat-value { color: #fbbf24; }

/* Stat-card icon badges: tinted glass instead of pale pastel fills. */
body.app-dark .stat-card .stat-icon { box-shadow: none; }
body.app-dark .stat-card.accent-blue .stat-icon { background: rgba(56,189,248,.14); color: #7dd3fc; }
body.app-dark .stat-card.accent-green .stat-icon { background: rgba(52,211,153,.14); color: #6ee7b7; }
body.app-dark .stat-card.accent-purple .stat-icon { background: rgba(168,85,247,.16); color: #d8b4fe; }
body.app-dark .stat-card.accent-orange .stat-icon { background: rgba(251,191,36,.14); color: #fcd34d; }

/* Filters: match the dark control treatment used across the app. */
body.app-dark .history-toolbar .header-filter {
  border: 1.5px solid var(--ad-line); background: var(--ad-glass-soft); color: #fff;
}
body.app-dark .history-toolbar .header-filter:focus {
  border-color: rgba(34,211,238,.45); box-shadow: 0 0 0 3px rgba(34,211,238,.12);
}
body.app-dark .row-action-trigger { color: var(--ad-text-3); }
body.app-dark .row-action-trigger:hover,
body.app-dark .row-action-trigger.active {
  color: #fff; background: rgba(255,255,255,.08); border-color: transparent;
}
body.app-dark .row-action-trigger:focus-visible { outline: 2px solid rgba(34,211,238,.55); }

/* ---- 28. Study Planner refinements -------------------------------------------------------------
   The page's light rules now live in styles.css (moved out of an inline <style> block), so section
   14 above applies again and only the pieces called out in review need saying here. */

/* Progress bars: 5px instead of 8px, and a flat cyan fill rather than a saturated navy-to-teal
   gradient. At 8px with a gradient they were the loudest thing in the Today panel. */
body.app-dark .sp-progress { height: 5px; background: rgba(255,255,255,.07); }
body.app-dark .sp-progress-fill { background: rgba(34,211,238,.65); }
body.app-dark .sp-mini-bar .sp-progress { height: 4px; }

/* Calendar: smaller cells, calmer fills. min-height 52px with aspect-ratio 1 made a month of
   mostly-empty cells dominate the tab; 40px keeps the shape readable and hands the width back
   to the Today panel beside it. */
body.app-dark .sp-cal-grid { max-width: 340px; gap: 5px; }
body.app-dark .sp-cal-day {
  min-height: 40px; border-radius: 7px;
  background: rgba(255,255,255,.035); color: var(--ad-text-4); border: 1px solid transparent;
}
body.app-dark .sp-cal-day.none { background: rgba(255,255,255,.025); color: var(--ad-text-4); }
body.app-dark .sp-cal-day.met { background: rgba(52,211,153,.16); color: #6ee7b7; }
body.app-dark .sp-cal-day.partial { background: rgba(251,191,36,.14); color: #fcd34d; }
body.app-dark .sp-cal-day.today {
  background: rgba(34,211,238,.12); color: #67e8f9;
  border-color: rgba(34,211,238,.45); box-shadow: none;
}
body.app-dark .sp-cal-dow { color: var(--ad-text-4); font-size: 10px; }

/* Legend stays, but as a caption rather than a row of swatches competing with the grid. */
body.app-dark .sp-legend { color: var(--ad-text-4); font-size: 11px; gap: 14px; }
body.app-dark .sp-legend .met::before { background: rgba(52,211,153,.55); }
body.app-dark .sp-legend .partial::before { background: rgba(251,191,36,.5); }
body.app-dark .sp-legend .none::before { background: rgba(255,255,255,.10); }

/* Today panel carries the focus: hairline divider instead of a light rule, tiles on glass. */
body.app-dark .sp-insight-panel { border-left: 1px solid var(--ad-line-soft); padding-left: 22px; }
body.app-dark .sp-tile {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); border-radius: 10px;
}
body.app-dark .sp-tile-val { color: #fff; }
body.app-dark .sp-tile-lbl { color: var(--ad-text-4); }

/* Plan actions beside the exam chip + the readiness zero-state CTA. */
body.app-dark .sp-plan-action { color: var(--ad-text-3); }
body.app-dark .sp-plan-action:hover { color: #fff; }
body.app-dark .sp-plan-action.danger { color: #fda4af; }
body.app-dark .sp-stat-cta { color: var(--ad-cyan-soft); }

/* Tabs: the light pill group was a pale #eef1f6 block on a dark page. */
body.app-dark #sp-tabs.tabs {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); border-radius: 11px;
}
body.app-dark #sp-tabs .tab { color: var(--ad-text-3); }
body.app-dark #sp-tabs .tab:hover { color: #fff; }
body.app-dark #sp-tabs .tab.active {
  background: rgba(255,255,255,.08); color: #fff; box-shadow: none;
}
body.app-dark #sp-tab-calendar .section-card h3 { border-bottom: 1px solid var(--ad-line-soft); }

/* Tip card: the five weakest remaining domains, between the body copy and the bottom-pinned CTA. */
body.app-dark .dash-tip-list { margin-top: 16px; }
body.app-dark .dash-tip-list-head {
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 9.5px; letter-spacing: .14em; text-transform: uppercase;
  color: rgba(196,181,253,.55); margin-bottom: 8px;
}
/* min-height, not height: the row must not be resizable by its content (a long domain name would
   otherwise change how many rows fit and make the card's rhythm depend on the exam), but it still
   needs to grow if a browser renders the text larger than expected. */
body.app-dark .dash-tip-row {
  display: grid; grid-template-columns: minmax(0, 1fr) 46px 30px;
  align-items: center; gap: 8px; min-height: 36px;
  padding: 7px 0; text-decoration: none;
  border-top: 1px solid rgba(255,255,255,.05);
}
body.app-dark .dash-tip-row:first-of-type { border-top: none; }
body.app-dark .dash-tip-row-name {
  font-size: 12px; color: var(--ad-text-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
body.app-dark .dash-tip-row:hover .dash-tip-row-name { color: #fff; }
body.app-dark .dash-tip-row-bar {
  height: 4px; border-radius: 99px; background: rgba(255,255,255,.08); overflow: hidden;
}
body.app-dark .dash-tip-row-bar > span {
  display: block; height: 100%; border-radius: 99px; background: rgba(168,85,247,.55);
}
body.app-dark .dash-tip-row-pct {
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 11px; color: var(--ad-text-4); text-align: right;
}

/* Readiness by Domain is the ONLY card on this dashboard with an internal scrollbar, because it
   is the only one whose job is to be a complete list. A fixed height (not a max-height) is what
   makes the left column stop moving: with a ceiling, a four-domain exam still produced a shorter
   card than a fifteen-domain one, so Recent Attempts jumped every time the exam changed.
   Three bands - heading, rows, note - with only the middle one scrolling, so the exam selector and
   the note stay put while the rows move under them. 340px shows five rows. */
@media (min-width: 961px) {
  /* The ROW sets the height now (grid-template-rows in styles.css), so the card takes 100% of it
     rather than declaring a number of its own - that is what puts its bottom edge on the same line
     as Readiness Snapshot beside it. Three bands, only the middle one scrolling, so the exam
     selector and the note stay put while the rows move under them. */
  body.app-dark #domain-section {
    display: grid; grid-template-rows: auto minmax(0, 1fr) auto;
    overflow: hidden;
  }
  body.app-dark #domain-bars {
    min-height: 0; overflow-y: auto; overflow-x: hidden;
  }
}
body.app-dark #domain-bars {
  overscroll-behavior: contain;
  scrollbar-gutter: stable; padding-right: 8px;
  scrollbar-width: thin; scrollbar-color: rgba(148,163,184,.35) transparent;
}
body.app-dark .domain-foot {
  padding-top: 10px; font-size: 11px; color: rgba(255,255,255,.45);
}
body.app-dark #domain-bars.has-more {
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 26px), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 26px), transparent);
}
body.app-dark #domain-bars:focus-visible {
  outline: 2px solid var(--ad-cyan-soft); outline-offset: 2px; border-radius: 8px;
}
body.app-dark #domain-bars::-webkit-scrollbar { width: 6px; }
body.app-dark #domain-bars::-webkit-scrollbar-thumb {
  background: rgba(148,163,184,.35); border-radius: 3px;
}
body.app-dark #domain-bars::-webkit-scrollbar-track { background: transparent; }

/* Load placeholders (see .dash-skel-bar in styles.css). The light base and the near-white sweep
   read as flashing panels on the dark canvas, so both drop to a whisper here. */
body.app-dark .dash-skel-bar { background: rgba(255,255,255,.055); }
body.app-dark .dash-skel-bar::after {
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.075), transparent);
}

/* ---- 29. Profile + sidebar footer weight -------------------------------------------------------
   Sidebar: the Pro card and the user row started at different x (card inner edge was margin 12 +
   padding 14 = 26px, the user row was 12px), which is the "inconsistent side padding" in review.
   Both sit on 24px now. The row is also shorter and the avatar's ring is thinner, so the footer
   stops out-weighing the nav above it. */
body.app-dark .sidebar-card { margin: 0 12px 10px; padding: 12px; }
body.app-dark .sidebar-card.is-pro::before {
  width: 72px; height: 72px; background: rgba(52,211,153,.12); filter: blur(28px);
}
body.app-dark .sidebar-foot,
body.app-dark .sidebar-user { padding: 10px 24px; gap: 10px; }
/* Size only. This block used to restore a white halo ring here, which put the glow back on the
   avatar the quieter treatment above removes - the second definition wins on source order. */
body.app-dark .sidebar-user-avatar { width: 32px; height: 32px; }

/* Read-only fields: clearly not editable, still legible. Muting the text alone made them look
   broken; the dashed edge says "fixed value" without dimming the content past reading. */
body.app-dark input:disabled,
body.app-dark input[readonly],
body.app-dark .form-input:disabled,
body.app-dark .form-input[readonly] {
  background: rgba(255,255,255,.02);
  border-style: dashed; border-color: rgba(255,255,255,.10);
  color: var(--ad-text-3); cursor: not-allowed; opacity: 1;
}

/* Account / Security panels: one padding value so the two columns read as a pair. */
body.app-dark .profile-grid > .section-card { padding: 22px; }
/* Save Changes / Update Password sit with their fields rather than drifting to the panel floor. */
body.app-dark #profile-form,
body.app-dark #password-form { margin-bottom: 0; }
body.app-dark #profile-form .btn-quiet,
body.app-dark #password-form .btn-quiet { margin-top: 4px; }

/* The profile hero keeps its gradient, but Upgrade/Extend access is a .btn-secondary now, so it
   needs to read against that gradient rather than the page. */
body.app-dark #access-plan-cta {
  background: rgba(255,255,255,.12); border-color: rgba(255,255,255,.22); color: #fff;
}
body.app-dark #access-plan-cta:hover { background: rgba(255,255,255,.20); }

/* ---- 30. Last of the de-inlined page blocks ----------------------------------------------------
   exams.html, admin.html and pricing.html carried the final page-level <style> tags. Moving them
   into styles.css fixed the cascade order, which is enough for pricing (section 18 already covers
   it). These two had light values with no dark counterpart yet. */

/* Catalog "refreshing" spinner: was #6b7280 text on a #cbd2dd ring with an indigo head. */
body.app-dark .catalog-refreshing { color: var(--ad-text-4); }
body.app-dark .catalog-refreshing .catalog-refreshing-dot {
  border-color: rgba(255,255,255,.14); border-top-color: var(--ad-cyan);
}

/* Admin: recover-payment form inside a modal. */
body.app-dark .recover-payment-form > label,
body.app-dark .recover-payment-help { color: var(--ad-text-3); }

/* ---- 31. Last light-surface leaks, found by scratchpad/lightscan.cjs ---------------------------
   Catalog pagination: `.pagination button` is background:#fff in the base sheet and had no dark
   counterpart, so the Previous/Next/page buttons rendered as white chips with black labels. */
body.app-dark .pagination button {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line);
  color: var(--ad-text-2); border-radius: 8px;
}
body.app-dark .pagination button:hover:not(:disabled) {
  background: rgba(255,255,255,.08); border-color: rgba(255,255,255,.16); color: #fff;
}
body.app-dark .pagination button:disabled { opacity: .35; }
body.app-dark .pagination button.page-active {
  background: rgba(34,211,238,.16); border-color: rgba(34,211,238,.45); color: #67e8f9;
}
body.app-dark .pagination .page-info { color: var(--ad-text-3); }
body.app-dark .pagination .page-ellipsis { color: var(--ad-text-4); }
body.app-dark .admin-card-pagination {
  border-top-color: var(--ad-line-soft);
  background: rgba(5, 8, 14, .18);
}

/* Profile: "Change Password" heading (was an inline colour:#333) and the security values. */
body.app-dark .pf-subheading { color: #fff; }
body.app-dark .security-value { color: var(--ad-text-2); }
body.app-dark .security-value-warn { color: #fcd34d; }

/* ---- 32. Readiness Snapshot --------------------------------------------------------------------
   Cyan/teal accents; red only where a score actually missed the target, so the card reads as
   information rather than as a verdict on every number in it. The gap headline stays in the sans
   face - it is a sentence, and mono on prose is what makes a panel read like a terminal log. Mono
   goes on the three values and the scale, which are genuinely numeric. */
body.app-dark .rsnap-gap { color: #fff; }
body.app-dark .rsnap-gap-clear { color: #6ee7b7; }
body.app-dark .rsnap-gap-empty { color: rgba(255,255,255,.82); }
body.app-dark .rsnap-read { color: var(--ad-text-3); }
body.app-dark .rsnap-track { background: rgba(255,255,255,.07); }
body.app-dark .rsnap-fill {
  background: linear-gradient(90deg, rgba(34,211,238,.55), var(--ad-cyan));
  box-shadow: 0 0 12px -2px rgba(34,211,238,.55);
}
body.app-dark .rsnap-fill.is-clear {
  background: linear-gradient(90deg, rgba(52,211,153,.55), var(--ad-emerald));
  box-shadow: 0 0 12px -2px rgba(52,211,153,.5);
}
body.app-dark .rsnap-mark-target { background: rgba(255,255,255,.75); }
body.app-dark .rsnap-mark-best { background: rgba(255,255,255,.32); }
body.app-dark .rsnap-scale {
  color: var(--ad-text-4);
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 9.5px; letter-spacing: .06em;
}
body.app-dark #rsnap-target-cap, body.app-dark .rsnap-cap { color: var(--ad-text-3); }
body.app-dark .rsnap-stat-val {
  color: #fff;
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 17px; letter-spacing: -.02em;
}
body.app-dark .rsnap-stat-val.trend-pass { color: #6ee7b7; }
body.app-dark .rsnap-stat-val.trend-fail { color: #fda4af; }
body.app-dark .rsnap-stat-val.rsnap-stat-target { color: var(--ad-cyan-soft); }
body.app-dark .rsnap-stat-lbl { color: var(--ad-text-4); }
body.app-dark .trend-note { color: var(--ad-text-4); }
body.app-dark .trend-cta { color: var(--ad-cyan-soft); }
body.app-dark .trend-cta:hover { color: #a5f3fc; text-decoration: none; }

/* ---- 30. Study Planner polish ------------------------------------------------------------------
   Review: "the structure is good, but it still feels assembled rather than finished" - 8/10.
   Two of the six items turned out to be selector-grouping mistakes in section 14 rather than
   matters of taste; those are called out inline. The rest is contrast, weight and spacing. */

/* 30a. Panel stacking. .sp-summary and .sp-calendar-layout are GRID WRAPPERS, not surfaces, but
   section 14 handed them the same glass + border + inset-black-ring as a real card. So the three
   metric cards sat on a slab, and the calendar sat in a panel inside .section-card inside the
   page - three overlays deep. That is the "too heavy / too dim" in review. The wrappers go back to
   being wrappers; only .sp-stat and .sp-insight-panel stay surfaces. */
body.app-dark .sp-summary,
body.app-dark .sp-calendar-layout {
  background: none; border: 0; box-shadow: none; border-radius: 0;
}

/* 30b. Secondary copy. Every quiet string on the page was on --ad-text-4 - 30% white, about 3.4:1
   on this canvas. Body-weight copy moves to 70% and captions to 55%. Only the uppercase 11px
   eyebrow stays below that, where quiet reads as intent rather than as a fault. */
body.app-dark .sp-section-subtitle,
body.app-dark .sp-stat-meta,
body.app-dark .sp-insight-note,
body.app-dark .sp-track-sub,
body.app-dark .sp-domain-top { color: rgba(255,255,255,.70); }
body.app-dark .sp-stat-label,
body.app-dark .sp-tile-lbl,
body.app-dark .sp-legend,
body.app-dark .sp-insight-label,
body.app-dark .sp-pill-sub,
body.app-dark .sp-val-muted,
body.app-dark .sp-cal-q { color: rgba(255,255,255,.55); }
body.app-dark .sp-insight-kicker { color: rgba(255,255,255,.42); }

/* 30c. Metric cards: equal height, one icon size, one accent language. acc-teal's top line was
   #07625b, a light-theme ink that is invisible on #070A12, and .sp-stat-icon.green had no dark
   override at all - it kept its #dcfce7 mint tile and read as a light patch beside two dark ones. */
body.app-dark .sp-stat { padding: 18px; border-radius: 14px; box-shadow: none; }
body.app-dark .sp-stat:hover {
  border-color: rgba(34,211,238,.22); box-shadow: 0 0 28px -14px rgba(34,211,238,.5);
}
body.app-dark .sp-stat::before { height: 2px; opacity: 1; }
body.app-dark .sp-stat.acc-amber::before { background: linear-gradient(90deg, rgba(251,191,36,.85), rgba(251,191,36,.10)); }
body.app-dark .sp-stat.acc-teal::before  { background: linear-gradient(90deg, rgba(45,212,191,.85), rgba(45,212,191,.10)); }
body.app-dark .sp-stat.acc-green::before { background: linear-gradient(90deg, rgba(52,211,153,.85), rgba(52,211,153,.10)); }
body.app-dark .sp-stat-head { min-height: 34px; margin-bottom: 14px; }
body.app-dark .sp-stat-icon { width: 34px; height: 34px; box-shadow: none; }
body.app-dark .sp-stat-icon.green { background: rgba(52,211,153,.16); color: #6ee7b7; }
body.app-dark .sp-stat-value { font-size: 25px; }
body.app-dark .sp-stat-value.sp-stat-title { font-size: 16px; color: #fff; }
body.app-dark .sp-stat-label { font-size: 11px; letter-spacing: .6px; margin-top: 6px; }

/* 30d. The "N questions left today" strip. .sp-track-body was grouped with .sp-tile in section 14,
   so the text wrapper in the middle of the strip drew its own bordered inset card - the "looks
   like another large input" in review. It is a text wrapper and nothing else. */
body.app-dark .sp-track-body {
  background: none; border: 0; border-radius: 0; padding: 0; color: var(--ad-text-2);
}
body.app-dark .sp-track-status {
  padding: 10px 14px; border-radius: 10px; gap: 12px; align-items: center;
  margin-bottom: 18px; box-shadow: none;
}
body.app-dark .sp-track-ico {
  width: 26px; height: 26px; border-radius: 8px;
  background: rgba(255,255,255,.07); box-shadow: none;
}
body.app-dark .sp-track-msg { font-size: 13px; font-weight: 700; }
body.app-dark .sp-track-sub { font-size: 11.5px; }
/* Practice now is a main study action, so it takes the cyan the rest of the app gives them. It was
   inheriting .sp-track-cta's light-theme `background: #fff` - a white button sitting on ink. */
body.app-dark .sp-track-cta {
  background: linear-gradient(90deg, var(--ad-cyan), #2dd4bf);
  border: 0; color: #052b33; font-weight: 700; border-radius: 999px;
  padding: 7px 16px; box-shadow: 0 0 20px -8px rgba(34,211,238,.55);
}
body.app-dark .sp-track-cta:hover {
  background: linear-gradient(90deg, var(--ad-cyan), #2dd4bf); filter: brightness(1.06);
}

/* 30e. Progress bars. .sp-mini-bar-top - the LABEL ROW - was grouped with the progress-FILL
   selectors in section 14, so it carried linear-gradient(cyan, violet) as its own background.
   That is the bar the labels looked like they were sitting on; it was never a spacing problem.
   .sp-mini-bar, the outer wrapper, was grouped with the TRACK selectors and drew a pill for the
   same reason. Label row above, track below, nothing painted behind the text. */
body.app-dark .sp-mini-bar { background: none; border-radius: 0; margin-bottom: 18px; }
body.app-dark .sp-mini-bar-top {
  background: none; border-radius: 0;
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; margin-bottom: 9px;
}
body.app-dark .sp-mini-bar-lbl { font-size: 12px; font-weight: 600; color: rgba(255,255,255,.70); }
body.app-dark .sp-mini-bar-val {
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12.5px; font-weight: 700; color: #fff; font-variant-numeric: tabular-nums;
}
body.app-dark .sp-progress { height: 6px; background: rgba(255,255,255,.09); }
body.app-dark .sp-mini-bar .sp-progress { height: 6px; margin-top: 0; }
body.app-dark .sp-progress-fill { background: linear-gradient(90deg, var(--ad-cyan), #2dd4bf); }

/* 30f. Calendar. Cells were 3-4% white sitting on a 4% panel, which is why a month of them read as
   one dark rectangle. Today gets a real ring rather than a slightly bluer cell, and the legend is
   pinned to the grid's own 340px so it sits under the calendar instead of drifting across the
   column. Cell hover no longer lifts - 35 cells that move is noise on a page being calmed down. */
body.app-dark .sp-cal-day {
  background: rgba(255,255,255,.055); color: rgba(255,255,255,.62); border: 1px solid transparent;
}
body.app-dark .sp-cal-day.none    { background: rgba(255,255,255,.04); color: rgba(255,255,255,.42); }
body.app-dark .sp-cal-day.met     { background: rgba(52,211,153,.20); color: #6ee7b7; }
body.app-dark .sp-cal-day.partial { background: rgba(251,191,36,.18); color: #fcd34d; }
body.app-dark .sp-cal-day.today {
  background: rgba(34,211,238,.16); color: #a5f3fc; border-color: rgba(34,211,238,.8);
  box-shadow: inset 0 0 0 1.5px rgba(34,211,238,.8), 0 0 18px -6px rgba(34,211,238,.55);
}
body.app-dark .sp-cal-day:hover {
  transform: none; box-shadow: inset 0 0 0 1px rgba(255,255,255,.14);
}
body.app-dark .sp-cal-day.today:hover {
  box-shadow: inset 0 0 0 1.5px rgba(34,211,238,.8), 0 0 18px -6px rgba(34,211,238,.55);
}
body.app-dark .sp-cal-dow { color: rgba(255,255,255,.45); font-size: 10px; letter-spacing: .5px; }
body.app-dark .sp-cal-today { color: inherit; opacity: .85; }
body.app-dark .sp-legend { max-width: 340px; font-size: 10.5px; gap: 12px; margin-top: 14px; }
body.app-dark .sp-legend span::before { width: 8px; height: 8px; border-radius: 2px; }

/* 30g. Today panel: a surface, not a surface plus a rule. It carried the full glass shadow - which
   includes an inset black ring - AND a left border, on top of the .section-card it sits inside. */
body.app-dark .sp-insight-panel {
  background: rgba(255,255,255,.035);
  border: 1px solid var(--ad-line);
  box-shadow: none; border-radius: 14px; padding: 20px 22px;
}
body.app-dark .sp-insight-title { font-size: 19px; }
body.app-dark .sp-insight-note { font-size: 12.5px; margin-bottom: 16px; }
body.app-dark .sp-tile {
  background: rgba(255,255,255,.045); border: 1px solid var(--ad-line); box-shadow: none;
}
body.app-dark .sp-insight-micro {
  background: rgba(255,255,255,.035); border: 1px solid var(--ad-line-soft);
  color: rgba(255,255,255,.62);
}

/* 30h. No violet on this page. The active exam pill and the active tab both used a cyan-to-violet
   wash. Violet is the account/admin accent (section 12), so on a study surface it reads as a
   second accent competing with the cyan actions rather than as a selection. */
body.app-dark .sp-pill.active {
  background: rgba(34,211,238,.13); border-color: rgba(34,211,238,.35);
  box-shadow: 0 0 22px -10px rgba(34,211,238,.5);
}
body.app-dark #sp-tabs .tab.active {
  background: rgba(34,211,238,.14); color: #fff;
  box-shadow: inset 0 0 0 1px rgba(34,211,238,.22);
}

/* ---- 31. Dashboard Tip card: fixed row rhythm, scroll the overflow ------------------------------
   The card is equal-height with Recent Attempts, so the row area has a height it did not choose.
   This used to be absorbed with justify-content:space-evenly, which made the gap between rows a
   function of how many domains an exam has - the rows landed somewhere different on every exam and
   lined up with nothing. Rows now sit at a FIXED rhythm and .dash-tip-rows scrolls when there are
   more than fit. Do not reintroduce space-evenly.
   min-height:0 is load-bearing twice over: a flex item's default min-height:auto refuses to shrink
   below its content, so without it the rows would push the card past Recent Attempts instead of
   scrolling. */
/* Natural height, NOT flex:1. The list no longer scrolls, so a flexible track would simply squash
   three fixed rows against the card floor and clip whichever came last. The CTA takes the slack
   through margin-top:auto instead. */
body.app-dark .dash-tip-list { display: flex; flex-direction: column; }
/* .dash-tip-count is gone from the markup: it stated a total across every domain while the rows
   showed a subset, so "7 domains below target" sat above four rows and read as a contradiction. */
/* Deliberately NOT scrollable, and no link out either. The card shows the weakest domains that fit
   its fixed height and stops there; Readiness by Domain is on the same screen for the full list, so
   a "+N more" hand-off was pointing at something already visible. */
/* The one list that does scroll gets a visible ring when focus lands in it - the See-all link
   moves focus here, so it has to be obvious where the arrow keys will act. */
body.app-dark #domain-bars:focus-visible,
body.app-dark #domain-bars:focus {
  outline: 2px solid var(--ad-cyan-soft); outline-offset: 2px; border-radius: 8px;
}
/* A domain at or above 70% appears here only as context, when fewer than five sit below it. It
   reads as fine rather than as another thing to go and fix. */
body.app-dark .dash-tip-row.is-ok .dash-tip-row-bar > span { background: rgba(52,211,153,.5); }
body.app-dark .dash-tip-row.is-ok .dash-tip-row-pct { color: rgba(110,231,183,.85); }

/* ---- 32. Study Planner, review round 2 ---------------------------------------------------------
   Calendar day numbers read dim. Note that every cell carries met / partial / none, so the base
   .sp-cal-day colour never actually applies - "inactive days" means .none specifically, and that
   was the 42% white doing it. Today already reads correctly and is left alone. */
body.app-dark .sp-cal-day.none { color: rgba(255,255,255,.68); background: rgba(255,255,255,.05); }
body.app-dark .sp-cal-day { color: rgba(255,255,255,.80); }
body.app-dark .sp-cal-num { font-weight: 600; }
body.app-dark .sp-cal-dow { color: rgba(255,255,255,.55); }
body.app-dark .sp-cal-q { color: rgba(255,255,255,.50); }

/* Tighter tail. The calendar column ends well above the Today panel beside it, so the card's full
   24px bottom padding sat under an already-short column and read as another empty band. */
body.app-dark #sp-tab-calendar .section-card { padding-bottom: 16px; }
body.app-dark .sp-legend { margin-top: 12px; }

/* ---- 33. Study Planner: Next 7 days ------------------------------------------------------------
   Read-only forward strip under the calendar. Every cell carries the SAME five rows - weekday,
   date, track, value, tag - whether or not the tag has text, so no cell is ever a different height
   from its neighbours and the row cannot reflow as data lands. The container holds its settled
   height from the start for the same reason. */
body.app-dark .sp-next7 {
  display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 8px;
  min-height: 104px;
}
body.app-dark .sp-next7-msg {
  grid-column: 1 / -1; align-self: center;
  color: rgba(255,255,255,.55); font-size: 12.5px;
}
body.app-dark .sp-n7-cell {
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  padding: 10px 6px 8px; border-radius: 10px;
  background: rgba(255,255,255,.04); border: 1px solid transparent;
}
body.app-dark .sp-n7-dow {
  font-size: 10px; font-weight: 700; letter-spacing: .5px; text-transform: uppercase;
  color: rgba(255,255,255,.50);
}
body.app-dark .sp-n7-date {
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 17px; font-weight: 700; line-height: 1; color: rgba(255,255,255,.85);
}
body.app-dark .sp-n7-bar {
  display: block; width: 100%; height: 4px; border-radius: 999px;
  background: rgba(255,255,255,.09); overflow: hidden;
}
body.app-dark .sp-n7-bar > span {
  display: block; height: 100%; border-radius: 999px;
  background: linear-gradient(90deg, var(--ad-cyan), #2dd4bf);
}
body.app-dark .sp-n7-val {
  font-size: 11.5px; font-weight: 600; color: rgba(255,255,255,.62);
  font-variant-numeric: tabular-nums;
}
/* Always present, usually empty - it is what keeps all seven cells the same height. */
body.app-dark .sp-n7-tag {
  min-height: 12px; font-size: 9px; font-weight: 800; letter-spacing: .6px;
  text-transform: uppercase; color: var(--ad-cyan-soft); line-height: 12px;
}
body.app-dark .sp-n7-cell.is-today {
  background: rgba(34,211,238,.10); border-color: rgba(34,211,238,.35);
}
body.app-dark .sp-n7-cell.is-today .sp-n7-date { color: #a5f3fc; }
body.app-dark .sp-n7-cell.is-today .sp-n7-val { color: #fff; }
body.app-dark .sp-n7-cell.is-met .sp-n7-bar > span { background: rgba(52,211,153,.75); }
body.app-dark .sp-n7-cell.is-exam {
  background: rgba(251,191,36,.10); border-color: rgba(251,191,36,.32);
}
body.app-dark .sp-n7-cell.is-exam .sp-n7-dow { color: #fcd34d; }
body.app-dark .sp-n7-cell.is-exam .sp-n7-date { color: #fcd34d; }
/* Past the target date there is nothing left to plan, so the cell reads as out of scope. */
body.app-dark .sp-n7-cell.is-after { background: rgba(255,255,255,.02); }
body.app-dark .sp-n7-cell.is-after .sp-n7-date,
body.app-dark .sp-n7-cell.is-after .sp-n7-val { color: rgba(255,255,255,.28); }
body.app-dark .sp-n7-cell.is-after .sp-n7-bar { background: rgba(255,255,255,.04); }
body.app-dark .sp-next7-foot {
  margin-top: 12px; font-size: 11.5px; color: rgba(255,255,255,.55);
}
body.app-dark .sp-next7-card { margin-top: 16px; }
/* Reflow, not squeeze: below the point where seven cells stop being legible they wrap, and on a
   phone they stack two-up rather than becoming seven slivers. */
@media (max-width: 900px) {
  body.app-dark .sp-next7 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (max-width: 520px) {
  body.app-dark .sp-next7 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ---- 34. Attempts page polish ------------------------------------------------------------------
   Body copy up a step, muted greys up 10-18%, a header band that is actually visible, and the
   readiness card's placeholder demoted out of the numeric type scale. */

/* Row text. 14.5px with a slightly taller row - at 14px on a wide screen the date and exam columns
   read light against the panel. */
body.app-dark #history-body table td { font-size: 14.5px; padding: 14px 16px; }
body.app-dark #history-body table td:first-child { color: rgba(255,255,255,.78); }

/* Muted text. --ad-text-4 is 30% white and --ad-text-3 is 45%; every one of these is a label meant
   to be read rather than sensed. The topbar subtitle was the dimmest thing on the page at 35%. */
body.app-dark .topbar-subtitle { color: rgba(255,255,255,.48); }
body.app-dark .page-header p { color: rgba(255,255,255,.58); }
body.app-dark .hist-score-max { color: rgba(255,255,255,.45); }
body.app-dark .stat-card .stat-label { color: rgba(255,255,255,.50); }
body.app-dark .stat-help { color: rgba(255,255,255,.45); }
body.app-dark .table-meta { color: rgba(255,255,255,.52); }

/* Header strip. It was 3% white sitting on a panel already around 4%, so the band was effectively
   invisible and the column labels floated over the rows. Lighter, with a rule above as well as
   below so it reads as a header rather than as a first row. */
body.app-dark #history-body table thead th {
  background: rgba(255,255,255,.055);
  color: rgba(255,255,255,.46);
  border-top: 1px solid var(--ad-line);
  border-bottom: 1px solid rgba(255,255,255,.12);
}

/* "Choose exam" is a prompt, not a measurement, but it rendered in the 32px numeric face:
   `body.app-dark .stat-card .stat-value` (0,3,1) outranks `.stat-card .stat-value.stat-empty`
   (0,3,0) in styles.css, so the 15px declared there never applied. It also inherited the violet
   background-clip from the accent rule, which is why a placeholder read as a gradient headline. */
body.app-dark .stat-card .stat-value.stat-empty {
  font-size: 17px; font-weight: 600; letter-spacing: 0;
  background: none; -webkit-background-clip: border-box; background-clip: border-box;
  color: rgba(255,255,255,.62) !important;
}

/* ---- 35. Saved Questions zero state ------------------------------------------------------------
   Layout lives in styles.css (.es-center) so it is not theme-specific. Only the copy weight is
   here: the body line and the bullet list were both reading too faint to be the explanation of
   what the page is for. */
body.app-dark .empty-state-card p { color: rgba(255,255,255,.68); }
body.app-dark .es-points li { color: rgba(255,255,255,.62); }

/* ---- 36. Plans page: make it a workspace page --------------------------------------------------
   pricing.html serves BOTH audiences from one file: a signed-in user keeps body.app-dark, and
   public-mode.js strips that class for a logged-out visitor and swaps in the public shell. So
   every rule here is gated on .app-dark and the public pricing page is untouched by all of it. */

/* 36a. The marketing footer. Product / Collections / Paths / Company inside the app shell reads as
   a landing page pasted into the workspace. Hidden for signed-in users; the compact line in the
   content rail carries the legal text that actually has to be there. */
body.app-dark .cg-ftr { display: none; }
.app-page-footnote { display: none; }
body.app-dark .app-page-footnote {
  display: block; margin: 40px 0 0; padding-top: 18px;
  border-top: 1px solid var(--ad-line-soft);
  color: rgba(255,255,255,.42); font-size: 12px; line-height: 1.6;
}
body.app-dark .app-page-footnote a { color: rgba(255,255,255,.58); text-decoration: none; }
body.app-dark .app-page-footnote a:hover { color: var(--ad-cyan-soft); }

/* 36b. Section rhythm. About / Compare / FAQ each carried their own margin (44px, 26px, 44px) and
   each centred itself at 820px inside a 1360px rail, so they read as three loose blocks floating
   away from the cards rather than as sections of one page. One 48px step between majors, and they
   sit on the same rail the cards do. */
body.app-dark .pricing-about,
body.app-dark .pricing-compare,
body.app-dark .pricing-faq { max-width: none; margin: 48px 0 0; }
body.app-dark .pricing-compare { margin-top: 28px; }

/* 36c. FAQ as a contained card. Two columns above 900px: at full rail width a single column gives
   140-character lines, and stacking six of them is what made the section feel longer than the
   pricing it supports. Secondary and compact, which is what it should be. */
body.app-dark .pricing-faq {
  background: var(--ad-glass), var(--ad-panel);
  border: 1px solid var(--ad-line);
  box-shadow: var(--ad-glass-shadow);
  border-radius: 16px;
  padding: 26px 28px 28px;
}
body.app-dark .pricing-faq h2 {
  text-align: left; font-size: 18px; margin: 0 0 20px;
}
body.app-dark .pricing-faq-list { display: grid; gap: 20px 36px; }
@media (min-width: 900px) {
  body.app-dark .pricing-faq-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
body.app-dark .pricing-faq-list h3 { font-size: 14px; margin: 0 0 5px; }
body.app-dark .pricing-faq-list p {
  font-size: 13px; line-height: 1.6; color: rgba(255,255,255,.62);
}

/* 36d. The About block is body copy, not a hero - left-aligned on the rail with a readable measure
   rather than centred text running the full width. */
body.app-dark .pricing-about { text-align: left; }
body.app-dark .pricing-about h2 { text-align: left; font-size: 18px; }
body.app-dark .pricing-about-lead,
body.app-dark .pricing-about-fine { max-width: 92ch; }
body.app-dark .pricing-compare { text-align: left; }
body.app-dark .pricing-compare-links { justify-content: flex-start; }

/* 36e. Reassurance pills. Kept - they are the last thing read before a payment - but demoted to a
   quiet single line so they stop competing with the plan cards directly under them. */
body.app-dark .pricing-assurance-row { gap: 16px; margin: 10px 0 18px; }
body.app-dark .pricing-assurance-row span {
  font-size: 11px; color: rgba(255,255,255,.45); font-weight: 500;
}

/* 36f. Voucher band lines up with the card grid rather than running wider than the thing it
   applies to. */
body.app-dark .pricing-offer-band { max-width: none; margin: 0 0 18px; }

/* 36g. Trust strip above the cards, not beside them. */
body.app-dark .pricing-proof-grid { margin: 0 0 20px; }

/* 36h. Card actions pin to the bottom edge. .pricing-card is already a column flex box with a
   472px floor, but .pricing-action carried no margin-top:auto, so on the shorter cards the button
   stopped wherever the bullet list ended and the four buttons sat at four different heights. */
body.app-dark .pricing-action { margin-top: auto; padding-top: 16px; }
body.app-dark .pricing-action .btn {
  min-height: 40px; display: inline-flex; align-items: center; justify-content: center;
}

/* ---- 37. Plans page, round 2 -------------------------------------------------------------------
   Still gated on .app-dark, so the public pricing page keeps its own presentation. Nothing here
   touches the shared rail - see the note at the end. */

/* 37a. Current-plan badge in the header row. It was position:absolute at top:-12px, straddling the
   card's top border, which is what made it read as pasted on. Now it is the header's right chip
   (pricing.js puts it in place of the "fit" chip on that card), so it costs no height either. */
body.app-dark .pricing-current-tag {
  position: static; transform: none; top: auto; left: auto;
  background: rgba(34,211,238,.16); color: var(--ad-cyan-soft);
  border: 1px solid rgba(34,211,238,.34);
  font-size: 10px; font-weight: 700; letter-spacing: .4px; text-transform: uppercase;
  padding: 3px 10px; border-radius: 999px; white-space: nowrap;
}

/* 37b. Shorter cards. A 472px floor plus 22px padding held every card well past its content, and
   the bullet lists are close enough in length that nothing needed the slack. */
body.app-dark .pricing-card { min-height: 0; padding: 18px 18px 16px; }
body.app-dark .pricing-card-top { margin-bottom: 10px; }
body.app-dark .pricing-card h3 { font-size: 17px; }
body.app-dark .pricing-desc { font-size: 12.5px; line-height: 1.5; }
/* Bullet sizing lives in 38a - it needs padding LONGHAND so the checkmark keeps its gutter. */
body.app-dark .pricing-card-note { font-size: 11.5px; }

/* 37c. Access summary sits at the headline's top line instead of stretching the whole head row,
   which is what made it read as a detached slab beside the title. */
body.app-dark .pricing-page-head { align-items: start; gap: 24px; }
body.app-dark .pricing-account-card { padding: 14px 16px; border-radius: 12px; }
body.app-dark .pricing-account-card p { font-size: 12px; line-height: 1.5; }

/* 37d. Stats strip: quieter, and on the SAME rail as the cards below it. It was max-width 980
   centred while the card grid runs the full rail, so its left edge sat inboard of the cards - part
   of why the top of the page looked indented relative to the pricing. */
body.app-dark .pricing-proof-grid { max-width: none; gap: 10px; margin: 0 0 18px; }
body.app-dark .pricing-proof-grid div { padding: 8px 12px; border-radius: 10px; }
body.app-dark .pricing-proof-grid strong { font-size: 15px; }
body.app-dark .pricing-proof-grid span { font-size: 10.5px; }

/* 37e. Voucher lines up with the grid rather than centring at 720px under a full-rail row. */
body.app-dark .voucher-redeem-section { max-width: none; margin: 20px 0 0; }
body.app-dark .pricing-offer-band { max-width: none; }

/* 37f. NOT changed on purpose: the page rail. .page-container is `max-width: var(--app-content-max);
   margin: 0 auto` inside .app-main, which is already offset by the sidebar - so the content IS
   centred in the app content area, not in the browser window. The side margins come from
   --app-content-max being 1360px under .app-dark on a much wider monitor. That token is shared by
   all eight signed-in pages, and overriding it here would make the title and left edge jump
   between Dashboard and Plans - the exact thing the shared rail exists to prevent. */

/* ---- 38. Plans page, round 3 -------------------------------------------------------------------
   Still .app-dark only. */

/* 38a. REGRESSION FIX. Section 37b set `padding: 4px 0` on the feature bullets, and the shorthand
   zeroed the 24px left padding that .pricing-features li::before (position:absolute; left:0) sits
   in - so every checkmark landed on top of the first letter. Longhand only from here. */
body.app-dark .pricing-features li {
  font-size: 12.5px; line-height: 1.45;
  padding-top: 4px; padding-bottom: 4px; padding-left: 22px; padding-right: 0;
}
body.app-dark .pricing-features li::before { color: var(--ad-cyan); font-weight: 700; }

/* 38b. Disabled / current-state buttons. These were .btn-secondary plus an inline
   style="cursor:default;opacity:.7" - an inline style outranks every stylesheet, so the theme
   could never touch them and .7 opacity on an already-muted button was close to unreadable.
   Muted dark, full-strength text, unmistakably not a call to action. */
body.app-dark .btn-state {
  background: rgba(255,255,255,.045);
  border: 1px solid var(--ad-line);
  color: rgba(255,255,255,.58);
  cursor: default; box-shadow: none; font-weight: 600; border-radius: 999px;
}
body.app-dark .btn-state:hover {
  background: rgba(255,255,255,.045); color: rgba(255,255,255,.58);
  filter: none; transform: none; border-color: var(--ad-line);
}

/* 38c. Card fine print - the terms that used to be bullets. Quiet, directly above the action. */
body.app-dark .pricing-card-fine {
  margin-top: 12px; padding-top: 10px;
  border-top: 1px solid var(--ad-line-soft);
  font-size: 10.5px; line-height: 1.45; color: rgba(255,255,255,.40);
}

/* 38d. "What you are paying for" as a two-column trust section. The copy alone left the right half
   of a full-rail row empty, which read as missing content rather than as a decision. */
body.app-dark .pricing-about {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(260px, 340px);
  gap: 32px; align-items: start;
}
body.app-dark .pricing-about-main { min-width: 0; }
/* Hidden by default so the public pricing page renders exactly as before; .app-dark turns it on. */
.pricing-included { display: none; }
body.app-dark .pricing-included {
  display: block;
  background: var(--ad-glass), var(--ad-panel);
  border: 1px solid var(--ad-line);
  border-radius: 14px; padding: 18px 20px;
}
body.app-dark .pricing-included h3 {
  margin: 0 0 12px; font-size: 12px; font-weight: 700;
  letter-spacing: .5px; text-transform: uppercase; color: rgba(255,255,255,.55);
}
body.app-dark .pricing-included ul { list-style: none; margin: 0; padding: 0; }
body.app-dark .pricing-included li {
  position: relative; padding: 7px 0 7px 22px;
  font-size: 13px; line-height: 1.4; color: rgba(255,255,255,.78);
  border-top: 1px solid var(--ad-line-soft);
}
body.app-dark .pricing-included li:first-child { border-top: 0; padding-top: 0; }
body.app-dark .pricing-included li::before {
  content: '\2713'; position: absolute; left: 0; top: 7px;
  color: var(--ad-cyan); font-weight: 700;
}
body.app-dark .pricing-included li:first-child::before { top: 0; }
@media (max-width: 900px) {
  body.app-dark .pricing-about { grid-template-columns: minmax(0, 1fr); gap: 24px; }
}

/* ---- 39. Plans page, micro-polish --------------------------------------------------------------
   Cards a step shorter again, and the two blocks that were reading as detached brought onto the
   pricing block's own surface language. */
body.app-dark .pricing-card { padding: 16px 16px 14px; }
body.app-dark .pricing-card h3 { margin-bottom: 4px; }
body.app-dark .pricing-desc { margin-bottom: 10px; }
body.app-dark .pricing-card-note { padding: 7px 10px; font-size: 11px; margin-bottom: 10px; }
body.app-dark .pricing-features { margin: 0; }
body.app-dark .pricing-action { padding-top: 12px; }

/* Voucher: same surface as a pricing card, so it reads as the last row of the pricing block
   rather than a slab that happens to sit under it. */
body.app-dark .voucher-redeem-section {
  background: var(--ad-glass), var(--ad-panel);
  border: 1px solid var(--ad-line);
  border-radius: 14px; padding: 16px 18px; margin-top: 16px;
}

/* Included-with-paid-access card: heading on the same 11px uppercase step as every other section
   label on the page, and rows tightened to match the card bullets they summarise. */
body.app-dark .pricing-included { padding: 16px 18px; }
body.app-dark .pricing-included h3 {
  font-size: 11px; letter-spacing: .6px; margin-bottom: 10px; color: rgba(255,255,255,.50);
}
body.app-dark .pricing-included li { padding: 6px 0 6px 22px; font-size: 12.5px; }
body.app-dark .pricing-included li::before { top: 6px; }
body.app-dark .pricing-included li:first-child::before { top: 0; }

/* The page-level context line stays for free and single-exam users, where it orients rather than
   repeats - so it needs to be readable. (It is removed entirely for Pro; see pricing.js.) */
body.app-dark .pricing-current-context { color: rgba(255,255,255,.58); }

/* ---- 40. Profile page balance ------------------------------------------------------------------
   Security was one card running ~280px taller than Account, so the left column ended in a hole.
   It is now two cards (profile.html): the password form takes column two, the status rows take
   third place in the grid and therefore sit under Account, filling that column. .profile-grid is
   already `1fr 1fr` with align-items:start and collapses to one column on mobile, so the split
   needs no layout rules of its own - only the trim below. */
body.app-dark .pf-card-signin .section-body { padding-top: 4px; }
body.app-dark .pf-card-signin .security-row { padding: 10px 0; }
body.app-dark .pf-card-signin .security-row:last-child { padding-bottom: 2px; }

/* Extend access is a status affordance for someone who already has access, not a journey action -
   so it steps back to an outline. Filled treatments stay for Start Practice / Upgrade / Practice
   Now. It sits on the hero gradient, hence the white-alpha border rather than a theme token. */
body.app-dark #access-plan-cta {
  background: transparent;
  border: 1px solid rgba(255,255,255,.32);
  color: rgba(255,255,255,.88);
  font-weight: 600; box-shadow: none;
}
body.app-dark #access-plan-cta:hover {
  background: rgba(255,255,255,.10); border-color: rgba(255,255,255,.48); color: #fff;
}

/* Hero: a little more air between the plan panel and the dates beside it, and inside the meta
   tiles, which were the densest part of the page. */
body.app-dark .profile-access-meta-item { padding: 10px 12px; }
body.app-dark .profile-access-desc { margin-bottom: 14px; }

/* ---- 41. Searchable exam combobox --------------------------------------------------------------
   Replaces the native <select> on Attempts. At 150+ options the OS-drawn list ignored the theme
   entirely and had no search - see public/js/exam-combobox.js for why the select is kept. */
body.app-dark .cbx-field {
  background: var(--ad-glass-soft); border: 1px solid rgba(255,255,255,.07);
  color: var(--ad-text-2);
}
body.app-dark .cbx-field:hover { border-color: rgba(255,255,255,.14); color: #fff; }
body.app-dark .cbx-field.is-set { color: #fff; }
body.app-dark .cbx-field[aria-expanded="true"] {
  border-color: rgba(34,211,238,.45); color: #fff;
}
body.app-dark .cbx-field:focus-visible { outline: 2px solid rgba(34,211,238,.55); outline-offset: 1px; }
body.app-dark .cbx-pop {
  background: var(--ad-panel); border: 1px solid rgba(255,255,255,.10);
  box-shadow: 0 24px 60px -18px rgba(0,0,0,.85);
}
body.app-dark .cbx-search-wrap { border-bottom: 1px solid var(--ad-line-soft); }
body.app-dark .cbx-search-ico { color: rgba(255,255,255,.55); }
body.app-dark .cbx-search {
  background: rgba(255,255,255,.05); border: 1px solid rgba(255,255,255,.09); color: #fff;
}
body.app-dark .cbx-search::placeholder { color: rgba(255,255,255,.38); }
body.app-dark .cbx-search:focus { outline: none; border-color: rgba(34,211,238,.45); }
body.app-dark .cbx-list { padding-top: 6px; }
body.app-dark .cbx-row-name { color: rgba(255,255,255,.86); }
body.app-dark .cbx-row-meta { color: rgba(255,255,255,.42); }
body.app-dark .cbx-row.is-active { background: rgba(255,255,255,.07); }
body.app-dark .cbx-row.is-active .cbx-row-name { color: #fff; }
body.app-dark .cbx-row.is-selected .cbx-row-name { color: var(--ad-cyan-soft); font-weight: 600; }
body.app-dark .cbx-row.is-selected.is-active { background: rgba(34,211,238,.12); }
body.app-dark .cbx-group { color: rgba(255,255,255,.38); }
body.app-dark .cbx-empty { color: rgba(255,255,255,.45); }
body.app-dark .cbx-list::-webkit-scrollbar { width: 10px; }
body.app-dark .cbx-list::-webkit-scrollbar-thumb {
  background: rgba(255,255,255,.12); border-radius: 999px; border: 3px solid transparent;
  background-clip: content-box;
}

/* ---- 42. Profile columns are independent, not grid rows ----------------------------------------
   Three cards in a two-column grid puts the third on row 2, and row 2 cannot begin until the
   TALLEST row-1 card ends. So the column whose first card was shorter got a dead band before its
   second card, and reordering only chose which column suffered - Account-short for one account
   type, Security-short for another. Two flex columns share no rows at all: each side stacks tight
   to its own content, whatever the account state makes each card. */
body.app-dark .profile-grid { align-items: start; }
body.app-dark .profile-col {
  display: flex; flex-direction: column; gap: 20px; min-width: 0;
}
body.app-dark .profile-col > .section-card { margin: 0; }
/* Advanced account actions now sits inside column two. Its light-theme margin-top would stack on
   top of the column's 20px gap. */
.profile-page .acct-advanced { margin-top: 14px; max-width: 1080px; margin-left: auto; margin-right: auto; }

/* Per-exam access rows. The light theme is a white card on white; here they are glass rows inside
   an already-glass panel, so they sit quieter than the profile header above them. */
body.app-dark .pf-access-label { color: var(--ad-text-4); }
body.app-dark .pf-access-row {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); border-radius: 10px;
}
body.app-dark .pf-access-name { color: rgba(255,255,255,.88); font-weight: 600; }
body.app-dark .pf-access-meta { color: var(--ad-text-3); }
body.app-dark .pf-access-name.pf-access-none { color: var(--ad-text-3); font-weight: 600; }
body.app-dark .pf-access-meta a { color: var(--ad-cyan-soft); }

/* ---- 43. Saved Questions zero state ------------------------------------------------------------
   Structure is in styles.css; this is the palette. Anchored under the page header rather than
   floating mid-canvas, main card beside a short how-to panel, and a STATIC list placeholder - no
   shimmer, because nothing is loading and a loading animation would say otherwise. */
body.app-dark .saved-empty-kicker { color: var(--ad-cyan-soft); }
body.app-dark .saved-empty-main h3 { color: #fff; font-size: 19px; }
body.app-dark .saved-empty-main > p { color: rgba(255,255,255,.68); max-width: 56ch; }
body.app-dark .saved-preview-row {
  background: rgba(255,255,255,.028); border: 1px solid var(--ad-line-soft);
}
body.app-dark .saved-preview-row > span { background: rgba(255,255,255,.07); }
body.app-dark .saved-preview-row > i { background: rgba(255,255,255,.05); }
body.app-dark .saved-preview-row:first-child > span { background: rgba(255,255,255,.10); }
body.app-dark .saved-side-card {
  background: rgba(255,255,255,.035); border: 1px solid var(--ad-line);
}
body.app-dark .saved-side-label { color: rgba(255,255,255,.42); }
body.app-dark .saved-side-card h4 { color: rgba(255,255,255,.90); }
body.app-dark .saved-side-card .es-points li { color: rgba(255,255,255,.62); font-size: 12.5px; }
body.app-dark .bookmarks-status-chip {
  background: rgba(255,255,255,.05); border: 1px solid var(--ad-line);
  color: rgba(255,255,255,.62);
}

/* ---- 45. Single Exam Pass picker (pricing) ---------------------------------------------------
   This is a checkout step, so it should read as part of the app: a glass panel with real rows, not
   a white sheet holding a native <select size="8">. There is no light-theme counterpart - the
   picker only ever opens on pricing.html, which is always .app-dark - so structure lives here
   with the palette instead of being split across styles.css. */
body.app-dark .se-overlay {
  position: fixed; inset: 0; z-index: 1000;
  display: flex; align-items: center; justify-content: center; padding: 20px;
  /* The pricing cards behind were competing with the modal; dim harder and blur them out. */
  background: rgba(4, 6, 12, .78);
  -webkit-backdrop-filter: blur(7px); backdrop-filter: blur(7px);
}
body.app-dark .se-panel {
  width: 100%; max-width: 560px; max-height: calc(100vh - 40px);
  display: flex; flex-direction: column;
  padding: 24px 24px 20px; border-radius: 18px;
  background: var(--ad-glass), var(--ad-panel);
  border: 1px solid rgba(255,255,255,.10);
  box-shadow: 0 48px 96px -32px rgba(0,0,0,.92), inset 0 1px 0 rgba(255,255,255,.06);
}
body.app-dark .se-head h3 { margin: 0 0 5px; color: #fff; font-size: 19px; font-weight: 600; letter-spacing: -.015em; }
body.app-dark .se-head p { margin: 0 0 16px; color: var(--ad-text-3); font-size: 13px; line-height: 1.5; }

body.app-dark .se-search { position: relative; display: flex; align-items: center; }
body.app-dark .se-search-ico {
  position: absolute; left: 13px; width: 16px; height: 16px;
  color: var(--ad-text-4); pointer-events: none;
}
body.app-dark .se-search input {
  width: 100%; height: 42px; padding: 0 14px 0 38px; box-sizing: border-box;
  border-radius: 11px; border: 1px solid var(--ad-line);
  background: rgba(255,255,255,.04); color: #fff;
  font: inherit; font-size: 14px; outline: none;
  transition: border-color .15s, background .15s, box-shadow .15s;
}
body.app-dark .se-search input::placeholder { color: var(--ad-text-4); }
body.app-dark .se-search input:focus {
  border-color: rgba(34,211,238,.55); background: rgba(255,255,255,.06);
  box-shadow: 0 0 0 3px rgba(34,211,238,.14);
}

body.app-dark .se-active {
  margin: 12px 0 0; font-size: 12px; color: var(--ad-text-4); line-height: 1.5;
}

body.app-dark .se-list {
  margin: 14px 0 0; display: flex; flex-direction: column; gap: 6px;
  max-height: 44vh; overflow-y: auto; overscroll-behavior: contain;
  /* Keep the scrollbar visible but not a 6px hairline. */
  scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.18) transparent;
  padding-right: 4px; margin-right: -4px;
}
body.app-dark .se-list::-webkit-scrollbar { width: 10px; }
body.app-dark .se-list::-webkit-scrollbar-track { background: transparent; }
body.app-dark .se-list::-webkit-scrollbar-thumb {
  background: rgba(255,255,255,.16); border-radius: 999px;
  border: 3px solid transparent; background-clip: content-box;
}
body.app-dark .se-list::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,.28); background-clip: content-box; }

body.app-dark .se-row {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 11px 14px; border-radius: 12px; cursor: pointer; text-align: left;
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); color: inherit;
  font: inherit; transition: background .15s, border-color .15s;
}
body.app-dark .se-row:hover { background: rgba(255,255,255,.06); border-color: rgba(255,255,255,.14); }
body.app-dark .se-row.selected {
  background: rgba(34,211,238,.09); border-color: rgba(34,211,238,.55);
  box-shadow: 0 0 0 1px rgba(34,211,238,.35);
}
body.app-dark .se-row-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
body.app-dark .se-row-code {
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 10px; font-weight: 500; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ad-cyan-soft);
}
body.app-dark .se-row-name { color: rgba(255,255,255,.92); font-size: 14px; font-weight: 600; }
body.app-dark .se-row-meta { color: var(--ad-text-4); font-size: 12px; }
body.app-dark .se-row-check { width: 18px; height: 18px; flex-shrink: 0; color: var(--ad-cyan); opacity: 0; transition: opacity .15s; }
body.app-dark .se-row.selected .se-row-check { opacity: 1; }

body.app-dark .se-empty { padding: 18px 2px 4px; color: var(--ad-text-3); font-size: 13px; }

body.app-dark .se-foot {
  display: flex; justify-content: flex-end; gap: 10px;
  margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--ad-line-soft);
}
body.app-dark .se-btn {
  height: 40px; padding: 0 18px; border-radius: 10px; cursor: pointer;
  font: inherit; font-size: 14px; font-weight: 600;
  transition: background .15s, border-color .15s, color .15s, opacity .15s;
}
body.app-dark .se-btn-quiet {
  background: rgba(255,255,255,.05); border: 1px solid var(--ad-line); color: var(--ad-text-2);
}
body.app-dark .se-btn-quiet:hover { background: rgba(255,255,255,.10); color: #fff; }
body.app-dark .se-btn-go {
  border: 0; color: #04121a;
  background: linear-gradient(135deg, var(--ad-cyan), #0ea5e9);
  box-shadow: 0 10px 24px -12px rgba(34,211,238,.7);
}
body.app-dark .se-btn-go:hover:not(:disabled) { filter: brightness(1.06); }
body.app-dark .se-btn-go:disabled {
  background: rgba(255,255,255,.06); color: var(--ad-text-4);
  box-shadow: none; cursor: not-allowed;
}

/* Phone: near full width, and rows get real touch height. */
@media (max-width: 560px) {
  body.app-dark .se-overlay { padding: 12px; align-items: flex-end; }
  body.app-dark .se-panel { max-width: none; padding: 20px 18px 16px; border-radius: 16px; max-height: calc(100vh - 24px); }
  body.app-dark .se-list { max-height: 50vh; }
  body.app-dark .se-row { padding: 13px 14px; }
  body.app-dark .se-foot { padding-top: 14px; }
  body.app-dark .se-btn { flex: 1; height: 44px; }
}

/* ---- 46. Admin user drawer (.udr-*) -------------------------------------------------------------
   The drawer's own CSS is injected by ensureUserDrawerStyles() in admin.js and is all light values,
   so on the dark admin page it read as a white browser-default sheet pasted over the app. That
   injected <style> lands in <head> AFTER this file, but every rule here carries `body.app-dark`, so
   it outranks the single-class originals regardless of order - no !important needed.
   Nothing outside .udr-* / .acd-* is touched, and the .badge pills inside the header already pick up
   the shared dark badge treatment from section 11. */
body.app-dark .udr-backdrop {
  background: rgba(4,6,12,.72);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
}
/* Inset panel rather than a full-height slab welded to the viewport edge. The closed transform has
   to clear the 16px offset as well as the panel width, or a sliver stays on screen. */
body.app-dark .udr-drawer {
  top: 16px; right: 16px; bottom: 16px; height: auto;
  border-radius: 16px; overflow: hidden;
  background: linear-gradient(180deg, rgba(255,255,255,.045), rgba(255,255,255,.02)), var(--ad-panel);
  border: 1px solid rgba(255,255,255,.09);
  box-shadow: 0 40px 90px -40px rgba(0,0,0,.92), inset 0 1px 0 rgba(255,255,255,.05);
  transform: translateX(calc(100% + 32px));
}
body.app-dark .udr-drawer.open { transform: translateX(0); }
@media (max-width: 900px) {
  body.app-dark .udr-drawer {
    top: 0; right: 0; bottom: 0; height: 100vh; border-radius: 0; border: 0;
    transform: translateX(100%);
  }
  body.app-dark .udr-drawer.open { transform: translateX(0); }
}

body.app-dark .udr-header {
  background: linear-gradient(180deg, rgba(255,255,255,.04), transparent);
  border-bottom: 1px solid var(--ad-line);
  padding: 20px 24px;
}
body.app-dark .udr-name { color: #fff; font-size: 19px; font-weight: 700; letter-spacing: -.015em; }
body.app-dark .udr-email { color: var(--ad-text-3); }
body.app-dark .udr-avatar {
  width: 46px; height: 46px; border-radius: 12px; font-size: 15px; color: #fff;
  background: linear-gradient(135deg, rgba(34,211,238,.22), rgba(168,85,247,.22));
  border: 1px solid rgba(255,255,255,.10);
}
body.app-dark .udr-close {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line);
  color: var(--ad-text-3); border-radius: 9px;
}
body.app-dark .udr-close:hover { background: rgba(255,255,255,.09); color: #fff; }

/* The action row was #fafbfc. The Actions button is already a dark glass pill from section 6, so on
   a near-white band it read as invisible - the row was the problem, not the button. */
body.app-dark .udr-actions {
  background: rgba(255,255,255,.02); border-bottom: 1px solid var(--ad-line);
}
body.app-dark .udr-actions .btn-secondary {
  background: rgba(255,255,255,.07); border-color: rgba(255,255,255,.14); color: #fff;
}
body.app-dark .udr-actions .btn-secondary:hover { background: rgba(255,255,255,.12); }
body.app-dark .udr-menu {
  background: var(--ad-panel); border: 1px solid rgba(255,255,255,.10);
  border-radius: 12px; box-shadow: 0 24px 48px -24px rgba(0,0,0,.9); padding: 6px;
}
body.app-dark .udr-menu-item { color: var(--ad-text-2); border-radius: 8px; }
body.app-dark .udr-menu-item:hover { background: rgba(255,255,255,.07); color: #fff; }
body.app-dark .udr-menu-sep { background: var(--ad-line); }
body.app-dark .udr-menu-danger { color: #fda4af; }
body.app-dark .udr-menu-danger:hover { background: rgba(244,63,94,.14); color: #fecdd3; }
body.app-dark .udr-remove-exam {
  background: rgba(244,63,94,.12); border: 1px solid rgba(244,63,94,.28); color: #fda4af;
}
body.app-dark .udr-remove-exam:hover { background: rgba(244,63,94,.20); color: #fff; }

body.app-dark .udr-tabs { background: transparent; border-bottom: 1px solid var(--ad-line); }
body.app-dark .udr-tab { color: var(--ad-text-3); border-bottom: 2px solid transparent; }
body.app-dark .udr-tab:hover { color: #fff; background: rgba(255,255,255,.04); }
body.app-dark .udr-tab.active { color: var(--ad-cyan-soft); border-bottom-color: var(--ad-cyan); }

body.app-dark .udr-body { background: transparent; }
body.app-dark .udr-card {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line);
  border-radius: 12px; padding: 14px;
}
/* A long plan label or exam name used to wrap and push its tile taller than the rest of the row. */
body.app-dark .udr-card-val {
  color: #fff; font-size: 17px; letter-spacing: -.015em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
body.app-dark .udr-card-lbl { color: var(--ad-text-4); font-size: 10px; letter-spacing: .1em; }
body.app-dark .udr-dl dt { color: var(--ad-text-3); }
body.app-dark .udr-dl dd { color: #fff; }
body.app-dark .udr-title { color: var(--ad-text-4); letter-spacing: .1em; }

body.app-dark .udr-scroll {
  border: 1px solid var(--ad-line); border-radius: 12px; background: rgba(255,255,255,.02);
}
/* Opaque, like the main admin sticky headers: a translucent wash lets rows read through the column
   labels as they scroll under. This one sticks to .udr-scroll, which is the correct scrollport. */
body.app-dark .udr-tbl th {
  background: linear-gradient(rgba(255,255,255,.075), rgba(255,255,255,.075)), var(--ad-panel);
  color: var(--ad-text-4); border-bottom: 1px solid var(--ad-line);
  letter-spacing: .1em; padding: 10px 12px;
}
body.app-dark .udr-tbl td {
  color: var(--ad-text-2); border-bottom: 1px solid rgba(255,255,255,.045); padding: 10px 12px;
}
body.app-dark .udr-tbl tbody tr:last-child td { border-bottom: 0; }
body.app-dark .udr-tbl tbody tr:hover td { background: rgba(255,255,255,.03); }
/* An empty tab was a bare grey sentence floating in the panel. */
body.app-dark .udr-empty {
  color: var(--ad-text-4); text-align: center; padding: 22px 16px;
  border: 1px dashed rgba(255,255,255,.10); border-radius: 12px;
  background: rgba(255,255,255,.015);
}

/* The confirm dialog opens FROM the drawer and layers above it (z 1100). Left light it would be a
   white box on top of a dark panel, so it gets the same treatment - beyond the letter of the .udr-*
   scope, but it is the same surface. */
body.app-dark .acd-backdrop {
  background: rgba(4,6,12,.72);
  -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
}
body.app-dark .acd-dialog {
  background: linear-gradient(180deg, rgba(255,255,255,.045), rgba(255,255,255,.02)), var(--ad-panel);
  border: 1px solid rgba(255,255,255,.10); border-radius: 16px;
  box-shadow: 0 40px 80px -32px rgba(0,0,0,.9);
}
body.app-dark .acd-title { color: #fff; }
body.app-dark .acd-body { color: var(--ad-text-2); }
body.app-dark .acd-match-label { color: var(--ad-text-3); }
body.app-dark .acd-match {
  background: rgba(255,255,255,.04); border: 1px solid var(--ad-line);
  color: #fff; border-radius: 10px;
}
body.app-dark .acd-match:focus { border-color: rgba(34,211,238,.55); outline: none; }
body.app-dark .acd-error {
  background: rgba(244,63,94,.12); border: 1px solid rgba(244,63,94,.28); color: #fda4af;
}
body.app-dark .acd-danger {
  background: linear-gradient(135deg, #f43f5e, #e11d48); border: 0; color: #fff;
}
body.app-dark .acd-danger:hover { filter: brightness(1.06); }

/* ---- 47. Admin > Exams row actions --------------------------------------------------------
   Every row carried a small "More" text button, which read as a repeated form control rather
   than a table affordance. It is now a 34x34 icon trigger with a compact popover.

   Scoped to `.action-menu-icon` so the shared `.action-menu` machinery in styles.css stays
   usable for any future text-label menu. The panel is `position: fixed` with coordinates set in
   admin.js, NOT absolute: `.table-scroll-wrap` sets overflow-x:auto, which makes the wrapper a
   scroll container on both axes and clips an absolutely-positioned panel - worst on the last
   rows, exactly where a row menu needs to open. */
body.app-dark .admin-container .admin-actions-cell {
  width: 1%; white-space: nowrap; text-align: right;
}
body.app-dark .admin-container .action-menu-icon > .action-menu-btn {
  width: 34px; height: 34px; padding: 0; gap: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 8px;
  background: rgba(255, 255, 255, .04);
  border: 1px solid var(--ad-line);
  box-shadow: 0 1px 2px rgba(0, 0, 0, .28);
  color: var(--ad-text-3);
  transition: color .15s, border-color .15s, background .15s, box-shadow .15s;
}
/* the shared rule appends a caret via ::after; an icon button must not carry one */
body.app-dark .admin-container .action-menu-icon > .action-menu-btn::after { content: none; }
body.app-dark .admin-container .action-menu-icon > .action-menu-btn svg {
  width: 16px; height: 16px; display: block;
}
body.app-dark .admin-container .action-menu-icon > .action-menu-btn:hover,
body.app-dark .admin-container .action-menu-icon.open > .action-menu-btn {
  color: var(--ad-cyan-soft);
  border-color: rgba(103, 232, 249, .45);
  background: rgba(103, 232, 249, .08);
}
/* focus-visible only, so a mouse click does not leave a ring behind */
body.app-dark .admin-container .action-menu-icon > .action-menu-btn:focus-visible {
  outline: none;
  color: var(--ad-cyan-soft);
  border-color: rgba(103, 232, 249, .55);
  box-shadow: 0 0 0 3px rgba(103, 232, 249, .22);
}

body.app-dark .admin-container .action-menu-icon .action-menu-items {
  position: fixed; right: auto; top: 0; left: 0;    /* coordinates come from positionExamMenu() */
  min-width: 190px; padding: 6px;
  border-radius: 8px;
  background: #121a2b;
  border: 1px solid rgba(103, 232, 249, .22);
  box-shadow: 0 14px 34px rgba(0, 0, 0, .5), 0 2px 8px rgba(0, 0, 0, .35);
  overflow: visible;                               /* so the items' rounded corners show */
  z-index: 400;
}
body.app-dark .admin-container .action-menu-icon .action-menu-item {
  display: flex; align-items: center; gap: 9px;
  width: 100%; padding: 8px 10px;
  border-radius: 6px;
  font-size: 13px; font-weight: 500; line-height: 1.3;
  white-space: nowrap;
}
body.app-dark .admin-container .action-menu-icon .action-menu-item svg {
  width: 15px; height: 15px; flex: 0 0 auto;
}
body.app-dark .admin-container .action-menu-icon .action-menu-item.action-danger { color: #fda4af; }
body.app-dark .admin-container .action-menu-icon .action-menu-item.action-danger:hover {
  background: rgba(244, 63, 94, .14); color: #fecdd3;
}
body.app-dark .admin-container .action-menu-icon .action-menu-item.action-affirm { color: var(--ad-emerald); }
body.app-dark .admin-container .action-menu-icon .action-menu-item.action-affirm:hover {
  background: rgba(52, 211, 153, .14); color: #6ee7b7;
}
body.app-dark .admin-container .action-menu-icon .action-menu-item:focus-visible {
  outline: none; box-shadow: inset 0 0 0 2px rgba(103, 232, 249, .35);
}

/* Single column: the card no longer sits beside anything, so a fixed height would only crop it.
   Natural height, with a ceiling on the list so it cannot run the length of the page.
   MUST stay after the fixed-height rule above - same specificity, so source order is what decides. */
@media (max-width: 960px) {
  /* Single column: nothing sits beside these, so a fixed height would only crop them. The domain
     list keeps a ceiling so it cannot run the length of the page; the tip has three rows and needs
     none. */
  body.app-dark .dash-tip { height: auto; }
  body.app-dark #domain-bars { max-height: 300px; }
}

/* ---- 26. Catalog card simplification -------------------------------------------------------
   The catalog was running six accent colours at once - cyan, amber, emerald, rose, violet and the
   per-vendor blue - across four bordered pills and three bordered metric boxes, on every one of
   twelve cards. Nothing was emphasised because everything was.

   The system this settles on:
     cyan     one job per card - the action, on hover and focus. Plus the selected filter chip.
     neutral  level, vendor, exam code, domains, metadata. Everything that is a label.
     semantic amber/emerald/rose are reserved for real states, so they are not spent on a tier name.

   Scoped to .exam-grid so the dashboard's own .exam-card.dash-exam-card keeps its layout; the
   badge and code rules below are deliberately NOT scoped, because the colour system is meant to
   hold everywhere those badges appear. */

/* 1 + 2. Level and exam code become neutral labels. The tier is information, not a warning: an
   emerald "Foundational" beside a rose "Professional" implied a good/bad axis that does not
   exist - they are just different exams. These override the traffic-light rules in section 17
   and the violet .diff-advanced in 22a, so they carry matching specificity plus a later position. */
body.app-dark .exam-diff-badge,
body.app-dark .exam-card .diff-foundation,
body.app-dark .exam-card .diff-intermediate,
body.app-dark .exam-card .diff-advanced,
body.app-dark .diff-foundation,
body.app-dark .diff-intermediate,
body.app-dark .diff-advanced {
  background: none;
  border: 1px solid rgba(255,255,255,.14);
  color: var(--ad-text-2);
  font-weight: 500;
}
body.app-dark .exam-code-chip {
  background: none; border: none; padding: 0; border-radius: 0;
  color: var(--ad-text-3); font-weight: 500; letter-spacing: .04em;
}

/* "Domain focus" stops shouting. Uppercase came from styles.css; the 1px of tracking that made
   sentence case look gappy came from the section-22 rule, which re-declares letter-spacing at .1em
   at the SAME specificity (0,2,1) - so this has to sit after it, not merely match it. An earlier
   copy of this override was silently losing that race. */
body.app-dark .exam-domains-head strong {
  text-transform: none; letter-spacing: .02em; font-size: 10.5px;
}


/* 6. One metadata line replacing three bordered boxes. The separator is drawn between items
   rather than written into the markup, so a missing duration or pass score cannot leave a
   dangling pipe. */
body.app-dark .exam-grid .exam-meta-row {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 10px;
  margin: 14px 0 0; font-size: 12.5px; color: var(--ad-text-3);
  font-variant-numeric: tabular-nums;
}
body.app-dark .exam-grid .exam-meta-row > span + span::before {
  content: '|'; margin-right: 10px; color: rgba(255,255,255,.16);
}

/* 5. The domain panel loses its frame and its chips. Two plain rows and a muted overflow line -
   the eyebrow stays because two bare strings under a description do not read as domains. */
body.app-dark .exam-grid .exam-domains-section {
  background: none; border: 0; border-radius: 0; padding: 0; margin-top: 14px;
}
body.app-dark .exam-grid .exam-domain-chips {
  display: flex; flex-direction: column; align-items: flex-start; gap: 4px; margin-top: 8px;
}
body.app-dark .exam-grid .exam-domain-row {
  font-size: 13px; line-height: 1.45; color: var(--ad-text-2);
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* 3. The overflow count was an amber pill. Amber means "warning" in the rest of this app, and
   four more domains is not a warning. */
body.app-dark .exam-grid .exam-domain-more {
  font-size: 12.5px; color: var(--ad-text-4); background: none; border: 0; padding: 0;
}

/* 7. The action. A text link that only takes colour on hover or keyboard focus, so twelve of them
   down a page stay quiet while still being obviously clickable. */
body.app-dark .exam-grid .exam-start-btn {
  display: inline-flex; align-items: center; gap: 6px;
  background: none; border: 0; border-radius: 4px; padding: 2px 0;
  font-size: 13px; font-weight: 600; color: var(--ad-text-2); text-decoration: none;
  transition: color .15s;
}
body.app-dark .exam-grid .exam-start-btn:hover,
body.app-dark .exam-grid .exam-card:hover .exam-start-btn { color: var(--ad-cyan-soft); }
body.app-dark .exam-grid .exam-start-btn:focus-visible {
  outline: 2px solid var(--ad-cyan); outline-offset: 3px; color: var(--ad-cyan-soft);
}
/* styles.css appends its own '>' via ::after. The markup now carries a real arrow element so the
   arrow can animate on hover, which a pseudo-element on the same node cannot do independently -
   without this the card shows two chevrons. */
body.app-dark .exam-grid .exam-start-btn::after { content: none; }
body.app-dark .exam-grid .exam-start-arrow { transition: transform .15s; }
body.app-dark .exam-grid .exam-start-btn:hover .exam-start-arrow { transform: translateX(2px); }
@media (prefers-reduced-motion: reduce) {
  body.app-dark .exam-grid .exam-start-btn:hover .exam-start-arrow { transform: none; }
}

/* 8. A neutral card. --ad-panel is #0A0E1A, which carries a faint blue cast; at twelve cards
   filling the viewport that cast is what reads as "blue tint". #0e1017 is the same value the
   marketing certification-path cards use, so the signed-in catalogue and the public path pages
   now agree. The glass gradient goes with it - one flat surface, one border. */
body.app-dark .exam-grid .exam-card {
  background: #0e1017;
  border: 1px solid rgba(255,255,255,.09);
  box-shadow: none;
}
body.app-dark .exam-grid .exam-card:hover {
  background: #12151e; border-color: rgba(255,255,255,.16);
}

/* ---- 27. Study Planner: fewer colours, fewer panels -----------------------------------------
   The page was carrying amber, teal, green, cyan, orange and pink at once, across a card-inside-a-
   card-inside-a-card. What survives is what encodes something real: the calendar legend (green /
   amber / grey ARE the three day states), cyan for the selected exam, the active tab, today and
   progress, and red for Remove - but only once you are pointing at it. */

/* 1. Three identical neutral summary cards. The acc-* top borders and the coloured icon chips are
   dropped in study-plan.js; these rules make sure the surface underneath is the same neutral panel
   used everywhere else, and that a stray .amber / .teal class cannot repaint the icon. An exam 41
   days away is not a warning, and a readiness figure is not a success. */
body.app-dark .sp-stat {
  background: var(--ad-glass), var(--ad-panel);
}
body.app-dark .sp-stat::before { display: none; }
body.app-dark .sp-stat-icon,
body.app-dark .sp-stat-icon.ind,
body.app-dark .sp-stat-icon.amber,
body.app-dark .sp-stat-icon.teal,
body.app-dark .sp-stat-icon.green {
  background: none; color: var(--ad-text-4); width: auto; height: auto;
}

/* 3. The Today panel stops being a panel. It was a bordered, rounded, glass surface sitting inside
   .section-card - a card inside a card - when all it needed was to be the right-hand half of one
   card. A single hairline does the separating. */
body.app-dark .sp-insight-panel {
  background: none; box-shadow: none; border-radius: 0;
  border: 0; border-left: 1px solid var(--ad-line); padding-left: 26px;
}
@media (max-width: 900px) {
  body.app-dark .sp-insight-panel {
    border-left: 0; border-top: 1px solid var(--ad-line); padding-left: 0; padding-top: 22px;
  }
}

/* 4. A 2x2 grid divided by lines rather than four little cards. Borders on the grid itself, so
   there is one line between cells instead of eight card edges meeting in pairs. */
body.app-dark .sp-tiles {
  gap: 0; margin: 16px 0;
  border-top: 1px solid var(--ad-line); border-left: 1px solid var(--ad-line);
}
body.app-dark .sp-tile {
  background: none; border: 0; border-radius: 0;
  border-right: 1px solid var(--ad-line); border-bottom: 1px solid var(--ad-line);
  /* align-items:flex-start is load-bearing: the base rule is a ROW with align-items:center, and
     flipping only the direction leaves that centring on the new cross axis - the values come out
     centred in each cell instead of forming two readable columns. */
  padding: 12px 14px; display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
}
body.app-dark .sp-tile-val { color: #fff; font-size: 15px; font-weight: 600; }
body.app-dark .sp-tile-lbl { font-size: 11.5px; }

/* 6 + 7. The tracker bar becomes quiet text. It was a filled, bordered alert repeating the number
   already in the summary card above it, with a second Practice now beside the panel's own. Neutral
   is now genuinely neutral - no fill, no border, no icon chip - and it keeps the good/danger tints
   only for the two states that are actually events. */
body.app-dark .sp-track-status.neutral {
  background: none; border-color: transparent; padding-left: 0; padding-right: 0;
  color: var(--ad-text-3); font-weight: 500;
}
body.app-dark .sp-track-status.neutral .sp-track-ico { color: var(--ad-text-4); }

/* 8. Edit / Remove as icon buttons. Red is held back until hover or focus, so a destructive action
   is not shouting at someone who only came to check their streak. */
body.app-dark .sp-plan-action {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; padding: 0; border-radius: 8px;
  background: none; border: 1px solid transparent; color: var(--ad-text-4);
  cursor: pointer; transition: color .15s, background .15s, border-color .15s;
}
body.app-dark .sp-plan-action:hover {
  color: #fff; background: rgba(255,255,255,.06); border-color: var(--ad-line);
}
body.app-dark .sp-plan-action:focus-visible {
  outline: 2px solid var(--ad-cyan); outline-offset: 2px;
}
body.app-dark .sp-plan-action.danger { color: var(--ad-text-4); }
body.app-dark .sp-plan-action.danger:hover {
  color: #fda4af; background: rgba(244,63,94,.12); border-color: rgba(244,63,94,.28);
}
body.app-dark .sp-plan-action.danger:focus-visible { outline-color: #fda4af; }

/* ---- 46. Saved Questions: one panel, not two islands ----------------------------------------
   The zero state was a narrow card on the left and a how-to panel pinned to the right of a
   1600px rail, with ~360px of nothing between them - which read as a page that had failed to
   finish rendering rather than a page with nothing in it.

   The rail itself is NOT touched. It is shared by all eight authenticated pages so the title and
   content edge never jump between sections; narrowing it here would make Saved the one page whose
   heading starts somewhere else. The PANEL gets the width instead, and centres inside the rail. */
body.app-dark .saved-empty-shell {
  /* Capped and LEFT-aligned, not centred. Centring the panel inside a 1600px rail while the page
     heading stays at the rail's left edge puts the two out of line with each other - the panel
     starts 117px right of its own title. Sharing the left edge is what makes it read as one
     composition, and it is how every other page in the app is built. */
  max-width: 1060px; margin: 4px 0 0;
  grid-template-columns: minmax(0, 1fr) minmax(260px, 330px);
  gap: 0;
  background: var(--ad-glass), var(--ad-panel);
  border: 1px solid var(--ad-line);
  border-radius: 16px;
  overflow: hidden;
}
/* The two halves are now regions of one surface, so neither carries its own frame. A single
   hairline separates them. */
body.app-dark .saved-empty-shell > .empty-state-card,
body.app-dark .saved-empty-shell > .saved-side-card {
  background: none; border: 0; border-radius: 0; box-shadow: none; margin: 0;
}
/* .empty-state-card carries max-width:520px for its standalone use elsewhere. Inside the panel
   that leaves a 200px trench between the two halves - the exact gap this change set out to close -
   so the left half fills its grid column instead. */
body.app-dark .saved-empty-shell > .empty-state-card { max-width: none; }
body.app-dark .saved-empty-shell > .saved-empty-main { padding: 32px 34px; }
body.app-dark .saved-empty-shell > .saved-side-card {
  padding: 32px 30px; border-left: 1px solid var(--ad-line);
}
@media (max-width: 860px) {
  /* The columns HAVE to be restated here. styles.css collapses .saved-empty-shell to one column at
     this width, but the dark rule above is (0,2,0) against its (0,1,0) and outranks it at every
     width - a media query does not change specificity. Without this the shell kept two columns on a
     phone: the sidebar took its 330px minimum and the main half was left 26px, so the heading, the
     copy and both buttons rendered as a vertical ribbon. The border and padding rules below were
     already written as if the collapse had happened, which is what hid it. */
  body.app-dark .saved-empty-shell { grid-template-columns: minmax(0, 1fr); }
  body.app-dark .saved-empty-shell > .saved-side-card {
    border-left: 0; border-top: 1px solid var(--ad-line); padding: 26px 30px 30px;
  }
  body.app-dark .saved-empty-shell > .saved-empty-main { padding: 28px 30px 4px; }
}

/* 7. Neutral bullets. Cyan is spent on the bookmark icon and the primary action; a list of three
   instructions does not need three more cyan dots competing with them. */
body.app-dark .saved-side-card .es-points li::before { background: rgba(255,255,255,.28); opacity: 1; }

/* 2. The count rides with the heading instead of floating at the far right edge of the rail. */
body.app-dark .bookmarks-page-head { display: block; }
body.app-dark .bookmarks-page-head h1 .bookmarks-status-chip,
body.app-dark .page-header h2 .bookmarks-status-chip {
  display: inline-block; vertical-align: middle; margin-left: 10px;
  position: relative; top: -1px; font-weight: 600;
}

/* The bookmark mark is the one cyan thing in the left half - it is what the reader has to go and
   find in the practice UI, so it is worth pointing at. */
body.app-dark .saved-empty-main .es-icon { color: var(--ad-cyan-soft); }

/* ---- 47. Plans: neutral cards, one accent ---------------------------------------------------
   The page was identifying each plan by colour - violet, emerald, teal top borders and matching
   badges - which made the palette carry information the prices should. Every card is now the same
   neutral surface and cyan means exactly one thing here: this is the plan you are on. */

/* 4. Neutral top borders. Cyan only on the current plan. */
body.app-dark .pricing-card,
body.app-dark .pricing-card.popular,
body.app-dark .pricing-card-free,
body.app-dark .pricing-card-single-exam,
body.app-dark .pricing-card-pro-monthly,
body.app-dark .pricing-card-pro-annual {
  background: var(--ad-glass), var(--ad-panel);
  border: 1px solid var(--ad-line);
  border-top: 2px solid rgba(255,255,255,.12);
  box-shadow: none;
}
body.app-dark .pricing-card.current {
  border-color: rgba(34,211,238,.35);
  border-top-color: var(--ad-cyan);
}

/* 5 + 6. The kicker is suppressed in pricing.js on every card but Pro Annual, and there too only
   while it is not the current plan. With the slot usually empty the row aligns from the left. */
body.app-dark .pricing-card-top { justify-content: flex-start; gap: 8px; }
body.app-dark .pricing-plan-kicker,
body.app-dark .pricing-plan-fit {
  background: none; border: 1px solid rgba(255,255,255,.14); color: var(--ad-text-3);
  font-weight: 500;
}
body.app-dark .pricing-current-tag {
  background: none; border: 1px solid rgba(34,211,238,.35); color: var(--ad-cyan-soft);
  font-weight: 600;
}
/* The floating "Best Value" ribbon is already suppressed on the current card in pricing.js. */
body.app-dark .pricing-popular-tag {
  background: none; border: 1px solid rgba(255,255,255,.16); color: var(--ad-text-3);
  box-shadow: none; font-weight: 500;
}

/* 7. "Best for ..." stops being a framed box and becomes the muted line under the access period,
   which is where it was always meant to be read. */
body.app-dark .pricing-card-note {
  background: none; border: 0; border-radius: 0; padding: 0; margin-top: 6px;
  color: var(--ad-text-4); font-size: 12.5px; font-style: normal;
}

/* 10. Neutral ticks. Sixteen cyan checkmarks down the page were competing with four buttons. */
body.app-dark .pricing-features li::before,
body.app-dark .pricing-includes li::before { color: var(--ad-text-4); }

/* 3 + 8. The assurance row is one quiet line, and the access state is an inline line under the
   heading rather than a bordered card floating opposite it. */
body.app-dark .pricing-assurance-row {
  display: block; margin: 14px 0 0; padding: 0; border: 0; background: none;
  font-size: 12.5px; color: var(--ad-text-4);
}
body.app-dark .pricing-account-line {
  margin: 12px 0 0; font-size: 13px; color: var(--ad-text-3);
}
body.app-dark .pricing-account-line strong { color: var(--ad-text); font-weight: 600; }
body.app-dark .pricing-account-line strong::after {
  content: '\00b7'; margin: 0 8px; color: rgba(255,255,255,.25); font-weight: 400;
}

/* ---- 48. Profile: neutral identity, one access statement -------------------------------------
   The gradient banner (#0d1b3e -> #1a237e -> #07625b) and the cyan-to-violet avatar were the last
   of the old palette on this page, and they sat on the one panel that carries no data. */

/* 1. Neutral banner. Same surface as the panels below it, separated by a hairline. */
body.app-dark .acct-summary-hero {
  background: none; border-bottom: 1px solid var(--ad-line);
}
/* 2. Neutral avatar, cyan ring. The initial is the identity; the gradient was decoration. */
body.app-dark .profile-avatar {
  background: rgba(255,255,255,.05);
  border: 1px solid rgba(34,211,238,.45);
  box-shadow: none; color: var(--ad-text); font-weight: 600;
}
/* 3 + 4. The badge bar is one neutral role badge; the plan pill and "via <source>" are gone from
   the hero (profile.js) and live only in the Access block. */
body.app-dark .profile-hero-badges {
  background: none; border: 0; border-radius: 0; padding: 0;
}
body.app-dark .profile-hero-badges .badge,
body.app-dark .profile-hero-badges .badge-info {
  background: none; border: 1px solid rgba(255,255,255,.16); color: var(--ad-text-3);
  font-weight: 500;
}
body.app-dark .profile-hero-access {
  margin-top: 6px; font-size: 12.5px; color: var(--ad-text-4);
}

/* 10. Source / Expires / Time left as one row with hairline separators, not three framed boxes. */
body.app-dark .profile-access-meta {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0; margin-top: 12px;
}
body.app-dark .profile-access-meta-item {
  display: inline-flex; align-items: baseline; gap: 6px;
  background: none; border: 0; border-radius: 0; padding: 0 14px 0 0; margin: 0;
}
body.app-dark .profile-access-meta-item + .profile-access-meta-item {
  padding-left: 14px; border-left: 1px solid var(--ad-line);
}
body.app-dark .profile-access-meta-label {
  font-size: 11.5px; color: var(--ad-text-4); text-transform: none; letter-spacing: 0;
}
body.app-dark .profile-access-meta-value { font-size: 13px; color: var(--ad-text-2); font-weight: 600; }

/* 9. Amber is reserved for one thing on this page: two-step verification is off. */
body.app-dark .security-hint { display: none; }
body.app-dark .security-value-warn { color: var(--ad-text-2); }

/* 11. A commit button is quiet until the form differs from what was loaded, then it is the one
   solid action in its card. Without this, two permanently-solid buttons invite a click that does
   nothing at all. */
/* Keyed on :disabled / :enabled rather than a class. The button is disabled unless the form is
   dirty, so "enabled" IS "armed" - a separate .is-armed class was a second source of truth for the
   same state, and it lost the cascade to the disabled rule in a way that was invisible in the
   markup. Pseudo-classes cannot drift from the property they describe. */
body.app-dark button.pf-commit:disabled {
  background: rgba(255,255,255,.04); border: 1px solid var(--ad-line);
  color: var(--ad-text-4); cursor: not-allowed; opacity: 1;
}
body.app-dark button.pf-commit:enabled {
  background: var(--ad-cyan); border: 1px solid var(--ad-cyan); color: #05060d; cursor: pointer;
}
body.app-dark button.pf-commit:enabled:hover { background: #67e8f9; border-color: #67e8f9; }

/* ---- 49. Admin Exam Trends: numbers first, colour last --------------------------------------
   The page ran cyan, emerald, violet and amber across four unrelated counters, gold/silver/violet
   medals down three leaderboards, violet study-time values and a violet-to-cyan gradient on the
   active tab. None of it encoded anything. What survives: green for genuine positive growth, and
   cyan for the selected tab. */

/* 2. Neutral KPI cards with white values. The gradient-clipped numbers were unreadable as data -
   a figure that is a different colour on each card invites comparison between the colours. */
body.app-dark .admin-container #et-totals .stat-card { border-top-color: var(--ad-line); }
body.app-dark .admin-container #et-totals .stat-card::before { background: none; }
body.app-dark .admin-container #et-totals .stat-value {
  background: none; -webkit-background-clip: border-box; background-clip: border-box;
  color: #fff;
}

/* 3, 4 and 5 are fixed at source above (section 16's leaderboard block) rather than overridden
   here - the originals were ID- and nth-child-scoped, so a class-based override could not win. */

/* 6. The active tab loses its cyan-to-violet gradient for a neutral ground and a cyan underline -
   the same "selected" language the rest of the app uses. */
body.app-dark .admin-container .tab.active {
  color: #fff; background: rgba(255,255,255,.05);
  border-color: var(--ad-line); border-bottom: 2px solid var(--ad-cyan);
}

/* 7. The filter header becomes a compact toolbar: title left, filters right, on one line. It was
   a full section header wrapping three selects onto their own row. */
body.app-dark .admin-container #et-filters-head,
body.app-dark .admin-container .section-card > h3:has(.header-filters) {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  flex-wrap: wrap; padding-top: 14px; padding-bottom: 14px;
}
body.app-dark .admin-container .header-filters {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 0;
}
body.app-dark .admin-container .header-filter { height: 34px; padding: 0 10px; font-size: 13px; }

body.app-dark .admin-trends-filter-section,
body.app-dark .admin-trend-card,
body.app-dark .admin-trend-details-section {
  background:
    linear-gradient(180deg, rgba(255,255,255,.035), rgba(255,255,255,.012)),
    var(--ad-panel);
  border-color: var(--ad-line);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.035);
}
body.app-dark .trend-leaderboard-item {
  border-bottom-color: var(--ad-line-soft);
}
body.app-dark .trend-leaderboard-name {
  color: var(--ad-text);
}
body.app-dark .trend-leaderboard-value {
  color: var(--ad-text-2);
}
body.app-dark .trend-empty-panel {
  background: rgba(255,255,255,.025);
  border-color: var(--ad-line-soft);
}
body.app-dark .admin-trend-row:hover {
  background: rgba(34, 211, 238, .045);
}
body.app-dark .trend-exam-name {
  color: var(--ad-text);
}
body.app-dark .trend-exam-category {
  color: var(--ad-text-3);
}
body.app-dark .trend-number-cell,
body.app-dark .trend-time-cell {
  color: var(--ad-text-2);
}
body.app-dark .admin-trend-table th {
  white-space: nowrap;
}
body.app-dark .trend-domain-breakdown {
  background: rgba(5, 8, 14, .30);
  border-color: var(--ad-line-soft);
}
body.app-dark .trend-domain-table thead th {
  background: rgba(255,255,255,.045);
  color: var(--ad-text-4);
}
body.app-dark .trend-domain-table td {
  border-bottom-color: rgba(255,255,255,.04);
}
body.app-dark .trend-domain-name {
  color: var(--ad-text-2);
}
body.app-dark .trend-domain-count {
  color: var(--ad-text-3);
}
body.app-dark .trend-domain-pct.pass { color: #6ee7b7; }
body.app-dark .trend-domain-pct.fail { color: #f87171; }
body.app-dark .trend-domain-error {
  color: #fda4af;
}
body.app-dark .trend-domain-empty {
  color: var(--ad-text-3);
}

/* ---- 50. Plans is an account page, not a pricing page -----------------------------------------
   public/pricing.html serves two audiences from one file: the public pricing page and the
   signed-in Plans page (public-mode.js strips app-dark for logged-out visitors, so every rule
   here is invisible to the public page and its schema.org offers / SEO copy are untouched).

   What was wrong: a customer holding Pro Annual was shown a conversion page. Four tall cards led
   the page with four buttons that cannot be clicked ("Included with Pro", "Current plan"), the
   active plan was restated four times over, and underneath sat a "what you are paying for" pitch
   plus links inviting the paying customer to go compare CertGrid against Whizlabs.

   The split: everything below .plans-access is comparison material. It stays exactly as it was
   for a signed-in FREE user - they still have a decision to make and the assurance line, the
   savings figure and the pitch all help them make it. It is the paid states (body.plans-paid,
   set in pricing.js) that hide the sales furniture and collapse the grid, because for them
   nothing in it is purchasable. */

/* 1. The page title matches the other seven signed-in pages: a short noun, not a sentence.
   "Choose the access that matches your exam plan" is landing-page copy; pricing.js rewrites the
   text to "Plans" for a signed-in visitor and this sizes it like Dashboard / Exam History. */
body.app-dark .pricing-page-head h1 { font-size: 26px; letter-spacing: -.02em; margin: 0; }
body.app-dark .pricing-eyebrow { display: none; }

/* A free signed-in user gets no chip and no button here on purpose: the comparison grid sits open
   directly below with the real "Upgrade Now" actions in it, and a second button would just be the
   same call twice. The chip appears only when there is live access to report. */

/* 3. Numbers first, colour last. The annual saving is a figure worth reading, and it reads as one
   without being the only green thing on the page. */
body.app-dark .pricing-savings {
  color: var(--ad-text-2); font-weight: 600;
}

/* 4. Every signed-in state: the lines the access panel now owns. All three of these said the same
   thing the panel says, in the same place, so a free user was reading four versions of "you are on
   the free plan" before reaching anything actionable - the marketing sub-head, the account line,
   the current-context sentence, and the panel itself.
     head > div > p:not([class])  - "Start free, unlock one exam, or use Pro for full-catalog ..."
     .pricing-account-line        - "Free plan  ·  You can sample practice now, then unlock ..."
     #pricing-current-context     - "Start free, unlock one exam for 6 months, or choose Pro ..."
   The last one also rendered centred and bold, which read as a stray banner between two panels. */
body.app-dark .pricing-page-head > div > p:not([class]),
body.app-dark .pricing-account-line,
body.app-dark #pricing-current-context { display: none; }

/* 5. Paid states only: the pitch. Unlike the lines above these are legitimately useful to a
   signed-in FREE user who is still deciding, so they only go once there is nothing left to sell.
     .pricing-about    - "What you are paying for", aimed at someone who has not bought yet
     .pricing-compare  - CertGrid vs Whizlabs / Tutorials Dojo / MeasureUp
   The FAQ deliberately stays in both: refunds, failed payments and currency are exactly what a
   paying customer comes to this page to look up. The Razorpay assurance line is not hidden by plan
   at all - pricing.js moves it inside the disclosure so it appears with the options it describes. */
body.app-dark.plans-paid .pricing-about,
body.app-dark.plans-paid .pricing-compare { display: none; }

/* 6. One "Best Value", not two. pricing.js emits both a floating .pricing-popular-tag ribbon and
   a .pricing-plan-kicker chip reading the same words on the same card, so Pro Annual carried the
   badge twice - and the floating one is positioned over the card's top edge, where it was clipped
   by the border. The chip inside the header row is the one that sits in the layout. */
body.app-dark .pricing-popular-tag { display: none; }

/* 7. The in-card "Active: <exam> (until ...)" box is the access panel's line verbatim, and it was
   the last emerald thing on the page. The panel states it once, at the top, where it is read. */
body.app-dark .pricing-current-access { display: none; }

/* 8. Voucher redemption is the one thing on this page a Pro holder can actually do, so it sits
   above the collapsed grid rather than below it. pricing.js moves the node; this drops the
   heading from a section title to a panel label to match its new weight.

   The columns hug their content instead of `minmax(0, 1fr) auto`. Section 37e removed this
   panel's 720px cap so it lined up with the full-rail grid below it - but the grid now lives
   behind a disclosure, so the stretchy first column was left pushing the input and Apply button
   to the far right edge of a 1360px rail, a screen's width from the label they belong to. */
body.app-dark .voucher-redeem-section { justify-content: start; }
/* The columns, the padding and the mobile fallback all moved to section 52 with the rest of this
   panel's flattening. Re-declaring them here would be shadowed by it anyway. */

/* Without the "View Plans" button (nav.js drops it on this page - it linked here) the card's text
   was left holding the 11px of clearance that used to separate it from that button. */
body.app-dark .sidebar-card:not(:has(.btn)) .sidebar-card-text { margin-bottom: 0; }

/* 9. The FAQ closes the page for a paid user, so it gets the section spacing the removed
   .pricing-about used to provide. */
body.app-dark.plans-paid .pricing-faq { margin-top: 34px; }

@media (max-width: 720px) {
}

/* ---- 51. Dashboard polish: fewer ingredients, calmer hierarchy --------------------------------
   Placed last on purpose. Several rules this replaces sit at the same specificity (0,2,0) earlier
   in the file, so source order is what decides - the same reason section 26's .recommended-card
   accent had to come after the shared-surfaces rule in section 5. */

/* Sentence case. The .section-label markup lives only in dashboard.html, so dropping the transform
   here changes that page and nothing else - exams.js builds its catalog labels with
   cat.toUpperCase(), so those stay capitals from the string rather than the stylesheet. The metrics
   move with the case: 10px at .16em is tracking tuned for capitals and reads thin and gappy on
   sentence case. The catalog label keeps the old numbers so that page is pixel-identical. */
body.app-dark .section-label {
  font-size: 11.5px; letter-spacing: .01em; text-transform: none; font-weight: 600;
  color: var(--ad-text-3);
}
body.app-dark .section-label.catalog-section-label {
  font-size: 10px; letter-spacing: .16em; text-transform: uppercase; font-weight: 700;
  color: var(--ad-text-4);
}

/* The recommendation is the one thing this page exists to push, so it outweighs the four metric
   cards rather than matching them: more room, a bigger line, a real button. The metric cards
   report a number; this card asks for a decision. */
body.app-dark .recommended-card.is-hero { padding: 22px 24px; gap: 18px; align-items: center; }
body.app-dark .recommended-card.is-hero .rec-title { font-size: 20px; margin-top: 5px; }
body.app-dark .recommended-card.is-hero .rec-desc { font-size: 13.5px; }
body.app-dark .recommended-card.is-hero .rec-icon { width: 52px; height: 52px; }
body.app-dark .recommended-card.is-hero .btn { padding: 11px 22px; font-size: 14px; }

/* One accent, not two. The icon carried a cyan-to-violet gradient, which put the page's only
   violet on its most important card. Matched at the SAME specificity as section 16a's pair -
   `#rec-fallback .rec-icon` is (1,1,1) and a plain `body.app-dark .rec-icon` (0,2,0) loses to it,
   which is the trap 16a's own comment records. Both selectors are repeated so both are beaten. */
body.app-dark #rec-fallback .rec-icon,
body.app-dark .recommended-card .rec-icon {
  background: linear-gradient(135deg, rgba(34,211,238,.22), rgba(34,211,238,.06));
}

/* Recent attempts as a table rather than a stack of pairs. dashboard.js now emits one row per
   attempt instead of a row plus a date line, so the cells get explicit tracks and the rows tighten:
   five attempts used to mean ten rows, which is where the column's empty space came from. */
body.app-dark .recent-attempt-row {
  display: grid; grid-template-columns: minmax(70px, auto) 1fr auto auto;
  align-items: center; gap: 12px; padding: 8px 0;
  border-bottom: 1px solid rgba(255,255,255,.045);
}
body.app-dark .recent-attempt-date {
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 11px; color: var(--ad-text-4); white-space: nowrap;
}
body.app-dark .recent-attempt-score { font-variant-numeric: tabular-nums; white-space: nowrap; }
body.app-dark .recent-attempt-status { justify-self: end; }

/* The access card states a fact; it is not a pitch. In the rail it carried a lit border, a glow and
   a full-width button, which made it compete with the nav above it and the page beside it. Kept
   useful, made quiet: flat surface, no glow, and a small outline button instead of a filled block.
   nav.js injects `.sidebar-card .btn{display:block;width:100%}` inline at (0,2,0); these are (0,3,0)
   and win on specificity rather than order. */
body.app-dark .sidebar-card {
  background: rgba(255,255,255,.022); border-color: rgba(255,255,255,.06); box-shadow: none;
}
body.app-dark .sidebar-card.is-pro::before { display: none; }
body.app-dark .sidebar-card .btn {
  display: inline-flex; width: auto; padding: 5px 11px; font-size: 11px; font-weight: 600;
  margin-top: 8px; background: transparent; box-shadow: none;
  border: 1px solid rgba(34,211,238,.30); color: var(--ad-cyan-soft);
}
body.app-dark .sidebar-card .btn:hover {
  background: rgba(34,211,238,.10); border-color: rgba(34,211,238,.5);
}

/* ---- 52. Plans: a purchase page, not a billing table ----------------------------------------
   Placed last on purpose. Several rules here have to beat earlier ones at the same or higher
   specificity - section 47 hid the sub-head, and section 22's .pricing-card-pro-annual chip rules
   are (0,3,0), which no plain `body.app-dark .pricing-plan-fit` can reach - so source order is
   doing the work. The same trap section 51 records for the dashboard.

   The read: the page led with the word "Plans", then two bordered panels of account admin, and
   only then four cards of roughly equal weight. Everything above the cards has been quieted and
   everything that distinguishes one card from another has been sharpened. No new colour: cyan
   stops meaning two things at card level, and the last emerald on the page goes. */

/* 1. The sub-head comes back. Section 47 hid it as one of four duplicate lines under the heading;
   the other three are still hidden, so this is the only sub-head left and duplicates nothing.
   Same specificity as the rule that hides it (0,3,0 via the child combinator chain), so it wins
   only because it is later in the file. */
body.app-dark .pricing-page-head > div > p:not([class]) {
  display: block;
  margin: 8px 0 0; max-width: 62ch;
  font-size: 14px; line-height: 1.55; color: var(--ad-text-3);
}
body.app-dark .pricing-page-head h1 { font-size: 28px; }

/* 2. The access panel is gone. It was a bordered section with a heading, the scope, a date/exam-code
   line and a terms line - four stacked facts above the cards, which made the first screen read as an
   account statement rather than a place to choose something. It is one muted line now
   (.plans-access-line), styled in css/pricing-cards.css, which loads after this file. */

/* 3. The grid label, matched to the section labels on every other signed-in page. */
body.app-dark .plans-grid-heading {
  font-size: 11.5px; font-weight: 600; letter-spacing: .01em;
  color: var(--ad-text-3); margin: 26px 0 12px;
}

/* 4 + 5. Card hierarchy and the Pro Annual chip are NOT here. public/css/pricing-cards.css loads
   AFTER this file and names both shells (body.app-dark and html.public-mode), so it wins on source
   order at equal specificity - every card rule written here is inert. That is not a hypothetical:
   the first version of this section set the current-card outline and the Pro Annual tint here, and
   measuring the computed style showed the old values unchanged. The rules live in that file now.
   The same goes for the emerald `.pricing-card-pro-annual .pricing-plan-fit` rule in section 22,
   which section 47 tried and failed to neutralise from here. */

/* 6. The coverage note: what a practice plan includes, and that Guided Labs are not in it. */
/* --ad-text-3, not -4. It was the same .30 white as the "Showing" caption on the catalogue, but
   this line is the only place on the page that says Guided Labs are a separate purchase - a fact
   worth being able to read rather than merely having on the page. */
body.app-dark .pricing-coverage-line {
  margin: 6px 0 0; max-width: 88ch;
  font-size: 12px; line-height: 1.6; color: var(--ad-text-3);
}

/* 7. Voucher redemption as a compact row. It sits below the cards now (pricing.js moves the node),
   so it no longer has to hold its own against plan selection - a heading, a paragraph of
   explanation and a full panel surface were all sized for a position it no longer occupies. */
body.app-dark .voucher-redeem-section {
  margin: 22px 0 0; padding: 12px 14px;
  background: rgba(255,255,255,.02);
  border: 1px solid var(--ad-line-soft); border-radius: 12px;
  box-shadow: none;
  grid-template-columns: auto auto; align-items: center; gap: 16px;
}
body.app-dark .voucher-redeem-section h2 {
  font-size: 12.5px; font-weight: 600; margin: 0; color: var(--ad-text-2);
}
/* The explanatory sentence goes. "Apply a discount code before checkout, or redeem a grant from
   your organization" is instructions for a labelled text field beside a button marked Apply. */
body.app-dark .voucher-redeem-section > div > p { display: none; }
/* One column on a phone. This has to live AFTER the rule above, not in the 640px block section 47
   used to carry: a media query adds no specificity, so the unconditional two-column declaration
   was silently beating it. */
@media (max-width: 640px) {
  body.app-dark .voucher-redeem-section { grid-template-columns: minmax(0, 1fr); gap: 10px; }
}

/* 8. Billing questions: support content. Same treatment as the panels above - a flat tint instead
   of the card recipe, so the section that closes the page stops competing with the plans. */
body.app-dark .pricing-faq {
  background: rgba(255,255,255,.02);
  border: 1px solid var(--ad-line-soft);
  box-shadow: none;
  padding: 22px 24px 24px;
}
body.app-dark .pricing-faq h2 { font-size: 16px; margin: 0 0 18px; }
body.app-dark .pricing-faq-list p { color: var(--ad-text-3); }


/* ---- 53. Study planner: one workspace, not a tray of widgets ---------------------------------
   Placed last deliberately. Sections 14, 28, 30c-e and 33 all style these same classes at the same
   specificity, so nothing here would land earlier in the file - the same reason sections 51 and 52
   sit where they do.

   The page was assembled from nine modules stacked vertically: a plan pill row, three metric cards,
   a status strip, a tab bar, a calendar card containing a grid plus a panel containing a 2x2 tile
   grid, and a seven-cell forward strip. Most of the reduction is in the markup (the status strip's
   duplicate state and the tile grid are gone from study-plan.js); what is left here is weight. */

/* Metric cards: shorter, and no longer colour-coded. Three cards carried an amber, a teal and a
   green 2px top line plus an indigo/amber/teal icon chip - six accents encoding nothing, since
   "exam date" is not amber. One neutral card, one icon treatment. */
body.app-dark .sp-stat { padding: 13px 14px; border-radius: 12px; box-shadow: none; }
body.app-dark .sp-stat:hover { border-color: var(--ad-line); box-shadow: none; }
body.app-dark .sp-stat::before { display: none; }
body.app-dark .sp-stat-head { min-height: 0; margin-bottom: 8px; }
body.app-dark .sp-stat-icon {
  width: 28px; height: 28px; border-radius: 8px;
  background: rgba(255,255,255,.05); color: var(--ad-text-3); box-shadow: none;
}
body.app-dark .sp-stat-icon.ind,
body.app-dark .sp-stat-icon.amber,
body.app-dark .sp-stat-icon.teal,
body.app-dark .sp-stat-icon.green {
  background: rgba(255,255,255,.05); color: var(--ad-text-3);
}
body.app-dark .sp-stat-value { font-size: 20px; }
body.app-dark .sp-stat-value.sp-stat-title { font-size: 14.5px; }
/* Sentence case, and tracking to match. styles.css sets text-transform: uppercase at (0,1,0), so
   this needs saying explicitly rather than merely omitting it. */
body.app-dark .sp-stat-label {
  font-size: 11.5px; text-transform: none; letter-spacing: .01em; margin-top: 5px;
}
body.app-dark .sp-stat-meta { font-size: 11.5px; margin-top: 6px; }

/* One CTA in the summary area. "Start measured session" sat inside the readiness card while
   "Practice now" sat in the strip below it - two competing calls a row apart. The card's is a plain
   link now; the filled button in the Today panel is the page's action. */
body.app-dark .sp-stat-cta {
  color: var(--ad-cyan-soft); font-size: 12px; font-weight: 500; text-decoration: none;
  background: none; border: 0; padding: 0;
}
body.app-dark .sp-stat-cta:hover { color: #a5f3fc; text-decoration: underline; }

/* Status strip, for the four states that survive: a flat tint, no drop shadow. */
body.app-dark .sp-track-status {
  padding: 9px 12px; border-radius: 10px; margin-bottom: 16px; box-shadow: none;
}
body.app-dark .sp-track-msg { font-size: 12.5px; font-weight: 600; }
body.app-dark .sp-track-ico { width: 22px; height: 22px; border-radius: 7px; }
/* Practice now, flat. It was a cyan-to-teal gradient with an outer glow - the loudest object on the
   page, on a row that is now the exception rather than the norm. */
body.app-dark .sp-track-cta {
  background: var(--ad-cyan); border: 0; color: #052b33; font-weight: 600;
  border-radius: 999px; padding: 6px 14px; box-shadow: none;
}
body.app-dark .sp-track-cta:hover { background: #67e8f9; filter: none; }

/* Tabs: quieter, and the active one marked by a cyan underline rather than a filled pill. */
body.app-dark #sp-tabs.tabs { border-bottom: 1px solid var(--ad-line-soft); gap: 2px; }
body.app-dark #sp-tabs .tab {
  color: var(--ad-text-3); background: none; border: 0; border-bottom: 2px solid transparent;
  border-radius: 0; padding: 8px 12px; font-size: 13px; font-weight: 500;
}
body.app-dark #sp-tabs .tab:hover { color: #fff; background: none; }
body.app-dark #sp-tabs .tab.active {
  color: var(--ad-cyan-soft); background: none;
  border-bottom-color: var(--ad-cyan); font-weight: 600;
}

/* Calendar: 30px cells. Three states and today, which is what the legend already promised - the
   fills come down so a month of mostly-empty cells stops being the loudest block in the tab. */
body.app-dark .sp-cal-grid { max-width: 300px; gap: 4px; }
body.app-dark .sp-cal-day {
  min-height: 30px; border-radius: 6px;
  background: rgba(255,255,255,.025); border: 1px solid transparent;
}
body.app-dark .sp-cal-day.none    { background: rgba(255,255,255,.02); }
body.app-dark .sp-cal-day.met     { background: rgba(52,211,153,.13); color: #6ee7b7; }
body.app-dark .sp-cal-day.partial { background: rgba(251,191,36,.11); color: #fcd34d; }
body.app-dark .sp-cal-day.today {
  background: rgba(34,211,238,.10); color: #67e8f9; border-color: rgba(34,211,238,.40);
}
body.app-dark .sp-cal-num { font-size: 12px; }
body.app-dark .sp-cal-q { font-size: 9px; }
/* The 8px uppercase "TODAY" caption inside a 30px cell. The cyan border and fill already say it. */
body.app-dark .sp-cal-today { display: none; }

/* Today panel. The kicker stops shouting, and the tile grid it used to sit above is gone. */
body.app-dark .sp-insight-panel { border-left: 1px solid var(--ad-line-soft); padding-left: 20px; }
body.app-dark .sp-insight-kicker {
  font-size: 11.5px; font-weight: 600; letter-spacing: .01em; text-transform: none;
  color: var(--ad-text-3); margin-bottom: 4px;
}
body.app-dark .sp-insight-title { font-size: 17px; font-weight: 600; margin-bottom: 6px; }
body.app-dark .sp-insight-note { font-size: 12.5px; margin-bottom: 14px; }
/* Four facts on one line, wrapping if it must. Replaces .sp-tiles - see study-plan.js. */
body.app-dark .sp-statline {
  display: flex; flex-wrap: wrap; gap: 4px 14px; margin-bottom: 16px;
  font-size: 12px; color: var(--ad-text-4);
}
body.app-dark .sp-statline b {
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-weight: 500; color: rgba(255,255,255,.82); font-variant-numeric: tabular-nums;
}
body.app-dark .sp-insight-micro { font-size: 11.5px; }

/* Next 7 days as a strip. Every cell was a filled, padded tile with a 17px mono date and an
   uppercase weekday; today's was a cyan panel and exam day an amber one. The row is the module -
   the cells only have to be legible. */
body.app-dark .sp-next7 { gap: 6px; min-height: 0; }
body.app-dark .sp-n7-cell {
  gap: 3px; padding: 7px 4px 6px; border-radius: 8px;
  background: rgba(255,255,255,.025); border: 1px solid transparent;
}
body.app-dark .sp-n7-dow {
  font-size: 10px; font-weight: 500; letter-spacing: .01em; text-transform: none;
  color: var(--ad-text-4);
}
body.app-dark .sp-n7-date { font-size: 14px; font-weight: 500; color: var(--ad-text-2); }
body.app-dark .sp-n7-bar { height: 3px; }
body.app-dark .sp-n7-bar > span { background: rgba(34,211,238,.55); }
body.app-dark .sp-n7-val { font-size: 11px; font-weight: 500; color: var(--ad-text-4); }
body.app-dark .sp-n7-tag {
  font-size: 9.5px; font-weight: 500; letter-spacing: .01em; text-transform: none;
}
body.app-dark .sp-n7-cell.is-today {
  background: rgba(34,211,238,.07); border-color: rgba(34,211,238,.28);
}
body.app-dark .sp-n7-cell.is-today .sp-n7-date { color: #67e8f9; }
/* Exam day keeps amber - a date you cannot move is the one thing on this strip that is a real
   deadline, which is what amber means everywhere else in the app. Fill dropped, edge kept. */
body.app-dark .sp-n7-cell.is-exam {
  background: none; border-color: rgba(251,191,36,.28);
}
body.app-dark .sp-next7-foot { margin-top: 10px; font-size: 11.5px; }
body.app-dark .sp-legend { font-size: 11px; margin-top: 10px; }

/* Resets that have to be stated, not merely omitted. Each of these properties is set by a rule
   LATER in the file than the ones this section replaces - a shorthand left untouched keeps whatever
   the last declaration gave it, which is why the first pass measured a calendar cell still carrying
   an inset ring and a progress bar still carrying a gradient. */

/* Today's cell: a border and a tint. It also had `inset 0 0 0 1.5px cyan/.8` AND an 18px outer
   glow, on a 30px square. */
body.app-dark .sp-cal-day.today,
body.app-dark .sp-cal-day.today:hover {
  box-shadow: none; border-color: rgba(34,211,238,.40); color: #67e8f9;
  background: rgba(34,211,238,.10);
}
body.app-dark .sp-cal-day:hover { box-shadow: inset 0 0 0 1px rgba(255,255,255,.10); }

/* Progress fills: one flat cyan. Section 28 flattened this and section 30d put the cyan-to-teal
   gradient back, so the bars in the summary card and the Today panel were both gradients again. */
body.app-dark .sp-progress-fill { background: rgba(34,211,238,.55); }
body.app-dark .sp-mini-bar-val { font-size: 12px; font-weight: 500; color: var(--ad-text-2); }
body.app-dark .sp-mini-bar-lbl { font-weight: 500; }
body.app-dark .sp-mini-bar { margin-bottom: 14px; }

/* The exam pills. The active one was a cyan-to-VIOLET gradient with an outer glow in one rule and
   a flat tint with a glow in another; violet is gone from the rest of the app and this row is a
   selector, not the page's headline. */
body.app-dark .sp-pill.active {
  background: rgba(34,211,238,.10); border-color: rgba(34,211,238,.30);
  box-shadow: none; color: #fff;
}
body.app-dark .sp-pill { border-radius: 10px; }
/* Add exam sits with the plans it adds to, not as a third button style. */
body.app-dark .sp-pill-add { border-style: dashed; color: var(--ad-text-4); }
body.app-dark .sp-pill-add:hover { color: var(--ad-text-2); }

/* The readiness verdict. A filled rose pill was the loudest object in the summary row - and it is a
   verdict on a percentage, not an alert. Outline keeps the meaning at a quarter of the volume. */
body.app-dark .sp-stat-badge {
  background: none; font-weight: 500; font-size: 10.5px;
}
body.app-dark .sp-stat-badge.good    { border-color: rgba(52,211,153,.32); color: #6ee7b7; }
body.app-dark .sp-stat-badge.warn    { border-color: rgba(251,191,36,.32); color: #fcd34d; }
body.app-dark .sp-stat-badge.danger  { border-color: rgba(251,113,133,.32); color: #fda4af; }
body.app-dark .sp-stat-badge.neutral { border-color: var(--ad-line); color: var(--ad-text-3); }

/* Separators in the stats line, drawn between items rather than written into the markup - so a
   value that is ever conditional cannot leave a dangling dot. */
body.app-dark .sp-statline > span + span::before {
  content: '·'; margin-right: 14px; color: rgba(255,255,255,.20);
}
body.app-dark .sp-statline { gap: 4px 0; }
body.app-dark .sp-statline > span { margin-right: 14px; }
body.app-dark .sp-statline > span:last-child { margin-right: 0; }

/* ── Your access ──────────────────────────────────────────────────────────────────────────────
   What the account owns, and one action per item. A flat tint rather than the .section-card
   surface: this reports facts, it does not sell anything, and the recommended-card directly above
   it is the hero on this column - two card surfaces stacked would compete for the same job.

   Deliberately not a grid. Passes and paths are two different products from two different tables,
   and a two-column layout would imply a relationship between the left and right of each row that
   does not exist. */
body.app-dark .access-panel {
  padding: 14px 16px; border-radius: 12px;
  background: rgba(255,255,255,.02);
  border: 1px solid var(--ad-line-soft);
}
body.app-dark .access-group + .access-group { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--ad-line-soft); }
body.app-dark .access-group-label {
  font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--ad-text-4); margin-bottom: 8px;
}
body.app-dark .access-row {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 7px 0;
}
body.app-dark .access-row + .access-row { border-top: 1px solid rgba(255,255,255,.04); }
body.app-dark .access-row-text { flex: 1 1 auto; min-width: 0; }
body.app-dark .access-row-title {
  font-size: 13.5px; font-weight: 500; color: #fff; line-height: 1.4;
}
/* The date is the quietest thing in the row. It matters when it is close and not at all otherwise,
   so it should not compete with the exam name for attention. */
body.app-dark .access-row-meta {
  margin-top: 2px; font-size: 12px; color: var(--ad-text-4);
}
body.app-dark .access-row-cta { flex: 0 0 auto; }
body.app-dark .access-empty {
  margin: 0; font-size: 13px; color: var(--ad-text-3); line-height: 1.5;
}
body.app-dark .access-empty-links { margin: 6px 0 0; font-size: 12.5px; }
body.app-dark .access-empty-links a { color: var(--ad-cyan-soft); text-decoration: none; }
body.app-dark .access-empty-links a:hover { text-decoration: underline; }

@media (max-width: 620px) {
  body.app-dark .access-row-cta { width: 100%; }
}

/* ── Unfinished-session dialog ────────────────────────────────────────────────────────────────
   Three actions, and the ordering carries the meaning: continuing loses nothing, discarding
   cannot be undone, Cancel changes nothing. So continue is the primary, discard is a rose-tinted
   secondary rather than a solid danger fill - loud enough to read as destructive, quiet enough not
   to be the thing the eye lands on - and Cancel steps back to text weight.

   Explicitly NOT a btn-danger solid fill: that would make the irreversible action the brightest
   thing in the dialog, which is how the native confirm() got this wrong in the first place. */
body.app-dark .sc-actions {
  display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin-top: 4px;
}
body.app-dark .sc-danger {
  border-color: rgba(244,63,94,.34); color: #fda4af; background: rgba(244,63,94,.08);
}
body.app-dark .sc-danger:hover {
  border-color: rgba(244,63,94,.52); color: #fff; background: rgba(244,63,94,.16);
}
body.app-dark .sc-tertiary {
  border-color: transparent; background: transparent; color: var(--ad-text-4);
}
body.app-dark .sc-tertiary:hover { color: #fff; background: rgba(255,255,255,.05); }
/* The second sentence is the one that explains the choice, so it stays readable rather than
   dropping to the muted body colour the first line uses. */
body.app-dark #session-conflict-card .sc-choice { color: var(--ad-text-2); margin-top: 8px; }

/* Narrow screens: three buttons will not sit on one row, so they stack full-width in the same
   order rather than wrapping into a ragged two-plus-one. */
@media (max-width: 560px) {
  body.app-dark .sc-actions { flex-direction: column; }
  body.app-dark .sc-actions .btn { width: 100%; }
}

/* Domain rows: the name reads in full on its own line, the bar sits under it. */
body.app-dark .domain-bar-label { color: var(--ad-text-2); font-size: 13px; font-weight: 400; }
body.app-dark .domain-bar-pct {
  color: var(--ad-text-3); font-size: 12px; font-variant-numeric: tabular-nums;
}

/* ── Admin: Manage access modal ───────────────────────────────────────────────────────────────
   The current-access panel used to be a white box (#f9fafb with #111 text) inside a dark modal,
   set inline - which no stylesheet could correct, because an inline colour outranks all of them.
   It is styled by class now, and the panel states each product separately: practice entitlements
   and Learn grants are different tables and revoking the wrong one is the mistake worth designing
   against. */
body.app-dark .ma-current {
  margin-bottom: 16px; padding: 13px 14px; border-radius: 10px;
  background:
    linear-gradient(180deg, rgba(255,255,255,.035), rgba(255,255,255,.015)),
    rgba(255,255,255,.015);
  border: 1px solid var(--ad-line-soft);
}
body.app-dark .ma-current-k {
  font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--ad-text-4); margin: 0 0 6px;
}
body.app-dark .ma-current-k:not(:first-child) {
  margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--ad-line-soft);
}
body.app-dark .ma-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 7px 0;
}
body.app-dark .ma-row + .ma-row { border-top: 1px solid rgba(255,255,255,.04); }
body.app-dark .ma-row-text { min-width: 0; }
body.app-dark .ma-row-title { font-size: 13px; font-weight: 500; color: #fff; line-height: 1.4; }
body.app-dark .ma-row-meta { margin-top: 2px; font-size: 11.5px; color: var(--ad-text-4); }
body.app-dark .ma-empty { margin: 0; font-size: 12.5px; color: var(--ad-text-4); line-height: 1.5; }
body.app-dark .modal.is-manage-access {
  background:
    linear-gradient(180deg, rgba(17, 24, 39, .98), rgba(9, 13, 22, .98)),
    var(--ad-panel);
  border-color: rgba(148,163,184,.20);
}
body.app-dark .modal.is-manage-access h3 { color: var(--ad-text); }
body.app-dark .modal.is-manage-access .modal-desc {
  scrollbar-color: rgba(148,163,184,.35) transparent;
}
body.app-dark .modal.is-manage-access .modal-actions {
  background: rgba(5, 8, 14, .58);
  border-top-color: var(--ad-line);
}
body.app-dark .ma-intro { color: var(--ad-text-3); }
body.app-dark .ma-intro strong { color: var(--ad-text); }
body.app-dark .ma-field label { color: var(--ad-text-3); }
body.app-dark .ma-field .header-filter,
body.app-dark .ma-field .form-input {
  height: 38px;
  color: var(--ad-text);
  background: rgba(255,255,255,.045);
  border-color: var(--ad-line-soft);
}
body.app-dark .ma-field .header-filter:focus,
body.app-dark .ma-field .form-input:focus {
  border-color: rgba(34,211,238,.48);
  box-shadow: 0 0 0 3px rgba(34,211,238,.10);
}
body.app-dark .ma-field .form-input::placeholder { color: var(--ad-text-4); }
body.app-dark .ma-help { color: var(--ad-text-4); }
body.app-dark .ma-danger-note {
  color: #fda4af;
  background: rgba(244,63,94,.08);
  border-color: rgba(244,63,94,.28);
}

/* Two summary lines under the Learn grants, saying what the CUSTOMER'S dashboard resolves those
   grants to. .ma-note is the agreeing case and stays as quiet as .ma-empty. .ma-warn is the
   disagreeing case - grants on the table that resolve to no readable path - and it is the one thing
   in this panel an admin must not scroll past, so it takes amber and a tinted surface. Amber, not
   red: nothing is broken for the customer's money, the access is simply not landing. */
body.app-dark .ma-note {
  margin: 8px 0 0; font-size: 11.5px; color: var(--ad-text-4); line-height: 1.5;
}
body.app-dark .ma-warn {
  margin: 8px 0 0; padding: 8px 10px; border-radius: 8px;
  font-size: 12px; line-height: 1.5; color: var(--ad-amber);
  background: rgba(251,191,36,.07); border: 1px solid rgba(251,191,36,.24);
}

/* Remove is destructive but it is not the point of the panel, so it reads as a quiet outline
   rather than a filled danger button - the same reasoning as the unfinished-session dialog. */
body.app-dark .ma-remove {
  flex: 0 0 auto;
  background: transparent; border: 1px solid var(--ad-line-soft); color: var(--ad-text-3);
}
body.app-dark .ma-remove:hover {
  border-color: rgba(244,63,94,.42); color: #fda4af; background: rgba(244,63,94,.08);
}

/* Product picker. Two products, chosen before anything else, so the two field sets are mutually
   exclusive by construction rather than by remembering to look at a dropdown. */
body.app-dark .ma-seg {
  display: flex; gap: 4px; margin-bottom: 14px; padding: 3px;
  background: rgba(255,255,255,.03); border: 1px solid var(--ad-line-soft); border-radius: 10px;
}
body.app-dark .ma-seg-btn {
  flex: 1 1 0; padding: 7px 10px; border: 0; border-radius: 7px; cursor: pointer;
  background: transparent; color: var(--ad-text-3);
  font: inherit; font-size: 12.5px; font-weight: 500;
}
body.app-dark .ma-seg-btn:hover { color: #fff; }
body.app-dark .ma-seg-btn.is-on {
  background: rgba(34,211,238,.12); border: 1px solid rgba(34,211,238,.30); color: #fff;
}

/* ---- 54. Practice screen: reading contrast, calmer feedback, one aligned rail ----------------
   The answer-and-explain loop is where a practice session is actually spent, so this pass is about
   reading it, not decorating it. Four colours here were never converted at all - #666 on the
   distractor notes, #2e7d32 on the correct-answer note, #6b7280 on the upsell and #283593 on the
   step label - because a property that no dark rule states keeps whatever the LIGHT theme gave it.
   Measured on the composed background (translucent text over a translucent glass panel over the
   page), the notes came in at 3.2:1 and the "why other options are incorrect" heading at 2.7:1.

   Sections 20 and 23 already touch several of these selectors. This section is last in the file, so
   where a selector repeats, the value here is the one that ships. */

/* Revealed options. The saturated fills, the .55 borders and the two solid letter chips made the
   list the loudest thing on the screen at exactly the moment attention should be moving down to the
   explanation. Same states, half the weight: the tint still names correct and wrong, and the tick
   and cross still carry it for anyone who cannot use the colour. */
body.app-dark .option-item.correct-reveal {
  background: rgba(52,211,153,.10) !important;
  border-color: rgba(52,211,153,.40) !important;
  color: #d1fae5 !important;
}
body.app-dark .option-item.wrong-reveal {
  background: rgba(244,63,94,.10) !important;
  border-color: rgba(244,63,94,.40) !important;
  color: #ffe4e6 !important;
}
body.app-dark .option-item.correct-reveal .option-key {
  background: rgba(52,211,153,.20); border-color: rgba(52,211,153,.45); color: #6ee7b7;
}
body.app-dark .option-item.wrong-reveal .option-key {
  background: rgba(244,63,94,.18); border-color: rgba(244,63,94,.42); color: #fda4af;
}
/* The options you did not pick are still options - they have to be scannable while you read the
   explanation that rules them out. Section 23 raised the group opacity from .45 to .72, but opacity
   fades the row toward the page ground and never shows up in the computed colour: 70% white at .72
   is really about 50%, which measured 5.3:1 and read as disabled. Opacity off, muted colour stated
   outright, so the value is the value. */
body.app-dark .option-item.neutral-reveal { opacity: 1 !important; }
body.app-dark .option-item.neutral-reveal {
  background: rgba(255,255,255,.022); border-color: rgba(255,255,255,.07);
  color: rgba(255,255,255,.62);
}
body.app-dark .option-item.neutral-reveal .option-key {
  background: rgba(255,255,255,.05); border-color: var(--ad-line-soft); color: rgba(255,255,255,.50);
}

/* The letter chip was sized by its own glyph - min-width:28px plus 7px of padding measured 34px on
   "A)" and 32.5px on "B)", so the option text started at a different x on every row - and its
   padding put the chip's centre 3.2px below the centre of the first line of text it labels. A stated
   box fixes the ragged left edge and the alignment in one go.

   The width is a floor, not a fixed value: every lettered option hits the 30px minimum and so ends
   up identical, while a True/False question - whose "key" is the word True or False, not a letter -
   can still grow rather than have its label clipped by the box. */
body.app-dark .option-key {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 30px; height: 24px; padding: 0 6px; margin-top: 0;
  font-size: 12.5px; font-weight: 600; line-height: 1;
}

/* "Your answer" only has to say which row you chose. It does not need a border and a fill and bold
   caps to do it, sitting next to the tick that carries the actual verdict. */
body.app-dark .your-answer-chip {
  background: rgba(255,255,255,.07); border: 0; color: rgba(255,255,255,.66);
}

/* Feedback card. The banner was a full-bleed saturated slab with a 32px badge on a 16px bold line -
   an alert, restating what the row tint above it has already said. It stays coloured, because it is
   the verdict, but it becomes a header on the card rather than a second alarm. */
body.app-dark .practice-feedback .feedback-banner {
  padding: 11px 18px; font-size: 14.5px; letter-spacing: 0;
  border-bottom: 1px solid var(--ad-line);
}
body.app-dark .practice-feedback.correct .feedback-banner { background: rgba(52,211,153,.10); color: #6ee7b7; }
body.app-dark .practice-feedback.incorrect .feedback-banner { background: rgba(244,63,94,.10); color: #fda4af; }
body.app-dark .practice-feedback .feedback-banner-icon {
  width: 22px; height: 22px; font-size: 12.5px; background: rgba(255,255,255,.09);
}
body.app-dark .practice-feedback .feedback-body { padding: 18px 22px 20px; }

/* "Correct answer: A) Elasticity" is a correction, not an incident. Boxed and bordered it read as a
   third alert stacked under the banner and the green row; as a ruled line it reads as the sentence
   it is. Emerald stays on the rule so it still agrees with the correct option above. */
body.app-dark .correct-hint,
body.app-dark .practice-feedback.correct .correct-hint,
body.app-dark .practice-feedback.incorrect .correct-hint {
  background: transparent; border: 0; border-left: 2px solid rgba(52,211,153,.45);
  border-radius: 0; padding: 1px 0 1px 13px; margin-bottom: 18px;
  color: rgba(255,255,255,.84); font-weight: 500; font-size: 14.5px;
}

/* Long-form reading. */
body.app-dark .practice-feedback .explanation-text,
body.app-dark .review-explanation .explanation-text {
  color: rgba(255,255,255,.82); line-height: 1.72;
}
body.app-dark .explanation-label { color: var(--ad-cyan-soft); letter-spacing: .09em; }

/* The distractor notes and the correct-answer note: the four unconverted colours, plus a separator
   rule at rgba(0,0,0,.1) that is invisible on a near-black card, which is why the explanation and
   the notes below it read as one undifferentiated block. */
body.app-dark .wrong-notes { border-top-color: var(--ad-line); }
body.app-dark .wrong-notes-header { color: rgba(255,255,255,.55); letter-spacing: .09em; }
body.app-dark .wrong-note-item { color: rgba(255,255,255,.72); }
body.app-dark .wrong-note-icon { color: rgba(244,63,94,.80); }
body.app-dark .wrong-note-key { color: #fda4af; }
body.app-dark .correct-note-item .wrong-note-key { color: #6ee7b7; }
body.app-dark .correct-note-item .correct-note-icon { color: var(--ad-emerald); }
body.app-dark .correct-notes { margin-bottom: 12px; }
body.app-dark .expl-step-label { color: var(--ad-cyan-soft); }

/* Insight rail. The card labels were pure white at 10.5px/800 - brighter than the values they
   label - while everything a reader actually reads sat at 45% white, and the upsell copy at #6b7280
   never left the light theme at all (3.8:1). Labels down, reading text up. */
body.app-dark .pp-title { color: rgba(255,255,255,.62); }
body.app-dark .pp-sub { color: rgba(255,255,255,.55); }
body.app-dark .pp-row .pp-k { color: rgba(255,255,255,.62); }
body.app-dark .pp-lead { color: rgba(255,255,255,.70); }
body.app-dark .pp-row svg { color: rgba(255,255,255,.42); }
body.app-dark .pp-upsell { color: rgba(255,255,255,.62); }
/* Before the fourth answer this card has nothing to report, so it recedes to a hairline note
   instead of holding a full panel's worth of weight for a sentence about what will appear later.
   The class comes from renderPracticePanel - CSS cannot tell an empty card from a full one. */
body.app-dark .pp-insight.pp-insight-empty {
  background: transparent; border-color: var(--ad-line-soft); box-shadow: none;
}
body.app-dark .pp-insight-empty .pp-lead { color: rgba(255,255,255,.52); }

/* ── Question navigator, dark. Every class the light theme introduces gets a value here; a property
   no dark rule states keeps whatever light gave it, which is how this screen ended up with #666 on
   near-black four separate times. ── */
body.app-dark .pp-nav-cell {
  background: rgba(255,255,255,.045); border: 1px solid var(--ad-line); color: rgba(255,255,255,.74);
}
body.app-dark button.pp-nav-cell:hover {
  background: rgba(255,255,255,.10); border-color: rgba(255,255,255,.22); color: #fff;
}
body.app-dark .pp-nav-cell:focus-visible { outline: 2px solid var(--ad-cyan); outline-offset: 2px; }
/* Answered with no verdict disclosed (a timed sitting). */
body.app-dark .pp-nav-cell.is-answered {
  background: rgba(34,211,238,.10); border-color: rgba(34,211,238,.28); color: #fff;
}
/* Practice verdicts. Low-alpha tints: a two-row grid of saturated red and green is the loudest
   thing on a near-black page, and the number inside still has to be the readable part. */
body.app-dark .pp-nav-cell.is-correct {
  background: rgba(52,211,153,.12); border-color: rgba(52,211,153,.30); color: #fff;
}
body.app-dark .pp-nav-cell.is-wrong {
  background: rgba(217,72,95,.14); border-color: rgba(217,72,95,.34); color: #fff;
}
body.app-dark .pp-swatch.is-correct {
  background: rgba(52,211,153,.14); border-color: rgba(52,211,153,.30); color: var(--ad-emerald);
}
body.app-dark .pp-swatch.is-wrong {
  background: rgba(217,72,95,.16); border-color: rgba(217,72,95,.34); color: #f08ea0;
}
body.app-dark .ppn-right { color: var(--ad-emerald); }
body.app-dark .ppn-wrong { color: #f08ea0; }
/* The correct-answer letter sits inside the verdict banner and inherits its colour, which is the
   rose the banner already uses - stated so it cannot fall back to a light-theme value. */
body.app-dark .practice-feedback.incorrect .feedback-key { color: inherit; }
body.app-dark .pp-nav-cell.is-current {
  border-color: var(--ad-cyan); box-shadow: 0 0 0 1px var(--ad-cyan); color: var(--ad-cyan-soft);
}
body.app-dark .pp-nav-cell.is-locked {
  background: transparent; border-style: dashed; border-color: var(--ad-line-soft);
  color: rgba(255,255,255,.46);
}
body.app-dark .pp-nav-cell.is-saved::after { background: var(--ad-amber); }
body.app-dark .pp-nav-legend { color: rgba(255,255,255,.58); }
body.app-dark .pp-swatch { background: rgba(255,255,255,.045); border-color: var(--ad-line); }
body.app-dark .pp-swatch.is-answered { background: rgba(34,211,238,.14); border-color: rgba(34,211,238,.28); }
body.app-dark .pp-nav-cell.is-current { box-shadow: 0 0 0 2px var(--ad-cyan); }
/* The grid's scrollbar. The default overlay bar is nearly invisible on this ground, and this one
   has to be found: it is the only thing telling a reader there are more questions below. */
body.app-dark .pp-nav-grid { scrollbar-color: rgba(255,255,255,.28) transparent; }
body.app-dark .pp-nav-grid::-webkit-scrollbar-thumb {
  background: rgba(255,255,255,.28); background-clip: content-box;
}
body.app-dark .pp-nav-grid::-webkit-scrollbar-thumb:hover {
  background: rgba(255,255,255,.44); background-clip: content-box;
}
@media (max-width: 1100px) {
  body.app-dark .pp-nav-disclosure > summary:focus-visible { outline-color: var(--ad-cyan); }
}
body.app-dark .pp-swatch.is-saved { background: var(--ad-amber); border-color: var(--ad-amber); }
body.app-dark .pp-sample { color: rgba(255,255,255,.58); }
body.app-dark .pp-caveat { color: rgba(255,255,255,.60); }
body.app-dark .pp-nav-count, body.app-dark .pp-nav-subcount { color: rgba(255,255,255,.66); }

body.app-dark .ppn-save { color: var(--ad-amber); }
body.app-dark .pp-swatch.is-answered { color: var(--ad-emerald); }
body.app-dark .pp-swatch.is-saved {
  background: rgba(251,191,36,.10); border-color: rgba(251,191,36,.28); color: var(--ad-amber);
}
/* The Save control beside the question. */
body.app-dark .q-save { background: transparent; border-color: var(--ad-line); color: rgba(255,255,255,.72); }
body.app-dark .q-save:hover { background: rgba(255,255,255,.08); border-color: rgba(255,255,255,.22); color: #fff; }
body.app-dark .q-save:focus-visible { outline-color: var(--ad-cyan); }
body.app-dark .q-save.is-saved {
  background: rgba(251,191,36,.10); border-color: rgba(251,191,36,.30); color: var(--ad-amber);
}
/* "Why other options are incorrect" as a disclosure. */
body.app-dark .wrong-notes-collapsible { border-top-color: rgba(255,255,255,.10); }
body.app-dark .wrong-notes-collapsible > summary:hover { color: #fff; }
body.app-dark .wrong-notes-collapsible > summary:focus-visible { outline-color: var(--ad-cyan); }
/* Finish Practice, separated from the movement controls beside it. */
body.app-dark .bottom-nav-group #btn-end { border-left-color: rgba(255,255,255,.12); }

/* ── Exam setup: the two stat groups, the domain table and its disclosure ──────────────────────
   Every class the light theme introduces above needs a value here. */
/* The one number on this card that is CertGrid's rather than the exam's. */
body.app-dark .welcome-stat.is-cg {
  background: rgba(34,211,238,.05); border-color: rgba(34,211,238,.16);
}
body.app-dark .welcome-stat.is-cg .welcome-stat-val { color: var(--ad-cyan-soft); }
body.app-dark .welcome-domain-head { color: rgba(255,255,255,.58); border-bottom-color: var(--ad-line); }
body.app-dark .welcome-domain-qs { color: rgba(255,255,255,.86); }
body.app-dark .welcome-domain-pct { color: rgba(255,255,255,.86); }
body.app-dark .welcome-domains-all { border: 1px solid var(--ad-line); }
body.app-dark .welcome-domains-all > summary { color: var(--ad-cyan-soft); }
body.app-dark .wda-count { color: rgba(255,255,255,.58); }
body.app-dark .welcome-domains-all > summary:hover { background: rgba(255,255,255,.03); }
@media (max-width: 760px) {
  body.app-dark .welcome-domain-qs::after { color: rgba(255,255,255,.58); }
}

/* Fixed action bar. It floated: a hairline at 8% white over a page of the same colour gave it no
   relationship to the content it scrolls over. A soft upward shadow and a brighter rule land it as
   a layer above the page. */
body.app-dark .bottom-nav {
  border-top-color: rgba(255,255,255,.10);
  box-shadow: 0 -14px 30px -20px rgba(0,0,0,.9);
}
/* Secondary actions read as disabled at 70% white on a barely-there fill, and Save & Exit is a
   perfectly ordinary thing to want. */
body.app-dark .bottom-nav .btn-secondary {
  background: rgba(255,255,255,.045); border-color: rgba(255,255,255,.14);
  color: rgba(255,255,255,.82);
}
body.app-dark .bottom-nav .btn-secondary:hover {
  background: rgba(255,255,255,.10); border-color: rgba(255,255,255,.22); color: #fff;
}
/* Finish is the end of the session, so it stays in the danger register - but as an outline, not a
   filled rose block that outweighed the primary Next button beside it. */
body.app-dark .bottom-nav .btn-danger {
  background: transparent; border: 1px solid rgba(244,63,94,.38); color: #fda4af;
}
body.app-dark .bottom-nav .btn-danger:hover {
  background: rgba(244,63,94,.14); border-color: rgba(244,63,94,.58); color: #fff;
}
/* Next is the button pressed twenty times a session; give it the width to look like it. */
body.app-dark .bottom-nav .btn-primary { min-width: 104px; }

/* The per-type hints above the options. .multi-hint sat at 45% white, which is label contrast for a
   line that tells you how many answers to pick, and the True/False and row-select variants were
   inline style= in exam.js until now - a cream or near-white pill with dark text, on this page.
   See .multi-hint-tf / .multi-hint-rows in styles.css. */
body.app-dark .multi-hint {
  color: rgba(255,255,255,.62); background: rgba(255,255,255,.05); border-color: var(--ad-line);
}
body.app-dark .multi-hint-tf {
  background: rgba(251,191,36,.12); border-color: rgba(251,191,36,.24); color: #fcd34d;
}
body.app-dark .multi-hint-rows {
  background: rgba(99,102,241,.14); border-color: rgba(99,102,241,.26); color: #c7d2fe;
}

/* ---- 55. Profile: one account summary, readable labels, honest disabled states ---------------
   Measured before touching anything (composed contrast, over the glass panel over the page):
   eight surfaces on this page sat at 2.7:1 - the hero email, the access line, the plan
   description, the Source/Expires/Time-left labels, the Member-since/Last-login labels, the
   ACCOUNT HEALTH label, the form hints, and both disabled commit buttons. 30% white is a value for
   a decorative rule, not for a sentence someone is expected to read, and on a disabled button it
   is what makes the control look broken rather than not-yet-armed. */

/* -- Form fields: one geometry ---------------------------------------------------------------
   Display Name rendered as a 36px PILL while every password field below it was a 42px rounded
   rect. Cause: `body.app-dark select, input[type="text"], input[type="search"]` (section 9) sets
   radius 999px and height 36px for the topbar/admin search controls, and at (0,2,2) it outranks
   `body.app-dark .form-input` at (0,2,1) - so it captured every text input that happens to be a
   real form field too. This restates the form-field geometry at matching specificity, later in the
   file, keyed on the class that means "this is a form field". Search pills are untouched: they
   carry .header-filter / .exam-search and no .form-input.
   That rule also sets font-size: 13px against .form-input's 14px, which was the remaining 2px of
   the height difference - so the type size is restated here too, not just the box. */
body.app-dark input.form-input {
  height: auto; border-radius: 10px; padding: 11px 16px; font-size: 14px;
}
/* Read-only stays dashed - that is the signal - but the value inside it is information, so it is
   legible rather than 45% white. */
body.app-dark input.form-input:disabled,
body.app-dark input.form-input[readonly] {
  color: rgba(255,255,255,.62);
  border-color: rgba(255,255,255,.09);
}
body.app-dark .profile-page .form-group label { color: rgba(255,255,255,.62); }
body.app-dark .profile-page .form-hint { color: rgba(255,255,255,.52); }
body.app-dark .profile-page .form-group { margin-bottom: 16px; }

/* -- Disabled commit buttons: not-yet-armed, not broken -------------------------------------- */
body.app-dark button.pf-commit:disabled {
  background: rgba(255,255,255,.045); border: 1px solid rgba(255,255,255,.12);
  color: rgba(255,255,255,.55);
}

/* -- The summary card is ONE card ------------------------------------------------------------
   Section 15 gives .acct-plan the full glass panel treatment - background, border, shadow, 16px
   radius - inside .acct-summary, which already has all four. A card drawn inside a card is what
   made the top of this page read as a stack of boxes rather than one account summary; the same
   applied to each date row in the right-hand rail. Both lose their frames and keep their content:
   the plan sits on the summary surface, and the single hairline between the two halves is the only
   division the block needs. */
body.app-dark .acct-plan {
  background: none; border: 0; box-shadow: none; border-radius: 0;
}
body.app-dark .acct-stat {
  background: none; border: 0; border-radius: 0; padding: 0;
}
body.app-dark .acct-stats {
  gap: 11px; border-left: 1px solid var(--ad-line); padding-left: 22px;
}
/* Account health was the third framed panel inside this card - section 15 hands it the same glass
   background, border, shadow and 16px radius that .acct-summary itself already has. Unframed, it
   becomes the card's last row, divided by a hairline like the rows above it. The base rule's
   #eef2f7 top border and #fbfcfe fill were never converted, so both are stated here.
   22px horizontal throughout: that is the value the hero and body already used, and the per-exam
   access list carries it as an inline style, so anything else here would misalign that list. */
body.app-dark .acct-health {
  background: none; border: 0; border-top: 1px solid var(--ad-line);
  border-radius: 0; box-shadow: none;
  padding: 14px 22px; gap: 10px 16px;
}
/* The two halves get equal air from the hairline rather than the plan half being padded and the
   dates half crammed against it. */
body.app-dark .acct-summary-body { padding: 15px 20px; gap: 18px; }
body.app-dark .acct-summary-hero { padding: 15px 20px 14px; }

/* -- Reading contrast across the summary ----------------------------------------------------- */
body.app-dark .profile-hero-email { color: rgba(255,255,255,.62); }
body.app-dark .profile-hero-access { color: rgba(255,255,255,.55); }
body.app-dark .profile-access-desc { color: rgba(255,255,255,.62); }
body.app-dark .profile-access-meta-label { color: rgba(255,255,255,.52); }
body.app-dark .acct-stat-label { color: rgba(255,255,255,.58); }
body.app-dark .acct-stat-label svg { color: rgba(255,255,255,.40); }
body.app-dark .acct-health-label { color: rgba(255,255,255,.55); }
body.app-dark .danger-desc { color: rgba(255,255,255,.62); }
body.app-dark .health-pill.off { color: rgba(255,255,255,.62); }

/* -- Security card: the status rows and the password form are two acts -----------------------
   "Change password" is a heading with no top margin, so it sat as close to the 2FA row above it as
   its own fields sit below it. A rule and real space separate the two halves. */
body.app-dark .profile-page .pf-subheading {
  margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--ad-line-soft);
  color: #fff; font-size: 13.5px;
}
body.app-dark .profile-page .security-row { padding: 12px 0; }
/* Enable 2FA was cyan text on a near-invisible fill - a link pretending to be a button. It is the
   one action offered by this half of the card, so it gets a secondary button's surface. */
body.app-dark .mfa-enable-btn {
  background: rgba(255,255,255,.07); border: 1px solid rgba(255,255,255,.16);
  color: var(--ad-cyan-soft);
}
body.app-dark .mfa-enable-btn:hover {
  background: rgba(34,211,238,.14); border-color: rgba(34,211,238,.42); color: #fff;
}
body.app-dark #mfa-badge { color: rgba(255,255,255,.62); }

/* -- Narrow widths ---------------------------------------------------------------------------
   styles.css turns the dates rail's left divider into a top divider at 860px, but that rule is
   (0,1,0) and every dark rule here is (0,2,1) and unconditional, so the desktop divider won
   on mobile and the page carried both. Its colour was never converted either - #eef2f7 is a
   near-white hairline, and it was the brightest line on the phone layout. Both restated in dark
   values, inside the same breakpoint, so the two agree instead of racing. */
@media (max-width: 860px) {
  body.app-dark .acct-stats {
    border-left: 0; padding-left: 0;
    border-top: 1px solid var(--ad-line); padding-top: 15px;
  }
  body.app-dark .acct-summary-body { padding: 18px 22px; gap: 18px; }
  body.app-dark .acct-summary-hero { padding: 18px 22px 16px; }
  body.app-dark .acct-health { padding: 14px 22px; }
}
/* Source / Expires / Time left is a single row with hairline separators between the items. Once it
   wraps, those separators land at the START of a line, so every item after the first read as an
   indented fragment hanging off a stray vertical rule. Below 640px it stops pretending to be a row
   and becomes a stack. */
@media (max-width: 640px) {
  body.app-dark .profile-access-meta {
    display: grid; grid-template-columns: 1fr; gap: 5px; margin-top: 14px;
  }
  body.app-dark .profile-access-meta-item { padding: 0; }
  body.app-dark .profile-access-meta-item + .profile-access-meta-item {
    padding-left: 0; border-left: 0;
  }
}

/* ---- 56. Exam setup: one step up on every muted value ---------------------------------------
   Measured on the composed background before changing anything. Three surfaces were below 3.2:1 -
   the stat-tile labels (2.71), the section labels above Choose Mode and Question Pool (2.70), and
   the domain counts (3.11) - and a band of others sat at 4.3-4.5, which passes AA on paper and
   still reads as switched off next to white values at 17:1. Everything here moves ONE step, which
   is the whole intent: muted text stays muted, just legible.

   Not changed, having been measured: the exam description is already 7.45:1, and all three mode
   cards' SELECTED treatment is byte-identical (diffed every paint property across the three -
   background, border, ring, icon fill and colour - after waiting out the .18s transition; reading
   before it settles reports the pre-transition value and invents a difference that is not there). */

/* Stat tiles: 30% white on a 9.5px label under a 21px/800 value. */
body.app-dark .welcome-stat-lbl { color: rgba(255,255,255,.56); }
/* "CHOOSE MODE" - the only signpost left on the card. */
body.app-dark .welcome-section-label { color: rgba(255,255,255,.55); }
/* Mode descriptions are how someone picks a mode, so they are read, not skimmed - and since the
   floating per-mode helper line was removed they are the ONLY description of each mode, which is
   why they moved up from .58. */
body.app-dark .mode-card-desc { color: rgba(255,255,255,.70); }
/* The free-plan sentence under Practice Length. Overrides only this member of the section-21
   group, which also covers .welcome-mode-lead / .welcome-mode-hint / #welcome-trust. */
body.app-dark .welcome-field-hint { color: rgba(255,255,255,.58); }
/* Counts sat lower than everything else on the page at .34 - and they are the numbers the
   domain list exists to show. The bars are deliberately left alone. */
body.app-dark .welcome-domain-count { color: rgba(255,255,255,.60); }
/* Back to Dashboard is the escape hatch, not a disabled control. */
body.app-dark .welcome-actions .btn-secondary { color: rgba(255,255,255,.70); }
/* The rule above the actions is a divider, not a structural edge. */
body.app-dark .welcome-actions { border-top-color: rgba(255,255,255,.06); }

/* Unselected mode cards: the hover was a 2% background lift, which on this panel is close to
   nothing. Still quiet, but now it answers the pointer. */
body.app-dark .mode-card:hover {
  background: rgba(255,255,255,.07); border-color: rgba(255,255,255,.20);
}
/* And a focus ring that can actually appear: the radio inside each card was `display: none`, which
   takes it out of the tab order altogether - the mode could not be chosen from the keyboard and no
   focus style could ever show. It is visually hidden rather than removed now (see styles.css), so
   this is the keyboard counterpart of the selected ring. */
body.app-dark .mode-card:focus-within {
  border-color: rgba(34,211,238,.55);
  box-shadow: 0 0 0 3px rgba(34,211,238,.18);
}

/* RECOMMENDED sat in emerald at 700 beside a 14px/700 title, so the badge read as loud as the mode
   it qualifies. Neutral and lighter.
   The vertical alignment is fixed in styles.css, since the misalignment came from the shared
   `vertical-align: middle` and applied to both themes. */
body.app-dark .mode-card-rec {
  background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.14);
  color: rgba(255,255,255,.66); font-weight: 600; letter-spacing: .06em;
}

/* ---- 57. Exam setup, micro-pass -------------------------------------------------------------
   Section 56 already lifted the muted values into a 6.1-7.1 band; nothing here brightens further.
   Three things only: a smaller/softer selected marker, one point of type size on the stat labels,
   and the Back-to-Dashboard hover stated rather than inherited. */

/* The marker was a full-saturation #22d3ee disc with a dark glyph - the brightest object on a page
   whose loudest legitimate element is the primary button. The card's cyan fill, cyan border and
   3px ring already say "selected" three times over, so the marker can be the quietest of the four
   and still be unmistakable. Size comes from styles.css (18 -> 16px); this is the saturation. */
body.app-dark .mode-card.selected::after {
  background: rgba(34,211,238,.80); color: #04121a;
}

/* Stat-tile labels: 9.5px is small enough that legibility is as much about size as contrast, and
   contrast was already dealt with. One point up, and a touch less tracking - .13em on 9.5px caps
   was pushing the letters apart faster than it was helping them read. */
/* 11px, matching the light theme. These labels are what tell you which number you are looking
   at, and 10px uppercase at .11em tracking reads as a smudge under the value at normal zoom. */
body.app-dark .welcome-stat-lbl { font-size: 11px; letter-spacing: .08em; }

/* Back to Dashboard: the default is .70 from section 56. The hover was inheriting a 4% wash and a
   .18 border from an earlier section, which next to a .70 label read as barely a state change.
   Both states stated together here so they cannot drift apart again. */
body.app-dark .welcome-actions .btn-secondary:hover {
  background: rgba(255,255,255,.08); border-color: rgba(255,255,255,.24); color: #fff;
}

/* The rule between "password last changed" and the form that changes it (see #password-form in
   styles.css). The light value #eef2f7 is a near-white hairline on this panel. */
body.app-dark #password-form { border-top-color: var(--ad-line-soft); }

/* Attempts-strip pass/fail, dark half. These were inline style= in exam.js (#2e7d32 / #e65100),
   which no stylesheet can override - so a dark green and a burnt orange sat on a near-black strip.
   Same defect class as the multi-hint pills in section 54.
   Qualified with the strip and `strong` the same way as the light half: `body.app-dark
   .welcome-progress-strip strong` is (0,2,1) and sets #fff, so an unqualified
   `body.app-dark .wps-good` at (0,2,0) lost to it and the colours never applied. */
body.app-dark .welcome-progress-strip strong.wps-good { color: #6ee7b7; }
body.app-dark .welcome-progress-strip strong.wps-warn { color: #fcd34d; }

/* ---- 58. Drag-and-drop: the workspace is the task -------------------------------------------
   THE CAUSE of "it looks like a disabled widget floating in space": section 21 sets
   `.dd-empty, .dd-text { color: var(--ad-text-4) }` - and `.dd-text` is the LABEL ON EVERY
   DRAGGABLE CARD. So every option's text rendered at 30% white, about 2.7:1, while the card around
   it was at 70%. The cards were not dim; their words were. `.dd-empty` genuinely is a placeholder
   and stays muted; the two were sharing a rule and should not have been.

   Everything else here follows from that: once the words are readable, the numbers, the grip and
   the column labels are the next dimmest things, and the drop zone needs to look like it wants
   something. Sizing is in styles.css and applies to both themes. */

/* The label on a draggable card is content, not chrome. */
body.app-dark .dd-text { color: var(--ad-text); }
/* A genuinely empty slot: readable, but clearly not yet filled. */
body.app-dark .dd-slot-empty .dd-text { color: rgba(255,255,255,.52); }
body.app-dark .dd-empty { color: rgba(255,255,255,.52); }
/* OPTIONS / ANSWER AREA - the only signposts on the widget. */
body.app-dark .dd-col-label { color: rgba(255,255,255,.58); }
body.app-dark .dd-count { color: rgba(255,255,255,.62); }

/* Cards should look liftable. A 6% fill on a 3% zone is almost no separation, so the card comes up
   and the zone stays where it is. */
body.app-dark .dd-item {
  background: rgba(255,255,255,.075); border: 1px solid rgba(255,255,255,.13);
  color: var(--ad-text);
}
body.app-dark .dd-item[draggable="true"]:hover {
  background: rgba(255,255,255,.13); border-color: rgba(255,255,255,.24); color: #fff;
}
/* The grip is the affordance that says "pick me up", and it was the dimmest mark on the card. */
body.app-dark .order-grip { color: rgba(255,255,255,.48); }
body.app-dark .dd-item[draggable="true"]:hover .order-grip { color: rgba(255,255,255,.75); }
/* Keyboard parity with the pointer: these cards are reachable by tab in the order type. */
body.app-dark .dd-item[draggable="true"]:focus-visible {
  outline: none; border-color: rgba(34,211,238,.55); box-shadow: 0 0 0 3px rgba(34,211,238,.18);
}
/* Being dragged should be unmistakable, not merely faded. */
body.app-dark .dd-item.dragging {
  background: rgba(34,211,238,.10); border-color: rgba(34,211,238,.40); border-style: dashed;
  color: rgba(255,255,255,.70);
}

/* Numbered slots are how the answer is read back, so the numbers carry weight. */
body.app-dark .dd-num {
  background: rgba(255,255,255,.16); color: #fff; font-weight: 700;
}
body.app-dark .dd-item.placed .dd-num { background: var(--ad-cyan); color: #05141a; }
body.app-dark .dd-slot-empty .dd-num { background: rgba(255,255,255,.09); color: rgba(255,255,255,.55); }

/* The two zones should not look identical: one holds things, the other wants them. The answer zone
   keeps a dashed edge because it is a drop target; the source zone becomes solid because it is not. */
body.app-dark .dd-source {
  background: rgba(255,255,255,.025); border-style: solid; border-color: var(--ad-line);
}
body.app-dark .dd-answer {
  background: rgba(34,211,238,.04); border-style: dashed; border-color: rgba(34,211,238,.24);
}
body.app-dark .dd-zone.dd-zone-over {
  background: rgba(34,211,238,.13); border-color: rgba(34,211,238,.55); border-style: solid;
  box-shadow: inset 0 0 0 2px rgba(34,211,238,.16);
}
/* Placed and revealed states keep their existing meaning; the text just stops being 30% white. */
body.app-dark .dd-item.correct .dd-text { color: #a7f3d0; }
body.app-dark .dd-item.incorrect .dd-text { color: #fecdd3; }
body.app-dark .dd-item.placed .dd-text { color: #fff; }

/* ---- 59. True/False: DO NOT give it a two-column layout -------------------------------------
   A 2-up grid capped at 600px was built here and REVERTED at the user's request the same day. It
   measured correctly - equal widths, left-aligned with the question, stacking on mobile - and was
   still not wanted: True/False should read as normal full-width answer rows, identical to
   single-choice, so the answer pattern does not change shape between question types.
   `.options-list-tf` and its CSS are gone rather than left as a dead hook. True/False is also kept
   out of `.options-list-compact`, so its rows keep the height they had before either change. */
body.app-dark .multi-hint-tf { margin-bottom: 12px; }

/* ---- 60. Yes/No matrix: an anchored block, not a strip near the top ------------------------
   The Yes/No column headers were at 30% white, so the two things the reader has to aim at were the
   faintest marks on the widget. Row height and column width are in styles.css (both themes); this
   is the contrast and the row feedback. The Confirm button attaches via .practice-confirm-row,
   which is shared with the drag/drop and multi types. */
/* The Yes/No headers are `<th class="yn-col">`, already covered at 62% by
   `.statement-table thead th` above - measured, after a dark rule for the non-existent
   .yn-col-header class turned out to match nothing. Nothing to add for them. */
body.app-dark .stmt-text-col { color: var(--ad-text); }
/* Which row am I on? A matrix of similar sentences needs that more than a list does. */
body.app-dark .statement-table tbody tr:hover td { background: rgba(255,255,255,.045); }
body.app-dark .statement-row td { border-bottom-color: rgba(255,255,255,.07); }
body.app-dark .yn-circle { border-color: rgba(255,255,255,.28); }
body.app-dark .yn-radio:hover .yn-circle { border-color: rgba(34,211,238,.55); }
body.app-dark .yn-radio input[type="radio"]:focus-visible + .yn-circle {
  outline: none; box-shadow: 0 0 0 3px rgba(34,211,238,.22);
}

/* ---- 61. Scenario tables: data to inspect, not a panel to skim ------------------------------
   The embedded exam table read flat. It keeps its containment - a table needs an edge - but the
   edge and fill come down so it sits inside the question block instead of floating as a second
   card, while the CONTENT comes up. Header text was 62% white shared with the statement table;
   these headers label columns a reader has to aim at, so they get their own value.
   Sizing, tabular figures and the scroll behaviour are in styles.css and apply to both themes. */
body.app-dark .scenario-table {
  background: rgba(255,255,255,.022); border-color: rgba(255,255,255,.10);
}
body.app-dark .scenario-table thead th {
  background: rgba(255,255,255,.05); color: rgba(255,255,255,.72);
  border-bottom: 1px solid rgba(255,255,255,.12);
}
body.app-dark .scenario-table tbody td { color: rgba(255,255,255,.88); }
/* Row separators: present enough to follow a row across five columns, quiet enough not to grid. */
body.app-dark .scenario-table tbody td { border-top-color: rgba(255,255,255,.09); }
/* The technical columns are the ones being compared, so they read at full strength. */
body.app-dark .scenario-table .st-tech { color: #fff; }
body.app-dark .scenario-table tbody tr:hover td { background: rgba(255,255,255,.055); }

/* ---- 62. Results screen: the next action is the point ----------------------------------------
   The score says what happened; "What to do next" says how to recover, and it was the quietest
   panel on the page with a text link for its one recommendation. It now carries an accent rail and
   a real button. Also here: a practice attempt that missed the mark reads amber rather than red -
   practice is coaching, and borrowing the timed-exam failure colour overstated it. */
body.app-dark .rres-eyebrow { color: rgba(255,255,255,.62); }
body.app-dark .rres-head.is-practice.is-fail .rres-verdict { color: #fcd34d; }
/* The rail and the scale fill have to be overridden here too: `body.app-dark .rres-head.is-fail`
   is (0,3,2) and outranks the light theme's (0,3,1) `.is-practice.is-fail`, so without these the
   verdict went amber while the rail and the bar stayed exam-failure red. */
body.app-dark .rres-head.is-practice.is-fail::before { background: linear-gradient(90deg, #f59e0b, #fbbf24); }
body.app-dark .rres-head.is-practice.is-fail .rsnap-fill { background: linear-gradient(90deg, rgba(245,158,11,.55), #fbbf24); }
/* The pass-mark caption on the 0-1000 scale - the number the whole panel is measured against. */
body.app-dark .rres-head .rsnap-cap, body.app-dark .rres-scale .rsnap-cap { color: rgba(255,255,255,.68); }

/* The metric-row footnote, attached to the row it explains. */
body.app-dark .rres-metrics-note {
  background: rgba(255,255,255,.03); border-color: var(--ad-line); color: rgba(255,255,255,.62);
}

/* A 0% domain: the track carries the reading, since there is no fill to see. */
body.app-dark .domain-bar.is-zero { box-shadow: inset 0 0 0 1px rgba(255,255,255,.10); }
body.app-dark .domain-bar.is-zero::after { background: rgba(255,255,255,.28); }
/* Row scanability across four columns without lighting the table up. */
body.app-dark .rres-table tbody tr:hover td { background: rgba(255,255,255,.04); }

/* What to do next, as the action area. A left accent rail marks it as the recommendation rather
   than a fourth report panel, and the CTA is a button in the light theme's accent teal's dark
   sibling - cyan, matching every other primary action on the dark app. */
body.app-dark .rres-next {
  border-left: 3px solid rgba(34,211,238,.55);
}
body.app-dark .rres-next-lbl { color: rgba(255,255,255,.58); }
body.app-dark .rres-next-cta {
  background: var(--ad-cyan); color: #05141a; border-color: transparent;
}
body.app-dark .rres-next-cta:hover { background: #67e8f9; color: #05141a; }

/* Collapsed review header: same surface language as the panels above it, chevron on the baseline. */
body.app-dark .review-detail h3 {
  background: var(--ad-glass-soft); border: 1px solid var(--ad-line); border-radius: 12px;
}
body.app-dark .review-detail h3 .toggle-icon { color: rgba(255,255,255,.55); line-height: 1; }

/* ---- 63. Answer controls are purpose-built, and Confirm belongs to its widget ----------------
   Six review passes on this screen named six different widgets, and four of them named the same two
   faults: the control in the card was the browser's own, and the Confirm button was sized and placed
   like an afterthought. Measured before touching anything:
     - Confirm was `.btn-sm`: 29px tall at 13px type, submitting rows 54-72px tall, and on drag/drop
       it sat 212px below the answer zone because the source column ran 194px taller.
     - the ordering arrows measured 1.5:1 against their own background, and the DISABLED arrow
       measured 1.5:1 as well - so both looked disabled and neither looked pressable.
     - the hotspot drew a bordered card around a bordered box around each radio group.
     - checkbox and radio were native controls with `accent-color` painted on.
   Structure is in styles.css; this is the palette. */

/* -- Confirm: filled when the answer is complete, outline while it is not. Nothing is dimmed, --
   -- because nothing here is disabled: the button always answers, and an incomplete attempt gets  --
   -- the counted warning confirmPracticeAnswer() already writes.                                  */
body.app-dark .practice-confirm.is-waiting {
  background: transparent; background-image: none; color: var(--ad-cyan);
  border: 1.5px solid rgba(34,211,238,.42); box-shadow: none;
}
body.app-dark .practice-confirm.is-waiting:hover {
  background: rgba(34,211,238,.10); border-color: rgba(34,211,238,.70); color: #67e8f9;
}

/* -- Checkbox and radio, drawn by us --
   This replaces the earlier intent of making the input recede (15px at .72 opacity, so the row tint
   and letter badge carried the state). A native control cannot be styled to match this language, so
   it stopped competing - but the answer was to draw one, not to hide it. */
body.app-dark .option-item input[type="radio"],
body.app-dark .option-item input[type="checkbox"] {
  width: 20px; height: 20px; opacity: 1;
  background: rgba(255,255,255,.04); border: 2px solid rgba(255,255,255,.30);
}
body.app-dark .option-item:hover input:not(:checked):not(:disabled) { border-color: rgba(255,255,255,.48); }
body.app-dark .option-item input:checked { border-color: var(--ad-cyan); background: var(--ad-cyan); }
body.app-dark .option-item input[type="checkbox"]:checked::after { border-color: #05141a; }
body.app-dark .option-item input[type="radio"]:checked::after { background: #05141a; }
body.app-dark .option-item input:focus-visible { box-shadow: 0 0 0 3px rgba(34,211,238,.28); }
/* A revealed row keeps its mark legible - it is the record of what you picked. */
body.app-dark .option-item input:disabled { opacity: .78; }
body.app-dark .option-item.correct-reveal input:checked {
  background: var(--ad-emerald); border-color: var(--ad-emerald);
}
body.app-dark .option-item.wrong-reveal input:checked {
  background: var(--ad-rose); border-color: var(--ad-rose);
}
body.app-dark .option-item.correct-reveal input[type="radio"]:checked::after,
body.app-dark .option-item.wrong-reveal input[type="radio"]:checked::after { background: #10131c; }
body.app-dark .option-item.correct-reveal input[type="checkbox"]:checked::after,
body.app-dark .option-item.wrong-reveal input[type="checkbox"]:checked::after { border-color: #10131c; }

/* -- The instruction pill: instruction, not a tag. Was 11.8:1 sitting above an 18px stem. -- */
/* border-color too: the light rule gained a #dde4ef hairline so the pill keeps an edge on the
   light grey ground, and without this that hairline would paint on the dark theme as well. */
body.app-dark .multi-hint {
  background: rgba(255,255,255,.055); color: rgba(255,255,255,.72);
  border-color: rgba(255,255,255,.10);
}
body.app-dark .multi-hint-tf { background: rgba(251,191,36,.10); color: rgba(253,224,138,.86); }
body.app-dark .multi-hint-rows { background: rgba(255,255,255,.06); color: rgba(255,255,255,.74); }

/* -- Hotspot: one panel, hairline rows -- */
body.app-dark .hotspot-list { border: 1px solid var(--ad-line); background: var(--ad-glass-soft); }
body.app-dark .hs-row { background: transparent; border-bottom: 1px solid rgba(255,255,255,.07); }
body.app-dark .hs-row:last-child { border-bottom: 0; }
/* The prompt is the question half of the row, so it reads at body strength. */
body.app-dark .hs-stmt { color: var(--ad-text); }
body.app-dark .hs-choices { border: 0; background: transparent; }
/* Each task reads as its own group without going back to three cards. */
body.app-dark .hs-row:nth-child(even) { background: rgba(255,255,255,.022); }
/* The pills were readable but inert - a border you can see, a hover that answers, and a selected
   state that looks chosen rather than merely different. */
body.app-dark .hs-choice { border-color: rgba(255,255,255,.11); }
body.app-dark label.hs-choice:hover {
  background: rgba(255,255,255,.085); border-color: rgba(255,255,255,.26); color: #fff;
}
/* label + :hover is (0,3,2) and beats a plain .selected at (0,3,1), so a chosen pill lost its fill
   the moment the pointer sat on it. Selected has to carry the same weight, and its own hover. */
body.app-dark label.hs-choice.selected,
body.app-dark .hs-choice.selected {
  background: rgba(34,211,238,.14); border-color: rgba(34,211,238,.55);
  box-shadow: inset 0 0 0 1px rgba(34,211,238,.45); color: #fff;
}
body.app-dark label.hs-choice.selected:hover {
  background: rgba(34,211,238,.20); border-color: rgba(34,211,238,.75);
}
body.app-dark label.hs-choice:has(input:focus-visible) { box-shadow: 0 0 0 3px rgba(34,211,238,.28); }

/* -- Yes/No: the statement is the content, and it was reading at label strength (6.7:1). -- */
body.app-dark .statement-table tbody td.stmt-text-col { color: var(--ad-text); }

/* -- Ordering: a row you can plainly pick up, and arrows you can plainly press -- */
body.app-dark .order-item[draggable="true"]:hover {
  box-shadow: 0 6px 18px rgba(0,0,0,.30); border-color: rgba(255,255,255,.22);
}
body.app-dark .order-item[draggable="true"]:hover .order-grip { color: rgba(255,255,255,.82); }
body.app-dark .order-grip { color: rgba(255,255,255,.60); }
/* Being dragged, and where it will land. The drop marker was #1a237e - navy on near-black. */
body.app-dark .order-item.dragging {
  opacity: 1; background: rgba(34,211,238,.10); border-color: rgba(34,211,238,.42);
  border-style: dashed;
}
body.app-dark .order-item.drag-over { border-top: 3px solid var(--ad-cyan); }
/* The step number is the ordinal being changed, so it carries the accent rather than sitting at
   4.2:1 grey. */
body.app-dark .order-num {
  background: rgba(34,211,238,.16); color: #a5f3fc; border: 1px solid rgba(34,211,238,.28);
}
body.app-dark .order-btn {
  background: rgba(255,255,255,.07); border: 1px solid rgba(255,255,255,.16);
  color: rgba(255,255,255,.78);
}
body.app-dark .order-btn:hover:not([disabled]) {
  background: rgba(34,211,238,.16); border-color: rgba(34,211,238,.45); color: #fff;
}
body.app-dark .order-btn:focus-visible { box-shadow: 0 0 0 3px rgba(34,211,238,.30); }
/* Off on purpose: no fill, a hairline edge, and a mark you can still see. Not `opacity: .3`, which
   measured the same as the enabled control. */
body.app-dark .order-btn[disabled] {
  opacity: 1; background: transparent;
  border-color: rgba(255,255,255,.09); color: rgba(255,255,255,.30);
}

/* -- Drag and drop: the answer zone is the live half of the widget, so it reads as live -- */
body.app-dark .dd-answer { background: rgba(34,211,238,.055); }
body.app-dark .dd-zone.dd-zone-over {
  background: rgba(34,211,238,.13); border-color: var(--ad-cyan);
  box-shadow: inset 0 0 0 3px rgba(34,211,238,.16);
}

/* ---- 64. Technical exhibits --------------------------------------------------------------------
   A readiness question shows a Bicep template, a role document or a config and asks for the minimum
   change. It was rendered by formatStem() as <br>-joined prose in the 18px UI font, with the
   indentation collapsed by HTML - readable, but not an exhibit. Structure is in styles.css. */
body.app-dark .q-exhibit {
  background: rgba(255,255,255,.035); border-color: rgba(255,255,255,.12);
}
body.app-dark .q-exhibit-head {
  background: rgba(255,255,255,.05); border-bottom-color: rgba(255,255,255,.10);
  color: rgba(255,255,255,.60);
}
/* Near-full strength: this is the thing being examined. */
body.app-dark .q-exhibit-body { color: rgba(255,255,255,.90); }

/* ---- 65. Reveal weight: the answers carry the verdict, not the container --------------------
   The hotspot's revealed state tinted every row, so a mostly-wrong question turned the whole widget
   red - it read as a broken interaction and it competed with the explanation card that carries the
   actual verdict. The container is neutral now, one thin rail states the overall result, and the
   colour sits on the choices. Red is also desaturated a step for the same reason. */
body.app-dark .hotspot-list.is-wrong { border-left: 3px solid rgba(244,63,94,.65); }
body.app-dark .hotspot-list.is-revealed:not(.is-wrong) { border-left: 3px solid rgba(52,211,153,.65); }
body.app-dark .hs-choice.incorrect {
  background: rgba(244,63,94,.10); border-color: rgba(244,63,94,.30); color: #fbc4cc;
}
body.app-dark .hs-choice.correct {
  background: rgba(52,211,153,.11); border-color: rgba(52,211,153,.32); color: #a7f3d0;
}
/* Unselected, not-correct choices go quiet so the two that matter carry the row. (.hs-revealed-neutral
   was in the first draft of this rule and matches no markup - removed rather than shipped.) */
body.app-dark .hotspot-list.is-revealed .hs-choice:not(.correct):not(.incorrect) { opacity: .5; }

/* -- Multi-select: calmer until reveal -------------------------------------------------------
   A selected row is a pending choice, not a result, so it should not sit at result intensity. */
body.app-dark .option-item.selected {
  background: rgba(34,211,238,.085); border-color: rgba(34,211,238,.42);
}
body.app-dark .option-item.selected .option-key {
  background: rgba(34,211,238,.85); color: #04131a;
}

/* -- The count pill's three states -- */
body.app-dark .mc-tally { background: rgba(255,255,255,.10); }
body.app-dark .multi-hint-count.is-partial {
  background: rgba(251,191,36,.10); border-color: rgba(251,191,36,.24); color: #fcd88f;
}
body.app-dark .multi-hint-count.is-partial .mc-tally { background: rgba(251,191,36,.18); color: #fde3ab; }
body.app-dark .multi-hint-count.is-ready {
  background: rgba(34,211,238,.10); border-color: rgba(34,211,238,.26); color: #a5f3fc;
}
body.app-dark .multi-hint-count.is-ready .mc-tally { background: rgba(34,211,238,.20); color: #cffafe; }

/* -- A disabled Confirm that looks disabled -- */
body.app-dark .practice-confirm:disabled,
body.app-dark .practice-confirm.is-waiting:disabled {
  background: rgba(255,255,255,.05); background-image: none; color: rgba(255,255,255,.34);
  border: 1.5px solid rgba(255,255,255,.10); box-shadow: none; cursor: not-allowed;
}
body.app-dark .practice-confirm:disabled:hover {
  background: rgba(255,255,255,.05); border-color: rgba(255,255,255,.10); color: rgba(255,255,255,.34);
}

/* ---- 66. Micro-polish: badge weight, the wrong mark, and two widgets that were not done ------
   Six sign-off passes over the practice screen landed on the same two notes for every question
   type - the YOUR ANSWER marker competes with the option text beside it, and the mark on a wrong
   selection is louder than anything else on the screen - so both are settled here once, for all
   six answer controls, rather than per type. The rest of this section is the two widgets that came
   back under the bar: the hotspot's revealed neutrals, and the ordering row's controls.

   This section is last in the file. Where a selector repeats an earlier one, the value here ships. */

/* -- The YOUR ANSWER marker, ~8% quieter --------------------------------------------------------
   It only has to name the row you chose. Sitting beside the option label AND the tick that carries
   the verdict, it was the third thing asking to be read on a line that has two. */
body.app-dark .your-answer-chip {
  background: rgba(255,255,255,.055); border: 0; color: rgba(255,255,255,.60);
}

/* -- The mark on a wrong selection ---------------------------------------------------------------
   Full-strength --ad-rose (#f43f5e) inside a 20px control was the most saturated pixel on the page,
   on the one element that is 20px square. Muted a step (-15% luminance): still unmistakably the
   wrong mark, no longer the brightest thing in the answer list. The row tint, the letter badge and
   the cross at the end of the row all still say the same thing. */
body.app-dark .option-item.wrong-reveal input:checked {
  background: #d9485f; border-color: #d9485f;
}

/* -- The True/False type hint --------------------------------------------------------------------
   Amber is doing state work elsewhere on this screen (.multi-hint-count.is-partial is the state that
   BLOCKS a Confirm), so a permanent amber pill for "this one is True or False" was borrowing a
   voice it does not need. Same hue, dropped back to hint volume. */
body.app-dark .multi-hint-tf { background: rgba(251,191,36,.075); color: rgba(253,224,138,.72); }

/* -- Hotspot: the neutrals are still part of the choice set ---------------------------------------
   Revealed, the options you did not pick were set to opacity .5. Opacity fades a row toward the page
   ground and never appears in its computed colour - the same trap section 58 hit on
   .option-item.neutral-reveal - so they read as disabled rather than as "not this one", and the
   whole point of leaving four choices on screen is that you can compare them once the answer is out.
   Opacity off, the muted values stated outright. */
body.app-dark .hotspot-list.is-revealed .hs-choice:not(.correct):not(.incorrect) {
  opacity: 1; background: rgba(255,255,255,.028); border-color: rgba(255,255,255,.10);
  color: rgba(255,255,255,.66);
}
/* And the rail: the container is neutral now, so the one red bar beside it was carrying the whole
   question's verdict at full strength. 15% back. */
body.app-dark .hotspot-list.is-wrong { border-left: 3px solid rgba(244,63,94,.55); }

/* -- Yes/No: a wrong row that does not shout over its own statement ------------------------------ */
body.app-dark .statement-row.incorrect { background: rgba(244,63,94,.062); }
/* The correction under the answer. Cyan, because it is the fact you are meant to take away, but a
   size below the answer it corrects. */
body.app-dark .stmt-correct-answer { color: var(--ad-cyan-soft); font-size: 11.5px; }

/* -- Drag/drop reveal: label the columns, quieten the agreements --------------------------------- */
body.app-dark .dd-reveal .dd-col-label { color: rgba(255,255,255,.76); font-size: 12px; }
body.app-dark .dd-reveal .dd-item.correct {
  background: rgba(52,211,153,.075); border-color: rgba(52,211,153,.26);
}

/* -- Ordering: rows you can pick up, and one stepper instead of two buttons -----------------------
   The rows were the last widget still reading as a list of results rather than a thing being
   rearranged: a grey hover border, no keyboard state at all, and two separately boxed arrows parked
   at the right-hand end. Hover and focus-within now both tint toward the accent, the grip gets a box
   that says "move this row", and the arrows become one bordered stepper. Structure in styles.css. */
body.app-dark .order-item[draggable="true"]:hover {
  border-color: rgba(34,211,238,.42); background: rgba(34,211,238,.07);
  box-shadow: 0 6px 18px rgba(0,0,0,.30);
}
body.app-dark .order-item[draggable="true"]:focus-within {
  border-color: rgba(34,211,238,.62); background: rgba(34,211,238,.10);
}
body.app-dark .order-item[draggable="true"]:hover .order-grip {
  color: var(--ad-cyan-soft); background: rgba(34,211,238,.13);
}
body.app-dark .order-btns {
  border: 1px solid rgba(255,255,255,.17); background: rgba(255,255,255,.05);
}
body.app-dark .order-btn {
  background: transparent; border: 0; color: rgba(255,255,255,.84);
}
body.app-dark .order-btn + .order-btn { border-top: 1px solid rgba(255,255,255,.13); }
body.app-dark .order-btn:hover:not([disabled]) {
  background: rgba(34,211,238,.20); border-color: transparent; color: #fff;
}
/* Off, not faded: a filled-back half says which end of the list this row is at. */
body.app-dark .order-btn[disabled] {
  opacity: 1; background: rgba(255,255,255,.025); border-color: transparent;
  color: rgba(255,255,255,.26);
}
body.app-dark .order-btn:focus-visible { box-shadow: inset 0 0 0 2px var(--ad-cyan); }

/* -- Highest-impact domain chips (results screen). Structure in styles.css. -- */
body.app-dark .rres-focus {
  background: rgba(255,255,255,.06); border-color: var(--ad-line); color: rgba(255,255,255,.86);
}
body.app-dark .rres-focus-n {
  background: rgba(251,191,36,.15); border-color: rgba(251,191,36,.28); color: #fcd88f;
}

/* -- Attempt detail hero: the verdict, not the loudest thing on the page ------------------------
   A review screen exists to be acted on, and a full-strength rose slab was outweighing the coaching
   card beneath it. Rendered only by history-detail.js, so the results screen is unaffected. */
body.app-dark .result-banner.failed {
  background: linear-gradient(135deg, rgba(244,63,94,.095), rgba(168,85,247,.04));
  border-color: rgba(244,63,94,.20);
}
body.app-dark .result-banner.passed {
  background: linear-gradient(135deg, rgba(52,211,153,.10), rgba(34,211,238,.04));
  border-color: rgba(52,211,153,.20);
}
body.app-dark button.rres-next-cta { font-family: inherit; cursor: pointer; }

/* -- .es-rows: the History zero state's side panel. Structure in styles.css. ------------------
   The same surface language as the Saved side card it sits in, so the two zero states read as one
   pattern rather than two takes on it. */
body.app-dark .es-rows li { border-bottom: 1px solid rgba(255,255,255,.06); }
body.app-dark .es-row-ico {
  background: rgba(255,255,255,.05); border: 1px solid var(--ad-line); color: var(--ad-cyan-soft);
}
body.app-dark .es-row-text strong { color: rgba(255,255,255,.90); }
body.app-dark .es-row-text span { color: rgba(255,255,255,.58); }
body.app-dark .es-side-note {
  border-top: 1px solid rgba(255,255,255,.06); color: rgba(255,255,255,.46);
}


/* Inline `code` in question prose, dark. The exam page ships class="app-dark app-exam",
   so the light rule in styles.css is not the one most readers see.
   EVERY selector carries the body.app-dark prefix. A first attempt prefixed only the ones
   that started a line, which left the rest as global rules painting dark chips on the
   light theme. */
body.app-dark .question-text code,
body.app-dark .review-q-text code,
body.app-dark .bookmark-q-text code,
body.app-dark .option-text code,
body.app-dark .dd-text code,
body.app-dark .order-text code,
body.app-dark .explanation-para code,
body.app-dark .explanation-note code,
body.app-dark .wrong-note-item code,
body.app-dark .casestudy-body code,
body.app-dark .casestudy-intro code {
  background: rgba(255, 255, 255, .07);
  border-color: rgba(255, 255, 255, .14);
  color: #e2e8f0;
}


/* ---- Guided Labs rows (signed-in view) ------------------------------------------------------ */
body.app-dark .ml-row {
  background: rgba(255,255,255,.02); border-color: var(--ad-line);
}
body.app-dark .ml-row:hover { background: rgba(255,255,255,.05); border-color: rgba(255,255,255,.16); }
body.app-dark .ml-row-static:hover { background: rgba(255,255,255,.02); border-color: var(--ad-line); }
body.app-dark .ml-row-title { color: rgba(255,255,255,.90); }
body.app-dark .ml-row-blurb, body.app-dark .ml-row-meta { color: var(--ad-text-3); }
body.app-dark .ml-empty { color: var(--ad-text-3); }
body.app-dark .ml-foot a { color: var(--ad-cyan-soft); }
body.app-dark .ml-chip-owned { background: rgba(52,211,153,.14); color: #6ee7b7; border-color: rgba(52,211,153,.28); }
body.app-dark .ml-chip-free { background: rgba(255,255,255,.06); color: var(--ad-text-2); border-color: rgba(255,255,255,.12); }
body.app-dark .ml-chip-locked { background: transparent; color: var(--ad-text-3); border-color: rgba(255,255,255,.16); }


body.app-dark .ml-act {
  background: rgba(255,255,255,.06); color: rgba(255,255,255,.90); border-color: rgba(255,255,255,.14);
}
body.app-dark .ml-row:hover .ml-act {
  background: var(--ad-cyan); border-color: var(--ad-cyan); color: #05060d;
}
body.app-dark .ml-cat {
  background: rgba(255,255,255,.05); color: var(--ad-text-2); border-color: rgba(255,255,255,.10);
}
/* Tints, not fills. On a dark surface a saturated chip beside a cyan action button is two things
   shouting; these carry just enough hue to separate the groups at a glance. */
body.app-dark .ml-cat-kubernetes { background: rgba(56,189,248,.12); color: #7dd3fc; border-color: rgba(56,189,248,.22); }
body.app-dark .ml-cat-linux      { background: rgba(52,211,153,.12); color: #6ee7b7; border-color: rgba(52,211,153,.22); }
body.app-dark .ml-cat-containers { background: rgba(45,212,191,.12); color: #5eead4; border-color: rgba(45,212,191,.22); }
body.app-dark .ml-cat-python     { background: rgba(251,191,36,.12); color: #fcd34d; border-color: rgba(251,191,36,.22); }
body.app-dark .ml-cat-databases  { background: rgba(167,139,250,.12); color: #c4b5fd; border-color: rgba(167,139,250,.24); }
body.app-dark .ml-cat-iac        { background: rgba(232,121,249,.10); color: #f0abfc; border-color: rgba(232,121,249,.22); }
/* --ad-text-4 is the dimmest step in the scale and these are the page's wayfinding: at that
   contrast the reader had to look for the group boundaries instead of seeing them. */
body.app-dark .ml-group-label { color: rgba(255,255,255,.62); }

/* ---- Admin > Emails tab --------------------------------------------------------------------
   The base rules in styles.css are light-theme by necessity; without these the group bands render
   as white strips with near-invisible text, which is what the first render of this tab showed.
   Every value below is an existing --ad-* token, so this adds no new palette. */
body.app-dark .em-scope-note,
body.app-dark .em-inline-note { color: var(--ad-text-3); }

body.app-dark .em-stat-grid-sub .stat-card-sub .stat-value { color: var(--ad-text-2); }

body.app-dark .em-inline-filters + .em-inline-filters { border-left-color: var(--ad-line); }
body.app-dark .em-toggle { color: var(--ad-text-3); }
body.app-dark .em-toggle input { accent-color: var(--ad-cyan); }

body.app-dark .admin-icon-btn { color: var(--ad-text-2); }
body.app-dark .admin-icon-btn:hover { color: var(--ad-cyan-soft); }

/* A labelled band inside the table, not a white strip. */
body.app-dark .em-group-row td {
  background: rgba(255, 255, 255, .04);
  border-bottom-color: var(--ad-line-soft);
  color: var(--ad-text-3);
}
body.app-dark .em-group-count {
  background: rgba(255, 255, 255, .07);
  color: var(--ad-text-2);
}

body.app-dark .em-ok-state { color: var(--ad-emerald); }
body.app-dark .em-ok-dot {
  background: var(--ad-emerald);
  box-shadow: 0 0 0 3px rgba(52, 211, 153, .18);
}
body.app-dark .em-ok-neutral { color: var(--ad-text-3); }

/* Navy on near-black is unreadable; the theme's own accent is the link colour here. */
body.app-dark .em-user-link { color: var(--ad-cyan-soft); }
body.app-dark .em-user-link:focus-visible { outline-color: var(--ad-cyan); }

body.app-dark .em-shortid {
  background: rgba(255, 255, 255, .05);
  color: var(--ad-text-2);
}
body.app-dark .em-copy-btn { color: var(--ad-text-4); }
body.app-dark .em-copy-btn:hover {
  background: rgba(255, 255, 255, .07);
  color: var(--ad-cyan-soft);
}
body.app-dark .em-copy-btn:focus-visible { outline-color: var(--ad-cyan); }
