/*
 * SUNO - Motion
 * ============================================================================
 * Loaded after index.css. The only file that reads the motion tokens in
 * tokens.css §13.
 *
 * ---------------------------------------------------------------------------
 * THE RULE THAT SHAPES EVERY RULE BELOW
 * ---------------------------------------------------------------------------
 * Motion is opt-IN, never opt-out.
 *
 * The usual way to build a scroll reveal is to hide the element in the base
 * rule (`opacity: 0`) and let an animation bring it back. That leaves the
 * content permanently invisible for anyone who has asked their OS to reduce
 * motion, and invisible if the animation never fires for any other reason.
 * It is the most common way an "accessible" animation locks people out.
 *
 * So: every animation here lives inside
 *     @media (prefers-reduced-motion: no-preference)
 * and the hidden state exists ONLY inside a @keyframes `from`. With the media
 * query unmatched, none of these rules apply and every element renders in its
 * final, visible, un-transformed state. Reduced motion degrades to no motion,
 * which is the correct outcome, rather than to no content.
 *
 * index.css also carries a global `animation: none !important` kill-switch under
 * prefers-reduced-motion: reduce. That is a second, independent net. Neither one
 * relies on the other.
 *
 * ---------------------------------------------------------------------------
 * WHAT MOVES, AND WHY
 * ---------------------------------------------------------------------------
 * This dispatches emergency alerts. Motion here has to earn its place by
 * explaining something, and it must never delay an operator who already knows
 * what they came to do:
 *
 *   - entry stagger    shows that a list arrived in an order, and which item is
 *                      newest. Capped so a long list is never gated on it.
 *   - the reveal       the rendered alert is the product's payoff. It gets the
 *                      one genuinely expressive moment in the app.
 *   - tactile feedback a control that lifts to the cursor and gives under the
 *                      press tells you it is live before you commit to it. This
 *                      matters most on the irreversible actions.
 *   - progress         a bar that moves because the server said so, versus one
 *                      that moves to look busy. Only the first is honest.
 *
 * Everything animates transform and opacity only -- both composited, neither
 * triggering layout. No `top`/`left`/`width`/`height` animation anywhere.
 */


/* ===========================================================================
   1. PAGE AND SECTION ENTRY

   `.suno-enter` on a container animates its direct children in sequence.
   Delays are per-child via :nth-child rather than a JS loop or an inline style,
   so server-rendered and JS-injected markup behave identically.

   Capped at 10. Past that the delay would exceed half a second and start
   costing the operator time; item 11 onward simply arrives with item 10. That
   cap is a deliberate limit, not an oversight -- history and media tables here
   can run to hundreds of rows.
   =========================================================================== */

@keyframes suno-rise {
  from { opacity: 0; transform: translate3d(0, 10px, 0); }
  to   { opacity: 1; transform: none; }
}

@keyframes suno-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Used by the reveal in §2: a little scale as well as a lift, which reads as
   "arriving" rather than "sliding in from somewhere". */
@keyframes suno-reveal {
  from { opacity: 0; transform: translate3d(0, 14px, 0) scale(0.985); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: no-preference) {

  .suno-enter > * {
    animation: suno-rise var(--suno-duration-slow) var(--suno-ease-out) both;
  }

  .suno-enter > *:nth-child(1) { animation-delay: 0ms; }
  .suno-enter > *:nth-child(2) { animation-delay: calc(var(--suno-stagger) * 1); }
  .suno-enter > *:nth-child(3) { animation-delay: calc(var(--suno-stagger) * 2); }
  .suno-enter > *:nth-child(4) { animation-delay: calc(var(--suno-stagger) * 3); }
  .suno-enter > *:nth-child(5) { animation-delay: calc(var(--suno-stagger) * 4); }
  .suno-enter > *:nth-child(6) { animation-delay: calc(var(--suno-stagger) * 5); }
  .suno-enter > *:nth-child(7) { animation-delay: calc(var(--suno-stagger) * 6); }
  .suno-enter > *:nth-child(8) { animation-delay: calc(var(--suno-stagger) * 7); }
  .suno-enter > *:nth-child(9) { animation-delay: calc(var(--suno-stagger) * 8); }
  .suno-enter > *:nth-child(n+10) { animation-delay: calc(var(--suno-stagger) * 9); }

  /* A single element that should arrive on its own, no sequence. */
  .suno-rise { animation: suno-rise var(--suno-duration-slow) var(--suno-ease-out) both; }
  .suno-fade { animation: suno-fade var(--suno-duration-base) var(--suno-ease-out) both; }
}


/* ===========================================================================
   2. SCROLL-DRIVEN REVEAL - removed, and why not to bring it back
   ===========================================================================

   `.suno-onscroll` tied the rise in §1 to scroll position instead of page load,
   via `animation-timeline: view()` -- CSS-only, no IntersectionObserver, one
   independent timeline per element. It read well and it was wrong.

   REMOVED. It was a scroll-driven rise using
   `animation-timeline: view()` with `animation-range: cover 15% cover 45%`.

   It broke the rule at the top of this file, in the one way that matters: with
   `both` fill, an element whose scroll progress never reaches the range start holds
   the keyframe's `from` -- which is `opacity: 0`. A history page with one or two
   entries barely scrolls, so a card could render permanently INVISIBLE. The
   @supports fallback does not help, because the browsers that strand it are exactly
   the ones that support view().

   Moving the range to `cover 0%` fixes the common case but not the real one: when a
   view timeline is inactive the animation never advances, and CSS has no way to
   detect that and opt out. Forcing `opacity: 1` outside the range removes the risk
   but reintroduces a flash as the element re-enters it.

   So: content visibility is not something to make conditional on a timeline. History
   rows use the plain on-load rise instead (§1), which always completes and which
   this list's own pagination already caps at ten items. Deleted rather than left
   defined-but-unused, for the reason given in editorial.css §6. */


/* ===========================================================================
   3. THE REVEAL - the rendered alert arriving

   The one expressive moment. An operator has waited 1-2 minutes for this; the
   video appearing with no transition at all reads as a page glitch rather than
   as a result. The stage lifts on a teal-tinted shadow (tokens.css §11) so the
   payoff surface is the only thing in the app that carries brand-hued
   elevation.
   =========================================================================== */

@media (prefers-reduced-motion: no-preference) {

  .workspace-stage__reveal {
    animation: suno-reveal var(--suno-duration-reveal) var(--suno-ease-out) both;
  }

  /* The two follow-on actions (broadcast / download) arrive just after the
     video, not with it -- the video is the answer, these are what to do next. */
  .workspace-stage__done:not(.d-none) {
    animation: suno-rise var(--suno-duration-slow) var(--suno-ease-out) both;
    animation-delay: 140ms;
  }
}


/* ===========================================================================
   4. TACTILE CONTROLS

   Two separate signals, and they are not interchangeable:

     hover  the surface rises TOWARD the cursor (translateY negative, shadow
            steps up). Says "this is live".
     press  it gives (translateY positive, shadow steps down). Says "you got
            it" before the network has said anything.

   Spring easing is used on the press only. Text should never wobble, but a
   control giving under a finger is exactly where a small overshoot reads as
   physical rather than as sloppy.

   index.css §6 already presses .btn by 1px. This adds the lift half, and
   extends both to the card-shaped things that are also click targets.
   =========================================================================== */

@media (prefers-reduced-motion: no-preference) {

  .suno-tactile {
    transition: transform var(--suno-duration-fast) var(--suno-ease-out),
                box-shadow var(--suno-duration-fast) var(--suno-ease-out),
                border-color var(--suno-duration-fast) var(--suno-ease);
  }

  .suno-tactile:hover {
    transform: translate3d(0, var(--suno-lift), 0);
    box-shadow: var(--suno-shadow-md);
  }

  .suno-tactile:active {
    transform: translate3d(0, 1px, 0);
    box-shadow: var(--suno-shadow-xs);
    transition-duration: 80ms;
    transition-timing-function: var(--suno-ease-spring);
  }

  /* Buttons get the press from index.css §6; give them the spring curve and the
     matching lift so a button and a clickable card feel like one system. */
  .btn:not(:disabled):not(.btn-link):hover {
    transform: translate3d(0, -1px, 0);
  }

  .btn:not(:disabled):not(.btn-link):active {
    transition-timing-function: var(--suno-ease-spring);
  }
}


/* ===========================================================================
   5. PROGRESS

   Two different bars, and conflating them is a correctness problem, not a
   styling one.

   Determinate: the server reported a percentage. The bar animates to it, and
   the width transition is slow enough (600ms) to read as travel rather than as
   a jump between poll responses.

   Indeterminate: the synchronous render path never polls, so there is no
   progress to report. It gets a sweep that clearly is not a percentage. It must
   not creep forward on a timer -- a bar that invents progress is lying about
   state the app does not have.
   =========================================================================== */

@keyframes suno-sweep {
  from { transform: translate3d(-100%, 0, 0); }
  to   { transform: translate3d(300%, 0, 0); }
}

.suno-progress {
  position: relative;
  overflow: hidden;
  background: var(--suno-border);
  border-radius: var(--suno-radius-full);
  height: 6px;
}

.suno-progress__fill {
  height: 100%;
  border-radius: inherit;
  background: var(--suno-action);
  /* Width is the one non-composited property animated anywhere in this file.
     It is unavoidable on a progress fill and it is a single 6px-tall element,
     so the layout cost is not measurable. */
  transition: width 600ms var(--suno-ease-out);
}

@media (prefers-reduced-motion: no-preference) {
  .suno-progress--indeterminate .suno-progress__fill {
    width: 28%;
    animation: suno-sweep 1.5s var(--suno-ease) infinite;
  }
}

/* With motion reduced, the sweep cannot communicate "working". Fall back to a
   static half-filled bar plus the stage text, which carries the real state. */
@media (prefers-reduced-motion: reduce) {
  .suno-progress--indeterminate .suno-progress__fill { width: 50%; }
}


/* ===========================================================================
   6. SKELETON TO CONTENT

   The generator's field skeleton (index.css §13) is replaced wholesale when
   /api/templates/<id>/fields resolves. Swapping the subtree with no transition
   flashes; the fields fade up in sequence instead, which also makes the arrival
   order legible.
   =========================================================================== */

@media (prefers-reduced-motion: no-preference) {
  #dynamicFormContainer:not([aria-busy="true"]) > * {
    animation: suno-rise var(--suno-duration-base) var(--suno-ease-out) both;
  }
  #dynamicFormContainer:not([aria-busy="true"]) > *:nth-child(2) { animation-delay: 40ms; }
  #dynamicFormContainer:not([aria-busy="true"]) > *:nth-child(3) { animation-delay: 80ms; }
  #dynamicFormContainer:not([aria-busy="true"]) > *:nth-child(4) { animation-delay: 120ms; }
  #dynamicFormContainer:not([aria-busy="true"]) > *:nth-child(n+5) { animation-delay: 160ms; }
}


/* ===========================================================================
   7. CROSS-DOCUMENT VIEW TRANSITIONS - tried, and deliberately not shipped

   `@view-transition { navigation: auto; }` cross-fades between two same-origin
   documents, which is exactly what a server-rendered multi-page app wants: every
   navigation here is a full document load, so the browser paints a white frame
   between pages, and on the dark theme that flash is worse than on light.

   It was implemented and then removed, because of how this app navigates.

   auth.js's enforcePageAccess() sends a user to the surface their capabilities
   allow with `window.location.href = dest` AFTER the document has loaded and
   /api/auth/me has answered. That is a normal flow, not an error case -- a
   broadcaster landing on / is redirected to /broadcast every single time. That
   second navigation interrupts the view transition the first one started, and
   the browser reports the interruption as an uncaught promise rejection
   ("Transition was skipped") in the console.

   It is cosmetic, but it cannot be caught: there is no JS handler to attach when
   the transition was started declaratively by CSS. So the choice was between a
   cross-fade and a clean console on a legitimate user journey.

   Reproduce with: scripts/dev/capture_screenshots.py --check, which fails on
   uncaught page exceptions and is how this was found in the first place.

   Worth revisiting if capability routing ever moves server-side (a redirect
   issued as a 302 before the document loads would not interrupt anything).
   =========================================================================== */
