/* ============================================================================
   MEMORIZE MEDICINE — SHARED APPLICATION STYLESHEET  ("Canvas")
   ----------------------------------------------------------------------------
   PASS 2. This file is the 164 rules that were duplicated verbatim inside all
   fifteen inline <style> blocks — 16.9 KB per page, ~253 KB across the repo,
   with fifteen chances to drift on every edit. They now live here once, and
   they have been conformed to Canvas rather than merely re-tinted.

   LOAD ORDER, and why it matters:

       assets/app.css      <- this file: tokens, base, shared components
       <style> ... </style><- what remains inline is page-bespoke only
       assets/canvas.css   <- the thin layer that must outrank page-bespoke CSS

   That is exactly the order the shared block occupied before it was extracted,
   so no cascade relationship changed. Proof: a computed-style snapshot of 17
   properties on every element of all fifteen pages in both themes, before and
   after the move, differed on zero elements.

   CSP NOTE. Moving these rules out does NOT let 'unsafe-inline' come out of
   style-src, and REDESIGN-PASS1.md §5.2 was wrong to imply it would. That
   directive also governs style="..." attributes, of which 762 remain (750 in
   markup, 12 in app.js templates). See REDESIGN-PASS2.md §6 for the count and
   the route. What this file does buy is one cached stylesheet instead of
   fifteen re-parsed copies, and one place to edit.

   NO !important. Not one, anywhere in this file. Where something needed to win
   it was given the specificity to win.
   ========================================================================== */

/* ============================ 1. TYPEFACES ============================== */
/* Self-hosted, latin subset, ~104 KB total. The originals were @import'ed from
   fonts.googleapis.com, which style-src 'self' blocks outright — every page
   had been silently falling back to system fonts in production. */

@font-face{font-family:'Space Grotesk';font-style:normal;font-weight:300 700;
  font-display:swap;src:url('./fonts/spacegrotesk-var.woff2?v=175') 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+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,
  U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Source Sans 3';font-style:normal;font-weight:200 900;
  font-display:swap;src:url('./fonts/sourcesans3-var.woff2?v=175') 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+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,
  U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Source Sans 3';font-style:italic;font-weight:200 900;
  font-display:swap;src:url('./fonts/sourcesans3-var-italic.woff2?v=175') 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+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,
  U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;
  font-display:swap;src:url('./fonts/ibmplexmono-400.woff2?v=175') 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+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,
  U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:500;
  font-display:swap;src:url('./fonts/ibmplexmono-500.woff2?v=175') 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+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,
  U+2215,U+FEFF,U+FFFD}

/* ============================== 2. TOKENS =============================== */
/* The app's existing variable NAMES are kept and given Canvas VALUES, so the
   ~695 lines of page-bespoke CSS re-tint themselves without being touched.
   `:root` carries dark (it is what applies when the theme attribute is absent);
   `[data-theme="light"]` carries paper, and every page ships with it. */

:root{
  /* --- dark: now the opt-in --- */
  --orange:#FFA24D; --orange-hi:#FF751F;
  --blue:#5CABE5;   --blue-hi:#8CC5F0;
  --green:#6FB79C;  --green-hi:#8CCBB3;
  --ink:#F1F4F8;

  --bg:#141920; --bg2:#1B212A;
  --glass:#1B212A; --glass2:#212833;
  --stroke:#333C49; --stroke-hi:#5CABE5;
  --text:#F1F4F8; --text2:#B7C0CB; --text3:#96A0AE;

  --danger:#E8757C; --amber:#FFBA48;

  --paper:#141920; --surface:#1B212A; --surface-2:#212833; --surface-3:#2A323E;
  --line:#333C49; --line-2:#252D38;
  --brand:#5CABE5; --brand-ink:#8CC5F0; --brand-soft:#12283A; --brand-on:#08243C;
  --accent:#FF9040; --accent-ink:#FFB273; --accent-on:#2A1305; --accent-soft:#2A1B0E;
  --jade:#6FB79C; --jade-soft:#12291F; --jade-on:#04140F;
  --crimson:#E8757C; --crimson-soft:#2C1518; --crimson-on:#2A0E10;
  --amber-soft:#2A2110; --amber-on:#241800;

  /* Radii. Kept at the Pass 1 values by decision — Canvas proper runs at 2px,
     these are deliberately softer. Changing these two lines re-rounds the
     entire application. */
  --r-obj:10px; --r-btn:8px; --r-sm:7px; --r-tag:999px;

  /* Type roles */
  --fd:'Space Grotesk',system-ui,-apple-system,'Segoe UI',sans-serif;
  --fb:'Source Sans 3',system-ui,-apple-system,'Segoe UI',Arial,sans-serif;
  --fm:'IBM Plex Mono',ui-monospace,'SF Mono',Menlo,Consolas,monospace;

  /* The border does the work now; the 50px drop shadows are gone. */
  --shadow:0 1px 2px rgba(0,0,0,.28);

  /* Static fallbacks, then the color-mix refinement. Safari below 16.2 and
     older Android WebViews — which is a real share of this app's audience —
     drop any declaration containing color-mix(). Without the plain value
     first, the dot grid, every hatch pattern and several badge borders
     silently vanish on exactly those devices. */
  --dot:rgba(241,244,248,.16);
  --dot:color-mix(in srgb,var(--text) 16%,transparent);
  --pat-c:rgba(241,244,248,.30);
  --pat-c:color-mix(in srgb,var(--text) 30%,transparent);
  --edge-brand:rgba(92,171,229,.40);
  --edge-brand:color-mix(in srgb,var(--brand) 40%,transparent);
  --edge-accent:rgba(255,144,64,.40);
  --edge-accent:color-mix(in srgb,var(--accent) 40%,transparent);
  --edge-jade:rgba(111,183,156,.40);
  --edge-jade:color-mix(in srgb,var(--jade) 40%,transparent);
  --edge-crimson:rgba(232,117,124,.40);
  --edge-crimson:color-mix(in srgb,var(--crimson) 40%,transparent);
  --edge-amber:rgba(255,186,72,.40);
  --edge-amber:color-mix(in srgb,var(--amber) 40%,transparent);
  --veil:rgba(20,25,32,.55);
  --veil:color-mix(in srgb,var(--paper) 55%,transparent);
}

[data-theme="light"]{
  /* --- paper: the default --- */
  --orange:#B45309; --orange-hi:#C2570B;
  --blue:#2570B7;   --blue-hi:#1D5397;
  --green:#376F5C;  --green-hi:#2B5748;
  --ink:#293039;

  --bg:#F6F7FA; --bg2:#FFFFFF;
  --glass:#FFFFFF; --glass2:#FBFCFE;
  --stroke:#E4E8EE; --stroke-hi:#2570B7;
  --text:#293039; --text2:#4F5763; --text3:#5C6570;

  --danger:#B93B42; --amber:#8A6008;

  --paper:#F6F7FA; --surface:#FFFFFF; --surface-2:#FBFCFE; --surface-3:#E9F0F8;
  --line:#E4E8EE; --line-2:#EDF0F5;
  --brand:#2570B7; --brand-ink:#1D5397; --brand-soft:#E7F4FF; --brand-on:#FFFFFF;
  --accent:#B45309; --accent-ink:#8F4207; --accent-on:#FFFFFF; --accent-soft:#FFF3DE;
  --jade:#376F5C; --jade-soft:#E6F1ED; --jade-on:#FFFFFF;
  --crimson:#B93B42; --crimson-soft:#FBE9EA; --crimson-on:#FFFFFF;
  --amber-soft:#FFF3DE; --amber-on:#FFFFFF;

  --shadow:0 1px 2px rgba(41,48,57,.06);

  --dot:rgba(41,48,57,.15);
  --dot:color-mix(in srgb,var(--text) 15%,transparent);
  --pat-c:rgba(41,48,57,.30);
  --pat-c:color-mix(in srgb,var(--text) 30%,transparent);
  --edge-brand:rgba(37,112,183,.40);
  --edge-brand:color-mix(in srgb,var(--brand) 40%,transparent);
  --edge-accent:rgba(180,83,9,.40);
  --edge-accent:color-mix(in srgb,var(--accent) 40%,transparent);
  --edge-jade:rgba(55,111,92,.40);
  --edge-jade:color-mix(in srgb,var(--jade) 40%,transparent);
  --edge-crimson:rgba(185,59,66,.40);
  --edge-crimson:color-mix(in srgb,var(--crimson) 40%,transparent);
  --edge-amber:rgba(138,96,8,.40);
  --edge-amber:color-mix(in srgb,var(--amber) 40%,transparent);
  --veil:rgba(246,247,250,.72);
  --veil:color-mix(in srgb,var(--paper) 72%,transparent);
}

/* Six hatch patterns code the six specialties and the four course formats.
   The PATTERN is the information and the colour is decoration, so the coding
   survives greyscale printing, a monochrome e-reader and every form of colour
   blindness. This replaces the old scheme, where the only difference between
   "Cardiology" and "Neurology" was a hue. */
:root{
  --pat-hatch:repeating-linear-gradient(45deg,var(--pat-c) 0 1.5px,transparent 1.5px 8px);
  --pat-cross:repeating-linear-gradient(45deg,var(--pat-c) 0 1.1px,transparent 1.1px 9px),
              repeating-linear-gradient(-45deg,var(--pat-c) 0 1.1px,transparent 1.1px 9px);
  --pat-dot:radial-gradient(circle at 50% 50%,var(--pat-c) 1.9px,transparent 2px) 0 0/9px 9px;
  --pat-grid:repeating-linear-gradient(0deg,var(--pat-c) 0 1px,transparent 1px 9px),
             repeating-linear-gradient(90deg,var(--pat-c) 0 1px,transparent 1px 9px);
  --pat-rule:repeating-linear-gradient(0deg,var(--pat-c) 0 1.5px,transparent 1.5px 7px);
  --pat-stip:radial-gradient(circle at 28% 30%,var(--pat-c) 1.7px,transparent 1.8px) 0 0/12px 12px,
             radial-gradient(circle at 74% 70%,var(--pat-c) 1.1px,transparent 1.2px) 0 0/12px 12px;
}

/* ============================== 3. BASE ================================= */

*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html{-webkit-text-size-adjust:100%}

/* The dot grid is the substrate. Objects rest on it.
   The 0.3s background transition that used to live here is gone: it produced a
   visible smear across the whole page on every theme toggle, and it made
   automated colour auditing lie — the first Pass 2 contrast run sampled
   mid-transition and reported 180 failures that did not exist. */
body{
  min-height:100vh;
  color:var(--text);
  font-family:var(--fb);
  font-size:15px; line-height:1.58; letter-spacing:-.002em;
  -webkit-font-smoothing:antialiased;
  background-color:var(--paper);
  background-image:radial-gradient(circle at 1px 1px,var(--dot) 1.2px,transparent 1.3px);
  background-size:22px 22px;
}

/* Form controls do not inherit colour from the page in any browser -- they
   take a UA default of near-black. That was invisible on the dark theme
   wherever a bespoke button set a background but no colour, which is how
   flashcards' and tutor's card-type picker rendered its label at 1.39:1.
   Normalising here fixes it once for every control in the app. */
button,input,select,textarea{font-family:inherit;color:inherit}

h1,h2,h3,h4{font-family:var(--fd);font-weight:600;letter-spacing:-.011em;line-height:1.12}
a{color:var(--brand);text-decoration:none}
a:hover{color:var(--brand-ink);text-decoration:underline;text-underline-offset:2px}
[data-theme="light"] a{color:var(--brand)}
:focus-visible{outline:2px solid var(--brand);outline-offset:2px;border-radius:var(--r-sm)}
::selection{background:var(--brand);color:var(--brand-on)}
@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms;animation-iteration-count:1;transition-duration:.001ms}}

/* ========================= 4. THE ICON SYSTEM =========================== */
/* Pass 2 replaced the emoji icon set with drawn two-tone SVG. Emoji were the
   wrong tool for four reasons that all bite this particular product:
     - they render in the vendor's own colours, so they ignore the theme
       entirely and cannot be made to meet a contrast target;
     - they are drawn differently on every platform, so 🧾 is a receipt on iOS
       and something else on Windows;
     - many are simply missing on older Android, where they fall back to a
       tofu box;
     - screen readers announce them ("card index dividers"), which turns a
       decorative glyph into spoken noise in the middle of a label.
   The replacements take `currentColor`, so one icon works on both themes, on
   a tinted plate, and inside a filled button. They are aria-hidden, so the
   label beside them is what gets announced. */

.ic{
  width:1.15em; height:1.15em; flex:none; display:inline-block;
  vertical-align:-.19em; overflow:visible;
}
.ic .ib{fill:currentColor;opacity:.20}
.ic .is{fill:none;stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
/* On a filled surface the 20% body reads as mud; lift it and thin the line. */
.btn-primary .ic .ib,.btn-accent .ic .ib,.btn-success .ic .ib,.btn-danger .ic .ib{opacity:.30}
.stat-icon .ic{width:20px;height:20px}
.nav-icon .ic{width:17px;height:17px}
/* R146 — the combined device+Android install icon carries two distinct
   glyphs side by side (a generic phone outline, the Android robot), so the
   default 1.15em single-glyph size reads as a blur. Fixed px, independent
   of the button's font-size, same reasoning .stat-icon/.nav-icon already
   apply for their own sizing needs. */
.ic.ic-cross{width:26px;height:26px}
.pwa-install-btn .ic.ic-cross{width:22px;height:22px}

/* ========================== 5. ATMOSPHERE =============================== */
/* Was three blurred gradient orbs plus a masked dot field. Canvas keeps the
   dots only, un-masked and even, so the grid reads as a substrate rather than
   as weather. */

.atmos{
  position:fixed; inset:0; z-index:-1; overflow:hidden;
  background-color:var(--paper);
  background-image:radial-gradient(circle at 1px 1px,var(--dot) 1.2px,transparent 1.3px);
  background-size:22px 22px;
}
.atmos::before{content:none}
.orb{display:none}

/* ======================= 6. OBJECTS ON THE CANVAS ======================= */
/* Glass is gone. An object is a flat surface with a hairline edge, resting on
   the grid. No blur, no inner highlight, no 50px drop shadow. */

.glass{
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:var(--r-obj);
  box-shadow:var(--shadow);
  position:relative;
}
.glass::before{content:none}

/* .lift — SELECTION FEEDBACK, AND WHO IS ALLOWED TO HAVE IT.
   Pass 1 gave every .lift the corner-handle treatment of an object editor.
   That was wrong, and not for the reason recorded in its own §5.1: the problem
   is not that the elements are large, it is that 26 of the 27 are not
   clickable. A blue outline and four drag handles is the strongest promise of
   interactivity an interface can make, and a dashboard stat tile cannot keep
   it — you hover it, it offers to be picked up, you click, nothing happens.
   So the handles are now scoped to things that genuinely respond to a click,
   detected structurally rather than by class name:
       a.lift, button.lift, [role="button"].lift, [tabindex].lift, .lift a…
   Everything else keeps a quiet edge change and nothing more. */
.lift{transition:border-color .14s,outline-color .14s;position:relative}
.lift:hover{transform:none;box-shadow:var(--shadow);border-color:var(--text3)}

a.lift:hover,button.lift:hover,
.lift[role="button"]:hover,.lift[tabindex]:hover,label.lift:hover{
  border-color:var(--text); outline:1.5px solid var(--brand); outline-offset:4px;
}
a.lift::after,button.lift::after,
.lift[role="button"]::after,.lift[tabindex]::after,label.lift::after{
  content:""; position:absolute; inset:-5px; pointer-events:none; opacity:0;
  transition:opacity .14s; border-radius:2px;
  background:
    linear-gradient(var(--brand),var(--brand)) left  top    /7px 7px no-repeat,
    linear-gradient(var(--brand),var(--brand)) right top    /7px 7px no-repeat,
    linear-gradient(var(--brand),var(--brand)) left  bottom /7px 7px no-repeat,
    linear-gradient(var(--brand),var(--brand)) right bottom /7px 7px no-repeat;
}
a.lift:hover::after,button.lift:hover::after,
.lift[role="button"]:hover::after,.lift[tabindex]:hover::after,label.lift:hover::after{opacity:1}

/* ========================== 7. TYPE EFFECTS ============================= */
/* The gradient-text treatment survives, on the Canvas palette. It is the one
   place a second hue is allowed, because it carries the brand line. */

.grad-text,[data-theme="light"] .grad-text{
  background:linear-gradient(100deg,var(--blue) 0%,var(--blue-hi) 48%,var(--green) 100%);
  -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent;
}
.grad-orange,[data-theme="light"] .grad-orange{
  background:linear-gradient(100deg,var(--accent),var(--accent-ink));
  -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent;
}
.eyebrow{
  font-family:var(--fm); font-size:10.5px; font-weight:500; letter-spacing:.17em;
  text-transform:uppercase; color:var(--text3);
}

/* =========================== 8. APP SHELL ============================== */

.app{display:flex;height:100vh;overflow:hidden}

/* R37 (owner feedback #2): the sidebar's bottom-of-logo divider and the
   topbar's bottom border are two separate 1px lines that meet at the rail's
   right edge, so ANY difference between them reads as a broken seam. They used
   to be computed from unrelated numbers -- the rail from 18px sidebar padding +
   2/16 logo padding + whatever the wordmark's line box happened to be (62px),
   the topbar from 13px padding + a 34px icon button (60px) -- which is exactly
   the 2px step in the screenshot. Both now derive from one variable and are
   border-box, so the two lines are at the same y by construction and cannot
   drift when the mark, the wordmark's font or the icon-button size changes. */
:root{--head-h:60px}
.sidebar{
  width:248px; flex-shrink:0; display:flex; flex-direction:column;
  padding:0 0 14px; background:var(--surface);
  border-right:1px solid var(--line);
}
.sidebar-logo{
  height:var(--head-h); flex:none; display:flex; align-items:center; gap:8px;
  padding:0 14px; border-bottom:1px solid var(--line); margin-bottom:12px;
}
.sidebar-logo img{width:100%;max-width:176px;filter:none}
[data-theme="light"] .sidebar-logo img{filter:none}
/* The theme swap for the two mark files lives in canvas.css, not here. It has
   to outrank per-page rules like `.pnav-logo img{display:block}`, which score
   (0,1,1) against these classes' (0,1,0). Pass 1 solved that with !important;
   putting the rules in the layer that loads last solves it with the cascade.
   Leaving them here was a real regression -- in dark, BOTH marks rendered and
   the light one, being later in the DOM, was the one you saw. */

.nav-label{
  font-family:var(--fm); font-size:10px; font-weight:500; color:var(--text3);
  letter-spacing:.14em; text-transform:uppercase; padding:14px 22px 6px;
}
.nav-item{
  display:flex; align-items:center; gap:10px; margin:1px 10px; padding:8px 11px;
  border-radius:var(--r-sm); color:var(--text2); font-size:13.5px; font-weight:600;
  cursor:pointer; transition:background .15s,color .15s; position:relative;
  border:1px solid transparent; background:none; width:calc(100% - 20px);
  text-align:left; font-family:var(--fb);
}
.nav-item:hover{background:var(--surface-2);color:var(--text)}
.nav-item.active,[data-theme="light"] .nav-item.active{
  background:var(--brand-soft); border-color:transparent;
  color:var(--brand-ink); box-shadow:none;
}
/* The trace: one motif, reused. It marks the active nav item and nothing else
   in the rail, so "you are here" is never ambiguous. */
.nav-item.active::before{
  content:''; position:absolute; left:-10px; top:20%; bottom:20%; width:3px;
  border-radius:0 2px 2px 0; background:var(--brand); box-shadow:none;
}
.nav-icon{width:19px;display:flex;align-items:center;justify-content:center;color:var(--text3)}
.nav-item:hover .nav-icon{color:var(--text2)}
.nav-item.active .nav-icon{color:var(--brand)}
.nav-badge{
  margin-left:auto; font-family:var(--fm); font-size:10px; font-weight:500;
  color:var(--accent-on); background:var(--accent);
  border-radius:var(--r-tag); padding:2px 8px; box-shadow:none;
}
/* Task #57 (owner): "Created tests" and "Memorize notes" nest under QBank
   as a collapsible submenu instead of sitting as plain top-level rows.
   All new class names (.nav-parent/.nav-parent-link/.nav-caret/.nav-sub/
   .nav-subitem) -- .nav-item's own rules above are untouched, so every
   other page's flat nav list (admin/tutor/support/etc.) is unaffected.
   QBank itself stays a real <a href="qbank.html">; the caret only
   opens/closes the .nav-sub list beneath it (see memToggleNavGroup() /
   memOpenNavGroup() in student.html and qbank.html). */
.nav-parent{display:flex; align-items:center; margin:1px 10px; width:calc(100% - 20px)}
.nav-parent .nav-parent-link{margin:0; width:auto; flex:1; min-width:0}
.nav-caret{
  flex:none; width:26px; height:26px; display:flex; align-items:center; justify-content:center;
  margin-left:2px; border:1px solid transparent; border-radius:var(--r-sm);
  background:none; color:var(--text3); cursor:pointer; padding:0;
  transition:color .15s,background .15s,border-color .15s;
}
.nav-caret:hover{background:var(--surface-2); color:var(--text)}
.nav-caret:focus-visible{outline:2px solid var(--brand); outline-offset:1px}
.nav-caret .ic{width:14px; height:14px; transition:transform .2s ease}
.nav-parent.is-open .nav-caret .ic{transform:rotate(180deg)}
.nav-sub{display:flex; flex-direction:column; overflow:hidden; max-height:0; transition:max-height .22s ease}
.nav-sub.is-open{max-height:112px}
.nav-sub .nav-item{margin-left:32px; width:calc(100% - 42px); padding-top:7px; padding-bottom:7px}
.nav-sub .nav-item .nav-icon{width:16px}
@media(min-width:761px){
  /* Icon-only rail: no room for a caret or indentation, so the two
     sub-items just render as their own centered icon rows (like every
     other top-level item in this mode) and stay permanently reachable
     rather than disappearing behind a hidden toggle. */
  body.sb-mini .sidebar:not(.sidebar--drawer) .nav-parent{margin:0; padding:0; width:100%}
  body.sb-mini .sidebar:not(.sidebar--drawer) .nav-caret{display:none}
  body.sb-mini .sidebar:not(.sidebar--drawer) .nav-sub{max-height:none!important; overflow:visible}
  body.sb-mini .sidebar:not(.sidebar--drawer) .nav-sub .nav-item{margin:2px 8px; width:calc(100% - 16px)}
}

.sidebar-footer{
  margin-top:auto; padding:13px 18px 4px; border-top:1px solid var(--line);
  display:flex; align-items:center; gap:10px;
}
.avatar,.avatar.orange{
  width:36px; height:36px; border-radius:50%; flex-shrink:0;
  display:flex; align-items:center; justify-content:center;
  font-family:var(--fm); font-weight:500; font-size:13px;
  color:var(--text); background:var(--surface-3);
  border:1px solid var(--line); box-shadow:none;
}
.user-name{font-size:13.5px;font-weight:600}
.user-role{font-size:11.5px;color:var(--text3)}

/* R36 (owner feedback #2): collapsible sidebar. body.sb-mini shrinks the rail
   to a 68px icon rail — labels, section headings and badges hide, the logo
   mark stays, and each item's name travels into a title tooltip (set by
   initSidebarCollapse in app.js). Desktop only: below 760px the sidebar is an
   off-canvas drawer and the injected .sidebar--drawer is always one, so both
   are excluded from every rule here.

   R37 (owner feedback #2): the handle used to be a floating circle pinned to
   the rail's right edge at top:66px, i.e. straddling the one seam this pass is
   trying to make invisible, and hanging over the page content. It now lives
   INSIDE the header band, right-aligned opposite the wordmark, on the same
   baseline as the topbar's icon buttons. Being in normal flow it cannot
   overlap anything, it cannot desync from the divider, and it reads as part of
   the rail's own header rather than a sticker on the border.

   Collapsed, the wordmark and mark step aside and the button takes the header
   on its own, centred: at 68px there is not room for both, and a button that
   is visible in both states beats one that only appears on hover (this app is
   used on iPads, where there is no hover). */
.sb-collapse{display:none}
@media(min-width:761px){
  .sidebar{position:relative;transition:width .25s ease}
  .sb-collapse{
    display:flex;align-items:center;justify-content:center;flex:none;
    width:28px;height:28px;margin-left:auto;
    border-radius:var(--r-sm);border:1px solid transparent;background:none;
    color:var(--text3);cursor:pointer;padding:0;
    transition:color .15s,border-color .15s,background .15s;
  }
  .sb-collapse:hover{color:var(--text);border-color:var(--line);background:var(--surface-2)}
  .sb-collapse:focus-visible{outline:2px solid var(--brand);outline-offset:1px}
  .sb-collapse .ic{width:16px;height:16px;transition:transform .25s ease}
  body.sb-mini .sidebar:not(.sidebar--drawer){width:68px}
  body.sb-mini .sidebar:not(.sidebar--drawer) .sb-collapse .ic{transform:rotate(180deg)}
  /* one rule, so it works whether the mark is wrapped (student/tutor/admin/
     qbank) or a bare .lock (support) */
  body.sb-mini .sidebar:not(.sidebar--drawer) .sidebar-logo>*:not(.sb-collapse){display:none}
  body.sb-mini .sidebar:not(.sidebar--drawer) .sb-collapse{margin:0}
  body.sb-mini .sidebar:not(.sidebar--drawer) .lock-name,
  body.sb-mini .sidebar:not(.sidebar--drawer) .nav-label,
  body.sb-mini .sidebar:not(.sidebar--drawer) .nav-badge,
  body.sb-mini .sidebar:not(.sidebar--drawer) .nav-item>span:not(.nav-icon),
  body.sb-mini .sidebar:not(.sidebar--drawer) .sidebar-footer>div:not(.avatar){display:none}
  body.sb-mini .sidebar:not(.sidebar--drawer) .nav-item{justify-content:center;margin:2px 8px;padding:9px 0;width:calc(100% - 16px)}
  body.sb-mini .sidebar:not(.sidebar--drawer) .nav-item.active::before{left:-8px}
  body.sb-mini .sidebar:not(.sidebar--drawer) .sidebar-logo{padding:0;justify-content:center}
  body.sb-mini .sidebar:not(.sidebar--drawer) .sidebar-footer{justify-content:center;padding:13px 8px 4px}
}

.main{flex:1;overflow-y:auto;display:flex;flex-direction:column}
.topbar{
  display:flex; align-items:center; gap:12px; padding:0 26px;
  min-height:var(--head-h);
  position:sticky; top:0; z-index:20; flex-shrink:0;
  background:var(--paper); border-bottom:1px solid var(--line);
}
.topbar-title{font-family:var(--fd);font-size:16px;font-weight:600;letter-spacing:-.012em}
.topbar-right{margin-left:auto;display:flex;align-items:center;gap:8px}
.search-bar{
  display:flex; align-items:center; gap:8px; background:var(--surface);
  border:1px solid var(--line); border-radius:var(--r-sm);
  padding:7px 12px; width:230px; transition:border-color .15s; color:var(--text3);
}
.search-bar:focus-within{border-color:var(--brand)}
.search-bar input{
  background:none; border:none; outline:none; color:var(--text);
  font-family:var(--fb); font-size:13.5px; width:100%;
}
.search-bar input::placeholder{color:var(--text3)}
.icon-btn{
  /* flex:none matters. These sit in a flex row that runs out of width on a
     phone, and without it the button is squeezed to 21px -- below the 24px
     minimum target and visibly ovoid. */
  flex:none;
  width:34px; height:34px; border-radius:var(--r-sm); cursor:pointer;
  background:var(--surface); border:1px solid var(--line);
  display:flex; align-items:center; justify-content:center; font-size:15px;
  transition:border-color .15s,color .15s; position:relative; color:var(--text2);
}
.icon-btn:hover{border-color:var(--text3);color:var(--text);transform:none}
.notif-dot{
  position:absolute; top:6px; right:6px; width:7px; height:7px;
  border-radius:2px; background:var(--accent); box-shadow:none;
  border:1.5px solid var(--surface);
}
/* R144 — the topbar install control. Owner feedback: it "should look like
   its button" — as a bare .icon-btn it read as a settings toggle, the same
   weight as theme/notifications, when it's actually the one action-oriented
   CTA up there. Same brand fill as .btn.btn-primary (the "Start"/"Create
   your free account" buttons elsewhere), same 34px height as the icon-only
   controls beside it so the row stays aligned, but with a visible label so
   it reads as something to press, not a status icon. Collapses to icon-only
   below 760px, same shrink-for-space treatment as .pwa-bar-txt span. */
.pwa-install-btn{
  flex:none; display:inline-flex; align-items:center; gap:6px;
  height:34px; padding:0 14px; border-radius:var(--r-sm); cursor:pointer;
  background:var(--brand); border:1px solid var(--brand); color:var(--brand-on);
  font-family:var(--fb); font-size:13px; font-weight:700;
  transition:background .15s,border-color .15s;
}
.pwa-install-btn:hover{background:var(--brand-ink);border-color:var(--brand-ink)}
@media(max-width:760px){
  .pwa-install-btn{padding:0;width:34px;justify-content:center}
  .pwa-install-btn span{display:none}
}
.content{padding:24px 26px 44px;flex:1}

/* ============================ 9. BUTTONS =============================== */
/* Flat, one weight of border, no gradient, no shine sweep, no lift.

   EVERY VARIANT IS WRITTEN `.btn.btn-x`, NOT `.btn-x`, and that is not a
   stylistic choice. The base stylesheet carries `[data-theme="light"] a{color:
   var(--blue)}`, which scores (0,1,1) and beats a single-class `.btn-accent
   {color:#fff}` at (0,1,0). Every CTA that is an <a> — which is most of them —
   rendered its label in link-blue on an orange fill, about 1.02:1, invisible.
   `.btn.btn-x` scores (0,2,0) and wins cleanly, with no !important anywhere. */

.btn{
  display:inline-flex; align-items:center; justify-content:center; gap:7px;
  padding:9px 16px; border-radius:var(--r-btn);
  border:1px solid var(--line); background:var(--surface); color:var(--text);
  font-family:var(--fb); font-size:14px; font-weight:600;
  cursor:pointer; position:relative; overflow:visible; box-shadow:none;
  transition:background .15s,border-color .15s,color .15s,transform .1s;
}
.btn::after{content:none}
.btn:hover{transform:none;border-color:var(--text3);box-shadow:none}
.btn:active{transform:translateY(1px)}

.btn.btn-primary{background:var(--brand);border-color:var(--brand);color:var(--brand-on);font-weight:700}
.btn.btn-primary:hover{background:var(--brand-ink);border-color:var(--brand-ink);color:var(--brand-on)}
.btn.btn-accent{background:var(--accent);border-color:var(--accent);color:var(--accent-on);font-weight:700}
.btn.btn-accent:hover{background:var(--accent-ink);border-color:var(--accent-ink);color:var(--accent-on)}
.btn.btn-success{background:var(--jade);border-color:var(--jade);color:var(--jade-on);font-weight:600}
.btn.btn-success:hover{background:var(--jade);border-color:var(--text);color:var(--jade-on)}
.btn.btn-danger{background:var(--crimson);border-color:var(--crimson);color:var(--crimson-on);font-weight:600}
.btn.btn-danger:hover{background:var(--crimson);border-color:var(--text);color:var(--crimson-on)}
.btn.btn-ghost{background:transparent;border-color:var(--line);color:var(--text2)}
.btn.btn-ghost:hover{background:var(--surface-2);color:var(--text);border-color:var(--text3)}
.btn-lg{padding:12px 22px;font-size:15px;border-radius:var(--r-btn)}
.btn-block{width:100%}
.btn:disabled{opacity:.55;cursor:not-allowed}

/* ========================= 10. STATS AND CARDS ========================== */
/* The figure leads, in mono, tabular. The icon plate carries a hatch pattern
   instead of a coloured gradient — see the note on the patterns above. */

.stat-card{padding:18px;overflow:hidden}
.stat-card .spark{display:none}
.stat-icon{
  width:40px; height:40px; border-radius:var(--r-sm); font-size:18px;
  display:flex; align-items:center; justify-content:center; margin-bottom:12px;
  box-shadow:none; background-color:var(--surface-3); border:1px solid var(--line);
}
.si-blue  {background-image:var(--pat-hatch); color:var(--brand)}
.si-orange{background-image:var(--pat-stip);  color:var(--accent)}
.si-green {background-image:var(--pat-cross); color:var(--jade)}
.si-gray  {background-image:var(--pat-rule);  color:var(--text2)}
[data-theme="light"] .si-blue{color:var(--brand)}
[data-theme="light"] .si-orange{color:var(--accent)}
[data-theme="light"] .si-green{color:var(--jade)}
.stat-val{font-family:var(--fm);font-size:28px;font-weight:500;letter-spacing:-.035em;font-variant-numeric:tabular-nums}
.stat-lbl{font-size:12.5px;color:var(--text2);margin-top:3px;font-weight:400}
.stat-note{font-size:11.5px;font-weight:600;margin-top:8px}
.up,[data-theme="light"] .up{color:var(--jade)}
.down{color:var(--crimson)}
.card-title{font-family:var(--fd);font-size:15px;font-weight:600;letter-spacing:-.012em;margin-bottom:13px}

/* ========================== 11. PROGRESS =============================== */
/* An instrument, not a decoration: square ends, no shimmer, no glow. */

.progress,[data-theme="light"] .progress{
  height:6px; border-radius:0; background:var(--surface-3);
  overflow:hidden; position:relative;
}
.progress-fill{height:100%;border-radius:0;background:var(--brand);position:relative;overflow:hidden;box-shadow:none}
.progress-fill::after{content:none}

/* ======================= 12. BADGES AND CHIPS ========================== */
/* Status never rides on colour alone — every badge keeps its word, and the
   border gives a second, non-colour cue at a glance. */

.badge,a.badge{
  display:inline-flex; align-items:center; gap:5px;
  font-family:var(--fm); font-size:10.5px; font-weight:500;
  letter-spacing:.09em; text-transform:uppercase;
  border-radius:var(--r-tag); padding:3px 10px;
  border:1px solid var(--line); background:var(--surface); color:var(--text2);
}
.bg-blue  {background:var(--brand-soft);   color:var(--brand-ink); border:1px solid var(--edge-brand)}
.bg-orange{background:var(--accent-soft);  color:var(--accent);    border:1px solid var(--edge-accent)}
.bg-green {background:var(--jade-soft);    color:var(--jade);      border:1px solid var(--edge-jade)}
.bg-red   {background:var(--crimson-soft); color:var(--crimson);   border:1px solid var(--edge-crimson)}
.bg-amber {background:var(--amber-soft);   color:var(--amber);     border:1px solid var(--edge-amber)}
.bg-gray  {background:var(--surface-2);    color:var(--text2);     border:1px solid var(--line)}
[data-theme="light"] .bg-blue{color:var(--brand-ink)}
[data-theme="light"] .bg-orange{color:var(--accent)}
[data-theme="light"] .bg-green{color:var(--jade)}
[data-theme="light"] .bg-red{color:var(--crimson)}
[data-theme="light"] .bg-amber{color:var(--amber)}

/* ============================= 13. TABLE =============================== */

.table{width:100%;border-collapse:collapse;font-size:14px}
.table th{
  text-align:left; font-family:var(--fm); font-size:9.5px; font-weight:500;
  text-transform:uppercase; letter-spacing:.14em; color:var(--text3);
  padding:10px 15px; border-bottom:1px solid var(--line);
}
.table td{padding:12px 15px;border-bottom:1px solid var(--line-2);vertical-align:middle}
.table tr:last-child td{border-bottom:none}
.table tbody tr{transition:background .14s}
.table tbody tr:hover{background:var(--surface-2)}
.mono{font-family:var(--fm);font-size:12.5px;font-variant-numeric:tabular-nums}

/* ============================= 14. FORMS =============================== */

.field{margin-bottom:16px}
.field label{
  display:block; font-family:var(--fm); font-size:10px; font-weight:500;
  color:var(--text3); margin-bottom:6px; letter-spacing:.14em; text-transform:uppercase;
}
.input,.select{
  width:100%; padding:10px 13px; border-radius:var(--r-sm);
  background:var(--surface); border:1px solid var(--line); color:var(--text);
  font-family:var(--fb); font-size:14px; transition:border-color .15s,box-shadow .15s;
}
.input:focus,.select:focus{outline:none;border-color:var(--brand);box-shadow:0 0 0 3px var(--brand-soft)}
.input::placeholder{color:var(--text3)}
.select option{background:var(--surface);color:var(--text)}
.hint{font-size:12.5px;color:var(--text3);margin-top:6px}

/* ============================= 15. CHARTS ============================== */
/* Bars are readings. Flat fill, square top, no glow, no grow animation that
   makes a figure look like it is still settling when it is not. */

.bars{display:flex;align-items:flex-end;gap:12px;height:175px;padding-top:12px}
.bar-col{flex:1;display:flex;flex-direction:column;align-items:center;gap:7px;height:100%;justify-content:flex-end}
.bar{
  width:100%; max-width:44px; border-radius:1px; background:var(--brand);
  min-height:4px; transition:background .15s; box-shadow:none;
}
.bar:hover{filter:none;transform:none;background:var(--brand-ink)}
.bar.orange{background:var(--accent);box-shadow:none}
.bar.orange:hover{background:var(--accent-ink)}
.bar-lbl{font-family:var(--fm);font-size:10px;color:var(--text3);font-weight:500;letter-spacing:.06em}
.bar-val{font-family:var(--fm);font-size:11.5px;color:var(--text2);font-weight:500;font-variant-numeric:tabular-nums}

/* Department rows. `.dept-row`/`.dept-bar`/`.dept-fill` shipped in all fifteen
   inline stylesheets and were referenced by no markup anywhere in the repo —
   dead weight, ~370 bytes × 15. They are kept here once, conformed, because
   they are cheap now and the admin department screen may yet want them. */
.dept-row{display:flex;align-items:center;gap:11px;padding:8px 0;font-size:13.5px;font-weight:600}
.dept-bar{flex:1;height:6px;border-radius:0;background:var(--surface-3);overflow:hidden}
[data-theme="light"] .dept-bar{background:var(--surface-3)}
.dept-fill{height:100%;border-radius:0;background:var(--brand)}

/* ===================== 16. DEPARTMENT / FORMAT MARKERS ================== */
/* These replace an inline style that interpolated a colour straight out of the
   database into a style attribute:

       '<span class="badge" style="background:' + E(d.color) + '22;color:'
                                                + E(d.color) + '">'

   Memorize.esc() is an HTML escaper. It stops the value breaking OUT of the
   attribute, but it does nothing about CSS syntax INSIDE it, and style-src
   still carries 'unsafe-inline'. A departments.color of
   `red;position:fixed;inset:0;z-index:99999` was accepted and applied. The
   column is admin-writable rather than student-writable, so the severity is
   low, but it is an injection sink and it is now gone: the marker is a class,
   chosen deterministically from the department id, and nothing from the
   database reaches a stylesheet. */

.dmark{
  display:inline-block; width:11px; height:11px; flex:none;
  border:1px solid var(--text3); border-radius:2px;
  background-color:var(--surface-3); vertical-align:-1px;
}
.dp-1{background-image:var(--pat-hatch)}
.dp-2{background-image:var(--pat-cross)}
.dp-3{background-image:var(--pat-dot)}
.dp-4{background-image:var(--pat-grid)}
.dp-5{background-image:var(--pat-rule)}
.dp-6{background-image:var(--pat-stip)}
.badge .dmark{width:9px;height:9px;margin-inline-end:1px}
.di.dp-1,.di.dp-2,.di.dp-3,.di.dp-4,.di.dp-5,.di.dp-6{
  background-color:var(--surface-3); color:var(--text2); border:1px solid var(--line);
}

/* Course-format tiles. `t-blue`/`t-orange`/`t-green` come out of app.js's
   CATEGORY map and used to be three hues; they are three patterns now. */
.t-blue,.t-orange,.t-green{background-color:var(--surface-3);color:var(--text2)}
.t-blue{background-image:var(--pat-hatch)}
.t-orange{background-image:var(--pat-stip)}
.t-green{background-image:var(--pat-cross)}

/* ============================ 17. UTILITY ============================== */

.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.grid-4{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}
.flex{display:flex;align-items:center;gap:10px}
.flex-between{display:flex;align-items:center;justify-content:space-between;gap:10px}
/* R152.1 — .mt-24 was MISSING while .mb-24 existed, and course.html had been
   writing `<h2 class="mt-24" id="qdTopicsH">Topics</h2>` against it since the
   qbank layout landed. Measured in Chromium before the fix: margin-top 0px,
   so the Topics heading sat flush against the block above it with no space at
   all. Found by tools/audit-orphan-classes.js. An asymmetric utility set is
   exactly the shape that gets written confidently and never checked — if you
   add an .mt-N here, add the matching .mb-N. */
.mt-8{margin-top:8px}.mt-16{margin-top:16px}.mt-24{margin-top:24px}.mb-8{margin-bottom:8px}.mb-16{margin-bottom:16px}.mb-24{margin-bottom:24px}
.text2{color:var(--text2)}.text3{color:var(--text3)}.small{font-size:12.5px}
.pad{padding:18px}
@keyframes fadeUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.fu{animation:fadeUp .38s cubic-bezier(.2,.7,.3,1) both}
.d1{animation-delay:.05s}.d2{animation-delay:.10s}.d3{animation-delay:.15s}.d4{animation-delay:.20s}.d5{animation-delay:.25s}
.menu-btn{display:none}
.sidebar-close{display:none;width:32px;height:32px;font-size:13px;flex-shrink:0}
.nav-backdrop{
  position:fixed; inset:0; background:rgba(20,25,32,.55); z-index:45;
  opacity:0; pointer-events:none; transition:opacity .22s;
}
.nav-backdrop.show{opacity:1;pointer-events:auto}
@media(max-width:1000px){.grid-4{grid-template-columns:1fr 1fr}}
@media(max-width:760px){
  .sidebar{position:fixed;inset:0 auto 0 0;z-index:50;transform:translateX(-100%);transition:transform .22s ease;background:var(--surface)}
  .sidebar.open{transform:none;box-shadow:0 0 0 1px var(--line)}
  .menu-btn{display:flex}.sidebar-close{display:flex}
  /* R30: `.search-bar{display:none}` used to live here. It was written for the
     decorative topbar search, but the selector is global — it also removed the
     admin panel's WIRED student search and the enrol-modal people search on
     every phone, i.e. it deleted working features below 760px. Reflow instead
     of hide: topbar search drops to its own full-width row; panel/modal search
     bars shrink to fit. The doubled class + html prefix outranks the doubled
     utility classes in canvas.css (e.g. .u-w-240px.u-w-240px). */
  .topbar{flex-wrap:wrap}
  .topbar .search-bar{order:10;flex:1 1 100%}
  html .search-bar.search-bar{width:auto;max-width:100%;min-width:0;flex:1 1 150px;margin-left:0}
  .flex-between{flex-wrap:wrap}
  .grid-2,.grid-4{grid-template-columns:1fr}.content{padding:18px 16px 34px}
}

/* ========================= 18. THE LOGO LOCKUP ========================== */
/* The mark is a retention curve: two accent rises are the reviews, two ink
   decays are the forgetting between them. The second decay lands higher than
   the first, because each review buys a longer interval and leaves you above
   where you started. It also reads as a rising M. Shipped as SVG, which is
   why 32 base64 PNG lockups left the HTML in Pass 1. */

/* R26: min-width:0 + clipping on the name. The lockup sits in flex headers next
   to icon buttons on nearly every screen; white-space:nowrap with overflow
   visible meant that on any narrow header the wordmark did not shrink, it
   spilled OUT of its own box and the neighbouring icons drew on top of it
   (reported on the messages rail). Truncating is the correct failure. */
.lock{display:inline-flex;align-items:center;gap:10px;text-decoration:none;min-width:0}
.lock:hover{text-decoration:none}
.lock-mark{width:34px;height:29px;flex:none;position:relative}
.lock-mark img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
.lock-name{
  font-family:var(--fd); font-weight:600; font-size:19px;
  letter-spacing:-.02em; white-space:nowrap; color:var(--text); line-height:1.1;
  min-width:0; overflow:hidden; text-overflow:ellipsis;
}
.lock-name b{color:var(--brand);font-weight:600}
.sidebar-logo .lock-mark{width:30px;height:26px}
/* R37: the rail's header now carries the collapse handle as well as the lock,
   and at 17px "Memorize Medicine" overran it by ~19px. Sized down to fit with
   room to spare -- and given an ellipsis as a backstop, so a longer wordmark or
   a wider system font degrades to a truncation instead of printing the brand
   underneath a button. */
.sidebar-logo .lock-name{font-size:16px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* ==================== 18b. PUBLIC NAV (marketing pages) ================= */
/* R147 — owner sent a screen recording of faq.html / terms.html /
   privacy.html: the nav rendered as unstyled stacked links (no sticky bar,
   no burger fold, no right-aligned actions) and the footer columns
   stacked full-width with no gutter. Root cause: this whole block only
   ever lived inside index.html's own inline <style> — assets/app.css (the
   stylesheet every public page already links) never had it, so any public
   page other than index.html got the raw unstyled markup. Lives here now,
   once, so a new public page gets a working nav/footer for free instead of
   having to re-paste index.html's inline block (which is exactly the gap
   that let this ship in the first place — faq/terms/privacy in R145, then
   install-app.html repeated the same miss in R146). */
.pnav{display:flex;align-items:center;flex-wrap:wrap;gap:14px;padding:14px 5%;position:sticky;top:0;z-index:40;background:var(--paper);border-bottom:1px solid var(--stroke)}
.pnav-logo img{height:34px;display:block}
.pnav-links{display:none;gap:26px;margin-left:26px}
@media(min-width:860px){.pnav-links{display:flex}}
.pnav-links a{color:var(--text2);font-size:13px;font-weight:700;transition:color .15s}
.pnav-links a:hover{color:var(--text);text-decoration:none}
/* R30: below 860px the nav links were display:none with no menu — Courses /
   Departments / How-it-works were unreachable from a phone. The burger folds
   them out as a column inside the (already flex-wrap) nav. */
.pnav-burger{display:none;flex:none;width:34px;height:34px;border-radius:var(--r-sm);border:1px solid var(--stroke);background:none;color:var(--text2);cursor:pointer;align-items:center;justify-content:center;font-size:17px;line-height:1}
.pnav-burger:hover{border-color:var(--stroke-hi);color:var(--text)}
@media(max-width:859.98px){
  .pnav-burger{display:flex}
  .pnav.nav-open .pnav-links{display:flex;flex-direction:column;align-items:flex-start;gap:13px;order:9;flex-basis:100%;margin:4px 0 2px;padding-top:13px;border-top:1px solid var(--stroke)}
}
.pnav-right{margin-left:auto;display:flex;align-items:center;flex-wrap:wrap;gap:9px}

/* R158 — owner: a "Build my plan" entry on the homepage and catalog's own
   public nav, with "Checkout" living as a sub-item under it rather than as
   its own separate link (same request applied to the signed-in sidebar —
   see student.html/qbank.html's .nav-parent/.nav-sub, this is that same
   two-control idea — a real link that navigates plus a separate caret that
   only opens/closes a menu — adapted for a horizontal bar instead of a
   vertical list, so it needs its own small dropdown rather than reusing
   .nav-sub's max-height animation). */
.pnav-plan{position:relative;display:flex;align-items:stretch}
.pnav-plan-link{border-top-right-radius:0;border-bottom-right-radius:0}
.pnav-plan-caret{display:flex;align-items:center;justify-content:center;width:30px;flex:none;border:1px solid var(--stroke);border-left:none;border-top-right-radius:var(--r-sm);border-bottom-right-radius:var(--r-sm);background:none;color:var(--text2);cursor:pointer}
.pnav-plan-caret:hover{color:var(--text)}
.pnav-plan-caret .ic{width:14px;height:14px;transition:transform .15s ease}
.pnav-plan.is-open .pnav-plan-caret .ic{transform:rotate(180deg)}
.pnav-plan-menu{position:absolute;top:calc(100% + 8px);right:0;min-width:170px;background:var(--surface);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow);padding:6px;z-index:50}
.pnav-plan-menu a{display:block;padding:9px 10px;border-radius:8px;font-size:13px;font-weight:600;color:var(--text)}
.pnav-plan-menu a:hover{background:var(--surface-2);text-decoration:none}

/* ============== 19. INSTALL BANNER (app-store style, top of page) ======= */

.pwa-bar{
  display:flex; align-items:center; gap:12px;
  padding:10px 16px; background:var(--surface);
  border-bottom:1px solid var(--line); position:relative; z-index:60;
}
.pwa-bar[hidden]{display:none}
.pwa-bar-mark{width:34px;height:34px;flex:none;border-radius:var(--r-sm);overflow:hidden}
.pwa-bar-mark img{width:100%;height:100%;object-fit:cover;display:block}
.pwa-bar-txt{min-width:0;line-height:1.3}
.pwa-bar-txt b{display:block;font-family:var(--fd);font-size:14.5px;font-weight:600;color:var(--text)}
.pwa-bar-txt span{display:block;font-size:12.5px;color:var(--text2)}
.pwa-bar-get{
  margin-inline-start:auto; flex:none;
  background:var(--brand); border:1px solid var(--brand); color:var(--brand-on);
  font-family:var(--fb); font-weight:700; font-size:13px;
  padding:6px 16px; border-radius:var(--r-tag); cursor:pointer;
}
.pwa-bar-get:hover{background:var(--brand-ink);border-color:var(--brand-ink)}
.pwa-bar-x{
  flex:none; width:28px; height:28px; border-radius:50%;
  display:grid; place-items:center; color:var(--text3);
  background:transparent; border:0; cursor:pointer; line-height:1;
}
.pwa-bar-x:hover{background:var(--surface-3);color:var(--text)}
@media (display-mode:standalone){.pwa-bar{display:none}}
@media (max-width:520px){
  .pwa-bar{padding:9px 12px;gap:9px}
  .pwa-bar-txt span{display:none}
}

/* ============== 19b. DISCOUNT PROMO (banner + struck prices) ============ */
/* .promo-bar is injected by mountPromoBanner() in app.js when a discount is
   active — directly under #pwaBar where that exists, else at the top of the
   page. .promo-price/.promo-was/.promo-now are emitted by M.moneyHTML(): the
   old price struck through, the discounted price under it. */
.promo-bar{
  padding:9px 16px; text-align:center; position:relative; z-index:59;
  background:var(--accent); color:var(--accent-on);
  font-family:var(--fb); font-weight:700; font-size:13.5px; line-height:1.35;
  border-bottom:1px solid var(--line);
}
.promo-price{display:inline-flex;flex-direction:column;line-height:1.2;vertical-align:middle}
.promo-was{color:var(--text3);font-size:.72em;font-weight:600;text-decoration:line-through}
.promo-now{color:inherit}

/* ============ 19c. DEVICE MOCKUP (phone + tablet, install pages) ======== */
/* R148 — owner: "i want a phone/ipad/tablet view that shows our app and its
   icon for the install, it would be a good design." Shared here (not per-
   page) because it appears on both index.html's Download section and
   install-app.html — see §18b/§21's own comments on why shared chrome
   belongs in this file, not duplicated inline.

   Deliberately NOT a real screenshot: a screenshot goes stale the moment
   the dashboard is redesigned again (see R148's own dashboard/qbank
   investigation, and R145's whole redesign round before it) and is a much
   heavier asset to keep in sync across two device sizes and two themes. This
   is a small abstract suggestion of the app's UI — bars, cards, dotted rows
   in the real brand colors — built the same way the rest of this file draws
   icons and canvas objects: shapes, not imagery.

   Bezels are a fixed dark gradient in BOTH themes, deliberately not tied to
   --paper/--surface — a physical device's bezel is dark regardless of which
   theme the *page* is in, and re-tinting it to the light theme's pale
   surface would stop reading as a device at all. The screen content is
   likewise a fixed light gradient for the same reason: it represents a lit
   screen, not a themed panel. Only the icon badge's ring (var(--paper))
   and the accent colors used in the mini UI (var(--brand)/var(--accent)/
   var(--text3)/var(--line)) track the page theme, so the graphic still
   belongs to whichever theme it's viewed in without losing the "real
   object" read. */
.device-mock{
  position:relative; display:flex; align-items:flex-end; justify-content:center;
  height:210px; margin:0 auto 36px; padding-top:20px;
}
.dm-tablet{
  flex:none; width:236px; height:164px; padding:9px;
  background:linear-gradient(155deg,#2B2F38,#15171C);
  border-radius:16px; box-shadow:0 24px 48px -16px rgba(20,25,32,.45);
  transform:translateX(26px);
}
.dm-phone{
  position:relative; flex:none; width:100px; height:206px; padding:7px;
  background:linear-gradient(155deg,#2B2F38,#15171C);
  border-radius:22px; box-shadow:0 24px 48px -14px rgba(20,25,32,.5);
  transform:translateX(-34px);
}
.dm-notch{
  position:absolute; top:7px; left:50%; transform:translateX(-50%);
  width:32px; height:5px; border-radius:3px; background:#000; z-index:2;
}
.dm-screen{
  width:100%; height:100%; overflow:hidden; box-sizing:border-box;
  background:linear-gradient(165deg,#EEF2F9,#FFFFFF);
  display:flex; flex-direction:column; gap:7px;
}
.dm-tablet .dm-screen{border-radius:8px;padding:10px 12px}
.dm-phone .dm-screen{border-radius:16px;padding:14px 9px 9px;gap:6px}
.dm-bar{height:6px;border-radius:3px;background:var(--brand);opacity:.85;width:44%}
.dm-bar.dm-bar-lg{width:60%}
.dm-bar.dm-bar-dim{width:28%;opacity:.5;background:var(--text3)}
.dm-cards{display:flex;gap:5px;margin-top:2px}
.dm-cards span{flex:1;height:30px;border-radius:5px;background:#fff;
  border:1px solid var(--line);box-shadow:0 1px 2px rgba(20,25,32,.05)}
.dm-cards span.dm-card-hi{background:linear-gradient(135deg,var(--brand-soft),#fff)}
.dm-row{display:flex;gap:5px;align-items:center}
.dm-row .dm-dot{width:14px;height:14px;border-radius:4px;background:var(--accent);
  opacity:.85;flex:none}
.dm-row .dm-dot.dm-dot-brand{background:var(--brand)}
.dm-row .dm-dot.dm-dot-jade{background:var(--jade)}
.dm-row .dm-lines{flex:1;display:flex;flex-direction:column;gap:3px}
.dm-row .dm-lines i{display:block;height:3.5px;border-radius:2px;background:var(--line)}
.dm-row .dm-lines i:first-child{width:70%;background:var(--text3);opacity:.6}
.dm-row .dm-lines i:last-child{width:45%}
.dm-icon{
  position:absolute; left:50%; bottom:-16px; transform:translateX(50px);
  width:58px; height:58px; flex:none; z-index:3;
  border-radius:16px; border:3px solid var(--paper);
  box-shadow:0 12px 26px -8px rgba(20,25,32,.5);
}
.dm-icon img{width:100%;height:100%;display:block;border-radius:13px}
@media (max-width:520px){
  .device-mock{height:186px}
  .dm-tablet{width:200px;height:140px;transform:translateX(20px)}
  .dm-phone{width:88px;height:180px;transform:translateX(-28px)}
}

/* ================= 20. DOWNLOAD SECTION (landing page) ================== */

.getapp{text-align:center;padding:40px 28px}
.getapp h2{font-family:var(--fd);font-size:clamp(1.5rem,3vw,2rem);font-weight:600;margin-bottom:8px}
.getapp p{color:var(--text2);max-width:52ch;margin:0 auto 22px}
.getapp-row{display:flex;gap:12px;flex-wrap:wrap;justify-content:center}
.store-btn{
  display:inline-flex; align-items:center; gap:10px;
  padding:11px 20px; min-width:186px;
  background:var(--surface); border:1px solid var(--line); border-radius:var(--r-btn);
  color:var(--text); cursor:pointer; text-align:left;
  font-family:var(--fb); transition:border-color .15s,background .15s;
}
.store-btn:hover{border-color:var(--text3);background:var(--surface-2)}
.store-btn .sb-ico{font-size:20px;line-height:1;flex:none;color:var(--text2);display:flex}
.store-btn .sb-txt{line-height:1.25}
.store-btn .sb-txt small{
  display:block; font-family:var(--fm); font-size:9.5px; letter-spacing:.12em;
  text-transform:uppercase; color:var(--text3);
}
.store-btn .sb-txt b{display:block;font-size:14.5px;font-weight:600}
.store-btn.is-live{border-color:var(--brand)}
.store-btn.is-live:hover{border-color:var(--brand-ink);background:var(--brand-soft)}
.getapp-note{margin-top:16px;font-size:12.5px;color:var(--text3)}

/* =============== 20b. SHARED INSTALL CTA (app.js §7I) ===================
   One component (markup generated by memRenderInstallCta(), styled once,
   here) used on index.html, install-app.html and student.html alike — see
   app.js §7I for why the markup itself is generated in JS rather than
   hand-copied per page. A page opts in with an empty
   <div data-mem-install></div>; nothing about its look lives anywhere else. */
.mem-install-mount{text-align:center}
.dev-badges{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin-bottom:16px}
.dev-badge{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border-radius:var(--r-tag);background:var(--surface);border:1px solid var(--line);font-size:11.5px;font-weight:600;color:var(--text2)}
.dev-badge .ic{width:15px;height:15px;flex:none}
/* Task #52 — owner: "put a symbol represents the download instead of the
   word install" — the same ask index.html's own Task #32 already answered
   for its homepage button ("just icon represent download"). This extends
   that icon-only treatment to the shared component itself (app.js §7I's
   MEM_ICO_BTN was swapped for that SAME download-arrow path, so student.
   html's topbar and install-app.html's button now match index.html's
   button instead of it being the one exception). The "Install" span stays
   in the DOM, just visually clipped (sr-only technique, not display:none)
   — an icon-only control still needs a name for screen readers/keyboard
   users, per the identical reasoning already on index.html's own comment
   for its button. Padding drops from the base .btn's 16px sides to 9px so
   this reads as a square icon target, not a pill with empty space where
   the word used to sit. */
.install-btn{display:inline-flex;align-items:center;padding:9px}
.install-btn .ic{width:19px;height:19px}
.install-btn span{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}

/* the install-guide overlay — a bottom sheet on phones, a centered card from
   640px up. One element (#memInstallOv), content swapped per-guide by
   memShowIosGuide()/memShowGenericGuide() in app.js §7I. */
.mem-install-ov{
  position:fixed; inset:0; z-index:200;
  display:flex; align-items:flex-end; justify-content:center;
  background:rgba(10,14,20,.5);
  opacity:0; pointer-events:none; transition:opacity .2s;
}
.mem-install-ov.show{opacity:1;pointer-events:auto}
@media(min-width:640px){.mem-install-ov{align-items:center}}
.mem-install-card{
  position:relative; width:100%; max-width:400px; margin:0 0 0 0;
  background:var(--paper); border:1px solid var(--line);
  border-radius:var(--r-obj) var(--r-obj) 0 0;
  box-shadow:0 -12px 40px -12px rgba(0,0,0,.35);
  padding:26px 22px 22px;
  transform:translateY(24px); transition:transform .24s cubic-bezier(.2,.7,.3,1.1);
}
.mem-install-ov.show .mem-install-card{transform:translateY(0)}
@media(min-width:640px){
  .mem-install-card{border-radius:var(--r-obj);margin:16px;box-shadow:var(--shadow);transform:scale(.96)}
  .mem-install-ov.show .mem-install-card{transform:scale(1)}
}
.mem-install-close{
  position:absolute; top:12px; right:12px; width:30px; height:30px;
  border-radius:50%; border:1px solid var(--line); background:var(--surface);
  color:var(--text2); display:flex; align-items:center; justify-content:center;
  cursor:pointer; font-size:16px; line-height:1; padding:0;
}
.mem-install-close:hover{color:var(--text);border-color:var(--text3)}
.mem-install-anim{
  width:60px; height:60px; border-radius:var(--r-obj);
  background:var(--brand-soft); color:var(--brand);
  display:flex; align-items:center; justify-content:center;
  margin:0 auto 16px; animation:memBob 1.6s ease-in-out infinite;
}
.mem-install-anim .ic{width:28px;height:28px}
@keyframes memBob{0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)}}
.mem-install-card h3{font-size:17px;font-weight:700;margin:0 0 6px;text-align:center}
.mem-install-card .mem-install-sub{font-size:12.5px;color:var(--text3);text-align:center;margin-bottom:18px;line-height:1.5}
.mem-install-card ol{margin:0 0 18px;padding-left:20px;font-size:13.5px;line-height:1.85;color:var(--text2)}
.mem-install-card ol li b{color:var(--text);font-weight:600}
.mem-install-card ol .mem-inline-ic{display:inline-flex;width:14px;height:14px;vertical-align:-3px;margin:0 1px;color:var(--text2)}
.mem-install-more{display:block;text-align:center;font-size:12.5px;color:var(--brand);text-decoration:none;font-weight:600}
.mem-install-more:hover{text-decoration:underline}
@media (prefers-reduced-motion:reduce){.mem-install-anim{animation:none}}

/* ============================ 21. FOOTER =============================== */
/* R147 — same gap as the nav above: only border-color/typography lived here;
   the actual row layout (flex, padding, gutter, the 150px column floor) was
   inline-only in index.html, so every other public page's footer stacked
   full-width with no spacing. */

.foot{border-top:1px solid var(--line);padding:40px 5% 34px;max-width:1400px;margin:0 auto;display:flex;flex-wrap:wrap;gap:30px;justify-content:space-between;align-items:flex-start}
.foot .fcol{min-width:150px}
.foot .fcol b{
  display:block; font-family:var(--fm); font-size:12px; font-weight:600;
  letter-spacing:1.5px; text-transform:uppercase; color:var(--text3); margin-bottom:12px;
}
.foot .fcol a{display:block;color:var(--text2);font-size:13px;font-weight:600;padding:4px 0}
.foot .fcol a:hover{color:var(--text)}
/* Task #33/#34/#35-v1 (superseded by #35-v2 below) — owner: "the icons of
   the apps to appear," then "replace them with the real icons instead,"
   then "brand's color, remove their words, horizontal like biorender."
   Site-wide (assets/app.css, not a page-scoped override) because the
   footer markup itself is duplicated across every public page, not
   templated from one file — the alternative was five near-identical
   overrides.
   Task #35-v2 — the first pass put each icon on a solid var(--brand)
   circle; owner clarified the "biorender" reference meant biorender.com's
   OWN footer social-icon row specifically, which is flat monochrome line
   icons with no colour-badge shape behind them, sitting directly on the
   page. Confirmed by opening biorender.com and scrolling to its footer.
   Badges are gone — the SVGs use fill/stroke="currentColor" now, coloured
   by .foot .fcol .fic-row a{color:var(--brand)} the same way .ic/.is glyphs
   elsewhere pick up currentColor, just without that shared class (these are
   hand-authored, not run through the .ic/.ib/.is convention). The extra
   ".foot .fcol" prefix isn't decoration — .foot .fcol a{color:var(--text2)}
   two lines up outranks a bare ".fic-row a" on specificity (two classes vs
   one), which is why the icons first rendered in muted grey instead of
   brand blue; matching that selector's specificity is what makes the
   override actually win. Payment help/FAQ stay plain text links below (no
   .fic/.ic sizing needed for them). */
.fic-row{display:flex;gap:14px;margin:4px 0 10px}
.foot .fcol .fic-row a{display:flex;align-items:center;color:var(--brand);padding:0}
.foot .fcol .fic-row a:hover{color:var(--brand-ink)}
.fic-row svg{width:20px;height:20px;display:block}
.foot-bottom{padding:18px 5% 30px;text-align:center;font-size:12px;color:var(--text3);border-top:1px solid var(--line-2)}

/* ========================== 22. ACCESSIBILITY ========================== */
/* prefers-reduced-motion is handled in §3 by shortening durations rather than
   setting `animation:none`, which is safer: several panels in this app are
   revealed by an animation's end state, and killing the animation outright can
   leave them at opacity 0. */

@media (prefers-contrast:more){
  :root,[data-theme="light"]{--line:currentColor;--line-2:currentColor}
  .btn,.glass,.store-btn,.badge{border-width:1.5px}
  .ic .ib{opacity:.34}
}

/* ======================= 20. PRINT (Pass 3) ============================
   POLICY, set by the owner: flashcards and questions are protected content and
   must not print. Lecture PDFs may print, WITH the student's own annotations
   and notes — those are their work, they asked for them to be kept.

   READ THIS BEFORE RELYING ON IT. A print stylesheet is FRICTION, NOT
   PROTECTION. `@media print{display:none}` stops an honest student pressing
   Ctrl+P. It does nothing against a screenshot, "Save as PDF" with CSS
   disabled, reader mode, devtools, or a phone camera. Anyone determined has the
   content inside a minute. If real protection is wanted, it is server-side —
   per-student watermarking, short-lived signed URLs, rendering that never
   delivers a full page at once — and it is not a design pass.

   Nothing here uses !important. The suppression selectors are specific enough
   to win on their own, and the ban still holds.
   ===================================================================== */
@media print {

  /* --- shared: chrome never prints ------------------------------------ */
  .atmos, .toast, .nav, .topbar, .sidebar, .lnav, .lmain > .ltop,
  .theme-toggle, .themebtn, .m-modal-ov, .demo-badge,
  nav, aside, footer .nav-links {
    display: none;
  }
  html body {
    background: #fff;
    color: #000;
  }
  /* Scrolling panes must unwrap or their contents are guillotined at the box
     edge — the single most common way a screen print loses half its content. */
  html body .scroll, html body .pane, html body .lstage, html body .tbl-wrap {
    overflow: visible;
    max-height: none;
  }
  a[href]::after { content: ''; }        /* no URL footnotes on paper */

  /* --- protected content: refuse to print ---------------------------- */
  /* One page-filling notice replaces the body content. Done with a wrapper
     rule rather than `* {display:none}` so the notice itself survives. */
  html body.noprint > * { display: none; }
  html body.noprint::before {
    content: 'This material is protected and is not available to print. Your lecture PDFs can be printed from the lecture page.';
    display: block;
    font: 15px/1.6 Georgia, 'Times New Roman', serif;
    max-width: 46em;
    margin: 4em auto;
    text-align: center;
  }
}

/* ================= R31: focus-page navigation ========================== */
/* breadcrumb strip in the focus top bar (Dashboard › Course › <title>) */
.ltop-crumbs{display:flex;align-items:center;gap:7px;font-family:var(--fm);
  font-size:11px;font-weight:500;color:var(--text3);white-space:nowrap;flex-shrink:0}
.ltop-crumbs a{color:var(--text3);text-decoration:none;transition:color .15s}
.ltop-crumbs a:hover{color:var(--text)}
.crumb-sep{opacity:.55;user-select:none}
@media(max-width:760px){.ltop-crumbs{display:none}}

/* the injected drawer reuses .sidebar styles but must be off-canvas at
   EVERY width — focus pages have no .app flex shell for it to sit in */
.sidebar.sidebar--drawer{position:fixed;inset:0 auto 0 0;z-index:50;
  transform:translateX(-100%);transition:transform .22s ease;
  background:var(--surface);border-right:1px solid var(--line)}
.sidebar.sidebar--drawer.open{transform:none;box-shadow:0 0 0 1px var(--line)}
.sidebar--drawer .sidebar-close{display:flex}
.menu-btn--focus{display:flex;flex-shrink:0}

/* R31 #6: course.html#lesson-<id> return anchor — clear the sticky chrome
   and flash the row the student just left so the return is legible */
.lesson{scroll-margin-top:88px}
.lesson.hl{background:var(--brand-soft);transition:background 1.2s ease}

/* ═══ R60 — THE hidden ATTRIBUTE MUST ACTUALLY HIDE ═══════════════════════════
   [hidden] is display:none in the UA stylesheet, and ANY author rule with a
   display beats it — .btn sets display:inline-flex, so every
   `someButton.hidden = true` in the codebase was a no-op on a .btn. That is
   why Settings showed "Turn on notifications" and "Turn them off" side by
   side: paint() had correctly hidden one of them and the stylesheet had
   quietly un-hidden it.

   !important is load-bearing here rather than lazy. The whole point is to beat
   every display declaration in this file, present and future, including the
   utility classes; a plain [hidden]{display:none} would lose to .btn again on
   specificity. Placed at the end of the file so it also beats anything with
   equal specificity declared above it. */
[hidden]{display:none !important}
