/* Hanetic Design System — motion.css (Phase 3B)
   ------------------------------------------------------------------
   Canonical motion layer. Duration/easing tokens live in tokens.css
   (--hx-dur-fast / --hx-dur-med / --hx-ease); this file owns keyframes,
   animatable custom-property registrations, and the accessibility kill
   switch. Every page that uses the design system links this file
   (Portal/Dev/Cars/Promo/Finance as of Phase 3B; Travel joins in
   Phase 4). Phase 3C will add route-transition and scroll-reveal
   primitives here — do not scatter new @keyframes in theme files.

   Contract:
   - Motion must aid orientation/feedback, never gate or hide data.
   - Anything longer than --hx-dur-med needs a stated reason.
   - The reduced-motion switch below is the single source of truth for
     the new-system pages (legacy styles.css keeps its own equivalent
     for the pages still on it). */

/* Registered so the ticker-rail fade distance (themes/finance.css §8)
   can animate smoothly under a scroll-driven timeline instead of
   flipping discretely. Harmless where @property is unsupported: the
   fallback is the constant fade. */
@property --hx-rail-fade {
  syntax: "<length>";
  inherits: false;
  initial-value: 28px;
}

/* Scroll-driven: as a horizontal rail approaches its end, remove the
   trailing-edge fade so the last item is fully legible at rest. */
@keyframes hx-rail-unfade {
  to {
    --hx-rail-fade: 0px;
  }
}

/* ---- Route transitions (Phase 3C, choreography M3) ----
   Progressive cross-document View Transitions between the pages that link
   this file — all six as of Phase 4A (Portal/Dev/Cars/Promo/Finance/
   Travel). Browsers without the feature ignore the opt-in at-rule and
   every transition pseudo entirely: navigation stays a plain hard cut,
   no side effects. The shared shell gets its own named group so it reads
   as one persistent element while page content transitions beneath it.
   Wrapped in no-preference so reduced-motion users always get hard cuts.
   M3 choreography: outgoing page fades, incoming page fades in with a
   slight rise — same entrance family as hx-rise/scroll-reveal. Pure
   overlay animation on transition snapshots; layout (and therefore
   safe-area handling) is untouched. The hx-vt-out/hx-vt-in keyframes
   these reference are defined AFTER the scroll-reveal section below —
   check_web_invariants.py's motion-gating checks key off raw source
   order (first opacity:0 rule must be the reveal gate; the no-preference
   gate must precede the transition opt-in at-rule, comments included),
   and keyframe definitions hoist anyway. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }

  .site-header.hanetic-shell {
    view-transition-name: hanetic-shell;
  }

  /* Phase V-1: on desktop the rail (base.css) replaces the top shell as
     the persistent chassis element, so the named group moves with it —
     the rail stays static across a navigation while the canvas
     transitions beneath it ("OS 換視窗" per the brief §2a). Only one
     element may carry a given view-transition-name at a time, so the
     shell's own name must be cleared here, not just visually hidden. */
  @media (min-width: 1181px) {
    .site-header.hanetic-shell {
      view-transition-name: none;
    }

    .hx-rail {
      view-transition-name: hanetic-shell;
    }
  }

  ::view-transition-old(root) {
    animation: hx-vt-out var(--hx-dur-med) var(--hx-ease) both;
  }

  ::view-transition-new(root) {
    animation: hx-vt-in var(--hx-dur-med) var(--hx-ease) both;
  }

  ::view-transition-group(hanetic-shell) {
    animation-duration: var(--hx-dur-med);
    animation-timing-function: var(--hx-ease);
  }
}

/* ---- Scroll reveal (Phase 3C) ----
   Used by js/lib/reveal.js on Dev/Cars/Promo reading surfaces. Elements
   opt in via data-hx-reveal. Progressive by construction: the hidden
   state only applies once the script has run (it stamps hx-reveal-armed
   on <html> AFTER immediately revealing everything already in view), so
   no-JS, script-failure and reduced-motion all render content plainly
   visible. */
@media (prefers-reduced-motion: no-preference) {
  .hx-reveal-armed [data-hx-reveal] {
    opacity: 0;
    transform: translateY(14px);
    transition:
      opacity 420ms var(--hx-ease),
      transform 420ms var(--hx-ease);
  }

  .hx-reveal-armed [data-hx-reveal].hx-revealed {
    opacity: 1;
    transform: none;
  }
}

/* Route-transition keyframes (M3) — referenced by the view-transition
   block near the top of this file; defined down here on purpose, see the
   source-order note in that block's comment. */
@keyframes hx-vt-out {
  to {
    opacity: 0;
  }
}

@keyframes hx-vt-in {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
}

/* ---- Shared interaction vocabulary (Phase M1) ----
   Reusable classes, opt-in from markup (or a JS template rendering
   markup). Scaffolding (geometry, resting state) sits outside the
   media gate; anything that moves sits inside no-preference, so
   reduced-motion users get the plain resting state — never a hidden or
   half-animated one. No JS required by any of these. */

/* Sheen: one diagonal highlight sweeps across a card on hover/focus.
   Host gets position:relative + overflow:hidden — only apply to cards
   that don't need visible overflow. Resting state is fully invisible. */
.hx-sheen {
  position: relative;
  overflow: hidden;
}

.hx-sheen::after {
  content: "";
  position: absolute;
  inset: -1px;
  background: linear-gradient(105deg, transparent 42%, rgba(255, 255, 255, 0.16) 50%, transparent 58%);
  transform: translateX(-130%);
  opacity: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .hx-sheen::after {
    /* --hx-dur-slow: reason stated at the token definition (a sweep must
       traverse the card; --hx-dur-med reads as a flicker). */
    transition: transform var(--hx-dur-slow) var(--hx-ease), opacity var(--hx-dur-fast) ease;
  }

  .hx-sheen:hover::after,
  .hx-sheen:focus-visible::after {
    opacity: 1;
    transform: translateX(130%);
  }
}

/* Stagger: direct children of an .hx-stagger container rise in
   sequentially on load (and again if the container's innerHTML is
   swapped by a renderer — same replay behavior Finance's .enter-item
   already has). Entirely inside the gate: reduced-motion never sees the
   keyframe's hidden `from` state, and no-JS pages render it fine since
   this is pure CSS. Delays capped at 8 steps; later children share the
   last step so a long list never waits out a long tail. */
@keyframes hx-rise {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Portal Reveal follow-through: the five child pages retain their existing
   navigation structures and geometry. Their complete navigation surface
   (background included) only fades from transparent to its resting opacity;
   it is never inert and does not move. */
@keyframes hx-child-nav-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

@media (prefers-reduced-motion: no-preference) {
body:not(.portal-body):not(.finance-body) .hanetic-shell,
body:not(.portal-body) .hx-rail {
    animation: hx-child-nav-in 240ms ease-out;
  }
}

@media (prefers-reduced-motion: no-preference) {
  .hx-stagger > * {
    /* 460ms > --hx-dur-med, stated reason: matches the 420ms scroll-reveal
       above — entrance motion shares one speed family, distinct from the
       160/220ms feedback family. */
    animation: hx-rise 460ms var(--hx-ease) backwards;
  }

  .hx-stagger > :nth-child(1) { animation-delay: 40ms; }
  .hx-stagger > :nth-child(2) { animation-delay: 95ms; }
  .hx-stagger > :nth-child(3) { animation-delay: 150ms; }
  .hx-stagger > :nth-child(4) { animation-delay: 205ms; }
  .hx-stagger > :nth-child(5) { animation-delay: 260ms; }
  .hx-stagger > :nth-child(6) { animation-delay: 315ms; }
  .hx-stagger > :nth-child(7) { animation-delay: 370ms; }
  .hx-stagger > :nth-child(n + 8) { animation-delay: 425ms; }
}

/* Lift: hover/focus raises a card via transform while a pre-stacked,
   deeper-shadow layer crossfades in via opacity — box-shadow itself is
   never animated (motion contract §1.3: only transform/opacity move),
   only the ::before's opacity transitions between two already-resolved
   shadow states. Host needs position:relative for the ::before to anchor
   against (harmless on the common case: a static-flow grid/card item).
   Entirely inside the hover-capable + no-preference gate per the
   Interaction Identity Brief's device strategy (§4): touch devices get
   no hx-lift hover/press state at all (avoids iOS "sticky hover"); they
   rely on each page's own tap-feedback :active rule instead. */
.hx-lift {
  position: relative;
}

.hx-lift::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  box-shadow: var(--hx-shadow-float);
  opacity: 0;
  pointer-events: none;
}

@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
  .hx-lift {
    transition: transform var(--hx-dur-fast) var(--hx-ease);
  }

  .hx-lift::before {
    transition: opacity var(--hx-dur-fast) var(--hx-ease);
  }

  .hx-lift:hover,
  .hx-lift:focus-visible {
    transform: translateY(-4px);
  }

  .hx-lift:hover::before,
  .hx-lift:focus-visible::before {
    opacity: 1;
  }
}

/* Drift: an ambient, self-playing background-position wobble for a page's
   fixed atmosphere layer (never a photo layer — gradient/scrim layers
   only, per the brief). This is the one sanctioned exception to "motion
   must be triggered by user action" (principle §1.1): it reads as
   environment, not a UI element, and the amplitude sits under the notice
   threshold. background-position only — never layout, filter, or
   box-shadow. Mount by adding both `hx-drift` and the page's own root
   class to the <body> (a real class attribute, so the animation can be
   assigned once here while each theme file supplies the background-size
   that makes the drift have room to actually pan — see themes/portal.css
   for the mount). */
@keyframes hx-drift-bg {
  0%,
  100% {
    background-position: 50% 50%;
  }
  50% {
    background-position: 52% 48%;
  }
}

@media (prefers-reduced-motion: no-preference) {
  .hx-drift::after {
    animation: hx-drift-bg 120s var(--hx-ease) infinite;
  }
}

/* Parallax (Phase I-2): a hero photo pans slightly slower than the page
   scrolls, reading as depth ("window outside the frame"). Pure CSS
   scroll-driven animation — animation-timeline: scroll() ties progress to
   how far the document has scrolled, not to the element's own
   visibility. No JS fallback by design: browsers without CSS
   scroll-driven animations simply never match the @supports block below
   and render the plain, unscaled photo — a fully static resting state is
   already "complete" per principle §4, nothing to polyfill.

   `scale` and `translate` animate TOGETHER in the same keyframe, both
   starting at their identity value (scale 1, translate 0) — this is the
   part that took a real screenshot diff to catch: an earlier draft held
   `scale` constant at 1.16 outside the keyframe (as permanent
   "scaffolding," the way .hx-lift's shadow layer does) so only
   `translate` animated, but a constant non-1 scale is itself a visible
   difference at scroll=0 on any engine where @supports actually matches
   — this environment's WebKit does. Animating both from a shared
   identity origin makes scroll=0 exactly reproduce the pre-parallax
   image (confirmed via WebKit screenshot byte-comparison, see
   reports/phaseI2/NOTES.md), while the ratio between scale-buffer and
   translate stays constant at every point along the timeline (both grow
   linearly from 0 together), so the edge-safety math below still holds
   throughout the scroll, not just at the endpoint.

   Fully inside BOTH @supports and prefers-reduced-motion: no-preference,
   so unsupported browsers and reduced-motion users render pixel-
   identical to a page that never mounted this at all.

   Mounted via a direct element selector in themes/{portal,cars}.css
   (`.portal-photo-hero > img`, `.car-hero > img`), not an HTML class:
   I-2's allowed-files list is CSS-only, no HTML edits this phase.

   --hx-parallax-shift buffer math: the keyframe's end scale of 1.16
   gives (1.16-1)/2 = 8% of the hero's rendered height as edge buffer on
   each side. A translate of T only stays safely inside that buffer while
   hero height H satisfies H >= T / 0.08. The default (20px) is safe down
   to H >= 250px — covers every content-driven "auto height" hero across
   phone/tablet/short-landscape. The 40px bump is scoped to >=1181px
   only, where both Portal's and Cars' hero use the shared `min(720px,
   calc(100svh - 128px))` cap (720px >= 500px required for 40px) — see
   the mount comments in themes/{portal,cars}.css for the one confirmed
   exception (Portal goes min-height:auto at tablet width, which is why
   tablet stays on the conservative default instead of also getting the
   bump). */
@keyframes hx-parallax-pan {
  from {
    scale: 1;
    translate: 0 0;
  }
  to {
    scale: 1.16;
    translate: 0 var(--hx-parallax-shift, 20px);
  }
}

@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    .hx-parallax {
      animation: hx-parallax-pan linear both;
      animation-timeline: scroll(root block);
      animation-range: 0px 600px;
      --hx-parallax-shift: 20px;
    }

    @media (min-width: 1181px) {
      .hx-parallax {
        --hx-parallax-shift: 40px;
      }
    }

    /* Brief §9 I-2: short landscape halves the cap regardless of width —
       wins over the >=1181px bump above via source order for the
       wide-but-short combination too. */
    @media (orientation: landscape) and (max-height: 560px) {
      .hx-parallax {
        --hx-parallax-shift: 20px;
      }
    }
  }
}

/* Pointer pipeline (Phase I-3): hx-tilt / hx-spotlight. Both are pure CSS
   consumers of --hx-mx/--hx-my/--hx-pd, written by the single shared
   js/lib/pointer-fx.js pipeline (Brief §5) onto [data-hx-pointer]
   containers (plus one structural fallback for JS-rendered content — see
   that file's header comment). Resting state (before any pointer has
   entered, or reduced-motion/touch/harness where the script never runs)
   falls back to the var()'s default: dead-center for tilt (no rotation)
   and invisible for spotlight (opacity 0) — the "silent instrument" reads
   identically to a page that never mounted these at all. Entirely gated
   behind (hover: hover) and (pointer: fine) so touch devices never see
   a stuck/half-active state (no pointermove ever fires to update the
   vars), and reduced-motion so the tilt/opacity changes never animate for
   users who opted out. */
@media (hover: hover) and (pointer: fine) {
  @media (prefers-reduced-motion: no-preference) {
    .hx-tilt {
      transition: transform var(--hx-dur-med) var(--hx-ease);
    }

    .hx-tilt:hover,
    .hx-tilt:focus-visible {
      transform:
        perspective(800px)
        rotateX(calc((0.5 - var(--hx-my, 0.5)) * 6deg))
        rotateY(calc((var(--hx-mx, 0.5) - 0.5) * 6deg));
    }

    .hx-spotlight {
      position: relative;
      /* Default radius/intensity match Portal's dock-card spec (§3); Cars'
         "flashlight" mount overrides both, wider and stronger, via these
         same two variables (see themes/cars.css). */
      --hx-spotlight-radius: 240px;
      --hx-spotlight-intensity: 0.06;
    }

    .hx-spotlight::after {
      content: "";
      position: absolute;
      inset: 0;
      border-radius: inherit;
      pointer-events: none;
      opacity: 0;
      transition: opacity var(--hx-dur-med) var(--hx-ease);
      background: radial-gradient(
        var(--hx-spotlight-radius) at calc(var(--hx-mx, 0.5) * 100%) calc(var(--hx-my, 0.5) * 100%),
        rgba(255, 255, 255, var(--hx-spotlight-intensity)),
        transparent 70%
      );
    }

    .hx-spotlight:hover::after,
    .hx-spotlight:focus-visible::after {
      opacity: 1;
    }
  }
}

/* Update pulse: reuse .hx-value-changed (defined below) — Phase M1 made
   it the shared vocabulary for "this value just changed" everywhere
   (Finance figures via js/finance/motion.js, Portal live-status via
   js/portal/live-status.js). One soft flash, no loop. */

/* ---- Legacy Finance keyframes (Phase 4B port) ----
   Moved verbatim from styles.css when finance-core.css was generated
   (the port strips keyframes so this file stays the single home).
   Referenced by the ported .enter-item/.view-page/.detail-drawer/
   .status-pill.pulse/.icon-button.refreshing rules. */
@keyframes viewIn {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes itemIn {
  from {
    opacity: 0;
    transform: translateY(8px) scale(0.985);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

@keyframes drawerIn {
  from {
    opacity: 0;
    transform: translateY(-6px) scaleY(0.965);
  }
  to {
    opacity: 1;
    transform: translateY(0) scaleY(1);
  }
}

@keyframes pulse {
  0%,
  100% {
    opacity: 0.72;
    box-shadow: 0 0 0 0 rgba(67, 140, 74, 0);
  }
  50% {
    opacity: 1;
    box-shadow: 0 0 0 5px rgba(67, 140, 74, 0.12);
  }
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

/* ---- Refresh pulse ----
   Toggled on the connection badge while a fetch is in flight (Finance
   main.js) — moved here from components.css in Phase 3D so every
   keyframe lives in this file. Also unifies Finance onto hx-pulse: this
   file loads after legacy styles.css, so the rule below supersedes the
   legacy .status-pill.pulse animation (same 920ms opacity pulse). */
@keyframes hx-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}

.status-pill.pulse {
  animation: hx-pulse 920ms ease-in-out infinite;
}

/* ---- Changed-value feedback (Phase 3C, Finance) ----
   js/finance/motion.js adds hx-value-changed to the key figures whose
   value actually changed between snapshots. One soft pulse, no loop —
   feedback, not decoration. */
@keyframes hx-value-flash {
  0% {
    background-color: rgba(47, 139, 101, 0.22);
    box-shadow: 0 0 0 4px rgba(47, 139, 101, 0.12);
  }
  100% {
    background-color: transparent;
    box-shadow: 0 0 0 4px transparent;
  }
}

.hx-value-changed {
  border-radius: 10px;
  animation: hx-value-flash 900ms var(--hx-ease) 1;
}

/* ---- Accessibility kill switch ----
   Scroll-driven animations are exempt on purpose: they are scroll-linked
   state (like the rail fade), not self-playing motion, and zeroing
   duration does not affect progress-based timelines anyway. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
