/* Whitestone Juniors — sections
   Order and rationale: evals/order-sheet.md §7. */

/* ============================================================
   Shared media bed
   Kit design.md §4.3: no text sits directly on the photograph.
   Text lives in a glass card; the glass card sits on a scrim.
   ============================================================ */

.media-bed { position: relative; isolation: isolate; overflow: hidden; }
.media-bed__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: -2;
}
.media-bed__scrim {
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--scrim-navy);
  pointer-events: none;
}
.on-media { color: #FFFFFF; }
.on-media .micro, .on-media .lead, .on-media .small { color: rgba(255, 255, 255, 0.78); }

/* ============================================================
   Repeating racket-mark texture — .faq and .terms ONLY
   The logo mark used as a texture, which is the Virgil "one repeated device"
   call from the order sheet.

   WHO GETS IT: the two sections that never receive a photograph. Measured ink
   coverage with the texture off — .acts 62%, .boards 45%, .parents 31%,
   .faq 7%, .terms 3%. The first three only look bare today because their
   placeholder frames are stand-ins; once the real shoot lands they are the
   densest blocks on the page and texture under them is just noise. .faq is an
   accordion of hairlines and .terms is pure type — neither ever fills up, and
   those are the two the texture is actually solving for. Do not add .tiled to
   a section that is getting a photo.

   assets/pattern-mark.png is an ALPHA tile (white on transparent, generated
   from icon-navy.png), used as a CSS mask — so one file serves both the paper
   and the navy bands and the colour comes from --tile-ink.

   Three things keep this off "wallpaper", which is what the straight
   grid-of-logos version read as:
   1. The tile scatters SEVEN marks at mixed sizes (22–42px) and rotations
      across a 240px cell — no lattice, and no two neighbours the same size.
   2. The paint is a radial gradient that is fully transparent at the section's
      centre, so the texture is a vignette: it holds the edges and clears out
      from under the copy instead of running flat behind it.
   3. Low strength: at 6–9% it reads as grain in the paper, not as logos.
   ============================================================ */

.tiled { --tile-ink: var(--navy); --tile-strength: 0.09; }
.band--navy.tiled { --tile-ink: #FFFFFF; --tile-strength: 0.06; }

@supports ((-webkit-mask-image: url("../assets/pattern-mark.png")) or (mask-image: url("../assets/pattern-mark.png"))) {
  .tiled::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    opacity: var(--tile-strength);
    background: radial-gradient(78% 62% at 50% 50%, transparent 0%, var(--tile-ink) 100%);
    -webkit-mask: url("../assets/pattern-mark.png") 0 0 / 240px 240px repeat;
            mask: url("../assets/pattern-mark.png") 0 0 / 240px 240px repeat;
  }
}

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

/* Scale the tile down with the viewport — a 240px cell on a 375px screen puts
   barely one and a half marks across, which reads as stray specks. */
@media (max-width: 767px) {
  .tiled::before { -webkit-mask-size: 168px 168px; mask-size: 168px 168px; }
}

/* ============================================================
   1. Hero
   ============================================================ */

/* Exactly one screen — not min-height. The caps below (and the two short-window
   blocks at the end of this section) keep the card inside that box, so a fixed
   height never clips. */
.hero {
  display: flex;
  align-items: flex-end;
  height: 100svh;
  padding-block: calc(var(--header-top) + var(--header-h) + var(--space-8)) var(--space-8);
  padding-inline: var(--gutter);
}
@supports not (height: 100svh) { .hero { height: 100vh; } }

.hero__card { max-width: 620px; }

/* The portrait source is 3:4; a 375x812 viewport is 0.46:1, so `cover` crops the
   sides. The racket sits top-right in that render, so a centred crop clips it —
   bias the crop right and up to keep the head and the etched wordmark whole. */
@media (max-width: 767px) {
  .hero .media-bed__img { object-position: 68% 22%; }
}

/* main.js parallax-translates this img yPercent 12 on scroll (desktop only).
   At full scroll it slides 12% of its own height downward, which — sized
   exactly to the container (inset:0/height:100%) — uncovers a strip at the
   top equal to the translate. The scrim is fully transparent right at that
   same top edge, so the gap fell through to body's white background: the
   "blank rectangle on scroll" bug. Overscan top+height past what the 12%
   translate can ever uncover (worked out against yPercent's own-height
   basis, with margin) so the image always fully covers the container. */
@media (min-width: 768px) {
  .hero .media-bed__img { top: -15%; height: 118%; }
}

/* The hero must fit the screen, not overflow it. The card's natural height is
   ~627px at the display cap, which is taller than a short browser window (a
   1000x500 CSS viewport is common at DPR 2). Both the display size and the lead
   are therefore capped against viewport HEIGHT as well as width — svh is the
   only unit that sees the constraint that is actually binding here.
   Above ~840px of viewport height these caps never engage and the kit's
   measured type scale applies unchanged. */
.hero h1 { font-size: min(var(--fs-display), 12svh); }
.hero__sub { font-size: max(1rem, min(var(--fs-lead), 3.1svh)); }

.hero__kicker { display: block; margin-bottom: var(--space-3); }
.hero h1 { margin-bottom: var(--space-4); }
.hero__sub { margin-bottom: var(--space-6); }
.hero__reassure { margin-top: var(--space-4); font-size: var(--fs-small); }

/* Short windows: tighten the box as well as the type. */
@media (max-height: 760px) {
  .hero { padding-block: calc(var(--header-top) + var(--header-h) + var(--space-4)) var(--space-4); }
  .hero__card { padding: var(--space-5) var(--space-6); }
  .hero h1 { margin-bottom: var(--space-3); }
  .hero__sub { margin-bottom: var(--space-4); }
  .hero__reassure { margin-top: var(--space-3); }
}

/* Very short windows (a ~1000x500 CSS viewport at DPR 2, or a half-height
   browser window). The kicker goes: it repeats what the proof pill 40px above
   it already says, so it is the one line that costs height and adds nothing.
   Everything that does conversion work — pill, headline, sub, button,
   reassurance — stays on screen. */
@media (max-height: 640px) and (min-width: 768px) {
  .hero { padding-block: calc(var(--header-top) + var(--header-h) + var(--space-3)) var(--space-3); }
  .hero__card { padding: var(--space-4) var(--space-6); }
  .hero__kicker { display: none; }
  .hero h1 { font-size: min(var(--fs-display), 10.5svh); margin-bottom: var(--space-2); }
  .hero__sub { margin-bottom: var(--space-3); }
  .hero__reassure { margin-top: var(--space-2); }
}

/* ============================================================
   2. Proof trio
   Kit §4.5. Type-only band by design — costs nothing to load and
   rests the eye between two photo-heavy sections.
   ============================================================ */

.proof { position: relative; overflow: hidden; }
.proof .wrap { position: relative; z-index: 1; }

/* The stat band now sits on real media instead of a tiled icon. The crop is
   deliberately tight — behind the veil it reads as stone texture, not as an
   object competing with the three numbers. */
.proof__img { object-position: 62% 50%; }
.proof__veil {
  position: absolute;
  inset: 0;
  z-index: -1;
  /* Radial only. The first version stacked an opaque navy linear-gradient on
     top, which hid the image completely no matter what opacity it carried.
     Lightest at 78% 40% — where the stone sits — and dense over the left half
     where the three numbers are. */
  background: radial-gradient(125% 95% at 80% 42%,
    rgba(6, 12, 51, 0.62) 0%,
    rgba(6, 12, 51, 0.86) 44%,
    rgba(6, 12, 51, 0.96) 76%);
  pointer-events: none;
}
.proof__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4);
}
@media (min-width: 768px) { .proof__grid { grid-template-columns: repeat(3, 1fr); gap: var(--space-6); } }

.proof__grid { margin: 0; }   /* <dl> ships a UA margin; the grid owns spacing */

.stat {
  padding: var(--space-6);
  text-align: left;
  /* Not --glass-fill-light: one of these three lands on the bright stone, and a
     translucent white fill put white text on a near-white backdrop there. */
  background: rgba(11, 18, 77, 0.55);
  border-color: rgba(255, 255, 255, 0.20);
  /* A <dl> requires dt before dd, and the design wants the number on top.
     Reversing the flow direction is the whole fix — no order:, no duplicated
     text, and the reading order stays label-then-value where it belongs.
     flex-end because column-reverse starts its main axis at the BOTTOM, and
     these cards are stretched to a common height by the grid. */
  display: flex;
  flex-direction: column-reverse;
  justify-content: flex-end;
}
.stat__num {
  margin: 0;
  font-family: var(--font-display);
  font-size: calc(var(--fs-display) * 0.6);
  line-height: 1;
  letter-spacing: var(--ls-display);
  color: #FFFFFF;
}
.stat__label { margin: var(--space-3) 0 0; }
.stat--todo .stat__num { color: rgba(255, 255, 255, 0.34); }

/* ============================================================
   3. Activities
   ============================================================ */

.acts__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-6);
}
@media (min-width: 768px) { .acts__grid { grid-template-columns: repeat(3, 1fr); } }

.act { display: flex; flex-direction: column; padding: 0; }
.act .ph-frame { border: none; border-radius: 0; border-bottom: 2px dashed rgba(11, 18, 77, 0.24); }
/* Landscape under 768px. A 4:5 frame at full mobile width is ~420px tall and
   three of them alone added ~700px to the page against the kit's length target. */
@media (max-width: 767px) { .act .ph-frame { aspect-ratio: 16 / 9; } }
.act__body { padding: var(--space-6); }
.act__body h3 { margin-bottom: var(--space-3); }

/* ============================================================
   4 + 9. CTA bands over media
   ============================================================ */

.ctaband {
  display: flex;
  align-items: center;
  min-height: 380px;
  padding-block: var(--space-section);
  padding-inline: var(--gutter);
}
.ctaband .media-bed__img { object-position: 78% 50%; }
.ctaband__card { max-width: 560px; }
.ctaband__card h2 { margin-bottom: var(--space-4); }
.ctaband__card p { margin-bottom: var(--space-6); }
.ctaband__micro { margin-top: var(--space-4); font-size: var(--fs-small); }

.ctaband--final { min-height: 440px; }
.ctaband--final .media-bed__img { object-position: 50% 45%; }

/* ============================================================
   6b. What players say
   Three quote cards, each with the face of the person who said it.
   Ported from the hub's prompt-testimonial-section-4 — the inverted
   body under a 4:3 portrait is the part worth having. The carousel it
   ships with is not: see the note in index.html for what was left
   behind and why. Kept on the raised paper so it reads as a
   continuation of §6 (the parents' facts) rather than a new argument.
   ============================================================ */

.says { background: var(--paper-raised); }

.says__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-5);
  margin: 0;
  padding: 0;
  list-style: none;
}
@media (min-width: 900px) { .says__grid { grid-template-columns: repeat(3, 1fr); } }

/* Navy card on the white band. Nothing else on this page inverts, which is
   the point — three quotes need to read as testimony rather than as more of
   the page's own voice, and a change of surface says that faster than a
   change of type ever does. */
.says__card {
  height: 100%;
  overflow: hidden;                 /* clips the portrait to the card radius */
  border-radius: var(--radius-l);
  background: var(--navy);
  /* transform only — the reference animates y with a spring, and this page
     already spends its JS budget elsewhere. A 250ms translate on the
     compositor is the same read for none of the cost. */
  transition: transform var(--dur-ui) var(--ease-out-quart),
              box-shadow var(--dur-ui) var(--ease-out-quart);
}
.says__card > figure { display: flex; flex-direction: column; height: 100%; margin: 0; }

@media (hover: hover) {
  .says__card:hover {
    transform: translateY(-8px);
    box-shadow: 0 18px 40px rgba(6, 12, 51, 0.22);
  }
}

/* The portrait. 4:3 from the source; square corners and no radius of its own
   because the card is already clipping it. The dashed placeholder border stays
   on three sides so the frame still reads as "no photograph yet" — it is the
   whole reason this is a .ph-frame and not an <img> with a grey background. */
.says__shot {
  flex: none;
  aspect-ratio: 4 / 3;
  border-radius: 0;
  border-width: 0 0 1px;
}

.says__body {
  display: flex;
  flex: 1;
  flex-direction: column;
  padding: var(--space-6);
  color: #FFFFFF;
}
/* The slot chips are navy-on-tint by default, which vanishes on a navy card. */
.says__body .slot { background: rgba(255, 255, 255, 0.18); color: #FFFFFF; }

.says__quote {
  margin: 0 0 var(--space-6);
  font-size: var(--fs-body);
  line-height: 1.55;
  color: rgba(255, 255, 255, 0.92);
}

/* margin-top:auto pushes the rule to the foot of the card even when the quote
   is short, so three cards of unequal copy still line their attributions up. */
.says__by {
  margin-top: auto;
  padding-top: var(--space-4);
  border-top: 1px solid rgba(255, 255, 255, 0.20);
}
.says__by > span { display: block; }
.says__name { font-weight: 700; color: #FFFFFF; }
.says__role { color: rgba(255, 255, 255, 0.62); }

/* ============================================================
   5. Boards — ghost heading + photo deck
   Was an isometric marquee wall ported from pp-course-marquee; that is kept
   verbatim in evals/removed-boards-marquee.md and its CSS came out with it.
   The deck (components.css) replaced it 2026-08-10: one photo at a time, at
   size, moved by the visitor rather than by a 13-second loop.
   The ghost heading below is unchanged and still the section's real <h2>.
   ============================================================ */

.boards { isolation: isolate; }

/* The heading used to hold the left half of a two-column split. It is now the
   layer the deck sits ON, so the deck is centred in its own column and the
   words come out both sides of it. */
.boards__grid { display: grid; gap: var(--space-8); position: relative; z-index: 1; }
@media (min-width: 900px) {
  /* justify-self, not margin-inline:auto. An auto inline margin cancels a grid
     item's default stretch and falls back to shrink-to-fit — and the deck's
     cards are absolutely positioned, so shrink-to-fit measures the foot row and
     nothing else. width:100% then restores the size but leaves the item at the
     column start, which is the whole point of centring it: the ghost words
     behind need to come out both sides, not one. */
  .boards__deck { width: 100%; max-width: 680px; justify-self: center; }
}

/* ------------------------------------------------------------
   Oversized ghost heading (Baseline reference)
   ------------------------------------------------------------ */

.ghost {
  margin: 0 auto var(--space-8);
  max-width: var(--container);
  font-size: clamp(2.75rem, 8.2vw, 8rem);
  line-height: 1.02;
  letter-spacing: -0.04em;
  color: var(--ghost-ink);
}

/* The two outer words have to clear the marquee that sits over the middle of
   them, and the ink word is the one that cannot be half-eaten. So above 900 the
   band ignores the 1120 container and runs gutter to gutter, which buys ~256px
   a side, and the type steps down to 7vw to fit "FRIDAY" into it. Below 900 it
   is an ordinary heading and keeps the container and the larger size. */

.ghost__row { display: flex; justify-content: space-between; gap: var(--space-4); }

/* One word per box so each can carry its own parallax. Overflow is visible
   here — js/words.js inserts its own mask inside. */
.ghost__w { display: inline-block; will-change: transform; }
.ghost__w--ink { color: var(--navy); }

/* Below 900 the heading is simply a heading above the marquee: the overlay
   composition needs a container wider than the frame for the outer words to
   have anywhere to go, and below 900 the frame already is the full width. */
@media (min-width: 900px) {
  .ghost {
    position: absolute;
    left: var(--gutter);
    right: var(--gutter);
    top: 50%;
    z-index: 0;
    max-width: none;
    margin: 0;
    font-size: clamp(3rem, 7vw, 7rem);
    transform: translateY(-50%);
    pointer-events: none;
    -webkit-user-select: none;
    user-select: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .ghost__w { will-change: auto; }
}

.boards__deck {
  /* Bigger than the deck's own default. This is a full band with 7vw type
     sitting behind it; 340px cards read as a widget dropped on a page. */
  --deck-card-w: clamp(210px, 30vw, 400px);
}

/* No panel around it. The marquee needed an opaque navy frame to contain a 3D
   wall and to crop its columns top and bottom; a deck of five photos needs
   neither, and dropping the frame is what lets the ghost words run behind the
   cards instead of stopping at a border. */

/* ============================================================
   10. Syarat & ketentuan
   ============================================================ */

.terms { position: relative; overflow: hidden; }
.terms .wrap { position: relative; z-index: 1; }

.terms__list {
  display: grid;
  gap: 0;
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: t;
}
.terms__list > li {
  display: grid;
  grid-template-columns: 48px 1fr;
  gap: var(--space-4);
  padding-block: var(--space-6);
  border-top: 1px solid rgba(11, 18, 77, 0.12);
}
.terms__list > li:first-child { border-top: none; }
.terms__n {
  font-family: var(--font-display);
  font-size: 1.25rem;
  line-height: 1.4;
  color: var(--navy);
  opacity: 0.34;
}
.terms__list h3 { margin-bottom: var(--space-2); }
@media (min-width: 768px) {
  .terms__list > li { grid-template-columns: 72px 1fr; }
}

/* ============================================================
   6. For parents
   No kit precedent — dual-audience requirement. The one section
   where the conversion guard permits more depth.
   ============================================================ */

.parents__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-6);
  align-items: start;
}
@media (min-width: 900px) { .parents__grid { grid-template-columns: 1fr 1fr; gap: var(--space-8); } }

.facts { display: grid; gap: 0; margin: 0; }
.facts__row {
  display: grid;
  grid-template-columns: 116px 1fr;
  gap: var(--space-4);
  padding-block: var(--space-4);
  border-top: 1px solid rgba(11, 18, 77, 0.12);
}
.facts__row:first-child { border-top: none; padding-top: 0; }
.facts dt { font-size: var(--fs-micro); letter-spacing: var(--ls-micro); text-transform: uppercase; font-weight: 700; color: var(--ink-70); }
.facts dd { margin: 0; }

/* ============================================================
   2b. Panel wipe — the seam from §2 (navy band) into §3 (paper)
   Six columns stair up over the viewport until the screen is one flat
   sheet of paper, then §3 resolves out of it. Known as a staggered
   panel wipe (video editing: a shutter wipe); data-wipe="converge"
   runs the barn-door variant, panels closing from both edges to the
   centre. Colour comes from --wipe-color, set per instance in the
   markup — here --paper, which is §3's own background, so the cover
   lifting reads as content arriving rather than as a cut.

   The page locks twice: §2 pins for 80vh while the columns rise over
   it, then §3 pins for 50vh while it resolves, with one screen of
   covered scroll carrying between them. main.js §2b has the reasoning
   for each. Nothing here is in the flow, and neither pin is created
   until the element has survived its guards — so a dead CDN or reduced
   motion leaves the seam exactly as it was, with no orphaned scroll to
   fall through.
   ============================================================ */
.wipe {
  position: fixed;
  inset: 0;
  /* Over the header (60), under the skip link (100). The header is meant
     to disappear behind the cover — two elements sliding over each other
     at the same moment is the one thing that breaks the illusion. */
  z-index: 70;
  display: grid;
  grid-template-columns: repeat(var(--wipe-bars, 6), 1fr);
  pointer-events: none;
  visibility: hidden;   /* main.js arms it; no JS -> it never paints */
}
.wipe__bar {
  background: var(--wipe-color, var(--navy));
  /* Adjacent columns land on fractional pixels at most viewport widths;
     without this, hairlines of the old section show through the cover. */
  box-shadow: 0 0 0 1px var(--wipe-color, var(--navy));
  will-change: transform;
}

/* ============================================================
   7. Yang jalanin — founder slideshow
   Ported from semua-ada-harapan Founders.tsx. Numbers kept from the
   source: 44% card cap at 901px, inset 0 0 160px 40px vignette,
   36px parallax amplitude, 7000ms dwell, 34px -> 92px pill widen,
   900ms crossfade on cubic-bezier(.4,0,.2,1).
   Changed: 112svh -> 88svh (length budget), cream card -> glass,
   gold #f2b31b fill -> --volt.
   ============================================================ */

/* Exactly one screen — no more, no less. Was 88svh + padding, which at a 535px
   viewport gave a 472px section holding a 355px card plus padding: the card
   overflowed its own section and got clipped by the header. Same fix as the
   hero: fix the height, then cap the contents against viewport height so they
   can never outgrow the box. */
.founders {
  position: relative;
  display: flex;
  align-items: flex-end;
  height: 100svh;
  padding-block: calc(var(--header-top) + var(--header-h) + var(--space-6)) var(--space-8);
  padding-inline: var(--gutter);
  overflow: hidden;
  isolation: isolate;
}
@supports not (height: 100svh) { .founders { height: 100vh; } }

/* Scaled up so the parallax drift never exposes an edge (source: scale(1.08)) */
.founders__bg {
  position: absolute;
  inset: 0;
  z-index: -2;
  transform: scale(1.08) translateY(var(--founders-parallax, 0px));
}
.founders__slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 900ms cubic-bezier(0.4, 0, 0.2, 1);
}
.founders__slide.is-active { opacity: 1; }
.founders__slide > .ph-frame,
.founders__slide > img { width: 100%; height: 100%; object-fit: cover; }

/* Placeholder standing in for the photo: same box, no aspect-ratio lock, and
   DARK — this section renders white text over its background, so a light
   placeholder made the glass card unreadable. Mirrors .band--navy .ph-frame. */
.ph-frame--bleed {
  aspect-ratio: auto;
  border: none;
  border-radius: 0;
  color: #FFFFFF;
  background:
    repeating-linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0 10px, transparent 10px 20px),
    linear-gradient(160deg, var(--navy) 0%, var(--navy-deep) 100%);
}
.ph-frame--bleed .ph-frame__brief { color: rgba(255, 255, 255, 0.62); }
.ph-frame--bleed .ph-frame__icon { filter: brightness(0) invert(1); opacity: 0.34; }
/* Keep the placeholder's own text out from under the quote card (the card caps
   at 44% from 901px). Irrelevant once a real photo replaces this. */
@media (min-width: 901px) {
  .ph-frame--bleed { align-items: flex-end; padding-right: 8%; text-align: right; }
  .ph-frame--bleed .ph-frame__brief { margin-left: auto; }
}

.founders__vignette {
  position: absolute;
  inset: 0;
  z-index: -1;
  box-shadow: inset 0 0 160px 40px rgba(0, 0, 0, 0.35);
  background: linear-gradient(90deg, rgba(6, 12, 51, 0.72) 0%, rgba(6, 12, 51, 0.28) 55%, rgba(6, 12, 51, 0.5) 100%);
  pointer-events: none;
}

.founders__inner {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
}
/* Light card on a dark background — faithful to the source, which uses a cream
   card over the photo rather than another dark panel. It is also the only light
   card on this page, which is what gives the section its own identity instead of
   being the third identical dark glass box. Text colours are re-declared because
   the section carries .on-media (white), which would be unreadable here. */
.founders__card {
  padding: clamp(1rem, 2.6svh, 2rem);
  background: rgba(245, 244, 239, 0.90);
  border-color: rgba(255, 255, 255, 0.6);
  color: var(--ink);
}
@media (min-width: 901px) { .founders__card { max-width: 44%; } }
.founders__card .micro,
.founders__card .founders__quote { color: var(--ink-70); }
.founders__card .founders__name { color: var(--navy); }
.founders__card .slot { background: rgba(11, 18, 77, 0.10); color: var(--navy); }

.founders__role { display: block; }
.founders__name {
  margin-top: var(--space-2);
  font-family: var(--font-display);
  font-size: min(var(--fs-h2), 5.6svh);
  line-height: var(--lh-h2);
  letter-spacing: var(--ls-display);
  text-transform: uppercase;
}
.founders__mark {
  display: block;
  margin-top: var(--space-2);
  font-family: var(--font-display);
  font-size: min(3.4rem, 6svh);
  line-height: 1;
  /* Source uses a pale gold #ddd0a8 here — decorative, not the accent colour.
     --volt cannot be used: it is join-actions-only, and it is invisible on a
     light card anyway. Navy at low opacity is the same move in this palette. */
  color: rgba(11, 18, 77, 0.30);
}
.founders__quote { margin-top: var(--space-1); font-size: max(0.9375rem, min(1rem, 2.6svh)); }

/* Short windows: the quote is the only elastic element, so it gives first. */
@media (max-height: 700px) {
  .founders { padding-block: calc(var(--header-top) + var(--header-h) + var(--space-3)) var(--space-6); }
  .founders__mark { display: none; }
}

/* Reduced motion: no drift, no crossfade. The photograph is a still either way. */
@media (prefers-reduced-motion: reduce) {
  .founders__bg { transform: none; }
  .founders__slide { transition: none; }
}

/* ============================================================
   8. FAQ
   ============================================================ */

.faq { position: relative; overflow: hidden; }
.faq .wrap { position: relative; z-index: 1; }

/* ---- Light-rays background (js/beams.js, [data-beams]) ----
   Replaces .tiled on this section. The scattered racket marks and an accordion
   of hairlines were competing at the same weight, and the marks won — the band
   read as wallpaper with questions on it. Beams sit far enough back to be
   atmosphere, and they move, which the texture never did.

   Beams are light ADDED to the surface, so this only works on a dark band. The
   navy under it stays: the canvas is transparent where no beam is. */
.beams__canvas {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  /* The canvas renders at ~1/3 scale (js/beams.js RES) and is scaled up here.
     A little CSS blur on top hides the upscale and is cheaper than rendering
     the same softness at full resolution. */
  filter: blur(10px);
}

/* The component's Framer Motion veil, minus Framer Motion and minus the
   full-surface backdrop-filter it rode on. Two jobs: breathe, so the band is
   never quite still, and hold a vignette so the beams stay off the copy. */
.beams__veil {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: radial-gradient(72% 58% at 42% 50%, var(--navy) 0%, transparent 78%);
  opacity: 0.55;
  animation: beams-breathe 11s var(--ease-material, ease-in-out) infinite;
}

/* On a 375px band the copy runs the full width, so a vignette sized to clear
   a left-hand column just covers everything and takes the beams with it. */
@media (max-width: 767px) {
  .beams__veil { background: linear-gradient(180deg, var(--navy) 0%, transparent 34%, transparent 66%, var(--navy) 100%); }
}

@keyframes beams-breathe {
  0%, 100% { opacity: 0.44; }
  50%      { opacity: 0.62; }
}

/* Both guards, because they fail differently: .is-beams-off is set by main.js
   and covers "the script ran and decided not to animate", while the media query
   covers "main.js never ran at all". Either way the band keeps a still frame —
   the beams are the section's visual, not an embellishment on top of one. */
.faq.is-beams-off .beams__veil { animation: none; }
@media (prefers-reduced-motion: reduce) {
  .beams__veil { animation: none; }
}
.faq__art {
  position: absolute;
  right: -14%;
  /* Anchored to the top of the band, NOT centred on it. `top: 50%` tracked the
     section's midpoint, so every accordion open/close (which changes the band's
     height) slid the racket ~26px. Top-anchored, it cannot move. 10.5rem is the
     centred position measured at 1440 with every item closed, so the resting
     composition is unchanged. */
  top: 10.5rem;
  width: min(560px, 62%);
  opacity: 0.22;
  z-index: 0;
  pointer-events: none;
  /* No mask needed any more: faq-object-800.png is a real transparent cut-out,
     so there is no sky rectangle to feather away. Opacity up 0.16 -> 0.22 —
     the mask was eating most of the object's density. */
}
/* Below 768 the art is sized off the viewport (62vw), so one fixed `top` would
   put its centre in a different place at 375 than at 767. 25rem - 31vw is the
   top that lands the centre 400px down the band at every width in the range —
   still height-independent, so it stays put when an answer opens. */
@media (max-width: 767px) { .faq__art { right: -30%; top: calc(25rem - 31vw); opacity: 0.14; } }

/* ============================================================
   8b. Zoom transition (ZoomParallax mechanic)
   Hidden below 768px — see the markup comment for why.
   ============================================================ */

.zoom { display: none; }

@media (min-width: 768px) {
  .zoom {
    display: block;
    position: relative;
    /* 260vh, with the CTA band pulled back up by 100vh (see .ctaband--final
       below) — net page height is unchanged from the old 160vh.
       Why the extra viewport: a sticky pin always releases exactly one screen
       ABOVE its next sibling, so no `end` value could ever put the card on
       screen when the zoom landed. The 100vh is pin travel the card now rises
       through, instead of dead scroll. */
    height: 260vh;
    /* Single colour, matching the FAQ band above it. It was --navy-deep against
       the FAQ's --navy, which put a hard two-tone seam across the page. */
    background: var(--navy);
  }
  .zoom__pin {
    position: sticky;
    top: 0;
    height: 100vh;
    overflow: hidden;
  }
  /* Feather the top and bottom edges so tiles crossing the section boundary
     dissolve instead of being sliced off by `overflow: hidden`. */
  .zoom__pin::before,
  .zoom__pin::after {
    content: "";
    position: absolute;
    left: 0;
    z-index: 3;
    width: 100%;
    height: 14vh;
    pointer-events: none;
  }
  .zoom__pin::before { top: 0;    background: linear-gradient(180deg, var(--navy), rgba(11, 18, 77, 0)); }
  .zoom__pin::after  { bottom: 0; background: linear-gradient(0deg,   var(--navy), rgba(11, 18, 77, 0)); }
  .zoom__stage {
    position: relative;
    width: 100%;
    height: 100%;
  }
  .zoom__t {
    position: absolute;
    top: 0;
    left: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    will-change: transform;
  }
  /* Tile 0 is the zoom TARGET, so it has to end on top. DOM order alone put
     tile 6 (scale 9) over everything, which is why the section read as slabs
     sliding past instead of a collage opening onto one image. */
  .zoom__t--0 { z-index: 7; }
  .zoom__t--1 { z-index: 6; }
  .zoom__t--2 { z-index: 5; }
  .zoom__t--3 { z-index: 4; }
  .zoom__t--4 { z-index: 3; }
  .zoom__t--5 { z-index: 2; }
  .zoom__t--6 { z-index: 1; }
  .zoom__t > picture {
    position: relative;
    display: block;
    overflow: hidden;
    border-radius: var(--radius-m);
    box-shadow: 0 24px 60px -30px rgba(0, 0, 0, 0.8);
  }
  .zoom__t img { width: 100%; height: 100%; object-fit: cover; }

  /* Per-tile slot geometry. The source's numbers (25vw x 25vh centre tile) were
     tuned for a tall desktop viewport; on a 1000x535 window they render 250x134
     — too small to read as a collage before the zoom swallows them. Every tile
     is scaled up here and the offsets widened to match, so the wall is legible
     at the start of the scroll rather than only in the first few frames. */
  .zoom__t--0 > picture { width: 34vw; height: 40vh; border-radius: 0; }
  .zoom__t--1 > picture { width: 30vw; height: 34vh; }
  .zoom__t--2 > picture { width: 22vw; height: 50vh; }
  .zoom__t--3 > picture { width: 26vw; height: 30vh; }
  .zoom__t--4 > picture { width: 24vw; height: 30vh; }
  .zoom__t--5 > picture { width: 30vw; height: 28vh; }
  .zoom__t--6 > picture { width: 18vw; height: 22vh; }

  .zoom__t--1 { top: -36vh; left: 2vw; }
  .zoom__t--2 { top: -6vh;  left: -32vw; }
  .zoom__t--3 { top: -4vh;  left: 33vw; }
  .zoom__t--4 { top: 34vh;  left: 8vw; }
  .zoom__t--5 { top: 33vh;  left: -28vw; }
  .zoom__t--6 { top: 30vh;  left: 32vw; }
}

/* No JS, or reduced motion: collapse the whole thing. A 200vh dead scroll with
   nothing animating is worse than not having it. */
/* The final CTA rides the tail of the pin. Pulled back over the zoom's last
   viewport so the card is already rising as the collage settles — the old
   layout finished the effect and then asked for another full screen of
   scrolling before the card existed. Net document height is unchanged; the
   100vh added to .zoom is taken straight back here. */
@media (min-width: 768px) {
  .ctaband--final {
    margin-top: -100vh;
    /* Matched to the pin's frame so nothing cuts into the photo underneath. */
    min-height: 100vh;
    position: relative;
    z-index: 2;
  }
  /* ONLY THE CARD MOVES. The band shipped its own copy of the photo, which
     meant a second image sliding up over the zoom's — two crops of the same
     picture crossing each other. The zoom's pinned frame is the background
     here. By the time the card lands, that pin is bottom-stuck (fixed in the
     document, not the viewport), so from then on the photo and the card scroll
     away together as one block. */
  .ctaband--final > picture,
  .ctaband--final .media-bed__scrim { display: none; }

  /* The pull-back is only valid while there is a zoom to be pulled over.
     main.js sets .is-off when GSAP/ScrollTrigger is missing or motion is
     reduced; without this the CTA hoists a full screen up and lands on top of
     the FAQ, covering it and swallowing every click on it. Caught in test.
     With no zoom behind it, the band needs its own photo back. */
  .zoom.is-off + .ctaband--final { margin-top: 0; min-height: 440px; }
  .zoom.is-off + .ctaband--final > picture { display: block; }
  .zoom.is-off + .ctaband--final .media-bed__scrim { display: block; }
}
/* Same guard for the case main.js never runs at all: the reduced-motion rule
   below hides .zoom in pure CSS, so .is-off would never be applied. */
@media (prefers-reduced-motion: reduce) {
  .ctaband--final { margin-top: 0; min-height: 440px; }
  .ctaband--final > picture { display: block; }
  .ctaband--final .media-bed__scrim { display: block; }
}

.zoom.is-off { display: none; }
@media (prefers-reduced-motion: reduce) { .zoom { display: none; } }

/* ============================================================
   10. Footer
   ============================================================ */

.footer { position: relative; overflow: hidden; isolation: isolate; padding-block: var(--space-12); }

/* Last tiled-icon watermark on the page, retired. band-mid is the tight crop of
   a single stone racket — too close to read as an object in a hero slot, which
   is exactly what makes it work as footer texture. No added page height. */
.footer__img {
  position: absolute;
  inset: 0;
  z-index: -2;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 30% 60%;
}
.footer__veil {
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(180deg, rgba(6, 12, 51, 0.93) 0%, rgba(6, 12, 51, 0.97) 100%);
  pointer-events: none;
}
.footer .wrap { position: relative; z-index: 1; }
.footer__top {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-6);
  padding-bottom: var(--space-8);
  border-bottom: 1px solid rgba(255, 255, 255, 0.16);
}
.footer__wordmark { width: min(280px, 62vw); filter: brightness(0) invert(1); }
.footer__sub { margin-top: var(--space-2); }
.footer__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-6);
  padding-top: var(--space-8);
}
@media (min-width: 768px) { .footer__grid { grid-template-columns: repeat(3, 1fr); } }
.footer__legal { margin-top: var(--space-8); font-size: var(--fs-micro); color: var(--ink-45); }

/* Hidden entry to palette.html. Quiet, not invisible — a link nobody can see or
   focus is a trap, so it keeps a real focus ring and an aria-label. */
/* 24x24 minimum hit area (WCAG 2.2 target size). Quiet is fine; a 15px tap
   target that only a mouse can hit is not. */
.footer__hidden {
  display: inline-block;
  min-width: 24px;
  min-height: 24px;
  line-height: 24px;
  text-align: center;
  vertical-align: middle;
  text-decoration: none;
  opacity: 0.35;
}
.footer__hidden:hover { opacity: 1; color: var(--volt); }
.footer__hidden:focus-visible { outline: 2px solid var(--volt); outline-offset: 3px; opacity: 1; }
