/*
 * Motion layer. Everything here only runs when motion-init.js has set the "motion" class on <html>, which it does
 * only if JavaScript works and the visitor has not asked for reduced motion. Without the class every element
 * is in its final state, so nothing depends on an animation finishing.
 */

/* The playful style lets entrances overshoot as well. */
:root[data-motion='playful'] {
  --ease-out: var(--ease-spring);
}

/* Page changes cross-fade; the header stays put while the content slides. */
/* Only opted in when motion is welcome; with reduced motion pages simply swap. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }
}

::view-transition-old(page) {
  animation: vt-out var(--duration-base) var(--ease-in-out) both;
}

::view-transition-new(page) {
  animation: vt-in var(--duration-slow) var(--ease-out) both;
}

@keyframes vt-out {
  to {
    opacity: 0;
    transform: translateY(calc(var(--move) * -12px));
  }
}

@keyframes vt-in {
  from {
    opacity: 0;
    transform: translateY(calc(var(--move) * 18px));
  }
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

/* Headlines: each word rises out of its own mask, one after the other. Hidden until main.js has split them. */
.motion [data-split]:not(.is-split) {
  visibility: hidden;
}

.motion [data-split] .word {
  display: inline-block;
  overflow: hidden;
  padding: 0 0.08em 0.08em 0;
  margin: 0 -0.08em -0.08em 0;
  vertical-align: top;
}

.motion [data-split] .word__inner {
  display: inline-block;
  transform: translateY(105%) rotate(calc(var(--tilt) * 6deg));
  transform-origin: left bottom;
  animation: word-rise var(--duration-scene) var(--ease-out) forwards;
  animation-delay: calc(var(--i, 0) * var(--stagger) + 80ms);
}

@keyframes word-rise {
  to {
    transform: none;
  }
}

/* The calm style has no travel at all, so the words fade instead of rising. */
.motion[data-motion='calm'] [data-split] .word__inner {
  transform: none;
  opacity: 0;
  animation-name: word-fade;
}

@keyframes word-fade {
  to {
    opacity: 1;
  }
}

/* Supporting text and actions in the hero and page heads follow the headline. */
.motion .hero__lead,
.motion .hero .button-row,
.motion .page-intro__lead {
  animation: fade-up var(--duration-scene) var(--ease-out) both;
  animation-delay: 420ms;
}

.motion .hero .button-row {
  animation-delay: 540ms;
}

@keyframes fade-up {
  from {
    opacity: 0;
    transform: translateY(calc(var(--move) * 20px)) rotate(calc(var(--tilt) * -1.5deg));
  }
}

/* Header check: the card lands, then every header ticks in like a test run. */
.motion .header-check {
  animation: card-land var(--duration-scene) var(--ease-out) both;
  animation-delay: 200ms;
}

@keyframes card-land {
  from {
    opacity: 0;
    transform: translateY(calc(var(--move) * 32px)) scale(var(--land-scale)) rotate(calc(var(--tilt) * 2deg));
  }
}

.motion .header-check__item {
  animation: row-in var(--duration-slow) var(--ease-out) both;
  animation-delay: calc(600ms + var(--i, 0) * 90ms);
}

.motion .header-check__status {
  animation: tick var(--duration-slow) var(--ease-spring) both;
  animation-delay: calc(760ms + var(--i, 0) * 90ms);
}

@keyframes row-in {
  from {
    opacity: 0;
    transform: translateX(calc(var(--move) * -12px));
  }
}

@keyframes tick {
  from {
    opacity: 0;
    transform: scale(0);
  }
}

/* Scroll reveals. main.js adds .is-visible once a block enters the viewport. */
.motion [data-reveal] {
  opacity: 0;
  transform: translateY(calc(var(--move) * 36px)) rotate(calc(var(--tilt) * 1deg));
  transition:
    opacity var(--duration-scene) var(--ease-out),
    transform var(--duration-scene) var(--ease-out);
  transition-delay: calc(var(--i, 0) * var(--stagger));
}

.motion [data-reveal].is-visible {
  opacity: 1;
  transform: none;
}

/* Children of a revealed block play in sequence. */
.motion [data-reveal] .check-list li,
.motion [data-reveal] .terminal__line,
.motion [data-reveal] .session-list__item,
.motion [data-reveal] .timeline__item {
  opacity: 0;
  transform: translateX(calc(var(--move) * -14px));
  transition:
    opacity var(--duration-slow) var(--ease-out),
    transform var(--duration-slow) var(--ease-out);
  transition-delay: calc(240ms + var(--i, 0) * var(--stagger));
}

.motion [data-reveal].is-visible .check-list li,
.motion [data-reveal].is-visible .terminal__line,
.motion [data-reveal].is-visible .session-list__item,
.motion [data-reveal].is-visible .timeline__item {
  opacity: 1;
  transform: none;
}

/* The activity line draws itself before the dots arrive. */
.motion [data-reveal] .timeline::before {
  transform: scaleY(0);
  transition: transform 1.2s var(--ease-out) 120ms;
}

.motion [data-reveal].is-visible .timeline::before {
  transform: none;
}

/* Admin table rows cascade in. */
.motion .data-table tbody tr {
  animation: row-in var(--duration-slow) var(--ease-out) both;
  animation-delay: calc(160ms + var(--i, 0) * 45ms);
}

.motion .filter-bar,
.motion .result-count {
  animation: fade-up var(--duration-slow) var(--ease-out) both;
}

.motion .result-count {
  animation-delay: 120ms;
}

/* Flash messages slide in; main.js plays the reverse before removing one. */
.motion .flash {
  animation: flash-in var(--duration-slow) var(--ease-spring) both;
  animation-delay: calc(var(--i, 0) * var(--stagger));
}

.motion .flash.is-leaving {
  animation: flash-out var(--duration-base) var(--ease-in-out) forwards;
}

@keyframes flash-in {
  from {
    opacity: 0;
    transform: translateY(calc(var(--move) * -14px)) scale(var(--land-scale));
  }
}

@keyframes flash-out {
  to {
    opacity: 0;
    transform: translateX(calc(var(--move) * 24px));
  }
}

/* Sign-in pages: the check chain fills up to the current step. */
.motion .auth-panel {
  animation: card-land var(--duration-scene) var(--ease-out) both;
}

.motion .check-chain::after {
  transform: scaleY(0);
  animation: chain-fill 1.1s var(--ease-out) 300ms forwards;
}

@keyframes chain-fill {
  to {
    transform: scaleY(var(--chain-progress, 0));
  }
}

.motion .check-chain__step {
  animation: row-in var(--duration-slow) var(--ease-out) both;
  animation-delay: calc(200ms + var(--i, 0) * 160ms);
}

.motion .check-chain__step--current .check-chain__node {
  animation: pulse 2.4s var(--ease-in-out) 1.2s infinite;
}

@keyframes pulse {
  0%,
  100% {
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-accent) 40%, transparent);
  }

  50% {
    box-shadow: 0 0 0 8px transparent;
  }
}

.motion .check-chain__step--done .check-chain__node::after {
  animation: tick var(--duration-slow) var(--ease-spring) both;
  animation-delay: calc(500ms + var(--i, 0) * 160ms);
}

/* Code ring: main.js sets --timer-offset from the real clock every second. */
.code-timer__progress {
  stroke-dashoffset: var(--timer-offset, 0);
  transition: stroke-dashoffset 1s linear;
}

.code-timer.is-resetting .code-timer__progress {
  transition: none;
}

/* Two-factor setup: the QR code is scanned in from top to bottom. */
.motion .qr svg {
  clip-path: inset(0 0 100% 0);
  animation: scan-reveal 1.1s var(--ease-in-out) 300ms forwards;
}

.motion .qr {
  position: relative;
  overflow: hidden;
}

.motion .qr::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 3px;
  background: var(--color-accent);
  box-shadow: 0 0 12px var(--color-accent);
  opacity: 0;
  animation: scan-line 1.1s var(--ease-in-out) 300ms;
}

@keyframes scan-reveal {
  to {
    clip-path: inset(0);
  }
}

@keyframes scan-line {
  0% {
    opacity: 1;
    transform: translateY(0);
  }

  95% {
    opacity: 1;
  }

  100% {
    opacity: 0;
    transform: translateY(var(--qr-size, 10rem));
  }
}

/* Recovery codes type themselves out, one after the other. */
.motion .recovery-codes code {
  clip-path: inset(0 100% 0 0);
  animation: type-in 0.7s steps(12, end) forwards;
  animation-delay: calc(300ms + var(--i, 0) * 120ms);
}

@keyframes type-in {
  to {
    clip-path: inset(0);
  }
}

/* Error page: the status digits drop in and bounce. */
.motion .error-code span {
  animation: digit-drop var(--duration-scene) var(--ease-spring) both;
  animation-delay: calc(var(--i, 0) * 110ms);
}

@keyframes digit-drop {
  from {
    opacity: 0;
    transform: translateY(calc(var(--move) * -60%)) rotate(calc(var(--tilt) * -8deg - var(--move) * 4deg));
  }
}

/* Dashboard summary badges switch on. */
.motion .badge--on .badge__dot {
  animation: tick var(--duration-slow) var(--ease-spring) 500ms both;
}

/* "Off" in the footer switch: like reduced motion, but chosen on this site only. */
[data-motion='off'] *,
[data-motion='off'] *::before,
[data-motion='off'] *::after {
  animation-duration: 0.01ms !important;
  animation-delay: 0ms !important;
  transition-duration: 0.01ms !important;
  transition-delay: 0ms !important;
}

:root[data-motion='off']::view-transition-group(*),
:root[data-motion='off']::view-transition-old(*),
:root[data-motion='off']::view-transition-new(*) {
  animation: none !important;
}

/* Footer switch for the animation styles */
.motion-switch {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

.motion-switch[hidden] {
  display: none;
}

.motion-switch__label {
  font-size: var(--text-xs);
  font-weight: 600;
}

.motion-switch ul {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin: 0;
  padding: 0.2rem;
  border: 1px solid var(--color-line);
  border-radius: 999px;
  list-style: none;
}

.motion-switch button {
  padding: 0.2rem 0.7rem;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--color-muted);
  font: inherit;
  font-size: var(--text-xs);
  font-weight: 650;
  cursor: pointer;
  transition:
    color var(--duration-fast) ease,
    background-color var(--duration-fast) ease;
}

.motion-switch button:hover {
  background: var(--color-surface-sunken);
  color: var(--color-ink);
}

.motion-switch button[aria-pressed='true'] {
  background: var(--color-accent);
  color: var(--color-accent-ink);
}
