/*
 * Motion layer.
 *
 * Everything here is native CSS scroll-driven animation — no GSAP, no Lenis,
 * no IntersectionObserver. That keeps the bundle at zero bytes of JS and means
 * the whole file can be copied into a WordPress theme's stylesheet untouched.
 *
 * Safety model:
 *   - Default state of every element is VISIBLE. Nothing is hidden by default.
 *   - Animations only switch on inside `@supports (animation-timeline: view())`,
 *     so a browser without scroll-driven animations shows a clean static page
 *     instead of a blank one.
 *   - Everything is disabled under `prefers-reduced-motion: reduce`.
 */

/* ---------------------------------------------------------------- keyframes */

@keyframes rise-in {
  from {
    opacity: 0;
    translate: 0 4rem;
  }
  to {
    opacity: 1;
    translate: 0 0;
  }
}

@keyframes fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* Vertical drift. Distance is per-element via --drift. */
@keyframes drift {
  from {
    translate: 0 var(--drift, 3rem);
  }
  to {
    translate: 0 calc(var(--drift, 3rem) * -1);
  }
}

/* Focus pull: the photo arrives soft and sharpens as it settles into view. */
@keyframes focus-pull {
  from {
    filter: blur(14px);
  }
  to {
    filter: blur(0);
  }
}

/*
 * Curtain wipe for image frames.
 *
 * A gradient mask rather than clip-path, so the reveal has a soft leading edge
 * instead of a ruled line. The mask is twice the element's height — black over
 * the top half, fading to transparent — and the animation slides it.
 *
 * Percentage mask positions resolve against (element size - mask size), which
 * is negative here, so `0 100%` parks the transparent half over the element and
 * `0 0` brings the opaque half across. The unanimated default is `0% 0%`, i.e.
 * fully visible, which is what browsers without scroll timelines get.
 */
.unveil {
  mask-image: linear-gradient(to bottom, #000 0 45%, transparent 62% 100%);
  mask-size: 100% 200%;
  mask-repeat: no-repeat;
}

@keyframes unveil {
  from {
    mask-position: 0 100%;
  }
  to {
    mask-position: 0 0;
  }
}

@keyframes header-condense {
  to {
    height: 4rem;
    background-color: color-mix(in oklab, var(--color-canvas) 92%, transparent);
    border-bottom-color: var(--color-line);
  }
}

@keyframes progress-grow {
  from {
    scale: 0 1;
  }
  to {
    scale: 1 1;
  }
}

@keyframes cue-bob {
  0%,
  100% {
    translate: 0 0;
    opacity: 0.55;
  }
  50% {
    translate: 0 0.45rem;
    opacity: 1;
  }
}

/* ------------------------------------------------------------ headline words */
/*
 * Each word rises out from behind its own clipped wrapper while the Cormorant
 * weight axis settles.
 *
 * The axis runs heavy to light, not light to heavy: the family's variable range
 * starts at 300, which is also the resting weight of the headings, so there is
 * no room to come up from below it. Arriving a little denser and thinning into
 * place gives the same sense of the type finding its footing.
 */
.word-mask {
  display: inline-block;
  overflow: clip;
  /* Clipping to the exact line box would shear descenders off the glyphs. */
  padding-bottom: 0.12em;
  margin-bottom: -0.12em;
  vertical-align: bottom;
}

.word {
  display: inline-block;
}

@keyframes word-rise {
  from {
    translate: 0 105%;
    opacity: 0;
    font-variation-settings: 'wght' 420;
  }
  to {
    translate: 0 0;
    opacity: 1;
    font-variation-settings: 'wght' 300;
  }
}

@media (prefers-reduced-motion: no-preference) {
  .word {
    animation: word-rise 1s cubic-bezier(0.22, 0.61, 0.36, 1) both;
    animation-delay: calc(0.25s + var(--w, 0) * 0.09s);
  }
}

/* -------------------------------------------------- above-the-fold entrance */
/*
 * The hero is already on screen at load, so it uses a plain time-based
 * animation rather than a scroll timeline.
 */
@media (prefers-reduced-motion: no-preference) {
  .enter {
    animation: rise-in 1.1s cubic-bezier(0.22, 0.61, 0.36, 1) both;
    animation-delay: var(--enter-delay, 0s);
  }

  .enter-fade {
    animation: fade-in 1.4s ease both;
    animation-delay: var(--enter-delay, 0s);
  }

  .scroll-cue {
    animation: cue-bob 2.6s ease-in-out infinite;
  }
}

/* ------------------------------------------------------ scroll-driven layer */

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    /*
     * Section content rises as it enters the viewport.
     *
     * Ranges are expressed in `entry` rather than `cover` on purpose: `cover`
     * is measured against the element's own height, so tall blocks would stay
     * half-faded long after they are fully on screen.
     */
    .reveal {
      animation: rise-in linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 38%;
    }

    /*
     * Stagger for grids: set --i on each child. Offsets stay small, or the last
     * item is still fading in when it is already most of the way up the screen.
     */
    .reveal-stagger > * {
      animation: rise-in linear both;
      animation-timeline: view();
      animation-range: calc(entry 0% + (var(--i, 0) * 4%)) calc(entry 36% + (var(--i, 0) * 4%));
    }

    /*
     * Parallax. The element must have room to move — its wrapper needs
     * `overflow: clip` or extra height, otherwise it will nudge the layout.
     */
    .parallax {
      animation: drift linear both;
      animation-timeline: view();
      animation-range: cover;
    }

    /* Photo frame: the mask wipes open as the frame enters. */
    .unveil {
      animation: unveil linear both;
      animation-timeline: view();
      animation-range: entry 10% entry 80%;
    }

    /*
     * Sits on the <img> itself, because the frame already carries the mask
     * reveal and the layer between them carries the parallax — one animation
     * per element, no cascade collisions.
     *
     * Blur is the most expensive filter there is, so the range is deliberately
     * short: it resolves early and then costs nothing for the rest of the
     * scroll.
     */
    .focus-pull {
      animation: focus-pull linear both;
      animation-timeline: view();
      animation-range: entry 15% entry 65%;
    }

    /* Header thins out once the page starts moving. */
    .site-header {
      animation: header-condense linear both;
      animation-timeline: scroll(root block);
      animation-range: 0 8rem;
    }
  }
}

/* -------------------------------------------------------------- section rail */
/*
 * Marks the current section with no JavaScript and no scroll listener.
 *
 * Each section publishes a named view timeline; `timeline-scope` on <body>
 * lifts those names high enough for the fixed rail — which is not a descendant
 * of any section — to reference them. Without it a timeline is visible only
 * inside the element's own subtree, which is what normally forces an
 * IntersectionObserver here.
 */
/*
 * Opacity only. The rail blends with `difference` (see global.css), and a hue
 * under an inverting blend comes out as its opposite, so colour cannot carry
 * the active state here.
 */
@keyframes rail-active {
  0%,
  100% {
    opacity: 0.45;
  }
  1%,
  99% {
    opacity: 1;
  }
}

@keyframes rail-line-grow {
  0%,
  100% {
    width: 0.75rem;
  }
  1%,
  99% {
    width: 2rem;
  }
}

/*
 * Labels make the rail legible, and labels need room. Below this width the
 * margin beside a 72rem container cannot hold one without crowding the text, so
 * the rail stays out of the way entirely.
 *
 * The base rule has to sit before the query: both have the same specificity, so
 * source order decides, and declaring the default afterwards would win.
 */
.section-rail {
  display: none;
}

@media (width >= 87.5rem) {
  .section-rail {
    display: block;
  }

  @supports (timeline-scope: --a) and (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
      body {
        timeline-scope: --s-about, --s-selfcheck, --s-offer, --s-diagnostics, --s-process,
          --s-faq, --s-contact;
      }

      #about {
        view-timeline-name: --s-about;
      }
      #selfcheck {
        view-timeline-name: --s-selfcheck;
      }
      #offer {
        view-timeline-name: --s-offer;
      }
      #diagnostics {
        view-timeline-name: --s-diagnostics;
      }
      #process {
        view-timeline-name: --s-process;
      }
      #faq {
        view-timeline-name: --s-faq;
      }
      #contact {
        view-timeline-name: --s-contact;
      }

      /*
       * A section is current from the moment its top crosses the middle of the
       * viewport until its bottom does. Sections are contiguous, so exactly one
       * item is lit at any scroll position.
       */
      .rail-item,
      .rail-item .rail-line {
        animation: rail-active linear both;
        animation-range: entry 50% exit 50%;
      }

      .rail-item .rail-line {
        animation-name: rail-line-grow;
      }

      [data-rail='about'],
      [data-rail='about'] .rail-line {
        animation-timeline: --s-about;
      }
      [data-rail='selfcheck'],
      [data-rail='selfcheck'] .rail-line {
        animation-timeline: --s-selfcheck;
      }
      [data-rail='offer'],
      [data-rail='offer'] .rail-line {
        animation-timeline: --s-offer;
      }
      [data-rail='diagnostics'],
      [data-rail='diagnostics'] .rail-line {
        animation-timeline: --s-diagnostics;
      }
      [data-rail='process'],
      [data-rail='process'] .rail-line {
        animation-timeline: --s-process;
      }
      [data-rail='faq'],
      [data-rail='faq'] .rail-line {
        animation-timeline: --s-faq;
      }
      [data-rail='contact'],
      [data-rail='contact'] .rail-line {
        animation-timeline: --s-contact;
      }
    }
  }
}

/* ------------------------------------------------------------ FAQ accordion */
/*
 * Animating a <details> open used to need JavaScript because `height: auto` is
 * not interpolatable. `interpolate-size: allow-keywords` lifts that, so the
 * whole accordion stays declarative. Without support the panel simply snaps
 * open, which is the normal native behaviour.
 */
@supports (interpolate-size: allow-keywords) {
  @media (prefers-reduced-motion: no-preference) {
    :root {
      interpolate-size: allow-keywords;
    }

    .faq-item::details-content {
      block-size: 0;
      overflow: hidden;
      transition:
        block-size 0.4s var(--ease-soft),
        content-visibility 0.4s allow-discrete;
    }

    .faq-item[open]::details-content {
      block-size: auto;
    }
  }
}

/* -------------------------------------------------------- horizontal process */
/*
 * The stage is taller than the viewport; the pin sticks to the top for that
 * extra height, and the track slides sideways across the pinned range. All of
 * the scroll distance comes from the stage, so there is no scroll hijacking —
 * the page scrolls at its normal speed throughout.
 *
 * `contain` is the range where the stage completely covers the scrollport,
 * which is exactly the period the pin is stuck.
 *
 * `width: max-content` on the track is what makes the travel distance
 * self-measuring: it sets the element's own width to its content, so `100%`
 * inside the keyframe resolves to the real track width and the distance needs
 * no hardcoded guess about card sizes.
 */
@media (width >= 48rem) {
  .process-stage {
    height: 300svh;
    view-timeline-name: --process;
  }

  .process-pin {
    position: sticky;
    top: 0;
    display: flex;
    height: 100svh;
    flex-direction: column;
    justify-content: center;
  }

  .process-track {
    width: max-content;
    gap: 1.5rem;
    /*
     * Distance from the viewport's left edge to the container's content edge —
     * mx-auto with max-w-6xl (72rem) and px-6 (1.5rem). The track starts here,
     * not at the viewport edge, so the travel has to account for it.
     */
    --content-left: max(1.5rem, calc((100vw - 72rem) / 2 + 1.5rem));
  }

  @supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
      .process-track {
        animation: track-travel linear both;
        animation-timeline: --process;
        animation-range: contain 0% contain 100%;
      }
    }
  }
}

@keyframes track-travel {
  from {
    translate: 0;
  }
  to {
    /*
     * Exactly the track's overflow: its own width less the width visible from
     * where it starts. `min()` clamps to zero so a track narrower than the
     * viewport never slides the wrong way.
     */
    translate: min(0px, calc(100vw - var(--content-left, 1.5rem) - 100%));
  }
}

/* Below md nothing is pinned, so the cards can reveal normally. */
@media (width < 48rem) {
  @supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
      .process-step {
        animation: rise-in linear both;
        animation-timeline: view();
        animation-range: calc(entry 0% + (var(--i, 0) * 4%)) calc(entry 36% + (var(--i, 0) * 4%));
      }
    }
  }
}

/* ---------------------------------------------------------- mobile CTA bar */
/*
 * Slides up once the hero is behind you, so it never covers the first screen.
 * Hidden by default and only revealed where a scroll timeline can drive it —
 * otherwise it would sit over the hero from the moment the page loads.
 */
@keyframes cta-rise {
  from {
    translate: 0 100%;
    opacity: 0;
  }
  to {
    translate: 0 0;
    opacity: 1;
  }
}

.mobile-cta {
  display: none;
  /* Clears the iPhone home indicator. */
  padding-bottom: env(safe-area-inset-bottom);
}

@supports (animation-timeline: scroll()) {
  /*
   * The width bound is load-bearing. This rule and Tailwind's `md:hidden` have
   * the same specificity, and motion.css is imported after the utilities, so
   * without it `display: block` wins and the bar appears on desktop too.
   */
  @media (prefers-reduced-motion: no-preference) and (width < 48rem) {
    .mobile-cta {
      display: block;
      animation: cta-rise linear both;
      animation-timeline: scroll(root block);
      animation-range: 85svh 100svh;
    }
  }
}

/* --------------------------------------------------------- scroll progress */
/*
 * Hidden by default and only revealed where a scroll timeline can drive it —
 * otherwise it would render as a permanent full-width rule across the top.
 */
.scroll-progress {
  display: none;
}

@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    .scroll-progress {
      display: block;
      transform-origin: left center;
      animation: progress-grow linear both;
      animation-timeline: scroll(root block);
    }
  }
}
