/* ============================================================================
   VITRINE — MOTION
   assets/vitrine.motion.css

   Requires: tokens · base
   Behaviour: assets/vitrine.motion.js

   ---------------------------------------------------------------------------
   THIS FILE DOES NOT OWN MOST OF THE MOTION IN THE PRODUCT

   A component's motion belongs to the component: a button's press dip lives
   in vitrine.button.css, an overlay's entrance in vitrine.overlay.css. A
   central motion file that reaches back into every component to animate it
   is how a design system ends up with two sources for one transition and a
   fight over which wins.

   What lives HERE is only what is genuinely cross-cutting:
     · page transitions          no component owns a navigation
     · shared-element artwork    it belongs to two surfaces at once
     · the equaliser             used by BOTH the track row and artwork, and
                                 previously duplicated in both
     · the policy                stated once, enforced in tokens

   ---------------------------------------------------------------------------
   THE POLICY

   1. MOTION EXPLAINS CAUSALITY. If an animation does not say where something
      came from, where it went, or that the system heard you, it is deleted.

   2. NOTHING OVERSHOOTS. There is deliberately no spring easing token.
      Springy motion is what "cheap animation" means.

   3. NOTHING ANIMATES ON THE AUDIO PATH. No transition may sit between a tap
      and a sound. Play is the one control with no entrance.

   4. TRANSFORM AND OPACITY ONLY. Animating width, height, top or margin
      relayouts the page every frame. Audited: zero layout-triggering
      properties are transitioned anywhere in this system.

   5. EVERY DURATION COMES FROM A TOKEN, so reduced-motion is one override
      rather than fifty.

   ---------------------------------------------------------------------------
   WHAT ANIMATES, AND WHAT DELIBERATELY DOES NOT

     buttons      press dip 80ms · fill change 140ms
     icons        colour + press only. Geometry NEVER animates: a play/pause
                  swap is an attribute flip, not a morph, because the most
                  pressed control in the app must not have a 200ms tell
     navigation   the active marker grows from 40% to full
     cards        the play affordance lifts 8px and fades in
     tabs         the segment marker slides
     modals       fade + 8px rise
     sheets       translate from the edge they belong to
     dropdowns    fade + 3% scale from the trigger's corner
     page         cross-fade via View Transitions
     player       artwork travels between docked and immersive
     artwork      cross-fade on load · ambient wash over 1100ms
     favourite    one scale pulse, no overshoot
     play/pause   NOTHING. See rule 3.
     loading      spinner rotation · skeletons are STATIC
     toasts       fade + 12px rise

     NOT ANIMATED, on purpose:
       hover on artwork (no zoom, no tilt)   list rows appearing
       text                                  numbers counting up
       skeletons (no shimmer)                page content on first paint
       grid reflow                           the sidebar collapse
   ========================================================================== */


/* ==========================================================================
   §1 · PAGE TRANSITIONS
   A soft navigation replaces the content region. Without a transition the
   swap is a hard cut; with one it reads as the same surface changing.

   Deliberately a cross-fade and nothing more. A directional slide implies a
   spatial relationship between pages that this product does not have — you
   can reach an album from three different places.
   ========================================================================== */

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: var(--vt-dur-base);
  animation-timing-function: var(--vt-ease-enter);
}

/* The old page leaves slightly faster than the new one arrives, so the two
   never both sit at 50% and grey the screen mid-swap. */
::view-transition-old(root) { animation-duration: var(--vt-dur-quick); }

/* The dock and the sidebar do NOT participate. They persist across a
   navigation, and cross-fading something that did not change is the single
   most common View Transitions mistake — the player visibly blinks while
   the audio keeps playing. */
.vt-dock, .vt-sidebar { view-transition-name: none; }


/* ==========================================================================
   §2 · SHARED ELEMENT — the artwork
   The one piece of motion in the product that is allowed to be the point.
   It exists to prove the player is ONE object with two registers rather
   than two screens.

   OWNERSHIP: only one element may hold a given view-transition-name at a
   time or the API throws and drops the whole transition. motion.js hands the
   name to whichever surface is on screen.
   ========================================================================== */

::view-transition-group(vt-nowart) {
  animation-duration: var(--vt-dur-slower);
  animation-timing-function: var(--vt-ease-enter);
}
::view-transition-old(vt-nowart),
::view-transition-new(vt-nowart) {
  /* The artwork is the same image at two sizes, so cross-fading it produces
     a visible double exposure. Holding both at full opacity lets the group's
     position and scale animation do the work alone. */
  animation: none;
  mix-blend-mode: normal;
  block-size: 100%;
  object-fit: cover;
}


/* ==========================================================================
   §3 · THE EQUALISER
   Used by the track row AND by artwork, and it was previously defined twice
   with two different class names for one mark. One definition now; both
   components position it and neither styles it.
   ========================================================================== */

.vt-eq {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  block-size: 0.875rem;
}

.vt-eq__bar {
  inline-size: 2px;
  block-size: 100%;
  border-radius: var(--vt-radius-full);
  background-color: var(--vt-accent);
  transform-origin: 50% 100%;
  animation: vt-eq var(--vt-dur-eq) var(--vt-ease-linear) infinite alternate;
}
/* Negative delays start each bar mid-cycle, so the three are never in sync.
   Three bars rising together reads as a loading indicator, not as audio. */
.vt-eq__bar:nth-child(2) { animation-delay: calc(var(--vt-dur-eq) / -3); }
.vt-eq__bar:nth-child(3) { animation-delay: calc(var(--vt-dur-eq) / -1.5); }

@keyframes vt-eq {
  from { transform: scaleY(0.28); }
  to   { transform: scaleY(1); }
}

/* Paused is a real state, distinct from playing and from stopped: the bars
   freeze rather than disappear, so the row still says "this is where you
   are". */
[data-vt-paused] .vt-eq__bar {
  animation-play-state: paused;
  transform: scaleY(0.45);
}


/* ==========================================================================
   §4 · ENTRANCE
   Deliberately minimal, and deliberately NOT applied to lists or grids.

   Staggering a grid of album covers looks impressive once and costs the
   user 400ms on every navigation forever. Content that was already there
   should not perform an entrance — only things that genuinely just arrived.
   ========================================================================== */

/* For content that truly appears in place: search results replacing other
   search results, a section revealed by an action. */
.vt-enter {
  animation: vt-enter var(--vt-dur-base) var(--vt-ease-enter) both;
}
@keyframes vt-enter {
  from { opacity: 0; translate: 0 var(--vt-space-2); }
  to   { opacity: 1; translate: 0 0; }
}

/* The mini player rising into the dock the first time playback starts. It
   happens ONCE per session — the dock appearing is a genuine state change,
   and after that it is furniture. */
.vt-dock[data-vt-entering] {
  animation: vt-dock-in var(--vt-dur-slow) var(--vt-ease-enter) both;
}
@keyframes vt-dock-in {
  from { translate: 0 100%; }
  to   { translate: 0 0; }
}


/* ==========================================================================
   §5 · PERFORMANCE
   ========================================================================== */

/* `will-change` is a promise to the compositor that costs memory for as long
   as it is set. It goes on ONLY while a thing is actually moving, and
   motion.js removes it afterwards. A permanent will-change on every card is
   worse than none. */
[data-vt-animating] { will-change: transform, opacity; }

/* An off-screen rail does not need to be laid out or painted while the page
   scrolls past it. `auto` lets the engine skip it and still keeps it
   findable by in-page search and by assistive tech. */
.vt-cardrail { content-visibility: auto; contain-intrinsic-size: auto 13rem; }

/* The ambient wash is the one long animation in the product, and it paints a
   full-viewport gradient. Promoting it to its own layer stops it repainting
   everything beneath it every frame of that 1100ms. */
.vt-featured__ambient { will-change: background; }


/* ==========================================================================
   §6 · REDUCED MOTION — the canonical policy
   Stated in three places and enforced in one:
     tokens  collapses every --vt-dur-* to 1ms
     base    a backstop for any literal duration that slipped through
     here    the rules that need more than a duration change

   1ms rather than 0 throughout, so transitionend still fires and nothing
   that waits on a transition breaks.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {

  /* Page transitions are a travelling animation with no state to preserve.
     They are removed rather than shortened. */
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }

  /* The equaliser conveys "playing", which the mark's PRESENCE already
     conveys. It holds a static stepped shape instead — still legible as an
     equaliser, no motion. */
  .vt-eq__bar { animation: none; transform: scaleY(0.5); }
  .vt-eq__bar:nth-child(2) { transform: scaleY(1); }
  .vt-eq__bar:nth-child(3) { transform: scaleY(0.7); }

  .vt-enter,
  .vt-dock[data-vt-entering] { animation: none; }

  /* A spinner still has to say "working". Rotation becomes a pulse rather
     than being removed — a motionless spinner communicates nothing, which is
     worse than no indicator at all.

     THE KEYFRAMES LIVE HERE; THE RULES LIVE WITH THEIR COMPONENTS.

     This paragraph used to end at the closing brace with nothing under it —
     the policy file described the pulse and never declared it, while two
     sheets each defined their own copy of the same keyframes and a third
     referenced them without defining any. One definition, in the sheet that
     is loaded on every surface including auth.

     The rules themselves stay in vitrine.button.css, vitrine.input.css and
     vitrine.player.css, because a component's reduced-motion behaviour
     belongs with the component — a page that does not load the player does
     not need the player's fallback. Each is scoped under .vt-app and marked
     important so it outranks the motion floor in vitrine.base.css §4, which
     otherwise pins every animation to 1ms and a single iteration and turns
     this fallback into the motionless spinner it exists to prevent. */
  @keyframes vt-pulse { from { opacity: 0.25; } to { opacity: 1; } }
}
