/* AUTHORED — not generated. Safe from extract-tokens.mjs --write.

   Section-to-section scroll choreography, with no JavaScript library.

   THE SAFETY RULE, and it is not negotiable: content is visible by default.
   Nothing here hides an element unless we have already proven we can bring it
   back. Two independent paths do that, and neither is required for the page to
   be readable:

     1. CSS scroll-driven animations, gated behind @supports. Chrome/Edge 115+
        and Safari 26+. If the browser lacks them the @supports block never
        applies and everything simply renders, statically, in place.

     2. An IntersectionObserver fallback for Firefox (still behind
        layout.css.scroll-driven-animations.enabled as of 152, June 2026).
        The script sets data-motion="js" on <html> ITSELF — so if the script
        never runs, the hiding rules never match. See guidelines/art-direction.md
        for the ~15 lines.

   Everything is additionally disabled under prefers-reduced-motion. */

/* ---- Keyframes -------------------------------------------------------- */
@keyframes reveal-up    { from { opacity: 0; transform: translateY(24px); } to { opacity: 1; transform: none; } }
@keyframes reveal-left  { from { opacity: 0; transform: translateX(-24px); } to { opacity: 1; transform: none; } }
@keyframes reveal-right { from { opacity: 0; transform: translateX(24px); } to { opacity: 1; transform: none; } }
@keyframes reveal-scale { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: none; } }
@keyframes fade-in      { from { opacity: 0; } to { opacity: 1; } }
@keyframes drift-up     { from { transform: translateY(20px); } to { transform: translateY(-20px); } }
@keyframes progress-x   { from { transform: scaleX(0); } to { transform: scaleX(1); } }

/* 3D plane settle — a card/screenshot leans forward, then rotates flat as it
   enters. Perspective is baked in so no ancestor `perspective` is needed.
   Transform-only: if it never runs, the plane is simply flat and fully visible. */
@keyframes tilt-settle  { from { transform: perspective(var(--perspective)) rotateX(var(--tilt-max)) translateY(14px); } to { transform: perspective(var(--perspective)) rotateX(0deg) translateY(0); } }
@keyframes tilt-left    { from { transform: perspective(var(--perspective)) rotateY(calc(-1 * var(--tilt-max))) translateX(-14px); } to { transform: perspective(var(--perspective)) rotateY(0deg) translateX(0); } }
@keyframes tilt-right   { from { transform: perspective(var(--perspective)) rotateY(var(--tilt-max)) translateX(14px); } to { transform: perspective(var(--perspective)) rotateY(0deg) translateX(0); } }
/* Layered parallax — foreground travels more than background for real depth. */
@keyframes drift-near   { from { transform: translateY(var(--depth-near)); } to { transform: translateY(calc(-1 * var(--depth-near))); } }
@keyframes drift-far    { from { transform: translateY(var(--depth-far)); } to { transform: translateY(calc(-1 * var(--depth-far))); } }
/* Horizontal step-rail — the track slides left as the visitor scrolls the
   pinned section. calc(-100% + 100vw) lands the track's right edge flush with
   the viewport's right edge, so the last step ends fully on screen regardless
   of how many steps there are. */
@keyframes rail-x       { to { transform: translateX(calc(-100% + 100vw)); } }

/* ---- Path 1: native scroll-driven animation --------------------------- */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    [data-reveal] {
      animation: reveal-up linear both;
      animation-timeline: view();
      /* BOTH endpoints must be `entry`, never `cover`.
         An element already on screen at load has finished `entry` (progress
         100%) so `both` fill lands it on the final keyframe — visible. With a
         `cover` endpoint it would sit mid-animation at a fractional opacity
         until the user scrolled: a permanently dimmed hero. Verified by
         .design-sync/verify-scroll.mjs, which fails on exactly that. */
      animation-range: entry 8% entry 92%;
    }
    [data-reveal="left"]  { animation-name: reveal-left; }
    [data-reveal="right"] { animation-name: reveal-right; }
    [data-reveal="scale"] { animation-name: reveal-scale; }
    [data-reveal="fade"]  { animation-name: fade-in; }

    /* Stagger a group by setting --i: 0,1,2… on the children. Start shifts;
       the end stays pinned inside `entry` so nothing can strand mid-animation. */
    [data-reveal][style*="--i"] { animation-range: entry calc(8% + (var(--i) * 7%)) entry 92%; }

    /* Parallax: small. 40px of total travel across the whole viewport pass.
       Layer it with "near" (foreground, more travel) and "far" (background,
       less) to build depth. Transform-only, so `cover` range is safe here. */
    [data-parallax] {
      animation: drift-up linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }
    [data-parallax="near"] { animation-name: drift-near; }
    [data-parallax="far"]  { animation-name: drift-far; }

    /* 3D tilt: a plane settles from a slight lean to flat as it enters view.
       Vertical by default; "left"/"right" lean in from the side. Ranges use
       `entry` (settles during entry, then rests flat) — and because tilt is
       transform-only it can never strand an element invisible, so it is exempt
       from the reveal `cover`-endpoint ban. Put data-tilt on the card or the
       screenshot you want to feel physical. */
    [data-tilt] {
      transform-origin: center 65%;
      animation: tilt-settle linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 60%;
      will-change: transform;
    }
    [data-tilt="left"]  { animation-name: tilt-left; }
    [data-tilt="right"] { animation-name: tilt-right; }

    /* Horizontal step-rail (desktop enhancement). The tall [data-rail] wrapper
       pins its [data-rail-view]; the [data-rail-track] slides left across the
       wrapper's scroll — a slideshow through the steps driven by vertical scroll.
       Author sets the wrapper height (~N × 100vh) to control pace. Below 900px
       this never applies and the rail is a native horizontal swipe strip (base
       rules below) — the right call for touch. */
    @media (min-width: 900px) {
      [data-rail] { view-timeline-name: --rail; }
      [data-rail] [data-rail-view] { position: sticky; top: 0; height: 100vh; overflow: hidden; display: flex; align-items: center; }
      [data-rail] [data-rail-track] {
        width: max-content;
        animation: rail-x linear both;
        animation-timeline: --rail;
        animation-range: contain 0% contain 100%;
      }
    }

    /* A pinned section whose progress bar tracks scroll through it.
       Put data-pin on the tall wrapper, data-progress on the bar inside. */
    [data-pin] { view-timeline-name: --pin; }
    [data-pin] [data-progress] {
      transform-origin: left center;
      animation: progress-x linear both;
      animation-timeline: --pin;
      animation-range: cover 0% cover 100%;
    }
  }
}

/* ---- Path 2: IntersectionObserver fallback ----------------------------
   Only ever active when the script has stamped <html data-motion="js">, and
   only where native timelines are absent. Both guards, deliberately. */
@media (prefers-reduced-motion: no-preference) {
  @supports not (animation-timeline: view()) {
    [data-motion="js"] [data-reveal] {
      opacity: 0;
      transform: translateY(24px);
      transition: opacity var(--dur-slow) var(--ease-out-expo),
                  transform var(--dur-slow) var(--ease-out-expo);
      transition-delay: calc(var(--i, 0) * 60ms);
    }
    [data-motion="js"] [data-reveal="left"]  { transform: translateX(-24px); }
    [data-motion="js"] [data-reveal="right"] { transform: translateX(24px); }
    [data-motion="js"] [data-reveal="scale"] { transform: scale(0.96); }
    [data-motion="js"] [data-reveal="fade"]  { transform: none; }

    [data-motion="js"] [data-reveal][data-inview] {
      opacity: 1;
      transform: none;
    }
  }
}

/* ---- Smooth scroll + gentle snap -------------------------------------
   Opt-in and deliberately conservative so it can never fight a phone's
   scroll (mobile-web is the primary surface; "never break mobile-web scroll"
   is a hard rule). `data-snap-root` on the scroll container turns on PROXIMITY
   snap — the user still scrolls freely and only eases to the nearest point;
   `mandatory` is banned here because it traps momentum on touch. `data-snap`
   marks each section's snap point. Smooth anchor scrolling is on document-wide
   for the persona/section nav, and off under reduced motion. */
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}
[data-snap-root] { scroll-snap-type: var(--scroll-snap); }
[data-snap] { scroll-snap-align: start; scroll-margin-top: var(--space-6, 4rem); }

/* Base step-rail: a native horizontal swipe strip that works everywhere and is
   the PRIMARY experience on mobile — thumb-swipe through the steps, snapping to
   each. On desktop the pinned scroll-drive enhancement above takes over (it
   raises overflow to hidden and animates the track), so this is both the mobile
   experience and the universal no-timeline fallback. Content is never hidden. */
[data-rail-view] { overflow-x: auto; scroll-snap-type: x proximity; -webkit-overflow-scrolling: touch; scrollbar-width: none; }
[data-rail-view]::-webkit-scrollbar { display: none; }
[data-rail-track] { display: flex; gap: var(--space-6, 2rem); }
[data-rail-track] > * { flex: 0 0 auto; scroll-snap-align: center; }

/* ---- Reduced motion: everything renders, nothing moves ---------------- */
@media (prefers-reduced-motion: reduce) {
  [data-reveal], [data-parallax], [data-progress], [data-tilt] {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
  html { scroll-behavior: auto; }
  [data-snap-root] { scroll-snap-type: none; }
}
