/* ==========================================================================
   Kara Piano Lessons — rebuilt 1:1 from the Framer project "Fluffy Area".
   Structure, copy, spacing and colour values were read directly off the
   live Framer node tree (padding/gap/radius/fills/text presets), not
   eyeballed from screenshots.
   ========================================================================== */

*, *::before, *::after { box-sizing: border-box; }
/* No CSS scroll-behavior:smooth — Lenis (see main.js) drives all scroll
   easing, including anchor-link jumps (its `anchors: true` option); native
   smooth-scroll left on top would fight Lenis's own per-frame easing. */
body {
  margin: 0;
  font-family: var(--font-body);
  color: var(--text-primary);
  background: var(--bg-light);
  -webkit-font-smoothing: antialiased;
}
img, video { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
p { margin: 0; }
h1, h2, h3, h4, h5, h6 { margin: 0; font-family: var(--font-heading); }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
.u-visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

/* ---- Fixed scroll-driven page background --------------------------------
   A fixed full-viewport layer behind the page that snaps instantly
   between Light / Dark / Subtle as each section scrolls into view — no
   crossfade. main.js swaps the data-bg attribute at the right scroll
   position; this just reads it, flat, no transition. */
.page-bg {
  position: fixed;
  inset: 0;
  z-index: -1;
  background: var(--bg-light);
}
body[data-bg="dark"]   .page-bg { background: var(--bg-dark); }
body[data-bg="subtle"] .page-bg { background: var(--bg-subtle); }
body[data-bg="light"]  .page-bg { background: var(--bg-light); }

/* ---- Layout shell --------------------------------------------------------- */
.shell {
  max-width: var(--content-max);
  margin-inline: auto;
}
.section {
  position: relative;
  padding: 104px var(--gutter);
}
.section__inner {
  max-width: var(--content-max);
  margin-inline: auto;
}
.section__heading {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  text-align: center;
  margin-bottom: 48px;
}
/* Every section label on the page uses this: mono, lime, a bullet either
   side. It used to be an uppercase bold body face, with the dotted mono
   version living only in the hero as .hero__eyebrow — two treatments for
   one thing. The hero's won, so it moved down here and the .hero__ variant
   is gone rather than being aliased. */
.eyebrow {
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 13px;
  letter-spacing: 0.02em;
  color: var(--lime-900);
  margin: 0;
}
.eyebrow__dot { padding: 0 4px; }
.section__title {
  font-weight: 600;
  font-size: var(--size-h2);
  line-height: 1.15;
  letter-spacing: -0.01em;
  max-width: 800px;
  color: var(--text-primary);
}
.section__title--inverse { color: var(--text-inverse); }
.section__subtext {
  font-family: var(--font-body);
  font-size: var(--size-body-lg);
  line-height: 1.6;
  max-width: 640px;
  color: var(--text-secondary);
  margin: 0;
}
.section__subtext--inverse { color: var(--text-inverse-muted); }

/* ---- Reveal-on-scroll -------------------------------------------------
   Content stays fully visible by default; only once JS confirms it can
   observe intersections does .reveal get an initial hidden state, so a
   slow/failed script never leaves a section permanently blank.
   The transition lives on the .is-visible rule only, not on the hidden
   rule above it — putting it on both meant *entering* the hidden state
   was itself an animated transition (default -> hidden), which then
   raced the very next hidden -> visible transition and got cut off
   mid-flight the moment is-visible landed (often within the same frame
   for anything already on screen at load). Hidden must apply instantly;
   only the reveal itself should animate. */
.js-reveal .reveal {
  opacity: 0;
  transform: translateY(24px);
}
.reveal.is-visible {
  opacity: 1;
  transform: none;
  transition: opacity 0.6s cubic-bezier(0.22,1,0.36,1), transform 0.6s cubic-bezier(0.22,1,0.36,1);
}

/* ---- Buttons --------------------------------------------------------------
   Three variants straight off the Framer "Button" component. */
/* Single shared button component — every CTA on the site (header, hero,
   path cards, founder, footer) uses .btn + a variant class, so sizing
   lives here once. Fixed at exactly 42px tall (border-box: 10px + 10px
   padding + 20px line-height + 2px border = 42px). */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 10px 22px;
  border-radius: var(--radius-lg);
  font-family: var(--font-heading);
  font-weight: 500;
  font-size: 14px;
  line-height: 20px;
  white-space: nowrap;
  cursor: pointer;
  transition: transform 0.25s ease, background-color 0.25s ease, border-color 0.25s ease;
  border: 1px solid transparent;
}
.btn svg { width: 15px; height: 15px; flex: none; }
.btn:hover { transform: translateY(-2px); }

.btn--primary { background: var(--brand-primary); color: var(--text-inverse); }
.btn--primary:hover { background: var(--brand-primary-hover); }

.btn--accent { background: var(--brand-accent); color: var(--text-on-accent); }
.btn--accent:hover { background: var(--brand-accent-hover); }

.btn--outline { background: var(--surface-default); color: var(--text-brand); border-color: var(--border-strong); }
.btn--outline:hover { background: var(--surface-subtle); }

/* ==========================================================================
   Header / Nav
   ========================================================================== */
/* Floats directly over the hero video (and everything else, since it's
   fixed) instead of reserving its own bar. */
.header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 40;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  background: transparent;
  pointer-events: none;
}
/* Transparent over the hero — just a hairline bottom edge, no bar — then
   crossfades to glass the moment the hero scrolls out from under it.
   header.classList.toggle('header--solid', …) drives this in main.js.
   This carried blur and NO tint for a while, on the argument that a
   translucent white fill reads as frosted plastic while real glass is
   blur alone. That held only while the nav links had a pill of their own
   to sit on. With the pill gone they sit directly on the page, and blur
   at this radius doesn't separate 14px text from the headings and body
   copy sliding underneath it — the section behind was legible straight
   through the nav.
   So there is a tint, but it's kept minimal and does the least work
   possible: 10%, with a wide 44px blur and a saturate boost doing the rest.
   The blur radius is carrying what the tint used to — at 12px the headings
   sliding under the bar were still legible through it, and every step of
   opacity added to fix that was a step toward the grey slab. Widening the
   blur smears them out instead, which costs nothing in colour.
   The saturate is the part that matters — a plain blur averages whatever is
   behind it toward grey, which is exactly what made a heavier tint read as
   a grey slab; pushing saturation back up keeps the colour of the page
   showing through the bar. It's band-aware too, mixing toward whichever
   surface the section below uses, so it darkens over the dark bands rather
   than fogging them. */
.header__inner {
  width: 100%;
  height: var(--header-h);
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* Full-bleed element, content-column children. The bar itself still spans
     the viewport (it carries the blur and the bottom hairline), but the logo
     and the nav now start and end on the same lines as every .section__inner
     down the page — previously they sat at the raw gutter, so on anything
     wider than --content-max the header was visibly out of step with the
     content beneath it. max() keeps the gutter as the floor on narrow
     screens, where the calc goes negative. */
  padding-inline: max(var(--gutter), calc((100% - var(--content-max)) / 2));
  background: transparent;
  /* Same token as before (--border-default), just cut to ~35% opacity via
     color-mix — solid slate-200 read as a hard, opaque line ("loud",
     "whitish") sitting over whatever tone was behind it; translucent lets
     it sit quietly on any of them instead of only matching light ones. */
  border-bottom: 1px solid color-mix(in srgb, var(--border-default) 35%, transparent);
  transition: backdrop-filter 0.3s ease;
  pointer-events: none;
}
.header--solid .header__inner {
  backdrop-filter: blur(44px) saturate(180%);
  -webkit-backdrop-filter: blur(44px) saturate(180%);
  background: color-mix(in srgb, var(--surface-default) 10%, transparent);
  transition: backdrop-filter 0.3s ease, background-color 0.3s ease;
}
body[data-bg="dark"] .header--solid .header__inner {
  background: color-mix(in srgb, var(--surface-inverse) 10%, transparent);
}
/* No blur to hide behind means the bar has to be effectively opaque, or
   the text underneath reads straight through it. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .header--solid .header__inner { background: var(--surface-default); }
  body[data-bg="dark"] .header--solid .header__inner { background: var(--surface-inverse); }
}
.header__inner > * { pointer-events: auto; }
.logo {
  display: flex;
  align-items: center;
}
/* Single mark (brand/logo-kara.svg), drawn flat in --brand-mark's lime
   (#7CCF00) with no outline — one colour that reads on both the light
   bands and the dark ones (Why/book/footer), so there's no need for the
   light/dark swap-by-data-bg pair this used to be.
   Sized off its own aspect ratio (74:24), kept small — this is now the
   only thing in the header row, no CTA button beside it. */
.logo__mark {
  width: 99px;
  height: 32px;
  display: block;
}
/* No pill any more — the links sit straight on the page. That removes the
   surface the colour used to be able to rely on, so the colour has to
   follow the page instead: body[data-bg] is already maintained by the band
   tracker in main.js, and every section the header crosses now declares a
   band (see the data-band attributes in index.html), so the nav can just
   answer to it.
   Light bands get the dark brand ink; dark bands get the muted inverse,
   which is the secondary text colour on a dark surface. */
.nav-links {
  display: flex;
  align-items: center;
  gap: 28px;
}
.nav-links a {
  font-family: var(--font-heading);
  font-weight: 500;
  font-size: 14px;
  /* --text-primary (slate-900), not --text-brand (indigo-950): at 14px the
     indigo reads as blue rather than as ink, and the headings down the page
     are already set in slate-900. */
  color: var(--text-primary);
  transition: color 0.2s ease;
}
/* Hover and current-section land on the same accent, and it is the same
   accent on every band: --brand-mark, the lime the wordmark itself is
   drawn in. Earlier passes stepped this up and down the ramp per band to
   chase contrast — lime-700 on light, lime-300 on dark — which read as two
   different greens depending where you were on the page. One value that
   matches the logo is the point, so it stays put.
   The link colour underneath it is what changes instead: brand ink on the
   light bands, plain white on the dark ones. */
.nav-links a:hover,
.nav-links a:focus-visible,
.nav-links a.is-current {
  color: var(--brand-mark);
}
/* :where() on the band selector is load-bearing, not decoration. Written as
   `body[data-bg="dark"] .nav-links a` this scores (0,2,2) and beats
   `.nav-links a:hover` / `.is-current` at (0,2,1) — so on the dark bands the
   base colour silently won and hover and the current item stopped going
   lime at all. :where() contributes nothing to specificity, dropping this to
   (0,1,2) and letting the state rules above take over where they should. */
body:where([data-bg="dark"]) .nav-links a { color: var(--text-inverse); }

/* First section on the page sits directly under the transparent header,
   so give it enough top clearance for the header row not to overlap
   its own content; every other section only needs scroll-margin so
   anchor jumps don't land underneath the fixed bar. */
.hero { padding-top: var(--header-h); }
section[id], footer[id] { scroll-margin-top: var(--header-h); }

/* No chip, no shadow — the button is just its glyph. It used to sit in a
   white rounded box, which on a page whose header is otherwise pure glass
   read as a floating element pasted over the design rather than part of
   it. The colour follows body[data-bg] the same way the desktop nav does,
   which is what a transparent button needs in order to survive the dark
   bands. */
.menu-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin-right: -10px; /* optical: pulls the glyph out to the content edge */
  border-radius: var(--radius-lg);
  color: var(--text-primary);
  background: transparent;
  box-shadow: none;
}
body:where([data-bg="dark"]) .menu-toggle { color: var(--text-inverse); }
/* The three bars ARE the X — the top and bottom rotate onto each other's
   diagonal while the middle collapses out. Because nothing is swapped for
   anything, the two states are the same three elements at different
   transforms, which is what makes the change readable as a movement
   instead of a cut. */
.menu-toggle__bars {
  position: relative;
  display: block;
  width: 22px;
  height: 15px;
}
.menu-toggle__bars span {
  position: absolute;
  left: 0;
  width: 100%;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  transition: transform 0.32s cubic-bezier(0.4, 0, 0.2, 1),
              opacity 0.18s ease;
}
.menu-toggle__bars span:nth-child(1) { top: 0; }
.menu-toggle__bars span:nth-child(2) { top: 6.5px; }
.menu-toggle__bars span:nth-child(3) { top: 13px; }
.menu-toggle[aria-expanded="true"] .menu-toggle__bars span:nth-child(1) {
  transform: translateY(6.5px) rotate(45deg);
}
/* Scaled out as well as faded so it reads as being absorbed into the
   crossing bars rather than simply switched off. */
.menu-toggle[aria-expanded="true"] .menu-toggle__bars span:nth-child(2) {
  opacity: 0;
  transform: scaleX(0.2);
}
.menu-toggle[aria-expanded="true"] .menu-toggle__bars span:nth-child(3) {
  transform: translateY(-6.5px) rotate(-45deg);
}
@media (prefers-reduced-motion: reduce) {
  .menu-toggle__bars span { transition: none; }
}
/* The sheet is opaque and always light, so the glyph over it must be too —
   otherwise opening the menu on a dark band leaves a white X on white. */
.header--menu-open .menu-toggle { color: var(--text-primary); }

.mobile-menu {
  display: none;
  flex-direction: column;
  gap: 4px;
  padding: 12px var(--gutter-mobile) 20px;
  background: var(--surface-default);
  border-bottom: 1px solid var(--border-default);
  box-shadow: var(--shadow-lg);
  pointer-events: auto;
}
.mobile-menu a:not(.btn) {
  padding: 12px 4px;
  font-family: var(--font-heading);
  font-size: 15px;
  color: var(--text-primary);
  border-bottom: 1px solid var(--border-default);
}
.mobile-menu .btn { margin-top: 12px; align-self: flex-start; }

/* ==========================================================================
   Hero — flat light band, centred content, interactive piano strip
   ========================================================================== */
.hero {
  position: relative;
  height: 100dvh;
  min-height: 560px;
  max-height: 980px;
  display: flex;
  flex-direction: column;
  background: var(--surface-muted);
  overflow: clip;
}
.hero__content {
  position: relative;
  z-index: 2;
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  max-width: 720px;
  margin-inline: auto;
  padding-inline: 24px;
  text-align: center;
}
.hero__heading {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}
.hero__title {
  font-weight: 700;
  font-size: var(--size-display);
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--text-primary);
  text-wrap: balance;
}
.hero__sub {
  font-family: var(--font-body);
  font-size: var(--size-body-lg);
  line-height: 1.6;
  color: var(--text-secondary);
  text-wrap: balance;
  margin: 0;
}
/* Piano + its mute control live together, close to each other, so the
   control reads as "belongs to the piano" rather than part of the hero
   copy above — a short 16px gap is the whole point. */
.hero__piano-wrap {
  position: relative;
  z-index: 2;
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}
/* The icon and its caption are one control, so nothing between them; the
   8px below is the whole distance from that control to the keys. */
.hero__mute-group {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
}
.hero__mute {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: var(--radius-full);
  transition: background-color 0.2s ease;
}
.hero__mute:hover { background: var(--surface-default); }
.hero__mute-icon { width: 22px; height: 22px; display: block; transition: color 0.2s ease; }
/* Muted (default) reads as the "disabled" state — dimmer; once sound is
   on, the icon itself should carry more weight so the state is legible
   at a glance, not just via which glyph is showing. */
.hero__mute-icon--off { color: var(--lime-950); }
.hero__mute-icon--on { color: var(--lime-950); display: none; }
.hero__mute[aria-pressed="true"] .hero__mute-icon--off { display: none; }
.hero__mute[aria-pressed="true"] .hero__mute-icon--on { display: block; }
.hero__mute-caption {
  font-family: var(--font-body);
  font-size: var(--size-caption);
  line-height: 1.4;
  color: var(--text-secondary);
  text-align: center;
}


/* Scroll cue — a small floating affordance, fixed to the viewport rather
   than any one section, so it can stay put across the hero AND the Two
   Paths scroller (the two sections most likely to read as "stuck" before
   the user has picked up the scroll rhythm). main.js toggles .is-hidden
   once the Why section arrives. Icon-only, a flat grey circle rather than
   the earlier glassy blur — the bounce is a slow, low-amplitude nudge, not
   an animation that competes for attention. */
.hero__scroll-cue {
  position: fixed;
  left: 50%;
  /* 2px gave zero real clearance from the true bottom of the screen — fine
     in a desktop browser with no chrome down there, but on an actual phone
     that's the address bar or the home-indicator safe area, and the cue
     read as cut off rather than floating. env() pushes it clear of that
     safe area on notched devices; the flat 14px is the floor everywhere
     else. Intentionally the same expression at every width — this is the
     one thing that should never look different on mobile. */
  bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  z-index: 5;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
  opacity: 0.85;
  transition: opacity 0.3s ease, visibility 0.3s ease;
}
.hero__scroll-cue:hover { opacity: 1; }
.hero__scroll-cue.is-hidden { opacity: 0; visibility: hidden; pointer-events: none; }
.hero__scroll-cue-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-full);
  background: var(--border-default);
  color: var(--text-muted);
  animation: hero-scroll-cue-bob 2.2s ease-in-out infinite;
}
.hero__scroll-cue:hover .hero__scroll-cue-icon { background: var(--border-strong); }
.hero__scroll-cue-icon svg { width: 16px; height: 16px; }
@keyframes hero-scroll-cue-bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(5px); }
}
@media (prefers-reduced-motion: reduce) {
  .hero__scroll-cue-icon { animation: none; }
}
/* ---- Interactive piano strip ---------------------------------------------
   Flat two-tone keys (matches the site's flat design language, no gloss/
   gradient realism). Generated by main.js to fill the available width with
   keys bleeding off both edges; JS maps cursor/touch position to the key
   beneath it and toggles .is-active, which is the only thing that moves —
   a lime fill plus a small downward shift, one key active at a time. */
.hero__piano {
  position: relative;
  width: 100%;
  height: 20vh;
  min-height: 114px;
  max-height: 204px;
  overflow: hidden;
  touch-action: none;
}
.hero__piano-keys {
  position: absolute;
  top: 0;
  left: 50%;
  height: 100%;
  display: flex;
  transform: translateX(-50%);
}
.piano-key {
  position: relative;
  flex: none;
  box-sizing: border-box;
  transition: transform 0.06s ease, background-color 0.06s ease;
}
.piano-key--white {
  height: 100%;
  background: var(--surface-default);
  border: 1px solid var(--border-default);
  border-top: none;
}
.piano-key--black {
  position: absolute;
  top: 0;
  height: 62%;
  background: var(--slate-900);
  border-radius: 0 0 3px 3px;
  z-index: 2;
}
.piano-key.is-active {
  background: var(--brand-accent);
  transform: translateY(6px);
}
.piano-key--black.is-active { transform: translateY(4px); }

/* ==========================================================================
   Two Paths — pinned horizontal filmstrip. One continuous flex row (sheet
   music, then the heading, then music, then each path card) pans left as
   the section scrolls past. main.js reads scroll position continuously but
   never binds translateX to it 1:1 — it always eases toward whichever of
   the three "stages" (art1+heading, art2+card1, art3+card2) the scroll
   position is nearest to, so the section only ever comes to rest on one of
   those three exact compositions, gliding smoothly between them rather
   than stopping wherever the user happens to stop scrolling.
   The pin is sized exactly like .hero (100dvh, same min/max clamp) so this
   reads as its own full-screen stage, not a normal padded content block.
   Each sheet-music image fades at its own leading/trailing edge (interior
   joins only — the very first and last images stay solid at their outer
   edge) so consecutive passages dissolve into one another like one
   continuous score with cards set into it, instead of looking like
   separate image chunks. The pin also masks its own trailing screen edge
   for whatever's mid-arrival during the glide between stages.
   Below 1024px (and under prefers-reduced-motion) the pin/mask/JS motion
   is dropped entirely in favour of a plain swipeable native strip — see
   the fallback block right after this one. ========================== */
/* One source of truth for the pinned frame's height. #pathsPin clamps
   100dvh between 560 and 980; #featureOverlay has to sit in exactly the
   same box, and when it was written as a bare 100dvh (with a -100dvh pull)
   the two silently disagreed on any viewport taller than 980px. The
   overlay then started ~(100dvh - 980px) too low and ran its own content
   off the bottom of the screen — on a 1300px-tall display the last list
   row and the CTA were simply below the fold. */
.paths-scroller { position: relative; --pin-h: clamp(560px, 100dvh, 980px); }
.paths-scroller__pin {
  position: sticky;
  top: 0;
  height: var(--pin-h);
  overflow: hidden;
  display: flex;
  align-items: center;
  -webkit-mask-image: linear-gradient(to right, #000 0%, #000 92%, transparent 100%);
          mask-image: linear-gradient(to right, #000 0%, #000 92%, transparent 100%);
}
.paths-scroller__track {
  display: flex;
  align-items: center;
  flex: none;
  gap: 6vw;
  padding-left: var(--gutter);
  padding-right: var(--gutter);
  will-change: transform;
}

/* Purely a grouping hook for the mobile layout below — on desktop it
   disappears from layout entirely so .pss-art/.pss-heading/.pss-card
   remain direct flex children of the track, unchanged. */
.pss-group { display: contents; }

.pss-art { flex: none; display: flex; align-items: center; }
/* Sized off HEIGHT, not width — each source excerpt is cropped tight to
   its own staff band (see brand/Music sheet/), but different passages
   still carry a different width-per-bar. Sizing off width (the old
   approach) let that show up as visibly different staff heights side by
   side; fixing the height and letting width follow each image's own
   aspect ratio keeps every staff the same size and sitting on the same
   line as it scrolls past. */
.pss-art__img {
  display: block; height: 6.1vw; max-height: 87px; min-height: 43px; width: auto;
  -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 12%, #000 88%, transparent 100%);
          mask-image: linear-gradient(to right, transparent 0%, #000 12%, #000 88%, transparent 100%);
}
/* The first passage has nothing before it to blend from, so only its
   interior-facing (right) edge fades — the very start of the filmstrip
   stays solid. */
.pss-art--first .pss-art__img {
  -webkit-mask-image: linear-gradient(to right, #000 0%, #000 88%, transparent 100%);
          mask-image: linear-gradient(to right, #000 0%, #000 88%, transparent 100%);
}
/* The interlude sits between the two cards, not beside a heading — it's
   pure connective tissue, so it's the one place narrowed, to pull both
   cards into the same pinned pass instead of one apiece. Same height as
   every other passage (unlike an earlier pass at this that shrank the
   whole image, making its notation visibly smaller than its neighbours);
   only the width is cropped, via a fixed box + object-fit:cover showing
   the excerpt's centre, trimming the empty staff margin at each edge
   rather than shrinking the notation itself. */
.pss-art--interlude, .pss-art--second {
  width: min(18vw, 260px);
  height: 6.1vw;
  max-height: 87px;
  min-height: 43px;
  overflow: hidden;
}
.pss-art--interlude .pss-art__img, .pss-art--second .pss-art__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

/* Trailing flourish after the second card — same crop technique as the
   interlude, just cropped to roughly its first three bars (measured
   directly off sheet-3.svg's own barlines, ~76% of its full width) and
   left-aligned so it shows the start of the excerpt, not a random
   middle slice. No mask override: it inherits the default fade-both-
   edges from .pss-art__img above, so it eases in off the card and
   dissolves away at the end rather than stopping solid. */
.pss-art--last {
  width: min(23vw, 330px);
  height: 6.1vw;
  max-height: 87px;
  min-height: 43px;
  overflow: hidden;
}
.pss-art--last .pss-art__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: left center;
}

.pss-heading { flex: none; width: min(52vw, 760px); margin-right: -2vw; }
.pss-heading__title {
  font-weight: 700;
  font-size: var(--size-h1);
  line-height: 1.15;
  letter-spacing: -0.015em;
  color: var(--text-primary);
}

.pss-card {
  flex: none;
  width: min(23vw, 340px);
  margin-right: -3vw;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 20px;
}
.pss-card__title { font-weight: 600; font-size: var(--size-h3); line-height: 1.2; color: var(--text-primary); }
.pss-card__subtext { font-family: var(--font-body); font-size: var(--size-body); line-height: 1.6; color: var(--text-secondary); }
/* row-gap is stated separately from the column gap: the three badges wrap
   to two lines below ~430px, and at that point the row spacing is what the
   eye reads as the block's rhythm, not the space between the logos. */
.pss-card__badges { display: flex; flex-wrap: wrap; align-items: center; column-gap: 22px; row-gap: 12px; }
.pss-card__badge { display: block; height: 26px; width: auto; }
.pss-card__tags { display: flex; flex-wrap: wrap; gap: 10px; }

.path-card__includes { display: flex; flex-direction: column; gap: 12px; padding: 2px 0 4px; }
.check-row { display: flex; align-items: flex-start; gap: 12px; }
.check-row svg { width: 17px; height: 17px; flex: none; margin-top: 2px; color: var(--text-accent); }
.check-row span { font-family: var(--font-body); font-size: var(--size-body-sm); line-height: 1.55; color: var(--text-secondary); }

.badge {
  padding: 6px 12px; border-radius: var(--radius-md);
  background: var(--surface-muted); color: var(--text-brand);
  font-family: var(--font-body); font-size: var(--size-body-sm); line-height: 1.55;
}
/* Sized to sit level with the exam-board badges in the card opposite, which
   render at 26px tall — the two cards are meant to compare at a glance, and
   a pill half again their height broke that read. */
.badge--pill {
  display: inline-flex; align-items: center;
  padding: 3px 14px;
  font-size: 12px;
  line-height: 1.5;
  border-radius: var(--radius-full);
  background: transparent;
  border: 1px solid var(--text-accent);
  color: var(--text-primary);
}

/* ---- Mobile / reduced-motion fallback ------------------------------------
   No pinning, no horizontal scroll-jacking, no JS transform at all (main.js
   checks this same breakpoint and prefers-reduced-motion before it ever
   touches #pathsTrack) — instead a plain vertical stack, read top to bottom
   like any other section: the heading first, then each card below it as you
   scroll. Each sheet-music image drops out of the flow and becomes a faint,
   oversized, non-interactive background sitting behind its own heading/card
   block rather than content of its own — "just a bg", not something you
   scroll past separately. .pss-group (display:contents on desktop, so it's
   invisible to the horizontal layout there) is what makes this possible: on
   mobile it becomes the positioning context for that background image. */
@media (max-width: 1023px), (prefers-reduced-motion: reduce) {
  .paths-scroller__pin {
    position: static;
    height: auto;
    min-height: 0;
    max-height: none;
    overflow: visible;
    display: block;
    -webkit-mask-image: none;
            mask-image: none;
  }
  .paths-scroller__track {
    display: flex;
    flex-direction: column;
    gap: 0;
    padding: 0;
    will-change: auto;
  }
  .pss-group {
    display: flex;
    flex-direction: column;
    position: relative;
    overflow: hidden;
    padding: 64px var(--gutter-mobile);
  }
  /* Card-less trailing group — purely a desktop filmstrip flourish. On
     the mobile vertical stack there's no runway to fill, so it'd just
     render as an empty padded section. */
  .pss-group--tail { display: none; }
  .pss-art {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0.08;
    pointer-events: none;
    z-index: 0;
  }
  .pss-art__img {
    width: 170%;
    max-width: none;
    min-width: 0;
    height: auto;
    max-height: none;
    min-height: 0;
    -webkit-mask-image: none;
            mask-image: none;
  }
  .pss-heading, .pss-card {
    position: relative;
    z-index: 1;
    width: 100%;
    max-width: none;
    margin-right: 0;
  }
  .pss-card__badges { column-gap: 16px; row-gap: 12px; }
}

/* ==========================================================================
   Songs vs Language — stat-led comparison
   The two numbers carry the argument, so they get the display weight and
   everything else stays deliberately quiet: no icon chips, no per-bullet
   glyphs, three short lines each. The "good" side is separated from the
   "bad" side by accent colour and a barely-there lime wash rather than by
   extra chrome, so the pair still reads as one object.
   Sits on its own static fill, same approach as .book-section/.footer —
   not the shared scroll-crossfade .page-bg — so there's no background
   transition as this section scrolls in or out. Default white, same as
   .founder-section — text uses the standard --text-primary/--text-secondary
   pair rather than the --inverse tokens, since there's no dark fill behind
   it any more. */
.why-section { background: var(--white); }





.compare-closing {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
  margin-top: 52px;
  text-align: center;
}
.compare-closing p {
  font-family: var(--font-body);
  font-size: var(--size-body-lg);
  line-height: var(--lh-body-lg);
  color: var(--text-primary);
  margin: 0;
}

/* ==========================================================================
   Book a Free Trial — lead form. The page's single conversion point;
   every CTA site-wide links to #book (see main.js for the submit
   handler — client-side only for now, see the comment there before
   this goes live).
   Sits on its own static fill (.book-section below), the same approach
   as .footer — not the shared scroll-crossfade .page-bg — so there's no
   background-colour transition to speak of: the section is just always
   this colour, from the first frame it's on screen.
   That fill is the brand indigo (--surface-inverse), matched to the
   footer directly beneath it, so the bottom of the page reads as one
   continuous dark block rather than two dark sections in slightly
   different hues — which is what slate-900 against the footer used to
   look like. */
.book-section { background: var(--surface-inverse); }
.book { display: flex; align-items: flex-start; gap: 64px; flex-wrap: wrap; }
.book__intro { flex: 1 1 380px; display: flex; flex-direction: column; gap: 18px; padding-top: 4px; }

/* The card has to lift off an indigo-950 section now, not a slate-900
   one, so it moves into the same family: indigo-900 is the one step up
   the ramp, and a translucent white hairline reads as an edge on it
   without introducing a third dark hue the way slate-700 did. */
.book-form, .book-form__success {
  flex: 1 1 440px;
  width: 100%;
  max-width: 480px;
  background: var(--surface-inverse-elevated);
  border: 1px solid color-mix(in srgb, var(--surface-default) 10%, transparent);
  border-radius: var(--radius-2xl);
  padding: 32px;
}
/* [hidden] and .book-form/.book-form__success have equal specificity, and
   these rules come later in the cascade, so a plain `display: flex` here
   would silently win over the browser's default [hidden] { display: none }
   the moment main.js toggles the .hidden property on submit — the success
   card would already be sitting there rendered underneath the form the
   whole time. The [hidden] variants below win that tie instead. */
.book-form { display: flex; flex-direction: column; gap: 16px; }
.book-form[hidden], .book-form__success[hidden] { display: none; }

.book-form__row { display: flex; gap: 16px; position: relative; }
/* Same specificity trap the .book-form[hidden] comment above describes:
   `display: flex` here and the browser's `[hidden] { display: none }` tie,
   and this rule comes later, so it silently won. That left #guardianRow —
   the Parent / Guardian name field, which is supposed to appear only once
   an under-18 age bracket is chosen — rendered and marked required for
   everyone, on every visit. */
.book-form__row[hidden] { display: none; }
.book-form__row--split .book-form__field { flex: 1 1 0; min-width: 0; }
/* A native select's closed state shows the whole option text, not the
   short code the old custom trigger showed, so this needs more than the
   136px it used to have. It still doesn't need to fit the longest name in
   the list — the field ellipsises, and the picker shows options in full. */
.book-form__row--phone .book-form__field--country-code { flex: 0 0 180px; }
.book-form__row--phone .book-form__field:not(.book-form__field--country-code) { flex: 1 1 0; min-width: 0; }

.book-form__field { display: flex; flex-direction: column; gap: 6px; flex: 1 1 auto; position: relative; }
.book-form__label {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-inverse-muted);
}
.book-form__required { color: var(--brand-accent); margin-left: 2px; }
.book-form__optional { color: var(--text-tertiary); text-transform: none; letter-spacing: 0; }

.book-form__input {
  width: 100%;
  padding: 10px 13px;
  border-radius: var(--radius-lg);
  border: 1px solid rgba(255, 255, 255, 0.16);
  background: rgba(255, 255, 255, 0.06);
  color: var(--text-inverse);
  font-family: var(--font-body);
  font-size: var(--size-body-sm);
  line-height: 1.3;
}
.book-form__input::placeholder { color: rgba(241, 245, 249, 0.4); }
.book-form__input:focus { outline: none; border-color: var(--brand-accent); background: rgba(255, 255, 255, 0.09); }

/* ---- Native select ------------------------------------------------------
   Only the closed field is styled here — appearance:none strips the OS
   button so it can match the text inputs beside it, and the chevron is the
   same sprite icon the rest of the site uses. The open list is left as
   system chrome on purpose: that popup is the part that has to scroll
   properly and support type-to-jump, and the browser's own is better at
   both than anything the page can rebuild.
   color-scheme: dark is what makes that native popup render dark in
   Chrome and Safari instead of dropping a white list out of a dark form.
   padding-right leaves room for the chevron, which sits on top of the
   select and is pointer-events:none so clicks fall through to it. */
.book-form__select-wrap { position: relative; display: block; }
.book-form__select {
  width: 100%;
  padding: 10px 34px 10px 13px;
  border-radius: var(--radius-lg);
  border: 1px solid rgba(255, 255, 255, 0.16);
  background: rgba(255, 255, 255, 0.06);
  color: var(--text-inverse);
  font-family: var(--font-body);
  font-size: var(--size-body-sm);
  line-height: 1.3;
  cursor: pointer;
  color-scheme: dark;
  appearance: none;
  -webkit-appearance: none;
  /* Long zone names ("America/Argentina/Rio_Gallegos (GMT-3)") must not be
     allowed to stretch the field and blow out the row it sits in. */
  text-overflow: ellipsis;
  white-space: nowrap;
  overflow: hidden;
}
.book-form__select:focus-visible { outline: none; border-color: var(--brand-accent); background: rgba(255, 255, 255, 0.09); }
/* :invalid catches the disabled "Select age" placeholder while it's still
   the selection, so the field greys out the way a real placeholder does. */
.book-form__select:invalid { color: rgba(241, 245, 249, 0.55); }
.book-form__select-chevron {
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
  width: 15px;
  height: 15px;
  pointer-events: none;
  color: var(--text-inverse-muted);
}

.book-form__submit { width: 100%; margin-top: 4px; padding-block: 12px; font-size: 14px; }
.book-form__privacy { font-family: var(--font-body); font-size: var(--size-caption); color: var(--text-inverse-muted); text-align: center; margin: 0; }

/* Centered rather than the form's own left-aligned rhythm — this isn't
   another field to scan, it's a single moment to land on, and a bare
   checkmark floating at the left edge read as an afterthought rather
   than the payoff of the form the visitor just finished. */
.book-form__success {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 16px;
  border-color: rgba(187, 244, 81, 0.3);
}
.book-form__success-badge {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  border-radius: var(--radius-full);
  background: var(--brand-accent);
  flex-shrink: 0;
}
.book-form__success-icon { width: 26px; height: 26px; color: var(--lime-950); }
.book-form__success h3 {
  font-weight: 600;
  font-size: var(--size-h5);
  line-height: 1.25;
  color: var(--text-inverse);
  margin: 0;
}
.book-form__success p {
  font-family: var(--font-body);
  color: var(--text-inverse-muted);
  margin: 0;
  max-width: 40ch;
}

/* ---- Field-level validation ---------------------------------------------
   Replaces the browser's native validation bubbles, which are unstyled and
   vanish on their own. Each message is wired to its control with
   aria-describedby and only becomes a live region once it actually has
   something to say, so screen readers announce errors instead of reading
   empty nodes. */
.book-form__error {
  font-family: var(--font-body);
  font-size: var(--size-body-sm);
  line-height: 1.4;
  color: #FCA5A5;
}
.book-form__error[hidden] { display: none; }

.book-form__field.has-error .book-form__input,
.book-form__field.has-error .book-form__select {
  border-color: #F87171;
  background: rgba(248, 113, 113, 0.06);
}
.book-form__field.has-error .book-form__input:focus,
.book-form__field.has-error .book-form__select:focus-visible {
  border-color: var(--brand-accent);
}


/* Form-level failure — network died, endpoint refused. Separate from the
   field errors because the fix is "try again", not "fix your input". */
.book-form__formerror {
  display: flex;
  gap: 10px;
  padding: 14px 16px;
  border-radius: var(--radius-lg);
  border: 1px solid rgba(248, 113, 113, 0.45);
  background: rgba(248, 113, 113, 0.08);
  font-family: var(--font-body);
  font-size: var(--size-body-sm);
  line-height: 1.5;
  color: #FECACA;
  margin: 0;
}
.book-form__formerror[hidden] { display: none; }
.book-form__formerror a { color: var(--brand-accent); text-decoration: underline; }


/* Busy state — the button is the only affordance that changes, and it
   stops accepting clicks, so a slow network can't produce two leads. */
.book-form__submit[aria-busy="true"] { opacity: 0.72; pointer-events: none; }

.book-form__success { outline: none; }
.book-form__success .btn { margin-top: 8px; }

/* ==========================================================================
   Founder
   Static default-white fill, not the shared scroll-crossfade .page-bg —
   same approach as .why-section/.book-section/.footer, so there's no
   background transition as this section scrolls in or out.
   ========================================================================== */
/* Light grey rather than flat white, so the section reads as its own
   band between the two white ones either side of it. */
.founder-section { background: var(--surface-subtle); }
.founder { display: flex; align-items: center; gap: 72px; flex-wrap: wrap; }
.founder__photo {
  width: 440px; height: 540px; flex: none;
  border-radius: var(--radius-2xl);
  object-fit: cover;
  background: var(--surface-muted);
}
.founder__story { flex: 1 1 480px; display: flex; flex-direction: column; align-items: flex-start; gap: 26px; }
.founder__title { font-weight: 600; font-size: var(--size-h1); line-height: 1.12; color: var(--text-primary); }
.founder__copy { display: flex; flex-direction: column; gap: 22px; }
.founder__copy p { font-family: var(--font-body); font-size: var(--size-body-lg); line-height: 1.65; color: var(--text-secondary); }
.founder__signature {
  display: flex; flex-direction: column; gap: 3px;
  padding-top: 22px; border-top: 1px solid var(--border-default);
  width: 100%;
}
.founder__signature strong { font-family: var(--font-body); font-weight: 700; font-size: var(--size-body-lg); color: var(--text-primary); }
.founder__signature span { font-family: var(--font-body); font-size: var(--size-body); color: var(--text-muted); }

/* ==========================================================================
   Feature card overlay — stacks on top of #pathsPin (position:sticky +
   negative margin, same technique as any "stacked sticky sections" scroll
   effect) and slides up to cover it once the horizontal filmstrip's own
   runway is spent; see the pssTick comment in main.js for the scroll math.
   Sticky by default (desktop, no reduced-motion) — the matching fallback
   in the >=1024px/reduced-motion media block below drops it back to a
   normal static block, same as .paths-scroller__pin's own fallback. */
.feature-overlay {
  position: sticky;
  top: 0;
  /* Same clamped box as .paths-scroller__pin — see the note there. */
  height: var(--pin-h);
  margin-top: calc(var(--pin-h) * -1);
  overflow: hidden;
  z-index: 2;
}
.feature-overlay__card {
  height: 100%;
  display: flex;
  align-items: center;
  background: var(--brand-primary);
  padding: 104px var(--gutter);
  transform: translate3d(0, 100%, 0);
  will-change: transform;
}
.feature-overlay__card .section__inner { width: 100%; }

/* Below 1024px (and under reduced motion) this stops being an overlay and
   becomes a plain block in the flow, same as .paths-scroller__pin's own
   fallback above.
   These rules USED to live in the shared fallback block up beside that
   one — and did nothing, because they sit at the same specificity as the
   .feature-overlay rules above and lost the tie on source order. The
   result on a phone: the card kept position:sticky, kept pulling itself
   up by a full viewport, and kept its translateY(100%), so it was shoved
   off its own box while its negative margin ate a screen's worth of the
   Two Paths section. The whole "Everything You Need to Start" panel was
   effectively missing on mobile. Declaration order is the entire fix. */
@media (max-width: 1023px), (prefers-reduced-motion: reduce) {
  .feature-overlay {
    position: static;
    height: auto;
    margin-top: 0;
    overflow: visible;
    z-index: auto;
  }
  .feature-overlay__card {
    display: block;
    height: auto;
    padding: 64px var(--gutter-mobile);
    transform: none;
  }
}

/* ==========================================================================
   Feature split — text column + full-height media
   ========================================================================== */
.feature-split { display: flex; align-items: stretch; gap: 64px; flex-wrap: wrap; }
.feature-split__content { flex: 1 1 480px; display: flex; flex-direction: column; align-items: flex-start; padding-block: 4px; }

.feature-split__header { display: flex; flex-direction: column; gap: 8px; margin-bottom: 40px; }
.feature-split__title { font-weight: 700; font-size: var(--size-h1); line-height: 1.12; letter-spacing: -0.01em; color: var(--slate-100); margin: 0; }
.feature-split__subtext { font-family: var(--font-body); font-size: var(--size-body-lg); line-height: 1.6; color: var(--slate-300); margin: 0; }

.feature-split__list { display: flex; flex-direction: column; gap: 24px; width: 100%; margin-bottom: 48px; }
.feature-split__item { display: flex; align-items: center; gap: 16px; }
.feature-split__item-num {
  flex: none; width: 40px; height: 40px;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-full);
  border: 1px solid var(--slate-700);
  background: var(--slate-800);
  font-family: var(--font-mono); font-weight: 500; font-size: 13px; letter-spacing: 0.02em;
  color: var(--lime-300);
}
.feature-split__item-body h3 { font-weight: 600; font-size: var(--size-h6); line-height: 1.3; color: var(--slate-100); margin: 0 0 2px; }
.feature-split__item-body p { font-family: var(--font-body); font-size: var(--size-body); line-height: 1.5; color: var(--slate-300); margin: 0; }

.feature-split__media {
  position: relative;
  flex: 1 1 440px;
  min-height: 560px;
  border-radius: var(--radius-2xl);
  border: 1px solid var(--slate-700);
  overflow: hidden;
}
.feature-split__media-img {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}

/* ==========================================================================
   FAQ accordion
   ========================================================================== */
.faq-list { display: flex; flex-direction: column; gap: 12px; max-width: 840px; margin-inline: auto; width: 100%; }
.faq-item {
  border: 1px solid var(--border-default);
  background: var(--surface-default);
  border-radius: var(--radius-xl);
  padding: 26px 28px;
  transition: border-color 0.25s ease;
}
.faq-item__header {
  display: flex; align-items: center; justify-content: space-between; gap: 20px;
  width: 100%; cursor: pointer;
}
.faq-item__question { font-weight: 400; font-size: var(--size-h6); line-height: 1.35; color: var(--text-primary); text-align: left; }
.faq-item__toggle { flex: none; display: inline-flex; width: 20px; height: 20px; color: var(--text-brand); }
.faq-item__toggle svg { width: 20px; height: 20px; display: block; }
.faq-item__toggle .icon-minus { display: none; }
.faq-item__answer {
  display: none;
  font-family: var(--font-body); font-size: var(--size-body); line-height: 1.6;
  color: var(--text-secondary);
  margin-top: 14px;
}
.faq-item.is-open .faq-item__answer { display: block; }
.faq-item.is-open .faq-item__toggle .icon-plus { display: none; }
.faq-item.is-open .faq-item__toggle .icon-minus { display: block; }

/* ==========================================================================
   Footer
   ========================================================================== */
.footer {
  /* Its own solid fill, not the fixed scroll-crossfade .page-bg layer
     everything else shares — that layer only turns dark once scrollY
     crosses the footer's own offset, so the colour used to only show up
     right as you hit the very bottom. This is just a plain static
     background — the brand indigo (--surface-inverse), matched to
     #book directly above it so the two read as one dark block — from
     the first frame the footer is on screen, no transition. */
  background: var(--surface-inverse);
  /* Weighted to the top, but not by as much as it looks: #book above ends
     on its own ~104px of bottom padding, and both sections are the same
     indigo, so that space and this read as one gap rather than two. 80
     here lands the pair around 180px — enough to separate the form from
     the mark, short of the void that 120 produced. */
  padding: 80px var(--gutter) 44px;
}
/* Two items: the social row bottom-left, the oversized mark bottom-right.
   align-items:flex-end is the whole composition — it drops both onto one
   shared baseline, so four small buttons can hold the left side against a
   mark several times their size. Centred, they'd float in the middle of
   the mark's height and read as unrelated to it. space-between keeps the
   mark pinned to the right edge at any width. */
.footer__columns {
  display: flex; flex-wrap: wrap; align-items: flex-end;
  justify-content: space-between; gap: 48px;
  /* Deliberately short. The mark and the legal rule below it are the last
     two things on the page and read as one closing unit; a large gap here
     left the mark floating between the form above and the rule below,
     belonging to neither. */
  margin-bottom: 36px;
}

/* Scaled-up wordmark. Sized by width off the viewport rather than by a
   fixed height, because the SVG is a 74:24 lockup — pinning the height and
   letting the width follow is what would overflow a narrow screen. The cap
   is the design size; the vw term keeps it clear of the social row as the
   viewport tightens, before the wrap kicks in.
   translate nudges it the last few pixels: the SVG carries a little
   built-in padding on its right edge, so flush-right by the box leaves the
   mark looking short of the margin the legal text below it sits on. */
.footer__mark { flex: 0 1 auto; display: flex; justify-content: flex-end; }
.footer__mark img {
  width: clamp(170px, 27vw, 390px);
  height: auto;
  display: block;
  /* 1.4% is measured, not eyeballed: the artwork's rightmost ink sits at
     x=72.94 in a 74-unit viewBox, i.e. 1.4% short of its own right edge.
     Flush-right by the box therefore leaves the mark visibly inside the
     margin the legal text below it holds; this puts the ink on it. */
  transform: translateX(1.4%);
}
.footer__social { display: flex; gap: 12px; }
/* These four now carry the entire left side of the footer on their own, so
   they have to hold their weight rather than sit there as faint squares: a
   7% white fill alone all but disappears on indigo-950. The hairline border
   is what actually draws them, the fill just lifts the interior. Circles,
   because they're answering a wordmark across the row and a rounded-square
   grid competes with it for structure. */
.social-btn {
  display: flex; align-items: center; justify-content: center;
  width: 46px; height: 46px; border-radius: var(--radius-full);
  background: rgba(255,255,255,0.05);
  border: 1px solid rgba(255,255,255,0.14);
  color: var(--text-inverse);
  transition: background-color 0.25s ease, border-color 0.25s ease, color 0.25s ease, transform 0.25s ease;
}
.social-btn svg { width: 19px; height: 19px; }
.social-btn:hover {
  background: var(--brand-accent);
  border-color: var(--brand-accent);
  color: var(--text-on-accent);
  transform: translateY(-2px);
}



/* The divider used to be a flat indigo-900 hairline over slate-900, which
   put a whole hue's worth of distance between line and background and read
   as a bright rule across the footer. On the indigo-950 fill the same 900
   is only one step up the ramp, and pulling it back to 55% opacity settles
   it the rest of the way — present, but not the first thing you see. */
.footer__legal {
  display: flex; flex-wrap: wrap; gap: 24px; justify-content: space-between;
  padding-top: 28px;
  border-top: 1px solid color-mix(in srgb, var(--border-inverse) 55%, transparent);
}
.footer__legal p { font-family: var(--font-body); font-size: var(--size-caption); line-height: 1.45; color: var(--text-inverse-muted); }
.footer__legal p:first-child { min-width: 240px; flex: 1; }
.footer__legal p:last-child { min-width: 260px; max-width: 420px; }


/* ==========================================================================
   Responsive
   ========================================================================== */
@media (max-width: 1023px) {
  .nav-links { display: none; }
  .menu-toggle { display: flex; }
  .header--menu-open .mobile-menu { display: flex; }
}

@media (max-width: 900px) {
  .compare-closing { margin-top: 40px; }
  .section { padding: 72px var(--gutter-mobile); }
  /* #week specifically wants more air than the general mobile section
     padding gives it — the eyebrow sits right under the header's own
     border here, and the bar chart runs right into the footer band below,
     so 72px reads tighter than everywhere else the same rule applies.
     The id beats .section's specificity outright, so this holds regardless
     of what order the two end up in. */
  #week { padding-block: 80px; }
  .header__inner { height: var(--header-h-mobile); padding-inline: var(--gutter-mobile); }
  .hero { padding-top: var(--header-h-mobile); height: auto; min-height: 100dvh; max-height: none; }
  section[id], footer[id] { scroll-margin-top: var(--header-h-mobile); }
  .hero__content { padding-block: 32px; }
  .book { gap: 40px; }
  .book-form, .book-form__success { max-width: none; padding: 32px; }
  .founder { gap: 40px; }
  .founder__photo { width: 100%; max-width: 360px; height: 380px; }
  .feature-split { gap: 40px; }
  .feature-split__media { min-height: 280px; width: 100%; }
  .footer__columns { gap: 40px; }
}

@media (max-width: 560px) {
  .section { padding: 56px var(--gutter-mobile); }
  .logo__mark { width: 80px; height: 26px; }
  .hero { padding-top: var(--header-h-mobile); }
  .hero__content { gap: 16px; padding-inline: var(--gutter-mobile); }
  .book-form__row--phone { flex-direction: column; }
  .book-form__row--phone .book-form__field--country-code { flex: 1 1 auto; }
  .book-form__row--split { flex-direction: column; }
  .founder__photo { height: 320px; }
  .faq-item { padding: 16px; }
  .footer { padding: 72px var(--gutter-mobile) 36px; }
  /* Stacked at this width, so there's no baseline left to share — both
     centre instead, and the mark drops its flush-right nudge. */
  .footer__columns { align-items: center; justify-content: center; gap: 36px; margin-bottom: 44px; }
  .footer__social { justify-content: center; }
  .footer__mark { width: 100%; justify-content: center; }
  .footer__mark img { width: min(78vw, 300px); transform: none; }
  .footer__legal { flex-direction: column; align-items: center; text-align: center; gap: 8px; }
}

@media (max-width: 380px) {
  .badge { font-size: 12px; padding: 5px 10px; }
  .badge--pill { padding: 3px 12px; }
}

/* ==========================================================================
   404 — standalone page (404.html)
   Uses the site's own shell and tokens rather than a one-off design: the
   subtle band as the ground, the display face for the numeral, and the
   same two button variants as everywhere else. The numeral scales off the
   viewport so it stays the dominant object on a phone as well as a
   monitor, and it is aria-hidden — "404" read aloud is noise; the heading
   underneath carries the actual meaning.
   ========================================================================== */
.nf {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--bg-subtle);
}
.nf__header {
  flex: none;
  display: flex;
  align-items: center;
  height: var(--header-h);
  padding-inline: var(--gutter);
}
.nf__body {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 48px var(--gutter) 72px;
  text-align: center;
}
.nf__code {
  font-family: var(--font-heading);
  font-weight: var(--weight-bold);
  font-size: clamp(112px, 22vw, 260px);
  line-height: 0.9;
  letter-spacing: -0.03em;
  color: var(--lime-900);
  margin: 0;
}
.nf__title {
  font-weight: var(--weight-semibold);
  font-size: var(--size-h3);
  line-height: var(--lh-h3);
  color: var(--text-primary);
  margin: 20px 0 0;
}
.nf__text {
  font-family: var(--font-body);
  font-size: var(--size-body-lg);
  line-height: var(--lh-body-lg);
  color: var(--text-secondary);
  max-width: 460px;
  margin: 12px 0 0;
}
.nf__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 12px;
  margin-top: 32px;
}
.nf__footnote {
  font-family: var(--font-body);
  font-size: var(--size-body-sm);
  line-height: var(--lh-body-sm);
  color: var(--text-muted);
  margin: 28px 0 0;
}
.nf__footnote a { color: var(--text-accent); text-decoration: underline; text-underline-offset: 2px; }

@media (max-width: 560px) {
  .nf__header { height: var(--header-h-mobile); padding-inline: var(--gutter-mobile); }
  .nf__body { padding-inline: var(--gutter-mobile); }
  .nf__title { font-size: var(--size-h4-m); }
  .nf__text { font-size: var(--size-body); }
  .nf__actions { width: 100%; flex-direction: column; align-items: stretch; }
  .nf__actions .btn { justify-content: center; }
}

/* ==========================================================================
   Mobile pass
   Placed last on purpose: everything here is a deliberate override of a
   desktop-first rule, and putting it at the end means each one wins on
   source order without needing invented specificity to do it.

   Two themes run through the block. First, touch: anything you tap gets to
   at least 44px, which is the smallest target both Apple's and Google's
   guidelines call reliable, and which several controls here were well
   under — the FAQ rows worst of all at 25px. Second, air: the desktop
   rhythm assumes a screen that can hold a whole section at once, and
   transplanted to a 390px viewport its padding stacked up into long empty
   runs between things. The values below tighten that without cramping.
   ========================================================================== */
@media (max-width: 900px) {
  /* ---- Touch targets ---------------------------------------------------- */
  .btn { padding-block: 13px; }
  /* The mark itself is only 26px tall; the link is given the full header
     row so the tappable area matches the thing that looks tappable. */
  .logo { min-height: 44px; }
  .hero__mute { width: 44px; height: 44px; }
  /* The visible row was 25px inside an 18px-padded card: the card looked
     tappable, only the text actually was. */
  .faq-item__header { min-height: 44px; }

  /* 16px is not a type choice — it's the threshold below which iOS Safari
     zooms the page on focus, which on a form this long leaves the user
     scrolled sideways into a field they then have to pinch back out of. */
  .book-form__input,
  .book-form__select {
    font-size: 16px;
    padding-block: 12px;
    min-height: 44px;
  }
  .book-form__select { padding-right: 38px; }

  /* ---- Rhythm ----------------------------------------------------------- */
  .pss-group { padding-block: 48px; }
  .feature-overlay__card { padding-block: 48px; }
  .feature-split__list { gap: 18px; margin-bottom: 32px; }
  .compare-closing { margin-top: 32px; }

  /* The hero balances its content between the header and the piano, so its
     own padding is the one place where a fixed block of air is load-bearing
     rather than leftover. Trimmed, not removed. */
  .hero__content { padding-block: 24px; }

  /* The piano is the only thing on this screen you can actually play with,
     and at 14vh it read as a decorative strip along the bottom edge. Given
     more of the height the hero was spending on empty air, it becomes the
     invitation it was meant to be — and the keys get wide enough to hit.
     Nudged up again from 18.5vh, still short of the original 19vh that
     crowded the stats and CTA above it on short phones. */
  .hero__piano { height: 18.8vh; min-height: 113px; }

  /* The menu's links run the full width of the sheet, so a CTA that stops
     a third of the way across reads as a stray element rather than the
     last row of the same list. */
  .mobile-menu .btn { width: 100%; }

  /* Shown on mobile too, fixed over the piano keys exactly as it is on
     desktop — kept here as a deliberate no-op rather than deleted so the
     mobile pass' shape stays intact if a future device-specific tweak to
     this cue is needed. */
}

/* ==========================================================================
   Mobile pass — composition
   The block above is about touch and rhythm. This one is about what the
   layout is trying to say, which is a different question and needed
   different answers.
   ========================================================================== */
@media (max-width: 900px) {
  /* ---- Two Paths: one section, not three --------------------------------
     On desktop these are frames of a horizontal filmstrip, and the padding
     that separates them is what makes the strip read. Stacked vertically
     that same padding turned them into three unrelated blocks with a
     heading floating above the first — a heading, a thing, another thing.
     The padding moves to the track so the section is padded once, and the
     two paths become bordered cards, which is what actually groups them:
     the border says "these two are a set, and the heading belongs to
     both". */
  .paths-scroller__track {
    /* Extra on top so the heading clears the fixed header rather than
       starting flush against its bottom edge. */
    padding: 80px var(--gutter-mobile) 56px;
    gap: 16px;
  }
  .pss-group {
    padding: 0;
    overflow: visible;
  }
  /* The sheet-music wash behind each group was a desktop flourish that
     survived the port. At this width it lands as fragments cropped by
     whatever the group's height happens to be — visual noise behind the
     one part of the page that has to be read carefully. */
  .pss-art { display: none; }

  .pss-heading {
    text-align: center;
    margin-bottom: 8px;
  }
  .pss-card {
    padding: 28px 20px;
    border: 1px solid var(--border-default);
    border-radius: var(--radius-2xl);
    background: var(--surface-default);
    text-align: center;
    align-items: center;
    gap: 16px;
  }
  .pss-card__tags,
  .pss-card__badges { justify-content: center; }
  /* Centred as a block, left-aligned inside it. Centring the rows
     themselves would leave every wrapped line starting at a different
     place, which is the one thing a checklist can't afford. */
  .path-card__includes {
    text-align: left;
    width: 100%;
    max-width: 30ch;
    margin-inline: auto;
  }

  /* ---- Everything You Need to Start: photo first -------------------------
     Reading order follows the source, which puts four numbered points
     before the picture. The picture is the faster read and the warmer one,
     so on a screen this narrow it goes first and earns the scroll. */
  .feature-split { flex-direction: column; }
  .feature-split__media {
    order: -1;
    /* It inherits a 560px floor from the desktop rule, where it's one of
       two columns filling a tall card. Stacked and full-width that became
       an 850px photo — most of a phone screen spent before a single word
       of the section. Fixed to a band that leads without swallowing. */
    flex: none;
    min-height: 0;
    height: 260px;
  }

  /* ---- Footer: mark, then the icons -------------------------------------
     Side by side on desktop they share a baseline. Stacked, the icons
     first meant four small circles opened the footer and the brand closed
     it, which is backwards — the mark is the thing that should land first. */
  .footer__columns { flex-direction: column; }
  .footer__mark { order: -1; }
}

/* ==========================================================================
   Mobile pass — type scale
   Five steps, and every one of them is a token the system already defines:
   39 / 31 / 16 / 13 / 11 are --size-h3, --size-h4, --size-body,
   --size-body-sm and --size-caption. Written as tokens rather than as
   those numbers so the scale keeps tracking brand/tokens.json instead of
   drifting into a second, hand-maintained set of sizes.

   It replaces a dozen per-element overrides scattered across three media
   queries, each set independently and several of which had quietly stopped
   agreeing with each other. One block, one hierarchy:

     39  the hero's headline, the one piece of display type on the page
     31  the header of every other container
     16  secondary text — the sentence under a heading, body copy
     13  tertiary text — list rows, supporting detail inside a card
     11  captions, eyebrows, field labels, legal
   ========================================================================== */
@media (max-width: 900px) {
  /* 39 — display */
  .hero__title { font-size: var(--size-h3); }

  /* 31 — container headers */
  .section__title,
  .pss-heading__title,
  .founder__title,
  .feature-split__title,
  .pss-card__title { font-size: var(--size-h4); }

  /* The hero's own subhead runs a step below the rest of the secondary text:
     it sits between a 39px headline and the stats, and at 16 it was taking
     four lines out of a viewport the piano also has to fit into. */
  .hero__sub { font-size: var(--size-body-sm); }

  /* 16 — secondary */
  .section__subtext,
  .pss-card__subtext,
  .founder__copy p,
  .feature-split__subtext,
  .compare-closing p,
  .faq-item__question,
  .feature-split__item-body h3,
  .book-form__input,
  .book-form__select { font-size: var(--size-body); }

  /* 13 — tertiary */
  .check-row span,
  .badge,
  .feature-split__item-body p,
  .faq-item__answer,
  .footer__col a,
  .book-form__error { font-size: var(--size-body-sm); }

  /* 11 — captions */
  .eyebrow,
  .book-form__label,
  .book-form__privacy,
  .footer__legal p,
  .hero__mute-caption { font-size: var(--size-caption); }
}

/* ==========================================================================
   Mobile pass — reading order and alignment
   ========================================================================== */
@media (max-width: 900px) {
  /* ---- Everything You Need to Start: heading, image, then the points ----
     The photo led the section, which put a picture before the reader knew
     what they were looking at. Heading first names the thing, the image
     then illustrates it, and the four points follow.
     display:contents is what makes that possible: the header, the list and
     the CTA are wrapped in .feature-split__content, so ordering alone can
     never interleave the photo between them — the photo is the wrapper's
     sibling, not its peer. Dissolving the wrapper promotes all four to
     direct children of the same flex column, where order does work. */
  .feature-split {
    flex-direction: column;
    align-items: flex-start;
    gap: 28px;
  }
  .feature-split__content { display: contents; }
  /* Its own 40px bottom margin now stacks on top of the column gap, since
     the wrapper that used to absorb it is display:contents. The gap alone
     is the spacing. */
  .feature-split__header { order: 1; margin-bottom: 0; }
  .feature-split__media  { order: 2; }
  .feature-split__list   { order: 3; margin-bottom: 0; }
  .feature-split__content > .btn { order: 4; }
  /* The media was flex: 1 1 440px as a column; as a block in the stack it
     needs to claim the full width itself. */
  .feature-split__media { width: 100%; }

  /* ---- Founder: heading, photo, then the story ---------------------------
     Same technique, same reason — the photo led with a face the reader
     doesn't have a name for yet. .founder__story holds the eyebrow and
     heading alongside the bio, signature and CTA; dissolving it promotes
     all five to direct children of .founder, siblings of the photo, where
     order can put the heading before it and the rest after. */
  .founder { flex-direction: column; align-items: center; }
  .founder__story { display: contents; }
  /* order/align-self apply by matching the element itself, not by asking
     it to be a literal DOM child of .founder — display:contents on
     .founder__story only changes how its children participate in the
     flex layout, not their place in the actual DOM tree, so a > combinator
     here would never match. Scoped to the section since .eyebrow is used
     on every section on the page. */
  .founder-section .eyebrow { order: 1; align-self: flex-start; }
  .founder__title        { order: 2; align-self: flex-start; }
  .founder__photo        { order: 3; }
  .founder__copy         { order: 4; align-self: stretch; }
  .founder__signature    { order: 5; align-self: stretch; }
  .founder__signature ~ .btn { order: 6; }

  /* Every pair here now spaces off .founder's own 40px gap instead of
     .founder__story's 26px — display:contents drops that gap along with
     the rest of the box it belonged to. That's fine for most of these
     pairs, but the tag-to-heading rhythm every other section keeps at
     8px (.section__heading) and the run-up to the signature's divider
     both want to be tighter than a flat 40px. Negative margin claws back
     the difference for just these two. */
  .founder__title { margin-top: -32px; }
  .founder__signature { margin-top: -28px; padding-top: 12px; }

  /* ---- Buttons centred --------------------------------------------------
     align-self covers the flex parents, which is most of them; text-align
     catches the few that lay their CTA out as a block. */
  .btn { align-self: center; }
  .founder__signature ~ .btn,
  .feature-split__content > .btn { align-self: center; }
  /* Full-width CTAs must not be shrunk back to their content by the
     centring above. */
  .book-form__submit,
  .mobile-menu .btn { align-self: stretch; }
}


/* ==========================================================================
   Mobile pass — headings centred, heading blocks tightened
   ========================================================================== */
@media (max-width: 900px) {
  /* ---- Centred ----------------------------------------------------------
     .section__heading already centres itself; these two lay their heading
     out inside a left-aligned column, so each needs saying. The subtext
     goes with the headline — a centred title over left-aligned copy reads
     as two decisions rather than one block. */
  .feature-split__header,
  .book__intro {
    align-items: center;
    text-align: center;
  }
  /* .feature-split__content is display:contents at this width, so its
     children inherit .feature-split's own alignment instead. */
  .feature-split { align-items: center; }
  .feature-split__list,
  .feature-split__item { text-align: left; }

  /* ---- Heading to subheading -------------------------------------------
     A headline and the sentence under it are one unit, and at desktop
     spacing the gap between them was reading as a section break. Pulled in
     across the board so every heading block sits at the same rhythm — the
     space below the block is what separates it from what follows, not the
     space inside it. */
  .section__heading { gap: 8px; margin-bottom: 28px; }
  .feature-split__header { gap: 6px; }
  .book__intro { gap: 8px; }
  .hero__content { gap: 12px; }

  /* ---- Form ------------------------------------------------------------- */
  .book-form,
  .book-form__success { padding: 16px; }
}


/* ==========================================================================
   Path card — duration block and fees line
   The duration block is the honest part of each card (how long this really
   takes), so it's set apart from the checklist above it rather than reading
   as a fifth bullet: its own tinted panel, and the closing line italic
   because it's the teacher speaking rather than a spec.
   ========================================================================== */
.pss-card__duration {
  width: 100%;
  padding: 16px 18px;
  border-radius: var(--radius-xl);
  border: 1px solid color-mix(in srgb, var(--brand-accent-text) 26%, transparent);
  background: color-mix(in srgb, var(--brand-accent-text) 5%, transparent);
  text-align: left;
}
.pss-card__duration-title {
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: var(--size-body);
  line-height: 1.3;
  color: var(--text-brand);
  margin: 0 0 6px;
}
.pss-card__duration-body {
  font-family: var(--font-body);
  font-size: var(--size-body-sm);
  line-height: 1.55;
  color: var(--text-secondary);
  margin: 0;
}
.pss-card__duration-note {
  font-family: var(--font-body);
  font-style: italic;
  font-size: var(--size-body-sm);
  line-height: 1.55;
  color: var(--text-accent);
  margin: 8px 0 0;
}
/* Pulled back against the CTA it belongs to: at the card's own 20px gap it
   read as a loose orphan rather than a footnote on the button above it.
   The pull has to live in the margin SHORTHAND — an earlier attempt set
   margin-top above a later `margin: 0` in this same rule, and the shorthand
   reset it to zero every time. */
.pss-card__fees {
  margin: -12px 0 0;
  font-family: var(--font-body);
  font-size: var(--size-caption);
  line-height: 1.45;
  color: var(--text-muted);
}


/* ==========================================================================
   The Week
   ========================================================================== */
.week-section { background: var(--surface-subtle); }
.week { display: flex; flex-direction: column; gap: 28px; max-width: 900px; margin-inline: auto; }

/* The proportions ARE the argument, so the segments are grown from their own
   minute counts — flex-grow: var(--min) — rather than from hand-written
   widths. 30/10/5/5 lands at exactly 60/20/10/10 percent, and editing a
   minute count in the markup can never leave the bar disagreeing with the
   number printed inside it. */
.week-bar {
  display: flex;
  gap: 6px;
  width: 100%;
  min-height: 108px;
}
.week-bar__seg {
  flex: var(--min) 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: 12px 8px;
  border-radius: var(--radius-lg);
  text-align: center;
}
.week-bar__min {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: var(--size-h4);
  line-height: 1;
  color: var(--text-brand);
}
/* The unit rides with the numeral rather than sitting in the label, so a
   narrow 5-minute segment still reads as a duration and not a bare digit. */
.week-bar__unit {
  font-size: var(--size-caption);
  font-weight: 500;
  margin-left: 3px;
}
.week-bar__label {
  font-family: var(--font-body);
  font-size: var(--size-body-sm);
  line-height: 1.3;
  color: var(--text-secondary);
}
/* Four steps of one hue rather than four different colours: the segments
   are parts of a single lesson, not four unrelated categories. */
/* Tint maps to duration, not to reading order: 30 gets the strongest step,
   both 10s share the next, and both 5s share the one after. The old ramp
   gave the two 5-minute blocks different tints purely because one came
   later, which implied a difference between them that doesn't exist. */
.week-bar__seg--playing { background: var(--lime-300); }
.week-bar__seg--theory  { background: var(--lime-200); }
.week-bar__seg--aural   { background: var(--lime-100); }
.week-bar__seg--reading { background: var(--lime-100); }



/* ==========================================================================
   New sections — mobile
   ========================================================================== */
@media (max-width: 900px) {
  /* Three across still works at 390px because the numerals are short; the
     labels wrap rather than the grid collapsing, which keeps the three
     readable as one comparable set. */

  .week { gap: 24px; }

  
  .pss-card__duration { padding: 14px 16px; }
  .pss-card__duration-title { font-size: var(--size-body); }
}

/* ==========================================================================
   Hero stats — value over label, three columns
   The numerals carry it and the words explain them, which is why they're
   stacked rather than inline: read as a run of three, the eye lands on
   250 / 8 / 6 first and picks up the labels after. Hairline rules do the
   separating so the strip needs no box of its own in a hero that is
   already carrying a headline, a CTA and a piano.
   ========================================================================== */
.hero-stats {
  display: flex;
  justify-content: center;
  align-items: stretch;
  gap: 0;
  /* A touch more below than above: the stats belong with the headline they
     back up, and the gap under them is what separates that group from the
     button rather than making the four things read as one stack. */
  margin: 6px 0 14px;
}
.hero-stats__item {
  padding-inline: 28px;
  text-align: center;
}
.hero-stats__item + .hero-stats__item {
  border-left: 1px solid color-mix(in srgb, var(--border-default) 70%, transparent);
}
.hero-stats__value {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: var(--size-h5);
  line-height: 1.1;
  letter-spacing: -0.01em;
  color: var(--text-brand);
  margin: 0;
}
.hero-stats__label {
  font-family: var(--font-body);
  font-size: var(--size-body-sm);
  line-height: 1.3;
  color: var(--text-secondary);
  margin: 5px 0 0;
}

@media (max-width: 900px) {
  /* Three across still, not stacked: stacking would cost the hero three
     rows of height that the piano needs. */
  .hero-stats { margin-block: 4px; }
  .hero-stats__item { padding-inline: 14px; }
  .hero-stats__value { font-size: var(--size-h6); }
  .hero-stats__label { font-size: var(--size-body-sm); }
  /* Horizontal, same as desktop. It ran vertically here for a while because
     the 5-minute blocks get narrow at this width — but a column of stacked
     bars reads as a list, and the whole point of the bar is that you see the
     30 against the 5 in one glance. The GROUPS stack (the check-in header
     can't survive a sixth of 358px), the bars inside them do not. */
  .week-bar { min-height: 92px; }
  .week-bar__seg {
    /* The 26px basis is handed out before anything is grown, so every block
       starts with a readable floor and only the REMAINING width is split by
       minute count. It pulls the 30 in and lets the two 5s breathe.
       This is a deliberate compression of the scale, not the true ratio:
       at 390 the blocks land near 127/60/43/43 rather than a strict
       159/53/26/26. Ranking and order still hold, and the minute figure is
       printed in every block, so nothing is misread — but the widths here
       are indicative rather than exact. Desktop is untouched and stays
       strictly proportional. */
    flex: var(--min) 1 26px;
    flex-direction: column;
    justify-content: center;
    gap: 1px;
    padding: 8px 4px;
    text-align: center;
  }
  /* Everything shrinks to buy width for the two 5-minute blocks, which are
     the constraint: at 390px they land near 35px each and have to hold a
     numeral and a word between them. */
  .week-bar__min { font-size: 19px; line-height: 1; min-width: 0; }
  .week-bar__unit { font-size: 9px; margin-left: 1px; }
  .week-bar__label { font-size: 10px; line-height: 1.15; }
  /* The two 5-minute blocks are the narrowest things on the page — about
     33px of usable width at 360. "Reading" measured 33px there and ran past
     its own edge, which is why the visible label is "Read" (the bar's
     aria-label still says sight-reading in full). These two also drop a
     type step; giving every label the smaller size would shrink Playing and
     Theory for no reason. */
  .week-bar__seg--aural .week-bar__label,
  .week-bar__seg--reading .week-bar__label { font-size: 9px; letter-spacing: -0.01em; }
  .week-bar__seg--aural,
  .week-bar__seg--reading { padding-inline: 2px; }
}

/* Below ~420px "Students Taught" and "Years of Experience" wrap to two
   lines while "Countries" stays on one, so the row's three bottoms land
   at two different heights — measured: all three sit flush again once the
   longer labels are pulled back onto a single line, which just needs a
   little less padding between items and a little less to set per letter.
   Deliberately placed after the 900px block above: it sets the same two
   properties at equal specificity, and a rule earlier in the file would
   lose the tie for every width this one also matches. */
@media (max-width: 420px) {
  .hero-stats__item { padding-inline: 8px; }
  .hero-stats__label { font-size: var(--size-caption); }
}

/* ==========================================================================
   What the trial is — one line under the booking copy
   It was two cards, which gave four sentences' worth of furniture to a fact
   that fits in six words. The 45 minutes is already stated in the sentence
   above; this only has to say how it splits.
   ========================================================================== */
.book__trial {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 14px;
  margin: 14px 0 0;
  font-family: var(--font-body);
  font-size: var(--size-body);
  line-height: 1.5;
  color: var(--text-inverse-muted);
}
/* A dot between the two, drawn rather than typed so it can't end a line. */
.book__trial span + span::before {
  content: '';
  display: inline-block;
  width: 3px;
  height: 3px;
  margin-right: 14px;
  border-radius: 50%;
  background: var(--brand-accent);
  vertical-align: middle;
}

/* ==========================================================================
   The Week — 60-minute chart
   Two groups on one row, sized against each other 50:10 from --group, so the
   row IS the week and the check-in occupies the sixth of it that it really
   is. Inside the lesson group the four blocks divide their 50 the same way.
   Every proportion comes from a number in the markup; none is written here.
   ========================================================================== */
.week-chart {
  display: flex;
  align-items: stretch;
  gap: 18px;
  width: 100%;
}
.week-group {
  flex: var(--group) 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
/* nowrap plus smaller type because the check-in group is only a sixth of the
   row: at the lesson group's size "Midweek check-in · 10 min" broke onto two
   lines there and one everywhere else, which made the two headers look like
   different components. */
.week-group__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin: 0;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border-default);
  white-space: nowrap;
}
.week-group__name {
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 14px;
  color: var(--text-primary);
}
.week-group__len {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-accent);
}
.week-group__foot {
  font-family: var(--font-body);
  font-size: var(--size-caption);
  line-height: 1.4;
  color: var(--text-muted);
  margin: 0;
}
/* The check-in shares the bar's lime family rather than sitting in a white
   card, so the eye reads it as another slice of the same hour. */
/* Ten minutes, so it takes the same step as Theory — the dashed edge is what
   marks it as the separate day, not a different colour. */
.week-bar__seg--checkin {
  background: var(--lime-200);
  border: 1px dashed color-mix(in srgb, var(--text-accent) 45%, transparent);
}

@media (max-width: 900px) {
  /* Stacked, and the groups stop competing for width — each one gets the
     full row and the bar inside it keeps its own vertical proportions. */
  /* Stays a row. Stacking the two groups turned one chart into two lists and
     lost the thing the chart is for — seeing the check-in against the lesson
     it belongs to. What made stacking necessary was the header: "Midweek
     check-in · 10 min" cannot fit a sixth of 358px on one line. The header
     stacks instead of the chart, and the word "Midweek" moves into the
     example below, where it was doing more work anyway. */
  .week-chart { gap: 8px; }
  .week-group__head {
    flex-direction: column;
    align-items: flex-start;
    gap: 1px;
    padding-bottom: 6px;
  }
  .week-group__name { font-size: 12px; }
  .week-group__len { font-size: 11px; }
}



/* ==========================================================================
   Locked Doors
   Token mapping for the brief's illustrative hexes — no raw colour here:
     --line             -> --border-strong  (slate-300, reads on white)
     --surface          -> --surface-muted  (slate-100, so a locked face is
                           visible against the section's own white)
     --accent           -> --brand-accent   (lime-300, the CTA lime)
     --accent-contrast  -> --text-on-accent (indigo-950, the existing pairing
                           already used for text sitting on that lime)
   ========================================================================== */
.doors-block {
  display: grid;
  grid-template-columns: 1.05fr 1fr;
  gap: clamp(28px, 5vw, 64px);
  align-items: center;
  max-width: 980px;
  margin-inline: auto;
}
.doors-block__figure { display: flex; flex-direction: column; gap: 16px; }

/* Four columns, no auto-fit and no wrap: the row of four IS the argument.
   Two-by-two would read as four doors rather than as one open and three
   shut, so this stays a fixed 4-track grid at every width. */
.doors {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(8px, 1.6vw, 16px);
  max-width: 420px;
}
/* grid-auto-flow places the four labels; the inputs are taken out of flow
   by their own absolute positioning above, so they never take a cell. */
.door {
  display: block;
  cursor: pointer;
  --door-swing: 1;
  --door-line: var(--border-strong);
  --door-face: var(--surface-muted);
  --door-detail: var(--border-strong);
  /* The colour lands faster than the swing finishes, so the door reads as
     "this one" before it has finished moving. */
  transition: --door-swing 0.34s cubic-bezier(0.34, 0.9, 0.3, 1),
              --door-line 0.2s ease,
              --door-face 0.2s ease,
              --door-detail 0.2s ease;
}
.door svg {
  width: 100%;
  height: auto;
  display: block;
  /* The frame stroke sits on the viewBox edge, so without this the arch tops
     and the sill get clipped by half a stroke width. */
  overflow: visible;
}

/* The state is carried by custom properties, not by descendant selectors,
   and that is not a style preference — it is the only thing that works.
   <use> clones the symbol into a shadow tree, and document CSS cannot select
   into it: `.door--open .door__face` never matches the cloned node, because
   the node it would match is the original inside <symbol>, which is not a
   descendant of .door--open. Written that way (as the brief drafted it) all
   four doors render in the base state and the open one is silently lost.
   Custom properties DO inherit across the shadow boundary, so the symbol
   paints from vars and each host sets them. */
.door {
  --door-line: var(--border-strong);
  --door-face: var(--surface-muted);
  --door-panel: var(--border-strong);
  --door-knob: var(--border-strong);
  --door-panel-opacity: 1;
  --door-keyhole-opacity: 1;
}
/* Registered so the browser knows what these are and can interpolate them.
   An unregistered custom property is just a string, and a string can only
   snap from one value to the next — the swing would have no motion at all.
   inherits: true is what carries them across the <use> shadow boundary,
   which is the only route into a cloned symbol. */
@property --door-swing {
  syntax: '<number>'; inherits: true; initial-value: 1;
}
@property --door-line {
  syntax: '<color>'; inherits: true; initial-value: transparent;
}
@property --door-face {
  syntax: '<color>'; inherits: true; initial-value: transparent;
}
@property --door-detail {
  syntax: '<color>'; inherits: true; initial-value: transparent;
}

/* Open: the door itself goes lime and swings back. The accent is on the
   LEAF, not on what's behind it — the thing you touched is the thing that
   lights up, and the space it leaves is just space. Hover on pointer
   devices, :checked on touch; the media query below stops the two ever both
   applying and leaving two doors open at once. */
.doors:hover .door:hover,
.door__input:checked + .door {
  /* 0.3, not 0.1 — the leaf keeps 30% of its width, which is roughly a door
     standing 70 degrees open (cos 70° ≈ 0.34). Swung further it foreshortens
     to a sliver and the panels, knob and keyhole stop being legible: the door
     is open but you can no longer see it is a door. */
  --door-swing: 0.3;
  --door-line: var(--brand-accent);
  --door-face: var(--brand-accent);
  --door-detail: var(--text-on-accent);
}

/* A mouse can hover, so hover is the whole interaction and leaving shuts the
   door. But a click ALSO checks the radio, which would leave that door open
   behind you — so :checked is neutralised on any door not currently hovered.
   Without this you can click one door, move to another, and have two open. */
@media (hover: hover) and (pointer: fine) {
  .door__input:checked + .door:not(:hover) {
    --door-swing: 1;
    --door-line: var(--border-strong);
    --door-face: var(--surface-muted);
    --door-detail: var(--border-strong);
  }
}

/* The radios are the mechanism, never the picture. */
.door__input {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%);
  white-space: nowrap; border: 0;
}

/* What's behind the door is the page, not a colour: an opening should read
   as empty. It's painted rather than left transparent so the leaf has
   something opaque to swing away from. */
.door__way     { fill: var(--surface-default); }

/* scaleX from the hinge, not rotateY: a 3D rotation inside a cloned <symbol>
   is where browser support gets thin, and horizontal foreshortening is what
   the eye actually reads as a door swinging away. transform-box: fill-box
   makes "left" mean the leaf's own left edge rather than the SVG viewport's. */
.door__leaf {
  transform-box: fill-box;
  transform-origin: left center;
  transform: scaleX(var(--door-swing));
}

.door__frame   { fill: none; stroke: var(--door-line); stroke-width: 1.5; }
.door__face    { fill: var(--door-face); stroke: var(--door-line); stroke-width: 1; }
.door__panel   { fill: none; stroke: var(--door-detail); stroke-width: 1; }
.door__knob    { fill: var(--door-detail); }
.door__keyhole { fill: var(--door-detail); }
.door__sill    { stroke: var(--door-line); stroke-width: 1.5; }

.doors__caption {
  font-family: var(--font-mono);
  font-size: var(--size-caption);
  line-height: 1.5;
  letter-spacing: 0.02em;
  color: var(--text-muted);
  margin: 0;
  max-width: 420px;
}

/* The only copy in this column now, so it steps up from body to body-lg:
   with no heading above it, it has to hold the right-hand side on its own. */
.doors-block__body {
  font-family: var(--font-body);
  font-size: var(--size-body-lg);
  line-height: 1.6;
  color: var(--text-secondary);
  margin: 0;
}

/* The rattle is gone on purpose. It meant "this one will not open", which
   was true when three doors were permanently shut — and is now the exact
   opposite of what hovering a door does. */
@media (prefers-reduced-motion: reduce) {
  .door { transition: none; }
}

@media (max-width: 760px) {
  .doors-block { grid-template-columns: 1fr; gap: 32px; }
  /* The figure takes the full column here, so the doors can use all of it
     rather than staying pinned to the desktop 420px. */
  .doors, .doors__caption { max-width: none; }
  .doors-block__body { font-size: var(--size-body); }
}

/* ==========================================================================
   Inline legal links — the privacy line under the form and the copyright
   line in the footer. The global `a { color: inherit; text-decoration:
   none }` reset would otherwise make these indistinguishable from the
   sentence around them.
   ========================================================================== */
.book-form__privacy a,
.footer__legal a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
  white-space: nowrap;
}
.book-form__privacy a:hover,
.footer__legal a:hover { text-decoration-color: currentColor; }

/* ==========================================================================
   Booking form — spam protection hooks
   ========================================================================== */
/* Honeypot. Positioned off-screen rather than display:none, because the
   crudest bots skip fields they can tell are hidden and the point is for
   them to fill it in. aria-hidden + tabindex=-1 in the markup keep it away
   from keyboard and screen-reader users. */
.book-form__trap {
  position: absolute;
  left: -10000px;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
/* Turnstile renders nothing for almost everyone (interaction-only), so the
   slot takes no space until Cloudflare actually needs to show a challenge. */
.book-form__turnstile:empty { display: none; }
.book-form__turnstile { display: flex; justify-content: center; }

/* ==========================================================================
   Legal pages (privacy.html)
   Long-form reading page on the same tokens as the rest of the site: light
   ground, a single comfortable measure, Oswald for headings and Inria Serif
   for the running text.
   ========================================================================== */
.legal-page { background: var(--bg-subtle); }

.legal__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  height: var(--header-h);
  padding-inline: var(--gutter);
  border-bottom: 1px solid var(--border-default);
  background: var(--surface-default);
}
.legal__back { padding-block: 9px; }

.legal { padding: 72px var(--gutter) 96px; }
.legal__inner { max-width: 720px; margin-inline: auto; }
/* .eyebrow is centred for section headings elsewhere on the site; on a
   left-aligned reading page it has to sit on the same edge as the text. */
.legal__inner > .eyebrow { text-align: left; justify-content: flex-start; margin: 0; }

.legal__title {
  font-weight: var(--weight-bold);
  font-size: var(--size-h1);
  line-height: var(--lh-h1);
  letter-spacing: var(--track-h1);
  color: var(--text-primary);
  margin: 12px 0 0;
}
.legal__lede {
  font-family: var(--font-body);
  font-size: var(--size-body-lg);
  line-height: var(--lh-body-lg);
  color: var(--text-secondary);
  margin: 20px 0 0;
}

.legal__section {
  margin-top: 48px;
  padding-top: 32px;
  border-top: 1px solid var(--border-default);
}
.legal__section h2 {
  font-weight: var(--weight-semibold);
  font-size: var(--size-h5);
  line-height: var(--lh-h5);
  color: var(--text-primary);
  margin: 0 0 14px;
}
.legal__section p,
.legal__section li {
  font-family: var(--font-body);
  font-size: var(--size-body);
  line-height: var(--lh-body);
  color: var(--text-secondary);
}
.legal__section p + p,
.legal__section ul + p,
.legal__section p + ul,
.legal__section .legal__table-wrap + p { margin-top: 12px; }
.legal__section p + .legal__table-wrap { margin-top: 16px; }
.legal__section ul { margin: 0; padding-left: 22px; }
.legal__section li + li { margin-top: 6px; }
.legal__section strong { color: var(--text-primary); }
.legal__section a {
  color: var(--text-accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.legal__table-wrap {
  overflow-x: auto;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-xl);
  background: var(--surface-default);
}
.legal__table { width: 100%; border-collapse: collapse; min-width: 460px; }
.legal__table th,
.legal__table td {
  text-align: left;
  vertical-align: top;
  padding: 12px 16px;
  font-family: var(--font-body);
  font-size: var(--size-body-sm);
  line-height: var(--lh-body-sm);
  border-bottom: 1px solid var(--border-default);
}
.legal__table tr:last-child td { border-bottom: 0; }
.legal__table th {
  font-family: var(--font-heading);
  font-weight: var(--weight-medium);
  font-size: var(--size-body-sm);
  color: var(--text-primary);
  background: var(--surface-subtle);
}
.legal__table td:first-child { color: var(--text-primary); white-space: nowrap; font-weight: var(--weight-bold); }
.legal__table td { color: var(--text-secondary); }

.legal__contact { list-style: none; padding-left: 0 !important; }

.legal__footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 24px var(--gutter);
  border-top: 1px solid var(--border-default);
  background: var(--surface-default);
  font-family: var(--font-body);
  font-size: var(--size-body-sm);
  color: var(--text-muted);
}
.legal__footer p { margin: 0; }
.legal__footer a { color: var(--text-muted); text-decoration: underline; text-underline-offset: 2px; }

@media (max-width: 560px) {
  .legal__header { height: var(--header-h-mobile); padding-inline: var(--gutter-mobile); }
  .legal { padding: 44px var(--gutter-mobile) 64px; }
  .legal__title { font-size: var(--size-h1-m); line-height: 1.15; }
  .legal__lede { font-size: var(--size-body); }
  .legal__section { margin-top: 36px; padding-top: 24px; }
  .legal__footer { padding-inline: var(--gutter-mobile); flex-direction: column; text-align: center; }
}
