/* ------------------------------------------------------------------
   Color system: chrome is neutral ink on near-black. Every colored
   element derives from ONE variable — --h, a hue angle on the same
   wheel domain coloring uses (hue = arg f(z), the convention the hero
   canvas and every visualization follow). Each lesson/viz page sets
   its own --h (from lessons.json / visualizations.json); the accent
   is computed from it. No colors picked off a palette.
   Surface/border values are lifted from the visualizations' own CSS
   (--panel #11141d, --grid #232838, --axis #3a4055) so page chrome
   and embedded interactives read as one material.
------------------------------------------------------------------- */

:root {
  --bg: #0a0c12;
  /* raw triplet, for rgba(var(--bg-rgb), alpha) — rgba() can't take a hex
     custom property directly, only comma-separated channel values */
  --bg-rgb: 10, 12, 18;
  --surface: #11141d;
  --surface-2: #161a26;
  --text: #e8ecf3;
  --muted: #97a1b6;
  --h: 215; /* default page hue; lesson & viz pages override via frontmatter */
  --border: #232838;
  --border-strong: #3a4055;
  --radius: 6px;
  --font-display: "Newsreader", Georgia, serif;
  --font-body: "Source Serif 4", Georgia, "Times New Roman", serif;
  --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}

/* Two accents, deliberately scoped differently — chrome vs signal.

   --accent is CHROME: links, buttons, focus rings. It is pinned to hue 215 and
   sits off the per-lesson wheel on purpose. Link color is a learned affordance,
   and letting it swing to Lesson 1's yellow or Lesson 7's green costs more in
   legibility than it buys in theme. Note it is written as a literal 215, not as
   var(--h): chrome deliberately does not track the page hue, and spelling that
   out stops the next reader assuming it does.

   --hue-accent is SIGNAL: the card stripes, i.e. the site-wide echo of domain
   coloring. This one does follow --h.

   Scoping matters here and is easy to get wrong. A custom property substitutes
   its var() references at the element where it is DECLARED, not where it is
   used — so a --hue-accent declared only on :root would bake in :root's own --h
   (215) and inherit that single color everywhere, which is exactly the trap the
   old --accent fell into: it read as per-lesson but rendered 215-blue on every
   page. So --hue-accent is declared on every element that sets --h: <body>
   (page hue, base.njk), both card types (per-card hue), and .viz-page-header
   (viz-pages.njk and gerald.njk). ANYTHING NEW THAT SETS --h MUST BE ADDED TO
   BOTH --hue-accent SELECTOR LISTS BELOW. */
:root {
  --accent: hsl(215 70% 70%);
  --accent-dim: hsl(215 55% 62% / 0.5);
  /* .btn-primary is the only consumer, and .btn-primary only ever renders in
     hero.njk, which only index.njk includes — a page with no hue frontmatter,
     so --h is always 215 there. It was written as hsl(var(--h) 75% 76%), which
     LOOKS like it belongs to the per-lesson signal family but has in fact
     always evaluated to exactly this literal. Named as chrome, where it
     actually belongs, not as a --hue-* token: if hero.njk is ever reused on a
     page with a different --h, this correctly stays put instead of silently
     drifting off the pinned chrome hue the way the old --accent did (see the
     comment above). No OKLCH variant, unlike --accent -- OKLCH earns its
     place when one formula has to hold steady across many hues; this token
     only ever represents one, so there's no spread here to flatten. */
  --accent-hover: hsl(215 75% 76%);
}

:root,
body,
.lesson-card,
.viz-card,
.viz-page-header {
  --hue-accent: hsl(var(--h) 70% 70%);

  /* The rest of the per-lesson signal family. Each of these was a bare
     hsl(var(--h) ...) literal repeated at its use sites with no name — eight
     distinct lightness/saturation pairs across the file, discoverable only by
     grepping. Naming them doesn't change a single rendered pixel: every value
     below is copied verbatim from where it used to live inline, not
     recomputed. They stay plain HSL rather than following --hue-accent
     through the OKLCH conversion above, for the same reason: this pass is a
     rename, not a recalculation. Declared on this same selector list, not
     :root alone, for the reason explained above — anything that resolves
     var(--h) at its declaration site inherits that trap. */
  --hue-border-hover: hsl(var(--h) 60% 55% / 0.7);  /* card hover border (viz-card, lesson-card) */
  --hue-cta: hsl(var(--h) 85% 72%);            /* viz/lesson card CTA + arrow text */
  --hue-label: hsl(var(--h) 85% 74%);          /* badges, kickers, step tags — the most-reused value */
  --hue-label-border: hsl(var(--h) 60% 60% / 0.4); /* badge border (lesson-num, viz-card-lesson) */
  --hue-rule: hsl(var(--h) 70% 62% / 0.8);     /* blockquote / proof / scrolly-step border-left */
  --hue-dot-border: hsl(var(--h) 70% 62%);     /* lesson-side tracker dot, opaque */
  --hue-dot-fill: hsl(var(--h) 70% 62% / 0.55);   /* lesson-side tracker dot, filled state */
}

/* HSL lightness is not perceptually uniform, so one L% renders at very
   different brightness per hue: hsl(var(--h) 70% 70%) runs 5.56:1 (hue 240,
   Lesson 6) to 14.24:1 (hue 55, Lesson 1) against --bg — a 2.56x spread from
   identical CSS, which made Lesson 1's stripe visibly heavier than Lesson 6's.
   OKLCH's lightness axis IS perceptually uniform, so pinning it holds every
   lesson's signal at one apparent weight: 8.64-9.55:1, a 1.11x spread, the
   residue being WCAG's own green-weighted luminance formula rather than the
   color system. Chrome is pinned to the same lightness so the two read as one
   material.

   --h is an HSL hue angle — the domain-coloring convention the hero canvas
   and every visualization share — and oklch()'s hue axis is a different
   wheel, so --h can't be handed to oklch() directly. Relative color syntax
   does the conversion: oklch(from <hsl color> L C h) re-expresses that exact
   hue at a fixed lightness. Chroma stays under the sRGB gamut ceiling at
   this lightness (0.1249, set by blue) — clipping would gamut-map lightness
   back per hue and silently reintroduce the spread this removes.

   Guarded by @supports because a custom property holding an unparseable
   value stays valid until substitution, so the usual two-declaration
   fallback does not work here. Browsers without relative color syntax keep
   the hsl() values above. */
@supports (color: oklch(from red l c h)) {
  :root {
    --accent: oklch(from hsl(215 70% 70%) 0.76 0.115 h);
    --accent-dim: oklch(from hsl(215 55% 62%) 0.68 0.08 h / 0.5);
  }

  :root,
  body,
  .lesson-card,
  .viz-card,
  .viz-page-header {
    --hue-accent: oklch(from hsl(var(--h) 70% 70%) 0.76 0.115 h);
  }
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 1.0625rem;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 500;
  letter-spacing: 0.005em;
  line-height: 1.15;
  color: var(--text);
  /* Defensive, not a fix for an active bug: at 320px, current titles like
     "Cauchy-Riemann" already break at the hyphen and "Indistinguishability"
     (the longest word on the site, 20 chars, no hyphen) still fits its box
     with margin to spare. But Lessons 9-12 don't have titles yet, and
     nothing here guarantees the next long compound word has a hyphen to
     break on. break-word only takes over as a last resort, after normal
     space/hyphen breaks are exhausted, so it costs nothing on every title
     that already fits. */
  overflow-wrap: break-word;
}

a {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-color: var(--accent-dim);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

a:hover { text-decoration-color: var(--accent); }
a:active { color: var(--accent-dim); }

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

/* KaTeX sits inside serif prose now; pull its default 1.21em scale in
   so Computer Modern's x-height matches Source Serif's */
.katex { font-size: 1.08em; }

/* Long display equations must scroll inside their own box rather than
   pushing the page sideways (README "Interactive patterns" convention:
   wide content scrolls in its own container, the body never scrolls
   horizontally). */
.katex-display {
  overflow-x: auto;
  overflow-y: hidden;
  max-width: 100%;
}

/* ---------- Nav ---------- */

.site-nav {
  position: sticky;
  top: 0;
  z-index: 50;
  background: rgba(var(--bg-rgb), 0.96);
  border-bottom: 1px solid var(--border);
}

.nav-inner {
  max-width: 1100px;
  margin: 0 auto;
  padding: 0.75rem 1.25rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.brand {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  color: var(--text);
  text-decoration: none;
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 1.1rem;
}

/* The brand mark is a literal color wheel: hue = arg z on the unit
   disk — domain coloring of f(z) = z, the site's first picture */
.brand-mark {
  display: grid;
  place-items: center;
  width: 1.9rem;
  height: 1.9rem;
  border-radius: 50%;
  background: conic-gradient(from 90deg,
    hsl(0 65% 52%), hsl(60 65% 45%), hsl(120 65% 42%),
    hsl(180 65% 45%), hsl(240 65% 60%), hsl(300 65% 52%), hsl(360 65% 52%));
  color: var(--bg);
  font-weight: 700;
  font-size: 1rem;
}

.nav-links { display: flex; gap: 1.4rem; flex-wrap: wrap; justify-content: flex-end; }

.nav-link {
  color: var(--muted);
  text-decoration: none;
  font-family: var(--font-mono);
  font-size: 0.82rem;
  letter-spacing: 0.01em;
  transition: color 0.15s ease;
}

.nav-link:hover { color: var(--text); }
.nav-link:active { color: var(--accent); }

/* ---------- Hero ---------- */

.hero {
  position: relative;
  overflow: hidden;
  border-bottom: 1px solid var(--border);
}

#hero-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0.9;
  filter: saturate(0.85);
}

.hero-veil {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(70% 90% at 50% 38%, transparent, rgba(var(--bg-rgb), 0.55)),
    linear-gradient(180deg, rgba(var(--bg-rgb), 0.55), rgba(var(--bg-rgb), 0.3) 45%, var(--bg));
}

/* Left column, not a centred stack. The old hero put eyebrow, title, tagline,
   CTAs and caption on one centred axis while every section below it is flush
   left — the hero was the only centred thing on the page. Width and padding
   match main.wide (1100px + 1.25rem), which the homepage uses, so the hero's
   left edge lands exactly on the section headings beneath it.
   Padding is bottom-heavy on purpose: a hero with equal or top-heavy padding
   floats above the page instead of settling into the next section. */
.hero-inner {
  position: relative;
  max-width: 1100px;
  margin: 0 auto;
  padding: 5rem 1.25rem 6.5rem;
  text-align: left;
}

.hero-eyebrow, .section-eyebrow {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 400;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 0.25rem;
}

.hero h1 {
  font-size: clamp(2.9rem, 7vw, 5rem);
  /* Was italic. Decided 2026-08-01, by looking at it both ways: display
     italic on a masthead is one of the most reliable AI tells there is, and
     that argument didn't get weaker for being reversible. Newsreader's
     roman cut carries the headline fine on its own. */
  font-weight: 500;
  margin: 0.4rem 0 1.1rem;
  text-shadow: 0 2px 24px rgba(var(--bg-rgb), 0.8);
}

.hero-tagline {
  font-size: clamp(1.05rem, 2vw, 1.2rem);
  color: var(--text);
  max-width: 46ch;
  margin: 0 0 2.2rem;
  text-shadow: 0 1px 12px rgba(var(--bg-rgb), 0.9);
}

.hero-ctas {
  display: flex;
  gap: 0.9rem;
  justify-content: flex-start;
  flex-wrap: wrap;
}

/* The caption annotates the canvas rather than continuing the masthead, so it
   sits in the opposite corner. That single asymmetry is what keeps the hero
   from reading as a generic left-aligned stack, and it leaves the middle of
   the fold showing the domain coloring — which is the subject of the site and
   the thing Lesson 0 teaches you to read, not empty space. */
.hero-caption {
  margin: 3rem 0 0 auto;
  max-width: 54ch;
  text-align: right;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  line-height: 1.6;
  color: var(--muted);
}

/* On a phone the hero is one narrow column and the diagonal has no room to
   read as composition, so the caption returns to the text's own edge instead
   of sitting ragged-left against it. */
@media (max-width: 640px) {
  .hero-caption {
    margin-left: 0;
    text-align: left;
  }
}

/* ---------- Buttons ---------- */

.btn {
  display: inline-block;
  padding: 0.7rem 1.3rem;
  border-radius: 4px;
  font-family: var(--font-mono);
  font-size: 0.85rem;
  letter-spacing: 0.02em;
  text-decoration: none;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

/* Shared press cue for both button variants. Deliberately doesn't redeclare
   background/border here: a pointer press keeps :hover true at the same time
   as :active, so the hover fill already carries over via the cascade for
   mouse users; this rule only adds the physical "sinks in" cue, which is what
   keyboard/touch activation (hover never true) still needs on its own. */
.btn:active { transform: translateY(1px); }

.btn-primary {
  background: var(--accent);
  border: 1px solid var(--accent);
  color: var(--bg);
  font-weight: 600;
}

.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }

.btn-ghost {
  border: 1px solid var(--border-strong);
  color: var(--text);
  background: rgba(var(--bg-rgb), 0.55);
}

.btn-ghost:hover { border-color: var(--accent); color: var(--accent); }

/* ---------- Layout ---------- */

main {
  max-width: 760px;
  margin: 0 auto;
  padding: 2.5rem 1.25rem;
}

main.wide { max-width: 1100px; }

.home-section { margin: 3.5rem 0; }
.home-section:first-child { margin-top: 1.5rem; }

.home-section h2, .section-head h2 {
  font-size: clamp(1.6rem, 3vw, 2.1rem);
  margin: 0 0 0.75rem;
}

.prose p { max-width: 68ch; }

.section-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}

.see-all {
  font-family: var(--font-mono);
  font-size: 0.82rem;
  text-decoration: none;
  white-space: nowrap;
  padding-bottom: 0.35rem;
}

.see-all:hover { text-decoration: underline; }
/* Color on press comes free from the global a:active rule above (higher
   specificity, no conflicting declaration here). Underline still needs its
   own :active — touch/keyboard activation never makes :hover true, so
   without this the underline would only ever show for mouse users. */
.see-all:active { text-decoration: underline; }

.muted { color: var(--muted); }

.page-head { max-width: 760px; }

.page-head h1 { font-size: clamp(2.2rem, 5vw, 3.2rem); margin: 0.3rem 0 0.9rem; }

.lede { color: var(--muted); font-size: 1.08rem; }

/* ---------- Visualization cards ---------- */

.viz-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
  gap: 1.1rem;
  margin-top: 1.6rem;
}

.viz-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding: 1.5rem 1.4rem 1.3rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  text-decoration: none;
  color: var(--text);
  overflow: hidden;
  transition: border-color 0.15s ease, background 0.15s ease;
}

/* Stripe keyed to each viz's hue — the site-wide echo of domain coloring.
   Flat --hue-accent, not a gradient: the old hue-to-hue+50 ramp read as the
   generic AI-site gradient rather than as this site's hue system (at the
   default --h it ran blue to violet), and a single hue states the
   one-lesson-one-hue idea more plainly than a two-stop sweep. */
.viz-card::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: var(--hue-accent);
  opacity: 0.85;
}

.viz-card:hover {
  background: var(--surface-2);
  border-color: var(--hue-border-hover);
}

.viz-card:active { transform: translateY(1px); }

.viz-card h2 {
  font-size: 1.2rem;
  margin: 0;
}

.viz-card p {
  color: var(--muted);
  font-size: 0.92rem;
  margin: 0;
  flex: 1;
}

.viz-card-cta {
  font-family: var(--font-mono);
  color: var(--hue-cta);
  font-size: 0.8rem;
}

.viz-card-lesson {
  align-self: flex-start;
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--hue-label);
  border: 1px solid var(--hue-label-border);
  border-radius: 3px;
  padding: 0.12rem 0.5rem;
  vertical-align: middle;
}

/* ---------- Individual visualization pages ---------- */

.viz-page-header { max-width: 860px; }

.viz-page-header .crumb { margin: 0 0 1rem; }

.viz-page-header .crumb a {
  color: var(--muted);
  text-decoration: none;
  font-family: var(--font-mono);
  font-size: 0.8rem;
}

.viz-page-header .crumb a:hover { color: var(--text); }
/* Needs its own rule rather than inheriting the generic a:active: this
   selector's higher specificity (two classes + a type) already beats the
   generic one at rest, so it would also beat it here. */
.viz-page-header .crumb a:active { color: var(--accent); }

.viz-page-header h1 {
  font-size: clamp(1.8rem, 4vw, 2.6rem);
  margin: 0 0 0.5rem;
}

.viz-page-description {
  color: var(--muted);
  margin: 0.25rem 0 0;
  max-width: 68ch;
}

.viz-page-footer {
  text-align: right;
  font-family: var(--font-mono);
  font-size: 0.82rem;
}

.viz-embed {
  width: 100%;
  height: 620px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin: 1.5rem 0;
  background: var(--bg);
}

.viz-embed-full {
  height: calc(100vh - 150px);
  min-height: 620px;
  margin: 1.4rem 0 0.6rem;
}

/* Breaks a wide embed (e.g. a side-by-side stage+controls explorer) out of
   the 760px prose column, matching the width .scrolly sections use — some
   visualizations genuinely need more than 760px to lay their panels out
   without wrapping/clipping. */
.viz-embed-wide-wrap {
  width: min(1080px, calc(100vw - 2.5rem));
  margin: 1.5rem 0 1.5rem calc((100% - min(1080px, 100vw - 2.5rem)) / 2);
}
.viz-embed-wide-wrap .viz-embed { margin: 0; height: 640px; }

/* ---------- Lessons & content ---------- */

/* Lesson bodies render as flat children of <main> — markdown, no .prose
   wrapper — so the 68ch cap that .prose p sets on the homepage never reached
   them. Measured in a browser at 1280x800: 720px of column = 82.3ch, running
   86 characters per line (median; 92 max), well past the 45-75ch the eye
   tracks comfortably. 68ch matches what .prose p already declares, so the two
   page types now agree instead of disagreeing by 14ch.
   Deliberately not applied to headings (display type wants the width), nor to
   .scrolly / .lesson-side / .lesson-pager, which sit outside the text column
   or break out wider on purpose. */
main > p,
main > ul,
main > ol,
main > blockquote {
  max-width: 68ch;
}

/* Display equations keep the full column rather than the prose measure —
   narrow text, wider math, the usual typesetting split. Capping them with the
   prose would push 4 of the site's 78 display equations into horizontal
   scroll (Lesson 6 x3, Lesson 5 x1) where only Lesson 5's already scrolls.
   720px is main's content box: its 760px max-width less 2.5rem of padding.
   Descendant selector, not child: KaTeX's auto-render wraps its output in a
   <span>, so .katex-display is a grandchild of the paragraph, not a child. */
main > p .katex-display {
  width: min(720px, 100vw - 2.5rem);
  /* releases the max-width:100% set above, which would otherwise clamp the
     equation back to the narrowed paragraph; the width above is already
     viewport-bounded, so nothing can push the page sideways */
  max-width: none;
}

.lesson-list {
  list-style: none;
  padding: 0;
}

.lesson-list li {
  margin: 0.75rem 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  transition: border-color 0.15s ease;
}

.lesson-list li:hover { border-color: var(--border-strong); }
/* :active on the li, not the a: the li is what visibly changes, and CSS
   applies :active to an activated element's ancestors too, so this correctly
   fires while the .lesson-list a filling it is being pressed. */
.lesson-list li:active { border-color: var(--accent); }

.lesson-list a {
  display: block;
  padding: 0.9rem 1.15rem;
  font-weight: 600;
  text-decoration: none;
}

/* Key-takeaway blocks: a rule in the lesson's hue, no panel fill */
blockquote {
  border-left: 2px solid var(--hue-rule);
  margin: 1.5rem 0;
  padding: 0.15rem 0 0.15rem 1.15rem;
  color: var(--muted);
}

blockquote strong, blockquote b { color: var(--text); }

/* ---------- Lesson path cards ---------- */

.lesson-path {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
  margin-top: 1.6rem;
}

.lesson-card {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 1.1rem;
  padding: 1.2rem 1.4rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  text-decoration: none;
  color: var(--text);
  overflow: hidden;
  transition: border-color 0.15s ease, background 0.15s ease;
}

/* No hue stripe here on purpose (mockup, 2026-08-22): a per-lesson hue was
   the thing that read as templated when scrolling the list. Border color is
   a plain neutral now; the lesson-glyph span below is the differentiator. */
.lesson-card::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 3px;
  background: var(--border);
}

.lesson-card:hover {
  background: var(--surface-2);
  border-color: var(--text);
}

.lesson-card:active { transform: translateY(1px); }

.lesson-num {
  flex: none;
  display: grid;
  place-items: center;
  width: 2.4rem;
  height: 2.4rem;
  margin-top: 0.1rem;
  border-radius: 4px;
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: 1rem;
  color: var(--text);
  border: 1px solid var(--border);
}

/* Topic-family glyph — replaces per-lesson hue as the visual differentiator.
   Same glyph across lessons in the same arc (e.g. the two Sigma lessons are
   both power-series lessons); grounded in the actual math, not decoration. */
.lesson-glyph {
  flex: none;
  display: grid;
  place-items: center;
  width: 2.4rem;
  height: 2.4rem;
  margin-top: 0.1rem;
  font-family: var(--font-display);
  font-size: 1.3rem;
  color: var(--muted);
}

.lesson-card-title {
  display: block;
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 1.15rem;
  line-height: 1.3;
}

.lesson-card-desc {
  display: block;
  margin-top: 0.3rem;
  color: var(--muted);
  font-size: 0.92rem;
}

.lesson-card-arrow {
  margin-left: auto;
  align-self: center;
  color: var(--muted);
  font-family: var(--font-mono);
}

.lesson-path-note { margin-top: 1.4rem; font-size: 0.92rem; }

/* ---------- Secondary aside cards (homepage) ---------- */

.aside-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1.1rem;
}

.aside-card {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  padding: 1.4rem 1.4rem 1.2rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  text-decoration: none;
  color: var(--text);
  transition: border-color 0.15s ease, background 0.15s ease;
}

.aside-card:hover {
  background: var(--surface-2);
  border-color: var(--border-strong);
}

.aside-card:active { transform: translateY(1px); }

.aside-card h2 {
  font-size: 1.3rem;
  margin: 0;
}

.aside-card p {
  color: var(--muted);
  font-size: 0.92rem;
  margin: 0;
  flex: 1;
}

.aside-card .section-eyebrow { margin: 0; }

.aside-cta {
  margin-top: 0.7rem;
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: 0.8rem;
}

/* ---------- Lesson sidebar ---------- */

/* A lesson-page port of the sticky card column the FTA chain viz uses
   (<aside class="side">): a small "track" card plus link-out cards. On
   wide screens it rides the right gutter next to the 760px prose column;
   below that it falls into the page flow as ordinary cards. */

.lesson-side {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
  margin: 1.8rem 0;
}

.lesson-side .side-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1rem 1.1rem;
  font-size: 0.9rem;
  line-height: 1.55;
}

.lesson-side .side-card h3 {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-weight: 400;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--hue-label);
  margin: 0 0 0.6rem;
}

.lesson-side .side-node {
  display: flex;
  gap: 0.6rem;
  align-items: baseline;
  padding: 0.22rem 0;
}

.lesson-side .side-node .dot {
  flex: none;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  border: 1.5px solid var(--border-strong);
  background: transparent;
  transform: translateY(-1px);
}

.lesson-side .side-node.done .dot {
  border-color: var(--hue-dot-border);
  background: var(--hue-dot-fill);
}

.lesson-side .side-node .lbl { color: var(--text); }
.lesson-side .side-node .sub { display: block; font-size: 0.78rem; color: var(--muted); }

.lesson-side .side-card p { margin: 0 0 0.5rem; color: var(--muted); }
.lesson-side .side-card p:last-child { margin-bottom: 0; }

/* "On this page" tracker card (built client-side by js/page-tracker.js).
   Link nodes reuse the side-node/dot language; dots fill as sections are
   reached, and the current section's label lifts to full text color. */

.lesson-side a.side-node { text-decoration: none; }
.lesson-side a.side-node .lbl { color: var(--muted); transition: color 0.25s; }
.lesson-side a.side-node:hover .lbl,
.lesson-side a.side-node.now .lbl { color: var(--text); }
/* Separate rule, not folded into the list above: :active reads as a more
   emphatic accent rather than reusing --text, which :hover and .now share. */
.lesson-side a.side-node:active .lbl { color: var(--accent); }
.lesson-side .tracker-card .side-node { padding: 0.18rem 0; }

@media (min-width: 1380px) {
  .lesson-side {
    position: fixed;
    top: 5.5rem;
    right: calc((100vw - 760px) / 2 - 296px);
    width: 268px;
    margin: 0;
    max-height: calc(100vh - 7rem);
    overflow-y: auto;
    transition: opacity 0.2s ease;
  }

  /* A pinned scrolly figure and a standalone wide viz-embed (.viz-embed-
     wide-wrap) both break out to a wider column than the prose and can
     occupy the same screen region as this fixed sidebar — hide it for as
     long as either kind is on screen (see scrolly.js, which despite the
     name now watches both). */
  body.wide-content-active .lesson-side {
    opacity: 0;
    pointer-events: none;
  }
}

/* ---------- Scroll-driven viz sections ---------- */

/* The figure pins via position:sticky; JS only swaps which step is
   .is-active and posts its state into the iframe. With scripting off,
   GSAP unavailable, or prefers-reduced-motion, .scrolly-static collapses
   the whole thing to a normal figure + prose (no pin, no dimming). */

.scrolly {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
  gap: 2rem;
  align-items: start;
  margin: 2.5rem 0;
  /* Break out of the 760px prose column: the pinned figure needs room
     for the visualization's two panels to sit side by side. (Margins, not
     transform — a transformed ancestor would break position:sticky.) */
  width: min(1080px, calc(100vw - 2.5rem));
  margin-left: calc((100% - min(1080px, 100vw - 2.5rem)) / 2);
}

.scrolly-figure {
  order: 2;
  position: sticky;
  top: max(4rem, calc(50vh - 310px));
}

.scrolly-figure iframe {
  width: 100%;
  height: min(82vh, 600px);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
}

.scrolly-steps {
  order: 1;
  display: flex;
  flex-direction: column;
  gap: 50vh;
  padding: 14vh 0 26vh;
}

.scrolly-step {
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 2px solid var(--hue-rule);
  border-radius: var(--radius);
  padding: 1rem 1.2rem;
  font-size: 0.98rem;
  /* Inactive steps recede, but they are still body text and still have to be
     readable: 0.35 blended --text over --surface computed to 2.90:1, under the
     4.5:1 AA floor. 0.489 is the exact break-even; 0.55 clears it at 5.37:1
     while keeping as much of the active/inactive separation as the dimming
     affordance needs. (Active steps are unchanged at 15.52:1.) */
  opacity: 0.55;
  transition: opacity 0.3s ease;
}

.scrolly-step.is-active { opacity: 1; }

.scrolly-step p { margin: 0.4rem 0 0; }

.step-tag {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--hue-label);
}

.scrolly-note {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  color: var(--muted);
  margin: 0.5rem 0 0;
}

/* Static fallback: reduced motion, JS off, or GSAP unreachable */
.scrolly-static { display: block; }
.scrolly-static .scrolly-figure { position: static; }
.scrolly-static .scrolly-steps { padding: 0; gap: 1rem; margin-top: 1.5rem; }
.scrolly-static .scrolly-step { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .scrolly-step { opacity: 1; transition: none; }
}

@media (max-width: 860px) {
  .scrolly { display: block; }
  .scrolly-figure {
    position: sticky;
    top: 3.4rem;
    z-index: 5;
    order: 0;
  }
}

/* Below ~640px the nav wraps to multiple rows; un-stick it so it stops
   eating viewport that the pinned figure needs */
@media (max-width: 640px) {
  .site-nav { position: static; }
  .scrolly-figure { top: 0.5rem; }
  .scrolly-figure iframe { height: 46vh; min-height: 340px; }
  .scrolly-steps { padding: 8vh 0 18vh; gap: 42vh; }
  .scrolly-static .scrolly-figure { position: static; }
}

/* ---------- Proof stepper ---------- */

/* Rendered by the proofStepper shortcode. Classes pf-<tag> are reserved
   for tagged terms: each tag gets --th (a hue offset from the page hue,
   set by the shortcode's scoped <style>) and keeps it in every step —
   the continuity of a quantity through the argument is the color. */

.proof {
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 2px solid var(--hue-rule);
  border-radius: var(--radius);
  padding: 1.4rem 1.5rem 1.2rem;
  margin: 2rem 0;
}

.proof-kicker {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--hue-label);
  margin: 0 0 0.6rem;
}

.proof-statement { margin: 0 0 1.1rem; }

.proof-legend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 0.6rem;
}

.proof-legend-label {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--muted);
}

.proof-chip {
  font-family: var(--font-body);
  font-size: 0.82rem;
  background: none;
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0.2rem 0.55rem;
  color: var(--text);
  cursor: default;
}

.proof-controls {
  display: flex;
  gap: 0.5rem;
  justify-content: flex-end;
  margin: 0 0 0.3rem;
}

.proof-btn {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  background: none;
  border: 1px solid var(--border-strong);
  border-radius: 4px;
  color: var(--muted);
  padding: 0.3rem 0.6rem;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}

.proof-btn:hover { color: var(--text); border-color: var(--accent); }
.proof-btn:active { color: var(--accent); border-color: var(--accent); transform: translateY(1px); }

.proof-steps { list-style: none; margin: 0.4rem 0 0; padding: 0; }

.proof-step {
  padding: 1rem 0 0.5rem;
  border-top: 1px solid var(--border);
  outline: none;
}

.proof-step-tag {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}

/* The payoff step of a proof — amber, deliberately outside the page-hue
   system, matching the .step.aha convention in the chain visualization */
.proof-step-aha {
  border-left: 3px solid hsl(42 80% 62% / 0.9);
  background: hsl(42 80% 62% / 0.06);
  border-radius: 0 var(--radius) var(--radius) 0;
  padding-left: 1.1rem;
  padding-right: 0.9rem;
  margin-top: 0.6rem;
}

.proof-step-aha-tag {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.65rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: hsl(42 90% 68%);
  margin-bottom: 0.2rem;
}

.proof-animated .proof-step-aha.is-visible { animation: proofAhaGlow 1.6s ease; }

@keyframes proofAhaGlow {
  0% { box-shadow: 0 0 0 0 hsl(42 90% 60% / 0.45); }
  70% { box-shadow: 0 0 0 10px transparent; }
  100% { box-shadow: none; }
}

.proof-step-math { margin: 0.3rem 0; overflow-x: auto; }

.proof-step-note {
  margin: 0.2rem 0 0.5rem;
  color: var(--muted);
  font-size: 0.95rem;
  max-width: 62ch;
}

/* Tagged-term highlight (KaTeX \htmlClass spans, note spans, legend chips) */
.proof [class^="pf-"], .proof [class*=" pf-"] {
  color: hsl(var(--th, var(--h)) 88% 76%);
  background: hsl(var(--th, var(--h)) 70% 58% / 0.12);
  border-radius: 3px;
  padding: 0.05em 0.18em;
  transition: background 0.2s ease;
}

.proof .pf-hot { background: hsl(var(--th, var(--h)) 70% 58% / 0.32); }

/* Click-to-reveal — armed by JS only, so no-JS readers see everything.
   .proof-interactive/.is-shown control display (which steps exist yet);
   .proof-animated/.is-visible control the fade/slide (skipped under
   reduced motion, where steps simply appear). Reveals are additive:
   the stepper never re-hides a step. */
.proof-interactive .proof-step { display: none; }

.proof-interactive .proof-step.is-shown { display: list-item; }

.proof-animated .proof-step {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.55s ease, transform 0.55s ease;
}

.proof-animated .proof-step.is-visible { opacity: 1; transform: none; }

.proof-progress {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--muted);
  align-self: center;
}

.proof-btn:disabled {
  opacity: 0.4;
  cursor: default;
}

.proof-btn:disabled:hover { color: var(--muted); border-color: var(--border-strong); }

@media (prefers-reduced-motion: reduce) {
  .proof-animated .proof-step { opacity: 1; transform: none; transition: none; }
  .proof-animated .proof-step-aha.is-visible { animation: none; }
}

/* ---------- Lesson pager ---------- */

.lesson-pager {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  max-width: 860px;
  margin: 3rem auto 0;
  padding-top: 1.5rem;
  border-top: 1px solid var(--border);
  font-size: 0.95rem;
}

.lesson-pager-link { color: var(--muted); text-decoration: none; }
.lesson-pager-link:hover { color: var(--accent); }
.lesson-pager-next { margin-left: auto; text-align: right; }
.lesson-pager-sep { color: var(--border); }

/* ---------- Footer ---------- */

footer {
  max-width: 1100px;
  margin: 3rem auto 0;
  padding: 1.5rem 1.25rem 2rem;
  color: var(--muted);
  font-size: 0.9rem;
  border-top: 1px solid var(--border);
  text-align: center;
}

/* ---------- Small screens ---------- */

@media (max-width: 480px) {
  .hero-inner { padding: 3.5rem 1.25rem 4.5rem; }
  .nav-links { gap: 1rem; }
  .brand { font-size: 0.95rem; white-space: nowrap; gap: 0.45rem; }
  .brand-mark { width: 1.6rem; height: 1.6rem; font-size: 0.85rem; }
  .nav-link { font-size: 0.78rem; }
  .viz-embed-full { height: 78vh; min-height: 480px; }
}

/* ---------- Notebook flip-through (how-i-built-this) ---------- */

.notebook-figure {
  margin: 1.6rem 0;
}

.notebook-video {
  display: block;
  width: 100%;
  max-width: 420px;
  margin: 0 auto;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
}
