/* ==========================================================================
   Motion — scroll-driven animation

   Content reacts to scrolling; it never travels in from off-canvas. Every
   effect is tied to scroll POSITION via CSS scroll-driven timelines rather
   than fired on a clock, so the page responds to the reader rather than
   performing at them.

   Three rules govern this file:

   1. Motion is opt-in. Everything lives inside
      `@media (prefers-reduced-motion: no-preference)`, so the reduced-motion
      experience is the default code path, not a set of overrides bolted on
      afterwards.

   2. Resting state is the correct state. No element's visibility depends on
      an animation running. Where scroll timelines are unsupported the page
      renders complete and static — never blank.

   3. Only `transform` and `opacity` animate. Nothing here triggers layout.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Parallax scaffolding

   Applies regardless of motion preference: the oversized, absolutely
   positioned layer is what gives the effect room to move without ever
   exposing a frame edge. With motion off it simply sits still, centred.

   Layer height is 120% of the frame, offset by -10%, leaving a 10% margin
   above and below. The animation travels ±8% of the LAYER (9.6% of the
   frame), staying inside that margin.
   -------------------------------------------------------------------------- */
.frame.frame--parallax > img,
.frame.frame--parallax > video,
.frame.frame--parallax > picture > img {
  position: absolute;
  inset: -10% 0;
  width: 100%;
  height: 120%;
}

@keyframes ep-parallax {
  from {
    transform: translate3d(0, calc(var(--parallax-shift) * -1), 0);
  }
  to {
    transform: translate3d(0, var(--parallax-shift), 0);
  }
}

/* Ken Burns. Driven by the view timeline so it advances as the reader
   scrolls past, rather than looping on a timer. */
@keyframes ep-kenburns {
  from {
    transform: scale(1);
  }
  to {
    transform: scale(var(--kenburns-scale));
  }
}

/* The ambient variant. A hero that has not been scrolled yet would
   otherwise be completely still — this keeps the water alive. Slow enough
   (40s) to be felt rather than seen. */
@keyframes ep-kenburns-ambient {
  0% {
    transform: scale(1) translate3d(0, 0, 0);
  }
  50% {
    transform: scale(var(--kenburns-scale)) translate3d(-1.2%, -0.8%, 0);
  }
  100% {
    transform: scale(1) translate3d(0, 0, 0);
  }
}

/* Reveal. Opacity plus a 12px rise — deliberately tiny. */
@keyframes ep-reveal {
  from {
    opacity: 0;
    transform: translate3d(0, var(--reveal-rise), 0);
  }
  to {
    opacity: 1;
    transform: translate3d(0, 0, 0);
  }
}

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

/* The hero panel image drifting against the video behind it.

   The video travels at --parallax-shift over the same range, so the two
   planes separate — that difference is the whole effect. Scale only, plus a
   little rise: the panel clips, so the image must never expose an edge. */
@keyframes ep-hero-inset {
  from {
    transform: translate3d(0, 0, 0) scale(1);
  }
  to {
    transform: translate3d(0, -6%, 0) scale(1.04);
  }
}

/* Registered so it interpolates as a colour. Unregistered custom properties
   flip discretely halfway through an animation instead of blending. */
@property --nav-hover {
  syntax: "<color>";
  inherits: true;
  initial-value: #3185fc;
}

@property --nav-line {
  syntax: "<color>";
  inherits: true;
  initial-value: #25e4e1;
}

/* The header ground fading in as the reader leaves the hero. The nav
   rollover rides along: white on the glass, Azure once it is navy. */
@keyframes ep-header-ground {
  from {
    background-color: transparent;
    border-block-end-color: transparent;
    --nav-hover: var(--brand-ghost);
    --nav-line: var(--brand-ghost);
  }
  to {
    background-color: var(--brand-navy);
    border-block-end-color: var(--line-accent);
    --nav-hover: var(--brand-azure);
    --nav-line: var(--brand-ice);
  }
}

@media (prefers-reduced-motion: no-preference) {
  /* ------------------------------------------------------------------------
     Ambient hero drift. Time-based, and the only motion here that is not
     scroll-driven — a hero must not be dead before the first scroll.
     ------------------------------------------------------------------------ */
  .frame--ambient > img,
  .frame--ambient > picture > img {
    animation: ep-kenburns-ambient var(--dur-ambient) var(--ease-in-out) infinite;
    will-change: transform;
  }

  @supports (animation-timeline: view()) {
    /* ----------------------------------------------------------------------
       Parallax. `cover` spans the whole time the frame is in the viewport,
       so the image travels its full range across one pass.
       ---------------------------------------------------------------------- */
    .frame.frame--parallax > img,
    .frame.frame--parallax > video,
    .frame.frame--parallax > picture > img {
      animation: ep-parallax linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
      will-change: transform;
    }

    .frame--parallax-subtle {
      --parallax-shift: var(--parallax-shift-subtle);
    }

    /* The hero is already at the top of the scrollport on load, so it uses
       the document timeline instead — it should drift as the reader leaves,
       not wait to enter. */
    .frame--hero.frame--parallax > img,
    .frame--hero.frame--parallax > video,
    .frame--hero.frame--parallax > picture > img {
      animation: ep-parallax linear both;
      animation-timeline: scroll();
      animation-range: 0 100svh;
    }

    /* ----------------------------------------------------------------------
       Ken Burns on scroll. Overrides the ambient loop where both are
       present, so a hero drifts gently until the reader takes over.
       ---------------------------------------------------------------------- */
    .frame--kenburns > img,
    .frame--kenburns > picture > img {
      animation: ep-kenburns linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
      will-change: transform;
    }

    /* ----------------------------------------------------------------------
       Reveal.

       Staggering uses --i to shift each sibling's animation-range rather
       than applying a delay: on a scroll timeline a delay is meaningless,
       but offsetting the range means each item resolves a little further
       down the scroll. The effect reads as a cascade.
       ---------------------------------------------------------------------- */
    .reveal {
      animation: ep-reveal linear both;
      animation-timeline: view();
      animation-range: entry calc(8% + (var(--i, 0) * 5%)) entry
        calc(52% + (var(--i, 0) * 5%));
    }

    .reveal--fade {
      animation-name: ep-fade;
    }

    /* Content laid over a hero should settle immediately on load, not wait
       for a scroll that may never come. */
    .reveal--onload {
      animation: ep-reveal var(--dur-slow) var(--ease-out) both;
      animation-timeline: auto;
      animation-delay: calc(120ms + (var(--i, 0) * var(--reveal-stagger)));
    }

    /* The drift goes on the media layer, not the panel: the panel carries
       .reveal--onload, and two `animation` shorthands on one element means
       the later one silently wins. Splitting them lets the panel fade in on
       load AND its image drift on scroll. */
    .hero__panel-media {
      animation: ep-hero-inset linear both;
      animation-timeline: scroll();
      animation-range: 0 100svh;
      will-change: transform;
    }

    /* ----------------------------------------------------------------------
       Header ground
       ---------------------------------------------------------------------- */
    .site-header--adaptive {
      animation: ep-header-ground linear both;
      animation-timeline: scroll();
      animation-range: 0 60svh;
    }
  }

  /* ------------------------------------------------------------------------
     Fallback for browsers without scroll-driven timelines. js/motion.js
     adds .js-motion to <html> only when it has confirmed support is missing
     AND it is going to observe — so these rules can safely start from
     opacity 0 without risking permanently invisible content.
     ------------------------------------------------------------------------ */
  .js-motion .reveal:not(.is-visible) {
    opacity: 0;
    transform: translate3d(0, var(--reveal-rise), 0);
  }

  .js-motion .reveal.is-visible {
    opacity: 1;
    transform: none;
    transition: opacity var(--dur-slow) var(--ease-out),
      transform var(--dur-slow) var(--ease-out);
    transition-delay: calc(var(--i, 0) * var(--reveal-stagger));
  }

  /* ------------------------------------------------------------------------
     Micro-interactions
     ------------------------------------------------------------------------ */
  .project-card__frame > img,
  .project-card__frame > picture > img {
    transition: transform var(--dur-slow) var(--ease-out);
  }

  .project-card:hover .project-card__frame > img,
  .project-card:hover .project-card__frame > picture > img,
  .project-card:focus-within .project-card__frame > img,
  .project-card:focus-within .project-card__frame > picture > img {
    transform: scale(var(--kenburns-scale));
  }
}

/* --------------------------------------------------------------------------
   Reduced motion

   Belt and braces. Nothing above should reach here, but a stray transition
   on a component is stopped dead.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
