Liftoff how it’s made

The idea

A rocket launches. The countdown ticks from T−10, smoke billows off the pad, the ground drops away, clouds rush past, the booster separates and tumbles, and the upper stage lights its own engine as the sky turns to stars. No JavaScript in the scene: the whole launch is one keyframe script on a clock.

Techniques

One keyframe script, two timelines

Every choreographed layer uses the same 0–100% script, written into keyframe percentages (0–12% countdown and ignition, 12–45% liftoff, 58–64% staging, 55–90% night falls). Each layer runs it on the same --loop clock:

.booster {
  inset: 45% 0 0;
  border-radius: 0 0 0.6vmin 0.6vmin;
  background: linear-gradient(#0000 8%, var(--c-accent) 0 13%, #0000 0), var(--hull-grad);
  animation: separate var(--loop) linear infinite both;
}
@keyframes separate {
  0%, 58% { translate: 0 0; rotate: 0deg; opacity: 1; }
  80%, 100% { translate: -8vmin 70dvh; rotate: -35deg; opacity: 0; }
}

The page never scrolls: the launch plays on the clock. The playback tab on the edge of the window sets its speed and whether it repeats; with loop off the launch runs once and holds its last frame, high in the stars.

Trade-off: putting the script in keyframe percentages instead of separate delays means one --loop length scales the whole launch, and the Launch length control is a single custom property. The cost is that each layer's keyframes hold its own hold-frames (0%, 58%), which is a little more verbose than ranges.

Two clocks on one element

The flames grow with the launch but flicker on a fast clock of their own. Animations are lists, so each runs at its own length:

.flame {
  inset: 100% 10% auto;
  block-size: 18vmin;
  animation: ignite var(--loop) linear infinite both, flicker 90ms linear infinite alternate;
}
@keyframes ignite { 0%, 5% { scale: 1 0; } 12%, 57% { scale: 1 1; } 59%, 100% { scale: 1 0; } }
@keyframes flicker { to { transform: scale(1.06, 0.86); } }

ignite animates the scale property and flicker animates transform. Individual transform properties compose with transform, so the two never fight.

A countdown and altimeter from typed integers

@property --t { syntax: "<integer>"; inherits: false; initial-value: 10; }
@property --alt { syntax: "<integer>"; inherits: false; initial-value: 0; }
.hud {
  inset: 4vmin auto auto 4vmin;
  padding: 0.4em 0.8em;
  border-radius: 0.5em;
  background: #000a; /* keeps white text above 4.5:1 on every sky state */
  counter-reset: t var(--t) alt var(--alt);
  animation: telemetry var(--loop) linear infinite both; /* paint-ok: counter text changes; tiny box */
}
.hud::before { content: "T\2212 " counter(t, decimal-leading-zero); display: block; }
.hud::after { content: "ALT " counter(alt) " KM"; display: block; }
@keyframes telemetry {
  0% { --t: 10; --alt: 0; }
  12% { --t: 0; --alt: 0; }
  100% { --t: 0; --alt: 420; }
}

Registered integers interpolate and round, so the HUD counts. The HUD is aria-hidden; the stage's aria-label describes the scene. A translucent black backing keeps the white text readable on the pale day sky as well as in space.

shape() nose cone with a polygon fallback

clip-path: polygon(50% 0, 85% 25%, 100% 45%, 100% 100%, 0 100%, 0 45%, 15% 25%);
clip-path: shape(from 50% 0%, curve to 100% 45% with 100% 12%, vline to 100%, hline to 0%, vline to 45%, curve to 50% 0% with 0% 12%, close);

A browser without shape() drops the second line and keeps the faceted polygon.

sibling-index() smoke spread

Each puff drifts sideways by --dx. Because a custom property accepts anything at parse time, the sibling-index() version sits behind @supports (order: sibling-index()), with inline --i as the fallback.

Performance notes

rawgzipbrotlibudgetok
9,140 B3,210 B2,788 B14,336 Byes

Every animated property is translate, rotate, scale, transform or opacity, apart from the HUD integers. Those change text, so each counter step costs a small layout and paint in the HUD box. The clouds have a static filter: blur() and only move, so they rasterise once. The sky is a 400%-tall gradient layer that is translated rather than having its background-position animated. Frame times and layer counts are not measured yet.

Accessibility

Known limitations

Reuse

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="light dark">
<title>Liftoff</title>
<!--css-->
<main class="stage" aria-label="A rocket on a launch pad counts down, lifts off, drops its booster and climbs through the clouds into space.">
<div class="sky" aria-hidden="true"></div>
<div class="stars" aria-hidden="true"></div>
<div class="clouds" aria-hidden="true"><i></i><i></i><i></i></div>
<div class="ground" aria-hidden="true"><div class="tower"></div><div class="smoke"><i style="--i:1"></i><i style="--i:2"></i><i style="--i:3"></i><i style="--i:4"></i><i style="--i:5"></i><i style="--i:6"></i></div></div>
<div class="rocket" aria-hidden="true"><div class="engine"></div><div class="upper"></div><div class="booster"><div class="flame"></div></div></div>
<p class="hud" aria-hidden="true"></p>
</main>
@import "lib/stage";

/* typed integers interpolate (and round), so an animation can count */
@property --t { syntax: "<integer>"; inherits: false; initial-value: 10; }
@property --alt { syntax: "<integer>"; inherits: false; initial-value: 0; }

@layer scene {
  .stage {
    /* one launch, on the clock; the playback tab sets speed and whether it repeats */
    --loop: 16s;
    --hull: #eceef4;
    --hull-shade: #8f97ad;
    --hull-grad: linear-gradient(90deg, var(--hull-shade), var(--hull) 30%, #fff 48%, var(--hull) 66%, var(--hull-shade));
    --fire: radial-gradient(ellipse 50% 100% at 50% 0, #fff 12%, #ffe066 30%, #ff8a1f 55%, #ff3d0000 100%);
    background: #cdeaff;
  }
  .stage * { position: absolute; }

  /* The script lives in keyframe percentages, so the same keyframes play on either timeline:
     0-12% countdown + ignition, 12-45% liftoff, 58-64% staging, 55-90% night falls */

  .sky {
    inset: 0 0 auto;
    block-size: 400%;
    translate: 0 -75%;
    background: linear-gradient(#01020a, #070b25 22%, #1d2a6b 45%, #4f7fd1 68%, #8fc6ff 85%, #cdeaff);
    animation: ascend var(--loop) linear infinite both;
  }
  @keyframes ascend { 0%, 12% { translate: 0 -75%; } 100% { translate: 0 0; } }

  .stars {
    inset: 0;
    opacity: 0;
    background:
      radial-gradient(1.5px 1.5px at 20% 30%, #fff 99%, #fff0),
      radial-gradient(1px 1px at 70% 60%, #fff 99%, #fff0),
      radial-gradient(1.2px 1.2px at 45% 85%, #fff 99%, #fff0),
      radial-gradient(1px 1px at 85% 15%, #fff 99%, #fff0);
    background-size: 180px 180px, 130px 130px, 230px 230px, 90px 90px;
    animation: stars var(--loop) linear infinite both;
  }
  @keyframes stars { 0%, 55% { opacity: 0; } 90%, 100% { opacity: 1; } }

  .clouds { inset: 0; }
  .clouds i {
    inset-block-start: -35%;
    inline-size: 60vmin;
    aspect-ratio: 3;
    opacity: 0.9;
    filter: blur(0.6vmin);
    --puff: radial-gradient(circle closest-side, #fff 70%, #fff0);
    background:
      var(--puff) 10% 70% / 40% 80% no-repeat,
      var(--puff) 45% 20% / 45% 100% no-repeat,
      var(--puff) 85% 75% / 38% 76% no-repeat;
    animation: drift var(--loop) linear infinite both;
  }
  .clouds i:nth-child(1) { inset-inline-start: -8%; --y: 170dvh; }
  .clouds i:nth-child(2) { inset-inline-start: 55%; inset-block-start: -70%; --y: 230dvh; scale: 1.4; }
  .clouds i:nth-child(3) { inset-inline-start: 20%; inset-block-start: -110%; --y: 290dvh; scale: 0.8; }
  @keyframes drift { 0%, 18% { translate: 0 0; } 75%, 100% { translate: 0 var(--y); } }

  .ground {
    inset: auto 0 0;
    block-size: 22%;
    background: linear-gradient(#6b8a55, #3b5233 40%, #2a3a25);
    animation: sink var(--loop) linear infinite both;
  }
  .ground::before { /* pad */
    content: "";
    position: absolute;
    inset: -2vmin auto auto calc(50% - 14vmin);
    inline-size: 28vmin;
    block-size: 4vmin;
    background: linear-gradient(#a2a6b0, #5a5d66);
    clip-path: polygon(8% 0, 92% 0, 100% 100%, 0 100%);
  }
  .tower {
    inset: auto auto 100% calc(50% + 7vmin);
    inline-size: 5vmin;
    block-size: 48vmin;
    border-inline: 0.6vmin solid #b8372b;
    background:
      repeating-linear-gradient(45deg, #0000 0 1.2vmin, #b8372b 0 1.6vmin),
      repeating-linear-gradient(-45deg, #0000 0 1.2vmin, #b8372b 0 1.6vmin);
  }
  @keyframes sink { 0%, 12% { translate: 0 0; } 45%, 100% { translate: 0 130dvh; } }

  .smoke { inset: 0; }
  .smoke i {
    --dx: calc((var(--i) - 3.5) * 9);
    inset: -7vmin auto auto calc(50% - 8vmin);
    inline-size: 16vmin;
    aspect-ratio: 1;
    border-radius: 50%;
    background: radial-gradient(closest-side, #f6f6f8, #d6d7dd 60%, #d6d7dd00);
    scale: 0;
    animation: puff var(--loop) linear infinite both;
  }
  @supports (order: sibling-index()) {
    .smoke i { --dx: calc((sibling-index() - (sibling-count() + 1) / 2) * 54 / sibling-count()); }
  }
  @keyframes puff {
    0%, 6% { scale: 0; translate: 0 0; opacity: 1; }
    20% { scale: 2.2; translate: calc(var(--dx) * 1vmin) -4vmin; opacity: 1; }
    45%, 100% { scale: 3.2; translate: calc(var(--dx) * 1.8vmin) -10vmin; opacity: 0; }
  }

  .rocket {
    inset: auto auto 22% calc(50% - 3.5vmin);
    inline-size: 7vmin;
    block-size: 44vmin;
    animation: liftoff var(--loop) linear infinite both;
  }
  @keyframes liftoff {
    0%, 12% { translate: 0 0; rotate: 0deg; animation-timing-function: ease-in; }
    45% { translate: 0 calc(-33dvh + 22vmin); rotate: 0deg; }
    100% { translate: 0 calc(-33dvh + 22vmin); rotate: 9deg; }
  }
  .upper {
    inset: 0 0 55%;
    background:
      radial-gradient(circle at 50% 58%, #1b2a4a 0 1.3vmin, #b8c0d4 0 1.7vmin, #0000 0),
      linear-gradient(#0000 80%, var(--c-accent) 0 86%, #0000 0),
      var(--hull-grad);
    clip-path: polygon(50% 0, 85% 25%, 100% 45%, 100% 100%, 0 100%, 0 45%, 15% 25%);
    clip-path: shape(from 50% 0%, curve to 100% 45% with 100% 12%, vline to 100%, hline to 0%, vline to 45%, curve to 50% 0% with 0% 12%, close);
  }
  .booster {
    inset: 45% 0 0;
    border-radius: 0 0 0.6vmin 0.6vmin;
    background: linear-gradient(#0000 8%, var(--c-accent) 0 13%, #0000 0), var(--hull-grad);
    animation: separate var(--loop) linear infinite both;
  }
  .booster::before, .booster::after { /* fins */
    content: "";
    position: absolute;
    inset: auto auto 0 -3vmin;
    inline-size: 3.5vmin;
    block-size: 9vmin;
    background: var(--c-accent);
    clip-path: polygon(100% 0, 100% 100%, 0 100%);
  }
  .booster::after { inset-inline-start: calc(100% - 0.5vmin); scale: -1 1; }
  @keyframes separate {
    0%, 58% { translate: 0 0; rotate: 0deg; opacity: 1; }
    80%, 100% { translate: -8vmin 70dvh; rotate: -35deg; opacity: 0; }
  }

  .flame, .engine { transform-origin: 50% 0; background: var(--fire); scale: 1 0; }
  .flame {
    inset: 100% 10% auto;
    block-size: 18vmin;
    animation: ignite var(--loop) linear infinite both, flicker 90ms linear infinite alternate;
  }
  .engine {
    inset: 45% 25% auto;
    block-size: 11vmin;
    animation: burn var(--loop) linear infinite both, flicker 90ms linear infinite alternate;
  }
  @keyframes ignite { 0%, 5% { scale: 1 0; } 12%, 57% { scale: 1 1; } 59%, 100% { scale: 1 0; } }
  @keyframes burn { 0%, 61% { scale: 1 0; } 65%, 100% { scale: 1 1; } }
  @keyframes flicker { to { transform: scale(1.06, 0.86); } }

  .hud {
    margin: 0;
    color: #fff;
    text-shadow: 0 1px 3px #0009;
    font: 600 clamp(0.8rem, 2.6vmin, 1.4rem)/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
    letter-spacing: 0.08em;
  }
  .hud {
    inset: 4vmin auto auto 4vmin;
    padding: 0.4em 0.8em;
    border-radius: 0.5em;
    background: #000a; /* keeps white text above 4.5:1 on every sky state */
    counter-reset: t var(--t) alt var(--alt);
    animation: telemetry var(--loop) linear infinite both; /* paint-ok: counter text changes; tiny box */
  }
  .hud::before { content: "T\2212 " counter(t, decimal-leading-zero); display: block; }
  .hud::after { content: "ALT " counter(alt) " KM"; display: block; }
  @keyframes telemetry {
    0% { --t: 10; --alt: 0; }
    12% { --t: 0; --alt: 0; }
    100% { --t: 0; --alt: 420; }
  }
  @media (prefers-reduced-motion: reduce) {
    .stage *, .stage *::before, .stage *::after { animation: none !important; } /* beats .clouds i / .smoke i */
  }
}

What ships: tokens and lib snippets inlined, lowered for the browser targets (minified in the real page).

@property --c-ink {
  syntax: "<color>";
  inherits: true;
  initial-value: #14121f;
}

@property --c-paper {
  syntax: "<color>";
  inherits: true;
  initial-value: #f4f1ff;
}

@property --c-accent {
  syntax: "<color>";
  inherits: true;
  initial-value: #6d4aff;
}

@property --c-car-wall {
  syntax: "<color>";
  inherits: true;
  initial-value: #e9e6dc;
}

@property --c-seat {
  syntax: "<color>";
  inherits: true;
  initial-value: #c9c4b4;
}

@property --c-line {
  syntax: "<color>";
  inherits: true;
  initial-value: #ff9500;
}

@property --c-tunnel {
  syntax: "<color>";
  inherits: true;
  initial-value: #05050a;
}

@property --c-lamp {
  syntax: "<color>";
  inherits: true;
  initial-value: #ffe9b0;
}

@property --c-platform {
  syntax: "<color>";
  inherits: true;
  initial-value: #dfe9ff;
}

@property --a-spin {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

@layer tokens {
  :root {
    color-scheme: light dark;
    --c-ink: light-dark(#14121f, #f4f1ff);
    --c-paper: light-dark(#f4f1ff, #0b0a14);
    --c-accent: light-dark(#6d4aff, #9d85ff);
    --c-car-wall: light-dark(#e9e6dc, #1a1826);
    --c-seat: light-dark(#c9c4b4, #2a2638);
    --c-line: light-dark(#ff9500, #ff9f1a);
    --c-tunnel: #05050a;
    --c-lamp: #ffe9b0;
    --c-platform: light-dark(#dfe9ff, #9fb4e8);
    --t-fast: .15s;
    --t-base: .4s;
    --t-slow: 1.2s;
    --t-drift: 24s;
    --t-rush: 1.6s;
    --t-swing: 3.6s;
    --ease-out: cubic-bezier(.22, 1, .36, 1);
    --ease-spring: linear(0, .22 2.1%, .73 6.5%, 1.04 10.7%, 1.09 13%, 1.02 20%, .99 27%, 1);
  }
}

@layer reset {
  *, :before, :after {
    box-sizing: border-box;
    margin: 0;
  }

  html {
    block-size: 100%;
  }
}

@layer base {
  body {
    background: var(--c-paper);
    min-block-size: 100dvh;
    color: var(--c-ink);
    font: 1rem / 1.5 system-ui, sans-serif;
    overflow: clip;
  }

  .stage {
    contain: strict;
    position: fixed;
    inset: 0;
    overflow: clip;
  }
}

@layer scene {
  .stage {
    --loop: 16s;
    --hull: #eceef4;
    --hull-shade: #8f97ad;
    --hull-grad: linear-gradient(90deg, var(--hull-shade), var(--hull) 30%, #fff 48%, var(--hull) 66%, var(--hull-shade));
    --fire: radial-gradient(ellipse 50% 100% at 50% 0, #fff 12%, #ffe066 30%, #ff8a1f 55%, #ff3d0000 100%);
    background: #cdeaff;
  }

  .stage * {
    position: absolute;
  }

  .sky {
    block-size: 400%;
    animation: ascend var(--loop) linear infinite both;
    background: linear-gradient(#01020a, #070b25 22%, #1d2a6b 45%, #4f7fd1 68%, #8fc6ff 85%, #cdeaff);
    inset: 0 0 auto;
    translate: 0 -75%;
  }

  @keyframes ascend {
    0%, 12% {
      translate: 0 -75%;
    }

    100% {
      translate: 0;
    }
  }

  .stars {
    opacity: 0;
    animation: stars var(--loop) linear infinite both;
    background: radial-gradient(1.5px 1.5px at 20% 30%, #fff 99%, #fff0) 0 0 / 180px 180px, radial-gradient(1px 1px at 70% 60%, #fff 99%, #fff0) 0 0 / 130px 130px, radial-gradient(1.2px 1.2px at 45% 85%, #fff 99%, #fff0) 0 0 / 230px 230px, radial-gradient(1px 1px at 85% 15%, #fff 99%, #fff0) 0 0 / 90px 90px;
    inset: 0;
  }

  @keyframes stars {
    0%, 55% {
      opacity: 0;
    }

    90%, 100% {
      opacity: 1;
    }
  }

  .clouds {
    inset: 0;
  }

  .clouds i {
    aspect-ratio: 3;
    opacity: .9;
    filter: blur(.6vmin);
    --puff: radial-gradient(circle closest-side, #fff 70%, #fff0);
    background: var(--puff) 10% 70% / 40% 80% no-repeat,
      var(--puff) 45% 20% / 45% 100% no-repeat,
      var(--puff) 85% 75% / 38% 76% no-repeat;
    inline-size: 60vmin;
    animation: drift var(--loop) linear infinite both;
    inset-block-start: -35%;
  }

  .clouds i:first-child {
    --y: 170dvh;
    inset-inline-start: -8%;
  }

  .clouds i:nth-child(2) {
    --y: 230dvh;
    inset-block-start: -70%;
    inset-inline-start: 55%;
    scale: 1.4;
  }

  .clouds i:nth-child(3) {
    --y: 290dvh;
    inset-block-start: -110%;
    inset-inline-start: 20%;
    scale: .8;
  }

  @keyframes drift {
    0%, 18% {
      translate: 0;
    }

    75%, 100% {
      translate: 0 var(--y);
    }
  }

  .ground {
    block-size: 22%;
    animation: sink var(--loop) linear infinite both;
    background: linear-gradient(#6b8a55, #3b5233 40%, #2a3a25);
    inset: auto 0 0;
  }

  .ground:before {
    content: "";
    clip-path: polygon(8% 0, 92% 0, 100% 100%, 0 100%);
    background: linear-gradient(#a2a6b0, #5a5d66);
    block-size: 4vmin;
    inline-size: 28vmin;
    position: absolute;
    inset: -2vmin auto auto calc(50% - 14vmin);
  }

  .tower {
    background: repeating-linear-gradient(45deg, #0000 0 1.2vmin, #b8372b 0 1.6vmin), repeating-linear-gradient(-45deg, #0000 0 1.2vmin, #b8372b 0 1.6vmin);
    border-inline: .6vmin solid #b8372b;
    block-size: 48vmin;
    inline-size: 5vmin;
    inset: auto auto 100% calc(50% + 7vmin);
  }

  @keyframes sink {
    0%, 12% {
      translate: 0;
    }

    45%, 100% {
      translate: 0 130dvh;
    }
  }

  .smoke {
    inset: 0;
  }

  .smoke i {
    --dx: calc((var(--i) - 3.5) * 9);
    aspect-ratio: 1;
    inline-size: 16vmin;
    animation: puff var(--loop) linear infinite both;
    background: radial-gradient(closest-side, #f6f6f8, #d6d7dd 60%, #d6d7dd00);
    border-radius: 50%;
    inset: -7vmin auto auto calc(50% - 8vmin);
    scale: 0;
  }

  @supports (order: sibling-index()) {
    .smoke i {
      --dx: calc((sibling-index() - (sibling-count() + 1) / 2) * 54 / sibling-count());
    }
  }

  @keyframes puff {
    0%, 6% {
      opacity: 1;
      translate: 0;
      scale: 0;
    }

    20% {
      scale: 2.2;
      translate: calc(var(--dx) * 1vmin) -4vmin;
      opacity: 1;
    }

    45%, 100% {
      scale: 3.2;
      translate: calc(var(--dx) * 1.8vmin) -10vmin;
      opacity: 0;
    }
  }

  .rocket {
    block-size: 44vmin;
    inline-size: 7vmin;
    animation: liftoff var(--loop) linear infinite both;
    inset: auto auto 22% calc(50% - 3.5vmin);
  }

  @keyframes liftoff {
    0%, 12% {
      animation-timing-function: ease-in;
      translate: 0;
      rotate: 0deg;
    }

    45% {
      translate: 0 calc(22vmin - 33dvh);
      rotate: 0deg;
    }

    100% {
      translate: 0 calc(22vmin - 33dvh);
      rotate: 9deg;
    }
  }

  .upper {
    background: radial-gradient(circle at 50% 58%, #1b2a4a 0 1.3vmin, #b8c0d4 0 1.7vmin, #0000 0),
      linear-gradient(#0000 80%, var(--c-accent) 0 86%, #0000 0),
      var(--hull-grad);
    clip-path: polygon(50% 0, 85% 25%, 100% 45%, 100% 100%, 0 100%, 0 45%, 15% 25%);
    clip-path: shape(from 50% 0%, curve to 100% 45% with 100% 12%, vline to 100%, hline to 0%, vline to 45%, curve to 50% 0% with 0% 12%, close);
    inset: 0 0 55%;
  }

  .booster {
    background: linear-gradient(#0000 8%, var(--c-accent) 0 13%, #0000 0), var(--hull-grad);
    animation: separate var(--loop) linear infinite both;
    border-radius: 0 0 .6vmin .6vmin;
    inset: 45% 0 0;
  }

  .booster:before, .booster:after {
    content: "";
    background: var(--c-accent);
    clip-path: polygon(100% 0, 100% 100%, 0 100%);
    block-size: 9vmin;
    inline-size: 3.5vmin;
    position: absolute;
    inset: auto auto 0 -3vmin;
  }

  .booster:after {
    inset-inline-start: calc(100% - .5vmin);
    scale: -1 1;
  }

  @keyframes separate {
    0%, 58% {
      opacity: 1;
      translate: 0;
      rotate: 0deg;
    }

    80%, 100% {
      opacity: 0;
      translate: -8vmin 70dvh;
      rotate: -35deg;
    }
  }

  .flame, .engine {
    transform-origin: 50% 0;
    background: var(--fire);
    scale: 1 0;
  }

  .flame {
    block-size: 18vmin;
    animation: ignite var(--loop) linear infinite both, flicker 90ms linear infinite alternate;
    inset: 100% 10% auto;
  }

  .engine {
    block-size: 11vmin;
    animation: burn var(--loop) linear infinite both, flicker 90ms linear infinite alternate;
    inset: 45% 25% auto;
  }

  @keyframes ignite {
    0%, 5% {
      scale: 1 0;
    }

    12%, 57% {
      scale: 1;
    }

    59%, 100% {
      scale: 1 0;
    }
  }

  @keyframes burn {
    0%, 61% {
      scale: 1 0;
    }

    65%, 100% {
      scale: 1;
    }
  }

  @keyframes flicker {
    to {
      transform: scale(1.06, .86);
    }
  }

  .hud {
    color: #fff;
    text-shadow: 0 1px 3px #0009;
    letter-spacing: .08em;
    counter-reset: t var(--t) alt var(--alt);
    animation: telemetry var(--loop) linear infinite both;
    background: #000a;
    border-radius: .5em;
    margin: 0;
    padding: .4em .8em;
    font: 600 clamp(.8rem, 2.6vmin, 1.4rem) / 1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
    inset: 4vmin auto auto 4vmin;
  }

  .hud:before {
    content: "T−" counter(t, decimal-leading-zero);
    display: block;
  }

  .hud:after {
    content: "ALT " counter(alt) " KM";
    display: block;
  }

  @keyframes telemetry {
    0% {
      --t: 10;
      --alt: 0;
    }

    12% {
      --t: 0;
      --alt: 0;
    }

    100% {
      --t: 0;
      --alt: 420;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .stage *, .stage :before, .stage :after {
      animation: none !important;
    }
  }
}

@layer utilities;

@property --t {
  syntax: "<integer>";
  inherits: false;
  initial-value: 10;
}

@property --alt {
  syntax: "<integer>";
  inherits: false;
  initial-value: 0;
}