/* Self-hosted variable font (static/fonts/plus-jakarta-sans.woff2, weight
   axis 200-800) - both this file's `body{font-family:...}` and
   bootstrap-overrides.css's `--bs-font-sans-serif` name "Plus Jakarta Sans"
   as the first choice, but until now NOTHING ever actually loaded that
   font (no <link>, no @font-face) - every visitor silently fell back to
   "Segoe UI"/system-ui the whole time, on every page. Self-hosted (one
   ~27KB file, no external request/CDN, matching the rest of the project's
   vendor-everything-locally convention - Bootstrap/jQuery/Bootstrap Icons
   are all local files already, no reason fonts should be the one exception
   that reaches out to fonts.googleapis.com) rather than a Google Fonts
   <link>, which would also need a CSP allowance in
   core/security_middleware.py for a font this project doesn't otherwise
   depend on the network for. */
@font-face {
  font-family: "Plus Jakarta Sans";
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url("/static/fonts/plus-jakarta-sans.7660bd9909fb.woff2") format("woff2");
}

/* Display serif (2026-08 font pairing pass) - self-hosted variable font
   (static/fonts/fraunces-variable.woff2, weight axis 600-900, optical-size
   axis included so it stays crisp at large display sizes), same
   self-hosting rationale as Plus Jakarta Sans above (no external request,
   no CSP allowance needed). Used ONLY for large display headings (see
   `--font-display` below and its consumers: .hero h1, .section-head h2,
   .page-hero h1, .auth-split-brand h2) - a deliberate contrast-axis pairing
   (serif display + geometric sans body/UI, not two similar sans-serifs)
   that gives the brand-register marketing surfaces real typographic
   character without touching product-register UI type (h3-h6, buttons,
   labels, data - all stay Plus Jakarta Sans, per this project's existing
   one-family-for-UI convention). */
@font-face {
  font-family: "Fraunces";
  font-style: normal;
  font-weight: 600 900;
  font-display: swap;
  src: url("/static/fonts/fraunces-variable.069cb90e11a3.woff2") format("woff2");
}

:root {
  --font-display: "Fraunces", Georgia, "Times New Roman", serif;
  --brand-600: #4f46e5;
  --brand-500: #6366f1;
  --brand-400: #818cf8;
  --brand-50: #eef2ff;
  --accent-500: #14b8a6;
  --ink-900: #0f172a;
  --ink-700: #334155;
  --ink-500: #64748b;
  --ink-300: #cbd5e1;
  --ink-100: #f1f5f9;
  --surface: #ffffff;
  --surface-alt: #f8fafc;
  --danger: #dc2626;
  --success: #16a34a;
  --radius-lg: 20px;
  --radius-md: 14px;
  --radius-sm: 8px;
  --shadow-sm: 0 1px 2px rgba(15, 23, 42, .06);
  --shadow-md: 0 8px 24px rgba(15, 23, 42, .08);
  --shadow-lg: 0 20px 45px rgba(79, 70, 229, .15);
  /* Fluid container: identical to the old flat 1180px on every laptop/
     desktop viewport up to ~1660px (the vast majority of real traffic -
     `100vw - 480px` stays below the 1180px floor until then, so the clamp
     floor wins and nothing changes there), then grows linearly to 1440px
     by ~1920px so large/ultra-wide monitors stop wasting a fixed ~260px+
     strip of bare background on each side. Always well under the viewport
     itself at every width (100vw - 480px < 100vw), so this can never cause
     horizontal overflow. */
  --container: clamp(1180px, 100vw - 480px, 1440px);
}

/* Light theme only, by design - a fixed palette everywhere regardless of
   OS/browser color-scheme preference, so buttons, links and text always
   read against the same light surfaces. */
:root { color-scheme: light; }

* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
html, body { height: 100%; }
/* The slide-in mobile nav uses position:fixed + translateX(100%) to sit
   just off-screen when closed. Some browsers still count that
   off-canvas, transformed bounding box toward the document's scrollable
   area, producing a spurious horizontal scrollbar/overflow even though
   nothing is visually out of place - this is the standard, defensive fix.

   2026-08-30 FIX: this used to read `html, body { overflow-x: hidden; }`
   - setting overflow-x on BOTH elements. Per the CSS Overflow spec, when
   overflow-x is anything but `visible` and overflow-y is left
   unspecified, the UA computes overflow-y as `auto` too. Combined with
   `body`'s own `height: 100%` two lines up, that turned `<body>` into a
   second, independent scroll container exactly the size of the
   viewport - the real page scroll stayed on `<html>` (the actual
   `document.scrollingElement`), but a mouse wheel/touch scroll over the
   page would instead scroll `<body>`'s own tiny internal scrollport
   (confirmed via `document.body.scrollTop` moving while `window.scrollY`
   never did), which visually looked exactly like "nothing below the
   fold" - the real content was there, just trapped inside an
   invisible-scrollbar box that never grew past one screen's worth.
   `overflow-x: hidden` on `html` ALONE already clips the off-canvas nav
   fine (`html` is the real scrolling element per the check above) -
   `body` no longer needs its own overflow-x rule, so it keeps its
   default `visible` overflow-y and stops competing with `html` for
   scroll ownership. */
html { overflow-x: hidden; }
/* Sitewide body canvas texture (2026-08-12, "full-bleed body background"
   pass). Every section/card on the site already paints its own explicit
   background (--surface white cards, --surface-alt alternating home
   sections, the dashboard's .dash-glow-zone, etc.), so this sits purely
   BEHIND all of that - there's nothing here for those to visually
   conflict with, it only ever shows through in the gaps between them
   (page margins, section seams) where the body used to be a flat
   --surface-alt grey.

   Texture is a single 480x480 tile (static/img/illustrations/
   body-texture.svg) of scattered education motifs - a math cluster
   (sigma/integral/pi/root), orbit rings, a simple molecule, a sine wave,
   a flask silhouette - each shape carrying its own baked-in 4.5% opacity
   in brand-600 (see that file's own comment for the exact number).

   Contrast check (computed, not eyeballed): blending 4.5% brand-600
   (#4f46e5 = rgb(79,70,229)) over --surface-alt (#f8fafc = rgb(248,250,
   252)) gives approx rgb(240,241,251) - a shift of at most 8 units on
   any channel out of 255. Body text is --ink-900 (#0f172a), which sits
   at ~15.8:1 contrast against plain --surface-alt; an 8-unit lightening
   of the background can only ever move that down to roughly 15:1 in the
   worst case, nowhere near the 4.5:1 floor. By construction (opacity
   this low) the texture cannot meaningfully affect legibility anywhere
   it appears under running text. */
body {
  margin: 0;
  font-family: "Plus Jakarta Sans", "Segoe UI", system-ui, -apple-system, Roboto, Helvetica, Arial, sans-serif;
  color: var(--ink-900);
  background-color: var(--surface-alt);
  background-image: url("/static/img/illustrations/body-texture.433ed25a1312.svg");
  background-repeat: repeat;
  background-position: top left;
  background-size: 480px 480px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  display: flex; flex-direction: column; min-height: 100vh;
}
/* Sticky footer: main grows to fill any leftover space so footer always
   sits at the bottom of the viewport on short pages, but still flows
   naturally below content on long pages. */
main { flex: 1 0 auto; }
footer { flex-shrink: 0; }
/* ---------- Typography scale (Task: sitewide typography pass, 2026-07-24) ----------
   Before this pass, h1-h4 all shared one flat font-weight/color rule with no actual
   font-size scale of their own (every page-specific heading either used a bespoke
   inline size like `.hero h1`/`.section-head h2`, or - on plain content pages like
   question_bank_detail.html/my_payments.html/student_dashboard.html - fell through
   to Bootstrap's own default browser-agnostic h1-h6 sizes, which don't share this
   project's rem scale/weight/letter-spacing at all, so two <h2>s on two different
   pages could render at visibly different sizes/weights for no reason). Establishes
   one real scale (h1 largest down to h6 smallest, weight easing off at h5/h6 rather
   than every heading being maximally bold) that every plain heading tag sitewide now
   follows unless a page opts into one of the bespoke oversized hero/section variants
   above/below (those keep their own clamp()-based sizing for their specific layout
   role - this scale is the sane *default*, not a replacement for those). */
h1 { font-size: 2rem; font-weight: 800; line-height: 1.2; letter-spacing: -.01em; color: var(--ink-900); margin: 0 0 .6em; }
h2 { font-size: 1.5rem; font-weight: 800; line-height: 1.25; letter-spacing: -.01em; color: var(--ink-900); margin: 0 0 .55em; }
h3 { font-size: 1.2rem; font-weight: 700; line-height: 1.3; letter-spacing: -.005em; color: var(--ink-900); margin: 0 0 .5em; }
h4 { font-size: 1.05rem; font-weight: 700; line-height: 1.35; color: var(--ink-900); margin: 0 0 .5em; }
h5 { font-size: .95rem; font-weight: 700; line-height: 1.4; color: var(--ink-900); margin: 0 0 .5em; }
h6 { font-size: .85rem; font-weight: 700; line-height: 1.4; color: var(--ink-700); text-transform: uppercase; letter-spacing: .04em; margin: 0 0 .5em; }
/* Bold/emphasis: plain b/strong stays the body-text color (not brand blue - color
   alone already used for links, weight alone is enough for in-paragraph emphasis so
   the two signals don't collide) at the surrounding text's own weight+50, never
   forced to 800/900 - a paragraph with several bold words shouldn't out-shout its
   own heading. */
b, strong { color: var(--ink-900); font-weight: 700; }

/* reCAPTCHA v3 badge: hidden per-page (it has no UI to click), but Google's
   terms require the disclosure text below wherever the API is loaded. */
.grecaptcha-badge { visibility: hidden; }
a { color: var(--brand-600); text-decoration: none; }
img { max-width: 100%; display: block; }
.container { max-width: var(--container); margin: 0 auto; padding: 0 24px; }
.icon { flex-shrink: 0; }

/* ---------- Navbar ----------
   Rebuilt 2026-07-24 to use Bootstrap5's real navbar/collapse/dropdown
   components (navbar-toggler + .collapse.navbar-collapse driven purely by
   bootstrap.bundle.min.js's own JS, data-bs-toggle="collapse"/"dropdown" -
   no custom hand-rolled open/close JS needed anymore). Only small brand-fit
   additions live here now; the old .nav-links/.dropdown-panel/.mega-panel/
   .mobile-toggle/.nav-scrim rules (and their >1300px-breakpoint overrides
   further down this file) drove a fully custom nav implementation that
   never actually used Bootstrap5's toggle/collapse JS API - removed
   entirely rather than left as confusing dead weight alongside the real
   Bootstrap markup now in templates/base.html. */
.navbar {
  z-index: 1030;
  transition: box-shadow .25s ease, background-color .25s ease, backdrop-filter .25s ease;
}
.navbar-scrolled {
  box-shadow: 0 10px 34px rgba(15, 23, 42, .09);
  background-color: rgba(255, 255, 255, .92) !important;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
.brand-badge {
  width: 36px; height: 36px; border-radius: 10px;
  background: var(--brand-600);
  display: flex; align-items: center; justify-content: center; color: #fff; font-weight: 900;
}
/* Navbar shell (2026-08-28 polish pass) - a soft elevation shadow instead
   of a flat 1px border-bottom, so the sticky navbar reads as a distinct,
   floating layer above page content on scroll rather than a hairline
   rule that visually fuses with whatever's directly under it. */
.site-navbar { box-shadow: 0 1px 3px rgba(15, 23, 42, .06), 0 1px 2px rgba(15, 23, 42, .04); padding-top: 12px; padding-bottom: 12px; }

/* Navbar brand lockup (2026-08 navbar redesign) - replaces the old
   raster-image fallback (static/img/navlogo.png, cropped via CSS to hide a
   mismatched baked-in tagline) with a real, self-built mark + wordmark: no
   image dependency, nothing to crop or mis-scale at any viewport. The
   `.brand-badge` gradient square gets a circular variant + a graduation-cap
   glyph instead of a plain "P", and the wordmark uses the site's own
   display weight/tracking so it reads as a confident lockup, not a plain
   text link. */
.navbar-brand-badge {
  width: 38px; height: 38px; border-radius: 11px; font-size: 1.05rem;
  box-shadow: 0 6px 16px rgba(79, 70, 229, .28);
}
.navbar-brand-word {
  font-size: 1.2rem; font-weight: 800; letter-spacing: -.01em; color: var(--ink-900);
}
/* Admin-uploaded logo (site.logo branch in base.html) - height-capped,
   width auto so nothing is ever stretched/squashed regardless of the
   uploaded image's own aspect ratio. */
.navbar-logo-img { height: 36px; width: auto; max-width: 160px; object-fit: contain; }

/* Nav links: an animated brand-gradient underline instead of a flat active
   color, so the current page and hover state both read with real motion
   feedback rather than a single static color swap. Scoped to `.nav-link-
   text` (base.html) specifically - the icon-only cart button and the
   avatar dropdown toggle also carry Bootstrap's `.nav-link` class but
   don't want an underline drawn under an icon. */
.nav-link-text {
  position: relative; font-weight: 600; margin: 0 2px; border-radius: 999px; color: var(--ink-700);
  padding: 8px 14px !important; transition: background .15s ease, color .15s ease;
}
.nav-link-text:hover { background: var(--ink-100); color: var(--ink-900); }
.nav-link-text.active { color: var(--brand-600); }
.nav-link-text::after {
  content: ""; position: absolute; left: 14px; right: 14px; bottom: 2px; height: 2px; border-radius: 2px;
  background: linear-gradient(90deg, var(--brand-600), var(--accent-500));
  transform: scaleX(0); transform-origin: left; transition: transform .25s cubic-bezier(.16, 1, .3, 1);
}
.nav-link-text:hover::after, .nav-link-text.active::after { transform: scaleX(1); }

/* Cart / icon-only nav buttons - a real circular hit target with a hover
   background instead of a bare icon floating in the nav row. */
.navbar-icon-btn {
  width: 40px; height: 40px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
  color: var(--ink-700); font-size: 1.05rem; transition: background .15s ease, color .15s ease;
}
.navbar-icon-btn:hover { background: var(--ink-100); color: var(--brand-600); }

/* Avatar dropdown toggle - a real initials chip (brand gradient circle)
   instead of a generic bi-person-circle glyph, matching the identity
   language `.brand-badge`/`.icon-badge` already use elsewhere on the site. */
.navbar-avatar-btn { display: flex; align-items: center; gap: 8px; padding: 6px 10px !important; border-radius: 999px; text-decoration: none; transition: background .15s ease; }
.navbar-avatar-btn:hover { background: var(--ink-100); }
.navbar-avatar {
  width: 30px; height: 30px; border-radius: 50%; flex-shrink: 0;
  background: var(--brand-600);
  color: #fff; font-weight: 800; font-size: .82rem; display: inline-flex; align-items: center; justify-content: center;
}
.navbar-avatar-btn span:not(.navbar-avatar) { color: var(--ink-900); font-weight: 600; font-size: .92rem; }
.navbar-avatar-caret { font-size: .7rem; color: var(--ink-500); }

/* Mega dropdown for "Classes": wider than Bootstrap's default dropdown-menu
   so a whole class/course/subject-group hierarchy can lay out in columns;
   collapses back to the dropdown's natural full-width block on narrow
   viewports (matches the navbar-collapse breakpoint) rather than
   overflowing sideways off-screen. */
.mega-dropdown-menu {
  min-width: 560px; max-width: min(880px, 92vw); border: none; border-radius: var(--radius-lg, 16px);
  box-shadow: 0 12px 32px rgba(15, 23, 42, .12), 0 2px 8px rgba(15, 23, 42, .06); margin-top: 10px !important;
}
@media (max-width: 991.98px) {
  .mega-dropdown-menu { min-width: 0; width: 100%; max-width: none; box-shadow: none; margin-top: 0 !important; }
}
.dropdown-menu { border-radius: var(--radius-md, 12px); border-color: var(--ink-100); box-shadow: 0 8px 24px rgba(15, 23, 42, .1); }

/* Navbar search box (2026-08-28 redesign, v2 - wider/taller pill with a
   real submit affordance) - a real pill-shaped search field with a soft
   neutral fill and a brand focus ring, sized to actually read as a
   primary piece of navbar chrome rather than a cramped 240px afterthought
   squeezed between the nav links and the avatar. Capped width + vertically
   centered at desktop (lg+) so it doesn't stretch across the whole
   nav-item row; goes full-width the moment the navbar collapses
   (<992px, matching every other navbar-expand-lg breakpoint already used
   in this file). */
.navbar-search-form { max-width: 300px; }
@media (min-width: 1200px) {
  .navbar-search-form { max-width: 340px; }
}
@media (max-width: 991.98px) {
  .navbar-search-form { max-width: none; }
}
.navbar-search-group {
  border-radius: 999px; overflow: hidden; background: var(--surface-alt);
  border: 1px solid var(--ink-200); box-shadow: 0 1px 2px rgba(15, 23, 42, .04);
  transition: border-color .15s ease, box-shadow .15s ease, background .15s ease;
}
.navbar-search-group:hover { border-color: var(--ink-300); }
.navbar-search-group:focus-within {
  border-color: var(--brand-400); background: #fff;
  box-shadow: 0 0 0 4px var(--brand-50), 0 2px 8px rgba(15, 23, 42, .06);
}
.navbar-search-group .input-group-text { background: transparent; border: none; color: var(--ink-500); padding: 0 4px 0 16px; font-size: .95rem; }
.navbar-search-group .form-control {
  background: transparent; border: none; box-shadow: none !important; padding: 10px 6px; font-size: .92rem; height: auto;
}
.navbar-search-group .form-control::placeholder { color: var(--ink-500); }
.navbar-search-submit-btn {
  border: none; background: transparent; color: var(--ink-500); width: 40px; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center; font-size: 1rem;
  transition: background .15s ease, color .15s ease;
}
.navbar-search-submit-btn:hover { background: var(--brand-50); color: var(--brand-600); }
.navbar-search-group:focus-within .navbar-search-submit-btn { color: var(--brand-600); }

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 11px 22px; border-radius: 999px; font-weight: 700; font-size: .95rem;
  /* Bug found sitewide (2026-08, during the home-page rhythm pass): this
     used to be a hardcoded `border: 1px solid transparent`, which - since
     this rule loads after bootstrap.min.css/bootstrap-overrides.css at
     equal (0,1,0) specificity - unconditionally won the cascade over
     Bootstrap's OWN `.btn-outline-primary`/`.btn-outline-secondary`/etc,
     which set their real border color via the `--bs-btn-border-color`
     custom property rather than a literal value. The result: every plain
     Bootstrap outline button anywhere on the site (navbar "Log in", the
     home hero's "Browse Classes" before it was fixed locally, any other
     page using `.btn-outline-primary` directly) rendered with a fully
     invisible border - text-only, no visible edge. Deferring to the
     variable (falling back to transparent only when nothing sets it, e.g.
     `.btn-primary`/`.btn-link` which have no border variant) fixes every
     such button sitewide in one place instead of requiring a per-page
     `!important` workaround like `.home-page .btn-outline-primary-strong`
     needed - that page-scoped class stays as an intentional STRONGER
     (2px) variant, not a workaround for this bug anymore. */
  border: 1px solid var(--bs-btn-border-color, transparent);
  cursor: pointer; transition: transform .15s ease, box-shadow .15s ease, background .15s ease, border-color .15s ease;
}
.btn:active { transform: scale(.97); }
/* Solid, not the diagonal two-stop brand gradient this file used to
   apply to nearly every filled surface (buttons, badges, icon circles) -
   the same 135deg indigo->teal recipe repeated on unrelated elements
   read as generic/AI-templated rather than designed. Depth now comes
   from box-shadow + a real hover-state color shift instead. */
.btn-primary { background: var(--brand-600); color: #fff; box-shadow: var(--shadow-lg); transition: background .15s ease, box-shadow .15s ease, transform .15s ease; }
.btn-primary:hover { background: var(--brand-500); box-shadow: 0 22px 50px rgba(79,70,229,.28); transform: translateY(-1px); }
.btn-outline { background: transparent; border-color: var(--ink-300); color: var(--ink-900); }
.btn-outline:hover { border-color: var(--brand-500); color: var(--brand-600); }
.btn-ghost { background: transparent; color: var(--ink-700); }
.btn-block { width: 100%; }
.btn-sm { padding: 8px 16px; font-size: .85rem; }

/* ---------- Hero ---------- */
.hero { position: relative; overflow: hidden; padding: 76px 0 90px; }
.hero-grid { display: grid; grid-template-columns: 1.05fr .95fr; gap: 48px; align-items: center; }
.eyebrow {
  display: inline-flex; align-items: center; gap: 8px; background: var(--brand-50); color: var(--brand-600);
  font-weight: 700; font-size: .8rem; padding: 6px 14px; border-radius: 999px; margin-bottom: 18px;
}
.hero h1 { font-family: var(--font-display); font-size: clamp(2.1rem, 4vw, 3.4rem); font-weight: 700; line-height: 1.08; margin: 0 0 18px; letter-spacing: -.02em; text-wrap: balance; }
.hero p.lead { font-size: 1.15rem; color: var(--ink-700); max-width: 540px; margin: 0 0 30px; }
.hero-actions { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 34px; }
.hero-stats { display: flex; gap: 34px; flex-wrap: wrap; }
.hero-stat b { display: block; font-size: 1.5rem; color: var(--ink-900); }
.hero-stat span { font-size: .85rem; color: var(--ink-500); }
.hero-art { position: relative; }
.blob {
  position: absolute; border-radius: 50%; filter: blur(10px); opacity: .5; z-index: 0;
  background: radial-gradient(circle at 30% 30%, var(--brand-400), transparent 70%);
  animation: float 8s ease-in-out infinite;
}
@keyframes float { 0%, 100% { transform: translateY(0) } 50% { transform: translateY(-18px) } }
.hero-search {
  position: relative; z-index: 2; background: var(--surface); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg); padding: 22px; border: 1px solid var(--ink-100);
}
.search-box { display: flex; align-items: center; gap: 10px; background: var(--surface-alt); border-radius: var(--radius-md); padding: 12px 16px; border: 1px solid var(--ink-100); }
.search-box input { border: none; background: none; outline: none; flex: 1; font-size: .95rem; color: var(--ink-900); }
.search-chips { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 14px; }
.chip { background: var(--brand-50); color: var(--brand-600); font-size: .8rem; font-weight: 600; padding: 6px 12px; border-radius: 999px; }

/* ---------- Hero photo panel (image-treatment) ----------
   Real photography (hero-study.jpg) layered into .hero-art as a small
   rounded, shadowed panel peeking from behind the bottom-left corner of
   the functional .hero-search mockup card - it does NOT replace the
   search mockup, just adds a photographic accent beside it. z-index 1
   keeps it above the abstract .blob (z-index 0) but below .hero-search
   (z-index 2), and it lives entirely inside the image-only .hero-art
   column, never behind or under the hero copy in the text column, so it
   can't affect that text's contrast. */
.hero-photo-wrap {
  position: absolute; z-index: 1; width: 200px; height: 240px;
  left: -26px; bottom: -26px; border-radius: var(--radius-lg);
  overflow: hidden; box-shadow: var(--shadow-lg); border: 4px solid var(--surface);
  transform: rotate(-4deg);
}
.hero-photo-wrap img { width: 100%; height: 100%; object-fit: cover; display: block; }
.hero-photo-wrap::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(160deg, rgba(79, 70, 229, .22), rgba(15, 23, 42, .05));
}
@media (max-width: 600px) {
  .hero-photo-wrap { width: 120px; height: 150px; left: -12px; bottom: -12px; border-width: 3px; }
}

/* ---------- Hero carousel (admin-managed HeroSlide rows, pages/models.py) ----------
   Multiple `.hero-slide`s share one `.hero-slide-track`; only `.is-active`
   is displayed (crossfade via `heroFadeIn`). JS (main.js) toggles
   `.is-active` + the `inert` attribute together, so an inactive slide's
   links/inputs are neither visible nor tabbable - inert is the actual
   accessibility gate, not just the opacity. Falls back to a single
   always-active slide with no JS at all (server renders `.is-active` on
   the first slide already), so nothing here is JS-dependent for the
   common one-slide case. */
.hero-slide { display: none; }
.hero-slide.is-active { display: block; }
@media (prefers-reduced-motion: no-preference) {
  .hero-slide.is-active { animation: heroFadeIn .5s ease both; }
}
@keyframes heroFadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }

.hero-offer-badge {
  display: inline-flex; align-items: center; gap: 6px; background: #fef3c7; color: #92400e;
  font-weight: 700; font-size: .78rem; padding: 6px 12px; border-radius: 999px; margin: 0 0 12px;
}
.btn-hero-fill { color: #fff; border: 2px solid transparent; }
.btn-hero-fill:hover { filter: brightness(1.07); }
.btn-hero-outline { background: transparent; border: 2px solid; }
.btn-hero-outline:hover { background: rgba(15, 23, 42, .05); }

.hero-logo-wrap {
  position: relative; z-index: 1; display: flex; align-items: center; justify-content: center;
  width: 100%; min-height: 320px;
}
.hero-logo-wrap img { width: 220px; height: 220px; object-fit: contain; filter: drop-shadow(0 18px 30px rgba(15, 23, 42, .12)); }

.hero-search-inline { max-width: 460px; margin-top: 8px; }
.hero-search-inline .search-box { background: var(--surface); box-shadow: var(--shadow-lg); }

.hero-carousel-controls { display: flex; align-items: center; justify-content: center; gap: 18px; margin-top: 8px; }
.hero-carousel-arrow {
  display: flex; align-items: center; justify-content: center; width: 38px; height: 38px;
  border-radius: 50%; border: 1px solid var(--ink-100); background: var(--surface); color: var(--ink-700);
  cursor: pointer; transition: background .15s, color .15s, border-color .15s;
}
.hero-carousel-arrow:hover { background: var(--brand-50); color: var(--brand-600); border-color: var(--brand-400); }
.hero-dots { display: flex; gap: 8px; }
.hero-dot {
  width: 9px; height: 9px; border-radius: 50%; background: var(--ink-300); border: none; padding: 0;
  cursor: pointer; transition: background .15s, transform .15s;
}
.hero-dot.is-active { background: var(--brand-600); transform: scale(1.2); }
@media (max-width: 600px) {
  .hero-logo-wrap { min-height: 220px; }
  .hero-logo-wrap img { width: 150px; height: 150px; }
}

/* ---------- Sections ---------- */
.section { padding: 72px 0; }
.section-head { text-align: center; max-width: 640px; margin: 0 auto 44px; }
.section-head h2 { font-family: var(--font-display); font-weight: 700; font-size: clamp(1.6rem, 3vw, 2.2rem); margin: 10px 0 12px; letter-spacing: -.01em; text-wrap: balance; }
.section-head p { color: var(--ink-500); font-size: 1.05rem; }
.kicker { color: var(--brand-600); font-weight: 700; letter-spacing: .06em; text-transform: uppercase; font-size: .8rem; }

/* ---------- Cards / Grid ---------- */
.grid { display: grid; gap: 24px; }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-4 { grid-template-columns: repeat(4, 1fr); }
.grid-2 { grid-template-columns: repeat(2, 1fr); }
.card {
  background: var(--surface); border-radius: var(--radius-md); border: 1px solid var(--ink-100);
  padding: 28px; box-shadow: var(--shadow-sm); transition: transform .2s ease, box-shadow .2s ease;
}
.card:hover { transform: translateY(-4px); box-shadow: var(--shadow-md); }
.card-icon {
  width: 60px; height: 60px; border-radius: 16px; display: flex; align-items: center; justify-content: center;
  background: var(--brand-50); color: var(--brand-600); margin-bottom: 18px;
}
.card h3 { margin: 0 0 8px; font-size: 1.1rem; }
.card p { margin: 0; color: var(--ink-500); font-size: .93rem; }

.level-card { background: var(--surface); border-radius: var(--radius-lg); padding: 30px; border: 1px solid var(--ink-100); position: relative; overflow: hidden; }
.level-card::before {
  content: ""; position: absolute; inset: 0; background: linear-gradient(160deg, var(--brand-50), transparent 60%); opacity: .7;
}
.level-card * { position: relative; }
.level-badge { width: 46px; height: 46px; border-radius: 12px; background: var(--brand-600); color: #fff; display: flex; align-items: center; justify-content: center; margin-bottom: 16px; }

/* ---------- CTA band ---------- */
.cta-band {
  background: var(--brand-600); border-radius: var(--radius-lg);
  padding: 56px; color: #fff; display: flex; align-items: center; justify-content: space-between; gap: 30px; flex-wrap: wrap;
}
.cta-band h2 { margin: 0 0 8px; font-size: 1.8rem; }
.cta-band p { margin: 0; opacity: .9; }
.cta-band .btn-primary { background: #fff; color: var(--brand-600); box-shadow: none; }

/* ---------- Full-bleed photo band (image-treatment) ----------
   A standalone section - independent of .cta-band above - that gives the
   homepage one moody, full-bleed photography moment using the dark/
   textured open-notes.jpg as a background. A near-opaque dark scrim
   (rgba(15,23,42,.82-.9), i.e. close to solid var(--ink-900)) sits between
   the photo and the text so white heading/body copy stays comfortably
   above the 4.5:1 AA contrast minimum no matter which part of the source
   photo lands behind it. Background image is referenced the same way the
   sitewide self-hosted @font-face at the top of this file references
   /static/fonts/... directly - static/img/photos/ is a fixed, versioned
   collection, not per-view data. */
.photo-band {
  position: relative; border-radius: var(--radius-lg); overflow: hidden;
  padding: 64px 40px; text-align: center;
  background-image: linear-gradient(180deg, rgba(15, 23, 42, .82), rgba(15, 23, 42, .9)), url("/static/img/photos/open-notes.913ed87a83ea.jpg");
  background-size: cover; background-position: center;
}
.photo-band-content { position: relative; z-index: 1; max-width: 620px; margin: 0 auto; }
.photo-band-content .kicker { color: var(--brand-400); }
.photo-band-content h2 { color: #fff; margin: 10px 0 14px; font-size: clamp(1.6rem, 3vw, 2.2rem); }
.photo-band-content p { color: rgba(255, 255, 255, .85); font-size: 1.05rem; margin: 0 0 26px; }
@media (max-width: 600px) {
  .photo-band { padding: 48px 22px; }
}

/* ---------- Footer ----------
   Column layout is now a real Bootstrap5 `.row`/`.col-*` grid (see
   templates/base.html) instead of the old fixed 4-column CSS grid
   (`.footer-grid`, `1.4fr 1fr 1fr 1fr` with no tablet-width step between
   4-across and a single stacked column) - `.footer-col` below only styles
   the link-list typography/color inside each Bootstrap column, it no
   longer owns the column widths themselves. */
footer { background: var(--ink-900); color: #cbd5e1; padding: 60px 0 26px; margin-top: 40px; }
.footer-brand-word { font-size: 1.15rem; font-weight: 800; color: #fff; letter-spacing: -.01em; }
/* 2026-08-30 nav/footer polish pass: a short accent underline instead of
   plain white text gives each footer column a real section-heading feel
   at a glance, matching the brand-gradient underline already used on
   `.nav-link-text` above rather than introducing a new visual language. */
.footer-col h4 {
  color: #fff; font-size: .82rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  margin: 0 0 18px; padding-bottom: 10px; position: relative;
}
.footer-col h4::after {
  content: ""; position: absolute; left: 0; bottom: 0; width: 28px; height: 2px; border-radius: 2px;
  background: var(--brand-500);
}
.footer-col ul { list-style: none; margin: 0; padding: 0; }
.footer-col li { margin-bottom: 10px; }
.footer-col a { color: #94a3b8; font-size: .9rem; transition: color .15s ease; }
.footer-col a:hover { color: #fff; }
.footer-contact { margin: 18px 0 0; }
.footer-contact li { margin-bottom: 10px; }
.footer-contact a, .footer-contact span { display: flex; align-items: center; gap: 8px; color: #cbd5e1; font-size: .88rem; }
.footer-contact a:hover { color: #fff; }
.footer-contact .muted { color: #64748b; font-size: .78rem; }
.footer-social { display: flex; gap: 10px; margin-top: 18px; flex-wrap: wrap; }
.footer-social a {
  width: 36px; height: 36px; border-radius: 999px; background: rgba(255,255,255,.08);
  display: flex; align-items: center; justify-content: center; color: #fff; transition: background .15s ease, transform .15s ease;
}
.footer-social a:hover { background: var(--brand-500); transform: translateY(-2px); }

.social-chip {
  width: 40px; height: 40px; border-radius: 999px; background: var(--brand-50); color: var(--brand-600);
  display: flex; align-items: center; justify-content: center; transition: background .15s ease, color .15s ease, transform .15s ease;
}
.social-chip:hover { background: var(--brand-600); color: #fff; transform: translateY(-2px); }
.footer-bottom { border-top: 1px solid rgba(255,255,255,.1); margin-top: 40px; padding-top: 20px; display: flex; justify-content: space-between; flex-wrap: wrap; gap: 10px; font-size: .82rem; color: #64748b; }

/* ---------- Forms ---------- */
/* Explicit, guaranteed-4.5:1 placeholder color sitewide (2026-08-13) -
   previously left to Bootstrap's own default (`--bs-secondary-color`,
   opacity-derived from body text color), which is fine on plain white but
   drifts on any tinted input background (e.g. `.login-hero-form .form-
   control`'s var(--surface) fill). One explicit rule everywhere instead of
   guessing per-context. */
.form-control::placeholder { color: var(--ink-500); opacity: 1; }
.auth-shell { position: relative; max-width: 560px; margin: 60px auto; padding: 0 20px; }
/* 2026-08-30: the full-viewport auth-bg.svg wash (dot-grid scatter + soft
   blob shapes behind the card) is gone - on a real screen it read as a
   busy, drenched-gradient background competing with the card itself
   instead of supporting it (explicit "the gradients look cheap" feedback
   on the auth/cart flow). The page now sits on the site's plain
   --surface-alt background; the card/split-card's own header/brand-panel
   gradient is the only saturated color on the page, which is enough. */
/* No padding on .auth-card itself on purpose: the lower section of every
   template using it nests its content in Bootstrap's own
   `.card-body.p-4.p-md-5`, which already provides responsive padding
   (24px mobile / 48px desktop). Adding padding here too would double-stack
   it. The card is now two visually distinct bands - a branded gradient
   header (`.auth-card-header`, full-bleed, clipped to the card's own
   border-radius via overflow:hidden below) and a body with its own faint
   brand tint fading to white - rather than a single flat sheet with a
   small icon floating on it. */
.auth-card {
  position: relative;
  background: var(--surface); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md), 0 24px 60px rgba(79, 70, 229, .16);
  border: 1px solid var(--ink-100);
  overflow: hidden;
}
.auth-card .form-control, .auth-card .form-select { padding-top: .6rem; padding-bottom: .6rem; }
/* Faint brand tint at the top of the body, fading to plain white by the
   time it reaches the form fields - a echo of the header gradient above
   it rather than a hard cut from "saturated" to "flat white". */
.auth-card .card-body { padding-top: 32px; background: linear-gradient(180deg, var(--brand-50) 0%, var(--surface) 200px); }
.auth-card h1 { font-size: 1.5rem; margin: 0 0 6px; }
.auth-card .sub { color: var(--ink-500); margin-bottom: 26px; font-size: .93rem; }

/* Branded gradient header band - reuses the exact gradient already
   established on register.html's `.auth-split-brand` panel elsewhere in
   this file, so the single-column auth pages read as the same product
   rather than a generic Bootstrap card with an icon on it. */
.auth-card-header {
  position: relative;
  overflow: hidden;
  text-align: center;
  padding: 40px 40px 34px;
  background: linear-gradient(155deg, var(--brand-600) 0%, var(--brand-500) 55%, var(--accent-500) 130%);
}
.auth-card-header::before {
  content: ""; position: absolute; inset: -45% -25% auto auto; width: 240px; height: 240px;
  border-radius: 50%; background: radial-gradient(circle, rgba(255, 255, 255, .18), transparent 70%);
}
.auth-card-header::after {
  content: ""; position: absolute; inset: auto auto -55% -15%; width: 200px; height: 200px;
  border-radius: 50%; background: radial-gradient(circle, rgba(255, 255, 255, .12), transparent 70%);
}
.auth-card-header .icon-badge {
  position: relative;
  width: 54px; height: 54px; font-size: 1.35rem; margin-bottom: 10px;
  background: rgba(255, 255, 255, .18);
  color: #fff;
  box-shadow: none;
  border: 1px solid rgba(255, 255, 255, .3);
}
/* Small uppercase eyebrow above the heading - the Stripe/Linear-style
   third typographic voice (between the icon and the display heading)
   that gives the header real hierarchy instead of icon-then-heading. */
.auth-card-header .kicker {
  position: relative; display: block; font-size: .72rem; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase; color: rgba(255, 255, 255, .78);
  margin-bottom: 8px;
}
.auth-card-header h1 {
  position: relative; color: #fff; margin: 0 0 8px;
  font-family: var(--font-display); font-weight: 700; font-size: 1.85rem; letter-spacing: -.01em;
}
.auth-card-header .sub { position: relative; color: rgba(255, 255, 255, .86); margin: 0; font-size: .95rem; font-weight: 400; }
.auth-card-header .step-row { position: relative; display: flex; justify-content: space-between; align-items: center; margin-bottom: 18px; }
.auth-card-header .step-row .badge { background: rgba(255, 255, 255, .2) !important; color: #fff !important; border-color: rgba(255, 255, 255, .35) !important; }
.auth-card-header .step-row .helper-text { color: rgba(255, 255, 255, .8); margin-top: 0; }
/* Error/expired-link variant of the header (password_reset_confirm.html) -
   a warmer red gradient instead of the brand one, same treatment otherwise. */
.auth-card-header.auth-card-header-danger { background: linear-gradient(155deg, #dc2626 0%, #ef4444 60%, #f87171 130%); }
.icon-badge-danger { background: rgba(255, 255, 255, .18); color: #fff; box-shadow: none; border: 1px solid rgba(255, 255, 255, .3); }
/* Typographic hierarchy inside the white body: bold, slightly larger
   labels against regular-weight helper copy, rather than one flat weight
   for every line of text in the form. */
.auth-card .form-label, .auth-split-form .form-label { font-weight: 700; font-size: .87rem; color: var(--ink-900); }
.auth-card .form-text, .auth-split-form .form-text { font-weight: 400; color: var(--ink-500); }
.auth-card .btn-primary, .auth-split-form .btn-primary, .auth-card .btn-outline-secondary {
  font-weight: 700; letter-spacing: .01em; padding-top: .7rem; padding-bottom: .7rem;
}
/* ---------- OTP entry (verify.html, password_reset_verify_phone.html) ----------
   2026-08-13 OTP UX pass. `.otp-boxes` progressively replaces the single
   VerifyCodeForm text input (static/js/main.js's wireOtpBoxes) with 6
   individually-focused digit boxes; the real field is kept in the DOM
   (visually-hidden, aria-hidden) as the actual submitted value and as the
   no-JS fallback - see accounts/forms.py's VerifyCodeForm docstring. */
.otp-boxes { display: flex; justify-content: center; gap: 10px; margin: 4px 0 6px; }
.otp-box {
  width: 46px; height: 54px; text-align: center; font-size: 1.4rem; font-weight: 700;
  color: var(--ink-900); background: var(--surface-alt); border: 1.5px solid var(--ink-300);
  border-radius: var(--radius-sm); outline: none; transition: border-color .15s, box-shadow .15s, background-color .15s;
}
.otp-box:focus { border-color: var(--brand-500); background: var(--surface); box-shadow: 0 0 0 3px var(--brand-50); }
.otp-box.is-filled { border-color: var(--brand-400); background: var(--surface); }
.otp-boxes.is-invalid .otp-box { border-color: var(--danger); }
@media (max-width: 380px) { .otp-box { width: 38px; height: 48px; font-size: 1.2rem; } .otp-boxes { gap: 7px; } }

/* Expiry + resend status row beneath the OTP boxes - small, muted, never
   competing with the primary "Verify" action. `.otp-status-urgent` (last
   60s of the expiry countdown, set by main.js) shifts the expiry text to
   the danger color so a code about to lapse actually reads as urgent. */
.otp-status { display: flex; justify-content: center; gap: 6px; font-size: .82rem; color: var(--ink-500); margin-top: 2px; }
.otp-status-urgent { color: var(--danger); font-weight: 600; }
.otp-status-expired { color: var(--danger); font-weight: 600; }

.field { margin-bottom: 18px; }
.field label { display: block; font-weight: 600; font-size: .85rem; margin-bottom: 6px; color: var(--ink-700); }
.field input, .field textarea, .field select {
  width: 100%; padding: 12px 14px; border-radius: var(--radius-sm); border: 1px solid var(--ink-300);
  font-size: .95rem; background: var(--surface); color: var(--ink-900); outline: none; transition: border-color .15s;
}
.field input:focus, .field textarea:focus, .field select:focus { border-color: var(--brand-500); box-shadow: 0 0 0 3px var(--brand-50); }
.field .errorlist { list-style: none; padding: 0; margin: 6px 0 0; color: var(--danger); font-size: .82rem; }

/* Leading icons inside form fields (full name, email, phone, identifier,
   institution, password). Wraps just the input itself (not the label) in
   `.field-input-wrap` so the icon centers on the input box regardless of
   label height/wrapping - no fragile per-template pixel offsets needed. */
.field-input-wrap { position: relative; display: flex; align-items: center; }
.field-input-wrap .field-icon {
  position: absolute; left: 14px; top: 50%; transform: translateY(-50%); z-index: 2;
  color: var(--ink-500); font-size: .95rem; pointer-events: none; line-height: 1;
}
.field-input-wrap .form-control, .field-input-wrap .form-select { padding-left: 40px; }
/* Required-field marker - added next to the label text (not a placeholder
   trick) only for fields that are actually `required=True` on the Django
   form; optional fields simply carry no mark instead of an "(optional)"
   label suffix. */
.required-mark { color: var(--danger); margin-left: 2px; }
/* Right-side live-validation status icon (e.g. phone format check) -
   independent of the left-side `.field-icon` and the password toggle,
   sits at the same right offset a `.pw-field` toggle button would (this
   is only ever used on non-password fields, so no collision). Hidden
   until JS opts a field into a validity state via `.is-valid`/`.is-invalid`
   on the wrapper. */
.field-input-wrap .field-status-icon {
  position: absolute; right: 14px; top: 50%; transform: translateY(-50%);
  font-size: 1rem; display: none; pointer-events: none;
}
.field-input-wrap.is-valid .field-status-icon.bi-check-circle-fill { display: block; color: var(--success); }
.field-input-wrap.is-invalid .field-status-icon.bi-x-circle-fill { display: block; color: var(--danger); }
.field-match-note { font-size: .82rem; margin-top: 6px; }
.field-match-note.text-success { color: var(--success); }
.field-match-note.text-danger { color: var(--danger); }
/* Reserve room on the right for the phone field's live-validation icon
   (register.html) so typed digits never run under it. */
#phone-field-wrap .form-control { padding-right: 40px; }
/* Password inputs get wrapped a second time by main.js in `.pw-field`
   (right-side show/hide toggle, padding-right: 44px - see below). That
   wrapper nests inside `.field-input-wrap`, so this rule's padding-left
   and the `.pw-field` rule's padding-right both land on the same
   `.form-control` without conflicting. */

/* ---------- Auth split layout (register.html) ----------
   The single-column .auth-card works for a 1-2 field form (login,
   password reset) but a full registration form left a lot of the page
   as bare background either side of a narrow centered box - "full width"
   here isn't stretching the inputs themselves (a wide text input is its
   own usability problem) but giving the page real structure: a brand/
   value-prop panel + a form panel, side by side, the pattern most
   product signup flows use (Linear/Stripe/Notion-style split screen)
   instead of a lonely floating card. Field pairs that are natural
   alternatives (email/phone) or related (class/institution) sit in a
   2-column grid within the form panel itself, so the extra width is
   doing real layout work, not just padding. */
/* Widened 1000px -> 1180px (2026-08-13, explicit "increase the register
   form width" request) - the form panel's own 2-column field grid
   (auth-field-grid, register.html) was noticeably cramped at 1000px total
   width once split against the brand panel; matches this project's
   standard --container reading width instead of a bespoke narrower one. */
.auth-split { position: relative; max-width: 1180px; margin: 48px auto; padding: 0 20px; }
/* 2026-08-30: full-viewport auth-bg.svg wash removed here too - see
   .auth-shell's own note above for the full rationale. */
.auth-split-card {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, .85fr) minmax(0, 1.25fr);
  background: var(--surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg), 0 20px 60px rgba(79, 70, 229, .12);
  border: 1px solid var(--ink-100);
  overflow: hidden;
}
.auth-split-card::before {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 4px; z-index: 1;
  background: linear-gradient(90deg, var(--brand-600), var(--accent-500));
}
.auth-split-brand {
  position: relative;
  background: linear-gradient(155deg, var(--brand-600) 0%, var(--brand-500) 55%, var(--accent-500) 130%);
  color: #fff;
  padding: 44px 40px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 32px;
  overflow: hidden;
}
.auth-split-brand::before {
  content: "";
  position: absolute;
  inset: -40% -20% auto auto;
  width: 280px;
  height: 280px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 255, 255, .16), transparent 70%);
}
.auth-split-logo { display: flex; align-items: center; gap: 10px; font-weight: 800; font-size: 1.05rem; position: relative; }
.auth-split-logo .brand-badge { background: rgba(255, 255, 255, .18); }
.auth-split-brand h2 { font-family: var(--font-display); font-size: 1.7rem; line-height: 1.2; margin: 20px 0 10px; font-weight: 700; position: relative; text-wrap: balance; }
.auth-split-brand p { color: rgba(255, 255, 255, .82); font-size: .93rem; line-height: 1.55; margin: 0; position: relative; }
.auth-split-features { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 16px; position: relative; }
.auth-split-features li { display: flex; align-items: flex-start; gap: 12px; font-size: .88rem; color: rgba(255, 255, 255, .94); }
.auth-split-feature-icon {
  flex-shrink: 0; width: 32px; height: 32px; border-radius: 9px;
  background: rgba(255, 255, 255, .16); display: flex; align-items: center; justify-content: center; font-size: .95rem;
}
.auth-split-quote {
  position: relative; font-size: .82rem; color: rgba(255, 255, 255, .78);
  padding-top: 18px; border-top: 1px solid rgba(255, 255, 255, .2);
}
.auth-split-form { padding: 44px 48px; display: flex; flex-direction: column; justify-content: center; }
.auth-split-form h1 { font-family: var(--font-display); font-weight: 700; font-size: 1.9rem; letter-spacing: -.01em; margin: 0 0 8px; }
.auth-split-form .sub { color: var(--ink-500); margin-bottom: 22px; font-size: .95rem; font-weight: 400; }
/* More color/weight presence on the step indicator than Bootstrap's flat
   grey `.text-bg-light` default - ties it to the brand palette instead of
   reading as an unstyled leftover component. */
.auth-split-form .badge.text-bg-light { background: var(--brand-50) !important; color: var(--brand-600) !important; border-color: var(--brand-100, #e0e7ff) !important; font-weight: 700; letter-spacing: .02em; }
.auth-split-form .helper-text { color: var(--ink-500); font-weight: 500; }
.auth-field-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 16px; }
@media (max-width: 991.98px) {
  .auth-split-card { grid-template-columns: 1fr; }
  .auth-split-brand { display: none; }
}
@media (max-width: 575.98px) {
  .auth-split-form { padding: 32px 22px; }
  .auth-field-grid { grid-template-columns: 1fr; }
}

/* .login-hero-* (cardless login layout) removed 2026-08-30 - login now
   shares the same .auth-split-card shell as register/forgot-password
   (see templates/accounts/login.html + partials/auth_brand_panel.html)
   instead of being a one-off inconsistent layout next to them. */

/* Password show/hide toggle - wrapper + button injected purely by
   static/js/main.js around every `input[type=password]` sitewide, no
   template changes needed. `.form-control`'s own padding-right is bumped
   here (not in the widget's own `class` attr) so this is the single place
   that has to change if the field's own right-padding ever does. */
/* `width:100%` on `.pw-field input.form-control` below only ever meant
   "100% of `.pw-field` itself" - and `.pw-field` (a plain <div> JS inserts,
   see main.js) is not automatically full-width when its parent is a flex
   row, which every password field on this site's is: `.field-input-wrap`
   (the leading-icon wrapper around every password input - register, login,
   password reset) is `display:flex`, and a flex child with no flex-grow
   shrinks to fit its own content instead of filling the row (2026-08-13
   fix - the field rendered at its bare intrinsic input width, ~290px,
   instead of stretching to match every other field on the same form).
   `flex: 1 1 auto` makes it grow/shrink like a normal block field would;
   `min-width: 0` stops a flex item from refusing to shrink below its
   content's natural width, which would otherwise overflow the card on
   narrow viewports. */
.pw-field { position: relative; flex: 1 1 auto; min-width: 0; }
.pw-field input.form-control { padding-right: 44px; }
.pw-toggle-btn {
  position: absolute; top: 50%; right: 4px; transform: translateY(-50%);
  width: 34px; height: 34px; display: flex; align-items: center; justify-content: center;
  border: none; background: transparent; color: var(--ink-500); border-radius: var(--radius-sm);
  cursor: pointer; transition: color .15s ease, background .15s ease;
}
.pw-toggle-btn:hover { color: var(--brand-600); background: var(--brand-50); }
.pw-toggle-btn:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 1px; }

.form-error-top { background: #fef2f2; color: var(--danger); border: 1px solid #fecaca; padding: 12px 16px; border-radius: var(--radius-sm); margin-bottom: 18px; font-size: .88rem; }
/* `p.` prefix (not just `.form-alt`) is deliberate: a sitewide `.card p`
   reset (margin:0, elsewhere in this file) has equal class-specificity but
   an extra type selector, so a bare `.form-alt`/`.recaptcha-note` rule was
   silently losing that tie and collapsing to margin:0 inside any `.card`
   (auth-card is one) - matching its specificity here and relying on this
   rule's later position in the file to win the tie instead. */
p.form-alt { text-align: center; margin-top: 20px; font-size: .9rem; color: var(--ink-500); }
.helper-text { font-size: .8rem; color: var(--ink-500); margin-top: 6px; }
p.recaptcha-note { font-size: .75rem; color: var(--ink-500); margin-top: 18px; text-align: center; }

.messages { list-style: none; margin: 0 0 18px; padding: 0; }
.messages li { padding: 12px 16px; border-radius: var(--radius-sm); margin-bottom: 8px; font-size: .9rem; }
.messages .success { background: #f0fdf4; color: var(--success); border: 1px solid #bbf7d0; }
.messages .error { background: #fef2f2; color: var(--danger); border: 1px solid #fecaca; }

/* ---------- Course cards ---------- */
.chip { cursor: pointer; border: 1px solid transparent; }
.chip-active { background: var(--brand-600); color: #fff; }
/* 2026-08-12: bolder catalog-funnel treatment (levels -> classes -> courses ->
   subject groups). Same .course-card/.course-thumb component on all four
   listing pages so the funnel reads as one product, not four redesigns:
   taller photo-backed thumb (real self-hosted photography under the accent
   gradient, applied via .course-thumb-photo + a .course-thumb-overlay that
   reuses the existing .accent-* gradients as a tint layer), a frosted icon
   roundel, and a firmer hover lift than the base .card gives alone. */
/* `position: relative` here is load-bearing, not decorative: it's what
   makes Bootstrap's own `.stretched-link` (applied straight to the real
   CTA `<a>` in .course-footer, not a second invisible link - see that
   template comment) size its ::after against THIS card's box instead of
   the nearest other positioned ancestor up the tree. Whole card becomes
   the click target this way; the visible pill CTA stays exactly as
   designed since nothing about it changes, it just also happens to
   extend an invisible hit-area over its own card. */
.course-card { position: relative; display: flex; flex-direction: column; padding: 0; overflow: hidden; }
.course-card:hover { box-shadow: var(--shadow-lg); border-color: var(--brand-100, #e0e7ff); }
.course-thumb {
  position: relative; height: 150px; display: flex; align-items: center; justify-content: center; color: #fff; overflow: hidden;
}
.course-thumb-photo img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  transition: transform .5s ease;
}
.course-card:hover .course-thumb-photo img { transform: scale(1.08); }
.course-thumb-overlay { position: absolute; inset: 0; opacity: .78; mix-blend-mode: multiply; }
.course-thumb-photo::after {
  content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(15,23,42,0) 40%, rgba(15,23,42,.35) 100%);
}
.course-thumb .icon {
  opacity: .98; position: relative; z-index: 1; width: 54px; height: 54px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,.18);
  border: 1px solid rgba(255,255,255,.4); backdrop-filter: blur(3px);
}
.accent-indigo { background: linear-gradient(135deg, #4f46e5, #818cf8); }
.accent-teal { background: linear-gradient(135deg, #0d9488, #5eead4); }
.accent-violet { background: linear-gradient(135deg, #7c3aed, #c4b5fd); }
.accent-amber { background: linear-gradient(135deg, #d97706, #fcd34d); }
.accent-rose { background: linear-gradient(135deg, #e11d48, #fda4af); }
.accent-sky { background: linear-gradient(135deg, #0284c7, #7dd3fc); }
.course-badge {
  position: absolute; top: 10px; right: 10px; background: rgba(15,23,42,.55); color: #fff;
  font-size: .72rem; font-weight: 700; padding: 3px 10px; border-radius: 999px; z-index: 2;
}
.course-badge.free { background: var(--success); }
/* Three-tier model badges (2026-07-24) - Standard uses the brand color
   (notes free, a lighter-weight upsell than Premium); Premium uses a
   deeper indigo so it visually reads as the "more locked" tier. Both
   reuse existing brand tokens, no new one-off colors. */
.course-badge.tier-standard { background: var(--brand-500); cursor: help; }
/* Premium tier (2026-08-13 restyle): a plain monocolor gray chip - no
   brand color, no upsell styling - with a centered red lock glyph as the
   only content. Fixed equal width/height (not the pill's padding/text
   sizing) so the icon actually reads as centered in a coin-shaped badge,
   not left-aligned text with an icon in front of it. */
.course-badge.tier-premium-lock {
  width: 26px; height: 26px; padding: 0; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--ink-300); cursor: help;
}
.course-badge.tier-premium-lock i { color: var(--danger); font-size: .85rem; line-height: 1; }
.course-tag { display: block; font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--brand-600); margin: 16px 20px 6px; }
.course-card h3 { font-family: var(--font-display); margin: 20px 20px 6px; font-size: 1.15rem; font-weight: 700; }
.course-card p { margin: 0 20px 14px; color: var(--ink-500); font-size: .88rem; }
.course-meta { display: flex; gap: 14px; padding: 0 20px; font-size: .78rem; color: var(--ink-500); flex-wrap: wrap; }
.course-meta span { display: flex; align-items: center; gap: 4px; }
.course-footer { padding: 14px 20px 20px; border-top: 1px solid var(--ink-100); margin-top: 14px; }
.course-rating { color: #f59e0b; font-weight: 700; font-size: .85rem; }
.course-rating small { color: var(--ink-500); font-weight: 500; }

/* ---------- Stats band ---------- */
.stats-band { background: var(--ink-900); border-radius: var(--radius-lg); padding: 44px; }
.stats-band .grid { text-align: center; }
.stats-band b { display: block; font-size: 2rem; color: #fff; }
.stats-band span { color: #94a3b8; font-size: .85rem; }

/* ---------- Student dashboard (2026-08-12) ----------
   Owned classes only: everything below with a `dash-`/`progress-ring`/
   `.jump-back` prefix, plus the `.icon-tint-*` modifiers layered onto the
   pre-existing shared `.card-icon` (that base rule itself is untouched -
   these are additive modifier classes, not overrides). */
.dash-hero-grid { display: grid; grid-template-columns: 1.5fr 1fr; gap: 24px; align-items: stretch; }
.dash-profile-card, .dash-progress-card { display: flex; flex-direction: column; }
.dash-profile-card .field-label { font-size: .78rem; color: var(--ink-500); display: block; margin-bottom: 2px; }
.dash-profile-card .field-value { font-weight: 600; color: var(--ink-900); }
.dash-progress-card { align-items: center; text-align: center; justify-content: center; }
.progress-ring {
  position: relative; width: 132px; height: 132px; border-radius: 50%; margin: 4px auto 14px;
  background: conic-gradient(var(--brand-600) calc(var(--pct, 0) * 1%), var(--ink-100) 0);
  display: flex; align-items: center; justify-content: center;
}
.progress-ring-inner {
  width: 100px; height: 100px; border-radius: 50%; background: var(--surface);
  display: flex; flex-direction: column; align-items: center; justify-content: center; box-shadow: var(--shadow-sm);
}
.progress-ring-inner strong { font-family: var(--font-display); font-size: 1.5rem; color: var(--ink-900); line-height: 1; }
.progress-ring-inner span { font-size: .68rem; color: var(--ink-500); text-transform: uppercase; letter-spacing: .05em; margin-top: 4px; }

/* Subscription expiry ring (dashboard Subscriptions tab, 2026-08-30) -
   same conic-gradient ring component as chapter-progress above, just
   smaller and with an admin/date-driven color instead of always brand:
   green (plenty of time left) -> yellow (renewal window) -> red (about
   to lapse), per days-remaining thresholds computed server-side
   (pages/views.py's _attach_expiry_ring) - color is by absolute days
   left, not percentage, so a 3-day warning reads as urgent regardless of
   whether the plan was a 1-month or 12-month one. */
.progress-ring-sm {
  position: relative; width: 84px; height: 84px; border-radius: 50%; margin: 0 auto 10px;
  background: conic-gradient(var(--ring-color, var(--brand-600)) calc(var(--pct, 0) * 1%), var(--ink-100) 0);
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.progress-ring-sm.is-red { --ring-color: var(--danger, #dc2626); }
.progress-ring-sm.is-yellow { --ring-color: #d97706; }
.progress-ring-sm.is-green { --ring-color: var(--success, #16a34a); }
.progress-ring-sm-inner {
  width: 66px; height: 66px; border-radius: 50%; background: var(--surface);
  display: flex; flex-direction: column; align-items: center; justify-content: center; box-shadow: var(--shadow-sm);
}
.progress-ring-sm-inner strong { font-family: var(--font-display); font-size: 1.05rem; color: var(--ink-900); line-height: 1; }
.progress-ring-sm-inner span { font-size: .58rem; color: var(--ink-500); text-transform: uppercase; letter-spacing: .04em; margin-top: 2px; }
.dash-progress-card p { color: var(--ink-500); font-size: .88rem; margin: 0; }
.jump-back-card {
  display: flex; align-items: center; gap: 16px; background: var(--brand-600);
  color: #fff; border-radius: var(--radius-md); padding: 20px 22px; margin-bottom: 24px;
}
.jump-back-card .jump-back-icon {
  width: 48px; height: 48px; border-radius: 50%; background: rgba(255,255,255,.18); border: 1px solid rgba(255,255,255,.35);
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.jump-back-card .jump-back-kicker { font-size: .72rem; text-transform: uppercase; letter-spacing: .06em; opacity: .85; display: block; margin-bottom: 2px; }
.jump-back-card h3 { color: #fff; margin: 0 0 2px; font-size: 1.08rem; }
.jump-back-card p { color: rgba(255,255,255,.85); margin: 0; font-size: .85rem; }
.jump-back-card .btn { margin-left: auto; flex-shrink: 0; }
/* Restrained per-card accent-coding for the four feature cards (Progress /
   Bookmarks / Question Bank / Security) - reuses the site's existing
   accent-* hues as soft icon tiles plus a thin top rule, not a full
   background repaint. */
.dash-feature-card { border-top: 3px solid transparent; }
.dash-feature-card.tint-indigo { border-top-color: var(--brand-500); }
.dash-feature-card.tint-teal { border-top-color: #0d9488; }
.dash-feature-card.tint-violet { border-top-color: #7c3aed; }
.dash-feature-card.tint-amber { border-top-color: #d97706; }
.icon-tint-indigo { background: var(--brand-50); color: var(--brand-600); }
.icon-tint-teal { background: #ccfbf1; color: #0d9488; }
.icon-tint-violet { background: #ede9fe; color: #7c3aed; }
.icon-tint-amber { background: #fef3c7; color: #d97706; }
@media (max-width: 900px) {
  .dash-hero-grid { grid-template-columns: 1fr; }
}

/* ---------- Dashboard header + illustration polish (2026-08-12) ----------
   Fixes the "same flat blank background" complaint on the logged-in
   dashboard: a soft two-tone radial wash behind the header/profile/progress/
   tile area, plus a small original line-art illustration (open book +
   graduation cap + sparkles, built from plain shapes/paths - no external
   assets) filling the previously-empty space beside the "Welcome back"
   heading. Colors are the site's existing tokens/hues only (brand-50,
   brand-400/500/600, accent-500, the same #ccfbf1 teal tint already used by
   .icon-tint-teal, the same #d97706 amber already used by .accent-amber) -
   no new hues introduced. */
.dash-glow-zone { position: relative; }
.dash-glow-zone::before {
  content: ""; position: absolute; top: -60px; right: -90px; width: 420px; height: 420px;
  background: radial-gradient(circle, var(--brand-50) 0%, rgba(238, 242, 255, 0) 72%);
  border-radius: 50%; z-index: -1; pointer-events: none;
}
.dash-glow-zone::after {
  content: ""; position: absolute; bottom: -50px; left: -70px; width: 300px; height: 300px;
  background: radial-gradient(circle, #ccfbf1 0%, rgba(204, 251, 241, 0) 72%);
  border-radius: 50%; z-index: -1; pointer-events: none; opacity: .65;
}
.dash-header-flex { display: flex; align-items: center; justify-content: space-between; gap: 24px; }
.dash-header-decor { flex-shrink: 0; width: 190px; opacity: .96; }
.dash-header-decor svg { width: 100%; height: auto; display: block; overflow: visible; }
@media (max-width: 900px) {
  .dash-header-decor { display: none; }
}

/* ---------- Shared original-illustration primitives (2026-08-12) ----------
   Reused by the dashboard header art above and by the empty-state
   illustrations further below (.empty-state-illustration svg) - simple flat
   geometric shapes, no gradients/ids, so the same markup can appear more
   than once per page with zero id-collision risk. */
.illo-blob-shape { fill: var(--brand-50); }
.illo-blob-shape.illo-blob-muted { fill: var(--ink-100); }
.illo-book { fill: var(--brand-500); }
.illo-book + .illo-book { fill: var(--brand-400); }
.illo-book-spine { stroke: var(--ink-900); stroke-width: 2; opacity: .22; }
.illo-cap { fill: var(--ink-900); stroke: var(--accent-500); stroke-width: 2; stroke-linejoin: round; }
.illo-cap-string { stroke: var(--accent-500); stroke-width: 2; stroke-linecap: round; }
.illo-cap-tassel { fill: var(--accent-500); }
.illo-sparkle { fill: #d97706; }
.illo-dot { fill: var(--brand-400); }
.illo-dot-sm { fill: var(--accent-500); }
.illo-glass { stroke: var(--brand-500); stroke-width: 6; stroke-linecap: round; fill: none; }
.illo-glass.illo-glass-muted { stroke: var(--ink-300); }
.illo-x { stroke: var(--ink-300); stroke-width: 4; stroke-linecap: round; }
.illo-post { stroke: var(--ink-300); stroke-width: 5; stroke-linecap: round; fill: none; }
.illo-flag-a { fill: var(--brand-500); }
.illo-flag-b { fill: var(--accent-500); }

/* ---------- Steps / How it works ---------- */
.steps { counter-reset: step; }
.step { position: relative; padding-left: 56px; }
.step::before {
  counter-increment: step; content: counter(step);
  position: absolute; left: 0; top: 0; width: 40px; height: 40px; border-radius: 50%;
  background: var(--brand-600); color: #fff;
  display: flex; align-items: center; justify-content: center; font-weight: 800;
}

/* ---------- Testimonials ---------- */
.testimonial-card { display: flex; flex-direction: column; gap: 14px; }
.testimonial-stars { color: #f59e0b; letter-spacing: 2px; }
.testimonial-person { display: flex; align-items: center; gap: 12px; margin-top: auto; }
.avatar-badge {
  width: 42px; height: 42px; border-radius: 50%; background: var(--brand-50); color: var(--brand-600);
  display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: .85rem; flex-shrink: 0;
}
.testimonial-person .name { font-weight: 700; font-size: .9rem; }
.testimonial-person .role { color: var(--ink-500); font-size: .78rem; }

/* ---------- Pricing ---------- */
.price-card { text-align: center; position: relative; }
.price-card.highlight { border: 2px solid var(--brand-500); box-shadow: var(--shadow-lg); }
.price-card .price { font-size: 2.2rem; font-weight: 800; margin: 10px 0; }
.price-card .price small { font-size: .95rem; color: var(--ink-500); font-weight: 500; }
.price-card ul { list-style: none; padding: 0; margin: 18px 0; text-align: left; }
.price-card li { display: flex; gap: 8px; align-items: flex-start; margin-bottom: 10px; font-size: .88rem; color: var(--ink-700); }
.price-card li .icon { color: var(--success); flex-shrink: 0; margin-top: 2px; }
.price-ribbon { position: absolute; top: -12px; left: 50%; transform: translateX(-50%); background: var(--brand-600); color: #fff; font-size: .72rem; font-weight: 700; padding: 4px 14px; border-radius: 999px; }
.price-tagline { font-size: .85rem; color: var(--ink-500); margin: -4px 0 6px; }

/* ---------- Motion ----------
   `.fade-up`/`.delay-*` used to be a CSS @keyframes animation that fired
   unconditionally at page load (main.js/animations.js history, 2026-08
   pass) - harmless above the fold, but pointless (and slightly jarring)
   for every card in a below-the-fold grid, which had already finished
   "revealing" long before a visitor ever scrolled to see it. Real motion
   now lives in static/js/animations.js (GSAP + ScrollTrigger, self-hosted
   per this project's CSP - script-src 'self'): it groups `.fade-up`
   elements by their shared parent (so a row of cards staggers together)
   and only plays once the group scrolls into view, with the whole system
   a no-op when GSAP fails to load or `prefers-reduced-motion` is set. No
   rule needed here any more - every `.fade-up` element is plain, fully
   visible content by default (the JS-hidden state only ever exists
   transiently, set and animated away by GSAP itself), so a visitor with
   JS disabled or blocked simply sees the finished layout with no
   animation at all, never a stuck-invisible element. */
/* .navbar / .navbar-scrolled base rules now live in the Navbar section
   near the top of this file (2026-08 navbar redesign consolidated them). */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .blob { animation: none; }
  .card, .btn, .btn-bookmark-star, .footer-social a, .social-chip { transition: none; }
}

/* content pages */
.prose { max-width: 800px; margin: 0 auto; }
.prose h2 { margin-top: 40px; font-size: 1.3rem; }
.prose h3 { font-size: 1.05rem; }
.prose p, .prose li { color: var(--ink-700); }
.page-hero { padding: 56px 0 20px; text-align: center; }
.page-hero .kicker { display: block; margin-bottom: 8px; }
.page-hero h1 { font-family: var(--font-display); font-weight: 700; font-size: clamp(1.8rem, 3.5vw, 2.6rem); margin: 0 0 12px; text-wrap: balance; }

/* ---------- About page photography (image-treatment) ----------
   Two real-photo moments on the About page: a mission/story photo+text
   split (library.jpg) and a wide banner photo (classroom-asia.jpg) inside
   the "Our Story" section giving general Nepali-education atmosphere -
   deliberately captioned as illustrative context, not as a picture of
   this platform's own (older, Class 11/12) students. */
.about-media-row { display: grid; grid-template-columns: .8fr 1.2fr; gap: 44px; align-items: center; }
.about-media-photo { border-radius: var(--radius-lg); overflow: hidden; box-shadow: var(--shadow-md); aspect-ratio: 4 / 5; }
.about-media-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.about-media-text h2 { margin: 0 0 14px; font-size: clamp(1.4rem, 2.6vw, 1.9rem); letter-spacing: -.01em; }
.about-media-text p { color: var(--ink-700); font-size: 1.02rem; line-height: 1.65; margin: 0 0 14px; }
.about-media-text p:last-child { margin-bottom: 0; }

.about-story-photo { border-radius: var(--radius-lg); overflow: hidden; box-shadow: var(--shadow-md); margin-bottom: 16px; }
.about-story-photo img { width: 100%; height: 320px; object-fit: cover; display: block; }
.about-story-caption { font-size: .8rem; color: var(--ink-500); text-align: center; margin: 0 0 34px; }

@media (max-width: 960px) {
  .about-media-row { grid-template-columns: 1fr; }
  .about-media-photo { aspect-ratio: 16 / 9; max-width: 460px; margin: 0 auto; }
}
@media (max-width: 600px) {
  .about-story-photo img { height: 200px; }
}

/* ---------- FAQ accordion (native <details>, no JS needed) ---------- */
.faq-list { max-width: 780px; margin: 0 auto; display: flex; flex-direction: column; gap: 12px; }
.faq-item {
  background: var(--surface); border: 1px solid var(--ink-100); border-radius: var(--radius-md);
  padding: 6px 22px;
}
.faq-item summary {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 16px 0; font-weight: 700; cursor: pointer; list-style: none; color: var(--ink-900);
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary .icon { transition: transform .2s ease; color: var(--ink-500); }
.faq-item[open] summary .icon { transform: rotate(180deg); }
.faq-item p { color: var(--ink-500); margin: 0 0 18px; font-size: .92rem; }

/* Scrollable/infinite-scroll FAQ list (2026-08-29, static/js/main.js's
   initFaqScroller()) - a fixed viewport height with its own scrollbar
   rather than the page itself growing unbounded as more batches load in.
   `overscroll-behavior:contain` stops a scroll-to-the-end here from also
   scrolling the outer page (common infinite-scroll-in-a-box annoyance). */
.faq-scroll {
  max-height: 640px; overflow-y: auto; overscroll-behavior: contain;
  padding: 4px 4px 4px 0; scroll-behavior: smooth;
}
#faq-scroll-sentinel { height: 1px; }
.faq-item.faq-item-loading { opacity: 0; animation: faq-item-fade-in .3s ease forwards; }
@keyframes faq-item-fade-in { to { opacity: 1; } }

/* Nav mobile breakpoint is now Bootstrap5's own `navbar-expand-lg` (992px),
   handled entirely by bootstrap.min.css + bootstrap.bundle.min.js's
   collapse component - the old custom >1300px hamburger/slide-in
   breakpoint above (and the .nav-links/.mobile-toggle/.dropdown-panel/
   .mega-panel/.nav-scrim rules it drove) is gone along with that markup. */

/* Content-layout breakpoints (grid columns, hero stacking) - independent
   of the nav breakpoint above. */
@media (max-width: 960px) {
  .hero-grid { grid-template-columns: 1fr; }
  .grid-3, .grid-4, .grid-2 { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 600px) {
  .grid-3, .grid-4, .grid-2 { grid-template-columns: 1fr; }
  .cta-band { flex-direction: column; text-align: center; }
}

/* Homepage mobile density (2026-08-29) - `.hero`'s 76/90px padding and
   `.section`'s flat 72px top/bottom were written for desktop and never
   scaled down; stacked across the ~10 sections this page renders, that's
   several screens of pure whitespace on a phone before any real content
   shows. `.hero-logo-wrap`'s 320px/220px min-height (sized so a 220px
   logo has room to breathe next to the hero copy on desktop) is worse on
   mobile: once `.hero-grid` stacks to one column above, it's a mostly-
   empty box under a small centered logo - the single biggest "blank
   content" offender reported. Trimming all of these is presentation-only
   (no markup/context changes) and doesn't touch tablet/desktop at all. */
@media (max-width: 768px) {
  .hero { padding: 40px 0 48px; }
  .hero-actions { margin-bottom: 22px; }
  .section { padding: 44px 0; }
  .section-head { margin: 0 auto 28px; }
  .home-page .section-cozy { padding: 32px 0; }
  .stats-band { padding: 26px 18px; }
  .level-card { padding: 22px; }
  .photo-band { padding: 40px 20px; }
}
@media (max-width: 600px) {
  .hero-logo-wrap { min-height: 0; padding: 12px 0 4px; }
  .hero-logo-wrap img { width: 110px; height: 110px; }
  .blob { width: 200px !important; height: 200px !important; opacity: .35; }
}

/* Hero UX pass (2026-08-29): on a small screen the hero art (photo or
   fallback logo) was still stacking BELOW the text/search - just extra
   scroll distance before the actual search box, not content anyone
   needed. Hidden outright below 768px rather than just shrunk further -
   there's no real use for hero artwork competing with the search bar for
   a phone's limited vertical space. The search box itself goes full-
   width (was capped at max-width:460px, a desktop-only constraint that
   left it looking like an odd narrow strip on mobile instead of a real,
   easy-to-tap full-row search field). */
@media (max-width: 768px) {
  .hero-art { display: none; }
  .hero-search-inline { max-width: none; width: 100%; }
}

/* ---------- Reader (watermarked, offline-cacheable) ---------- */
.reader-shell { position: relative; background: var(--surface); border-radius: var(--radius-lg); border: 1px solid var(--ink-100); overflow: hidden; }
.reader-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 20px 28px; border-bottom: 1px solid var(--ink-100); flex-wrap: wrap; }
/* 2026-08-30: bookmark/mark-complete toolbar buttons swapped from raw
   ★/☆/✓ text characters to real Bootstrap Icons, matching the icon
   vocabulary the rest of the site already uses (.btn-bookmark-star etc.) */
.reader-toolbar .bi-star-fill { color: #f59e0b; }
.reader-content { position: relative; z-index: 1; padding: 8px 28px 36px; color: var(--ink-700); line-height: 1.8; max-width: 760px; }
.no-select { user-select: none; -webkit-user-select: none; }
.reader-watermark {
  position: absolute; inset: 0; z-index: 2; pointer-events: none; overflow: hidden;
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 60px; padding: 40px; opacity: .07;
  transform: rotate(-22deg) scale(1.3);
}
.reader-watermark span { font-size: .78rem; font-weight: 700; white-space: nowrap; color: var(--ink-900); }
@media print {
  .reader-content, .reader-shell { display: none !important; }
  body::after { content: "Printing is disabled for this content."; display: block; text-align: center; padding: 80px; font-size: 1.2rem; }
}

/* ---------- Chapter reader layout: togglable sidebar + full-width content ----------
   Redesigned 2026-07-24 (reader modernization pass) - see PROGRESS.md.
   Desktop (>=992px, Bootstrap's `lg`): a real 2-column grid, sidebar column
   collapses to 0 via .sidebar-collapsed so the content area can go
   genuinely full-width, not just "narrow column next to a sidebar".
   Mobile (<992px): the sidebar becomes an off-canvas panel (fixed,
   translated off-screen by default) toggled via #readerSidebarToggle - see
   chapter_read.html's extra_js block. Reuses the same --radius-*/--shadow-*
   tokens as the rest of the site, no new one-off values. */
/* Full-width per explicit user request ("use container-fluid... utilize
   full width") - chapter_read.html's outer wrapper is Bootstrap's
   `.container-fluid` (100% width, no max-width cap); this rule only
   supplies the same 24px side gutter the sitewide `.container` class uses
   (see main.css's own `.container` rule) so content still has breathing
   room from the viewport edge - it does NOT cap the width. */
.reader-page-section .container-fluid { padding: 0 24px; }
.reader-top-bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 20px; }
.reader-breadcrumb { font-size: .85rem; color: var(--ink-500); }
.reader-sidebar-toggle {
  display: inline-flex; align-items: center; gap: 8px; min-height: 44px; padding: 8px 16px;
}
.reader-sidebar-backdrop { display: none; }

.reader-layout { display: grid; grid-template-columns: 300px 1fr; gap: 28px; align-items: flex-start; position: relative; transition: grid-template-columns .2s ease; }
.reader-layout.sidebar-collapsed { grid-template-columns: 0 1fr; }
.reader-layout.sidebar-collapsed .reader-sidebar { opacity: 0; visibility: hidden; }

@media (min-width: 992px) {
  .reader-sidebar { position: sticky; top: 16px; max-height: calc(100vh - 32px); overflow-y: auto; transition: opacity .15s ease; }
}

/* Off-canvas pattern below the `lg` breakpoint - matches Bootstrap's own
   navbar-expand-lg collapse point used sitewide, so "mobile" means the
   same thing here as everywhere else in the nav. */
@media (max-width: 991.98px) {
  .reader-layout, .reader-layout.sidebar-collapsed { display: block; }
  .reader-sidebar {
    position: fixed; top: 0; left: 0; bottom: 0; width: min(320px, 86vw); z-index: 1050;
    background: var(--surface); box-shadow: var(--shadow-lg); border-radius: 0; border: none;
    padding: 20px; overflow-y: auto; transform: translateX(-100%); transition: transform .25s ease;
    max-height: none;
  }
  .reader-layout.sidebar-open .reader-sidebar { transform: translateX(0); opacity: 1; visibility: visible; }
  .reader-sidebar-backdrop {
    position: fixed; inset: 0; z-index: 1040; background: rgba(15, 23, 42, .45);
  }
  .reader-layout.sidebar-open .reader-sidebar-backdrop { display: block; }
  .reader-page-section .container-fluid { padding: 0 16px; }
  .reader-header-bar { padding: 20px 18px; }
  .reader-toolbar { padding: 16px 18px; }
  .reader-content { padding: 8px 18px 28px; max-width: 100%; }
  .reader-pdf-section { padding: 0 18px 18px; }
  .reader-below-tabs { padding: 4px 18px 18px; }
  .reader-below-tabs .reader-content { padding-left: 0; padding-right: 0; }
}
@media (max-width: 480px) {
  .pdf-thumb-panel { width: 88px; flex: 0 0 88px; }
}
.reader-sidebar h4 { font-size: .82rem; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-500); margin: 18px 0 6px; }
.reader-sidebar h4:first-child { margin-top: 0; }
.reader-sidebar ul { list-style: none; margin: 0 0 4px; padding: 0; }
.reader-sidebar li { margin: 2px 0; }
.reader-sidebar a { display: block; padding: 6px 10px; border-radius: 8px; font-size: .9rem; color: var(--ink-700); text-decoration: none; }
.reader-sidebar a:hover { background: var(--ink-100); }
.reader-sidebar a.active { background: var(--brand-100, #eef2ff); color: var(--brand-700, #4338ca); font-weight: 600; }
.reader-tabs { display: flex; gap: 8px; margin-bottom: 14px; border-bottom: 1px solid var(--ink-100); }
.reader-tabs a { padding: 8px 14px; font-size: .88rem; text-decoration: none; color: var(--ink-500); border-bottom: 2px solid transparent; }
.reader-tabs a.active { color: var(--ink-900); border-bottom-color: var(--ink-900); font-weight: 600; }
.reader-pdf-section { padding: 0 28px 28px; }

/* ---- Below-PDF tabbed section (Overview / Past Questions / Important
   Questions) - 2026-08-12 reader restructure. Reuses the exact
   .nav-tabs/.tab-content Bootstrap5 component + .subject-nav-tabs visual
   treatment already established on subject_detail.html, just re-homed
   here so the PDF above reads as the clear primary focus and this reads
   as a secondary, still full-width companion section underneath it. */
.reader-below-tabs { padding: 4px 28px 28px; }
.reader-below-tabs .nav-tabs { margin-bottom: 18px; }
.reader-below-tabs .tab-pane { padding-top: 4px; }
.reader-overview-meta { color: var(--ink-500); font-size: .85rem; margin-bottom: 10px; }
/* .reader-content already carries its own 28px horizontal padding (shared
   rule above, used elsewhere too) - avoid doubling it up now that it also
   sits inside .reader-below-tabs's own 28px padding. */
.reader-below-tabs .reader-content { padding-left: 0; padding-right: 0; padding-top: 0; }
.pdf-viewer-wrap { position: relative; border: 1px solid var(--ink-100); border-radius: var(--radius-lg); overflow: hidden; background: var(--ink-100); max-width: 100%; }
.pdf-viewer-wrap iframe { width: 100%; height: 78vh; min-height: 480px; border: 0; display: block; }
.pdf-viewer-wrap.no-select { -webkit-touch-callout: none; }

/* pdf.js canvas viewer (replaces the old <iframe src="blob:...">) */
.pdf-viewer-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 10px 14px; background: var(--surface); border-bottom: 1px solid var(--ink-100); }
.pdf-viewer-toolbar .pdf-page-info { font-size: .85rem; color: var(--ink-700); display: flex; align-items: center; gap: 6px; }
.pdf-viewer-toolbar #pdf-page-num { width: 56px; text-align: center; border: 1px solid var(--ink-100); border-radius: 6px; padding: 4px 6px; }
.pdf-viewer-toolbar #pdf-zoom-level { font-size: .85rem; color: var(--ink-500); min-width: 44px; text-align: center; }
.pdf-toolbar-spacer { flex: 1; }
.pdf-canvas-container { max-height: 78vh; overflow: auto; text-align: center; padding: 16px; background: var(--ink-100); }
.pdf-canvas-container canvas { max-width: 100%; box-shadow: 0 2px 12px rgba(0,0,0,.12); background: #fff; }

/* pdf.js reader v2: thumbnails / search / fullscreen (2026-07-24 follow-up) */
.pdf-viewer-body { display: flex; align-items: stretch; }
.pdf-thumb-panel { width: 132px; flex: 0 0 132px; max-height: 78vh; overflow-y: auto; padding: 10px 8px; background: var(--surface); border-right: 1px solid var(--ink-100); display: flex; flex-direction: column; gap: 8px; }
.pdf-thumb-panel.collapsed { display: none; }
.pdf-thumb-item { border: 2px solid transparent; border-radius: 6px; padding: 4px; cursor: pointer; background: none; text-align: center; }
.pdf-thumb-item canvas { max-width: 100%; display: block; box-shadow: 0 1px 4px rgba(0,0,0,.15); background: #fff; }
.pdf-thumb-item .pdf-thumb-num { font-size: .72rem; color: var(--ink-500); margin-top: 4px; }
.pdf-thumb-item.active { border-color: var(--brand-600, #4f46e5); }
.pdf-thumb-item.active .pdf-thumb-num { color: var(--brand-700, #4338ca); font-weight: 600; }
.pdf-canvas-scroll { flex: 1 1 auto; min-width: 0; }
.pdf-search-bar { display: flex; align-items: center; gap: 8px; padding: 8px 14px; background: var(--surface); border-bottom: 1px solid var(--ink-100); flex-wrap: wrap; }
.pdf-search-bar input[type="search"] { flex: 1 1 160px; min-width: 120px; border: 1px solid var(--ink-100); border-radius: 6px; padding: 5px 10px; font-size: .88rem; }
.pdf-search-bar .pdf-search-count { font-size: .8rem; color: var(--ink-500); min-width: 54px; text-align: center; }
mark.pdf-search-hit { background: #ffe58a; color: inherit; border-radius: 2px; }
mark.pdf-search-hit.current { background: #ff9f43; }
.pdf-viewer-wrap:fullscreen, .pdf-viewer-wrap:-webkit-full-screen { background: var(--ink-100); }
.pdf-viewer-wrap:fullscreen .pdf-canvas-container, .pdf-viewer-wrap:-webkit-full-screen .pdf-canvas-container { max-height: calc(100vh - 96px); }
.pdf-viewer-status-inner { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.pdf-viewer-status-inner .spinner-border { width: 2rem; height: 2rem; }
.pdf-viewer-retry-btn { margin-top: 4px; }
.pdf-page-stage { position: relative; display: inline-block; }
/* Minimal port of pdf.js's own text_layer_builder.css, renamed to avoid any
   collision with unrelated `.textLayer` selectors elsewhere - only used to
   position an invisible, real text layer over the canvas so in-document
   search can highlight the actual matched words in the actual place they
   render, using pdf.js's own TextLayer API (setLayerDimensions sets the
   required --scale-factor/dimensions on this container automatically). */
.pdf-search-textlayer { position: absolute; text-align: initial; inset: 0; overflow: clip; opacity: 1; line-height: 1; text-size-adjust: none; forced-color-adjust: none; transform-origin: 0 0; z-index: 2; }
.pdf-search-textlayer span, .pdf-search-textlayer br { color: transparent; position: absolute; white-space: pre; cursor: text; transform-origin: 0% 0%; }
.pdf-search-textlayer span.markedContent { top: 0; height: 0; }
.pdf-search-textlayer .pdf-search-hit { background: rgba(255,193,7,.55); border-radius: 2px; }
.pdf-search-textlayer .pdf-search-hit.current { background: rgba(255,111,0,.8); }

/* ---------- Rendered-text copy-protection (syllabus, question/answer HTML,
   notice bodies) - deterrence only, same honest limitation as the PDF
   watermark: this cannot stop someone from viewing page source, using
   browser devtools, or taking a screenshot. It raises friction for casual
   copy-paste/scraping and (via the watermark overlay) leaves a
   traceability mark if a screenshot does leak. Scoped ONLY to
   `.protected-content` containers - deliberately NOT applied to `body`,
   nav, forms, or marketing pages, so normal text selection still works
   everywhere else on the site. See static/js/content-protect.js for the
   matching contextmenu blocker + watermark injection. */
.protected-content {
  position: relative;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  /* Reading-measure cap (Task: typography pass) - this is the shared class
     across syllabus (subject_detail.html), notice bodies, chapter notes and
     question-bank Q&A, i.e. exactly the "long-form text" surfaces named in
     the typography brief. notice_detail.html/chapter_read.html already cap
     their own outer container/`.reader-content` independently (this rule is
     a no-op there, not fighting an existing cap); subject_detail.html's
     syllabus tab and question_bank_detail.html's Q&A cards had NO cap at
     all before this and sat at the full `.container` width (1180px) on
     large screens - a genuinely too-wide, hard-to-track-line-to-line
     measure for paragraph text. 720px keeps roughly 70-90 characters/line
     at this project's body font-size, the conventional readable range. */
  max-width: 720px;
}
/* Syllabus tab (subject_detail.html) explicitly opts OUT of the reading-
   measure cap above per direct user request ("show the content will full
   width properly") - syllabus content reads more like a structured
   outline (headings/topic lists) than continuous prose paragraphs, so the
   width tradeoff that helps chapter notes/Q&A doesn't apply the same way
   here. Scoped to this one tab only via the pane's own id, not a change
   to `.protected-content` itself - chapter notes/notice bodies/Q&A keep
   their existing 720px cap untouched. */
#tab-syllabus .protected-content { max-width: none; }
.protected-content-watermark {
  position: absolute; inset: 0; z-index: 2; pointer-events: none; overflow: hidden;
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 48px; padding: 24px; opacity: .06;
  transform: rotate(-22deg) scale(1.25);
}
.protected-content-watermark span { font-size: .74rem; font-weight: 700; white-space: nowrap; color: var(--ink-900); }
@media print {
  /* Consistent with the chapter-reader precedent above: content stays
     un-downloadable rather than allowing a clean, watermark-free printout
     (the PDF flow makes the same call - inline delivery, never a plain
     attachment save). */
  .protected-content { display: none !important; }
  .protected-content-print-notice { display: block !important; text-align: center; padding: 40px; font-size: 1.05rem; color: #000; }
}
.protected-content-print-notice { display: none; }

/* ---------- Notice popup modal ----------
   Removed 2026-07-24 (see PROGRESS.md "Dropdown/modal CSS audit"): this used
   to be a full hand-rolled `.modal-backdrop-custom`/`.modal-dialog`/
   `.modal-content`/`.modal-header`/`.modal-body`/`.btn-close` rule set from
   before Bootstrap5 was actually vendored (the block's own comment said so
   literally). `templates/base.html`'s popup notice is a genuine Bootstrap5
   `.modal` now (Phase 6), so these rules were dead weight EXCEPT that they
   reused the exact same class names Bootstrap's own `.modal-dialog`/
   `.modal-content`/`.modal-header`/`.modal-body`/`.btn-close` use - and
   because this file (main.css) loads AFTER bootstrap.min.css, these custom
   rules silently overrode real Bootstrap's own modal component styling
   (a different `.modal-dialog` max-width, a custom `.modal-header` flex
   layout, a recolored `.btn-close`) for every Bootstrap modal on the site,
   not just the notice popup - exactly the "old CSS fighting the new
   component" failure mode the nav-dropdown bug report described, just
   found on the modal rather than the dropdown itself. Removed entirely;
   Bootstrap's own bootstrap.min.css now styles all of these unopposed. */

/* ---------- Notices list/detail ---------- */
.notice-type-badge { display: inline-block; padding: 3px 10px; border-radius: 999px; font-size: .75rem; font-weight: 700; }
.notice-type-exam { background: #fef3c7; color: #92400e; }
.notice-type-system { background: #e0e7ff; color: #3730a3; }
.notice-type-popup { background: #fce7f3; color: #9d174d; }

/* ---------- Ad slots (core/templatetags/ads.py's show_ad tag) ---------- */
/* Ad slot styling (2026-07-24): deliberately NOT a dedicated `.ad-slot`
   class - the rendered ad now reuses `.card` (see core/templatetags/ads.py)
   plus inline styles, so there is no ad/banner/sponsor-named selector in
   this stylesheet for a cosmetic filter list to hide by class. */

/* Task (2026-07-24 follow-up): the rendered card had an inline
   `padding:12px` overriding `.card`'s own 28px padding (inline style wins
   over a class rule), and no width cap on its wrapping container - looked
   like a bare, unstyled image sitting below the page rather than an
   intentional block. Fixed in templates/base.html (width cap, see
   `col-lg-8` wrapper) and here: a small, generically-named helper class
   for the "Promoted" disclosure line so it reads as a subdued caption
   (matching the site's own `.card p` muted-text language) rather than
   plain unstyled text. Name is deliberately generic - no "ad"/"promo"/
   "sponsor" substring - so it can't become a new filter-list target.
   `.card` itself already supplies the border/radius/shadow every other
   card on the site gets - no ad-specific override needed. */
.fine-print-label { color: var(--ink-500); font-size: .72rem; letter-spacing: .04em; text-transform: uppercase; text-align: center; margin: 10px 0 0; }

/* Side-rail ("side banner"/skyscraper) and native in-feed ad placements
   (core/templatetags/ads.py's _side_rail_markup/_infeed_markup) are
   deliberately NOT styled from here. Both reuse `.card`/`.fine-print-label`
   above for their actual visual chrome (shared with ordinary site
   content - expensive collateral damage for anyone trying to block them),
   and the side rail's positioning/visibility is emitted as a `<style>`
   block scoped to that one creative's own id at render time, not a class
   defined here - see _side_rail_markup's docstring for why: a single
   shared class used by every impression sitewide is one "block this
   element" click away from killing the whole placement forever. */

/* =====================================================================
   Card / tab / alert redesign (2026-07-24) - see PROGRESS.md and
   images/"semester list.png", "subject-detail.png", "lecture-detail.png".
   Scoped to: .subject-card / .chapter-card (partials/subject_card.html,
   partials/chapter_card.html), the subject-detail hero band + nav-tabs,
   the chapter-reader header bar + sidebar list-group, and .alert.
   ===================================================================== */

/* ---- Icon badge (circular, brand-colored) used on subject/chapter cards ---- */
.icon-badge {
  width: 44px; height: 44px; border-radius: 50%; display: inline-flex;
  align-items: center; justify-content: center; font-size: 1.1rem; flex-shrink: 0;
}
.icon-badge-primary { background: var(--brand-600); color: #fff; box-shadow: 0 4px 10px rgba(79,70,229,.28); }

/* ---- Subject/chapter card bodies ---- */
.subject-card, .chapter-card { padding: 0; overflow: hidden; }
.subject-card .card-body, .chapter-card .card-body { padding: 22px 22px 16px; }
.subject-card-title, .chapter-card-title { font-size: 1.05rem; line-height: 1.35; margin-right: 10px; }
.subject-card-desc, .chapter-card-preview {
  color: var(--ink-500); font-size: .88rem; line-height: 1.55; margin: 6px 0 14px;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.card-meta-row { display: flex; gap: 14px; flex-wrap: wrap; font-size: .78rem; color: var(--ink-500); align-items: center; }
.card-meta-row span { display: inline-flex; align-items: center; gap: 5px; }
.card-meta-row i.bi { color: var(--brand-500); font-size: .9rem; }
.card-divider { height: 3px; background: linear-gradient(90deg, var(--brand-600), var(--brand-400)); margin: 16px 0 0; }
.card-footer-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 12px 22px; background: var(--brand-50); }
.btn-bookmark-star {
  border: none; background: none; padding: 4px; font-size: 1.15rem; line-height: 1; color: var(--brand-600);
  cursor: pointer; border-radius: 8px; transition: transform .15s ease;
}
.btn-bookmark-star:hover { transform: scale(1.12); }
.btn-bookmark-star.bookmarked, .btn-bookmark-star i.bi-star-fill { color: #f59e0b; }

/* ---- Subject-detail hero band + illustration ---- */
.subject-hero-band { background: var(--ink-50, #f1f5f9); padding: 48px 0 32px; border-radius: 0 0 var(--radius-lg) var(--radius-lg); }
/* 2026-08-30: illustration itself now reuses .dash-header-decor's own
   width/responsive rules (static/css/main.css, Dashboard section) plus the
   shared .illo-* line-art primitives - this class only widens the fixed
   190px dashboard-header sizing slightly for the hero band's own column,
   where there's a bit more room to give the illustration presence. */
.subject-hero-illustration.dash-header-decor { width: 210px; }

/* ---- Subject nav-tabs (Chapters / Syllabus / Question Banks) ---- */
.subject-nav-tabs .nav-link {
  color: var(--ink-500); font-weight: 600; border: none; border-bottom: 3px solid transparent;
  border-radius: 0; padding: 10px 18px;
}
.subject-nav-tabs .nav-link:hover { color: var(--brand-600); border-color: var(--brand-100, #e0e7ff); }
.subject-nav-tabs .nav-link.active { color: var(--brand-600); background: none; border-color: var(--brand-600); }

/* ---- Chapter reader header bar (templates/catalog/chapter_read.html) ----
   Purely additive - sits ABOVE the existing #pdf-viewer-wrap/.reader-toolbar
   region, no id/class shared with the pdf.js viewer or its JS. */
.reader-header-bar {
  background: linear-gradient(120deg, var(--brand-600), var(--brand-500));
  color: #fff; border-radius: var(--radius-lg) var(--radius-lg) 0 0; padding: 26px 28px;
}
.reader-header-bar h1 { color: #fff; margin: 0 0 4px; }
.reader-header-bar .reader-header-meta { color: rgba(255,255,255,.85); font-size: .88rem; }
.reader-header-bar .btn-important-questions {
  background: rgba(255,255,255,.16); border: 1px solid rgba(255,255,255,.5); color: #fff;
}
.reader-header-bar .btn-important-questions:hover { background: #fff; color: var(--brand-600); }

/* ---- Chapter-reader sidebar as a real Bootstrap list-group ---- */
.reader-sidebar .list-group-item { border-radius: 8px; margin-bottom: 2px; border: none; font-size: .9rem; }
.reader-sidebar .list-group-item.active { background: var(--brand-50); color: var(--brand-700, #4338ca); font-weight: 600; }
.reader-sidebar .list-group-item:hover:not(.active) { background: var(--ink-100); }
.reader-sidebar h4 small { font-weight: 400; text-transform: none; letter-spacing: 0; }

/* ---- Important Questions: distinct, card-style treatment (2026-07-24
   reader-modernization pass) - a soft variant of the brand icon-badge, plus
   a brand-colored top accent so this card reads as a clearly separate
   section from Past Questions above it, not a visual continuation of it. */
.icon-badge-soft { background: var(--brand-50); color: var(--brand-600); }
.important-questions-card { border-top: 4px solid var(--brand-600); }
.important-questions-card h3 { color: var(--brand-600); }
.iq-item { display: flex; gap: 14px; padding: 16px 0; border-bottom: 1px solid var(--ink-100); }
.iq-item:last-of-type { border-bottom: none; }
.iq-item .icon-badge { width: 38px; height: 38px; font-size: 1rem; font-weight: 700; margin-top: 2px; }

/* ---- Mobile touch-target pass for the reader's own controls (PDF
   toolbar, reader-actions bar) - independent of any sitewide button size,
   scoped to this page only per the mobile-responsiveness audit. ---- */
@media (max-width: 599.98px) {
  .pdf-viewer-toolbar .btn, .reader-toolbar .btn { min-height: 42px; }
  .pdf-viewer-toolbar #pdf-page-num { width: 48px; }
  .pdf-viewer-toolbar, .pdf-search-bar { gap: 8px; }
}

/* ---- Styled alerts (Django messages, base.html) ----
   Layered on top of Bootstrap's own .alert - left accent bar, icon slot,
   generous padding/radius, subtle shadow. Uses Bootstrap's own semantic
   colors (no off-palette colors), just more visual character. */
.alert {
  border: none; border-left: 5px solid currentColor; border-radius: var(--radius-md, 12px);
  padding: 16px 20px; box-shadow: var(--shadow-sm); display: flex; align-items: flex-start; gap: 12px;
}
.alert .alert-icon { font-size: 1.25rem; line-height: 1; margin-top: 1px; flex-shrink: 0; }
.alert .alert-message { flex: 1 1 auto; }
.alert-success { border-left-color: var(--bs-success, #16a34a); }
.alert-danger { border-left-color: var(--bs-danger, #dc2626); }
.alert-warning { border-left-color: var(--bs-warning, #d97706); }
.alert-info { border-left-color: var(--bs-info, #0284c7); }

/* ---------- Question-bank "physical paper" layout (2026-07-24) ----------
   Question papers/model sets styled to actually look like a printed exam
   paper (letterhead, Full-Marks/Time info bar, serif body, numbered
   questions with marks in the margin like a real paper) rather than a
   generic card list. Deliberately branded with this platform's own site
   name in the letterhead, not a fabricated exam-board letterhead, so it
   reads as "our compiled reproduction of this paper", not an attempt to
   impersonate the actual issuing body. Answers stay in the app's normal
   sans-serif UI font (via .exam-answer below) so the unlocked "answer key"
   never gets mistaken for part of the original paper's own text. */
/* Widened from the original 900px so the exam-paper column can keep a
   comfortable reading width (via .exam-page-main) while still leaving room
   for the sibling-papers sidebar (.exam-page-sidebar) alongside it - see
   the "put a sidebar and show other questions name too" pass below. */
.exam-page-container { max-width: 1220px; }
.exam-breadcrumb { margin-bottom: 20px; font-size: .85rem; color: var(--ink-500); }
.exam-paper {
  background: #fff;
  border: 1px solid var(--ink-100);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  padding: 40px clamp(20px, 5vw, 56px) 32px;
  /* Keeps the physical-paper reading width consistent whether or not the
     sibling-papers sidebar is present alongside it (see .exam-page-layout
     below) - was implicitly capped by the narrower .exam-page-container
     before that sidebar existed. */
  max-width: 900px;
}
.exam-paper-letterhead { text-align: center; border-bottom: 3px double var(--ink-900); padding-bottom: 16px; margin-bottom: 20px; }
.exam-paper-site { font-family: Georgia, 'Times New Roman', serif; font-weight: 700; font-size: 1.05rem; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-900); }
.exam-paper-type { font-size: .82rem; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-500); margin-top: 4px; }
.exam-paper-title { font-family: Georgia, 'Times New Roman', serif; font-size: 1.5rem; font-weight: 700; color: var(--ink-900); margin: 10px 0 4px; }
.exam-paper-subject-line { font-family: Georgia, 'Times New Roman', serif; font-size: 1rem; color: var(--ink-700); }
.exam-paper-code, .exam-paper-class { color: var(--ink-500); font-size: .92rem; }
.exam-paper-infobar {
  font-family: Georgia, 'Times New Roman', serif;
  text-align: center;
  font-size: .95rem;
  color: var(--ink-700);
  border-top: 1px solid var(--ink-100);
  border-bottom: 1px solid var(--ink-100);
  padding: 10px 0;
  margin-bottom: 18px;
}
.exam-paper-infobar p { margin: 0; }
.exam-paper-instruction {
  font-family: Georgia, 'Times New Roman', serif;
  font-style: italic;
  text-align: center;
  color: var(--ink-700);
  font-size: .92rem;
  margin-bottom: 28px;
}
.exam-paper-group { margin-bottom: 28px; }
.exam-paper-group-header { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; flex-wrap: wrap; border-bottom: 1px solid var(--ink-900); padding-bottom: 6px; margin-bottom: 16px; }
.exam-paper-group-title { font-family: Georgia, 'Times New Roman', serif; font-size: 1.15rem; font-weight: 700; margin: 0; }
.exam-paper-group-marks { font-family: Georgia, 'Times New Roman', serif; font-size: .9rem; color: var(--ink-500); font-style: italic; }
.exam-question { padding: 14px 0; border-bottom: 1px dashed var(--ink-100); }
.exam-question:last-child { border-bottom: none; }
.exam-question-row { display: flex; align-items: baseline; gap: 10px; font-family: Georgia, 'Times New Roman', serif; font-size: 1.02rem; color: var(--ink-900); line-height: 1.6; }
.exam-question-num { font-weight: 700; flex-shrink: 0; min-width: 1.6em; }
.exam-question-body { flex: 1 1 auto; }
.exam-question-body p { margin: 0; }
.exam-question-marks { flex-shrink: 0; font-weight: 700; white-space: nowrap; }
.exam-answer { margin: 10px 0 0 2.4em; font-family: var(--bs-body-font-family, inherit); font-size: .92rem; }
.exam-answer summary { cursor: pointer; color: var(--brand-600); font-weight: 600; }
.exam-answer > div { margin-top: 8px; padding: 12px 14px; background: var(--ink-100); border-radius: var(--radius-sm); }
.exam-bookmark-btn { margin: 8px 0 0 2.4em; }
@media (max-width: 599.98px) {
  .exam-paper { padding: 28px 16px 24px; }
  .exam-question-row { flex-wrap: wrap; }
  .exam-answer, .exam-bookmark-btn { margin-left: 0; }
}

/* ---- Sibling question-bank quick-nav (2026-08-12, "no quick way to go to
   other past questions" UX pass, later widened into a real sidebar per
   "put a sidebar and show other questions name too") - a compact dropdown
   right next to the breadcrumb, plus a persistent list-group sidebar
   alongside the paper (see .exam-page-layout/.exam-page-sidebar further
   below), both listing other published QuestionBanks for the same subject
   by their real title. Reuses the site's existing
   .btn-outline/.dropdown/.list-group conventions - no new button variant
   introduced. ---- */
.exam-quicknav-bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 20px; }
.exam-quicknav-dropdown .dropdown-menu { max-height: 360px; overflow-y: auto; min-width: 260px; }
.exam-quicknav-dropdown .dropdown-item.active { background: var(--brand-50); color: var(--brand-700, #4338ca); }
.exam-quicknav-meta { display: block; font-size: .76rem; color: var(--ink-500); }
.exam-quicknav-dropdown .dropdown-item.active .exam-quicknav-meta { color: var(--brand-600); }

/* ---- Sibling-papers sidebar (2026-08-12 follow-up, "put a sidebar and
   show other questions name too") - real two-column layout, exam paper +
   a persistent sidebar of sibling papers by their actual title, same
   `.list-group`/`.list-group-item-action`/`.active` visual language as
   chapter_read.html's `.reader-sidebar` (reused, not reinvented, for
   consistency between these two sibling reader-style pages). The compact
   dropdown above stays too, for a quicker jump without leaving the top of
   the page - the sidebar is the persistent, always-visible version. ---- */
.exam-page-layout { display: grid; grid-template-columns: 1fr 280px; gap: 28px; align-items: start; }
.exam-page-layout.exam-page-layout-solo { grid-template-columns: 1fr; }
.exam-page-main { min-width: 0; }
.exam-page-sidebar { padding: 20px; }
@media (min-width: 768px) {
  .exam-page-sidebar { position: sticky; top: 16px; }
}
.exam-page-sidebar h4 { font-size: .95rem; margin: 0 0 2px; }
.exam-page-sidebar .list-group-item { border-radius: 8px; margin-bottom: 2px; border: none; display: flex; flex-direction: column; gap: 2px; }
.exam-page-sidebar .list-group-item.active { background: var(--brand-50); color: var(--brand-700, #4338ca); }
.exam-page-sidebar .list-group-item.active .exam-quicknav-meta { color: var(--brand-600); }
.exam-page-sidebar .list-group-item:hover:not(.active) { background: var(--ink-100); }
.exam-quicknav-panel-title { font-weight: 600; font-size: .92rem; }
@media (max-width: 767.98px) {
  .exam-page-layout { grid-template-columns: 1fr; }
}
@media (max-width: 599.98px) {
  .exam-quicknav-bar { flex-direction: column; align-items: stretch; }
  .exam-quicknav-dropdown .dropdown-toggle { width: 100%; }
}

/* ---------- Cart line items (finance/cart.html) ---------- */
.cart-coupon-form { max-width: 220px; }
.cart-coupon-note { font-size: .78rem; margin-top: 5px; display: flex; align-items: center; gap: 5px; min-height: 1.1em; transition: color .15s ease; }
.cart-coupon-error { color: var(--danger); }
/* Success reads as "money saved" (green), not another brand-purple accent
   competing with the primary CTA - 2026-08-13 real-time-coupon pass. */
.cart-coupon-applied { color: var(--success); font-weight: 600; }
.btn-icon-ghost {
  width: 34px; height: 34px; padding: 0; border-radius: var(--radius-sm);
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid transparent; background: transparent; color: var(--ink-500);
  transition: background .15s ease, color .15s ease;
}
.btn-icon-ghost:hover { background: #fee2e2; color: var(--danger); }
.cart-empty-icon { font-size: 2.4rem; color: var(--ink-300); display: block; margin-bottom: 10px; }

/* Header cells, subject name, and the price column - a real price should
   read as the most prominent number in the row, not compete with body
   text, so it gets the display serif and heavier weight while the plan/
   duration text next to it stays plain body weight. */
.cart-table thead th {
  font-size: .74rem; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-500);
  font-weight: 700; border-bottom: 2px solid var(--ink-100); padding-bottom: 10px;
}
.cart-table td { vertical-align: middle; }
.cart-line-subject { font-weight: 600; color: var(--ink-900); }
.cart-line-amount {
  font-family: var(--font-display); font-size: 1.1rem; font-weight: 700; color: var(--ink-900);
  white-space: nowrap; transition: color .2s ease;
}
/* A discounted line's amount reads as a real "you saved money" price, same
   green as the applied-coupon note above, instead of the neutral ink
   every price otherwise gets. */
.cart-line-amount.is-discounted { color: var(--success); }
.cart-total-row td { font-family: var(--font-display); font-size: 1.3rem; }
.cart-total-amount { color: var(--brand-600); transition: color .2s ease; }
.cart-actions-bar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; margin-top: 24px; }

/* Real-time recalculation feedback (2026-08-13, AJAX coupon apply -
   finance/views.py's ApplyCouponView JSON branch + static/js/finance-
   flow.js) - a brief brand-tinted background flash on whichever amount
   cell(s) just changed, so a total that updates without a page reload
   still reads as "something happened here", not a silent DOM swap a
   student might not even notice. Respects reduced motion by just skipping
   the flash (the color/text change itself still communicates the update). */
@keyframes cart-amount-flash {
  from { background-color: var(--brand-50); }
  to { background-color: transparent; }
}
.cart-amount-flash { animation: cart-amount-flash 900ms ease-out; }
@media (prefers-reduced-motion: reduce) {
  .cart-amount-flash { animation: none; }
}

/* Seasonal-offer teaser banner - surfaces the currently-running featured
   Coupon (Coupon.is_featured + its own validity window, set by an admin
   for e.g. a Dashain/exam-season promo) so a student doesn't have to
   already know a code exists to benefit from it. Monocolor-on-brand
   rather than a loud gradient - this is a helpful nudge, not a marketing
   takeover of the checkout flow. */
.seasonal-offer-banner {
  display: flex; align-items: center; gap: 14px; background: var(--brand-600); color: #fff;
  border-radius: var(--radius-md); padding: 14px 18px; margin-bottom: 20px; box-shadow: var(--shadow-sm);
}
.seasonal-offer-icon { font-size: 1.3rem; flex-shrink: 0; opacity: .9; }
.seasonal-offer-body { flex: 1 1 auto; font-size: .92rem; }
.seasonal-offer-code {
  font-family: var(--font-display); font-weight: 700; background: rgba(255, 255, 255, .18);
  padding: 1px 8px; border-radius: 6px; letter-spacing: .02em;
}
.seasonal-offer-apply-all { flex-shrink: 0; font-weight: 700; color: var(--brand-600); }
@media (max-width: 575.98px) {
  .seasonal-offer-banner { flex-wrap: wrap; }
  .seasonal-offer-apply-all { width: 100%; }
}

/* ---------- Checkout order summary + vendor QR cards (finance/checkout.html) ---------- */
.order-summary-card { border: 1px solid var(--ink-100); }
.order-summary-total { font-family: var(--font-display); font-size: 1.35rem; font-weight: 700; color: var(--brand-600); }
.vendor-qr-name { font-family: var(--font-display); }
.order-summary-note {
  display: flex; align-items: center; gap: 6px; margin: 14px 0 0; padding-top: 14px;
  border-top: 1px dashed var(--ink-100); font-size: .8rem; color: var(--ink-500);
}
.order-summary-note i { color: var(--success, #16a34a); }

/* Small trust/reassurance row under the checkout submit button - standard
   e-commerce checkout convention (Stripe/Shopify-style "secure checkout"
   strip). Only states things actually true of this flow: submission goes
   over the same HTTPS/CSRF-protected POST every form on the site uses, and
   every order is genuinely reviewed by an admin (Payment.status starts
   pending_review, see finance/models.py) before a subscription activates -
   no unverifiable "encrypted at rest" or fixed turnaround-time claims. */
.checkout-trust-row {
  list-style: none; display: flex; flex-wrap: wrap; gap: 10px 20px; justify-content: center;
  margin: 14px 0 0; padding: 0; font-size: .78rem; color: var(--ink-500);
}
.checkout-trust-row li { display: flex; align-items: center; gap: 6px; }
.checkout-trust-row i { color: var(--brand-600); font-size: .9rem; }

/* ---------- Checkout "standard payment flow" pass (2026-08-12 follow-up) ----------
   Turns the old duplicate "informational QR grid + separate <select>" into
   one interactive card grid: each card IS the radio input (native
   `<input type="radio" name="vendor_qr">`/`name="method"`, hand-rendered
   from `form.<field>.field.choices` in the template rather than the
   widget's own <select> markup - the POSTed field name/values are
   byte-identical to the old <select>, only the on-page control changed).
   Two-column checkout layout (`.checkout-layout`) so the page uses the
   site's real container width instead of a centered 460-670px column, and
   a soft radial background wash (`.finance-glow-zone`) so these
   transactional pages don't read as flat/gray next to the rest of the
   now-colorful site. */
.finance-glow-zone { position: relative; }
.finance-glow-zone::before {
  content: ""; position: absolute; top: -80px; right: -120px; width: 480px; height: 480px;
  background: radial-gradient(circle, var(--brand-50) 0%, rgba(238, 242, 255, 0) 70%);
  border-radius: 50%; z-index: -1; pointer-events: none;
}
.finance-glow-zone::after {
  content: ""; position: absolute; bottom: -60px; left: -100px; width: 340px; height: 340px;
  background: radial-gradient(circle, #ccfbf1 0%, rgba(204, 251, 241, 0) 72%);
  border-radius: 50%; z-index: -1; pointer-events: none; opacity: .6;
}

/* Cart -> Checkout -> Confirmation progress pill bar. Reflects the real
   three-page flow (view_cart -> checkout -> my_payments), not a fake
   in-page multi-step form - the checkout form itself stays one page/one
   POST, this only orients the student on where they are across pages. */
.checkout-steps { display: flex; align-items: center; gap: 6px; margin-bottom: 24px; flex-wrap: wrap; }
.checkout-step { display: flex; align-items: center; gap: 8px; font-size: .85rem; color: var(--ink-500); font-weight: 600; }
.checkout-step-num {
  width: 24px; height: 24px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
  background: var(--ink-100); color: var(--ink-500); font-size: .76rem; font-weight: 700; flex-shrink: 0;
}
.checkout-step.is-current .checkout-step-num { background: var(--brand-600); color: #fff; }
.checkout-step.is-current { color: var(--ink-900); }
.checkout-step.is-done .checkout-step-num { background: var(--success); color: #fff; }
.checkout-step.is-done { color: var(--ink-700); }
.checkout-step-sep { width: 24px; height: 1px; background: var(--ink-200, var(--ink-100)); flex-shrink: 0; }

/* Two-column checkout: form/QR/upload on the left, order summary sticky
   on the right - the standard e-commerce checkout convention, and a real
   use of the site's wide `--container` instead of a cramped single column. */
.checkout-layout { display: grid; grid-template-columns: 1fr; gap: 24px; align-items: start; }
@media (min-width: 992px) {
  .checkout-layout { grid-template-columns: 1.6fr 1fr; }
  /* `.navbar` is `sticky-top` (site-wide, base.html) - offset the summary's
     own sticky position below its real rendered height (~72px with its
     py-2 padding + 36px logo + border) plus a little breathing room, so the
     card settles just under the navbar instead of being covered by it. */
  .checkout-aside { position: sticky; top: 90px; }
}
/* Order summary moves above the form on phones/tablets (grid `order`, no
   markup duplication needed) so a mobile student sees the total they're
   paying before scrolling through the QR grid + upload form - the standard
   "sticky order summary" pattern degrades to "summary first" once there's
   no room to keep it pinned beside the form. */
@media (max-width: 991.98px) {
  .checkout-aside { order: -1; }
}

.qr-select-grid, .method-select-grid { display: grid; grid-template-columns: 1fr; gap: 12px; margin-bottom: 4px; }
@media (min-width: 480px) {
  .qr-select-grid { grid-template-columns: 1fr 1fr; }
  .method-select-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 992px) and (max-width: 1199.98px) {
  .qr-select-grid, .method-select-grid { grid-template-columns: 1fr; }
}

/* Each card's own <input type="radio"> is stretched to cover the card via
   `.qr-select-input`/`.method-select-input` (visually hidden, not
   display:none, so it stays keyboard-focusable/tabbable) - clicking or
   tabbing+space anywhere on the label selects it, and `:checked + .../
   :has()` isn't relied on for the highlight (older-engine safety): a tiny
   inline script-free `<label>` wraps the input, so the browser's own
   label-click-activates-control behavior does the work, and JS-free
   `:checked ~` sibling styling below handles the visual state. */
.qr-select-card, .method-select-card {
  position: relative; display: block; cursor: pointer; text-align: left;
  border: 2px solid var(--ink-100); border-radius: var(--radius-md); background: var(--surface);
  padding: 16px; transition: border-color .15s ease, box-shadow .15s ease, background .15s ease;
}
.qr-select-card:hover, .method-select-card:hover { border-color: var(--brand-300, var(--brand-500)); }
/* Custom checkmark badge instead of a plain native radio dot: `appearance:
   none` keeps the element a real, still-tabbable/space-selectable radio
   (nothing about name/value/required changes), just repainted as a small
   circle that fills solid brand-600 with an inline SVG checkmark once
   `:checked` - a clearer "this one is selected" signal at a glance than a
   generic browser dot, especially next to the QR image grid. */
.qr-select-input, .method-select-input {
  position: absolute; top: 14px; right: 14px; width: 22px; height: 22px; margin: 0;
  -webkit-appearance: none; appearance: none; border: 2px solid var(--ink-300); border-radius: 50%;
  background-color: #fff; cursor: pointer; transition: border-color .15s ease, background-color .15s ease;
}
.qr-select-input:checked, .method-select-input:checked {
  border-color: var(--brand-600); background-color: var(--brand-600);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='white'%3E%3Cpath d='M13.485 1.929a1 1 0 0 1 0 1.414l-7.5 7.5a1 1 0 0 1-1.414 0l-3.5-3.5a1 1 0 1 1 1.414-1.414L5.5 9.243l6.793-6.793a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: center; background-size: 12px 12px;
}
.qr-select-card:has(.qr-select-input:checked), .method-select-card:has(.method-select-input:checked) {
  border-color: var(--brand-600); background: var(--brand-50); box-shadow: 0 0 0 3px rgba(79, 70, 229, .14), var(--shadow-sm, 0 1px 3px rgba(0,0,0,.06));
}
.qr-select-input:focus-visible, .method-select-input:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 2px; }

/* The QR image itself is the visual focus of the card: a clear white
   "scan me" frame (border + corner-bracket styling) around a properly
   sized (not tiny, not stretched) square image, name/account details
   labeled clearly beneath it. */
.qr-select-frame {
  display: block; width: 100%; max-width: 168px; aspect-ratio: 1 / 1; margin: 0 auto 12px;
  padding: 10px; background: #fff; border: 1px solid var(--ink-100); border-radius: var(--radius-sm);
  box-shadow: var(--shadow-sm, 0 1px 3px rgba(0,0,0,.06));
}
.qr-select-frame img { width: 100%; height: 100%; object-fit: contain; display: block; }
.qr-select-vendor-head { display: flex; align-items: center; justify-content: center; gap: 6px; margin-bottom: 2px; }
.qr-select-icon { color: var(--brand-600); font-size: 1rem; flex-shrink: 0; }
/* Real vendor logos (core.templatetags.vendor_logos) - height-capped,
   width auto, so eSewa's wide wordmark and IME's square mark both sit at
   a consistent visual weight next to the vendor name text. */
.qr-select-vendor-logo { height: 18px; width: auto; flex-shrink: 0; }
.method-select-logo { height: 22px; width: auto; }
.qr-select-name { font-family: var(--font-display); text-align: center; margin: 0; }
.qr-select-details { font-size: .8rem; color: var(--ink-500); text-align: center; white-space: pre-line; margin: 2px 0 0; }
.qr-select-instructions { font-size: .78rem; color: var(--ink-500); text-align: center; margin: 4px 0 0; }

.method-select-card { padding: 14px 40px 14px 16px; }
.method-select-body { display: flex; align-items: center; gap: 12px; }
.method-select-icon {
  width: 38px; height: 38px; border-radius: 50%; flex-shrink: 0; display: inline-flex; align-items: center;
  justify-content: center; background: var(--brand-50); color: var(--brand-600); font-size: 1.05rem;
}
.method-select-label { font-weight: 600; color: var(--ink-900); }

/* ---------- Subject picker (finance/add_to_cart.html, 2026-08-30) ----------
   Search-as-you-type combobox replacing a flat "every non-free Subject"
   <select> - see static/js/subject-picker.js's own docstring for the full
   rationale. `.subject-picker-results` is a floating panel (not part of
   document flow) so it overlays whatever sits below the search box
   instead of pushing the rest of the form down while open. */
.subject-picker { position: relative; }
.subject-picker-results {
  position: absolute; z-index: 20; top: calc(100% + 6px); left: 0; right: 0;
  max-height: 320px; overflow-y: auto; background: var(--surface);
  border: 1px solid var(--ink-100); border-radius: var(--radius-md); box-shadow: var(--shadow-md);
  padding: 6px;
}
.subject-picker-result {
  display: flex; flex-direction: column; gap: 2px; width: 100%; text-align: left;
  border: none; background: none; padding: 10px 12px; border-radius: var(--radius-sm);
  cursor: pointer; transition: background-color .12s ease;
}
.subject-picker-result:hover, .subject-picker-result.is-active { background: var(--brand-50); }
.subject-picker-result-name { font-weight: 600; color: var(--ink-900); font-size: .93rem; }
.subject-picker-result-context { font-size: .8rem; color: var(--ink-500); }
.subject-picker-empty { padding: 16px 12px; text-align: center; color: var(--ink-500); font-size: .88rem; }

/* Selected-subject chip - replaces the search box once a subject is
   picked, same "here's what you chose, with an easy way to change it"
   pattern as .upload-dropzone's has-file state. */
.subject-picker-selected {
  display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
  background: var(--brand-50); border: 1px solid var(--brand-100, #e0e7ff); border-radius: var(--radius-md);
  padding: 14px 16px;
}
.subject-picker-selected-info { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.subject-picker-selected-info strong { color: var(--ink-900); font-size: .97rem; }
.subject-picker-selected-context { color: var(--ink-500); font-size: .85rem; }
/* "Includes: Physics, Chemistry, Biology..." - shown both in the search
   result list and on the selected-bundle chip so a buyer sees exactly
   which subjects a bundle grants access to before paying for it. */
.subject-picker-selected-subjects { flex-basis: 100%; color: var(--ink-500); font-size: .82rem; margin-top: 2px; }
.subject-picker-result-subjects { display: block; color: var(--ink-500); font-size: .78rem; margin-top: 2px; }

/* Plan cards reuse .method-select-card's exact shell (checkmark radio,
   hover/checked states) - only the body content differs (duration label
   + price instead of a payment-method icon+label). */
.subject-picker-plan-price { color: var(--ink-500); font-size: .82rem; }

/* ---------- Drag-and-drop screenshot upload (finance/checkout.html,
   finance/respond_reverification.html) ----------
   Progressive enhancement only: the real `<input type="file" name=...">`
   Django renders is kept in the DOM, unmodified in name/type, and is what
   actually submits with the form - it's stretched via `inset: 0` to cover
   the whole dropzone at opacity 0, so both a plain click AND a native
   browser drag-and-drop of a file land on the real input with zero JS
   required for the drop itself to work. `static/js/finance-flow.js` only
   adds: a `.is-dragover` highlight class while dragging, and a filename/
   image-thumbnail preview once a file is chosen (via the input's own
   native `change` event, which fires identically whether the file was
   picked from the dialog or dropped). If JS fails to load, the box still
   works as a plain (if unstyled-on-drag) file input. */
.upload-dropzone {
  position: relative; border: 2px dashed var(--ink-300); border-radius: var(--radius-md);
  padding: 28px 20px; text-align: center; background: var(--surface-alt); transition: border-color .15s ease, background .15s ease;
}
.upload-dropzone.is-dragover { border-color: var(--brand-600); background: var(--brand-50); }
.upload-dropzone.has-file { border-style: solid; border-color: var(--brand-300, var(--brand-500)); }
.upload-dropzone input[type="file"] {
  position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; margin: 0; padding: 0;
}
.upload-dropzone-icon { font-size: 1.8rem; color: var(--brand-500); display: block; margin-bottom: 8px; }
.upload-dropzone-text { margin: 0 0 2px; font-weight: 600; color: var(--ink-700); font-size: .92rem; }
.upload-dropzone-hint { margin: 0; }
.upload-dropzone-preview { display: flex; align-items: center; justify-content: center; gap: 12px; flex-wrap: wrap; }
.upload-dropzone-thumb {
  width: 56px; height: 56px; object-fit: cover; border-radius: var(--radius-sm); border: 1px solid var(--ink-100);
  background: var(--surface);
}
.upload-dropzone-filename { font-weight: 600; color: var(--ink-900); font-size: .88rem; word-break: break-all; }
.upload-dropzone-change {
  font-size: .78rem; color: var(--brand-600); font-weight: 700; text-transform: uppercase; letter-spacing: .03em;
}

/* ---------- Quick subject filter (finance/add_to_cart.html) - client-side
   only, filters the native <select>'s own <option> list as the student
   types (no backend query, no change to the field's name/options/values).
   ---------- */
.subject-filter-wrap { position: relative; margin-bottom: 8px; }
.subject-filter-wrap .field-icon { color: var(--ink-500); }
.subject-filter-empty { font-size: .8rem; color: var(--ink-500); margin: 6px 0 0; display: none; }
.subject-filter-empty.is-visible { display: block; }

/* ---------- Payment/order status pills (finance/my_payments.html) - same
   pill-badge language as .notice-type-badge above, colored with the site's
   existing semantic success/warning/danger tokens rather than one-off hex
   values, keyed off the actual Payment.STATUS_CHOICES / Order.overall_status()
   values so the class always matches a real status, never guessed. ---------- */
.payment-status-badge { display: inline-block; padding: 3px 10px; border-radius: 999px; font-size: .75rem; font-weight: 700; white-space: nowrap; }
.payment-status-pending_review,
.payment-status-awaiting_reverification { background: #fef3c7; color: #92400e; }
.payment-status-approved { background: #dcfce7; color: #166534; }
.payment-status-rejected { background: #fee2e2; color: #991b1b; }
.payment-status-refunded,
.payment-status-partially_approved,
.payment-status-empty { background: #e0e7ff; color: #3730a3; }

.order-card-head { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; }
.order-card-head-title { display: flex; align-items: center; gap: 10px; }
.order-card-meta { color: var(--ink-500); font-size: .85rem; margin: 4px 0 0; }
.order-amount { font-weight: 600; color: var(--ink-700); }
/* 2026-08-30: was `border-left: 4px solid` (a side-stripe accent) - replaced
   with a full tinted card + leading icon badge, consistent with how every
   other callout on this site (empty-state, seasonal-offer-banner) signals
   its type through background + iconography, not a colored edge. */
.action-needed-card { background: #fffbeb; border-color: #fde68a; }
.action-needed-card .action-needed-icon {
  width: 40px; height: 40px; border-radius: 999px; background: #fef3c7; color: #92400e;
  display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-size: 1.1rem;
}

/* ---------- Catalog browse/search/filter surfaces (search & filter UX pass, 2026-08-12) ----------
   Shared utilities for templates/catalog/{search,subject_list,level_list,class_list,
   course_list,subject_group_list}.html - these are task-oriented "find a subject" pages,
   not marketing pages, so type stays restrained (small, muted meta/lead text rather than
   another oversized heading) per the product-register guidance in the type-scale comment
   above. Replaces several ad-hoc `style="color:var(--ink-500);"` / `style="padding-top:0;"`
   / `style="text-align:center;padding:60px;"` one-offs that were duplicated near-verbatim
   across all six of these templates. */

/* Muted lead paragraph directly under a page-hero h1 (result counts, short helper copy).
   Deliberately body-sized (not a heading-scale step) - it's secondary to the h1, and on a
   browse/search surface it shouldn't compete for attention. */
.hero-lead { color: var(--ink-500); font-size: .95rem; margin: 8px 0 0; }

/* .section immediately following a .page-hero, so the hero's own bottom padding is the
   only gap between them (avoids doubled vertical space). */
.section-tight { padding-top: 0; }

/* Empty/zero-result state used by every listing + search page's "nothing here" card. */
.empty-state { text-align: center; padding: 60px 24px; }
.empty-state .icon { color: var(--ink-300, var(--ink-500)); margin: 0 auto 14px; }
.empty-state-icon { font-size: 2rem; color: var(--ink-300); display: block; margin-bottom: 14px; }
.empty-state h3 { margin: 0 0 8px; }
.empty-state p { color: var(--ink-500); font-size: .93rem; margin: 0 auto 18px; max-width: 420px; }
.empty-state-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }

/* ---------- Empty-state original illustrations (2026-08-12) ----------
   Original SVG line-art (search.html's two empty states, level_list.html's
   "no levels yet" state) replacing plain single-color Bootstrap-icon glyphs
   against bare background - a soft brand-tinted blob behind simple flat
   geometric shapes, using .illo-* primitives defined above. Owned/scoped to
   this new wrapper class only; .empty-state itself is untouched. */
.empty-state-illustration { width: 190px; max-width: 60%; margin: 0 auto 18px; }
.empty-state-illustration svg { width: 100%; height: auto; display: block; overflow: visible; }

/* ---------- Page-load transition (2026-08-12) ----------
   A very small, safe entrance for full page loads: Django renders true
   multi-page navigations (no SPA layer), so a heavy JS-driven
   fade-out-old/fade-in-new page-transition system would risk a flash of
   unstyled content and a broken back/forward experience for little gain.
   A cheap CSS-only fade+lift on <body> gives every navigation a slightly
   smoother arrival instead, with no JS dependency and no risk of ever
   leaving content stuck hidden (animation runs once, `both` keeps the end
   state, and reduced-motion users never see it at all since the whole rule
   only exists inside the `no-preference` media query). */
@media (prefers-reduced-motion: no-preference) {
  body { animation: page-fade-in .45s ease both; }
  @keyframes page-fade-in {
    from { opacity: 0; transform: translateY(6px); }
    to { opacity: 1; transform: none; }
  }
}

/* Search input focus ring - reuses the same visual language as .field input:focus
   (border -> brand-500, soft brand-50 glow) so the search box doesn't invent a new
   focus treatment. Applied to the wrapper via :focus-within since the input itself has
   no border/outline of its own (the wrapper draws the border). */
.search-box { transition: border-color .15s ease, box-shadow .15s ease; }
.search-box:focus-within { border-color: var(--brand-500); box-shadow: 0 0 0 3px var(--brand-50); }

/* Result-type section headers on search.html (Courses vs Subjects), and the small
   "N results" line above a listing grid - kept at restrained sizes/weights, icon does the
   distinguishing work rather than bumping font-size. */
.result-heading { display: flex; align-items: center; gap: 8px; }
.result-heading .icon { color: var(--brand-600); }
.result-count-line { color: var(--ink-500); font-size: .85rem; font-weight: 600; margin: 0 0 14px; }

/* Tier-filter pill bar (subject_list.html's Free/Notes free/Premium row, also reusable
   anywhere else a short set of GET-param filter pills is needed) - wraps the existing
   .btn.rounded-pill pills already built in the template in a bordered/backgrounded group
   so they read as one control instead of floating loose, consistent gap between pills,
   and enough padding that the active pill's contrast doesn't collide with the container
   edge. Active vs inactive contrast is inherited from Bootstrap's own
   .btn-primary/.btn-outline-secondary (already WCAG AA), this just gives them a home. */
.tier-filter-group {
  background: var(--surface-alt); border: 1px solid var(--ink-100); border-radius: var(--radius-md);
  padding: 6px; gap: 6px !important;
}
.tier-filter-group .btn { font-weight: 600; }

/* Structural pass (2026-08-12 follow-up): the plain centered `.page-hero` reads flat on
   these six pages - same treatment repeated with nothing but a kicker + h1, no visual
   anchor. `.catalog-hero` is an additive modifier (stacked alongside `.page-hero`, never
   replacing it - `.page-hero`'s own rules are shared sitewide and out of scope here) that
   gives the catalog/search hero a soft radial brand wash and a hairline bottom edge so the
   page reads as a distinct "find something" surface rather than a generic content banner. */
.catalog-hero {
  position: relative; background: radial-gradient(120% 140% at 50% -20%, var(--brand-50) 0%, transparent 60%);
  border-bottom: 1px solid var(--ink-100);
}

/* Enlarged, higher-contrast search box used only on search.html's own hero (`.search-hero`
   wraps the existing `.hero-search`/`.search-box` markup - those base rules are untouched
   so every other `.hero-search` on the site is unaffected). Bigger target, bigger type,
   a visible brand-colored submit affordance - the primary action on a dedicated search
   landing should look like the primary action. */
.search-hero .search-box { padding: 16px 20px; gap: 14px; box-shadow: var(--shadow-md); }
.search-hero .search-box input { font-size: 1.05rem; }
.search-hero .search-box .icon { color: var(--ink-500); }
.search-hero .search-box:focus-within .icon { color: var(--brand-600); }

/* Quick-link chips (bordered pill links, distinct from the plain-text `.chip` badge used
   elsewhere) for the "browse instead" prompts on search.html's empty states. */
.quick-link-chip {
  display: inline-flex; align-items: center; gap: 8px; padding: 10px 18px; border-radius: 999px;
  background: var(--surface); border: 1px solid var(--ink-100); color: var(--ink-900); font-weight: 600;
  font-size: .88rem; transition: border-color .15s ease, transform .15s ease, box-shadow .15s ease;
}
.quick-link-chip:hover { border-color: var(--brand-500); color: var(--brand-600); transform: translateY(-2px); box-shadow: var(--shadow-sm); }
.quick-link-chip .icon { color: var(--brand-600); }

/* subject_list.html's search box + tier-filter bar: on wide viewports these sat in two
   full-width rows for no reason, wasting vertical space above the results grid. Row them
   up side by side (search grows, filter group keeps its natural width) above the
   `--container` page's `.grid-3` breakpoint fallback (matches the existing 900px grid
   collapse below), and let them stack back to full-width rows on narrower screens where
   there isn't room. */
.filter-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 14px 16px; }
.filter-toolbar .hero-search { flex: 1 1 320px; margin: 0; }
.filter-toolbar .tier-filter-group { margin: 0; flex: 0 0 auto; }
@media (max-width: 899.98px) {
  .filter-toolbar { flex-direction: column; align-items: stretch; }
  .filter-toolbar .tier-filter-group { justify-content: center; }
}

/* ===========================================================================
   Legal pages (terms.html, privacy.html) + Contact page (contact.html)
   ---------------------------------------------------------------------------
   Owned block - only these three templates reference the classes below.
   Nothing above this comment was touched. Builds on the existing
   `.page-hero` / `.prose` / `.icon-badge` / `.card` / `.field-input-wrap`
   patterns already used sitewide rather than inventing parallel systems.
   =========================================================================== */

/* Icon badge sitting inside the hero of a legal page, echoing the same
   `.icon-badge.icon-badge-primary` component already used on auth cards -
   gives Terms/Privacy a quiet "official document" anchor instead of a bare
   kicker + h1. */
.legal-hero-icon { margin: 0 auto 14px; display: flex; }

/* "Last updated" metadata chip - was a buried `<p style="color:...">` line;
   now reads as a real piece of document metadata (dated, bordered, pill
   shaped) rather than a stray sentence under the title. */
.legal-meta-chip {
  display: inline-flex; align-items: center; gap: 7px;
  background: var(--surface); border: 1px solid var(--ink-100); border-radius: 999px;
  padding: 7px 16px; font-size: .82rem; font-weight: 600; color: var(--ink-700);
  box-shadow: var(--shadow-sm);
}
.legal-meta-chip .bi { color: var(--brand-600); font-size: .9rem; }

/* Sticky table-of-contents sidebar. Sits in the natural `col-lg-3` gutter
   next to the `.prose` content column on terms/privacy. Below `lg` it just
   stacks above the content as a normal bordered card (no sticky, no room
   for a rail on narrow screens). */
.legal-toc {
  background: var(--surface); border: 1px solid var(--ink-100); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm); padding: 20px;
}
.legal-toc-title {
  display: block; color: var(--ink-500); font-weight: 700; font-size: .72rem;
  letter-spacing: .06em; text-transform: uppercase; margin-bottom: 10px;
}
.legal-toc ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.legal-toc a {
  display: block; color: var(--ink-700); font-size: .87rem; font-weight: 600;
  padding: 7px 10px; border-radius: var(--radius-sm); line-height: 1.35;
  transition: background-color .15s ease, color .15s ease;
}
.legal-toc a:hover { background: var(--brand-50); color: var(--brand-600); }
@media (min-width: 992px) {
  .legal-toc-sticky { position: sticky; top: 90px; }
}

/* Numbered legal sections get a little breathing room + a hairline
   separator so the document reads as distinct clauses rather than one
   unbroken scroll - and `scroll-margin-top` keeps the sticky navbar from
   covering a heading you just jumped to via the TOC. */
.prose .legal-section { padding-top: 28px; margin-top: 28px; border-top: 1px solid var(--ink-100); scroll-margin-top: 90px; }
.prose .legal-section:first-of-type { padding-top: 0; margin-top: 8px; border-top: none; }
.prose .legal-section h2 { margin-top: 0; }

/* Contact page: leading icons on the single-line text fields, reusing the
   exact `.field-input-wrap`/`.field-icon` system already built for the
   accounts forms (see main.css's "Leading icons inside form fields"
   rules above) - no parallel icon system invented here. Only the
   `.mb-3`/label wiring around them is contact-page markup. */
.contact-card { border-radius: var(--radius-lg); box-shadow: var(--shadow-md); }

/* ==========================================================================
   Ads (2026-08-12 visual pass)
   ==========================================================================
   Real classes for the markup core/templatetags/ads.py's _card_markup /
   _popup_markup / _infeed_markup_subject / _side_rail_markup build, in
   place of the bare inline `style="..."` those used to carry (the ads
   still looked like an unstyled `<img>` slapped on the page even after the
   2026-07-24 containment fix above, because none of the "premium content
   card" chrome - corner disclosure badge, deliberate media clipping/ratio -
   was ever actually written down anywhere).

   Naming keeps the same rule the existing `.fine-print-label` comment above
   already spells out: no "ad"/"promo"/"sponsor" substring in a class name
   that's shared across every impression sitewide, so a filter-list rule
   that matches on the name can't be written against it in the first place.
   `disclosure-*` describes what these classes are FOR (the human-readable
   "this is paid placement" disclosure - badge, caption) without saying so
   in the selector itself.

   Two things stay deliberately outside this section, unchanged:
   - `_infeed_markup_course` keeps reusing the real `.course-badge` pill
     (see its own docstring) instead of a new overlay class - it already
     matches the surrounding grid's real "Free"/"3 classes" badges.
   - `_side_rail_markup`'s `<style>` block (position:fixed/display, scoped
     to that one creative's own `#promo-rail-<pk>` id) AND its close
     button's inline style BOTH stay exactly as architected, unclassed -
     see that function's docstring for why a shared selector there is
     actively dangerous (one ad-blocker "block this element" click
     disables that control sitewide forever). Only the rail's own
     media/link chrome (shared with the card/infeed placements below, same
     as `.card` already is) moves into classes here - the close button
     gets none. */

/* Card/aside/banner placement (_card_markup) and the side rail's own link
   wrapper (_side_rail_markup) - the actual `<a class="card ...">` link,
   laid out as a block so the whole card is clickable, with the image
   clipped to the card's own corner radius (the 2026-07-24 fix's
   overflow:hidden, now written down instead of inline). `position:relative`
   so `.disclosure-badge` below can anchor to this card's own image, not the
   page. */
.disclosure-card { display: block; text-decoration: none; overflow: hidden; position: relative; }
.disclosure-card img { max-width: 100%; height: auto; border-radius: 8px; display: block; margin: 0 auto; }

/* Compact rendering variant (2026-08-12, first-ad-slot density tuning) -
   opt-in via `{% show_ad "banner" compact=True %}`'s new `compact` kwarg
   (core/templatetags/ads.py's `_card_markup`), used ONLY at whichever call
   site is the FIRST ad slot on a page (base.html's sitewide banner,
   subject_list.html/course_list.html's "top" banner_slot) - a reader's
   very first impression of a page shouldn't be dominated by a full-height
   banner before they've engaged with any real content. `object-fit: cover`
   crops the same creative image into a shorter box (rather than shrinking
   it proportionally, which would read as a tiny stretched thumbnail), so
   it still reads as a deliberate, designed crop. Every other placement -
   in-feed, aside, sticky-timed, side rail, popup/video-popup - keeps its
   existing, larger sizing entirely unchanged; ads further down the page,
   once a reader has already engaged with content, are allowed the full
   visual weight they had before this pass. */
.disclosure-card-compact img { max-height: 110px; width: 100%; object-fit: cover; }
/* 2026-08-13 presentation pass: `.card`'s own 28px all-round padding (kept
   deliberately for every OTHER ad placement, so they read as organic
   content cards - see this section's intro comment) reads as a lot of dead
   white space around a short, wide 1200x300-ratio banner creative
   specifically - a thin strip of image floating in a large white box.
   Tightened for the compact variant only; every other placement (aside,
   in-feed, side rail, popup, leaderboard) is untouched. */
.disclosure-card-compact { padding: 14px; }

/* Full-bleed tint band wrapping the sitewide bottom-of-page banner slot
   (base.html) - see that template's own comment for why: without it, a
   single white ad card sat directly in the seam between the page's white
   content and the footer's dark navy, reading as a stray/disconnected
   element rather than a real section of the page. */
.ad-slot-band { background: var(--surface-alt); padding: 32px 0; }

/* Small pill badge overlaid on the top-left corner of the creative image -
   genuine visual identity + honest disclosure in one glance, replacing the
   old bare "Promoted" caption line that made an ad look like an unstyled
   afterthought rather than a designed content unit. Dark translucent chip
   (not `.badge`'s brand colors) so it reads as a system-level label sitting
   *on* the image, not as another clickable-looking pill competing with the
   real "Learn more" CTA below it. */
.disclosure-badge {
  position: absolute; top: 10px; left: 10px; z-index: 1;
  background: rgba(15, 23, 42, .78); color: #fff; font-size: .68rem; font-weight: 700;
  letter-spacing: .05em; text-transform: uppercase; padding: 4px 10px; border-radius: 999px;
  box-shadow: var(--shadow-sm);
}

/* Popup placement (_popup_markup) - the modal body's image, centered, same
   8px creative-image radius as the card placement above. The modal's own
   `.modal-content` already supplies the card-like border/shadow chrome via
   Bootstrap, and the "Promoted" disclosure lives in the real modal title
   (`<h5 class="modal-title">`) rather than a caption here, so nothing else
   needed on top of centering + the image radius. */
.disclosure-modal-body { text-align: center; }
.disclosure-modal-body img { max-width: 100%; height: auto; border-radius: 8px; }

/* In-feed native card link (_infeed_markup_subject/_infeed_markup_course) -
   stretches the whole `.card`/`.course-card` into one clickable link like
   the real content cards around it do, and clips the top image band to the
   card's own top corners (`--radius-lg` matches the card's own radius
   variable rather than a hardcoded value, so a future radius token change
   doesn't leave the ad's corners mismatched from real cards'). */
.disclosure-native-link { display: flex; flex-direction: column; height: 100%; text-decoration: none; color: inherit; }
.disclosure-native-media { overflow: hidden; border-radius: var(--radius-lg) var(--radius-lg) 0 0; position: relative; }
.disclosure-native-media img { width: 100%; height: 150px; object-fit: cover; display: block; }
.disclosure-course-media { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

/* Side rail (_side_rail_markup) - applied straight to the creative `<img>`
   itself (no separate media wrapper needed at this placement's size);
   sizing/fit only, no positioning (the wrapper's position:fixed/display
   stay in that function's inline per-id `<style>` block, unchanged, see
   its docstring). */
.disclosure-rail-media { width: 100%; height: auto; display: block; }

/* Side rail dismiss (x) button - deliberately has NO class here. Its
   docstring is explicit that this button carries no class/id at all, on
   purpose: an earlier revision gave it a shared class for exactly this
   kind of look-only styling and that was reverted specifically because a
   shared selector on this one element is a single "block this element"
   click away from disabling every future side-rail ad's dismiss control
   sitewide, forever (see _side_rail_markup's "Robustness against manual
   per-element blocking" docstring section). The polish this pass adds -
   `--ink-900`/`--brand-600`-token colors and a `background` transition for
   the hover swap - is written directly into that function's own inline
   `style="..."` string instead, so the look improves without adding the
   shared selector the docstring warns against. */

/* Leaderboard placement (2026-08-12, _leaderboard_markup) - a new ad type:
   a wide, short horizontal strip (728x90, see
   Campaign.CREATIVE_ASPECT_SPEC) for above/below a content section,
   genuinely distinct from the vertical card placements above both in
   shape and DOM. At 90px tall there's no room for a `.disclosure-badge`
   corner overlay without covering real ad content, so the "Sponsored" tag
   sits inline to the LEFT of the image in a slim flex row instead - same
   dark chip look as `.disclosure-badge`, just not absolutely positioned. */
.disclosure-leaderboard {
  display: flex; align-items: center; gap: 12px; text-decoration: none;
  background: var(--surface); border: 1px solid var(--ink-100); border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm); padding: 10px 14px; max-width: 100%;
  transition: box-shadow .2s ease, transform .2s ease;
}
.disclosure-leaderboard:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); }
.disclosure-leaderboard-tag {
  flex: 0 0 auto; background: rgba(15, 23, 42, .78); color: #fff; font-size: .68rem; font-weight: 700;
  letter-spacing: .05em; text-transform: uppercase; padding: 4px 10px; border-radius: 999px;
}
.disclosure-leaderboard img { flex: 1 1 auto; max-width: 100%; max-height: 90px; width: auto; height: auto; display: block; border-radius: 6px; }

/* ==========================================================================
   core.FAQ page (templates/core/faq_list.html) - Task: real, admin-editable,
   searchable/filterable/paginated FAQ system.

   The accordion itself reuses the existing `.faq-item`/`.faq-list` rules
   above verbatim (same `<details>/<summary>` markup as the static block
   that used to live directly in about.html) - nothing new needed there.
   The one addition below is `.faq-item .faq-answer`: the old rule only
   ever styled a single plain `<p>` inside each item, but FAQ.answer is now
   CKEditor-authored rich text (can contain its own <p>/<ul>/<a> tags), so
   the template wraps it in a <div class="faq-answer"> instead of a <p>
   (avoids inline-editor-produced <p> tags ending up nested inside a
   template-level <p>, which is invalid HTML) and this carries forward the
   same color/size/spacing `.faq-item p` already had, plus sane defaults
   for the richer content it may now contain.
   ========================================================================== */
.faq-item .faq-answer { color: var(--ink-500); margin: 0 0 18px; font-size: .92rem; }
.faq-item .faq-answer > *:last-child { margin-bottom: 0; }
.faq-item .faq-answer p { margin: 0 0 10px; }
.faq-item .faq-answer ul, .faq-item .faq-answer ol { margin: 0 0 10px; padding-left: 1.25em; }
.faq-item .faq-answer a { color: var(--brand-600); text-decoration: underline; }

/* ==========================================================================
   Home page (2026-08 rhythm/copy/CTA/carousel pass) - templates/pages/home.html
   ==========================================================================
   Everything below is scoped under the page's own `.home-page` wrapper div
   (added directly in home.html) so none of it leaks onto other pages that
   also use `.section`/`.btn-outline-primary`/etc.

   Rhythm: the page used to alternate `var(--surface)`/`var(--surface-alt)`
   inconsistently (see the inline `style="background:..."` home.html already
   had here and there), which meant several visually-similar sections sat
   back to back with the *same* flat background and nothing else to mark a
   transition - reads as one long blank scroll. home.html now assigns an
   explicit, strictly-alternating background to every section in source
   order via those same inline styles (no new class needed for that part -
   it's just data, not a reusable rule). What *is* new here:
   `.home-page .section-cozy` trims the shared `.section` 72px top/bottom
   padding for sections that already contain their own padded, visually
   self-contained band (stats-band / photo-band / cta-band / the carousel),
   so the section wrapper doesn't stack a second helping of empty space on
   top of the band's own padding - was the single biggest source of the
   "freaking blank" gap right around the photo-band section in particular. */
.home-page .section-cozy { padding: 48px 0; }

/* "Get started free" / "Browse Classes" and every other secondary CTA on
   this page were plain `.btn-outline-primary` - and it turns out that class
   was rendering with a fully TRANSPARENT border, not just a thin one: this
   file's own generic `.btn { border: 1px solid transparent; }` rule (further
   up, in the Buttons section) loads after bootstrap.min.css/bootstrap-
   overrides.css and wins the cascade at equal specificity, so Bootstrap's
   `border: var(--bs-btn-border-width) solid var(--bs-btn-border-color)`
   never actually painted a visible border - only the brand-colored text
   survived, which is exactly why this read as invisible/washed-out rather
   than merely thin. Setting `border-color`/`border-style` explicitly here
   (not just `border-width`) is what actually fixes it, since a real color
   value beats the earlier rule's `transparent` at the same specificity.
   Also gives it a real filled hover state (not Bootstrap's default) so it
   reads as a confident second action, without going fully solid - that's
   reserved for the one true primary action per section: "Get started free"
   in the hero, "Get started"/"Start studying"/"Create your account" (all
   plain `.btn-primary`, unaffected by this bug) elsewhere on the page. */
.home-page .btn-outline-primary-strong {
  border-width: 2px; border-style: solid; border-color: var(--brand-600);
  color: var(--brand-600); background: transparent; font-weight: 800;
  transition: transform .15s ease, box-shadow .15s ease, background .15s ease, color .15s ease;
}
.home-page .btn-outline-primary-strong:hover,
.home-page .btn-outline-primary-strong:focus-visible {
  background: var(--brand-600); border-color: var(--brand-600); color: #fff; box-shadow: var(--shadow-md);
}

/* "Browse all classes" section-closing CTA, under Featured Subjects
   (2026-08-14) - was a bare 34px inline top-margin with nothing below it,
   so the button read as pinched directly under the card grid with no
   breathing room before the section's own bottom padding kicked in. A
   real class instead of an inline style, with deliberate space on both
   sides and a slightly larger touch target so it reads as a genuine
   section-ending action, not an afterthought tacked under the grid. */
.featured-subjects-cta { margin: 40px 0 12px; }
.featured-subjects-cta .btn { padding: 14px 30px; font-size: 1.02rem; }

/* Testimonial carousel: converts the old static 4-up grid into a real
   Bootstrap carousel (native `data-bs-ride`, no custom JS) so it reads as
   one story at a time and doesn't just repeat the same card-grid pattern
   every other section on the page already uses. Bootstrap's default
   control chrome (white arrows meant to sit on a photo) is invisible on a
   plain card, so it's swapped for small brand-colored circular controls +
   a brand-colored pill/dot indicator row, matching the rest of the site's
   `--brand-*` tokens instead of Bootstrap's black/white carousel defaults. */
.home-testimonial-carousel { padding: 4px 64px 0; }
.home-testimonial-carousel .testimonial-card { max-width: 640px; margin: 0 auto; min-height: 220px; }
.home-testimonial-carousel .carousel-item { transition: opacity .5s ease; }
.home-testimonial-carousel .carousel-control-prev,
.home-testimonial-carousel .carousel-control-next {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 44px; height: 44px; border-radius: 50%; opacity: 1;
  background: var(--surface); box-shadow: var(--shadow-md); border: 1px solid var(--ink-100);
}
.home-testimonial-carousel .carousel-control-prev:hover,
.home-testimonial-carousel .carousel-control-next:hover { background: var(--brand-50); }
.home-testimonial-carousel .carousel-control-prev { left: 0; }
.home-testimonial-carousel .carousel-control-next { right: 0; }
.home-testimonial-carousel .carousel-control-prev-icon,
.home-testimonial-carousel .carousel-control-next-icon {
  width: 1.1rem; height: 1.1rem;
}
.home-testimonial-carousel .carousel-control-prev-icon {
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%234f46e5'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0'/%3e%3c/svg%3e");
}
.home-testimonial-carousel .carousel-control-next-icon {
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%234f46e5'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e");
}
.home-testimonial-carousel .carousel-indicators {
  position: static; margin: 26px 0 0; gap: 8px;
}
.home-testimonial-carousel .carousel-indicators [data-bs-target] {
  width: 8px; height: 8px; border-radius: 999px; background: var(--ink-300);
  opacity: 1; border: none; transition: width .2s ease, background .2s ease;
}
.home-testimonial-carousel .carousel-indicators .active { width: 24px; background: var(--brand-600); }
@media (max-width: 700px) {
  .home-testimonial-carousel { padding: 4px 0 0; }
  .home-testimonial-carousel .carousel-control-prev,
  .home-testimonial-carousel .carousel-control-next { display: none; }
}

/* ---------- Floating WhatsApp button (2026-08) ----------
   `position:fixed` bottom-right on every page that has a WhatsApp number
   configured; z-index sits below Bootstrap's modal/toast layer (see the
   semantic scale implied by `.navbar{z-index:1030}`/modal defaults) but
   above ordinary content, matching `.reader-sidebar`'s off-canvas 1050 as
   the nearest existing "floating chrome" precedent without colliding with
   it. `.wa-float-hidden` (home.html only, toggled visible again by the
   IntersectionObserver in main.js) keeps it out of the way of the hero's
   own CTAs/search mockup until the visitor has scrolled past it. */
.wa-float {
  position: fixed; right: 22px; bottom: 22px; z-index: 1035;
  width: 56px; height: 56px; border-radius: 50%;
  background: #25d366; color: #fff; display: flex; align-items: center; justify-content: center;
  font-size: 1.6rem; box-shadow: 0 10px 28px rgba(15, 23, 42, .22);
  transition: transform .2s cubic-bezier(.16, 1, .3, 1), opacity .2s ease, box-shadow .2s ease;
  opacity: 1; transform: scale(1);
}
.wa-float:hover { box-shadow: 0 14px 34px rgba(15, 23, 42, .3); transform: scale(1.06); color: #fff; }
.wa-float-hidden { opacity: 0; transform: scale(.6); pointer-events: none; }
@media (max-width: 575.98px) {
  .wa-float { right: 16px; bottom: 16px; width: 50px; height: 50px; font-size: 1.4rem; }
}
@media (prefers-reduced-motion: reduce) {
  .wa-float { transition: opacity .15s ease; }
}

/* ---------- LaTeX/rich-text content images (2026-08-12, "show the
   questions latex, and images properly, also the answers will have latex
   and all properly") ----------
   Question/answer/note text (QuestionBankQuestion/PastQuestion/
   ImportantQuestion.question|answer, Chapter.note) is CKEditor-authored
   rich-text HTML that can contain plain <img> tags from the editor's image
   upload, alongside $...$ LaTeX rendered client-side by
   static/js/latex-render.js (see question_bank_detail.html/
   chapter_read.html extra_head/extra_js). Without this, an inline <img>
   wider than its column would overflow the fixed-width .exam-paper
   ("physical paper" layout above) or the .reader-content column. Scoped to
   the same containers latex-render.js targets, so this is purely
   responsive image behavior - no layout/component changes. */
.exam-question-body img,
.exam-answer img,
.reader-content img,
.iq-item img {
  max-width: 100%;
  height: auto;
  display: block;
  margin: 10px auto;
  border-radius: var(--radius-sm);
  border: 1px solid var(--ink-100);
}
/* KaTeX's own rendered <span class="katex">...</span> markup is inline
   math, not a block image - never let the generic `img` rule above (or
   anything else in this section) affect it. KaTeX ships its own internal
   sizing; only guard against the exam paper's fixed max-width squeezing
   display-mode ($$...$$) equations awkwardly on narrow screens. */
.exam-question-body .katex-display,
.exam-answer .katex-display,
.reader-content .katex-display,
.iq-item .katex-display {
  max-width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
}

/* ==========================================================================
   Catalog + core "properly styled background" pass (2026-08-13)
   --------------------------------------------------------------------------
   Task: "please style all the pages properly, style the background and all
   properly" - most catalog listing pages (class/course/subject_group/level/
   subject_list/search/faq) already got a `.page-hero.catalog-hero` radial
   wash + `.section.section-tight` body in an earlier pass; this pass brings
   the remaining flat pages (subject_detail's own hero band, my_bookmarks,
   notice_list, notice_detail, sms_credit) up to the same standard, reusing
   `.page-hero`/`.catalog-hero`/`.hero-lead`/`.section-tight`/`.empty-state`
   wherever the markup already matches rather than inventing parallel rules.
   Only truly new pieces are below. Scoped, additive classes only - nothing
   above this comment is touched.
   ========================================================================== */

/* Generic alternating-section background, the same "surface -> surface-alt"
   rhythm home.html already uses between stacked sections, promoted to a
   reusable class for any owned page here that has more than one section
   stacked (e.g. notice_detail.html's hero band -> body card, sms_credit's
   hero -> result card). Deliberately plain - just the token swap, no other
   side effects - so it composes safely with `.section`/`.section-tight`. */
.section-alt { background: var(--surface-alt); }

/* subject_detail.html's `.subject-hero-band` (defined earlier in this file)
   is a flat solid `--ink-50` band - reads flat/gray next to the rest of the
   now-glow-washed catalog. `.subject-hero-glow` is an additive modifier
   (two classes beats the original rule's one on specificity, so it wins
   without editing the original declaration) that layers the same soft
   brand-tinted radial wash `.catalog-hero` uses on top of that base color,
   rather than pseudo-element blobs - the band already paints an opaque
   background of its own, so pseudo-elements behind it (the dash/finance
   glow-zone technique) would simply be covered and invisible here. */
.subject-hero-band.subject-hero-glow {
  background: radial-gradient(120% 160% at 12% -25%, var(--brand-50) 0%, transparent 55%), var(--ink-50, #f1f5f9);
}
/* Matches the serif `--font-display` treatment every other page's hero h1
   already gets via `.hero h1`/`.page-hero h1` - subject_detail's hero band
   uses its own plain `<h1>` markup (not `.page-hero`), so it fell back to
   the sitewide sans-serif `h1` rule and looked out of step with every
   other subject/class/course page title. */
.subject-hero-title { font-family: var(--font-display); }

/* my_bookmarks.html: each of the three bookmark groups (Chapters / Question
   bank questions / Past questions) used to be a bare `<h3>` with nothing to
   distinguish one shelf from the next. A small colored icon badge per
   group (reusing the existing `.icon-badge` shape + the sitewide
   `.icon-tint-*` hues already used on the dashboard's feature cards) gives
   each shelf a visual anchor and lets the three read as distinct sections
   rather than one long undifferentiated list. */
.bookmark-group-head { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
.bookmark-group-head h3 { font-family: var(--font-display); margin: 0; font-size: 1.1rem; }

/* ---- My Bookmarks list rows (2026-08-30, replaced plain <ul style="float:right">) ---- */
.bookmark-list-card { padding: 6px 0; margin-bottom: 32px; }
.bookmark-list { list-style: none; margin: 0; padding: 0; }
.bookmark-list-item {
  display: flex; align-items: center; gap: 12px; padding: 14px 24px;
  border-bottom: 1px solid var(--ink-100); transition: background-color .15s ease;
}
.bookmark-list-item:last-child { border-bottom: none; }
.bookmark-list-item:hover { background: var(--surface-alt); }
.bookmark-item-icon { color: var(--brand-500); font-size: 1rem; flex-shrink: 0; }
.bookmark-item-link { flex: 1 1 auto; min-width: 0; font-weight: 600; color: var(--ink-900); text-decoration: none; }
.bookmark-item-link:hover { color: var(--brand-600); text-decoration: underline; }
.bookmark-item-meta { flex-shrink: 0; font-size: .8rem; color: var(--ink-500); }
@media (max-width: 576px) {
  .bookmark-list-item { flex-wrap: wrap; padding: 12px 18px; }
  .bookmark-item-meta { margin-left: 26px; }
}

/* sms_credit.html: give the result card the same subtle top-accent
   treatment used elsewhere on staff/account surfaces, so the number
   doesn't sit in a totally plain white box against the new `--surface-alt`
   section behind it. */
.sms-credit-card { border-top: 3px solid var(--brand-500); }

/* notice_detail.html: the hero band now carries the title/meta (via the
   shared `.page-hero.catalog-hero`); nudge its badge up against the kicker
   above and the title below instead of the default block spacing a bare
   `.notice-type-badge` gets when dropped into a `.page-hero`. */
.notice-detail-hero .notice-type-badge { margin: 4px 0 2px; }

/* ---------------------------------------------------------------------
   about.html / contact.html visual-redesign pass (2026-08-30) - scoped
   additions only, no shared component base rules (.card/.btn/.badge)
   touched here.
   --------------------------------------------------------------------- */

/* about.html hero illustration - the about page had zero decorative art
   despite being the site's "who we are" page; reuses the exact .illo-*
   line-art vocabulary already established by the homepage hero/dashboard
   header (static/css/main.css's earlier .illo-* rules) rather than a new
   visual language, so it reads as part of the same site, not a bolt-on. */
.about-hero-illo { max-width: 220px; margin: 28px auto 0; opacity: .92; }
.about-hero-illo svg { width: 100%; height: auto; }

/* contact.html: the info cards (Email/Phone/Address/Follow) previously had
   the exact same static elevation as every other .card on the site - a
   small hover lift on JUST this page's info cards (not a global .card
   change, which is out of this pass's scope) makes the contact-info list
   feel a little more like something you can act on. */
.contact-card, .col-lg-5 .card { transition: transform .18s ease, box-shadow .18s ease; }
.col-lg-5 .card:hover { transform: translateY(-2px); box-shadow: var(--shadow-lg); }

/* ==========================================================================
   Checkout & payment flow polish (2026-08-30)
   templates/finance/{add_to_cart,cart,checkout,my_payments,
   respond_reverification}.html - scoped additions only, the shared
   .card/.btn/.badge/.form-control base rules are untouched. Cart/checkout
   already had strong bones (step indicator, QR/method select cards, upload
   dropzone, order summary sidebar); this section fills in the two weaker
   pages in the funnel (the bare add-to-cart form, and an under-styled
   re-upload flow) plus a few consistency touches across the rest.
   ========================================================================== */

/* add_to_cart.html: turn each form section into its own visually distinct
   step instead of one undifferentiated stack of label+input pairs - a
   small numbered badge + heading per group, same "make the funnel's steps
   legible" goal the existing `.checkout-steps` bar serves one level up. */
.cart-form-step { display: flex; gap: 14px; padding: 18px 0; border-bottom: 1px solid var(--ink-100); }
.cart-form-step:last-of-type { border-bottom: none; padding-bottom: 4px; }
.cart-form-step-num {
  width: 30px; height: 30px; border-radius: 999px; background: var(--brand-50); color: var(--brand-600);
  font-weight: 700; font-size: .9rem; display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.cart-form-step-body { flex: 1 1 auto; min-width: 0; }
.cart-form-step-body .form-label { font-weight: 600; color: var(--ink-900); }

/* respond_reverification.html: the rejection reason used to be a plain
   <p class="sub"> under the title - easy to skim past on the exact page
   where reading it carefully matters most. A dedicated, warm (not alarming
   red) callout makes it the visual anchor of the page, with reassuring
   framing copy alongside it instead of just the raw admin note. */
.reverify-reason-card {
  display: flex; gap: 14px; align-items: flex-start; background: #fffbeb; border: 1px solid #fde68a;
  border-radius: var(--radius-md); padding: 16px 18px; margin-bottom: 24px; text-align: left;
}
.reverify-reason-icon {
  width: 38px; height: 38px; border-radius: 999px; background: #fef3c7; color: #92400e;
  display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-size: 1.05rem;
}
.reverify-reason-label { font-size: .78rem; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; color: #92400e; margin: 0 0 3px; }
.reverify-reason-text { margin: 0; color: var(--ink-700); font-size: .92rem; }

/* my_payments.html: a small subject-type icon per order card head, echoing
   the same "icon anchors the section" treatment used on
   .bookmark-group-head above, so a list of several orders scans faster
   than a wall of identical text headers. */
.order-card-icon {
  width: 34px; height: 34px; border-radius: 10px; background: var(--brand-50); color: var(--brand-600);
  display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-size: .95rem;
}
