/* Galahad motion system — one curve, two durations, one arrival.
   The grammar is lifted from the product itself (docs/product-ui/02-components.md,
   section 4) so the site moves the way the software moves.

   Loaded from <head> AFTER index.html's <style> block. Every selector here is
   prefixed with :root purely to out-specify the older [data-reveal] rules still
   living in that block, which this file supersedes. Drop the prefix once those
   rules are gone.

   Driven by motion.jsx (window.useReveal / useScrollProgress / useParallax /
   useCounter / useCursorGlow). */

:root {
  --ease: cubic-bezier(0.16, 1, 0.3, 1);   /* the only entry curve */
  --ease-exit: cubic-bezier(0.4, 0, 1, 1); /* exits only */
  --dur-state: 150ms;                      /* reversible: hover, focus, press */
  --dur-arrival: 420ms;                    /* arrivals. Below ~350ms the eye records the result, not the movement */
  --stagger: 55ms;                         /* per index, capped at 8 items */
  --m-lift: 6px;
  --m-blur: 3px;
}

/* ---------- Scroll reveal ---------- */

/* Baseline is VISIBLE. index.html's <style> hides [data-reveal] unconditionally,
   which blanks the page when a script fails; reset it here. */
:root [data-reveal],
:root [data-reveal-children] > * {
  opacity: 1;
  transform: none;
  filter: none;
  transition: none;
}

/* Armed only once JS proves it is alive: the inline head script puts `.motion`
   on <html>. No class, no hiding. */
:root.motion [data-reveal],
:root.motion [data-reveal-children] > * {
  opacity: 0;
  transform: translate3d(0, var(--m-lift), 0);
  filter: blur(var(--m-blur));
  transition:
    opacity var(--dur-arrival) var(--ease),
    transform var(--dur-arrival) var(--ease),
    filter var(--dur-arrival) var(--ease);
}

/* The arrival: content resolving in a slot already reserved for it. It lands on
   `none`, never on blur(0) / translate(0), because a residual filter or transform
   makes the element a backdrop root and kills any backdrop-filter inside it for
   good. That is why this is a transition and not a keyframe: an animation's fill
   value normalises `none` to the identity function, a transition ends on its own
   declared value. Same reason there is no `will-change` here. */
:root.motion [data-reveal].is-in,
:root.motion [data-reveal-children].is-in > * {
  opacity: 1;
  transform: none;
  filter: none;
}

/* Opt-out for anything carrying glass: a filter or transform on an ancestor
   leaves backdrop-filter nothing to sample, so glass enters on opacity alone. */
:root.motion [data-reveal="fade"] { transform: none; filter: none; }

/* 55ms per index, capped at 8 items. */
:root.motion [data-reveal-children].is-in > *:nth-child(1) { transition-delay: 0ms; }
:root.motion [data-reveal-children].is-in > *:nth-child(2) { transition-delay: calc(1 * var(--stagger)); }
:root.motion [data-reveal-children].is-in > *:nth-child(3) { transition-delay: calc(2 * var(--stagger)); }
:root.motion [data-reveal-children].is-in > *:nth-child(4) { transition-delay: calc(3 * var(--stagger)); }
:root.motion [data-reveal-children].is-in > *:nth-child(5) { transition-delay: calc(4 * var(--stagger)); }
:root.motion [data-reveal-children].is-in > *:nth-child(6) { transition-delay: calc(5 * var(--stagger)); }
:root.motion [data-reveal-children].is-in > *:nth-child(7) { transition-delay: calc(6 * var(--stagger)); }
:root.motion [data-reveal-children].is-in > *:nth-child(n+8) { transition-delay: calc(7 * var(--stagger)); }

/* ---------- Scroll-linked scenes ---------- */

/* useScrollProgress writes --p (0 to 1) on the element; these read it. Scrubbable
   in both directions, transform and opacity only, so a scrub costs no layout.
   The fallback of 1 means an unarmed element sits at its finished state. */
:root.motion .scene-fade  { opacity: calc(0.2 + 0.8 * var(--p, 1)); }
:root.motion .scene-rise  { transform: translate3d(0, calc((1 - var(--p, 1)) * 32px), 0); }
:root.motion .scene-scale { transform: scale(calc(0.94 + 0.06 * var(--p, 1))); }

/* useParallax writes --parallax-y in px. */
:root.motion .parallax { transform: translate3d(0, var(--parallax-y, 0px), 0); will-change: transform; }

/* ---------- Cursor glow ---------- */

/* useCursorGlow writes --mx / --my (0 to 1), --mx-px / --my-px (raw px) and
   --glow (0 or 1) on the element. Specular highlight that tracks the pointer.

   The hook also puts .cursor-glow on every element it drives, and one of them
   already owns a specular of its own: the glass CTA paints the pointer into its
   lens rim on .cta-glass::after (theme-dark.css). One pseudo-element cannot
   carry two background stacks, and this file loads last, so the generic overlay
   silently replaced the rim. It yields instead. Anything that paints its own
   specular is named here rather than out-specified, so the exception is one
   grep away and neither side needs !important. The base rule below still
   applies to them: it is the positioning contract the hook depends on. */
.cursor-glow { position: relative; isolation: isolate; }
.cursor-glow:not(.cta-glass)::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: radial-gradient(
    var(--glow-size, 160px) circle at var(--mx-px, 50%) var(--my-px, 50%),
    rgba(255, 255, 255, var(--glow-strength, 0.35)),
    transparent 60%
  );
  opacity: var(--glow, 0);
  transition: opacity var(--dur-state) var(--ease);
  mix-blend-mode: overlay;
}

/* ---------- Reduced motion ---------- */

/* The head script already refuses to arm `.motion` under this query, so this is
   the second belt: it covers a runtime toggle, where the class is already on.
   Everything lands at its final state, nothing is left blank. */
@media (prefers-reduced-motion: reduce) {
  :root [data-reveal],
  :root [data-reveal-children] > * {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    transition: none !important;
  }
  :root .scene-fade { opacity: 1 !important; }
  :root .scene-rise,
  :root .scene-scale,
  :root .parallax { transform: none !important; }
  /* Same scope as the rule it switches off. The CTA's rim is not this overlay
     and does not travel: theme-dark.css recentres its hotspot and keeps it. */
  .cursor-glow:not(.cta-glass)::after { display: none; }
}

/* ---------------------------------------------------------------------------
   LE PIN. Une scene scrubbee doit tenir l'ecran le temps que son --p aille de
   0 a 1, puis rendre la page. C'est un sticky dans une piste haute, jamais une
   interception de la molette: le scroll natif, le clavier et le trackpad
   restent ceux du navigateur. motion.jsx deplace la mesure sur la piste, sans
   quoi le sticky figerait --p. --pin-span = viewports de scroll EN PLUS de
   l'ecran tenu. Voir motion.jsx, useScrollProgress.
   ------------------------------------------------------------------------ */
.pin-track { height: calc((var(--pin-span, 1.4) + 1) * 100vh); }
.pin-track { height: calc((var(--pin-span, 1.4) + 1) * 100svh); }
.pin-stage {
  position: sticky;
  top: 0;
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* Sous 768px la scene est plus haute que l'ecran: un sticky plus grand que son
   viewport ne colle plus, il defile. La piste se desarme. */
@media (max-width: 767px) {
  .pin-track { height: auto; }
  .pin-stage { position: static; min-height: 0; display: block; }
}

@media (prefers-reduced-motion: reduce) {
  .pin-track { height: auto; }
  .pin-stage { position: static; min-height: 0; display: block; }
}
