/* === ANIMATION PATTERNS ===
   Purposeful animation vocabulary — use to signal state changes, not for decoration.
   Restraint is premium; overuse is cheap.
*/

/* fadeIn — subtle 250ms opacity transition for page content appearing.
   Used on: page loads, data arrival replacing skeletons. */
@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

.animate-fade-in {
  animation: fadeIn 250ms ease forwards;
}

/* slideUp — card/panel entrance from bottom (translateY 16px -> 0, 250ms ease-out).
   Used on: new cards appearing in lists, bottom panel expansion. */
@keyframes slideUp {
  from {
    opacity: 0;
    transform: translateY(16px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.animate-slide-up {
  animation: slideUp 250ms ease-out forwards;
}

/* pulse — loading skeleton shimmer animation (left-to-right gradient sweep, 1.5s infinite).
   Used on: all skeleton loaders. */
@keyframes pulse {
  0% {
    background-position: -200% 0;
  }
  100% {
    background-position: 200% 0;
  }
}

.animate-pulse {
  background: linear-gradient(
    90deg,
    var(--color-gray-200) 25%,
    var(--color-gray-100) 50%,
    var(--color-gray-200) 75%
  );
  background-size: 200% 100%;
  animation: pulse 1.5s infinite ease-in-out;
}

/* Dark-mode shimmer — the default light-gray gradient renders as bright
   white slabs against a dark surface, making the animation invisible and
   the skeleton look static. Swap to dark grays so the sweep is visible
   on both MudBlazor's body class and our karting-theme-dark MudLayout class. */
.mud-theme-dark .animate-pulse,
.karting-theme-dark .animate-pulse {
  background: linear-gradient(
    90deg,
    var(--color-gray-700) 25%,
    var(--color-gray-600) 50%,
    var(--color-gray-700) 75%
  );
  background-size: 200% 100%;
}

/* scaleIn — modal/dialog entrance (scale 0.95 -> 1.0 + fadeIn, 200ms ease-out).
   Used on: modals, confirmation dialogs, popovers. */
@keyframes scaleIn {
  from {
    opacity: 0;
    transform: scale(0.95);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

.animate-scale-in {
  animation: scaleIn 200ms ease-out forwards;
}

/* checkmark — success checkmark SVG stroke animation (draw stroke over 400ms, then subtle scale bounce).
   Used on: setup save, run entry save, plan generation complete, telemetry normalization complete. */
@keyframes checkmark-stroke {
  0% {
    stroke-dashoffset: 48;
  }
  100% {
    stroke-dashoffset: 0;
  }
}

@keyframes checkmark-bounce {
  0% { transform: scale(1); }
  50% { transform: scale(1.1); }
  100% { transform: scale(1); }
}

.animate-checkmark path {
  stroke-dasharray: 48;
  stroke-dashoffset: 48;
  animation: checkmark-stroke 400ms ease-out forwards;
}

.animate-checkmark {
  animation: checkmark-bounce 300ms ease-out 400ms forwards;
}

/* valueHighlight — brief background color flash (accent -> transparent, 600ms ease-out).
   Used on: setup parameter values that changed (after Apply Plan, after Apply Favorite, after end-of-day sync). */
@keyframes valueHighlight {
  from {
    background-color: var(--color-primary-100);
  }
  to {
    background-color: transparent;
  }
}

.animate-value-highlight {
  animation: valueHighlight 600ms ease-out forwards;
}

/* crossfade — quick 150ms opacity crossfade between content.
   Used on: page-to-page navigation on web (SPA transition).
   NOT used on: list scrolling, tab switches (these should be instant). */
@keyframes crossfade {
  from { opacity: 0; }
  to { opacity: 1; }
}

.animate-crossfade {
  animation: crossfade 150ms ease forwards;
}

/* countUp — helper class for numerical value animation.
   The actual counting logic is in the KMetricCard component.
   Used on: dashboard stat cards on first load, Driver DNA metric values. */
.animate-count-up {
  transition: all 600ms ease-out;
}

/* cursorBlink — blinking | cursor (opacity 1->0->1, 800ms infinite).
   Used on: AI streaming TokenStreamingDisplay. */
@keyframes cursorBlink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0; }
}

.animate-cursor-blink {
  animation: cursorBlink 800ms infinite;
}

/* typingDot — three-dot iMessage-style typing indicator. Each dot uses
   the same keyframes with a different animation-delay so they pulse in
   sequence; opacity and transform run together so the dot appears to
   "breathe" rather than just blink.
   Used on: KTypingIndicator (Pit Mechanic chat). */
@keyframes typingDot {
  0%, 60%, 100% {
    opacity: 0.35;
    transform: translateY(0) scale(0.85);
  }
  30% {
    opacity: 1;
    transform: translateY(-2px) scale(1);
  }
}

@media (prefers-reduced-motion: reduce) {
  /* Replace the breathe with a slow opacity pulse so the indicator is
     still visible but not distracting for users with motion sensitivities. */
  @keyframes typingDot {
    0%, 100% { opacity: 0.5; transform: none; }
    50%      { opacity: 1;   transform: none; }
  }
}

/* === SPINNER === */
@keyframes spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

.animate-spin {
  animation: spin 600ms linear infinite;
}

/* Slower variant for steering wheel badge — mechanical feel at 1.2s. */
.animate-spin--slow {
  animation: spin 1200ms linear infinite;
}

/* slideAlongTrack — continuous left-to-right translation for loading indicator.
   Used on: LoadingSpinner track animation (kart logo sliding along a rail). */
@keyframes slideAlongTrack {
  0% {
    transform: translateX(-100%);
  }
  100% {
    transform: translateX(calc(var(--track-width) + 100%));
  }
}

/* === OVERLAY BACKDROP === */
.overlay-backdrop {
  position: fixed;
  inset: 0;
  background-color: rgba(0, 0, 0, 0.5);
  z-index: var(--z-modal);
  animation: fadeIn 200ms ease forwards;
}

/* === CELEBRATION / FIREWORKS ===
   Used by <SubscriptionCelebration /> on the subscription overview page
   immediately after a successful Stripe checkout to celebrate the new tier.
   Pure CSS — no JS interop — driven by keyframes on absolutely-positioned
   sparks that radiate out from a single origin point. Respects
   prefers-reduced-motion. */

.celebration-root {
  position: relative;
  overflow: hidden;
  animation: celebrationBannerIn 500ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
}

@keyframes celebrationBannerIn {
  0% {
    opacity: 0;
    transform: translateY(-12px) scale(0.98);
  }
  60% {
    opacity: 1;
    transform: translateY(2px) scale(1.01);
  }
  100% {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

.celebration-fireworks {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
  z-index: 0;
}

/* Content (text + close button) layered above the firework sparks so the
   close icon stays visible and clickable on narrow phones, where the
   bursts otherwise crowd over it. */
.celebration-content {
  position: relative;
  z-index: 1;
}

.celebration-close {
  position: relative;
  z-index: 2;
  pointer-events: auto;
}

/* Stack the headline block and close button on top of each other on the
   smallest phones — at 320 px the icon button gets squeezed off the row
   otherwise, hiding the dismiss affordance behind the fireworks. */
@media (max-width: 480px) {
  .celebration-content {
    flex-direction: column;
    align-items: stretch;
  }
}

.celebration-burst {
  position: absolute;
  width: 0;
  height: 0;
}

/* Three bursts fire at different origins and slight time offsets so the
   finale feels layered rather than a single pop. */
.celebration-burst--one {
  left: 18%;
  top: 55%;
  animation: burstFade 2200ms ease-out 100ms 2 both;
}
.celebration-burst--two {
  left: 50%;
  top: 30%;
  animation: burstFade 2200ms ease-out 400ms 2 both;
}
.celebration-burst--three {
  left: 82%;
  top: 55%;
  animation: burstFade 2200ms ease-out 700ms 2 both;
}

@keyframes burstFade {
  0% { opacity: 1; }
  80% { opacity: 1; }
  100% { opacity: 0; }
}

/* Each .spark is one ember flying out from its parent burst. 12 sparks
   per burst, each at 30° increments, travel ~80px then fade. */
.celebration-spark {
  position: absolute;
  left: 0;
  top: 0;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--celebration-spark-color, #ffd166);
  box-shadow: 0 0 8px var(--celebration-spark-color, #ffd166);
  transform: translate(-50%, -50%);
  opacity: 0;
  animation: sparkFly 1400ms cubic-bezier(0.1, 0.6, 0.3, 1) forwards;
}

@keyframes sparkFly {
  0% {
    opacity: 1;
    transform:
      translate(-50%, -50%)
      rotate(var(--angle))
      translateX(0)
      scale(1);
  }
  70% {
    opacity: 1;
  }
  100% {
    opacity: 0;
    transform:
      translate(-50%, -50%)
      rotate(var(--angle))
      translateX(90px)
      scale(0.4);
  }
}

/* 12-point starburst pattern, each spark assigned one angle. */
.celebration-spark:nth-child(1)  { --angle:   0deg; }
.celebration-spark:nth-child(2)  { --angle:  30deg; }
.celebration-spark:nth-child(3)  { --angle:  60deg; }
.celebration-spark:nth-child(4)  { --angle:  90deg; }
.celebration-spark:nth-child(5)  { --angle: 120deg; }
.celebration-spark:nth-child(6)  { --angle: 150deg; }
.celebration-spark:nth-child(7)  { --angle: 180deg; }
.celebration-spark:nth-child(8)  { --angle: 210deg; }
.celebration-spark:nth-child(9)  { --angle: 240deg; }
.celebration-spark:nth-child(10) { --angle: 270deg; }
.celebration-spark:nth-child(11) { --angle: 300deg; }
.celebration-spark:nth-child(12) { --angle: 330deg; }

/* Alternate spark colours per burst so the three bursts feel distinct. */
.celebration-burst--one  .celebration-spark { --celebration-spark-color: #22c55e; } /* success */
.celebration-burst--two  .celebration-spark { --celebration-spark-color: #147efb; } /* primary */
.celebration-burst--three .celebration-spark { --celebration-spark-color: #f59e0b; } /* warning/gold */

/* Accessibility: respect the user's OS-level motion preference. Users
   who opted out still see the content and a soft fade, but no kinetic
   sparks or banner bounce. */
@media (prefers-reduced-motion: reduce) {
  .celebration-root {
    animation: fadeIn 300ms ease forwards;
  }
  .celebration-fireworks {
    display: none;
  }
}

/* === INTERACTIVE CARD (hover lift + press feedback) ===
   Modern 2026 card affordance pattern shared by Reddit, Linear, Vercel,
   Stripe, GitHub: on hover the card lifts ~2 px; on press it sinks back and
   scales slightly to confirm the tap. Both transitions are very fast
   (150 ms / 80 ms) so they read as a physical response, not a slow animation.

   The lift is transform-only (no animated box-shadow — animating a two-layer
   shadow is a per-frame paint) and is gated to @media (hover: hover) so it
   stays a cheap desktop-only composite and a touch tap doesn't leave a sticky
   :hover lift. No static will-change here: a permanent GPU layer per card
   across non-virtualized lists inflated memory — promotion is left to the
   browser, which already handles a 2 px translate cheaply.

   Apply to MudCard via `Class="k-interactive-card"`. Compose with
   `cursor-pointer` when the card is clickable. Respects
   prefers-reduced-motion. */

.k-interactive-card {
  transition: transform var(--motion-fast) var(--ease-out-quart);
}

@media (hover: hover) {
  .k-interactive-card:hover {
    transform: translateY(-2px);
  }

  .k-interactive-card:active {
    transform: translateY(0) scale(0.98);
    transition-duration: var(--motion-instant);
  }

  /* When the card is the currently-selected detail (master-detail), suppress
     the hover lift so the selection state stays the anchor. */
  .k-interactive-card.k-interactive-card--selected:hover {
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .k-interactive-card,
  .k-interactive-card:hover,
  .k-interactive-card:active {
    transform: none;
    transition: none;
  }
}

/* === STAGGER GRID ENTRANCE ===
   Twitter/Reddit/Instagram feed-style sequential fade-in for grid items.
   Children animate in at +20 ms intervals, capped at 6 children — after that
   every later item shares the 6th child's delay, so even a long list's last
   card finishes appearing within ~0.5 s of mount instead of trickling in for
   ~0.9 s on every visit.

   Apply to the grid container (MudGrid wrapper, MudStack, plain <div>)
   via `Class="k-stagger-grid"`. */

.k-stagger-grid > * {
  animation: k-stagger-in var(--motion-slow) var(--ease-out-quart) backwards;
}

@keyframes k-stagger-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.k-stagger-grid > *:nth-child(1)  { animation-delay: calc(var(--stagger-step) * 0); }
.k-stagger-grid > *:nth-child(2)  { animation-delay: calc(var(--stagger-step) * 1); }
.k-stagger-grid > *:nth-child(3)  { animation-delay: calc(var(--stagger-step) * 2); }
.k-stagger-grid > *:nth-child(4)  { animation-delay: calc(var(--stagger-step) * 3); }
.k-stagger-grid > *:nth-child(5)  { animation-delay: calc(var(--stagger-step) * 4); }
.k-stagger-grid > *:nth-child(6)  { animation-delay: calc(var(--stagger-step) * 5); }
.k-stagger-grid > *:nth-child(n+7) { animation-delay: calc(var(--stagger-step) * 5); }

@media (prefers-reduced-motion: reduce) {
  .k-stagger-grid > * {
    animation: none;
  }
}

/* === HOVER GLOW (primary CTA) ===
   Linear / Stripe / Vercel use a subtle colored glow on hover to signal
   a primary call-to-action. Apply via `Class="k-glow-primary"` on a
   MudButton (Variant.Filled, Color.Primary) that you want to draw
   the eye to. */

.k-glow-primary {
  transition: box-shadow var(--motion-base) var(--ease-out-quart),
              transform var(--motion-fast) var(--ease-out-quart);
}

.k-glow-primary:hover {
  box-shadow: 0 0 0 1px var(--mud-palette-primary),
              0 8px 24px -8px rgba(20, 126, 251, 0.6);
}

.k-glow-primary:active {
  transform: scale(0.98);
  transition-duration: var(--motion-instant);
}

@media (prefers-reduced-motion: reduce) {
  .k-glow-primary,
  .k-glow-primary:hover,
  .k-glow-primary:active {
    transform: none;
    transition: none;
  }
}

/* === MOBILE FLOATING ACTION BUTTON ===
   The PageHeader "Add X" / "Generate" CTA at top-right works well on
   desktop, but on phones the corner is a long thumb stretch — every
   modern mobile app (Reddit, Twitter, Gmail, Material Design) anchors
   the primary action to the bottom-right inside thumb reach.

   Apply to MudFab via `Class="k-mobile-fab"`. Position respects
   --safe-bottom / --safe-right so the button clears the iOS home
   indicator and notch in landscape. */

.k-mobile-fab {
  position: fixed;
  right: max(16px, var(--safe-right));
  bottom: max(16px, calc(16px + var(--safe-bottom)));
  z-index: var(--z-dropdown);
  /* Pop-in with --ease-out-back so the button feels confidently
     placed, not anxiously appearing. */
  animation: k-fab-in var(--motion-base) var(--ease-out-back) backwards;
}

@keyframes k-fab-in {
  from {
    opacity: 0;
    transform: scale(0.4);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

@media (prefers-reduced-motion: reduce) {
  .k-mobile-fab {
    animation: none;
  }
}

/* === LIVE PULSE DOT (status indicator) ===
   Subtle outward halo every 1.8 s — used on ProcessingStatusBadge and
   any "live" / "online" indicator. Same visual vocabulary as Discord's
   online dot and Twitter Spaces' live indicator. */

@keyframes k-live-pulse {
  0% {
    box-shadow: 0 0 0 0 currentColor;
    opacity: 1;
  }
  70% {
    box-shadow: 0 0 0 8px transparent;
    opacity: 0.6;
  }
  100% {
    box-shadow: 0 0 0 0 transparent;
    opacity: 1;
  }
}

.k-live-pulse {
  animation: k-live-pulse 1.8s var(--ease-out-quart) infinite;
}

@media (prefers-reduced-motion: reduce) {
  .k-live-pulse {
    animation: none;
  }
}

/* === DISMISSIBLE CARD ===
   Slide-right + fade + collapse used when user dismisses a dashboard action card.
   The component adds .dismissible-card--dismissing, waits 280ms, then removes the item. */

@keyframes dismiss-slide-out {
  from {
    opacity: 1;
    transform: translateX(0);
    max-height: 400px;
    padding-top: 0;
    padding-bottom: 0;
    margin-top: 0;
    margin-bottom: 0;
  }
  to {
    opacity: 0;
    transform: translateX(32px);
    max-height: 0;
    padding-top: 0;
    padding-bottom: 0;
    margin-top: 0;
    margin-bottom: 0;
  }
}

.dismissible-card {
  overflow: hidden;
}

.dismissible-card--dismissing {
  animation: dismiss-slide-out 280ms ease-in forwards;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .dismissible-card--dismissing {
    animation-duration: 1ms;
  }
}

/* === LEADERBOARD ===
   Finite (3 cycles ≈ 6s) so we don't hold the compositor awake forever.
   MOB-E014: the previous `infinite` shimmer/pulse was a measurable battery
   drain on the leaderboard page. */
@keyframes leaderboard-shimmer {
  0% { border-color: #F4C430; }
  50% { border-color: #FFED85; }
  100% { border-color: #F4C430; }
}

@keyframes leaderboard-slide-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}

@keyframes leaderboard-you-pulse {
  0%, 100% { box-shadow: inset 3px 0 0 var(--mud-palette-primary), 0 0 0 1px var(--mud-palette-primary); }
  50% { box-shadow: inset 3px 0 0 var(--mud-palette-primary), 0 0 0 3px var(--mud-palette-primary); }
}

.leaderboard-lap-time {
  font-family: var(--font-mono);
  font-weight: 600;
}

.leaderboard-podium-1 {
  animation: leaderboard-shimmer 2s ease-in-out 3;
}

.leaderboard-you {
  animation: leaderboard-you-pulse 2s ease-in-out 3;
}

@media (prefers-reduced-motion: reduce) {
  .leaderboard-podium-1,
  .leaderboard-you {
    animation: none;
  }
}
