/* ==========================================================================
   reveals.css — scroll-triggered motion. Last file in the cascade, so it can
   safely override component state.

   Contract with reveals.js:
     [data-reveal]          a block that fades + rises on enter
     [data-reveal="img"]    a media frame whose image wipes + settles
     [data-reveal-group]    staggers its [data-reveal] children
     .is-in                 added once, on enter; never removed

   Nothing here runs before JS: the .no-js and pre-init guards below keep
   content visible if the script fails to load.
   ========================================================================== */

/* --- Block reveal --------------------------------------------------------- */
[data-reveal] {
  opacity: 0;
  transform: translate3d(0, var(--reveal-y), 0);
  transition:
    opacity   var(--d-reveal) var(--ease-reveal) var(--reveal-delay, 0ms),
    transform var(--d-reveal) var(--ease-reveal) var(--reveal-delay, 0ms);
  will-change: opacity, transform;
}
[data-reveal].is-in {
  opacity: 1;
  transform: none;
  will-change: auto;
}

/* --- Image reveal ---------------------------------------------------------
   A curtain wipes up off the frame while the image itself settles back from
   --image-scale. Two elements moving at different rates is what makes it read
   as a reveal rather than a fade.

   The curtain is a scaleY() pseudo-element, NOT clip-path on the host.
   clip-path on an observed element collapses its IntersectionObserver
   intersection rect to zero, so the observer never fires and the reveal never
   runs. scaleY is also composited, so this is the cheaper of the two.     */
[data-reveal='img'] {
  position: relative;
  opacity: 1;
  transform: none;
}
[data-reveal='img']::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: var(--curtain);
  transform: scaleY(1);
  transform-origin: bottom;
  transition: transform var(--d-reveal) var(--ease-reveal) var(--reveal-delay, 0ms);
}
[data-reveal='img'].is-in::before { transform: scaleY(0); }

[data-reveal='img'] > img,
[data-reveal='img'] > video,
[data-reveal='img'] > picture {
  transform: scale(var(--image-scale));
  transition: transform calc(var(--d-reveal) + 300ms) var(--ease-reveal) var(--reveal-delay, 0ms);
  will-change: transform;
}
[data-reveal='img'].is-in > img,
[data-reveal='img'].is-in > video,
[data-reveal='img'].is-in > picture { transform: none; will-change: auto; }

/* Curtain matches whatever sits behind the frame */
.panel-dark [data-reveal='img']::before,
.tile--dark [data-reveal='img']::before { background: var(--ink-card); }
.tedx [data-reveal='img']::before       { background: var(--ink); }

/* --- Mask reveal — a heading rises out of its own box -----------------------
   reveals.js upgrades every h1/h2[data-reveal] to this and wraps the content
   in .mask__in. overflow:hidden on the host does not affect IntersectionObserver
   (clip-path would), so the observer still fires.                         */
[data-reveal='mask'] {
  opacity: 1;
  transform: none;
  overflow: hidden;
  padding-bottom: 0.12em;   /* room for descenders inside the clip */
  margin-bottom: -0.12em;
}
[data-reveal='mask'] > .mask__in {
  display: block;
  transform: translate3d(0, 108%, 0);
  transition: transform var(--d-reveal) var(--ease-reveal) var(--reveal-delay, 0ms);
  will-change: transform;
}
[data-reveal='mask'].is-in > .mask__in { transform: none; will-change: auto; }

/* --- Entrance variants ---------------------------------------------------- */
.why__bento > [data-reveal] {
  transform: translate3d(0, var(--reveal-y), 0) scale(0.96);
}
.why__bento > [data-reveal].is-in { transform: none; }

.hero__cluster > [data-reveal]:nth-child(1) { transform: translate3d(-16px, 28px, 0) rotate(-2deg); }
.hero__cluster > [data-reveal]:nth-child(2) { transform: translate3d(16px, 28px, 0) rotate(2deg); }
.hero__cluster > [data-reveal]:nth-child(3) { transform: translate3d(0, 36px, 0) scale(0.97); }
.hero__cluster > [data-reveal].is-in { transform: none; }

/* About page: pillars fly in from their side, the circle scales up, journey
   cards slide in from the right, team cards rise with a tilt. */
.pillars__stage > .pillar:nth-of-type(1)[data-reveal],
.pillars__stage > .pillar:nth-of-type(2)[data-reveal] { transform: translate3d(-48px, 0, 0); }
.pillars__stage > .pillar:nth-of-type(3)[data-reveal],
.pillars__stage > .pillar:nth-of-type(4)[data-reveal] { transform: translate3d(48px, 0, 0); }
.pillars__centre[data-reveal] { transform: scale(0.82); }
/* The reset must carry the same weight as the four rules above, or the cards
   keep their 48px offset after the reveal — which reads as a broken grid,
   worst of all on a phone, where they hang off both edges. */
.pillars__stage > .pillar[data-reveal].is-in,
.pillars__stage > [data-reveal].is-in { transform: none; }

/* Horizontal tracks: cards off the right edge never intersect the viewport,
   so they key off the viewport's own .is-in with an nth-child stagger. */
.journey__viewport[data-reveal], .track__viewport[data-reveal] { opacity: 1; transform: none; }
.journey__viewport:not(.is-in) .journey__card { opacity: 0; transform: translate3d(64px, 0, 0); }
.journey__card {
  transition: opacity var(--d-reveal) var(--ease-reveal), transform var(--d-reveal) var(--ease-reveal);
}
.track__viewport:not(.is-in) .team__card { opacity: 0; transform: translate3d(0, 40px, 0) rotate(1.5deg); }
.team__card {
  transform-origin: 50% 100%;
  transition: opacity var(--d-reveal) var(--ease-reveal), transform var(--d-reveal) var(--ease-reveal);
}
.journey__card:nth-child(2), .team__card:nth-child(2) { transition-delay: 90ms; }
.journey__card:nth-child(3), .team__card:nth-child(3) { transition-delay: 180ms; }
.journey__card:nth-child(4), .team__card:nth-child(4) { transition-delay: 270ms; }
.journey__card:nth-child(n+5), .team__card:nth-child(n+5) { transition-delay: 360ms; }
html:not(.reveals-ready) .journey__card, html:not(.reveals-ready) .team__card { opacity: 1 !important; transform: none !important; }

.vision__item[data-reveal] { transform: translate3d(-32px, 0, 0); }
.vision__item[data-reveal].is-in { transform: none; }

/* the full-height founder panel wipes open from its left edge (clip on the
   child, never on the observed element, or the observer never fires) */
.founder__portrait[data-reveal] { opacity: 1; transform: none; }
.founder__portrait [data-reveal='img']::before { display: none; }
.founder__portrait .media { transition: clip-path 1.1s var(--ease-reveal); }
html.reveals-ready .founder__portrait:not(.is-in) .media { clip-path: inset(0 100% 0 0); }
.founder__sign { transition: opacity 0.9s var(--ease-out) 0.5s, transform 1.1s var(--ease-reveal) 0.5s; }
html.reveals-ready .founder__portrait:not(.is-in) .founder__sign { opacity: 0; transform: translate3d(0, 24px, 0); }

/* --- Parallax --------------------------------------------------------------
   reveals.js writes --py (px) on the media frame; only the image moves, the
   frame clips it. The image is oversized so the drift never shows an edge. */
[data-parallax] > img,
[data-parallax] > video {
  transform: translate3d(0, var(--py, 0px), 0) scale(1.12);
  will-change: transform;
}
/* a parallax frame that also reveals: settle from 1.22 to the 1.12 resting scale */
[data-parallax][data-reveal='img']:not(.is-in) > img { transform: translate3d(0, var(--py, 0px), 0) scale(1.22); }
[data-parallax][data-reveal='img'].is-in > img       { transform: translate3d(0, var(--py, 0px), 0) scale(1.12); }

/* --- Line reveal — headings rise per line behind a mask ------------------- */
[data-reveal='lines'] { opacity: 1; transform: none; }
[data-reveal='lines'] .line { display: block; overflow: hidden; }
[data-reveal='lines'] .line > span {
  display: block;
  transform: translate3d(0, 100%, 0);
  transition: transform var(--d-reveal) var(--ease-reveal) var(--reveal-delay, 0ms);
}
[data-reveal='lines'].is-in .line > span { transform: none; }

/* --- Counter -------------------------------------------------------------- */
[data-count] { font-variant-numeric: tabular-nums; }

/* --- Pre-init and no-JS guards --------------------------------------------
   Without these a failed script leaves the page blank.                    */
.no-js [data-reveal],
.no-js [data-reveal='img'],
html:not(.reveals-ready) [data-reveal] {
  opacity: 1 !important;
  transform: none !important;
}
.no-js [data-reveal='img']::before,
html:not(.reveals-ready) [data-reveal='img']::before { transform: scaleY(0) !important; }
.no-js [data-reveal='lines'] .line > span,
html:not(.reveals-ready) [data-reveal='lines'] .line > span { transform: none !important; }
.no-js [data-reveal='mask'] > .mask__in,
html:not(.reveals-ready) [data-reveal='mask'] > .mask__in { transform: none !important; }
.no-js [data-reveal='img'] > img,
html:not(.reveals-ready) [data-reveal='img'] > img { transform: none !important; }

@media (prefers-reduced-motion: reduce) {
  [data-reveal], [data-reveal='img'] { opacity: 1; transform: none; }
  [data-reveal='img']::before { transform: scaleY(0); }
  [data-reveal='lines'] .line > span { transform: none; }
  [data-reveal='mask'] > .mask__in { transform: none; }
  [data-parallax] > img, [data-parallax] > video { transform: none; }
  .hero__cluster { transform: none; }
  html.reveals-ready .founder__portrait:not(.is-in) .media { clip-path: none; }
  html.reveals-ready .founder__portrait:not(.is-in) .founder__sign { opacity: 1; transform: none; }
}

/* --- Word fill (scroll-linked) --------------------------------------------
   Set by reveals.js: each word carries its own opacity, driven by how far the
   block has travelled through the viewport. Re-runs in both directions.   */
.fill-word {
  opacity: 0.16;
  transition: opacity 120ms linear;
  will-change: opacity;
}
.no-js .fill-word,
html:not(.reveals-ready) .fill-word { opacity: 1 !important; }
@media (prefers-reduced-motion: reduce) { .fill-word { opacity: 1 !important; } }
