/**
 * DONAT — motion.
 *
 * The smallest layer in the application and the last one base.css declares, so a rule here damps
 * anything above it without `!important`. It owns every `@keyframes` in the app, which is why they
 * are named for what they do rather than for the component that uses them: four of them cover the
 * whole site.
 *
 * ## One moment, not scattered effects
 *
 * A public page has exactly one page-load sequence — the hero copy rises in a short stagger and
 * the footage behind it washes in. Everything below the fold waits for its
 * own scroll reveal, and a reveal is a 12px rise and a fade: nothing moves sideways, nothing
 * scales, nothing bounces. Only `transform` and `opacity` animate anywhere in this file, never
 * `top`, `left`, `width` or `height`, so no frame of any of it can cause a layout.
 *
 * ## What this layer must not do
 *
 * Draw. No colour, no padding and no layout lives here; a state class that changes how something
 * *looks* belongs to the layer that owns the component. `.is-paused` on the hero's toggle is
 * the example — which glyph it shows is site.css's decision, and all this file adds is that the
 * change is not a hard cut.
 */

@layer motion {

  /* ==========================================================================================
     Four curves
     ========================================================================================== */

  /* One rise for the whole application. The travel is a custom property rather than a second
     keyframe, because a hero line and a toast differ by 6px and nothing else. */
  @keyframes rise {
    from { opacity: 0; transform: translateY(var(--rise-from, 12px)); }
    to { opacity: 1; transform: none; }
  }

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

  /* The signature. Scaling the element scales its radial-gradient mask with it, so the ring opens
     rather than the footage zooming. */
  @keyframes stage-in {
    from { opacity: 0; }
    to { opacity: 1; }
  }

  /* Waiting, expressed as breathing. Opacity only: the colour of a skeleton bar belongs to
     ui.css, and a gradient sweep would mean owning it from here. */
  @keyframes soft-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.45; }
  }

  /* ==========================================================================================
     Page load
     ========================================================================================== */

  /**
   * The hero stagger is positional rather than named: it animates the direct children of
   * `.hero__copy` in document order — eyebrow, title, expansion, tagline, actions, facts — so
   * reordering a line in the markup needs no rule here. 60ms apart is enough to read as a
   * sequence and short enough to be over before anyone has reached for the scroll wheel.
   */
  .hero__copy > * {
    --rise-from: 14px;
    animation: rise var(--dur-4) var(--ease-out) calc(var(--hero-step, 0) * 60ms) both;
  }

  /* A table, not five rules: the only thing that varies is the step. */
  .hero__copy > :nth-child(2) { --hero-step: 1; }
  .hero__copy > :nth-child(3) { --hero-step: 2; }
  .hero__copy > :nth-child(4) { --hero-step: 3; }
  .hero__copy > :nth-child(5) { --hero-step: 4; }
  .hero__copy > :nth-child(6) { --hero-step: 5; }

  /**
   * The footage washes in behind the copy rather than under it.
   *
   * Opacity only, and no transform: the stage is a full-bleed video and scaling one costs a
   * composite of the whole band on every frame of the entrance for an effect nobody would name.
   * This replaced a scale on the old circular aperture, where the movement was the point.
   */
  .hero__stage {
    animation: stage-in var(--dur-4) var(--ease-out) 80ms both;
  }

  /* The toggle is unhidden mid-load, once main.js knows the footage will actually play. It fades
     in so its arrival in the corner of the hero does not read as a layout glitch. */
  .hero__toggle:not([hidden]) {
    animation: wash-in var(--dur-2) var(--ease) both;
  }

  .hero__toggle > svg {
    transition: opacity var(--dur-1) var(--ease);
  }

  /* ==========================================================================================
     The rotating word
     ========================================================================================== */

  /* main.js replaces the text at the far end of the fade — its `SWAP_MS` is this duration — so
     the outgoing and incoming words are never both legible. It toggles one class and never
     assigns `className`, which would drop `.rotor` and with it the inline-block that lets a
     transform move anything at all. */
  .rotor {
    transition:
      opacity var(--dur-2) var(--ease),
      transform var(--dur-2) var(--ease);
  }

  .rotor.is-swapping {
    opacity: 0;
    transform: translateY(-0.22em);
  }

  /* ==========================================================================================
     Scroll reveals
     ========================================================================================== */

  /**
   * The contract with main.js, exactly: on a page carrying `reveal-on-scroll` it adds
   * `.alive-reveal` and an `--alive-delay` to every target, adds `.is-visible` when the target
   * intersects, and removes both on `animationend`. That last step is why the entrance is a
   * keyframe and not a transition — a filled animation left in place keeps pinning `transform`,
   * and a card still being animated cannot be hovered.
   *
   * The gate is a class on <body>, so a page whose script never ran hides nothing.
   */
  .reveal-on-scroll .alive-reveal {
    opacity: 0;
  }

  .reveal-on-scroll .alive-reveal.is-visible {
    animation: rise var(--dur-3) var(--ease-out) var(--alive-delay, 0ms) both;
  }

  /* ==========================================================================================
     Overlays
     ========================================================================================== */

  /* The sheet slides; site.css owns where from. A transition rather than a keyframe because it has
     to reverse mid-flight when somebody taps the button twice. */
  .sheet {
    transition: transform var(--dur-3) var(--ease-out);
  }

  /* The scrim and the dialog both come out of `hidden` / `display: none`, where a transition has no
     first frame to run from, so their entrance is a keyframe. Neither gets an exit: main.js
     restores `hidden` immediately, and fading out something that has already stopped taking clicks
     would be a lie about where the page is. */
  .sheet-scrim.is-open,
  .pdf-modal.open {
    animation: wash-in var(--dur-2) var(--ease) both;
  }

  .pdf-modal.open .pdf-modal__panel {
    --rise-from: 10px;
    animation: rise var(--dur-3) var(--ease-out) both;
  }

  /* ==========================================================================================
     Waiting, and the things that announce themselves
     ========================================================================================== */

  .skeleton .skeleton-bar {
    animation: soft-pulse 1.4s var(--ease) infinite;
  }

  /* The dot is what says the page is *still* offline rather than that it was, once. */
  .offline-banner__dot {
    animation: soft-pulse 1.8s var(--ease) infinite;
  }

  /* Both of these arrive unasked, in a corner, over something somebody is already reading, so both
     rise into place instead of appearing. pwa.js and dashboard.js remove them outright when they
     are done, so neither has an exit to animate. The status region's own element carries the
     centring transform, which is why this sits on the text inside it. */
  .sw-toast {
    --rise-from: 16px;
    animation: rise var(--dur-3) var(--ease-out) both;
  }

  .status-region__text {
    --rise-from: 8px;
    animation: rise var(--dur-2) var(--ease-out) both;
  }

  /* ==========================================================================================
     Reduced motion — last, and total
     ========================================================================================== */

  /**
   * Everything above is an entrance or a state change, and none of it carries information the
   * arrival of the content does not. Under `reduce` it therefore resolves in a single frame rather
   * than being retuned.
   *
   * The delays matter as much as the durations: `both` fill holds a target at its `from` state
   * while its delay counts down, so a stagger that survived this block would leave text invisible
   * for a third of a second on the one setting that asked for less of that.
   */
  @media (prefers-reduced-motion: reduce) {
    html {
      scroll-behavior: auto;
    }

    *,
    *::before,
    *::after {
      animation-delay: 0s !important;
      animation-duration: 1ms !important;
      animation-iteration-count: 1 !important;
      scroll-behavior: auto !important;
      transition-delay: 0s !important;
      transition-duration: 1ms !important;
    }

    /* Belt and braces. main.js does not run the reveal module under `reduce`, but a target whose
       observer never fires — or a page restored from a cache written before it — must not be able
       to sit at `opacity: 0` for the rest of the visit. */
    .reveal-on-scroll .alive-reveal,
    .reveal-on-scroll .alive-reveal.is-visible {
      opacity: 1;
      transform: none;
    }
  }
}
