/* ==========================================================================
   Laurels — motion.css
   The motion layer. site.css owns the resting state and never moves; this
   file only adds arrival. Delete it (and site.js) and the page is unchanged.

   Character is Weight. design/tokens.json motion.character, SETTLED
   2026-08-15: "Weight — objects have mass. [...] Things start, decelerate
   over a real distance, and settle." So: long ease-out curves, real travel,
   no bounce, no overshoot, no loop, nothing that celebrates
   (motion.rules.never-bounce, motion.rules.motion-never-celebrates).

   Three mechanisms, in the order the reader meets them:

     1. Load     — plain CSS keyframes on the hero. No JS, so no flash of
                   unstyled or unhidden content, and it always completes.
     2. Scroll   — the deck resolve, driven by view() so the reader is the
                   timeline. Behind @supports; without it the deck renders
                   in site.css's resting fan, which is correct.
     3. Entrance — everything below the fold, armed by site.js. The pre-state
                   lives behind [data-mo], an attribute only JS ever writes,
                   and JS removes it again once the element has settled.

   Every pre-state is expressed as a from-only @keyframes. CSS then uses the
   element's own cascaded value as the implicit 100% keyframe, so site.css
   stays the single source of truth for every resting value — including the
   deck's per-breakpoint fan and the .page stack's per-layer opacities. This
   file never restates a resting value it did not invent.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Registration. Custom properties are token streams to the browser until
   they are typed; untyped, they snap between keyframes instead of
   interpolating. These five are the handoff site.css documents at its
   "The deck" section — the motion pass drives them, not the transform.
   -------------------------------------------------------------------------- */

@property --card-x {
  syntax: '<length>';
  inherits: false;
  initial-value: 0px;
}

@property --card-y {
  syntax: '<length>';
  inherits: false;
  initial-value: 0px;
}

@property --card-rot {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

@property --card-scale {
  syntax: '<number>';
  inherits: false;
  initial-value: 1;
}

@property --card-opacity {
  syntax: '<number>';
  inherits: false;
  initial-value: 1;
}

/* --------------------------------------------------------------------------
   Local values. Only what the token layer cannot carry, each stating why.
   -------------------------------------------------------------------------- */

:root {
  /* tokens.json motion.travel is 16pt — "Standard entrance distance.
     Elements move this far and no further" — measured on a 390pt phone. At 1440px that
     is roughly one line of body text and reads as a twitch, not as mass. So
     travel is scaled with the viewport while the token stays the floor and
     the whole range stays a multiple of it: 16px at phone width, ~31px at
     1440, hard-capped at 2.5x. Nothing here invents a new base distance. */
  --travel-in: clamp(
    var(--travel),
    calc(var(--travel) * 0.5 + 1.6vw),
    calc(var(--travel) * 2.5)
  );

  /* The statement headline is set at --step-figure, up to 144px. An entrance
     distance proportionate to body copy disappears under it. */
  --travel-lg: calc(var(--travel-in) * 1.75);
}

/* --------------------------------------------------------------------------
   Keyframes. All from-only: the implicit 100% is the element's own resting
   value, so opacity returns to 0.62 on .page-3 and to 0.07 on .band-mark
   without this file knowing either number.
   -------------------------------------------------------------------------- */

@keyframes mo-fade {
  from { opacity: 0; }
}

/* translate/scale, not transform: they compose in front of transform, so an
   element that already carries a resting transform (.page, .quest-card,
   .band-mark) keeps it untouched. */
@keyframes mo-rise {
  from { opacity: 0; translate: 0 var(--travel-in); }
}

@keyframes mo-rise-sm {
  from { opacity: 0; translate: 0 calc(var(--travel-in) * 0.55); }
}

@keyframes mo-rise-lg {
  from { opacity: 0; translate: 0 var(--travel-lg); }
}

@keyframes mo-settle {
  from {
    opacity: 0;
    translate: 0 calc(var(--travel-in) * 0.5);
    scale: 0.94;
  }
}

@keyframes mo-draw {
  from { transform: scaleX(0); }
}

/* The mark is at building scale; it arrives by shedding size, not distance. */
@keyframes mo-mark {
  from { opacity: 0; scale: 1.06; }
}

/* The committed day. A state change, not a celebration — it grows into
   place on the standard curve and stops there. */
@keyframes mo-day {
  from { opacity: 0; scale: 0.6; }
}

@keyframes mo-answer {
  from { opacity: 0; translate: 0 calc(var(--travel-in) * -0.35); }
}

/* --- the deck resolve ------------------------------------------------------
   Gathered: three cards nearly coincident and nearly parallel, the front one
   sitting back and small. Resting (site.css): the front squared forward at
   full size, the other two fanned up and to the right, receding. Offsets
   come off the 4pt scale; the angles and scales are of a kind with the ones
   site.css already writes literally at .deck-front / .deck-mid / .deck-back.
   -------------------------------------------------------------------------- */

@keyframes deck-front-in {
  from {
    --card-x: var(--space-3);
    --card-y: var(--space-2);
    --card-rot: 1.6deg;
    --card-scale: 0.94;
  }
}

@keyframes deck-mid-in {
  from {
    --card-x: var(--space-2);
    --card-y: var(--space-1);
    --card-rot: 1.15deg;
    --card-scale: 0.955;
    --card-opacity: 0.95;
  }
}

@keyframes deck-back-in {
  from {
    --card-x: var(--space-1);
    --card-y: 0px;
    --card-rot: 0.7deg;
    --card-scale: 0.97;
    --card-opacity: 0.86;
  }
}

/* ==========================================================================
   Everything that travels or is staged lives in here. Under
   prefers-reduced-motion this block simply does not apply, and the floor at
   the bottom of the file takes over. Nothing needs to be undone.
   ========================================================================== */

@media (prefers-reduced-motion: no-preference) {

  /* ------------------------------------------------------------------------
     1 · Load — the hero, and only the hero.
     Pure CSS, so the pre-state is a keyframe rather than a declaration and
     can never survive its animation. One group, so --stagger-tight.
     ------------------------------------------------------------------------ */

  .hero-copy h1,
  .hero-copy .lead,
  .hero-action,
  .hero-copy .micro {
    animation-duration: var(--dur-slow);
    animation-timing-function: var(--ease-staged);
    /* backwards, not both: the from-state holds through the delay, and the
       moment the animation ends it stops applying altogether. Nothing this
       file wrote is left on the element afterwards. */
    animation-fill-mode: backwards;
    animation-delay: calc(var(--i, 0) * var(--stagger-tight));
  }

  .hero-copy h1    { animation-name: mo-rise; --i: 0; }
  .hero-copy .lead { animation-name: mo-rise; --i: 1; }
  .hero-action     { animation-name: mo-rise; --i: 2; }
  .hero-copy .micro{ animation-name: mo-rise; --i: 3; }

  /* ------------------------------------------------------------------------
     2 · Scroll — the deck resolves under the reader's own scroll.

     This used to run on scroll(root) starting at zero, because the deck was
     the right-hand column of the hero and therefore always in the first
     viewport: every view() range would already have been passed at load, and
     the deck would have rendered resolved before the reader had done
     anything. That premise died when the hero became a full-bleed photograph
     and the deck moved into its own section below it. The deck now arrives
     from below the fold, which is exactly what view() describes, so view() is
     what it hangs off. Nothing else changed: same keyframes, same curves, and
     the same 0.72 / 0.86 / 1.0 ratios between the three cards.

     The ranges end in `cover`, not `entry`. An `entry` range is only as long
     as the element is tall — 267px for a card — and the resolve would be over
     before the deck had properly arrived. `cover` spans the viewport plus the
     element, so at a 900px window the front card completes over roughly 400px
     of scroll, which is what the old --deck-run gave it. The start stays in
     `entry` so nothing moves until the card's top edge is actually on screen.

     Depth comes from response, not distance. The front card completes over
     the shortest run and on the tighter curve, so it leads; the back card is
     still travelling when the front has settled. That lag is the mass.
     ------------------------------------------------------------------------ */

  @supports (animation-timeline: view()) {

    .deck-front,
    .deck-mid,
    .deck-back {
      animation-duration: auto;
      animation-fill-mode: both;
      animation-timeline: view(block);
      animation-range-start: entry 4%;
      /* Three elements animating for as long as the deck is on screen, and a
         custom-property animation cannot be composited on its own. This is
         where a layer hint is actually earned. */
      will-change: transform;
    }

    .deck-front {
      animation-name: deck-front-in;
      animation-timing-function: var(--ease-staged);
      animation-range-end: cover 34%;
    }

    .deck-mid {
      animation-name: deck-mid-in;
      animation-timing-function: var(--ease-standard);
      animation-range-end: cover 41%;
    }

    .deck-back {
      animation-name: deck-back-in;
      animation-timing-function: var(--ease-standard);
      animation-range-end: cover 47%;
    }
  }

  /* ------------------------------------------------------------------------
     3 · Entrances.

     [data-mo] is written only by site.js, and only on elements that were
     still below the fold when it ran — so nothing the reader can already see
     is ever hidden, and there is no flash. site.js removes the attribute
     again once the animation has finished, taking will-change with it.
     ------------------------------------------------------------------------ */

  [data-mo] {
    animation-duration: var(--dur-slow);
    animation-timing-function: var(--ease-standard);
    animation-fill-mode: backwards;
    animation-play-state: paused;
    animation-delay: calc((var(--i, 0) + var(--i-base, 0)) * var(--stagger-tight));
  }

  [data-mo].is-in { animation-play-state: running; }

  /* Only the large movers. The rest animate opacity and translate on small
     boxes, which every engine promotes on its own. */
  .row-media[data-mo],
  .figure-line[data-mo],
  .cell-entry[data-mo],
  .cell-dark[data-mo],
  .close-inner > [data-mo] {
    will-change: opacity, translate;
  }

  /* The drawn editorial rule lived here until 2026-08-21. It held a block's
     border-top colour back while armed, wiped an identical hairline across
     it, then handed back to the real border. Every element it ran on has
     since lost its rule: the section title and .section-head when headings
     were centred, .row when the hairline under a centred heading turned out
     to be underlining the heading rather than the row, and .col with the
     section it belonged to. The mo-draw keyframe stays — the statement's
     warm bar still uses it. */

  /* --- section openings --------------------------------------------------- */

  .shell > .section-title[data-mo],
  .section-head[data-mo] { animation-name: mo-rise; }

  /* --- 2 · the feature rows ----------------------------------------------
     The row itself only draws its rule; copy and diagram carry the movement,
     the diagram trailing the copy by one beat so the pair reads left to
     right rather than as one slab. */

  .row-copy[data-mo]  { animation-name: mo-rise; }
  .row-media[data-mo] { animation-name: mo-rise; --i: 1; }

  /* The two options are equal by design: identical treatment, one beat
     apart, no hierarchy implied. */
  .opt[data-mo] {
    animation-name: mo-settle;
    animation-duration: var(--dur-mid);
    --i-base: 2;
  }

  /* One day is named. It arrives after the grid it sits in. */
  .cal-on[data-mo] {
    animation-name: mo-day;
    animation-duration: var(--dur-mid);
    --i-base: 3;
  }

  /* Back of the stack first, so the chronicle assembles under the newest
     page rather than on top of it. */
  .page[data-mo] {
    animation-name: mo-rise-sm;
    animation-duration: var(--dur-mid);
    --i-base: 2;
  }

  /* --- 3 · the statement --------------------------------------------------
     The heaviest object on the page, so the longest travel, on the staged
     curve, with its warm bar drawn last. */

  .figure-line[data-mo] {
    animation-name: mo-rise-lg;
    animation-timing-function: var(--ease-staged);
  }

  .figure-line[data-mo]::before {
    transform-origin: 0 50%;
    animation: mo-draw var(--dur-slow) var(--ease-standard) backwards paused;
    animation-delay: var(--stagger-staged);
  }

  .figure-line[data-mo].is-in::before { animation-play-state: running; }

  .statement-note[data-mo] { animation-name: mo-rise; --i: 1; }

  /* --- 4 · the domains ---------------------------------------------------- */

  .chip[data-mo] {
    animation-name: mo-settle;
    animation-duration: var(--dur-mid);
  }

  .cell-entry[data-mo] { animation-name: mo-rise; }

  .cell-dark[data-mo] { animation-name: mo-rise; }

  /* --- 7 · the closing band ----------------------------------------------- */

  .close-inner > [data-mo] { animation-name: mo-rise; }

  /* Both are the mark's treatment at building scale, so both arrive the same
     way: by shedding size rather than travelling. */
  .band-mark-close[data-mo],
  .foot-signature[data-mo] {
    animation-name: mo-mark;
    animation-timing-function: var(--ease-staged);
  }

  /* ------------------------------------------------------------------------
     Interaction. Feedback only — nothing here draws attention to itself.
     ------------------------------------------------------------------------ */

  /* site.css transitions colour and shadow on the button; scale is added to
     the same list rather than replacing it, so the press has weight without
     the resting or hover state changing at all. */
  .btn {
    transition: background-color var(--dur-fast) var(--ease-standard),
                box-shadow var(--dur-fast) var(--ease-standard),
                scale var(--dur-fast) var(--ease-standard);
  }

  .btn:active { scale: 0.985; }

  /* The answer has mass: it settles down into the space the summary opened
     rather than appearing in it. The box itself still resizes instantly —
     animating height is out of bounds, and half of it would read worse than
     none of it. */
  .faq-item[open] .faq-a {
    animation: mo-answer var(--dur-mid) var(--ease-standard) backwards;
  }

  /* + to −. site.css runs this at --dur-fast, which reads as a snap on a
     control this size. Opening takes --dur-mid; closing takes 75% of it, the
     standing rule for exits. */
  .faq-mark::before,
  .faq-mark::after {
    transition-duration: calc(var(--dur-mid) * 0.75);
    transition-timing-function: var(--ease-standard);
  }

  .faq-item[open] .faq-mark::before,
  .faq-item[open] .faq-mark::after {
    transition-duration: var(--dur-mid);
  }
}

/* ==========================================================================
   The floor.

   design/tokens.json motion.rules.reduce-motion: "Reduce Motion collapses
   every entrance to a cross-fade in place at duration.fast. Nothing travels,
   nothing is staged [...] This is an accessibility floor, not an option."
   That is exactly this block and nothing more — no travel, no stagger, no
   scroll timeline, no drawn rules.

   site.css already disables all animation under this query with a `*`
   selector and !important. These selectors are more specific, so the
   cross-fade it specifies survives; everything else stays killed.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {

  [data-mo] {
    animation: mo-fade var(--dur-fast) var(--ease-standard) both paused !important;
  }

  [data-mo].is-in { animation-play-state: running !important; }

  .hero-copy h1,
  .hero-copy .lead,
  .hero-action,
  .hero-copy .micro {
    animation: mo-fade var(--dur-fast) var(--ease-standard) both !important;
  }
}
