/* Page transition — a CSS-driven rocket launch played on every single page
   load (fresh visit, refresh, internal link, back/forward — anything that
   loads this document), see js/page-transition.js for the orchestration.
   Everything here targets only `transform`/`translate`/`scale`/`rotate`/
   `opacity`/`filter` so it stays on the compositor thread (60fps), and is
   fully gated by prefers-reduced-motion at the bottom of this file.

   The overlay is visible *by default* (no class needed) so it's already
   covering the screen, fully opaque, on this document's very first paint —
   no flash of the page underneath, ever. JS only ever moves it forward
   through the sequence and finally hides it once the reveal is done:
     overlay:  (default, visible) -> .is-leaving -> .pt-done (hidden)
     rocket:   (default) -> .pt-rocket--arrived -> .pt-rocket--held -> .pt-rocket--liftoff

   The reveal itself is NOT a curtain sliding off — a single flat rectangle
   moving as one rigid block reads as a cheap loading-screen wipe no matter
   how nice the decoration on it is. Instead the whole "sky" tears apart: a
   dozen hand-placed cloud/mist puffs (.pt-cloud, see below) are completely
   invisible at rest — the background gradient and stars look exactly as
   they always did — and only briefly bloom into view, each on its own
   slightly-offset timing, as the veil is torn open, then scatter outward
   and fade. The base gradient fades in parallel as the safety net that
   guarantees the real page is never visible a moment before it should be. */

/* Scroll lock while the overlay is up. Pins <body> in place with
   position:fixed (JS sets the inline `top` offset to -scrollY so there's no
   visual jump) instead of the more common `overflow: hidden` on <html> —
   that would toggle the scrollbar on and off, and since <html>'s
   overflow-y is permanently "scroll" (see base.css), it never needs to. */
html.pt-locked body {
  position: fixed;
  left: 0;
  right: 0;
  width: 100%;
}

.page-transition {
  position: fixed;
  inset: 0;
  z-index: 10000;
  opacity: 1;
  pointer-events: auto;
  overflow: hidden;
  /* Solid, fully opaque, near-black base first — a genuinely dark universe:
     mostly black/near-black voids, a little deep violet, and only the
     faintest whisper of violet/rose colour. Nothing here can ever let the
     real page underneath show through regardless of the layers above it. */
  background-color: #010102;
  background-image:
    /* darker "void" patches for depth/contrast, layered among the colour.
       Each gets an explicit vmax radius instead of the default
       "farthest-corner" sizing — on a wide/short viewport, farthest-corner
       measures from an off-center point to the *diagonally opposite*
       corner, which balloons on ultra-wide screens and produces a hard,
       flat dark band along one edge instead of a soft localized patch. */
    radial-gradient(circle 40vmax at 12% 82%, rgba(0, 0, 0, 0.65), transparent),
    radial-gradient(circle 42vmax at 88% 12%, rgba(0, 0, 0, 0.6), transparent),
    radial-gradient(circle 36vmax at 65% 92%, rgba(0, 0, 0, 0.55), transparent),
    radial-gradient(circle 34vmax at 6% 22%, rgba(0, 0, 0, 0.5), transparent),
    /* very light violet / rose touches — kept deliberately faint */
    radial-gradient(circle 42vmax at 20% 15%, rgba(88, 28, 135, 0.14), transparent),
    radial-gradient(circle 44vmax at 84% 20%, rgba(168, 168, 178, 0.16), transparent),
    radial-gradient(circle 48vmax at 74% 84%, rgba(157, 23, 77, 0.09), transparent),
    radial-gradient(circle 58vmax at 50% 48%, rgba(49, 46, 129, 0.08), transparent),
    linear-gradient(180deg, #04040c 0%, #010106 55%, #000000 100%);
  /* Slow-then-sudden: mist thins out gradually, then the last bit clears
     fast — matches the .pt-cloud puffs' own easing so the whole scene
     dissolves as one coherent motion, not two mismatched fades. */
  transition: opacity 1.15s cubic-bezier(0.6, 0.04, 0.98, 0.335);
}

/* Two large, softly blurred colour blobs drifting slowly on their own —
   purely decorative motion behind the stars/rocket so the whole universe
   reads as alive, not a static poster. transform/translate only. */
.page-transition::before,
.page-transition::after {
  content: "";
  position: absolute;
  inset: -25%;
  z-index: -1;
  pointer-events: none;
  filter: blur(70px);
  will-change: translate;
}

.page-transition::before {
  background: radial-gradient(circle 65vmax at 28% 32%, rgba(124, 58, 237, 0.22), transparent);
  animation: pt-nebula-drift-a 16s ease-in-out infinite;
}

.page-transition::after {
  background: radial-gradient(circle 65vmax at 72% 70%, rgba(219, 39, 119, 0.1), transparent);
  animation: pt-nebula-drift-b 20s ease-in-out infinite;
}

@keyframes pt-nebula-drift-a {
  0%, 100% {
    translate: 0 0;
  }
  50% {
    translate: 4% 5%;
  }
}

@keyframes pt-nebula-drift-b {
  0%, 100% {
    translate: 0 0;
  }
  50% {
    translate: -5% -4%;
  }
}

.page-transition.is-leaving {
  opacity: 0;
}

.page-transition.pt-done {
  opacity: 0;
  pointer-events: none;
}

.page-transition__vignette {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(ellipse at center, transparent 55%, rgba(2, 3, 12, 0.55) 100%);
}

/* A bloom of light where the rocket actually breaks through, right as it
   ignites into liftoff — gives the scattering clouds below a clear focal
   point to break apart *from*, instead of dissolving with no apparent
   cause. A child of the overlay: it fades out along with everything else
   during .is-leaving via the parent's own opacity transition. */
.page-transition__burst {
  position: absolute;
  top: -15%;
  left: 50%;
  width: 130vmax;
  height: 130vmax;
  translate: -50% -50%;
  scale: 0.4;
  border-radius: 50%;
  background: radial-gradient(
    circle,
    rgba(191, 219, 254, 0.12) 0%,
    rgba(124, 58, 237, 0.06) 30%,
    transparent 55%
  );
  opacity: 0;
  filter: blur(1px);
  pointer-events: none;
  transition: opacity 0.45s ease-out, scale 0.9s cubic-bezier(0.22, 1, 0.36, 1);
}

.page-transition.pt-liftoff .page-transition__burst {
  opacity: 1;
  scale: 1;
}

/* Hand-placed cloud/mist puffs (same "deterministic, not Math.random()"
   spirit as the star field) — invisible at rest (see .pt-cloud), each one
   only briefly blooms into view, on its own slightly different duration/
   delay (set inline per puff), as the veil tears open, then scatters
   outward and fades — so the breakup reads as uneven and organic rather
   than a single uniform wipe. */
.page-transition__clouds {
  position: absolute;
  inset: -15%;
  pointer-events: none;
}

.pt-cloud {
  position: absolute;
  top: var(--y);
  left: var(--x);
  width: var(--size);
  height: var(--size);
  translate: -50% -50%;
  scale: 0.8;
  border-radius: 50%;
  background: radial-gradient(
    circle,
    rgba(147, 197, 253, 0.13) 0%,
    rgba(124, 58, 237, 0.08) 45%,
    transparent 75%
  );
  filter: blur(22px);
  /* Invisible at rest — the background gradient and stars underneath must
     look exactly as they did before this puff existed, with zero visual
     difference. Each puff only ever appears as part of its own .is-leaving
     animation below (a keyframe, not a transition, since it needs to go
     0 -> peak -> 0 rather than between two static end-states): a brief
     bloom of light right where a patch of "mist" is torn away, then it
     scatters and fades, rather than a shape that was already sitting there
     quietly dissolving. */
  opacity: 0;
}

.page-transition.is-leaving .pt-cloud {
  animation: pt-cloud-tear var(--dur, 1s) ease-out var(--delay, 0s) both;
}

@keyframes pt-cloud-tear {
  0% {
    opacity: 0;
    scale: 0.8;
    filter: blur(18px);
  }
  18% {
    opacity: 0.28;
    scale: 1;
    filter: blur(22px);
  }
  100% {
    opacity: 0;
    scale: var(--scale-to, 1.7);
    filter: blur(46px);
  }
}

/* Stars — fixed, hand-placed positions (not Math.random()), so the field is
   identical and deterministic on every transition. */
.page-transition__stars {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* Every star continuously drifts straight down (a "translate" animation,
   independent of the "transform" property used elsewhere) — this is the
   parallax illusion that sells the rocket climbing: the whole starfield
   falls away beneath it. Three depth bands (far/mid/near) fall at
   different speeds/distances/sizes for real depth, not just uniform noise. */
.pt-star {
  position: absolute;
  top: var(--y);
  left: var(--x);
  width: 1.5px;
  height: 1.5px;
  border-radius: 50%;
  background: #fff;
  opacity: 0.42;
  /* Falls noticeably faster than the raw per-star duration: the 0.55
     multiplier is a single global speed-up dial (no need to touch the 60
     hand-placed --fdur values individually). */
  animation: pt-star-fall calc(var(--fdur, 8s) * 0.55) linear infinite;
  animation-delay: var(--fdelay, 0s);
}

.pt-star--tint {
  background: #a9b8ff;
}

.pt-star--far {
  width: 1.2px;
  height: 1.2px;
  opacity: 0.32;
}

.pt-star--mid {
  width: 1.6px;
  height: 1.6px;
  opacity: 0.54;
}

.pt-star--near {
  width: 2.3px;
  height: 2.3px;
  opacity: 0.85;
  box-shadow: 0 0 4px rgba(255, 255, 255, 0.7);
}

.pt-star--twinkle {
  animation:
    pt-star-fall calc(var(--fdur, 8s) * 0.55) linear infinite,
    pt-twinkle var(--tdur, 4s) ease-in-out infinite;
  animation-delay: var(--fdelay, 0s), var(--tdelay, 0s);
}

@keyframes pt-star-fall {
  0% {
    translate: 0 0;
  }
  100% {
    translate: 0 var(--fdist, 110px);
  }
}

@keyframes pt-twinkle {
  0%, 100% {
    opacity: 0.2;
  }
  50% {
    opacity: 0.9;
  }
}

/* Only a mixed depth subset shows on small screens (spec: ~6-10 on mobile,
   loosened slightly since the falling motion reads better with a few more). */
@media (max-width: 640px) {
  .pt-star:nth-child(n + 15) {
    display: none;
  }
}

/* Rocket + effects stack ------------------------------------------------ */

.page-transition__rocket-wrap {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 130px;
  height: 130px;
  margin: -65px 0 0 -65px;
  /* Where the exhaust actually is on the transparent PNG (measured directly
     off the pixels: bottom-center of the flame motif already drawn into the
     logo), used to anchor every effect layer without touching the image. */
  --engine-x: 49.8%;
  --engine-y: 86.1%;

  translate: 0 25vh;
  scale: 0.82;
  rotate: -2deg;
  opacity: 0;
  transition:
    translate 0.32s cubic-bezier(0.34, 1.56, 0.64, 1),
    scale 0.32s cubic-bezier(0.34, 1.56, 0.64, 1),
    rotate 0.32s ease-out,
    opacity 0.2s ease-out;
}

@media (min-width: 768px) {
  .page-transition__rocket-wrap {
    width: 140px;
    height: 140px;
    margin: -70px 0 0 -70px;
  }
}

@media (max-width: 480px) {
  .page-transition__rocket-wrap {
    width: 92px;
    height: 92px;
    margin: -46px 0 0 -46px;
  }
}

.page-transition__rocket-wrap.pt-rocket--arrived {
  translate: 0 0;
  scale: 1;
  rotate: 0deg;
  opacity: 1;
  animation: pt-idle-bob 2.6s ease-in-out infinite;
}

.page-transition__rocket-wrap.pt-rocket--held {
  translate: 0 0;
  scale: 1;
  rotate: 0deg;
  opacity: 1;
  /* Purely vertical: the one-shot anticipation dip, then a continuous,
     fairly fast up/down vibration (never rotation, never sideways) — reads
     as the engine physically shaking the rocket, not swaying it. */
  animation:
    pt-ignition-shake 0.15s ease-out both,
    pt-held-vibrate 0.9s linear 0.15s infinite;
}

.page-transition__rocket-wrap.pt-rocket--liftoff {
  translate: 0 -130vh;
  scale: 0.88;
  rotate: 0deg;
  opacity: 1;
  animation: none;
  transition:
    translate 0.3s cubic-bezier(0.55, 0, 1, 0.45),
    scale 0.3s cubic-bezier(0.55, 0, 1, 0.45);
}

@keyframes pt-idle-bob {
  0%, 100% {
    transform: translateY(0) rotate(0deg);
  }
  50% {
    transform: translateY(-2px) rotate(0.3deg);
  }
}

/* One-shot: brief anticipation dip building up to the held vibration's full
   amplitude, then settles at pt-held-vibrate's own starting offset (6px) so
   there's no jump between the two. Purely vertical the whole way through. */
@keyframes pt-ignition-shake {
  0% {
    transform: translateY(0);
  }
  35% {
    transform: translateY(9px);
  }
  46% {
    transform: translateY(2px);
  }
  57% {
    transform: translateY(10px);
  }
  68% {
    transform: translateY(3px);
  }
  79% {
    transform: translateY(8px);
  }
  100% {
    transform: translateY(6px);
  }
}

/* A real, noticeably bigger vertical vibration — fast up/down jitter with
   a wide swing, all on translateY, nothing else. */
@keyframes pt-held-vibrate {
  0% {
    transform: translateY(6px);
  }
  10% {
    transform: translateY(11px);
  }
  20% {
    transform: translateY(1px);
  }
  30% {
    transform: translateY(10px);
  }
  40% {
    transform: translateY(2px);
  }
  50% {
    transform: translateY(12px);
  }
  60% {
    transform: translateY(0.5px);
  }
  70% {
    transform: translateY(10px);
  }
  80% {
    transform: translateY(2.5px);
  }
  90% {
    transform: translateY(11px);
  }
  100% {
    transform: translateY(6px);
  }
}

.pt-logo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  z-index: 3;
  /* Never filtered/blurred — the logo itself always stays crisp, per spec. */
}

/* Flame -------------------------------------------------------------------
   Three stacked layers (aura / main / core), narrow at the top, widening
   downward, transform-origin at the top so scaling reads as growth from the
   engine rather than from the middle. */

.pt-flame {
  position: absolute;
  top: var(--engine-y);
  left: var(--engine-x);
  translate: -50% 0;
  width: 56px;
  height: 130px;
  z-index: 2;
  pointer-events: none;
}

.pt-flame__aura,
.pt-flame__main,
.pt-flame__core {
  position: absolute;
  top: 0;
  left: 50%;
  translate: -50% 0;
  transform-origin: top center;
  border-radius: 50% 50% 45% 45% / 60% 60% 40% 40%;
  opacity: 0;
  transition: height 0.22s ease-out, width 0.22s ease-out, opacity 0.22s ease-out;
}

.pt-flame__aura {
  width: 34px;
  height: 10px;
  background: radial-gradient(ellipse at top, rgba(124, 58, 237, 0.75), rgba(124, 58, 237, 0) 75%);
  filter: blur(4px);
}

.pt-flame__main {
  width: 16px;
  height: 9px;
  background: linear-gradient(180deg, #3b82f6 0%, rgba(59, 130, 246, 0.15) 100%);
}

.pt-flame__core {
  width: 7px;
  height: 7px;
  background: radial-gradient(ellipse at top, #ffffff 0%, #e9ffff 45%, rgba(103, 232, 249, 0.2) 100%);
}

.pt-rocket--arrived .pt-flame__aura {
  width: 30px;
  height: 12px;
  opacity: 0.55;
}

.pt-rocket--arrived .pt-flame__main {
  height: 11px;
  opacity: 0.8;
}

.pt-rocket--arrived .pt-flame__core {
  height: 8px;
  opacity: 0.9;
}

.pt-rocket--held .pt-flame__aura {
  width: 60px;
  height: 56px;
  opacity: 0.85;
  animation: pt-flame-flicker 0.5s ease-in-out infinite alternate;
}

.pt-rocket--held .pt-flame__main {
  width: 30px;
  height: 46px;
  opacity: 1;
  animation: pt-flame-flicker 0.42s ease-in-out infinite alternate-reverse;
}

.pt-rocket--held .pt-flame__core {
  width: 13px;
  height: 28px;
  opacity: 1;
  animation: pt-flame-flicker 0.36s ease-in-out infinite alternate;
}

/* The flame's growth is driven by the exact same duration/easing curve as
   the rocket-wrap's own `translate` climb just below, so the two are
   literally the same function of time: the higher it climbs, the bigger
   the flame gets, in lockstep — not an independent fixed-length burst. */
.pt-rocket--liftoff .pt-flame__aura {
  width: 88px;
  height: 118px;
  opacity: 0.8;
  transition: height 0.3s cubic-bezier(0.55, 0, 1, 0.45), width 0.3s cubic-bezier(0.55, 0, 1, 0.45),
    opacity 0.3s ease-in;
}

.pt-rocket--liftoff .pt-flame__main {
  width: 42px;
  height: 104px;
  opacity: 0.97;
  transition: height 0.3s cubic-bezier(0.55, 0, 1, 0.45), width 0.3s cubic-bezier(0.55, 0, 1, 0.45);
}

.pt-rocket--liftoff .pt-flame__core {
  width: 18px;
  height: 72px;
  opacity: 1;
  transition: height 0.3s cubic-bezier(0.55, 0, 1, 0.45), width 0.3s cubic-bezier(0.55, 0, 1, 0.45);
}

@keyframes pt-flame-flicker {
  from {
    scale: 1 1;
    opacity: 0.9;
  }
  to {
    scale: 1.08 1.12;
    opacity: 1;
  }
}

/* Smoke -------------------------------------------------------------------- */

.pt-smoke {
  position: absolute;
  top: var(--engine-y);
  left: var(--engine-x);
  translate: -50% -20%;
  width: 4px;
  height: 4px;
  z-index: 1;
  pointer-events: none;
}

.pt-smoke__wisp {
  position: absolute;
  top: 0;
  left: 0;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(163, 177, 220, 0.28) 0%, rgba(163, 177, 220, 0) 72%);
  filter: blur(3px);
  opacity: 0;
}

.pt-rocket--held .pt-smoke__wisp {
  animation: pt-smoke-puff 1.1s ease-out infinite;
  animation-delay: var(--delay, 0s);
}

.pt-rocket--liftoff .pt-smoke__wisp {
  animation: pt-smoke-blowdown 0.3s ease-in forwards;
  animation-delay: calc(var(--delay, 0s) * 0.4);
}

@keyframes pt-smoke-puff {
  0% {
    translate: 0 0;
    scale: 0.4;
    opacity: 0;
    filter: blur(3px);
  }
  20% {
    opacity: 0.24;
  }
  100% {
    translate: var(--drift) 34px;
    scale: 1.9;
    opacity: 0;
    filter: blur(6px);
  }
}

@keyframes pt-smoke-blowdown {
  0% {
    translate: 0 0;
    opacity: 0.2;
  }
  100% {
    translate: calc(var(--drift) * 0.6) 60px;
    opacity: 0;
  }
}

/* Sparks / particles -------------------------------------------------------- */

.pt-sparks {
  position: absolute;
  top: var(--engine-y);
  left: var(--engine-x);
  translate: -50% 0;
  width: 2px;
  height: 2px;
  z-index: 1;
  pointer-events: none;
}

.pt-spark {
  position: absolute;
  top: 0;
  left: 0;
  width: 2px;
  height: 2px;
  border-radius: 50%;
  background: #93c5fd;
  opacity: 0;
}

.pt-spark:nth-child(3n) {
  background: #67e8f9;
}

.pt-spark:nth-child(3n + 1) {
  background: #c4b5fd;
}

.pt-spark:nth-child(5n) {
  width: 2px;
  height: 5px;
  border-radius: 1px;
}

.pt-rocket--held .pt-spark,
.pt-rocket--liftoff .pt-spark {
  animation: pt-spark-fly 0.32s ease-in forwards;
  animation-delay: var(--sdelay, 0s);
}

.pt-rocket--liftoff .pt-spark {
  animation-duration: 0.22s;
}

@keyframes pt-spark-fly {
  0% {
    translate: 0 0;
    opacity: 0.9;
  }
  100% {
    translate: var(--sx) var(--sy);
    opacity: 0;
  }
}

/* On small screens, thin the particle/smoke count for performance (spec:
   fewer particles/smoke on mobile) rather than hiding the whole layer. */
@media (max-width: 640px) {
  .pt-smoke__wisp:nth-child(n + 6),
  .pt-spark:nth-child(n + 11) {
    display: none;
  }
}

/* Liftoff: the rest of the star field drifts down very slightly to sell the
   speed. Keyed off a class on the overlay root itself (set alongside
   .pt-rocket--liftoff on the rocket) since the stars element is a DOM
   sibling that comes *before* rocket-wrap, not after. */
.page-transition__stars {
  transition: translate 0.3s ease-in;
}

.page-transition.pt-liftoff .page-transition__stars {
  translate: 0 10px;
}

@media (prefers-reduced-motion: reduce) {
  .page-transition,
  .page-transition::before,
  .page-transition::after,
  .page-transition__rocket-wrap,
  .pt-flame__aura,
  .pt-flame__main,
  .pt-flame__core,
  .pt-smoke__wisp,
  .pt-spark,
  .pt-star,
  .pt-cloud,
  .page-transition__burst {
    animation: none !important;
    transition: opacity 0.15s ease-out !important;
    scale: 1 !important;
    filter: none !important;
  }

  .page-transition__rocket-wrap {
    display: none;
  }
}
