/* ==========================================================================
   Langford Academy — marketing site
   One role colour carries each surface: pinkish red for the parent app,
   the Langford purple for the student app, blue for the teacher app. The
   whole page recolours when you switch role, because the role IS the
   information architecture.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */

:root {
  /* Neutrals, tinted toward the product's violet canvas. Never pure #fff/#000. */
  --card: oklch(99.6% 0.003 300);
  --ink: oklch(21% 0.028 288);
  --ink-2: oklch(46% 0.022 288);
  --ink-3: oklch(62% 0.018 288);
  --line: oklch(90.5% 0.014 295);
  --line-soft: oklch(94.5% 0.010 295);

  --ok: oklch(58% 0.15 152);
  --bad: oklch(58% 0.19 25);

  /* Motion */
  --e-out: cubic-bezier(0.23, 1, 0.32, 1);
  --e-in-out: cubic-bezier(0.77, 0, 0.175, 1);
  --e-drawer: cubic-bezier(0.32, 0.72, 0, 1);
  /* Role recolour. Long and eased at both ends: it is a change of world,
     not a hover, so it should start and finish softly. */
  --swap: 620ms;
  --e-swap: cubic-bezier(0.65, 0, 0.35, 1);

  /* Rhythm */
  --gutter: clamp(1.15rem, 4.5vw, 4.5rem);
  --nav-h: 4.9rem;
  --r: 14px;
  --r-sm: 9px;

  /* Pointer parallax, written by JS on a spring */
  --rx: 0deg;
  --ry: 0deg;

  color-scheme: light;
}

/* ==========================================================================
   ROLE THEMES — the only colour decisions in the project.

   Three roles, three jewel tones of matching weight:

     parent = pinkish red    student = Langford purple    teacher = blue

   Student is the exact ink from the logo artwork, #683C9F, measured off
   purple.png. Parent and teacher are tuned to sit at the same perceived
   weight beside it rather than at their "natural" brightness, which is why
   the blue is deeper than a stock #2563EB and the red is deeper than a
   stock hot pink. Equal weight is what makes the role switch read as one
   palette instead of three unrelated colours.

   To rebrand, edit the six `--brand*` lines below and the two `--page*` lines
   under each role. Nothing else in this file hard-codes a brand colour; every
   button, pill, chart and link reads `var(--brand)`.

     --brand       the colour itself, used for fills and text
     --brand-hi    hover state, one step lighter
     --brand-deep  pressed / on-colour text
     --brand-wash  10% tint for soft backgrounds
     --brand-line  28% tint for borders
     --page        page background
     --page-2      alternating band background
   ========================================================================== */

:root,
[data-role="parent"] {
  --brand: oklch(51% 0.196 13);
  --brand-hi: oklch(58% 0.202 14);
  --brand-deep: oklch(42% 0.165 12);
  --brand-wash: oklch(51% 0.196 13 / 0.1);
  --brand-line: oklch(51% 0.196 13 / 0.28);
  --page: oklch(97.3% 0.013 16);
  --page-2: oklch(94.8% 0.024 14);
}

[data-role="student"] {
  --brand: oklch(46% 0.155 301);
  --brand-hi: oklch(53% 0.168 302);
  --brand-deep: oklch(38% 0.13 300);
  --brand-wash: oklch(46% 0.155 301 / 0.1);
  --brand-line: oklch(46% 0.155 301 / 0.28);
  --page: oklch(97.3% 0.014 300);
  --page-2: oklch(94.8% 0.026 300);
}

[data-role="teacher"] {
  --brand: oklch(49% 0.183 264);
  --brand-hi: oklch(56% 0.188 265);
  --brand-deep: oklch(40% 0.155 263);
  --brand-wash: oklch(49% 0.183 264 / 0.1);
  --brand-line: oklch(49% 0.183 264 / 0.28);
  --page: oklch(97.4% 0.013 258);
  --page-2: oklch(94.9% 0.026 258);
}

/* --------------------------------------------------------------------------
   2. Reset
   -------------------------------------------------------------------------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

* {
  margin: 0;
}

body {
  background: var(--page);
  color: var(--ink);
  font-family: Manrope, ui-sans-serif, system-ui, sans-serif;
  font-size: clamp(0.975rem, 0.94rem + 0.16vw, 1.075rem);
  line-height: 1.6;
  font-weight: 450;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  transition: background-color var(--swap) var(--e-swap);
}

img,
svg {
  display: block;
  max-width: 100%;
}

/* Components below set display, which beats the UA rule for [hidden].
   Without this, hiding a panel or a filtered-out row silently does nothing. */
[hidden] {
  display: none !important;
}

img {
  height: auto;
}

button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
}

a {
  color: inherit;
}

ul,
ol {
  list-style: none;
  padding: 0;
}

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

::selection {
  background: var(--brand);
  color: var(--card);
}

/* --------------------------------------------------------------------------
   3. Typography
   -------------------------------------------------------------------------- */

h1,
h2,
h3,
h4,
.display {
  font-family: Gabarito, Manrope, sans-serif;
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.035em;
  text-wrap: balance;
}

h1 {
  font-size: clamp(2.4rem, 1.2rem + 4.1vw, 5rem);
  letter-spacing: -0.05em;
  line-height: 0.92;
}

h2 {
  font-size: clamp(2.15rem, 1.3rem + 3.6vw, 4.6rem);
  line-height: 0.98;
  letter-spacing: -0.045em;
}

h3 {
  font-size: clamp(1.15rem, 1.05rem + 0.5vw, 1.45rem);
  line-height: 1.15;
  letter-spacing: -0.025em;
  font-weight: 700;
}

.lead {
  font-size: clamp(1.05rem, 0.98rem + 0.45vw, 1.3rem);
  line-height: 1.5;
  color: var(--ink-2);
  max-width: 46ch;
  text-wrap: pretty;
}

p {
  max-width: 64ch;
  text-wrap: pretty;
}

.kicker {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.76rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--brand);
  transition: color var(--swap) var(--e-swap);
}

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

.faint {
  color: var(--ink-3);
}

.num {
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}

/* --------------------------------------------------------------------------
   4. Layout
   -------------------------------------------------------------------------- */

.shell {
  width: 100%;
  max-width: 80rem;
  margin-inline: auto;
  padding-inline: var(--gutter);
}

section {
  scroll-margin-top: var(--nav-h);
}

.band {
  padding-block: clamp(4rem, 8vh + 2rem, 7.5rem);
  position: relative;
}

.band--tint {
  background: var(--page-2);
  transition: background-color var(--swap) var(--e-swap);
}

.head {
  max-width: 44rem;
  margin-bottom: clamp(2.25rem, 5vh, 3.75rem);
  display: grid;
  gap: 1rem;
  justify-items: start;
}

.head--wide {
  max-width: none;
  grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
  align-items: end;
  gap: clamp(1.25rem, 4vw, 3.5rem);
}

.head--wide > :first-child {
  display: grid;
  gap: 1rem;
  justify-items: start;
}

.skip {
  position: absolute;
  left: var(--gutter);
  top: 0.5rem;
  z-index: 100;
  padding: 0.6rem 1rem;
  background: var(--brand);
  color: var(--card);
  font-weight: 700;
  border-radius: var(--r-sm);
  transform: translateY(-180%);
  transition: transform 180ms var(--e-out);
}

.skip:focus-visible {
  transform: translateY(0);
}

/* --------------------------------------------------------------------------
   5. Buttons
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.85rem 1.4rem;
  min-height: 2.9rem;
  border: 1px solid transparent;
  border-radius: 999px;
  background: var(--brand);
  color: var(--card);
  font-family: Gabarito, sans-serif;
  font-weight: 700;
  font-size: 0.95rem;
  letter-spacing: -0.015em;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: transform 150ms var(--e-out), background-color var(--swap) var(--e-swap),
    border-color var(--swap) var(--e-swap), color var(--swap) var(--e-swap),
    box-shadow 220ms var(--e-out);
}

.btn:active {
  transform: scale(0.97);
}

.btn--ghost {
  background: transparent;
  color: var(--ink);
  border-color: var(--line);
}

.btn--quiet {
  background: var(--card);
  color: var(--brand);
  border-color: var(--brand-line);
}

.btn--sm {
  padding: 0.55rem 1rem;
  min-height: 2.3rem;
  font-size: 0.85rem;
}

.btn--block {
  width: 100%;
}

.arrow {
  transition: transform 240ms var(--e-out);
}

.textlink {
  color: var(--brand);
  font-weight: 700;
  text-decoration: none;
  background-image: linear-gradient(currentColor, currentColor);
  background-size: 100% 1.5px;
  background-repeat: no-repeat;
  background-position: 0 100%;
  padding-bottom: 1px;
  transition: color var(--swap) var(--e-swap), background-size 260ms var(--e-out);
}

/* --------------------------------------------------------------------------
   6. Nav + role switch
   -------------------------------------------------------------------------- */

/* ---- Logo ------------------------------------------------------------
   The artwork is a single-colour silhouette, so it is used as a CSS mask
   rather than an <img>. The element's background paints through it, which
   means the logo is drawn in var(--brand) and recolours with the role
   switch from one file. No per-colour PNG exports to keep in sync.
   ---------------------------------------------------------------------- */

.logo {
  /* Must be blockified: as an inline span, width and height are ignored and
     the logo collapses to nothing outside a flex or grid parent. */
  display: block;
  background-color: var(--brand);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: contain;
  mask-size: contain;
  transition: background-color var(--swap) var(--e-swap);
  flex: none;
}

.logo--mark {
  width: 3.05rem;
  height: 2.67rem;
  -webkit-mask-image: url("../logo/mark.png");
  mask-image: url("../logo/mark.png");
}

.logo--lockup {
  width: 7.6rem;
  height: 8.72rem;
  -webkit-mask-image: url("../logo/lockup.png");
  mask-image: url("../logo/lockup.png");
}

/* On the brand-filled CTA panel the logo has to invert to stay visible */
.close .logo {
  background-color: var(--card);
}

/* ---- Nav, tied to the scroll ------------------------------------------
   Two numbers drive it, both written by the same rAF loop that runs the acts:

     --navp  0..1  the hero surface fading in underneath as Act 1 scrubs
     --navt  0..1  the turn to brand colour as Act 2 arrives, eased in JS

   Every colour below is one color-mix between its normal value and its
   on-brand value, so the whole bar crosses over as a single movement
   instead of a dozen properties transitioning at slightly different rates.
   ---------------------------------------------------------------------- */

.nav {
  --navp: 0;
  --navt: 0;
  /* Fixed, not sticky, and fully transparent until the one-app section.
     The hero runs the full viewport underneath, so the ambient field drifts
     straight through the bar instead of stopping at it. */
  position: fixed;
  inset-inline: 0;
  inset-block-start: 0;
  z-index: 60;
  /* Arrives as white carrying a trace of the live theme, not a solid brand
     slab: the bar should read as the page surface, not as a banner. */
  background-color: color-mix(
    in oklab,
    color-mix(in oklab, var(--card) 91%, var(--brand)) calc(var(--navt) * 100%),
    transparent
  );
  border-bottom: 1px solid color-mix(in oklab, var(--brand-line) calc(var(--navt) * 100%), transparent);
  box-shadow: 0 8px 24px -20px oklch(21% 0.028 288 / calc(var(--navt) * 0.55));
}

/* Foreground crosses to the on-brand value over the same --navt */
.nav__inner {
  display: flex;
  align-items: center;
  gap: clamp(0.6rem, 2vw, 1.75rem);
  width: 100%;
  height: var(--nav-h);
}

.nav__end {
  display: flex;
  align-items: center;
  gap: 0.55rem;
}

/* ---- Wordmark ---------------------------------------------------------
   LANGFORD is set in Monoton, the decorative face, and takes the live role
   colour. ACADEMY sits under it in Poppins: quiet, evenly tracked, and
   deliberately plain so the display line above it does all the talking.
   ---------------------------------------------------------------------- */

.mark {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  text-decoration: none;
}

.mark__text {
  display: grid;
  gap: 0;
  font-family: Poppins, sans-serif;
  line-height: 1;
}

/* ACADEMY sits centred under LANGFORD with a rule either side, and its
   width is driven by the line above rather than set independently, so the
   two stay locked together at any size. */
.mark__sub {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.45em;
  font-family: Poppins, sans-serif;
  font-size: 0.48em;
  font-weight: 500;
  letter-spacing: 0.42em;
  text-indent: 0.42em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-top: 0.12em;
}

.mark__sub i {
  flex: 1;
  height: 1px;
  background: currentColor;
  opacity: 0.55;
}

.mark__text b {
  /* Monoton is a single-weight decorative face: its inline stripes are the
     letterform, so it needs size and air or the detail closes up. */
  font-family: Monoton, Gabarito, cursive;
  /* Monoton ships a single weight, so weight has to come from a stroke.
     Kept light: just enough to firm up the rules, well short of closing the
     inline stripes that are the whole point of the face. */
  font-weight: 400;
  font-size: var(--wordmark-size, 1.58rem);
  line-height: 1.08;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--brand);
  /* Monoton ships ONE weight (400). Its apparent weight is set by this
     stroke, not font-weight. Raise it to thicken, lower it to thin. */
  -webkit-text-stroke: var(--wordmark-ink, 0.35px) currentColor;
  text-stroke: var(--wordmark-ink, 0.35px) currentColor;
  paint-order: stroke fill;
  transition: color var(--swap) var(--e-swap);
}

/* ---- Links ---- */

.nav__links {
  display: flex;
  gap: clamp(0.5rem, 1.4vw, 1.1rem);
  font-size: 0.92rem;
  font-weight: 600;
  /* Equal air either side balances a wide mark against a wide right cluster */
  margin-inline: auto;
}

.nav__links a {
  position: relative;
  padding: 0.4rem 0.55rem;
  border-radius: 8px;
  color: var(--ink-2);
  text-decoration: none;
  transition: color 200ms var(--e-out), background-color 200ms var(--e-out);
}

.nav__links a[aria-current="true"] {
  color: var(--brand);
}

/* Underline grows from the centre, so the active mark reads as a state
   rather than as a second thing arriving. */
.nav__links a::after {
  content: "";
  position: absolute;
  inset-block-end: 0.1rem;
  inset-inline: 0.55rem;
  height: 2px;
  border-radius: 2px;
  background: var(--brand);
  transform: scaleX(0);
  transition: transform 260ms var(--e-out), background-color var(--swap) var(--e-swap);
}

.nav__links a[aria-current="true"]::after {
  transform: scaleX(1);
}

/* ---- Role switch ------------------------------------------------------
   The page's main control. Equal-width cells mean the indicator only ever
   needs translating, never resizing, so the movement is a single cheap
   transform.
   ---------------------------------------------------------------------- */

.roleswitch {
  position: relative;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0;
  padding: 3px;
  background: var(--page-2);
  border: 1px solid var(--line);
  border-radius: 999px;
  transition: background-color var(--swap) var(--e-swap), border-color 260ms var(--e-out);
}

.roleswitch__thumb {
  position: absolute;
  inset-block: 3px;
  inset-inline-start: 3px;
  width: calc((100% - 6px) / 3);
  border-radius: 999px;
  background: var(--card);
  box-shadow: 0 1px 3px oklch(21% 0.028 288 / 0.16);
  transform: translateX(calc(var(--i, 0) * 100%));
  transition: transform 420ms var(--e-drawer);
  pointer-events: none;
}

.roleswitch button {
  position: relative;
  z-index: 1;
  padding: 0.48rem 0.9rem;
  border: 0;
  border-radius: 999px;
  background: transparent;
  font-family: Poppins, sans-serif;
  font-size: 0.85rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--ink-2);
  cursor: pointer;
  white-space: nowrap;
  transition: color 260ms var(--e-out);
}

.roleswitch button[aria-pressed="true"] {
  color: var(--brand);
}

.roleswitch--hero {
  padding: 4px;
}

.roleswitch--hero button {
  padding: 0.6rem 1.25rem;
  font-size: 0.92rem;
}

/* ---- Burger + drop-down menu ---- */

.burger {
  display: none;
  width: 2.5rem;
  height: 2.5rem;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--card);
  cursor: pointer;
  position: relative;
  transition: transform 150ms var(--e-out), border-color 200ms var(--e-out);
}

.burger:active {
  transform: scale(0.94);
}

.burger span {
  position: absolute;
  inset-inline-start: 50%;
  width: 1.05rem;
  height: 2px;
  margin-inline-start: -0.525rem;
  border-radius: 2px;
  background: var(--ink);
  transition: transform 300ms var(--e-out), opacity 200ms var(--e-out);
}

.burger span:first-child {
  inset-block-start: calc(50% - 4px);
}

.burger span:last-child {
  inset-block-start: calc(50% + 2px);
}

.burger[aria-expanded="true"] span:first-child {
  transform: translateY(3px) rotate(45deg);
}

.burger[aria-expanded="true"] span:last-child {
  transform: translateY(-3px) rotate(-45deg);
}

.navmenu {
  display: none;
  overflow: hidden;
  border-top: 1px solid var(--line);
  background: var(--page);
  transition: grid-template-rows 340ms var(--e-out);
}

.navmenu__inner {
  display: grid;
  gap: 0.15rem;
  padding-block: 0.75rem 1.1rem;
}

.navmenu a {
  padding: 0.7rem 0.25rem;
  font-size: 1rem;
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
  border-radius: 8px;
}

.navmenu a.btn {
  margin-top: 0.6rem;
  color: var(--card);
  justify-content: center;
}

@media (max-width: 72rem) {
  .nav__links,
  .nav__cta {
    display: none;
  }

  .burger {
    display: block;
  }

  .navmenu {
    display: grid;
    grid-template-rows: 0fr;
  }

  .navmenu[data-open="true"] {
    grid-template-rows: 1fr;
  }

  .navmenu > nav {
    min-height: 0;
  }
}

/* --------------------------------------------------------------------------
   7. Hero
   -------------------------------------------------------------------------- */

.hero {
  padding-block: clamp(2.5rem, 5vh, 4.5rem) clamp(3rem, 7vh, 6rem);
  position: relative;
  overflow: hidden;
}

/* --------------------------------------------------------------------------
   9. Miniature app UI, used inside phones and panels
   -------------------------------------------------------------------------- */

.ui-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--ink-2);
}

.ui-title {
  font-family: Gabarito, sans-serif;
  font-size: 1rem;
  font-weight: 800;
  letter-spacing: -0.03em;
}

.ui-card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: 0.65rem;
  display: grid;
  gap: 0.35rem;
}

.ui-card--brand {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--card);
}

.ui-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.ui-row--between {
  justify-content: space-between;
}

.ui-avatar {
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  object-fit: cover;
  flex: none;
  background: var(--page-2);
}

.ui-avatar--lg {
  width: 3rem;
  height: 3rem;
}

.ui-name {
  font-size: 0.8rem;
  font-weight: 800;
  letter-spacing: -0.02em;
}

.ui-sub {
  font-size: 0.68rem;
  color: var(--ink-3);
}

.ui-pill {
  font-size: 0.62rem;
  font-weight: 800;
  padding: 0.16rem 0.45rem;
  border-radius: 999px;
  background: var(--brand-wash);
  color: var(--brand);
  white-space: nowrap;
  transition: background-color var(--swap) var(--e-swap), color var(--swap) var(--e-swap);
}

.ui-pill--live {
  background: oklch(58% 0.15 152 / 0.14);
  color: var(--ok);
}

.ui-pill--paid {
  background: oklch(58% 0.15 152 / 0.14);
  color: var(--ok);
}

.ui-price {
  font-family: Gabarito, sans-serif;
  font-weight: 800;
  font-size: 0.9rem;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
}

.ui-btn {
  border: 0;
  border-radius: 999px;
  background: var(--brand);
  color: var(--card);
  font-family: Gabarito, sans-serif;
  font-weight: 700;
  font-size: 0.72rem;
  padding: 0.4rem 0.8rem;
  text-align: center;
  transition: background-color var(--swap) var(--e-swap);
}

.ui-tabs {
  display: flex;
  gap: 0.3rem;
  overflow: hidden;
}

.ui-tab {
  font-size: 0.62rem;
  font-weight: 700;
  padding: 0.25rem 0.55rem;
  border-radius: 999px;
  color: var(--ink-3);
  border: 1px solid var(--line);
  white-space: nowrap;
}

.ui-tab[data-on] {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--card);
  transition: background-color var(--swap) var(--e-swap);
}

/* ---- Role tints -------------------------------------------------------
   Re-declare the brand tokens on a subtree so a component can hold one
   role's colour regardless of which role the page is currently in. This is
   what lets all three apps appear together in their own colours.
   ---------------------------------------------------------------------- */

.tint-parent {
  --brand: oklch(51% 0.196 13);
  --brand-hi: oklch(58% 0.202 14);
  --brand-deep: oklch(42% 0.165 12);
  --brand-wash: oklch(51% 0.196 13 / 0.1);
  --brand-line: oklch(51% 0.196 13 / 0.28);
}

.tint-student {
  --brand: oklch(46% 0.155 301);
  --brand-hi: oklch(53% 0.168 302);
  --brand-deep: oklch(38% 0.13 300);
  --brand-wash: oklch(46% 0.155 301 / 0.1);
  --brand-line: oklch(46% 0.155 301 / 0.28);
}

.tint-teacher {
  --brand: oklch(49% 0.183 264);
  --brand-hi: oklch(56% 0.188 265);
  --brand-deep: oklch(40% 0.155 263);
  --brand-wash: oklch(49% 0.183 264 / 0.1);
  --brand-line: oklch(49% 0.183 264 / 0.28);
}

/* ==========================================================================
   Triptych — the three apps, in their three colours, at once.
   The palette is the product's best asset and the role switch was hiding
   two thirds of it. Here all three stand together; the live role steps
   forward and the other two recede without disappearing.
   ========================================================================== */

.tri {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(0.6rem, 2vw, 1.75rem);
  align-items: end;
  padding-block: clamp(1.5rem, 4vw, 3rem) 0;
  perspective: 1800px;
  /* Held narrow so the three read as a set at a glance, not as three
     billboards you have to scan across. */
  max-width: 48rem;
  margin-inline: auto;
  margin-block-end: clamp(3rem, 7vw, 5.5rem);
}

.tri__cell {
  display: grid;
  justify-items: center;
  gap: 1rem;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  font: inherit;
  color: inherit;
  transition: transform 620ms var(--e-out), opacity 620ms var(--e-out);
  transform: translate3d(0, 1.5rem, 0) scale(0.945);
  opacity: 0.84;
}

.tri__cell[data-active="true"] {
  transform: none;
  opacity: 1;
}

.tri__phone {
  position: relative;
  width: 100%;
  aspect-ratio: 9 / 13.4;
  border-radius: clamp(14px, 2.4vw, 26px);
  background: var(--card);
  border: 1px solid var(--line);
  overflow: hidden;
  box-shadow:
    0 2px 4px oklch(21% 0.028 288 / 0.04),
    0 22px 44px -16px oklch(21% 0.028 288 / 0.18);
  padding: clamp(0.55rem, 1.4vw, 0.85rem);
  display: grid;
  align-content: start;
  gap: clamp(0.3rem, 0.8vw, 0.5rem);
}

.tri__cell[data-active="true"] .tri__phone {
  box-shadow:
    0 2px 4px oklch(21% 0.028 288 / 0.05),
    0 30px 60px -18px oklch(21% 0.028 288 / 0.3),
    0 0 0 4px var(--brand-wash);
}

/* A colour band across the top of each device, so the three read as three
   even at thumbnail size. */
.tri__phone::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  height: 5px;
  background: var(--brand);
}

.tri__label {
  display: grid;
  gap: 0.15rem;
  text-align: center;
}

.tri__label b {
  font-family: Gabarito, sans-serif;
  font-size: clamp(1rem, 0.9rem + 0.5vw, 1.3rem);
  font-weight: 800;
  letter-spacing: -0.03em;
  color: var(--brand);
}

.tri__label span {
  font-size: 0.82rem;
  color: var(--ink-3);
}

/* --------------------------------------------------------------------------
   11. Class finder — the interactive centrepiece
   -------------------------------------------------------------------------- */

.finder {
  display: grid;
  grid-template-columns: minmax(0, 14.5rem) minmax(0, 1fr);
  gap: clamp(1.25rem, 3vw, 2.5rem);
  align-items: start;
}

.filters {
  position: sticky;
  top: calc(var(--nav-h) + 1.25rem);
  display: grid;
  gap: 1.5rem;
  padding: 1.25rem;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r);
}

@media (max-width: 62rem) {
  /* Single column: filters on top as a wrapping row, results below. The
     two-column grid left the results column too narrow to hold the sort bar,
     which then overflowed the viewport. */
  .finder {
    grid-template-columns: minmax(0, 1fr);
  }

  .filters {
    position: static;
    grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  }

  /* The results list drops its own fixed-height inner scroll: a scroll box
     inside the page scroll traps a thumb on touch. It flows with the page
     instead, all classes visible, one scroll for everything. */
  .results {
    height: auto;
    overflow: visible;
    mask-image: none;
    margin: 0;
    padding: 0;
  }
}

.filters fieldset {
  border: 0;
  padding: 0;
  display: grid;
  gap: 0.6rem;
}

.filters legend {
  font-size: 0.7rem;
  font-weight: 800;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-3);
  padding: 0;
  margin-bottom: 0.3rem;
}

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}

.chip {
  padding: 0.35rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: transparent;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--ink-2);
  cursor: pointer;
  transition: transform 140ms var(--e-out), background-color 200ms var(--e-out),
    border-color 200ms var(--e-out), color 200ms var(--e-out);
}

.chip:active {
  transform: scale(0.95);
}

.chip[aria-pressed="true"] {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--card);
  font-weight: 700;
}

.rangerow {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  font-size: 0.8rem;
  color: var(--ink-3);
}

.rangerow output {
  color: var(--brand);
  font-weight: 800;
  font-family: Gabarito, sans-serif;
  font-variant-numeric: tabular-nums;
  transition: color var(--swap) var(--e-swap);
}

input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 3px;
  border-radius: 3px;
  background: var(--line);
  cursor: pointer;
}

input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 17px;
  height: 17px;
  border-radius: 50%;
  background: var(--brand);
  border: 2px solid var(--card);
  box-shadow: 0 1px 4px oklch(21% 0.028 288 / 0.25);
  transition: transform 160ms var(--e-out);
}

input[type="range"]::-moz-range-thumb {
  width: 17px;
  height: 17px;
  border-radius: 50%;
  background: var(--brand);
  border: 2px solid var(--card);
}

input[type="range"]:active::-webkit-slider-thumb {
  transform: scale(1.2);
}

.switchrow {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  font-size: 0.87rem;
  color: var(--ink-2);
}

.switch {
  position: relative;
  width: 38px;
  height: 21px;
  flex: none;
  border-radius: 999px;
  background: var(--line);
  border: 0;
  cursor: pointer;
  transition: background-color 220ms var(--e-out);
}

.switch::after {
  content: "";
  position: absolute;
  inset-block-start: 3px;
  inset-inline-start: 3px;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  background: var(--card);
  box-shadow: 0 1px 3px oklch(21% 0.028 288 / 0.28);
  transition: transform 260ms var(--e-drawer);
}

.switch[aria-checked="true"] {
  background: var(--brand);
}

.switch[aria-checked="true"]::after {
  transform: translateX(17px);
}

.resultsbar {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 1rem;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 1rem;
}

.resultsbar p {
  font-size: 0.87rem;
  color: var(--ink-2);
}

.resultsbar b {
  color: var(--brand);
  font-family: Gabarito, sans-serif;
  font-weight: 800;
  transition: color var(--swap) var(--e-swap);
}

.sort {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.85rem;
  color: var(--ink-3);
}

.sort select {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: 0.4rem 0.65rem;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  transition: border-color 200ms ease;
}

.sort select:hover {
  border-color: var(--brand-line);
}

/* The list scrolls inside its own box rather than running the page down for
   ten rows. That is what keeps the filter panel genuinely still: it has
   nothing left to travel against, so it reads as a fixed control surface
   with a live list beside it instead of a sticky thing chasing the scroll. */
.results {
  display: grid;
  gap: 0.5rem;
  align-content: start;
  height: clamp(22rem, 52vh, 30rem);
  overflow-y: auto;
  overscroll-behavior: contain;
  scroll-behavior: smooth;
  padding: 0.15rem 0.5rem 0.15rem 0.15rem;
  margin: -0.15rem -0.5rem -0.15rem -0.15rem;
  /* the last row fades out, so it is obvious there is more below */
  mask-image: linear-gradient(to bottom, #000 calc(100% - 2.5rem), transparent);
  scrollbar-width: thin;
  scrollbar-color: var(--brand-line) transparent;
}

.results::-webkit-scrollbar { width: 6px; }
.results::-webkit-scrollbar-thumb {
  background: var(--brand-line);
  border-radius: 99px;
}

/* A class listing is a row: what and for whom, what it is, what it costs.
   No teacher photo: a stock face says nothing a student is choosing on, and
   ten of them turn the list into a gallery. The subject and the grade are
   the two things being scanned for, so they take that column instead. */
.class {
  display: grid;
  grid-template-columns: 7.25rem minmax(0, 1fr) auto;
  gap: clamp(0.7rem, 1.8vw, 1.1rem);
  align-items: center;
  padding: 0.6rem 0.75rem;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r);
  transition: transform 250ms var(--e-out), border-color 250ms var(--e-out),
    box-shadow 250ms var(--e-out);
}

.class[hidden] {
  display: none;
}

.class__tag {
  display: grid;
  gap: 0.1rem;
  padding: 0.5rem 0.6rem;
  border-radius: var(--r-sm);
  background: var(--brand-wash);
  border: 1px solid var(--brand-line);
}

.class__tag b {
  font-family: Gabarito, sans-serif;
  font-size: 0.92rem;
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--brand);
  line-height: 1.15;
}

.class__tag span {
  font-size: 0.72rem;
  color: var(--ink-3);
  line-height: 1.2;
}

.class__top {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  flex-wrap: wrap;
  margin-bottom: 0.2rem;
}

.class__top h3 {
  font-size: 1rem;
}

.class__meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.2rem 0.85rem;
  font-size: 0.83rem;
  color: var(--ink-3);
}

.class__buy {
  display: grid;
  gap: 0.4rem;
  justify-items: end;
}

.class__fee {
  font-family: Gabarito, sans-serif;
  font-weight: 800;
  font-size: 1.15rem;
  letter-spacing: -0.035em;
  font-variant-numeric: tabular-nums;
}

.class__fee span {
  font-size: 0.7rem;
  font-weight: 600;
  color: var(--ink-3);
  letter-spacing: 0;
}

@media (max-width: 40rem) {
  .class {
    grid-template-columns: minmax(0, 1fr);
    row-gap: 0.6rem;
  }

  /* On a phone the tag reads better as one line above the title than as a
     narrow column squeezing the name into two. */
  .class__tag {
    grid-auto-flow: column;
    justify-content: start;
    align-items: baseline;
    gap: 0.45rem;
    padding: 0.3rem 0.55rem;
    justify-self: start;
  }

  .class__buy {
    grid-column: 1 / -1;
    grid-auto-flow: column;
    justify-items: start;
    align-items: center;
    justify-content: space-between;
    padding-top: 0.6rem;
    border-top: 1px solid var(--line-soft);
  }
}

.empty {
  padding: 2.5rem 1.25rem;
  text-align: center;
  border: 1px dashed var(--line);
  border-radius: var(--r);
  display: grid;
  justify-items: center;
  gap: 0.75rem;
}

.empty p {
  color: var(--ink-2);
  font-size: 0.92rem;
  max-width: 32ch;
}

/* --------------------------------------------------------------------------
   12. Checkout demo
   -------------------------------------------------------------------------- */

.checkout {
  display: grid;
  gap: 0.55rem;
}

.line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  font-size: 0.9rem;
}

.line dt {
  color: var(--ink-3);
}

.line dd {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.line--total {
  border-top: 1px solid var(--line);
  margin-top: 0.35rem;
  padding-top: 0.7rem;
  font-size: 1.05rem;
}

.line--total dd {
  color: var(--brand);
  font-family: Gabarito, sans-serif;
  font-weight: 800;
  transition: color var(--swap) var(--e-swap);
}

.methods {
  display: grid;
  gap: 0.4rem;
}

.method {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: transparent;
  font-size: 0.87rem;
  font-weight: 600;
  cursor: pointer;
  text-align: left;
  transition: transform 140ms var(--e-out), border-color 220ms var(--e-out),
    background-color 220ms var(--e-out);
}

.method:active {
  transform: scale(0.985);
}

.method[aria-pressed="true"] {
  border-color: var(--brand);
  background: var(--brand-wash);
}

.method__radio {
  width: 15px;
  height: 15px;
  border-radius: 50%;
  border: 1.5px solid var(--line);
  flex: none;
  display: grid;
  place-items: center;
  transition: border-color 220ms var(--e-out);
}

.method[aria-pressed="true"] .method__radio {
  border-color: var(--brand);
}

.method[aria-pressed="true"] .method__radio::after {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--brand);
}

.method__tag {
  margin-inline-start: auto;
  font-size: 0.68rem;
  font-weight: 700;
  color: var(--ink-3);
}

/* Hold to pay: slow and deliberate down, snappy back up */
.hold {
  position: relative;
  overflow: hidden;
  margin-top: 0.35rem;
  background: transparent;
  color: var(--brand);
  border-color: var(--brand);
}

.hold__fill {
  position: absolute;
  inset: 0;
  background: var(--brand);
  clip-path: inset(0 100% 0 0);
  transition: clip-path 200ms var(--e-out), background-color var(--swap) var(--e-swap);
}

.hold[data-holding="true"] .hold__fill {
  clip-path: inset(0 0 0 0);
  transition: clip-path 1300ms linear;
}

.hold[data-done="true"] .hold__fill {
  clip-path: inset(0 0 0 0);
  transition: clip-path 180ms var(--e-out);
}

.hold__label {
  position: relative;
  z-index: 1;
  transition: filter 180ms ease, opacity 180ms ease, color 180ms ease;
}

.hold[data-holding="true"] .hold__label,
.hold[data-done="true"] .hold__label {
  color: var(--card);
}

.hold[data-swapping="true"] .hold__label {
  filter: blur(4px);
  opacity: 0.55;
}

/* ==========================================================================
   Drenched band. The page was 95% near-white, so "one colour carries the
   surface" was never actually true. Payments is where the platform makes
   its promise about money, so that is where the colour commits.
   ========================================================================== */

.band--drench {
  background: var(--brand);
  color: var(--card);
  transition: background-color var(--swap) var(--e-swap);
  isolation: isolate;
}

/* A fine dot field, not a gradient wash: it gives the flat fill a surface
   without reading as an effect. */
.band--drench::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: radial-gradient(oklch(100% 0 0 / 0.13) 1px, transparent 1px);
  background-size: 18px 18px;
}

.band--drench .kicker,
.band--drench h2,
.band--drench h3 {
  color: var(--card);
}

.band--drench .muted {
  color: oklch(100% 0 0 / 0.78);
}

.band--drench .flow {
  background: oklch(100% 0 0 / 0.22);
  border-color: oklch(100% 0 0 / 0.22);
}

.band--drench .flow__step {
  background: var(--brand);
  color: var(--card);
}

.band--drench .flow__n {
  color: oklch(100% 0 0 / 0.72);
}

.band--drench .flow__step p {
  color: oklch(100% 0 0 / 0.8);
}

.band--drench .steps b {
  background: oklch(100% 0 0 / 0.2);
  color: var(--card);
}

.band--drench .steps span {
  color: oklch(100% 0 0 / 0.78);
}

/* The checkout card stays on the page's own surface, so it reads as the
   product sitting on top of the brand rather than dissolving into it. */
.band--drench .mock {
  color: var(--ink);
  box-shadow: 0 30px 70px -30px oklch(21% 0.028 288 / 0.55);
}

.band--drench .mock h3,
.band--drench .mock .ui-title {
  color: var(--ink);
}

/* Staggered rows: the list should assemble, not appear all at once */
.results [data-reveal] {
  transition-duration: 520ms;
}

/* --------------------------------------------------------------------------
   14. Pricing
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   14. One section, three role variants

   Deliberately three different objects rather than three sets of cards with
   the copy swapped. A parent is being asked to trust a stranger, so they get
   an attrition chart. A teacher is being shown a market, so they get a live
   feed and counters. A student is being shown themselves, so they get their
   own record moving. Same section slot, three arguments, three shapes.
   -------------------------------------------------------------------------- */

/* ---- Parent: how many are left at each step ---- */

.funnel {
  display: grid;
  gap: 0.4rem;
}

/* Each step is a button because each one opens. The bar is the argument and
   the number sits on it, so the row reads as a quantity before it reads as
   a list item. */
/* Three fixed tracks: label, rail, count. The bar used to be the row itself,
   so at 11% the last step was a stub with its number hanging off the end and
   nothing lined up down the column. Only the fill inside the rail varies now,
   which is what makes it read as a chart rather than five ragged blocks. */
.funnel__step {
  display: grid;
  grid-template-columns: minmax(9rem, 15rem) minmax(0, 1fr) 5.5rem;
  grid-template-areas:
    "what rail n"
    "why  why  why";
  align-items: center;
  gap: 0 clamp(0.85rem, 2vw, 1.6rem);
  padding: clamp(0.6rem, 1.4vw, 0.85rem) clamp(0.6rem, 1.5vw, 1rem);
  border: 0;
  border-radius: var(--r-sm);
  background: none;
  text-align: left;
  cursor: pointer;
  font: inherit;
  color: inherit;
  transition: background-color 260ms var(--e-out);
}

.funnel__rail {
  grid-area: rail;
  height: 0.7rem;
  border-radius: 99px;
  background: color-mix(in oklab, var(--brand) 8%, transparent);
  overflow: hidden;
}

.funnel__rail i {
  display: block;
  height: 100%;
  width: var(--w);
  border-radius: 99px;
  background: var(--brand-wash);
  transform-origin: left center;
  transform: scaleX(0);
}

/* Fills grow left to right as the section arrives, one after the other, so
   the drop from 1,842 to 214 is something you watch rather than read. */
[data-reveal].is-in .funnel__rail i {
  animation: funnel-grow 900ms cubic-bezier(0.16, 1, 0.3, 1) both;
  animation-delay: calc(var(--i) * 110ms);
}

@keyframes funnel-grow {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

.funnel__n {
  grid-area: n;
  font-family: Gabarito, sans-serif;
  font-size: clamp(1.25rem, 1rem + 0.9vw, 1.7rem);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.045em;
  font-variant-numeric: tabular-nums;
  color: var(--brand);
  text-align: right;
}

.funnel__what {
  grid-area: what;
  font-family: Gabarito, sans-serif;
  font-size: clamp(0.95rem, 0.9rem + 0.2vw, 1.08rem);
  font-weight: 700;
  letter-spacing: -0.02em;
}

/* Closed by default: the numbers carry the section, the detail is for the
   parent who wants to press on it. grid-template-rows animates cleanly;
   height does not. */
.funnel__why {
  grid-area: why;
  display: grid;
  grid-template-rows: 0fr;
  font-size: 0.87rem;
  line-height: 1.5;
  color: var(--ink-2);
  transition: grid-template-rows 340ms var(--e-out), opacity 240ms var(--e-out);
  opacity: 0;
}

.funnel__why > * { min-height: 0; }

.funnel__step[aria-expanded="true"] .funnel__why {
  grid-template-rows: 1fr;
  opacity: 1;
  padding-top: 0.4rem;
}

@media (hover: hover) and (pointer: fine) {
  .funnel__step:hover { background: color-mix(in oklab, var(--brand) 5%, transparent); }
}

.funnel__step:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* The last row is the answer to the headline, so its fill is solid brand
   rather than a wash and its count is the largest thing in the column. */
.funnel__step--end .funnel__rail i { background: var(--brand); }
.funnel__step--end .funnel__n { font-size: clamp(1.6rem, 1.2rem + 1.4vw, 2.2rem); }
.funnel__step--end .funnel__what { color: var(--brand); }

.funnel__seam {
  margin-top: clamp(1rem, 2.4vw, 1.6rem);
  max-width: 46rem;
  font-size: 0.86rem;
  line-height: 1.55;
  color: var(--ink-3);
}

/* ---- Teacher: the room is bigger than the town ----

   Rebuilt as one full-width object rather than a feed beside a column of
   numbers, which never balanced: the two halves had nothing to do with each
   other and neither could set the height. The argument is a quantity, so the
   quantity is drawn. 412 dots, one per ten students. Flip the switch and the
   field collapses to the four dots a room in one town reaches. The comparison
   is the section; everything else annotates it. */

.reach {
  display: grid;
  gap: clamp(1rem, 2.4vw, 1.6rem);
}

.reach__switch {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.reach__switch input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.reach__switch label {
  padding: 0.45rem 1rem;
  border-radius: 99px;
  border: 1px solid var(--line);
  background: var(--card);
  font-size: 0.88rem;
  font-weight: 600;
  cursor: pointer;
  transition:
    background-color 240ms var(--e-out),
    border-color 240ms var(--e-out),
    color 240ms var(--e-out);
}

.reach__switch input:checked + label {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--card);
}

.reach__switch input:focus-visible + label {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* auto-fill, so the field reflows to whatever width it is given and the dot
   count stays honest at every size instead of being a fixed grid that
   overflows on a phone. */
.reach__field {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(0.55rem, 1fr));
  gap: 0.32rem;
  padding: clamp(0.9rem, 2vw, 1.4rem);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r);
  box-shadow:
    0 1px 2px oklch(21% 0.028 288 / 0.04),
    0 18px 36px -26px oklch(21% 0.028 288 / 0.26);
}

.reach__field i {
  aspect-ratio: 1;
  border-radius: 99px;
  background: color-mix(in oklab, var(--brand) 9%, transparent);
  transform: scale(0.55);
  transition:
    background-color 420ms var(--e-out),
    transform 420ms var(--e-out);
  /* the wave runs outward from the first dot rather than all at once */
  transition-delay: calc(var(--n) * 1.6ms);
}

.reach__field i.is-lit {
  background: var(--brand);
  transform: scale(1);
}

/* The global 64ch cap on paragraphs is right for prose and wrong here: this
   is a figure with its unit written out, and at 76 characters the cap broke
   the number onto its own line. */
.reach__scale {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.45rem;
  max-width: none;
  font-size: 0.88rem;
  color: var(--ink-2);
}

.reach__scale b {
  font-family: Gabarito, sans-serif;
  font-size: clamp(1.5rem, 1.1rem + 1.2vw, 2.1rem);
  font-weight: 800;
  letter-spacing: -0.04em;
  font-variant-numeric: tabular-nums;
  color: var(--brand);
}

/* ---- The searches, running past ---- */

.reach__ticker {
  display: grid;
  gap: 0.55rem;
}

.reach__label {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  font-size: 0.7rem;
  font-weight: 800;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ink-3);
}

.reach__label::before {
  content: "";
  width: 0.45rem;
  height: 0.45rem;
  border-radius: 99px;
  background: var(--brand);
  animation: reach-pulse 2600ms ease-in-out infinite;
}

@keyframes reach-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: 0.35; transform: scale(0.8); }
}

/* A rail rather than a stacked list: the searches are ambient evidence, and
   running them sideways lets them be present without taking a column. */
.reach__rail {
  overflow: hidden;
  mask-image: linear-gradient(to right, transparent, #000 4rem, #000 calc(100% - 4rem), transparent);
}

.reach__run {
  display: flex;
  gap: 0.45rem;
  width: max-content;
  animation: reach-run 46s linear infinite;
}

.reach__rail:hover .reach__run { animation-play-state: paused; }

@keyframes reach-run {
  /* the list is duplicated, so travelling exactly half its width loops
     without a seam */
  to { transform: translateX(-50%); }
}

.reach__run span {
  display: inline-flex;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.45rem 0.85rem;
  border-radius: 99px;
  background: var(--brand-wash);
  font-size: 0.85rem;
  white-space: nowrap;
}

.reach__run em {
  font-style: normal;
  font-size: 0.76rem;
  color: var(--ink-3);
}

/* ---- The three counts, on one line ---- */

.reach__facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: clamp(0.9rem, 2.4vw, 2rem);
  padding-top: clamp(0.4rem, 1.4vw, 0.9rem);
}

.reach__fact {
  position: relative;
  display: grid;
  gap: 0.2rem;
  align-content: start;
  padding-top: 0.85rem;
}

.reach__fact::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 2px;
  background: var(--brand);
  transform: scaleX(0);
  transform-origin: left center;
}

[data-reveal].is-in .reach__fact::before {
  animation: reach-rule 700ms var(--e-out) both;
  animation-delay: calc(var(--r, 0) * 160ms + 300ms);
}

@keyframes reach-rule {
  to { transform: scaleX(1); }
}

.reach__num {
  font-family: Gabarito, sans-serif;
  font-size: clamp(1.9rem, 1.3rem + 1.6vw, 2.6rem);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.05em;
  font-variant-numeric: tabular-nums;
  color: var(--brand);
}

.reach__fact span {
  font-size: 0.87rem;
  line-height: 1.4;
  color: var(--ink-2);
}

@media (prefers-reduced-motion: reduce) {
  .reach__label::before,
  .reach__run { animation: none; }
  .reach__field i { transition-delay: 0ms; }
  .reach__fact::before { animation: none; transform: scaleX(1); }
}

/* ---- Student: the gap between then and now ---- */

.growth {
  display: grid;
  gap: clamp(1rem, 2.4vw, 1.6rem);
}

.growth__pick {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.growth__pick input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.growth__pick label {
  padding: 0.45rem 0.95rem;
  border-radius: 99px;
  border: 1px solid var(--line);
  background: var(--card);
  font-size: 0.88rem;
  font-weight: 600;
  cursor: pointer;
  transition:
    background-color 240ms var(--e-out),
    border-color 240ms var(--e-out),
    color 240ms var(--e-out);
}

.growth__pick input:checked + label {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--card);
}

.growth__pick input:focus-visible + label {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

.growth__panels {
  display: grid;
}

.growth__panel {
  grid-area: 1 / 1;
  display: grid;
  gap: 0.75rem;
  align-content: start;
  opacity: 0;
  pointer-events: none;
  transition: opacity 220ms var(--e-out);
}

.growth:has(input:nth-of-type(1):checked) .growth__panel:nth-child(1),
.growth:has(input:nth-of-type(2):checked) .growth__panel:nth-child(2),
.growth:has(input:nth-of-type(3):checked) .growth__panel:nth-child(3) {
  opacity: 1;
  pointer-events: auto;
}

.growth__row {
  display: grid;
  grid-template-columns: minmax(7rem, 12rem) minmax(0, 1fr) 3rem;
  align-items: center;
  gap: clamp(0.7rem, 2vw, 1.4rem);
  font-size: 0.9rem;
}

.growth__row b {
  font-weight: 600;
  color: var(--ink-2);
}

/* Two marks on one rail: where you were, where you are. The distance
   between them is the only thing this section is trying to say, so it is
   the only thing drawn. */
.growth__track {
  position: relative;
  height: 0.55rem;
  border-radius: 99px;
  background: var(--line-soft, var(--brand-wash));
}

.growth__track::before,
.growth__track::after {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  border-radius: 99px;
}

/* then: a tick standing where you were, not a fill underneath. As a fill it
   was painted over by the current bar the instant it grew past it, which
   erased the one measurement the section exists to show. */
.growth__track::before {
  left: var(--then);
  width: 2px;
  inset-block: -0.22rem;
  margin-left: -1px;
  border-radius: 1px;
  background: color-mix(in oklab, var(--brand) 55%, transparent);
  z-index: 2;
}

/* now: grows out of it */
.growth__track::after {
  width: 0;
  background: var(--brand);
}

/* Scoped to the panel that is actually showing. An animation starts when its
   selector begins to match, so switching subject replays the fill instead of
   revealing three bars that already finished growing off screen. */
.growth:has(input:nth-of-type(1):checked) .growth__panel:nth-child(1) .growth__track::after,
.growth:has(input:nth-of-type(2):checked) .growth__panel:nth-child(2) .growth__track::after,
.growth:has(input:nth-of-type(3):checked) .growth__panel:nth-child(3) .growth__track::after {
  animation: growth-fill 820ms cubic-bezier(0.16, 1, 0.3, 1) both;
  animation-delay: calc(var(--i) * 90ms);
}

@keyframes growth-fill {
  from { width: var(--then); }
  to   { width: var(--now); }
}

.growth__row i {
  font-style: normal;
  font-family: Gabarito, sans-serif;
  font-weight: 800;
  font-size: 0.92rem;
  color: var(--brand);
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.growth__said {
  margin-top: 0.35rem;
  max-width: 48rem;
  font-size: 0.88rem;
  line-height: 1.55;
  color: var(--ink-2);
}

.growth__key {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.78rem;
  color: var(--ink-3);
}

.growth__dot {
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 99px;
  display: inline-block;
}

/* The key mirrors the marks: a tick for then, a round dot for now. */
.growth__dot--then {
  width: 2px;
  height: 0.85rem;
  border-radius: 1px;
  background: color-mix(in oklab, var(--brand) 55%, transparent);
}

.growth__dot--now { background: var(--brand); margin-left: 0.9rem; }

@media (max-width: 52rem) {
  .reach { grid-template-columns: minmax(0, 1fr); }
  .funnel__step { grid-template-columns: 4.75rem minmax(0, 1fr); }
  .growth__row { grid-template-columns: minmax(0, 1fr) 3rem; }
  .growth__track { grid-column: 1 / -1; }
}

@media (prefers-reduced-motion: reduce) {
  [data-reveal].is-in .funnel__bar { animation: none; transform: scaleX(var(--w-num)); }
  .growth__track::after { animation: none !important; width: var(--now); }
  .reach__list li { animation: none; }
}

/* --------------------------------------------------------------------------
   15. Testimonials
   -------------------------------------------------------------------------- */

/* Reviews. Three across, quote first, attribution under it, a hairline over
   each one and nothing boxed. The photos are gone for good: nobody's actual
   review comes with a headshot, and three of them read as marketing before
   you have read a word.

   The four-star ones stay. A page of unanimous fives reads as copywriting;
   one specific complaint, answered in the same sentence by the person making
   it, buys more credibility than the other two combined. */

.reviews {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: clamp(1.25rem, 3vw, 2.75rem);
  align-items: start;
}

.review {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  margin: 0;
  padding-top: clamp(0.9rem, 2vw, 1.2rem);
  border-top: 2px solid var(--brand-line);
}

.review blockquote {
  order: -1;
  margin: 0;
  font-family: Gabarito, sans-serif;
  font-size: clamp(0.98rem, 0.92rem + 0.3vw, 1.18rem);
  line-height: 1.38;
  letter-spacing: -0.022em;
}

.review blockquote::before { content: "“"; }
.review blockquote::after { content: "”"; }

.review__who {
  display: grid;
  gap: 0.15rem;
  margin-top: auto;
}

.review__who b {
  font-family: Gabarito, sans-serif;
  font-size: 0.95rem;
  font-weight: 800;
  letter-spacing: -0.025em;
}

.review__who span {
  font-size: 0.78rem;
  line-height: 1.35;
  color: var(--ink-3);
}

.review__stars {
  margin-top: 0.15rem;
  font-size: 0.78rem;
  letter-spacing: 0.14em;
  color: var(--brand);
}

/* Quieter, not hidden, which is roughly how a four-star review reads. */
.review__stars--four {
  color: color-mix(in oklab, var(--brand) 62%, transparent);
}

/* --------------------------------------------------------------------------
   16. FAQ
   -------------------------------------------------------------------------- */

/* The heading holds its own column beside the questions rather than sitting
   in a band above a narrow list with half the page empty to its right. */
.faqwrap {
  display: grid;
  grid-template-columns: minmax(0, 0.78fr) minmax(0, 1.22fr);
  gap: clamp(1.5rem, 4vw, 4.5rem);
  align-items: start;
}

.faqwrap__aside {
  position: sticky;
  top: calc(var(--nav-h) + 2.5rem);
  display: grid;
  gap: 0.85rem;
  justify-items: start;
}

.faqwrap__aside h2 {
  font-size: clamp(2rem, 1.3rem + 2.4vw, 3.2rem);
}

.faqwrap__aside .muted {
  max-width: 26ch;
}

@media (max-width: 56rem) {
  .faqwrap {
    grid-template-columns: minmax(0, 1fr);
  }

  .faqwrap__aside {
    position: static;
    margin-bottom: clamp(1.25rem, 4vw, 2rem);
  }
}

/* Each question is its own surface with space between, and the open one
   lifts and tints. The old flat run of hairlines gave the answer no edges,
   so an open item bled into the two below it. */
.faq {
  display: grid;
  gap: 0.5rem;
  max-width: none;
  border-top: 0;
}

.faq__item {
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--card);
  transition:
    border-color 300ms var(--e-out),
    background-color 300ms var(--e-out),
    box-shadow 300ms var(--e-out);
}

.faq__item:has(.faq__q[aria-expanded="true"]) {
  border-color: var(--brand-line);
  background: var(--brand-wash);
  box-shadow: 0 18px 34px -26px color-mix(in oklab, var(--brand) 60%, transparent);
}

@media (hover: hover) and (pointer: fine) {
  .faq__item:hover { border-color: var(--brand-line); }
}

.faq__q {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.25rem;
  padding: clamp(0.95rem, 2vw, 1.15rem) clamp(1rem, 2.2vw, 1.35rem);
  background: none;
  border: 0;
  text-align: left;
  font-family: Gabarito, sans-serif;
  font-size: clamp(1rem, 0.95rem + 0.3vw, 1.15rem);
  font-weight: 700;
  letter-spacing: -0.025em;
  color: inherit;
  cursor: pointer;
  transition: color 200ms ease;
}

.faq__q[aria-expanded="true"] { color: var(--brand); }

.faq__q:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: -2px;
  border-radius: var(--r);
}

/* A ringed plus rather than a bare one: it reads as a control, and it gives
   the open state something to fill. */
.faq__icon {
  flex: none;
  width: 1.65rem;
  height: 1.65rem;
  position: relative;
  border-radius: 99px;
  border: 1px solid var(--brand-line);
  color: var(--brand);
  transition:
    background-color 300ms var(--e-out),
    border-color 300ms var(--e-out),
    color 300ms var(--e-out);
}

.faq__q[aria-expanded="true"] .faq__icon {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--card);
}

.faq__icon::before,
.faq__icon::after {
  content: "";
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 50%;
  width: 0.7rem;
  height: 2px;
  margin-block-start: -1px;
  margin-inline-start: -0.35rem;
  border-radius: 2px;
  background: currentColor;
  transition: transform 300ms var(--e-out);
}

.faq__icon::after {
  transform: rotate(90deg);
}

.faq__q[aria-expanded="true"] .faq__icon::after {
  transform: rotate(0deg);
}

.faq__a {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 360ms var(--e-out);
}

.faq__q[aria-expanded="true"] + .faq__a {
  grid-template-rows: 1fr;
}

.faq__a > div {
  overflow: hidden;
}

.faq__a p {
  padding: 0 clamp(1rem, 2.2vw, 1.35rem) clamp(1rem, 2vw, 1.2rem);
  font-size: 0.92rem;
  line-height: 1.55;
  color: var(--ink-2);
}

/* --------------------------------------------------------------------------
   17. Closing CTA
   -------------------------------------------------------------------------- */

.close {
  background: var(--brand);
  color: var(--card);
  border-radius: clamp(16px, 3vw, 28px);
  padding: clamp(2.25rem, 6vw, 4.5rem) clamp(1.25rem, 4vw, 3rem);
  display: grid;
  justify-items: center;
  text-align: center;
  gap: 1.25rem;
  transition: background-color var(--swap) var(--e-swap);
}

.close h2 {
  max-width: 17ch;
}

.close p {
  color: oklch(100% 0 0 / 0.82);
  max-width: 42ch;
}

.close .btn {
  background: var(--card);
  color: var(--brand);
}

.signup {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  width: min(30rem, 100%);
}

.signup input[type="email"] {
  flex: 1 1 13rem;
  min-height: 2.9rem;
  padding: 0.75rem 1rem;
  border-radius: 999px;
  border: 1px solid oklch(100% 0 0 / 0.35);
  background: oklch(100% 0 0 / 0.12);
  color: var(--card);
  transition: border-color 200ms ease, background-color 200ms ease;
}

.signup input[type="email"]::placeholder {
  color: oklch(100% 0 0 / 0.6);
}

.signup input[type="email"]:focus {
  outline: none;
  border-color: var(--card);
  background: oklch(100% 0 0 / 0.2);
}

.signup input[aria-invalid="true"] {
  border-color: oklch(92% 0.12 30);
}

.formmsg {
  min-height: 1.3rem;
  font-size: 0.85rem;
  color: oklch(100% 0 0 / 0.78);
}

/* Until there is a message it reserves no space, so it does not leave a band
   of dead colour under the button. It grows back the moment a status lands. */
.formmsg:empty {
  min-height: 0;
}

.formmsg[data-tone="bad"] {
  color: oklch(93% 0.11 30);
}

.spinner {
  width: 13px;
  height: 13px;
  border: 2px solid var(--brand-wash);
  border-top-color: var(--brand);
  border-radius: 50%;
  animation: spin 540ms linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

/* --------------------------------------------------------------------------
   18. Footer
   -------------------------------------------------------------------------- */

.footer {
  padding-block: clamp(2.5rem, 5vh, 4rem) 1.75rem;
  border-top: 1px solid var(--line);
  margin-top: clamp(3rem, 7vh, 5rem);
}

.footer__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.3fr) repeat(auto-fit, minmax(8.5rem, 1fr));
  gap: clamp(1.5rem, 3.5vw, 2.75rem);
  padding-bottom: 2.5rem;
}

.footer h4 {
  font-size: 0.72rem;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ink-3);
  font-weight: 800;
  margin-bottom: 0.85rem;
}

.footer ul {
  display: grid;
  gap: 0.55rem;
  font-size: 0.9rem;
  color: var(--ink-2);
}

.footer a {
  text-decoration: none;
  transition: color 180ms ease;
}

.footer__base {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 1rem;
  justify-content: space-between;
  align-items: center;
  padding-top: 1.5rem;
  border-top: 1px solid var(--line);
  font-size: 0.82rem;
  color: var(--ink-3);
}

/* --------------------------------------------------------------------------
   19. Reveal
   -------------------------------------------------------------------------- */

[data-reveal] {
  opacity: 0;
  transform: translate3d(0, 16px, 0);
  transition: opacity 640ms var(--e-out), transform 640ms var(--e-out);
  transition-delay: var(--d, 0ms);
}

[data-reveal].is-in {
  opacity: 1;
  transform: none;
}

/* --------------------------------------------------------------------------
   20. Reduced motion — fewer and gentler, not zero
   -------------------------------------------------------------------------- */

/* ==========================================================================
   CINEMATIC ACTS

   Each .act is a tall scroll region containing one sticky stage. JS writes a
   single custom property --p (0 to 1) describing how far through the act the
   viewport has travelled; every transform below is a calc() off that number.

   Why not CSS animation-timeline: it is still missing in Firefox and landed
   only recently in Safari, and this is the page's primary structure rather
   than a flourish. One rAF-throttled handler writing one property is
   supported everywhere and stays on transform and opacity throughout.
   ========================================================================== */

.act {
  --p: 0;
  position: relative;
  height: calc(var(--len, 260) * 1vh);
}

.act__pin {
  position: sticky;
  top: var(--nav-h);
  height: calc(100svh - var(--nav-h));
  display: grid;
  align-content: center;
  overflow: hidden;
}

/* Nothing pins when there is no room to pin into */
@media (max-height: 34rem), (max-width: 46rem) {
  /* absolute against a static pin would land it on the content */
  .a1__cue {
    display: none;
  }

  .act {
    height: auto;
  }

  .act__pin {
    position: static;
    height: auto;
    padding-block: clamp(3rem, 9vh, 5rem);
    overflow: visible;
  }

  /* The cue is absolute against the pin. Once the pin is static it anchors to
     the act instead and lands on the content, so it goes. */
  
/* A lit segment travelling down a rail, rather than a line blinking on and
   off. Continuous motion reads as "keep going"; a blink reads as an error. */



/* A lit segment running down a rail. Continuous travel reads as "keep
   going"; the old version faded a whole line in and out, which reads as a
   fault. The rail is faint so the runner is the only thing with weight. */
.a1__cue i {
  position: relative;
  /* flex: none — as a flex item with no content it was collapsing to zero
     height regardless of the height set below, which is why the rail never
     drew. */
  flex: none;
  display: block;
  width: 3px;
  height: 3.2rem;
  border-radius: 3px;
  overflow: hidden;
  background: color-mix(in oklab, var(--brand) 26%, transparent);
}

.a1__cue i::after {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  height: 40%;
  border-radius: 3px;
  background: var(--brand);
  animation: cue-run 1.9s linear infinite;
}

/* linear, and the travel starts and ends only just off the rail: with an
   ease the runner parks at the extremes, which are out of sight, so it is
   missing for most of the cycle. */
@keyframes cue-run {
  0% { transform: translateY(-55%); }
  100% { transform: translateY(160%); }
}


  .a1__centre {
    position: static;
    transform: none;
    opacity: 1;
  }

  .a1__mark {
    position: static;
    transform: none;
    margin-top: 2.5rem;
  }

  .a1__word {
    font-size: clamp(3rem, 17vw, 8rem);
  }

  .a1__word span {
    transform: none;
    opacity: 0.22;
  }

  .a1__slogan {
    opacity: 1;
    transform: none;
    letter-spacing: 0.32em;
    text-indent: 0.32em;
  }

  .a2__fan {
    height: auto;
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 0.75rem;
    width: 100%;
  }

  .a2__cell {
    position: static;
    width: auto;
    transform: none;
  }

  .a2__cap {
    opacity: 1;
  }

  .a2__head,
  .a3__track {
    transform: none;
    opacity: 1;
  }

  .a3__track {
    flex-wrap: wrap;
    width: auto;
  }

  .a4__wire path {
    stroke-dashoffset: 0;
  }

  .flow__step {
    opacity: 1;
  }
}

/* ---- Act 1 ------------------------------------------------------------
   Two things move against each other. The centre block rises and clears;
   the name rides up from its crop at the bottom edge into the middle,
   letter by letter, and only then does the slogan arrive under it.
   ---------------------------------------------------------------------- */

.a1 .act__pin {
  display: block;
  position: sticky;
  /* starts at the very top: the bar floats over it */
  top: 0;
  height: 100svh;
  /* Quiet vertical depth. The previous radial read as a red smear parked on
     the right-hand side, which is exactly what it looked like. */
  background:
    radial-gradient(ellipse 130% 78% at 50% 120%, var(--brand-wash), transparent 60%),
    linear-gradient(var(--page), var(--page-2));
  transition: background var(--swap) var(--e-swap);
}

/* Fine dot field, cleared through the middle so the headline sits on
   empty ground. One full tile per cycle, so the loop is invisible. */

/* ---- Line reveal ------------------------------------------------------
   Each line sits clipped below its own baseline and wipes up into place.
   The clip is inset past the box on the vertical so ascenders and
   descenders are never shaved during the travel.
   ---------------------------------------------------------------------- */

.a1__centre .ln {
  display: block;
  clip-path: inset(-25% 0 100% 0);
  transform: translate3d(0, 0.42em, 0);
  transition:
    clip-path 640ms var(--e-out) var(--d, 0ms),
    transform 640ms var(--e-out) var(--d, 0ms);
  will-change: clip-path, transform;
}

.a1__centre .ln.is-shown {
  clip-path: inset(-25% 0 -25% 0);
  transform: none;
}

/* the button row is a flex line, so it keeps its own display */
.a1__actions.ln {
  display: flex;
}

.a1__proof.ln {
  display: grid;
}

/* ==========================================================================
   Transparent liquid glass
   Genuinely see-through: the fill is 14%, so the wordmark and the drifting
   field read straight through it. At that transparency the panel would
   disappear, so its edges have to do the work instead of its surface:

     saturate()  keeps colour behind it from washing out to grey
     sheen       a light fall across the top half, as on a curved surface
     rim         bright at two points, dim between: light catching an edge
   ========================================================================== */

.glass {
  position: relative;
  isolation: isolate;
  background-color: color-mix(in oklab, var(--card) 14%, transparent);
  background-image: linear-gradient(
    to bottom,
    oklch(100% 0 0 / 0.3) 0%,
    oklch(100% 0 0 / 0.06) 48%,
    oklch(100% 0 0 / 0.12) 100%
  );
  -webkit-backdrop-filter: blur(20px) saturate(200%) brightness(1.04);
  backdrop-filter: blur(20px) saturate(200%) brightness(1.04);
  border: 0;
  box-shadow:
    inset 0 1px 0 oklch(100% 0 0 / 0.7),
    inset 0 -1px 0 oklch(100% 0 0 / 0.25),
    0 12px 30px -16px oklch(21% 0.028 288 / 0.3);
}

/* The rim, drawn by masking a 1px-padded box against itself so the gradient
   only survives in the border area. The only way to get a gradient border
   on a fully rounded pill. */
.glass::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  pointer-events: none;
  background: linear-gradient(
    135deg,
    oklch(100% 0 0 / 0.95) 0%,
    oklch(100% 0 0 / 0.2) 28%,
    oklch(100% 0 0 / 0.08) 62%,
    oklch(100% 0 0 / 0.9) 100%
  );
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask-composite: exclude;
}

/* Text sits above the sheen */
.glass > * {
  position: relative;
  z-index: 1;
}

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .glass {
    background-color: color-mix(in oklab, var(--card) 88%, transparent);
  }
}


/* ==========================================================================
   CLOUDS  (standalone)
   Delete .a1__clouds from index.html and this whole block to remove. Nothing
   else references it.

   Each cloud is one element whose background is four overlapping soft
   ellipses, so the silhouette is lumpy rather than a single oval, then
   blurred once. They cross the hero horizontally on long, mismatched loops.
   ========================================================================== */

.a1__clouds {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

.a1__clouds i {
  position: absolute;
  inset-block-start: var(--top);
  inset-inline-start: 0;
  display: block;
  width: var(--w);
  height: var(--h);
  opacity: var(--o, 0.5);
  mix-blend-mode: overlay;
  background:
    radial-gradient(closest-side, oklch(100% 0 0) 0%, transparent 100%) 12% 62% / 42% 66% no-repeat,
    radial-gradient(closest-side, oklch(100% 0 0) 0%, transparent 100%) 38% 30% / 46% 84% no-repeat,
    radial-gradient(closest-side, oklch(100% 0 0) 0%, transparent 100%) 66% 56% / 40% 72% no-repeat,
    radial-gradient(closest-side, oklch(100% 0 0) 0%, transparent 100%) 88% 38% / 30% 58% no-repeat;
  filter: blur(14px);
  will-change: transform;
  animation: cloud-drift var(--dur) linear infinite;
  animation-delay: var(--t, 0s);
}

@keyframes cloud-drift {
  from { transform: translate3d(-60%, 0, 0); }
  to { transform: translate3d(160vw, 0, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .a1__clouds i {
    animation: none;
    transform: translate3d(20vw, 0, 0);
  }
}

/* ---- Hero composition -------------------------------------------------
   Five parts, each with its own measure so the block reads as a tapered
   column rather than five things of similar width stacked up.
   ---------------------------------------------------------------------- */

.a1__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.6rem 1.5rem;
  margin-bottom: clamp(2.1rem, 4.6vh, 3.1rem);
}

.a1__alt {
  font-size: 0.92rem;
}

/* Three claims, evenly weighted, on one hairline. Value on top, qualifier
   under it, so the row scans as three facts rather than a sentence. */
/* Glass, and for once earned: a giant wordmark and a drifting colour field
   pass behind this row, so there is something real to refract. Over a flat
   background the same treatment would just be fog. */
.a1__proof {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(0.75rem, 2.5vw, 2.25rem);
  width: min(40rem, 100%);
  margin-inline: auto;
  padding: clamp(0.85rem, 2vh, 1.15rem) clamp(1rem, 3vw, 1.9rem);
  border-radius: 999px;
  transition: background-color var(--swap) var(--e-swap);
}

/* Without backdrop-filter the panel must carry its own opacity or the text
   sits on whatever happens to be behind it. */

.a1__proof li {
  display: grid;
  gap: 0.15rem;
  text-align: center;
}

.a1__proof b {
  font-family: Gabarito, sans-serif;
  font-size: clamp(0.95rem, 0.88rem + 0.3vw, 1.15rem);
  font-weight: 800;
  letter-spacing: -0.025em;
  color: var(--brand);
  transition: color var(--swap) var(--e-swap);
}

.a1__proof span {
  font-size: 0.78rem;
  line-height: 1.3;
  color: var(--ink-3);
}

/* The name plus its slogan, held as one centred block. Inset from the edges
   rather than bleeding, so it reads as an object and not a background. */

/* Each letter carries its own index, so later letters lag slightly and the
   name lands as a wave rather than a single block. Translate and opacity
   only, nothing per-letter that would cost a repaint. */

/* Arrives last, once the name is home */
/* Set in Poppins as tracked caps, echoing the ACADEMY line under the logo
   mark. A second face here stops the slogan reading as a caption of the
   wordmark and lets it stand as its own line. */

/* ---- Act 1 ------------------------------------------------------------
   Two things move against each other. The centre block rises and clears;
   the name rides up from its crop at the bottom edge into the middle,
   letter by letter, and only then does the slogan arrive under it.
   ---------------------------------------------------------------------- */

/* Fine dot field, cleared through the middle so the headline sits on
   empty ground. One full tile per cycle, so the loop is invisible. */

/* The name plus its slogan, held as one centred block. Inset from the edges
   rather than bleeding, so it reads as an object and not a background. */

/* Each letter carries its own index, so later letters lag slightly and the
   name lands as a wave rather than a single block. Translate and opacity
   only, nothing per-letter that would cost a repaint. */

/* Arrives last, once the name is home */

/* Word by word, each a beat behind the last, once the name is nearly home */

  

  

  

  

  

  

  

  

  .a2__head,
  .a3__track {
    transform: none;
    opacity: 1;
  }

  

  

  

/* ---- Act 1 ------------------------------------------------------------
   Two things move against each other. The centre block rises and clears;
   the name rides up from its crop at the bottom edge into the middle,
   letter by letter, and only then does the slogan arrive under it.
   ---------------------------------------------------------------------- */

/* Fine dot field, cleared through the middle so the headline sits on
   empty ground. One full tile per cycle, so the loop is invisible. */

/* The name plus its slogan, held as one centred block. Inset from the edges
   rather than bleeding, so it reads as an object and not a background. */

/* Each letter carries its own index, so later letters lag slightly and the
   name lands as a wave rather than a single block. Translate and opacity
   only, nothing per-letter that would cost a repaint. */

/* Arrives last, once the name is home */

/* Word by word, each a beat behind the last, once the name is nearly home */

/* Full-bleed connection field. A booking is a signal travelling to a
   teacher; the pulses run the paths and land on the nodes. */

/* ---- Act 1 ------------------------------------------------------------
   Two things move against each other. The centre block rises and clears;
   the name rides up from its crop at the bottom edge into the middle,
   letter by letter, and only then does the slogan arrive under it.
   ---------------------------------------------------------------------- */

.a1 {
  --len: 240;
}

/* Fine dot field, cleared through the middle so the headline sits on empty
   ground. Travels one full tile per cycle, so the loop is invisible. */

.a1__centre {
  position: absolute;
  inset-inline: var(--gutter);
  inset-block-start: 50%;
  z-index: 2;
  display: grid;
  justify-items: center;
  text-align: center;
  /* Not one uniform gap. Badge sits close to the headline, the lead breathes,
     the actions get a clear step, and the proof row is set apart. */
  gap: 0;
  /* Sits above true centre so the cropped name has room below */
  transform: translate3d(0, calc(-66% + var(--p) * -26vh), 0);
  /* The bar floats over the hero, so keep the block out from under it */
  padding-block-start: var(--nav-h);
  opacity: calc(1 - var(--p) * 1.7);
  will-change: transform, opacity;
}

.a1__badge {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 0.9rem 0.4rem 0.7rem;
  border-radius: 999px;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--ink-2);
}

.a1__badge i {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--brand);
  transition: background-color var(--swap) var(--e-swap);
}

.a1 h1 {
  max-width: 17ch;
  margin-inline: auto;
}

.a1 h1 em {
  display: block;
  font-style: normal;
  color: var(--brand);
  transition: color var(--swap) var(--e-swap);
}

.a1 .lead {
  max-width: 46ch;
  margin-inline: auto;
}

.btn--lg {
  padding: 1rem 1.9rem;
  min-height: 3.2rem;
  font-size: 1rem;
}

/* The name plus its slogan, held as one centred block. Inset from the edges
   rather than bleeding, so it reads as an object and not a background. */
.a1__mark {
  position: absolute;
  inset-inline: var(--gutter);
  inset-block-start: 50%;
  z-index: 1;
  text-align: center;
  pointer-events: none;
  transform: translate3d(0, calc(-50% - 5px), 0) scale(calc(0.965 + var(--p) * 0.035));
}

.a1__word {
  margin: 0;
  font-family: Gabarito, sans-serif;
  font-weight: 800;
  font-size: clamp(4rem, 21.5vw, 18.5rem);
  line-height: 0.82;
  letter-spacing: -0.055em;
  color: var(--brand);
  white-space: nowrap;
  transition: color var(--swap) var(--e-swap);
}

/* Each letter carries its own index. The amplitude peaks half way and
   returns to zero, so the word is dead level at rest and level again once
   home: the wave only exists while it is travelling. */
.a1__word span {
  display: inline-block;
  transform: translate3d(
    0,
    calc((1 - var(--p)) * 52vh + var(--p) * (1 - var(--p)) * var(--i) * 3.4vh),
    0
  );
  opacity: calc(0.38 + var(--p) * 0.3);
  will-change: transform, opacity;
}

/* Set in Poppins as tracked caps, echoing the ACADEMY line under the logo
   mark. A second face stops it reading as a caption of the wordmark and
   lets it stand as its own line. */
.a1__slogan {
  margin: clamp(1.5rem, 4.5vh, 3rem) auto 0;
  font-family: Poppins, sans-serif;
  font-size: clamp(0.68rem, 0.6rem + 0.32vw, 0.9rem);
  font-weight: 500;
  text-transform: uppercase;
  color: var(--brand);
  /* Tracking settles inward as it arrives: one smooth reveal rather than a
     word-by-word pop, which read as choppy. text-indent cancels the trailing
     space tracking adds, so the line stays optically centred. */
  letter-spacing: calc(0.62em - var(--p) * 0.3em);
  text-indent: calc(0.62em - var(--p) * 0.3em);
  opacity: calc((var(--p) - 0.5) * 2.1);
  transform: translate3d(0, calc((1 - var(--p)) * 1.6vh), 0);
  will-change: opacity, transform;
  transition: color var(--swap) var(--e-swap);
}

/* A lit segment travelling down a rail, rather than a line blinking on and
   off. Continuous motion reads as "keep going"; a blink reads as an error. */



.a1__cue {
  position: absolute;
  inset-block-end: clamp(0.85rem, 3vh, 1.75rem);
  /* Bottom-left, not centre: the cropped name owns the bottom middle */
  inset-inline-start: var(--gutter);
  z-index: 3;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.4rem;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-2);
  opacity: calc(1 - var(--p) * 3);
}

@keyframes cue {
  0%,
  100% {
    transform: scaleY(0.4);
    transform-origin: top;
    opacity: 0.4;
  }
  50% {
    transform: scaleY(1);
    transform-origin: top;
    opacity: 1;
  }
}

/* ---- Ambient field -----------------------------------------------------
   Three soft colour bodies drifting on long, mismatched loops, plus a slow
   rise of fine particles. Built from radial-gradients rather than blurred
   elements, so there is no filter cost, and every loop is a different prime
   length so the field never visibly repeats.
   ---------------------------------------------------------------------- */

.a1__amb {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

.a1__amb i {
  position: absolute;
  display: block;
  background: var(--brand-wash);
  /* A radial fill fades before the edge, so a border-radius morph would be
     invisible. Solid fill plus one bounded blur is what makes the silhouette
     actually read as liquid. Blur is static; only transform and radius move. */
  filter: blur(56px);
  will-change: transform, border-radius;
  transition: background-color var(--swap) var(--e-swap);
}

@keyframes flow-a {
  to { transform: translate3d(14%, 9%, 0) scale(1.18); }
}

@keyframes flow-b {
  to { transform: translate3d(-12%, 14%, 0) scale(0.86); }
}

@keyframes flow-c {
  to { transform: translate3d(9%, -11%, 0) scale(1.12); }
}

/* ---- Liquid blobs, grain and sweep ------------------------------------
   Three bodies drift and morph on mismatched loops, so the silhouette is
   never the same twice. Over them, a fine grain gives the flat colour a
   surface, and a wide band of light crosses now and then.

   To drop either layer, delete its element in index.html: .a1__sweep for
   the light pass, .a1__grain for the texture. Nothing else depends on them.
   ---------------------------------------------------------------------- */

@keyframes morph-a {
  0% { border-radius: 62% 38% 46% 54% / 55% 44% 56% 45%; }
  50% { border-radius: 38% 62% 63% 37% / 41% 59% 41% 59%; }
  100% { border-radius: 55% 45% 34% 66% / 62% 38% 62% 38%; }
}

@keyframes morph-b {
  0% { border-radius: 44% 56% 61% 39% / 43% 57% 43% 57%; }
  50% { border-radius: 66% 34% 39% 61% / 58% 41% 59% 42%; }
  100% { border-radius: 36% 64% 55% 45% / 49% 52% 48% 51%; }
}

/* ---- Bokeh, in three depth planes -------------------------------------
   Orbs rise on a continuous loop with negative delays, so the field is
   already mid-flight on load and each one recycles unseen: it reads as new
   orbs arriving rather than a loop restarting.

   Depth is consistent across four properties at once. Nearer means larger,
   faster, more opaque, softer-focused, and further-travelling under the
   pointer. Getting all five to agree is what sells the space.
   ---------------------------------------------------------------------- */

.a1__amb b {
  position: absolute;
  inset-block-end: -14%;
  inset-inline-start: var(--x);
  width: var(--r);
  aspect-ratio: 1;
  margin-inline-start: calc(var(--r) / -2);
  border-radius: 50%;
  background: radial-gradient(
    circle at 36% 32%,
    var(--brand) 0%,
    var(--brand-line) 42%,
    transparent 72%
  );
  opacity: 0;
  will-change: transform, opacity;
  animation: float-up var(--d) linear infinite;
  animation-delay: var(--t);
  transition: background var(--swap) var(--e-swap);
}

@keyframes float-up {
  0% { transform: translate3d(0, 0, 0); opacity: 0; }
  10% { opacity: var(--o, 0.3); }
  88% { opacity: var(--o, 0.3); }
  100% { transform: translate3d(var(--drift, 2vw), -128vh, 0); opacity: 0; }
}

/* far */
.a1__amb b.d1 {
  --o: 0.16;
  --drift: 1vw;
  filter: blur(0.5px);
  translate: calc(var(--mx, 0) * 10px) calc(var(--my, 0) * 7px);
}

/* mid */
.a1__amb b.d2 {
  --o: 0.3;
  --drift: 2.5vw;
  filter: blur(1.5px);
  translate: calc(var(--mx, 0) * 26px) calc(var(--my, 0) * 17px);
}

/* near, and softest: the closest thing to the lens is the least in focus */
.a1__amb b.d3 {
  --o: 0.42;
  --drift: 4vw;
  filter: blur(3.5px);
  translate: calc(var(--mx, 0) * 46px) calc(var(--my, 0) * 31px);
}

/* ---- Pointer parallax --------------------------------------------------
   JS writes --mx / --my (-1..1) on the act, spring-smoothed. Depth is
   applied with the `translate` property rather than `transform`, so it
   composes with the drift animations already running on transform instead
   of fighting them for the same slot.
   ---------------------------------------------------------------------- */

.a1__amb i:nth-of-type(1) {
  translate: calc(var(--mx, 0) * 34px) calc(var(--my, 0) * 22px);
}

.a1__amb i:nth-of-type(2) {
  translate: calc(var(--mx, 0) * -26px) calc(var(--my, 0) * 17px);
}

.a1__amb i:nth-of-type(3) {
  translate: calc(var(--mx, 0) * 48px) calc(var(--my, 0) * 33px);
}

/* ---- Act 2: three apps fan apart ---- */

.a2 {
  --len: 300;
}

.a2__stage {
  display: grid;
  gap: clamp(1.25rem, 3.5vh, 2.5rem);
  justify-items: center;
  padding-inline: var(--gutter);
}

.a2__head {
  text-align: center;
  /* justify-items:center shrink-wraps grid children, so this needs an
     explicit width or the heading collapses to a narrow column. */
  width: min(46rem, 100%);
  display: grid;
  justify-items: center;
  gap: 0.75rem;
  /* Recedes rather than disappearing: it is still the section title */
  opacity: calc(1 - var(--p) * 0.45);
  transform: translate3d(0, calc(var(--p) * -3vh), 0);
}

.a2__head h2 {
  font-size: clamp(1.9rem, 1.2rem + 2.6vw, 3.4rem);
}

.a2__fan {
  position: relative;
  display: grid;
  place-items: center;
  width: min(52rem, 92vw);
  height: clamp(18rem, 44vh, 26rem);
}

/* Stacked at the start, spread by the end. Each cell carries its own target
   offset, so one formula moves all three without keyframes. */
.a2__cell {
  position: absolute;
  width: clamp(8.5rem, 14vw, 11.5rem);
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
  transform:
    translate3d(calc(var(--dx) * var(--p)), calc(var(--dy) * var(--p)), 0)
    rotate(calc(var(--rot) * var(--p)))
    scale(calc(0.88 + 0.12 * var(--p)));
  z-index: var(--z, 1);
  will-change: transform;
}

.a2__cap {
  display: grid;
  gap: 0.1rem;
  text-align: center;
  margin-top: 0.85rem;
  opacity: calc((var(--p) - 0.55) * 3);
}

.a2__cap b {
  font-family: Gabarito, sans-serif;
  font-size: clamp(0.92rem, 0.85rem + 0.35vw, 1.15rem);
  font-weight: 800;
  letter-spacing: -0.03em;
  color: var(--brand);
}

.a2__cap span {
  font-size: 0.76rem;
  color: var(--ink-3);
}

/* ---- Act 3: the catalogue runs sideways ---- */

.a3 {
  --len: 320;
}

.a3__pin {
  align-content: center;
  gap: clamp(0.9rem, 3vh, 1.75rem);
}

.a3__head {
  padding-inline: var(--gutter);
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem 2rem;
  align-items: baseline;
  justify-content: space-between;
}

.a3__track {
  display: flex;
  gap: clamp(0.75rem, 1.6vw, 1.25rem);
  padding-inline: var(--gutter);
  width: max-content;
  transform: translate3d(calc(var(--reel-x, 0px) * var(--p) * -1), 0, 0);
  will-change: transform;
}

.a3__card {
  width: clamp(14rem, 22vw, 18rem);
  flex: none;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r);
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  display: grid;
  align-content: start;
  transition: border-color 260ms var(--e-out), transform 260ms var(--e-out);
}

@media (hover: hover) and (pointer: fine) {
  .a3__card:hover {
    border-color: var(--brand-line);
    transform: translateY(-3px);
  }
}

.a3__shot {
  position: relative;
  aspect-ratio: 5 / 3.4;
  overflow: hidden;
  background: var(--page-2);
}

.a3__shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 22%;
}

.a3__shot .ui-pill {
  position: absolute;
  inset-block-start: 0.6rem;
  inset-inline-start: 0.6rem;
  background: var(--card);
  color: var(--brand);
  box-shadow: 0 2px 8px oklch(21% 0.028 288 / 0.22);
}

.a3__body {
  display: grid;
  gap: 0.28rem;
  padding: clamp(0.7rem, 1.4vw, 0.95rem);
}

.a3__body b {
  font-family: Gabarito, sans-serif;
  font-size: 1rem;
  font-weight: 800;
  letter-spacing: -0.028em;
  line-height: 1.15;
}

.a3__body span {
  font-size: 0.78rem;
  color: var(--ink-3);
}

.a3__fee {
  display: flex;
  align-items: baseline;
  gap: 0.35rem;
  margin-top: 0.35rem;
  padding-top: 0.5rem;
  border-top: 1px solid var(--line-soft);
  font-family: Gabarito, sans-serif;
  font-size: 1.1rem;
  font-weight: 800;
  letter-spacing: -0.035em;
  color: var(--brand);
  font-variant-numeric: tabular-nums;
}

.a3__fee em {
  font-family: Manrope, sans-serif;
  font-style: normal;
  font-size: 0.72rem;
  font-weight: 500;
  color: var(--ink-3);
  letter-spacing: 0;
}

.a3__bar {
  height: 2px;
  margin-inline: var(--gutter);
  background: var(--line);
  border-radius: 2px;
  overflow: hidden;
}

.a3__bar i {
  display: block;
  height: 100%;
  background: var(--brand);
  transform: scaleX(var(--p));
  transform-origin: left;
}

/* ---- Act 4: the money draws its own path ---- */

.a4 {
  --len: 300;
  background: var(--brand);
  color: var(--card);
  transition: background-color var(--swap) var(--e-swap);
}

.a4__pin {
  gap: clamp(1.1rem, 3.5vh, 2.25rem);
  padding-inline: var(--gutter);
}

.a4 h2,
.a4 .kicker {
  color: var(--card);
}

.a4__lead {
  color: oklch(100% 0 0 / 0.78);
  max-width: 46ch;
}

.a4__wire {
  width: 100%;
  height: clamp(3rem, 8vh, 6rem);
  overflow: visible;
}

.a4__wire path {
  fill: none;
  stroke: oklch(100% 0 0 / 0.85);
  stroke-width: 2;
  /* butt, not round: a round cap leaves a dot floating when fully undrawn */
  stroke-linecap: butt;
  stroke-dasharray: 1000;
  /* The line draws itself as the act advances */
  stroke-dashoffset: calc(1000 * (1 - var(--p)));
}

.flow {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1px;
  background: oklch(100% 0 0 / 0.22);
  border: 1px solid oklch(100% 0 0 / 0.22);
  border-radius: var(--r);
  overflow: hidden;
}

/* The three role acts each hold their own .a4__body, so the pin's grid gap
   moves down a level with them. */
.a4__body {
  display: grid;
  gap: clamp(1.1rem, 3.5vh, 2.25rem);
}

.flow__step {
  background: var(--brand);
  padding: clamp(0.85rem, 1.9vw, 1.4rem);
  display: grid;
  align-content: start;
  gap: 0.4rem;
  transition: opacity 520ms var(--e-out), background-color 520ms var(--e-out);
}

/* Lit from --p in CSS rather than a data attribute set by JS. With three
   flows in the document the old JS counted all twelve steps as one run and
   lit them a third of the way in. Each step now reads the scroll itself. */
.flow__step:nth-child(1) { --lit: clamp(0, calc((var(--p) - 0.04) / 0.10), 1); }
.flow__step:nth-child(2) { --lit: clamp(0, calc((var(--p) - 0.24) / 0.10), 1); }
.flow__step:nth-child(3) { --lit: clamp(0, calc((var(--p) - 0.44) / 0.10), 1); }
.flow__step:nth-child(4) { --lit: clamp(0, calc((var(--p) - 0.64) / 0.10), 1); }

.flow__step {
  opacity: calc(0.34 + var(--lit, 1) * 0.66);
  background: color-mix(
    in oklab,
    oklch(100% 0 0 / 0.12) calc(var(--lit, 1) * 100%),
    var(--brand)
  );
}

.flow__n {
  font-family: Gabarito, sans-serif;
  font-size: 0.7rem;
  font-weight: 800;
  color: oklch(100% 0 0 / 0.72);
}

.flow__step h3 {
  color: var(--card);
  font-size: clamp(0.98rem, 0.9rem + 0.35vw, 1.2rem);
}

.flow__step p {
  font-size: 0.84rem;
  color: oklch(100% 0 0 / 0.8);
}

@media (max-width: 56rem) {
  .flow {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* ---- Reduced motion: the acts stop scrubbing and simply lay out ---- */

/* ---- Act 1 atmosphere -------------------------------------------------
   Two ambient layers plus the connection lines. The dot field is the same
   motif as the payments act, so the atmosphere is a recurrence rather than
   a new decoration. Both loops run at different lengths, so they never
   resync and the background never visibly repeats.
   ---------------------------------------------------------------------- */

.a1 .act__pin::after {
  content: "";
  position: absolute;
  inset: -6%;
  pointer-events: none;
  background-image: radial-gradient(var(--brand-line) 1px, transparent 1px);
  background-size: 34px 34px;
  opacity: 0.22;
  /* Cleared through the middle so the headline sits on empty ground */
  -webkit-mask-image: radial-gradient(ellipse 54% 50% at 50% 44%, transparent 30%, #000 88%);
  mask-image: radial-gradient(ellipse 54% 50% at 50% 44%, transparent 30%, #000 88%);
  animation: a1-drift 38s linear infinite;
}

@keyframes a1-drift {
  to { transform: translate3d(30px, 30px, 0); }
}

/* Connection lines: a booking is a signal travelling to a teacher, so the
   pulses run the paths and land on the nodes. pathLength=1000 on every path
   lets three different curves share one keyframe and stay in step. */

.a1__stage {
  position: relative;
  z-index: 1;
}

@media (prefers-reduced-motion: reduce) {
  .a1 .act__pin::before,
  .a1 .act__pin::after {
    animation: none;
  }
}

/* ==========================================================================
   ROLE-SPECIFIC CONTENT

   The switch does not just recolour: most of the page is different depending
   on who signed in. A parent never asked to search the catalogue, and a
   teacher does not care what a class costs to buy. Only payments, pricing,
   FAQ and the sign-up are common to all three.

   Hidden with display so nothing role-irrelevant is in the accessibility
   tree, keyboard order or find-in-page.
   ========================================================================== */

[data-for] {
  display: none;
}

[data-role="parent"] [data-for~="parent"],
[data-role="student"] [data-for~="student"],
[data-role="teacher"] [data-for~="teacher"] {
  display: block;
  animation: role-in 620ms var(--e-swap) both;
}

@keyframes role-in {
  from {
    opacity: 0;
    transform: translate3d(0, 14px, 0);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* A bloom of the incoming colour washes the whole page once on switch.
   It is the only full-screen effect on the site, and it lasts 700ms. */
.bloom {
  position: fixed;
  inset: 0;
  z-index: 90;
  pointer-events: none;
  opacity: 0;
  background:
    radial-gradient(ellipse 80% 60% at 82% 4%, var(--brand), transparent 62%),
    radial-gradient(ellipse 70% 60% at 10% 96%, var(--brand), transparent 62%);
}

[data-switching] .bloom {
  animation: bloom 700ms var(--e-swap);
}

@keyframes bloom {
  0% { opacity: 0; }
  38% { opacity: 0.16; }
  100% { opacity: 0; }
}

/* ---- Role feature block ---- */

.rolework {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
  gap: clamp(1.75rem, 4.5vw, 4rem);
  align-items: center;
}

.rolework--flip > :first-child {
  order: 2;
}

@media (max-width: 60rem) {
  .rolework {
    grid-template-columns: 1fr;
  }

  .rolework--flip > :first-child {
    order: 0;
  }
}

.rolework__copy {
  display: grid;
  gap: 1rem;
  justify-items: start;
}

/* Attendance register: the parent-only view */
.register {
  display: grid;
  gap: 2px;
}

.register__row {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: 0.75rem;
  align-items: center;
  padding: 0.6rem 0.7rem;
  border-radius: var(--r-sm);
  background: var(--page-2);
  font-size: 0.87rem;
  transition: background-color var(--swap) var(--e-swap);
}

.register__row b {
  font-weight: 700;
}

.register__row time {
  color: var(--ink-3);
  font-size: 0.8rem;
  font-variant-numeric: tabular-nums;
}

.tick {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.72rem;
  font-weight: 800;
  padding: 0.18rem 0.5rem;
  border-radius: 999px;
  white-space: nowrap;
}

.tick--in {
  background: oklch(58% 0.15 152 / 0.14);
  color: var(--ok);
}

.tick--late {
  background: oklch(78% 0.135 85 / 0.2);
  color: oklch(52% 0.13 75);
}

.tick--miss {
  background: oklch(58% 0.19 25 / 0.13);
  color: var(--bad);
}

/* Fee dial: the teacher-only view */
.dial {
  display: grid;
  gap: 0.9rem;
}

.dial__out {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  font-family: Gabarito, sans-serif;
  font-weight: 800;
  letter-spacing: -0.045em;
  font-variant-numeric: tabular-nums;
}

.dial__fee {
  font-size: clamp(2.2rem, 5vw, 3.2rem);
  color: var(--brand);
  transition: color var(--swap) var(--e-swap);
}

.dial__unit {
  font-size: 0.85rem;
  font-weight: 500;
  font-family: Manrope, sans-serif;
  letter-spacing: 0;
  color: var(--ink-3);
}

.dial__split {
  display: grid;
  gap: 0.5rem;
  padding-top: 0.85rem;
  border-top: 1px solid var(--line);
}

/* --------------------------------------------------------------------------
   Scrollbar
   Themed to the live role colour, and thin enough to read as part of the
   page rather than a system chrome bar bolted to the edge.
   -------------------------------------------------------------------------- */

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scrollbar-width: thin;
  scrollbar-color: var(--brand-line) transparent;
  /* Same as the body, so no white edge shows through at a sub-pixel seam or
     when the page rubber-bands past its ends on a phone. */
  background: var(--page);
}

::-webkit-scrollbar {
  width: 11px;
  height: 11px;
}

::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background-color: var(--brand-line);
  border-radius: 999px;
  /* Transparent border with background-clip keeps the thumb from touching
     the edges, so it reads as a floating pill rather than a filled gutter. */
  border: 3px solid transparent;
  background-clip: content-box;
  transition: background-color var(--swap) var(--e-swap);
}

::-webkit-scrollbar-thumb:hover {
  background-color: var(--brand);
}

::-webkit-scrollbar-corner {
  background: transparent;
}

/* The horizontal rail inside the catalogue reel keeps its own quiet bar */
.a3__rail {
  scrollbar-width: none;
}

.a3__rail::-webkit-scrollbar {
  display: none;
}

/* The generic line-reveal rule sets display:block, which flattens any line
   that is itself a layout. Restate the layouts after it. */
.a1__centre .a1__proof.ln {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.a1__centre .a1__proof li {
  display: grid;
  gap: 0.15rem;
  text-align: center;
}

.a1__centre .a1__actions.ln {
  display: flex;
}

@media (max-width: 34rem) {
  .a1__centre .a1__proof.ln {
    grid-template-columns: 1fr;
  }
}

/* ==========================================================================
   Hero spacing and scroll cue — stated last so they win on order.
   Earlier dedupe passes kept the older copies of these selectors, which is
   why the margins and the cue rail kept disappearing.
   ========================================================================== */

.a1__badge {
  margin-bottom: clamp(1.5rem, 3.2vh, 2.2rem);
}

.a1 h1 {
  margin-bottom: clamp(1.35rem, 3vh, 1.9rem);
}

.a1 .lead {
  margin-bottom: clamp(2rem, 4.4vh, 3rem);
}

.a1__actions {
  margin-bottom: clamp(2.1rem, 4.6vh, 3.1rem);
}

/* The rail is a flex item with no content: without flex:none it collapses
   to zero height whatever height is set. */
.a1__cue i {
  flex: none;
  position: relative;
  display: block;
  width: 3px;
  height: 3.2rem;
  border-radius: 3px;
  overflow: hidden;
  background: color-mix(in oklab, var(--brand) 30%, transparent);
}

.a1__cue i::after {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  height: 40%;
  border-radius: 3px;
  background: var(--brand);
  animation: cue-run 1.9s linear infinite;
}

/* ==========================================================================
   Hero proportion pass
   The block was three unrelated sizes stacked: an enormous headline, a much
   smaller lead, then small everything. Pulling the headline down and the
   lead up closes that gap, and every element now shares one width envelope
   so the column reads as a single shape instead of wide-narrow-wide.
   ========================================================================== */

.a1 h1 {
  font-size: clamp(2.3rem, 1.1rem + 3.5vw, 4.3rem);
  line-height: 0.96;
  letter-spacing: -0.042em;
  max-width: 18ch;
  margin-bottom: clamp(1.1rem, 2.4vh, 1.6rem);
}

/* Lead sized up so the step down from the headline is a step, not a cliff */
.a1 .lead {
  font-size: clamp(1.02rem, 0.95rem + 0.5vw, 1.28rem);
  line-height: 1.55;
  max-width: 44ch;
  margin-bottom: clamp(1.7rem, 3.6vh, 2.4rem);
}

.a1__badge {
  margin-bottom: clamp(1.2rem, 2.6vh, 1.7rem);
}

.a1__actions {
  gap: 0.6rem 1.75rem;
  margin-bottom: clamp(1.7rem, 3.6vh, 2.4rem);
}

/* One width envelope: the claims panel now matches the lead's measure
   rather than sitting narrower than the text above it. */
.a1__proof {
  width: min(46rem, 100%);
  padding: clamp(0.95rem, 2.2vh, 1.3rem) clamp(1.1rem, 3vw, 2.1rem);
}

.a1__proof b {
  font-size: clamp(0.98rem, 0.9rem + 0.35vw, 1.2rem);
}

.a1__proof span {
  font-size: 0.8rem;
}

/* ---- Interaction ------------------------------------------------------- */

/* Each claim is its own hit area: hovering lifts it and brings the divider
   forward, so the row is something you can pick through rather than a label. */
.a1__proof li {
  position: relative;
  padding-block: 0.15rem;
  border-radius: 10px;
  transition: transform 260ms var(--e-out);
}

.a1__proof li + li::before {
  content: "";
  position: absolute;
  inset-block: 12%;
  inset-inline-start: calc(clamp(0.75rem, 2.5vw, 2.25rem) / -2);
  width: 1px;
  background: color-mix(in oklab, var(--brand) 22%, transparent);
}

@media (hover: hover) and (pointer: fine) {
  .a1__proof li:hover {
    transform: translateY(-2px);
  }

  .a1__proof li:hover b {
    color: var(--brand-hi);
  }
}

/* The primary action carries a little more presence than a default button */
.a1 .btn--lg {
  padding: 1.05rem 2.1rem;
  font-size: 1.02rem;
  box-shadow: 0 10px 24px -12px var(--brand);
  transition: transform 180ms var(--e-out), box-shadow 260ms var(--e-out),
    background-color var(--swap) var(--e-swap);
}

@media (hover: hover) and (pointer: fine) {
  .a1 .btn--lg:hover {
    transform: translateY(-2px);
    box-shadow: 0 16px 32px -12px var(--brand);
  }
}

.a1 .btn--lg:active {
  transform: translateY(0) scale(0.98);
}

/* The live dot earns its place: a slow breath, not a blink */
.a1__badge i {
  animation: badge-beat 2.8s var(--e-in-out) infinite;
}

@keyframes badge-beat {
  0%, 100% { transform: scale(1); opacity: 0.85; }
  50% { transform: scale(1.35); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .a1__badge i {
    animation: none;
  }
}

@media (max-width: 34rem) {
  .a1__proof li + li::before {
    display: none;
  }
}

/* ==========================================================================
   Hero layout — left-aligned
   Centred stacked everything on one axis, which is why it read as static and
   why nothing seemed proportional: five centred rows of different widths
   have no shared edge to measure against. Left-aligning gives them one, and
   sets the block against the centred wordmark below instead of echoing it.
   ========================================================================== */

.a1__centre {
  inset-inline-start: var(--gutter);
  inset-inline-end: auto;
  width: min(44rem, calc(100% - var(--gutter) * 2));
  justify-items: start;
  text-align: left;
  /* sits just above the optical centre, clear of the bar */
  transform: translate3d(0, calc(-52% + var(--p) * -26vh), 0);
  padding-block-start: 0;
}

.a1 h1,
.a1 .lead {
  margin-inline: 0;
}

.a1 h1 {
  max-width: 15ch;
}

.a1 .lead {
  max-width: 42ch;
}

.a1__actions {
  justify-content: flex-start;
}

/* The claims sit under the column, aligned to the same left edge, so the
   whole block shares one spine. */
.a1__proof {
  width: 100%;
  margin-inline: 0;
  border-radius: 16px;
  text-align: left;
}

.a1__proof li {
  text-align: left;
}

.a1__proof li + li::before {
  inset-block: 8%;
}

@media (max-width: 60rem) {
  .a1__centre {
    inset-inline: var(--gutter);
    width: auto;
  }
}

@media (max-width: 34rem) {
  .a1__proof {
    border-radius: 18px;
  }
}

/* Headline wants two lines, not three: 15ch was breaking "See that /
   it happened." Widening the measure lets each clause hold its own line. */
.a1 h1 {
  max-width: 19ch;
}

.a1 .lead {
  max-width: 46ch;
  margin-bottom: clamp(2rem, 4.2vh, 2.8rem);
}

.a1__actions {
  margin-bottom: clamp(2.1rem, 4.4vh, 2.9rem);
}

.a1__proof {
  width: min(34rem, 100%);
}

/* the wordmark was riding up into the claims panel */
.a1__word span {
  transform: translate3d(
    0,
    calc((1 - var(--p)) * 62vh + var(--p) * (1 - var(--p)) * var(--i) * 3.4vh),
    0
  );
}

/* ==========================================================================
   Hero back to centred
   Keeps the proportion work from the left-aligned pass — headline measure,
   the lead sized up so the step down is a step, the even vertical rhythm,
   the wordmark clearance — and puts the axis back in the middle.
   ========================================================================== */

.a1__centre {
  inset-inline: var(--gutter);
  width: auto;
  justify-items: center;
  text-align: center;
  transform: translate3d(0, calc(-52% + var(--p) * -26vh), 0);
}

.a1 h1 {
  max-width: 19ch;
  margin-inline: auto;
}

.a1 .lead {
  max-width: 46ch;
  margin-inline: auto;
}

.a1__actions {
  justify-content: center;
}

.a1__proof {
  width: min(44rem, 100%);
  margin-inline: auto;
  text-align: center;
  border-radius: 999px;
}

.a1__proof li {
  text-align: center;
}

.a1__proof li + li::before {
  inset-block: 12%;
}

@media (max-width: 34rem) {
  .a1__proof {
    border-radius: 20px;
  }
}

/* ==========================================================================
   Final hero adjustments
   ========================================================================== */

/* Claims: smaller across the board so the row supports the headline rather
   than competing with it. */
.a1__proof {
  width: min(36rem, 100%);
  padding: clamp(0.7rem, 1.6vh, 0.95rem) clamp(0.9rem, 2.4vw, 1.6rem);
}

.a1__proof b {
  font-size: clamp(0.84rem, 0.79rem + 0.22vw, 0.98rem);
}

.a1__proof span {
  font-size: 0.7rem;
  line-height: 1.25;
}

.a1__proof li + li::before {
  inset-block: 16%;
}

/* The wordmark carries the bottom of the hero, so it needs to be read, not
   just sensed. */
.a1__word span {
  opacity: calc(0.56 + var(--p) * 0.26);
}

/* The rail above SCROLL is gone; the label stands alone. */
.a1__cue i,
.a1__cue i::after {
  display: none;
}

.a1__cue {
  gap: 0;
}

/* The wordmark was only peeking over the bottom edge. Raising the rest
   position so most of it is in frame from the start, while still leaving it
   somewhere to travel as the act scrubs. */
.a1__word span {
  transform: translate3d(
    0,
    calc((1 - var(--p)) * 49vh + var(--p) * (1 - var(--p)) * var(--i) * 3.4vh),
    0
  );
}

/* ==========================================================================
   Clearing the wordmark
   The word is where it should be, so the column moves instead: lifted a
   little, and its internal gaps tightened, so the claims panel clears the
   letters without the badge riding up under the bar.
   ========================================================================== */

.a1__centre {
  transform: translate3d(0, calc(-57% + var(--p) * -26vh), 0);
}

.a1 .lead {
  margin-bottom: clamp(1.5rem, 3vh, 2rem);
}

.a1__actions {
  margin-bottom: clamp(1.6rem, 3.2vh, 2.1rem);
}

/* ==========================================================================
   ACT 2 — one app, three people
   Three handsets at a size you could hold, each running a different screen,
   all three role colours on stage at once. The section fills the viewport
   and the phones rise on their own beats as the act scrubs.
   ========================================================================== */

.a2 {
  /* Between the two extremes: 460 was a lot of wheel, 210 crossed all three
     phases too quickly. The transitions carry the smoothness, this only sets
     how much scroll the sequence occupies. */
  --len: 300;
}

.a2__pin {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: clamp(0.9rem, 2.6vh, 2rem);
  padding-block: clamp(1rem, 3.5vh, 2.25rem);
  padding-inline: var(--gutter);
}

.a2__head {
  display: grid;
  justify-items: center;
  gap: 0.55rem;
  text-align: center;
  width: min(52rem, 100%);
  margin-inline: auto;
  opacity: calc(1 - var(--p) * 0.45);
}

/* The heading was 4.1rem over a 1rem paragraph: no step between them, just a
   drop. Pulling the heading down and the paragraph up puts them in the same
   family, which is what reads as proportion. */
/* The heading has to be the widest thing here. It was 16ch against a 44ch
   paragraph, so the supporting line was the biggest shape on screen and the
   hierarchy read backwards. Heading wider, paragraph narrower than it. */
.a2__head h2 {
  font-size: clamp(1.7rem, 1.05rem + 2.2vw, 3rem);
  line-height: 1.02;
  max-width: 22ch;
  margin-inline: auto;
}

/* One line, not a block. Three lines of body text under a one-line heading
   is a bigger shape than the heading however narrow you make it. */
.a2__head .muted {
  font-size: clamp(0.9rem, 0.87rem + 0.2vw, 1.02rem);
  line-height: 1.4;
  max-width: none;
  text-wrap: nowrap;
}

@media (max-width: 42rem) {
  .a2__head .muted {
    text-wrap: pretty;
    max-width: 30ch;
  }
}

/* ---- The stage ---------------------------------------------------------
   One handset, three screens stacked on the same spot. --p drives which one
   is up and what colour the device is wearing, so the animation is the claim
   being demonstrated rather than a flourish laid over it.

   --a and --b are how far the scroll sits into each handover, 0 to 1. They
   overlap nothing: at any moment at most one is mid-flight.
   -------------------------------------------------------------------------- */

/* Three phases cut from the act's --p. They overlap on purpose: the split
   begins before the screens have finished going dark, so it reads as one
   move rather than three steps waiting on each other. */
.a2__stage {
  /* Raw linear ramps. Used only as input to the eased values below: a bare
     clamp starts and stops dead, which is what made the move feel choppy. */
  --tsplit: clamp(0, calc((var(--p) - 0.16) / 0.42), 1);
  --tboot: clamp(0, calc((var(--p) - 0.66) / 0.26), 1);

  /* Smootherstep, 6t^5-15t^4+10t^3. Smoothstep's second derivative jumps at
     both ends, which is what you feel as a catch when the phones start and
     stop. This one is continuous there, so the move sets off and settles
     without a seam. */
  --split: calc(
    var(--tsplit) * var(--tsplit) * var(--tsplit) *
    (var(--tsplit) * (var(--tsplit) * 6 - 15) + 10)
  );
  --boot: calc(
    var(--tboot) * var(--tboot) * var(--tboot) *
    (var(--tboot) * (var(--tboot) * 6 - 15) + 10)
  );

  position: relative;
  height: 100%;
  min-height: 0;
}

/* All three sit on the same spot, so at rest they read as one handset. */
.a2__unit {
  position: absolute;
  inset-block: 0;
  /* nudged right of true centre */
  inset-inline-start: calc(50% + 30px);
  width: min(19rem, 26vw);
  transform: translateX(-50%);
  will-change: transform, opacity;
  /* The scroll value updates every frame, so without this the phones track
     the wheel exactly and a fast flick throws them across instantly. The
     transition damps it: they always take the same time to travel, however
     hard you scroll. */
  transition:
    transform 1150ms var(--e-out),
    opacity 900ms var(--e-out);
}

/* The outer two ride out from behind the centre one as --split advances */
.a2__unit[data-step="0"] {
  transform: translateX(calc(-50% - var(--split) * 122%));
  opacity: var(--split);
  z-index: 1;
}

.a2__unit[data-step="1"] {
  z-index: 3;
}

.a2__unit[data-step="2"] {
  transform: translateX(calc(-50% + var(--split) * 122%));
  opacity: var(--split);
  z-index: 1;
}

/* The centre handset is the one you see at rest, so it inherits the live
   theme and follows the role switch. The outer two hold the other two role
   colours, so the split still reveals three. */
.a2__unit[data-step="0"] { --brand: oklch(46% 0.155 301); }
.a2__unit[data-step="2"] { --brand: oklch(49% 0.183 264); }

[data-role="student"] .a2__unit[data-step="0"] { --brand: oklch(51% 0.196 13); }
[data-role="teacher"] .a2__unit[data-step="2"] { --brand: oklch(51% 0.196 13); }

.a2__unit {
  --brand-wash: color-mix(in oklab, var(--brand) 10%, transparent);
  --brand-line: color-mix(in oklab, var(--brand) 28%, transparent);
}

/* ---- Power state ---- */

/* The splash. Up from the start, so the single handset is a loading screen
   rather than a live app, and it holds through the split. It only clears
   once the boot ramp runs, which is when the real screens arrive. */
.iphone__splash {
  position: absolute;
  /* Inset by the bezel and carrying the screen's radius, so the handset
     still reads as a handset: dark frame around it, island on top of it.
     z-index sits under the island (3) and over the screen (auto). */
  inset: clamp(0.25rem, 0.6vw, 0.5rem);
  z-index: 2;
  border-radius: clamp(1.15rem, 2.5vw, 2.2rem);
  /* Each handset splashes in its own role colour, so the split reveals three
     colours rather than three identical black rectangles. */
  background: var(--brand);
  opacity: calc(1 - var(--boot));
  pointer-events: none;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 5cqw;
  /* same damping as the handsets: the boot always takes its own time */
  transition:
    opacity 900ms var(--e-out),
    background-color var(--swap) var(--e-swap);
}

.iphone__boot {
  width: 26%;
  aspect-ratio: 392 / 343;
  background-color: oklch(100% 0 0);
  -webkit-mask: url("../logo/mark.png") no-repeat center / contain;
  mask: url("../logo/mark.png") no-repeat center / contain;
  /* breathes gently while it waits, the way a splash does */
  animation: splash-breathe 2.6s var(--e-in-out) infinite;
}

@keyframes splash-breathe {
  0%,
  100% {
    opacity: 0.72;
  }
  50% {
    opacity: 1;
  }
}

/* Sized in cqw like everything else on the glass, so the lockup holds its
   proportion to the handset at any size instead of being set in rem and
   drifting as the phone scales. */
.iphone__name {
  display: grid;
  justify-items: center;
  gap: 1.4cqw;
  font-family: Poppins, sans-serif;
  font-size: 7.4cqw;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-indent: 0.16em;
  text-transform: uppercase;
  color: oklch(100% 0 0);
  line-height: 1;
}

.iphone__name em {
  font-style: normal;
  font-size: 4cqw;
  font-weight: 500;
  letter-spacing: 0.42em;
  text-indent: 0.42em;
  color: oklch(100% 0 0 / 0.72);
}

/* ---- Screen ---- */

.scr {
  position: absolute;
  inset: 0;
  padding: 5cqw;
  padding-block-start: 14cqw;
  display: grid;
  align-content: start;
  /* wider than the gap inside a card, so cards read as separate objects */
  gap: 4cqw;
  /* arrives with the boot, a beat behind the splash lifting */
  opacity: var(--boot);
  transition: opacity 950ms var(--e-out) 90ms;
}

@media (max-width: 62rem) {
  .a2__stage {
    --split: 1;
    --dim: 0;
    --boot: 1;
    display: grid;
    /* minmax(0,1fr), not 1fr: with the acts static the handsets are no longer
       clipped by a pin, so a content-based column floor let three height-sized
       phones push the row wider than the tablet and scroll the page. Letting
       the columns shrink, and sizing the phone off its column width instead of
       a 100svh height, makes three-across fit. */
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 0.6rem;
    height: auto;
  }

  .a2__unit .iphone {
    height: auto;
  }

  .a2__unit,
  .a2__unit[data-step="0"],
  .a2__unit[data-step="1"],
  .a2__unit[data-step="2"] {
    position: static;
    transform: none;
    opacity: 1;
    width: 100%;
  }

  .a2__unit[data-step="0"] { order: 1; }
  .a2__unit[data-step="1"] { order: 2; }
  .a2__unit[data-step="2"] { order: 3; }
}

@media (prefers-reduced-motion: reduce) {
  .a2__stage {
    --dim: 0;
    --boot: 1;
    --split: 1;
  }

  .iphone__boot {
    opacity: 0;
  }
}

/* ---- The handset -------------------------------------------------------- */

.iphone {
  position: relative;
  /* The splash is a sibling of the screen, so without a container here its
     cqw units resolve against the section and the lockup blows out. */
  container-type: inline-size;
  height: 100%;
  aspect-ratio: 9 / 19.5;
  max-width: 100%;
  border-radius: clamp(1.4rem, 3vw, 2.6rem);
  padding: clamp(0.25rem, 0.6vw, 0.5rem);
  background: linear-gradient(160deg, oklch(34% 0.02 288), oklch(17% 0.02 288));
  box-shadow:
    inset 0 0 0 1px oklch(100% 0 0 / 0.24),
    0 28px 56px -26px oklch(18% 0.02 288 / 0.65),
    0 8px 16px -10px oklch(18% 0.02 288 / 0.45);
}

.iphone__island {
  position: absolute;
  inset-block-start: clamp(0.7rem, 1.5vw, 1.05rem);
  inset-inline-start: 50%;
  transform: translateX(-50%);
  width: 30%;
  height: clamp(0.8rem, 1.6vw, 1.15rem);
  border-radius: 999px;
  background: oklch(11% 0.01 288);
  z-index: 3;
}

/* container-type lets everything inside size in cqw, so one set of values
   works at every handset size and no mobile overrides are needed. */
.iphone__screen {
  container-type: inline-size;
  position: relative;
  height: 100%;
  border-radius: clamp(1.15rem, 2.5vw, 2.2rem);
  background: var(--page);
  overflow: hidden;
  /* Padding and layout belong to .scr now: the three screens stack absolutely
     in here, so this is only the glass they sit behind. */
  transition: background-color var(--swap) var(--e-swap);
}

/* ---- Screen furniture --------------------------------------------------- */

.scr__bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 5.4cqw;
  font-weight: 700;
  color: var(--ink-2);
}

.scr__dots {
  display: flex;
  gap: 1cqw;
}

.scr__dots i {
  width: 1.8cqw;
  height: 1.8cqw;
  border-radius: 50%;
  background: var(--ink-3);
}

.scr__title {
  font-family: Gabarito, sans-serif;
  font-size: 9.5cqw;
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1.05;
}

.scr__sub,
.scr__meta {
  font-size: 5cqw;
  color: var(--ink-3);
  line-height: 1.3;
}

/* A section label needs air above it and none below: it belongs to the card
   underneath, not floating between the two. */
.scr__label {
  font-size: 4.4cqw;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-block-start: 2cqw;
  margin-block-end: -1.6cqw;
}

/* Title gets its own space rather than sitting in the run of cards */
.scr__title {
  margin-block-end: 1cqw;
}

.scr__sub {
  margin-block-start: -3cqw;
}

/* Real apps breathe. These were dense bordered boxes stacked tight, which is
   what read as "normal": hairline borders everywhere and no air between
   groups. Borders out, padding and radius up, and the gap between cards is
   now bigger than the gap inside them, so groups actually group. */
.scr__card {
  display: grid;
  gap: 2.4cqw;
  padding: 4.4cqw;
  border-radius: 6cqw;
  background: var(--card);
  border: 0;
  box-shadow: 0 1cqw 3cqw -1.5cqw oklch(21% 0.028 288 / 0.14);
}

.scr__card--brand {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--card);
  transition: background-color var(--swap) var(--e-swap);
}

.scr__card--brand .scr__meta {
  color: oklch(100% 0 0 / 0.8);
}

.scr__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 2cqw;
  font-size: 5.4cqw;
}

.scr__row b {
  font-weight: 700;
  letter-spacing: -0.01em;
}

.scr__row em {
  font-style: normal;
  font-family: Gabarito, sans-serif;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.scr__row em.ok {
  color: var(--ok);
}

.scr__row em.warn {
  color: oklch(56% 0.13 75);
}

.scr__row em.bad {
  color: var(--bad);
}

.scr__big {
  font-family: Gabarito, sans-serif;
  font-size: 12cqw;
  font-weight: 800;
  letter-spacing: -0.04em;
  line-height: 1;
}

.scr__pill {
  justify-self: start;
  font-size: 4.4cqw;
  font-weight: 700;
  padding: 1.2cqw 2.6cqw;
  border-radius: 999px;
  background: oklch(100% 0 0 / 0.24);
}

.scr__btn {
  margin-block-start: 0.8cqw;
  text-align: center;
  font-family: Gabarito, sans-serif;
  font-weight: 700;
  font-size: 5cqw;
  padding: 2.2cqw;
  border-radius: 999px;
  background: var(--brand);
  color: var(--card);
  transition: background-color var(--swap) var(--e-swap);
}

.scr__search {
  display: flex;
  align-items: center;
  gap: 2cqw;
  font-size: 5cqw;
  color: var(--ink-3);
  padding: 2.4cqw 3cqw;
  border-radius: 999px;
  background: var(--card);
  border: 1px solid var(--line);
}

.scr__search span {
  width: 3.2cqw;
  height: 3.2cqw;
  border-radius: 50%;
  border: 1.2px solid var(--ink-3);
  flex: none;
}

.scr__chips {
  display: flex;
  gap: 1.4cqw;
}

.scr__chips span {
  font-size: 4.2cqw;
  font-weight: 700;
  padding: 1.2cqw 2.4cqw;
  border-radius: 999px;
  border: 1px solid var(--line);
  color: var(--ink-3);
  white-space: nowrap;
}

.scr__chips span[data-on] {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--card);
}

/* ---- The two moving parts inside the glass ------------------------------ */

.scr__meter {
  display: block;
  height: 1.8cqw;
  border-radius: 999px;
  background: var(--line);
  overflow: hidden;
}

.scr__meter i {
  display: block;
  height: 100%;
  width: var(--fill, 60%);
  border-radius: 999px;
  background: var(--brand);
  transform-origin: left;
  animation: meter-fill 2.4s var(--e-out) infinite alternate;
}

@keyframes meter-fill {
  from {
    transform: scaleX(0.3);
  }
  to {
    transform: scaleX(1);
  }
}

.scr__live {
  font-size: 4.4cqw;
  font-weight: 800;
  color: var(--brand);
  display: inline-flex;
  align-items: center;
  gap: 1.4cqw;
  white-space: nowrap;
}

.scr__live::before {
  content: "";
  width: 2.2cqw;
  height: 2.2cqw;
  border-radius: 50%;
  background: var(--brand);
  animation: live-beat 1.9s var(--e-in-out) infinite;
}

@keyframes live-beat {
  0%,
  100% {
    transform: scale(1);
    opacity: 0.75;
  }
  50% {
    transform: scale(1.5);
    opacity: 1;
  }
}

/* ---- Caption ------------------------------------------------------------ */

.a2__cap {
  display: grid;
  gap: 0.1rem;
  text-align: center;
}

.a2__cap b {
  font-family: Gabarito, sans-serif;
  font-size: clamp(0.92rem, 0.86rem + 0.32vw, 1.15rem);
  font-weight: 800;
  letter-spacing: -0.03em;
  color: var(--brand);
}

.a2__cap span {
  font-size: 0.76rem;
  color: var(--ink-3);
}

/* ---- Narrow ------------------------------------------------------------- */

@media (max-width: 62rem) {
  .a2__phones {
    grid-template-columns: 1fr;
    gap: 2.25rem;
    align-items: start;
  }

  .a2__device {
    grid-template-rows: auto auto;
    height: auto;
  }

  .a2__device:nth-child(2) {
    padding-block-end: 0;
  }

  .iphone {
    height: auto;
    width: min(15rem, 70vw);
  }
}

@media (prefers-reduced-motion: reduce) {
  .a2__device {
    transform: none;
    opacity: 1;
  }

  .scr__meter i,
  .scr__live::before {
    animation: none;
  }
}

/* ==========================================================================
   FEATURE EXPLORER
   Options one side, a live panel the other. Driven by radio inputs rather
   than JS: :checked handles click and touch, :has() handles hover preview,
   and it keeps working with scripting off. Parent puts the options left,
   teacher right, via .explore--right.
   ========================================================================== */

.explore {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
  gap: clamp(1.5rem, 4vw, 3.5rem);
  align-items: start;
}

/* Teacher runs panel-then-options, and the option column is narrower than
   the parent's: its labels are shorter, so the same width left a gap. */
.explore--right {
  grid-template-columns: minmax(0, 1.35fr) minmax(0, 0.65fr);
}

.explore__opts {
  display: grid;
  gap: 0.5rem;
  align-content: start;
}

.explore__opts input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.explore__opts label {
  display: grid;
  gap: 0.2rem;
  padding: 1rem 1.15rem;
  border-radius: var(--r);
  border: 1px solid var(--line);
  background: var(--card);
  cursor: pointer;
  transition:
    border-color 260ms var(--e-out),
    background-color 260ms var(--e-out),
    transform 260ms var(--e-out);
}

.explore__opts label b {
  font-family: Gabarito, sans-serif;
  font-size: 1.02rem;
  font-weight: 800;
  letter-spacing: -0.025em;
}

.explore__opts label span {
  font-size: 0.85rem;
  color: var(--ink-3);
}

@media (hover: hover) and (pointer: fine) {
  .explore__opts label:hover {
    border-color: var(--brand-line);
    transform: translateX(3px);
  }
}

.explore__opts input:checked + label {
  border-color: var(--brand);
  background: var(--brand-wash);
  box-shadow: inset 3px 0 0 var(--brand);
}

.explore__opts input:focus-visible + label {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* ---- The panel ---- */

/* One fixed height for every panel in both sections, so switching option
   never resizes the box and parent and teacher sit at the same scale. */
/* A fixed height, not a minimum: with min-height the tallest panel in each
   section set that section's size, so parent (four cards) grew taller than
   teacher (three). Both boxes are now identical whatever is inside them. */
.explore__views {
  display: grid;
  position: relative;
  height: clamp(21rem, 38vh, 26rem);
}

.explore__view {
  grid-area: 1 / 1;
  display: grid;
  gap: 0.7rem;
  /* stretch, not space-between: space-between pushed the cards to the two
     ends and dumped every spare pixel into one gap in the middle. stretch
     hands the spare height to the auto rows instead, so the cards grow a
     little and the rhythm between them stays even. */
  align-content: stretch;
  padding: clamp(1rem, 2.4vw, 1.6rem);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r);
  box-shadow: 0 20px 44px -28px oklch(21% 0.028 288 / 0.3);
  /* no scrollbar inside a preview panel: content is authored to fit */
  overflow: hidden;
  opacity: 0;
  transform: translateY(8px);
  pointer-events: none;
  /* Short: a hover preview has to feel like a response, not a reveal. */
  transition: opacity 200ms var(--e-out), transform 200ms var(--e-out);
  will-change: opacity, transform;
}

/* Positional, not ID-coupled. The old version named every input twice per
   role, so adding or reordering an option silently broke the pairing. This
   matches the nth input to the nth panel, so both roles share one rule set
   and a fifth option needs no CSS at all. */
.explore:has(.explore__opts input:nth-of-type(1):checked) .explore__view:nth-child(1),
.explore:has(.explore__opts input:nth-of-type(2):checked) .explore__view:nth-child(2),
.explore:has(.explore__opts input:nth-of-type(3):checked) .explore__view:nth-child(3),
.explore:has(.explore__opts input:nth-of-type(4):checked) .explore__view:nth-child(4),
.explore:has(.explore__opts input:nth-of-type(5):checked) .explore__view:nth-child(5) {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

/* Hover previews without committing. Hiding first and re-showing one keeps
   the checked panel from stacking under the hovered one. */
@media (hover: hover) and (pointer: fine) {
  .explore:has(.explore__opts label:hover) .explore__view {
    opacity: 0;
    transform: translateY(8px) scale(0.995);
    pointer-events: none;
  }

  .explore:has(.explore__opts label:nth-of-type(1):hover) .explore__view:nth-child(1),
  .explore:has(.explore__opts label:nth-of-type(2):hover) .explore__view:nth-child(2),
  .explore:has(.explore__opts label:nth-of-type(3):hover) .explore__view:nth-child(3),
  .explore:has(.explore__opts label:nth-of-type(4):hover) .explore__view:nth-child(4),
  .explore:has(.explore__opts label:nth-of-type(5):hover) .explore__view:nth-child(5) {
    opacity: 1;
    transform: none;
    pointer-events: auto;
  }
}

/* The row-by-row stagger is gone. It meant every pointer move re-ran a
   transition on the panel AND on each of its children, and with :has()
   re-matching on top of that the hover felt like it was dragging. One
   element animating is what makes it feel instant. */

/* Isolated so a panel swap cannot force layout or paint outside its own box */
.explore__view {
  contain: layout paint;
}

@media (prefers-reduced-motion: reduce) {
  .explore__view {
    transition-duration: 1ms;
    transform: none;
  }
}

/* No :has() support: show the checked one and skip the hover preview */
@supports not selector(:has(*)) {
  .explore__view:first-child {
    opacity: 1;
    transform: none;
    pointer-events: auto;
  }
}

@media (max-width: 58rem) {
  .explore,
  .explore--right {
    grid-template-columns: 1fr;
  }

  .explore--right .explore__views {
    order: 2;
  }
}

/* ==========================================================================
   Celebration + explorer depth
   ========================================================================== */

/* Sparks are appended to <body> and positioned by WAAPI, so they only need
   to exist and be out of the way of pointers. */
.spark {
  position: fixed;
  top: 0;
  left: 0;
  width: 9px;
  height: 9px;
  border-radius: 2px;
  background: oklch(78% 0.16 85);
  pointer-events: none;
  z-index: 200;
  will-change: transform, opacity;
}

/* The confirm buttons: press, then settle into the done state.

   The colour swap used to be instant, which put the whole event on the
   sparks alone. Now the button dips under the finger, springs a little past
   its size as the sparks leave it, and eases into green — so the burst looks
   like it came out of the button rather than landing on top of it. */
.btn[data-celebrate] {
  transition:
    background-color 420ms var(--e-out),
    color 420ms var(--e-out),
    box-shadow 420ms var(--e-out),
    transform 260ms var(--e-out);
}

.btn[data-celebrate]:active {
  transform: scale(0.96);
  transition-duration: 90ms;
}

.btn.is-done {
  background: var(--ok);
  color: var(--card);
  box-shadow: 0 10px 26px -14px var(--ok);
  animation: btn-pop 520ms var(--e-out);
}

@keyframes btn-pop {
  0%   { transform: scale(0.96); }
  38%  { transform: scale(1.035); }
  100% { transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .btn.is-done { animation: none; }
  .btn[data-celebrate] { transition: background-color 200ms linear, color 200ms linear; }
}

/* ---- Depth ----
   The explorer was two flat rectangles side by side. Options sit close to
   the page, the panel floats above it, and the selected option lifts toward
   the panel: three planes rather than one. */

.explore__opts label {
  box-shadow:
    0 1px 2px oklch(21% 0.028 288 / 0.04),
    0 4px 10px -6px oklch(21% 0.028 288 / 0.12);
}

@media (hover: hover) and (pointer: fine) {
  .explore__opts label:hover {
    box-shadow:
      0 2px 4px oklch(21% 0.028 288 / 0.06),
      0 10px 22px -10px oklch(21% 0.028 288 / 0.2);
  }
}

.explore__opts input:checked + label {
  box-shadow:
    inset 3px 0 0 var(--brand),
    0 3px 6px oklch(21% 0.028 288 / 0.07),
    0 16px 30px -14px color-mix(in oklab, var(--brand) 55%, transparent);
}

.explore__view {
  box-shadow:
    0 1px 2px oklch(21% 0.028 288 / 0.04),
    0 12px 24px -14px oklch(21% 0.028 288 / 0.22),
    0 40px 70px -40px oklch(21% 0.028 288 / 0.4);
}

/* The cards inside the panel sit on their own quiet plane, so the panel
   reads as a surface holding things rather than a box of outlines. */
.explore__view .ui-card {
  border-color: transparent;
  background: var(--page);
  box-shadow: inset 0 0 0 1px oklch(21% 0.028 288 / 0.05);
}

.explore__view .btn {
  box-shadow: 0 10px 22px -12px var(--brand);
}

/* A soft brand glow behind the panel, tying it to the live role colour */
.explore__views::before {
  content: "";
  position: absolute;
  inset: 12% -6% -10% -6%;
  z-index: -1;
  border-radius: 50%;
  background: radial-gradient(ellipse, var(--brand-wash), transparent 70%);
  pointer-events: none;
}

.explore__views {
  isolation: isolate;
}

/* Cards inside a stretched panel: the row grew, so centre the content in it
   rather than letting the text sit against the top edge. */
.explore__view .ui-card { align-content: center; }

/* ---------------------------------------------------------------------------
   Class list: filter feedback and the Book button
   --------------------------------------------------------------------------- */

/* A filter change reorders and hides rows in one frame, which reads as a jump.
   The rows that survive replay a very short settle, staggered by their new
   position, so the list looks like it answered rather than blinked. */
@keyframes class-settle {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

.results[data-settle] .class:not([hidden]) {
  animation: class-settle 260ms var(--e-out) both;
  animation-delay: calc(var(--i, 0) * 22ms);
}

@media (prefers-reduced-motion: reduce) {
  .results[data-settle] .class:not([hidden]) { animation: none; }
  .results { scroll-behavior: auto; }
}

/* Book confirms in place first, then sends you down to checkout. Without it
   the click had no local answer at all — the page just moved. */
/* Book throws sparks too, so it gets the same press-and-settle as the other
   celebrate buttons rather than a snappier one of its own. */
.class__buy .btn {
  transition:
    background-color 420ms var(--e-out),
    box-shadow 420ms var(--e-out),
    transform 260ms var(--e-out);
}

.class__buy .btn:active {
  transform: scale(0.96);
  transition-duration: 90ms;
}

.class.is-booked {
  border-color: var(--brand);
  box-shadow: inset 3px 0 0 var(--brand), 0 10px 24px -16px var(--brand);
}

.class.is-booked .class__buy .btn {
  animation: btn-pop 520ms var(--e-out);
}

.class.is-booked .class__buy .btn {
  background: var(--brand-deep);
}

/* Student finder headline: at 44rem this sentence broke over three lines with
   a stub on the last one. Wider plus balanced wrapping settles it at two. */
#find .head {
  max-width: 54rem;
}

#find .head h2 {
  text-wrap: balance;
}

/* ---------------------------------------------------------------------------
   One section: proportion, colour and interaction pass
   --------------------------------------------------------------------------- */

/* Both headlines broke over three lines with a stub on the last one. The
   wide head splits 1.3fr / 1fr by default; these two sentences want more
   room on the left than that, and balanced wrapping to settle at two. */
/* Both headlines are ~52 characters and both broke over three lines with a
   stub on the last. text-wrap: balance is already on globally and does not
   help: it evens the lines out, it does not reduce how many there are. Two
   lines needs about 890px at the page h2 size, and the 1.3fr column gives
   660px. So: a wider column and a slightly smaller cut, only in this
   section, which lands both at two. */
#one .head--wide {
  grid-template-columns: minmax(0, 1.75fr) minmax(0, 1fr);
}

#one .head--wide h2 {
  font-size: clamp(1.95rem, 1.2rem + 2.7vw, 3.5rem);
}

/* ---- Parent funnel ---- */

/* Colour was doing no work: five identical washes narrowing. The fill now
   deepens as the field thins, so the row that survives is the most saturated
   thing in the section and the eye reads narrowing and darkening as one
   movement. */
.funnel__rail i {
  background: color-mix(in oklab, var(--brand) calc(16% + var(--i) * 11%), transparent);
}

.funnel__step--end .funnel__rail i {
  background: var(--brand);
}

/* Hovering one step recedes the rest. Comparison is the whole argument of a
   funnel, and you cannot compare five things that all shout equally. */
@media (hover: hover) and (pointer: fine) {
  .funnel:hover .funnel__step {
    opacity: 0.45;
    transition: opacity 260ms var(--e-out);
  }

  .funnel:hover .funnel__step:hover,
  .funnel .funnel__step[aria-expanded="true"] {
    opacity: 1;
  }

  .funnel__step:hover .funnel__rail i {
    filter: saturate(1.15);
  }
}

/* The percentage still standing, sitting under its label. Small, because the
   count is the headline figure and this is the reading of it. */
.funnel__rate {
  grid-area: what;
  align-self: end;
  justify-self: start;
  margin-top: 1.55rem;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--brand);
  opacity: 0.75;
  font-variant-numeric: tabular-nums;
}

.funnel__step {
  grid-template-areas:
    "what rail n"
    "what why  why";
  align-items: center;
  row-gap: 0.1rem;
}

/* The label block owns two lines now (name, then rate), so the row needs to
   be tall enough to hold both without the rail drifting off centre. */
.funnel__what {
  align-self: start;
  margin-top: 0.1rem;
}

.funnel__step { min-height: 3.4rem; }

/* ---- Teacher reach ---- */

/* A feed that never visibly changes is just a list. The newest row lands in
   brand and cools to the resting wash over two seconds, so the eye catches
   the arrival even when it is not watching the top of the list. */
@keyframes reach-in {
  0% {
    opacity: 0;
    transform: translateY(-10px);
    background: color-mix(in oklab, var(--brand) 26%, transparent);
  }
  30% {
    opacity: 1;
    transform: none;
    background: color-mix(in oklab, var(--brand) 26%, transparent);
  }
  100% {
    opacity: 1;
    transform: none;
    background: var(--brand-wash);
  }
}

.reach__list li {
  animation-duration: 2000ms;
}

/* A live indicator on the label, because the claim is that this is still
   happening rather than that it happened once. */
.reach__label {
  display: flex;
  align-items: center;
  gap: 0.45rem;
}

.reach__label::before {
  content: "";
  width: 0.45rem;
  height: 0.45rem;
  border-radius: 99px;
  background: var(--brand);
  animation: reach-pulse 2600ms ease-in-out infinite;
}

@keyframes reach-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: 0.35; transform: scale(0.8); }
}

/* Each count sits over a rule that draws itself as the number runs, so the
   three facts land rather than simply appearing. */
.reach__fact {
  position: relative;
  border-bottom: 0;
}

.reach__fact::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  height: 1px;
  width: 100%;
  background: var(--line);
  transform: scaleX(0);
  transform-origin: left center;
}

[data-reveal].is-in .reach__fact::after {
  animation: reach-rule 700ms var(--e-out) both;
  animation-delay: calc(var(--r, 0) * 160ms + 300ms);
}

@keyframes reach-rule {
  to { transform: scaleX(1); }
}

@media (prefers-reduced-motion: reduce) {
  .reach__label::before,
  .reach__fact::after { animation: none; }
  .reach__fact::after { transform: scaleX(1); }
}

/* ---------------------------------------------------------------------------
   Standalone documents: about, privacy, terms

   These share the shell and the type but not the machinery. No scroll acts,
   no role switch, no reveal observer, so they work with the stylesheet alone
   and nothing on them waits for main.js.
   --------------------------------------------------------------------------- */

.doc__bar {
  border-bottom: 1px solid var(--line);
  background: var(--card);
}

.doc__barin {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 0.9rem;
}

.doc {
  padding-block: clamp(2.5rem, 7vh, 5rem);
}

/* One column at reading width, left aligned. A legal page centred on the
   viewport is a legal page nobody finishes. */
.doc__shell > * {
  max-width: 44rem;
}

.doc h1 {
  font-size: clamp(2rem, 1.3rem + 2.6vw, 3.4rem);
  margin-block: 0.4rem 1.5rem;
}

.doc h2 {
  font-size: clamp(1.2rem, 1.05rem + 0.6vw, 1.5rem);
  margin-block: 2.4rem 0.7rem;
}

.doc p {
  margin-bottom: 0.9rem;
  line-height: 1.65;
  color: var(--ink-2);
}

.doc__lead {
  font-size: 1.05rem;
  color: var(--ink) !important;
}

.doc__list {
  display: grid;
  gap: 0.7rem;
  margin-bottom: 1rem;
  padding-left: 1.1rem;
  list-style: disc;
  line-height: 1.6;
  color: var(--ink-2);
}

.doc__list b { color: var(--ink); }

/* The unfinished-document warning. Loud on purpose: these pages carry
   bracketed placeholders where the binding text has to go, and a draft
   policy that looks finished is worse than no policy at all. */
.doc__flag {
  padding: 1rem 1.15rem;
  margin-bottom: 2rem;
  border: 1px solid var(--brand);
  border-radius: var(--r);
  background: var(--brand-wash);
  font-size: 0.9rem;
  line-height: 1.55;
}

.doc__flag b { display: block; margin-bottom: 0.25rem; }

.doc__note {
  margin-top: 2.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
  font-size: 0.86rem;
}

.doc__back { margin-top: 2.75rem; }

.doc__shell .footer__base a { color: inherit; }

/* ===========================================================================
   RESPONSIVE PASS
   One consolidated block, last in the file so it wins. Desktop is untouched:
   every rule lives inside a max-width query. Tablet (<= 60rem) is mostly a
   safety net; phone (<= 40rem) does the real work.
   =========================================================================== */

/* ---- Tablet: iPad portrait and down -------------------------------------- */
@media (max-width: 60rem) {
  .head--wide {
    grid-template-columns: minmax(0, 1fr);
    gap: clamp(0.75rem, 2.5vw, 1.5rem);
  }

  .footer__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* ---- Phone nav (rebuilt) -------------------------------------------------
   Same parts as the desktop bar, rearranged for a phone: the links collapse
   into the burger menu and the role switch takes its own row, because its
   three labels cannot share a line with the logo and the burger.

   The important difference is the surface. On desktop the bar starts fully
   transparent and only fades its background in once you scroll, so the hero
   runs underneath it. At phone width that reads as broken: the hero text
   passes straight through the bar, and the band under the switch looks like a
   stray strip rather than part of the bar. So on a phone the bar carries its
   own opaque surface from the start and nothing shows through it.

   Row heights are explicit so the total is a known number, and --nav-h is set
   to exactly that: the hero, the sticky filter panel and the act pins all use
   it as their top clearance, and if it disagrees with the real bar the
   content either tucks underneath or floats away from it.

     0.45rem  padding top
     2.75rem  row 1, logo + burger
     0.35rem  gap
     2.15rem  row 2, role switch
     0.45rem  padding bottom
     -------
     6.15rem
   -------------------------------------------------------------------------- */
@media (max-width: 40rem) {
  :root {
    --nav-h: 6.15rem;
  }

  /* Opaque from the start, and the scroll-driven tint is overridden rather
     than left to fight this rule. */
  .nav {
    background-color: var(--card);
    border-bottom: 1px solid var(--brand-line);
    box-shadow: 0 6px 18px -14px oklch(21% 0.028 288 / 0.4);
  }

  .nav__inner {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-rows: 2.75rem 2.15rem;
    column-gap: 0.75rem;
    row-gap: 0.35rem;
    align-items: center;
    height: var(--nav-h);
    min-height: 0;
    padding-block: 0.45rem;
  }

  /* Explicit line placement. Named grid areas were tried here and silently
     failed to span the switch across the row, so it is placed by line. */
  .mark {
    grid-column: 1;
    grid-row: 1;
    min-width: 0;
  }

  .burger {
    grid-column: 2;
    grid-row: 1;
    justify-self: end;
    margin-inline-end: 0.35rem;
  }

  /* The links live in the drop-down at this width. */
  .nav__links,
  .nav__cta {
    display: none;
  }

  /* Stretched, not centred: centred, the pill sat at its natural height
     inside a taller row and the leftover read as an empty band under it. */
  .nav__end {
    grid-column: 1 / -1;
    grid-row: 2;
    align-self: stretch;
    justify-content: center;
    min-width: 0;
  }

  /* minmax(0, 1fr) rather than the base 1fr: 1fr's automatic minimum is the
     button's own text width, which set a floor the switch could not shrink
     below and made width and max-width useless against it. */
  .roleswitch {
    width: 100%;
    max-width: 19rem;
    min-width: 0;
    height: 100%;
    margin-inline: auto;
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  .roleswitch button {
    padding: 0 0.4rem;
    font-size: 0.78rem;
  }
}

/* ---- Phone hero (.a1) ----------------------------------------------------
   Mobile-only, approved. Desktop hero is left exactly as designed.

   The centre block and the wordmark are absolutely pinned with
   inset-inline: var(--gutter) against a positioning box that is wider than a
   phone viewport, so their content ran off the right edge. Re-pinning them to
   0 with padding guarantees they sit inside the screen. Then the headline and
   the giant wordmark come down to a size one phone screen can hold. */
@media (max-width: 40rem) {
  /* Pin both blocks to the full viewport with border-box padding, so their
     content physically cannot exceed the screen whatever the earlier
     absolute-positioning rules were measuring against. */
  .a1__centre,
  .a1__mark {
    inset-inline: 0;
    width: 100%;
    max-width: 100vw;
    padding-inline: var(--gutter);
    box-sizing: border-box;
  }

  /* The block is centred on the pin with inset-block-start:50% and a -52%
     translate, so it centres on the whole viewport and its top edge, the
     badge, ends up under the fixed bar. Moving the centre point down by half
     the bar's height centres it in the space actually left below the bar,
     without touching the transform the scroll scrub drives. */
  .a1__centre {
    overflow: hidden;
    inset-block-start: calc(50% + var(--nav-h) / 2);
  }

  .a1 h1 {
    font-size: clamp(1.7rem, 7.5vw, 2.2rem);
    max-width: 13ch;
  }

  .a1 .lead {
    font-size: 0.95rem;
    line-height: 1.5;
    max-width: 26ch;
  }

  .a1__word {
    font-size: clamp(2.6rem, 13vw, 4.5rem);
    max-width: 100%;
  }

  .a1__actions {
    flex-direction: column;
    gap: 0.85rem;
  }

  .a1 .btn--lg { width: min(20rem, 100%); }

  /* Three claims in a row will not fit; stack them into a rounded card and
     drop the vertical dividers that only make sense side by side. */
  .a1__proof {
    grid-template-columns: 1fr;
    gap: 0.5rem;
    width: min(17rem, 100%);
    border-radius: var(--r);
    padding: 1rem 1.25rem;
  }

  .a1__proof li + li::before { display: none; }

  /* No SCROLL label on a touch screen: a swipe is the obvious gesture and the
     wordmark rising says the same thing. */
  .a1__cue { display: none; }
}

/* ---- Phone: One app section (a2) -----------------------------------------
   The acts already go static below 46rem, so the three handsets are a normal
   block here rather than a scroll rig. On a phone the fan-out has no room and
   three phones across are unreadably small, so they stand in a single
   vertical column instead, each one booting from its splash as it scrolls
   into view. data-reveal on each unit gives the stagger for free. */
@media (max-width: 40rem) {
  .a2__stage {
    grid-template-columns: 1fr;
    gap: clamp(2rem, 8vw, 3rem);
    max-width: 15rem;
    margin-inline: auto;
  }

  .a2__unit,
  .iphone {
    height: auto;
  }

  /* Boot: each handset holds its splash, then lifts it and the real screen
     fades in a beat behind, the same move the desktop scrub does. Driven by
     the animation itself rather than a scroll-triggered class: putting
     data-reveal on the units to get an is-in hook also handed them the
     reveal's transform, which beat the centring transform the fan relies on
     and pulled the middle handset off centre at desktop widths. The three
     are staggered so they wake one after another down the column. */
  .a2__unit .iphone__splash {
    animation: a2-boot-splash 1.6s var(--e-out) both;
  }

  .a2__unit .scr {
    animation: a2-boot-screen 1.1s var(--e-out) 0.65s both;
  }

  .a2__unit[data-step="1"] .iphone__splash { animation-delay: 0.25s; }
  .a2__unit[data-step="1"] .scr { animation-delay: 0.9s; }
  .a2__unit[data-step="2"] .iphone__splash { animation-delay: 0.5s; }
  .a2__unit[data-step="2"] .scr { animation-delay: 1.15s; }
}

/* Splash holds, then lifts. */
@keyframes a2-boot-splash {
  0%, 38% { opacity: 1; }
  100% { opacity: 0; }
}

/* Screen dark, then wakes. */
@keyframes a2-boot-screen {
  0%, 30% { opacity: 0; }
  100% { opacity: 1; }
}

@media (max-width: 40rem) and (prefers-reduced-motion: reduce) {
  .a2__unit .iphone__splash { animation: none; opacity: 0; }
  .a2__unit .scr { animation: none; opacity: 1; }
}

/* ---- Phone: clip hero decorative layers -----------------------------------
   On mobile the act pin is overflow:visible (so a static pin does not clip its
   own content), which lets the ambient blobs and the drifting clouds spill a
   few px past the viewport. overflow-x:hidden on the body already stops any
   scroll, but clipping the decorative layers themselves keeps scrollWidth
   equal to the viewport so nothing can jiggle sideways on any browser. */
@media (max-width: 46rem) {
  .a1__amb,
  .a1__clouds {
    overflow: hidden;
  }
}

/* ---- Flow steps: tap to light ---------------------------------------------
   On desktop the steps light in turn as the act scrubs. On a phone the act is
   static, so they sat dim with nothing to do. They are buttons now: tapping
   one lights it and gives a small press, so the section responds to touch
   instead of just displaying. Works at every width, the scrub and the tap
   share the same lit look. */
.flow__step {
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  border: 0;
  text-align: left;
  font: inherit;
  color: inherit;
}

.flow__step.is-lit {
  --lit: 1;
  opacity: 1;
}

.flow__step:focus-visible {
  outline: 2px solid oklch(100% 0 0 / 0.8);
  outline-offset: -3px;
}

.flow__step.is-pressed {
  animation: flow-pop 420ms var(--e-out);
}

@keyframes flow-pop {
  0% { transform: scale(1); }
  35% { transform: scale(0.97); }
  100% { transform: scale(1); }
}

@media (hover: hover) and (pointer: fine) {
  .flow__step:hover { --lit: 1; opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .flow__step.is-pressed { animation: none; }
}

/* ---- Phone: kill the sideways pan ----------------------------------------
   overflow-x:hidden on the body alone does not stop a phone panning: the
   viewport takes its overflow from the root element, so the page can still be
   dragged sideways even with the body clipped. Setting it on html as well is
   what actually stops it.

   clip rather than hidden: hidden on one axis forces the other to auto, which
   turns the element into a scroll container and breaks position:sticky inside
   it (the filter panel, the act pins). clip does not, so the sticky pieces
   keep working. */
@media (max-width: 40rem) {
  html,
  body {
    overflow-x: clip;
  }

  /* The form wraps at this width, so the button drops onto its own line.
     Centred, it sits under the middle of the field rather than hanging off
     the left edge of the card. */
  .signup {
    justify-content: center;
  }
}

/* ---- The collapsed drop-down was still 30px tall --------------------------
   .navmenu closes by animating grid-template-rows from 1fr to 0fr, and
   .navmenu > nav carries min-height:0 so its content can collapse. But
   padding is not content: the inner nav's 0.75rem/1.1rem padding-block kept
   rendering inside the zero-height row, leaving a ~30px band of bar under the
   role switch with nothing in it. Measured: .nav 130px against a 98px inner.

   So the padding lives on the open state only, and transitions with the rows
   rather than being held through the close. The border goes with it, or it
   draws a second line under the one the bar already has. */
@media (max-width: 72rem) {
  .navmenu {
    border-top: 0;
  }

  .navmenu__inner {
    padding-block: 0;
    transition: padding-block 340ms var(--e-out);
  }

  .navmenu[data-open="true"] {
    border-top: 1px solid var(--line);
  }

  .navmenu[data-open="true"] .navmenu__inner {
    padding-block: 0.75rem 1.1rem;
  }
}

/* ===========================================================================
   NAV: ALIGNMENT, AND ABOUT IN THE BAR

   Measured before this pass, in all three roles:

   - Between 40rem and 72rem the links and the CTA hid, and nothing told the
     role switch and the burger where to go. Both packed up against the logo
     and the right half of the bar stood empty; at 1100px the burger sat in
     the middle of the screen.
   - Just above 72rem the teacher links ("Getting started", "Who finds you")
     wrapped onto two lines inside the bar. With About added that would have
     happened at every laptop width, so the links now go to the menu below
     80rem, never wrap, and the bar trims a little padding to fit them.
   - The role-gated links arrived with the page's 14px rise, so on every
     role switch two of the links sat visibly low for half a second.
   =========================================================================== */

.nav__links {
  gap: clamp(0.1rem, 0.6vw, 0.6rem);
}

.nav__links a {
  padding-inline: 0.5rem;
  white-space: nowrap;
}

.nav__links a::after {
  inset-inline: 0.5rem;
}

.nav .roleswitch button {
  padding-inline: 0.78rem;
}

/* Fade only: a 78px bar has no room for anything to travel in. */
.nav__links a[data-for],
.navmenu a[data-for] {
  animation: nav-role-in 320ms var(--e-out) both;
}

@keyframes nav-role-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* A page you are on reads the same as a section you are in. */
.nav__links a[aria-current="page"],
.navmenu a[aria-current="page"] {
  color: var(--brand);
}

.nav__links a[aria-current="page"]::after {
  transform: scaleX(1);
}

@media (max-width: 80rem) {
  .nav__links,
  .nav__cta {
    display: none;
  }

  .burger {
    display: block;
  }

  .navmenu {
    display: grid;
    grid-template-rows: 0fr;
    border-top: 0;
  }

  .navmenu[data-open="true"] {
    grid-template-rows: 1fr;
    border-top: 1px solid var(--line);
  }

  .navmenu > nav {
    min-height: 0;
  }

  .navmenu__inner {
    padding-block: 0;
    transition: padding-block 340ms var(--e-out);
  }

  .navmenu[data-open="true"] .navmenu__inner {
    padding-block: 0.75rem 1.1rem;
  }

  /* The switch and the burger belong on the right, where the CTA was. */
  .nav__end {
    margin-inline-start: auto;
  }
}

@media (max-width: 40rem) {
  /* The phone bar is a grid with the switch on its own row: no auto margin. */
  .nav__end {
    margin-inline-start: 0;
  }
}

/* ---- The bar on the other pages -------------------------------------------
   About, privacy and terms carry the same bar as the home page, links, role
   switch and all. They have no hero running under it, so here it is opaque
   from the start and sits in the flow rather than being fixed over it. */
.nav--plain {
  position: sticky;
  background-color: color-mix(in oklab, var(--card) 91%, var(--brand));
  border-bottom: 1px solid var(--brand-line);
  box-shadow: 0 8px 24px -20px oklch(21% 0.028 288 / 0.55);
}


/* ===========================================================================
   PHONE AND TABLET FIXES
   =========================================================================== */

/* At 768px the page could be dragged 7px sideways: the hero's drifting
   decoration pokes past the edge and the clip only applied below 40rem.
   clip rather than hidden, so position: sticky keeps working. */
html,
body {
  overflow-x: clip;
}

/* #one's heading split is scoped by id, so it outranked the tablet rule that
   stacks every other wide heading. At 390px its paragraph ran as a column
   two words wide beside the headline. */
@media (max-width: 60rem) {
  #one .head--wide {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* The label column was 4.75rem on narrow screens while the row still asked
   for three tracks, so "Teaching on Langford today" broke over four lines and
   the rate floated a line below it. Label and count now share the top line,
   the rate sits under the label, and the rail runs the full width. */
@media (max-width: 52rem) {
  .funnel__step {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "what n"
      "rate n"
      "rail rail"
      "why  why";
    row-gap: 0.2rem;
    min-height: 0;
  }

  .funnel__what {
    align-self: end;
    margin-top: 0;
  }

  .funnel__rate {
    grid-area: rate;
    align-self: start;
    margin-top: 0;
  }

  .funnel__rail {
    margin-top: 0.45rem;
  }
}

/* ===========================================================================
   ABOUT PAGE

   People first. The page opens on a short heading and then the three
   portraits, so on a laptop all three faces are on screen before any
   scrolling. They stand on one line at one size, founder first.

   Names and bios line up across the columns too (subgrid), so a name that
   wraps onto two lines never pushes that person's bio lower than the others.

   Each person wears one of the product's three role colours: the founder in
   parent red, the co-founder in purple, the chief executive in blue. That
   colour stays put whichever role the rest of the page is switched to.
   =========================================================================== */

.about-team {
  padding-block: clamp(2.25rem, 6vh, 4.25rem) clamp(4rem, 9vh, 7rem);
}

.about-team__head {
  display: grid;
  grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
  gap: clamp(1rem, 5vw, 5rem);
  align-items: end;
  margin-bottom: clamp(2rem, 5vh, 3.25rem);
}

.about-team__head h1 {
  max-width: 17ch;
  margin-top: 0.85rem;
  font-size: clamp(2.2rem, 1.2rem + 3vw, 4.1rem);
}

.about-team__head .lead {
  max-width: 42ch;
  font-size: clamp(1rem, 0.95rem + 0.3vw, 1.15rem);
}

/* ---- The three people ---- */

.team {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  column-gap: clamp(1.5rem, 3.5vw, 3rem);
  row-gap: 0;
}

/* Four shared rows: portrait, role, name, words. */
.person {
  display: grid;
  grid-row: span 4;
  grid-template-rows: subgrid;
  align-content: start;
  scroll-margin-top: calc(var(--nav-h) + 1.5rem);
}

.person__photo {
  margin-bottom: 1.35rem;
}

.person__frame {
  display: block;
  overflow: hidden;
  aspect-ratio: 4 / 5;
  border-radius: var(--r);
  background: var(--brand-wash);
  box-shadow: 0 24px 48px -34px oklch(21% 0.028 288 / 0.65);
}

.person__frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 20%;
  transition: transform 700ms var(--e-out);
}

/* The person's colour, under the portrait. */
.person__photo::after {
  content: "";
  display: block;
  height: 0.35rem;
  margin-top: 0.7rem;
  border-radius: 99px;
  background: var(--brand);
}

@media (hover: hover) and (pointer: fine) {
  .person:hover .person__frame img {
    transform: scale(1.035);
  }
}

/* The role, set like ACADEMY under the wordmark: Poppins, tracked, a rule. */
.person__role {
  display: flex;
  align-items: center;
  align-self: end;
  gap: 0.6rem;
  margin-bottom: 0.55rem;
  font-family: Poppins, sans-serif;
  font-size: 0.74rem;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--brand);
}

.person__role::before {
  content: "";
  flex: none;
  width: 1.75rem;
  height: 1px;
  background: currentColor;
}

.person__name {
  align-self: start;
  font-size: clamp(1.45rem, 1.1rem + 1vw, 2rem);
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: -0.035em;
}

/* The honorific is part of the name, not a label on it: same line, same
   face, a weight lighter, in the person's colour. */
.person__hon {
  font-weight: 600;
  letter-spacing: -0.015em;
  color: var(--brand);
}

.person__body {
  padding-top: 0.9rem;
}

.person__bio {
  max-width: 42ch;
  font-size: 0.97rem;
  line-height: 1.62;
  color: var(--ink-2);
}

.person .textlink {
  display: inline-block;
  margin-top: 0.85rem;
  font-size: 0.92rem;
}

/* ---- What Langford is, and the lines we hold ---- */

.story {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  gap: clamp(2.25rem, 7vw, 7rem);
  align-items: start;
}

.story h2 {
  max-width: 11ch;
  margin-bottom: clamp(0.5rem, 1.5vw, 1rem);
}

.story__lead p {
  max-width: 50ch;
  margin-top: 1rem;
  font-size: clamp(1rem, 0.96rem + 0.2vw, 1.1rem);
  line-height: 1.65;
  color: var(--ink-2);
}

/* What Langford is comes first and reads a step louder than why. */
.story__lead p:first-of-type {
  font-size: clamp(1.1rem, 1rem + 0.4vw, 1.3rem);
  line-height: 1.5;
  color: var(--ink);
}

.story__rules h3 {
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--ink);
  font-size: clamp(1.15rem, 1.05rem + 0.5vw, 1.45rem);
}

.rules li {
  display: grid;
  gap: 0.3rem;
  padding-block: 1.1rem;
  border-bottom: 1px solid var(--line);
}

.rules b {
  font-family: Gabarito, sans-serif;
  font-size: clamp(1.02rem, 0.98rem + 0.25vw, 1.15rem);
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--ink);
}

.rules span {
  font-size: 0.95rem;
  line-height: 1.58;
  color: var(--ink-2);
}

/* ---- Where we are, and how to reach us ---- */

.about-close h2 {
  max-width: 15ch;
}

.about-close__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 1rem 1.75rem;
  margin-top: 0.35rem;
}

.about-close__link {
  max-width: 34ch;
  font-size: 0.92rem;
  font-weight: 600;
  line-height: 1.45;
  color: oklch(100% 0 0 / 0.86);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.25em;
  transition: color 200ms var(--e-out);
}

@media (hover: hover) and (pointer: fine) {
  .about-close__link:hover {
    color: var(--card);
  }
}

.about__legal {
  max-width: none;
  margin-top: clamp(2rem, 5vw, 3rem);
  font-size: 0.84rem;
  text-align: center;
  color: var(--ink-3);
}

/* ---- About: tablet ---- */

@media (max-width: 62rem) {
  .team {
    grid-template-columns: minmax(0, 1fr);
    row-gap: clamp(2.5rem, 6vw, 3.5rem);
  }

  /* One person per row, portrait beside the words and the words centred on
     it. Three portraits across a tablet are too narrow to read as people. */
  .person {
    grid-row: auto;
    grid-template-columns: minmax(0, 14rem) minmax(0, 1fr);
    grid-template-rows: 1fr auto auto auto 1fr;
    column-gap: clamp(1.5rem, 4.5vw, 3rem);
  }

  .person__photo {
    grid-column: 1;
    grid-row: 1 / -1;
    margin-bottom: 0;
  }

  .person__role,
  .person__name,
  .person__body {
    grid-column: 2;
  }

  .person__role { grid-row: 2; }
  .person__name { grid-row: 3; }
  .person__body { grid-row: 4; }
}

@media (max-width: 56rem) {
  .about-team__head,
  .story {
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
  }

  .story h2 {
    max-width: 16ch;
  }
}

/* ---- About: phone ----
   Portrait and name side by side, the bio full width under both, so each
   person takes about one screen rather than a screen-tall photo each. */
@media (max-width: 40rem) {
  .person {
    grid-template-columns: clamp(7.5rem, 38vw, 10rem) minmax(0, 1fr);
    grid-template-rows: 1fr auto auto 1fr auto;
    column-gap: 1.1rem;
  }

  .person__photo {
    grid-row: 1 / 5;
    align-self: start;
  }

  .person__photo::after {
    height: 0.28rem;
    margin-top: 0.5rem;
  }

  .person__role {
    grid-row: 2;
    font-size: 0.64rem;
    line-height: 1.45;
    letter-spacing: 0.12em;
  }

  /* The longer titles wrap at this width, and a rule beside two lines of
     label points at neither of them. */
  .person__role::before {
    display: none;
  }

  .person__name {
    grid-row: 3;
    font-size: clamp(1.3rem, 5.4vw, 1.6rem);
  }

  .person__body {
    grid-column: 1 / -1;
    grid-row: 5;
    padding-top: 1.1rem;
  }

  .person__bio {
    max-width: none;
  }
}

/* ===========================================================================
   PHONE HERO: IN THE FLOW BELOW THE BAR

   Once the acts stop pinning (46rem and down) nothing scrubs the hero, but
   the block was still absolutely centred on the screen. Measured on real
   phone sizes, the top of it slid under the fixed bar: the badge sat 53px
   under it on a 375x667 phone and 101px under on 320x568, more in the teacher
   view where the headline runs to four lines. The letters of the big name
   also kept their 52vh starting offset, which landed the name on top of the
   claims panel.

   Here the hero is laid out in normal flow: bar clearance on top, the block,
   then the name under it, all centred in at least one screen of height.
   Nothing moves on scroll at these widths, so nothing is lost.
   =========================================================================== */
@media (max-width: 46rem) {
  .a1 .act__pin {
    position: relative;
    top: auto;
    display: flex;
    flex-direction: column;
    justify-content: center;
    height: auto;
    min-height: 100svh;
    padding-block: calc(var(--nav-h) + clamp(1.25rem, 4vh, 2rem)) clamp(1.75rem, 5vh, 3rem);
    overflow: hidden;
  }

  .a1__centre {
    position: relative;
    inset: auto;
    margin-inline: auto;
    transform: none;
    overflow: visible;
  }

  .a1__mark {
    position: relative;
    inset: auto;
    margin-top: clamp(1.5rem, 5vh, 2.5rem);
    transform: none;
  }

  .a1__word span {
    transform: none;
  }
}

/* ===========================================================================
   LOGO LOCKUP: MARK SIZED TO THE WORDMARK

   Measured from the rendered pixels: the mark was 42.7px tall beside 33.2px
   of lettering (LANGFORD cap top to ACADEMY baseline). The centres matched,
   so the mark overhung the words by about 4.5px above and 5px below, which
   is what reads as out of line. It is now the height of the lettering, so
   its top sits level with the top of LANGFORD and its foot level with the
   foot of ACADEMY. Width keeps the artwork's 392:343 proportion.
   =========================================================================== */
.mark .logo--mark {
  width: 2.4rem;
  height: 2.1rem;
}

.mark {
  gap: 0.55rem;
}

/* ===========================================================================
   PHONE: THE ONE APP HANDSETS, CENTRED

   The stage kept its gutter padding inside a 15rem max-width, so the content
   box was narrower than the 15rem handset and every phone spilled 18px to the
   right. No padding at this width, a fixed stage width, and the handset
   fills its column.
   =========================================================================== */
@media (max-width: 40rem) {
  /* An explicit width, not a max-width: the handset fills its column, so the
     stage cannot take its size from the handset without both collapsing. */
  .a2__stage {
    width: min(15rem, 100%);
    padding-inline: 0;
  }

  .a2__unit .iphone {
    width: 100%;
  }
}

/* ===========================================================================
   FEATURE EXPLORER: PLAYS ITSELF

   main.js moves to the next option every two seconds while the section is on
   screen and nobody is using it, and sets data-auto="on" while it counts. A
   line along the foot of the selected option fills over those two seconds, so
   the change never comes as a surprise. Pausing removes the line; resuming
   draws it from the start again.
   =========================================================================== */
.explore__opts label {
  position: relative;
  overflow: hidden;
}

.explore[data-auto="on"] .explore__opts input:checked + label::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  height: 3px;
  background: var(--brand);
  transform-origin: left center;
  animation: explore-tick 2000ms linear both;
}

@keyframes explore-tick {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

/* ===========================================================================
   PARENT: THE SIEVE (replaces the bar funnel)

   The section's claim is a ratio, so the form is a hundred squares: one per
   applicant in every hundred. The five checks sit beside them as a numbered
   list, which is a real sequence, and doubles as the data table: every count
   and share is written out. Picking a check clears the squares that stopped
   there. One hue from the live role, light to solid; numbers stay in ink.
   =========================================================================== */

.sieve {
  display: grid;
  grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: center;
}

.sieve__viz {
  position: relative;
  display: grid;
  gap: 1.35rem;
  padding: clamp(1.25rem, 3vw, 2.25rem);
  border: 1px solid var(--line);
  border-radius: calc(var(--r) + 8px);
  background: var(--card);
  box-shadow: 0 30px 60px -44px oklch(21% 0.028 288 / 0.55);
}

.sieve__grid {
  display: grid;
  grid-template-columns: repeat(10, minmax(0, 1fr));
  gap: clamp(4px, 0.8vw, 8px);
}

.sieve__dot {
  display: block;
  aspect-ratio: 1;
  border-radius: 28%;
  background: var(--brand);
  transition:
    background-color 380ms var(--e-out),
    transform 380ms var(--e-out);
  transition-delay: var(--dl, 0ms);
}

.sieve__dot.is-out {
  background: color-mix(in oklab, var(--brand) 13%, transparent);
  transform: scale(0.7);
}

@media (hover: hover) and (pointer: fine) {
  .sieve__dot:hover {
    outline: 2px solid var(--ink);
    outline-offset: 1px;
  }
}

.sieve__read {
  display: flex;
  align-items: center;
  gap: 1rem;
}

.sieve__big {
  flex: none;
  min-width: 2.1ch;
  font-family: Gabarito, sans-serif;
  font-size: clamp(3.2rem, 2.2rem + 3vw, 4.8rem);
  font-weight: 800;
  line-height: 0.85;
  letter-spacing: -0.055em;
  font-variant-numeric: tabular-nums;
  text-align: end;
  color: var(--ink);
}

.sieve__txt {
  display: grid;
  gap: 0.2rem;
}

.sieve__unit {
  font-family: Gabarito, sans-serif;
  font-size: 1.05rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--ink);
}

.sieve__cap {
  font-size: 0.92rem;
  color: var(--ink-2);
}

.sieve__key {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.25rem;
  font-size: 0.8rem;
  color: var(--ink-3);
}

.sieve__key span {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
}

.sieve__key i {
  width: 0.7rem;
  height: 0.7rem;
  border-radius: 28%;
  background: var(--brand);
}

.sieve__key i.is-out {
  background: color-mix(in oklab, var(--brand) 13%, transparent);
  box-shadow: inset 0 0 0 1px var(--brand-line);
}

.sieve__tip {
  position: absolute;
  left: var(--tx, 0);
  top: var(--ty, 0);
  z-index: 3;
  max-width: none;
  padding: 0.35rem 0.6rem;
  border-radius: 6px;
  background: var(--ink);
  color: var(--card);
  font-size: 0.76rem;
  font-weight: 600;
  white-space: nowrap;
  pointer-events: none;
  transform: translate(-50%, calc(-100% - 8px));
}

/* ---- The five checks ---- */

.sieve__steps {
  position: relative;
  display: grid;
  gap: 0.3rem;
}

/* The rail joining the numbers, so the list reads as one path */
.sieve__steps::before {
  content: "";
  position: absolute;
  inset-block: 1.9rem;
  inset-inline-start: calc(1rem + 1rem - 1px);
  width: 2px;
  border-radius: 2px;
  background: var(--line);
}

.sieve__step {
  position: relative;
  display: grid;
  grid-template-columns: 2rem minmax(0, 1fr) auto;
  grid-template-areas:
    "n what count"
    ". why  why";
  align-items: center;
  column-gap: 0.9rem;
  width: 100%;
  padding: 0.85rem 1rem;
  border: 0;
  border-radius: var(--r);
  background: none;
  font: inherit;
  color: inherit;
  text-align: start;
  cursor: pointer;
  transition: background-color 200ms var(--e-out), box-shadow 200ms var(--e-out);
}

@media (hover: hover) and (pointer: fine) {
  .sieve__step:hover {
    background: color-mix(in oklab, var(--card) 60%, transparent);
  }
}

.sieve__step:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

.sieve__n {
  grid-area: n;
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  background: var(--card);
  box-shadow: inset 0 0 0 1px var(--line);
  font-family: Gabarito, sans-serif;
  font-size: 0.85rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
  transition: background-color 260ms var(--e-out), color 260ms var(--e-out);
}

.sieve__what {
  grid-area: what;
  font-family: Gabarito, sans-serif;
  font-size: 1.02rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--ink);
}

.sieve__count {
  grid-area: count;
  display: grid;
  justify-items: end;
  font-family: Gabarito, sans-serif;
  font-size: 1.15rem;
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

.sieve__count small {
  font-family: Manrope, sans-serif;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0;
  color: var(--ink-3);
}

.sieve__why {
  grid-area: why;
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  font-size: 0.9rem;
  line-height: 1.5;
  color: var(--ink-2);
  transition: grid-template-rows 340ms var(--e-out), opacity 240ms var(--e-out), padding 340ms var(--e-out);
}

.sieve__why > span {
  min-height: 0;
  overflow: hidden;
}

.sieve__step.is-past .sieve__n {
  background: var(--brand-wash);
  box-shadow: none;
  color: var(--brand);
}

.sieve__step[aria-pressed="true"] {
  background: var(--card);
  box-shadow: 0 1px 0 var(--line), 0 16px 32px -26px oklch(21% 0.028 288 / 0.5);
}

.sieve__step[aria-pressed="true"] .sieve__n {
  background: var(--brand);
  box-shadow: none;
  color: var(--card);
}

.sieve__step[aria-pressed="true"] .sieve__why {
  grid-template-rows: 1fr;
  opacity: 1;
  padding-top: 0.4rem;
}

@media (max-width: 52rem) {
  .sieve {
    grid-template-columns: minmax(0, 1fr);
    gap: 1.75rem;
  }
}

@media (max-width: 40rem) {
  .sieve__step {
    grid-template-columns: 1.75rem minmax(0, 1fr) auto;
    column-gap: 0.75rem;
    padding: 0.75rem 0.7rem;
  }

  .sieve__n {
    width: 1.75rem;
    height: 1.75rem;
  }

  .sieve__steps::before {
    inset-inline-start: calc(0.7rem + 0.875rem - 1px);
  }

  .sieve__big {
    font-size: 3.1rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  .sieve__dot,
  .sieve__why {
    transition: none;
  }
}

/* ===========================================================================
   FOOTER

   The same footer on every page. Brand and contact on the left, the four
   link groups on the right, each group marked in its role colour. It sits on
   the page's tint so it reads as the floor of the site rather than one more
   band, and ends on the name, set large and faint, cropped at the bottom
   edge, the way the home hero ends on it.
   =========================================================================== */

.footer {
  position: relative;
  overflow: hidden;
  margin-top: 0;
  padding-block: clamp(3.5rem, 8vh, 5.5rem) 0;
  border-top: 0;
  background: var(--page-2);
  transition: background-color var(--swap) var(--e-swap);
}

.footer__top {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 2fr);
  gap: clamp(2.5rem, 6vw, 6rem);
  padding-bottom: clamp(2.5rem, 6vh, 3.5rem);
}

.footer__brand {
  display: grid;
  align-content: start;
  justify-items: start;
  gap: 1.1rem;
}

.footer__pitch {
  max-width: 32ch;
  font-size: 0.95rem;
  line-height: 1.6;
  color: var(--ink-2);
}

.footer__mail {
  padding-bottom: 2px;
  font-family: Gabarito, sans-serif;
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--ink);
  text-decoration: none;
  overflow-wrap: anywhere;
  background-image: linear-gradient(var(--brand), var(--brand));
  background-size: 100% 2px;
  background-position: 0 100%;
  background-repeat: no-repeat;
  transition: color 180ms var(--e-out);
}

.footer__nav {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(1.5rem, 3vw, 2.5rem);
}

.footer__h {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  margin-bottom: 1rem;
  font-family: Poppins, sans-serif;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink);
}

.footer__h::before {
  content: "";
  flex: none;
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 3px;
  background: var(--brand);
}

.footer__nav ul {
  display: grid;
  gap: 0.75rem;
  font-size: 0.93rem;
}

.footer__nav a {
  padding-bottom: 2px;
  color: var(--ink-2);
  text-decoration: none;
  background-image: linear-gradient(currentColor, currentColor);
  background-size: 0 1px;
  background-position: 0 100%;
  background-repeat: no-repeat;
  transition: color 180ms var(--e-out), background-size 260ms var(--e-out);
}

.footer__nav a[aria-current="page"] {
  color: var(--ink);
  background-size: 100% 1px;
}

@media (hover: hover) and (pointer: fine) {
  .footer__nav a:hover {
    color: var(--ink);
    background-size: 100% 1px;
  }

  .footer__mail:hover {
    color: var(--brand);
  }

  .footer__up:hover {
    color: var(--ink);
  }
}

.footer .footer__base {
  padding-block: 1.25rem;
  border-top: 1px solid color-mix(in oklab, var(--ink) 12%, transparent);
  font-size: 0.82rem;
  color: var(--ink-3);
}

.footer__up {
  font-weight: 600;
  color: var(--ink-2);
  text-decoration: none;
  transition: color 180ms var(--e-out);
}

.footer__word {
  max-width: none;
  margin: clamp(0.5rem, 2vw, 1.5rem) 0 -0.16em;
  font-family: Gabarito, sans-serif;
  font-size: clamp(5.5rem, 24vw, 23rem);
  font-weight: 800;
  line-height: 0.74;
  letter-spacing: -0.065em;
  text-align: center;
  white-space: nowrap;
  color: color-mix(in oklab, var(--brand) 16%, transparent);
  user-select: none;
  pointer-events: none;
  transition: color var(--swap) var(--e-swap);
}

@media (max-width: 62rem) {
  .footer__top {
    grid-template-columns: minmax(0, 1fr);
  }
}

@media (max-width: 46rem) {
  .footer__nav {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    row-gap: 2.25rem;
  }
}

@media (max-width: 40rem) {
  .footer .footer__base {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.6rem;
  }
}

/* ===========================================================================
   ABOUT: DEPTH AND THE RECORD

   The portraits get a quiet field behind them and a shadow in each person's
   colour; the why section gets its picture, one class record with the three
   views of it; the four lines Langford holds get a section of their own.
   =========================================================================== */

.about-team {
  position: relative;
  isolation: isolate;
}

.about-team::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: radial-gradient(var(--brand-line) 1px, transparent 1.3px);
  background-size: 30px 30px;
  opacity: 0.5;
  -webkit-mask-image: radial-gradient(ellipse 80% 55% at 50% 0%, #000 10%, transparent 75%);
  mask-image: radial-gradient(ellipse 80% 55% at 50% 0%, #000 10%, transparent 75%);
}

.about-team::after {
  content: "";
  position: absolute;
  inset: 35% 0 0;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(ellipse 60% 60% at 50% 45%, var(--brand-wash), transparent 70%);
}

.person__frame {
  position: relative;
  box-shadow: 0 30px 56px -38px color-mix(in oklab, var(--brand) 65%, oklch(21% 0.028 288));
}

/* A hairline on the photo edge, so a pale background never melts into the page */
.person__frame::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: inset 0 0 0 1px oklch(0% 0 0 / 0.1);
  pointer-events: none;
}

#why .story {
  align-items: center;
}

.record {
  display: grid;
  justify-items: center;
  gap: 1.1rem;
  margin: 0;
}

.record__stack {
  position: relative;
  width: min(100%, 29rem);
  padding: 0 1.6rem 1.6rem 0;
}

/* The other two views, stacked behind the one in front */
.record__ghost {
  position: absolute;
  inset: 0 1.6rem 1.6rem 0;
  border: 1px solid var(--brand-line);
  border-radius: calc(var(--r) + 4px);
  background: color-mix(in oklab, var(--brand) 5%, var(--card));
}

.record__ghost:nth-child(1) {
  transform: translate(1.6rem, 1.6rem);
}

.record__ghost:nth-child(2) {
  transform: translate(0.8rem, 0.8rem);
}

.record__card {
  position: relative;
  padding: clamp(1.1rem, 2.6vw, 1.6rem);
  border: 1px solid var(--line);
  border-radius: calc(var(--r) + 4px);
  background: var(--card);
  box-shadow: 0 30px 60px -40px oklch(21% 0.028 288 / 0.55);
}

.record__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--line-soft);
}

.record__title {
  font-family: Gabarito, sans-serif;
  font-size: 1.2rem;
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -0.025em;
  color: var(--ink);
}

.record__meta {
  margin-top: 0.3rem;
  font-size: 0.82rem;
  color: var(--ink-3);
}

.record__pill {
  flex: none;
  padding: 0.3rem 0.65rem;
  border-radius: 99px;
  background: color-mix(in oklab, var(--ok) 12%, transparent);
  font-size: 0.74rem;
  font-weight: 700;
  color: var(--ok);
}

.record__views {
  display: grid;
  gap: 0.55rem;
  padding-top: 1rem;
}

.record__views li {
  display: grid;
  grid-template-columns: 5.5rem minmax(0, 1fr);
  align-items: center;
  gap: 0.75rem;
  padding: 0.7rem 0.8rem;
  border-radius: var(--r-sm);
  background: color-mix(in oklab, var(--brand) 6%, var(--card));
}

.record__who {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-family: Poppins, sans-serif;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-2);
}

.record__who::before {
  content: "";
  flex: none;
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  background: var(--brand);
}

.record__what {
  font-size: 0.9rem;
  font-weight: 600;
  line-height: 1.4;
  color: var(--ink);
}

.record figcaption {
  max-width: 36ch;
  font-size: 0.86rem;
  text-align: center;
  color: var(--ink-3);
}

/* The three views arrive one after another once the card is in */
.record[data-reveal] .record__views li {
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 500ms var(--e-out), transform 500ms var(--e-out);
}

.record[data-reveal].is-in .record__views li {
  opacity: 1;
  transform: none;
}

.record[data-reveal].is-in .record__views li:nth-child(2) { transition-delay: 140ms; }
.record[data-reveal].is-in .record__views li:nth-child(3) { transition-delay: 280ms; }

@media (prefers-reduced-motion: reduce) {
  .record[data-reveal] .record__views li {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

.rules-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(2rem, 4vw, 3rem) clamp(2rem, 6vw, 5rem);
}

.rules-grid li {
  display: grid;
  align-content: start;
  gap: 0.55rem;
  padding-top: 1.25rem;
  border-top: 2px solid var(--ink);
}

.rules-grid b {
  font-family: Gabarito, sans-serif;
  font-size: clamp(1.25rem, 1.05rem + 0.7vw, 1.6rem);
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -0.03em;
  text-wrap: balance;
  color: var(--ink);
}

.rules-grid span {
  max-width: 44ch;
  font-size: 0.98rem;
  line-height: 1.6;
  color: var(--ink-2);
}

@media (max-width: 46rem) {
  .rules-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}

@media (max-width: 40rem) {
  .record__stack {
    padding: 0 1rem 1rem 0;
  }

  .record__ghost {
    inset: 0 1rem 1rem 0;
  }

  .record__ghost:nth-child(1) {
    transform: translate(1rem, 1rem);
  }

  .record__ghost:nth-child(2) {
    transform: translate(0.5rem, 0.5rem);
  }

  .record__views li {
    grid-template-columns: minmax(0, 1fr);
    gap: 0.3rem;
  }
}

/* ---- About: why, evened out ------------------------------------------------
   The heading ran to two lines in a narrow column while the three paragraphs
   were set at two sizes and two colours, and the record floated lower on the
   right, so the block read as uneven. The heading now spans the section on
   one line, and the words and the record start on one top edge, the words at
   one size, one colour and one measure. */
#why .head {
  max-width: none;
  margin-bottom: clamp(1.75rem, 4vh, 2.5rem);
}

#why .head h2 {
  max-width: none;
}

#why .story {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(2rem, 6vw, 5.5rem);
  align-items: start;
}

#why .story__lead {
  display: grid;
  align-content: start;
  gap: 1.1rem;
}

#why .story__lead p,
#why .story__lead p:first-of-type {
  max-width: 52ch;
  margin: 0;
  font-size: clamp(1rem, 0.96rem + 0.2vw, 1.1rem);
  line-height: 1.7;
  color: var(--ink-2);
}

#why .record {
  justify-items: stretch;
}

#why .record__stack {
  width: 100%;
  max-width: 31rem;
}

#why .record figcaption {
  text-align: start;
}

@media (max-width: 56rem) {
  #why .story {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ---- Sieve: proportion --------------------------------------------------------
   At a wide screen the squares grew to about 37px and the panel stood nearly
   twice the height of the list beside it. The panel is held to 25rem, so the
   two halves read as a pair. */
.sieve {
  grid-template-columns: minmax(0, 25rem) minmax(0, 1fr);
}

@media (max-width: 52rem) {
  .sieve {
    grid-template-columns: minmax(0, 1fr);
  }

  .sieve__viz {
    width: 100%;
    max-width: 28rem;
  }
}

/* Credit for the site's builder, in the footer's base line */
.footer__credit {
  white-space: nowrap;
  font-weight: 600;
  color: var(--ink-2);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
  transition: color 180ms var(--e-out);
}

@media (hover: hover) and (pointer: fine) {
  .footer__credit:hover {
    color: var(--brand);
  }
}


/* ---- Footer base line: the credit sized down, and a tidy phone layout ------
   The credit sits a step below the other base-line text so it reads as a
   signature rather than a heading. On a phone the four items were stacked one
   under another and the tagline broke with a single word on its last line:
   now the copyright and Back to top share the first line, the tagline sits
   under them in two even lines, and the credit closes the block. */
.footer__by {
  font-size: 0.74rem;
  color: var(--ink-3);
}

.footer__credit {
  font-weight: 500;
}

@media (max-width: 40rem) {
  .footer .footer__base {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "copy up"
      "line line"
      "by   by";
    align-items: center;
    gap: 0.6rem 1rem;
    padding-block: 1.25rem;
  }

  .footer__base > span:first-child {
    grid-area: copy;
  }

  .footer__base > span:nth-child(2) {
    grid-area: line;
    max-width: 36ch;
    text-wrap: balance;
  }

  .footer__by {
    grid-area: by;
  }

  .footer__up {
    grid-area: up;
    justify-self: end;
  }
}

/* ---- Footer name on phones: as large as on desktop --------------------------
   The clamp floor left the name at about three quarters of a phone's width, so
   it read small where the desktop one spans the page. Sized to the viewport
   here it runs edge to edge (the word is about 3.4em wide), with the same
   crop at the floor. */
@media (max-width: 40rem) {
  .footer__word {
    font-size: 27vw;
  }
}
