/* ==========================================================================
   Animations
   Every transition in the site uses --ease. Nothing bounces.
   The reduced-motion block at the bottom is authoritative: it must neutralise
   every animation defined above it, not just the loader's.
   ========================================================================== */

@keyframes spin { to { transform: rotate(360deg); } }

@keyframes fade-up {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ── Phase 1 — wordmark + slogan fade up ────────────────────────────────────
   These live in the hero permanently now, not in the loader overlay, so the
   fade runs on every load rather than only during the full sequence. That is
   deliberate: a 600ms rise is the right entrance whether or not the collage
   drop plays.

   `backwards`, NOT `forwards`, and no `opacity: 0` on the rule itself. The
   resting state of this text must be visible, with the animation only
   borrowing it downward at the start — otherwise the wordmark's visibility
   depends on an animation completing, and anything that stops the animation
   running leaves it invisible forever. `backwards` applies the from-keyframe
   before the animation starts and hands the property back afterwards, so the
   fallback is "visible" rather than "gone". */

.hero__wordmark,
.hero__slogan {
  animation: fade-up var(--loader-text-in) var(--ease) backwards;
}

.hero__slogan { animation-delay: 90ms; }

/* ── Loader phase 2 — the drop ──────────────────────────────────────────────
   The frames must read as falling *past the camera* and settling into the
   plane, not sliding down from the top. Three things sell that:

     1. translateZ(500px) at the start, resolved against the stage's
        perspective: 1200px — so the frame begins physically nearer the viewer
        and its apparent size shrinks as Z returns to 0.
     2. scale(1.9) on top of the Z travel, exaggerating the approach.
     3. blur(6px) → 0, standing in for a lens racking into focus.

   The 88% stop is a 3.5% overshoot on scale. It settles back over the last
   ~110ms, which is what stops the landing from feeling like a hard stop.

   Timing: start = --loader-phase2 + (--order × --loader-stagger).
   With 30 frames at 32ms the last one *starts* at 500 + 928 = 1428ms and
   lands at 2328ms. js/loader.js does not hard-code that number — it derives
   the FLIP start from the last frame's own animationend, so changing the
   stagger or the frame count keeps the sequence in step by itself.           */

@keyframes polaroid-drop {
  0% {
    opacity: 0;
    transform: translate3d(0, -40vh, 500px) scale(1.9) rotate(var(--drop-rot));
    filter: blur(6px);
  }
  35% { opacity: 1; }
  88% {
    transform: translate3d(0, 0, 0) scale(1.035) rotate(var(--rot));
    filter: blur(0);
  }
  100% {
    opacity: 1;
    transform: translate3d(0, 0, 0) scale(1) rotate(var(--rot));
    filter: blur(0);
  }
}

.is-dropping .polaroid {
  will-change: transform, opacity, filter;
  animation: polaroid-drop var(--loader-frame-dur) var(--ease) both;
  animation-delay: calc(var(--loader-phase2) + var(--order) * var(--loader-stagger));
}

/* Once the sequence is over, drop the compositor hint. */
.collage.is-settled .polaroid { will-change: auto; }

/* ── Loader phase 4 — header chips stagger in ───────────────────────────────
   Menu, search, then language, then account — 60ms apart. */

/* Every delay is offset by --loader-header-hold so the whole entrance begins
   only once the loader has finished fading out. Before this, the bar and its
   chips animated on top of a veil that was still disappearing — three things
   moving at once, which read as busy rather than composed. */
.header.is-entering .header__left > *,
.header.is-entering .header__right > * {
  opacity: 0;
  /* .icon-btn carries `transition: opacity ...` for its hover state. Without
     this, setting opacity:0 here hands control to that transition and the
     buttons visibly fade OUT before the entrance has even started, then sit
     invisible for the whole hold. The entrance owns opacity; the hover
     transition gets it back the moment .is-entering is dropped. */
  transition: none;
  animation: fade-up 420ms var(--ease) forwards;
}

.header.is-entering .header__left  > *:nth-child(1) { animation-delay: var(--loader-header-hold); }
.header.is-entering .header__left  > *:nth-child(2) { animation-delay: calc(var(--loader-header-hold) + var(--loader-chip-step) * 1); }
.header.is-entering .header__right > *:nth-child(1) { animation-delay: calc(var(--loader-header-hold) + var(--loader-chip-step) * 2); }
.header.is-entering .header__right > *:nth-child(2) { animation-delay: calc(var(--loader-header-hold) + var(--loader-chip-step) * 3); }
.header.is-entering .header__right > *:nth-child(3) { animation-delay: calc(var(--loader-header-hold) + var(--loader-chip-step) * 4); }

/* The bar itself fades in once the hero has settled and the loader is gone. */
@keyframes header-bar-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* `both` matters twice here: `backwards` is what holds the bar at opacity 0
   through --loader-header-hold instead of showing it and then hiding it, and
   `forwards` keeps it visible until js/loader.js drops the class. */
.header.is-entering {
  animation: header-bar-in var(--loader-header-in) var(--ease) var(--loader-header-hold) both;
}

/* ── Header entrance on pages with no loading sequence ──────────────────────
   The homepage gets its header via the loader's phase 3. Everywhere else the
   bar simply existed at paint, which read as abrupt next to it. This drops it
   in over 600ms so every page opens the same way.

   `backwards`, and no opacity/transform on the rule itself — the resting state
   must be the visible one, so a header whose animation never runs is still a
   header. Applied via a class the build puts on those pages rather than a
   :has() check, because :has() is live: the homepage's loader is removed from
   the DOM at the end of the sequence, and a :not(:has([data-loader])) rule
   would suddenly start matching and re-run the animation right then. */
@keyframes header-drop-in {
  from { opacity: 0; transform: translateY(-100%); }
  to   { opacity: 1; transform: translateY(0); }
}

.header--enter {
  animation: header-drop-in var(--t-slow) var(--ease) backwards;
}

/* ── Scroll reveals ─────────────────────────────────────────────────────────
   IntersectionObserver adds .is-in. --i is the stagger index (product cards
   use 120ms steps). Reveals are one-shot; the observer unobserves on entry. */

[data-reveal] {
  opacity: 0;
  transform: translateY(24px);
  transition:
    opacity var(--t-slow) var(--ease),
    transform var(--t-slow) var(--ease);
  transition-delay: calc(var(--i, 0) * 120ms);
}

[data-reveal].is-in {
  opacity: 1;
  transform: translateY(0);
}

/* Hero exit: the collage fades over the last 30% of the pinned scroll. The
   opacity is written on .hero__stage-inner, not on .collage — see the note in
   layout.css about opacity flattening a preserve-3d subtree. */

/* ── Returning visitor ──────────────────────────────────────────────────────
   sessionStorage flag set — no sequence, just a 400ms curtain lift. */

.loader--replay {
  animation: header-bar-in var(--loader-replay) var(--ease) reverse both;
  pointer-events: none;
}

/* ── Reduced motion ─────────────────────────────────────────────────────────
   Render the final state immediately. This covers the loader, the drop, the
   header stagger, scroll reveals, the parallax fade, hovers, and the drawer,
   modal and accordion transitions — every animation in the project.          */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }

  .hero__wordmark,
  .hero__slogan { animation: none; }

  .is-dropping .polaroid {
    animation: none;
    opacity: 1;
    filter: none;
    transform: rotate(var(--rot));
    will-change: auto;
  }

  .header--enter,
  .header.is-entering,
  .header.is-entering .header__left > *,
  .header.is-entering .header__right > * {
    animation: none;
    opacity: 1;
  }

  [data-reveal] {
    opacity: 1;
    transform: none;
    transition: none;
  }

  /* The parallax loop is not started at all under reduced motion (see
     js/parallax.js), so the collage keeps its resting opacity. */
  .hero__stage-inner { opacity: 1; }

  .product-card:hover,
  .product-card:focus-within { transform: none; }

  .field-inline__submit:hover:not(:disabled) { transform: none; }

  /* The two in-flight spinners are the one place a loop is load-bearing —
     it is the only signal that a request is still running. Slowed, not
     stopped, and stripped of the rotation illusion. */
  .btn-google.is-busy::after,
  .field-inline__submit[data-loading]::after {
    animation-duration: 1600ms !important;
    border-top-color: currentColor;
    opacity: 0.55;
  }
}
