/* ---------------------------------------------------------------------------
   Motion system.

   Every animation on the site is defined here and nowhere else, so the whole
   thing can be reasoned about -- and switched off -- in one place.

   Three rules it follows:

   1. Nothing is hidden unless script is running. The reveal styles are scoped to
      html.js, a class set by boot.js before the body paints. If the script fails
      to load, or is blocked, or the browser has no IntersectionObserver, the
      page renders as ordinary static HTML with everything visible. An animation
      that can hide content on failure is not an animation, it is an outage.

   2. Nothing animates that a reader asked not to. prefers-reduced-motion is
      honoured at the bottom of this file by neutralising the animations rather
      than the visibility, so a reduced-motion reader sees the same finished
      page, immediately.

   3. Only compositor properties. transform, opacity, filter and clip-path.
      Animating height or top would run layout on every frame of every card.

   The content security policy forbids third-party script, so there is no
   animation library here and cannot be: this is CSS plus roughly a hundred
   lines of vanilla JS driving an IntersectionObserver.
   --------------------------------------------------------------------------- */

/* ---- Cross-document view transitions -------------------------------------
   Chromium follows same-origin navigations with a real transition rather than a
   white flash. Browsers without it simply navigate as before -- there is no
   fallback to write, which is what makes this worth doing. */
@view-transition {
  navigation: auto;
}

::view-transition-old(root) {
  animation: vt-out var(--dur-2) var(--ease-soft) both;
}

::view-transition-new(root) {
  animation: vt-in var(--dur-3) var(--ease-out-expo) both;
}

@keyframes vt-out {
  to { opacity: 0; transform: translateY(-8px); }
}

@keyframes vt-in {
  from { opacity: 0; transform: translateY(14px); }
}

/* The header and footer are the same objects either side of a navigation, so
   they are named and held still while the page body moves under them. */
.site-header { view-transition-name: site-header; }
.site-footer { view-transition-name: site-footer; }

::view-transition-old(site-header),
::view-transition-new(site-header),
::view-transition-old(site-footer),
::view-transition-new(site-footer) {
  animation: none;
  mix-blend-mode: normal;
}

/* ---- The theme wipe -------------------------------------------------------
   motion.js swaps the theme inside a same-document view transition and the new
   painting is clipped to a circle growing from the toggle. The old painting is
   held underneath, so what the eye sees is the new theme flooding the page from
   the button that caused it. The circle centre and radius come from the button
   position, written onto the root as custom properties. */
::view-transition-group(root) { animation-duration: var(--dur-4); }

html.theme-wipe::view-transition-old(root) {
  animation: none;
  z-index: 0;
}

html.theme-wipe::view-transition-new(root) {
  animation: theme-flood var(--dur-4) var(--ease-out-expo) both;
  z-index: 1;
}

@keyframes theme-flood {
  from { clip-path: circle(0 at var(--wipe-x, 50%) var(--wipe-y, 0)); }
  to { clip-path: circle(var(--wipe-r, 150vmax) at var(--wipe-x, 50%) var(--wipe-y, 0)); }
}

/* ---- Reveal on scroll -----------------------------------------------------
   One attribute, a few named variants. --d is a per-element delay written by
   motion.js from the element index within its group, which is what turns a grid
   of twelve cards into a wave instead of twelve simultaneous fades. */
html.js [data-reveal] {
  opacity: 0;
  transition:
    opacity var(--dur-4) var(--ease-out-expo) var(--d, 0ms),
    transform var(--dur-4) var(--ease-out-expo) var(--d, 0ms),
    filter var(--dur-4) var(--ease-out-expo) var(--d, 0ms),
    clip-path var(--dur-5) var(--ease-out-expo) var(--d, 0ms);
  will-change: opacity, transform;
}

html.js [data-reveal="up"] { transform: translate3d(0, 34px, 0); }
html.js [data-reveal="rise"] { transform: translate3d(0, 90px, 0); }
html.js [data-reveal="scale"] { transform: scale(.94); }
html.js [data-reveal="blur"] { filter: blur(14px); transform: translate3d(0, 20px, 0); }
/* A photograph uncovering itself reads better than one fading up. */
html.js [data-reveal="mask"] { clip-path: inset(0 0 100% 0); transform: scale(1.06); }
html.js [data-reveal="fade"] { transform: none; }

html.js [data-reveal].is-in {
  opacity: 1;
  transform: none;
  filter: none;
  clip-path: inset(0 0 0 0);
  will-change: auto;
}

/* ---- Entrance keyframes used by components ---- */
@keyframes rise-in {
  from { opacity: 0; transform: translate3d(0, 120%, 0) rotate(2deg); }
  to { opacity: 1; transform: none; }
}

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

@keyframes fade-up {
  from { opacity: 0; transform: translate3d(0, 18px, 0); }
  to { opacity: 1; transform: none; }
}

@keyframes pop-in {
  0% { opacity: 0; transform: scale(.6); }
  100% { opacity: 1; transform: scale(1); }
}

/* The hero mesh. Two blobs drifting on different periods so the ground never
   repeats visibly -- a single looping gradient reads as a screensaver. */
@keyframes aurora-a {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  33% { transform: translate3d(6%, -8%, 0) scale(1.15); }
  66% { transform: translate3d(-5%, 6%, 0) scale(.92); }
}

@keyframes aurora-b {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1.05); }
  40% { transform: translate3d(-8%, 5%, 0) scale(.9); }
  75% { transform: translate3d(7%, 7%, 0) scale(1.2); }
}

@keyframes float-soft {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-10px); }
}

@keyframes marquee-track {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(-50%, 0, 0); }
}

@keyframes sheen-sweep {
  from { transform: translateX(-120%) skewX(-18deg); }
  to { transform: translateX(320%) skewX(-18deg); }
}

@keyframes scroll-cue {
  0% { transform: translateY(0); opacity: 0; }
  35% { opacity: 1; }
  100% { transform: translateY(14px); opacity: 0; }
}

@keyframes shimmer {
  from { background-position: -180% 0; }
  to { background-position: 180% 0; }
}

@keyframes spin-slow {
  to { transform: rotate(360deg); }
}

/* ---- Page-load choreography ----------------------------------------------
   The header and the hero are above the fold, so they cannot wait for a scroll
   observer to notice them. They animate on load instead, on a short ladder. */
html.js .site-header { animation: fade-in var(--dur-3) var(--ease-soft) both; }

/* The word is the clipping box; the span inside it is what moves. If the moving
   element were also the clipping one it would clip itself and nothing would show. */
html.js .hero-word > span {
  display: inline-block;
  animation: rise-in var(--dur-5) var(--ease-out-expo) both;
  animation-delay: calc(120ms + var(--i, 0) * 70ms);
}

html.js .hero-lede { animation: fade-up var(--dur-4) var(--ease-out-expo) 420ms both; }
html.js .hero-actions { animation: fade-up var(--dur-4) var(--ease-out-expo) 560ms both; }
html.js .hero-panel { animation: fade-up var(--dur-4) var(--ease-out-expo) 660ms both; }
html.js .hero-eyebrow { animation: fade-up var(--dur-3) var(--ease-out-expo) 60ms both; }

/* ---- Scroll progress ------------------------------------------------------
   Where scroll-driven animation is supported this costs no script at all: the
   bar is scaled by the document scroll timeline on the compositor. Elsewhere it
   simply stays at zero width and is invisible, which is an acceptable nothing. */
.scroll-progress {
  position: fixed;
  z-index: 200;
  inset-block-start: 0;
  inset-inline: 0;
  height: 3px;
  transform: scaleX(0);
  transform-origin: 0 50%;
  background: linear-gradient(90deg, var(--accent), var(--ember-300));
  pointer-events: none;
}

[dir="rtl"] .scroll-progress { transform-origin: 100% 50%; }

@supports (animation-timeline: scroll()) {
  .scroll-progress {
    animation: progress-grow linear;
    animation-timeline: scroll(root block);
  }

  @keyframes progress-grow {
    to { transform: scaleX(1); }
  }
}

/* Parallax without a scroll handler, again only where the browser can do it on
   the compositor. Nothing depends on it visually. */
@supports (animation-timeline: view()) {
  html.js .parallax-slow {
    animation: parallax-drift linear both;
    animation-timeline: view();
    animation-range: cover 0% cover 100%;
  }

  @keyframes parallax-drift {
    from { transform: translate3d(0, -6%, 0); }
    to { transform: translate3d(0, 6%, 0); }
  }
}

/* ---- Reduced motion -------------------------------------------------------
   The finished page, instantly. Visibility is restored rather than left to a
   transition that has been shortened to nothing, and the decorative loops are
   stopped outright -- a drifting gradient at 0.01ms is still a drifting
   gradient once it has been through one frame. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  html.js [data-reveal],
  html.js [data-reveal].is-in {
    opacity: 1;
    transform: none;
    filter: none;
    clip-path: none;
    transition: none;
  }

  html.js .site-header,
  html.js .hero-word > span,
  html.js .hero-lede,
  html.js .hero-actions,
  html.js .hero-panel,
  html.js .hero-eyebrow {
    animation: none;
    opacity: 1;
    transform: none;
  }

  .aurora,
  .marquee-track,
  .scroll-cue,
  .scroll-progress,
  .parallax-slow {
    animation: none !important;
  }

  *, *::before, *::after {
    transition-duration: .01ms !important;
    animation-iteration-count: 1 !important;
  }

  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}
