/* ============================================================
   TOKENS
============================================================ */
:root {
  /* New brand palette — radial-stroke logo:
     11% #A27DFF (purple) · 52% #50C8FF (cyan) · 100% #FFC3F9 (pink) */
  --brand-purple: #A27DFF;
  --brand-cyan:   #50C8FF;
  --brand-pink:   #FFC3F9;

  /* Legacy tokens remapped to the new palette so every existing
     var(--blue/--violet/--green) usage flips automatically without a
     per-rule rewrite. Pink replaces the old "green" hot stop, cyan
     replaces old "blue", purple replaces old "violet". */
  --blue:   #50C8FF;
  --violet: #A27DFF;
  --green:  #FFC3F9;

  --grad: linear-gradient(95deg, #A27DFF 0%, #50C8FF 50%, #FFC3F9 100%);
  --grad-soft: linear-gradient(95deg, rgba(162,125,255,.12) 0%, rgba(80,200,255,.12) 50%, rgba(255,195,249,.12) 100%);
  --grad-text: radial-gradient(circle at 50% 50%, #A27DFF 0%, #50C8FF 50%, #FFC3F9 100%);

  --bg: #FBF8F1;
  --bg-warm: #F4F0E6;
  --paper: #F8F4EC;
  --pink-soft: #FCE8F3;  /* pale tint of brand pink (--green #FFC3F9) over cream — marquee band */
  --ink: #0E0E16;
  --ink-soft: #2A2A35;
  --mute: #6A6A75;
  --mute-soft: #9A9AA3;
  --hair: rgba(14,14,22,.10);
  --hair-strong: rgba(14,14,22,.16);

  /* Single sans-serif typeface across the whole site — DM Sans.
     Both vars point at the same family so existing rules that say
     font-family: var(--serif) continue to work; they just render
     in sans now. Keep the variable names so the diff stays small. */
  --serif: "DM Sans", -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  --sans: "DM Sans", -apple-system, BlinkMacSystemFont, system-ui, sans-serif;

  --max: 1280px;
  --pad-x: clamp(20px, 4vw, 56px);
  --section-y: clamp(80px, 10vw, 160px);

  --ease: cubic-bezier(.22,.61,.36,1);
  --ease-out: cubic-bezier(.16,1,.3,1);
}

/* ============================================================
   RESET + BASE
============================================================ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
body {
  font-family: var(--sans);
  font-weight: 400;
  color: var(--ink);
  background: var(--bg);
  line-height: 1.55;
  font-size: 18px;
  letter-spacing: -0.005em;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Anchor + isolate so the document-level brand ribbon (z-index:-1) paints
     above the page background but below all content. */
  position: relative;
  isolation: isolate;
}

/* Warm grain texture — adds depth without noise */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 1;
  opacity: .045;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.6' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .9 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
  mix-blend-mode: multiply;
}

img, svg { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
button { font: inherit; cursor: pointer; border: 0; background: none; color: inherit; }

::selection { background: var(--violet); color: #fff; }

/* ============================================================
   TYPOGRAPHY
============================================================ */
.eyebrow {
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--mute);
  display: inline-flex;
  align-items: center;
  gap: 10px;
}
.eyebrow::before {
  content: "";
  width: 18px; height: 1px;
  background: var(--ink);
  display: inline-block;
}

h1, h2, h3, h4 {
  font-family: var(--serif);
  font-weight: 400;
  letter-spacing: -0.015em;
  color: var(--ink);
}

.display {
  font-family: var(--serif);
  font-weight: 400;
  /* Pulled down from clamp(40, 5.6vw, 84) so the hero H1 wraps to fewer
     lines on common desktop widths; was reaching 4-5 lines around 1280px. */
  font-size: clamp(36px, 4.6vw, 72px);
  line-height: 0.98;
  letter-spacing: -0.03em;
}

.h2 {
  font-family: var(--serif);
  font-size: clamp(34px, 4.8vw, 68px);
  line-height: 0.98;
  letter-spacing: -0.028em;
}

.h3 {
  font-family: var(--serif);
  font-size: clamp(26px, 3.2vw, 40px);
  line-height: 1.08;
  letter-spacing: -0.015em;
}

.lede {
  font-family: var(--sans);
  font-size: clamp(17px, 1.55vw, 21px);
  line-height: 1.5;
  color: var(--ink-soft);
  font-weight: 400;
  max-width: 60ch;
}

/* ============================================================
   ANIMATED BRAND GRADIENT — italic accents + stats + signature + CTA

   Three palettes smoothly fuse on an 18s cycle (≈6s per palette).
   The gradient angle also drifts ~17° across the full cycle — the
   subtle angle motion is what makes the colour feel like it's
   *moving through* the elements rather than just shifting in place.

   Implemented with @property-registered colours so a single
   @keyframes can drive everything: italic accents, stats, founder
   signature, and the primary CTA button.

   The global prefers-reduced-motion rule near end of styles
   collapses all animations to .01ms, so no per-element fallback.
============================================================ */
@property --em-cx {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 0%;
}
@property --em-cy {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 50%;
}

em.gradient, .grad-text {
  font-style: italic;
  /* Two-tone radial gradient with an orbiting center. Tall narrow
     ellipse — colours shift dramatically as the center orbits since the
     visible window through the gradient changes a lot per percent of
     cx/cy. Blue (cyan) is the dominant midtone; purple bookends the orbit
     at 0% and 125%, so the text drifts between blue and purple (no pink). */
  background: radial-gradient(
    ellipse 70% 200% at var(--em-cx) var(--em-cy),
    var(--brand-purple)  0%,
    var(--brand-cyan)   55%,
    var(--brand-purple) 125%
  );
  background-size: 240% 280%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
  /* Italic glyphs can extend past their line/inline box — ascenders,
     descenders, and side bearings on letters like y/h/r/k. With
     background-clip:text, the gradient paint is masked to the inline
     box rect, so any glyph ink outside gets clipped. The padding
     expands the paint region; the negative margin cancels the layout
     impact so wrapping/alignment stay identical. */
  padding: 0.18em 0.08em;
  margin: -0.18em -0.08em;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

/* HERO ONLY — the typed word keeps the live orbit; that motion is the
   hero's signature. (Section-heading em.gradient words are frozen below.)
   Slow orbit (18s) + overshoot beyond the text box at the cardinal
   extremes — the center swings from -10%/110% so the colour redistributes
   gently across each glyph during the cycle. */
.grad-text {
  animation: gradient-orbit 18s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}

/* SECTION HEADINGS — same purple↔cyan radial, but FROZEN (no per-frame
   repaint, so these stop competing for the main thread that paints the
   testimonials etc.). Center parked so the blend runs purple on the left
   → cyan on the right (matches the brand's left-to-right gradient
   direction, both colours clearly present). To reposition the colour,
   just nudge these two numbers — it's a static paint either way, costs
   nothing. */
em.gradient {
  animation: none;
  --em-cx: 0%;
  --em-cy: 18%;
}

@keyframes gradient-orbit {
  0%   { --em-cx: -10%; --em-cy:  20%; }
  20%  { --em-cx:  35%; --em-cy:  -8%; }
  40%  { --em-cx: 110%; --em-cy:  35%; }
  60%  { --em-cx:  75%; --em-cy: 110%; }
  80%  { --em-cx:  10%; --em-cy:  85%; }
  100% { --em-cx: -10%; --em-cy:  20%; }
}

/* Applied by scripts.js to any animated element that is currently off-screen.
   gradient-orbit animates @property values that feed a radial-gradient, which
   the compositor cannot handle — every frame is a real repaint on the main
   thread. Roughly two dozen elements run it (plus the scrolling tracks), and
   they all used to run whether or not they were on screen, so the render
   pipeline never got a quiet moment. That starves whichever section actually
   needs painting — most visibly the testimonials, whose cards are the widest
   thing on the page. A paused animation nobody can see is indistinguishable
   from a running one, so this is a pure performance measure with no visual
   effect. Fallback is safe: if the observer never runs, nothing is paused. */
.anim-idle,
.anim-idle::before,
.anim-idle::after {
  animation-play-state: paused !important;
}

/* ============================================================
   LAYOUT
============================================================ */
.wrap {
  max-width: var(--max);
  margin: 0 auto;
  padding: 0 var(--pad-x);
  position: relative;
  z-index: 2;
}
.wrap--narrow { max-width: 960px; }

section { position: relative; z-index: 2; }

.section { padding: var(--section-y) 0; }

/* Section heading block — single column, anchored to the .wrap
   left edge so it lines up with the hero H1. The numbered eyebrow
   that used to live in a left column has been removed. */
.section-head {
  margin-bottom: clamp(48px, 6vw, 88px);
}
.section-head .head-content { max-width: 720px; }
/* Match the hero eyebrow→headline gap (.hero-eyebrow margin-bottom: 28px)
   so every section's caption breathes the same way as the hero. */
.section-head .eyebrow { margin-bottom: 28px; }
.section-head .h2 { margin-top: 0; }
.section-head .lede { margin-top: 22px; }

/* ============================================================
   BUTTONS
============================================================ */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 16px 26px;
  border-radius: 999px;
  font-family: var(--sans);
  font-weight: 500;
  font-size: 15px;
  letter-spacing: -0.005em;
  transition: transform .35s var(--ease-out), box-shadow .35s var(--ease-out), background .35s var(--ease);
  position: relative;
  overflow: hidden;
  white-space: nowrap;
}
.btn .arrow,
.s-arrow .arrow,
.btn-link .arrow {
  width: 16px; height: 16px;
  transition: transform .35s var(--ease-out);
  flex-shrink: 0;
}
.btn:hover .arrow { transform: translateX(4px); }
.service:hover .s-arrow .arrow { transform: translateX(4px); }

.btn-primary {
  background: var(--ink);
  color: var(--paper);
}
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 12px 32px -8px rgba(14,14,22,.35); }

/* Gradient border by default; fills with gradient on hover via ::before.
   Layered backgrounds: cream interior on padding-box, animated gradient on
   border-box. Transparent border lets the gradient show as a clean outline. */
.btn-grad {
  position: relative;
  isolation: isolate;
  background:
    linear-gradient(var(--bg), var(--bg)) padding-box,
    radial-gradient(
      ellipse 80% 220% at var(--em-cx, 50%) var(--em-cy, 50%),
      var(--brand-purple)  0%,
      var(--brand-cyan)   55%,
      var(--brand-purple) 125%
    ) border-box;
  background-size: auto, 220% 260%;
  border: 5px solid transparent;
  color: var(--ink);
  transition: color .3s var(--ease), transform .3s var(--ease), box-shadow .3s var(--ease);
}
.btn-grad::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(
    ellipse 80% 220% at var(--em-cx, 50%) var(--em-cy, 50%),
    var(--brand-purple)  0%,
    var(--brand-cyan)   55%,
    var(--brand-purple) 125%
  );
  background-size: 220% 260%;
  border-radius: inherit;
  opacity: 0;
  transition: opacity .3s var(--ease);
  z-index: -1;
  pointer-events: none;
}
.btn-grad:hover {
  color: #fff;
  transform: translateY(-2px);
  box-shadow: 0 16px 40px -12px rgba(162,125,255,.45);
}
.btn-grad:hover::before {
  opacity: 1;
}

/* Touch / mobile tier: no real hover state exists, so the gradient-fill
   that normally appears on hover becomes the default. The 5px transparent
   border stays so outer dimensions match desktop (no layout shift).
   :hover lift/shadow suppressed because :hover sticks after a tap on
   touch devices — would leave the button visually "stuck" lifted. */
@media (hover: none), (max-width: 880px) {
  .btn-grad { color: #fff; }
  .btn-grad::before { opacity: 1; }
  .btn-grad:hover {
    transform: none;
    box-shadow: none;
  }
}

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

.btn-link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 15px;
  font-weight: 500;
  padding: 4px 0;
  border-bottom: 1px solid var(--ink);
  transition: gap .3s var(--ease);
}
.btn-link:hover { gap: 14px; }

/* ============================================================
   NAV
============================================================ */
.nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 100;
  padding: 18px var(--pad-x);
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* Transparent at the top so the hero ticker shows THROUGH the floating
     nav. The ticker's own cream top-scrim (.hero-ticker::before — "the nav
     legibility band") fades the photos behind the links so they stay
     readable. Frosts to cream once scrolled past the hero. */
  background: transparent;
  transition: background .4s var(--ease), backdrop-filter .4s var(--ease), padding .4s var(--ease), border-color .4s var(--ease);
  border-bottom: 1px solid transparent;
  /* Force a persistent GPU compositor layer. Without this, Chrome Android
     evicts the nav's painted layer when the URL bar slides in at scrollY=0,
     producing a one-frame gap where the navbar appears blank. */
  transform: translateZ(0);
  -webkit-transform: translateZ(0);
}
.nav.scrolled {
  /* Past the hero there's no ticker behind the bar — frost it to cream so the
     links stay legible over whatever section scrolls underneath. */
  background: rgba(251,248,241,.82);
  backdrop-filter: saturate(140%) blur(14px);
  -webkit-backdrop-filter: saturate(140%) blur(14px);
  padding: 12px var(--pad-x);
  border-bottom-color: var(--hair);
}
.nav-brand {
  /* Soaks up ALL the left-hand slack so the links + CTA are pushed to the
     RIGHT of the bar — restoring the original right-biased link position. (The
     original used position:absolute; left:50%; right:220px to sit the links in
     the right portion; that rule now overlaps the wider "Register for a free
     session" pill, so instead the brand grows to shove the links right and the
     CTA keeps a fixed gap below — the closest non-overlapping match.) */
  flex: 1;
  display: flex;
  align-items: center;
  gap: 12px;
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink);
  transition: color .3s var(--ease);
}
.nav-brand .brand-mark { height: 44px; }
/* All-caps wordmark — italic doesn't read well in this treatment, so the
   em is leveled to upright. The semantic emphasis stays in markup. */
.nav-brand span em { font-style: normal; }

/* Stacked wordmark — "Breezy" on top, "Minutes" beneath, sitting flush
   left against the heart logo. Used in both the nav and the footer (the
   footer overrides line-height + gap below for its serif/larger size).
   Default sizing here is tuned for the nav: line-height 1.15 + 5px gap
   gives the 13px text a ~35px stacked height, which sits proportionally
   against the 44px heart logo (~80% of logo height — balanced without
   towering). */
.brand-text {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  line-height: 1.15;
  gap: 5px;
}

.nav-links {
  /* Natural width, in flow. The brand column (flex:1) eats the left slack and
     pushes the links to the right; the CTA's left margin holds a fixed gap on
     the right. Net: links sit right-of-centre, near the CTA — the original
     look — and being flex children they can't overlap it. */
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  font-size: 13px;
  font-weight: 400;
}
/* Each link mirrors the CTA (.btn-grad): a gradient BORDER over a cream fill
   with black text; on hover the gradient fills in and the text turns white.
   Same purple -> cyan orbit, kept in sync via gradient-orbit. */
.nav-links a {
  position: relative;
  isolation: isolate;
  padding: 7px 15px;
  border-radius: 999px;
  border: 1.5px solid transparent;
  background:
    linear-gradient(var(--bg), var(--bg)) padding-box,
    radial-gradient(
      ellipse 80% 220% at var(--em-cx, 50%) var(--em-cy, 50%),
      var(--brand-purple)  0%,
      var(--brand-cyan)   55%,
      var(--brand-purple) 125%
    ) border-box;
  background-size: auto, 220% 260%;
  color: var(--ink);
  white-space: nowrap;
  transition: color .3s var(--ease), transform .3s var(--ease);
}
.nav-links a::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: radial-gradient(
    ellipse 80% 220% at var(--em-cx, 50%) var(--em-cy, 50%),
    var(--brand-purple)  0%,
    var(--brand-cyan)   55%,
    var(--brand-purple) 125%
  );
  background-size: 220% 260%;
  opacity: 0;
  transition: opacity .3s var(--ease);
  z-index: -1;
  pointer-events: none;
}
.nav-links a:hover {
  color: #fff;
  transform: translateY(-1px);
}
.nav-links a:hover::before { opacity: 1; }
/* Orbit the link gradients in sync with the CTA. The visible gradient BORDER
   on the link itself orbits continuously (that's the design). The ::before is
   the hover FILL — it sits at opacity:0 until hovered, so orbiting it while
   it's invisible is a full repaint per frame, forever, in a position:fixed bar
   that never leaves the screen. Orbit it only while it's actually on show. */
.nav-links a {
  animation: gradient-orbit 18s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}
.nav-links a:hover::before,
.nav-links a:focus-visible::before {
  animation: gradient-orbit 18s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}
/* Natural width, pinned to the right by the brand's flex:1. The left margin is
   the breathing gap between the links and the CTA (the only thing standing
   between them now that the links are shoved fully right). */
.nav-cta { flex: 0 0 auto; display: flex; align-items: center; gap: 14px; margin-left: 40px; }

/* The bar is opaque cream in every state, so the wordmark, hamburger, and
   link text all stay ink — no "white over dark photo" flips needed. */

/* Hamburger toggle */
.nav-toggle {
  display: none;
  background: none;
  border: 0;
  cursor: pointer;
  padding: 8px;
  z-index: 102;
  color: var(--ink);
  transition: color .3s var(--ease);
}
.nav-toggle span {
  display: block;
  width: 24px;
  height: 2px;
  background: currentColor;
  border-radius: 2px;
  margin: 5px 0;
  transition: transform .3s var(--ease), opacity .2s var(--ease);
}
.nav-toggle.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-toggle.open span:nth-child(2) { opacity: 0; }
.nav-toggle.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

/* Mobile menu overlay */
.nav-mobile {
  position: fixed;
  inset: 0;
  background: var(--bg);
  z-index: 99;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 28px;
  font-family: var(--serif);
  font-size: clamp(28px, 5vw, 40px);
  letter-spacing: -0.015em;
  opacity: 0;
  visibility: hidden;
  transform: scale(.96);
  transition: opacity .3s var(--ease), transform .3s var(--ease), visibility 0s .3s;
}
.nav-mobile.open {
  opacity: 1;
  visibility: visible;
  transform: scale(1);
  transition: opacity .3s var(--ease), transform .3s var(--ease);
}
.nav-mobile a { color: var(--ink); }
.nav-mobile .btn-primary {
  margin-top: 18px;
  font-family: var(--sans);
  font-size: 15px;
  letter-spacing: 0;
}

@media (max-width: 1280px) {
  .nav-links { display: none; }
  .nav-cta { display: none; }
  .nav-toggle { display: block; }
}

/* ============================================================
   HERO  —  full-bleed split: left content padded to wrap, right
   image flush to viewport edge with cross-fading photo stack.
============================================================ */
.hero {
  /* Breathing room below the fixed (transparent) nav so the portrait's head
     and the heading don't ride right up against it. Kept under the 70px the
     grid subtracts from its min-height, so the whole hero block shifts down as
     a rigid unit — the head-above-heading proportion is preserved and the hero
     still never exceeds the viewport (no scroll). */
  padding: clamp(32px, 4.5vh, 52px) 0 0;
  position: relative;
}

.hero-grid {
  display: grid;
  /* Text takes the remaining space; the portrait gets a clamped column so it
     never collapses on smaller laptops / tablet-landscapes. At ~1456px the
     clamp lands on ~495px (the approved desktop look); below ~1382px it holds
     a steady 470px floor instead of shrinking with a raw 1fr — which is what
     dropped the head far below the heading at 1024–1366. Capped at 500px so
     it doesn't grow absurdly large on very wide screens. */
  grid-template-columns: minmax(0, 1fr) clamp(470px, 34vw, 700px);
  align-items: stretch;
  min-height: calc(100vh - 70px);
}

/* Right column — a single static cut-out portrait, grounded to the bottom of
   the hero and kept under half the hero width. Intersecting edge masks fade
   the cropped sides + bottom into the cream so the cut-out never reads as a
   hard rectangle (the head stays crisp). Decorative → aria-hidden in markup. */
.hero-figure {
  position: relative;
  z-index: 1;
  align-self: stretch;          /* fill the full grid-row height */
  display: flex;
  align-items: flex-end;        /* ground the portrait to the hero bottom */
  justify-content: center;
  height: 100%;
  overflow: hidden;
  pointer-events: none;
}
.hero-figure-img {
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  /* The portrait is taller than it is wide, so on short desktop viewports the
     height cap — not the column width — is what sizes it, and at 82vh its crown
     rode up behind the fixed nav's CTA button (~1440x700). 78vh clears it. On
     normal-height screens the column width binds first, so this is invisible. */
  max-height: min(78vh, 700px);
  object-fit: contain;
  /* Lift the bottom-grounded portrait slightly so its head sits a touch
     higher / closer to the heading. margin (not transform) because the
     fadeUp animation owns transform and would override a static one. */
  margin-bottom: 24px;
  /* Edge vignette: fade left + right sides AND the bottom (masks intersect),
     leaving the top crisp so the head reads sharply. */
  -webkit-mask-image:
    linear-gradient(to right, transparent 0, #000 9%, #000 91%, transparent 100%),
    linear-gradient(to bottom, #000 0, #000 78%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-image:
    linear-gradient(to right, transparent 0, #000 9%, #000 91%, transparent 100%),
    linear-gradient(to bottom, #000 0, #000 78%, transparent 100%);
  mask-composite: intersect;
  opacity: 0;
  animation: fadeUp 1s var(--ease-out) .35s forwards;
}

/* Left column — content stays aligned to the global .wrap left edge */
.hero-content-wrap {
  position: relative;
  z-index: 3;
  display: flex;
  align-items: center;
  /* Bias the text lower so it balances with the bottom-grounded portrait
     instead of floating in the upper-left (more top pad, less bottom pad). */
  padding-top: clamp(130px, 19vh, 188px);
  padding-bottom: clamp(28px, 3.5vw, 48px);
  padding-right: clamp(32px, 5vw, 80px);
  /* Align left padding to the same x-position as .wrap (which is
     centered with max-width: var(--max) and inner padding var(--pad-x)). */
  padding-left: max(var(--pad-x), calc((100vw - var(--max)) / 2 + var(--pad-x)));
}
.hero-content {
  position: relative;
  max-width: 760px;
  width: 100%;
  /* Shared text measure: the H1 and the body paragraph both cap at this
     width so their right edges line up (the heading no longer overruns the
     paragraph). ~matches the body's old 44ch on desktop. */
  --hero-measure: 48rem;
}

.hero-eyebrow {
  margin-bottom: 32px;
  /* Soft cream halo lifts the small text off the ribbon without a box. */
  text-shadow: 0 0 10px var(--bg), 0 0 18px var(--bg);
  opacity: 0;
  animation: fadeUp .8s var(--ease-out) .15s forwards;
}

.hero h1 {
  position: relative;
  z-index: 3;
  /* Cap to the shared measure so the heading wraps within the paragraph's
     right edge instead of overrunning it. */
  max-width: var(--hero-measure);
  line-height: 1.05;
  opacity: 0;
  animation: fadeUp 1s var(--ease-out) .25s forwards;
}

/* Hero heading typing effect — one cycling word (easy/natural/yours) gets
   typed + erased by JS (see scripts.js), with a blinking caret beside it.
   .type stays on one line so the caret never wraps to the next line. */
.hero h1 .type { white-space: nowrap; }
.hero h1 .type-caret {
  display: inline-block;
  width: 0.055em;
  height: 0.78em;
  margin-left: 0.06em;
  /* Same orbiting two-tone gradient as the inline .grad-text word, so the
     caret reads as part of the gradient phrase rather than a flat dark bar.
     The caret is a solid fill (no background-clip:text), so the gradient
     paints the bar directly; gradient-orbit drives its own --em-cx/--em-cy. */
  background: radial-gradient(
    ellipse 70% 200% at var(--em-cx) var(--em-cy),
    var(--brand-purple)  0%,
    var(--brand-cyan)   55%,
    var(--brand-purple) 125%
  );
  background-size: 240% 280%;
  vertical-align: -0.04em;
  border-radius: 1px;
  animation: caret-blink 1.05s steps(1) infinite,
             gradient-orbit 18s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}
@keyframes caret-blink { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }

.hero-body {
  max-width: var(--hero-measure);
  margin-top: 18px;
  font-size: clamp(17px, 1.3vw, 19px);
  line-height: 1.5;
  color: var(--ink-soft);
  /* Soft cream halo lifts the body copy off the ribbon without a box. */
  text-shadow: 0 0 10px var(--bg), 0 0 16px var(--bg), 0 0 16px var(--bg);
  opacity: 0;
  animation: fadeUp .9s var(--ease-out) .45s forwards;
}

.hero-cta {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 22px;
  opacity: 0;
  animation: fadeUp .9s var(--ease-out) .6s forwards;
}
/* Larger primary CTA in the hero */
.hero-cta .btn {
  padding: 19px 38px;
  font-size: 17px;
}

.hero-meta {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 56px;
  font-size: 13px;
  color: var(--mute);
  text-shadow: 0 0 10px var(--bg), 0 0 16px var(--bg);
  opacity: 0;
  animation: fadeUp .9s var(--ease-out) .75s forwards;
}
.hero-meta .dot {
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--brand-purple);
  box-shadow: 0 0 0 4px rgba(162,125,255,.18);
  animation: pulse 2.4s ease-in-out infinite;
}
@keyframes pulse {
  0%, 100% { box-shadow: 0 0 0 4px rgba(162,125,255,.18); }
  50% { box-shadow: 0 0 0 8px rgba(162,125,255,.06); }
}

/* Single column — the portrait stacks above the text. Triggers for narrow
   viewports (≤900px) AND for any tablet-shaped screen up to 1400px wide with an
   aspect ratio ≤ 3:2 — that catches BOTH iPad portrait (≤1024w, tall) and iPad
   LANDSCAPE (1024–1366w, ~4:3), which on width alone stayed in the desktop grid
   and left the bottom-grounded portrait sitting well below the heading. True
   laptops (16:10 / 16:9, aspect ≥ ~1.6) stay side-by-side via the clamped grid
   above, where the proportion holds. */
@media (max-width: 900px), (max-width: 1400px) and (max-aspect-ratio: 3 / 2) {
  .hero {
    padding-top: 0;               /* figure padding-top owns the nav clearance here */
  }
  .hero-grid {
    grid-template-columns: 1fr;
    min-height: 0;
  }
  .hero-figure {
    order: 1;
    align-items: flex-end;
    height: auto;
    padding-top: clamp(112px, 13vh, 140px); /* clear the fixed nav + breathing room */
  }
  .hero-figure-img {
    max-height: 52vh;
    margin-bottom: 0;             /* the desktop lift doesn't apply when stacked */
  }
  .hero-content-wrap {
    order: 2;
    padding: clamp(36px, 6vw, 64px) var(--pad-x) clamp(56px, 9vw, 88px);
  }
}

@keyframes fadeUp {
  from { opacity: 0; transform: translateY(20px); }
  to { opacity: 1; transform: translateY(0); }
}

/* ============================================================
   MARQUEE / TRUST
============================================================ */
.marquee {
  /* Soft brand gradient band (purple → cyan → pink), mirroring --grad at a
     low alpha so it reads as the brand tri-colour over cream while keeping
     dark text legible. Layered over --bg so the alpha blends onto cream. */
  background:
    linear-gradient(95deg,
      rgba(162,125,255,.24) 0%,
      rgba(80,200,255,.26) 50%,
      rgba(255,195,249,.34) 100%),
    var(--bg);
  border-top: 1px solid var(--hair-strong);
  border-bottom: 1px solid var(--hair-strong);
  padding: 30px 0;
  overflow: hidden;
  position: relative;
}
.marquee::before, .marquee::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0;
  width: 90px;
  z-index: 2;
  pointer-events: none;
}
/* Edge fades approximate the band's colour at each end (lavender at the
   purple left edge, pink at the right) so the moving text dissolves into the
   band rather than into a cream notch. */
.marquee::before { left: 0; background: linear-gradient(90deg, #E1DEF4, rgba(225,222,244,0)); }
.marquee::after { right: 0; background: linear-gradient(-90deg, #FCE7F3, rgba(252,231,243,0)); }

.marquee-track {
  display: flex;
  gap: 64px;
  white-space: nowrap;
  animation: scroll 38s linear infinite;
  width: max-content;
}
.marquee-item {
  font-family: var(--serif);
  font-style: italic;
  font-size: 24px;
  font-weight: 500;
  letter-spacing: -0.01em;
  color: var(--ink);
  display: flex;
  align-items: center;
  gap: 64px;
}
/* Gradient-filled diamond separator — the brand gradient clipped to the glyph
   so the punctuation itself carries the tri-colour. */
.marquee-item::after {
  content: "✦";
  font-style: normal;
  font-size: 17px;
  background: var(--grad);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
}
@keyframes scroll {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

/* ============================================================
   INTRO / POSITIONING
============================================================ */
.intro {
  /* Unified with --section-y so intro and other sections share the same
     vertical rhythm instead of intro being noticeably taller. */
  padding-top: var(--section-y);
  padding-bottom: 0;
}
/* Intro is now a centred testimonial pull-quote: a gradient opening
   mark, the quote, then a footer row pairing the attribution (left)
   with the gradient closing mark (right). */
.intro-quote-stack {
  display: flex;
  flex-direction: column;
  /* Left-anchored to the .wrap edge (like the original founder's-notebook
     block) so the quote lines up with the hero and section headings — not
     centred. 720px keeps the original measure. Children stretch to 720 by
     default, which keeps the closing mark at the right edge in .intro-foot;
     the open mark has a definite width so it stays left at its own size. */
  max-width: 720px;
}
.intro-qmark {
  width: clamp(46px, 5vw, 68px);
  height: auto;
  display: block;
}
.intro-qmark--open { margin-bottom: clamp(28px, 3vw, 44px); }
.intro-quote-block { margin: 0; }
.intro-quote {
  font-family: var(--serif);
  /* A touch below the founder's-notebook scale — large, but not overpowering. */
  font-size: clamp(22px, 3vw, 38px);
  line-height: 1.22;
  letter-spacing: -0.02em;
  color: var(--ink);
}
.intro-quote + .intro-quote { margin-top: clamp(20px, 2vw, 32px); }
.intro-quote em { font-style: italic; }
.intro-foot {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: 24px;
  margin-top: clamp(32px, 3.5vw, 52px);
}
.intro-qmark--close { flex-shrink: 0; }
.intro-source {
  display: flex;
  align-items: center;
  gap: 14px;
  font-size: 14px;
  color: var(--mute);
}

/* Attribution portrait. Same treatment as the testimonial cards' avatars
   (gradient disc behind a circular crop) at a smaller size, so the two
   places the same person appears look like the same system. The gradient
   ring also covers the moment before a lazy image decodes — no empty hole. */
.intro-avatar {
  width: 52px; height: 52px;
  border-radius: 50%;
  background: var(--grad);
  display: grid;
  place-items: center;
  overflow: hidden;
  flex-shrink: 0;
}
.intro-avatar img {
  width: 100%; height: 100%;
  object-fit: cover;
  border-radius: 50%;
  display: block;
}
.intro-attr { display: flex; flex-direction: column; gap: 3px; }
.intro-person {
  font-size: 15.5px;
  font-weight: 600;
  color: var(--ink);
  letter-spacing: -0.01em;
  line-height: 1.25;
}
.intro-role { font-size: 13.5px; line-height: 1.35; color: var(--mute); }

@media (max-width: 640px) {
  /* Stack the footer so the long attribution doesn't crowd the mark. */
  .intro-foot { flex-direction: column; align-items: flex-start; gap: 18px; }
  .intro-qmark--close { order: -1; }
}


/* ============================================================
   SERVICES
============================================================ */
#services.section { padding-bottom: 0; }

.program-block {
  margin-top: clamp(36px, 5vw, 72px);
}
.program-block:first-of-type { margin-top: clamp(24px, 4vw, 52px); }

/* Static gradient panel per topic: a vibrant blue → pink rounded panel that
   holds the full-width heading and, beneath it, a row of 3 program cards.
   NO sticky behaviour — the whole thing sits in normal flow and auto-sizes to
   its content. background-size 100% 100% stretches the smooth gradient to
   fill the panel however tall it grows; border-radius clips it. */
.program-panel {
  border-radius: 32px;
  background: url("images/learn-gradient-b.webp") center / 100% 100% no-repeat;  /* blue → pink, 90% (same asset as the learn panels) */
  padding: clamp(36px, 5.5vw, 68px) clamp(24px, 4.5vw, 56px);
}

/* Block heading — large sentence-case serif (weight 400, tight tracking),
   ALWAYS on one line (no wrap). Sized so "Business communication" fits one
   line down to a ~320px phone; grows large on desktop below. */
.block-title {
  font-family: var(--serif);
  font-weight: 400;
  letter-spacing: -0.02em;
  font-size: clamp(22px, 6.4vw, 38px);
  line-height: 1.05;
  color: var(--ink);
  margin: 0 0 clamp(26px, 3.4vw, 44px);
  white-space: nowrap;
}

/* ---- Three static program cards in a row on the gradient panel ---- */
.program-cards {
  display: grid;
  grid-template-columns: 1fr;   /* mobile: stacked */
  gap: clamp(16px, 1.8vw, 26px);
}

/* Neutral white card that, on hover, lifts and fades in a pale brand-gradient
   wash behind the dark text — the same mechanic as the testimonial cards. */
.pcard {
  position: relative;
  isolation: isolate;
  background: #fff;
  border: 1px solid rgba(20, 12, 40, 0.06);
  border-radius: 24px;
  /* Trimmed from clamp(32px, 3.4vw, 52px). At the old value a 326px card
     spent 104px on padding and left 226px for text — about 30 characters a
     line. These cards held a single sentence when that was set; they hold
     paragraphs now, and the width is worth more than the air. */
  padding: clamp(28px, 2.6vw, 38px);
  display: flex;
  flex-direction: column;
  box-shadow: 0 18px 44px -30px rgba(20, 12, 40, 0.4);
  transition:
    transform .4s var(--ease),
    box-shadow .4s var(--ease),
    border-color .35s var(--ease);
}
/* Hover wash — pale pink → pale cyan → pale lilac (light brand tints). At
   z-index:-1 inside the card's isolated context it paints above the white
   background but below the text, so it crossfades in cleanly on hover. */
.pcard::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(135deg, #FFDCF8 0%, #D2ECFF 48%, #DFD3FF 100%);
  opacity: 0;
  transition: opacity .35s var(--ease);
  z-index: -1;
  pointer-events: none;
}
.pcard:hover {
  transform: translateY(-6px);
  box-shadow: 0 28px 60px -24px rgba(20, 12, 40, 0.28);
  border-color: transparent;
}
/* Held under full strength so the wash reads as a light tint over the white
   card rather than a solid pastel fill. */
.pcard:hover::before { opacity: .6; }

.pcard h3 {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(27px, 2.7vw, 38px);
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--ink);
  margin: 0 0 clamp(18px, 1.5vw, 24px);
}
.pcard p {
  font-size: clamp(15px, 1.15vw, 18px);
  line-height: 1.6;
  color: var(--ink-soft);
  /* Cards now hold several paragraphs, so these flow normally from the top.
     What gets bottom-anchored is .pcard-foot (the toggle + CTA), which is
     what actually needs to line up across the equal-height cards. */
  margin: 0 0 clamp(12px, 1vw, 16px);
  /* Only bites in the STACKED layout, where a card spans the full panel and
     an uncapped line would run past 100 characters. Never binds in the
     three-column layout. */
  max-width: 68ch;
}
.pcard-more > p:last-child { margin-bottom: 0; }

/* "Best for:" — the Business-only qualifier. It lives OUTSIDE the card, as
   its own pill beneath it, so it can be read without expanding anything.
   Translucent rather than solid white so it reads as a caption attached to
   the card above rather than as a second, smaller card. */
.bestfor-pill {
  margin: 0;
  background: rgba(255, 255, 255, .5);
  border: 1px solid rgba(255, 255, 255, .6);
  border-radius: 18px;
  padding: clamp(14px, 1.3vw, 18px) clamp(18px, 1.7vw, 24px);
  font-size: clamp(14px, 1vw, 15.5px);
  line-height: 1.5;
  color: var(--ink-soft);
}
.bestfor-pill b { font-weight: 600; color: var(--ink); }

/* Toggle + CTA, pinned to the bottom of the card. */
.pcard-foot {
  margin-top: auto;
  padding-top: clamp(16px, 1.5vw, 22px);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(12px, 1.1vw, 16px);
}

/* Hidden by default: with no JS the cards ship fully expanded, so a control
   that collapses them would do nothing. scripts.js reveals it. */
.pcard-toggle { display: none; }
.program-cards.is-collapsible .pcard-toggle {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 0;
  border: 0;
  background: none;
  font-family: var(--sans);
  font-size: 14.5px;
  font-weight: 500;
  color: var(--mute);
  cursor: pointer;
  transition: color .25s var(--ease);
}
.program-cards.is-collapsible .pcard-toggle:hover { color: var(--ink); }
.pcard-toggle .chev {
  width: 15px; height: 15px;
  flex-shrink: 0;
  transition: transform .35s var(--ease);
}
/* Chevron points down when there is more to read, up when expanded. */
.pcard-toggle[aria-expanded="true"] .chev { transform: rotate(180deg); }

/* The collapse itself. The OPEN height is set inline by scripts.js from the
   content's own scrollHeight — deliberately not a fixed value here. A fixed
   cap has to be guessed against the narrowest card at the smallest
   three-column width, and any later copy edit silently clips text once it
   grows past the guess. */
.program-cards.is-collapsible .pcard-more {
  max-height: 0;
  opacity: 0;
  overflow: hidden;
  transition:
    max-height .45s var(--ease),
    opacity .3s var(--ease);
}
.program-cards.is-collapsible .pcard.is-open .pcard-more { opacity: 1; }

/* Collapsed, the cards stretch to equal height — tidy, since their opening
   lines are similar lengths. Once one is expanded that would drag the other
   two to match it and leave them mostly empty, so the row switches to
   top-aligned and only the open card is tall. */
.program-cards.has-open { align-items: start; }

.pcard .s-arrow {
  margin-top: 0;   /* .pcard-foot's auto top-margin anchors this group to the bottom */
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 16px;
  font-weight: 600;
  color: var(--ink);
  align-self: flex-start;
  border-bottom: 1.5px solid var(--ink);
  padding-bottom: 2px;
  white-space: nowrap;
  transition: gap .3s var(--ease);
}
.pcard:hover .s-arrow { gap: 14px; }
.pcard:hover .s-arrow .arrow { transform: translateX(4px); }

/* Three columns — but only once there is genuinely room for three columns
   of prose. These cards used to hold a single line each, so 761px was fine;
   they now carry full descriptions, and at 761px each card is ~190px wide,
   leaving about 123px of text — roughly 14 characters per line. Measured
   upward from there: 1000px still only bought 186px of text (~25 chars).
   1180px is where a card clears ~250px, which is a narrow but readable
   column. Below it they stack full-width instead.
   NOTE: this governs COLUMN COUNT only. "Read more" collapsing used to be
   gated on the same width in scripts.js; it now applies at every width, so
   this number can move on its own. */
@media (min-width: 1180px) {
  .program-cards { grid-template-columns: repeat(3, 1fr); }

  /* The Business row interleaves card, pill, card, pill, card, pill in the
     DOM — the order the STACKED layout needs. Here that order is placed
     explicitly into two rows so each pill lands under its own card. Both
     rows size themselves, so all three cards share a height and all three
     pills share a height, however unevenly the text falls. */
  .program-cards--bestfor > :nth-child(1) { grid-column: 1; grid-row: 1; }
  .program-cards--bestfor > :nth-child(2) { grid-column: 1; grid-row: 2; }
  .program-cards--bestfor > :nth-child(3) { grid-column: 2; grid-row: 1; }
  .program-cards--bestfor > :nth-child(4) { grid-column: 2; grid-row: 2; }
  .program-cards--bestfor > :nth-child(5) { grid-column: 3; grid-row: 1; }
  .program-cards--bestfor > :nth-child(6) { grid-column: 3; grid-row: 2; }
}

/* Desktop: larger one-line heading + roomier panel. */
@media (min-width: 881px) {
  .program-panel {
    padding: clamp(64px, 6.5vw, 104px) clamp(48px, 4.5vw, 72px);
  }
  .block-title {
    font-size: clamp(40px, 5.4vw, 64px);
    margin-bottom: clamp(40px, 4vw, 64px);
  }
  .program-cards { gap: clamp(22px, 2vw, 32px); }
}

/* ============================================================
   WHAT YOU'LL LEARN
   A sticky "stack": three full-height panels pin to the top of the
   viewport one after another as you scroll — panel 2 rises and
   covers panel 1, then panel 3 covers panel 2, then the section
   releases. Each panel is split in two: a gradient field holding a
   stack of expand-on-hover pills (left), and the heading + sentence
   (right). Each panel carries an OPAQUE cream background so a pinned
   panel fully hides the one beneath it (no bleed-through).
============================================================ */
.learn { background: transparent; }
.learn-stack {
  display: flex;
  flex-direction: column;
  gap: clamp(20px, 3vw, 40px);
}

/* Each panel is one rounded, full-bleed gradient card (no more sticky
   stack, no more cream half). Block heading sits on the LEFT, the pill
   stack on the RIGHT. The gradient lives on a ::before so the rounded
   corners + overflow clip it cleanly; content rides a raised layer
   above it (isolation gives the panel its own stacking context). */
.learn-panel {
  position: relative;
  isolation: isolate;
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: start;   /* heading + pills both start at the top */
  gap: clamp(24px, 4vw, 64px);
  border-radius: 28px;
  overflow: hidden;
  /* Vertical padding raised from clamp(40px, 5.5vw, 88px): both panels now
     hold a heading plus a copy card rather than a heading plus one line,
     and the old value left the content pressed against the panel edges. */
  padding: clamp(52px, 6.6vw, 108px) clamp(32px, 5vw, 84px);
}
.learn-panel::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background-size: cover;
  background-position: center;
}
/* Alternate the two 90%-opacity brand gradients (baked into the PNGs). */
.learn-panel:nth-child(1)::before { background-image: url("images/learn-gradient-a.webp"); } /* pink → blue */
.learn-panel:nth-child(2)::before,
.learn-panel:nth-child(3)::before { background-image: url("images/learn-gradient-b.webp"); } /* blue → pink */

/* LEFT — block heading + one-line description, on the gradient. */
.learn-copy {
  position: relative;
  z-index: 1;
  grid-column: 1;
  grid-row: 1;      /* keep heading + pills on one row (no diagonal) */
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.learn-copy h3 {
  font-family: var(--serif);
  font-size: clamp(28px, 3.4vw, 44px);
  line-height: 1.12;
  letter-spacing: -0.02em;
  /* Was a flat 18px, set when the heading sat above a single line of text.
     Against the copy card that gap read as cramped — the heading needs to
     clear the card's own top padding as well as itself. */
  margin-bottom: clamp(26px, 2.8vw, 40px);
}
.learn-desc {
  color: var(--ink-soft);
  font-size: clamp(17px, 1.5vw, 20px);
  line-height: 1.55;
  /* Was 32ch, sized for a one-line description. The panels now carry a
     full paragraph, and 32ch turned that into a very tall, very narrow
     ribbon of text. 46ch is still inside the comfortable 45–75ch reading
     measure while letting the written line breaks below survive. */
  max-width: 46ch;
}
.learn-desc + .learn-desc { margin-top: 1em; }

/* Copy card — a translucent white surface for a panel's body copy, so a
   longer description reads as content on a surface instead of loose text
   floating on the gradient. Deliberately quieter than the solid-white pill
   cards opposite: half-opaque white with a soft edge highlight, so the
   panel gradient still shows through and the pills stay the brighter
   element. No backdrop-filter — a blur here would add a per-frame
   compositing cost on exactly the low-powered laptops this section was
   already tuned for. */
.learn-note {
  background: rgba(255, 255, 255, .5);
  border: 1px solid rgba(255, 255, 255, .6);
  border-radius: 22px;
  padding: clamp(22px, 2.2vw, 32px) clamp(24px, 2.4vw, 34px);
  box-shadow: 0 14px 34px -22px rgba(14, 14, 22, .28);
}
/* Inside the card the column is already narrow — let the text use the full
   width rather than being capped again by the paragraph measure. */
.learn-note .learn-desc { max-width: none; }

/* BOTTOM-ALIGNED: the copy card's bottom edge sits level with the last
   pill's, on both panels — even though panel 1's copy is the taller column
   and panel 2's pill stack is.

   Two rules doing different jobs, which is what makes it work either way
   round WITHOUT dragging the heading along:

   - .learn-copy STRETCHES to the full row height and pushes its two
     children apart, so the heading stays pinned to the top of the panel
     and only the card drops to the bottom. That is what moves panel 2's
     card down while its heading stays put.
   - .learn-visual bottom-aligns, so the pill stack drops to meet the
     card. That is what moves panel 1's pills down.

   Whichever column is shorter is the one that visibly moves; the taller
   one already defines the row height and stays where it is.

   Panel 3 is excluded — it stacks copy ABOVE a full-width pill grid, so
   there is no second column to align against. */
.learn-panel:nth-child(1) .learn-copy,
.learn-panel:nth-child(2) .learn-copy {
  align-self: stretch;
  justify-content: space-between;
}
.learn-panel:nth-child(1) .learn-visual,
.learn-panel:nth-child(2) .learn-visual { align-self: end; }

/* RIGHT — the pill stack. */
.learn-visual {
  position: relative;
  z-index: 1;
  grid-column: 2;
  grid-row: 1;
  display: flex;
  justify-content: center;
}
.learn-cards {
  display: flex;
  flex-direction: column;
  gap: clamp(14px, 1.4vw, 20px);
  width: 100%;
  max-width: 480px;
}

/* Big "in your face" pills. Title always shown; body reveals on
   hover / focus (kept from the original). */
.learn-card {
  background: #fff;
  border-radius: 20px;
  padding: clamp(20px, 1.8vw, 30px) clamp(24px, 2vw, 34px);
  text-align: center;
  cursor: default;
  box-shadow:
    0 12px 28px -12px rgba(14, 14, 22, .30),
    0 2px 6px -2px rgba(14, 14, 22, .12);
  transition:
    transform .35s var(--ease),
    box-shadow .35s var(--ease);
}
.learn-card-title {
  font-family: var(--serif);
  font-size: clamp(21px, 1.9vw, 30px);
  font-weight: 500;
  letter-spacing: -0.015em;
  color: var(--ink);
  line-height: 1.2;
}
.learn-card-body {
  font-size: clamp(15px, 1.15vw, 17px);
  line-height: 1.5;
  color: var(--ink-soft);
  max-height: 0;
  opacity: 0;
  overflow: hidden;
  margin-top: 0;
  /* max-height transition lets the pills shift smoothly as this one grows. */
  transition:
    max-height .45s var(--ease),
    opacity .3s var(--ease),
    margin-top .45s var(--ease);
}
.learn-card:hover,
.learn-card:focus-within {
  transform: translateY(-2px);
  box-shadow:
    0 18px 38px -14px rgba(14, 14, 22, .36),
    0 3px 8px -2px rgba(14, 14, 22, .14);
}
.learn-card:hover .learn-card-body,
.learn-card:focus-within .learn-card-body {
  max-height: 160px;
  opacity: 1;
  margin-top: 10px;
}

/* "Apply it where it actually counts" (3rd panel) has 6 pills. Stack the
   heading ABOVE a full-width row of pills (rather than heading-left /
   pills-right) so each pill gets enough width for its label to sit on
   one line — no mid-word breaks. */
.learn-panel:nth-child(3) { grid-template-columns: 1fr; }
.learn-panel:nth-child(3) .learn-copy   { grid-column: 1; grid-row: 1; }
.learn-panel:nth-child(3) .learn-visual { grid-column: 1; grid-row: 2; }
.learn-panel:nth-child(3) .learn-cards {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: start;
  max-width: none;
}

/* Tablet / phone — single column, pills expanded (touch can't hover). */
@media (max-width: 880px) {
  .learn-panel {
    grid-template-columns: 1fr;
    gap: clamp(24px, 6vw, 40px);
    border-radius: 22px;
    padding: clamp(36px, 8vw, 56px) clamp(24px, 6vw, 44px);
  }
  /* Stack: heading on top, pills below. */
  .learn-copy   { grid-column: 1; grid-row: 1; }
  .learn-visual { grid-column: 1; grid-row: 2; }
  .learn-cards,
  .learn-panel:nth-child(3) .learn-cards {
    display: flex;
    flex-direction: column;
    max-width: none;
  }
  .learn-card { text-align: left; }
  .learn-card-body { max-height: 220px; opacity: 1; margin-top: 10px; }
}

/* ============================================================
   FORMATS STRIP
   Four delivery-format mini-cards sitting beneath the topic-based
   Services cascade. WHAT we cover lives above; HOW it's delivered
   lives here. Section-head--compact drops the lede so the strip
   reads as a tight follow-up to the cascade, not a full section.
============================================================ */
.section-head--compact { margin-bottom: clamp(28px, 4vw, 56px); }
.formats {
  /* Pull up tight under Services since they're conceptually paired. */
  padding-top: 0;
}
.formats-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 16px;
}
.format-card {
  padding: 24px 22px;
  border: 1px solid var(--hair-strong);
  border-radius: 16px;
  background: var(--bg);
  transition: border-color .3s var(--ease), transform .4s var(--ease);
}
.format-card:hover {
  border-color: var(--ink);
  transform: translateY(-2px);
}
.format-card h4 {
  font-family: var(--serif);
  font-size: 17px;
  font-weight: 500;
  letter-spacing: -0.01em;
  margin-bottom: 10px;
  color: var(--ink);
}
.format-card p {
  font-size: 15.5px;
  line-height: 1.5;
  color: var(--ink-soft);
}

@media (max-width: 1080px) {
  .formats-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 560px) {
  .formats-grid { grid-template-columns: 1fr; }
}

/* ============================================================
   GALLERY
   Horizontal slideshow ticker. Photos drift right→left on a
   CSS keyframe animation (track duplicated in markup so the
   translateX(0 → -50%) wrap-around is seamless). On top of
   the drift, a tiny JS IIFE measures each card's distance from
   the viewport's horizontal center every animation frame and
   sets a CSS variable --p (0 at center, 1 at the edges). The
   inner <img> uses that variable to scale from 1.0 (edge) up
   to 1.12 (center) — so each photo "swells" as it passes the
   middle of the viewport and shrinks back as it slides past.
   Hover pauses the drift so visitors can dwell on a photo.
============================================================ */
/* The gallery marquee is full-bleed and sits at the section's visual bottom. 
   Removing bottom padding prevents double-spacing with the next section. */
.gallery.section { padding-bottom: 0; }

.gallery-marquee {
  position: relative;
  overflow: hidden;
  margin-top: clamp(40px, 5vw, 80px);
}
/* Edge fades match the section's --paper bg so photos enter/exit
   subtly rather than popping in at a hard crop line. */
.gallery-marquee::before, .gallery-marquee::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0;
  width: clamp(40px, 8vw, 140px);
  z-index: 2;
  pointer-events: none;
}
.gallery-marquee::before { left: 0;  background: linear-gradient(90deg,  var(--paper), transparent); }
.gallery-marquee::after  { right: 0; background: linear-gradient(-90deg, var(--paper), transparent); }

/* The animated track. width: max-content lets the row size to its
   contents; the keyframe slides it -50% to land Set B exactly where
   Set A started — visually seamless loop. Vertical padding leaves
   room for the centered photo to scale up without clipping the row
   above/below. */
.gallery-track {
  display: flex;
  align-items: center;
  gap: clamp(14px, 1.6vw, 24px);
  /* Generous vertical padding leaves room for the larger cards to swell on
     hover (and cast their shadow) without the marquee's overflow clipping. */
  padding: clamp(52px, 5vw, 88px) var(--pad-x);
  width: max-content;
  animation: gallery-scroll 90s linear infinite;
  /* No will-change: the running animation already promotes this to its own
     compositor layer, and will-change made that layer permanent — a
     ~15,000px-wide texture held on the GPU for the whole visit, whether or
     not the gallery is anywhere near the viewport. On the shared-memory
     Intel graphics in an old MacBook Air that is enough to starve the
     other big layer on the page (the testimonials track) and leave it
     unpainted. */
}
/* Pause only when an actual photo card is hovered — not anywhere in the
   marquee strip. The track's tall vertical padding made the old
   `.gallery-marquee:hover` hot-zone far bigger than the cards themselves. */
.gallery-track:has(.gphoto:hover) { animation-play-state: paused; }
@keyframes gallery-scroll {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .gallery-track { animation: none; }
  .gallery-track .gphoto img { transform: none !important; }
}

/* Individual photo card. Fixed width so cards stay consistent and the
   center-scale math (distance from viewport center vs. card center) is
   stable. The aspect-ratio frame crops mixed portrait/landscape source
   photos to one common shape — landscape 4:3 reads as the most natural
   "documentary moment" frame for classroom/cohort photos. */
.gphoto {
  position: relative;
  /* Much larger cards — fewer, bigger photos read on the strip at once. */
  flex: 0 0 clamp(320px, 32vw, 500px);
  aspect-ratio: 4 / 3;
  margin: 0;
  border-radius: 16px;
  border: 1px solid var(--hair);
  background: var(--bg-warm);
  overflow: hidden;
  z-index: 1;
  /* --p is written by JS each frame: 0 = card center is at viewport
     center, 1 = card center is at (or beyond) viewport edge. */
  --p: 1;
  transition: transform .4s var(--ease-out), box-shadow .4s var(--ease-out);
}
/* Hover: the card itself swells and lifts above its neighbours. The track
   pauses (the :has rule above) so the enlarged card holds still under the
   cursor. The inner img keeps its own center-scale, so the two compose. */
.gphoto:hover {
  /* No hover zoom (no transform) — just a soft lift shadow. */
  z-index: 3;
  box-shadow: 0 28px 56px -22px rgba(20, 12, 40, .38);
}
.gphoto img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  /* No scale/zoom — the photos ride the ticker at a constant size.
     (The old center-scale effect and its per-frame rAF measuring loop
     were removed; see scripts.js.) */
}

/* Tablet — slightly smaller cards so 2-3 fit in view at once. */
@media (max-width: 880px) {
  .gphoto { flex-basis: clamp(200px, 36vw, 260px); border-radius: 14px; }
  /* Slower ticker on smaller viewports — fewer cards visible at once,
     each one deserves more dwell time before sliding out. */
  .gallery-track { animation-duration: 120s; }
}
/* Phone — single-column-ish: one card dominates view, neighbors peek
   in from the edges. Larger basis relative to viewport so the swell
   reads clearly. */
@media (max-width: 480px) {
  .gphoto { flex-basis: 86vw; border-radius: 12px; }
  .gallery-track { gap: 12px; animation-duration: 150s; }
}

/* ============================================================
   MILESTONES STRIP
   Blue-themed horizontal timeline beneath the founder prose.
   A thin connecting line runs across the dots; each dot pulses
   on a 2.4s loop, and the final "Today" cell pulses brighter
   so it reads as the present moment in the sequence.
============================================================ */
.milestones {
  /* Purple timeline accent — single source of truth for every dot,
     line, label and glow in this block. */
  --ms-accent: #A27DFF;
  --ms-accent-rgb: 162, 125, 255;
  list-style: none;
  padding: 0;
  /* Full-width: now a direct child of .wrap (moved out of the founder
     right column), so the five milestones spread across the whole
     content width. Extra top margin separates it from the grid above. */
  margin: clamp(56px, 7vw, 100px) 0 0;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 14px;
  padding-top: clamp(32px, 4vw, 48px);
  position: relative;
}
/* The connecting line — a 1px horizontal that runs through the row of
   dots. Positioned at the same y as the dots so they appear threaded. */
.milestones::before {
  content: "";
  position: absolute;
  top: clamp(32px, 4vw, 48px);
  left: 5px;
  right: 5px;
  height: 1px;
  background: linear-gradient(
    to right,
    rgba(var(--ms-accent-rgb), 0) 0%,
    rgba(var(--ms-accent-rgb), 0.35) 6%,
    rgba(var(--ms-accent-rgb), 0.35) 94%,
    rgba(var(--ms-accent-rgb), 0) 100%
  );
}
.milestones li {
  display: flex;
  flex-direction: column;
  gap: 10px;
  position: relative;
}
.milestones .ms-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--ms-accent);
  box-shadow: 0 0 0 4px rgba(var(--ms-accent-rgb), 0.16);
  animation: pulse-ms 2.4s ease-in-out infinite;
  /* Stagger each milestone's pulse so they don't all blink in unison */
  animation-delay: calc(var(--ms-i, 0) * 0.4s);
  margin-top: -1px;
}
.milestones li:nth-child(1) { --ms-i: 0; }
.milestones li:nth-child(2) { --ms-i: 1; }
.milestones li:nth-child(3) { --ms-i: 2; }
.milestones li:nth-child(4) { --ms-i: 3; }
.milestones li:nth-child(5) { --ms-i: 4; }
/* The final "Today" cell gets a stronger glow + brighter base */
.milestones .ms-today .ms-dot {
  box-shadow: 0 0 0 6px rgba(var(--ms-accent-rgb), 0.24);
  animation-name: pulse-ms-strong;
}
.milestones .ms-when {
  font-family: var(--serif);
  font-style: italic;
  font-size: 13px;
  letter-spacing: 0.04em;
  color: var(--ms-accent);
  margin-top: 4px;
}
.milestones .ms-what {
  font-size: 14px;
  line-height: 1.4;
  color: var(--ink-soft);
}
@keyframes pulse-ms {
  0%, 100% { box-shadow: 0 0 0 4px rgba(var(--ms-accent-rgb), 0.16); }
  50%      { box-shadow: 0 0 0 9px rgba(var(--ms-accent-rgb), 0.05); }
}
@keyframes pulse-ms-strong {
  0%, 100% { box-shadow: 0 0 0 6px rgba(var(--ms-accent-rgb), 0.28); }
  50%      { box-shadow: 0 0 0 12px rgba(var(--ms-accent-rgb), 0.08); }
}

@media (max-width: 880px) {
  .milestones { grid-template-columns: repeat(2, 1fr); gap: 24px 16px; margin-top: clamp(40px, 6vw, 64px); }
  .milestones::before { display: none; }
}
@media (max-width: 480px) {
  .milestones { grid-template-columns: 1fr; gap: 18px; }
}

/* ============================================================
   AUDIENCE / BUILT FOR
============================================================ */
/* A gradient ribbon under the heading, then a row of four photo cards
   (image + label beneath) naming who breezy minutes is for. */

/* Full-bleed ribbon. The SVG sits on its own full-width band (a direct
   child of the section, outside .wrap). overflow:hidden plus a band that is
   never narrower than the viewport guarantees the SVG's left/right edges
   stay off-screen; the ribbon's actual tapered ends live outside the SVG's
   viewBox, so they are clipped and never render at any breakpoint. */
/* Pull the ribbon a little closer to the heading than the global
   section-head gap, so the two read as a pair. */
#audience .section-head { margin-bottom: clamp(20px, 3vw, 44px); }

.aud-ribbon {
  position: relative;
  width: 100%;
  overflow: hidden;
  margin: clamp(4px, 0.8vw, 12px) 0 clamp(44px, 6vw, 96px);
  line-height: 0;            /* kill inline-image descender gap */
}
.aud-ribbon-img {
  display: block;
  width: 100%;
  min-width: 1100px;         /* keep the wave readable + edges off narrow screens */
  height: auto;
  /* Re-centre when min-width forces the band wider than the viewport, so the
     wave stays centred and both cut edges sit equally off-screen. */
  margin-left: 50%;
  transform: translateX(-50%);
  max-width: none;
}

.aud-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(18px, 2vw, 30px);
}

.aud-card {
  display: flex;
  flex-direction: column;
  margin: 0;
}

/* Rounded photo. Fixed aspect ratio + object-fit:cover so the four cards
   read as one uniform row regardless of each source image's proportions. */
.aud-photo {
  display: block;
  aspect-ratio: 4 / 3;
  border-radius: 16px;
  overflow: hidden;
  background: var(--hair);
  box-shadow: 0 18px 40px -26px rgba(20, 12, 40, 0.30);
}
.aud-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Label beneath the photo, left-aligned in the site's quiet idiom. */
.aud-card figcaption {
  margin-top: clamp(12px, 1.2vw, 18px);
  font-family: var(--serif);
  font-size: clamp(15px, 1.25vw, 18px);
  font-weight: 500;
  letter-spacing: -0.01em;
  line-height: 1.3;
  color: var(--ink);
  max-width: 16ch;
}

/* Four-across is too tight on phones — drop to a 2x2 grid. */
@media (max-width: 880px) {
  .aud-grid { grid-template-columns: repeat(2, 1fr); gap: clamp(16px, 4vw, 24px); }
}

/* ============================================================
   TRANSFORMATIONS / TESTIMONIALS
============================================================ */
/* Like gallery, transforms has a full-bleed marquee at the bottom. 
   Removing bottom padding prevents double-spacing. */
.transforms.section { padding-bottom: 0; }
.transforms { background: var(--bg); border-top: 1px solid var(--hair); border-bottom: 1px solid var(--hair); }
/* Marquee container — full-bleed, sits outside .wrap. Edge fades match the
   section's --paper bg so cards enter/exit subtly rather than popping. */
.transforms-marquee {
  position: relative;
  overflow: hidden;
  margin-top: clamp(40px, 5vw, 80px);
}
.transforms-marquee::before, .transforms-marquee::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0;
  width: clamp(40px, 8vw, 140px);
  z-index: 2;
  pointer-events: none;
}
.transforms-marquee::before { left: 0; background: linear-gradient(90deg, var(--paper), transparent); }
.transforms-marquee::after  { right: 0; background: linear-gradient(-90deg, var(--paper), transparent); }

/* The animated track. Cards have a fixed flex-basis (320–420px) so they
   stay readable at every viewport. align-items: stretch equalises card
   heights to the tallest in the visible set. The duplicate set in the
   HTML lets translateX(0 → -50%) loop seamlessly.

   Seamless-loop math: for translateX(-50%) to land the duplicate set
   exactly where the originals started, every card must "own" its trailing
   gap (via margin-inline-end) and the track must have NO horizontal
   padding and NO flex `gap`. That way the track is 6 identical slots of
   (card-width + gap), and half the track is exactly 3 slots = the start
   of the duplicate set. Any flex `gap` or horizontal track padding
   throws the math off by gap/2 + pad-x, producing a small but visible
   jump at the wrap-around. */
.transforms-track {
  display: flex;
  align-items: stretch;
  padding: clamp(20px, 2.5vw, 36px) 0;
  width: max-content;
  /* Duration scales with the card count so scroll SPEED stays constant.
     Tuned at 40s for 3 cards; now 8 cards → 40 * 8/3 ≈ 107s. */
  animation: transforms-scroll 107s linear infinite;
}
.transforms-track:hover { animation-play-state: paused; }
.transforms-track .tcard {
  flex: 0 0 clamp(320px, 28vw, 420px);
  margin-inline-end: clamp(20px, 2vw, 32px);
}
@keyframes transforms-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
  .transforms-track { animation: none; }
}
/* Larger card silhouette — generous padding, soft rounded corners, and a
   warm cream wash that sets the trio apart from the section's --paper bg.
   Cards grow to their natural content height (align-items: start above)
   so a longer testimonial doesn't force the others to stretch. */
.tcard {
  padding: clamp(36px, 3.4vw, 56px) clamp(32px, 3vw, 48px);
  background: var(--bg-warm);
  border: 1px solid var(--hair);
  border-radius: 20px;
  display: flex;
  flex-direction: column;
  position: relative;
  isolation: isolate;
  transition:
    transform .4s var(--ease),
    box-shadow .4s var(--ease),
    border-color .35s var(--ease),
    color .35s var(--ease);
}
/* Hover gradient layer. Sits behind everything in the card (z-index:-1
   inside the card's isolated stacking context) and fades in via opacity
   — CSS can't smoothly tween between two background-images, but opacity
   on a pseudo-element gives a clean crossfade. */
.tcard::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  /* Pastel diagonal in the brand palette — pale pink → pale cyan → pale
     lilac (light tints of #FFC3F9 / #50C8FF / #A27DFF). Kept light so dark
     ink text on top stays legible (>16:1 contrast even on the deepest
     stop) — the hover reads as "same dark text, soft brand wash behind it"
     rather than a colour-flip. */
  background: linear-gradient(135deg, #FFDCF8 0%, #D2ECFF 48%, #DFD3FF 100%);
  opacity: 0;
  transition: opacity .35s var(--ease);
  z-index: -1;
  pointer-events: none;
}
.tcard:hover {
  transform: translateY(-6px);
  box-shadow: 0 28px 60px -24px rgba(14,14,22,.22);
}
.tcard:hover::before { opacity: 1; }
.tcard-head {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-bottom: 24px;
}
.tcard-attr { display: flex; flex-direction: column; gap: 2px; }
.tcard .quote-mark {
  font-family: var(--serif);
  font-style: italic;
  font-size: 72px;
  line-height: 0.6;
  color: var(--violet);
  margin-bottom: 16px;
  opacity: .65;
  user-select: none;
  transition: color .35s var(--ease), opacity .35s var(--ease);
}
.tcard p {
  font-family: var(--serif);
  font-size: clamp(18px, 1.45vw, 22px);
  line-height: 1.5;
  color: var(--ink);
  letter-spacing: -0.005em;
  margin: 0;
  transition: color .35s var(--ease);
}
.tcard p + p { margin-top: 14px; }
.tcard p em { font-style: italic; }
.tcard .avatar {
  width: 64px; height: 64px;
  border-radius: 50%;
  background: var(--grad);
  display: grid;
  place-items: center;
  overflow: hidden;
  flex-shrink: 0;
  transition: background .35s var(--ease);
}
.tcard .avatar img {
  width: 100%; height: 100%;
  object-fit: cover;
  border-radius: 50%;
  display: block;
}
.tcard .person {
  font-size: 17px;
  color: var(--ink);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.2;
  transition: color .35s var(--ease);
}
.tcard .role {
  font-size: 13.5px;
  color: var(--mute);
  line-height: 1.3;
  transition: color .35s var(--ease);
}

/* Hover state border softens to transparent so the cream border doesn't
   fight the pastel gradient. Text + avatar styling are intentionally
   inherited from the neutral state — dark ink text on the pastel wash
   reads cleanly, so there's no colour inversion on hover anymore. */
.tcard:hover { border-color: transparent; }

/* Center the round avatar on the cluster around the teacher in the wide
   group photo (testimonial-2 is landscape ~1280×580). Applies in both
   neutral and hover states. */
.tcard--group .avatar img { object-position: 42% 38%; }

@media (max-width: 880px) {
  .tcard { border-radius: 18px; }
  /* Touch: keep the ticker auto-scrolling BUT also let visitors swipe through
     at their own pace. The marquee becomes a horizontal scroller; scripts.js
     advances scrollLeft each frame and pauses while the visitor is dragging,
     resuming shortly after. The CSS transform animation is turned off (it would
     fight native scroll) — the JS drives it instead. The duplicate (loop) set
     stays in place so the auto-scroll wraps seamlessly. (Wider screens keep the
     pure-CSS ticker.) */
  /* No scroll-snap: it fights the JS auto-scroll (each per-frame nudge would
     get snapped back), and a continuous ticker shouldn't snap anyway. Manual
     swipes scroll freely and smoothly. */
  .transforms-marquee {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }
  /* The edge fades are pinned to the marquee and would scroll out of place
     on a manual scroller — drop them; the peeking next card signals more. */
  .transforms-marquee::before, .transforms-marquee::after { display: none; }
  .transforms-track {
    animation: none;
    width: max-content;
    padding-inline: var(--pad-x);
  }
}

/* ============================================================
   STATS
============================================================ */
.stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0;
  border-top: 1px solid var(--hair-strong);
  border-bottom: 1px solid var(--hair-strong);
}
.stat {
  padding: clamp(36px, 4vw, 56px) clamp(20px, 2vw, 32px);
  border-right: 1px solid var(--hair);
  position: relative;
}
.stat:last-child { border-right: 0; }
.stat .big {
  font-family: var(--serif);
  font-size: clamp(48px, 7vw, 96px);
  line-height: 1;
  letter-spacing: -0.025em;
  background: var(--grad-text);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}
.stat .big sup {
  font-size: 0.5em;
  vertical-align: super;
  -webkit-text-fill-color: var(--ink);
  background: none;
}
.stat .label {
  margin-top: 12px;
  font-size: 14px;
  color: var(--mute);
  max-width: 22ch;
}

@media (max-width: 760px) {
  .stats { grid-template-columns: repeat(2, 1fr); }
  .stat:nth-child(2) { border-right: 0; }
  .stat:nth-child(1), .stat:nth-child(2) { border-bottom: 1px solid var(--hair); }
  /* Three stats in a 2-up grid would leave the last one orphaned in a half
     cell — let it run the full width instead. */
  .stat:nth-child(3) { grid-column: 1 / -1; }
}

/* ============================================================
   ABOUT FOUNDER  —  two-column split: photo placeholder | prose
============================================================ */
.founder {
  /* Unified with --section-y so the founder section matches the other
     full-width sections instead of being slightly shorter. */
  padding-top: var(--section-y);
  padding-bottom: 0;
}

.founder-grid {
  display: grid;
  grid-template-columns: minmax(280px, 1fr) 1.35fr;
  gap: clamp(40px, 6vw, 96px);
  align-items: start;
}

/* Founder photo — actual portrait. The story beside it runs roughly twice
   the photo's height, so a static portrait would leave a tall empty column
   under itself. Sticky within its grid area (the grid keeps align-items:
   start, so the area is full-height while the box is not) lets the portrait
   ride alongside the prose instead. Offset clears the fixed nav. */
.founder-photo {
  position: sticky;
  top: clamp(108px, 13vh, 136px);
  aspect-ratio: 4 / 5;
  border-radius: 2px;
  overflow: hidden;
  background: var(--paper);
}
.founder-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 25%;
  display: block;
}

/* Right column content */
.founder-content {
  display: flex;
  flex-direction: column;
}
.founder-heading {
  margin: 0 0 36px;
}
.founder-prose p {
  font-size: 18px;
  line-height: 1.6;
  color: var(--ink-soft);
  margin: 0 0 18px;
  max-width: 60ch;
}
.founder-prose p:last-of-type { margin-bottom: 0; }
.founder-prose p em {
  font-style: italic;
  color: var(--ink);
}

/* ---- Certification names ----------------------------------------------
   The hard credibility claims in the story, so the NAMES are marked while
   the sentences around them stay ordinary prose — body size, body spacing,
   nothing stepped up. Weight and colour only: bold, full --ink against the
   softer --ink-soft of the paragraph.

   No gradient, at the user's request. That also keeps an old contrast
   thread closed: the gradient measured 2.86:1 at its purple end and 1.86:1
   at its cyan end against the cream, where AA wants 4.5:1. Full --ink is
   17.4:1. Not italic either — italic is already spoken for in this prose,
   where it marks institution names like Smart Training Resources.       */
.founder-prose .cred {
  font-weight: 700;
  color: var(--ink);
}
.founder-sign {
  font-family: var(--serif);
  font-style: italic;
  font-size: 28px;
  margin-top: 36px;
  align-self: flex-end;
  background: var(--grad-text);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}

@media (max-width: 760px) {
  .founder-grid {
    grid-template-columns: 1fr;
    gap: 40px;
  }
  /* Stacked single column — nothing to ride alongside, so drop the sticky. */
  .founder-photo { position: static; aspect-ratio: 4 / 5; max-width: 420px; }
  .founder-sign { align-self: flex-start; }
}

/* ============================================================
   CTA SECTION
============================================================ */
/* Light variant — sits on --paper like the rest of the page, with the
   brand-gradient orb softened so it reads as a subtle wash on cream
   instead of glowing on black. All explicit cream/paper text colours
   have been removed so the section inherits --ink from the body. */
.cta-section {
  position: relative;
  /* Generous top padding so the heading clears the gradient's top curve at
     every width (the cream "dip" is ~32% of the section height); tighter
     bottom padding cushions the contact meta above the page end. */
  padding: clamp(330px, 30vw, 410px) 0 clamp(72px, 9vw, 130px);
  background: var(--bg);
  overflow: hidden;
}
.cta-section .wrap { position: relative; z-index: 2; }
.cta-section .eyebrow + h2 { margin-top: 28px; }

/* Curved brand gradient closing the page. The SVG's drawn ends sit outside
   its viewBox, so no edge ever shows; stretching it to fill (object-fit:fill,
   preserveAspectRatio:none in the file) keeps the curve depth a constant
   share of the section height across breakpoints, and guarantees the gradient
   reaches the very bottom of the page. The 0.75 fill-opacity lives in the SVG. */
.cta-bg {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  pointer-events: none;
}
.cta-bg img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: fill;
}

/* Two-line heading — tight line break between the two sentences. */
.cta-head span { display: block; }
.cta-head span + span { margin-top: 0.1em; }
.cta-orb {
  position: absolute;
  width: 80vw;
  max-width: 1000px;
  aspect-ratio: 1;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  /* Same brand stops (purple → cyan → pink) but opacity ~halved so the
     orb reads as a gentle tint on cream rather than the rave glow that
     worked on the dark background. Blur kept at 60px for a soft edge. */
  background: radial-gradient(circle, rgba(162,125,255,.18) 0%, rgba(80,200,255,.12) 35%, rgba(255,195,249,.10) 60%, transparent 75%);
  border-radius: 50%;
  filter: blur(60px);
  z-index: 1;
  pointer-events: none;
  animation: orbDrift 20s ease-in-out infinite;
}
.cta-section h2 {
  font-family: var(--serif);
  font-size: clamp(32px, 4.6vw, 60px);
  line-height: 1.04;
  letter-spacing: -0.025em;
  max-width: 30ch;
  margin-bottom: 30px;
}
.cta-section .lede {
  color: var(--mute);
  max-width: 52ch;
  margin-bottom: 48px;
}
.cta-section .btn-grad { font-size: 16px; padding: 18px 32px; }

.cta-meta {
  margin-top: 64px;
  padding-top: 32px;
  border-top: 1px solid rgba(14,14,22,.12);
  display: flex;
  gap: 48px;
  flex-wrap: wrap;
  font-size: 14px;
  color: var(--mute);
}
.cta-meta strong { font-weight: 500; font-family: var(--serif); font-style: italic; font-size: 17px; color: var(--ink); }
.cta-meta div { display: flex; flex-direction: column; gap: 4px; }
.cta-meta a { text-decoration: none; color: inherit; transition: opacity .2s var(--ease); }
.cta-meta a:hover { opacity: .7; }
/* WhatsApp / LinkedIn lines: brand glyph + label on one row. Each glyph
   carries its own brand colour, which is the one place on the page that
   isn't a Breezy Minutes colour — that's deliberate, it's how these read
   as recognisable service marks rather than decoration. */
.cta-wa,
.cta-li { display: inline-flex; align-items: center; gap: 8px; }
.cta-wa .wa-icon { width: 18px; height: 18px; flex: none; fill: #25D366; }
.cta-li .li-icon { width: 18px; height: 18px; flex: none; fill: #0A66C2; }

/* ============================================================
   MOBILE OVERRIDES (phones)
   Larger hero + section headings so the type carries the small
   screen, and the tall-section footer shape with its own spacing.
============================================================ */
@media (max-width: 640px) {
  /* Hero leads the page — bump the headline and body up from their
     clamp floors so they don't read small on a phone. */
  .display { font-size: clamp(40px, 10vw, 52px); }
  .hero-body { font-size: 18px; }
  /* Section headings a touch larger, in keeping with the rest of the site. */
  .h2 { font-size: clamp(38px, 9.2vw, 46px); }

  /* The mobile footer shape fills the section like the desktop one: the cream
     dips into the gradient along the top curve and the gradient fills down to
     the page end, with ALL the CTA copy sitting inside the gradient below the
     dip. Top padding pushes the heading clear of the curve's deepest point. */
  .cta-section { padding-top: clamp(360px, 96vw, 400px); }
}

/* ============================================================
   REVEAL ON SCROLL

   Every rule here hangs off .js on <html>, which the inline snippet at
   the top of index.html sets. Without that class NOTHING is hidden.

   Why: these rules park real content at opacity 0 and rely on scripts.js
   to hand it back. That makes the JS a single point of failure for
   whether the page has any content at all — if the file is blocked by an
   extension, lost to a bad cache entry, or the observer simply never
   fires, the visitor gets a full-height empty gap with no way to recover.
   A client on an old MacBook Air hit exactly that on the testimonials.
   Gating on .js means a page whose script never ran renders plainly and
   completely instead of blank.
============================================================ */
.js .reveal {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity .9s var(--ease-out), transform .9s var(--ease-out);
}
.js .reveal.in {
  opacity: 1;
  transform: translateY(0);
}
.js .reveal-stagger > * {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity .8s var(--ease-out), transform .8s var(--ease-out);
}
.js .reveal-stagger.in > * { opacity: 1; transform: translateY(0); }
.js .reveal-stagger.in > *:nth-child(1) { transition-delay: 0s; }
.js .reveal-stagger.in > *:nth-child(2) { transition-delay: .08s; }
.js .reveal-stagger.in > *:nth-child(3) { transition-delay: .16s; }
.js .reveal-stagger.in > *:nth-child(4) { transition-delay: .24s; }
.js .reveal-stagger.in > *:nth-child(5) { transition-delay: .32s; }
.js .reveal-stagger.in > *:nth-child(6) { transition-delay: .40s; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}

/* ============================================================
   BRAND GRADIENT — extends the radial orbit to non-text brand
   elements: stat numbers, founder signature, and the primary CTA
   button. Same @keyframes (gradient-orbit) and timing as
   em.gradient so everything fuses in sync with the italic accents.
   Placed after the original .stat/.founder-sign/.btn-grad rules
   so the cascade lets these win.
============================================================ */
.stat .big,
.founder-sign,
.btn-grad,
.btn-grad::before {
  animation: gradient-orbit 18s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}
.stat .big,
.founder-sign {
  background-image: radial-gradient(
    ellipse 70% 200% at var(--em-cx, 50%) var(--em-cy, 50%),
    var(--brand-purple)  0%,
    var(--brand-cyan)   55%,
    var(--brand-purple) 125%
  );
  background-size: 240% 280%;
}

/* ============================================================
   BRAND MARK — static gradient-stroke logo (nav + footer).
   The animated mesh/warp logo was retired. Nav and footer now
   render the heart mark as a flat radial-gradient stroke, frozen
   (no animation). Each instance carries its own <radialGradient>
   id (bm-grad-nav / bm-grad-footer) to avoid SVG <defs> id
   collisions across the page. overflow:visible keeps the square
   stroke-cap tips from clipping at the viewBox edge.
============================================================ */
.brand-mark {
  background: none !important;
  width: auto !important;
  line-height: 0 !important;
  display: inline-flex !important;
  align-items: center !important;
}
.bm-logo {
  display: block;
  height: 100%;
  width: auto;
  overflow: visible;
}

