/*!
 * motion-v3.css — StrayBirds V3 Motion Library (M1–M9)
 * Owner: motion agent. Do not edit from other agents; read only.
 * Pairs with motion-v3.js (window.SBMotion). Load both on every V3 page:
 *   <link rel="stylesheet" href="/motion-v3.css">
 *   <script src="/motion-v3.js" defer></script>
 *
 * DESIGN CONTRACT
 * ----------------
 * - Content must be fully visible with NO javascript. Every "hidden until
 *   revealed" rule below is scoped under `html.js-motion`, a class that
 *   motion-v3.js adds to <html> the moment it runs. Without that class
 *   (JS blocked/failed) every element renders in its final state.
 * - With JS, one-shot reveals (wipe, rise, fade) start hidden and gain a
 *   `.sb-in` class from an IntersectionObserver when they scroll into
 *   view, which is what actually plays the keyframe animation. Elements
 *   marked `data-sb-eager` play on load instead of waiting for scroll
 *   (use this ONLY for above-the-fold hero content).
 * - Continuous loops (marquee, sticker spin, pulse, Ken Burns) render and
 *   animate immediately (no JS required to see them) but motion-v3.js
 *   adds `.sb-paused` to pause them (animation-play-state) while they are
 *   off-screen or the tab is hidden, for battery/perf.
 * - `prefers-reduced-motion: reduce` removes all movement everywhere,
 *   independent of JS: loops stop outright, one-shot reveals resolve to
 *   their end state via a near-zero animation-duration (so no frame of
 *   motion is visible), and parallax transforms are neutralised.
 *
 * CLASSES
 * -------
 * M1 sb-wipe / sb-wipe2   Slant wipe-in. Put `sb-wipe` on the first
 *                         headline line, `sb-wipe2` on the second line of
 *                         the same reveal (it carries the +0.25s delay
 *                         from the spec). Scroll-triggered by default;
 *                         add `data-sb-eager` on the wrapping hero for
 *                         on-load playback.
 * M2 sb-ken               Ken Burns slow zoom (14s, alternate) on a
 *                         hero/full-bleed <img>. Put it on an element
 *                         whose parent has `overflow:hidden` (the class
 *                         itself does not clip). Loops immediately;
 *                         paused off-screen/hidden tab automatically.
 * M3 sb-marquee-viewport  Wrapper: `overflow:hidden`, one line, ARIA role
 *                         "marquee". Put ONE copy of the slogan spans
 *                         inside a child `.sb-marquee` element — SBMotion
 *                         duplicates that content once at init and marks
 *                         the clone `aria-hidden="true"` so screen readers
 *                         only hear it once. Do NOT hand-duplicate the
 *                         content yourself (the Claude Design mockups
 *                         hard-code two copies for the static comp; strip
 *                         that back to one copy when wiring the real
 *                         page, or SBMotion will end up doubling it again
 *                         into four).
 * M4 sb-spin              16s linear infinite rotation for the round
 *                         sticker badge. Loops immediately; auto-paused
 *                         off-screen/hidden tab.
 * M5 sb-rise              Rise-in: opacity 0 → 1, translateY(24px) → 0,
 *                         0.7s ease-out. Scroll-triggered, one-shot.
 *                         Siblings sharing a parent auto-stagger 80ms
 *                         apart in DOM order (override the base amount
 *                         with `data-sb-stagger="120"` in ms on the
 *                         parent, or pin one item's own delay with
 *                         `data-sb-delay="240"` in ms).
 * M6 sb-pulse             2.2s breathing box-shadow ring, for the primary
 *                         CTA and "live" status dots. Loops immediately;
 *                         auto-paused off-screen/hidden tab.
 * M7 sb-fade              Generic 0.5s fade-in-place (also the base class
 *                         used internally by the crossfade helper, see
 *                         SBMotion.crossfade() in motion-v3.js). Scroll
 *                         triggered like sb-rise/sb-wipe unless
 *                         `data-sb-eager` is set.
 *    sb-crossfade         Put this on an <img> you intend to hot-swap
 *                         with SBMotion.crossfade(img, src, alt, srcset).
 *                         Purely a transition hook — safe to leave on
 *                         permanently once an image can be swapped
 *                         (colour picker / fold toggle photos).
 * M8 [data-parallax]      Any element with a numeric `data-parallax`
 *                         speed factor (e.g. `data-parallax="0.15"`,
 *                         `data-parallax="-0.08"` for the opposite
 *                         direction) drifts vertically on scroll,
 *                         transform-only, rAF-throttled. Use 2–3 distinct
 *                         speeds per mosaic for a parallax feel. Disabled
 *                         entirely under reduced motion. The element (or
 *                         an ancestor) needs `overflow:hidden` if the
 *                         drift shouldn't peek past its box.
 * M9 sb-carousel          Scroll-snap filmstrip. Structure:
 *                         <div class="sb-carousel-wrap">
 *                           <div class="sb-carousel" role="region"
 *                                aria-label="…" tabindex="0">
 *                             <div class="sb-carousel-item">…</div>…
 *                           </div>
 *                           <button class="sb-carousel-prev" aria-label="Previous">‹</button>
 *                           <button class="sb-carousel-next" aria-label="Next">›</button>
 *                         </div>
 *                         Buttons are optional (native swipe/keyboard
 *                         scroll always works because `.sb-carousel` is
 *                         itself a focusable, scrollable, labelled
 *                         region); SBMotion wires the buttons when
 *                         present and disables them at the ends.
 *
 * DATA ATTRIBUTES
 * ---------------
 * data-sb-eager            Reveal on load, not on scroll (hero content).
 * data-sb-stagger="80"     Stagger step in ms for `.sb-rise` children of
 *                          this element (default 80, matches spec).
 * data-sb-delay="240"      Exact reveal delay in ms for one element,
 *                          overrides auto-stagger.
 * data-parallax="0.15"     M8 parallax speed factor (can be negative).
 *
 * JS-added state classes (do not set these by hand):
 * html.js-motion    — JS booted; enables all hidden-until-revealed rules.
 * .sb-in            — one-shot reveal has played / is playing.
 * .sb-paused        — loop is paused (off-screen or tab hidden).
 * .sb-cloned        — marks the accessible clone made for a marquee.
 */

@keyframes sbMarquee { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@keyframes sbSpin { from { transform: rotate(-12deg); } to { transform: rotate(348deg); } }
@keyframes sbWipe {
  from { clip-path: polygon(0 0, 0 0, 0 100%, 0 100%); }
  to { clip-path: polygon(0 0, 100% 0, calc(100% - var(--sb-slab-cut, 18px)) 100%, 0 100%); }
}
@keyframes sbRise { from { opacity: 0; transform: translateY(24px); } to { opacity: 1; transform: none; } }
@keyframes sbPulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(217, 58, 24, .55); }
  60% { box-shadow: 0 0 0 12px rgba(217, 58, 24, 0); }
}
@keyframes sbKen { from { transform: scale(1); } to { transform: scale(1.08); } }
@keyframes sbFade { from { opacity: 0; } to { opacity: 1; } }

/* M2 / M4 / M6 / M3 — continuous loops. Play immediately (no-JS visible),
   JS pauses them off-screen / hidden tab via .sb-paused. */
.sb-ken { animation: sbKen 14s ease-in-out infinite alternate; }
.sb-spin { animation: sbSpin 16s linear infinite; }
.sb-pulse { animation: sbPulse 2.2s ease-out infinite; }
.sb-marquee { animation: sbMarquee 22s linear infinite; display: inline-flex; align-items: center; width: max-content; will-change: transform; }
.sb-marquee-seg { display: inline-flex; align-items: center; gap: inherit; }
.sb-marquee-viewport { overflow: hidden; display: block; max-width: 100%; }

.sb-ken.sb-paused,
.sb-spin.sb-paused,
.sb-pulse.sb-paused,
.sb-marquee.sb-paused { animation-play-state: paused; }

/* M1 / M5 / M7 — one-shot reveals. Hidden pre-state only applies once JS
   has booted (html.js-motion); otherwise content is visible by default. */
html.js-motion .sb-wipe:not(.sb-in),
html.js-motion .sb-wipe2:not(.sb-in) { clip-path: polygon(0 0, 0 0, 0 100%, 0 100%); }
html.js-motion .sb-rise:not(.sb-in) { opacity: 0; transform: translateY(24px); }
html.js-motion .sb-fade:not(.sb-in) { opacity: 0; }

html.js-motion .sb-wipe.sb-in { animation: sbWipe .8s cubic-bezier(.2, .8, .2, 1) both; }
html.js-motion .sb-wipe2.sb-in { animation: sbWipe .8s cubic-bezier(.2, .8, .2, 1) .25s both; }
html.js-motion .sb-rise.sb-in { animation: sbRise .7s ease-out both; animation-delay: var(--sb-delay, 0ms); }
html.js-motion .sb-fade.sb-in { animation: sbFade .5s ease-out both; }

/* M7 crossfade helper target — see SBMotion.crossfade() in motion-v3.js */
.sb-crossfade { transition: opacity .25s ease-out; }
.sb-crossfade.sb-crossfade-out { opacity: 0; }

/* M8 — scroll parallax. Transform-only, applied by motion-v3.js. */
[data-parallax] { will-change: transform; }

/* M9 — swipe carousel */
.sb-carousel-wrap { position: relative; }
.sb-carousel {
  display: flex;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scroll-behavior: smooth;
  scrollbar-width: none;
  gap: var(--sb-carousel-gap, 12px);
}
.sb-carousel::-webkit-scrollbar { display: none; }
.sb-carousel:focus-visible { outline: 3px solid #3B5BDB; outline-offset: 2px; }
.sb-carousel-item { flex: 0 0 auto; scroll-snap-align: start; scroll-snap-stop: always; }
.sb-carousel-prev,
.sb-carousel-next {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 44px;
  height: 44px;
  min-width: 44px;
  min-height: 44px;
  border: none;
  border-radius: 999px;
  background: #1E201D;
  color: #F4F1E8;
  font-size: 20px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 2;
}
.sb-carousel-prev { left: 4px; }
.sb-carousel-next { right: 4px; }
.sb-carousel-prev:disabled,
.sb-carousel-next:disabled { opacity: .35; cursor: default; }
.sb-carousel-prev:focus-visible,
.sb-carousel-next:focus-visible { outline: 3px solid #3B5BDB; outline-offset: 2px; }

/* Reduced motion: stop every loop outright; resolve one-shot reveals to
   their end state via a near-instant duration so nothing visibly moves;
   neutralise parallax; keep the carousel natively swipeable/keyboard
   scrollable (only smooth-scroll behaviour is removed). */
@media (prefers-reduced-motion: reduce) {
  .sb-ken,
  .sb-spin,
  .sb-pulse,
  .sb-marquee { animation: none !important; }

  html.js-motion .sb-wipe:not(.sb-in),
  html.js-motion .sb-wipe2:not(.sb-in),
  html.js-motion .sb-rise:not(.sb-in),
  html.js-motion .sb-fade:not(.sb-in) {
    opacity: 1;
    transform: none;
    clip-path: polygon(0 0, 100% 0, calc(100% - var(--sb-slab-cut, 18px)) 100%, 0 100%);
  }
  html.js-motion .sb-wipe.sb-in,
  html.js-motion .sb-wipe2.sb-in,
  html.js-motion .sb-rise.sb-in,
  html.js-motion .sb-fade.sb-in {
    animation-duration: .001ms !important;
    animation-delay: 0s !important;
  }

  .sb-crossfade { transition: none !important; }

  [data-parallax] { transform: none !important; }

  .sb-carousel { scroll-behavior: auto; }
}

/* ==========================================================================
   app.js image swap (colour picker / fold toggle): shared, so any v3 page can use it.
   app.js createImageSwap() adds .motion-swap-host to the image's parent and a .motion-swap-layer clone that fades in;
   wash() adds a .motion-wash radial behind the photo (--wash, --swap-from set by app.js). Hosts that use it should
   also carry .color-photo for the wash/slide, and the fold toggle uses #fold-image. Not gated on .js-motion:
   app.js only creates these nodes itself. This is separate from M7 (.sb-crossfade / SBMotion.crossfade).
   ========================================================================== */
.motion-swap-host { position: relative; }
.motion-swap-layer { position: absolute; margin: 0; opacity: 0; pointer-events: none; }
.motion-swap-host.is-swapping > .motion-swap-layer { opacity: 1; transition: opacity .34s ease; }
.motion-swap-host.is-swapping > img:not(.motion-swap-layer) { opacity: 0; transition: opacity .34s ease; }
.motion-swap-host.is-loading > img:not(.motion-swap-layer) { opacity: .5; transition: opacity .2s ease .18s; }
.color-photo > img:not(.motion-swap-layer) { position: relative; z-index: 1; }
.color-photo > .motion-swap-layer { z-index: 2; }
@media (prefers-reduced-motion: no-preference) {
  @keyframes sb-color-wash { from { opacity: .9; transform: scale(.25); } 50% { opacity: .6; } to { opacity: 0; transform: scale(1.15); } }
  @keyframes sb-swap-slide { from { translate: var(--swap-from, 16px) 0; } }
  .color-photo > .motion-wash {
    position: absolute; inset: 0 6%; z-index: 0; pointer-events: none;
    background: radial-gradient(closest-side, var(--wash, #ef4926), color-mix(in srgb, var(--wash, #ef4926) 45%, transparent) 55%, transparent);
    animation: sb-color-wash .95s cubic-bezier(.2, .7, .2, 1) forwards;
  }
  .color-photo.is-swapping > .motion-swap-layer { animation: sb-swap-slide .5s cubic-bezier(.2, .7, .2, 1) backwards; }
  @keyframes sb-fold-squash { 0%, 100% { scale: 1; } 30% { scale: .9 1.04; } 62% { scale: 1.025 .985; } }
  @keyframes sb-fold-stretch { 0%, 100% { scale: 1; } 30% { scale: 1.05 .96; } 62% { scale: .985 1.015; } }
  .motion-swap-host > #fold-image, .motion-swap-host > #fold-image ~ .motion-swap-layer { transform-origin: 50% 92%; }
  .motion-swap-host.is-folding > img { animation: sb-fold-squash .6s cubic-bezier(.3, .7, .3, 1); }
  .motion-swap-host.is-unfolding > img { animation: sb-fold-stretch .6s cubic-bezier(.3, .7, .3, 1); }
}
