/* ============================================================
   iChess Academy — Stage 1
   Tokens, type, header/nav, hero, proof stat band, footer.
   ============================================================ */

@property --beam-angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

:root {
  --ink:          #0E1621;
  --ink-2:        #18232F;
  --ivory:        #F7F4EE;
  --ivory-2:      #EFEAE1;
  /* Not pure #FFF: the palette is warm, and a cool white card on --ivory reads
     as a different material. This is one step lighter than --ivory, enough for
     the hero achievement cards to lift off the section without a colour shift. */
  --white:        #FDFCFA;
  --gold:         #C2A14D;
  --gold-deep:    #7E6520;
  --gold-soft:    #E4D9BC;
  --muted:        #5F6673;
  --muted-dark:   #8792A1;
  --line:         #D9D3C7;
  --line-dark:    #2A3746;
  --btn-text:     #12100A;

  /* Newsreader: h1, h2, the stat band numerals, and the founder's name
     under his photo. Archivo: everything else, including h3. This is a
     role split, not a size threshold — see DESIGN.md. */
  --serif: 'Newsreader', 'Iowan Old Style', Georgia, serif;
  --sans:  'Archivo', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

  --max:          1140px;
  /* Sticky-header height, used only by scroll-padding-top so anchored sections
     land below the header instead of behind it. 76px .navin + 1px border.
     Overridden to 85px at >=900px, where .is-scrolled .navin is 84px — that is
     the state the header is always in during an anchor jump. */
  --header-h:     77px;
  --pad-section:  clamp(56px, 9vw, 96px);
  --shadow-card:  0 1px 2px rgba(14,22,33,.05), 0 10px 30px rgba(14,22,33,.06);
  --transition:   200ms ease;

  /* Alpha of the checkerboard squares behind #results, kept here so it can be
     nudged in one place. Applied to the ink tint, matching the hero and body
     textures.

     This board is unmasked and covers the whole section, so the alpha is now
     the ONLY control over how loud it is — there is no fade to hide behind,
     and every value lands behind the quote as well as in the corners. Measured
     square-to-square contrast against ivory, at 1440px:

       .055  12.2/255   double the hero; square edges cut through the quote
       .030   7.1/255   still heavier than the hero
       .022   5.8/255   a dead match for the hero, not lighter
       .018   4.8/255   ~19% under the hero — visible, clearly softer

     .018 is the value that satisfies both halves of the brief: legible as a
     board across the full section, and unmistakably lighter than the hero's
     .028/96px. It reads as an overcorrection when masked down to two corner
     fragments, which is what it was last used for — over a full section the
     field of repetition carries it. Do not read the old "too faint" note as
     applying here. */
  --testimonial-pattern-opacity: 0.018;
}

/* ------------------------------------------------------------
   Reset & base
   ------------------------------------------------------------ */

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

/* Every in-page anchor glides rather than jumps, and lands clear of the sticky
   header. #top resolves to a negative target once the padding is subtracted,
   which the browser clamps to 0 — exactly the true top we want. */
html {
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h) + 16px);
}

/* Chessboard page texture. A repeating-conic-gradient rather than an image file:
   the gradient's four quadrants land on the tile's four corners, so a 128px tile
   gives 64px squares in true board alternation with nothing to download. It sits
   on the page ground, so anything painting an opaque background over it — .dark,
   the reach map, .pricing-card — hides it without needing a rule of its own.
   .band carries the same image (see the shared rule below) so both ivory tones
   are textured and it doesn't switch on and off from section to section.

   The two tints are written literally, not as tokens: the silver exists for this
   texture alone and must not become available to anything else on the site.
   Both sit at the threshold of visibility — this should register as a faint
   grain in the paper, never as a legible grid. Raise them and it reads as
   squares; if they are changed, change both by the same amount so neither
   colour starts to dominate. */
body,
.band {
  background-image: repeating-conic-gradient(
    rgba(194,161,77,.030) 0% 25%,      /* gold */
    rgba(150,158,168,.030) 0% 50%);    /* silver — texture only, nowhere else */
  background-size: 128px 128px;
}

body {
  margin: 0;
  background-color: var(--ivory);
  color: var(--ink);
  font-family: var(--sans);
  font-weight: 400;
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  font-optical-sizing: auto;
  font-synthesis: none;
}

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

a { color: inherit; }

.wrap {
  max-width: var(--max);
  margin: 0 auto;
  padding: 0 24px;
}

@media (min-width: 640px) {
  .wrap { padding: 0 28px; }
}

section { padding: var(--pad-section) 0; }

/* ------------------------------------------------------------
   Type scale
   ------------------------------------------------------------ */

h1, h2, h3 {
  margin: 0 0 18px;
}

h1 {
  font-family: var(--serif);
  font-size: clamp(42px, 5.8vw, 74px);
  line-height: 1.05;
  font-weight: 500;
  letter-spacing: -.02em;
}

h2 {
  font-family: var(--serif);
  font-size: clamp(31px, 3.7vw, 48px);
  line-height: 1.12;
  font-weight: 500;
  letter-spacing: -.015em;
}

h3 {
  font-family: var(--sans);
  font-size: clamp(18px, 1.4vw, 22px);
  line-height: 1.35;
  font-weight: 600;
  letter-spacing: -.005em;
}

p { margin: 0 0 16px; max-width: 62ch; }

.lede {
  font-size: clamp(17px, 1.5vw, 19px);
  max-width: 52ch;
  color: var(--muted);
}

.small { font-size: 14px; color: var(--muted); }

/* ------------------------------------------------------------
   Section eyebrow
   ------------------------------------------------------------ */

.eyebrow {
  font-family: var(--sans);
  font-size: 11px;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--gold-deep);
  font-weight: 600;
  margin: 0 0 20px;
  display: flex;
  align-items: baseline;
}

.dark { background: var(--ink); color: var(--ivory); }
.dark .eyebrow { color: var(--gold); }
.dark .lede { color: var(--muted-dark); }
.dark .small { color: var(--muted-dark); }

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

.btn {
  display: inline-block;
  background: var(--gold);
  color: var(--btn-text);
  padding: 16px 32px;
  border-radius: 2px;
  text-decoration: none;
  font-family: var(--sans);
  font-size: 16px;
  font-weight: 600;
  letter-spacing: .02em;
  border: 1px solid var(--gold);
  cursor: pointer;
  transition: background var(--transition), color var(--transition), border-color var(--transition);
}

.btn:hover,
.btn:focus-visible {
  background: var(--ink);
  color: var(--gold);
  border-color: var(--ink);
}

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

.btn.ghost:hover,
.btn.ghost:focus-visible {
  background: var(--ink);
  color: var(--gold);
  border-color: var(--ink);
}

.btn.sm { padding: 11px 20px; font-size: 14px; font-weight: 500; }

.textlink {
  font-size: 14.5px;
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid var(--gold);
  padding-bottom: 2px;
  transition: opacity var(--transition);
}

.textlink:hover,
.textlink:focus-visible { opacity: .72; }

/* Focus outlines — every interactive element, never removed without replacement */
a:focus-visible,
button:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

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

.grid2 {
  display: grid;
  grid-template-columns: 1fr;
  gap: 36px;
  align-items: start;
}

@media (min-width: 900px) {
  .grid2 {
    grid-template-columns: 1.05fr .95fr;
    gap: 64px;
  }
}

/* ------------------------------------------------------------
   Header / navigation
   Header background is a solid --ivory at all times (not the
   wireframe's translucent + backdrop-blur treatment): the
   header logo uses mix-blend-mode: multiply to drop its baked
   white background, and multiply only reads correctly against
   a flat, predictable, fully opaque backdrop. Both this solid
   background and the multiply blend are a workaround for the
   source PNG's opaque background — remove both once a
   transparent export of the logo exists.
   ------------------------------------------------------------ */

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--ivory);
  border-bottom: 1px solid var(--line);
  transition: border-color var(--transition), box-shadow var(--transition);
}

.site-header.is-scrolled {
  border-bottom-color: var(--muted);
  box-shadow: var(--shadow-card);
}

.navin {
  height: 76px;
  padding: 0 24px;
  display: flex;
  align-items: center;
  gap: 26px;
  transition: height var(--transition);
}

@media (min-width: 640px) {
  .navin { padding: 0 28px; }
}

.brand { display: inline-flex; align-items: center; }

.brand img {
  height: 52px;
  width: auto;
  mix-blend-mode: multiply;
  transition: height var(--transition);
}

/* Caps across all three navs (here, .mobile-nav a, .foot-nav a). 13px because
   capitals read larger than their point size and the .06em tracking widens the
   row further — the horizontal rows are width-constrained and cannot absorb
   both. gap is 20px rather than 26px for the same reason: at a 900px viewport
   the row shares 844px with a 240px logo and a ~185px CTA, and 26px gaps do
   not fit. See the note in .mobile-nav a for why the panel keeps 16px. */
.navlinks {
  list-style: none;
  margin: 0 0 0 auto;
  padding: 0;
  display: none;
  gap: 20px;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}

/* Underline sweeps in from the left on hover/focus. Shared with
   .foot-nav a below — same gesture in both navs. */
.navlinks a {
  position: relative;
  text-decoration: none;
  color: inherit;
  transition: color var(--transition);
}

.navlinks a::after,
.foot-nav a::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -3px;
  height: 1px;
  background: var(--gold);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 220ms ease;
}

.navlinks a:hover::after,
.navlinks a:focus-visible::after,
.foot-nav a:hover::after,
.foot-nav a:focus-visible::after { transform: scaleX(1); }

.navlinks a:hover,
.navlinks a:focus-visible { color: var(--gold-deep); }

/* Visible at every width. It used to be display:none below 900px, which left
   phones with no call to action until the #book section ~8000px down. The
   sub-900px sizing that makes it fit alongside the logo and the hamburger is
   in the max-width blocks below. */
.header-cta { display: inline-block; }

/* Label swap for the header CTA only. The mobile-nav and #book CTAs keep the
   full string — they have a full row to themselves. Both spans are always in
   the markup and CSS picks one; display:none also takes the hidden one out of
   the accessibility tree, so only ever one label is exposed. The anchor's
   aria-label carries the full name at every width regardless. */
.cta-short { display: none; }

.nav-toggle {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  background: transparent;
  border: none;
  cursor: pointer;
}

.nav-toggle-bars,
.nav-toggle-bars::before,
.nav-toggle-bars::after {
  display: block;
  width: 22px;
  height: 2px;
  background: var(--ink);
  transition: transform var(--transition), opacity var(--transition);
}

.nav-toggle-bars { position: relative; }

.nav-toggle-bars::before,
.nav-toggle-bars::after {
  content: "";
  position: absolute;
  left: 0;
}

.nav-toggle-bars::before { top: -7px; }
.nav-toggle-bars::after { top: 7px; }

.nav-toggle[aria-expanded="true"] .nav-toggle-bars { background: transparent; }
.nav-toggle[aria-expanded="true"] .nav-toggle-bars::before { transform: translateY(7px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle-bars::after { transform: translateY(-7px) rotate(-45deg); }

.mobile-nav {
  border-top: 1px solid var(--line);
  background: var(--ivory);
  padding: 22px 24px 28px;
}

.mobile-nav[hidden] { display: none; }

.mobile-nav ul {
  list-style: none;
  margin: 0 0 22px;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

/* Caps and tracking as the other two navs, but the size stays 16px. This is a
   vertical list of full-width tap targets with no fitting pressure at all, so
   dropping to 13px would buy nothing and cost touch legibility on the one nav
   that is only ever used on a phone. */
.mobile-nav a {
  display: block;
  padding: 12px 0;
  border-bottom: 1px solid var(--line);
  text-decoration: none;
  color: var(--ink);
  font-size: 16px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
}

@media (min-width: 900px) {
  /* 84px .is-scrolled .navin + 1px border. The header is always scrolled by the
     time an anchor jump happens, so this — not the 112px resting height — is
     what scroll-padding-top has to clear. */
  :root { --header-h: 85px; }

  .navlinks { display: flex; }
  .nav-toggle, .mobile-nav { display: none; }

  .navin { height: 112px; }
  .brand img { height: 80px; }
  .site-header.is-scrolled .navin { height: 84px; }
  .site-header.is-scrolled .brand img { height: 56px; }
}

/* ------------------------------------------------------------
   Header below 900px — fitting logo + CTA + hamburger.

   The budget is tight and the arithmetic is worth recording, because
   the obvious lever (shrinking the logo) is not the binding one.
   Content width is the viewport minus .navin's 48px of padding: 327px
   at 375, 272px at 320. With the CTA shown, .navin has four flex
   children (.brand, the empty <nav>, .header-cta, .nav-toggle), so
   three gaps apply — 78px at the resting 26px gap, before a single
   glyph is drawn. The hamburger is 40px and the logo is 3:1 (1200x400
   intrinsic), so 156px wide at height:52px.

   "Book a Demo Session" at 13px measures ~150px with padding. That is
   ~317px of content at 320px against a 272px budget: the LABEL is what
   overflows, not the logo, which is why the CTA carries a short label
   here and the full one returns at 900px.
   ------------------------------------------------------------ */
@media (max-width: 899px) {
  .navin { gap: 16px; }

  .header-cta {
    font-size: 13px;
    padding: 9px 14px;
    white-space: nowrap;
  }

  .cta-full { display: none; }
  .cta-short { display: inline; }
}

/* Smallest phones: the 375px fit above still overflows 320px, so the
   logo drops to 44px (132px wide) and the gaps tighten again. 360px
   clears it without either, so this stops at 359px. */
@media (max-width: 359px) {
  .navin { gap: 10px; }
  .brand img { height: 44px; }
}

/* Skip link */
.skip-link {
  position: absolute;
  left: 16px;
  top: -60px;
  background: var(--ink);
  color: var(--gold);
  padding: 12px 20px;
  border-radius: 2px;
  text-decoration: none;
  font-size: 14px;
  font-weight: 600;
  z-index: 100;
  transition: top var(--transition);
}

.skip-link:focus-visible { top: 16px; outline: 2px solid var(--gold); outline-offset: 2px; }

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

.hero {
  padding-top: clamp(32px, 4vw, 56px);
  padding-bottom: var(--pad-section);
  position: relative;
  /* Opaque, so the body's page-wide 64px texture stops at the hero and does not
     moiré against the hero's own 96px board below. Same colour as the body, so
     no seam appears where the hero ends. Same technique .band uses. */
  background-color: var(--ivory);
}

/* The hero's own chessboard, distinct from the page texture: 96px squares (192px
   tile — the conic gradient's four quadrants land on the tile's four corners),
   deliberately coarser than the body's 128px grain so the two do not moiré. It
   is a pseudo-element, not a background on .hero itself, because the mask that
   dissolves it into the section below would otherwise fade the hero's content
   with it.

   One tone against transparent, not two tints against each other. The previous
   gold/silver pair differed by only ~0.1/255 in LUMINANCE — the channel the eye
   actually resolves in a fine repeating pattern — so it was invisible at any
   alpha, and raising the alpha (twice) scaled the wrong quantity: it scaled a
   hue difference the eye cannot resolve at this frequency. A single ink tint
   against bare section ivory gives a 6.2/255 luminance step. Move the alpha if
   this needs to be quieter or louder; do not reintroduce a second tint. */
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image: repeating-conic-gradient(
    rgba(14,22,33,.028) 0% 25%,
    rgba(14,22,33,0)    0% 50%);
  background-size: 192px 192px;
  /* Full strength for the top 70%, dissolving across the bottom 30% so the board
     does not end on a hard line above the stat band. */
  -webkit-mask-image: linear-gradient(to bottom, #000 70%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 70%, transparent 100%);
}

.hero > .wrap { position: relative; z-index: 1; }

/* ------------------------------------------------------------
   Hero load entrance — CSS only, no class and no JS.

   animation-fill-mode: backwards is what makes this safe: the
   from-state applies only during the delay, so the stylesheet's
   normal state stays fully visible. A JS-driven version (base
   opacity: 0 plus a class) leaves the hero blank if the script
   fails or is slow — the one failure this must not have.

   The hero's right column is the chess artwork, which carries its
   own entrance — see the hero artwork block below.
   ------------------------------------------------------------ */

@keyframes hero-rise {
  from { opacity: 0; translate: 0 16px; }
}

.hero h1,
.hero .lede {
  animation: hero-rise 640ms cubic-bezier(.22,.61,.36,1) backwards;
}

.hero .lede { animation-delay: 110ms; }

/* ------------------------------------------------------------
   Hero photograph. Replaced the .hero-placeholder panel.

   The crop is CSS-only: the container's ratio is 455 / 252 — 90%
   of the file's natural 455 / 280 — and the img sits at
   width:100% / height:auto inside it, so the image's own
   proportions are untouched and its bottom tenth simply overflows
   and is clipped. No object-fit: cover, which would crop the
   sides as well and change the shape.

   Expressing the crop as a ratio rather than a pixel height keeps
   it at exactly 90% at every width, including mobile where the
   image renders narrower than its natural 455px.

   max-width is the file's own pixel width (455px), not a round
   number: the source is a 455x280 lossy WebP of about 14 KB, and
   anything wider upscales it. The desktop right column is ~485px,
   so the cap does leave a little slack — that is deliberate.
   ------------------------------------------------------------ */

.hero-media {
  width: 100%;
  max-width: 455px;
  aspect-ratio: 455 / 252;
  overflow: hidden;
  border-radius: 10px;
  border: 1px solid var(--line);
  box-shadow: var(--shadow-card);
}

.hero-media img {
  width: 100%;
  height: auto;
  display: block;
  image-rendering: auto;
}

/* ------------------------------------------------------------
   Credential rail — the founder's three headline credentials,
   sitting on the section ivory directly beneath the photo. It
   shares the photo's grid column, so its width tracks the photo
   with nothing to keep in sync. Replaced the old .founder-stats
   dark box, which put four stats immediately above the full-bleed
   proof band and read as eight statistics in a row.
   ------------------------------------------------------------ */

.cred-rail {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  border-top: 1px solid var(--line);
  margin-top: 24px;
}

.cred {
  padding: 20px 8px;
  text-align: center;
}

.cred + .cred { border-left: 1px solid var(--line); }

/* --gold-deep, not --gold: at 26px on ivory the lighter gold fails
   contrast. The dark bands can use --gold because they sit on --ink. */
.cred .n {
  font-family: var(--serif);
  font-size: 26px;
  font-weight: 500;
  color: var(--gold-deep);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

/* Hold the counter's final width while it counts up, so the rules
   either side don't shift. Moved here from .stat .n with the stats. */
.cred .n[data-count="2328"] { min-width: 4ch; }
.cred .n[data-count="15"] { min-width: 3ch; }

.cred .l {
  font-family: var(--sans);
  font-size: 10.5px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--muted);
  margin-top: 6px;
}

@media (max-width: 899px) {
  .cred .n { font-size: 22px; }
}

/* ------------------------------------------------------------
   Proof stat band — full-bleed, outside the 1140px container
   ------------------------------------------------------------ */

.stat-band {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1px;
  background: var(--line-dark);
}

.stats.stat-band {
  width: 100vw;
  margin-left: calc(50% - 50vw);
  padding: 0;
}

@media (min-width: 900px) {
  .stats.stat-band { grid-template-columns: repeat(3, 1fr); }
}

.stat {
  background: var(--ink);
  padding: 34px 24px;
  text-align: center;
}

.stat .n {
  font-family: var(--serif);
  font-size: 42px;
  font-weight: 500;
  color: var(--gold);
  line-height: 1;
  margin-bottom: 9px;
  font-variant-numeric: tabular-nums;
}

.stat .n[data-count="30"] { min-width: 2ch; }
.stat .n[data-count="8"] { min-width: 1ch; }
.stat .n[data-count="5"] { min-width: 1ch; }

/* The one non-counting figure in a counting group ("IM") holds back until
   its neighbours have finished, so it doesn't sit static beside a run of
   moving numerals. Applies to both groups: the full-bleed proof band and
   the hero credential rail. */
.stat-static .n,
.cred-static .n { opacity: 1; }

.stat-band.is-counting .stat-static .n,
.cred-rail.is-counting .cred-static .n { opacity: 0; }

.stat-band.is-counted .stat-static .n,
.cred-rail.is-counted .cred-static .n {
  opacity: 1;
  transition: opacity 400ms ease;
}

.stat .l {
  font-size: 12px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted-dark);
}

/* ------------------------------------------------------------
   Section heading reveal — eyebrow, then h2, then lede, on the
   section entering view. Applies to every section except the
   hero, which has its own load entrance.

   .head-reveal and the --d delay are applied by initHeadReveals()
   in script.js, so the markup carries no reveal plumbing. The
   trigger class is .is-heads-in, deliberately NOT .is-visible:
   #pricing and #students already use .is-visible for their own
   reveals, and adding it here would fire those at this
   observer's threshold instead of their own.
   ------------------------------------------------------------ */

@keyframes head-in {
  from { opacity: 0; translate: 0 10px; }
}

.is-heads-in .head-reveal {
  animation: head-in 520ms ease-out backwards;
  animation-delay: calc(var(--d) * 1ms);
}

/* ------------------------------------------------------------
   Section band (ivory-2) — alternates with the default ivory
   section background for rhythm: a1/c3 ivory, b2/d4 band.
   ------------------------------------------------------------ */

/* The ivory-2 fill is opaque and would hide the body's board texture, so .band
   carries the same background-image — see the shared body, .band rule up in the
   base styles. Only the fill colour differs here. Tile alignment with the body
   is not maintained (a section's own box is the origin); at this tint the seam
   is not perceptible, and matching it would mean measuring every section offset. */
.band { background-color: var(--ivory-2); }

/* ------------------------------------------------------------
   Visually hidden utility (screen-reader-only text)
   ------------------------------------------------------------ */

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ------------------------------------------------------------
   a1 · The approach
   ------------------------------------------------------------ */

#approach { overflow-x: clip; }

/* ------------------------------------------------------------
   Two columns from 900px: the argument on the left, the founder
   on the right, with the achievement strip spanning both below.

   The text lives in its own .section-col grid item rather than
   sitting as loose children of .wrap. An earlier version put the
   grid on .wrap directly and gave the figure grid-row: 1 / span 4
   to sit beside the four text elements — but a grid item taller
   than the tracks it spans has its excess height distributed
   ACROSS those tracks, so the 997px figure inflated the eyebrow's
   row to 150px, the h2's to 238px and so on, and the four text
   elements were stretched apart with their text stranded at the
   top of each row. Nothing on the container could undo that:
   align-content was not spreading the rows, the rows themselves
   had grown. One item per column is the fix.
   ------------------------------------------------------------ */

#approach .wrap {
  display: grid;
  grid-template-columns: 1fr;
}

/* Single column: the photo leads, the argument follows. */
.approach-figure {
  order: -1;
  margin-bottom: 44px;
}

@media (min-width: 900px) {
  #approach .wrap {
    grid-template-columns: 1.05fr .95fr;
    column-gap: 64px;
    /* Both columns hug their own content at the top instead of stretching to
       the taller one's height. */
    align-items: start;
  }

  .approach-figure {
    order: 0;
    margin-bottom: 0;
  }

  #approach .hero-credentials { grid-column: 1 / -1; }
}

/* Deliberate rhythm down the left column. Normal margin collapsing is back now
   that these are in a plain block container, so lede -> bullets is the bullets'
   own 32px collapsing against the lede's 16px, and only the h2 needs nudging
   off the global 18px. */
#approach h2 { margin-bottom: 20px; }

/* One source of truth for how wide the photo actually renders. The name and role
   below it are bound to the same value (see .approach-name), so the text centres
   against the PHOTO rather than the column and the two cannot drift apart. */
.approach-figure {
  text-align: center;
  --photo-w: 260px;
}

@media (min-width: 900px) {
  .approach-figure { --photo-w: 340px; }
}

/* Square, and cropped high: object-position lands the frame on the face rather
   than the centre of an 800x800 half-body source.

   height: auto is load-bearing, not tidying. The <img> carries width="800"
   height="800", and those attributes map to CSS presentational hints. An author
   `width` overrides the width hint, but with no author `height` the hint's
   height: 800px survives — and once both axes are definite, aspect-ratio is
   ignored outright. That is what previously produced a 340x800 box, which
   border-radius: 50% then drew as a tall ellipse. Removing height: auto brings
   the ellipse straight back. */
.approach-photo {
  width: 100%;
  max-width: var(--photo-w);
  height: auto;
  margin: 0 auto;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  object-position: center 38%;
  border-radius: 14px;
  border: 1px solid var(--line);
  box-shadow: var(--shadow-card);
}

/* Both lines are <p>, so they inherit the global `p { max-width: 62ch }` from the
   type scale. Their horizontal margins were 0, not auto, so once 62ch computed
   NARROWER than the figure column the paragraph box sat flush left and its centred
   text no longer lined up with the photo above it. At 13px Archivo that is ~443px
   against a ~484px column, which pulled the role line ~21px left of the photo's
   centre; the 20px Newsreader name resolved wider than the column and so clamped
   and stayed correct. Capping both at the photo's own width and centring the boxes
   with auto margins ties the text to the photo at every width — max-width rather
   than width so text and photo shrink together below --photo-w. */
.approach-name,
.approach-role {
  max-width: var(--photo-w);
  margin-left: auto;
  margin-right: auto;
}

.approach-name {
  margin-top: 28px;
  margin-bottom: 2px;
  font-family: var(--serif);
  font-size: 20px;
  font-weight: 500;
  color: var(--ink);
}

.approach-role {
  margin-top: 0;
  margin-bottom: 0;
  font-family: var(--sans);
  font-size: 13px;
  color: var(--muted);
}

/* The argument, as four bullets. The marker is an aria-hidden <svg><use> of the
   sprite's rook, sitting as a flex item alongside the li's own text — same
   layout as #fit's checkmark list. It replaced an ink dot drawn with ::before
   plus a gold halo pulsing on ::after; a sprite symbol needs a real element to
   reference it, so the marker moved into the markup. @keyframes reach-pulse is
   no longer used here but stays defined below for #students' .pulse-ring. */
.approach-bullets {
  list-style: none;
  margin: 32px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 28px;
}

.approach-bullets li {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 18px;
  font-family: var(--sans);
  font-weight: 400;
  font-size: clamp(16px, 1.4vw, 18px);
  line-height: 1.65;
  color: var(--ink);
  max-width: 62ch;
}

/* Google Fonts only loads Archivo 400/500/600, and body sets
   font-synthesis:none as a guard against requesting an unloaded weight —
   a bare <strong> (bold/700) would fall to browser fallback instead of a
   guaranteed 600, so the weight is set explicitly here. */
.approach-bullets li strong {
  font-weight: 600;
  color: var(--ink);
}

/* The marker is the sprite's rook, used as-is from the inline <symbol> (see the
   <svg><use> in each <li>) — it has to be a real element because a pseudo-element
   cannot reference a symbol. The rook's own edge keeps a static drop-shadow rather
   than a box-shadow: box-shadow follows the element's BOX, so on an SVG it would
   draw a rectangular halo instead of hugging the silhouette. The pulse itself lives
   on li::before below, for the reason recorded there.
   margin-top optically centres the 18px icon on the first line (~30px line box at
   this size and line-height: 1.65), matching where the dot sat. */
.approach-bullets li .approach-bullet-icon {
  flex: none;
  width: 18px;
  height: 18px;
  margin-top: .35em;
  fill: var(--ink);
  /* Static warmth on the rook's own edge. The PULSE is not here — see li::before.
     A pulsing drop-shadow was measured at a 10/255 peak-to-trough difference with
     0% of pixels shifting more than 20: drop-shadow traces the alpha silhouette,
     and a small thin glyph has almost no perimeter to emanate from, so the effect
     was real, running, and invisible. */
  filter: drop-shadow(0 0 6px rgba(194,161,77,.45));
  position: relative;
  z-index: 1;
}

/* The pulsing glow, as a shape of its own behind the rook. A radial gradient has
   a solid body to brighten, so it survives compositing over the ivory band where
   a silhouette-tracing drop-shadow did not — measured 37/255 peak-to-trough with
   ~11% of pixels shifting more than 20, against the drop-shadow's 10/255 and 0%.
   The gradient holds near-full strength out to 38% before falling off, because
   the centre is hidden behind the rook: the only part anyone sees is the annulus
   around it, so that is the part that has to carry the colour.

   Geometry: the 18px icon sits at the li's left edge, so its centre is 9px across
   and (.35em + 9px) down. The 28px glow is offset by half its own size from that
   centre — hence 9 - 14 and .35em + 9 - 14. */
.approach-bullets li::before {
  content: "";
  position: absolute;
  left: -5px;
  top: calc(.35em - 5px);
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: radial-gradient(circle,
    rgba(194,161,77,.72) 0%,
    rgba(194,161,77,.62) 38%,
    rgba(194,161,77,0) 72%);
  filter: blur(3px);
  pointer-events: none;
  z-index: 0;
  animation: rook-glow 2.8s ease-in-out infinite;
}

@keyframes rook-glow {
  0%, 100% { opacity: .35; transform: scale(.78); }
  50%      { opacity: 1;   transform: scale(1.28); }
}

/* Staggered across the 2.8s cycle so the four never brighten together — four
   markers pulsing in unison read as a loading state rather than decoration.
   :nth-child rather than a --i custom property per <li>: the list is four static
   items, so this needs no markup. */
.approach-bullets li:nth-child(1)::before { animation-delay: 0s; }
.approach-bullets li:nth-child(2)::before { animation-delay: .7s; }
.approach-bullets li:nth-child(3)::before { animation-delay: 1.4s; }
.approach-bullets li:nth-child(4)::before { animation-delay: 2.1s; }


/* ------------------------------------------------------------
   b2 · The process — single-column stepper. The spine
   (.step::before) and the chevrons between cards
   (.step::after) are decorative pseudo-elements so the <ol>
   stays pure content for assistive tech; the step-num circle
   (.step-num) is a real element since it carries the visible
   "01"–"08" glyph. Badges are CENTRED in each row
   (align-items: center) so a badge sits level with the middle of
   its card rather than its top edge. That makes a badge's centre
   cardHeight/2 — a different number per step, since some titles
   wrap to two lines — so the spine can no longer be one absolute
   bar with fixed end offsets. It is drawn instead as a per-step
   segment: each .step::before spans its own row plus the gap
   below it, and the first and last are trimmed to 50%, which IS
   the badge centre under align-items: center. No measured
   constants, correct at any card height and any breakpoint.
   On scroll into view the spine draws first (8 segments × 112ms
   ≈ 900ms, staggered so it reads as one line drawing downward),
   then each card fades in translateY(12px)→0, staggered 80ms
   apart, starting only after the spine finishes (transition-delay:
   900ms + i*80ms).
   ------------------------------------------------------------ */

.timeline {
  list-style: none;
  margin: 44px auto 0;
  padding: 0;
  position: relative;
  max-width: 620px;
  display: flex;
  flex-direction: column;
  row-gap: var(--step-gap);
  /* The single axis the spine, the badges and the chevrons all sit on.
     .step-num is a 44px flex item at the row's left edge, so its centre is
     deterministically 22px in; the spine and chevrons centre on the same
     value via translateX(-50%) rather than carrying their own offsets. */
  --spine-x: 22px;
  /* One source of truth for the row gap: .step::before reaches exactly this far
     past its own row to meet the next segment, so the two cannot drift apart. */
  --step-gap: 32px;
}

.step {
  position: relative;
  display: flex;
  align-items: center;
  gap: 24px;
}

/* One spine segment per step: its own full height plus the gap below, so
   consecutive segments butt together into a continuous line from 01 to 08.
   The badges are opaque and sit above it (z-index: 1), so the line passing
   behind a circle is never seen. */
.step::before {
  content: "";
  position: absolute;
  left: var(--spine-x);
  top: 0;
  bottom: calc(-1 * var(--step-gap));
  width: 2px;
  background: var(--line);
  transform: translateX(-50%) scaleY(0);
  transform-origin: top;
  transition: transform 112ms linear;
  transition-delay: calc(var(--i) * 112ms);
}

/* Trim the ends to the badge centres. Under align-items: center, 50% of the
   step is exactly where .step-num's centre sits. */
.step:first-child::before { top: 50%; }
.step:last-child::before { bottom: 50%; }

.timeline.is-visible .step::before {
  transform: translateX(-50%) scaleY(1);
}

/* The card reveal sits on the two real children, not on the <li>. The spine is
   now .step::before, so fading the <li> would fade its own spine segment with
   it and the line could never draw ahead of the cards. Animation rather than a
   base opacity: 0 — same reasoning as #pedagogy's reveal: without JS the steps
   simply render, nothing hidden, and hover's transform is left untouched
   because an animation and a transition do not contend for the same property. */
@keyframes step-in {
  from { opacity: 0; translate: 0 12px; }
}

.timeline.is-visible .step-num,
.timeline.is-visible .step-card {
  animation: step-in 500ms ease-out backwards;
  animation-delay: calc(900ms + var(--i) * 80ms);
}

.step-num {
  flex: 0 0 44px;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--ink);
  color: var(--gold);
  font-family: var(--serif);
  font-weight: 500;
  font-size: 17px;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  z-index: 1;
  /* Static gold halo — a ring plus a soft bloom. No transition: this is
     presence, not motion, so reduced motion has nothing to cancel. */
  box-shadow: 0 0 0 4px rgba(194,161,77,.14), 0 0 18px rgba(194,161,77,.22);
}

.step-card {
  flex: 1 1 auto;
  min-width: 0;
  background: var(--ivory);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 22px 26px;
  transition: transform 200ms ease, border-color 200ms ease, box-shadow 200ms ease;
}

.step:nth-child(even) .step-card { background: var(--ivory-2); }

.step-card:hover {
  transform: translateY(-3px) scale(1.015);
  border-color: var(--gold);
  box-shadow: var(--shadow-card);
}

/* All caps: 15px rather than the old clamp(18px, 1.5vw, 21px), since uppercase
   plus .06em tracking widens the line by roughly a third. text-wrap: balance
   keeps the longest title ("Preliminary assessment and report") from dropping a
   single orphaned word onto line two on narrow screens. */
.step-card h3 {
  font-family: var(--sans);
  font-weight: 600;
  font-size: 15px;
  text-transform: uppercase;
  letter-spacing: .06em;
  text-wrap: balance;
  margin: 0;
}

/* Uppercase costs the titles about a third of their width, and on a 375px
   screen the 44px badge and its gap leave the card only ~205px to set them in
   — enough to push "Preliminary assessment and report" onto three lines, since
   "PRELIMINARY ASSESSMENT" alone measures 227px. Tightening the padding and the
   badge gap here buys 24px, which brings every title back to two lines at most.
   The badge is still 44px at the row's left edge, so --spine-x is unaffected. */
@media (max-width: 480px) {
  .step { gap: 16px; }
  .step-card { padding: 20px 18px; }
}

/* Gold chevron between each pair of cards, centred on the spine. bottom: -21px
   puts the 10px box's centre 16px below the step — the midpoint of the 32px
   gap — and translateX(-50%) centres it on --spine-x, the same axis the spine
   and the badges use. */
.step:not(:last-child)::after {
  content: "";
  position: absolute;
  left: var(--spine-x);
  bottom: -21px;
  width: 10px;
  height: 10px;
  border-right: 2px solid var(--gold-deep);
  border-bottom: 2px solid var(--gold-deep);
  transform: translateX(-50%) rotate(45deg);
  z-index: 1;
}

/* ------------------------------------------------------------
   c3 · Pedagogy — bordered icon cards. Fill is --ivory (no
   --white token exists in :root); separation comes from the
   1px --line border, not fill contrast, per this pass's explicit
   build spec — a near-white fill on the --ivory-2 section
   background wouldn't read on its own either way.
   ------------------------------------------------------------ */

/* Titles wrap to different line counts ("Tournament selection, planning and
   preparation" runs to three lines where "Proven curriculum" runs to one), so
   left alone the two rows size independently and the cards come out ragged.
   grid-auto-rows: 1fr sizes every row to the tallest row rather than to its own
   content, and align-items: stretch (grid's default, stated here because the
   layout now depends on it) makes each card fill that row — so all seven match
   at any breakpoint without measuring anything. --card-min is the explicit
   floor beneath that; see .included-item. */
.included-grid {
  display: grid;
  grid-template-columns: 1fr;
  grid-auto-rows: 1fr;
  align-items: stretch;
  gap: 20px;
  margin-top: 44px;
  /* Tallest natural card height, measured at the ≥1140px layout where the cards
     are narrowest and "Tournament selection, planning and preparation" wraps to
     three lines: 28px padding + 52px tile + 18px tile margin + 3 × 25.65px line
     + 28px padding + 2px borders. Every card is held to it. */
  --card-min: 205px;
}

@media (min-width: 640px) {
  .included-grid { grid-template-columns: repeat(2, 1fr); }
}

/* Eight tracks, each card spanning two, so row 2's three cards can start
   one track in and sit centred under the four above — with 20px gaps a
   2-of-8 span is (W-140)/4 + 20 = W/4 - 15, exactly a 1-of-4 track's
   (W-60)/4, so no card changes width and nothing stretches to fill.
   Cards 6 and 7 auto-flow behind card 5 into tracks 4 and 6. */
@media (min-width: 1140px) {
  .included-grid { grid-template-columns: repeat(8, 1fr); }
  .included-item { grid-column: span 2; }
  .included-item:nth-child(5) { grid-column: 2 / span 2; }
}

/* Column flex so the icon tile stays pinned at the top and the title block takes
   whatever height is left — without it, a stretched card would centre its
   content and the tiles would stop lining up row to row. */
.included-item {
  background: var(--ivory);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 28px 26px;
  display: flex;
  flex-direction: column;
  min-height: var(--card-min);
  transition: border-color 200ms ease, transform 200ms ease, box-shadow 200ms ease;
}

.included-item:hover {
  border-color: var(--gold);
  transform: translateY(-3px);
  box-shadow: var(--shadow-card);
}

.icon-tile {
  width: 52px;
  height: 52px;
  border-radius: 10px;
  background: var(--ivory-2);
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 18px;
  transition: background 200ms ease;
}

/* Scroll reveal, matching the cadence of the process and pricing
   sections either side of this one. Animation rather than a base
   opacity: 0 — see the hero entrance note; without JS the cards
   simply render, no class, no animation, nothing hidden. */
@keyframes pedagogy-in {
  from { opacity: 0; translate: 0 12px; }
}

#pedagogy.is-visible .included-item {
  animation: pedagogy-in 500ms ease-out backwards;
  animation-delay: calc(var(--i) * 70ms);
}

.icon-tile svg { fill: var(--ink); }

.included-item:hover .icon-tile { background: var(--gold-soft); }

.included-item h3 {
  font-family: var(--sans);
  font-weight: 600;
  font-size: clamp(17px, 1.4vw, 19px);
  color: var(--ink);
  margin: 0;
}

/* ------------------------------------------------------------
   Pricing — four tier cards (dark, on the ivory section) + a
   separate "playing partner" strip (a different service, not a
   fifth tier — see plan). No .band class on the section itself:
   plain ivory is the default for unclassed sections already
   (see #process), so no background rule is needed here.
   ------------------------------------------------------------ */

/* The lede that used to sit between the h2 and the toggle ("Sessions are priced by
   level…") is gone, so the h2 and the toggle are now adjacent siblings. Their own
   margins would collapse to 20px (max of the h2's 18px and the toggle's 20px), so
   both are set explicitly here to land a deliberate 32px with no collapsing to
   reason about. */
#pricing h2 { margin-bottom: 0; }

.currency-toggle {
  position: relative;
  display: flex;
  justify-content: center;
  width: fit-content;
  margin: 32px auto 0;
  background: var(--ivory-2);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 3px;
}

.currency-toggle-pill {
  position: absolute;
  top: 3px;
  left: 3px;
  height: calc(100% - 6px);
  width: calc(50% - 3px);
  background: var(--ink);
  border-radius: 999px;
  transition: transform 220ms ease;
  z-index: 0;
}

.currency-toggle[data-active="usd"] .currency-toggle-pill {
  transform: translateX(100%);
}

/* flex: 1 is flex-basis: 0, so neither button's own content seeds its width. The
   container is width: fit-content, so it sizes to the SUM of the two max-content
   contributions and then splits that equally at 1:1 — giving each button the
   AVERAGE of the two natural widths. That was 77.85px: comfortable for "₹ INR"
   (35.69px of text) but 2.16px short of "$ USD" (40.02px), so USD broke at its one
   space and rendered the $ stacked above the label.

   flex: 1 stays, because equal halves are exactly what .currency-toggle-pill's
   width: calc(50% - 3px) + translateX(100%) depends on. What was missing is a floor
   under that equal width. min-width supplies it: 86px clears the widest segment
   ("$ USD" at 82.16px in Archivo, 80.30px in the Segoe UI fallback that paints
   first under font-display: swap) with a little headroom, and applies to BOTH
   segments so the pill is the same width in either currency — a pill that resized
   mid-toggle would read as a glitch.

   nowrap alone would not do: with flex-basis 0 the automatic minimum size would
   floor USD at its min-content while INR stayed at the average, making the halves
   unequal and breaking the pill maths. */
.currency-toggle-btn {
  position: relative;
  z-index: 1;
  flex: 1;
  min-width: 86px;
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 5px;
  white-space: nowrap;
  font-family: var(--sans);
  font-weight: 500;
  font-size: 14px;
  padding: 8px 20px;
  border: none;
  border-radius: 999px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  transition: color 200ms;
}

.currency-toggle-btn[aria-pressed="true"] {
  color: var(--gold);
}

.pricing-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 20px;
  margin-top: 44px;
}

@media (min-width: 640px) {
  .pricing-grid { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 1140px) {
  .pricing-grid { grid-template-columns: repeat(4, 1fr); }
}

.pricing-card {
  background: var(--ink);
  border: 1px solid var(--line-dark);
  border-radius: 12px;
  padding: 32px 26px;
  display: flex;
  flex-direction: column;
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 500ms ease-out, transform 500ms ease-out, border-color 220ms ease, box-shadow 220ms ease;
  transition-delay: calc(var(--i) * 90ms);
}

#pricing.is-visible .pricing-card { opacity: 1; transform: translateY(0); }

.pricing-card:hover {
  transform: translateY(-4px);
  border-color: var(--gold);
  box-shadow: 0 0 0 1px var(--gold), 0 0 28px rgba(194,161,77,.22), 0 18px 40px rgba(14,22,33,.16);
}

.pricing-tier {
  font-family: var(--sans);
  font-weight: 600;
  font-size: 15px;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--ivory);
  margin: 0;
}

/* Symbol, numeral and /hr suffix are one baseline-aligned group with a
   single 4px gap. The numeral carried min-width: 4ch to stop the group
   resizing on the INR↔USD swap; on a 3-digit price that reserved a whole
   empty character between the numeral and /hr. The swap already fades
   out and back via .is-swapping, and the cards are grid-sized, so the
   width no longer needs pinning — tabular-nums keeps the count steady. */
.pricing-price {
  margin: 20px 0 0;
  display: inline-flex;
  align-items: baseline;
}

/* The symbol was a bare inline span, so its flex baseline came from whatever font
   face actually drew the glyph. Google Fonts ships Archivo split by unicode-range:
   "$" (U+0024) is in the latin subset file, "₹" (U+20B9) falls in latin-ext
   (U+20AD–U+20C0). Two files, two sets of ascent/descent metrics — so the two
   currencies resolved different first baselines from identical markup and the "$"
   rode high above the numeral. Giving the span its own box, family, size and
   line-height fixes the baseline at the span rather than inheriting it from the
   glyph's face, so both symbols now sit identically.

   The 4px gap that used to space all three items is gone: the symbol carries its
   own 2px right margin and .pricing-unit its own 4px left margin, so symbol→numeral
   and numeral→/hr can differ. */
.pricing-symbol {
  display: inline-flex;
  align-items: baseline;
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1;
  margin-right: 2px;
  color: var(--gold);
}

.pricing-num {
  font-family: var(--serif);
  font-weight: 500;
  font-size: clamp(40px, 3.6vw, 52px);
  color: var(--gold);
  font-variant-numeric: tabular-nums;
}

.pricing-unit {
  font-family: var(--sans);
  font-size: 15px;
  margin-left: 4px;
  color: var(--muted-dark);
}

.pricing-price,
.pricing-partner-price {
  transition: opacity 180ms ease, transform 180ms ease;
}

.pricing-price.is-swapping,
.pricing-partner-price.is-swapping {
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity 140ms ease, transform 140ms ease;
}

.pricing-rule {
  height: 1px;
  background: var(--line-dark);
  margin: 16px 0;
}

.pricing-level {
  display: flex;
  align-items: center;
  gap: 16px;
}

.pricing-check { flex: none; width: 18px; height: 18px; }

.pricing-level span {
  font-family: var(--sans);
  font-size: 14.5px;
  color: var(--ivory);
}

.pricing-partner {
  margin-top: 24px;
  background: var(--ivory-2);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 26px 30px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 500ms ease-out, transform 500ms ease-out;
  transition-delay: 390ms;
}

#pricing.is-visible .pricing-partner { opacity: 1; transform: translateY(0); }

@media (min-width: 640px) {
  .pricing-partner {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 20px;
  }
}

.pricing-partner-name {
  font-family: var(--sans);
  font-weight: 600;
  font-size: 16px;
  color: var(--ink);
  margin: 0;
}

.pricing-partner-level {
  font-family: var(--sans);
  font-size: 14px;
  color: var(--muted);
  margin: 2px 0 0;
}

/* Same grouping as .pricing-price above — including the per-item margins that
   replaced the shared gap, so .pricing-symbol behaves identically in both places. */
.pricing-partner-price { margin: 0; display: inline-flex; align-items: baseline; }

.pp-amount {
  font-family: var(--serif);
  font-weight: 500;
  font-size: 26px;
  color: var(--gold-deep);
  font-variant-numeric: tabular-nums;
}

.pp-note {
  font-family: var(--sans);
  font-size: 14px;
  margin-left: 4px;
  color: var(--muted);
}

.pricing-currency-note {
  font-family: var(--sans);
  font-size: 13px;
  color: var(--muted);
  margin: 24px 0 0;
}

/* ------------------------------------------------------------
   d4 · Fit — a single flip card: "who this is for" on the front,
   "and who it isn't" on the back.
   ------------------------------------------------------------ */

/* Checkmark grid — 1-3 down the left column, 4-6 down the right, via
   grid-auto-flow:column so DOM order (already the reading order) fills
   columns before rows. Draw-in reuses the getTotalLength() technique from
   .arc in #students (see initFitChecks in script.js). */
.fit-ticks {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr;
  row-gap: 22px;
}

@media (min-width: 640px) {
  .fit-ticks {
    grid-template-columns: 1fr 1fr;
    grid-auto-flow: column;
    grid-template-rows: repeat(3, auto);
    gap: 32px;
  }
}

@media (min-width: 900px) {
  .fit-ticks { column-gap: 48px; row-gap: 30px; }
}

.fit-ticks li {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  font-family: var(--sans);
  font-weight: 500;
  font-size: clamp(16px, 1.35vw, 17.5px);
  line-height: 1.55;
  color: var(--ink);
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 460ms ease-out, transform 460ms ease-out;
  transition-delay: calc(var(--i) * 70ms);
}

.fit-ticks.is-visible li { opacity: 1; transform: translateY(0); }

.fit-tick-mark {
  flex: none;
  width: 18px;
  height: 18px;
  margin-top: .18em;
}

.fit-tick-mark polyline {
  transition: stroke-dashoffset 380ms ease-out;
  transition-delay: calc(var(--i) * 70ms + 120ms);
}

.fit-ticks.is-visible .fit-tick-mark polyline { stroke-dashoffset: 0 !important; }

/* ------------------------------------------------------------
   Fit flip card. Replaces the old .fit-not-band, a full-bleed ink
   strip that ran below the list. Both faces sit in the same grid
   cell (grid-area 1/1), the same stacking .tc-stage uses for the
   testimonial slides — so the card's height is whichever face is
   taller and neither can be clipped. Never set a height here.
   ------------------------------------------------------------ */

.fit-flip {
  margin-top: 28px;
  perspective: 1600px;
}

.fit-flip-inner {
  display: grid;
  transform-style: preserve-3d;
  transition: transform 600ms cubic-bezier(.4,0,.2,1);
}

.fit-flip.is-flipped .fit-flip-inner { transform: rotateY(180deg); }

.fit-face {
  grid-area: 1 / 1;
  position: relative;
  border-radius: 14px;
  padding: clamp(28px, 4vw, 48px);
  /* Room for the corner control so it never collides with the content. */
  padding-bottom: clamp(76px, 8vw, 96px);
  backface-visibility: hidden;
}

.fit-face-front {
  background: var(--ivory-2);
  border: 1px solid var(--line);
}

.fit-face-front .lede { margin-bottom: 0; }

.fit-face-back {
  transform: rotateY(180deg);
  background: var(--ink);
  color: var(--ivory);
  /* Symmetric again — the front's extra bottom padding exists only to clear
     its absolutely-positioned control, which the back does not have. */
  padding-bottom: clamp(28px, 4vw, 48px);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
}

.fit-face-back .eyebrow {
  color: var(--gold);
  justify-content: center;
}

.fit-not-statement {
  font-family: var(--serif);
  font-size: clamp(20px, 2vw, 27px);
  line-height: 1.45;
  max-width: 46ch;
  margin: 0;
}

/* Persistent affordance — without a visible control nothing signals that the
   card has a second face. Absolute on the front so it pins to the corner;
   static on the back, where the content is centred and short. */
.fit-flip-btn {
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 500;
  color: var(--gold-deep);
  background: transparent;
  border: 1px solid var(--gold);
  border-radius: 999px;
  padding: 8px 16px;
  cursor: pointer;
  transition: background 200ms ease, color 200ms ease;
}

.fit-face-front .fit-flip-btn {
  position: absolute;
  right: clamp(20px, 3vw, 32px);
  bottom: clamp(20px, 3vw, 32px);
}

.fit-face-front .fit-flip-btn:hover,
.fit-face-front .fit-flip-btn:focus-visible {
  background: var(--gold);
  color: var(--btn-text);
}

.fit-face-back .fit-flip-btn {
  color: var(--gold);
  margin-top: 28px;
}

.fit-face-back .fit-flip-btn:hover,
.fit-face-back .fit-flip-btn:focus-visible {
  background: var(--gold);
  color: var(--btn-text);
}

/* ------------------------------------------------------------
   Achievement strip. Originally the standalone #founder section,
   then the hero; it now sits under both columns of #approach,
   with the founder's photo. The .hero- prefix on the class names
   is historical and left alone — the strip's styling was carried
   over unchanged, and renaming it buys nothing.
   ------------------------------------------------------------ */

.hero-credentials { margin-top: 48px; }

/* Achievement strip — one full-width band spanning both columns, replacing
   the horizontally scrolling card rail. No card fills, no borders around cells
   and no shadows: the strip's own top/bottom rules, 1px vertical rules between
   cells, and the cell padding carry the whole structure. A gold bar runs the
   full height of each cell's text; the medal/title is the headline and the event
   is supporting context. Marker is aria-hidden and decorative; the year is real
   content (a placeholder awaiting real dates from Kumaran), so it stays in the
   accessibility tree even while hidden. */

.achievements {
  list-style: none;
  margin: 0;
  padding: 0;
}

.hero-achievements-list {
  display: grid;
  grid-template-columns: 1fr;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

/* Three then two. Row 2's cells need a top rule of their own — the strip's own
   border-bottom is the outer edge, not a divider between rows. */
@media (min-width: 640px) {
  .hero-achievements-list { grid-template-columns: repeat(3, 1fr); }
  .ach:nth-child(3n+1) { border-left: none; }
  .ach:nth-child(n+4) { border-top: 1px solid var(--line); }
}

@media (min-width: 1140px) {
  .hero-achievements-list { grid-template-columns: repeat(5, 1fr); }
  .ach:nth-child(3n+1) { border-left: 1px solid var(--line); }
  .ach:nth-child(n+4) { border-top: none; }
  .ach:first-child { border-left: none; }
}

.ach {
  display: flex;
  align-items: stretch;
  gap: 16px;
  padding: 22px 20px;
  /* Vertical rules separate the cells; the rule is suppressed wherever a cell
     starts a row so none ever lands on the strip's outer edge. */
  border-left: 1px solid var(--line);
  /* Kept from when the strip was in the hero, where it continued that section's
     entrance cadence. In #approach it runs on load while still below the fold,
     so it has finished long before the strip is scrolled to and the cells simply
     render normally. Harmless, and left in place because the strip was moved
     with its styling unchanged. */
  animation: hero-rise 640ms cubic-bezier(.22,.61,.36,1) backwards;
  animation-delay: calc(290ms + var(--i) * 70ms);
}

/* Single column: every cell starts a row, so no vertical rule anywhere. Cells
   are divided by a horizontal rule instead. */
@media (max-width: 639px) {
  .ach { border-left: none; }
  .ach + .ach { border-top: 1px solid var(--line); }
}

.ach-marker {
  flex: none;
  width: 3px;
  align-self: stretch;
  background: var(--gold);
  border-radius: 2px;
}

.ach-text { min-width: 0; }

.ach-title {
  font-family: var(--sans);
  font-weight: 600;
  font-size: 15px;
  color: var(--ink);
  margin: 0;
}

.ach-context {
  font-family: var(--sans);
  font-size: 13px;
  color: var(--muted);
  margin: 4px 0 0;
}

/* Placeholder years are hidden rather than deleted: the spans and their
   TODO comments stay in the markup so the real dates can drop straight in.
   Remove .is-pending on each span (index.html) to reveal them again. */
.ach-year.is-pending { display: none; }

.ach-year {
  margin-top: 6px;
  font-size: 13px;
  color: var(--muted-dark);
  letter-spacing: .04em;
  white-space: nowrap;
}


/* ------------------------------------------------------------
   f6 · Reach — dotted world map with arcs radiating from
   Chennai. The SVG was pre-built from real coastline data with
   true marker coordinates and labels already positioned (via
   per-label text-anchor) to avoid collision at the base 11-unit
   label size. It lives inlined in index.html and nowhere else —
   the standalone assets/img/reach-map.svg it came from was
   deleted once the inline copy became the only one in use.
   Inlined verbatim, plus one addition:
   .pulse-ring circles (SVG has no ::before/::after, so the
   pulse ring is a real sibling <circle>, duplicating each
   marker's own position/size). Arc reveal uses stroke-dasharray/
   dashoffset measured at runtime via getTotalLength() in
   script.js (initReachMap) — never a guessed length, since arcs
   differ in length. Section background is --ink (pre-existing
   .dark on #students, not introduced by this pass) — labels use
   the dark-background variant: ivory fill, ink stroke halo.
   ------------------------------------------------------------ */

/* No background override here. This section used to be stepped to --ink-2 with
   a --line-dark rule, because the "And who it isn't" band closing #fit was also
   .dark and the two ran together as one long dark stretch. That band is now the
   back of the #fit flip card, so #students is the only dark section on the page
   and takes the plain .dark --ink. */

.reach-map {
  display: block;
  width: 100%;
  height: auto;
  margin: 26px 0 0;
}

.arc {
  transition: stroke-dashoffset 1.4s ease-out;
  transition-delay: calc(var(--i) * 180ms);
}

#students.is-visible .arc {
  stroke-dashoffset: 0 !important;
}

.pulse-ring {
  transform-box: fill-box;
  transform-origin: center;
}

#students.is-visible .pulse-ring {
  animation: reach-pulse 2.6s ease-out infinite;
  animation-delay: calc(var(--i) * 0.3s);
}

#students.is-visible .pulse-ring-origin {
  animation-duration: 2s;
}

@keyframes reach-pulse {
  0% { transform: scale(1); opacity: .5; }
  100% { transform: scale(2.4); opacity: 0; }
}

.lbl {
  font-family: var(--sans);
  font-size: 11px;
  letter-spacing: .08em;
  text-transform: uppercase;
  fill: var(--ivory);
  paint-order: stroke;
  stroke: var(--ink);
  stroke-width: 6px;
  stroke-linejoin: round;
}

.lbl-origin { font-weight: 600; }

/* Each label follows its own arc in. Arc i finishes at
   180i + 1400ms (.arc: 1.4s transition, 180ms per-index delay),
   so starting the 300ms fade at 180i + 1300 lands it 200ms after
   its own line completes. .lbl-origin has no arc — it comes in
   with the origin pulse instead.

   Animation, not a base opacity: 0: these labels currently render
   unconditionally, and hiding them by default would blank the map's
   place names entirely if the script never ran. */
@keyframes lbl-in {
  from { opacity: 0; }
}

#students.is-visible .lbl {
  animation: lbl-in 300ms ease-out backwards;
  animation-delay: calc(var(--i) * 180ms + 1300ms);
}

#students.is-visible .lbl-origin { animation-delay: 200ms; }

/* .lbl font-size is in SVG USER UNITS, not CSS pixels: .reach-map is
   width:100%/height:auto over viewBox="0 33 1000 375", so everything inside
   scales with the container. At 375px the map paints ~327px wide — a 0.327
   scale — which is why the old 9px step rendered at ~2.9 CSS px and was
   unreadable. Stepping it to 11 would have reached ~3.6 CSS px and changed
   nothing perceptible.

   32 units is what actually lands near 11 CSS px at 375px. stroke-width is the
   ink halo behind the glyphs and has to scale with them (the base ratio is
   6/11), hence 14. Desktop keeps the base 11 units — at the 1084px container
   the scale is ~1.08, so it already renders ~12 CSS px. */
@media (max-width: 899px) {
  .lbl { font-size: 32px; stroke-width: 14px; }
}

/* ------------------------------------------------------------
   f6 · Results — testimonial rotator. .tc-slide stacks all four
   slides in the same grid cell (grid-area 1/1); script.js
   toggles .is-on/.is-leaving to drive the slide transition below
   — no position:absolute needed for the stacking itself.
   ------------------------------------------------------------ */

/* Full-width board texture behind the testimonials.

   #results is a bare <section> with no class, so it had no background of its
   own and the body texture — a two-tone gold/silver conic at 128px, i.e. 64px
   squares — showed straight through. This REPLACES that within the section
   rather than layering over it: the opaque background-color is what stops the
   body grid from reaching the page here, and it is load-bearing. Two grids at
   64px and 96px over the same area moiré, and the artefact reads as a
   rendering fault rather than as texture.

   144px background-size gives 72px squares — finer than the hero's 96px, which
   keeps the two boards from reading as one continuous surface. A
   repeating-conic with 25%/50% stops makes each square HALF the
   background-size; that factor of two is the easiest thing to get wrong here.

   Full-bleed and unmasked, the same treatment as the hero, just softer. An
   earlier pass ran full-width at .045/96px and was too loud behind the quote,
   which is what sent this to corner fragments; the fragments never read as a
   board, so coverage is back and the alpha carries the load instead — .018 at
   72px squares, against the hero's .028 at 96px. Alpha and the measurements
   behind it live in --testimonial-pattern-opacity at the top of this file.

   The masks are gone: no mask-image, no -webkit-mask-composite, and no
   below-900px variant (that rule existed only to drop one of the two corner
   fragments). The section still owns the pattern via its own ::before, so it
   cannot bleed into #students above or #faq below. */
#results {
  position: relative;
  background-color: var(--ivory);
}

#results::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image: repeating-conic-gradient(
    rgba(14,22,33, var(--testimonial-pattern-opacity)) 0% 25%,
    rgba(14,22,33,0)                                   0% 50%);
  background-size: 144px 144px;
}


/* Lifts the content over the ::before, the same pattern as .hero > .wrap. */
#results > .wrap {
  position: relative;
  z-index: 1;
}

.tc { max-width: 940px; margin: 40px auto 0; }

.tc-stage { display: grid; }

.tc-slide {
  grid-area: 1 / 1;
  display: flex; flex-direction: column; gap: 28px;
  opacity: 0; visibility: hidden; transform: translateX(24px);
  transition: opacity 340ms ease, transform 340ms ease, visibility 0s linear 340ms;
}

.tc-slide.is-on {
  opacity: 1; visibility: visible; transform: translateX(0);
  transition: opacity 340ms ease 40ms, transform 340ms ease 40ms, visibility 0s linear 0s;
}

.tc-slide.is-leaving {
  opacity: 0; visibility: visible; transform: translateX(-24px);
  transition: opacity 340ms ease, transform 340ms ease, visibility 0s linear 340ms;
}

.tc-text { max-width: 62ch; margin: 0 auto; text-align: center; }

.tc-eyebrow {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  margin: 0 0 18px;
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .22em;
  color: var(--gold-deep);
}

.tc-eyebrow-rule {
  display: inline-block;
  width: 28px;
  height: 1px;
  background: var(--gold);
}

.tc-quote {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(17px, 1.5vw, 21px);
  line-height: 1.5;
  color: var(--ink);
  margin: 0 0 20px;
}

/* max-width: none on both. These are <p>, so the global p { max-width: 62ch }
   applied — and ch resolves against each element's OWN font, so the 15px name
   and the 13.5px location each got a different, narrower-than-parent box. With
   no auto inline margin those boxes sat flush-left inside the centred .tc-text,
   so their centred text landed on three different axes. Letting both fill
   .tc-text puts them on the quote's axis. */
.tc-name {
  font-family: var(--sans);
  font-weight: 600;
  font-size: 15px;
  color: var(--ink);
  max-width: none;
  margin: 0;
}

.tc-location {
  font-family: var(--sans);
  font-size: 13.5px;
  color: var(--muted);
  max-width: none;
  margin: 4px 0 0;
}

/* Capped well inside .tc's 940px and centred: pushed to the full stage
   width the dots and the Next button end up at opposite edges with a
   void between them, out of proportion to the quote they belong to. */
/* flex-start + margin-left:auto on .tc-next rather than space-between, so the
   pause button can sit next to the dots without a wrapper element. */
.tc-bar {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 18px;
  max-width: 560px;
  margin: 32px auto 0;
}

/* .to-top is position:fixed at right:24px, 48px wide, z-index 40 — it owns the
   last 72px of the viewport whenever it is on, and being fixed it cannot be
   cleared vertically by scrolling. Above 640px that never matters: .tc-bar is
   capped at 560px and centred, so .tc-next lands ~368px clear at 1440 and
   ~98px clear at 900. At 375px the content edge sits 24px from the viewport
   edge, the two coincide, and the fixed button paints straight over .tc-next.
   Reserving its column here is the fix; widening the bar would move .tc-next
   further right, into the collision rather than out of it. */
@media (max-width: 639px) {
  .tc-bar { padding-right: 72px; }
}

/* The dots' tap targets below are as wide as --dot-pitch; at the old 16px
   pitch (6px dot + 10px gap) four 44px targets overlapped by 28px each, and
   since later siblings paint on top, dots 1-3 were effectively untappable.
   Deriving the gap FROM the pitch keeps the two in lockstep: whatever the
   pitch, centres sit exactly that far apart and the targets meet edge to edge
   without ever overlapping. */
.tc-dots {
  --dot-pitch: 44px;
  display: flex;
  align-items: center;
  gap: calc(var(--dot-pitch) - 6px);
}

/* At 320px the row is 272px wide and a 44px pitch needs 286px — the cluster
   would push .tc-next out of the container. 38px is the largest pitch that
   fits (268px), and it still clears the 24px WCAG 2.5.8 minimum comfortably.
   The vertical 44px is untouched, so only the horizontal axis gives. */
@media (max-width: 399px) {
  .tc-dots { --dot-pitch: 38px; }
}

.tc-dot {
  position: relative;
  width: 6px;
  height: 6px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: var(--line);
  cursor: pointer;
  transition: width 320ms cubic-bezier(.4,0,.2,1);
}

/* The touch target, invisible and centred on the dot. Centring via transform
   rather than symmetric negative insets is load-bearing: .tc-dot.is-on
   animates from 6px to 22px wide, and fixed insets would drag the target
   off-centre for the whole 320ms of that transition. */
.tc-dot::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: var(--dot-pitch, 44px);
  height: 44px;
}

.tc-dot.is-on {
  width: 22px;
  border-radius: 3px;
  background: var(--gold);
}

/* Quieter than .tc-next by design: Next is the primary control and keeps its
   ink fill, so pause reads as secondary with an outline only. Existing tokens
   only — --gold-deep is the small-element-on-light hover per the Two-Gold
   Rule, and there is deliberately no glow here. */
.tc-play {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 50%;
  color: var(--ink);
  cursor: pointer;
  transition: color var(--transition), border-color var(--transition);
}

.tc-play:hover,
.tc-play:focus-visible {
  color: var(--gold-deep);
  border-color: var(--gold-deep);
}

/* House focus convention, same as .skip-link and .puzzle-sq. The colour shift
   above is a hover affordance; on its own it is not a focus indicator. */
.tc-play:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

.tc-play-icon {
  width: 16px;
  height: 16px;
  fill: currentColor;
}

/* Exactly one icon shows; .is-paused is toggled by script.js alongside the
   aria-label, so the visual and the accessible name never disagree. */
.tc-icon-play { display: none; }
.tc-play.is-paused .tc-icon-pause { display: none; }
.tc-play.is-paused .tc-icon-play { display: block; }

.tc-next {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-left: auto;
  width: 52px;
  height: 52px;
  padding: 0;
  background: var(--ink);
  border: none;
  border-radius: 50%;
  cursor: pointer;
  box-shadow: 0 0 0 5px rgba(194,161,77,.16), 0 6px 20px rgba(14,22,33,.2);
  transition: transform 200ms ease, box-shadow 200ms ease;
}

.tc-next:hover,
.tc-next:focus-visible {
  transform: scale(1.07);
  box-shadow: 0 0 0 8px rgba(194,161,77,.16), 0 6px 20px rgba(14,22,33,.2);
}

.tc-arrow {
  display: inline-block;
  color: var(--gold);
}

/* ------------------------------------------------------------
   FAQ
   Exclusive-open accordion. The animated visual state (raised
   card, chevron rotation, grid-template-rows 0fr→1fr expand) is
   driven entirely by the script-controlled .faq-item-open class,
   NOT the native [open] attribute — the browser's own exclusive
   `name`-group close (removing [open] on a sibling) happens as
   an atomic native operation that skips CSS transitions, so
   keying the animated properties off [open] directly produced a
   snap on auto-close. script.js (initFaqAccordion) intercepts
   summary clicks, keeps [open] present for the item being
   closed until its .faq-item-open-driven shrink finishes, then
   removes [open]. Under prefers-reduced-motion it only mirrors
   [open] onto .faq-item-open on the native `toggle` event,
   without intercepting — native instant behavior, unanimated.
   ------------------------------------------------------------ */

.faq { border-top: 1px solid var(--line); }

.faq details {
  border-bottom: 1px solid var(--line);
  background: transparent;
}

.faq details:last-child { border-bottom: none; }

.faq details.faq-item-open {
  background: var(--ivory);
  border-radius: 6px;
  box-shadow: var(--shadow-card);
  border-bottom-color: transparent;
}

.faq summary {
  font-family: var(--sans);
  font-weight: 500;
  font-size: clamp(16px, 1.3vw, 18px);
  color: var(--ink);
  cursor: pointer;
  list-style: none;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 20px;
  padding: 22px 24px;
}

.faq summary::-webkit-details-marker { display: none; }

.faq summary:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

.faq details:not(.faq-item-open) summary:hover .faq-q {
  text-decoration: underline;
  text-underline-offset: 2px;
}

.faq-chevron {
  flex: none;
  color: var(--gold-deep);
  transition: transform 200ms ease, color var(--transition);
}

.faq details:not(.faq-item-open) summary:hover .faq-chevron { color: var(--gold); }

.faq details.faq-item-open .faq-chevron {
  transform: rotate(180deg);
  color: var(--gold);
}

.faq-answer-wrap {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 260ms ease;
}

.faq details.faq-item-open .faq-answer-wrap { grid-template-rows: 1fr; }

.faq-answer-wrap > div { overflow: hidden; }

.faq p {
  margin: 0;
  padding: 0 24px 24px;
  color: var(--muted);
  font-size: 15.5px;
  line-height: 1.65;
  max-width: 62ch;
}

/* ------------------------------------------------------------
   Booking
   ------------------------------------------------------------ */

/* The section holds three elements — eyebrow, h2, button — so it carries about
   a third less vertical padding than the global step: each term of
   clamp(56px, 9vw, 96px) cut to two thirds, which keeps the reduction
   proportional at every viewport rather than only at the cap. Enough air to
   read as composed, not so much that it leaves a void above the footer. */
#book { padding: clamp(38px, 6vw, 64px) 0; }

#book .wrap { text-align: center; }

/* Two gaps, each owned by exactly one value — the button has no margin-top, per
   the convention the old three-step rhythm used before the lede and the
   disclosure were removed. */
#book .eyebrow { justify-content: center; margin-bottom: 16px; }
#book h2 { max-width: 20ch; margin: 0 auto 36px; }

@keyframes beam-travel {
  to { --beam-angle: 360deg; }
}

.book-btn {
  position: relative;
  display: inline-block;
  background: var(--ink);
  color: var(--gold);
  font-family: var(--sans);
  font-weight: 500;
  font-size: 17px;
  padding: 20px 44px;
  border-radius: 999px;
  text-decoration: none;
  /* Steady outer glow, sitting under the travelling beam (::before) and its
     blurred trail (::after). The beam reads as motion; this reads as presence. */
  box-shadow: 0 0 34px rgba(194,161,77,.28), 0 8px 24px rgba(14,22,33,.28);
  transition: transform 200ms ease, box-shadow 220ms ease;
}

.book-btn::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 2px;
  background: conic-gradient(
      from var(--beam-angle),
      var(--line) 0%,
      var(--line) 62%,
      var(--gold) 74%,
      var(--gold-soft) 80%,
      var(--gold) 86%,
      var(--line) 96%,
      var(--line) 100%);
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  -webkit-mask-composite: xor;
  pointer-events: none;
  animation: beam-travel 5s linear infinite;
}

.book-btn::after {
  content: '';
  position: absolute;
  inset: -2px;
  border-radius: inherit;
  background: conic-gradient(
      from var(--beam-angle),
      transparent 0%,
      transparent 62%,
      var(--gold) 74%,
      var(--gold-soft) 80%,
      var(--gold) 86%,
      transparent 96%,
      transparent 100%);
  filter: blur(10px);
  opacity: .32;
  z-index: -1;
  pointer-events: none;
  animation: beam-travel 5s linear infinite;
}

.book-btn:hover::before,
.book-btn:hover::after {
  animation-duration: 2.5s;
}

.book-btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 0 46px rgba(194,161,77,.36), 0 8px 24px rgba(14,22,33,.28);
}

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

footer {
  background: var(--ink);
  color: var(--muted-dark);
  padding: clamp(40px, 5vw, 60px) 0;
}

.foot-inner {
  max-width: 640px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

.foot-brand {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  border: 1px solid var(--line-dark);
  border-radius: 50%;
  padding: 10px;
  background: var(--ink-2);
  /* A tight ring plus a wide bloom — the ring holds the circle's edge against
     --ink, the bloom lifts the mark off the footer. */
  box-shadow: 0 0 0 6px rgba(194,161,77,.08), 0 0 30px rgba(194,161,77,.18);
  transition: box-shadow 240ms ease;
}

.foot-brand:hover,
.foot-brand:focus-visible {
  box-shadow: 0 0 0 6px rgba(194,161,77,.10), 0 0 40px rgba(194,161,77,.26);
}

.foot-brand img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

.foot-nav { margin-top: 24px; }

.foot-nav ul {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 28px;
  list-style: none;
  margin: 0;
  padding: 0;
}

/* The sweeping ::after bar (defined with .navlinks a::after above)
   replaces the old text-decoration hover — the two would otherwise
   stack into a double underline. */
.foot-nav a {
  position: relative;
  font-family: var(--sans);
  font-weight: 700;
  font-size: 13px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted-dark);
  text-decoration: none;
  transition: color 180ms ease;
}

.foot-nav a:hover,
.foot-nav a:focus-visible {
  color: var(--ivory);
}

.foot-social {
  margin-top: 20px;
  display: flex;
  justify-content: center;
  gap: 14px;
}

.foot-social a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 42px;
  border: 1px solid var(--line-dark);
  border-radius: 50%;
  transition: border-color 180ms ease, transform 180ms ease;
}

.foot-social a:hover,
.foot-social a:focus-visible {
  border-color: var(--gold);
  transform: scale(1.06);
}

.foot-social svg { transition: stroke 180ms ease; }

.foot-social a:hover svg,
.foot-social a:focus-visible svg { stroke: var(--gold); }

.foot-email {
  margin-top: 16px;
  font-family: var(--sans);
  font-size: 14.5px;
  color: var(--muted-dark);
  text-decoration: underline;
  text-decoration-color: transparent;
  transition: color 180ms ease, text-decoration-color 180ms ease;
}

.foot-email:hover,
.foot-email:focus-visible {
  color: var(--gold);
  text-decoration-color: currentColor;
}

.foot-bottom {
  margin-top: 24px;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.foot-rule {
  border: 0;
  height: 1px;
  width: 100%;
  max-width: 420px;
  background: var(--line-dark);
  margin: 0 0 20px;
}

.foot-copy {
  font-family: var(--sans);
  font-size: 13px;
  color: var(--muted-dark);
  margin: 0;
}

/* ------------------------------------------------------------
   Back to top — appears once the reader is 600px down. z-index 40
   sits under the sticky header (50) and the mobile nav panel (100),
   above everything in the page flow.
   ------------------------------------------------------------ */

.to-top {
  position: fixed;
  right: 24px;
  bottom: 24px;
  z-index: 40;
  width: 48px;
  height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border-radius: 50%;
  background: var(--ink);
  border: 1px solid var(--gold);
  cursor: pointer;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 300ms ease, transform 200ms ease, box-shadow 200ms ease,
              visibility 0s linear 300ms;
}

.to-top.is-on {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition: opacity 300ms ease, transform 200ms ease, box-shadow 200ms ease,
              visibility 0s linear 0s;
}

.to-top:hover {
  transform: translateY(-3px);
  box-shadow: 0 0 24px rgba(194,161,77,.3);
}

@media (max-width: 639px) {
  .to-top {
    right: 16px;
    bottom: 16px;
    width: 44px;
    height: 44px;
  }
}

/* ------------------------------------------------------------
   FAQ puzzle — a real back-rank mate, playable with one click.
   It is the entire left column of the FAQ grid: the section
   heading now sits above the grid, so the board starts at the
   same y as the questions on the right (.grid2 is already
   align-items: start). No top margin for that reason. Below
   900px .grid2 collapses to one column and source order puts
   the puzzle above the questions.

   Squares are <button>s, so they carry focus, keyboard
   activation and an accessible name for free. Everything below
   is presentation only.
   ------------------------------------------------------------ */

.puzzle-prompt {
  font-family: var(--sans);
  font-size: 14px;
  color: var(--muted);
  margin: 0 0 14px;
}

/* Squares are sized on the grid, not the buttons, so the board is always
   exactly 8 squares wide and cannot be stretched by a piece. */
.puzzle-board {
  position: relative;
  display: grid;
  grid-template-columns: repeat(8, 26px);
  grid-template-rows: repeat(8, 26px);
  width: max-content;
  border: 1px solid var(--line);
}

@media (min-width: 1140px) {
  .puzzle-board {
    grid-template-columns: repeat(8, 30px);
    grid-template-rows: repeat(8, 30px);
  }
}

.puzzle-sq {
  appearance: none;
  border: 0;
  padding: 0;
  margin: 0;
  background: var(--ivory-2);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
}

.puzzle-sq.is-dark { background: rgba(14,22,33,.14); }

/* Highlights ride on an overlay rather than the square's own background-color.
   Animating background-color with animation-fill-mode: forwards REPLACES the
   square's colour, so a light square would lose --ivory-2 and a dark one its
   rgba(14,22,33,.14) once the highlight settled. A transparent ::after layered
   over the square composites instead, so both square colours survive underneath
   and one pair of rules works for light and dark alike. */
.puzzle-sq::after {
  content: "";
  position: absolute;
  inset: 0;
  background-color: rgba(194,161,77,0);
  pointer-events: none;
  z-index: 0;
}

.puzzle-sq:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: -2px;
  z-index: 2;
}

/* Above the highlight overlay: the ::after is a positioned descendant and would
   otherwise paint over this non-positioned child and wash the piece gold. */
.puzzle-piece {
  width: 78%;
  height: 78%;
  pointer-events: none;
  position: relative;
  z-index: 1;
}

/* The sprite's rook, king and pawn carry no hard-coded fills, so both sides are
   just a fill swap. The white pieces need the ink outline to read at all against
   a light square. */
.puzzle-piece.is-w {
  fill: var(--ivory);
  stroke: var(--ink);
  stroke-width: 1;
  stroke-linejoin: round;
}

.puzzle-piece.is-b { fill: var(--ink); }

/* The travelling rook is taken out of the square grid so it can cross the board
   without being clipped by the origin square. */
/* PUZZLE_FLIGHT_MS in script.js must match this 520ms — the script removes the
   flyer and hands the rook to the destination square on that timer. */
.puzzle-piece.is-flying {
  position: absolute;
  z-index: 3;
  transition: transform 520ms cubic-bezier(.4,0,.2,1);
}

/* The answer marker. Fades up to .35 as the rook lands (43% of 460ms ≈ 200ms),
   then settles to .18 and STAYS — animation-fill-mode: forwards is what keeps the
   solved square marked rather than flashing and reverting. */
@keyframes puzzle-arrive {
  0%   { background-color: rgba(194,161,77,0); }
  43%  { background-color: rgba(194,161,77,.35); }
  100% { background-color: rgba(194,161,77,.18); }
}

/* The square the rook left, fading out over exactly the flight duration so the
   old highlight is gone the moment the rook settles. */
@keyframes puzzle-depart {
  from { background-color: rgba(194,161,77,.18); }
  to   { background-color: rgba(194,161,77,0); }
}

/* No new colour: the "wrong" flash is the existing --muted at low alpha, so it
   reads as a dulled square rather than an error state. */
@keyframes puzzle-wrong {
  from { background: color-mix(in srgb, var(--muted) 38%, transparent); }
}

/* Shape only, no colour — the flash above carries the whole visual response. */
@keyframes puzzle-pulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.06); }
}

.puzzle-sq.is-arrived::after { animation: puzzle-arrive 460ms ease-out forwards; }
.puzzle-sq.is-origin::after { animation: puzzle-depart 520ms cubic-bezier(.4,0,.2,1) forwards; }

/* Two animations, two durations: the existing 420ms flash and the 260ms scale.
   z-index lifts the pulsing square over its neighbours so the 6% overshoot is not
   painted under the adjacent squares' backgrounds. */
.puzzle-sq.is-wrong {
  animation: puzzle-wrong 420ms ease-out, puzzle-pulse 260ms ease-out;
  z-index: 1;
}

/* Hidden until the move resolves — initPuzzle adds .is-in only once the rook has
   landed, so the verdict never arrives before the answer it describes. */
.puzzle-result {
  font-family: var(--sans);
  font-size: 14px;
  color: var(--ink);
  margin: 12px 0 0;
  min-height: 1.6em;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 300ms ease, transform 300ms ease;
}

.puzzle-result.is-in {
  opacity: 1;
  transform: translateY(0);
}

.puzzle-reset {
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  margin-top: 4px;
  font-family: var(--sans);
  font-size: 13px;
  color: var(--muted);
  border-bottom: 1px solid var(--line);
  cursor: pointer;
}

.puzzle-reset:hover,
.puzzle-reset:focus-visible { color: var(--ink); border-bottom-color: var(--gold); }

/* ------------------------------------------------------------
   Reduced motion
   ------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }

  .stat .n,
  .stat-static .n,
  .cred .n,
  .cred-static .n {
    opacity: 1 !important;
  }

  .book-btn::before,
  .book-btn::after {
    animation: none;
    --beam-angle: 135deg;
  }

  .book-btn:hover {
    transform: none;
  }

  .faq-chevron,
  .faq-answer-wrap {
    transition: none;
  }

  .arc {
    stroke-dashoffset: 0 !important;
    transition: none !important;
  }

  .pulse-ring {
    animation: none !important;
  }

  .fit-ticks li {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }

  .fit-tick-mark polyline {
    stroke-dashoffset: 0 !important;
    transition: none !important;
  }

  .pricing-card,
  .pricing-partner {
    opacity: 1 !important;
    transform: none !important;
    transition: border-color 220ms ease !important;
  }

  .pricing-card:hover {
    transform: none !important;
  }

  /* Same selector shape as the default rule so the static filter wins on order
     without !important. animation: none rather than leaning on the global
     animation-duration: 0.01ms — an *infinite* animation at 0.01ms would cycle
     absurdly fast instead of sitting still. */
  /* The glow stays — it is what gives the marker its warmth — but held at a fixed
     mid-cycle value instead of cycling. animation: none rather than leaning on the
     global animation-duration: 0.01ms, which would make an *infinite* animation
     cycle absurdly fast rather than sit still. */
  .approach-bullets li::before {
    animation: none !important;
    opacity: .7;
    transform: scale(1);
  }

  .approach-bullets li .approach-bullet-icon {
    filter: drop-shadow(0 0 6px rgba(194,161,77,.40));
  }

  .currency-toggle-btn {
    transition: none !important;
  }

  .currency-toggle-pill {
    transition: none !important;
  }

  .included-item:hover {
    transform: none;
  }

  .foot-social a:hover,
  .foot-social a:focus-visible {
    transform: none;
  }

  .step::before {
    transform: translateX(-50%) scaleY(1) !important;
    transition: none !important;
  }

  /* The puzzle stays fully playable: initPuzzle checks the same media query and
     re-parents the rook to e8 instantly instead of flying it there, so the move
     still happens — it just doesn't travel, and the verdict is written at the
     same moment rather than waiting on a flight that never runs.

     The wrong-square flash and its pulse are pure decoration, so cancelling them
     loses nothing. The arrival highlight is NOT decoration — it is how the solved
     square stays marked — so instead of cancelling it, .is-arrived is pinned
     straight to the settled value it would have animated to. */
  .puzzle-sq.is-wrong {
    animation: none !important;
  }

  .puzzle-sq.is-arrived::after {
    animation: none !important;
    background-color: rgba(194,161,77,.18);
  }

  .puzzle-sq.is-origin::after {
    animation: none !important;
  }

  .puzzle-result {
    transition: none !important;
  }

  .puzzle-piece.is-flying {
    transition: none !important;
  }

  .timeline.is-visible .step-num,
  .timeline.is-visible .step-card {
    animation: none !important;
  }


  .tc-slide {
    transition: none !important;
  }

  .tc-next {
    transition: none !important;
  }

  .tc-next:hover,
  .tc-next:focus-visible {
    transform: none !important;
  }

  .tc-dot {
    transition: none !important;
  }

  /* Motion added in the entrance/reveal pass. Every one of these is a
     class-triggered animation over a fully visible normal state, so
     cancelling the animation is the whole fix — there is no base
     opacity: 0 or transform left behind to reset. */
  .hero h1,
  .hero .lede,
  #pedagogy.is-visible .included-item,
  .is-heads-in .head-reveal,
  #students.is-visible .lbl,
  #students.is-visible .lbl-origin {
    animation: none !important;
  }

  .navlinks a::after,
  .foot-nav a::after {
    transition: none !important;
  }

  /* Motion added in this pass. Anchors jump instead of gliding; the credential
     rail, step cards and back-to-top keep their hover colour changes but lose the
     lift; the flip card swaps faces by opacity instead of rotating. Nothing
     here hides content — every face and card stays readable and operable.
     .step-num's glow is a static box-shadow, so there is no motion to cancel. */
  html {
    scroll-behavior: auto !important;
  }

  .ach {
    animation: none !important;
  }

  .step-card:hover,
  .to-top:hover {
    transform: none !important;
  }

  .to-top {
    transition: opacity 0.01ms !important;
  }

  .fit-flip-inner {
    transition: none !important;
    transform: none !important;
  }

  /* No rotation means backface-visibility can no longer do the hiding, so the
     faces are swapped explicitly. Exactly one is ever visible or focusable. */
  .fit-face {
    backface-visibility: visible;
    transform: none !important;
  }

  .fit-face-front,
  .fit-flip.is-flipped .fit-face-back {
    opacity: 1;
    visibility: visible;
  }

  .fit-face-back,
  .fit-flip.is-flipped .fit-face-front {
    opacity: 0;
    visibility: hidden;
  }
}

/* ------------------------------------------------------------
   Serif-usage reference
   ------------------------------------------------------------ */

/* Selectors permitted to use var(--serif) (Newsreader), and only these:
   h1, h2, .stat .n, .cred .n, .step-num, .tc-quote,
   .pricing-num, .pp-amount, .approach-name.
   Archivo is used for everything else, including h3. This is a role
   split, not a size threshold — each of these is an assigned role by
   design intent. Keep this list accurate: if a later pass adds another
   serif usage, update it here. The step-num usage was added in the
   a1/b2 content-section pass per that stage's explicit build spec.
   .approach-name (the founder's name under his photo) was added in the
   coordinate-removal pass, per that stage's explicit build spec; .coord
   was dropped in the same pass, along with the coordinate conceit itself.
   The testimonial quote was added in the FAQ pass per its build spec —
   DESIGN.md's Role-Split Rule prose still lists only the original
   five and should be reconciled in a documentation pass. (.report-title
   and .report-rating b, formerly also on this list, were removed with
   the #approach report-card demo in a later content pass. .foot-logo,
   formerly also on this list, was removed with the "iChess. Academy"
   wordmark in the footer's centred single-column rebuild pass — the
   footer now uses an icon mark, no serif text.)
   The FAQ question text (.faq summary) moved from serif to Archivo in
   the FAQ accordion redesign pass, at explicit build-spec direction.
   The Reach section's country list (.reach-list, serif) was replaced
   entirely by the dotted-map redesign pass — the map's labels use
   Archivo per that pass's explicit build spec, so the selector is
   dropped here rather than left dangling.
   .cred .n (the hero credential rail's numerals) was added in the
   credential-rail pass, inheriting the serif-numeral role from the
   .founder-stats dark box it replaced. .pricing-num and .pp-amount were
   found already using the serif but missing from this list; they are
   recorded here rather than changed, since the price figure is the same
   numeral role — no type change was made to either.
   Newsreader has a real opsz axis (font-optical-sizing: auto on body
   lets it track size automatically) and a real weight range (400/500/
   600) — font-synthesis: none stays set globally on body as a guard
   against the CSS ever requesting a weight/style that isn't loaded. */
