/* Apple-style motion, shared by site/ and site2/ (identical copies, kept in
   sync by tests). Owner ask 2026-09-24: scroll storytelling like apple.com plus
   iOS-style springs.

   Contract: this file only knows attributes and native elements. Pages opt in
   with data-m (scroll presets), data-m-part / data-m-step / data-m-mark
   (animated pieces), data-m-pin (the sticky frame of a pinned scene),
   data-m-fill (the inline span that fills), data-press (press feedback) and
   data-vt="header" (the header that stays put across page changes). Anything
   specific to one site lives in that site's own stylesheet.

   Interaction curves come from the employee system's
   common/static/common/motion.css (owner ask 2026-09-20), so the apps and the
   website move the same way.

   Scroll layer: html.m-native uses CSS scroll-driven animations; html.m-fallback
   (Firefox and friends) gets the same @keyframes, paused and seeked with a
   negative animation-delay from --m-p, which motion.js writes on every host.
   Each preset's range lives in --m-range / --m-start / --m-end, and both drivers
   read those, so the two paths cannot drift apart. Nothing below applies until
   html.m-live is set, and none of it applies with reduced motion: the default
   is the plain, fully visible page. */

@property --m-p { syntax: '<number>'; inherits: true; initial-value: 0; }
/* A piece's slice of its host's range. Not inherited: a nested piece that
   declares no slice of its own gets the whole range, not its parent's. */
@property --a { syntax: '<number>'; inherits: false; initial-value: 0; }
@property --b { syntax: '<number>'; inherits: false; initial-value: 1; }

:root {
  --motion-spring: cubic-bezier(.32, .72, 0, 1);   /* iOS sheet / navigation */
  --motion-bounce: cubic-bezier(.34, 1.4, .64, 1); /* small overshoot */
  --motion-exit: cubic-bezier(.4, 0, 1, 1);
  --motion-glide: cubic-bezier(.25, .9, .3, 1.04); /* fallback for the glide spring */
  --motion-pop: cubic-bezier(.34, 1.56, .64, 1);   /* fallback for the pop spring */
  --motion-fast: 180ms;
  --motion-base: 320ms;
  --motion-slow: 460ms;
  --motion-glide-duration: 570ms;
  --motion-pop-duration: 820ms;
}

/* Damped springs sampled into linear(): glide = damping .78 / response .42 s
   (2% overshoot), pop = damping .62 / response .5 s (8% overshoot). */
@supports (transition-timing-function: linear(0, 1)) {
  :root {
    --motion-glide: linear(0, 0.04 3.6%, 0.134 7.1%, 0.257 10.7%, 0.387 14.3%, 0.514 17.9%, 0.628 21.4%, 0.726 25%, 0.807 28.6%, 0.872 32.1%, 0.921 35.7%, 0.958 39.3%, 0.984 42.9%, 1.001 46.4%, 1.012 50%, 1.017 53.6%, 1.02 57.1%, 1.02 60.7%, 1.018 64.3%, 1.016 67.9%, 1.013 71.4%, 1.011 75%, 1.008 78.6%, 1.006 82.1%, 1.005 85.7%, 1.003 89.3%, 1.002 92.9%, 1.001 96.4%, 1);
    --motion-pop: linear(0, 0.058 3.6%, 0.196 7.1%, 0.37 10.7%, 0.548 14.3%, 0.709 17.9%, 0.843 21.4%, 0.944 25%, 1.014 28.6%, 1.057 32.1%, 1.079 35.7%, 1.083 39.3%, 1.078 42.9%, 1.066 46.4%, 1.051 50%, 1.036 53.6%, 1.023 57.1%, 1.012 60.7%, 1.004 64.3%, 0.998 67.9%, 0.995 71.4%, 0.993 75%, 0.993 78.6%, 0.994 82.1%, 0.995 85.7%, 0.996 89.3%, 0.997 92.9%, 0.998 96.4%, 1);
  }
}

/* ---------------------------------------------------------------------------
   Interaction: springs that answer a tap, an open or a page change.
   ------------------------------------------------------------------------ */
@media (prefers-reduced-motion: no-preference) {
  :root { interpolate-size: allow-keywords; }

  /* Press feedback: give under the finger, spring back on release. `scale` is
     its own property, so it composes with any transform the element has.
     --m-extra-transition carries the element's other transitions. */
  [data-press] {
    transition: scale var(--motion-base) var(--motion-bounce), var(--m-extra-transition, opacity 0s);
  }
  [data-press]:active:not(:disabled, [aria-disabled="true"]) {
    scale: .96;
    transition-duration: 90ms;
    transition-timing-function: var(--motion-spring);
  }

  /* Dialogs grow in with a slight overshoot and shrink back out. `display` and
     `overlay` are discrete participants, so the dialog stays in the top layer
     until the exit has played; engines without allow-discrete just close. */
  dialog {
    opacity: 0;
    scale: .95;
    translate: 0 6px;
    transition:
      opacity 200ms var(--motion-exit),
      scale 200ms var(--motion-exit),
      translate 200ms var(--motion-exit),
      overlay 200ms allow-discrete,
      display 200ms allow-discrete;
  }
  dialog[open] {
    opacity: 1;
    scale: 1;
    translate: 0 0;
    transition:
      opacity var(--motion-base) var(--motion-spring),
      scale var(--motion-slow) var(--motion-bounce),
      translate var(--motion-slow) var(--motion-bounce),
      overlay var(--motion-slow) allow-discrete,
      display var(--motion-slow) allow-discrete;
  }
  @starting-style {
    dialog[open] { opacity: 0; scale: .92; translate: 0 12px; }
  }
  /* Literal durations: ::backdrop does not inherit custom properties everywhere. */
  dialog::backdrop {
    opacity: 0;
    transition: opacity 200ms ease, overlay 200ms ease allow-discrete, display 200ms ease allow-discrete;
  }
  dialog[open]::backdrop { opacity: 1; }
  @starting-style {
    dialog[open]::backdrop { opacity: 0; }
  }

  /* Disclosures open and close to their content height. */
  details::details-content {
    height: 0;
    overflow: clip;
    transition: height var(--motion-base) var(--motion-spring), content-visibility var(--motion-base) allow-discrete;
  }
  details[open]::details-content { height: auto; }

  /* Page changes: the header stays, the page eases in. Same-origin only, so
     file:// previews simply navigate without it. */
  @view-transition { navigation: auto; }
  [data-vt="header"] { view-transition-name: m-header; }
  ::view-transition-group(m-header) { animation: none; }
  ::view-transition-old(root) { animation: m-page-out 180ms var(--motion-exit) both; }
  ::view-transition-new(root) { animation: m-page-in var(--motion-slow) var(--motion-spring) both; }

  @keyframes m-page-in { from { opacity: 0; transform: translateY(14px) scale(.99); } }
  @keyframes m-page-out { to { opacity: 0; } }
}

/* ---------------------------------------------------------------------------
   Scroll layer: presets. Ranges are data for both drivers (see header).
   ------------------------------------------------------------------------ */
[data-m="zoom"]  { --m-range: cover;   --m-start: 0;  --m-end: 50; }
[data-m="fill"]  { --m-range: cover;   --m-start: 20; --m-end: 58; }
[data-m="spot"]  { --m-range: cover;   --m-start: 6;  --m-end: 38; }
[data-m="rise"]  { --m-range: cover;   --m-start: 0;  --m-end: 30; }
[data-m="grow"]  { --m-range: cover;   --m-start: 0;  --m-end: 48; }
[data-m="line"]  { --m-range: cover;   --m-start: 14; --m-end: 50; }
[data-m="drift"] { --m-range: cover;   --m-start: 0;  --m-end: 100; }
[data-m="exit"]  { --m-range: exit;    --m-start: 0;  --m-end: 100; }
[data-m="scene"],
[data-m="track"],
[data-m="stage"] { --m-range: contain; --m-start: 0;  --m-end: 100; }

[data-m="spot"] { --m-anim: m-spot; }
[data-m="rise"] { --m-anim: m-rise; }
[data-m="grow"] { --m-anim: m-grow; }
[data-m="zoom"] :is(img, video) { --m-anim: m-zoom; }
[data-m="fill"] [data-m-fill] { --m-anim: m-fill; }
[data-m-part="rail-x"] { --m-anim: m-rail-x; }
[data-m-part="rail-y"] { --m-anim: m-rail-y; }
[data-m-part="track"] { --m-anim: m-track; }

/* Pinned scenes: step i of n owns the slice [i/n, (i+1)/n] of the scene. */
[data-m-steps="2"] { --n: 2; }
[data-m-steps="3"] { --n: 3; }
[data-m-steps="4"] { --n: 4; }
[data-m-steps="5"] { --n: 5; }
:is([data-m-step], [data-m-mark])[data-i="0"] { --i: 0; }
:is([data-m-step], [data-m-mark])[data-i="1"] { --i: 1; }
:is([data-m-step], [data-m-mark])[data-i="2"] { --i: 2; }
:is([data-m-step], [data-m-mark])[data-i="3"] { --i: 3; }
:is([data-m-step], [data-m-mark])[data-i="4"] { --i: 4; }
:is([data-m-step], [data-m-mark]) {
  --a: calc(var(--i, 0) / var(--n, 1));
  --b: calc((var(--i, 0) + 1) / var(--n, 1));
}
[data-m-step] { --m-anim: m-step; }
[data-m-step][data-i="0"] { --m-anim: m-step-first; }
[data-m-step]:is([data-last]) { --m-anim: m-step-last; }
[data-m-mark] { --m-anim: m-mark; }
[data-m-mark][data-i="0"] { --m-anim: m-mark-first; }
[data-m-mark]:is([data-last]) { --m-anim: m-mark-last; }

@media (prefers-reduced-motion: no-preference) {
  /* The pieces that move. */
  .m-live :is([data-m="spot"], [data-m="rise"], [data-m="grow"], [data-m="zoom"] :is(img, video),
      [data-m="fill"] [data-m-fill], [data-m-part], [data-m-step], [data-m-mark]) {
    animation-name: var(--m-anim, none);
    animation-timing-function: linear;
    animation-fill-mode: both;
  }

  @supports ((animation-timeline: view()) and (animation-range: entry)) {
    /* Inset 0: the default (auto) shrinks the view by the scroller's
       scroll-padding, which the fallback driver does not see. */
    .m-live.m-native [data-m] { view-timeline: --m-host block; view-timeline-inset: 0; }
    .m-live.m-native :is([data-m="spot"], [data-m="rise"], [data-m="grow"], [data-m="zoom"] :is(img, video),
        [data-m="fill"] [data-m-fill], [data-m-part], [data-m-step], [data-m-mark]) {
      animation-duration: auto;
      animation-timeline: --m-host;
      animation-range-start: var(--m-range) calc((var(--m-start) + (var(--m-end) - var(--m-start)) * var(--a, 0)) * 1%);
      animation-range-end: var(--m-range) calc((var(--m-start) + (var(--m-end) - var(--m-start)) * var(--b, 1)) * 1%);
    }
  }

  .m-live.m-fallback :is([data-m="spot"], [data-m="rise"], [data-m="grow"], [data-m="zoom"] :is(img, video),
      [data-m="fill"] [data-m-fill], [data-m-part], [data-m-step], [data-m-mark]) {
    animation-duration: 1s;
    animation-play-state: paused;
    animation-delay: calc(clamp(0, (var(--m-p) - var(--a, 0)) / (var(--b, 1) - var(--a, 0)), 1) * -1s);
  }

  /* Media in a zoom frame need the frame to clip them. */
  .m-live [data-m="zoom"] { overflow: clip; }
  .m-live [data-m="zoom"] :is(img, video) { will-change: scale; }

  /* Fill: the span paints the text itself: an opaque "unlit" layer that
     shrinks away from the start of the text, over the lit layer. Sites set
     --m-fill-lit (an image) and --m-fill-dim (an opaque colour). The host's own
     text background, if any, steps aside while the fill runs (only when the span
     is there: a host without one keeps its own text visible). */
  .m-live [data-m="fill"]:has([data-m-fill]) { background: none; }
  .m-live [data-m="fill"] [data-m-fill] {
    color: transparent;
    -webkit-text-fill-color: transparent;
    background-image:
      linear-gradient(var(--m-fill-dim, #3a3440), var(--m-fill-dim, #3a3440)),
      var(--m-fill-lit, linear-gradient(#fff, #fff));
    background-repeat: no-repeat;
    background-position: 100% 0, 0 0;
    background-size: 100% 100%, 100% 100%;
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-box-decoration-break: slice;
    box-decoration-break: slice;
  }

  /* Pinned scene: tall host, sticky frame, steps stacked in one cell. */
  .m-live [data-m="scene"] { height: calc(var(--n, 4) * 100svh + 60svh); }
  .m-live [data-m="scene"] > [data-m-pin] {
    position: sticky;
    top: 0;
    height: 100svh;
    overflow: clip;
  }
  .m-live [data-m="scene"] [data-m-stack] { display: grid; }
  .m-live [data-m="scene"] [data-m-stack] > [data-m-step] { grid-area: 1 / 1; }

  /* Horizontal track: vertical scroll drives the row sideways. Sites set
     --m-track-length to the row's overflow (row width minus frame width). */
  .m-live [data-m="track"] { height: calc(100svh + var(--m-track-length, 150vw)); }
  .m-live [data-m="track"] > [data-m-pin] {
    position: sticky;
    top: 0;
    height: 100svh;
    overflow: clip;
    container-type: inline-size;
  }
  .m-live [data-m-part="track"] { width: max-content; }

  @keyframes m-zoom { from { scale: 1.16; } to { scale: 1; } }
  @keyframes m-fill { from { background-size: 100% 100%, 100% 100%; } to { background-size: 0% 100%, 100% 100%; } }
  @keyframes m-spot { from { opacity: .22; } to { opacity: 1; } }
  @keyframes m-rise { from { opacity: 0; translate: 0 48px; } to { opacity: 1; translate: none; } }
  @keyframes m-grow { from { scale: .86; } to { scale: 1; } }
  @keyframes m-rail-x { from { scale: 0 1; } to { scale: 1 1; } }
  @keyframes m-rail-y { from { scale: 1 0; } to { scale: 1 1; } }
  @keyframes m-track { from { translate: 0 0; } to { translate: calc(-100% + 100cqw) 0; } }
  @keyframes m-step {
    0% { opacity: 0; translate: 0 36px; }
    18%, 82% { opacity: 1; translate: none; }
    100% { opacity: 0; translate: 0 -36px; }
  }
  @keyframes m-step-first {
    0%, 82% { opacity: 1; translate: none; }
    100% { opacity: 0; translate: 0 -36px; }
  }
  @keyframes m-step-last {
    0% { opacity: 0; translate: 0 36px; }
    18%, 100% { opacity: 1; translate: none; }
  }
  @keyframes m-mark {
    0%, 100% { opacity: .38; }
    18%, 82% { opacity: 1; }
  }
  @keyframes m-mark-first {
    0%, 82% { opacity: 1; }
    100% { opacity: .38; }
  }
  @keyframes m-mark-last {
    0% { opacity: .38; }
    18%, 100% { opacity: 1; }
  }
}

/* ---------------------------------------------------------------------------
   Reduced motion (and ?motion=off) stops everything, not only what this file
   adds: every element is held to a near-zero duration so transitionend and
   animationend still fire and nothing waits forever.
   ------------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
  *, *::before, *::after {
    transition-duration: .01ms !important;
    transition-delay: 0ms !important;
    animation-duration: .01ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
  }
}
.m-off *, .m-off *::before, .m-off *::after {
  transition-duration: .01ms !important;
  transition-delay: 0ms !important;
  animation-duration: .01ms !important;
  animation-delay: 0ms !important;
  animation-iteration-count: 1 !important;
}
