/* ============================================================
   GameText — shared in-game presentation layer
   ------------------------------------------------------------
   One DOM overlay that every game (Canvas2D and WebGL alike)
   paints its words and its countdown through, so a banner in
   Golf and a banner in Slalom are literally the same object.

   Everything that MOVES here is `transform` or `opacity` and
   nothing else, because those are the only two properties the
   compositor can animate without waking the main thread. The
   expensive-looking parts — the extruded shading, the dark
   halo that keeps text readable over snow, the gradient face —
   are all STATIC paint, resolved once when a node is created
   and never touched again while it is on screen. The result is
   that a banner or a countdown costs the game loop nothing.

   Depth is done for real, not faked with a blur: the letters
   and the digits live inside a `perspective` and arrive by
   rotating in and dollying forward, so the whole overlay reads
   as type standing in the world rather than a label pasted on
   top of it.
   ============================================================ */

.gt-layer {
  position: fixed;
  inset: 0;
  z-index: 30;
  pointer-events: none;          /* never eat a gameplay tap */
  overflow: hidden;              /* cannot reintroduce scrolling */
  contain: layout style paint;   /* isolated from page layout entirely */
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;

  /* Default tone (amber). Individual pieces override. */
  --gt-face: linear-gradient(177deg, #FFF9EC 0%, #FFE6B0 20%, #F9C063 44%, #E08E24 68%, #A15A11 100%);
  --gt-side: #7d480e;
  --gt-side-2: #5c340a;
  --gt-halo: rgba(7, 15, 10, .58);
  --gt-glow: rgba(246, 176, 73, .5);
  --gt-rule: linear-gradient(90deg, rgba(246,176,73,0) 0%, rgba(255,225,160,.85) 22%, rgba(255,246,226,.95) 50%, rgba(255,225,160,.85) 78%, rgba(246,176,73,0) 100%);
}

/* ---- Tones -------------------------------------------------
   Four, and no more. Amber is the brand and the default; the
   other three exist because "Birdie" and "Out of bounds" are
   not the same news and should not be the same colour.
   ------------------------------------------------------------ */
.gt-tone-amber {
  --gt-face: linear-gradient(177deg, #FFF9EC 0%, #FFE6B0 20%, #F9C063 44%, #E08E24 68%, #A15A11 100%);
  --gt-side: #7d480e; --gt-side-2: #5c340a;
  --gt-glow: rgba(246, 176, 73, .5);
  --gt-rule: linear-gradient(90deg, rgba(246,176,73,0) 0%, rgba(255,225,160,.85) 22%, rgba(255,246,226,.95) 50%, rgba(255,225,160,.85) 78%, rgba(246,176,73,0) 100%);
}
.gt-tone-mint {
  --gt-face: linear-gradient(177deg, #F4FFF6 0%, #CFF6D5 20%, #93E39D 44%, #47AE5B 68%, #1C6A2C 100%);
  --gt-side: #185b26; --gt-side-2: #0f3d19;
  --gt-glow: rgba(111, 203, 119, .48);
  --gt-rule: linear-gradient(90deg, rgba(111,203,119,0) 0%, rgba(180,240,190,.85) 22%, rgba(240,255,244,.95) 50%, rgba(180,240,190,.85) 78%, rgba(111,203,119,0) 100%);
}
.gt-tone-rose {
  --gt-face: linear-gradient(177deg, #FFF2EF 0%, #FFCFC6 20%, #F79080 44%, #D8503A 68%, #8C2716 100%);
  --gt-side: #7d2415; --gt-side-2: #55160c;
  --gt-glow: rgba(226, 92, 72, .45);
  --gt-rule: linear-gradient(90deg, rgba(226,92,72,0) 0%, rgba(255,190,178,.8) 22%, rgba(255,238,234,.95) 50%, rgba(255,190,178,.8) 78%, rgba(226,92,72,0) 100%);
}
.gt-tone-ice {
  --gt-face: linear-gradient(177deg, #FFFFFF 0%, #EAF4FF 18%, #C3DAF3 42%, #7FA3CE 68%, #3D5E88 100%);
  --gt-side: #33506f; --gt-side-2: #1e3450;
  --gt-glow: rgba(190, 220, 255, .5);
  --gt-rule: linear-gradient(90deg, rgba(150,190,235,0) 0%, rgba(210,232,255,.85) 22%, rgba(255,255,255,.95) 50%, rgba(210,232,255,.85) 78%, rgba(150,190,235,0) 100%);
}

/* ============================================================
   BANNER
   ============================================================ */

.gt-banner {
  position: absolute;
  left: 0; right: 0;
  top: var(--gt-top, 36%);
  transform: translateY(-50%);
  text-align: center;
  padding: 0 18px;

  /* One shared vanishing point for the whole line, so the
     letters at the ends rotate into place at a slightly
     different angle from the ones in the middle. That
     differential is what stops it reading as a flat sprite
     sheet of characters. */
  perspective: 900px;
  perspective-origin: 50% 62%;

  font-family: 'Bricolage', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  line-height: 1.04;
}

/* Soft feathered scrim. Not a chip, not a box: a wide radial
   falloff with no edge you can point at. It is what keeps
   cream type legible over bright snow, and dark type legible
   in Swoop's cave, without ever looking like a UI panel. */
.gt-banner-scrim {
  position: absolute;
  left: 50%; top: 50%;
  width: 300%; height: 360%;
  transform: translate3d(-50%, -50%, 0);
  /* Many stops, reaching zero exactly at the edge: the falloff has to be
     smooth enough that there is no ellipse you can trace with a finger.
     Wider than the viewport on purpose, so only the vertical falloff is
     ever visible and it reads as a band of shade in the scene. */
  background: radial-gradient(closest-side,
    rgba(6, 13, 9, .40) 0%,
    rgba(6, 13, 9, .355) 22%,
    rgba(6, 13, 9, .27) 41%,
    rgba(6, 13, 9, .175) 58%,
    rgba(6, 13, 9, .09) 74%,
    rgba(6, 13, 9, .03) 88%,
    rgba(6, 13, 9, 0) 100%);
  opacity: 0;
  animation: gtScrimIn var(--gt-in, 620ms) cubic-bezier(.22, .9, .3, 1) both;
}

.gt-w {
  display: inline-block;
  white-space: nowrap;           /* wrap at words, never mid-word */
  transform-style: preserve-3d;  /* carry the banner's perspective down */
}

.gt-l {
  display: inline-block;
  position: relative;
  color: transparent;
  backface-visibility: hidden;

  /* The dark halo + the extrusion + the cast shadow all live on
     THIS layer, behind the gradient face drawn by ::after. A
     text-stroke centred on the glyph outline leaves ~half of
     its width standing proud of the face, which is the halo. */
  -webkit-text-stroke: .055em var(--gt-halo);
  text-shadow:
     0 .022em 0 var(--gt-side),
     0 .044em 0 var(--gt-side-2),
     0 .07em .09em rgba(4, 9, 6, .5),
     0 .16em .3em rgba(4, 9, 6, .42);

  animation: gtLetterIn var(--gt-in, 620ms) both;
  /* NOTE: animation-delay is set INLINE by gametext.js, not here.
     `animation-delay: calc(var(--i) * var(--gt-stagger))` is the obvious way
     to write this and it is measurably wrong: a delay whose value comes from
     a custom property keeps Blink from handing the animation to the
     compositor, and the whole staggered wave then runs a style recalc every
     frame (0.035ms/frame at 5x CPU throttle, versus 0.000 with a plain
     value). --i is still set on every letter because it is what makes the
     wave legible when you inspect the DOM; nothing reads it. */
}

/* The lit face. Painted once; never animated. */
.gt-l::after {
  content: attr(data-c);
  position: absolute;
  left: 0; top: 0;
  -webkit-text-stroke: 0;
  text-shadow: none;
  color: transparent;
  background-image: var(--gt-face);
  -webkit-background-clip: text;
          background-clip: text;
}

/* Emoji and other colour glyphs can't be clipped to a gradient
   without turning into a silhouette, so they opt out. */
.gt-l.gt-l-plain {
  color: #F2EAD8;
  -webkit-text-stroke: 0;
}
.gt-l.gt-l-plain::after { content: none; }

/* Title: the big moment. Birdie, the lap time, the finish. */
.gt-banner-title {
  font-size: clamp(38px, 13.5vw, 68px);
  font-weight: 800;
  letter-spacing: -0.035em;
  --gt-in: 640ms;
}

/* Hint: the instructional line. Same motion, different voice —
   small, tracked out, quieter. It has to sit under the action
   without competing with it. */
.gt-banner-hint {
  font-size: clamp(14px, 4.2vw, 18px);
  font-weight: 700;
  letter-spacing: .115em;
  text-transform: uppercase;
  --gt-in: 480ms;
}
.gt-banner-hint .gt-l {
  -webkit-text-stroke: .085em var(--gt-halo);
  text-shadow: 0 .05em .12em rgba(4, 9, 6, .55), 0 .16em .34em rgba(4, 9, 6, .4);
}
.gt-banner-hint .gt-banner-rule { display: none; }
/* An instruction has to be readable at a glance in the corner of the eye
   while the player is already moving, and Slalom's is read against pure
   white snow. So the hint gets a near-cream face instead of the amber one
   (amber and lit snow are too close in luminance), a heavier halo, and a
   deeper band of shade. It still has no edge you can point at. */
.gt-banner-hint .gt-l {
  --gt-face: linear-gradient(177deg, #FFFFFF 0%, #FFF6E4 44%, #F3DDB4 100%);
  --gt-halo: rgba(5, 11, 8, .82);
  -webkit-text-stroke: .125em var(--gt-halo);
}
.gt-banner-hint .gt-banner-scrim {
  height: 620%; width: 300%;
  background: radial-gradient(closest-side,
    rgba(5, 11, 8, .62) 0%,
    rgba(5, 11, 8, .55) 22%,
    rgba(5, 11, 8, .42) 41%,
    rgba(5, 11, 8, .27) 58%,
    rgba(5, 11, 8, .14) 74%,
    rgba(5, 11, 8, .05) 88%,
    rgba(5, 11, 8, 0) 100%);
}

/* Kicker: the small label above a title ("HOLE 2"), when there
   is genuinely something to say. Arrives first, leaves first. */
.gt-banner-kicker {
  display: block;
  position: relative;
  font-size: clamp(11px, 3.2vw, 13px);
  font-weight: 800;
  letter-spacing: .22em;
  text-transform: uppercase;
  /* Same two-layer construction as the letters — a dark stroked copy
     underneath, the solid cream copy on top — because `paint-order` is not
     reliable for -webkit-text-stroke and a stroke painted OVER the fill is
     exactly how you get an eyebrow that vanishes against a bright sky. */
  color: transparent;
  -webkit-text-stroke: 3.2px rgba(5, 11, 8, .78);
  text-shadow: 0 1px 4px rgba(4, 9, 6, .9), 0 0 18px rgba(4, 9, 6, .7);
  margin-bottom: .5em;
  opacity: 0;
  transform: translate3d(0, 6px, 0);
  animation: gtKickerIn 420ms cubic-bezier(.2, .85, .3, 1) both;
}
.gt-banner-kicker::after {
  content: attr(data-c);
  position: absolute;
  left: 0; right: 0; top: 0;
  -webkit-text-stroke: 0;
  text-shadow: none;
  color: #FFF6E4;
}

/* Sub-line: the quiet instruction that belongs WITH a title rather than
   as its own banner ("tap anywhere to return"). Arrives last, leaves first. */
.gt-banner-sub {
  display: block;
  position: relative;
  font-size: clamp(11px, 3.1vw, 13px);
  font-weight: 700;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: transparent;
  -webkit-text-stroke: 3px rgba(5, 11, 8, .72);
  text-shadow: 0 1px 4px rgba(4, 9, 6, .85);
  margin-top: .55em;
  opacity: 0;
  animation: gtKickerIn 420ms cubic-bezier(.2, .85, .3, 1) both;  /* delay set inline */
}
.gt-banner-sub::after {
  content: attr(data-c);
  position: absolute;
  left: 0; right: 0; top: 0;
  -webkit-text-stroke: 0;
  text-shadow: none;
  color: rgba(255, 246, 228, .86);
}
.gt-banner.gt-out .gt-banner-sub { animation: gtKickerOut var(--gt-out, 340ms) ease both; }

/* A single hairline rule that wipes out from the centre under
   the words — the sports-broadcast lower-third move. It is the
   detail that makes the type read as designed rather than
   dropped in, and it is one composited scaleX. */
.gt-banner-rule {
  display: block;
  height: 2px;
  width: min(78%, 340px);
  margin: .42em auto 0;
  border-radius: 2px;
  background: var(--gt-rule);
  transform: scale3d(0, 1, 1);
  opacity: 0;
  animation: gtRuleIn 620ms cubic-bezier(.16, .9, .28, 1) both;  /* delay set inline */
}

/* ---- Banner motion ---------------------------------------
   Two decaying overshoots, not one. A single bounce reads as
   an easing preset; a second, much smaller counter-move is
   what makes it read as something with mass settling down.
   The `--i` stagger turns that per-letter settle into a wave
   travelling along the word.
   ----------------------------------------------------------- */
@keyframes gtLetterIn {
  0% {
    opacity: 0;
    transform: translate3d(0, .66em, -80px) rotateX(-58deg) scale(.8);
    animation-timing-function: cubic-bezier(.12, .74, .26, 1);
  }
  38% {
    opacity: 1;
    transform: translate3d(0, -.175em, 28px) rotateX(12deg) scale(1.065);
    animation-timing-function: cubic-bezier(.42, 0, .3, 1);
  }
  60% {
    transform: translate3d(0, .055em, -8px) rotateX(-4deg) scale(.982);
    animation-timing-function: cubic-bezier(.35, 0, .25, 1);
  }
  80% {
    transform: translate3d(0, -.016em, 3px) rotateX(1.2deg) scale(1.005);
    animation-timing-function: cubic-bezier(.3, 0, .2, 1);
  }
  100% {
    opacity: 1;
    transform: translate3d(0, 0, 0) rotateX(0deg) scale(1);
  }
}

@keyframes gtLetterOut {
  0%   { opacity: 1; transform: translate3d(0, 0, 0) rotateX(0deg) scale(1); }
  100% { opacity: 0; transform: translate3d(0, -.34em, 54px) rotateX(26deg) scale(1.05); }
}

@keyframes gtScrimIn  { from { opacity: 0; } to { opacity: 1; } }
@keyframes gtScrimOut { from { opacity: 1; } to { opacity: 0; } }
@keyframes gtKickerIn { from { opacity: 0; transform: translate3d(0, 8px, 0); }
                        to   { opacity: 1; transform: translate3d(0, 0, 0); } }
@keyframes gtKickerOut{ from { opacity: 1; } to { opacity: 0; transform: translate3d(0, -6px, 0); } }
@keyframes gtRuleIn   { 0%   { opacity: 0; transform: scale3d(0, 1, 1); }
                        30%  { opacity: 1; }
                        100% { opacity: 1; transform: scale3d(1, 1, 1); } }
@keyframes gtRuleOut  { from { opacity: 1; transform: scale3d(1, 1, 1); }
                        to   { opacity: 0; transform: scale3d(.2, 1, 1); } }

/* Exit. The wave runs the same direction it came in, faster,
   so the line feels like it leaves rather than blinks off. */
.gt-banner.gt-out .gt-l {
  animation: gtLetterOut var(--gt-out, 340ms) cubic-bezier(.5, 0, .78, .5) both;
  /* exit delays are rewritten inline too, for the same compositing reason */
}
.gt-banner.gt-out .gt-banner-scrim {
  animation: gtScrimOut var(--gt-out, 340ms) ease both;
  animation-delay: 60ms;
}
.gt-banner.gt-out .gt-banner-kicker {
  animation: gtKickerOut var(--gt-out, 340ms) ease both;
}
.gt-banner.gt-out .gt-banner-rule {
  animation: gtRuleOut var(--gt-out, 340ms) cubic-bezier(.5, 0, .78, .5) both;
}

/* ============================================================
   COUNTDOWN
   ============================================================ */

.gt-cd {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: 'Bricolage', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}

/* The scrim IS the game fade-in: it starts near-opaque over the
   canvas and lifts across the count, so the world resolves out
   of the dark as the numbers run. Radial, not flat, so the
   numbers sit in a pool of shade and the corners clear first. */
.gt-cd-scrim {
  position: absolute;
  inset: -10%;
  background:
    radial-gradient(58% 42% at 50% 46%, rgba(8, 16, 11, .90) 0%, rgba(8, 16, 11, .80) 40%, rgba(8, 16, 11, .58) 72%, rgba(8, 16, 11, .40) 100%),
    linear-gradient(rgba(8, 16, 11, .34), rgba(8, 16, 11, .34));
  animation: gtCdScrim var(--gt-total, 3800ms) cubic-bezier(.32, .5, .3, 1) both;
}
/* The shade lifts in stages and then clears completely across the
   final beat, so the world finishes arriving exactly as GO releases.
   It is also what keeps the numerals legible over bright snow —
   they are always sitting in a pool of shade until the last moment. */
@keyframes gtCdScrim {
  0%   { opacity: calc(var(--gt-scrim, 1) * 1);   animation-timing-function: cubic-bezier(.3,.5,.4,1); }
  26%  { opacity: calc(var(--gt-scrim, 1) * .74); animation-timing-function: linear; }
  55%  { opacity: calc(var(--gt-scrim, 1) * .52); animation-timing-function: linear; }
  77%  { opacity: calc(var(--gt-scrim, 1) * .38); animation-timing-function: cubic-bezier(.4,0,.3,1); }
  100% { opacity: 0; }
}

.gt-cd-stage {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  perspective: 1100px;
  perspective-origin: 50% 47%;
}

/* Warm bloom behind the numeral. Sells the numeral as a light
   source instead of a decal, and it is one scale+opacity. */
.gt-cd-glow {
  position: absolute;
  left: 50%; top: 47%;
  width: 88vw; height: 88vw;
  max-width: 460px; max-height: 460px;
  margin: -44vw 0 0 -44vw;
  border-radius: 50%;
  background: radial-gradient(50% 50% at 50% 50%, var(--gt-glow) 0%, rgba(0,0,0,0) 68%);
  opacity: 0;
  transform: translate3d(0, 0, 0) scale(.62);
  animation: gtCdGlow var(--gt-beat, 1000ms) cubic-bezier(.16, .85, .3, 1) both;
}
@keyframes gtCdGlow {
  0%   { opacity: 0;   transform: scale(.55); }
  16%  { opacity: .95; transform: scale(1.06); }
  46%  { opacity: .42; transform: scale(1.0); }
  100% { opacity: 0;   transform: scale(1.24); }
}

.gt-cd-num {
  position: relative;
  transform-style: preserve-3d;
  font-size: clamp(130px, 43vw, 224px);
  font-weight: 800;
  letter-spacing: -0.05em;
  line-height: 1;
  animation: gtCdNum var(--gt-beat, 1000ms) both;
}

.gt-cd-num > span {
  display: block;
  backface-visibility: hidden;
}
.gt-cd-num > span + span {
  position: absolute;
  left: 0; top: 0;
  width: 100%;
}

/* Layer 1 — the cast shadow, sitting a long way behind the face
   in real Z. Because it is genuinely further away, the arrival
   rotation parallaxes it against the numeral instead of
   dragging a fixed blur along underneath. */
.gt-cd-cast {
  color: transparent;
  text-shadow: 0 .10em .13em rgba(3, 8, 5, .75), 0 .2em .34em rgba(3, 8, 5, .55);
  transform: translate3d(0, .035em, -95px);
}

/* Layer 2 — the extruded side wall. Stacked hard-edged shadows
   marching down-right build a solid body under the face. */
.gt-cd-side {
  color: var(--gt-side);
  text-shadow:
     .006em .008em 0 var(--gt-side),
     .012em .016em 0 var(--gt-side),
     .018em .024em 0 var(--gt-side-2),
     .024em .032em 0 var(--gt-side-2),
     .030em .040em 0 var(--gt-side-2),
     .036em .048em 0 rgba(28, 15, 4, .95),
     .042em .056em 0 rgba(22, 12, 3, .9);
  transform: translate3d(0, 0, -8px);
}

/* Layer 3 — the lit face, top-lit through to a deep shadow at
   the baseline, with a hairline rim where the light catches. */
.gt-cd-face {
  color: transparent;
  background-image: var(--gt-face);
  -webkit-background-clip: text;
          background-clip: text;
  transform: translate3d(0, 0, 0);
}
.gt-cd-rim {
  color: transparent;
  -webkit-text-stroke: .008em rgba(255, 250, 235, .55);
  transform: translate3d(0, -.006em, 2px);
}

/* ---- Countdown motion ------------------------------------
   Arrive from depth, overshoot toward the camera, counter-
   settle, hang, then release forward and dissolve. The hang is
   the point: numbers that never stop moving feel cheap, and
   numbers that stop dead feel static. This one settles, holds
   with a breath in it, and then leaves.
   ----------------------------------------------------------- */
@keyframes gtCdNum {
  0% {
    opacity: 0;
    transform: translate3d(0, -.05em, -360px) rotateX(-44deg) rotateZ(-3deg);
    animation-timing-function: cubic-bezier(.1, .78, .22, 1);
  }
  17% {
    opacity: 1;
    transform: translate3d(0, 0, 62px) rotateX(9deg) rotateZ(.9deg);
    animation-timing-function: cubic-bezier(.44, 0, .26, 1);
  }
  30% {
    transform: translate3d(0, 0, -20px) rotateX(-3deg) rotateZ(-.3deg);
    animation-timing-function: cubic-bezier(.34, 0, .22, 1);
  }
  41% {
    transform: translate3d(0, 0, 4px) rotateX(.8deg) rotateZ(0deg);
    animation-timing-function: cubic-bezier(.4, 0, .5, 1);
  }
  50% {
    opacity: 1;
    transform: translate3d(0, 0, 0) rotateX(0deg) rotateZ(0deg);
    animation-timing-function: linear;
  }
  72% {
    opacity: 1;
    /* the breath: 22px of dolly across 220ms, barely readable
       as motion but the numeral is never dead on the screen */
    transform: translate3d(0, 0, 22px) rotateX(-.6deg);
    animation-timing-function: cubic-bezier(.5, 0, .86, .42);
  }
  100% {
    opacity: 0;
    transform: translate3d(0, -.02em, 210px) rotateX(6deg);
  }
}

/* ---- The retry beat ---------------------------------------
   A 4s cinematic is right the first time and a toll booth the
   eighth, and Slalom gets restarted a dozen times in a sitting.
   So a repeat count runs at ~560ms a digit.

   The percentages are NOT the same ones played faster. Compressed
   linearly, the overshoot and the counter-settle land 60ms apart
   and read as one smeared bounce; so the arrival takes a bigger
   share of a shorter beat, the counter-settle is pushed out and
   deepened to stay legible, the second overshoot is dropped (it
   cannot be perceived in 40ms), and the hold is cut to almost
   nothing. Same shape, re-choreographed for the tempo.
   ----------------------------------------------------------- */
.gt-cd-quick .gt-cd-num { animation-name: gtCdNumQuick; }
@keyframes gtCdNumQuick {
  0% {
    opacity: 0;
    transform: translate3d(0, -.04em, -300px) rotateX(-40deg) rotateZ(-2.4deg);
    animation-timing-function: cubic-bezier(.1, .8, .2, 1);
  }
  26% {
    opacity: 1;
    transform: translate3d(0, 0, 58px) rotateX(8deg) rotateZ(.7deg);
    animation-timing-function: cubic-bezier(.45, 0, .24, 1);
  }
  46% {
    transform: translate3d(0, 0, -26px) rotateX(-3.6deg) rotateZ(-.3deg);
    animation-timing-function: cubic-bezier(.3, 0, .2, 1);
  }
  62% {
    opacity: 1;
    transform: translate3d(0, 0, 0) rotateX(0deg) rotateZ(0deg);
    animation-timing-function: cubic-bezier(.5, 0, .86, .42);
  }
  100% {
    opacity: 0;
    transform: translate3d(0, -.02em, 190px) rotateX(5deg);
  }
}
.gt-cd-quick .gt-cd-num.gt-cd-go { animation-name: gtCdGoQuick; }
@keyframes gtCdGoQuick {
  0% {
    opacity: 0;
    transform: translate3d(0, 0, -420px) rotateX(-26deg);
    animation-timing-function: cubic-bezier(.06, .84, .16, 1);
  }
  22% {
    opacity: 1;
    transform: translate3d(0, 0, 86px) rotateX(6deg);
    animation-timing-function: cubic-bezier(.42, 0, .2, 1);
  }
  40% {
    transform: translate3d(0, 0, -18px) rotateX(-2deg);
    animation-timing-function: cubic-bezier(.34, 0, .2, 1);
  }
  56% {
    opacity: 1;
    transform: translate3d(0, 0, 0) rotateX(0deg);
    animation-timing-function: cubic-bezier(.55, 0, .9, .3);
  }
  100% {
    opacity: 0;
    transform: translate3d(0, 0, 230px) scale(1.03);
  }
}
.gt-cd-quick .gt-cd-glow  { animation-duration: var(--gt-beat, 560ms); }
.gt-cd-quick .gt-cd-ring  { animation-duration: 560ms; }
.gt-cd-quick .gt-cd-ring-2 { animation-delay: 80ms; animation-duration: 640ms; }

/* GO is not a number. It comes in harder, holds longer, and
   goes out sideways-flat rather than receding — it is a
   release, not another beat in a sequence. */
.gt-cd-num.gt-cd-go {
  font-size: clamp(92px, 30vw, 166px);
  letter-spacing: -0.03em;
  animation-name: gtCdGo;
}
@keyframes gtCdGo {
  0% {
    opacity: 0;
    transform: translate3d(0, 0, -520px) rotateX(-30deg) scale(1);
    animation-timing-function: cubic-bezier(.07, .82, .18, 1);
  }
  14% {
    opacity: 1;
    transform: translate3d(0, 0, 96px) rotateX(7deg) scale(1);
    animation-timing-function: cubic-bezier(.4, 0, .22, 1);
  }
  26% {
    transform: translate3d(0, 0, -26px) rotateX(-2.4deg);
    animation-timing-function: cubic-bezier(.36, 0, .2, 1);
  }
  38% {
    transform: translate3d(0, 0, 0) rotateX(0deg);
    animation-timing-function: linear;
  }
  66% {
    opacity: 1;
    transform: translate3d(0, 0, 14px) rotateX(0deg);
    animation-timing-function: cubic-bezier(.55, 0, .9, .3);
  }
  100% {
    opacity: 0;
    transform: translate3d(0, 0, 250px) rotateX(0deg) scale(1.04);
  }
}

/* Shockwave on GO only. Two rings, offset, thin, gone in 700ms.
   Restraint is the whole design: every other beat gets nothing. */
.gt-cd-ring {
  position: absolute;
  left: 50%; top: 47%;
  width: 240px; height: 240px;
  margin: -120px 0 0 -120px;
  border-radius: 50%;
  border: 2px solid rgba(255, 240, 208, .55);
  opacity: 0;
  transform: scale(.24);
  animation: gtCdRing 760ms cubic-bezier(.14, .82, .28, 1) both;
}
.gt-cd-ring.gt-cd-ring-2 {
  border-width: 1px;
  border-color: rgba(255, 240, 208, .3);
  animation-delay: 110ms;
  animation-duration: 860ms;
}
@keyframes gtCdRing {
  0%   { opacity: 0;   transform: scale(.24); }
  10%  { opacity: .85; }
  100% { opacity: 0;   transform: scale(2.9); }
}

/* Countdown teardown when a game ends the count early. */
.gt-cd.gt-out { animation: gtScrimOut 200ms ease both; }

/* ============================================================
   GAME FADE-IN
   The canvas comes up out of the dark under the count, with a
   whisper of a dolly (1.028 -> 1) so the world settles into
   frame rather than simply appearing at full strength.
   ============================================================ */
.gt-fadein {
  animation: gtGameIn var(--gt-fade, 900ms) cubic-bezier(.26, .68, .3, 1) both;
  transform-origin: 50% 52%;
}
@keyframes gtGameIn {
  0%   { opacity: var(--gt-fade-from, 0); transform: scale(1.028); }
  55%  { opacity: 1; }
  100% { opacity: 1; transform: scale(1); }
}

/* ============================================================
   REDUCED MOTION
   Degrade, do not disable. The information still arrives, the
   hierarchy still reads, the depth cues (shading, halo, scrim)
   are all still there — only the travel is removed.
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  .gt-l {
    animation-name: gtFadeIn;
    animation-duration: 260ms;
    animation-timing-function: ease-out;
    animation-delay: 0ms !important;
  }
  .gt-banner.gt-out .gt-l {
    animation-name: gtFadeOut;
    animation-duration: 200ms;
    animation-delay: 0ms !important;
  }
  .gt-banner-kicker { animation-name: gtFadeIn; animation-duration: 260ms; }
  .gt-banner-rule   { animation-name: gtFadeIn; animation-duration: 260ms; animation-delay: 0ms; transform: none; }
  .gt-banner.gt-out .gt-banner-rule { animation-name: gtFadeOut; transform: none; }
  .gt-cd-num, .gt-cd-num.gt-cd-go {
    animation-name: gtCdFade;
    animation-timing-function: ease;
  }
  .gt-cd-ring { display: none; }
  .gt-cd-glow { animation-name: gtFadeInOut; }
  .gt-fadein  { animation-name: gtFadeIn; }
  @keyframes gtCdFade { 0% { opacity: 0; } 14% { opacity: 1; } 78% { opacity: 1; } 100% { opacity: 0; } }
}

@keyframes gtFadeIn     { from { opacity: 0; } to { opacity: 1; } }
@keyframes gtFadeOut    { from { opacity: 1; } to { opacity: 0; } }
@keyframes gtFadeInOut  { 0% { opacity: 0; } 20% { opacity: .6; } 100% { opacity: 0; } }
