/* ==========================================================================
   Keen design system

   Notes on the choices here, since "why does this look generic" is a fair
   question to ask of any interface:

   - TYPE. Two families doing different jobs. Bricolage Grotesque sets the
     headings: it has real quirks in the letterforms, which is the point --
     the default geometric sans is what every AI product ships and it reads
     as templated. Inter is the body face because it was drawn for screens at
     small sizes. Both fall back to the system stack so the app still looks
     deliberate offline.
   - SPACE. Everything is on a 4px rhythm. Generic interfaces usually fail on
     spacing before they fail on colour: inconsistent gaps read as careless.
   - DEPTH. Surfaces use a hairline top highlight plus a soft shadow rather
     than flat borders alone, so cards feel lit rather than drawn.
   - COLOUR. One accent gradient used sparingly. Slop looks like slop partly
     because accent colour gets sprayed everywhere; here it marks exactly one
     primary action per screen.
   ========================================================================== */

/* Fonts are loaded by a <link> in index.html, deliberately NOT by @import here.
   An @import makes the browser fetch Google Fonts before applying ANY of this
   stylesheet, so if that request is slow or blocked the whole app renders
   unstyled. School and campus networks block Google Fonts routinely, and this
   app is aimed at students on exactly those networks. Loaded separately, a
   missing font costs you the typeface and nothing else. */

:root {
  /* ============================ THEME TOKENS ============================
     Moved off near-black + violet gradient. That palette reads as "AI app"
     because every AI product shipped it in the same eighteen months, and a
     study tool that looks like a chatbot gets judged like one.

     This is warm paper and ink with a single amber accent. Amber because
     sharpening is friction and heat, and because almost nothing in edtech
     uses it -- the category is wall-to-wall blue.
     ==================================================================== */

  /* Light is the default: students study in daylight, and a bright surface
     reads as "notebook" rather than "terminal". */
  --bg: #faf7f2;
  --bg-elevated: #ffffff;
  --surface: #ffffff;
  --surface-solid: #ffffff;
  --surface-hover: #f4efe7;
  --border: #e6ded1;
  --border-bright: #d4c8b6;

  --text: #1c1917;
  --text-secondary: #57534e;
  --text-muted: #8b8378;

  --accent: #c2410c;
  --accent-2: #ea580c;
  --accent-soft: #c2410c14;
  --accent-glow: #c2410c33;
  --accent-contrast: #ffffff;
  --gradient: linear-gradient(135deg, #ea580c 0%, #c2410c 100%);
  --gradient-warm: linear-gradient(135deg, #f59e0b 0%, #c2410c 100%);

  --good: #15803d;
  --good-soft: #15803d14;
  --bad: #b91c1c;
  --bad-soft: #b91c1c14;
  --warn: #a16207;

  /* Generous radii. Sharp corners plus dark surfaces is exactly the look
     we are moving away from. */
  --r-sm: 12px;
  --r: 18px;
  --r-lg: 24px;
  --r-xl: 32px;

  /* Soft, warm-tinted shadows rather than pure black: black shadows on a
     warm background look muddy. */
  --shadow-sm: 0 1px 2px rgba(60, 40, 20, .06);
  --shadow: 0 4px 20px -6px rgba(60, 40, 20, .12);
  --shadow-lg: 0 24px 60px -18px rgba(60, 40, 20, .22);

  --ease: cubic-bezier(.4, 0, .2, 1);
  --spring: cubic-bezier(.34, 1.56, .64, 1);

  --nav-h: 64px;

  /* ---- per-subject colour ----
     A colour per subject makes a course grid scannable at a glance, which
     matters far more once the home screen IS the course list. */
  --subj-math: #1d4ed8;
  --subj-science: #047857;
  --subj-english: #b45309;
  --subj-social: #be123c;
  --subj-language: #6d28d9;
  --subj-arts: #a21caf;
  --subj-cs: #0e7490;
  --subj-business: #0f766e;
  --subj-health: #c2410c;
  --subj-engineering: #475569;
  --subj-other: #78716c;
}

/* Dark theme. Warm charcoal, not blue-black: keeping the same warm hue
   family across both themes is what stops the dark mode feeling like a
   different product. */
:root[data-theme="dark"] {
  --bg: #17140f;
  --bg-elevated: #211d16;
  --surface: #211d16;
  --surface-solid: #211d16;
  --surface-hover: #2b261d;
  --border: #38312610;
  --border: #382f26;
  --border-bright: #4d4235;

  --text: #f5f0e8;
  --text-secondary: #bdb3a4;
  --text-muted: #8a8072;

  --accent: #fb923c;
  --accent-2: #f59e0b;
  --accent-soft: #fb923c1f;
  --accent-glow: #fb923c44;
  --accent-contrast: #1c1206;
  --gradient: linear-gradient(135deg, #fb923c 0%, #f59e0b 100%);
  --gradient-warm: linear-gradient(135deg, #fbbf24 0%, #fb923c 100%);

  --good: #4ade80;
  --good-soft: #4ade8018;
  --bad: #f87171;
  --bad-soft: #f8717118;
  --warn: #fbbf24;

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .4);
  --shadow: 0 4px 20px -6px rgba(0, 0, 0, .5);
  --shadow-lg: 0 24px 60px -18px rgba(0, 0, 0, .7);

  --subj-math: #60a5fa;
  --subj-science: #34d399;
  --subj-english: #fbbf24;
  --subj-social: #fb7185;
  --subj-language: #a78bfa;
  --subj-arts: #e879f9;
  --subj-cs: #22d3ee;
  --subj-business: #2dd4bf;
  --subj-health: #fb923c;
  --subj-engineering: #94a3b8;
  --subj-other: #a8a29e;
}

* { box-sizing: border-box; }
*:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  min-height: 100dvh;
  background: var(--bg);
  background-image:
    radial-gradient(900px 500px at 12% -8%, color-mix(in srgb, var(--accent) 7%, transparent), transparent 60%),
    radial-gradient(800px 500px at 92% 4%, color-mix(in srgb, var(--accent-2) 6%, transparent), transparent 55%);
  background-attachment: fixed;
  /* iOS Safari does not implement background-attachment: fixed properly. It
     sizes the image to the *whole document* rather than the viewport, so on a
     long page the gradients render magnified and the app looks like it has
     slowly zoomed in. Touch devices get a scrolling background instead; the
     gradients are decorative, so nothing is lost. */
  color: var(--text);
  font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  font-size: 15.5px;
  /* Looser leading and a hair of tracking. Cramped text is one of the strongest
     "generated by a machine" signals in an interface. */
  line-height: 1.72;
  letter-spacing: .003em;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

@media (hover: none) and (pointer: coarse) {
  body { background-attachment: scroll; }
}

h1, h2, h3, h4, .display {
  font-family: 'Bricolage Grotesque', -apple-system, BlinkMacSystemFont, sans-serif;
  font-weight: 700;
  letter-spacing: -0.025em;
  line-height: 1.16;
  margin: 0 0 .6rem;
}
h1 { font-size: clamp(2rem, 5vw, 3.4rem); font-weight: 800; }
h2 { font-size: 1.4rem; }
h3 { font-size: 1.08rem; }
p { margin: 0 0 1.1rem; }

/* Section header: title, optional count, optional action. Gives long pages a
   rhythm instead of a wall of identical cards. */
.section-head {
  display: flex; align-items: center; gap: .7rem;
  margin: 2.25rem 0 1rem;
}
.section-head:first-child { margin-top: 0; }
.section-head h2 { margin: 0; }
.section-icon {
  width: 30px; height: 30px; border-radius: 9px; flex-shrink: 0;
  display: grid; place-items: center; font-size: .95rem;
  background: var(--accent-soft); border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent); color: var(--accent);
}
.count-badge {
  font-size: .72rem; font-weight: 700; padding: .18rem .5rem;
  border-radius: 999px; background: var(--surface-hover);
  color: var(--text-secondary); border: 1px solid var(--border);
}
.section-head .spacer { margin-left: auto; }
a { color: var(--accent); }

.muted { color: var(--text-secondary); }
.dim { color: var(--text-muted); font-size: .85rem; }
.hidden { display: none !important; }
.center { text-align: center; }

/* Small caps label used for metadata rows */
.label {
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* ============================ layout ============================ */
.shell { max-width: 1120px; margin: 0 auto; padding: 2rem 1.25rem 6rem; }
.narrow { max-width: 480px; margin: 0 auto; }
.view { animation: rise .32s var(--ease); }
@keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes pop { from { opacity: 0; transform: scale(.94); } to { opacity: 1; transform: none; } }

/* ============================ top bar ============================ */
.topbar {
  position: sticky; top: 0; z-index: 40;
  height: var(--nav-h);
  display: flex; align-items: center; gap: 1rem;
  padding: 0 1.25rem;
  background: #0a0b10cc;
  backdrop-filter: saturate(160%) blur(14px);
  border-bottom: 1px solid var(--border);
}
.brand {
  display: flex; align-items: center; gap: .6rem;
  font-family: 'Bricolage Grotesque', sans-serif; font-weight: 800; font-size: 1.15rem;
  letter-spacing: -.03em; color: var(--text); text-decoration: none;
  white-space: nowrap;
}
/* The Keen mark. Was a tilted amber tile; it is now the graph.
   Keep this geometry identical to the favicon in index.html. */
.brand-mark {
  width: 27px; height: 27px; flex-shrink: 0;
  overflow: visible;
}
/* The axes are the fixed part, so they sit back. The data line is the subject
   of the mark and carries full weight. */
.brand-axes { stroke: var(--accent); opacity: .38; }
.brand-line { stroke: var(--accent); }

/* ---- icons ---------------------------------------------------------------
   The nav used text glyphs (☺ ▦ ◱ ＋). Those are font-dependent, so they drew
   differently on every device and some phones substituted colour emoji, which
   is why the profile icon looked like an emoji. Real SVG, drawn by us. */
.mn-icon { width: 21px; height: 21px; display: block; fill: currentColor; }
.mn-icon.mn-stroke {
  fill: none; stroke: currentColor; stroke-width: 1.9;
  stroke-linecap: round; stroke-linejoin: round;
}

/* A tappable row that navigates somewhere. Used for Plan and Settings on the
   profile page now that they no longer have mobile nav slots. */
.row-link {
  display: flex; align-items: center; gap: .85rem; width: 100%;
  background: var(--surface-solid); border: 1px solid var(--border);
  border-radius: var(--r); padding: .9rem 1rem;
  font-family: inherit; font-size: .95rem; color: var(--text);
  text-align: left; cursor: pointer; min-height: 60px;
  transition: background .18s var(--ease), border-color .18s var(--ease);
}
.row-link:hover { background: var(--surface-hover); border-color: var(--accent-soft); }
.row-link-icon {
  width: 22px; height: 22px; flex-shrink: 0; fill: none;
  stroke: var(--accent); stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round;
}
.row-link-main { display: flex; flex-direction: column; gap: .1rem; flex: 1; min-width: 0; }
.row-link-main .dim { font-size: .82rem; }
.row-link-go { color: var(--text-secondary); font-size: 1.3rem; line-height: 1; }

/* ---- study set editor ----------------------------------------------------
   Rebuilt because the old editor was three unlabelled inputs in a row. The
   fixes here are all about knowing where you are: a numbered card, a persistent
   label above each field rather than a placeholder that disappears, a live
   readiness count, and a visible marker on a half-filled row. */
.set-head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap; margin-bottom: 1.25rem;
}
.set-progress {
  font-size: .85rem; font-weight: 600;
  padding: .35rem .8rem; border-radius: 999px;
  background: var(--surface); border: 1px solid var(--border);
  color: var(--text-secondary); white-space: nowrap;
}
.set-progress.is-ready {
  background: color-mix(in srgb, var(--good) 12%, transparent);
  border-color: color-mix(in srgb, var(--good) 40%, transparent);
  color: var(--good);
}

.field { display: block; }
.field-label { display: block; font-size: .85rem; font-weight: 600; margin-bottom: .35rem; }
.field-help { display: block; font-size: .8rem; color: var(--text-secondary); margin-top: .4rem; line-height: 1.5; }

.card-editor-row {
  display: flex; align-items: flex-start; gap: .75rem;
  background: var(--surface-solid); border: 1px solid var(--border);
  border-radius: var(--r); padding: .9rem 1rem;
  transition: border-color .18s var(--ease);
}
/* One field filled and the other empty means the card will be silently dropped
   on save. Say so now, in place, instead of after the fact. */
.card-editor-row.is-partial { border-color: color-mix(in srgb, var(--bad) 45%, transparent); }
.cr-num {
  flex: 0 0 auto; width: 26px; height: 26px; display: grid; place-items: center;
  border-radius: 8px; background: var(--surface-hover);
  font-size: .8rem; font-weight: 700; color: var(--text-secondary); margin-top: 1.35rem;
}
.cr-fields { flex: 1; min-width: 0; display: grid; grid-template-columns: 1fr 1.4fr; gap: .7rem; }
.cr-field { display: block; min-width: 0; }
.cr-label {
  display: block; font-size: .72rem; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-secondary); margin-bottom: .25rem;
}
.cr-field input { width: 100%; }
.cr-del {
  flex: 0 0 auto; width: 34px; height: 34px; margin-top: 1.1rem;
  display: grid; place-items: center;
  border: 1px solid transparent; border-radius: 9px; background: transparent;
  color: var(--text-secondary); font-size: 1.15rem; line-height: 1; cursor: pointer;
  transition: all .16s var(--ease);
}
.cr-del:hover { background: color-mix(in srgb, var(--bad) 12%, transparent); color: var(--bad); }

.set-actions { display: flex; gap: .6rem; align-items: center; margin-top: 1.5rem; flex-wrap: wrap; }

@media (max-width: 620px) {
  /* Side by side, the two fields are each about 130px on a phone, which is not
     enough to read a definition you are editing. Stack them. */
  .cr-fields { grid-template-columns: 1fr; gap: .55rem; }
  .cr-num { margin-top: 1.5rem; }
  /* 34px is below the 44px minimum, and this is the DELETE control. A
     too-small destructive target is the worst combination: you miss it when
     you want it and hit it when you do not. */
  .cr-del { margin-top: 1.5rem; width: 44px; height: 44px; }
  .set-actions .btn { flex: 1 1 100%; }
}

/* ---- device-appropriate instructions -------------------------------------
   "Click or press Space to flip" is nonsense on a phone: there is no click and
   no Space key. Rather than write for the average device and be wrong on both,
   ship both strings and let the media query pick. */
.on-touch { display: none; }
@media (hover: none) and (pointer: coarse) {
  .on-pointer { display: none; }
  .on-touch { display: inline; }
}

/* ---- group invite code ---------------------------------------------------
   The countdown is part of the control, not decoration. It turns amber in the
   last 30 seconds so you know to regenerate before you start reading the code
   out to somebody. */
.invite-box {
  display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;
  margin: 1rem 0;
}
.invite-meta { display: flex; align-items: center; gap: .7rem; flex-wrap: wrap; }
.invite-timer {
  font-variant-numeric: tabular-nums;
  font-size: .85rem; font-weight: 600; color: var(--text-secondary);
  white-space: nowrap;
}
.invite-timer.is-urgent { color: var(--warn, #d97706); }
.invite-timer.is-dead { color: var(--text-secondary); }
.code-display.is-empty { opacity: .35; letter-spacing: .3em; }

/* ---- flashcard swipe -----------------------------------------------------
   The card follows the finger, so it needs no transition while dragging and a
   spring back when released. touch-action:pan-y lets the page still scroll
   vertically while horizontal drags belong to the card. */
.flash-card { touch-action: pan-y; }
.flash-card.is-dragging { transition: none; cursor: grabbing; }
/* A coloured edge appears once the drag passes the commit threshold, so you
   know which way it will land before you let go. */
.flash-card[data-swipe="right"] { box-shadow: 0 0 0 3px var(--good), var(--shadow-lg); }
.flash-card[data-swipe="left"]  { box-shadow: 0 0 0 3px var(--bad), var(--shadow-lg); }
.swipe-legend { display: flex; justify-content: center; gap: 1.4rem; margin-top: .6rem; }
.swipe-legend span { font-size: .82rem; color: var(--text-secondary); }
.swipe-legend b { font-weight: 650; }
.swipe-legend .sl-know { color: var(--good); }
.swipe-legend .sl-retest { color: var(--bad); }

/* Avatar artwork. Stroke rather than fill so the shapes stay legible small and
   pick up whatever colour the container sets. */
.avatar-art {
  width: 62%; height: 62%; display: block;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}
.avatar-opt .avatar-art, #avatar .avatar-art { width: 58%; height: 58%; }
#profile-avatar .avatar-art { stroke-width: 1.5; }
.avatar-opt, #profile-avatar, #avatar { display: grid; place-items: center; }

.nav { display: flex; gap: .15rem; margin-left: .75rem; }
.nav-btn {
  position: relative;
  background: none; border: 0; cursor: pointer;
  font-family: inherit; font-size: .9rem; font-weight: 500;
  color: var(--text-secondary);
  padding: .5rem .85rem; border-radius: var(--r-sm);
  transition: color .18s var(--ease), background .18s var(--ease);
}
.nav-btn:hover { color: var(--text); background: var(--surface-hover); }
.nav-btn.active { color: var(--text); background: var(--surface-hover); }
.nav-btn.active::after {
  content: ''; position: absolute; left: 50%; bottom: -1px;
  width: 18px; height: 2px; border-radius: 2px; background: var(--gradient);
  transform: translateX(-50%);
}
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: .6rem; }

.streak {
  display: flex; align-items: center; gap: .35rem;
  font-size: .82rem; font-weight: 600;
  padding: .35rem .7rem; border-radius: 999px;
  background: #fb923c18; color: #fbbf24; border: 1px solid #fb923c33;
}
.avatar {
  width: 32px; height: 32px; border-radius: 50%;
  background: var(--gradient); color: #fff;
  display: grid; place-items: center;
  font-weight: 700; font-size: .82rem;
  border: 0; cursor: pointer;
  font-family: 'Bricolage Grotesque', sans-serif;
}

/* ============================ cards ============================ */
.card {
  position: relative;
  background: var(--surface-solid);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 1.5rem;
  box-shadow: var(--shadow-sm);
}
/* hairline highlight along the top edge: makes the surface read as lit */
.card::before {
  content: ''; position: absolute; inset: 0 0 auto; height: 1px;
  background: linear-gradient(90deg, transparent, #ffffff12, transparent);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
}
.card + .card { margin-top: 1rem; }
.card-pad-lg { padding: 2rem; }

.stack { display: flex; flex-direction: column; gap: 1rem; }
.row { display: flex; align-items: center; gap: .75rem; }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; }
.grid-3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; }
.grid-4 { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: .85rem; }

/* ============================ buttons ============================ */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
  font-family: inherit; font-size: .92rem; font-weight: 600;
  color: var(--text);
  background: var(--surface-hover);
  border: 1px solid var(--border-bright);
  border-radius: var(--r);
  padding: .68rem 1.2rem;
  cursor: pointer;
  transition: transform .16s var(--spring), background .18s var(--ease), border-color .18s var(--ease);
  min-height: 44px; /* comfortable touch target */
}
.btn:hover:not(:disabled) { background: #242841; transform: translateY(-1px); }
.btn:active:not(:disabled) { transform: translateY(0) scale(.985); }
.btn:disabled { opacity: .45; cursor: not-allowed; }

.btn-primary {
  background: var(--gradient); border-color: transparent; color: #fff;
  box-shadow: 0 6px 20px -6px var(--accent-glow);
}
.btn-primary:hover:not(:disabled) { background: var(--gradient); filter: brightness(1.08); }
.btn-ghost { background: transparent; border-color: transparent; color: var(--text-secondary); }
.btn-ghost:hover:not(:disabled) { background: var(--surface-hover); color: var(--text); }
/* .btn-secondary was used in the markup but never defined, so Sign out in
   Settings rendered as a raw browser button: Arial, grey, square, 2px outset
   border. It is the destructive-ish action on the page, so it gets a quiet
   outline rather than a filled button. */
.btn-secondary {
  display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
  font: inherit; font-weight: 600; line-height: 1;
  padding: .6rem 1.1rem; min-height: 44px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
  transition: all .18s var(--ease);
}
.btn-secondary:hover:not(:disabled) {
  background: var(--surface-hover);
  border-color: color-mix(in srgb, var(--bad) 45%, var(--border));
  color: var(--bad);
}
.btn-secondary:disabled { opacity: .45; cursor: not-allowed; }

.btn-block { width: 100%; }
.btn-sm { padding: .45rem .8rem; font-size: .84rem; min-height: 36px; border-radius: var(--r-sm); }
.btn-lg { padding: .9rem 1.6rem; font-size: 1rem; border-radius: var(--r); }

.linkish {
  background: none; border: 0; padding: 0; cursor: pointer;
  font-family: inherit; font-size: .86rem; font-weight: 500;
  color: var(--accent); text-align: left;
}
.linkish:hover { color: var(--accent-2); text-decoration: underline; }
/* A link-styled button is still a tap target. At 17px tall these were well
   under the 44px minimum, which on a phone means you hit them by luck. Padding
   is vertical only, so the text stays visually flush with the sentence around
   it while the hit area extends above and below. */
@media (hover: none) {
  .linkish { padding: .7rem 0; min-height: 44px; }
}

/* ============================ forms ============================ */
.form { display: flex; flex-direction: column; gap: .9rem; }
label { display: flex; flex-direction: column; gap: .4rem; font-size: .82rem; font-weight: 500; color: var(--text-secondary); }
input[type=text], input[type=email], input[type=password], input[type=number], input[type=search], select {
  font-family: inherit; font-size: .95rem;
  background: #0d0f1a;
  border: 1px solid var(--border);
  color: var(--text);
  padding: .78rem .9rem; border-radius: var(--r);
  transition: border-color .18s var(--ease), box-shadow .18s var(--ease);
  min-height: 46px;
  width: 100%;
}
input::placeholder { color: var(--text-muted); }

/* ---- autofill ----
   When Chrome autofills a field it paints its own near-white background and
   near-black text via the -webkit-autofill pseudo-class, which overrides the
   dark styling above. On this dark theme that produced a glaring white email
   box next to correctly-styled dark fields.

   background-color cannot be overridden directly on autofilled inputs, so the
   standard workaround is an inset box-shadow large enough to cover the field,
   plus -webkit-text-fill-color for the text. The long transition delay stops
   the white flashing in for a frame before the shadow paints. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active,
select:-webkit-autofill,
textarea:-webkit-autofill {
  -webkit-box-shadow: 0 0 0 1000px #0d0f1a inset !important;
  box-shadow: 0 0 0 1000px #0d0f1a inset !important;
  -webkit-text-fill-color: var(--text) !important;
  caret-color: var(--text);
  border: 1px solid var(--border) !important;
  -webkit-background-clip: text;
  background-clip: text;
  transition: background-color 6000s ease-in-out 0s, color 6000s ease-in-out 0s;
}
input:-webkit-autofill:focus {
  border-color: var(--accent) !important;
  -webkit-box-shadow: 0 0 0 1000px #0d0f1a inset, 0 0 0 3px var(--accent-soft) !important;
  box-shadow: 0 0 0 1000px #0d0f1a inset, 0 0 0 3px var(--accent-soft) !important;
}
/* Firefox uses its own autofill flag. */
input:autofill { background: #0d0f1a; color: var(--text); }
input:focus, select:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.error { color: var(--bad); font-size: .85rem; margin: .6rem 0 0; }
.notice { color: var(--good); font-size: .85rem; margin: .6rem 0 0; }
.hint { color: var(--text-muted); font-size: .8rem; margin: .4rem 0 0; }

.segmented { display: flex; gap: .25rem; padding: .25rem; background: #0d0f1a; border: 1px solid var(--border); border-radius: var(--r); }
.segmented button {
  flex: 1; background: none; border: 0; cursor: pointer;
  font-family: inherit; font-size: .88rem; font-weight: 600; color: var(--text-secondary);
  padding: .55rem; border-radius: var(--r-sm);
  transition: all .18s var(--ease);
}
.segmented button.active { background: var(--surface-hover); color: var(--text); }

/* ============================ landing ============================ */
.hero {
  display: grid; grid-template-columns: 1.05fr .95fr;
  gap: 4rem; align-items: center;
  min-height: calc(100dvh - var(--nav-h) - 4rem);
  padding: 2rem 0;
}
.eyebrow {
  display: inline-flex; align-items: center; gap: .5rem;
  font-size: .78rem; font-weight: 600; letter-spacing: .01em;
  padding: .4rem .85rem; border-radius: 999px;
  background: var(--accent-soft); color: var(--accent);
  border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
  margin-bottom: 1.5rem;
}
.hero h1 { margin-bottom: 1.1rem; }
.hero h1 .grad {
  background: var(--gradient); -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
}
.lede { font-size: 1.1rem; color: var(--text-secondary); max-width: 44ch; margin-bottom: 2rem; }

.feature-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 1.1rem; }
.feature { display: flex; gap: .9rem; align-items: flex-start; }
.feature-icon {
  flex-shrink: 0; width: 34px; height: 34px; border-radius: 10px;
  display: grid; place-items: center; font-size: 1rem;
  background: var(--accent-soft); border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
}
.feature strong { display: block; font-weight: 600; font-size: .95rem; margin-bottom: .1rem; }
.feature span { font-size: .88rem; color: var(--text-secondary); line-height: 1.5; }

.auth-card { animation: pop .4s var(--spring); }

/* ============================ onboarding ============================ */
.onboard { max-width: 720px; margin: 0 auto; }

/* ---- locked, full-screen onboarding ----
   While onboarding is pending the top bar is hidden entirely (JS also hides the
   nav buttons, and showView refuses to leave). Hiding chrome in CSS alone would
   not be enough: keyboard users could still tab to an invisible nav button. */
body.onboarding-lock .topbar { display: none; }
body.onboarding-lock #main {
  min-height: 100dvh; display: grid; align-items: center;
  padding-top: 2rem; padding-bottom: 2rem;
}
body.onboarding-lock .onboard { width: 100%; animation: rise .4s var(--ease); }
/* A soft spotlight so the step feels like its own screen. */
body.onboarding-lock::before {
  content: ''; position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background: radial-gradient(60rem 40rem at 50% -10%, var(--accent-soft), transparent 70%);
}
@media (max-width: 640px) {
  body.onboarding-lock #main { align-items: start; padding-top: 1.5rem; }
}
.steps { display: flex; gap: .4rem; margin-bottom: 2rem; }
.step-dot { flex: 1; height: 4px; border-radius: 2px; background: var(--border); transition: background .3s var(--ease); }
.step-dot.done { background: var(--gradient); }

.choice-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: .7rem; margin: 1.25rem 0; }
.choice {
  background: var(--surface-solid); border: 1.5px solid var(--border);
  border-radius: var(--r); padding: 1.1rem .9rem; cursor: pointer;
  font-family: inherit; color: var(--text); text-align: center;
  transition: all .2s var(--ease);
}
.choice:hover { border-color: var(--border-bright); background: var(--surface-hover); transform: translateY(-2px); }
.choice.selected { border-color: var(--accent); background: var(--accent-soft); box-shadow: 0 0 0 3px var(--accent-soft); }
.choice-title { font-family: 'Bricolage Grotesque', sans-serif; font-weight: 700; font-size: 1.05rem; display: block; }
.choice-sub { font-size: .78rem; color: var(--text-muted); }

.course-search { position: relative; margin-bottom: 1rem; }
.course-list { max-height: 380px; overflow-y: auto; display: flex; flex-direction: column; gap: .4rem; padding-right: .25rem; }
.course-list::-webkit-scrollbar { width: 8px; }
.course-list::-webkit-scrollbar-thumb { background: var(--border-bright); border-radius: 4px; }

.course-row {
  display: flex; align-items: center; gap: .75rem;
  padding: .7rem .9rem; border-radius: var(--r);
  background: var(--surface-solid); border: 1.5px solid var(--border);
  cursor: pointer; text-align: left; font-family: inherit; color: var(--text);
  transition: all .16s var(--ease); width: 100%;
}
.course-row:hover { border-color: var(--border-bright); background: var(--surface-hover); }
.course-row.selected { border-color: var(--accent); background: var(--accent-soft); }
.course-row-name { flex: 1; font-weight: 500; font-size: .92rem; }
.course-check {
  width: 20px; height: 20px; border-radius: 6px; flex-shrink: 0;
  border: 1.5px solid var(--border-bright); display: grid; place-items: center;
  font-size: .7rem; color: transparent;
}
.course-row.selected .course-check { background: var(--gradient); border-color: transparent; color: #fff; }

.pill {
  display: inline-flex; align-items: center;
  font-size: .68rem; font-weight: 700; letter-spacing: .04em;
  padding: .18rem .5rem; border-radius: 6px; flex-shrink: 0;
}
.pill-ap { background: #f43f5e22; color: #fda4af; }
.pill-honors { background: #a78bfa22; color: #c4b5fd; }
.pill-regular { background: #64748b22; color: #cbd5e1; }
.pill-test-prep { background: #34d39922; color: #6ee7b7; }

/* ============================ study mode picker ============================ */
.mode-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 1rem; }
.mode-card {
  position: relative; overflow: hidden;
  background: var(--surface-solid); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: 1.4rem;
  cursor: pointer; text-align: left; font-family: inherit; color: var(--text);
  transition: transform .22s var(--spring), border-color .2s var(--ease);
}
.mode-card:hover { transform: translateY(-4px); border-color: var(--border-bright); }
.mode-card::after {
  content: ''; position: absolute; inset: auto -30% -60% auto;
  width: 160px; height: 160px; border-radius: 50%;
  background: var(--mode-glow, var(--accent-glow)); filter: blur(48px); opacity: .5;
  transition: opacity .3s var(--ease);
}
.mode-card:hover::after { opacity: .9; }
.mode-emoji { font-size: 1.7rem; display: block; margin-bottom: .7rem; }
.mode-card h3 { margin-bottom: .2rem; font-size: 1.12rem; }
.mode-card p { font-size: .85rem; color: var(--text-secondary); margin: 0; line-height: 1.45; }

/* ============================ quiz ============================ */
.quiz-wrap { max-width: 720px; margin: 0 auto; }

.quiz-meta {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  margin-bottom: 1.25rem; padding-bottom: 1rem;
  border-bottom: 1px solid var(--border);
}
/* The old build printed subject/topic/difficulty as three unlabelled pills
   that ran together and read as one nonsense word. Now it is a readable
   breadcrumb with the difficulty separated out as a distinct chip. */
.breadcrumb {
  display: flex; align-items: center; gap: .45rem; flex-wrap: wrap;
  font-size: .82rem; color: var(--text-secondary); min-width: 0;
}
.breadcrumb .crumb-course { font-weight: 600; color: var(--text); }
.breadcrumb .sep { color: var(--text-muted); }
.chip {
  display: inline-flex; align-items: center; gap: .3rem;
  font-size: .7rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  padding: .25rem .6rem; border-radius: 999px;
  border: 1px solid var(--border-bright); color: var(--text-secondary);
  margin-left: auto;
}
.chip-easy { background: var(--good-soft); color: #6ee7b7; border-color: #34d39933; }
.chip-medium { background: #fbbf2418; color: #fcd34d; border-color: #fbbf2433; }
.chip-hard { background: var(--bad-soft); color: #fda4af; border-color: #fb718533; }

.q-prompt {
  font-family: 'Bricolage Grotesque', sans-serif;
  font-size: 1.32rem; font-weight: 600; line-height: 1.4;
  letter-spacing: -.015em;
  white-space: pre-wrap;
  margin: 0 0 1.6rem;
}

.choices { display: flex; flex-direction: column; gap: .6rem; }
.choice-btn {
  display: flex; align-items: center; gap: .85rem; width: 100%;
  background: var(--surface-solid); border: 1.5px solid var(--border);
  border-radius: var(--r); padding: .95rem 1.1rem;
  font-family: inherit; font-size: .98rem; line-height: 1.45; color: var(--text);
  text-align: left; cursor: pointer;
  transition: transform .16s var(--ease), border-color .16s var(--ease), background .16s var(--ease);
  min-height: 54px;
}
.choice-btn:hover:not(:disabled) { border-color: var(--accent); background: var(--surface-hover); transform: translateX(3px); }
.choice-btn:disabled { cursor: default; }
.choice-key {
  flex-shrink: 0; width: 26px; height: 26px; border-radius: 8px;
  background: #0d0f1a; border: 1px solid var(--border-bright);
  display: grid; place-items: center;
  font-size: .78rem; font-weight: 700; color: var(--text-muted);
  font-family: 'Bricolage Grotesque', sans-serif;
  transition: all .2s var(--ease);
}
.choice-btn.correct { border-color: var(--good); background: var(--good-soft); }
.choice-btn.correct .choice-key { background: var(--good); border-color: var(--good); color: #052e1a; }
.choice-btn.wrong { border-color: var(--bad); background: var(--bad-soft); }
.choice-btn.wrong .choice-key { background: var(--bad); border-color: var(--bad); color: #4c0519; }

/* ---- answer feedback motion ----
   The colour change alone reads as static. A confirming pop on the right answer
   and a shake on the wrong one make the result register before the explanation
   is read. Both land under 400ms so rapid drilling is never gated on animation. */
.choice-btn.correct { animation: answer-pop .42s var(--spring); position: relative; z-index: 1; }
.choice-btn.wrong { animation: answer-shake .4s var(--ease); }
@keyframes answer-pop {
  0% { transform: scale(1); } 38% { transform: scale(1.035); } 100% { transform: scale(1); }
}
@keyframes answer-shake {
  0%, 100% { transform: translateX(0); }
  18% { transform: translateX(-6px); } 38% { transform: translateX(5px); }
  58% { transform: translateX(-3px); } 78% { transform: translateX(2px); }
}
.choice-btn.correct .choice-key { animation: key-ring .55s var(--ease); }
@keyframes key-ring {
  0% { box-shadow: 0 0 0 0 var(--good); } 100% { box-shadow: 0 0 0 10px transparent; }
}

/* ---- question transitions ----
   Applied per question load, so consecutive questions feel like cards being
   dealt rather than text swapped in place. */
.q-enter { animation: q-in .34s var(--ease) both; }
@keyframes q-in {
  from { opacity: 0; transform: translateY(12px) scale(.995); }
  to { opacity: 1; transform: none; }
}
/* The :not() guards are load-bearing. q-enter stays on the card after the
   entrance finishes, and this rule has the same specificity as
   ".choice-btn.correct" while sitting later in the file -- without the guards it
   would win the cascade and the answer feedback above would never play. */
.q-enter .choice-btn:not(.correct):not(.wrong) { animation: q-choice-in .32s var(--ease) backwards; }
.q-enter .choice-btn:not(.correct):not(.wrong):nth-child(1) { animation-delay: .05s; }
.q-enter .choice-btn:not(.correct):not(.wrong):nth-child(2) { animation-delay: .10s; }
.q-enter .choice-btn:not(.correct):not(.wrong):nth-child(3) { animation-delay: .15s; }
.q-enter .choice-btn:not(.correct):not(.wrong):nth-child(4) { animation-delay: .20s; }
.q-enter .choice-btn:not(.correct):not(.wrong):nth-child(5) { animation-delay: .25s; }
.q-enter .choice-btn:not(.correct):not(.wrong):nth-child(6) { animation-delay: .30s; }
@keyframes q-choice-in {
  from { opacity: 0; transform: translateX(-8px); } to { opacity: 1; transform: none; }
}

.feedback {
  margin-top: 1.3rem; padding: 1.15rem 1.25rem;
  border-radius: var(--r); background: var(--bg-elevated);
  border-left: 3px solid var(--border-bright);
  animation: rise .26s var(--ease);
}
.feedback.is-correct { border-left-color: var(--good); }
.feedback.is-wrong { border-left-color: var(--bad); }
.feedback-head {
  font-family: 'Bricolage Grotesque', sans-serif; font-weight: 700; font-size: 1rem;
  display: flex; align-items: center; gap: .45rem; margin-bottom: .5rem;
}
.feedback.is-correct .feedback-head { color: var(--good); }
.feedback.is-wrong .feedback-head { color: var(--bad); }
.feedback-body { font-size: .92rem; color: var(--text-secondary); line-height: 1.55; }
.delta {
  display: flex; gap: 1.25rem; flex-wrap: wrap;
  margin-top: .9rem; padding-top: .8rem; border-top: 1px solid var(--border);
  font-size: .8rem; color: var(--text-muted);
}
.delta b { color: var(--text); font-weight: 600; }
.delta .up { color: var(--good); }
.delta .down { color: var(--bad); }

.progress-track { height: 5px; background: var(--border); border-radius: 3px; overflow: hidden; margin-bottom: 1.25rem; }
.progress-fill { height: 100%; background: var(--gradient); border-radius: 3px; transition: width .4s var(--ease); }

/* ============================ flashcards ============================ */
.flash-stage { perspective: 1400px; margin: 1.5rem 0; }
.flash-card {
  position: relative; width: 100%; min-height: 300px;
  transform-style: preserve-3d; transition: transform .55s var(--spring);
  cursor: pointer;
}
.flash-card.flipped { transform: rotateY(180deg); }
.flash-face {
  position: absolute; inset: 0;
  backface-visibility: hidden; -webkit-backface-visibility: hidden;
  display: grid; place-items: center; text-align: center;
  padding: 2.5rem 2rem;
  background: var(--surface-solid); border: 1px solid var(--border);
  border-radius: var(--r-xl); box-shadow: var(--shadow);
}
.flash-face.back { transform: rotateY(180deg); background: var(--bg-elevated); }
.flash-term { font-family: 'Bricolage Grotesque', sans-serif; font-size: 1.9rem; font-weight: 700; letter-spacing: -.02em; }
.flash-def { font-size: 1.1rem; color: var(--text-secondary); line-height: 1.55; max-width: 46ch; }
.flash-hint { position: absolute; bottom: 1.1rem; left: 0; right: 0; font-size: .74rem; color: var(--text-muted); }

/* ============================ match game ============================ */
.match-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: .7rem; }
.match-tile {
  min-height: 96px; padding: .9rem;
  display: grid; place-items: center; text-align: center;
  background: var(--surface-solid); border: 1.5px solid var(--border);
  border-radius: var(--r); cursor: pointer;
  font-family: inherit; font-size: .84rem; line-height: 1.35; color: var(--text);
  transition: all .18s var(--ease);
}
.match-tile:hover:not(:disabled) { border-color: var(--border-bright); transform: translateY(-2px); }
.match-tile.picked { border-color: var(--accent); background: var(--accent-soft); box-shadow: 0 0 0 3px var(--accent-soft); }
/* A correct pair flashes green, then collapses. Confirming the match before it
   disappears stops it reading as "the tile vanished for no reason". */
.match-tile.matched { pointer-events: none; animation: tile-match .44s var(--ease) forwards; }
@keyframes tile-match {
  0% { opacity: 1; transform: scale(1); border-color: var(--good); background: var(--good-soft); }
  35% { opacity: 1; transform: scale(1.06); border-color: var(--good); background: var(--good-soft); }
  100% { opacity: 0; transform: scale(.85); border-color: var(--good); background: var(--good-soft); }
}
.match-tile.miss { border-color: var(--bad); background: var(--bad-soft); animation: shake .35s; }
@keyframes shake { 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } }
.match-tile.is-term { font-family: 'Bricolage Grotesque', sans-serif; font-weight: 700; font-size: .96rem; }

.timer {
  font-family: 'Bricolage Grotesque', sans-serif; font-size: 2.4rem; font-weight: 800;
  letter-spacing: -.03em; font-variant-numeric: tabular-nums;
}

/* ============================ dashboard ============================ */
.stat-card { padding: 1.15rem 1.25rem; }
.stat-value {
  font-family: 'Bricolage Grotesque', sans-serif; font-size: 2.1rem; font-weight: 800;
  letter-spacing: -.03em; line-height: 1;
}
.stat-label { font-size: .76rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: .07em; font-weight: 600; margin-top: .4rem; }

.topic-row {
  display: grid; grid-template-columns: 1fr 110px 52px;
  gap: 1rem; align-items: center;
  padding: .7rem 0; border-bottom: 1px solid var(--border);
}
.topic-row:last-child { border-bottom: 0; }
.topic-name { font-size: .92rem; font-weight: 500; }
.bar { height: 6px; background: #0d0f1a; border-radius: 3px; overflow: hidden; }
.bar span { display: block; height: 100%; border-radius: 3px; transition: width .5s var(--ease); }
.pct { text-align: right; font-size: .85rem; font-weight: 600; font-variant-numeric: tabular-nums; }

.unit-row {
  display: flex; align-items: center; gap: .85rem;
  padding: .75rem .95rem; border-radius: var(--r);
  background: var(--surface-solid); border: 1px solid var(--border);
  cursor: pointer; width: 100%; text-align: left;
  font-family: inherit; color: var(--text);
  transition: all .16s var(--ease);
}
.unit-row:hover { border-color: var(--accent); transform: translateX(3px); }
.unit-row.empty { opacity: .55; cursor: not-allowed; }
.unit-row.empty:hover { transform: none; border-color: var(--border); }
.unit-num {
  flex-shrink: 0; width: 30px; height: 30px; border-radius: 9px;
  background: #0d0f1a; border: 1px solid var(--border-bright);
  display: grid; place-items: center; font-size: .78rem; font-weight: 700;
  font-family: 'Bricolage Grotesque', sans-serif; color: var(--text-muted);
}
.unit-name { flex: 1; font-size: .92rem; font-weight: 500; }
.unit-count { font-size: .78rem; color: var(--text-muted); font-variant-numeric: tabular-nums; }

/* ============================ group ============================ */
.code-display {
  font-family: 'Bricolage Grotesque', sans-serif; font-size: 1.9rem; font-weight: 800;
  letter-spacing: .22em; padding: .9rem 1.2rem; text-align: center;
  background: #0d0f1a; border: 1px dashed var(--border-bright); border-radius: var(--r);
}
.seat-track { display: flex; gap: .4rem; margin: 1.25rem 0; }
.seat { flex: 1; height: 8px; border-radius: 4px; background: #0d0f1a; border: 1px solid var(--border); }
.seat.filled { background: var(--gradient); border-color: transparent; }

.lb-row {
  display: grid; grid-template-columns: 38px 1fr 68px 68px;
  gap: .8rem; align-items: center; padding: .7rem 0;
  border-bottom: 1px solid var(--border); font-size: .9rem;
}
.lb-row:last-child { border-bottom: 0; }
.lb-rank {
  width: 28px; height: 28px; border-radius: 9px; display: grid; place-items: center;
  font-family: 'Bricolage Grotesque', sans-serif; font-weight: 700; font-size: .82rem;
  background: #0d0f1a; color: var(--text-muted); border: 1px solid var(--border);
}
.lb-row.first .lb-rank { background: var(--gradient-warm); color: #fff; border-color: transparent; }
.num { text-align: right; font-variant-numeric: tabular-nums; font-size: .86rem; }

/* ============================ pricing ============================ */
.price-card { display: flex; flex-direction: column; position: relative; }
.price-card.featured { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent), 0 12px 40px -12px var(--accent-glow); }
.price-badge {
  position: absolute; top: -10px; left: 50%; transform: translateX(-50%);
  background: var(--gradient); color: #fff; font-size: .68rem; font-weight: 700;
  padding: .25rem .7rem; border-radius: 999px; letter-spacing: .04em; text-transform: uppercase;
  white-space: nowrap;
}
.price {
  font-family: 'Bricolage Grotesque', sans-serif; font-size: 2.4rem; font-weight: 800;
  letter-spacing: -.03em; margin: .4rem 0 1.1rem;
}
.price small { font-size: .9rem; font-weight: 500; color: var(--text-muted); letter-spacing: 0; }
.price-card ul { list-style: none; padding: 0; margin: 0 0 1.4rem; flex: 1; display: flex; flex-direction: column; gap: .6rem; }
.price-card li { font-size: .89rem; color: var(--text-secondary); padding-left: 1.5rem; position: relative; }
.price-card li::before { content: '✓'; position: absolute; left: 0; color: var(--good); font-weight: 700; }

.banner {
  padding: .9rem 1.15rem; border-radius: var(--r); font-size: .87rem;
  background: #fbbf2412; border: 1px solid #fbbf2430; color: #fcd34d;
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
}

/* ============================ toast ============================ */
.toast {
  position: fixed; bottom: calc(var(--nav-h) + 1.25rem); left: 50%;
  transform: translateX(-50%);
  background: var(--surface-solid); border: 1px solid var(--border-bright);
  padding: .8rem 1.3rem; border-radius: var(--r);
  font-size: .9rem; font-weight: 500;
  box-shadow: var(--shadow-lg); z-index: 90;
  animation: rise .28s var(--spring);
  max-width: calc(100vw - 2rem);
}
.toast.bad { border-color: var(--bad); color: #fda4af; }
.toast.good { border-color: var(--good); color: #6ee7b7; }

.empty-state { text-align: center; padding: 3rem 1.5rem; }
.empty-emoji { font-size: 2.6rem; display: block; margin-bottom: .8rem; opacity: .8; }

/* ============================ mobile nav ============================ */
.mobile-nav {
  display: none;
  position: fixed; bottom: 0; left: 0; right: 0; z-index: 50;
  background: #0a0b10f2; backdrop-filter: blur(14px) saturate(160%);
  border-top: 1px solid var(--border);
  padding: .4rem .5rem calc(.4rem + env(safe-area-inset-bottom));
}
.mobile-nav-inner { display: flex; }
.mobile-nav button {
  flex: 1; background: none; border: 0; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; gap: .18rem;
  padding: .45rem .2rem; color: var(--text-muted);
  font-family: inherit; font-size: .66rem; font-weight: 600;
  border-radius: var(--r-sm); transition: color .18s var(--ease);
  min-height: 50px;
}
.mobile-nav button .mn-icon { font-size: 1.18rem; line-height: 1; }
.mobile-nav button.active { color: var(--text); }

/* ============================ responsive ============================ */
@media (max-width: 900px) {
  .hero { grid-template-columns: 1fr; gap: 2.5rem; min-height: 0; padding-top: 1rem; }
  .hero-visual { order: -1; }
}

@media (max-width: 720px) {
  .shell { padding: 1.25rem 1rem calc(var(--nav-h) + 2rem); }
  .nav { display: none; }
  .mobile-nav { display: block; }
  .card { padding: 1.25rem; border-radius: var(--r); }
  .card-pad-lg { padding: 1.4rem; }
  .q-prompt { font-size: 1.15rem; }
  .choice-btn { padding: .85rem .95rem; font-size: .94rem; }
  .flash-card { min-height: 260px; }
  .flash-term { font-size: 1.5rem; }
  .flash-def { font-size: 1rem; }
  .match-grid { grid-template-columns: repeat(2, 1fr); gap: .5rem; }
  .match-tile { min-height: 86px; font-size: .78rem; padding: .6rem; }
  .lb-row { grid-template-columns: 34px 1fr 56px; }
  .lb-row > :nth-child(4) { display: none; }
  .topic-row { grid-template-columns: 1fr 70px 44px; gap: .6rem; }
  .stat-value { font-size: 1.7rem; }
  .streak { display: none; }
  .timer { font-size: 2rem; }
  .code-display { font-size: 1.5rem; letter-spacing: .16em; }
  .toast { bottom: calc(var(--nav-h) + 1rem); }
}

@media (max-width: 380px) {
  .choices { grid-template-columns: 1fr; }
  .brand span:not(.brand-mark) { display: none; }
}

/* ==========================================================================
   Additions: course cards, chat, custom sets, bug reporter, motion
   ========================================================================== */

/* ---- course cards on the home screen ----
   These were rendering dark and flat because a <button> inherits neither the
   card's background nor its text colour reliably across browsers. Setting both
   explicitly, and lifting the surface, makes them read as tappable cards. */
.course-card {
  display: block; width: 100%; text-align: left;
  font-family: inherit; color: var(--text);
  background: var(--surface-solid);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 1.35rem 1.5rem;
  cursor: pointer;
  transition: transform .22s var(--spring), border-color .2s var(--ease), background .2s var(--ease);
}
.course-card:hover { transform: translateY(-3px); border-color: var(--border-bright); background: var(--surface-hover); }
.course-card h3 { color: var(--text); margin-bottom: .3rem; }
.course-card .course-meta { color: var(--text-secondary); font-size: .86rem; }
.course-card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1rem; }

.progress-mini { height: 6px; background: #0d0f1a; border-radius: 3px; overflow: hidden; margin-top: 1rem; }
.progress-mini span { display: block; height: 100%; background: var(--gradient); border-radius: 3px; transition: width .8s var(--ease); }

/* ---- home stat strip: makes the top of the app feel occupied ---- */
.stat-strip {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: .8rem; margin-bottom: 1.75rem;
}
.mini-stat {
  background: var(--surface-solid); border: 1px solid var(--border);
  border-radius: var(--r); padding: .9rem 1rem;
  display: flex; align-items: center; gap: .7rem;
}
.mini-stat-icon {
  width: 32px; height: 32px; border-radius: 9px; flex-shrink: 0;
  display: grid; place-items: center; font-size: .95rem;
}
.mini-stat-value { font-family: 'Bricolage Grotesque', sans-serif; font-weight: 800; font-size: 1.25rem; line-height: 1; }
.mini-stat-label { font-size: .7rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: .06em; font-weight: 600; }

/* ---- group chat ---- */
.chat-layout { display: grid; grid-template-columns: 210px 1fr; gap: 1rem; }
.channel-list { display: flex; flex-direction: column; gap: .3rem; }
.channel-btn {
  display: flex; align-items: center; gap: .5rem; width: 100%;
  background: none; border: 1px solid transparent; border-radius: var(--r-sm);
  padding: .55rem .7rem; cursor: pointer; text-align: left;
  font-family: inherit; font-size: .88rem; color: var(--text-secondary);
  transition: all .16s var(--ease);
}
.channel-btn:hover { background: var(--surface-hover); color: var(--text); }
.channel-btn.active { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 28%, transparent); color: var(--text); }
.channel-hash { color: var(--text-muted); font-weight: 700; }
.channel-btn .count-badge { margin-left: auto; }

.chat-window {
  display: flex; flex-direction: column;
  height: 420px; background: #0d0f1a;
  border: 1px solid var(--border); border-radius: var(--r);
}
.chat-messages { flex: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: .8rem; }
.chat-messages::-webkit-scrollbar { width: 8px; }
.chat-messages::-webkit-scrollbar-thumb { background: var(--border-bright); border-radius: 4px; }

.msg { display: flex; gap: .65rem; animation: rise .25s var(--ease); }
.msg-avatar {
  width: 30px; height: 30px; border-radius: 50%; flex-shrink: 0;
  display: grid; place-items: center; font-size: .74rem; font-weight: 700;
  font-family: 'Bricolage Grotesque', sans-serif; color: #fff; background: var(--gradient);
}
.msg-body { min-width: 0; }
.msg-head { display: flex; align-items: baseline; gap: .5rem; }
.msg-author { font-weight: 600; font-size: .86rem; }
.msg-time { font-size: .7rem; color: var(--text-muted); }
.msg-text { font-size: .92rem; color: var(--text-secondary); word-wrap: break-word; }
.msg.mine .msg-avatar { background: var(--gradient-warm); }

.chat-compose { display: flex; gap: .5rem; padding: .75rem; border-top: 1px solid var(--border); }
.chat-compose input { flex: 1; min-height: 42px; }

/* ---- custom sets ---- */
.set-card {
  background: var(--surface-solid); border: 1px solid var(--border);
  border-radius: var(--r); padding: 1.1rem 1.25rem;
  display: flex; align-items: center; gap: 1rem;
  transition: border-color .18s var(--ease), transform .18s var(--ease);
}
.set-card:hover { border-color: var(--border-bright); transform: translateX(3px); }
.set-icon {
  width: 40px; height: 40px; border-radius: 11px; flex-shrink: 0;
  display: grid; place-items: center; font-size: 1.1rem;
  background: var(--accent-soft); border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
}
/* The old three-column grid for the set editor lived here. It came LATER in
   the file than the rebuilt .card-editor-row rules, so it silently won the
   cascade and squashed the new layout: the fields kept the old 1fr 1fr 40px
   track sizing and were pushed off to the right of the row number. Removed
   rather than reordered, because two rules describing the same component is
   how this happens in the first place. */

/* ---- bug reporter ---- */
.bug-item {
  display: flex; align-items: center; gap: .7rem;
  padding: .65rem 0; border-bottom: 1px solid var(--border); font-size: .89rem;
}
.bug-item:last-child { border-bottom: 0; }
.status-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.status-open { background: var(--warn); }
.status-fixed { background: var(--good); }
.status-wontfix { background: var(--text-muted); }

/* ---- ToS acceptance ---- */
.check-row {
  display: flex; align-items: flex-start; gap: .65rem;
  font-size: .84rem; color: var(--text-secondary); line-height: 1.55;
  cursor: pointer;
}
.check-row input[type=checkbox] {
  width: 20px; height: 20px; margin: .12rem 0 0; flex-shrink: 0;
  accent-color: var(--accent); cursor: pointer;
}

/* ---- modal ---- */
.modal-backdrop {
  position: fixed; inset: 0; z-index: 100;
  background: #05060acc; backdrop-filter: blur(6px);
  display: grid; place-items: center; padding: 1.25rem;
  animation: fade-in .2s var(--ease);
}
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
.modal {
  width: min(680px, 100%); max-height: 86dvh;
  display: flex; flex-direction: column; /* header pinned, body scrolls */
  background: var(--surface-solid); border: 1px solid var(--border-bright);
  border-radius: var(--r-xl); padding: 1.75rem;
  box-shadow: var(--shadow-lg); animation: pop .3s var(--spring);
}

/* The header stays put so "Close" is reachable in a long document. */
.modal-head {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding-bottom: 1rem; border-bottom: 1px solid var(--border); flex: 0 0 auto;
}

/* Legal copy is rendered Markdown, not preformatted text. An earlier version
   used white-space: pre-wrap on escaped source, which showed readers the raw
   "#", "**" and ">" characters. */
.modal-doc {
  flex: 1 1 auto; overflow-y: auto;
  padding-right: .5rem; margin-top: 1rem;
  font-size: .9rem; color: var(--text-secondary); line-height: 1.75;
  overscroll-behavior: contain;
}
.modal-doc > :first-child { margin-top: 0; }
.modal-doc h2 { font-size: 1.15rem; color: var(--text); margin: 1.6rem 0 .5rem; }
.modal-doc h3 { font-size: 1rem; color: var(--text); margin: 1.35rem 0 .4rem; }
.modal-doc h4, .modal-doc h5, .modal-doc h6 { font-size: .92rem; color: var(--text); margin: 1.15rem 0 .35rem; }
.modal-doc p { margin: 0 0 .85rem; }
.modal-doc strong { color: var(--text); font-weight: 600; }
.modal-doc ul, .modal-doc ol { margin: 0 0 .9rem; padding-left: 1.35rem; }
.modal-doc li { margin-bottom: .35rem; }
.modal-doc a { color: var(--accent); text-decoration: underline; }
.modal-doc code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .84em; padding: .1rem .32rem;
  background: var(--bg-elevated); border: 1px solid var(--border);
  border-radius: 5px; color: var(--text);
}
.modal-doc blockquote {
  margin: 0 0 1rem; padding: .85rem 1rem;
  background: var(--bg-elevated); border-left: 3px solid var(--accent);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
}
.modal-doc blockquote p:last-child { margin-bottom: 0; }
.modal-doc hr { border: 0; border-top: 1px solid var(--border); margin: 1.5rem 0; }

/* Stop the page behind the dialog from scrolling. */
body.modal-open { overflow: hidden; }

/* ---- motion ----
   Staggered entrance so a grid assembles rather than snapping into place.
   Capped at 10 children: past that the last item feels laggy, not polished. */
.stagger > * { animation: rise .42s var(--ease) backwards; }
.stagger > *:nth-child(1) { animation-delay: .02s; }
.stagger > *:nth-child(2) { animation-delay: .06s; }
.stagger > *:nth-child(3) { animation-delay: .10s; }
.stagger > *:nth-child(4) { animation-delay: .14s; }
.stagger > *:nth-child(5) { animation-delay: .18s; }
.stagger > *:nth-child(6) { animation-delay: .22s; }
.stagger > *:nth-child(7) { animation-delay: .26s; }
.stagger > *:nth-child(8) { animation-delay: .30s; }
.stagger > *:nth-child(9) { animation-delay: .34s; }
.stagger > *:nth-child(n+10) { animation-delay: .38s; }

@keyframes pulse-ring {
  0% { box-shadow: 0 0 0 0 var(--accent-glow); }
  70% { box-shadow: 0 0 0 12px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
.pulse { animation: pulse-ring 1.8s var(--ease) infinite; }

@keyframes bump { 50% { transform: scale(1.14); } }
.bump { animation: bump .34s var(--spring); }

.skeleton {
  background: linear-gradient(90deg, var(--surface-hover) 25%, #242841 50%, var(--surface-hover) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.4s linear infinite;
  border-radius: var(--r-sm);
}
@keyframes shimmer { to { background-position: -200% 0; } }

@media (max-width: 720px) {
  .chat-layout { grid-template-columns: 1fr; }
  .channel-list { flex-direction: row; overflow-x: auto; padding-bottom: .35rem; }
  .channel-btn { white-space: nowrap; }
  .chat-window { height: 380px; }
  /* Old set-editor grid overrides removed with the rule they modified. The
     mobile behaviour now lives with the rest of the editor styles. */
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    transition-duration: .01ms !important;
  }
  .flash-card { transition: none; }
}

/* ---------------------------------------------------------------- settings */
.kv { display: grid; grid-template-columns: max-content 1fr; gap: .55rem 1.25rem; margin: 0; }
.kv dt { color: var(--dim); font-size: .9rem; }
.kv dd { margin: 0; font-weight: 600; }

.plan-switch { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: .9rem; }
.plan-switch button { flex: 1 1 8rem; transition: transform .12s ease, border-color .12s ease; }
.plan-switch button:hover { transform: translateY(-2px); }
.plan-switch button.active { border-color: var(--accent); color: var(--accent); }

.form-row { display: flex; flex-direction: column; gap: .35rem; margin-bottom: .85rem; }
.form-row label { font-size: .85rem; color: var(--dim); }

/* ==========================================================================
   Mobile pass
   Most testers are on phones. These fix the things that make a web app feel
   like a web app instead of an app.
   ========================================================================== */

/* iOS zooms the whole page when a focused input is under 16px. That zoom is
   not reversible by the user and it wrecks the layout mid-signup. */
input, textarea, select { font-size: 16px; }

/* Kill the grey flash on tap and the 300ms delay browsers add for
   double-tap zoom. Both make every button feel laggy. */
* { -webkit-tap-highlight-color: transparent; }
button, .choice-btn, .match-tile, .course-card, .nav-btn { touch-action: manipulation; }

/* Stop the whole page rubber-banding when a scrollable panel hits its end. */
body { overscroll-behavior-y: none; }

@media (max-width: 720px) {
  /* Respect the notch and the home indicator. Without this the bottom nav
     sits under the indicator and the last item is hard to hit. */
  .mobile-nav { padding-bottom: env(safe-area-inset-bottom, 0); }
  .shell { padding-bottom: calc(var(--nav-h) + env(safe-area-inset-bottom, 0px) + 2rem); }
  .topbar { padding-top: env(safe-area-inset-top, 0); }

  /* THUMB REACH. On a tall phone the answer choices sat at the top of the
     screen, which is the hardest area to reach one-handed. Pin the prompt at
     the top and push the choices into the lower half where the thumb is. */
  #quiz-card { display: flex; flex-direction: column; min-height: calc(100dvh - var(--nav-h) - 9rem); }
  .q-prompt {
    position: sticky; top: 0; z-index: 5;
    background: var(--card); margin: 0 -1.25rem; padding: .5rem 1.25rem 1rem;
  }
  /* Push the choices into the lower half of the screen, where a thumb
     actually reaches on a 6-inch phone. */
  .choices { margin-top: auto; }
  .choice-btn { min-height: 56px; text-align: left; }

  /* Bigger targets across the board. 44px is Apple's floor, not a target. */
  .btn, .btn-secondary, .nav-btn { min-height: 48px; }
  .mobile-nav button { min-height: 56px; }
  .match-tile { min-height: 92px; }

  /* Long course names were clipping mid-word instead of wrapping. */
  .course-card, .topic-row, .lb-row { overflow-wrap: anywhere; }

  /* Modals were taller than the viewport with no way to scroll to the close
     button, which trapped you on the Terms dialog. */
  .modal { max-height: 82dvh; overflow-y: auto; -webkit-overflow-scrolling: touch; }
}

/* Landscape phones are short. Reclaim vertical space. */
@media (max-height: 500px) and (orientation: landscape) {
  .flash-card { min-height: 180px; }
  .q-prompt { font-size: 1rem; }
  .choice-btn { min-height: 44px; }
}

/* ---- learning profile card ---- */
.profile-card { border-color: color-mix(in srgb, var(--accent) 35%, var(--border)); }
.profile-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; text-align: center; }
.profile-num { display: block; font-family: var(--font-head); font-size: 1.85rem; font-weight: 700; line-height: 1.1; }
.profile-lbl { display: block; font-size: .78rem; color: var(--dim); margin-top: .2rem; }
.chip-row { display: flex; flex-wrap: wrap; gap: .4rem; }
.chip-weak { background: color-mix(in srgb, var(--bad) 18%, transparent); border: 1px solid color-mix(in srgb, var(--bad) 40%, transparent); }
@media (max-width: 720px) { .profile-grid { grid-template-columns: repeat(2, 1fr); gap: 1.1rem .5rem; } }

/* ============================ utilities ============================
   These exist because the Content-Security-Policy sets style-src WITHOUT
   'unsafe-inline' (server.js), which makes the browser silently DISCARD every
   style="..." attribute. The markup relied on 60+ of them, so headings
   rendered at the wrong size and flex rows refused to wrap -- with no console
   error to explain it. Generated 1:1 from the old inline declarations so the
   rendered result is unchanged, while the CSP stays strict. */
.u-ai-center { align-items:center; }
.u-ai-flex-start { align-items:flex-start; }
.u-bg-accent-soft { background:var(--accent-soft); }
.u-bg-bad-soft { background:var(--bad-soft); }
.u-bg-good-soft { background:var(--good-soft); }
.u-bg-hf5a524 { background:#f5a524; }
.u-bg-hfb923c22 { background:#fb923c22; }
.u-border-color-hf5a524 { border-color:#f5a524; }
.u-c-bad { color:var(--bad); }
.u-c-good { color:var(--good); }
.u-c-h1a1200 { color:#1a1200; }
.u-c-hb9aeff { color:var(--accent); }
.u-c-hfbbf24 { color:#fbbf24; }
.u-d-flex { display:flex; }
.u-flex-1 { flex:1; }
.u-fs-1p6rem { font-size:1.6rem; }
.u-fs-1p7rem { font-size:1.7rem; }
.u-fs-1p9rem { font-size:1.9rem; }
.u-fs-2rem { font-size:2rem; }
.u-fs-3p2rem { font-size:3.2rem; }
.u-fs-p85rem { font-size:.85rem; }
.u-fs-p92rem { font-size:.92rem; }
.u-fs-p9rem { font-size:.9rem; }
.u-fw-wrap { flex-wrap:wrap; }
.u-fwt-500 { font-weight:500; }
.u-g-1rem { gap:1rem; }
.u-g-p4rem { gap:.4rem; }
.u-g-p5rem { gap:.5rem; }
.u-g-p6rem { gap:.6rem; }
.u-g-p75rem { gap:.75rem; }
.u-glow-h34d39966 { --mode-glow: color-mix(in srgb, var(--subj-science) 40%, transparent); }
.u-glow-h38bdf866 { --mode-glow: color-mix(in srgb, var(--subj-cs) 40%, transparent); }
.u-glow-learn { --mode-glow: color-mix(in srgb, var(--subj-math) 40%, transparent); }
.u-glow-hfb718566 { --mode-glow: color-mix(in srgb, var(--subj-social) 40%, transparent); }
.u-glow-hfb923c66 { --mode-glow: color-mix(in srgb, var(--accent) 40%, transparent); }
.u-jc-center { justify-content:center; }
.u-jc-flex-end { justify-content:flex-end; }
.u-jc-space-between { justify-content:space-between; }
.u-m-0 { margin:0; }
.u-m-1p1rem-0-p45rem { margin:1.1rem 0 .45rem; }
.u-m-1rem-0 { margin:1rem 0; }
.u-mb-1p25rem { margin-bottom:1.25rem; }
.u-mb-1p5rem { margin-bottom:1.5rem; }
.u-mb-1rem { margin-bottom:1rem; }
.u-mb-p15rem { margin-bottom:.15rem; }
.u-mb-p3rem { margin-bottom:.3rem; }
.u-mb-p4rem { margin-bottom:.4rem; }
.u-mb-p5rem { margin-bottom:.5rem; }
.u-mb-p8rem { margin-bottom:.8rem; }
.u-minw-0 { min-width:0; }
.u-ml-auto { margin-left:auto; }
.u-mt-0 { margin-top:0; }
.u-mt-1p1rem { margin-top:1.1rem; }
.u-mt-1p25rem { margin-top:1.25rem; }
.u-mt-1p3rem { margin-top:1.3rem; }
.u-mt-1rem { margin-top:1rem; }
.u-mt-p75rem { margin-top:.75rem; }
.u-pt-3rem { padding-top:3rem; }
.u-w-0pct { width:0%; }
.u-ws-pre-wrap { white-space:pre-wrap; }

/* ==========================================================================
   Mobile pass

   Corrections for real device behaviour rather than a scaled-down desktop.
   ========================================================================== */

/* iOS Safari zooms the page when a focused input's font-size is under 16px.
   Body text is 15.5px, so every form field inherited a zoom trigger: tapping
   Email on the sign-in screen jerked the layout sideways. 16px is the
   threshold, forced only on touch devices so desktop keeps its intended scale. */
@media (hover: none) and (pointer: coarse) {
  input, select, textarea { font-size: 16px; }
}

/* Kill the grey tap flash, keep :focus-visible for keyboard users. */
@media (hover: none) {
  button, .btn, a { -webkit-tap-highlight-color: transparent; }
}

/* Nothing should ever scroll sideways. One overflowing flex child used to push
   the whole page, which on a phone reads as the layout being broken. */
html, body { max-width: 100%; overflow-x: hidden; }

/* ---- browse-and-add rows -------------------------------------------------
   A course row is two targets: the name opens the course, the button adds it.
   Keeping Add outside the row button matters because nesting a button inside a
   button is invalid HTML and the inner click never fires reliably. */
.course-row-wrap { display: flex; align-items: stretch; gap: .5rem; }
.course-row-wrap .course-row { flex: 1; min-width: 0; }

/* The Add control was a filled primary button on every row. With 159 rows that
   is 159 shouting orange buttons down the page, which made the catalogue
   exhausting to look at and gave no clue which row mattered. It is now a quiet
   circular affordance that only fills in on hover or focus. The row still reads
   as a list of classes rather than a wall of calls to action. */
.course-add {
  flex: 0 0 auto; align-self: center;
  width: 38px; height: 38px; padding: 0;
  display: grid; place-items: center;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-secondary);
  font-size: 1.15rem; font-weight: 500; line-height: 1;
  cursor: pointer;
  transition: all .16s var(--ease);
}
.course-add:hover:not(:disabled), .course-add:focus-visible {
  background: var(--accent); border-color: var(--accent);
  color: var(--accent-contrast, #fff);
}
.course-add.is-added {
  background: color-mix(in srgb, var(--good) 14%, transparent);
  border-color: color-mix(in srgb, var(--good) 42%, transparent);
  color: var(--good);
}
.course-add.is-added:hover { background: var(--bad-soft, color-mix(in srgb, var(--bad) 12%, transparent));
  border-color: color-mix(in srgb, var(--bad) 42%, transparent); color: var(--bad); }
/* Label is for screen readers; the glyph carries it visually. */
.course-add .sr-only {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}
@media (max-width: 480px) {
  /* Still a 44px target on a phone, still not a full-width orange bar. */
  .course-add { width: 44px; height: 44px; }
}

@media (max-width: 720px) {
  /* Flex rows stack instead of overflowing. Several carry four buttons
     (Cancel / Terms / Privacy / Sign out) that cannot fit at 390px. */
  .row { flex-wrap: wrap; }
  .row-between { flex-wrap: wrap; gap: .6rem; }
  /* Allow flex children to shrink below content width so long course names wrap
     rather than widening the page. */
  .row > *, .row-between > * { min-width: 0; }

  /* Respect the notch and home indicator. */
  .topbar { padding-left: max(1rem, env(safe-area-inset-left));
            padding-right: max(1rem, env(safe-area-inset-right)); }
  .shell  { padding-left: max(1rem, env(safe-area-inset-left));
            padding-right: max(1rem, env(safe-area-inset-right)); }

  /* Headings set at a fixed rem size overflow once the utility classes replace
     the inline styles. Cap them fluidly. */
  .u-fs-2rem   { font-size: clamp(1.45rem, 6.5vw, 2rem); }
  .u-fs-1p9rem { font-size: clamp(1.4rem, 6vw, 1.9rem); }
  .u-fs-1p7rem { font-size: clamp(1.3rem, 5.5vw, 1.7rem); }
  .u-fs-1p6rem { font-size: clamp(1.25rem, 5.2vw, 1.6rem); }
  .u-fs-3p2rem { font-size: clamp(2.2rem, 12vw, 3.2rem); }

  h1, h2, h3, .q-prompt, .flash-term { overflow-wrap: break-word; }

  /* The dialog used desktop padding, leaving little room for the document. */
  .modal { padding: 1.15rem; width: 100%; max-height: 92dvh; border-radius: var(--r-lg); }
  .modal-backdrop { padding: .6rem; }
  .modal-doc { font-size: .88rem; }

  /* Account actions become full-width rows: easier to hit, and it separates
     "Sign out" from the legal links. */
  #view-plan .card .row .btn { flex: 1 1 100%; }
}

@media (max-width: 380px) {
  .btn { padding: .68rem .9rem; font-size: .88rem; }
  .match-tile { min-height: 78px; font-size: .74rem; }
  .mobile-nav button { font-size: .6rem; }
  .price-card .price { font-size: 2.2rem; }
}

/* Phones in landscape are short, not narrow: a 100dvh onboarding step would
   scroll awkwardly, so let it size to content. */
@media (max-height: 480px) and (orientation: landscape) {
  body.onboarding-lock #main { min-height: 0; align-items: start; }
}

/* ==================== onboarding plan comparison ==================== */
/* Shown as the last onboarding step. Two columns rather than three: the
   student has just picked classes and needs one decision, not a pricing page. */
.plan-compare {
  display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin: 1.5rem 0 .5rem;
}
.plan-col {
  position: relative;
  background: var(--surface-solid); border: 1.5px solid var(--border);
  border-radius: var(--r-lg); padding: 1.35rem;
}
.plan-col-featured {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent-soft), 0 12px 40px -18px var(--accent-glow);
}
.plan-col-head { margin-bottom: .9rem; }
.plan-col .price { font-family: 'Bricolage Grotesque', sans-serif; font-weight: 800; font-size: 2rem; letter-spacing: -.03em; }
.plan-col .price small { font-size: .9rem; font-weight: 600; color: var(--text-muted); }
.plan-points { list-style: none; margin: 0; padding: 0; display: grid; gap: .55rem; }
.plan-points li { display: flex; gap: .6rem; align-items: flex-start; font-size: .9rem; line-height: 1.5; }
.tick { color: var(--good); font-weight: 700; flex-shrink: 0; }
.tick-muted { color: var(--text-muted); }
.tick-no { color: var(--bad); }

@media (max-width: 640px) {
  /* Stacking keeps each column readable; Premium leads so the comparison is
     framed by what they gain rather than what they lack. */
  .plan-compare { grid-template-columns: 1fr; }
  .plan-col-featured { order: -1; }
}

/* ==================== locked (premium) features ====================
   Free users SEE everything and are told what unlocks it, rather than having
   features hidden. Hiding them makes the app look thin and gives no reason to
   upgrade; showing them locked makes the value obvious. */
.locked {
  position: relative;
  opacity: .55;
  filter: saturate(.5);
}
.locked::after {
  content: '🔒';
  position: absolute; top: .55rem; right: .65rem;
  font-size: .95rem; line-height: 1;
  opacity: .9; pointer-events: none;
}
.lock-badge {
  display: inline-flex; align-items: center; gap: .3rem;
  font-size: .66rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  padding: .2rem .45rem; border-radius: 999px;
  background: var(--accent-soft); color: var(--accent); border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
  vertical-align: middle;
}
/* The whole tile stays clickable so the tap explains the lock instead of
   doing nothing, which is the most common complaint about gated UI. */
.locked { cursor: pointer; }
.locked:hover { opacity: .72; }

.upsell-inline {
  display: flex; align-items: center; gap: .75rem; flex-wrap: wrap;
  padding: .9rem 1.1rem; margin-bottom: 1.25rem;
  border-radius: var(--r);
  background: var(--accent-soft); border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
  font-size: .9rem;
}
.upsell-inline .btn { margin-left: auto; }
@media (max-width: 720px) {
  .upsell-inline .btn { margin-left: 0; width: 100%; }
}

/* ==================== AP exam practice ==================== */
.u-glow-hf43f5e66 { --mode-glow: color-mix(in srgb, var(--subj-arts) 40%, transparent); }

.ap-table { width: 100%; border-collapse: collapse; font-size: .88rem; }
.ap-table th {
  text-align: left; font-size: .68rem; letter-spacing: .1em; text-transform: uppercase;
  color: var(--text-muted); padding: .5rem .6rem; border-bottom: 1px solid var(--border);
}
.ap-table td { padding: .7rem .6rem; border-bottom: 1px solid var(--border); vertical-align: top; }
.ap-table tr:last-child td { border-bottom: 0; }

/* Timer turns red only in the final minute: colouring it earlier trains
   students to panic rather than to pace. */
.timer-low { color: var(--bad); animation: bump .9s var(--ease) infinite; }

.frq-input {
  width: 100%; font-family: inherit; font-size: .95rem; line-height: 1.7;
  background: #0d0f1a; border: 1px solid var(--border); border-radius: var(--r);
  color: var(--text); padding: .9rem 1rem; resize: vertical; min-height: 240px;
}
.frq-input:focus { border-color: var(--accent); outline: none; box-shadow: 0 0 0 3px var(--accent-soft); }

.ap-checks { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }
.ap-check { display: flex; gap: .6rem; align-items: flex-start; font-size: .9rem; }
.ap-check-mark { font-weight: 700; flex-shrink: 0; }
.ap-check.ok .ap-check-mark { color: var(--good); }
.ap-check.no .ap-check-mark { color: var(--warn); }

/* Rubric rows are the honest half of the feature: the student scores their own
   writing against the published criteria. */
.rubric-row { padding: .9rem 0; border-top: 1px solid var(--border); }
.rubric-row:first-of-type { border-top: 0; }
.rubric-head { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; margin-bottom: .25rem; }
.rubric-points { display: flex; gap: .4rem; margin-top: .6rem; flex-wrap: wrap; }
.rubric-pt {
  min-width: 40px; min-height: 40px; padding: .4rem .7rem;
  background: var(--surface-solid); border: 1.5px solid var(--border);
  border-radius: var(--r-sm); color: var(--text-secondary);
  font-family: inherit; font-weight: 700; cursor: pointer;
  transition: all .16s var(--ease);
}
.rubric-pt:hover { border-color: var(--border-bright); color: var(--text); }
.rubric-pt.sel { background: var(--accent-soft); border-color: var(--accent); color: var(--text); }
.ap-total { font-family: 'Bricolage Grotesque', sans-serif; font-weight: 800; font-size: 1.3rem; }

.choice-btn.picked { border-color: var(--accent); background: var(--accent-soft); }
.btn-block { width: 100%; justify-content: flex-start; }

@media (max-width: 720px) {
  /* The table would otherwise force a horizontal scroll on a phone. */
  .ap-table { font-size: .8rem; }
  .ap-table th:nth-child(2), .ap-table td:nth-child(2) { display: none; }
  .frq-input { min-height: 300px; font-size: 16px; } /* 16px: stops iOS zooming */
  .rubric-pt { min-width: 44px; min-height: 44px; }
  #ap-submit { width: 100%; margin-left: 0; margin-top: .5rem; }
}

/* ==================== quota meter ==================== */
/* Pips make the daily cap concrete. "3 left" is a number; three dots going
   dark as you spend them is a feeling, and it is what makes the limit land
   before the student hits it. */
.quota-meter { display: flex; align-items: center; gap: .6rem; }
.quota-pips { display: flex; gap: .25rem; }
.pip {
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--accent); box-shadow: 0 0 8px var(--accent-glow);
  transition: background .3s var(--ease), box-shadow .3s var(--ease);
}
.pip.spent { background: var(--border); box-shadow: none; }
.quota-low { color: var(--warn) !important; font-weight: 600; }

/* ==================== paywall ==================== */
.paywall {
  position: fixed; inset: 0; z-index: 120;
  display: grid; place-items: center; padding: 1.25rem;
  background: #05060ae6; backdrop-filter: blur(10px);
  animation: fade-in .22s var(--ease);
  overflow-y: auto;
}
.paywall-card {
  width: min(460px, 100%);
  background: var(--surface-solid);
  border: 1px solid var(--accent);
  border-radius: var(--r-xl); padding: 2rem 1.75rem;
  box-shadow: var(--shadow-lg), 0 0 60px -20px var(--accent-glow);
  animation: pop .32s var(--spring);
  text-align: center;
}
.paywall-kicker {
  display: inline-block; margin-bottom: .75rem;
  font-size: .66rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
  padding: .28rem .7rem; border-radius: 999px;
  background: var(--accent-soft); color: var(--accent); border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
}
.paywall-card h2 { font-size: 1.5rem; margin: 0 0 .5rem; }
.paywall-sub { color: var(--text-secondary); font-size: .95rem; margin: 0 0 1.25rem; }

/* The student's own numbers, not invented social proof. */
.paywall-stats { display: flex; justify-content: center; gap: 1.5rem; margin-bottom: 1.25rem; flex-wrap: wrap; }
.paywall-stat { display: flex; flex-direction: column; }
.paywall-stat strong { font-family: 'Bricolage Grotesque', sans-serif; font-size: 1.6rem; font-weight: 800; line-height: 1; }
.paywall-stat span { font-size: .7rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: .08em; margin-top: .2rem; }

.paywall-points { list-style: none; margin: 0 0 1.25rem; padding: 0; display: grid; gap: .5rem; text-align: left; }
/* The tick is a flex item; everything after it must be ONE flex item.
   With plain `display:flex` on the li, a <strong> mid-sentence became its own
   flex child, so "Every class" and "you are taking, not one subject" wrapped
   independently and left a ragged hole between them. Obvious at 375px, invisible
   on a desktop where the line never wrapped. Grid with a fixed first column and
   the text in the second fixes it structurally. */
.paywall-points li {
  display: grid; grid-template-columns: auto 1fr; gap: .6rem;
  align-items: start; font-size: .9rem; line-height: 1.5;
}

.paywall-price { margin-bottom: 1rem; font-family: 'Bricolage Grotesque', sans-serif; }
.paywall-price strong { font-size: 2.1rem; font-weight: 800; letter-spacing: -.03em; }

.btn-block-center { width: 100%; justify-content: center; }
#paywall-dismiss { margin-top: .5rem; }
.paywall-fineprint { font-size: .75rem; margin: .75rem 0 0; }

@media (max-width: 720px) {
  .paywall { padding: .75rem; align-items: start; padding-top: 2rem; }
  .paywall-card { padding: 1.5rem 1.25rem; }
  .paywall-card h2 { font-size: 1.3rem; }
  .paywall-stats { gap: 1.1rem; }
  /* Full-width buttons: the two most likely thumbs positions on a phone. */
  .btn-block-center { min-height: 50px; }
}

/* ==================== upgrades ==================== */
/* Offline notice. Fixed to the top so it is visible whatever you are doing,
   and worded to say your work is safe rather than that something broke. */
.offline-banner {
  position: fixed; top: 0; left: 0; right: 0; z-index: 200;
  padding: .6rem 1rem;
  text-align: center; font-size: .85rem; font-weight: 600;
  background: var(--warn); color: #1a1200;
  animation: slide-down .25s var(--ease);
}
@keyframes slide-down { from { transform: translateY(-100%); } to { transform: none; } }
body.is-offline .topbar { top: 38px; }

/* Busy state for guarded buttons: communicates that the tap registered, which
   is what stops people tapping again. */
.is-busy { opacity: .65; pointer-events: none; position: relative; }
.is-busy::after {
  content: ''; position: absolute; right: .8rem; top: 50%;
  width: 13px; height: 13px; margin-top: -7px;
  border: 2px solid currentColor; border-right-color: transparent;
  border-radius: 50%; animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* Respect reduced motion for the new animations too. */
@media (prefers-reduced-motion: reduce) {
  .offline-banner { animation: none; }
  .is-busy::after { animation-duration: 2s; }
}

/* Focus ring on the paywall and dialogs, for keyboard users. */
.paywall-card :focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* ==================== dashboard stat grid ====================
   BUG: on narrow phones the four stat cards squeezed to one column and the
   labels ("Topics started", "Weak spots") overflowed their cards, because
   grid-4 used minmax(150px, 1fr) and the value was a fixed 2.1rem.

   Fixed as an explicit 2x2 with a fluid value size and labels allowed to
   wrap onto two lines. */
@media (max-width: 720px) {
  #stat-row.grid-4 { grid-template-columns: 1fr 1fr; gap: .7rem; }
  #stat-row .stat-card { padding: .95rem .9rem; min-width: 0; }
  #stat-row .stat-value { font-size: clamp(1.5rem, 8vw, 2.1rem); }
  #stat-row .stat-label {
    font-size: .64rem; letter-spacing: .05em;
    white-space: normal; overflow-wrap: break-word; hyphens: auto;
    line-height: 1.3;
  }
  /* The home mini-stat strip has the same problem. */
  .stat-strip { grid-template-columns: 1fr 1fr; gap: .6rem; }
  .mini-stat { padding: .75rem .8rem; gap: .55rem; min-width: 0; }
  .mini-stat > div { min-width: 0; }
  .mini-stat-label, .mini-stat-value { overflow-wrap: break-word; }
}
@media (max-width: 380px) {
  #stat-row .stat-label { font-size: .6rem; }
}

/* Segmented control, used for the theme picker and course sorting. */
.seg {
  display: inline-flex; padding: 4px; gap: 2px;
  background: var(--surface-hover); border: 1px solid var(--border);
  border-radius: 999px;
}
.seg-btn {
  border: 0; background: none; cursor: pointer;
  font-family: inherit; font-size: .85rem; font-weight: 600;
  color: var(--text-secondary);
  padding: .5rem 1rem; border-radius: 999px; min-height: 40px;
  transition: background .18s var(--ease), color .18s var(--ease);
}
.seg-btn:hover { color: var(--text); }
.seg-btn.active { background: var(--surface-solid); color: var(--text); box-shadow: var(--shadow-sm); }
@media (max-width: 720px) { .seg { width: 100%; } .seg-btn { flex: 1; } }

/* ==================== landing page ====================
   A scrolling marketing page rather than a bare signup form. Testers said the
   app felt empty, and the first thing they saw was a form with no argument
   attached to it. */
.landing { animation: none; }        /* the page scrolls; do not slide it in */

.hero-v2 {
  display: grid; grid-template-columns: 1.1fr .9fr;
  gap: 3rem; align-items: center;
  min-height: min(78dvh, 620px);
  padding: 2rem 0 3rem;
}
.hero-copy h1 {
  font-size: clamp(2.2rem, 5.5vw, 3.9rem);
  line-height: 1.05; margin-bottom: 1.1rem;
}
.hero-copy .lede { font-size: 1.08rem; max-width: 46ch; color: var(--text-secondary); }
.hero-cta { display: flex; gap: .75rem; flex-wrap: wrap; margin: 1.75rem 0 .75rem; }

/* Generated artwork, held behind a warm wash so text stays readable whatever
   the image does. Decorative only, so it is a background rather than an <img>
   with alt text. */
.hero-art {
  min-height: 340px; border-radius: var(--r-xl);
  background-image: url('/img/hero.jpg');
  background-size: cover; background-position: center;
  box-shadow: var(--shadow-lg);
  border: 1px solid var(--border);
}

/* Photography in the marketing bands. width/height are set on the tag so the
   browser reserves the space and the text below does not jump when it loads. */
.band-art {
  display: block; width: 100%; height: auto;
  max-height: 420px; object-fit: cover; object-position: center 42%;
  border-radius: var(--r-xl);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-lg);
  margin: 0 0 2.5rem;
}

.band { padding: 4rem 0; }
.band-alt {
  background: var(--bg-elevated);
  border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
  /* Full-bleed inside the centred shell. */
  margin-left: calc(50% - 50vw); margin-right: calc(50% - 50vw);
  padding-left: max(1.25rem, calc(50vw - 560px)); padding-right: max(1.25rem, calc(50vw - 560px));
}
.band-title { font-size: clamp(1.6rem, 4vw, 2.4rem); margin-bottom: .6rem; }
.band-lede { color: var(--text-secondary); max-width: 60ch; font-size: 1.02rem; margin-bottom: 2rem; }

.proof-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.5rem; }
.proof { display: flex; flex-direction: column; gap: .4rem; }
.proof-num {
  font-family: 'Bricolage Grotesque', sans-serif; font-weight: 800;
  font-size: 3rem; line-height: 1; color: var(--accent);
}
.proof-label { color: var(--text-secondary); font-size: .92rem; max-width: 28ch; }

.why-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.25rem; }
.why-card {
  background: var(--surface-solid); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: 1.6rem;
}
.why-card h3 { font-size: 1.12rem; margin-bottom: .5rem; }
.why-card p { color: var(--text-secondary); font-size: .93rem; margin: 0; }
.why-icon {
  display: grid; place-items: center;
  width: 44px; height: 44px; border-radius: 14px;
  font-size: 1.2rem; margin-bottom: .9rem;
  background: var(--accent-soft); color: var(--accent);
}
.why-icon[data-subj="math"]    { background: color-mix(in srgb, var(--subj-math) 12%, transparent);    color: var(--subj-math); }
.why-icon[data-subj="science"] { background: color-mix(in srgb, var(--subj-science) 12%, transparent); color: var(--subj-science); }
.why-icon[data-subj="english"] { background: color-mix(in srgb, var(--subj-english) 12%, transparent); color: var(--subj-english); }
.why-icon[data-subj="social"]  { background: color-mix(in srgb, var(--subj-social) 12%, transparent);  color: var(--subj-social); }

.tier-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.25rem; align-items: start; }
.tier {
  position: relative;
  background: var(--surface-solid); border: 1.5px solid var(--border);
  border-radius: var(--r-lg); padding: 1.6rem;
}
.tier-featured {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent-soft), var(--shadow);
}
.tier h3 { font-size: 1.05rem; margin-bottom: .3rem; }
.tier-price {
  font-family: 'Bricolage Grotesque', sans-serif; font-weight: 800;
  font-size: 2.2rem; letter-spacing: -.03em; line-height: 1;
}
.tier-price small { font-size: .85rem; font-weight: 600; color: var(--text-muted); }
.tier-alt { font-size: .82rem; color: var(--text-muted); margin: .4rem 0 0; }
.tier-points { list-style: none; margin: 1.1rem 0 0; padding: 0; display: grid; gap: .5rem; }
.tier-points li { display: flex; gap: .55rem; align-items: flex-start; font-size: .89rem; line-height: 1.45; }

.signup-wrap { max-width: 460px; }
.signup-card { border-radius: var(--r-lg); }

@media (max-width: 860px) {
  .hero-v2 { grid-template-columns: 1fr; gap: 2rem; min-height: 0; padding-top: 1rem; }
  /* Art below the copy on a phone: the promise should be the first thing read,
     not a decorative image pushing it under the fold. */
  .hero-art { order: 2; min-height: 200px; }
  .hero-copy { order: 1; }
  .hero-cta .btn { flex: 1 1 100%; }
  .band { padding: 2.5rem 0; }
  .band-alt { padding-left: 1.25rem; padding-right: 1.25rem; }
  .proof-num { font-size: 2.2rem; }
  .signup-wrap { max-width: none; }
}

/* ==================== XP, streaks, celebrations ==================== */
.xp-bar { display: flex; align-items: center; gap: .55rem; }
.xp-level-badge {
  display: grid; place-items: center;
  width: 26px; height: 26px; border-radius: 9px; flex-shrink: 0;
  background: var(--gradient); color: var(--accent-contrast);
  font-family: 'Bricolage Grotesque', sans-serif; font-weight: 800; font-size: .78rem;
}
.xp-track {
  width: 90px; height: 7px; border-radius: 4px; overflow: hidden;
  background: var(--surface-hover); border: 1px solid var(--border);
}
.xp-fill {
  height: 100%; width: 0%; border-radius: 4px;
  background: var(--gradient);
  transition: width .55s var(--ease);
}
.xp-count { font-size: .7rem; white-space: nowrap; }

.streak {
  display: inline-flex; align-items: center; gap: .3rem;
  font-size: .78rem; font-weight: 700;
  padding: .3rem .6rem; border-radius: 999px;
  background: color-mix(in srgb, var(--warn) 14%, transparent);
  color: var(--warn); border: 1px solid color-mix(in srgb, var(--warn) 30%, transparent);
}
/* At risk = answered yesterday, nothing today. A pulse, not a scolding. */
.streak.at-risk { animation: streak-pulse 2.2s var(--ease) infinite; }
@keyframes streak-pulse { 50% { opacity: .55; } }

/* +XP floats off the card as confirmation the answer counted. */
.xp-float {
  position: absolute; right: 1.4rem; top: 1.1rem;
  font-family: 'Bricolage Grotesque', sans-serif; font-weight: 800; font-size: 1.05rem;
  color: var(--good); pointer-events: none;
  animation: xp-rise 1.1s var(--ease) forwards;
}
@keyframes xp-rise {
  0% { opacity: 0; transform: translateY(6px) scale(.9); }
  22% { opacity: 1; transform: translateY(0) scale(1); }
  100% { opacity: 0; transform: translateY(-26px) scale(1); }
}

/* One celebration at a time, queued, bottom-centre so it never covers the
   question you are mid-way through reading. */
.celebrate {
  position: fixed; left: 50%; bottom: calc(var(--nav-h) + 1.5rem);
  transform: translateX(-50%);
  z-index: 150; display: flex; align-items: center; gap: .75rem;
  padding: .85rem 1.3rem; border-radius: 999px;
  background: var(--surface-solid); border: 1.5px solid var(--accent);
  box-shadow: var(--shadow-lg);
  animation: celebrate-in .4s var(--spring);
  max-width: calc(100vw - 2rem);
}
.celebrate.leaving { animation: celebrate-out .3s var(--ease) forwards; }
.celebrate strong { font-family: 'Bricolage Grotesque', sans-serif; font-size: 1rem; }
.celebrate span:not(.celebrate-icon) { font-size: .82rem; color: var(--text-secondary); }
.celebrate-icon {
  display: grid; place-items: center; flex-shrink: 0;
  width: 34px; height: 34px; border-radius: 11px; font-size: 1.05rem;
  background: var(--accent-soft); color: var(--accent);
}
.celebrate-level { border-color: var(--accent); }
.celebrate-badge { border-color: var(--good); }
.celebrate-badge .celebrate-icon {
  background: color-mix(in srgb, var(--good) 14%, transparent); color: var(--good);
}
@keyframes celebrate-in {
  from { opacity: 0; transform: translate(-50%, 20px) scale(.94); }
  to { opacity: 1; transform: translate(-50%, 0) scale(1); }
}
@keyframes celebrate-out {
  to { opacity: 0; transform: translate(-50%, 12px) scale(.97); }
}

@media (max-width: 860px) {
  /* The bar is the first thing to lose on a narrow top bar; the level badge
     and streak carry the information. */
  .xp-track, .xp-count { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .streak.at-risk { animation: none; }
  .xp-float { animation-duration: .01ms; }
}

/* ==================== class board (home) ==================== */
.course-board { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: .9rem; }
.course-tile {
  position: relative; overflow: hidden;
  display: flex; flex-direction: column; gap: .45rem; align-items: stretch;
  text-align: left; cursor: pointer;
  background: var(--surface-solid); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: 1.1rem 1.1rem 1rem;
  font-family: inherit; color: var(--text);
  transition: transform .18s var(--spring), border-color .18s var(--ease), box-shadow .18s var(--ease);
}
.course-tile:hover { transform: translateY(-2px); border-color: var(--border-bright); box-shadow: var(--shadow); }

/* ---- reorder handle ------------------------------------------------------
   The wrapper is the grid item now, so the handle can sit outside the button.
   It floats over the top-right corner rather than taking a row of its own,
   which would push every tile taller for a control most people never touch. */
.course-tile-wrap { position: relative; min-width: 0; }
.course-tile-wrap.dragging { opacity: .55; transform: scale(.98); }
.course-tile-wrap.dragging .course-tile { border-color: var(--accent); }

.tile-handle {
  position: absolute; top: 8px; right: 8px; z-index: 2;
  width: 32px; height: 32px;
  display: grid; place-items: center;
  border-radius: 9px;
  color: var(--text-secondary);
  cursor: grab;
  opacity: 0;
  transition: opacity .16s var(--ease), background .16s var(--ease);
  /* Without this the browser claims the gesture for scrolling before the
     first pointermove ever reaches us, and the tile never moves. */
  touch-action: none;
}
.tile-handle svg {
  width: 15px; height: 15px; fill: none; stroke: currentColor;
  stroke-width: 1.9; stroke-linecap: round;
}
.tile-handle:hover, .tile-handle:focus-visible {
  background: var(--surface-hover); color: var(--text); opacity: 1;
}
.course-tile-wrap:hover .tile-handle { opacity: .7; }
.course-tile-wrap.dragging .tile-handle { opacity: 1; cursor: grabbing; }

/* On touch there is no hover, so a handle that only appears on hover is a
   handle that does not exist. Always visible on a phone. */
@media (hover: none) {
  .tile-handle { opacity: .55; width: 40px; height: 40px; }
}

/* While a tile is being dragged the page must not scroll under the finger. */
body.is-reordering { overflow: hidden; touch-action: none; }
body.is-reordering .course-tile { pointer-events: none; }
/* Colour spine identifies the subject at a glance, which is the whole reason
   a class grid beats a list. */
.course-tile-bar { position: absolute; left: 0; top: 0; bottom: 0; width: 4px; background: var(--subj-other); }
.course-tile[data-subj="math"]        .course-tile-bar { background: var(--subj-math); }
.course-tile[data-subj="science"]     .course-tile-bar { background: var(--subj-science); }
.course-tile[data-subj="english"]     .course-tile-bar { background: var(--subj-english); }
.course-tile[data-subj="social"]      .course-tile-bar { background: var(--subj-social); }
.course-tile[data-subj="language"]    .course-tile-bar { background: var(--subj-language); }
.course-tile[data-subj="arts"]        .course-tile-bar { background: var(--subj-arts); }
.course-tile[data-subj="cs"]          .course-tile-bar { background: var(--subj-cs); }
.course-tile[data-subj="business"]    .course-tile-bar { background: var(--subj-business); }
.course-tile[data-subj="health"]      .course-tile-bar { background: var(--subj-health); }
.course-tile[data-subj="engineering"] .course-tile-bar { background: var(--subj-engineering); }

.course-tile-top { display: flex; align-items: flex-start; justify-content: space-between; gap: .5rem; }
.course-tile-name { font-family: 'Bricolage Grotesque', sans-serif; font-weight: 700; font-size: 1rem; line-height: 1.25; }
.course-tile-meta { font-size: .78rem; color: var(--text-muted); }
.course-tile-track { height: 6px; border-radius: 3px; background: var(--surface-hover); overflow: hidden; margin-top: .3rem; }
.course-tile-fill { display: block; height: 100%; width: 0; border-radius: 3px; background: var(--gradient); transition: width .5s var(--ease); }
.course-tile-stats { display: flex; justify-content: space-between; gap: .5rem; font-size: .78rem; color: var(--text-secondary); }
.course-weak { color: var(--bad); font-weight: 600; }
.course-tile-next { font-size: .78rem; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.course-tile-when { font-size: .72rem; }

.seg-sm .seg-btn { padding: .38rem .7rem; font-size: .78rem; min-height: 34px; }

/* ==================== one class ==================== */
.course-head { display: flex; align-items: center; gap: .9rem; margin-bottom: 1rem; }
.course-head-dot { width: 14px; height: 44px; border-radius: 7px; background: var(--subj-other); flex-shrink: 0; }
.course-head-dot[data-subj="math"] { background: var(--subj-math); }
.course-head-dot[data-subj="science"] { background: var(--subj-science); }
.course-head-dot[data-subj="english"] { background: var(--subj-english); }
.course-head-dot[data-subj="social"] { background: var(--subj-social); }
.course-head-dot[data-subj="language"] { background: var(--subj-language); }
.course-head-dot[data-subj="arts"] { background: var(--subj-arts); }
.course-head-dot[data-subj="cs"] { background: var(--subj-cs); }
.course-head-dot[data-subj="business"] { background: var(--subj-business); }
.course-head-dot[data-subj="health"] { background: var(--subj-health); }
.course-head-dot[data-subj="engineering"] { background: var(--subj-engineering); }
.course-progress { display: flex; align-items: center; gap: .8rem; margin-bottom: .5rem; }
.course-progress .progress-track { flex: 1; margin: 0; }

.unit-list { display: flex; flex-direction: column; }
.unit-row {
  display: flex; align-items: center; gap: 1rem; width: 100%;
  background: none; border: 0; border-bottom: 1px solid var(--border);
  padding: .95rem .2rem; cursor: pointer; text-align: left;
  font-family: inherit; color: var(--text);
  transition: background .16s var(--ease);
}
.unit-row:hover:not(:disabled) { background: var(--surface-hover); }
.unit-row:disabled { opacity: .45; cursor: default; }
.unit-row-main { display: flex; flex-direction: column; gap: .15rem; flex: 1; min-width: 0; }
.unit-row-name { font-weight: 500; font-size: .95rem; }
.unit-row-meta { font-size: .78rem; }
.unit-row-go { color: var(--text-muted); }

/* ==================== profile ==================== */
.profile-head { display: flex; align-items: center; gap: 1.1rem; margin-bottom: 1.5rem; }
.profile-avatar {
  width: 74px; height: 74px; border-radius: 24px; flex-shrink: 0;
  display: grid; place-items: center; font-size: 2rem;
  background: var(--gradient); color: var(--accent-contrast);
  box-shadow: var(--shadow);
}
.level-card {
  background: var(--surface-solid); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: 1.25rem; margin-bottom: 1.25rem;
}
.level-big { font-family: 'Bricolage Grotesque', sans-serif; font-weight: 800; font-size: 1.35rem; }

.profile-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: .8rem; margin-bottom: 1rem; }
.pstat {
  background: var(--surface-solid); border: 1px solid var(--border);
  border-radius: var(--r); padding: .9rem .7rem; text-align: center; min-width: 0;
}
.pstat-num { display: block; font-family: 'Bricolage Grotesque', sans-serif; font-weight: 800; font-size: 1.5rem; line-height: 1; }
.pstat-label { display: block; font-size: .68rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: .06em; margin-top: .35rem; overflow-wrap: break-word; }

.badge-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: .8rem; }
.badge {
  display: flex; flex-direction: column; align-items: center; text-align: center; gap: .35rem;
  background: var(--surface-solid); border: 1px solid var(--border);
  border-radius: var(--r); padding: 1.1rem .8rem;
}
/* Locked badges stay visible: an empty slot you can see is a goal, an absent
   one is nothing at all. */
.badge.locked { opacity: .42; filter: grayscale(1); }
.badge.earned { border-color: var(--good); }
.badge-icon {
  display: grid; place-items: center; width: 42px; height: 42px; border-radius: 14px;
  font-size: 1.2rem; background: var(--accent-soft); color: var(--accent);
}
.badge.earned .badge-icon { background: color-mix(in srgb, var(--good) 14%, transparent); color: var(--good); }
.badge-name { font-family: 'Bricolage Grotesque', sans-serif; font-weight: 700; font-size: .88rem; }
.badge-desc { font-size: .72rem; color: var(--text-muted); line-height: 1.35; }

.profile-courses { display: grid; gap: .5rem; }
.pcourse { display: flex; align-items: center; gap: .75rem; padding: .6rem 0; border-bottom: 1px solid var(--border); }
.pcourse-dot { width: 10px; height: 28px; border-radius: 5px; background: var(--subj-other); flex-shrink: 0; }
.pcourse[data-subj="math"] .pcourse-dot { background: var(--subj-math); }
.pcourse[data-subj="science"] .pcourse-dot { background: var(--subj-science); }
.pcourse[data-subj="english"] .pcourse-dot { background: var(--subj-english); }
.pcourse[data-subj="social"] .pcourse-dot { background: var(--subj-social); }
.pcourse[data-subj="language"] .pcourse-dot { background: var(--subj-language); }
.pcourse[data-subj="arts"] .pcourse-dot { background: var(--subj-arts); }
.pcourse[data-subj="cs"] .pcourse-dot { background: var(--subj-cs); }
.pcourse[data-subj="business"] .pcourse-dot { background: var(--subj-business); }
.pcourse[data-subj="health"] .pcourse-dot { background: var(--subj-health); }
.pcourse[data-subj="engineering"] .pcourse-dot { background: var(--subj-engineering); }
.pcourse-name { display: block; font-weight: 500; font-size: .93rem; }
.pcourse-meta { display: block; font-size: .78rem; }

.avatar-picker { display: flex; flex-wrap: wrap; gap: .6rem; }
.avatar-opt {
  width: 54px; height: 54px; border-radius: 18px; cursor: pointer;
  display: grid; place-items: center; font-size: 1.3rem;
  background: var(--surface-hover); border: 2px solid var(--border);
  color: var(--text-secondary); font-family: inherit;
  transition: all .18s var(--ease);
}
.avatar-opt:hover { border-color: var(--border-bright); color: var(--text); }
.avatar-opt.selected { background: var(--gradient); color: var(--accent-contrast); border-color: transparent; }

@media (max-width: 720px) {
  .course-board { grid-template-columns: 1fr; }
  .profile-stats { grid-template-columns: 1fr 1fr; }
  .profile-avatar { width: 60px; height: 60px; font-size: 1.6rem; border-radius: 20px; }
  .badge-grid { grid-template-columns: 1fr 1fr; }
}

/* Billing period toggle inside the paywall. */
.paywall-interval { width: 100%; margin-bottom: .9rem; }
.paywall-interval .seg-btn { flex: 1; font-size: .8rem; }
.paywall-equiv { font-size: .78rem; margin-top: .2rem; }
