/* AvoLingo — shared additions for the multi-page site (English + Korean).
   Loaded after tokens.css and styles.css on every public page.
   Every value resolves to a token that already exists; no new colour,
   radius, shadow or type step is introduced here. */

/* ============ Korean typography ============ */

/* Latin glyphs stay DM Sans; Hangul falls through to Pretendard. */
:root:lang(ko) {
  --font-sans: "DM Sans", "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont,
               "Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", sans-serif;
}
/* Break Korean between words, never inside one. */
:lang(ko) body { word-break: keep-all; overflow-wrap: break-word; }
/* The 0.9 display leading is tuned for Latin caps; Hangul needs more room. */
:lang(ko) .headline,
:lang(ko) .h1,
:lang(ko) .h2,
:lang(ko) .heading { line-height: 1.16; letter-spacing: -0.02em; }
:lang(ko) .subheading,
:lang(ko) .body { letter-spacing: -0.005em; }
/* styles.css writes the step label in English; Korean pages get their own */
:lang(ko) .step__num::before { content: "단계 " counter(step, decimal-leading-zero) " — "; }

/* ============ Header state ============ */

/* NAV ALIGNMENT — HOMEPAGE ONLY (ruling after round 4, bar-hero.md).
   On the homepage the logo's left edge and the right-hand button's right edge
   line up with the hero card, as on the reference: the nav's content box
   reads the same --stage-edge as the card (tokens.css), min(viewport - 80px,
   1680px) centred, so the two cannot drift apart. `.nav__links
   { margin-inline: auto }` already pins the logo flush left and the actions
   (or the <=960 menu toggle) flush right, so moving the box moves both ends.
   SCOPE: `body:has(.hero-slider)` matches only a page that carries the hero
   — index.html and ko/index.html. Every other page keeps §9.2's nav aligned
   to its own 72rem .wrap content, exactly as before round 4 (round 4 applied
   this to all 18 public pages and left the logo 48-167px off their content
   edge). `:has()` is already relied on by the nav in styles.css.
   The mobile menu lives inside this same .wrap: on the homepage it moves in
   with the logo (20px at <=640, 40px at 641-960); elsewhere it is unchanged. */
/* 2026-10-09 (user): the header sits in the SAME place on every page — the
   homepage's edges — so the logo and the buttons never move between pages.
   The old `body:has(.hero-slider)` scope (homepage only) is dropped. */
.nav > .wrap {
  max-width: none;
  padding-inline: var(--stage-edge);
}

/* "page" = this exact page; "true" = a page inside that section (e.g. product.html under Product) */
.nav__link[aria-current="page"],
.nav__link[aria-current="true"] {
  color: var(--color--grey-1000);
}
.nav__dropitem[aria-current="page"] { background: var(--color--grey-50); }
.nav__droplang[aria-current="true"] { color: var(--color--grey-1000); font-weight: 500; }

/* ============ Layout helpers ============ */

/* copy beside a visual; stacks under 960px */
.split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--sp-40);
  align-items: center;
}
.split--top { align-items: start; }

/* ============ Checklist ============ */

.checklist { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-12); }
.checklist li {
  display: flex; align-items: flex-start; gap: var(--sp-8);
  font-size: var(--fs-body-m); line-height: var(--lh-prose);
  color: var(--color--grey-800);
}
.checklist li::before {
  content: "";
  flex: none;
  width: 1.3em; height: 1.3em; margin-top: 0.12em;
  border-radius: var(--radius--circle);
  background: var(--color--green-100) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='m3 6.2 2 2 4-4.4' fill='none' stroke='%23003d3d' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 62% no-repeat;
}
.checklist b { color: var(--color--grey-1000); font-weight: 500; }
.hero__copy > .checklist { margin-top: var(--sp-24); }
/* Contact (user, 2026-10-11): the tutor in a headset (Higgsfield Topaz 4K) replaces the
   checklist, flush with the column's right edge so her open hand points at the form; her
   white ground is the page's white. "한눈에 들어오는 사이즈… 심미적으로": a head-to-elbow
   crop that always fits the first screen with the headline. The column is one screen tall
   (viewport − nav − the hero's top padding − a small gap) and the figure takes the height
   the copy leaves, drawn bottom-right. The fade starts below her hand (hand ≤ 86% of the
   crop). */
.hero__copy:has(> .contact-figure) {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  height: clamp(34rem, calc(100svh - var(--nav-h) - var(--sp-40) - var(--sp-12)), 58rem);
}
.contact-figure {
  display: block;
  flex: 1 1 0;
  min-height: 16rem;
  width: 100%;
  object-fit: contain;
  object-position: right bottom;
  margin-top: var(--sp-12);
  -webkit-mask-image: linear-gradient(to bottom, #000 88%, transparent);
          mask-image: linear-gradient(to bottom, #000 88%, transparent);
}
@media (max-width: 860px) {
  .hero__copy:has(> .contact-figure) { height: auto; }
  .contact-figure { flex: none; min-height: 0; height: auto; width: min(60%, 16rem); align-self: center; }
}
@media (max-width: 640px) { .contact-figure { display: none; } }   /* phones: the form follows the copy directly */

/* ============ Branch — "if this happened → next comes this" ============ */

.branch { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-12); }
.branch__card {
  display: grid; align-content: start; gap: var(--sp-8);
  padding: var(--sp-20);
  background: var(--color--white);
  border-radius: var(--radius--l);
  box-shadow: var(--shadow--card);
}
.branch__if { margin: 0; font-size: var(--fs-body-xs); color: var(--color--grey-600); line-height: var(--lh-prose); }
.branch__then {
  margin: 0;
  font-size: var(--fs-h5); font-weight: 500; line-height: var(--lh-heading);
  letter-spacing: var(--kern-heading); color: var(--color--grey-1000);
}

/* ============ FAQ — native details/summary ============ */

.faq { max-width: 52rem; margin-inline: auto; border-top: 1px solid var(--color--grey-100); }
.faq details { border-bottom: 1px solid var(--color--grey-100); }
.faq summary {
  list-style: none;
  cursor: pointer;
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-16);
  padding-block: var(--sp-20);
  font-size: var(--fs-h5); font-weight: 500; line-height: var(--lh-heading);
  letter-spacing: var(--kern-heading); color: var(--color--grey-1000);
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: "+";
  flex: none;
  display: grid; place-items: center;
  width: 1.8em; height: 1.8em;
  border-radius: var(--radius--circle);
  background: var(--color--grey-100); color: var(--color--grey-800);
  transition: transform 300ms var(--ease);
}
.faq details[open] summary::after { transform: rotate(45deg); }
.faq details > p {
  margin: 0 0 var(--sp-20);
  max-width: 60ch;
  font-size: var(--fs-body-m); line-height: var(--lh-prose); color: var(--color--grey-800);
}

/* ============ Forms ============ */

.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-16); }
.form-grid .field--full { grid-column: 1 / -1; }
textarea.input { min-height: 7.2rem; resize: vertical; line-height: var(--lh-prose); }
.form-success { display: grid; gap: var(--sp-12); text-align: left; }
/* a component's own display must never beat the hidden attribute */
[hidden] { display: none !important; }
.form-success:focus { outline: none; }

/* ============ #needs — Monzo-style card carousel ============

   Built to bar-needs-carousel.md (reference: monzo.com, "Quit the app juggling
   act"). Its eight mechanisms, and where each lives below:
     1. centred head: pill, h2, one sentence, ONE primary button
     2. a wide 2:1 card, text in the left half, a SQUARE video in the right
     3. text vertically centred in its half: title, paragraph, link — nothing else
     4. every card a different pastel, each with its own family's inks (all AA)
     5. one card centred, both neighbours peeking, gap ~4% of the card, loops
     6. one step = one card + gap, fast-start ease-out; autoplay every 8s
     7. a centred control row under the cards: pause/play, dots, next
     8. phones (<= 640): no carousel, a vertical stack, video on top

   Structure, format, box size and media-in-box are the reference's; colour,
   type, pill and buttons are ours. Behaviour: [data-needs] in js/main.js.

   RADIUS. Monzo's corner (bar-needs-overlay.md mechanism 1): 64px on a 565px
   card = 0.113 x the card's height. --radius--3xl (57.8px at 1440, 0.103 of
   the 563px card) is the ladder step nearest; at <= 960 the card is ~320-400px
   tall and --radius--2xl is nearest (38px at 768 against a 36px target; 35.8px
   on a 375 phone against Monzo's 32). The video takes the same value on all
   four corners. NO outline or hairline (user direction 2026-09-27).

   CARD WIDTH. 62.4rem is 1126px at 1440 against the reference's 1129px. It is
   capped at 84vw — §10.3's own "carousel slide -> 84%" — so a 1024 screen still
   shows both neighbours (card 860, peeks ~39px a side). A rem literal on the
   same footing as the old row's `min(80rem, ...)`: there is no width token
   near it (--max-width is 72rem).

   GAP. --sp-24 is 43.3px at 1440 = 3.85% of the card, the step nearest the
   reference's 48/1129 = 4.25% (--sp-32 would be 5.1%). */
.needs__head {
  max-width: 64rem;
  margin: 0 auto var(--sp-40);
  text-align: center;
}
/* §5.4's pill -> heading gap is styles.css's adjacency rule `.pill + h2`;
   restating it here would double it. §3.3: the muted slot is RE-THEMED for this
   head, not the colour repainted on the component. */
.needs__head { --_theme---page--text-muted: var(--color--grey-800); }
/* The reference's sub-copy is 20px / 1.4 at a muted ink: .subheading is body-l
   (21.7px at 1440) at 1.4, the nearest rank. 36rem keeps it inside a 45-75ch
   measure while the head column is wider. */
.needs__head .subheading {
  margin: var(--sp-16) auto 0;
  max-width: 36rem;
}
/* The single CTA: the site's primary button, centred by the head's
   text-align (it is inline-flex). */
.needs__cta { margin-top: var(--sp-24); }
/* §4.1.2 — the Hangul heading row (1.2 / -0.016em) as a section-level override
   of site.css's global 1.16 / -0.02em, which design-system.md records as a
   known non-conformance naming #needs. */
:lang(ko) #needs .h2 { line-height: var(--lh-heading); letter-spacing: var(--kern-heading); }
:lang(ko) #needs .subheading { line-height: var(--lh-ko-body); letter-spacing: normal; }

/* ---- 5. The stage ---------------------------------------------------------
   The carousel sits OUTSIDE .wrap so the neighbours can run to the viewport's
   edges. `overflow-x: clip` keeps the off-stage cards (+-2 steps) from ever
   widening the page; clip, not hidden, so the y axis stays visible and does
   not become a scroll container. main already clips (styles.css) — this is
   the section's own guarantee, not a reliance on it.

   Every card sits in the SAME grid cell, centred, and is moved off it by
   --_pos whole steps. js/main.js writes --_pos (-1 left neighbour, 0 active,
   1 right neighbour, 2 waiting off-stage right). The nth-child defaults below
   are that same starting state, so the first paint (and a page without JS)
   already shows card 1 centred with 4 and 2 peeking — nothing jumps when the
   script arrives. No clones: each clip exists once, so a video never restarts
   when the loop wraps. */
.needs-carousel {
  --_card-w: min(62.4rem, 84vw);
  --_gap: var(--sp-24);
  --_step: calc(var(--_card-w) + var(--_gap));
  overflow-x: clip;
}
.needs-carousel__slides {
  display: grid;
  grid-template-columns: var(--_card-w);
  justify-content: center;
}
.needs-card:nth-child(1) { --_pos: 0; }
.needs-card:nth-child(2) { --_pos: 1; }
.needs-card:nth-child(3) { --_pos: 2; }
.needs-card:nth-child(4) { --_pos: -1; }
/* Monzo's opening frame: nothing on the left until the row first moves
   (js/main.js removes .is-fresh). Desktop/tablet only — phones stack. */
@media (min-width: 641px) {
  .needs-carousel.is-fresh .needs-card:nth-child(4) { visibility: hidden; }
}

/* ---- 2 & 4. The card ------------------------------------------------------
   Two equal columns on an exact 2:1 box, so the right column is a square by
   construction. overflow: hidden with a radius clips everything to the card's
   outer corners; the video (and its overlay) repeat the same --_radius on
   their own four corners, so the inner edge is rounded too, as Monzo's is. */
.needs-card {
  grid-area: 1 / 1;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  aspect-ratio: 2 / 1;
  --_radius: var(--radius--3xl);
  border-radius: var(--_radius);
  overflow: hidden;
  isolation: isolate;
  background: var(--_surface);
  color: var(--_heading);
  transform: translateX(calc(var(--_pos) * var(--_step)));
  /* 6. The step. --ease-global (§8.1's global curve, an ease-out-cubic) is
     the published curve nearest the reference's measured profile: it reaches
     61% of the travel by 150ms and 98% by 400ms of 550ms, against the
     reference's ~50% / ~95%. --ease-nav (quartic) front-loads harder (72% /
     99%). The DURATION is a recorded deviation — see --needs-step-dur in
     tokens.css. */
  transition: transform var(--needs-step-dur) var(--ease-global);
}
/* The wrap-around: a card leaving at -2 is re-seated at +2 while off-stage,
   with its transition switched off for that one frame (js/main.js). */
.needs-card.is-seating { transition: none; }

/* §3.2 — each card a different pastel, paired with its own family's inks
   (tokens.css). User direction 2026-09-27, after Lattice's "Trusted by top
   performers" cards. */
.needs-card--individual { --_surface: var(--_theme---needs--individual-surface); --_heading: var(--_theme---needs--individual-heading); --_text: var(--_theme---needs--individual-text); }
.needs-card--k12        { --_surface: var(--_theme---needs--k12-surface);        --_heading: var(--_theme---needs--k12-heading);        --_text: var(--_theme---needs--k12-text); }
.needs-card--university { --_surface: var(--_theme---needs--university-surface); --_heading: var(--_theme---needs--university-heading); --_text: var(--_theme---needs--university-text); }
.needs-card--corporate  { --_surface: var(--_theme---needs--corporate-surface);  --_heading: var(--_theme---needs--corporate-heading);  --_text: var(--_theme---needs--corporate-text); }

/* ---- 3. The text half -----------------------------------------------------
   Vertically centred, left-aligned. Side padding is --sp-32, the same length
   as the card's radius (both 3.2rem), so the column's edge starts where the corner's curve
   ends — the reference pads 48px inside a 64px corner for the same reason. */
.needs-card__text {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-start;
  min-width: 0;
  padding: var(--sp-24) var(--sp-32);
}
/* Title: the h4 rank (28.9px at 1440; the reference sets 31px). */
.needs-card__title {
  font-size: var(--fs-h4);
  font-weight: 500;
  line-height: var(--lh-snug);
  letter-spacing: var(--kern-heading);
  color: var(--_heading);
}
/* Body: body-s (16.2px at 1440; the reference's 16px) at the rich-text 1.4. */
.needs-card__body {
  margin-top: var(--sp-12);
  font-size: var(--fs-body-s);
  line-height: var(--lh-prose);
  color: var(--_text);
}
/* The CTA: Lattice's white pill ("Read the story") — the house .pill (§9.3)
   re-themed white at 80% with the page ink, text left and the arrow circle
   right. */
.needs-card__link {
  --_theme---pills--pill-background: var(--_theme---needs--button);
  --_theme---pills--pill-text: var(--_theme---needs--button-text);
  --_theme---pills--icon-background: var(--color--white);
  margin-top: var(--sp-20);
  gap: var(--sp-16);
  padding: 0.6em 0.6em 0.6em 1.2em;
  font-size: var(--fs-body-s);
}
:lang(ko) .needs-card__title { line-height: var(--lh-heading); }
:lang(ko) .needs-card__body  { line-height: var(--lh-ko-body); }

/* ---- 2. The media half: a square, edge to edge ----------------------------
   The clips are 832x1114 portrait, so the square crops height only; each
   card's --_focus-y keeps its subjects' faces in frame. */
.needs-card__media { position: relative; min-width: 0; }
.needs-card__video {
  position: absolute; inset: 0;
  display: block;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: 50% var(--_focus-y, 50%);
}
/* Monzo: the photo is rounded on all four corners — its inner (left) corners
   too, not only the two it shares with the card. Same ladder step as the card. */
.needs-card__video { border-radius: var(--_radius); }
.needs-card--individual { --_focus-y: 35%; }
.needs-card--k12        { --_focus-y: 75%; }
.needs-card--university { --_focus-y: 30%; }
.needs-card--corporate  { --_focus-y: 40%; }

/* ---- The practice lines on the video — bar-needs-overlay.md ----------------
   Monzo's white list that writes itself over the photo ("Salary ... +£2,600"),
   as LIVE text: four rows, each a speaking situation on the left and a short
   English line for it on the right (placeholder copy, 1-3 words a side).

   TYPE. Row 1 one rank up — h4 over h5 (28.9 / 21.7px at 1440, 1.33x; the
   reference's ~1.3x), both scaled with the square (5.1% / 3.9% of it at 1440,
   the reference's proportions). Weight 500, the real weight (§4.1.1), heading
   tracking. Rows sit --sp-4 apart, so baseline to baseline is ~1.5x the type
   size (the reference's 1.3-1.6x). Label left, value right-aligned: two edges,
   one grid, like the reference's list.

   PLACEMENT. Insets --sp-32 (57.8px at 1440 = 10.3% of the square; the
   reference sits its text ~11% in). NO scrim or darkening layer of any kind
   (user ruling 2026-09-27): legibility comes from placement and the glyphs.
   The block is anchored to the BOTTOM band of the square, measured per clip
   to be clear of every face (faces sit in the upper 10-62% of all four
   crops) and, within that, the darker part of the frame. --_say-y lifts one
   card's block where its bottom band is brighter (unitless share of the
   free height, 0 = bottom). Plain white glyphs: no text-shadow, box, band
   or gradient (user ruling 2026-09-28).

   MOTION (mechanisms 3-5). js/main.js splits each label and value into word
   masks (inline-block, clipped top and bottom) holding one inline-block per
   character, and drives data-say on the list:
     (none) plain text, at rest — no JS, or the split never ran
     idle   every character parked one line-box below its baseline, clipped
     play   each character rises to rest over §8.1's Global pair, started at
            lead + row x --needs-say-row + slot x --needs-say-stagger
     done   at rest, no animation (paused, reduced motion)
   `both` fill holds the finished list after the last character lands. */
.needs-say {
  --_inset: var(--sp-32);
  position: absolute;
  z-index: 1;
  left: var(--_inset);
  right: var(--_inset);
  bottom: calc(var(--_inset) + var(--_say-y, 0) * (100% - 2 * var(--_inset)));
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  margin: 0;
  padding: 0;
  list-style: none;
  color: var(--_theme---needs--say-text);
  pointer-events: none;
}
.needs-say li {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--sp-16);
  font-size: var(--fs-h5);
  font-weight: 500;
  line-height: var(--lh-heading);
  letter-spacing: var(--kern-heading);
  white-space: nowrap;
}
.needs-say li:first-child { font-size: var(--fs-h4); }
.needs-say__value { text-align: right; }
/* The word is the line mask: clipped above and below its own line box, open
   to the sides so the heading's negative tracking never shaves a glyph. The
   extra bottom room keeps descenders inside it. */
.needs-say__word {
  display: inline-block;
  vertical-align: top;
  clip-path: inset(0 -0.25em -0.08em);
}
.needs-say__ch { display: inline-block; }
.needs-say[data-say="idle"] .needs-say__ch { transform: translateY(110%); }
.needs-say[data-say="play"] .needs-say__ch {
  animation: needs-say-rise var(--dur) var(--ease-global) both;
  animation-delay: calc(var(--needs-say-lead) + var(--r, 0) * var(--needs-say-row) + var(--i, 0) * var(--needs-say-stagger));
}
@keyframes needs-say-rise { from { transform: translateY(110%); } to { transform: none; } }
/* Per clip, measured at 1440 (white against the footage under each row over
   16 frames; faces located with the browser's FaceDetector on the same
   crops). Faces end at 47 / 26 / 51 / 60% of the square's height
   (individual / K-12 / university / corporate), so every block stays in the
   bottom band; the lift picks the darker strip within it. */
.needs-card--individual .needs-say { --_say-y: 0.1; }   /* 58-82%: sweaters, table edge */
.needs-card--university .needs-say { --_say-y: 0.1; }   /* 58-82%: jackets, jeans */

/* ---- 7. The control row ---------------------------------------------------
   Centred under the cards at --sp-16. Restyled 2026-09-27 at the user's
   request to be THE HERO'S CONTROLS, placed below instead of on the sides:
   the same 44px white arrow discs with their soft shadow and 1.8-stroke
   chevrons (.hero-slider__arrow, M3), a PREVIOUS arrow added to match the
   hero's pair, and the same 6px pips / 24px active pip / 8px gap with the
   dark dwell fill (.hero-slider__dot, M4) — all from the hero's tokens.
   Order: pause, previous, dots, next. The pause disc is kept (it stops the
   autoplay AND the looping videos, WCAG 2.2.2) and wears the arrows' disc
   so the row reads as one set. Unlike the hero's arrows these are always
   visible: they sit on the white page, not over a photo. */
/* Placed like Monzo's row (bar-needs-carousel.md M7: centred, 32px under
   the cards, the dots in the middle): a 1fr | auto | 1fr grid puts the DOTS
   on the section's centre line whatever sits either side — pause + previous
   end-aligned on the left, next start-aligned on the right. A flex row
   centred the whole group instead, which pushed the dots right of centre by
   the pause button's width. */
.needs-carousel__controls {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  column-gap: var(--sp-8);
  margin-top: var(--sp-16);
}
.needs-carousel__toggle { grid-column: 1; grid-row: 1; justify-self: end; margin-inline-end: calc(var(--hero-arrow) + var(--sp-8) + var(--sp-8)); }
.needs-carousel__prev { grid-column: 1; grid-row: 1; justify-self: end; }
.needs-carousel__dots { grid-column: 2; }
.needs-carousel__next { grid-column: 3; justify-self: start; }
.needs-carousel__btn {
  display: grid;
  place-items: center;
  inline-size: var(--hero-arrow);
  block-size: var(--hero-arrow);
  padding: 0;
  border: 0;
  border-radius: var(--radius--circle);
  background: var(--_theme---hero--control);
  color: var(--_theme---hero--control-icon);
  box-shadow: var(--_theme---hero--arrow-shadow);
  cursor: pointer;
  font-size: var(--fs-body-m);
}
.needs-carousel__btn:focus-visible,
.needs-carousel__dot:focus-visible { border-radius: var(--radius--circle); }
.needs-carousel__btn svg { inline-size: 1.2em; block-size: 1.2em; }
.needs-carousel__toggle svg { inline-size: 1em; block-size: 1em; }
.needs-carousel__toggle .i-play { display: none; }
.needs-carousel__toggle.is-paused .i-play { display: block; }
.needs-carousel__toggle.is-paused .i-pause { display: none; }

/* Dots: the hero's exactly (see .hero-slider__dot): 6px pips, the active one
   24px, an 8px visual gap, --radius--s so they resolve to a circle and a
   stadium, each button 24px tall for the target size. Grey-400 at rest; the
   active pip's dark bar fills over the dwell while autoplay runs. */
.needs-carousel__dots { display: flex; align-items: center; }
.needs-carousel__dot {
  display: grid;
  place-items: center;
  padding-block: calc((24px - var(--hero-pip)) / 2);
  padding-inline: calc(var(--hero-pip-gap) / 2);
  border: 0;
  background: none;
  cursor: pointer;
  line-height: 0;
}
.needs-carousel__pip {
  position: relative;
  display: block;
  overflow: hidden;
  inline-size: var(--hero-pip);
  block-size: var(--hero-pip);
  border-radius: var(--radius--s);
  background: var(--_theme---hero--dot);
  transition: inline-size var(--dur) var(--ease-global);
}
.needs-carousel__dot[aria-current="true"] .needs-carousel__pip { inline-size: var(--hero-pip-active); }
.needs-carousel__pip::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--_theme---hero--dot-fill);
  transform: translateX(-100%);
}
.needs-carousel__dot[aria-current="true"] .needs-carousel__pip::after { transform: none; }
.needs-carousel.is-timing .needs-carousel__dot[aria-current="true"] .needs-carousel__pip::after {
  animation: needs-dwell var(--needs-dwell) linear both;
}
@keyframes needs-dwell { from { transform: translateX(-100%); } to { transform: none; } }

/* ---- Tablet (641-960): the same carousel, the card at §10.3's 84% ----------
   At 768 the card is 645x323, so its text half is a 323px square: the title
   steps down to h5 and the padding to --sp-20 so three levels still sit
   comfortably inside it. Body and link keep body-s (14.3px at 768). */
@media (max-width: 960px) {
  .needs-card { --_radius: var(--radius--2xl); }
  /* The square is ~320-400px here: the rows keep the square's proportions —
     one rank down (h5 over h6: 17.9 / 14.9px on a 375 phone, 5.2% / 4.3% of
     its 343px square) — and --sp-24 insets (~10% of the square, 35.8px at
     375). */
  .needs-say { --_inset: var(--sp-24); }
  .needs-say li { font-size: var(--fs-h6); gap: var(--sp-12); }
  .needs-say li:first-child { font-size: var(--fs-h5); }
  .needs-card__text { padding: var(--sp-20); }
  .needs-card__title { font-size: var(--fs-h5); line-height: var(--lh-heading); }
  .needs-card__body { margin-top: var(--sp-8); }
}

/* ---- 8. Phones (<= 640): a plain vertical stack ---------------------------
   No carousel and no controls: every card is in flow, full column width, the
   square video on top (order only — the video is decorative and hidden from
   assistive tech, so reading order is unchanged), the text below. The gap is
   --sp-32 (47.7px at 375; the reference's 48). js/main.js reads the same
   query and drops the carousel semantics and `inert` here. */
@media (max-width: 640px) {
  .needs-carousel__slides {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-32);
    padding-inline: var(--gutter);
  }
  .needs-card {
    grid-area: auto;
    grid-template-columns: minmax(0, 1fr);
    aspect-ratio: auto;
    transform: none;
    transition: none;
  }
  .needs-card__media { order: -1; aspect-ratio: 1; }
  .needs-card__text { padding: var(--sp-24) var(--sp-20) var(--sp-24); }
  .needs-card__title { font-size: var(--fs-h4); line-height: var(--lh-snug); }
  :lang(ko) .needs-card__title { line-height: var(--lh-heading); }
  /* body-m here, not body-s: on the fluid phone root body-s is 13.4px at
     375, too small for a paragraph under a 24px title (body-m: 14.9px). */
  .needs-card__body { font-size: var(--fs-body-m); }
  .needs-carousel__controls { display: none; }
}

/* §10.4 — no slide animation and no progress wipe. js/main.js does not start
   autoplay or the videos here; the posters stand in. */
@media (prefers-reduced-motion: reduce) {
  .needs-card,
  .needs-carousel__pip { transition: none; }
  .needs-carousel.is-timing .needs-carousel__dot[aria-current="true"] .needs-carousel__pip::after { animation: none; }
  /* The practice lines show whole at once, whatever state the script set —
     even if the reader presses play (that resumes the videos, not this). */
  .needs-say[data-say] .needs-say__ch { transform: none; animation: none; }
}

/* ---- The per-clip play/pause control (WCAG 2.2.2) — now used by #curriculum's
        looping tile only (#needs moved to a section-level pause button). ---- */
/* THE REVEAL RUNS THROUGH A CUSTOM PROPERTY, NOT THROUGH COMPETING `opacity`
   DECLARATIONS. Every rule below sets --_toggle-reveal; only this rule sets
   opacity. That keeps the unavailable state (further down) from having to win a
   specificity race against the hover and focus rules, which is how the old
   version ended up with a focus rule it could not safely override.

   NO transition-delay ANYWHERE. It used to be --dur on both the hover and the
   focus path, and on the focus path it was a real accessibility defect: this
   button is the last focusable node in the card, so Shift+Tab backwards into a
   card lands focus on it at the same instant :focus-within first matches — and
   an opacity:0 button is still in the tab order while its focus ring is
   invisible. The control was unlit for 300ms and not fully lit until 540ms, and
   the reduced-motion block zeroed the delay, so the only readers who could
   reach an invisible focused control were the ones who had not asked for less
   motion. §10.1 does not admit an invisible focus indicator.

   It is gone from :hover too. 300ms delay + 240ms duration is 540ms
   trigger-to-settled against §8.1's published 240-300ms band and its sentence
   "Nothing on the page is slower except the two marquees"; there is no delay
   token in §8 to spend. The reason the delay existed was that a play triangle
   appearing as the picture froze read as a broken player — and that reading is
   what js/main.js now fixes at the source: the triangle appears because the
   picture really has stopped, and pressing it really does start the clip. An
   honest control needs no cover. */
.scene__toggle {
  position: absolute; top: var(--sp-8); right: var(--sp-8); z-index: 2;
  display: grid; place-items: center;
  width: var(--sp-20); height: var(--sp-20);
  border: 0; border-radius: var(--radius--m);
  background: var(--_theme---scene-toggle--background);
  color: var(--_theme---scene-toggle--icon);
  cursor: pointer;
  --_toggle-reveal: 0;
  opacity: var(--_toggle-reveal);
  /* §8.1 floors durations at 240ms; this carried a raw 200ms. */
  transition: opacity var(--dur-fast) var(--ease);
}
.scene__toggle svg { width: 1em; height: 1em; }
.scene__toggle .i-play { display: none; }
.scene__toggle.is-paused .i-play { display: block; }
.scene__toggle.is-paused .i-pause { display: none; }
/* The three REVEALS (hover, focus-within, the reader's own pause) are keyed off
   the host card, so they live with the host: see .curriculum__media. Focus
   there snaps in (transition-duration: 0s) because an indicator that has to
   animate into existence is not an indicator; the reader's own pause keys off
   .is-user-paused, not .is-paused, so off-screen tiles stay unlit. */
/* A card below the play threshold cannot start, so the control fades and stops
   taking presses rather than accepting one and doing nothing. This is the whole
   visible feedback for that refusal; js/main.js sets the attribute and ignores
   the click. Because only the base rule sets `opacity`, this needs no
   specificity fight — it multiplies whatever reveal is in force. */
.scene__toggle[aria-disabled='true'] {
  opacity: calc(var(--_toggle-reveal) * 0.4);
  cursor: default;
}
@media (hover: none) { .scene__toggle { --_toggle-reveal: 1; } }

/* ============ Responsive ============ */

/* .split and .branch used to collapse at the deleted 900px rule and now collapse
   at 960px — 60px earlier. Deliberate and checked: both are plain two-column
   grids (site.css:38 and :66) folding to a single column, which cannot overflow,
   and .form-grid never had a 900px rule at all. Recorded because the file no
   longer shows where they used to fold. */
@media (max-width: 960px) {
  .split, .branch { grid-template-columns: minmax(0, 1fr); gap: var(--sp-24); }
}
@media (max-width: 640px) {
  .form-grid { grid-template-columns: minmax(0, 1fr); }
}
@media (prefers-reduced-motion: reduce) {
  .faq summary::after { transition: none; }
}

/* ============ Final CTA — open on the gradient, with AVO ============ */

/* Hidden for now (client, 2026-10-01): the AVO gradient outro is off on every
   page. Delete this line to bring it back; the markup is untouched. */
.ctashell { display: none !important; }

/* The white card is gone: the copy sits directly on the mesh. .mesh > * already
   gives these position:relative and z-index:1, so they clear the gradient blobs. */
.ctaopen { width: min(46rem, 100% - 2.4rem); text-align: center; }
.ctaopen .btn-group { justify-content: center; }

/* AVO, very large and cropped by the bottom edge the way the inspora footer
   crops its character (there the blob spans ~87% of the width and ~86% of the
   height of the panel).

   Two elements, deliberately: .ctaavo is the CROP WINDOW and is shorter than
   the artwork, while .ctaavo__plate keeps the plate's exact 1248x1664 box. The
   eye positions are percentages measured against that artwork, so they only
   line up while their parent matches its ratio — cropping the parent directly
   would slide the pupils off his face as soon as he was scaled up. */
.ctaavo {
  position: relative;
  width: min(760px, 94vw);
  aspect-ratio: 1248 / 980;     /* plate is 1248/1664, so the lower ~41% is cut */
  overflow: hidden;
  margin: var(--sp-32) auto calc(-1 * var(--sp-64));
  user-select: none;
}
.ctaavo__plate { position: relative; width: 100%; }
/* img today, <video> if the sprout-twitch loop replaces the still plate — either
   way it must fill the plate exactly, since the eye percentages are measured
   against the artwork's box. */
.ctaavo__plate img,
.ctaavo__plate video { width: 100%; height: auto; display: block; }

/* Eye geometry measured off the artwork by connected-component labelling, not
   guessed: left sclera 35.817%/37.200% at 11.619%x4.087%, right 52.324%/37.200%
   at 11.458%x4.087% of the 1248x1664 plate. That plate has its sockets painted
   out, so collapsing .eye for a blink just reveals the lavender face beneath —
   which is why no white band shows mid-blink. */
/* The whites are the supplied artwork, each eye split out of the plate and given
   a transparent margin so its soft edge is not shaved by the border-radius clip.

   The background-colour MUST stay transparent. It used to be rgb(232,232,236) as
   a load-failure fallback, but once the transparent margin was added that grey
   showed through the 4% band between the artwork's oval and the clip ellipse —
   reading as a grey ring around each eye. Transparent lets the lavender face
   behind show through the margin instead, so the oval sits clean on his face. */
.ctaavo__plate .eye {
  position: absolute; overflow: hidden; border-radius: 50%;
  background: transparent center / 100% 100% no-repeat;
  transform-origin: bottom center;
  transition: transform 110ms cubic-bezier(.33, 0, .5, 1);
  will-change: transform;
}
/* Enlarged 8% about each eye's own centre so the supplied oval is not clipped at
   its edge. The box is scaled rather than padded on purpose: .eye keeps
   border-radius:50% + overflow:hidden to contain the pupil, so the artwork's
   oval and the clipping ellipse have to stay the same shape — padding the box
   would let the pupil travel outside the visible white.
   left:  11.619 -> 12.549%, 4.087 -> 4.414%, origin shifted back by half the growth
   right: 11.458 -> 12.375%, same height */
/* Enlarged a further 10% about each eye's own centre (cumulative 1.19x on the
   original boxes). Scaled about the centre so the eyes stay put on his face. */
.ctaavo__plate .eye--l {
  left: 34.724%; top: 36.817%; width: 13.804%; height: 4.855%;
  background-image: url("../assets/characters2/web/eye-sclera-left.png");
}
.ctaavo__plate .eye--r {
  left: 51.247%; top: 36.817%; width: 13.613%; height: 4.855%;
  background-image: url("../assets/characters2/web/eye-sclera-right.png");
}
.ctaavo__plate.is-blinking .eye { transform: scaleY(0.04); transition-duration: 85ms; }

/* Pupil sized from 105 open-eye frames of the clip, not from a single still. */
/* The pupil is the render's own material too — it is graded, not flat (the real
   one runs luminance 4-87 against a flat #140f16's spread of 0). No border-radius
   here on purpose: the PNG's alpha already carries the soft edge, and clipping it
   to a hard oval would cut away the very anti-aliasing that sells it.
   Sourced from the widest UNCLIPPED frame so it is not foreshortened sideways;
   its 60% height is stretched to the box's 68.2%, which is invisible on a soft
   gradient and keeps the larger pupil size. */
.ctaavo__plate .pupil {
  position: absolute; left: 50%; top: 50%;
  background: center / 100% 100% no-repeat;
  transform: translate(-50%, -50%);
  will-change: transform;
}
.ctaavo__plate .eye--l .pupil {
  width: 41.5%; height: 68.2%;
  background-image: url("../assets/characters2/web/eye-pupil-left.png");
}
.ctaavo__plate .eye--r .pupil {
  width: 43.5%; height: 70.3%;
  background-image: url("../assets/characters2/web/eye-pupil-right.png");
}

@media (prefers-reduced-motion: reduce) {
  .ctaavo__plate .eye { transition: none; }
  .ctaavo__plate.is-blinking .eye { transform: none; }
}
@media (max-width: 640px) {
  .ctaopen .btn-group, .ctaopen .btn-group .btn { width: 100%; }
  .ctaavo { margin-bottom: calc(-1 * var(--sp-32)); }
}

/* ============ 2b. "How it's taught" — the method section (#method) ============

   Built to bar.md's seven mechanisms; the media box's transition is built to
   bar-method-stack.md (Klarna's card stack). This file carries the STATIC
   state: item 1 active, its body shown, card 1 up in the box. The scroll
   controller (js/main.js) writes --in / --out on each card and moves
   .is-active.

   FOUR type sizes, and only four (bar.md §6), section heading LARGER than the
   item headings:
     --fs-h1      3.2rem / 57.8px   section h2        (reference 64)
     --fs-h2      2.6rem / 46.9px   item headings     (reference 52)
     --fs-body-s  0.9rem / 16.2px   lede + bodies     (reference 16)
     --fs-body-xs 0.8rem / 14.4px   opener pill, chip (reference 14)
   The heading pair's RATIO is what mechanisms 2 and 6 actually turn on, and
   57.8/46.9 = 1.233 against the reference's 64/52 = 1.231. The body sits on
   --fs-body-s so the headline-to-body span is ~3.6:1 rather than the 3.2:1
   --fs-body-m gave; the reference spans 4:1 off a 16px body, and our whole
   shortfall was at the body end, never at the heading end. --fs-headline was
   rejected for the section h2: §4.1 ranks it hero tier and it is the hero
   <h1>'s own token on this very page. */

.method__head {
  max-width: 44rem;
  margin: 0 auto var(--sp-40);
  text-align: center;
}
/* §5.4 — the opener runs pill -> heading -> description. The pill-to-heading
   gap is NOT set here on purpose: §4.3 publishes it as an adjacency rule,
   `.pill + h2 { margin-top: 0.8rem }` in styles.css, and this markup matches
   that selector. A descendant rule here would both double the measured value
   and bypass the mechanism §4.3 exists to enforce. */
.method__lede {
  margin: var(--sp-16) auto 0;
  font-size: var(--fs-body-s);
  line-height: var(--lh-body);
  color: var(--color--grey-800);
}

/* §5.2 / bar.md §6 — asymmetric 55/45 with the MEDIA as the wider half.
   50rem @1440 is 902px: minus the 4rem gap leaves 830px, splitting 456/374 —
   Klarna's measured 452 media / 372 text, reproduced in rem rather than px. */
.method__body {
  display: grid;
  grid-template-columns: 55fr 45fr;
  gap: var(--sp-40);
  align-items: center;
  max-width: 50rem;
  margin-inline: auto;
}

/* bar-method-stack.md §1, §4 — ONE clipping box, and every card stacked in it
   at the same rect. The box takes the app images' own ratio, 848x1136, so each
   card shows its whole image with no crop and no letterbox; the six files put
   the phone at the identical pixel position, so with one rect for all six the
   phone sits at the same place and size on every card (§6). Radius is the
   ladder step nearest Klarna's measured 24px: --radius--l, 21.7px @1440
   (--radius--xl, 28.9px, is 4.9px off; this is 2.3px). The box paints
   nothing itself; only the cards are ever visible. */
.method__media {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  aspect-ratio: 848 / 1136;
  /* All four corners (user, 2026-10-06): the "no radius at the bottom" rule
     is for the phone SCREEN inside a mockup, not for the box that holds it.
     Cards inherit all four. */
  border-radius: var(--radius--l);
  /* No fill, no border, no shadow: the box only clips. Beside a card that has
     shrunk to 0.7, the reader must see the white page, as on Klarna — a
     grey-50 fill here showed as two grey strips flanking the shrinking card. */
  background: none;
}

/* §2, §3 — each card is drawn from two numbers the controller writes on it:
     --in   0..1  how far it has RISEN into the box (0 = waiting one full box
                  height below, clipped; 1 = covering the box)
     --out  0..1  how far it has LEFT (scale 1 -> 0.7 about its top centre,
                  opacity 1 -> 0, no translation)
   translateY's 100% is the card's own height, which IS the box height, so the
   waiting card sits exactly one box-height down. Later cards paint above
   earlier ones (DOM order, and the controller also writes z-index), so the
   incoming card is always drawn over the outgoing one.
   Each card is itself rounded (the box's radius, inherited) and clips its
   image, as Klarna's cards are: the rising card's top corners and the
   shrinking card's four corners stay rounded inside the box mid-handover,
   instead of showing square image corners against the box's curve.
   No JS: card 1 is up and every later card waits below, clipped. */
.method {
  --method-shrink: 0.3;
}
.method__card {
  --in: 1;
  --out: 0;
  position: absolute;
  inset: 0;
  overflow: hidden;
  border-radius: inherit;
  transform-origin: 50% 0;
  transform: translateY(calc((1 - var(--in)) * 100%)) scale(calc(1 - var(--method-shrink) * var(--out)));
  opacity: calc(1 - var(--out));
}
.method__card + .method__card { --in: 0; }
/* While the scroll drives the stack, each card is its own compositing layer.
   Painted into one shared layer, a card shrinking by fractions of a pixel per
   frame left stale 1px slivers of its earlier edges beside it (measured:
   faint vertical lines 2-13px right of the shrinking card that outlived the
   scroll by seconds, and vanished on a fresh paint). As separate layers the
   transform and opacity are composited, never repainted, so nothing is left
   behind. Scoped to the pin so the six layers exist only while they work. */
.method.is-pinned .method__card {
  will-change: transform, opacity;
  backface-visibility: hidden;
}
.method__card img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* A heading click changes the card with the same rise/shrink, played over
   the house 300ms instead of scrubbed by the scroll — unpinned (tablet), and
   pinned too, where the page jumps straight to the card's rest point
   (.is-jumping) rather than smooth-scrolling ~800ms of cards past the reader. Cards the jump passes over are marked .is-skip and change at
   once, so only the outgoing and incoming cards are ever seen moving. */
.method:not(.is-pinned) .method__card,
.method.is-jumping .method__card {
  transition: transform var(--dur) var(--ease-global), opacity var(--dur) var(--ease-global);
}
.method:not(.is-pinned) .method__card.is-skip,
.method.is-jumping .method__card.is-skip { transition: none; }

/* §5 — the caption chip, inset bottom-left, naming the active item.
   Forest ink ground carrying white, so no untokenised rgba scrim is invented.
   max-width keeps the longest title ("What your field asks") inside the window
   instead of running under the overflow:hidden edge; it wraps rather than clips.
   §4.1 pairs --fs-body-xs with weight 400 — 500 belongs to the label/tag rows. */
.method__chip {
  position: absolute;
  left: var(--sp-16);
  bottom: var(--sp-16);
  z-index: 1;
  margin: 0;
  max-width: calc(100% - 2 * var(--sp-16));
  padding: 0.4em 1.2em;
  border-radius: var(--radius--tag);
  background: var(--color--grey-1000);
  color: var(--color--white);
  font-size: var(--fs-body-xs);
  font-weight: 400;
  line-height: var(--lh-body);
}

.method__list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-16); }
.method__item-h { margin: 0; }

/* §2 — exactly two heading states, and the difference is COLOUR ONLY.
   Same size, same weight, same position; the eye is led by value.
   line-height 1.0 is §4.1's own h2 row (styles.css's .h2 rides at 0.9, which
   is too tight once an item heading wraps to two lines in a 374px column). */
.method__trigger {
  display: block;
  width: 100%;
  /* §10.1: when Tab scrolls a trigger into view, keep its 3px-offset ring on screen. */
  scroll-margin-block: var(--sp-16);
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  text-align: left;
  font-family: inherit;
  font-size: var(--fs-h2);
  font-weight: 500;
  line-height: var(--lh-tight);
  letter-spacing: var(--kern-headline);
  color: var(--color--grey-400);
  cursor: pointer;
  transition: color var(--dur) var(--ease-global);
}
.method__item.is-active .method__trigger { color: var(--color--grey-1000); }

/* Hover/focus stops one step short of the active ink. Painting it grey-1000
   put two dark headings in one frame while the pointer rested, which bar.md §2
   forbids; grey-600 sits between the inactive grey-400 and the ink, so the
   affordance still reads but the active item keeps the only dark value.
   The active item is excluded so hovering it never makes it lighter. */
.method__item:not(.is-active) .method__trigger:hover,
.method__item:not(.is-active) .method__trigger:focus-visible { color: var(--color--grey-600); }

/* Hangul needs more leading than the Latin --lh-tight, and a looser track.
   Both values are tokens: §4.1 permits three tracking values only, and -0.02em
   was a fourth sitting between --kern-headline and --kern-heading. */
:lang(ko) .method__trigger { line-height: var(--lh-heading); letter-spacing: var(--kern-heading); }
/* §4.1.2 — the section title too; the global :lang(ko) .h1 rule (1.16 / -0.02em) is a known non-conformance. */
:lang(ko) .method__title { line-height: var(--lh-heading); letter-spacing: var(--kern-heading); }
/* §4.1.2 — Hangul body copy sets at 1.7, not the Latin 1.3. */
:lang(ko) .method__lede,
:lang(ko) .method__text { line-height: var(--lh-ko-body); }

/* §3 — one body paragraph exists at a time. The other four panels carry the
   `hidden` attribute, so there is nothing to fade and nothing to count. */
/* Measure is declared on the element as a character count (§4.2), not as a
   max-width here — the lede already did it that way. */
.method__text {
  margin: var(--sp-12) 0 0;
  font-size: var(--fs-body-s);
  line-height: var(--lh-body);
  color: var(--color--grey-800);
}

@media (max-width: 960px) {
  .method__body { grid-template-columns: minmax(0, 1fr); gap: var(--sp-24); }
  .method__media { width: min(100%, 20rem); margin-inline: auto; }
}

/* §7 — mobile keeps ALL SIX headings and shrinks the media to 129px wide
   (once a 129x129 square crop; now the whole phone at the images' ratio).
   The list is never truncated or turned into a carousel.

   8.6rem is bar.md's measured 129px expressed on the fluid root (§1), so the
   thumbnail breathes with the viewport instead of stepping: at 375 the root is
   14.895px, and 8.6rem lands at 128.1px.

   The thumbnail is deliberately left IN FLOW rather than `position: sticky`.
   Pinning it is scroll behaviour and belongs to the controller piece, and a
   sticky thumbnail in a one-column block overlaps the headings as they scroll
   up behind it — measured at 375, where it covered items 3 and 4. The
   controller should pin it together with whatever inset keeps the list clear.

   The item headings step down to --fs-h3 here. At 375 --fs-h2 rendered within
   a hair of the section h2 and the two ranks read as one, worst in Korean
   where the glyphs carry more ink at equal size. This holds the count at four
   sizes and opens the step to 1.6x. */
@media (max-width: 640px) {
  .method__body { display: block; max-width: none; }
  /* The whole phone, never a crop of it (bar-method-stack.md §6): the box
     keeps the images' own 848:1136 ratio at every width. This static size is
     what reduced motion and no-JS show; pinned, the box grows to the space
     the list leaves (see the controller's CSS below). */
  .method__media {
    width: 8.6rem;
    margin-inline: 0;
  }
  /* §4.1's h3 row: line-height 1.1 (--lh-snug) / tracking -0.016em
     (--kern-heading). KO keeps §4.1.2's 1.2 through :lang(ko) .method__trigger,
     which out-ranks this rule. */
  .method__trigger {
    font-size: var(--fs-h3);
    line-height: var(--lh-snug);
    letter-spacing: var(--kern-heading);
  }
  /* The caption chip is desktop-only. At 375 the plate is 128px, so the chip
     spanned three quarters of it and sat across the subjects' faces — spending
     the image's focal point to repeat the heading 40px below it. Stacked into
     one column, proximity already ties plate to heading, which is the whole
     job the chip does at desktop across an 80px gutter. bar.md §5 keeps it
     where it earns its place; §7's mobile view never asks for it. */
  .method__chip { display: none; }
  .method__list { margin-top: var(--sp-24); }
}

/* ============ 2c. The scroll controller's own CSS ============================

   Everything in this block is inert until the controller (js/main.js) adds
   `.is-pinned`, which it does only after MEASURING that the frame fits the
   viewport it is in. No JS, reduced motion, a viewport too short, or a frame
   that cannot fit -> none of this applies and the static layout above stands
   unchanged. That is deliberate: a pin that does not pin is worse than no pin.

   The pin-height problem, stated: the section measures ~1005px inside a 900px
   viewport, and 66px of that viewport is already spent on the sticky nav. The
   frame is solved by making the MEDIA height-driven (below) rather than by
   cropping it, and by the controller declining to pin whenever even that is
   not enough. */

/* The runway. The controller injects this as a sibling AFTER .wrap, because a
   sticky child is constrained by its containing block's CONTENT box: padding
   on .method, or a margin on .wrap itself, buys exactly zero travel (the
   sticky rect is shrunk by the sticky box's own margins). Only a real sibling
   with real height lengthens the scroll the frame can stay pinned through.
   n states = n-1 handovers, so the runway is n-1 steps long — derived, not
   the constant 4 that was left behind when a sixth item was added. */
.method__runway {
  height: calc((var(--method-n, 6) - 1) * var(--method-step));
}
/* bar-method-stack.md §2, §5 — one handover's worth of scroll. The rise is
   1:1 with the scroll, so a handover costs exactly one box height
   (--method-box, measured and written by the controller), and a short rest
   (--method-dwell) separates one landed card from the next rise: at 1440x900
   the box is ~568px and the step ~611px, against Klarna's 565 / ~600.
   --method-step-min only matters where the box is small (the 640px
   thumbnail): there the rise is still 1:1 over the thumbnail's height and the
   rest of the step is dwell, so each item keeps a readable stretch of scroll. */
.method {
  --method-dwell: var(--sp-24);
  --method-step-min: 0px;
  --method-step: max(var(--method-box, 62svh) + var(--method-dwell), var(--method-step-min));
}

/* The pin has two modes; the controller picks one by measuring, and both
   carry .is-pinned (which is what switches the stack to scroll-driven).

   .is-pin-frame — head AND body pin together. Used wherever it fits (1440x900
   and taller). .wrap already contains head + body, so the pin needs no new
   element: it becomes a viewport-tall flex column with the content centred.

   .is-pin-body — only the media + list block pins, and the section heading
   scrolls away above it, which is what Klarna's own block does (its sticky
   element is the 904x811 media + list, not the heading). Used on laptop
   viewports (1440x789, 1366x768, 1280x800 ...) where head + body cannot share
   one screen: giving the heading's ~250px back to the box is what lets the
   stack run there at all, instead of the section degrading to a static phone. */
.method.is-pin-frame > .wrap {
  position: sticky;
  top: var(--nav-h);
  height: calc(100svh - var(--nav-h));
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-block: var(--sp-12);
}
.method.is-pin-frame .method__head { flex: none; margin-bottom: var(--sp-24); }
/* flex-GROW, not just shrink: the body must end up with a DEFINITE height, or
   the media's `height: 100%` below has nothing to resolve against and silently
   falls back to the list's height — which sized the media to 382px against a
   374px list, 8px from failing §6's "media is the wider column". */
.method.is-pin-frame .method__body { flex: 1 1 auto; min-height: 0; width: 100%; }
/* width: 100% — .method__body centres itself with auto inline margins, and in
   a flex column auto margins shrink an item to fit its content. The body's
   width then followed whichever panel was showing: in KO the whole body, and
   the media box with it, moved up to 24px sideways as the active item changed,
   taking the phone along (bar-method-stack.md §6). Full width, capped by the
   body's own max-width, holds the box still. */

/* The body-only pin: the body itself is the sticky, viewport-tall element, so
   its height is definite and the media's `height: 100%` resolves against it
   exactly as it does inside the pinned frame. */
/* --sp-8 of breathing room, not the frame's --sp-12: this mode exists for
   short viewports, and those 15px are what let EN's list (two headings wrap
   in the 374px column) fit a 1440x650 window. */
.method.is-pin-body .method__body {
  position: sticky;
  top: var(--nav-h);
  height: calc(100svh - var(--nav-h));
  padding-block: var(--sp-8);
}

/* Desktop: the media turns HEIGHT-driven so the frame can fit a viewport
   shorter than the media's natural 611px. `width: auto` lets the aspect-ratio
   derive the width, which is what keeps bar.md §5's single ratio intact —
   stretching the media to the full 55fr column while its height is definite
   would override aspect-ratio outright and change the ratio. max-height is
   the natural size, so from ~1080px of viewport height upward nothing moves
   at all. The controller refuses the pin at any height where this would make
   the media NARROWER than the list, which §6 forbids. */
@media (min-width: 961px) {
  .method.is-pinned .method__body { align-items: stretch; }
  /* The sixth item's cost is paid here, and it is paid in the EN edition. At
     1440x900 the pinned frame offers 792px of inner height; KO's list measures
     inside it, but EN's runs 584px because TWO of its headings wrap to two
     lines in the 374px column ("The words you need", "Your record adds up",
     94px each against 47px for the rest) — 16px over, so the controller
     correctly refused to pin and the two editions behaved differently.
     Five gaps at --sp-12 instead of --sp-16 return 36px, which clears EN with
     20px to spare and leaves KO with more. Scoped to the pinned desktop state
     so the static, tablet and mobile rhythms are untouched; evaluate() reads
     the list AFTER adding .is-pinned, so it measures this value and not the
     looser one. */
  .method.is-pinned .method__list { align-self: center; gap: var(--sp-12); }
  /* The body-only pin, sized to its content (2026-09-28). With a two-line
     heading the frame no longer fits 1440x900, so the controller falls back
     to this mode — and a viewport-tall sticky body centred a 612px phone in
     835px, leaving a 184px hole between the description and the phone on the
     way in (the frame mode shows 47px). The pinned block is now only as tall
     as the capped media plus its padding, and its sticky `top` centres that
     block in the viewport instead, so the phone still sits mid-screen while
     pinned but arrives right under the heading. The list (<= 584px, EN) fits
     inside the 33.9rem media height it is centred against. */
  .method.is-pin-body .method__head { margin-bottom: var(--sp-24); }
  .method.is-pin-body .method__body {
    --_pin-h: min(calc(100svh - var(--nav-h)), calc(33.9rem + 2 * var(--sp-8)));
    height: var(--_pin-h);
    top: calc(var(--nav-h) + (100svh - var(--nav-h) - var(--_pin-h)) / 2);
  }
  .method.is-pinned .method__media {
    height: 100%;
    max-height: 33.9rem;
    width: auto;
    justify-self: center;
    /* Once max-height caps it (the body-only pin at 1440x789 offers ~695px),
       the box sits centred against the list rather than hanging from the top
       with the spare height all below it. Where the box fills the height
       (1440x900) this moves nothing. */
    align-self: center;
  }
}

/* §7 — mobile. The whole frame pins, so the thumbnail holds its screen y while
   the active item advances beneath it. The list sits BELOW the thumbnail
   inside that same pinned frame and the two never share space, which is the
   inset that fixes the earlier bare `position: sticky` on the thumbnail alone
   (that one kept the square while the list scrolled up behind it, covering
   items 3 and 4). The tighter list gap is what buys the frame its fit at
   375x812, and when it has none the controller declines to pin rather than
   clipping the list.

   Re-measured after the sixth item landed: at 375x812 the frame needed 731px
   against 722px of inner height — 9px over, and the pin was being refused on
   mobile in both editions. Five gaps at --sp-12 return 36px, leaving 27px of
   headroom. The earlier note that this size had "~50px to spare" was written
   when there were five items and is no longer true of any of them. */
@media (max-width: 640px) {
  .method { --method-step-min: 40svh; }
  .method.is-pin-frame .method__head { margin-bottom: var(--sp-16); }
  .method.is-pinned .method__list { gap: var(--sp-12); }
}

/* Phones and tablets — the body-only pin in ONE column. The heading scrolls
   away, and the pinned block is the media box over the list: the list keeps
   its natural height (all six headings, the active body), and the box takes
   every pixel left above it — `minmax(0, 1fr)` row, `height: 100%`, width
   derived from the images' ratio — so the whole phone shows, uncropped, as
   large as the screen allows (≈300px tall at 375x812, ≈190px at 375x667).
   The list's height is constant (the panels reserve the tallest body), so the
   box, and the phone in it, never moves or resizes between cards. */
@media (max-width: 960px) {
  .method.is-pin-body .method__body {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr) auto;
    gap: var(--sp-16);
  }
  /* --method-media-min is the floor the controller tests against: the static
     phone thumbnail's own height (8.6rem wide at 848:1136 = 11.5rem). Below
     it the pin is refused rather than shrinking the phone past the size the
     unpinned layout already shows. */
  .method { --method-media-min: 11.5rem; }
  .method.is-pin-body .method__media {
    height: 100%;
    max-height: 33.9rem;
    width: auto;
    min-height: var(--method-media-min);
    margin-inline: 0;
    justify-self: center;
  }
  .method.is-pin-body .method__list { margin-top: 0; gap: var(--sp-8); }
}

/* Short phones (375x667, 360x640 ...): six full-size h3 titles left the box a
   170-190px thumbnail. Here the INACTIVE titles step down to §4.1's h5 row
   (1.2rem, line-height 1.2, -0.016em — also §4.1.2's Hangul heading row), one
   line each, still grey, still the same buttons (focusable, §10.1's ring,
   rows 24px+ apart for WCAG 2.5.8). The active title and its body stay as
   they are. Every title sits on one line at both sizes, so the list is always
   five small rows + one large row + the reserved body: its height never
   changes as the active item moves, and the box (and the phone) never moves
   or resizes between cards. The box takes the ~40% of the pinned height the
   compaction frees, reaching about half of it. */
@media (max-width: 640px) and (max-height: 700px) {
  .method.is-pin-body .method__item:not(.is-active) .method__trigger {
    font-size: var(--fs-h5);
    line-height: var(--lh-heading);
    letter-spacing: var(--kern-heading);
    /* At h5 size the titles are no longer large text, so grey-400 (3.13:1)
       fails AA; grey-600 is §3.1's muted ink for small text (4.60:1). */
    color: var(--color--grey-600);
  }
}

/* §3 — one paragraph at a time, and the hidden ones still reserve their
   space: the controller measures the tallest panel and writes it here, so the
   list keeps one constant height and the pinned frame cannot resize as the
   active item advances. */
.method__panel { min-height: var(--method-panel-h, auto); }

@media (prefers-reduced-motion: reduce) {
  .method__card,
  .method:not(.is-pinned) .method__card,
  .method.is-jumping .method__card { transition: none; }
  .method__trigger { transition: none; }
  /* The pin is undone in CSS as well as in JS, so that a preference switched
     on while the page is open cannot leave a stale pin running. */
  .method.is-pin-frame > .wrap,
  .method.is-pin-body .method__body {
    position: static;
    height: auto;
    padding-block: 0;
  }
  .method.is-pin-frame > .wrap { display: block; }
  .method__runway { display: none; }
}

/* ============ 3. The app — sections A / B / C ============================
   #app-tour (A) · #app-anywhere (C). Built to bar-app.md's seven mechanisms.
   Skeleton: the plates are empty frames, not images. Section B (#app-daily,
   "Three things, every day.") was deleted 2026-09-21 at the client's request,
   with its .appchap / .appsteps / .appshot--portrait rules; B-only rows are
   struck from the map below.

   TOKEN MAP — the reference's px against what is actually written here.
   Root is fluid (§1); the px column is the value at a 1440 viewport (18.048px).

     reference          token                    @1440     mechanism
     ---------------------------------------------------------------------
     container 1140     63rem                    1137px    1, 6
     media 538x538      1fr of a 2-col grid       540px    1, 2  aspect-ratio 1/1
     radius 32          --radius--xl             28.9px    2
     radius 24          --radius--l              21.7px    2  (rounder rows > B ✓)
     heading 52/400     --fs-h2 @ weight 500     46.9px    3  (see WEIGHT below)
     heading 64/700     --fs-h1 @ weight 500     57.8px    3  (= #method's .h1)
     body 16/1.45       --fs-body-s / --lh-prose 16.2/1.4  —
     eyebrow 16/500     .pill (--fs-body-xs)     14.4px    4  (never > body ✓)
     pill button        .btn .btn--primary as-is  —        5

   WEIGHT, stated plainly. The reference splits its two heading ranks by size
   AND weight (52/400 against 64/700). We can only split by size: the webfont is
   requested `wght@400;500`, §4.1 publishes no 400 heading, and 700 would render
   synthetic bold — which §4.1.1 already rejected once in this project. So both
   ranks sit at 500 and the rank is carried by size alone: 57.8/46.9 = 1.233,
   against the reference's 64/52 = 1.231. The ratio is reproduced exactly; the
   weight delta is not available and is not faked.

   MECHANISM 1. A and C share one grid, one container and one plate. The mirror
   is DOM order only — no `order` property — so tab order stays visual order.
   §5.2 calls a 50/50 split "the exception, not the default"; this is that
   exception, and it is load-bearing: mechanism 1 requires A's and C's media and
   text columns to measure the same, and the reference's 538/538 is 50/50.

   No animation is introduced in this block. The only motion in these sections
   is .btn's and .pill's existing hover/focus transitions, which §10.4's global
   reduced-motion rule in styles.css already neutralises. */

/* ---- The placeholder plate. Shared by all three, sized by the modifier. ----
   A deliberate empty frame, not a broken image: a tinted ground holding its
   ratio with the brand mark centred. aspect-ratio + a grid parent means it can
   never collapse to zero height, with or without content inside it. */
.appshot {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: grid;
  place-items: center;
  /* sand-200, not sand-100. §5.3's page canvas is a white -> sand-50 -> white
     wash, and these three sections all land in its sand-50 stretch, where a
     sand-100 plate is a ~2% step and reads as a rendering fault rather than as
     a frame someone meant to leave empty. sand-200 is the next rung of the same
     family and is unambiguously a plate. The mark steps with it, to sand-600. */
  background: var(--color--sand-200);
  color: var(--color--sand-600);
}
.appshot__mark { width: var(--sp-40); height: var(--sp-40); }

/* Drop-in target for the real export. Already cover-cropped, so an asset that
   misses its ratio by a little crops instead of stretching (hard-fails list). */
.appshot__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* §2 — two plate components, not one at two sizes. */
.appshot--square   { aspect-ratio: 1 / 1;     border-radius: var(--radius--xl); }
/* All three plates carry the SAME treatment — the modifiers set ratio and
   radius only. B's used to be pure white on the sand panel, which measured
   1.03:1 against its own ground: with no border and no glyph contrast it read
   as a hole punched in the section rather than as a reserved frame, and it was
   the odd one out among its own siblings. sand-200 on sand-100 is a legible
   step and matches A and C exactly. */

/* ---- A and C: the paired rows ---- */
.approw__grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-32);
  align-items: center;
  max-width: 63rem;
  margin-inline: auto;
}

/* §4.3 — the pill-to-heading gap is NOT set here. `.pill + h2 { margin-top:
   0.8rem }` in styles.css already matches this markup; a descendant rule would
   double it and bypass the adjacency mechanism §4.3 exists to enforce. A
   previous section in this project was failed for exactly that.
   Only the line-height is touched: styles.css's `.h2` rides 0.9, which pinches
   once a 46.9px heading wraps in a 540px column. --lh-tight is §4.1's own
   published h2 value, and it is the same correction #method made. */
.approw__title { line-height: var(--lh-tight); }

.approw__body {
  margin-top: var(--sp-16);
  font-size: var(--fs-body-s);
  line-height: var(--lh-body);
  color: var(--color--grey-800);
}

/* §4 — the three bullet lines. Body-sized, so the fixed order reads
   eyebrow(small) → heading → body → bullets(body) → button. */
.appbullets { margin-top: var(--sp-20); display: grid; gap: var(--sp-12); }
.appbullets__item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--sp-8);
  align-items: start;
  font-size: var(--fs-body-s);
  line-height: var(--lh-body);
  color: var(--color--grey-800);
}
/* The house icon slot (§3.3), so the tick names no colour of its own. */
.appbullets__tick {
  display: grid;
  place-items: center;
  width: var(--sp-12);
  height: var(--sp-12);
  /* Optical centring on the FIRST line of the bullet, derived rather than
     nudged: half the difference between that line box and the disc. */
  margin-top: calc((var(--fs-body-s) * var(--lh-body) - var(--sp-12)) / 2);
  border-radius: var(--radius--circle);
  background: var(--_theme---icons--background);
  color: var(--_theme---icons--stroke);
}
.appbullets__tick svg { width: var(--sp-8); height: var(--sp-8); }

.approw__cta { margin-top: var(--sp-24); }

/* ---- Hangul metrics. Two corrections, both the same move that
   :lang(ko) .method__trigger already makes for headings. ----

   HEADINGS: more leading and a looser track, on published tokens. No fourth
   tracking value is introduced (§4.1 permits three).

   BODY: §4.1 pairs --fs-body-s with 1.3, and that is what the Latin body above
   now runs. Hangul at 1.3 sets visibly denser than the English in the same box:
   syllable blocks are full-height, with no x-height for the leading to breathe
   into, so a three-line Korean paragraph nearly closes up. The ladder tops out
   at --lh-prose (1.4), still short of what Hangul wants at this size, so
   --lh-ko-body was published in tokens.css §4 alongside the other --lh-*
   values rather than written here as a loose number. Latin keeps §4.1's 1.3;
   only :lang(ko) reads the new token. ---- */
:lang(ko) .approw__title { line-height: var(--lh-heading); letter-spacing: var(--kern-heading); }

:lang(ko) .approw__body,
:lang(ko) .appbullets__item { line-height: var(--lh-ko-body); }
/* The tick's optical offset is derived from the line box it centres against,
   so it has to follow the leading. */
:lang(ko) .appbullets__tick {
  margin-top: calc((var(--fs-body-s) * var(--lh-ko-body) - var(--sp-12)) / 2);
}

/* ---- §10.3 breakpoints: 1080 / 960 / 860 / 640 only ---- */

@media (max-width: 960px) {
  /* §10.3 — paired rows collapse to one column with the feature grid. */
  .approw__grid { grid-template-columns: minmax(0, 1fr); gap: var(--sp-24); }
  .appshot--square { width: min(100%, 26rem); margin-inline: auto; }

}

@media (max-width: 640px) {
  /* §10.3 — buttons go full-width. Same shape as .ctaopen's rule above. */
  .approw__cta, .approw__cta .btn { width: 100%; }
  .appshot--square { width: 100%; }
}

/* ============================================================
   #brands — the learner wall. Built to bar-brands.md.
   ------------------------------------------------------------
   Used by index.html and ko/index.html only, so this block belongs to that
   one section. Read the numbers against bar-brands.md's measured table, but
   read them through the fluid root first: tokens.css §1 names its steps at a
   nominal 10px root and the real root is 18.05px at 1440, so --sp-12 is
   21.7px on screen, not 12. Every figure below is quoted at 1440, which is
   the width the reference was measured at.

   NOT BUILT HERE, deliberately: mechanism 1 (the scroll-linked counter-travel),
   the responsive collapse, the loading strategy and the a11y audit. The seam
   for the motion is --brands-travel; see .brands__row.
   ============================================================ */

/* One card geometry for the whole wall — mechanism 3 is a repetition claim, so
   the width is declared once on the section and every card reads it.
     18.3rem = 330.3px at 1440, against the reference's 330.
   There is no width ladder in tokens.css to take a step from (--sp-* tops out
   at 6.4rem and --max-width is a container, not a card), so this is a
   documented rem literal on the same footing as .scenes' `min(80rem, ...)`,
   which bar.md already sanctions for the same reason.

   THE GUTTER IS --sp-12, and --sp-24 was rejected. The reference's 24px is
   7.3% of its 330px card; on our root --sp-24 (2.4rem, 43.3px) is 13.1% of
   ours — nearly double, and at that spacing the thing stops reading as a wall
   and starts reading as a grid, which is the whole distinction mechanism 2
   exists to protect. --sp-16 (28.9px, 8.8%) was the other candidate and is
   still a third wider than the reference's proportion. --sp-12 lands at 21.7px
   / 6.6%, the nearest step on both the absolute and the proportional
   reading. The cost of the choice, stated: the neighbouring #needs row runs
   --sp-24 between cards of almost the same width, so the two sections do not
   share a gutter. That is a divergence on purpose — #needs is four cards
   inside the viewport, this is eighteen running past both edges. */
.brands {
  --brands-card-w: 18.3rem;
  --brands-gap: var(--sp-12);
}

/* ---- Mechanism 5. The header: two things, centred, nothing else ----
   64rem = 1155px at 1440 against the reference's 1140px header column, and it
   is the same figure .needs__head uses, so the site's two centred section
   headers measure the same. NO EYEBROW PILL: #who opens with one and this
   deliberately does not, because mechanism 5 counts the column's children and
   a third one is its stated fail. NO PARAGRAPH, for the same reason. */
/* HEADER-TO-WALL: --sp-56 (101px), up from --sp-40 (72px). Round 1 set the
   header loose inside and tight outside — 43px heading->button against 72px
   button->row, a 1.7:1 ratio where the reference runs 20 : ~105, about 5:1.
   The heading and button are one object and the wall is another; the space
   has to say so. --sp-56 is the ladder step nearest the reference's ~105px.
   --sp-64 (115px) was rejected as the next step past it, not towards it. */
.brands__head {
  max-width: 64rem;
  margin: 0 auto var(--sp-56);
  text-align: center;
}
/* HEADING -> BUTTON is NOT set here. §4.3 publishes the adjacency
   `h2 + .button { margin-top: 0.8em }`, and styles.css:116 implements it as
   `h2 + .btn-group`, so the button sits in a .btn-group and that rule sets the
   gap — the same one every other heading-over-buttons on the site gets.
   Round 1's hand-written --sp-24 margin (43.3px) is deleted.

   What it renders, stated because it is not what §4.3's comment predicts:
   the em resolves on the .btn-group's OWN font-size (inherited body, 18.05px),
   not on the heading's, so the gap is 14.4px, not 0.8 x 46.9 = 37.5px. That is
   the published rule as the site already renders it everywhere, and it lands
   near the reference's 20px, so it is taken as-is rather than re-derived.
   Writing a margin that reproduced the 37.5px here would put this section's
   rhythm on a private rule — the thing §4.3 exists to stop.

   .btn-group is a flex row, so text-align cannot centre it; this can. */
.brands__head .btn-group { justify-content: center; }

/* The .h2 rank is published at line-height 1.0 (design-system.md §4.1, h2
   row). styles.css:76 still carries 0.9, which is the headline row's value.
   Correcting it globally would change every h2 on the page, and this piece
   may not touch other sections, so the published value is applied here only;
   the global drift is left for its own fix. --lh-tight is the token for 1.0. */
.brands .h2 { line-height: var(--lh-tight); }

/* ---- Mechanism 2. The rows bleed past both viewport edges ----
   Nine cards is 174.3rem = 3146px at 1440, 2.18x the viewport (the reference
   runs twelve cards at 4222px, 2.93x). Both overflow far enough that the
   check — a cut card against the left edge AND against the right edge — holds
   at every offset the motion piece can reach.

   The centring is .scenes' mechanism: a max-content row pushed to 50% and
   pulled back by half its own width. It DEPENDS ON `main { overflow: clip }`
   (styles.css:22) exactly as .scenes does — without that clip a 3146px row
   inside a 1440px page is a horizontal scrollbar, which mechanism 2 fails on
   explicitly. Nothing else records that coupling for this block.

   THE MOTION SEAM. transform composes three terms: the centring, the static
   offset the row is parked at, and --brands-travel, which is 0 here and is
   the ONLY thing the scroll controller is meant to write. Keeping the
   centring and the park out of the controller's hands means a controller bug
   can slide the rows but cannot un-centre them or lose the bleed. */
.brands__rows { --brands-travel: 0rem; }
.brands__row {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  gap: var(--brands-gap);
  width: max-content;
  position: relative;
  left: 50%;
  transform: translateX(calc(-50% + var(--brands-offset, 0rem) + var(--brands-travel)));
}
/* Mechanism 3 asks for one gutter on both axes, so the vertical gap between
   the rows is the same token as the horizontal gap inside them. */
.brands__row + .brands__row { margin-top: var(--brands-gap); }

/* The static park. Centred exactly, both rows would cut at the same fraction
   of a card at both edges and the wall would read as a two-row grid that
   happens to overflow — the reference's rows are visibly out of step with
   each other. The card pitch is 19.5rem (351.9px at 1440); these two offsets
   are an eighth of a pitch left and a fifth of a pitch right, which leaves the
   edge cards 42% / 68% visible on row A and 77% / 33% on row B. Offsets near a
   whole pitch were rejected by measurement, not by eye: at half a pitch the
   left-hand card of row B shows a 5px sliver, which reads as a rendering
   fault rather than as a wall continuing. Both values are --sp-* steps so the park stays on the
   ladder. This is a 1440 choice and it is provisional — once mechanism 1
   lands the offset sweeps continuously and this becomes only the value the
   rows hold at rest, which is also what they must hold under
   prefers-reduced-motion (bar-brands.md's hard-fail list). */
.brands__row--a { --brands-offset: calc(-1 * var(--sp-24)); }
.brands__row--b { --brands-offset: var(--sp-40); }

/* ---- Mechanism 3. One card, one hairline, one radius ----
   RATIO. 800/536 = 1.4925 is the derivatives' own ratio and the reference's
   330/221 is 1.4932 — a 0.05% difference, so the images fill the window with
   no crop at all. bar-brands.md's ruling holds the ratio; the assets were cut
   to it upstream, so nothing is cropped here and no face is cut.

   RADIUS. --radius--xl (1.6rem, 28.9px) against the reference's 32px, 10%
   under. --radius--2xl (2.4rem, 43.3px) was rejected: it is 35% over, and it
   is what .scene already uses on the big portrait windows in #needs, so the
   two sections would stop being distinguishable by their rounding. --radius--l
   (21.7px) is the ordinary card radius on this site, and mechanism 3 asks for
   visibly rounder than that — --radius--xl is a third rounder than the
   ordinary card and exactly twice the panel's radius below, which is the
   32/16 relationship the reference's two corners have.

   BORDER. One hairline, the same on all eighteen. --hairline is grey-100,
   which resolves over this white page to about #ebeeee against the
   reference's #e2e2e7 — the site's own hairline, not a new grey. Per-card
   border colours are a stated fail, so there is exactly one rule.

   BACKGROUND. sand-200 under the photograph, not white: if an image 404s the
   card stays a plate that holds its ratio rather than a white hole with a
   panel floating in it. Same reasoning, same rung, as .appshot. */
.bcard {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  flex: none;
  width: var(--brands-card-w);
  aspect-ratio: 800 / 536;
  border: 1px solid var(--hairline);
  border-radius: var(--radius--xl);
  background: var(--color--sand-200);
}
/* Absolute, not a block-level image, so the panel and the photograph
   share one stacking context and the hover scale below cannot push the card. */
.bcard__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--dur) var(--ease-global);
}

/* ---- Mechanism 4, deliberately short by one: the panel, and no chip ----
   The reference's top-left chip carries an offer ("1.5% cashback"). We have no
   offer, and bar-brands.md forbids inventing one. Round 1 filled the slot with
   a kind of learner; it failed twice over — the KO edition stamped the English
   "K–12" on five cards, and the vocabulary was the builder's, not the site's.
   The site's own vocabulary was then tried and also rejected: the header menu
   names five audiences for eighteen cards, so any chip drawn from it repeats
   a word on up to five cards in a row, AND the panel's category line already
   carries exactly that word. A chip that restates the panel under it is a
   third overlay's worth of noise with none of the information. Cities were
   the only value that varied, and only seven of the eighteen photographs have
   one. Nothing honest varied across all eighteen, so the chip is removed from
   every card. The two-overlay count is traded for a wall that does not repeat
   itself; that trade is the coordinator's stated order of priority.

   THE PANEL INSET is --sp-8 (14.4px) against the reference's 13px, the
   nearest step. With the chip gone, the reference's 17px chip inset no longer
   needs a step of its own. */

/* The panel reads --_theme---cards--*, which is already white ground /
   grey-1000 heading / grey-800 body — the reference's own #ffffff, #282836,
   #504f5f arrangement, on slots that exist. No new slot was published for
   this section, because the one that fits was already there.
     grey-1000 on white  16.1:1
     grey-800  on white   7.9:1
   Its radius is --radius--m (14.4px) against the card's --radius--xl
   (28.9px): the ratio is exactly 2:1, which is the relationship mechanism 4
   checks ("visibly tighter", 16 against 32), rather than the raw px.
   Height is left to the content: 14.4 padding x2 + a 21.1px name line +
   an 18.8px category line = 69px against the reference's 70. Pinning 70px
   would break the moment the Korean line wraps. */
.bcard__panel {
  position: absolute;
  left: var(--sp-8);
  right: var(--sp-8);
  bottom: var(--sp-8);
  z-index: 1;
  display: flex;
  align-items: center;
  gap: var(--sp-8);
  padding: var(--sp-8) var(--sp-12);
  border-radius: var(--radius--m);
  background: var(--_theme---cards--background);
}

/* The reference's 36px round slot holds a partner logo. WE HAVE NO PARTNERS
   and inventing one is a fabricated claim, not a placeholder, so the slot
   holds the card's audience icon instead — the same five shapes the header's
   Solutions menu draws beside the same five names, so glyph and category line
   are one vocabulary, not two. ONE treatment for all eighteen cards. The colour never
   varies by card — mechanism 3's "a card with its own colour fails" is about
   the border, but a wall whose eighteen glyphs are eighteen colours fails the
   same idea. sand-200, not sand-100, for .appshot's reason: a 2% step against
   the panel's white reads as a rendering fault rather than as a deliberate
   disc. --sp-20 is 36.1px against the reference's 36. */
.bcard__glyph {
  flex: none;
  display: grid;
  place-items: center;
  width: var(--sp-20);
  height: var(--sp-20);
  border-radius: var(--radius--circle);
  background: var(--color--sand-200);
  color: var(--color--grey-800);
}
.bcard__glyph svg { width: 1.1rem; height: 1.1rem; }

/* ---- Mechanisms 6 and the 700 problem ----
   Three sizes in the section and no more: --fs-h2 on the heading (46.9px),
   --fs-body-s on the button label AND the name (16.2px — the same rung, which
   is what keeps the set at three rather than four), --fs-body-xs on the
   category (14.4px; with the chip gone it is the only thing on that rung).
   Span 46.9/16.2 = 2.89:1 against the reference's
   3.25:1; the gap is the h2 rank bar-brands.md pins us to, not a free choice.

   THE 700. The webfont ships wght@400;500, so asking for 700 renders
   synthetic bold — a smeared outline, not a weight. The reference leans on 700
   twice, on the heading and on the card name, and both are re-expressed
   without it:
     HEADING — the rank already carries it. .h2 is 46.9px at 500 with
       --kern-headline (-0.024em) and §4.1's 1.0 leading, so it reads as a
       display line by size, tracking and leading rather than by mass.
     NAME — the contrast against the category underneath does the work. 500 vs
       400 is only half of it; the name is also a rung larger (16.2 vs 14.4)
       and a step darker (grey-1000 vs grey-800). Three signals at once out-
       separate the one signal the reference used.
   font-synthesis-weight is switched off across the section so that a future
   `<b>`, or a UA default, cannot quietly reintroduce the smear this rules out. */
.brands { font-synthesis-weight: none; }
.bcard__text { min-width: 0; }
/* The name is set on §4.1's body-s row — 1.3 leading, 0 tracking — at 500
   for the label role. Round 1 set it at the heading row's 1.2 and then needed
   a :lang(ko) patch to loosen it for Hangul; on the body row the Latin and
   Hangul names share one published value and the patch is gone. */
.bcard__name {
  display: block;
  font-size: var(--fs-body-s);
  font-weight: 500;
  line-height: var(--lh-body);
  color: var(--_theme---cards--heading);
}
.bcard__cat {
  display: block;
  font-size: var(--fs-body-xs);
  font-weight: 400;
  line-height: var(--lh-body);
  color: var(--_theme---cards--text);
}

/* ---- Hover ----
   THE MEDIA SCALES, NOT THE CARD — .scene's ruling, and it applies harder
   here: the cards sit shoulder to shoulder at a 21.7px gutter, so growing one
   would visibly crowd its neighbours and, inside a row that the motion piece
   is about to translate, would fight that transform on the same axis.
   1.04, where .scene uses 1.06: that card is 313px of portrait and this one is
   221px of landscape, so the same proportional zoom pushes more of the frame
   out of a shorter window. Gated behind (hover: hover) because a touch device
   latches :hover and would leave one card zoomed after a tap. */
@media (hover: hover) {
  .bcard:hover .bcard__img { transform: scale(1.04); }
}

/* §10.4 — the page's global reduced-motion block in styles.css names its
   hover transforms one component at a time, so this one names itself. */
@media (prefers-reduced-motion: reduce) {
  .bcard__img { transition: none; }
  .bcard:hover .bcard__img { transform: none; }
}

/* ---- Hangul heading, the same correction .approw__title records ----
   The global `:lang(ko) .h2` at the top of this file sets 1.16 / -0.02em —
   two values §4.1 does not publish (it permits three trackings: -0.024,
   -0.016, 0.08em). This section does not inherit them. It takes the pair the
   later KO fixes in this file settled on (.method__trigger, .approw__title): --lh-heading (1.2) for Hangul's full-height syllable
   blocks, and --kern-heading (-0.016em), the looser of the two published
   heading trackings, because -0.024 closes Hangul up. The global rule is left
   alone — it governs other sections this piece may not touch.

   The card labels need no KO rule: name and category are both on body rows
   now (1.3 / 0), and --lh-ko-body (1.7) is a paragraph correction that would
   only push a one-line label off the panel's centre. */
:lang(ko) .brands .h2 { line-height: var(--lh-heading); letter-spacing: var(--kern-heading); }

/* ============ Learnable languages (#languages) — getlucida.com's strip ============

   Measured on getlucida.com at 1440: one row of flag + name pairs, 24px round
   flags, 10px between flag and name, 60px between pairs, name 14px/500 in near-
   black, the band ~94px tall with 27px above and below, both ends faded by a
   0→12.5% / 87.5→100% mask. Its speed could not be measured (the page drives it
   from a rAF ticker, which the hidden browser pane throttled to zero), so the
   40s loop below is a judgement, ~50px/s.

   It rides the existing .marquee component, whose mask is already 12% / 88% —
   within half a percent of the reference — and which already pauses on hover
   and focus and stands still under reduced motion. Only the gap differs, so
   the keyframe is re-declared: the stock one subtracts half of --sp-16, and at
   any other gap the loop would jump at the seam.

   Tokens: 24px flag → 1.35rem (24.4px); 10px → --sp-8 (14.4px, the ladder has
   nothing nearer that clears a round flag); 60px → --sp-32 (57.8px); 14px/500 →
   --fs-body-xs at 500, the label row; 27px → --sp-16 (28.9px). */
.langs { padding-block: var(--sp-16); }
.langs__marquee { margin-top: 0; outline-offset: var(--sp-4); }
.langs__row {
  gap: var(--sp-32);
  animation-name: langs-marquee;
  animation-duration: 40s;
}
@keyframes langs-marquee { to { transform: translate3d(calc(-50% - var(--sp-32) / 2), 0, 0); } }
.langs__item {
  display: flex;
  align-items: center;
  gap: var(--sp-8);
  font-size: var(--fs-body-xs);
  font-weight: 500;
  line-height: var(--lh-body);
  color: var(--color--grey-1000);
  white-space: nowrap;
}
.langs__item img { width: 1.35rem; height: 1.35rem; flex: none; }
/* Flags: Tossface (Toss's emoji font), user direction 2026-09-27.
   Copyright (c) 2022, 2023 Viva Republica, with Reserved Font Name Tossface.
   Tossface License — full text at /licenses/tossface-LICENSE.txt;
   https://github.com/toss/tossface. Only its regional-indicator part
   (TossFaceFontMac-10) is declared here. 2026-10-14: self-hosted with only the
   13 flags the site shows (unused glyphs removed, kept glyph images
   byte-identical; agreed with Toss): ~55 KB instead of ~680 KB. Add a flag to the
   page → regenerate the file (tools/fonts/subset_tossface.py). */
@font-face {
  font-family: "Tossface Flags";
  src: url("../assets/fonts/tossface/TossFaceFontMac-10.subset.woff2") format("woff2");
  unicode-range: U+1F1E6-1F1FF;
  /* block, not swap (2026-10-09): swap painted the OS flag first, so two flag
     styles showed on one page while the font loaded. */
  font-display: block;
}
/* One flag component for every page (2026-10-09 audit): the official Tossface
   glyph, never an OS emoji or a drawn SVG. Size comes from the context. */
.toss-flag { flex: none; font-family: "Tossface Flags", sans-serif; line-height: 1; font-style: normal; }
.langs__flag {
  flex: none;
  font-family: "Tossface Flags", sans-serif;
  font-size: 1.35rem;
  line-height: 1;
}

/* WCAG 2.2.2 — hover-pause alone leaves touch readers with no way to stop it.
   A click or tap on the strip toggles this (js/main.js). */
.langs__marquee.is-paused .marquee__row { animation-play-state: paused; }

/* Reduced motion: the stock .marquee fallback is a sideways-scrolling strip,
   which here would show the hidden duplicate too. Instead the list stops and
   wraps into one centred block of the 13 real items. */
@media (prefers-reduced-motion: reduce) {
  .langs__marquee { overflow: visible; -webkit-mask-image: none; mask-image: none; }
  .langs__row { width: auto; flex-wrap: wrap; justify-content: center; gap: var(--sp-12) var(--sp-24); }
  .langs__item[aria-hidden="true"] { display: none; }
}

/* ============ The moving gradient word — §8.4.1, built to bar-gradtext.md ============

   Verbatim from lattice.com/about ("A partner you can trust."): one inline
   span around one emphasised word, the letters themselves carrying the ink —
   no box, highlight or underline. The span restyles nothing typographic, so it
   inherits the heading's size, weight, tracking and leading; punctuation after
   it stays in the heading ink (outside the span, or in .gradtext__ink below).

   Palindrome stops (1, 2, 3, 2, 1) at 100deg on an image twice the WORD's
   width, slid by exactly its own size (0% -> -200%): 12s, linear, forever, no
   trigger, the pale band travelling left to right (as the reference's does,
   filmed) with no seam at the loop.
   -webkit-text-fill-color, not `color`, clears the glyphs, so a parent that
   sets `color` through a slot cannot paint over the gradient. Stops are the
   --_theme---gradtext slots (tokens.css). */
.gradtext {
  position: relative;
  background-image: linear-gradient(100deg,
    var(--_theme---gradtext--stop-1), var(--_theme---gradtext--stop-2), var(--_theme---gradtext--stop-3),
    var(--_theme---gradtext--stop-2), var(--_theme---gradtext--stop-1));
  background-size: 200% 100%;
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: gradtext 12s linear infinite;
}
@keyframes gradtext { 0% { background-position: 0% center; } 100% { background-position: -200% center; } }

/* PUNCTUATION THAT KERNS INTO THE WORD (ours, not the reference's; Lattice's
   "trust" ends on a "t" and never meets it). A background paints only inside
   the span's box, and in EN "your way." the arm of the final "y" reaches
   past its own advance into the kerned full stop. With the full stop outside
   the span, that arm rendered transparent, cut on a vertical line. Padding
   the span's end to reach it breaks shaping at that edge (Chrome drops the
   "y." kern at any inline edge carrying padding, border or margin), and the
   engines disagree on it, so no compensation value is safe. Instead the
   full stop sits INSIDE the span, in a bare nested inline box: no padding,
   so the kern survives in every engine, and the gradient box now reaches
   past the arm. The nested box paints the heading's own ink over the
   clipped gradient. currentColor is the heading's color (inherited
   unchanged through .gradtext), so the full stop matches the words outside
   the span without naming a second source. KO needs none of this:
   "나답게" has no overhang and is followed by a space.

   THE RIM. background-clip:text masks the gradient by ALL the span's text,
   the nested full stop included, so the gradient also sits under the stop and
   shows through its anti-aliased edge: up to 22/255 off the ink-white line at
   the turquoise phase. A text-stroke cannot cover it, because the mask takes
   in the stroke and the rim just moves outward (measured 0-0.06em: no gain).
   Instead, zero-offset, zero-blur shadows in the page's own background are
   painted under the stop's ink. Each one repaints the edge pixels towards the
   background by the glyph's own coverage, so the gradient's share falls
   geometrically, and the ink on top then lands as it does on plain text.
   Twelve is the fewest that holds every edge pixel within 3/255 of the
   ink-white line at eight phases of the loop, at 1440 and 375. They cast
   nothing outside the glyph, so weight, extent and layout are untouched. */
.gradtext__ink {
  -webkit-text-fill-color: currentColor;
  --gradtext-knockout: 0 0 0 var(--_theme---page--background);
  text-shadow:
    var(--gradtext-knockout), var(--gradtext-knockout), var(--gradtext-knockout),
    var(--gradtext-knockout), var(--gradtext-knockout), var(--gradtext-knockout),
    var(--gradtext-knockout), var(--gradtext-knockout), var(--gradtext-knockout),
    var(--gradtext-knockout), var(--gradtext-knockout), var(--gradtext-knockout);
}

/* §10.4 — Lattice ships no reduced-motion rule; ours holds the gradient
   still at its first frame (the §8.4 halo rule), still in all three stops. */
@media (prefers-reduced-motion: reduce) {
  .gradtext { animation: none; }
}

/* ============================================================
   #your-way — the scatter. Built to bar-scatter.md.
   ------------------------------------------------------------
   Reference: tedy.app/ca/en, "25 flexible spending categories", measured
   live at 1440x900 and 375x812. Its MOTION is copied with nothing added;
   type, pill, button, chip styling and colour are this system's.

   HOW A FRAME IS DRAWN. js/main.js ([data-scatter]) reads the scroll
   position inside the section as one progress value p (0 = stage pinned,
   1 = pin released) and writes four numbers on the section, each 0..1:
     --scatter-travel  the cards' straight run out from the centre
     --scatter-fade    the seven non-entry cards fading in over the entry card
     --scatter-copy    pill / heading / body / button fading and rising in
     --scatter-chip    the chips fading onto their corners
   Everything below is geometry derived from those four. The defaults are 1,
   so without JS — and under reduced motion, where the controller writes
   nothing — the section is simply the finished frame.

   THE MOTION, as the reference's (R10, R11, R14), both in js/main.js:
     1. Over scroll everything is LINEAR in p: the cards run straight out
        and stop dead at p 0.62; the copy and chips fade in straight lines.
     2. Over time the drawn frame follows the scroll first-order — moving on
        the very first frame and decaying (half at ~150ms, still by ~770ms),
        which is what softens the cards' stop.
   While the stage is pinned, html's site-wide smooth scroll is switched
   off (below), so the page's own glide does not stack on this one.
   The timings live in js/main.js (SCATTER_TIMELINE).

   POSITIONS. Each card's final centre is a FRACTION of the frame (--fx of
   its width, --fy of its height), measured off the reference's final frame
   (e.g. its entry card lands at +578 / -182px of a 1377 x 900 stage =
   +0.420 / -0.202). Fractions, via container query units, so the ring keeps
   its shape on any screen; the controller then checks each card against the
   copy block and pushes it straight outward if the two would touch
   (--x-px / --y-px) — the hard fail is copy and card overlapping.
   ============================================================ */

/* Sizes. The reference's two final sizes are 165 / 124px at 1440 (10.3125 /
   7.75rem on its fixed 16px root). On our fluid root (18.05px at 1440) that
   is 9.2rem = 166px and 6.9rem = 125px. There is no size ladder for media in
   tokens.css, so these are documented rem literals, the same footing as
   .brands' --brands-card-w. On phones the reference uses ONE size, 79px
   (4.95rem at 16px): 5.3rem is 79px on our 375 root. */
/* Between 961 and ~1440 the rem sizes would grow faster than the frame
   (the fluid root outpaces the width), crowding the ring against a copy
   block that does not shrink: at 1024 a 154px tile is 15% of the frame
   against 11.6% at 1440. So each size is also capped at its 1440 share of
   the viewport — 11.5vw / 8.65vw, which ARE 9.2rem / 6.9rem at 1440 — and
   meets the tablet sizes (7rem / 6rem) at 961 with no step. */
.scatter {
  --scatter-l: min(9.2rem, 11.5vw);
  --scatter-s: min(6.9rem, 8.65vw);
  --scatter-travel: 1;
  --scatter-fade: 1;
  --scatter-copy: 1;
  --scatter-chip: 1;
  /* How close the finished frame may come to the nav above and the viewport
     below, measured to the outermost thing drawn — card OR chip. js/main.js
     reads it. --sp-24 (43px at 1440): the reference's top card sits 41px
     from its stage's top. Round 2 held the cards at --sp-40 (72px), which
     left a dead band top and bottom; round 1 let a chip sit 17px under
     the nav. On desktop the chips of the top and bottom rows hang toward
     the copy, not the edge, so the ring can reach this line. */
  --scatter-edge: var(--sp-24);
  position: relative;
}

/* ---- Mechanism 1. The pinned stage ----
   The reference is 200vh tall at >=1024 with a 100svh sticky stage, so ONE
   viewport of scroll drives the whole animation. Our nav is sticky and
   opaque (§9.2), so the stage pins UNDER it — top: --nav-h, height is the
   viewport less the nav, the same box #method pins into — and the run stays
   one viewport. The tall runway exists only while the controller is live
   (.is-live): no JS, no pin; reduced motion, no pin. */
.scatter__stage {
  position: relative;
  height: calc(100svh - var(--nav-h));
  overflow: clip;              /* cards may be cut by the screen edge, never scroll it */
}
/* + --scatter-pre: the question's pre-roll, pinned scroll IN FRONT of the
   run (the question block below). The run itself is unchanged. */
.scatter.is-live { height: calc(100svh - var(--nav-h) + var(--scatter-pre, 0px) + var(--scatter-run, 100svh)); }
.scatter.is-live .scatter__stage { position: sticky; top: var(--nav-h); }
/* ONE glide, not two. styles.css gives html `scroll-behavior: smooth` for
   anchor links; a programmatic scroll then glides for ~300ms on its own and
   the frame's follow lags a second time on top of it (measured t50 228ms
   against the reference's ~150ms). While the stage is pinned — and only
   then — js/main.js marks html, and the scroll lands at once. */
html.is-scatter-pinned { scroll-behavior: auto; }

/* The ring's box. 80rem is .scenes' own cap, so at 1920 the cards spread as
   wide as #needs' row does and no wider. container-type makes the frame the
   unit for each card's cqw / cqh fraction. */
.scatter__frame {
  position: relative;
  height: 100%;
  max-width: 80rem;
  margin-inline: auto;
  container-type: size;
}

/* ---- Mechanism 5. The copy, in the cleared centre ----
   §5.4 order: pill -> heading -> body -> button, centred. The pill-to-heading
   gap is styles.css's `.pill + h2` adjacency and is not restated here.
   Arrival is the reference's own: opacity 0 -> 1 while rising and growing
   from scale 0.94. It rose 18px there; the bar asks ~18-30px, and --sp-12
   (21.7px at 1440) is the ladder step nearest the reference. */
.scatter__copy {
  position: absolute;
  left: 50%;
  top: 50%;
  z-index: 20;
  width: min(100% - 2 * var(--gutter), 40rem);
  translate: -50% -50%;
  text-align: center;
  opacity: var(--scatter-copy);
  transform:
    translateY(calc((1 - var(--scatter-copy)) * var(--sp-12)))
    scale(calc(0.94 + 0.06 * var(--scatter-copy)));
}
/* The button is not a target while the copy is still invisible. */
.scatter.is-copy-hidden .scatter__copy { pointer-events: none; }

/* HEADING — the largest type in the section by far (mechanism 5). The
   reference set 85.6px against a 16.8px body, 5.1x; `headline` (4rem, 72.2px
   at 1440) against `body` (18.05px) is 4.0x, and `jumbo` (6.4x) would push
   the copy block into the ring. Rank, weight, 0.9 leading and -0.024em come
   from .headline untouched. Measure is the element's `characters` (§4.2). */
.scatter__title { margin-inline: auto; }

/* BODY — the reference's narrow measure, 416px at 1440. 24rem is 433px. The
   slot is re-themed, as .needs__head does, rather than the colour repainted. */
.scatter__copy { --_theme---page--text-muted: var(--color--grey-800); }
.scatter__body {
  max-width: 24rem;
  margin: var(--sp-16) auto 0;
}
/* DESCRIPTION -> BUTTON by adjacency, not by a margin of its own. §4.3
   publishes the paragraph-to-action step as `p + .pill-link, p + .ratings
   { margin-top: 1.2rem }` (styles.css implements it for `p + .arrow-link`);
   the button here is the same kind of action after the same kind of
   paragraph, so it takes the same 1.2rem (21.7px at 1440). Scoped to this
   section: a global `p + .btn-group` would move buttons in sections this
   piece may not touch. .btn-group is a flex row, so it centres by
   justify-content. */
.scatter__body + .btn-group { margin-top: 1.2rem; }
.scatter__cta { justify-content: center; }

/* ---- Mechanisms 2-4. The cards ----
   Centred on the frame, then moved by one transform: a straight line from
   the centre to (x, y), scaled from --s0 down to 1 over the same stretch.
   At travel 0 every card sits dead centre at its starting scale; at 1 it is
   at its spot at its final size. --x / --y default to the fractions and are
   replaced by px only when the controller has had to clear the copy. */
.scatter__card {
  --size: var(--scatter-l);
  --x: var(--x-px, calc(var(--fx) * 100cqw));
  --y: var(--y-px, calc(var(--fy) * 100cqh));
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--size);
  aspect-ratio: 1;
  margin: calc(var(--size) / -2) 0 0 calc(var(--size) / -2);
  /* The tile's scale now: --s0 at travel 0, 1 at travel 1. The card box
     itself only TRANSLATES; the photo and the chip layer each apply --k,
     so the photo can be laid out large and scaled down (below). */
  --k: calc(var(--s0, 2.9) + (1 - var(--s0, 2.9)) * var(--scatter-travel));
  opacity: var(--scatter-fade);
  transform: translate(calc(var(--x) * var(--scatter-travel)), calc(var(--y) * var(--scatter-travel)));
}
/* RADIUS, ruling R6. At rest the corner is --radius--xl (1.6rem: 28.9px on
   a 166px tile at 1440, 17%; the reference's 32 / 165 is 19%), at every
   width. R4 makes these tiles imagery, not §9.4 cards, so the card step
   does not bind them — and no shadow. In flight the corner is NOT
   corrected: like the reference's (a fixed radius on a tile that is then
   scaled, ~86px at 2.7x), it grows with the tile, which is what lets the
   early stack fuse into one soft cluster instead of hard-cornered frames.
   The radius is judged at rest; scaling is a transform of the whole tile.
   R20: where a tile renders under ~64px (the shortest phones), js/main.js
   steps it down the §7 ladder (--tile-radius: --radius--l, --m...) so the
   corner stays at or under ~30% of the tile and it never reads as a circle. */
/* SHARPNESS. Laid out at 1x and scaled UP, the stacked photo was
   rasterised at its final 166px and magnified 2-3x (soft, 32% of the
   source's edge detail). So the photo is LAID OUT at its largest size —
   --s0 times the tile — and scaled DOWN by --k / --s0: the browser
   rasterises it large, and every frame is a reduction. The radius is laid
   out at the same factor, so it still renders --radius--xl at rest and
   grows with the tile in flight (R6). No will-change, which would freeze
   the raster at one scale. */
.scatter__img {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(100% * var(--s0, 2.9));
  height: calc(100% * var(--s0, 2.9));
  max-width: none;
  object-fit: cover;
  border-radius: calc(var(--tile-radius, var(--radius--xl)) * var(--s0, 2.9));
  transform: translate(-50%, -50%) scale(calc(var(--k) / var(--s0, 2.9)));
}
/* The chip's layer: the tile's own box, scaled by --k about its centre, so
   the chip rides the tile exactly as it did inside it (the reference's do). */
.scatter__pin {
  position: absolute;
  inset: 0;
  transform: scale(var(--k));
}

/* Mechanism 2 — the entry card. Visible from the first frame, on top of the
   stack, starting at 2.15x: 357px at 1440, a quarter of the frame, the
   reference's 355px of 1377. The other seven start at 2.9x (one at 3x, as
   there) and fade in over it.
   Departures from the measured fractions, each for our frame:
     basics 0.435 (was 0.450) so its label fits between card and screen edge;
     the entry card -0.100 (was -0.202) and hobby -0.160 (was -0.138), so
     the right side's middle is not left empty beside the copy (round 2);
     presentation 0.160 (was 0.117), so the top row spreads right as well;
     presentation and basics sit a step in from the edge line (-0.33 / 0.33)
     so no two tiles share a top — eight heights, as the reference has;
     travel 0.270 (was 0.300), which leaves basics' label room between the
     two tiles from 961 to 1280, where the frame is narrowest.
   Vertical fractions past the --scatter-edge line are clamped to it by the
   controller, which is what lets the ring reach the stage's height. */
.scatter__card--everyday      { --fx:  0.415; --fy: -0.100; --s0: 2.15; z-index: 16; opacity: 1; }
.scatter__card--business-trip { --fx: -0.276; --fy: -0.420; --s0: 3;    z-index: 10; }
.scatter__card--presentation  { --fx:  0.160; --fy: -0.330; --size: var(--scatter-s); z-index: 14; }
.scatter__card--travel        { --fx:  0.270; --fy:  0.293; z-index: 13; }
.scatter__card--major         { --fx: -0.372; --fy:  0.266; z-index: 12; }
.scatter__card--class-review  { --fx: -0.189; --fy:  0.440; --size: var(--scatter-s); z-index: 11; }
.scatter__card--hobby         { --fx: -0.431; --fy: -0.160; --size: var(--scatter-s); z-index: 9; }
.scatter__card--basics        { --fx:  0.435; --fy:  0.330; --size: var(--scatter-s); z-index: 8; }

/* ---- Mechanism 6. The chips ----
   One per card, pinned half off one of its corners. The user's call, over
   ruling R1 (recorded in bar-scatter.md): pastel chips, not the neutral
   .scene__label. Type is §4.1.1's row for a label over imagery, --fs-body-s
   at 500 (16.2px at 1440, 13.4px at 375, over the ~12px floor). Fills cycle
   three tints by position, the way §9.8 cycles its quote tints, each on its
   own family's ink (§3.2), through the pill slots (§3.3).
   IN FLIGHT the chip rides its tile's scale, as the reference's do (its
   first chip measured 146px wide against 123px at rest — 1.19x — when it
   first shows at p 0.56). Ours now start at the same p.
   RADIUS: --radius--tag (1rem), §7's radius for tags, "pill-shaped at their
   size". The chip is 2.1em tall (1.3 line + 2 x 0.4em) = 1.89rem, so half
   its height is 0.945rem and 1rem always rounds the ends fully, at every
   width, because both ride the same root. (Round 1's 3em was not on the
   ladder.)
   IN FLIGHT, too: chips come in from p 0.54 with their tiles still at up
   to ~1.2x, as the reference's do.

   THE HANG. --chip-drop is minus a THIRD of the chip's own height — line
   box plus both paddings, in the chip's em — so top/bottom: var(--chip-drop)
   hangs a third of the chip past the tile's edge and keeps two thirds on
   the photo; with the --sp-8 sideways hang (the reference's ~0.85rem) about
   40% of the chip is off its tile, the reference's own 31-44%. Declared on
   the card so the corner rules can name it; the em resolves on the chip.
   These corners are the no-JS frame: js/main.js re-places every chip for
   the screen (it may slide one along its edge, or move it to the other
   edge, to keep it off the copy, other tiles and other chips). */
.scatter__card {
  /* §9.6's tag is padded 0.2 / 0.4rem at its 0.5rem type — 0.4 / 0.8em.
     The chip keeps those proportions at its own size (R15: body-s / 500). */
  --chip-pad-y: 0.4em;
  --chip-pad-x: 0.8em;
  --chip-drop: calc(-1 / 3 * (var(--lh-body) * 1em + 2 * var(--chip-pad-y)));
  --chip-hang: calc(-1 * var(--sp-8));
}
.scatter__chip {
  position: absolute;
  top: var(--chip-t, auto);
  right: var(--chip-r, auto);
  bottom: var(--chip-b, auto);
  left: var(--chip-l, auto);
  padding: var(--chip-pad-y) var(--chip-pad-x);
  border-radius: var(--radius--tag);
  background: var(--_theme---pills--pill-background);
  color: var(--_theme---pills--pill-text);
  font-size: var(--fs-body-s);
  font-weight: 500;
  line-height: var(--lh-body);
  white-space: nowrap;
  opacity: var(--scatter-chip);
  /* The rise runs over the FIRST HALF of the fade and then holds: from 0.5
     opacity on, the chip is already where it rests, so the clearances it
     keeps at rest (R19) hold for every frame it is readable. */
  transform: translateY(calc((1 - min(1, 2 * var(--scatter-chip))) * 0.5em));
}
.scatter__card:nth-child(3n + 1) { --_theme---pills--pill-background: var(--color--lime-100); --_theme---pills--pill-text: var(--color--lime-1000); }
.scatter__card:nth-child(3n + 2) { --_theme---pills--pill-background: var(--color--pink-100); --_theme---pills--pill-text: var(--color--pink-1000); }
.scatter__card:nth-child(3n + 3) { --_theme---pills--pill-background: var(--color--blue-100); --_theme---pills--pill-text: var(--color--blue-1000); }

/* Corners vary card to card. The top row's chips hang off BOTTOM corners
   and the bottom row's off TOP corners — toward the copy, never toward the
   nav or the viewport's foot — so the ring can reach --scatter-edge. */
.scatter__card--everyday,
.scatter__card--business-trip { --chip-r: var(--chip-hang); --chip-b: var(--chip-drop); }
.scatter__card--presentation  { --chip-l: var(--chip-hang); --chip-b: var(--chip-drop); }
.scatter__card--major,
.scatter__card--basics,
.scatter__card--class-review  { --chip-l: var(--chip-hang); --chip-t: var(--chip-drop); }
.scatter__card--travel        { --chip-r: var(--chip-hang); --chip-t: var(--chip-drop); }
.scatter__card--hobby         { --chip-r: var(--chip-hang); --chip-t: var(--chip-drop); }

/* ---- Hangul, §4.1.2 ----
   The global `:lang(ko) .headline` at the top of this file sets 1.16 /
   -0.02em, values §4.1 never published; this section takes the published
   row instead, as .brands and .approw do. Body copy to 1.7, tracking 0. */
:lang(ko) .scatter__title { line-height: var(--lh-heading); letter-spacing: var(--kern-heading); }
:lang(ko) .scatter__body  { line-height: var(--lh-ko-body); letter-spacing: 0; }

/* ---- §10.3 ----
   The run shortens with the screen exactly as the reference's does: 168vh
   tall (68% of a viewport of scroll) between phone and desktop, 150vh (half
   a viewport) on phones. */
@media (max-width: 960px) {
  /* The reference's middle sizes, 7rem / 6rem (112 / 96px on its 16px
     root): at 9.2rem a 768 screen gives the ring cards a fifth of its
     width and the outer ones run off the edge. */
  .scatter { --scatter-run: 68svh; --scatter-l: 7rem; --scatter-s: 6rem; }
  /* BAND spots (--bx / --by). From 641 up the controller first tries the
     ring (the --fx / --fy spots): side tiles sit beside the heading's and
     the body's TEXT, which at 768 leaves 167px a side. Only where a side
     tile cannot fit inside the gutter and clear of the copy — measured,
     per screen — does it fall back to these two bands, four columns each,
     held inside the gutter (no bleed above 640, R7). */
  .scatter__card--hobby         { --bx: -0.380; --by: -0.323; }
  .scatter__card--business-trip { --bx: -0.140; --by: -0.429; }
  .scatter__card--presentation  { --bx:  0.120; --by: -0.330; }
  .scatter__card--everyday      { --bx:  0.380; --by: -0.420; }
  .scatter__card--major         { --bx: -0.360; --by:  0.330; }
  .scatter__card--class-review  { --bx: -0.120; --by:  0.435; }
  .scatter__card--travel        { --bx:  0.140; --by:  0.330; }
  .scatter__card--basics        { --bx:  0.380; --by:  0.420; }
}
@media (max-width: 640px) {
  /* Mechanism 7, phone: one tile size and the same two bands, but here the
     outer columns are deliberately cut by the screen edge (R7), as the
     reference's are. The reference's phone frame also keeps one tile beside
     its heading; ours would need a heading measure narrower than any
     published `characters` step leaves room for at 375 (R13), so all eight
     stay in the bands. Fractions of a 375 x 758 frame (812 less the nav);
     js/main.js derives the verticals, so 375x667 and 320x640 fit too. */
  .scatter { --scatter-run: 50svh; --scatter-l: 5.3rem; --scatter-s: 5.3rem; --scatter-edge: var(--sp-40); }
  /* The heading steps down to --fs-h1 here, the hero's own phone ruling
     (R4c, .hero-slider__title) and the reference's phone heading (52px).
     At `headline` the KO heading sets four lines at 320 and the copy block
     leaves no band for the cards on a 640-tall screen. */
  .scatter__title { font-size: var(--fs-h1); line-height: var(--lh-tight); }   /* §4.1 h1 row: 1.0 */
  .scatter__card--hobby         { --fx: -0.440; --fy: -0.323; --bx: -0.440; --by: -0.323; }
  .scatter__card--business-trip { --fx: -0.173; --fy: -0.429; --bx: -0.173; --by: -0.429; }
  .scatter__card--presentation  { --fx:  0.107; --fy: -0.330; --bx:  0.107; --by: -0.330; }
  .scatter__card--everyday      { --fx:  0.373; --fy: -0.420; --bx:  0.373; --by: -0.420; }
  .scatter__card--major         { --fx: -0.400; --fy:  0.330; --bx: -0.400; --by:  0.330; }
  .scatter__card--class-review  { --fx: -0.147; --fy:  0.435; --bx: -0.147; --by:  0.435; }
  .scatter__card--travel        { --fx:  0.147; --fy:  0.330; --bx:  0.147; --by:  0.330; }
  /* 0.44, not further out: the new basics photo puts her face at 62% of
     the frame, and at 0.467 the screen edge cut it in half. */
  .scatter__card--basics        { --fx:  0.440; --fy:  0.420; --bx:  0.440; --by:  0.420; }
  /* Chips stay on screen. A chip on the outer corner of a card near the
     edge would be cut with it, so those run toward the middle instead:
     everyday, major and the two cards the edge cuts carry theirs on their
     INNER corner, 2.4rem in from the card's inner edge and
     running inward — the reference does the same with its own cut card
     (chip inset, not hung). Hobby and basics take the BOTTOM inner corner:
     a chip cannot rise above a neighbouring card (each card is its own
     stacking context), and on top those two ran under one. */
  .scatter__card--everyday { --chip-r: calc(100% - var(--sp-24)); --chip-t: var(--chip-drop); --chip-b: auto; }
  .scatter__card--major    { --chip-l: calc(100% - var(--sp-24)); --chip-r: auto; }
  /* Presentation's top-left chip met business-trip's bottom-right one in
     the gap between the two cards; it takes its own bottom-right corner. */
  .scatter__card--presentation { --chip-l: auto; --chip-t: auto; --chip-r: var(--chip-hang); --chip-b: var(--chip-drop); }
  .scatter__card--hobby    { --chip-r: auto; --chip-l: calc(100% - var(--sp-24)); --chip-t: auto; --chip-b: var(--chip-drop); }
  .scatter__card--basics   { --chip-l: auto; --chip-r: calc(100% - var(--sp-24)); --chip-t: auto; --chip-b: var(--chip-drop); }
}

/* §10.4 — the finished frame, standing still. The controller also refuses
   the pin; this holds even if the preference flips before it hears. */
@media (prefers-reduced-motion: reduce) {
  .scatter.is-live { height: auto; }
  .scatter.is-live .scatter__stage { position: relative; top: 0; }
  .scatter__copy, .scatter__card, .scatter__chip { opacity: 1; transform: none; }
  .scatter__card {
    transform: translate(var(--x), var(--y));
  }
}

/* ============================================================
   #your-way — the question, a pre-roll in front of the scatter.
   Built to bar-scatter-question.md.
   ------------------------------------------------------------
   Reference: toss.im/en-us, "Money doesn't stop at banking", measured live
   at 1440x900 on 2026-09-28: two sentences, one <span> per word, scrubbed
   by scroll in a pinned stage; sentence 2 replaces sentence 1 in the same
   centred place. Type, ink and spacing are this system's.

   THE PRE-ROLL. --scatter-pre is pinned scroll added IN FRONT of the
   scatter's run (.scatter.is-live's height, above). js/main.js measures it:
   the first --scatter-pre of the pin drives the words, the rest drives the
   scatter exactly as before (SCATTER_TIMELINE is relative to its own run).
   Through the whole pre-roll the scatter is drawn at its p 0: the entry
   card alone. Its length is Toss's pacing, in px of scroll at 1440x900:
     per-word ramp 300 · stagger 65 · hold 400 from a line's last word in
     to its first word out · sentence 2 starts entering when sentence 1's
     first word is half gone (238 into its ramp) · a 100px beat of the
     photo alone before the cards move.
     EN, 9 + 8 words: 820 + 400 + 238 + 755 + 400 + 755 + 100 = 3468 = 385svh
     KO, 5 + 6 words: 560 + 400 + 238 + 625 + 400 + 625 + 100 = 2948 = 328svh
   The controller lays that same schedule over whatever length this
   resolves to, so on other screens every px above scales with the height.

   THE WORD. The controller writes two numbers on each word, 0..1 and
   already eased: --w-in (ease-out cubic: most of it in the first third)
   and --w-out (ease-in cubic: slow start, fast end).
     in   opacity 0 -> 1 · scale 0.94 -> 1 · rise --sp-8 -> 0 · blur --sp-8 -> 0
     out  opacity 1 -> 0 · scale 1 -> 1.08 · lift 0 -> --sp-4 · blur 0 -> 5/6 of --sp-8
   Toss: +14px and 14px of blur in, -8px and 12px out. At 1440 --sp-8 is
   14.4px, --sp-4 7.2px and 5/6 of --sp-8 12.0px.

   PLACE. Centred across the frame, inside the gutters, and centred in the
   white band between the stage's top (under the nav) and the entry photo's
   top at p 0 — Toss sets its heading above its object. The controller
   measures that band (--scatter-q-room); the fallback is the same sum.

   RANK (ruling Q3). ONE rank at every width, h3 — §1: no type-size
   breakpoints (the hero's R4c step is the hero's alone). Under the answer's
   `headline` / phone h1 everywhere. §4.1's h3 row, 1.1 / -0.016em, DM Sans
   500, the page's heading ink. KO per §4.1.2 (1.2 / -0.016em, keep-all
   inherited from `:lang(ko) body`). A sentence may wrap to 3 lines at 320
   only; 2 at most from 375 up.

   NO PRE-ROLL, NO WAVE. Without JS and under reduced motion the section is
   not .is-live: no pre-roll is added and the per-word lines are not drawn.
   The question stays in its visually-hidden sentence, read before the
   section's heading.
   ============================================================ */
.scatter { --scatter-pre: 385svh; }
.scatter:lang(ko) { --scatter-pre: 328svh; }

.scatter__question {
  --scatter-q-rise: var(--sp-8);
  --scatter-q-lift: var(--sp-4);
  --scatter-q-blur-in: var(--sp-8);
  --scatter-q-blur-out: calc(var(--sp-8) * 5 / 6);
  position: absolute;
  top: 0;
  left: var(--gutter);
  right: var(--gutter);
  z-index: 20;
  height: var(--scatter-q-room, calc(50% - var(--scatter-l) * 2.15 / 2));
  display: grid;
  place-items: center;
  margin: 0;
  text-align: center;
  color: var(--_theme---page--text);
  font-size: var(--fs-h3);
  font-weight: 500;
  line-height: var(--lh-snug);           /* §4.1 h3 row: 1.1 */
  letter-spacing: var(--kern-heading);
  pointer-events: none;
}
/* Both sentences in ONE grid cell: sentence 2 replaces sentence 1 in place. */
.scatter__line {
  grid-area: 1 / 1;
  max-width: 100%;
  text-wrap: balance;
}
.scatter:not(.is-live) .scatter__line { display: none; }
.scatter__word {
  display: inline-block;
  opacity: calc(var(--w-in, 0) * (1 - var(--w-out, 0)));
  transform:
    translateY(calc((1 - var(--w-in, 0)) * var(--scatter-q-rise) - var(--w-out, 0) * var(--scatter-q-lift)))
    scale(calc(0.94 + 0.06 * var(--w-in, 0) + 0.08 * var(--w-out, 0)));
  filter: blur(calc((1 - var(--w-in, 0)) * var(--scatter-q-blur-in) + var(--w-out, 0) * var(--scatter-q-blur-out)));
}
:lang(ko) .scatter__question { line-height: var(--lh-heading); }
@media (prefers-reduced-motion: reduce) {
  .scatter__line { display: none; }
}

/* ============================================================
   #curriculum — two picture tiles under a centred opener.
   Built to bar-curriculum.md (v2).
   ------------------------------------------------------------
   Used by index.html and ko/index.html only; everything is namespaced
   .curriculum*. Two references, two jobs (bar v2):
     speak.com   "The only way to learn a language is to speak it" —
                 STRUCTURE: centred opener, two boxes side by side, one
                 centred button. (Its floating note on the photo was
                 dropped at the user's request, 2026-09-27.)
     klarna.com/us/investments "Start investing in minutes" — the BOX:
                 a flat, square, rounded picture tile, its title and text
                 BELOW it, left-aligned; a subtitle tight under the heading.
   Fonts, colours, weights and components are AvoLingo's.

   THE RHYTHM, reference px -> --sp-* ladder at 1440:
     heading -> description    ~8 (Klarna) -> --sp-8   14.4px
     description -> tiles      24 (Klarna) -> --sp-16  28.9px
     tile gap                  16          -> --sp-8   14.4px
     tile -> first text line   16 (Klarna) -> --sp-8   14.4px
     title -> text             4           -> --sp-4    7.2px
     tiles -> close            48 (Speak)  -> --sp-24  43.3px
   heading -> description is --sp-8 rather than the nearer --sp-4: at 7.2px
   the .h2's descenders (lh 1.0) touch the 18px description's ascenders.
   description -> tiles is --sp-16 rather than the nearer --sp-12 (21.7):
   at --sp-12 it would sit only one step above the tile gap and the opener
   would read as part of the row; --sp-16 keeps it at 2x the heading gap.

   TYPE, Klarna's ratios on the §4.1 ladder (Klarna 64 / 32 / 16):
     heading  .h2        46.9px   heading / title = 1.62  (bar: 1.6-2.4)
     title    --fs-h4    28.9px   title / text    = 1.78  (bar: >= 1.5)
     text     --fs-body-s 16.2px
   .h1 (57.8) would give Klarna's exact 2.0 against h4, but the 107-character
   EN heading sets three lines at 1440 in .h1; the bar asks for two.
   ============================================================ */

/* ---- Mechanisms 1 & 2. The opener: pill -> heading -> description ----
   Centred. The pill -> heading gap is styles.css's adjacency rule
   `.pill + h2`, not restated (the reasoning .method__head records). The
   description's muted ink comes from RE-THEMING the slot, as .needs__head
   does (§3.3), and it keeps .needs__head's 36rem measure. */
.curriculum__head {
  margin: 0 auto var(--sp-16);
  text-align: center;
  --_theme---page--text-muted: var(--color--grey-800);
}
/* .h2 rides 0.9 in styles.css; on a two-line heading that pinches, so §4.1's
   h2 row (1.0) is applied, as .hiw__heading does. */
.curriculum__heading { line-height: var(--lh-tight); }
.curriculum__lede {
  margin: var(--sp-8) auto 0;
  max-width: 36rem;
}

/* ---- Mechanism 3. Two equal square tiles spanning the container ----
   50/50 (coordinator ruling C1). minmax(0, 1fr) so a long word cannot
   unequal the tracks. role="list" in the markup: styles.css strips
   list-style from every ul (see .scenes). No surface on the box itself:
   no background, shadow, border or padding — the picture is the tile. */
.curriculum__boxes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-8);
  align-items: start;
  /* Sized to Speak's boxes (user, 2026-09-27: "too big next to Speak").
     Speak's card is 622 x 625 at 1440 with its text INSIDE; ours puts the
     text under a square tile, so at the full 72rem container a box ran
     614 x 754 (819 tall at 1920). A 56rem pair (C7) gives ~498px tiles and
     ~640px boxes at 1440, Speak's footprint. Phones are narrower than
     56rem, so they are unaffected. */
  max-width: 56rem;
  margin: 0 auto;
  padding: 0;
  list-style: none;
}
.curriculum__box { margin: 0; }

/* The tile: imagery, not a §9.4 card (§9.4 note R4) — a §7 radius and no
   elevation. --radius--l (21.7px) is the step nearest Klarna's 24.
   No ground colour: the pictures carry their own (bar: "no added tint"). */
.curriculum__media {
  position: relative;
  aspect-ratio: 1;
  overflow: hidden;
  border-radius: var(--radius--l);
}
.curriculum__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* The left tile's clip (the app's onboarding in the #method phone mockup) is
   played by js/main.js [data-scene-video], exactly as #needs' clips are: on
   screen only, paused under reduced motion, held while pointed at, with the
   same .scene__toggle. #needs reveals that toggle from `.scene`; the tile is
   its card here, so the same three reveals are keyed off .curriculum__media. */
/* Boxes keep all four corners even when a phone runs off their bottom edge
   (user, 2026-10-06: the square-bottom rule is only for the phone screen). */
.curriculum__media:hover .scene__toggle { --_toggle-reveal: 1; }
.curriculum__media:focus-within .scene__toggle { --_toggle-reveal: 1; transition-duration: 0s; }
.curriculum__media .scene__toggle.is-user-paused { --_toggle-reveal: 1; }

/* ---- Mechanism 4. Title, text: below the tile, left-aligned ----
   (The "01 · …" step labels and the AI note on the photo were removed at the
   user's request, 2026-09-27.)
   No max-width on the block or the text, so every line starts on the tile's
   left edge and runs to its right edge, as Klarna's do. The title is the
   first line under each tile, so the two titles start at the same y. */
/* Klarna's placement, re-measured 2026-09-27 at the user's request (the
   text "sat awkwardly"): tile -> title ~20px, title 0.073 of the tile,
   title -> text 16px, text = half the title, same ink, running the full
   tile width. On our 614px tile that is --sp-12 / h3 (36.1px, 0.059) /
   --sp-8 / body (18px) in the page ink, with no measure of its own. */
.curriculum__copy {
  padding-top: var(--sp-12);
  text-align: left;
}
/* Weight 500, never Klarna's 700; heading ink, no accent (§3.4). h3 row. */
.curriculum__title {
  margin: 0;
  font-size: var(--fs-h3);
  font-weight: 500;
  line-height: var(--lh-snug);
  letter-spacing: var(--kern-heading);
  color: var(--_theme---page--text);
}
.curriculum__text {
  margin-top: var(--sp-8);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--_theme---page--text);
}

/* ---- Mechanism 6. The close: one button ----
   Centred, --sp-24 under the tiles' text (Speak: 48px boxes -> button).
   The feature line and the small print under the tiles were removed at
   the user's request, 2026-09-27. */
.curriculum__close {
  margin-top: var(--sp-24);
  text-align: center;
}
.curriculum__cta {
  justify-content: center;
  margin-top: 0;
}

/* ---- Hangul, §4.1.2 ----
   The global `:lang(ko) .h2` at the top of this file sets 1.16 / -0.02em;
   this section takes the published row, 1.2 / -0.016em, as .hiw and
   .scatter do. Body copy takes --lh-ko-body and tracking 0 (the global
   `:lang(ko) .body` sets -0.005em, which §4.1.2 does not publish).
   word-break: keep-all is inherited from `:lang(ko) body`. */
:lang(ko) .curriculum__heading { line-height: var(--lh-heading); letter-spacing: var(--kern-heading); }
:lang(ko) .curriculum__lede,
:lang(ko) .curriculum__text { line-height: var(--lh-ko-body); letter-spacing: 0; }
:lang(ko) .curriculum__title { line-height: var(--lh-heading); }

/* ---- Mechanism 7. Phone (§10.3 <= 640): stacked, same order ----
   Above 640 two tiles still fit (~351px each at 768). Below it each box
   takes the full column: square tile, then its left-aligned text. The gap
   between box 1's text and box 2's tile is --sp-24, so it reads as bigger
   than tile -> text (--sp-8) and the pairs do not run together.
   No type steps here (§1): the heading keeps its .h2 rank, so it stays
   clearly above the h3 box titles. (A phone-only h3 step on the heading was
   removed 2026-09-27: it made the heading the same size as the titles.) */
@media (max-width: 640px) {
  .curriculum__boxes { grid-template-columns: minmax(0, 1fr); row-gap: var(--sp-24); }
}
/* §10.4 — the section has no motion of its own (neither reference has any
   at section level), so reduced motion needs no rule here. */

/* ============================================================
   #adaptive — "how it works", three steps. Built to bar-howitworks.md.
   ------------------------------------------------------------
   Used by index.html and ko/index.html only. index.html and
   ko/index.html still carry the OLD #adaptive markup (.shead / .hcards /
   .hcard / .hmock, styled in styles.css), so none of those rules are touched;
   everything here is namespaced .hiw-*.

   Every figure is quoted at 1440 (root 18.05px, tokens.css §1), the width the
   reference was measured at. Reference numbers are Klarna's, in px.

   THE RHYTHM (mechanisms 3 and 6), mapped onto the --sp-* ladder:
     heading -> row   80  -> --sp-40  72.2px   (--sp-56 101px rejected: 26%
                                                over, where --sp-40 is 10% under)
     column gutter    48  -> --sp-24  43.3px   (--sp-32 57.8px rejected: 20%
                                                over. --sp-24 also keeps the
                                                reference's 80:48 = 1.67 ratio
                                                exactly: 72.2 / 43.3 = 1.67)
     frame -> title   11  -> --sp-8   14.4px   (--sp-4 7.2px rejected: nearer
                                                in ratio but 3.8px off against
                                                3.4px, and at 7px a 29px title
                                                touches the frame's edge)
     title -> body    ~8  -> --sp-8   14.4px   (round 2; see .hiw__body)
   Order holds: 72.2 > 43.3 > 14.4. Frame->title is 0.20 of heading->row
   (reference 0.14); mechanism 3's ceiling is 0.25.
   ============================================================ */

/* Mechanism 4 — ONE ink. Declared once on the section from the page-text
   slot (grey-1000); heading, titles and bodies all inherit it and nothing
   below names a colour, so the body is never greyed. */
.hiw { color: var(--_theme---page--text); }

/* ---- Mechanism 1. One centred heading, then straight into the row ----
   .h2 rank (46.9px / 500) against the reference's 52 / 400: the house heading
   weight, the webfont ships 400 and 500 only. Line-height: styles.css's .h2
   rides 0.9 (the headline row); §4.1 publishes 1.0 for h2, and on a heading
   that is two lines by content 0.9 pinches, so --lh-tight (1.0) is applied
   here only — the same correction .brands .h2 records.
   The line break is written in the markup after the first clause (the source
   breaks after its comma), so the two clauses read as a parallel pair. At
   narrow widths each clause may wrap again; text-wrap: balance (styles.css)
   evens those lines out. No max-width: the heading straddles the section's
   midline and a narrower box would force a third line in EN at 1440. */
.hiw__heading {
  line-height: var(--lh-tight);
  text-align: center;
}

/* ---- Mechanism 2. Three equal columns, edge to edge of the .wrap ----
   No max-width, no inset: the row IS the content column, 1241.7px at 1440
   (the reference's is 1140). Each column is (1241.7 - 2 x 43.3) / 3 = 385px.
   Gutter / column = 11.2% (reference 13.8%; --sp-32 would be 15.4%, so on the
   proportional reading as well as the absolute one --sp-24 is the nearer).
   minmax(0, 1fr) so a long word cannot unequal the three tracks.
   align-items: start — mechanism 5: bodies stay ragged, nothing stretches a
   column to match its neighbour. */
.hiw__steps {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  column-gap: var(--sp-24);
  align-items: start;
  margin: var(--sp-40) 0 0;
  padding: 0;
  list-style: none;
}

/* No background, border, shadow or padding on a step — mechanism 5's fail. */
.hiw__step { margin: 0; }

/* ---- Mechanism 3. The frame ----
   Full column width, 3:2 exactly (385 x 256.7 at 1440; reference 348 x 233,
   1.494). RADIUS: --radius--xl, 28.9px, against the reference's 32.
   --radius--2xl (43.3px) was rejected: 11px further off, and at 11.2% of the
   column against the reference's 9.2% it reads as a pill-cornered tile, while
   --radius--xl is 7.5%. It is still one rung rounder than the house card
   (--radius--l, 21.7px), which is what mechanism 3 asks of it.
   FILL (round 2): sand-100 via the panels slot, plus the site hairline.
   §3.2 takes surfaces from step 50/100 only, so round 1's sand-200 is gone.
   sand-100 alone measures ~1.07:1 on this white page and read as a loading
   skeleton: a fill with no edge looks like content that has not arrived yet.
   The fix is an edge, not a darker fill. `1px solid var(--hairline)`
   (grey-100) is the site's one published hairline, and is exactly the
   border the #brands cards draw round their photographs, so the frame now
   reads as a window drawn on purpose. The 1px width is the site-wide
   convention every --hairline rule uses (styles.css .divider, .bcard); it is
   a stroke, not a radius. Box-sizing is border-box, so the 3:2 is the outer
   edge and holds. No new colour: both values are published tokens.
   grey-50 was the other §3.2 surface and was rejected: a cool grey fill is
   the loading-skeleton convention itself; the warm house panel tint is not.
   Deliberately EMPTY: no icon, glyph or text (bar-howitworks.md hard fail).
   When images land they go inside as the only child; the img rule below
   already fills and crops, so no further CSS is needed. */
.hiw__frame {
  aspect-ratio: 3 / 2;
  overflow: hidden;
  border-radius: var(--radius--xl);
  border: 1px solid var(--hairline);
  background: var(--_theme---panels--background);
}
.hiw__frame > img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ---- Mechanism 4. Title and body ----
   TITLE: .h4 rank, 28.9px / 500, against the reference's 32 / 700. 700 is
   synthetic bold on this webfont and is forbidden; the rank carries the step
   instead. .h3 (36.1px) was rejected: 4.1px off against .h4's 3.1px, and the
   reference's heading:title ratio is 52/32 = 1.63 — .h2/.h4 is 46.9/28.9 =
   1.62, .h2/.h3 only 1.30, which would flatten the top of the hierarchy.
   Tight to the frame (--sp-8, see the header), left-aligned. */
.hiw__title { margin-top: var(--sp-8); }

/* BODY: --fs-body-s, 16.2px / 400, against the reference's 16 / 400.
   Title:body = 1.78 (reference 2.0); --fs-body-xs (14.4px) would make it 2.0
   but drops the body 1.6px under the reference and below the site's body rows.
   Line-height --lh-body (1.3): §4.1's body-s row. Round 1 used --lh-prose
   (1.4, the rich-text row) to chase the reference's 1.5; a one-sentence step
   caption is not rich text, so it takes its own row.

   TITLE -> BODY (round 2): --sp-8, 14.4px. Round 1 set 0 and the title
   collided with the body: DM Sans' content area is ~1.30em, so at .h4's
   published 1.1 leading the title's glyph box overhangs its line box by
   ~2.9px top and bottom, and measured 2.2px INTO the body's first line.
   The reference leaves ~8px clear (frame -> title ~11), three beats:
   picture / title / sentence. Clear space each step gives here:
     --sp-4  7.2 - 2.9 = ~4.3px clear   (3.7 under the reference's 8)
     --sp-8 14.4 - 2.9 = ~11.5px clear  (3.5 over)
   --sp-8 is marginally nearer, and it is the step that ends the collision
   decisively rather than just. It also means frame -> title and title ->
   body are the SAME box gap (14.4) and the same clear gap (~11.5, since the
   title overhangs its line box equally at top and bottom). So frame -> title
   is still the smallest gap between frame and text, but tied rather than
   strictly smallest, and the reference's slight lean (8 < 11, title nearer
   its sentence) is not reproduced. --sp-4 would have kept the lean but
   inverted the contract's "smallest gap" wording outright (7.2 < 14.4) and
   left the title visibly crowding the body. Mechanism 6 is unaffected:
   72.2 > 43.3 > 14.4. */
.hiw__body {
  margin-top: var(--sp-8);
  font-size: var(--fs-body-s);
  line-height: var(--lh-body);
}

/* ---- Hangul ----
   HEADING: the global `:lang(ko) .h2` (top of this file) sets 1.16 / -0.02em,
   two values §4.1 does not publish. This section takes the pair .brands and
   .approw settled on: --lh-heading (1.2) for full-height syllable blocks, and
   --kern-heading (-0.016em), because -0.024 closes Hangul up.
   TITLE: .h4 publishes 1.1, too tight for Hangul if a title ever wraps (in a
   narrow column, or when a longer title replaces one), so it takes the same
   --lh-heading correction; tracking is already --kern-heading on .h4.
   BODY: --lh-ko-body (1.7), the published Hangul paragraph leading.
   word-break: keep-all is inherited from `:lang(ko) body`. */
:lang(ko) .hiw .h2 { line-height: var(--lh-heading); letter-spacing: var(--kern-heading); }
:lang(ko) .hiw__title { line-height: var(--lh-heading); }
:lang(ko) .hiw__body { line-height: var(--lh-ko-body); }

/* ---- §10.3 breakpoints ----
   ≤960: the row stacks, with the feature grid (§10.3 "feature grid -> 1-col").
   At 960 three columns would be ~276px wide and at 768 ~213px, where the EN
   titles break to two lines in one column but not the others and the frames
   shrink below a legible picture.
   NO WIDTH CAP (round 2). Round 1 capped the stack at 26rem, a value on no
   published ladder. Nothing published fits: --max-width (72rem) is wider than
   the viewport here, and §4.2's character measures (10-22ch) are text
   measures, far too narrow for a picture frame. §10.3's own rule is simply
   "feature grid -> 1-col", so the column takes the .wrap's full content
   width, like every other stacked grid on the site. Cost, stated: a 3:2 frame
   is tall here (~605px at 960, ~478px at 768, ~225px at 375).
   The vertical gap between stacked steps is --sp-32, not the desktop
   --sp-24: stacked, the gap now separates one step's body from the next
   step's frame, and it must still read as bigger than frame -> title (--sp-8)
   while staying under heading -> row (--sp-40), so mechanism 6's order holds
   in the stack too. */
@media (max-width: 960px) {
  .hiw__steps {
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--sp-32);
  }
}

/* ============================================================================
   Homepage hero — a four-slide carousel
   Contract: bar-hero.md, through "Coordinator rulings after round 4". Round 5
   reshapes the COMPOSITION (one ~600px copy column 40px inside the card, the
   scrim shrinking with it), rebuilds the PHONE layout in the reference's
   order (photo first, centred copy below), takes the reference's fixed px,
   and repairs three accessibility faults (focus ring over the photo, the live
   region, inactive slides exposed to assistive tech).

   THIS DOES NOT TOUCH `.hero`. The old two-column hero rules in styles.css
   §9.3 are still used by product.html, contact.html and the solutions pages.
   NAMING: `.hero-slider`, so no selector here can reach a `.hero` elsewhere.

   STRUCTURE, per slide (the slide is one full viewport-wide cell):
       .hero-slider__slide   padding-inline = --stage-edge
         .hero-slider__copy  heading (h2), sentence, button
         .hero-slider__box   the rounded card: photograph + scrim
   Desktop: copy and card share one grid cell, copy on top (copy over image).
   Phone:   the square card in row 1, the copy centred in row 2 on white.
   The page's one <h1> is a visually-hidden heading BEFORE the carousel, so no
   rotating slide ever owns it and inactive slides can be `inert`.
   ========================================================================= */

.hero-slider {
  /* Card top 33px below the nav (the reference's, fixed px; recorded
     deviation from §9.3's 4rem). The section starts at the sticky nav's
     bottom edge (<main> has no padding), so this padding IS that offset. */
  padding-block-start: var(--hero-stage-top);
  padding-block-end: 0;

  /* Card height: the reference's hard step, 560 -> 742 at 1536 (see below). */
  --_hero-h: var(--hero-stage-h);

  /* Heading size: --fs-headline below 1536, the ruled 100px from 1536 up. */
  --_hero-title-fs: var(--fs-headline);

  /* ===== THE COPY COLUMN (round 5: the reference's ~600px, shared) =====
     Heading and body share ONE measure, --hero-copy-w (600px), inset
     --hero-copy-inset (40px) inside the card — the reference's composition.
     Round 4 tied the column to 14ch of the heading, which at 100-130px spread
     it to ~966-1256px and put the heading and the solid scrim across every
     subject's face. The copy box is the column plus its inset on both sides.
     No `ch`, so the copy no longer needs a borrowed font-size. (The scrim
     used to fade out at this column's edge; it now darkens the full frame.) */
  --_hero-column: calc(var(--hero-copy-w) + 2 * var(--hero-copy-inset));

  /* Fail-safe: a slide without an audience modifier gets the strongest of the
     four strengths, never the weakest. */
  --_hero-strength: var(--_theme---hero-scrim--university);
}

/* One strength per photograph — see the derivation table in tokens.css. */
.hero-slider__slide--individual { --_hero-strength: var(--_theme---hero-scrim--individual); }
.hero-slider__slide--k12        { --_hero-strength: var(--_theme---hero-scrim--k12); }
.hero-slider__slide--university { --_hero-strength: var(--_theme---hero-scrim--university); }
.hero-slider__slide--business   { --_hero-strength: var(--_theme---hero-scrim--business); }

/* ---- The viewport: full width of the section ----------------------------
   100% of <main>, i.e. the document width WITHOUT the scrollbar; nothing in
   this block is written in `vw`, so there is no horizontal scroll to leak. */
.hero-slider__viewport {
  position: relative;
  overflow: hidden;
}

/* ---- The track -----------------------------------------------------------
   One slide = one full viewport-wide cell, so an advance moves exactly one
   cell width. Laid out in `order`, which js/main.js rewrites; DOM order never
   changes. §8.1's published pair, 300ms with --ease-global. */
.hero-slider__track {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 100%;
  transition: transform var(--dur) var(--ease-global);
  will-change: transform;
}
.hero-slider__track.is-resetting { transition: none; }

.hero-slider__slide {
  display: grid;
  grid-template-columns: 100%;
  /* --stage-edge: 40px until the 1680px cap, then centred (tokens.css). */
  padding-inline: var(--stage-edge);
}

/* ---- The card ------------------------------------------------------------
   HEIGHT 560px below a 1536px viewport, 742px from 1536 up (the reference's
   hard step). A MIN height: the card stretches to its row, so copy that ever
   needed more room would grow the card rather than clip. It never does —
   the tallest slide is 714.8px in the 742 card (EN slide 4 at 2560).
   WIDTH min(viewport - 80px, 1680px), centred once capped.
   RADIUS 48px at every width, phone included (the reference's, fixed px).
   Overflow + isolation clip the photograph to the curve while the card is
   transformed by the entrance. */
.hero-slider__box {
  grid-area: 1 / 1;
  position: relative;
  isolation: isolate;
  overflow: hidden;
  min-block-size: var(--_hero-h);
  border-radius: var(--hero-radius);
  /* M6, MEASURED: this card — photo + scrim inside its rounded clip — grows
     from 0.20 about its own centre over 750ms on the traced curve and fades
     0.3 -> 1 over the first 250ms. The copy is NOT inside it, so the copy
     never scales or moves. Border-radius is never animated; the clipped
     layer scales, so the corners stay rounded at every size. Runs once:
     slide changes reorder with `order`, which never re-styles the card. */
  transform-origin: 50% 50%;
  animation:
    hero-enter-grow var(--hero-enter-dur) var(--hero-enter-ease) both,
    hero-enter-fade var(--hero-enter-fade-dur) linear both;
}

.hero-slider__media {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}
/* 2026-10-11 (user): the business photo moves right a touch, the original
   photo only (no extension, no visible zoom). The photo is the box's own
   16:7, so it has almost no room to pan: it is pinned left and enlarged by
   just 5% from its top-left corner, which shifts the man right by about 2%
   of the width and keeps his head. Phones (square crop) are unchanged. */
.hero-slider__slide--business .hero-slider__media { object-position: 0% 0%; transform: scale(1.05); transform-origin: 0 0; }

/* ---- The scrim (mechanism 8) ---------------------------------------------
   The WHOLE photograph is darkened evenly, not a left-hand gradient (user
   ruling 2026-09-26: the black fade behind the copy read as a layer; the
   photo itself should look darker, as on Speak's hero). Contrast is in
   tokens.css — one strength per photograph, derived against each photo's
   brightest region across the FULL frame, so the same numbers hold now that
   the wash covers the full frame. Phones hide it: the copy sits on the page
   below the photo there, so the picture keeps its own exposure. */
.hero-slider__scrim {
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--_theme---hero-scrim--ink) var(--_hero-strength), transparent);
}

.hero-slider__copy {
  grid-area: 1 / 1;
  align-self: center;
  position: relative;
  z-index: 1;                    /* above the card it shares a cell with */
  inline-size: var(--_hero-column);
  max-inline-size: 100%;
  padding: var(--hero-copy-inset);
  /* ENTRANCE: invisible until the growing card's left edge has passed the
     copy's first glyph (500ms, computed in tokens.css from the curve), then
     a quick fade on §8.1's button pair. Never white on the white page. */
  animation: hero-enter-copy var(--hero-enter-copy-dur) var(--ease) var(--hero-enter-copy-delay) both;
}

@keyframes hero-enter-grow { from { transform: scale(var(--hero-enter-scale)); } }
@keyframes hero-enter-fade { from { opacity: var(--hero-enter-opacity); } }
@keyframes hero-enter-copy { from { opacity: 0; } }

/* Type: the existing ranks at their published weights. Heading and body take
   the column's full width — ONE measure (the reference's), so the §4.2
   `characters` caps were removed from these two elements in the markup. */
.hero-slider__title { color: var(--_theme---hero--heading); font-size: var(--_hero-title-fs); }
.hero-slider__body  {
  color: var(--_theme---hero--body);
  margin-block-start: var(--sp-16);
  line-height: var(--lh-body);   /* §4.1 body-l = 1.3, not .subheading's 1.4 */
}
/* The site's green .btn--primary (user ruling). White on brand-400 is 2.57:1,
   the client-accepted §9.5 deviation. */
.hero-slider__cta   { margin-block-start: var(--sp-20); }

/* §4.1.2 — Hangul: headings 1.2 / -0.016em, body 1.7 and zero tracking. */
:lang(ko) .hero-slider__title { line-height: var(--lh-heading); letter-spacing: var(--kern-heading); }
:lang(ko) .hero-slider__body  { line-height: var(--lh-ko-body); letter-spacing: normal; }
/* A phrase that must not split when a long heading wraps (2026-10-10). */
.hero-slider__keep { white-space: nowrap; }

/* ---- Focus over the photograph (accessibility repair, round 5) ----------
   §10.1's ring — 2px accent at a 3px offset, set globally in styles.css —
   is kept exactly. Over the photo it measured ~1.1:1 against the scrim, so
   the hero's controls add a WHITE halo: a box-shadow spread of 7px (the 3px
   offset + the 2px ring + 2px beyond it). The shadow paints under the
   outline, so from the control outward the indicator reads white gap, blue
   ring, white rim. The blue is bounded by white on BOTH sides, 6.02:1,
   whatever the photograph does; the white rim against the photo is the
   second line of defence (>= 4.71:1 wherever the scrim is under it). On the
   white page the rim simply disappears into the page and the blue reads
   6.02:1 against white as before. */
.hero-slider__cta:focus-visible,
.hero-slider__dot:focus-visible {
  box-shadow: 0 0 0 7px var(--_theme---hero--focus-halo);
}
.hero-slider__arrow:focus-visible {
  box-shadow: 0 0 0 7px var(--_theme---hero--focus-halo), var(--_theme---hero--arrow-shadow);
}

/* ---- Arrows (M3) ---------------------------------------------------------
   The reference's fixed px: a 44px white disc, 8px from the VIEWPORT edge at
   every width, with its `0 2px 4px rgba(0,0,0,.1)` (tokens.css, via the
   ramp's black). While the card is 40px-inset the disc straddles the card
   edge by 12px, as the reference's does; once the card is capped and
   centred it sits in the white margin. Vertically centred on the card —
   the viewport's height IS the card's height on desktop. >= 60px clear of
   the first glyph at every width (the copy starts 40px inside the card).
   Transparent until the hero is hovered or the arrow is focused (measured
   rest state); focus snaps in and never pauses anything. */
.hero-slider__arrow {
  position: absolute;
  top: 50%;
  translate: 0 -50%;
  z-index: 2;
  display: grid;
  place-items: center;
  inline-size: var(--hero-arrow);
  block-size: var(--hero-arrow);
  padding: 0;
  border: 0;
  border-radius: var(--radius--circle);
  background: var(--_theme---hero--control);
  color: var(--_theme---hero--control-icon);
  box-shadow: var(--_theme---hero--arrow-shadow);
  cursor: pointer;
  font-size: var(--fs-body-m);
  --_arrow-reveal: 0;
  opacity: var(--_arrow-reveal);
  transition: opacity var(--dur-fast) var(--ease);
}
.hero-slider__arrow svg { inline-size: 1.2em; block-size: 1.2em; }
.hero-slider__arrow--prev { inset-inline-start: var(--hero-arrow-inset); }
.hero-slider__arrow--next { inset-inline-end: var(--hero-arrow-inset); }
.hero-slider__viewport:hover .hero-slider__arrow { --_arrow-reveal: 1; }
.hero-slider__arrow:focus-visible { --_arrow-reveal: 1; transition-duration: 0s; }

/* ---- Dots (M4) -----------------------------------------------------------
   The reference's fixed px: 6px pips, the active one 24x6, an 8px visual gap,
   the pips 16px below the card. The active pip's dark bar runs linearly over
   --dwell-carousel and IS the timer (js/main.js starts both in one frame).
   PIP SHAPE: --radius--s (0.4rem, >= 6px at every width) — more than half
   the 6px height, so it resolves to a true circle at 6x6 and a true stadium
   at 24x6. --radius--circle (50%) was the other candidate; on the 24x6 pip
   it resolves to an ELLIPSE (12px x 3px corners), not the reference's
   stadium. Either way the raw 999px is gone.
   TARGET SIZE vs an 8px gap: a 24px target round each 6px pip needs a
   >= 24px pitch, and 6 + 8 = 14px cannot hold one. So:
     desktop  each button is 24px tall and pip + 8px wide (14 x 24 inactive,
              32 x 24 active). WCAG 2.5.8's "equivalent" exception applies:
              the 44px arrows do the same job on the same page.
     phone    the arrows are hidden, the dots are the only control, so the
              buttons widen to 24 x 24 (an 18px visual gap between pips).
   The row's top margin is 16px minus the button's 9px of vertical padding,
   so the PIPS sit exactly 16px below the card. */
.hero-slider__dots {
  --_dot-pad-block: calc((24px - var(--hero-pip)) / 2);
  display: flex;
  justify-content: center;
  margin-block-start: calc(var(--hero-dots-offset) - var(--_dot-pad-block));
}
.hero-slider__dot {
  display: grid;
  place-items: center;
  padding-block: var(--_dot-pad-block);
  padding-inline: calc(var(--hero-pip-gap) / 2);
  border: 0;
  background: none;
  cursor: pointer;
  line-height: 0;
}
.hero-slider__pip {
  position: relative;
  display: block;
  overflow: hidden;
  block-size: var(--hero-pip);
  inline-size: var(--hero-pip);
  border-radius: var(--radius--s);
  background: var(--_theme---hero--dot);
  transition: inline-size var(--dur) var(--ease-global);
}
.hero-slider__dot[aria-current="true"] .hero-slider__pip { inline-size: var(--hero-pip-active); }
.hero-slider__pip::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--_theme---hero--dot-fill);
  transform: translateX(-100%);
}
.hero-slider.is-timing .hero-slider__dot[aria-current="true"] .hero-slider__pip::after {
  animation: hero-dwell var(--dwell-carousel) linear both;
}
@keyframes hero-dwell { to { transform: translateX(0); } }

/* ---- Wide: the card height and the heading step at 1536 -------------------
   RECORDED DEVIATIONS: 1536px is the reference's breakpoint, not one of
   §10.3's; the heading step is a §1 deviation (user-ruled, 100px). */
@media (min-width: 1536px) {
  .hero-slider {
    --_hero-h: var(--hero-stage-h-wide);
    --_hero-title-fs: var(--hero-title-wide);
    /* The column grows with the heading step (2026-10-10, per-slide headings): at 100px the
       600px measure broke two-line headings into four. Same ratio as below 1536. */
    --_hero-column: calc(var(--hero-copy-w) * 1.4 + 2 * var(--hero-copy-inset));
  }
}

/* ---- Phone (§10.3 <= 640): the reference's order ---------------------------
   Re-measured at 390x844: the square photo FIRST, directly under the nav;
   then the headline, CENTRED, dark on white; the body; then a COMPACT,
   centred button — its label's width, not the column's. Nothing sits over
   the photo, so there is no scrim and the copy takes page inks. */
@media (max-width: 640px) {
  .hero-slider__slide {
    grid-template-rows: auto auto;
    row-gap: var(--sp-24);
    /* Every slide's square starts at the same y, so the photo never jumps as
       the carousel advances; slides with shorter copy leave their spare
       height at the bottom. */
    align-content: start;
  }
  .hero-slider__box {
    grid-area: 1 / 1;
    align-self: start;
    min-block-size: 0;
    aspect-ratio: 1;            /* 350 x 350 at 390 (20px gutter each side) */
  }
  .hero-slider__copy {
    grid-area: 2 / 1;
    align-self: start;
    inline-size: auto;
    padding: 0;
    text-align: center;
    /* On white, below the photo: no white-on-white risk, so the copy fades
       with the card from the first frame instead of waiting for it. */
    animation: hero-enter-fade var(--hero-enter-fade-dur) linear both;
  }
  .hero-slider__scrim { display: none; }
  /* The square crop keeps the subject's face in frame: the individual photo's
     subject sits in the right third (face at ~80% of the image's width). */
  .hero-slider__slide--individual .hero-slider__media { object-position: 95% 50%; }
  .hero-slider__slide--k12 .hero-slider__media { object-position: 78% 50%; }
  .hero-slider__slide--k12 .hero-slider__media { object-position: 82% 50%; }
  .hero-slider__slide--university .hero-slider__media { object-position: 80% 50%; }
  .hero-slider__slide--business .hero-slider__media { object-position: 50% 50%; transform: none; }
  /* The h1 RANK's size and leading (--fs-h1 3.2rem = 47.8px at 390, --lh-tight),
     as ruled in R4c; the reference's 52px has no step on the ladder between
     --fs-h1 (47.8) and --fs-headline (59.7), and h1 is the nearer. KO keeps
     §4.1.2's 1.2 / -0.016em (`:lang(ko) .hero-slider__title` outranks this). */
  .hero-slider__title {
    color: var(--_theme---hero--phone-heading);
    font-size: var(--fs-h1);
    line-height: var(--lh-tight);
  }
  .hero-slider__body { color: var(--_theme---hero--phone-body); }
  /* Compact and centred: .btn is inline-flex, so it takes its label's width
     and the copy's text-align centres it. Height >= 44px: no .btn rung
     reaches 44px, so --sp-32 as a min-block-size (47.7px at 390). */
  .hero-slider__cta { min-block-size: var(--sp-32); }

  /* Arrows hidden (display:none also removes them from the tab order). */
  .hero-slider__arrow { display: none; }

  /* The dots are the only control here: widen each button to 24 x 24. */
  .hero-slider__dot { padding-inline: calc((24px - var(--hero-pip)) / 2); }
}

@media (prefers-reduced-motion: reduce) {
  /* No entrance, no fill, no animated move. js/main.js never starts the
     timer or sets .is-timing. The dots and arrows still move the carousel. */
  .hero-slider__box,
  .hero-slider__copy,
  .hero-slider.is-timing .hero-slider__dot[aria-current="true"] .hero-slider__pip::after { animation: none; }
  .hero-slider__track,
  .hero-slider__pip { transition: none; }
}

/* ============================================================
   #belief — "Learning should start with you". Built to bar-belief.md.
   ------------------------------------------------------------
   Used by index.html and ko/index.html only; everything is namespaced
   .belief*. Reference: klarna.com/us/klarna-onepay/, "How to use OnePay
   Later at Walmart" (measured 2026-09-27, 1440 x 900 and 375 x 812).
   The three images are PLACEHOLDERS (app mockups); the user will replace
   them. Any square-croppable picture drops into .belief__tile unchanged.

   TYPE, Klarna -> §4.1 ladder at 1440 (weights 500/400, never 700):
     heading  64 / 700  -> .h1       57.8px / 500   heading / title = 2.00
     title / body: as .curriculum__title / .curriculum__text (user,
     2026-09-27) — --fs-h3 / 500 and --fs-body / 400; no kicker.
   Klarna has no line under its heading; ours is quiet: --fs-body (18px),
   grey-800, as .curriculum__lede.

   RHYTHM, Klarna px -> --sp-* ladder at 1440:
     heading -> line          (ours)      -> --sp-8   14.4px
     heading block -> row     ~40         -> --sp-24  43.3px  (biggest)
     column gutter            16          -> --sp-8   14.4px
     tile -> title            (as #curriculum) --sp-12
     title -> body            (as #curriculum) --sp-8
   ============================================================ */

/* ---- Mechanism 1. One centred heading, then straight into the row ----
   40rem (722px at 1440) is Klarna's ~700px measure: both headings set on
   two lines. .h1 rides 1.0 (§4.1's h1 row), so no line-height fix is
   needed in EN. */
.belief__head {
  max-width: 40rem;
  margin: 0 auto var(--sp-24);
  text-align: center;
}
.belief__lede {
  margin: var(--sp-8) auto 0;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--color--grey-800);
}

/* ---- Mechanism 2. Three equal columns, the row as wide as the .wrap ----
   minmax(0, 1fr) so a long word cannot unequal the tracks. role="list" in
   the markup: styles.css strips list-style from every ul.
   ---- Mechanism 4 (ragged bottoms): align-items: start, nothing stretches. */
.belief__cols {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-8);
  align-items: start;
  margin: 0;
  padding: 0;
  list-style: none;
}
.belief__col { margin: 0; }

/* ---- Mechanism 3. The square tile ----
   Imagery, not a §9.4 card (§9.4 note R4): a §7 radius, no elevation, no
   border. --radius--l (21.7px) is the rung nearest Klarna's 24. The ground
   is the picture's own warm grey; nothing is added. object-position: top
   crops the 848 x 1136 mockups to their upper square, so the phone's top
   rises into the tile and is cut by its bottom edge, as Klarna's are. */
.belief__tile {
  position: relative;
  aspect-ratio: 1;
  overflow: hidden;
  border-radius: var(--radius--l);
}
/* The mastery tile: a photo with a UI clip floating on it, as Monzo's
   "Save like a superhero" tiles float an app card, centred, over the photo
   (user, 2026-09-28). The clip is cut from the ESP role-play recording
   (assets/belief/practice-roleplay-card.mp4, 786 x 490, 6s loop): the
   "Great job!" completion, then a crossfade to the Key Phrases the learner
   used, with white fades at the loop point. Centred across the tile and
   held low, clear of the face. js/main.js [data-scene-video] runs it as it
   runs #curriculum's clip: on screen only, paused under reduced motion,
   with the same .scene__toggle, revealed from the card. */
.belief__card {
  position: absolute;
  left: 13%;
  bottom: 8%;
  width: 74%;
  aspect-ratio: 786 / 490;
  overflow: hidden;
  border-radius: var(--radius--l);
  background: var(--color--white);
  box-shadow: var(--shadow--card);
}
.belief__card-video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.belief__card:hover .scene__toggle { --_toggle-reveal: 1; }
.belief__card:focus-within .scene__toggle { --_toggle-reveal: 1; transition-duration: 0s; }
.belief__card .scene__toggle.is-user-paused { --_toggle-reveal: 1; }
.belief__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center top;
}

/* The conversation tile (user, 2026-09-28): the app's speaking-lesson screen
   recording (assets/belief/conversation-loop.mp4, 540 x 1170: the
   check-in-desk lesson from the character's first line to the third
   sentence turning green, 21.3s; the lesson list trimmed off the head and
   the "Amazing Job!" end screen off the tail (user, 2026-09-28); the tail crossfaded 0.5s into the
   head so the loop has no jump) on the lavender-to-mint wash, in THE SAME
   PHONE the tile's mockup used (user: keep the original frame).
   assets/belief/phone-frame.webp is cut from assets/app/app-avatar-call.png
   itself: its bezel, side keys, status bar ("9:41", toned to the
   recording's #f7f7f7 top) and drop shadow (re-expressed as black alpha so
   it falls on the wash as it fell on the grey), with the screen below the
   status bar made transparent. It is the mockup's upper square, as the old
   <img> showed it (object-position: top), so the phone sits exactly where
   it did. The video sits under the opening: 848-px mockup coordinates,
   screen x 144-702 from y 219, widened 1px each side under the bezel so no
   seam shows. Runs through js/main.js [data-scene-video] like the mastery
   tile's clip: on screen only, poster under reduced motion, its own
   .scene__toggle. */
.belief__tile--phone {
  border-radius: var(--radius--l);   /* all four corners — the box, not the phone screen (user, 2026-10-06) */
  background: linear-gradient(180deg, var(--_theme---belief--wash-top), var(--_theme---belief--wash-bottom));
}
.belief__phone { position: absolute; inset: 0; }
.belief__phone-video {
  position: absolute;
  left: calc(143 / 848 * 100%);
  top: calc(218 / 848 * 100%);
  width: calc(560 / 848 * 100%);
  height: auto;
  aspect-ratio: 540 / 1170;
  display: block;
  object-fit: cover;
  background: var(--color--white);
}
.belief__phone-frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
.belief__phone:hover .scene__toggle { --_toggle-reveal: 1; }
.belief__phone:focus-within .scene__toggle { --_toggle-reveal: 1; transition-duration: 0s; }
.belief__phone .scene__toggle.is-user-paused { --_toggle-reveal: 1; }
.belief__phone .scene__toggle { top: calc(236 / 848 * 100%); right: calc(162 / 848 * 100%); }

/* ---- The roadmap tile: a live app card on the photo (bar-roadmap-card.md) ----
   Monzo's "Save like a superhero" left tile (monzo.com, measured
   2026-09-28, 1368px video sampled per frame): a white card floats on the
   photo, solidifies from frosted glass, holds, then values change in
   place, row 1 then row 2, and it rests. Ours is HTML, not footage, built
   from the app's Roadmaps screen (30_roadmaps_list.png, 1179 x 2556): two
   roadmaps, each with its book cover (assets/belief/roadmap-cover-*.webp),
   title, A2 badge, status pill and radio, in the app's reading order.
   "Avo" (the app's third line) is left out at every width. Titles stay
   English on /ko/ (the app's content; lang="en" on the card). The card
   is aria-hidden; one .visually-hidden sentence beside it says what it
   shows.

   GEOMETRY. The % vs rem split (recorded in design-system.md §10.3):
     % of the tile (image geometry — where the card sits on the photo):
       top      = tile 3's card top (100% - 8% - 74% x 490/786 = 45.87%),
                  so the two cards' top edges align at every width; the
                  face ends ~41% (41.6% at the photo's 1.04 push-in)
       width    84% (Monzo's 83.6%), but never wider than 19.5rem
       left     8% (Monzo's 8.2%); once the 19.5rem cap binds (stacked
                tiles, ~450-600px) the card is centred on the tile
       wide stacked tiles (>= 35rem, ~600-860px): the cap lifts, the card
                is 70% of the tile, centred (tile 3's is 74%), and the
                rows take Monzo's shape: title over A2 left, status pill
                in the right-hand value slot, radio; --sp-12 padding
     rem (the card itself, §1): max width 19.5rem (it is 18.8rem at 1440);
       HEIGHT comes from the content only — row padding --sp-8 inline /
       0.5rem block, gaps --sp-8, cover 1.6rem, radio 1.2rem — nothing is
       a share of the tile. At 1440 that is 35.5% of the tile (Monzo
       28.6%). The two rows are equal (grid-auto-rows 1fr).
     corner / shadow  --radius--l; at rest --shadow--card (§9.4's resting
                      rung), as tile 3's card

   TYPE — one treatment at every width (§1: no type-size breakpoints):
     title   --fs-h6 (1rem) / 500 / --lh-heading / --kern-heading
     chips   --fs-body-s (0.9rem) / 500 / --lh-body — §4.1.1 (R15) chips,
             §9.6 tag geometry (padding 0.2 / 0.4rem, --radius--tag)

   NARROW TILES — a layout switch, recorded in design-system.md §10.3.
   The row stacks at <= 860, so tiles are full-width there and carry the
   full card. Under a 21rem tile (the three-up row from 861 to ~1170px,
   and phones under ~350px) the title (~10.7rem) cannot sit on one line
   beside cover, chips and radio with air (at 20-21rem it fits, but the
   card would be 41-42% of the tile). There each row is its title and its
   radio only (cover, A2 and status pill step out), with --sp-8 padding
   all round; under a 17.2rem tile (861 to ~925px) the card also widens
   on the photo (left 4%, width 92%) so the title still fits on one line.
   Nothing wraps or is truncated. Type does not change.

   COLOUR. Tokens throughout — chips and leaf on the green family's 800
   over green-50 (7.3:1) — except the two status pills, which keep the
   app's own colours (sampled): In progress #00aeff with white text,
   Completed #e0f5ed with #008a45 text. The selected radio is the app's
   #1ab878 = --color--brand-400. Layout adds nothing to the blue pill: it
   sits in the text column at chip size, no weight or size of its own.

   MOTION (mechanisms 3-6). A RECORDED EXCEPTION to §8.1 — design-system.md
   §8.1 "the roadmap card"; tokens --roadmap-* in css/tokens.css. The CSS
   default below IS the final state (the screenshot, and the photo at its
   pushed-in 1.04), so no JS and reduced motion show exactly the animated
   end frame. js/main.js sets data-play="idle" whenever the tile is
   entirely off screen and "run" when 60% of the CARD is on screen;
   leaving and coming back replays it. Story, real labels only: your
   selection moves on to "Getting Around London", and "Arriving in London"
   is marked Completed.
     idle          clear frosted glass (60% white, 0.2rem backdrop blur,
                   no tint; the blur is animated away, none at rest;
                   radio rings in grey-600 so they read on the glass),
                   complete and legible: row 2 selected and "In progress"
     0 - 0.55s     linear: the glass fills to white, the rings settle to
                   the app's light grey; the shadow comes up from
                   --shadow-none to --shadow--card (rung colours only)
     0.55 - 1.05   hold
     1.05          the selection moves, like a real radio: in one frame
                   row 2's dot is gone and row 1's appears at 58%, then
     1.05 - 1.40   grows to full (350ms, --ease-global); exactly one dot
                   in every frame
     1.30 - 1.90   row 2: odometer. Two whole faces, blue "In progress"
                   and mint "Completed", share one cell inside the pill's
                   rounded clip and roll up together (600ms, sine). The
                   incoming face carries a band of its own colour above
                   it that grows with the roll (0 -> 0.8rem) and covers
                   the outgoing face's edge ahead of its text; at 57% of
                   the roll, when ~2px of it is left, the outgoing face
                   is switched off. At rest only the state's face is visible and
                   untransformed, so no fractional offset can leak
     0 - 3.6s      the photo pushes in from rest to 1.04
   The roll runs on --roadmap-roll-ease, a sine
   ease-in-out, so the movement reads as ~0.4s rather than a front-loaded
   flick; the glass is linear; the photo
   is --ease-global. §10.4: every idle/run rule sits inside
   prefers-reduced-motion: no-preference, and js/main.js never arms the
   tile under reduce. */
.belief__tile--roadmap { container-type: inline-size; }
.belief__tile--roadmap .belief__img {
  transform: scale(1.04);
  transform-origin: 45% 25%;
}
.belief__roadmap {
  --_roadmap-progress-bg: #00aeff;   /* app colour, In progress pill */
  --_roadmap-progress-ink: var(--color--white);
  --_roadmap-done-bg: #e0f5ed;       /* app colour, Completed pill */
  --_roadmap-done-ink: #008a45;
  --_roadmap-radio: 1.2rem;
  --_roadmap-ring: 0.08rem;
  --_roadmap-max: 19.5rem;
  /* the selected radio: brand green with the app's white dot (31%) */
  --_roadmap-fill: calc(var(--_roadmap-radio) * 0.345);
  position: absolute;
  left: max(8%, calc((100% - var(--_roadmap-max)) / 2));
  top: calc(92% - 74% * 490 / 786);   /* = tile 3's .belief__card top */
  width: min(84%, var(--_roadmap-max));
  display: grid;
  grid-auto-rows: 1fr;
  overflow: hidden;
  border-radius: var(--radius--l);
  background-color: var(--color--white);
  box-shadow: var(--shadow--card);
  color: var(--color--grey-1000);
}
.belief__roadmap-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  align-content: center;
  column-gap: var(--sp-8);
  padding: calc(var(--sp-4) * 1.25) var(--sp-8);
}
/* Monzo's one full-width hairline between the rows. */
.belief__roadmap-row + .belief__roadmap-row { border-top: 1px solid var(--color--grey-100); }
.belief__roadmap-cover {
  display: block;
  width: 1.6rem;
  height: auto;
  aspect-ratio: 3 / 4;
  border-radius: var(--radius--xs);
  background: var(--color--grey-50);   /* shows only if a cover fails; the
                                          markup onerror swaps in a clear GIF,
                                          so no broken-image glyph paints */
}
.belief__roadmap-text { display: block; }
.belief__roadmap-title {
  display: block;
  font-size: var(--fs-h6);
  font-weight: 500;
  line-height: var(--lh-heading);
  letter-spacing: var(--kern-heading);
}
.belief__roadmap-meta {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  margin-top: calc(var(--sp-4) / 2);
  font-size: var(--fs-body-s);
  font-weight: 500;
  line-height: var(--lh-body);
  white-space: nowrap;
}
/* §9.6 tag geometry for both chips (the pill's on its faces). */
.belief__roadmap-level,
.belief__roadmap-face { padding: calc(var(--sp-4) / 2) var(--sp-4); }
.belief__roadmap-level {
  display: inline-flex;
  align-items: center;
  gap: calc(var(--sp-4) / 2);
  border-radius: var(--radius--tag);
  background: var(--color--green-50);
  color: var(--color--green-800);
}
.belief__roadmap-leaf { width: 1em; height: 1em; flex: none; }
.belief__roadmap-leaves { fill: var(--color--green-800); }
.belief__roadmap-stem { stroke: var(--color--green-800); }
/* The status pill is a rounded clip. Row 2's faces share one grid cell
   (same size: the box is as wide as the longer label) and roll through
   it together. At rest only the state's face is visible, untransformed;
   the other is visibility: hidden and parked a full face away. */
.belief__roadmap-status {
  display: inline-block;
  overflow: hidden;
  border-radius: var(--radius--tag);
}
.belief__roadmap-drum { display: grid; }
.belief__roadmap-face {
  display: block;
  grid-area: 1 / 1;
  text-align: center;
}
.belief__roadmap-status--roll .belief__roadmap-face--progress {
  visibility: hidden;
  transform: translateY(-100%);
}
.belief__roadmap-face--progress { background: var(--_roadmap-progress-bg); color: var(--_roadmap-progress-ink); }
.belief__roadmap-face--done { background: var(--_roadmap-done-bg); color: var(--_roadmap-done-ink); }
/* The radio: a grey ring; selected = a brand-green disc with the white
   dot, which scales in from the centre (::before). */
.belief__roadmap-radio {
  position: relative;
  width: var(--_roadmap-radio);
  aspect-ratio: 1;
  border-radius: var(--radius--circle);
  box-shadow: inset 0 0 0 var(--_roadmap-ring) var(--color--grey-200);
}
.belief__roadmap-radio::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: inset 0 0 0 var(--_roadmap-fill) var(--color--brand-400);
  transform: scale(0);
}
.belief__roadmap-radio--on::before { transform: none; }

/* Narrow tiles, under 21rem (see NARROW TILES above; §10.3 deviation).
   Layout only — no type changes. */
@container (max-width: 21rem) {
  .belief__roadmap-row {
    grid-template-columns: minmax(0, 1fr) auto;
    column-gap: var(--sp-8);
    padding: var(--sp-8);
  }
  .belief__roadmap-cover,
  .belief__roadmap-meta { display: none; }
}
/* ...and under 17.2rem (861 to ~925px): a wider card keeps the title on
   one line. */
@container (max-width: 17.2rem) {
  .belief__roadmap { left: 4%; width: 92%; }
}
/* Wide stacked tiles, 35rem and up (the one-column row, ~600-860px):
   the card grows to 70% of the tile, centred (tile 3's card is 74%), and
   takes Monzo's row shape — title over A2 on the left, the status pill in
   the right-hand value slot, then the radio — so the width is used, not
   left empty; height is still the content's. Layout only (§10.3). */
@container (min-width: 35rem) {
  .belief__roadmap { left: 15%; width: 70%; }
  .belief__roadmap-row {
    grid-template-columns: auto minmax(0, 1fr) auto auto;
    grid-template-areas:
      "cover title status radio"
      "cover level status radio";
    row-gap: calc(var(--sp-4) / 2);
    padding: var(--sp-12);
  }
  .belief__roadmap-text,
  .belief__roadmap-meta { display: contents; }
  .belief__roadmap-cover { grid-area: cover; }
  .belief__roadmap-title { grid-area: title; }
  .belief__roadmap-level { grid-area: level; justify-self: start; }
  .belief__roadmap-status { grid-area: status; }
  .belief__roadmap-radio { grid-area: radio; }
}

@media (prefers-reduced-motion: no-preference) {
  /* idle — the state before the change, complete and legible */
  .belief__tile--roadmap[data-play="idle"] .belief__img { transform: none; }
  .belief__tile--roadmap[data-play="idle"] .belief__roadmap {
    background-color: color-mix(in srgb, var(--color--white) 60%, transparent);
    box-shadow: var(--shadow-none);
    -webkit-backdrop-filter: blur(0.2rem);
    backdrop-filter: blur(0.2rem);
  }
  .belief__tile--roadmap[data-play="idle"] .belief__roadmap-radio { box-shadow: inset 0 0 0 var(--_roadmap-ring) var(--color--grey-600); }
  .belief__tile--roadmap[data-play="idle"] .belief__roadmap-status--roll .belief__roadmap-face--progress { visibility: visible; transform: none; }
  .belief__tile--roadmap[data-play="idle"] .belief__roadmap-status--roll .belief__roadmap-face--done { visibility: hidden; transform: translateY(100%); }
  .belief__tile--roadmap[data-play="idle"] .belief__roadmap-radio--on::before { transform: scale(0); }
  .belief__tile--roadmap[data-play="idle"] .belief__roadmap-radio--was-on::before { transform: none; }

  /* run — once per arrival */
  .belief__tile--roadmap[data-play="run"] {
    --_row1: calc(var(--roadmap-glass-dur) + var(--roadmap-hold));
    --_row2: calc(var(--_row1) + var(--roadmap-stagger));
    --_vanish: calc(var(--_row2) + var(--roadmap-roll-dur) * 0.57);
  }
  /* Every run animation lets go when it ends (fill backwards or none):
     the resting state is then the plain CSS default — the same rules, and
     the same pixels, as no JS and reduced motion. */
  .belief__tile--roadmap[data-play="run"] .belief__img { animation: belief-roadmap-push var(--roadmap-photo-dur) var(--ease-global) backwards; }
  /* fill: backwards — once the glass has solidified the animation lets go
     and the card is its plain resting self (white, --shadow--card, no
     backdrop-filter), exactly the no-JS / reduced-motion card */
  .belief__tile--roadmap[data-play="run"] .belief__roadmap { animation: belief-roadmap-glass var(--roadmap-glass-dur) linear backwards; }
  .belief__tile--roadmap[data-play="run"] .belief__roadmap-radio { animation: belief-roadmap-ring var(--roadmap-glass-dur) linear backwards; }
  /* The swap, like a real radio: until --_row1 row 2 holds the dot and
     row 1 has none (the "hold" animations, no fill); at --_row1 both hold
     animations end in the same frame, row 2's dot is gone (its resting
     scale 0) and row 1's appears at 58% and grows. Exactly one dot in
     every frame. */
  .belief__tile--roadmap[data-play="run"] .belief__roadmap-radio--on::before {
    animation:
      belief-roadmap-dot-none var(--_row1) linear,
      belief-roadmap-dot-in var(--roadmap-dot-dur) var(--ease-global) var(--_row1);
  }
  .belief__tile--roadmap[data-play="run"] .belief__roadmap-radio--was-on::before { animation: belief-roadmap-dot-full var(--_row1) linear; }
  .belief__tile--roadmap[data-play="run"] .belief__roadmap-status--roll .belief__roadmap-face--progress {
    animation:
      belief-roadmap-roll-out var(--roadmap-roll-dur) var(--roadmap-roll-ease) var(--_row2) backwards,
      belief-roadmap-vanish calc(var(--roadmap-roll-dur) * 0.43) linear var(--_vanish);
  }
  .belief__tile--roadmap[data-play="run"] .belief__roadmap-status--roll .belief__roadmap-face--done { animation: belief-roadmap-roll-in var(--roadmap-roll-dur) var(--roadmap-roll-ease) var(--_row2) backwards; }
}
@keyframes belief-roadmap-push {
  from { transform: none; }
  to   { transform: scale(1.04); }
}
/* The glass is the backdrop blur plus the white's alpha, never opacity:
   an element's opacity also fades its own backdrop-filter. */
@keyframes belief-roadmap-glass {
  from {
    background-color: color-mix(in srgb, var(--color--white) 60%, transparent);
    box-shadow: var(--shadow-none);
    -webkit-backdrop-filter: blur(0.2rem);
    backdrop-filter: blur(0.2rem);
  }
  to {
    background-color: var(--color--white);
    box-shadow: var(--shadow--card);
    -webkit-backdrop-filter: blur(0);
    backdrop-filter: blur(0);
  }
}
@keyframes belief-roadmap-ring {
  from { box-shadow: inset 0 0 0 var(--_roadmap-ring) var(--color--grey-600); }
  to   { box-shadow: inset 0 0 0 var(--_roadmap-ring) var(--color--grey-200); }
}
@keyframes belief-roadmap-dot-none {
  from, to { transform: scale(0); }
}
@keyframes belief-roadmap-dot-full {
  from, to { transform: none; }
}
@keyframes belief-roadmap-dot-in {
  from { transform: scale(0.58); }
  to   { transform: none; }
}
/* visibility interpolates discretely with "visible" winning, so the
   outgoing face is visible for the whole roll and hidden only at its
   end; the incoming face is hidden only before the roll starts. */
@keyframes belief-roadmap-roll-out {
  from { visibility: visible; transform: none; }
  to   { visibility: hidden; transform: translateY(-100%); }
}
/* The incoming face carries a band of its own colour above it that
   grows with the roll (0 -> 0.8rem): it covers the outgoing face's lower
   edge ahead of the text, so the blue never thins to a sliver; at rest
   the band lies above the clip, outside it. */
@keyframes belief-roadmap-roll-in {
  from { visibility: hidden; transform: translateY(100%); box-shadow: 0 0 0 var(--_roadmap-done-bg); }
  to   { visibility: visible; transform: none; box-shadow: 0 calc(-2 * var(--sp-4)) 0 var(--_roadmap-done-bg); }
}
/* ...and at 57% of the roll, when only ~2px of it is left above the
   band, the outgoing face is switched off, so it never thins to a
   lingering sliver. */
@keyframes belief-roadmap-vanish {
  from, to { visibility: hidden; }   /* hidden for the rest of the roll;
                                        after it the resting rule hides it */
}

/* ---- Mechanisms 4 & 5. Title -> body, left-aligned ----
   The kicker labels were removed and the text set exactly as
   .curriculum__title / .curriculum__text at the user's request
   (2026-09-27: "match the curriculum section"): tile -> title --sp-12,
   title h3 / 500 / snug, title -> body --sp-8, body --fs-body in the page
   ink. No max-width, so every line runs the tile's full width. */
.belief__title {
  margin: 0;
  padding-top: var(--sp-12);
  font-size: var(--fs-h3);
  font-weight: 500;
  line-height: var(--lh-snug);
  letter-spacing: var(--kern-heading);
  color: var(--_theme---page--text);
}
.belief__body {
  margin-top: var(--sp-8);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--_theme---page--text);
}

/* ---- Hangul, §4.1.2 (as .curriculum) ----
   Headings take 1.2 / -0.016em (the global `:lang(ko) .h1` sets the
   unpublished 1.16 / -0.02em); body copy 1.7, tracking 0. word-break:
   keep-all is inherited from `:lang(ko) body`. */
:lang(ko) .belief__heading { line-height: var(--lh-heading); letter-spacing: var(--kern-heading); }
:lang(ko) .belief__title { line-height: var(--lh-heading); }
:lang(ko) .belief__lede,
:lang(ko) .belief__body { line-height: var(--lh-ko-body); letter-spacing: 0; }

/* ---- Mechanism 6. Phones and small tablets (§10.3 <= 860): the columns stack ----
   Full column width, tiles stay square, same text order. The gap between
   one column's body and the next tile is --sp-24, so it reads as larger
   than tile -> title (--sp-12) and the columns do not run together.
   Coordinator ruling 2026-09-28: stacked from 860 (a §10.3 width), not
   640. Three-up between 641 and 860 made 189-250px tiles, too small for
   the roadmap tile's app card to hold its titles on one line with air. */
@media (max-width: 860px) {
  .belief__cols { grid-template-columns: minmax(0, 1fr); row-gap: var(--sp-24); }
}
/* §10.4 — the roadmap card's motion lives inside prefers-reduced-motion:
   no-preference above; the mastery clip is paused by js/main.js. */

/* A control that is deliberately inert (its href was removed, 2026-10-01: the
   sign-up screen is hidden) keeps its look but must not feel clickable. */
[aria-disabled="true"] { cursor: default; }

/* ============================================================
   2026-10-09 phase 1 (audit: avolingo-website-audit-2026-10-09)
   ============================================================ */

/* ---- Home hero: one fixed promise over the rotating photos (F06) ----
   The per-slide copy was removed; this block sits in the viewport above the
   track, in the column the slide copy used, so the product is defined before
   any slide changes. Phones: below the square photo, on white, as before. */
.hero-slider__copy--fixed {
  position: absolute;
  inset-block: 0;
  inset-inline-start: var(--stage-edge);
  z-index: 2;
  display: flex;
  flex-direction: column;
  justify-content: center;
  pointer-events: none;
}
.hero-slider__copy--fixed > * { pointer-events: auto; }
.hero-slider__ctas { margin-block-start: var(--sp-24); }
.hero-slider__ctas .hero-slider__cta { margin-block-start: 0; }
/* The arrows reveal on hover at the card's edges; keep them clear of the copy
   by stacking them above it (they only show on hover/focus). */
.hero-slider__arrow { z-index: 3; }
@media (max-width: 640px) {
  .hero-slider__copy--fixed {
    position: static;
    inline-size: auto;
    padding: var(--sp-24) var(--stage-edge) 0;
    text-align: center;
    pointer-events: auto;
  }
  .hero-slider__ctas { justify-content: center; }
}

/* ---- Home: the four paths (F06/F13). 2026-10-10 (user): the publishers
   card is gone, so four equal, wider cards (no fixed height: 2026-10-10
   user found them too tall); the label has the arrow circle
   like every other home label (its padding is set for one); no underline
   on the card titles. ---- */
.paths__head { max-width: 46rem; margin-inline: auto; text-align: center; }
.paths__head .h2 { margin-top: var(--sp-12); }
.paths__line { margin-top: var(--sp-20); color: var(--color--grey-800); }
.paths__grid {
  list-style: none;
  margin: var(--sp-40) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-20);
}
.paths__card {
  position: relative;
  display: grid;                        /* icon on its own row above the title (user 2026-10-10: icon beside the title read oddly) */
  grid-template-columns: minmax(0, 1fr);
  grid-row: span 4;                     /* rows shared across the cards (subgrid): icons, titles, text and links line up */
  grid-template-rows: subgrid;
  align-items: start;
  gap: var(--sp-12);
  padding: var(--sp-20) var(--sp-20) var(--sp-16);
  border-radius: var(--radius--l);
  background: var(--color--grey-50);
  transition: background-color var(--dur) var(--ease-global), transform var(--dur) var(--ease-global);
}
.paths__card:hover { background: var(--color--grey-100); transform: translateY(-2px); }
.paths__emoji { font-family: "Tossface Nav", sans-serif; font-size: 2rem; line-height: 1; margin-bottom: var(--sp-8); }
.paths__title {
  margin-top: 0;
  font-size: var(--fs-body-xl);
  font-weight: 500;
  line-height: var(--lh-snug);
  color: var(--color--grey-1000);
}
.paths__link,
.paths__grid .paths__card .paths__title .paths__link { color: inherit; text-decoration: none; background: none; background-image: none; }   /* the site's inline-link underline (a background gradient) is for prose, not cards */
.paths__link::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.paths__link:focus-visible { outline: none; }
.paths__link:focus-visible::after { outline: 2px solid var(--color--accent); outline-offset: 3px; }
.paths__text { grid-column: 1 / -1; align-self: start; font-size: var(--fs-body); line-height: var(--lh-body); color: var(--color--grey-800); }
.paths__more {
  grid-column: 1 / -1;
  justify-self: start;
  align-self: end;
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  margin-top: var(--sp-8);
  font-size: var(--fs-body-sm);
  font-weight: 500;
  color: var(--color--grey-1000);
}
.paths__more svg { width: 0.75em; height: 0.75em; }
:lang(ko) .paths__title { line-height: var(--lh-heading); word-break: keep-all; }
:lang(ko) .paths__text, :lang(ko) .paths__line { line-height: var(--lh-ko-body); word-break: keep-all; }
@media (max-width: 1100px) { .paths__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 560px)  { .paths__grid { grid-template-columns: minmax(0, 1fr); } }

/* ---- Home final CTA: a heading and one line above the two buttons ---- */
.ctaopen__title { color: var(--color--grey-1000); }
.ctaopen__line { margin: var(--sp-16) auto var(--sp-32); max-width: 36rem; color: var(--color--grey-800); }
:lang(ko) .ctaopen__title { line-height: var(--lh-heading); word-break: keep-all; }
:lang(ko) .ctaopen__line { line-height: var(--lh-ko-body); word-break: keep-all; }

/* 2026-10-09 phase 2: forms with no receiving endpoint yet. The submit is
   disabled in the markup and this note says why (never a simulated success). */
.form-notice {
  margin-top: var(--sp-12);
  font-size: var(--fs-body-sm);
  line-height: var(--lh-body);
  color: var(--color--grey-800);
  text-align: center;
}
:lang(ko) .form-notice { line-height: var(--lh-ko-body); word-break: keep-all; }
/* Unconnected form submits keep their full look (user, 2026-10-09: the
   consultation button should look like the green rainbow demo button), so
   no fading; the notice under the button says why it doesn't send yet. */
.btn[disabled] { cursor: not-allowed; }
.btn--rainbow[disabled]:hover, .btn--rainbow[disabled]:active { --depth: 0.56em; transform: none; }

/* 2026-10-09 phase 2: "example" tag next to on-page UI cards and demo data
   (audit F12: screen figures are examples, not results). Small, quiet. */
.ks-card__eg,
.eg-tag {
  display: inline-block;
  margin-inline-start: 0.4em;
  padding: 0.1em 0.5em;
  border-radius: var(--radius--tag);
  background: var(--color--grey-100);
  color: var(--color--grey-800);
  font-size: 0.72em;
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: 0;
  vertical-align: 0.12em;
}
.eg-note {
  margin-top: var(--sp-12);
  font-size: var(--fs-body-sm);
  line-height: var(--lh-body);
  color: var(--color--grey-600);
}
:lang(ko) .eg-note { line-height: var(--lh-ko-body); word-break: keep-all; }

/* 2026-10-09 phase 2 (business #bz-app head): who decides what, as four
   short cells under the line. Card ground and type from the system. */
.bz-roles {
  list-style: none;
  margin: var(--sp-24) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-12);
  text-align: left;
}
.bz-roles li {
  display: grid;
  gap: var(--sp-4);
  padding: var(--sp-16) var(--sp-20);
  border-radius: var(--radius--l);
  background: var(--color--grey-50);
}
.bz-roles b { font-size: var(--fs-body); font-weight: 500; color: var(--color--grey-1000); }
.bz-roles span { font-size: var(--fs-body-sm); line-height: var(--lh-body); color: var(--color--grey-800); }
:lang(ko) .bz-roles span { line-height: var(--lh-ko-body); word-break: keep-all; }
/* Tossface marks over each cell, as on the homepage #paths cards (user
   2026-10-11). All four live in TossFaceFontMac-09 (css/styles.css). */
.bz-roles li > .bz-roles__emoji {   /* li: outranks :lang(ko) .bz-roles span */
  margin-bottom: var(--sp-8);
  font-family: "Tossface Nav", sans-serif;
  font-size: 2rem;
  line-height: 1;
  color: inherit;
}
@media (max-width: 1100px) { .bz-roles { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 560px)  { .bz-roles { grid-template-columns: minmax(0, 1fr); } }

/* 2026-10-09 phase 2 (publishers #unit): one unit as a marked learning
   example, beside the content-review steps. Site cards, tokens only. */
.pub-unit { padding-block: var(--sp-40); }   /* the page's plain-section rhythm (.pub-split, .pub-wideblock) */
.pub-unit__head { max-width: 46rem; margin-inline: auto; text-align: center; }
.pub-unit__head .h2 { margin-top: var(--sp-12); }
.pub-unit__line { margin-top: var(--sp-20); font-size: var(--fs-body-l); line-height: var(--lh-body); color: var(--color--grey-800); }
.pub-unit__grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-20); margin-top: var(--sp-40); }
.pub-unit__card { padding: var(--sp-40); border-radius: var(--radius--l); background: var(--color--grey-50); }
.pub-unit__card--review { background: var(--color--brand-50); }
.pub-unit__title { font-size: var(--fs-h4); font-weight: 500; line-height: var(--lh-snug); color: var(--color--grey-1000); }
.pub-unit__text { margin-top: var(--sp-12); font-size: var(--fs-body-l); line-height: var(--lh-body); color: var(--color--grey-800); }
.pub-flow, .pub-review { list-style: none; margin: var(--sp-24) 0 0; padding: 0; display: grid; gap: var(--sp-12); counter-reset: pubstep; }
.pub-flow__item { display: grid; grid-template-columns: 8.5rem minmax(0, 1fr); gap: var(--sp-16); padding-bottom: var(--sp-12); border-bottom: 1px solid var(--color--grey-100); }
.pub-flow__item:last-child { border-bottom: 0; padding-bottom: 0; }
.pub-flow__k { font-size: var(--fs-body-sm); font-weight: 500; color: var(--color--grey-1000); }
.pub-flow__v, .pub-review li { font-size: var(--fs-body); line-height: var(--lh-body); color: var(--color--grey-800); }
.pub-review li { counter-increment: pubstep; display: grid; grid-template-columns: 1.8rem minmax(0, 1fr); align-items: baseline; }
.pub-review li::before { content: counter(pubstep); font-size: var(--fs-body-sm); font-weight: 500; color: var(--color--grey-1000); }
:lang(ko) .pub-unit__line, :lang(ko) .pub-unit__text, :lang(ko) .pub-flow__v, :lang(ko) .pub-review li { line-height: var(--lh-ko-body); word-break: keep-all; }
:lang(ko) .pub-unit__title { line-height: var(--lh-heading); word-break: keep-all; }
@media (max-width: 860px) { .pub-unit__grid { grid-template-columns: minmax(0, 1fr); } .pub-unit__card { padding: var(--sp-32) var(--sp-24); } }
@media (max-width: 420px) { .pub-flow__item { grid-template-columns: minmax(0, 1fr); gap: var(--sp-4); } }
/* 2026-10-09 phase 2: the longer #practice / #included headings outgrow a
   phone column as unbreakable phrase groups; below 640 they wrap normally. */
@media (max-width: 640px) {
  #practice h2 .pub-beat, #practice h2 .pub-nowrap,
  #included h2 .pub-beat, #included h2 .pub-nowrap { white-space: normal; }
}

/* 2026-10-09 (user): the centre menu sits at the same spot on every page.
   In the flex row it centred in the space left over, so a longer page CTA
   ("우리 팀 교육 상담하기" vs "개인 학습 알아보기") nudged it sideways. From
   1200px it is pinned to the header's centre; below that it keeps the flow
   layout so it can never run into the logo or the buttons. */
@media (min-width: 1200px) {
  .nav__inner { position: relative; }
  .nav__links { position: absolute; left: 50%; transform: translateX(-50%); }
  .nav__actions { margin-inline-start: auto; }   /* the links no longer push it right */
}

/* 2026-10-09: with the "other routes" section removed, the consultation
   hero is the last thing in <main> (overflow: clip), so the form card's
   bottom edge and shadow were cut off. Give it the usual section gap. */
body:has([data-contact-form]) main > #top { padding-bottom: var(--section-gap); }

/* ---- 2026-10-10: enquiry form at the foot of every solution page ------
   One card for schools #demo, universities #talk, business (bz-close),
   publishers #start and the new individuals #inquiry. It is the schools
   demo card (css/schools.css .ks-demo__card) lifted here so every page
   shares it. No receiving endpoint yet: the submit stays disabled and the
   note sits at the TOP of the card, so the visitor knows before typing. */
.inq-card {
  max-width: 40rem;
  margin: var(--sp-40) auto 0;
  padding: var(--card-pad);
  border-radius: var(--radius--l);
  background: var(--color--white);
  box-shadow: var(--shadow--card);
  text-align: left;
  color: var(--color--grey-800);
}
.inq-card .input::placeholder { color: var(--color--grey-600); }
.inq-card .input:focus-visible { outline: 2px solid var(--color--accent); outline-offset: 3px; }
.inq-card textarea.input { min-height: 7rem; resize: vertical; }
.inq-card button[type="submit"] { margin-top: var(--sp-24); }
.inq-card .check { align-items: flex-start; }
.inq-card .check input { margin-top: 0.2em; flex: none; }
.form-notice--top {
  margin: 0 0 var(--sp-24);
  padding: var(--sp-12) var(--sp-16);
  border-radius: var(--radius--m);
  background: var(--color--grey-50);
  text-align: left;
}
.field__opt { font-weight: 400; color: var(--color--grey-600); }
.inq__msg { margin-top: var(--sp-16); padding: var(--sp-12) var(--sp-16); border-radius: var(--radius--m); font-size: var(--fs-body-s); }
.inq__msg--ok  { background: var(--color--brand-100); color: var(--color--brand-1000); }
.inq__msg--err { background: var(--color--red-50);   color: var(--color--red-1000); }
.inq__msg[hidden] { display: none; }
:lang(ko) .inq__msg, :lang(ko) .inq-card .field__hint { line-height: var(--lh-ko-body); word-break: keep-all; }
/* The page's earlier contact-page button now sits under the card as a
   quiet link (the route is kept, the form is the main action). */
.inq__alt { margin-top: var(--sp-20); font-size: var(--fs-body-sm); color: var(--color--grey-800); }
.inq__alt a { color: var(--color--grey-1000); text-decoration: underline; text-underline-offset: 0.2em; }
/* Individuals: the new foot section is the same sand outro panel. */
.inq-panel {
  margin: 0 var(--sp-8);
  padding-block: var(--sp-64);
  border-radius: var(--radius--2xl);
  background: var(--color--sand-100);
  color: var(--color--grey-1000);
  text-align: center;
}
.inq-panel__title, .inq-panel__line { margin-inline: auto; }
.inq-panel__title { color: var(--color--grey-1000); }
.inq-panel__line { margin-top: var(--sp-20); max-width: 40rem; font-size: var(--fs-body-l); line-height: var(--lh-body); color: var(--color--grey-800); }
:lang(ko) .inq-panel__title { line-height: var(--lh-heading); word-break: keep-all; }
:lang(ko) .inq-panel__line { line-height: var(--lh-ko-body); word-break: keep-all; }
@media (max-width: 640px) {
  .inq-card { padding: max(var(--card-pad), 20px); }
  .inq-card .form-grid { grid-template-columns: 1fr; }
  .inq-panel { padding-block: var(--sp-56); }
}


/* ---- Home: AI tutor in context (bar-tutor-context.md, 2026-10-10) ------
   monday.com "Work in context". A split opener (big two-line headline left;
   lead + body + the button pair right), then a tall track whose sticky stage
   holds the tutors and seven floating §9.4 cards in four loose clusters.
   js/main.js [data-tctx] sets --p (0→1 over the track) and moves each card in
   a straight line into the tutors (shrinking, fading) until they stand
   alone, as on monday. */
.tctx { padding-top: var(--section-gap); }
.tctx__head {
  display: grid;
  grid-template-columns: minmax(0, 46fr) minmax(0, 54fr);   /* §5.2 hero split */
  gap: var(--sp-32);
  align-items: start;               /* as monday: the lead's first line sits level with the headline's */
  position: relative;
  z-index: 4;
}
.tctx__title { color: var(--color--grey-1000); }
.tctx__intro { max-width: 34rem; padding-top: 0.5rem; }
.tctx__body { font-size: var(--fs-body-l); line-height: var(--lh-body); color: var(--color--grey-800); }
.tctx__body b { font-weight: 500; color: var(--color--grey-1000); }
.tctx__cta { margin-top: var(--sp-24); }
:lang(ko) .tctx__title { line-height: var(--lh-heading); letter-spacing: var(--kern-heading); word-break: keep-all; }
:lang(ko) .tctx__body { line-height: var(--lh-ko-body); word-break: keep-all; }

/* The track is the scroll distance; the stage is one screen under the nav.
   The stage starts under the opener, clear of its text (user 2026-10-10:
   no card over the words). */
.tctx__track { position: relative; height: 165svh; margin-top: var(--sp-40); }
.tctx__pin {
  position: sticky;
  top: var(--nav-h);
  height: calc(100svh - var(--nav-h));
  overflow: hidden;
}
.tctx__stage { position: relative; height: 100%; }

/* The tutors: cut at the waist and standing on the stage's bottom edge, as
   on monday — when the pin releases, that edge is exactly the top of the
   next section's panel, so the cut reads as the figure standing behind it
   and nothing ever covers the tutors. They grow ~8% upward (origin: bottom). */
.tctx__figure {
  position: absolute;
  z-index: 2;
  left: 50%;
  bottom: 0;
  width: min(46vw, 52rem, 92svh);
  height: auto;
  transform: translateX(-50%) scale(calc(1 + 0.1 * var(--p, 0)));
  transform-origin: 50% 100%;
}

/* Cards as on monday: one light grey tile (no white card, no shadow) with a
   small label and the app screen straight on it. Position and width are %
   of the stage (the 72rem wrap). */
.tctx__cards { position: absolute; inset: 0 var(--gutter); z-index: 1; }   /* cards travel behind the tutors and are swallowed there (as monday), fading as they go */
.tctx__card {
  position: absolute;
  left: calc(var(--x) * 1%);
  top: calc(var(--y) * 1%);
  width: calc(var(--w) * 1%);
  display: flex;
  flex-direction: column;
  gap: var(--sp-12);
  margin: 0;
  transform: translate(calc(var(--dx, 0) * 1px), calc(var(--dy, 0) * 1px)) scale(var(--s, 1));
  opacity: var(--o, 1);
  will-change: transform, opacity;
}
/* The K-12 #in-class cards' look (user, 2026-10-11: "솔루션 k12 쪽에 있는 방식으로…
   회색박스랑 글자 없애고"): no grey tile and no label — the real crop IS the
   card: white, --radius--l, the card shadow (css/schools.css .ks-agent), with
   no tilt (user, 2026-10-11: "배치는 각도 조절 하지 말아줘"). The labels stay for screen readers in the visually-hidden list. */
.tctx__label { display: none; }
.tctx__shot {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius--l);
  background: var(--_theme---cards--background);
  box-shadow: var(--shadow--card);
}


/* Reduced motion: no pin, no travel — the opening composition, still.
   Desktop only: below 1180 the blocks after this one already show a still
   stage of their own. Scoping it here makes that explicit, so the 52rem
   stage cap can never land on those layouts (a
   critic's reduced-motion test re-applied these rules last and the tutors
   overflowed the cap onto the closing band, 2026-10-11). */
@media (prefers-reduced-motion: reduce) and (min-width: 1180px) {
  .tctx__track { height: auto; margin-top: 0; }
  .tctx__pin { position: relative; top: 0; height: min(calc(100svh - var(--nav-h)), 52rem); }
  .tctx__card { transform: none; opacity: 1; }
  .tctx__figure { transform: translateX(-50%); }
}

/* Tablets and phones (below 1180, bar-tutor-context-responsive.md): one
   composed stage like desktop, shown still (js/main.js leaves it alone
   below 1180). No pin and no scroll track: the tutors stand on the stage's
   bottom edge (where #needs starts) and the cards float around them inside
   the page gutters (the cards' box is the stage's content box, as on
   desktop). Cards are drawn at their desktop size (1440: 245 / 230 / 230 /
   230 / 197 / 230 / 192px for the seven, in nth-child order — next lesson,
   streak, words, roadmaps, chat, tutors, study time; the same order in
   every language); only phones step down to 85% of it. Each card is
   placed from the side it sits on (--l or --r) and from the top (--t) or
   from the stage's bottom edge (--b); the inline --x/--y/--w stay for the
   desktop. The opener keeps the desktop's gap to the stage (≈ 4.8rem). */
@media (max-width: 1179px) {
  .tctx__track { height: auto; margin-top: 4.8rem; }
  .tctx__pin { position: relative; top: 0; height: min(calc(100svh - var(--nav-h)), 52rem); container-type: inline-size; }
  .tctx__card {
    left: var(--l, auto); right: var(--r, auto); top: var(--t, auto); bottom: var(--b, auto);
    width: var(--cw);
    transform: none; opacity: 1;
  }
  .tctx__figure { transform: translateX(-50%); }
  /* The headline is always exactly two lines ("Remembers you." /
     "Adapts to you."): never larger than its column fits. The widest
     line of any language with a two-line headline is ≈ 7.35em (ko). */
  .tctx__headline { container-type: inline-size; }
  .tctx__title { font-size: min(var(--fs-headline), 100cqi / 7.4); }
}
@media (min-width: 861px) and (max-width: 1179px) {
  .tctx__head { grid-template-columns: minmax(0, 54fr) minmax(0, 46fr); }   /* the headline column gets the wider share, as it carries the two long lines */
}
@media (max-width: 860px) {
  .tctx__head { grid-template-columns: minmax(0, 1fr); gap: var(--sp-20); align-items: start; }
}

/* Tablets (641–1179) keep every card at its full desktop size, ≥ 24px
   clear of every hand, head and hair, ≥ 60px above the waist cut, ≥ 12px
   apart (or one deliberate corner overlap). Cards sit at varied x and y;
   only the outermost card on each side touches the gutter. */

/* Landscape tablets (1024×768, 1112×834): the desktop picture in one
   screen. Tutors 49% wide, heads ~46% down the stage. Left: next lesson,
   roadmaps beside Isabelle's head, chat at her chest; right: words, the
   tutor picker beside Brad's hand, study time at his chest; the streak
   over the heads. Tops are % of the stage. */
@media (max-width: 1179px) and (orientation: landscape) {
  .tctx__figure { width: 49%; }
  .tctx__card:nth-child(1) { --l: 15px; --t: 2%;  --cw: 245px; }   /* next lesson */
  .tctx__card:nth-child(2) { --l: 46%;  --t: 13%; --cw: 230px; }   /* streak: over the heads */
  .tctx__card:nth-child(3) { --r: 15px; --t: 0;   --cw: 230px; }   /* words */
  .tctx__card:nth-child(4) { --l: 30px; --t: 42%; --cw: 230px; }   /* roadmaps: beside Isabelle's head */
  .tctx__card:nth-child(5) { --l: 0;    --b: 64px; --cw: 197px; }  /* chat: Isabelle's chest, outside her hand */
  .tctx__card:nth-child(6) { --r: 0;    --t: 36%; --cw: 230px; }   /* tutors: beside Brad's hand */
  .tctx__card:nth-child(7) { --r: 20px; --t: 66%; --cw: 192px; }   /* study time: Brad's chest */
}
/* Short landscape screens (a phone on its side): one screen is too short
   for the picture, so the stage keeps the desktop proportions instead. */
@media (max-width: 1179px) and (orientation: landscape) and (max-height: 600px) {
  .tctx__pin { height: auto; }
  .tctx__stage { height: auto; aspect-ratio: 14 / 9; }
}

/* Portrait tablets, 740–1019 (768, 820): tutors 62% wide (h = 49.34cqw).
   Here the raised hands reach so far out that no desktop-size card fits
   beside them at chest height (Isabelle's fingers start 165px from the
   left screen edge at 768), so all seven cards sit around and above the
   heads, the right-hand pair (study time, chat) beside Brad's head down to
   24px above his fingertips. The tutors' top is 577px − 11.84cqw; the
   stage is that plus their height. */
@media (min-width: 740px) and (max-width: 1179px) and (orientation: portrait) {
  .tctx__pin { height: auto; }
  .tctx__stage { height: calc(577px + 37.5cqw); }
  .tctx__figure { width: 62%; }
  .tctx__card:nth-child(1) { --l: 0;     --t: 0;     --cw: 245px; }   /* next lesson */
  .tctx__card:nth-child(2) { --r: 188px; --t: 140px; --cw: 230px; }   /* streak, tucked under words' corner as on desktop */
  .tctx__card:nth-child(3) { --r: 0;     --t: 20px;  --cw: 230px; }   /* words */
  .tctx__card:nth-child(4) { --l: 35px;  --t: 367px; --cw: 230px; }   /* roadmaps: over Isabelle's head */
  .tctx__card:nth-child(5) { --r: 0;     --t: 427px; --cw: 197px; }   /* chat: beside Brad's head */
  .tctx__card:nth-child(6) { --l: 15px;  --t: 209px; --cw: 230px; }   /* tutors */
  .tctx__card:nth-child(7) { --r: 20px;  --t: 324px; --cw: 192px; }   /* study time */
}
/* Portrait 1020–1179 (1024×1366): tutors 54% wide (h = 42.97cqw), Brad's
   head 40% down the stage; there is room outside the raised hands, so chat
   and study time sit at the two chests and the rest go around the heads. */
@media (min-width: 1020px) and (max-width: 1179px) and (orientation: portrait) {
  .tctx__stage { height: 69.47cqw; }
  .tctx__figure { width: 54%; }
  .tctx__card:nth-child(n) { --l: auto; --r: auto; }
  .tctx__card:nth-child(1) { --l: 20px; --t: 20px;                         --cw: 245px; }   /* next lesson */
  .tctx__card:nth-child(2) { --l: calc(52.16cqw - 37px - var(--gutter)); --t: calc(27.79cqw - 196px); --cw: 230px; }   /* streak: over Brad's head */
  .tctx__card:nth-child(3) { --r: 27px; --t: 0;                            --cw: 230px; }   /* words */
  .tctx__card:nth-child(4) { --l: 40px; --t: calc(46.44cqw - 200px);       --cw: 230px; }   /* roadmaps: beside Isabelle's head, above her hand */
  .tctx__card:nth-child(5) { --l: 0;    --t: 48.83cqw;                     --cw: 197px; }   /* chat: Isabelle's chest, outside her hand */
  .tctx__card:nth-child(6) { --r: 10px; --t: calc(36.81cqw - 174px);       --cw: 230px; }   /* tutors: beside Brad's head, above his hand */
  .tctx__card:nth-child(7) { --r: 0;    --t: 47.13cqw;                     --cw: 192px; }   /* study time: Brad's chest, outside his hand */
}

/* Portrait phones (up to 739): tutors 86% wide (h = 68.44cqw), nothing
   in front of them or behind them: five cards about 75–80% of desktop
   size (titles ≈ 10px), all fully visible, in two staggered columns that
   end 24px above the heads — next lesson, chat and study time on the left
   (the left column reaches down beside Brad's head to 24px above
   Isabelle's hair), words and the streak on the right (ending 24px above
   Brad's hair). On a phone the raised hands and forearms fill both outer
   sides down to the waist, so there is no room for a readable card at
   chest height. Roadmaps and the tutor picker are dropped. The tutors' top
   is 68.52cqw + 36px; the stage is that plus their height. */
@media (max-width: 739px) and (orientation: portrait) {
  .tctx__pin { height: auto; }
  .tctx__stage { height: calc(136.96cqw + 36px); }
  .tctx__figure { width: 86%; }
  .tctx__card:nth-child(4),
  .tctx__card:nth-child(6) { display: none; }
  .tctx__card:nth-child(1) { --l: 0; --t: calc(12.76cqw - 12px); --cw: 41.5cqw; }   /* next lesson */
  .tctx__card:nth-child(3) { --r: 0; --t: 0;                     --cw: 45cqw; }     /* words */
  .tctx__card:nth-child(2) { --r: 0; --t: calc(34.88cqw + 12px); --cw: 45cqw; }     /* streak */
  .tctx__card:nth-child(5) { --l: 0; --t: calc(46.11cqw);        --cw: 41.5cqw; }   /* chat */
  .tctx__card:nth-child(7) { --l: 0; --t: calc(69.75cqw + 12px); --cw: 38cqw; }     /* study time */
}

/* Phones: the button goes full width (§10.3). */
@media (max-width: 640px) {
  .tctx__title { font-size: min(var(--fs-h1), 100cqi / 7.4); }
  .tctx__cta { display: flex; width: 100%; justify-content: center; }
}

/* ---- #needs on the sand outro panel, sliding over the tutors -----------
   2026-10-10 (user): the section after #tutor sits on the sand rounded panel
   the solution pages end with (.ks-demo / .pub-close: sand-100, 2xl radius,
   0.4rem side margin). It starts right where #tutor's stage ends, so its top
   edge meets the tutors' waist cut (monday: the next section never covers
   the figure). */
#needs {
  position: relative;
  z-index: 5;
  margin-inline: var(--sp-8);
  border-radius: var(--radius--2xl);
  background: var(--color--sand-100);
  overflow: clip;
}

/* ============ Company details (footer) ============
   Korean e-commerce law: business name, address, registration numbers,
   representative, contact and hosting provider on every page. */
.footer__company {
  margin-top: var(--sp-32);
  font-style: normal;
  font-size: var(--fs-body-2xs);
  line-height: 1.6;
  color: var(--color--grey-600);
}
.footer__company p { margin: 0; }
.footer__company a { color: inherit; }
:lang(ko) .footer__company { word-break: keep-all; }

/* ============ Direct contact under enquiry forms ============
   Shown while the forms have no receiving endpoint, so a visitor always
   has a working way to reach us. */
.form-direct {
  margin: var(--sp-12) 0 0;
  font-size: var(--fs-body-sm);
  line-height: var(--lh-body);
  color: var(--color--grey-800);
  text-align: center;
}
.form-direct a { color: var(--_theme---page--text); text-decoration: underline; }
:lang(ko) .form-direct { line-height: var(--lh-ko-body); word-break: keep-all; }

/* Individual learners: app download block (sits between perks and FAQ) */
.iv-getapp { margin-block: var(--sp-40); }
.iv-getapp .btn-group { justify-content: center; margin-top: var(--sp-32); }

/* Store logos inside the app-download buttons */
.store-icon { width: 1.2em; height: 1.2em; flex: none; margin-inline-end: 0.15em; }

/* Free-trial terms under the store buttons */
.iv-getapp__trial {
  max-width: 36em; margin: var(--sp-24) auto 0;
  font-size: var(--fs-body-sm); line-height: var(--lh-body); color: var(--color--grey-800);
}
.iv-getapp__trial strong { font-weight: 500; color: var(--color--grey-1000); }
:lang(ko) .iv-getapp__trial { line-height: var(--lh-ko-body); word-break: keep-all; }

/* ============ Cookie choice banner (js/main.js) ============
   Bottom-left on computers, full width on phones. Both buttons have
   the same size, so declining is as easy as accepting. */
.consent-bar {
  position: fixed; z-index: 80;
  inset-inline-start: var(--sp-16); bottom: calc(var(--sp-16) + env(safe-area-inset-bottom, 0px));
  max-width: min(26rem, calc(100vw - 2 * var(--sp-16)));
  padding: var(--sp-16);
  background: var(--color--white); border-radius: 16px;
  box-shadow: 0 8px 28px rgb(0 31 31 / 0.14), 0 0 0 1px rgb(0 31 31 / 0.06);
  font-size: var(--fs-body-xs); line-height: 1.45; color: var(--_theme---page--text);
}
.consent-bar__text { margin: 0 0 var(--sp-12); }
.consent-bar__text a { color: inherit; text-decoration: underline; }
.consent-bar__actions { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-8); }
:lang(ko) .consent-bar__text, :lang(ja) .consent-bar__text { word-break: keep-all; }

/* ============ QR card on computers (js/main.js) ============
   "Get the app" opens it under the button. The logo sits on the code's
   centre; the code uses high error correction, so it still scans. */
.qr-card {
  position: fixed; z-index: 70;
  display: flex; align-items: center; gap: var(--sp-16);
  padding: var(--sp-16); padding-inline-end: var(--sp-40);
  background: var(--color--white); border-radius: 16px;
  box-shadow: 0 12px 36px rgb(0 31 31 / 0.16), 0 0 0 1px rgb(0 31 31 / 0.06);
  color: var(--_theme---page--text);
}
.qr-card__code { position: relative; flex: none; width: 132px; height: 132px; }
.qr-card__code > img:first-child { display: block; width: 100%; height: 100%; }
.qr-card__mark {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 24px; height: 24px; padding: 3px; border-radius: 7px; background: var(--color--white);
}
.qr-card__text { margin: 0; width: 150px; display: flex; flex-direction: column; gap: var(--sp-8); }
.qr-card__text strong { font-weight: 500; font-size: var(--fs-body-sm); line-height: var(--lh-body); }
.qr-card__text span { font-size: var(--fs-body-xs); color: var(--color--grey-800); }
:lang(ko) .qr-card__text, :lang(ja) .qr-card__text { word-break: keep-all; }
.qr-card__close {
  position: absolute; inset-block-start: var(--sp-8); inset-inline-end: var(--sp-8);
  width: 2rem; height: 2rem; border: 0; border-radius: 50%;
  background: transparent; color: var(--color--grey-600); font-size: 1.2rem; line-height: 1; cursor: pointer;
}
.qr-card__close:hover { background: var(--color--grey-50); }

/* ============ App bar on phones and tablets (js/main.js) ============
   Pages with <body data-app-bar> show it once the visitor scrolls. */
.app-bar {
  position: fixed; z-index: 60;
  inset-inline: var(--sp-12); bottom: calc(var(--sp-12) + env(safe-area-inset-bottom, 0px));
  margin-inline: auto; max-width: 30rem;
  display: flex; align-items: center; gap: var(--sp-12);
  padding-block: var(--sp-8); padding-inline: var(--sp-12) var(--sp-8);
  background: var(--color--white); border-radius: 16px;
  box-shadow: 0 8px 28px rgb(0 31 31 / 0.14), 0 0 0 1px rgb(0 31 31 / 0.06);
  font-size: var(--fs-body-xs); line-height: 1.3; color: var(--_theme---page--text);
  transform: translateY(calc(100% + var(--sp-24))); visibility: hidden;
  transition: transform 0.3s ease, visibility 0s linear 0.3s;
}
.app-bar.is-shown { transform: none; visibility: visible; transition: transform 0.3s ease; }
.app-bar__icon { flex: none; width: 40px; height: 40px; border-radius: 10px; box-shadow: 0 0 0 1px rgb(0 31 31 / 0.08); }
.app-bar__text { margin: 0; flex: 1; min-width: 0; display: flex; flex-direction: column; }
.app-bar__text strong { font-weight: 500; font-size: var(--fs-body-sm); }
.app-bar__text span { color: var(--color--grey-800); }
.app-bar__go { flex: none; white-space: nowrap; }
.app-bar__go .store-icon { width: 1.1em; height: 1.1em; margin-inline-end: 0; }
.app-bar__close {
  flex: none; width: 2rem; height: 2rem; border: 0; border-radius: 50%;
  background: transparent; color: var(--color--grey-600); font-size: 1.2rem; line-height: 1; cursor: pointer;
}
.app-bar__close:hover { background: var(--color--grey-50); }
@media (prefers-reduced-motion: reduce) { .app-bar, .app-bar.is-shown { transition: none; } }
/* Small phones: long button labels (Installieren, インストール) left the text a
   sliver, one syllable per line. Drop the icon and tighten the gaps there, and
   never hyphenate the brand or the trial line. */
.app-bar__text { hyphens: manual; }
@media (max-width: 380px) {
  .app-bar { gap: var(--sp-8); padding-inline: var(--sp-12) var(--sp-4); }
  .app-bar__icon { display: none; }
}

/* ============ Language suggestion (js/main.js) ============ */
.lang-offer {
  position: fixed; z-index: 60;
  inset-inline: var(--sp-16); bottom: var(--sp-16);
  margin-inline: auto; max-width: 30rem;
  display: flex; align-items: center; gap: var(--sp-12);
  padding: var(--sp-12) var(--sp-12) var(--sp-12) var(--sp-16);
  background: var(--color--white); border-radius: 14px;
  box-shadow: 0 8px 28px rgb(0 31 31 / 0.14), 0 0 0 1px rgb(0 31 31 / 0.06);
  font-size: var(--fs-body-xs); line-height: 1.4; color: var(--_theme---page--text);
}
.lang-offer__text { margin: 0; flex: 1; }
.lang-offer__go { flex: none; white-space: nowrap; }
.lang-offer__close {
  flex: none; width: 2rem; height: 2rem; border: 0; border-radius: 50%;
  background: transparent; color: var(--color--grey-600); font-size: 1.2rem; line-height: 1; cursor: pointer;
}
.lang-offer__close:hover { background: var(--color--grey-50); }
.lang-offer:lang(ja) .lang-offer__text, .lang-offer:lang(ko) .lang-offer__text { word-break: keep-all; }
/* Phones: sentence (and close) on the first row, the button full width below it. */
@media (max-width: 520px) {
  .lang-offer { display: grid; grid-template-columns: 1fr auto; row-gap: var(--sp-8); }
  .lang-offer__text { grid-column: 1; grid-row: 1; align-self: center; }
  .lang-offer__close { grid-column: 2; grid-row: 1; }
  .lang-offer__go { grid-column: 1 / -1; grid-row: 2; }
}

