/* CLIXHOUSE design system — motion & interaction layer
   ─────────────────────────────────────────────────────────────────
   THE single home for motion in the framework. Entrances, scroll
   choreography, tactile feedback, overlay enter/exit, page transitions,
   and thumb-zone mobile layout — all native CSS, zero dependencies,
   every effect honest about prefers-reduced-motion and gated so an
   unsupporting engine degrades to a correct static state.

   WHY a dedicated file (one standard way): motion used to be scattered
   across capabilities.css (reveals, scroll-progress, sticky-shrink,
   view-transitions). It now lives here, tokenized and direction-aware,
   so a site re-skins its entire motion character by swapping ONE
   attribute — data-motion — never by rewriting rules. capabilities.css
   keeps the non-motion natives (scroll-state, popover, masonry, …).

   ── The two non-negotiable gates ──
   Every animation in this file ships behind BOTH:
     1. @supports (<the primitive>)         → unsupported = static, never broken
     2. @media (prefers-reduced-motion: no-preference) → honors the user

   prefers-reduced-motion is an ethical floor, not a nice-to-have:
   vestibular disorders make large parallax/zoom motion physically
   nauseating. a11y.css also ships a global reduce → near-zero override;
   this file additionally NEVER starts non-essential motion unless the
   user has expressed no-preference.

   ── Tokens this layer reads (tokens-defaults.css) ──
     --motion-fast/base/slow   durations
     --motion-ease             standard in-out
     --motion-spring / --ease-spring[-soft]  linear() springs (overshoot)
     --ease-out-quint          decisive ease-out
     --motion-travel           reveal travel distance
     --reveal-range            view() animation-range window
     --stagger-step/-index/-delay  sequential choreography
     --vh-small/large/dynamic  chrome-aware mobile heights
     --safe-*                  notch / home-indicator insets

   ── Direction is the whole point ──
   data-motion="restrained" (default) | "kinetic" | "expressive" — the
   SAME classes below, re-charactered by the token presets. There are no
   rules, only directions; choose one per site or per section.

   Imported via: @import url("/static/design/motion.css") layer(components);

   Browser support is verified in docs/design/mobile-design.md (PRIMITIVES
   SUPPORT MATRIX) and docs/design/motion-system.md. Re-verify before widening.
*/

@layer components {

  /* ── ENTRANCES ────────────────────────────────────────────────── */
  /* TWO families, because they obey different clocks:

       .enter*   — TIME-based, plays once on load. For above-the-fold
                   content already in view (hero headline, CTA). Honors
                   --stagger-delay, so siblings cascade in. Universally
                   supported; gated only by reduced-motion.

       .reveal*  — SCROLL-based, plays as the element crosses the viewport
                   (animation-timeline: view()). For below-the-fold
                   content. Naturally staggered — each element enters its
                   scroll range at a different point, so no delay needed.

     THE TRAP (encoded once): scroll-driven animations IGNORE time-based
     animation-delay — only animation-range offsets them. So stagger lives
     on .enter (time), never on .reveal (scroll). Don't add animation-delay
     to a view()-timeline rule expecting a cascade; it silently does
     nothing. (This is why the old random()-jitter-on-.reveal was a no-op.)

     Both families share one keyframe (cx-enter) and express direction /
     scale / blur via custom props on the individual transform properties
     (translate/scale/filter — Baseline), so variants stay DRY and never
     fight a `transform` shorthand. All travel/scale/blur scale with the
     data-motion direction preset via --motion-travel. */

  @keyframes cx-enter {
    from {
      opacity: 0;
      translate: var(--enter-translate, 0 var(--motion-travel));
      scale: var(--enter-scale, 1);
      filter: var(--enter-filter, none);
    }
    to { opacity: 1; translate: 0 0; scale: 1; filter: none; }
  }

  /* ── .enter — time-based, staggerable load entrance ──────────── */
  @media (prefers-reduced-motion: no-preference) {
    /* Explicit variant list — NOT [class*="enter-"], which greedily matches
       any class containing the substring (e.g. .center-stage, .presenter-*)
       and would hijack unrelated brand classes. */
    .enter,
    .enter-up, .enter-down, .enter-left, .enter-right, .enter-scale, .enter-blur {
      animation: cx-enter var(--motion-base) var(--motion-ease) both;
      /* delay computed HERE so each item's own --stagger-index applies
         (a :root-derived --stagger-delay would freeze at index 0). */
      animation-delay: calc(var(--stagger-index) * var(--stagger-step));
    }
    /* directional from-states (where the element travels FROM) */
    .enter-up    { --enter-translate: 0 var(--motion-travel); }
    .enter-down  { --enter-translate: 0 calc(-1 * var(--motion-travel)); }
    .enter-left  { --enter-translate: calc(-1 * var(--motion-travel)) 0; }
    .enter-right { --enter-translate: var(--motion-travel) 0; }
    .enter-scale { --enter-scale: 0.94; --enter-translate: 0 0; }
    .enter-blur  { --enter-filter: blur(0.5rem); }

    /* Orchestration helper: cascade children with zero per-item markup.
       <ul class="stagger-children"> → each child gets a stepped delay.
       Pairs with any .enter* class on the children. sibling-index() isn't
       Baseline yet, so the steps are enumerated (covers the common 1–10). */
    .stagger-children > *:nth-child(1)  { --stagger-index: 0; }
    .stagger-children > *:nth-child(2)  { --stagger-index: 1; }
    .stagger-children > *:nth-child(3)  { --stagger-index: 2; }
    .stagger-children > *:nth-child(4)  { --stagger-index: 3; }
    .stagger-children > *:nth-child(5)  { --stagger-index: 4; }
    .stagger-children > *:nth-child(6)  { --stagger-index: 5; }
    .stagger-children > *:nth-child(7)  { --stagger-index: 6; }
    .stagger-children > *:nth-child(8)  { --stagger-index: 7; }
    .stagger-children > *:nth-child(9)  { --stagger-index: 8; }
    .stagger-children > *:nth-child(10) { --stagger-index: 9; }
  }

  /* ── .reveal — scroll-driven, naturally staggered ───────────── */
  /* Gated TWO ways — never invisible to a user who can't run it:
       1. @supports (animation-timeline: view()) → else content shows, no reveal
       2. @media (prefers-reduced-motion: no-preference) → honors the user
     LOAD-BEARING ORDER: animation-timeline MUST come AFTER the `animation`
     shorthand — the shorthand resets animation-timeline to auto, so a
     timeline declared first silently does nothing (the #1 "my scroll
     animation never runs" cause). */
  @supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
      .reveal,
      .reveal-x,
      .reveal-scale,
      .reveal-blur {
        opacity: 0;
        animation: cx-enter linear both;
        animation-timeline: view();
        animation-range: var(--reveal-range);
      }
      .reveal       { --enter-translate: 0 var(--motion-travel); }
      .reveal-x     { --enter-translate: calc(-1 * var(--motion-travel)) 0; }
      .reveal-scale { --enter-scale: 0.94; --enter-translate: 0 0; }
      .reveal-blur  { --enter-filter: blur(0.5rem); }
    }
  }

  /* ── SCROLL CHOREOGRAPHY — progress / parallax / sticky ──────── */
  /* All four ride native scroll-driven timelines (scroll() / view()),
     which run on the COMPOSITOR, off the main thread. This is why the old
     "never parallax on mobile" rule is relaxed here: that warning was about
     JS scroll-listener parallax thrashing the main thread. Compositor-
     driven parallax stays smooth on a phone — the only honest gate left is
     reduced-motion (vestibular care), which every block below carries. */

  /* Scroll progress bar — <div class="cx-scroll-progress" aria-hidden>. */
  .cx-scroll-progress {
    position: fixed;
    inset-block-start: 0;
    inset-inline: 0;
    block-size: 3px;
    background: var(--accent, currentColor);
    transform-origin: 0 50%;
    z-index: 100;
  }
  @supports (animation-timeline: scroll()) {
    @media (prefers-reduced-motion: no-preference) {
      .cx-scroll-progress {
        scale: 0 1;
        animation: cx-scroll-progress linear both;
        animation-timeline: scroll(root);
      }
    }
  }
  @keyframes cx-scroll-progress {
    from { scale: 0 1; }
    to   { scale: 1 1; }
  }

  /* Parallax — element drifts as it crosses the viewport. Depth tunable
     per element via --parallax-depth; compositor-cheap, mobile-safe. */
  @supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
      .parallax {
        animation: cx-parallax linear both;
        animation-timeline: view();
        animation-range: cover;
        /* no will-change: scroll-driven animations are already promoted to
           the compositor; a persistent hint only pins GPU layers (the very
           misuse this file's perf note warns against). */
      }
    }
  }
  @keyframes cx-parallax {
    from { translate: 0 calc(-1 * var(--parallax-depth, 2rem)); }
    to   { translate: 0 var(--parallax-depth, 2rem); }
  }

  /* Scroll-fade — fades IN on entry and OUT on exit (tied to view
     progress, unlike .reveal which only fades in). Good for section
     backgrounds / decorative layers that shouldn't linger at edges. */
  @supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
      .scroll-fade {
        animation: cx-scroll-fade linear both;
        animation-timeline: view();
        animation-range: cover;
      }
    }
  }
  @keyframes cx-scroll-fade {
    0%, 100% { opacity: 0; }
    25%, 75% { opacity: 1; }
  }

  /* Sticky-header shrink — a position: sticky header restyles itself over
     the first --shrink-range px of scroll. Add .cx-sticky-shrink to the
     sticky header; override --shrink-range / --shrink-padding for taste.
     Per Temani Afif (css-tip.com/sticky-header/). */
  @supports (animation-timeline: scroll()) {
    @media (prefers-reduced-motion: no-preference) {
      .cx-sticky-shrink {
        animation: cx-sticky-shrink linear both;
        animation-timeline: scroll();
        animation-range: 0 var(--shrink-range, 64px);
      }
    }
  }
  @keyframes cx-sticky-shrink {
    to {
      padding-block: var(--shrink-padding, 0.5rem);
      box-shadow: var(--shadow-2);
    }
  }

  /* ── TACTILE — touch-first press & hover feedback ────────────── */
  /* The "alive object" feel: the thing you touch responds instantly
     (<100ms perceived). Touch-FIRST — :active feedback is the baseline
     for everyone; hover is progressive enhancement gated to hover-capable
     pointers so a phone never gets a stuck hover state.

     Two platform fixes baked in:
       touch-action: manipulation  → kills the legacy ~300ms tap delay
       -webkit-tap-highlight-color → kills iOS's grey tap flash
     so OUR feedback is the only thing the user sees.

     Reduced-motion: the press still acknowledges, but via a non-moving
     dim instead of a squish — feedback without vestibular cost. */

  .tap, .pressable, .lift {
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
    transition:
      scale var(--motion-fast) var(--motion-ease),
      translate var(--motion-fast) var(--motion-ease),
      box-shadow var(--motion-fast) var(--motion-ease),
      filter var(--motion-fast) var(--motion-ease);
  }

  /* .tap — instant press squish for buttons / links / chips. */
  .tap:active { scale: 0.96; }

  /* .pressable — card/tile: lifts on hover (hover-only), presses on tap. */
  @media (hover: hover) {
    .pressable:hover { translate: 0 -2px; box-shadow: var(--shadow-3); }
    .lift:hover      { translate: 0 calc(-1 * var(--lift, 4px)); box-shadow: var(--shadow-3); }
  }
  .pressable:active { scale: 0.98; box-shadow: var(--shadow-1); }

  /* Reduced-motion: acknowledge the touch without moving. */
  @media (prefers-reduced-motion: reduce) {
    .tap:active,
    .pressable:active { scale: 1; translate: 0 0; filter: brightness(0.94); }
  }

  /* ── OVERLAYS — dialog / popover / drawer / accordion ────────── */
  /* Enter AND exit animation for top-layer overlays, the historically
     hard part. The four moving parts:
       @starting-style          → the "from" state on first render (enter)
       transition-behavior: allow-discrete → lets display + overlay tween,
                                  so the element animates OUT before it's
                                  removed from the top layer (exit)
       :popover-open / [open]    → the "to" (open) state
       reduced-motion            → instant, no transform

     Support (verified 2026): Chrome 117+, Safari 17.5+. Firefox runs the
     visual transition + @starting-style but does NOT yet tween the
     discrete `display` property — so in Firefox the EXIT hides instantly
     (enter still animates). Fails safe; never depend on the FF exit tween.

     Direction-aware: duration/easing come from the tokens, so a
     data-motion="kinetic" page snaps these faster automatically. */

  [popover],
  dialog {
    opacity: 0;
    scale: 0.96;
    transition:
      opacity  var(--motion-base) var(--motion-ease),
      scale    var(--motion-base) var(--motion-ease),
      translate var(--motion-base) var(--motion-ease),
      display  var(--motion-base) allow-discrete,
      overlay  var(--motion-base) allow-discrete;
  }
  [popover]:popover-open,
  dialog[open] {
    opacity: 1;
    scale: 1;
    translate: 0 0;
  }
  @starting-style {
    [popover]:popover-open,
    dialog[open] { opacity: 0; scale: 0.96; }
  }

  /* Backdrop fade for modal dialogs. */
  dialog::backdrop {
    background: rgb(0 0 0 / 0);
    transition:
      background-color var(--motion-base) var(--motion-ease),
      display var(--motion-base) allow-discrete,
      overlay var(--motion-base) allow-discrete;
  }
  dialog[open]::backdrop { background: rgb(0 0 0 / 0.5); }
  @starting-style {
    dialog[open]::backdrop { background: rgb(0 0 0 / 0); }
  }

  /* .drawer — slides in from an edge instead of scaling. Put on a
     [popover] or <dialog>; set --drawer-from for the off-screen offset.
     Default: slides up from the bottom (thumb-reachable sheet). */
  .drawer {
    scale: 1;
    translate: var(--drawer-from, 0 100%);
  }
  .drawer:popover-open,
  .drawer[open] { translate: 0 0; scale: 1; }
  @starting-style {
    .drawer:popover-open,
    .drawer[open] { translate: var(--drawer-from, 0 100%); }
  }

  /* .collapsible — accordion/disclosure that animates to height:auto.
     interpolate-size: allow-keywords is enabled globally in reset.css;
     the tween is Chromium-only today (Chrome 129+), so it's @supports-
     gated — elsewhere the panel snaps open/closed (correct, just instant).
     Toggle via [data-open] or :has(> details[open]) at the call site. */
  @supports (interpolate-size: allow-keywords) {
    @media (prefers-reduced-motion: no-preference) {
      .collapsible {
        height: 0;
        overflow: clip;
        transition: height var(--motion-base) var(--motion-ease);
      }
      .collapsible[data-open] { height: auto; }
    }
  }

  /* Reduced-motion: overlays appear/disappear without scale/translate. */
  @media (prefers-reduced-motion: reduce) {
    [popover], dialog, .drawer {
      transition: opacity var(--motion-fast) linear,
                  display var(--motion-fast) allow-discrete,
                  overlay var(--motion-fast) allow-discrete;
      scale: 1;
      translate: 0 0;
    }
    @starting-style {
      [popover]:popover-open, dialog[open], .drawer:popover-open, .drawer[open] {
        scale: 1; translate: 0 0;
      }
    }
  }

  /* ── PAGE TRANSITIONS — cross-document View Transitions ──────── */
  /* The biggest 2026 win for a no-deps, server-rendered MPA like ours:
     native page transitions with ZERO JavaScript. Both pages opt in via
     @view-transition { navigation: auto }; the browser snapshots the old
     page, cross-fades to the new, and MORPHS any element that carries the
     same view-transition-name across both pages.

     Support: Chrome 126+, Safari 18.2+ (cross-document). Firefox doesn't
     ship cross-document yet — it simply navigates normally (instant, no
     break). Pure progressive enhancement.

     Gotchas (CSS-Tricks "Cross-Document View Transitions"): 4s hard
     timeout; render-blocking images / font-display:block / slow first
     paint silently skip it; programmatic location.href, cross-origin, and
     POST submits never trigger. */
  @media (prefers-reduced-motion: no-preference) {
    @view-transition { navigation: auto; }
  }

  /* Prevent stretched-bitmap distortion when an element's aspect-ratio
     differs from its replacement across pages. */
  ::view-transition-old(*),
  ::view-transition-new(*) { object-fit: cover; }

  /* Default root cross-fade — tokenized so data-motion presets retime it. */
  @media (prefers-reduced-motion: no-preference) {
    ::view-transition-old(root),
    ::view-transition-new(root) {
      animation-duration: var(--motion-base);
      animation-timing-function: var(--motion-ease);
    }
  }

  /* ── Named recipes — opt in by tagging elements ───────────────── */
  /* view-transition-name must be UNIQUE per page. Put ONE of these
     classes on the element that should persist/morph across navigations;
     the same class on the next page makes the browser morph between them.

       .vt-hero    → a hero image/figure morphs position+size page→page
       .vt-title   → a heading morphs (e.g. card title → detail title)
       .vt-nav     → a persistent nav/header stays put (no flicker)
       .vt-content → the main content region slides instead of cross-fading */
  .vt-hero    { view-transition-name: cx-hero; }
  .vt-title   { view-transition-name: cx-title; }
  .vt-nav     { view-transition-name: cx-nav; }
  .vt-content { view-transition-name: cx-content; }

  /* Content slide: old slides out left, new slides in from the right —
     spatial continuity (you moved "forward"). Tokenized + reduced-motion. */
  @media (prefers-reduced-motion: no-preference) {
    ::view-transition-old(cx-content) {
      animation: cx-vt-out var(--motion-base) var(--motion-ease) both;
    }
    ::view-transition-new(cx-content) {
      animation: cx-vt-in var(--motion-base) var(--motion-ease) both;
    }
    /* Keep the nav rock-steady — no cross-fade flicker on a persistent bar. */
    ::view-transition-group(cx-nav) { animation-duration: 0s; }
  }
  @keyframes cx-vt-out { to   { opacity: 0; translate: calc(-1 * var(--motion-travel)) 0; } }
  @keyframes cx-vt-in  { from { opacity: 0; translate: var(--motion-travel) 0; } }

  /* ── THUMB-ZONE — mobile-ergonomic layout primitives ─────────── */
  /* 75% of mobile interaction is thumb-driven (Hoober, n=1,333). The
     reachable arc is anchored at the BOTTOM; the top corners are the dead
     zone. So primary actions live in a bottom bar, sized for fingers and
     padded clear of the notch / home indicator.

     Touch-target floor: 44×44 CSS px (Apple HIG / WCAG 2.5.5 AAA). WCAG
     2.5.8 AA's 24px is a hard floor we never design TO. Interactive
     children of these primitives are forced to the 44px floor here.

     Safe areas need <meta name="viewport" content="...,viewport-fit=cover">
     for the insets to be non-zero; they resolve to 0 otherwise, so these
     primitives stay correct on every device. */

  /* .action-bar — fixed bottom action row in the thumb arc. */
  .action-bar,
  .reachable-nav {
    position: fixed;
    inset-inline: 0;
    inset-block-end: 0;
    z-index: 50;
    display: flex;
    gap: var(--space-xs, 0.75rem);
    align-items: center;
    background: var(--surface-1, Canvas);
    box-shadow: var(--shadow-3);
    /* clear the home indicator + notch side-insets */
    padding-block: var(--space-xs, 0.75rem);
    padding-block-end: calc(var(--space-xs, 0.75rem) + var(--safe-bottom));
    padding-inline-start: max(var(--space-s, 1rem), var(--safe-left));
    padding-inline-end:   max(var(--space-s, 1rem), var(--safe-right));
  }
  .reachable-nav { justify-content: space-around; }
  .action-bar    { justify-content: stretch; }
  .action-bar > * { flex: 1; }

  /* 44px touch-target floor for interactive children. */
  .action-bar > a,    .action-bar > button,
  .reachable-nav a,   .reachable-nav button {
    min-block-size: 44px;
    min-inline-size: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* .hero-full — full-height hero. Defaults to svh (smallest viewport):
     it fills on first paint WITHOUT overflow when browser chrome is shown,
     and does NOT jitter as the chrome animates — the failure mode of dvh.
     Use .hero-immersive (lvh) for an app-shell fill, or the --vh-dynamic
     token directly only when you genuinely want live-tracking height. */
  .hero-full {
    min-block-size: var(--vh-small);
    padding-block-start: max(var(--space-l, 2rem), var(--safe-top));
    padding-block-end:   max(var(--space-l, 2rem), var(--safe-bottom));
    display: grid;
    align-content: center;
  }
  .hero-immersive { min-block-size: var(--vh-large); }

  /* .safe-pad — pad an element clear of all four insets (e.g. a fixed
     top bar, an edge-to-edge sheet). */
  .safe-pad {
    padding-block-start: var(--safe-top);
    padding-block-end:   var(--safe-bottom);
    padding-inline-start: var(--safe-left);
    padding-inline-end:   var(--safe-right);
  }

}
