/* ── MOTION — Smoke It! splash/login motion layer (pairs with js/splash-motion.js)
   Modelled on the NEXUS Portal motion layer (zz-motion.css / motion.js).

   PERSONALITY: Premium, with smoke/fog as a "gas" material -- slow, controlled,
   0% overshoot, durations stretched toward 2x. Tone: noir dread, not play.
     Signature easing  --mo-ease  cubic-bezier(.4,0,.2,1)  (every entrance/settle)
       exits use       --mo-exit  cubic-bezier(.4,0,1,1)   (accelerate away)
       loops use       --mo-sine  cubic-bezier(.37,0,.63,1) (seamless sine)
     Duration palette  quick 350ms / std 600ms / slow 1100ms
       (direct-manipulation feedback is its own tier: hover 90ms, press 130ms,
        settle 250ms -- the skill's interaction table, not the palette)
     Entrance pattern  "surfacing": opacity 0 + translateY(10px) + scale(.98)
                       -> rest, on --mo-ease. The hero adds a smoke puff that
                       dissipates off the wordmark.

   BORROWED (house motion sites, read-only):
     Nexus Portal zz-motion.css/motion.js  html.mo-on gate, html.mo-paused,
                                           backwards fill, one IIFE script
     hydraslicer.com js/main.js            canvas fit-on-real-resize, resumable
                                           frame loop, reduced-motion still frame
     ghostaholics.com src (logo)           per-letter split + --i stagger
     damienchristian.com src/motion.js     index-driven cascade delay, "never
                                           hide without JS" rule
     rebirthritual.com                     held-top breathe curve; exit shape of
                                           its vt-out (fade + lift 10px)
     kismetcandles.com                     glow pulse on opacity only (halo layer)

   SAFETY MODEL: every rule is scoped under `html.mo-on`, which splash-motion.js
   adds only when prefers-reduced-motion is NOT set. JS off, JS broken or reduced
   motion = the static page in splash.css, fully visible, nothing hidden.
   Entrances use fill-mode `backwards`, so nothing can stick at opacity 0.
   Hot paths animate transform and opacity only. html.mo-paused (tab hidden)
   pauses every ambient loop; the rain canvas stops drawing.
   ───────────────────────────────────────────────────────────────────────── */
:root {
    --mo-ease: cubic-bezier(.4, 0, .2, 1);
    --mo-exit: cubic-bezier(.4, 0, 1, 1);
    --mo-sine: cubic-bezier(.37, 0, .63, 1);
    --mo-quick: 350ms;
    --mo-std: 600ms;
    --mo-slow: 1100ms;
    --mo-hover: 90ms;
    --mo-press: 130ms;
    --mo-settle: 250ms;
}

/* ---------- Keyframes ---------- */

@keyframes mo-surface {
    from { opacity: 0; transform: translateY(10px) scale(.98); }
    to   { opacity: 1; transform: none; }
}
@keyframes mo-fade-in { from { opacity: 0; } to { opacity: 1; } }

/* Hero: the wordmark condenses out of the smoke. */
@keyframes mo-condense {
    from { opacity: 0; transform: translateY(8px) scale(1.05); }
    40%  { opacity: .35; }
    to   { opacity: 1; transform: none; }
}
@keyframes mo-puff {
    from { opacity: .95; transform: scale(.75); }
    55%  { opacity: .55; }
    to   { opacity: 0; transform: translateY(-14%) scale(1.7); }
}

/* Ambient loops -- low amplitude, seamless (alternate or matched ends). */
@keyframes mo-drift {
    from { transform: scale(1.04); }
    to   { transform: scale(1.09) translate3d(-1%, -.8%, 0); }
}
@keyframes mo-fog-a {
    0%   { transform: translate3d(0, 0, 0) scale(1); }
    50%  { transform: translate3d(9vw, -1.5vh, 0) scale(1.08); }
    100% { transform: translate3d(18vw, -3vh, 0) scale(1.15); }
}
@keyframes mo-fog-b {
    0%   { transform: translate3d(0, 0, 0) scale(1.1); }
    50%  { transform: translate3d(-8vw, -2vh, 0) scale(1); }
    100% { transform: translate3d(-16vw, -3vh, 0) scale(.92); }
}
@keyframes mo-plume {
    from { opacity: .45; transform: scale(1); }
    to   { opacity: .7;  transform: translate3d(2%, -6%, 0) scale(1.12); }
}
@keyframes mo-grain {
    0%   { transform: translate(0, 0); }
    20%  { transform: translate(-4%, 3%); }
    40%  { transform: translate(3%, -5%); }
    60%  { transform: translate(-6%, -2%); }
    80%  { transform: translate(5%, 4%); }
    100% { transform: translate(-2%, 6%); }
}
/* Breath with a held top (0 -> 42% -> 58% -> 0), after rebirthritual.com's
   candle `breathe`: a plateau reads as a living glow, a pure sine as a pulse.
   Start and end frames match, so the loop is seamless without alternate. */
@keyframes mo-breathe {
    0%   { opacity: .2; }
    42%  { opacity: .66; }
    58%  { opacity: .6; }
    100% { opacity: .2; }
}
/* A tired neon tube: mostly steady, two brief dips per 11s cycle. */
@keyframes mo-flicker {
    0%, 70%, 72.5%, 74%, 100% { opacity: 1; }
    71%   { opacity: .45; }
    73%   { opacity: .6; }
}

/* Interaction */
@keyframes mo-shake {
    0%   { transform: translateX(0); }
    16%  { transform: translateX(-10px); }
    36%  { transform: translateX(10px); }
    56%  { transform: translateX(-7px); }
    76%  { transform: translateX(4px); }
    100% { transform: translateX(0); }
}
@keyframes mo-leave {
    to { opacity: 0; transform: translateY(-8px) scale(.98); }
}

/* ---------- Layer 0: ambient scene ---------- */

html.mo-on .scene { animation: mo-fade-in var(--mo-slow) var(--mo-ease) backwards; }
html.mo-on .scene__art {
    will-change: transform;
    animation: mo-drift 60s var(--mo-sine) infinite alternate;
}
html.mo-on .smoke { will-change: transform; }
html.mo-on .smoke--1 { animation: mo-fog-a 42s var(--mo-sine) infinite alternate; }
html.mo-on .smoke--2 { animation: mo-fog-b 50s var(--mo-sine) -12s infinite alternate; }
html.mo-on .smoke--3 { animation: mo-fog-a 58s var(--mo-sine) -30s infinite alternate-reverse; }
html.mo-on .scene__grain { animation: mo-grain 1.2s steps(6) infinite; }

/* ---------- Layer 1: hero ---------- */

/* Per-letter condense, 40ms apart (letter split + --i stagger borrowed from
   ghostaholics.com's logo; its spring/blur swapped for our no-overshoot
   ease). The letters land 150ms..1290ms: a 1.1s hero reveal overall. */
html.mo-on .stage .wordmark .ch {
    animation: mo-condense 900ms var(--mo-ease) calc(150ms + var(--i, 0) * 40ms) backwards;
}
html.mo-on .stage .wordmark .ch.dim {
    animation:
        mo-condense 900ms var(--mo-ease) calc(150ms + var(--i, 0) * 40ms) backwards,
        mo-flicker 11s steps(1, end) 3s infinite;
}
html.mo-on .wordmark__puff {
    position: absolute;
    left: -5%;
    top: -45%;
    width: 110%;
    height: 190%;
    border-radius: 50%;
    background: radial-gradient(closest-side, rgba(196, 188, 206, .62), rgba(170, 162, 184, .32) 50%, rgba(170, 162, 184, .08) 80%, transparent 100%);
    filter: blur(16px);
    pointer-events: none;
    opacity: 0;
    animation: mo-puff 1200ms var(--mo-ease) 100ms backwards;
}
html.mo-on .wordmark__smoke { animation: mo-plume 9s var(--mo-sine) 1.3s infinite alternate; }

/* ---------- Layer 2: secondary cascade (110ms apart, 440ms span) ----------
   Index-driven delay (--mo-i) as in Nexus motion.js / damienchristian.com's
   --file-delay, instead of one hard-coded delay per element. */

html.mo-on .stage .eyebrow,
html.mo-on .stage .tagline,
html.mo-on .stage .lede,
html.mo-on .stage .actions,
html.mo-on .stage .soon {
    animation: mo-surface var(--mo-quick) var(--mo-ease) backwards;
}
html.mo-on .stage :is(.eyebrow, .tagline, .lede, .actions, .soon) {
    animation-delay: calc(1150ms + var(--mo-i, 0) * 110ms);
}
html.mo-on .soon .dot { animation: mo-breathe 2.4s var(--mo-sine) 2s infinite; }

/* ---------- The button: breathing halo + direct feedback ---------- */

html.mo-on .btn-neon::before {
    content: '';
    position: absolute;
    inset: -2px;
    border-radius: inherit;
    box-shadow: 0 0 26px rgba(255, 45, 85, .6), 0 0 64px rgba(255, 45, 85, .3);
    opacity: .2;
    pointer-events: none;
    animation: mo-breathe 3.6s var(--mo-sine) 2.2s infinite;
}
html.mo-on .btn-neon {
    transition:
        transform var(--mo-hover) var(--mo-ease),
        background-color var(--mo-hover) var(--mo-ease),
        border-color var(--mo-hover) var(--mo-ease);
}
html.mo-on .btn-neon::after,
html.mo-on .btn-neon .arrow {
    transition: opacity var(--mo-hover) var(--mo-ease), transform var(--mo-hover) var(--mo-ease);
}
html.mo-on .btn-neon:hover { transform: translateY(-2px); }
html.mo-on .btn-neon:hover .arrow { transform: translateX(4px); }
html.mo-on .btn-neon:active {
    transform: translateY(0) scale(.97);
    transition-duration: var(--mo-press);
}
/* Release: JS adds .mo-settle so the return eases out over 250ms instead of
   inheriting the 90ms hover timing. */
html.mo-on .btn-neon.mo-settle { transition-duration: var(--mo-settle); }

/* ---------- Login page ---------- */

html.mo-on .stage--login .login-card {
    animation: mo-surface var(--mo-std) var(--mo-ease) 200ms backwards;
}
/* Same specificity as the splash hero rule above and later, so the card's small
   wordmark skips the smoke reveal and only carries the neon flicker. */
html.mo-on .login-card .wordmark { animation: mo-flicker 11s steps(1, end) 2s infinite; }

/* Error: a firm 360ms shake, decaying amplitude, no overshoot past rest. A
   server-rendered error shakes on load instead of surfacing. */
html.mo-on .stage--login .login-card[data-state="error"] {
    animation: mo-shake 360ms cubic-bezier(.36, 0, .64, 1) both;
}
html.mo-on .login-card .alert:not([hidden]) {
    animation: mo-fade-in var(--mo-quick) var(--mo-ease) backwards;
}

/* Success: card lifts away while the scene sinks to ink, then JS navigates. */
html.mo-on.mo-leaving .stage--login .login-card {
    animation: mo-leave var(--mo-quick) var(--mo-exit) forwards;
}
html.mo-on.mo-leaving .scene__fade {
    animation: mo-fade-in 500ms var(--mo-exit) forwards;
}

/* ---------- Tab hidden: freeze the ambient layer ---------- */

html.mo-paused .scene__art,
html.mo-paused .smoke,
html.mo-paused .scene__grain,
html.mo-paused .wordmark__smoke,
html.mo-paused .wordmark .ch.dim,
html.mo-paused .login-card .wordmark,
html.mo-paused .btn-neon::before,
html.mo-paused .soon .dot { animation-play-state: paused; }

/* Belt and braces: even if mo-on were somehow set, reduced motion wins. */
@media (prefers-reduced-motion: reduce) {
    html.mo-on *, html.mo-on *::before, html.mo-on *::after {
        animation: none !important;
        transition: none !important;
    }
}
