:root {
  --cyan: #2de2e6;
  --pink: #f92aad;
  --orange: #ff9f1c;
}

* {
  box-sizing: border-box;
}

html,
body {
  height: 100%;
  margin: 0;
}

body {
  display: grid;
  place-items: center;
  min-height: 100dvh;
  padding: 6vw;
  background: linear-gradient(160deg, #170b2e 0%, #2b1252 45%, #4a1942 75%, #6e2142 100%);
  overflow: hidden;
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace;
}

.stage {
  position: relative;
  display: inline-block;
}

.ascii {
  margin: 0;
  /* 47 monospace columns wide and can't wrap (white-space: pre below), so
     this has to stay narrow enough to fit at every viewport width or the
     right edge gets clipped by body's overflow: hidden. 2.7vw is sized off
     the actual content width (100vw minus the 2x 6vw body padding = 88vw)
     with margin for monospace character-advance width varying by font
     (~0.6-0.62em across the stack below). The floor is low enough that it
     only takes over once vw would already be smaller than it anyway, so
     there's no width where the text stops shrinking but the viewport
     keeps shrinking under it. */
  font-size: clamp(0.5rem, 2.7vw, 3.4rem);
  line-height: 1.03;
  white-space: pre;
  user-select: none;

  /* Static fallback look: a solid gradient across the whole mark, shown
     immediately when JS doesn't run (or prefers-reduced-motion is set) so
     the page never depends on script.js to display correctly. Once JS
     wraps the text in per-character spans, each span carries its own
     inline color sampled from this same gradient instead. */
  background: linear-gradient(90deg, var(--cyan), var(--pink), var(--orange));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* Hidden up front (via the .js class added synchronously in <head>) so the
   static gradient mark never flashes before script.js replaces it with the
   per-character build. .enhanced is added once the character spans are in
   place and ready to animate. */
html.js .ascii {
  opacity: 0;
}

html.js .ascii.enhanced {
  opacity: 1;
}

.ch {
  display: inline-block;
  opacity: 0;
  transform: scale(0.4);
  text-shadow: 0 0 0 currentColor;
}

/* While a stroke cell is cycling through random rune-ish glyphs (script.js
   swaps its text every few dozen ms), it's dim and flickery — not yet real. */
.ch.scrambling {
  opacity: 0.8;
  transform: scale(1);
  text-shadow: 0 0 6px currentColor;
  animation: ch-flicker 120ms steps(2, jump-none) infinite;
}

@keyframes ch-flicker {
  from {
    opacity: 0.45;
  }
  to {
    opacity: 0.95;
  }
}

/* Background/spacer cells just fade in quietly underneath the letters. */
.ch.revealed {
  animation: ch-fade 0.6s ease-out forwards;
}

/* Stroke cells lock in with a flourish: picking up from the scrambled state,
   they flash bright, overshoot in size with a random tilt (--tilt is set
   inline per cell), then settle to a steady glow. */
.ch.revealed.ink {
  animation: ch-conjure 0.55s cubic-bezier(0.2, 1.5, 0.4, 1) forwards;
}

@keyframes ch-fade {
  0% {
    opacity: 0;
    transform: scale(0.6);
  }
  100% {
    opacity: 1;
    transform: scale(1);
    text-shadow: 0 0 4px currentColor;
  }
}

@keyframes ch-conjure {
  0% {
    opacity: 0.7;
    transform: scale(0.9);
    text-shadow: 0 0 4px currentColor;
  }
  55% {
    opacity: 1;
    transform: scale(1.35) rotate(var(--tilt, 0deg));
    text-shadow: 0 0 22px currentColor;
  }
  100% {
    opacity: 1;
    transform: scale(1) rotate(0deg);
    text-shadow: 0 0 8px currentColor;
  }
}

@media (prefers-reduced-motion: reduce) {
  .ch {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
    text-shadow: 0 0 10px currentColor !important;
  }
}

/* Effects spawned by script.js during the conjuring. Reduced-motion users
   never get any of these since script.js bails out before spawning them
   (see the .js guard at the top of the file). */
.spark,
.mote {
  position: absolute;
  top: 0;
  left: 0;
  line-height: 1;
  pointer-events: none;
  will-change: transform, opacity;
  text-shadow: 0 0 6px currentColor;
}

/* Sparks burst out of a freshly-locked glyph in a random direction (--dx /
   --dy set inline), twinkle mid-flight, and wink out. */
.spark {
  animation: spark-fly var(--dur, 1s) cubic-bezier(0.2, 0.7, 0.3, 1) forwards;
}

@keyframes spark-fly {
  0% {
    opacity: 0;
    transform: translate(0, 0) scale(0.3) rotate(0deg);
  }
  12% {
    opacity: 1;
    transform: translate(calc(var(--dx, 0px) * 0.2), calc(var(--dy, 0px) * 0.12)) scale(1.15)
      rotate(calc(var(--rot, 0deg) * 0.2));
  }
  55% {
    opacity: 0.55;
  }
  70% {
    opacity: 1;
  }
  100% {
    opacity: 0;
    transform: translate(var(--dx, 0px), var(--dy, 0px)) scale(0.4) rotate(var(--rot, 0deg));
  }
}

/* Ambient motes drift slowly across the mark while it forms, twinkling. */
.mote {
  animation: mote-drift var(--dur, 1.6s) ease-in-out forwards;
}

@keyframes mote-drift {
  0% {
    opacity: 0;
    transform: translate(0, 0) scale(0.4);
  }
  30% {
    opacity: 0.9;
    transform: translate(calc(var(--dx, 0px) * 0.3), calc(var(--dy, 0px) * 0.3)) scale(1);
  }
  60% {
    opacity: 0.4;
  }
  80% {
    opacity: 0.8;
  }
  100% {
    opacity: 0;
    transform: translate(var(--dx, 0px), var(--dy, 0px)) scale(0.5);
  }
}

/* A soft flash of light at each spot where the magic first takes hold. */
.burst {
  position: absolute;
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(circle, currentColor 0%, transparent 65%);
  mix-blend-mode: screen;
  animation: burst 0.8s ease-out forwards;
}

@keyframes burst {
  0% {
    opacity: 0.9;
    transform: scale(0.2);
  }
  100% {
    opacity: 0;
    transform: scale(1.8);
  }
}
