/* ==========================================================================
   motion-core.css: static layouts and CSS cores for motion-core.js
   --------------------------------------------------------------------------
   What it does
     The resting state for every data-* recipe (what no-JS, reduced-motion and
     CDN-failure visitors see), the CSS halves of the marquee and sticky stack,
     and Lenis's recommended CSS. Load it after tokens.css.
   Options (custom properties)
     --marquee-gap (2rem)   gap between marquee items and at the loop seam
     --pin-gap (4vw), --pin-pad (4vw), --pin-panel-w (min(72vw, 56rem))
     --stack-top (10svh), --stack-step (1rem), --stack-gap (8svh), --stack-shade-color (#000)
   Fallback behaviour
     Nothing here starts hidden. Motion rules apply only when all three hold:
     @media (prefers-reduced-motion: no-preference), :root[data-motion="full"] and
     a class that motion-core.js adds once it has wired the matching control
     (.is-live, .is-pinned).
   Tested against
     lenis 1.3.26 (its dist/lenis.css is copied verbatim below), gsap 3.15.0
     SplitText mask markup, Chromium 145, 2026-09-25 (split masks measured at
     0px layout shift; sticky cards verified stuck at their offset).
   ========================================================================== */

/* ---- Lenis 1.3.26 dist/lenis.css ----------------------------------------- */
html.lenis,
html.lenis body { height: auto; }
.lenis:not(.lenis-autoToggle).lenis-stopped { overflow: clip; }
.lenis [data-lenis-prevent],
.lenis [data-lenis-prevent-wheel],
.lenis [data-lenis-prevent-touch],
.lenis [data-lenis-prevent-vertical],
.lenis [data-lenis-prevent-horizontal] { overscroll-behavior: contain; }
.lenis.lenis-smooth iframe { pointer-events: none; }
.lenis.lenis-autoToggle {
  transition-property: overflow;
  transition-duration: 1ms;
  transition-behavior: allow-discrete;
}

/* ---- Split lines: data-reveal="lines" and data-intro ---------------------
   SplitText wraps each .rv-line in .rv-line-mask (overflow: clip). Pad the mask
   so tight display leading does not shave descenders and accents, and cancel
   the padding with negative margins so the split heading keeps its exact layout.
   Not `margin-block: -0.12em`: adjacent siblings' negative margins COLLAPSE to the
   most negative one instead of adding, so every line gap kept +0.12em and the
   content below jumped when the split reverted (measured: +29px under a 3-line
   121px heading). Each later mask overlaps the previous by the full 0.24em
   instead; only the outer edges take 0.12em. SplitText docs: avoid
   text-wrap: balance on split text. `wrap` is set at rest too, so line breaks
   do not change at split time. */
.rv-line-mask { padding-block: 0.12em; margin-block: -0.24em 0; }
.rv-line-mask:first-child { margin-top: -0.12em; }
.rv-line-mask:last-child { margin-bottom: -0.12em; }
[data-reveal="lines"], [data-intro] { text-wrap: wrap; }

/* ---- data-reveal="image": the wrapper clips the media's scale settle ----- */
[data-reveal="image"] { overflow: clip; }

/* ---- data-parallax on media: the direct parent is the clipping frame ----- */
:where(:has(> :is(img, video, canvas, picture)[data-parallax])) { overflow: clip; }

/* ---- data-marquee ----------------------------------------------------------
   Markup: [data-marquee] > [data-marquee-track] > [data-marquee-group] > items,
   plus <button type="button" data-marquee-toggle aria-pressed="false" hidden>.
   At rest: one wrapped row, everything readable, no duplicate, no button. */
[data-marquee-track],
[data-marquee-group] {
  display: flex;
  flex-wrap: wrap;
  gap: var(--marquee-gap, 2rem);
  margin: 0;
  padding: 0;
  list-style: none;
}
[data-marquee].is-live { overflow: clip; }
[data-marquee].is-live [data-marquee-track] { flex-wrap: nowrap; width: max-content; gap: 0; }
[data-marquee].is-live [data-marquee-group] {
  flex-wrap: nowrap;
  flex-shrink: 0;
  padding-inline-end: var(--marquee-gap, 2rem); /* one group + one gap = exactly -50% of the track */
}
@media (prefers-reduced-motion: no-preference) {
  :root[data-motion="full"] [data-marquee].is-live [data-marquee-track] {
    animation: marquee var(--marquee-duration, 40s) linear infinite;
  }
  :root[data-motion="full"] [data-marquee][data-marquee-direction="right"].is-live [data-marquee-track] {
    animation-direction: reverse;
  }
  /* Hover/focus pause helps reading; the button is what satisfies WCAG 2.2.2 for keyboard and touch.
     Same prefix as the rule above: that rule's `animation` shorthand resets play-state to running,
     so a less specific pause rule would silently lose. */
  :root[data-motion="full"] [data-marquee].is-live:is(:hover, :focus-within, .is-paused) [data-marquee-track] {
    animation-play-state: paused;
  }
}
@keyframes marquee { to { translate: -50% 0; } }

/* JS-only controls ship with the `hidden` attribute and stay hidden until motion-core wires
   them. Guard it: an author rule such as `.btn { display: inline-flex }` beats the UA
   [hidden] style, and would leave a dead button in no-JS and reduced-motion views. */
[data-marquee-toggle][hidden],
[data-motion-toggle][hidden] { display: none !important; }

/* ---- data-pin="horizontal" -------------------------------------------------
   Markup: section[data-pin="horizontal"] > … [data-pin-track] > [data-pin-panel]*
   At rest, under reduced motion and below pinMinWidth, the panels are a plain
   vertical stack. motion-core adds .is-pinned only on wide screens with full motion.
   Keep the pinned section <= 100svh tall and body copy out of it (house rule). */
[data-pin="horizontal"] [data-pin-track] { display: grid; gap: var(--pin-gap, 2rem); }
[data-pin="horizontal"].is-pinned {
  height: 100svh;
  overflow: clip;                  /* the translated track must never widen the page */
  display: flex;
  flex-direction: column;
  justify-content: center;
}
[data-pin="horizontal"].is-pinned [data-pin-track] {
  display: flex;
  position: relative;              /* panel.offsetLeft is measured from the track (keyboard focus) */
  width: max-content;
  gap: var(--pin-gap, 4vw);
  padding-inline: var(--pin-pad, 4vw);
}
[data-pin="horizontal"].is-pinned [data-pin-panel] { flex: 0 0 var(--pin-panel-w, min(72vw, 56rem)); }
:where(:has(> [data-pin-media])) { overflow: clip; }

/* ---- data-stack: sticky stacking cards, CSS-first --------------------------
   Every direct child sticks a step lower than the one before, so later cards
   slide over earlier ones with no JS at all. Give cards an opaque background.
   JS only adds the covered card's recede (scale plus a .stack-shade overlay).
   Sticky IS the mechanism: a card rule such as `.card { position: relative }`
   would silently switch it off, so the selector below carries one extra
   pseudo-class of specificity (0,2,0) to win over single-class component rules.
   A sticky card is already a containing block for the absolute .stack-shade.
   No ancestor between the cards and the scroller may set overflow hidden/clip. */
[data-stack] { display: grid; gap: var(--stack-gap, 8svh); }
[data-stack] > :nth-child(n) {
  position: sticky;
  top: calc(var(--stack-top, 10svh) + var(--i, 0) * var(--stack-step, 1rem));
  transform-origin: 50% 0;
}
[data-stack] > :nth-child(2) { --i: 1; }
[data-stack] > :nth-child(3) { --i: 2; }
[data-stack] > :nth-child(4) { --i: 3; }
[data-stack] > :nth-child(5) { --i: 4; }
[data-stack] > :nth-child(6) { --i: 5; }
[data-stack] > :nth-child(7) { --i: 6; }
[data-stack] > :nth-child(n + 8) { --i: 7; }
.stack-shade {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--stack-shade-color, #000);
  opacity: 0;
  pointer-events: none;
}
