/* =========================================================
   Play: the home page, the scenes, the rigs, the closing band
   and the footer's assembly line.

   After duolingo.com, turned to engineering: one white page, a big
   friendly Skull in every picture, the house's keys around it, rigs
   (small machines) that boot, chunky keys that press, lowercase
   headlines in each row's colour, and one coloured block at the
   bottom with an assembly line along its top.

   Performance rules this file keeps (the README has the why):
   - Nothing loops inside an SVG. Idle life is the HTML wrapper
     floating (.scene-float), which the compositor animates for
     almost nothing. Inside a drawing, motion is brief and answers
     something: the pop-in, a hover, a blink, a poke.
   - Never animate layout. Everything moves with translate, rotate
     and scale, and the rover's drift is in container units.
   - Off screen, nothing moves (.is-offscreen, from /js/play.js).
   ========================================================= */

/* ═══════════════════════════ SCENES ═══════════════════════════ */

.scene {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

.scene .key,
.scene .key-face,
.scene .scene-rig {
  transform-box: fill-box;
  transform-origin: center;
}

.scene__char[data-skull-poke] {
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

/* A blink is a class for 190ms (skull.js), not a loop. */
.sk.is-blinking .sk-eye {
  transform: scaleY(0.08);
}

/* Off screen, everything inside holds still. */
.is-offscreen,
.is-offscreen * {
  animation-play-state: paused !important;
}

@media (prefers-reduced-motion: no-preference) {
  /* Each key pops in, one after another, when its scene arrives; then it
     answers hover with a wiggle and a press with a duck. */
  .scene .key {
    transition:
      rotate 420ms var(--ease-spring),
      scale 420ms var(--ease-spring);
    animation: key-pop 620ms var(--ease-spring) backwards;
  }

  .scene .key:nth-of-type(1) { animation-delay: 260ms; }
  .scene .key:nth-of-type(2) { animation-delay: 340ms; }
  .scene .key:nth-of-type(3) { animation-delay: 420ms; }
  .scene .key:nth-of-type(4) { animation-delay: 500ms; }
  .scene .key:nth-of-type(5) { animation-delay: 580ms; }
  .scene .key:nth-of-type(6) { animation-delay: 660ms; }

  .scene .key:hover {
    rotate: 10deg;
    scale: 1.14;
  }

  .scene .key:nth-of-type(even):hover {
    rotate: -10deg;
  }

  .scene .key:active {
    rotate: -14deg;
    scale: 0.9;
    transition-duration: 120ms;
  }

  /* The Skull drops into its scene with a squash. On its own wrapper, so a
     poke's move (on .sk-char inside) never fights the arrival. */
  .scene .scene-rig {
    animation: scene-arrive 900ms var(--ease-out-soft) 180ms both;
  }

  /* A scene waits for its section: until it is on screen, its keys hold at
     the first frame of the pop and the Skull at the first of its arrival. */
  .play-js [data-reveal]:not(.is-visible) :is(.key, .scene-rig) {
    animation-play-state: paused;
  }

  /* The whole drawing floats, on the HTML element around it. */
  .scene-float {
    will-change: translate;
    animation: scene-float 6s ease-in-out infinite;
  }

  .home-row--flip .scene-float {
    animation-duration: 7s;
    animation-delay: -2s;
  }

  /* ---------- Poked ----------
     A scene's Skull plays the next move from its own list (data-skull-moves,
     MOVES in scenes.mjs) in step with its next face. Scoped on the svg, so the
     rule outranks any always-on animation on the same element. Hop and shake
     are the rig's own (motion.css). */
  svg.sk.is-spin .sk-char {
    transform-origin: 50% 55%;
    animation: move-spin 720ms var(--ease-spring);
  }

  svg.sk.is-jelly .sk-char {
    animation: move-jelly 760ms var(--ease-out-soft);
  }

  svg.sk.is-flip .sk-char {
    transform-origin: 50% 55%;
    animation: move-flip 820ms var(--ease-out-soft);
  }

  svg.sk.is-bounce .sk-char {
    animation: move-bounce 900ms var(--ease-out-soft);
  }

  svg.sk.is-dizzy .sk-char {
    transform-origin: 50% 55%;
    animation: move-dizzy 1.2s var(--ease-out-soft);
  }

  /* ---------- Rover stunts ----------
     The footer's rover, poked: the Skull, the chassis and the wheels
     together (.rover-rig), one stunt per poke, in step with its face. A
     wheelie on the back wheel, a jump, a barrel roll, a boost with the
     thruster lit, and a drift. Anchored at the wheels, because stunts start
     from the belt. */
  .rover-rig {
    transform-box: fill-box;
    transform-origin: 50% 100%;
  }

  .rover-wheel {
    transform-box: fill-box;
    transform-origin: center;
  }

  svg.scene.is-wheelie .rover-rig { transform-origin: 18% 100%; animation: rover-wheelie 900ms var(--ease-out-soft); }
  svg.scene.is-jump .rover-rig { animation: rover-jump 900ms var(--ease-out-soft); }
  svg.scene.is-barrel .rover-rig { transform-origin: 50% 60%; animation: rover-barrel 1s var(--ease-out-soft); }
  svg.scene.is-boost .rover-rig { animation: rover-boost 1.1s var(--ease-out-soft); }
  svg.scene.is-boost .rover-flame { animation: rover-flame 1.1s steps(1); }
  svg.scene.is-drift .rover-rig { animation: rover-drift 760ms var(--ease-out-soft); }
  svg.scene.is-dizzy .rover-rig { transform-origin: 50% 60%; animation: move-dizzy 1.2s var(--ease-out-soft); }
  svg.scene:is(.is-boost, .is-drift, .is-wheelie) .rover-wheel { animation: rover-spin 600ms linear 2; }
}

@keyframes key-pop {
  0% { scale: 0; }
  70% { scale: 1.12; }
  100% { scale: 1; }
}

@keyframes scene-arrive {
  0% { translate: 0 16px; scale: 0.92 1.06; }
  45% { translate: 0 -5px; scale: 1.04 0.96; }
  72% { translate: 0 1px; scale: 0.99 1.01; }
  100% { translate: 0 0; scale: 1 1; }
}

@keyframes scene-float {
  0%, 100% { translate: 0 0; }
  50% { translate: 0 -10px; }
}

@keyframes move-spin {
  0% { rotate: 0deg; scale: 1; }
  35% { scale: 0.9; }
  100% { rotate: -360deg; scale: 1; }
}

@keyframes move-jelly {
  0% { scale: 1 1; }
  18% { scale: 1.22 0.8; }
  36% { scale: 0.84 1.18; }
  54% { scale: 1.1 0.92; }
  72% { scale: 0.96 1.05; }
  100% { scale: 1 1; }
}

@keyframes move-flip {
  0% { scale: 1 1; }
  25% { scale: 0 1.04; }
  50% { scale: -1 1; }
  75% { scale: 0 1.04; }
  100% { scale: 1 1; }
}

@keyframes move-bounce {
  0% { translate: 0 0; scale: 1 1; }
  14% { translate: 0 0; scale: 1.14 0.82; }
  40% { translate: 0 -46px; scale: 0.9 1.12; rotate: 8deg; }
  62% { translate: 0 0; scale: 1.16 0.84; rotate: 0deg; }
  76% { translate: 0 -10px; scale: 0.97 1.04; }
  88% { translate: 0 0; scale: 1.04 0.96; }
  100% { translate: 0 0; scale: 1 1; }
}

@keyframes move-dizzy {
  0% { rotate: 0deg; }
  30% { rotate: -540deg; }
  50% { rotate: -700deg; translate: 0 -6px; }
  64% { rotate: -735deg; }
  76% { rotate: -712deg; }
  88% { rotate: -724deg; }
  100% { rotate: -720deg; translate: 0 0; }
}

@keyframes rover-wheelie {
  0% { rotate: 0deg; }
  25% { rotate: -22deg; }
  60% { rotate: -18deg; }
  78% { rotate: 2deg; }
  100% { rotate: 0deg; }
}

@keyframes rover-jump {
  0% { translate: 0 0; scale: 1 1; }
  15% { translate: 0 4px; scale: 1.06 0.9; }
  50% { translate: 0 -64px; scale: 0.96 1.05; rotate: -6deg; }
  82% { translate: 0 3px; scale: 1.06 0.92; rotate: 0deg; }
  100% { translate: 0 0; scale: 1 1; }
}

@keyframes rover-barrel {
  0% { translate: 0 0; rotate: 0deg; }
  20% { translate: 0 -30px; }
  60% { translate: 0 -46px; rotate: 360deg; }
  84% { translate: 0 3px; rotate: 360deg; }
  100% { translate: 0 0; rotate: 360deg; }
}

@keyframes rover-boost {
  0% { translate: 0 0; rotate: 0deg; }
  14% { translate: -10px 0; rotate: 3deg; }
  45% { translate: 90px 0; rotate: -4deg; }
  75% { translate: 70px 0; rotate: 0deg; }
  100% { translate: 0 0; }
}

@keyframes rover-flame {
  0% { opacity: 1; }
  60% { opacity: 0; }
}

@keyframes rover-drift {
  0% { translate: 0 0; rotate: 0deg; }
  35% { translate: 30px 0; rotate: 10deg; }
  65% { translate: 18px 0; rotate: -6deg; }
  100% { translate: 0 0; rotate: 0deg; }
}

@keyframes rover-spin {
  to { rotate: 360deg; }
}

/* ═══════════════════════════ THE HOME HERO ═══════════════════════════ */

.home-hero {
  display: grid;
  align-items: center;
  padding-block: 1.5rem 3rem;
}

.home-hero__inner {
  display: grid;
  gap: 1.5rem;
  justify-items: center;
}

.home-hero__art {
  width: min(84vw, 24rem);
}

.home-hero__text {
  display: grid;
  gap: 2rem;
  justify-items: center;
  width: min(100%, 26rem);
  text-align: center;
}

.home-hero__title {
  font-size: clamp(1.625rem, 1.2rem + 1.6vw, 2.25rem);
  font-weight: 700;
  line-height: 1.22;
  color: var(--ink-700);
  text-wrap: balance;
}

.home-hero__keys {
  display: grid;
  gap: 0.875rem;
  width: 100%;
}

/* Below 52em the pages' tabs are docked along the bottom (--dock), and on a
   phone the opening should fit above them when the page opens: the
   headline, the statement and both keys, with a breath of white before the
   tabs. So the drawing gives way: it is as tall as the screen allows once the
   header, those words and keys and the tabs (--hero-rest) are counted, and
   never wider than it was. It is 400 by 540, so its width is that height
   times 20/27. The ask line follows below. */
@media (max-width: 63.99em) {
  .home-hero {
    --hero-rest: 27.25rem;
    padding-top: 0.5rem;
  }

  .home-hero__inner {
    gap: 0.75rem;
  }

  .home-hero__art {
    width: min(84vw, 24rem, max(7rem, (100svh - var(--hero-rest) - var(--dock, 0px)) * 20 / 27));
  }

  .home-hero__text {
    gap: 1.25rem;
  }

  .home-hero__statement {
    margin-top: -0.25rem;
  }
}

@media (min-width: 64em) {
  /* Capped, so a tall viewport (a big monitor, or the page zoomed out) does
     not float the hero in a screen of white. */
  .home-hero {
    min-height: min(calc(100svh - 4.5rem), 52rem);
  }

  .home-hero__inner {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 4rem;
    align-items: center;
  }

  .home-hero__art {
    width: min(100%, 32rem);
    justify-self: end;
  }

  .home-hero__text {
    justify-self: start;
    width: min(100%, 28rem);
  }
}

/* ═══════════════════════════ THE STRIP ═══════════════════════════ */

.strip {
  margin-top: clamp(2rem, 5vw, 3.5rem);
  border-block: 2px solid var(--rule);
}

.strip__row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.25rem;
}

/* Hidden until play.js has wired them; faded rather than removed at either
   end, so the row keeps its shape and shows which way there is more. */
.strip__arrow {
  display: none;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 0;
  border-radius: var(--radius-pill);
  background: none;
  color: var(--ink-500);
  cursor: pointer;
  transition:
    color 160ms var(--ease),
    background-color 160ms var(--ease),
    translate 260ms var(--ease-spring),
    opacity 200ms var(--ease);
}

.strip.is-wired .strip__arrow {
  display: grid;
}

.strip__arrow:hover:not(:disabled) {
  color: var(--ink);
  background: var(--periwinkle-50);
}

.strip__arrow[data-strip-prev]:hover:not(:disabled) { translate: -2px 0; }
.strip__arrow[data-strip-next]:hover:not(:disabled) { translate: 2px 0; }
.strip__arrow:active:not(:disabled) { scale: 0.9; }

.strip__arrow:disabled {
  opacity: 0.3;
  cursor: default;
}

.strip__list {
  display: flex;
  gap: 0.5rem 2rem;
  overflow-x: auto;
  scrollbar-width: none;
  padding-block: 1.1rem 0.9rem;
  padding-inline: 0.25rem;
}

.strip__list::-webkit-scrollbar {
  display: none;
}

.strip__list > li {
  flex: none;
}

/* Positioned, so the visually hidden note inside a link is placed inside the
   scroller with it. Otherwise it is positioned against the page, at its
   scrolled-out x, and widens the document. */
.strip__link {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  min-height: 2.75rem;
  color: var(--ink-500);
  font-family: var(--font-display);
  font-size: 0.875rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-decoration: none;
  text-transform: uppercase;
  white-space: nowrap;
  transition: color 160ms var(--ease);
}

.strip__link:hover,
.strip__link:focus-visible {
  color: var(--accent-ink);
}

/* The key hops off its ledge with a tilt, alternating along the row, so a
   pointer run along the strip reads as a row of keys being played. */
.strip__key {
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  border: 2px solid var(--ink);
  border-radius: 10px;
  background: var(--accent-bright);
  box-shadow: 0 3px 0 0 var(--accent);
  color: var(--ink);
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0;
  translate: 0 0;
  rotate: 0deg;
  transition:
    translate 320ms var(--ease-spring),
    rotate 320ms var(--ease-spring),
    box-shadow 320ms var(--ease-spring);
}

.strip__link:hover .strip__key,
.strip__link:focus-visible .strip__key {
  translate: 0 -5px;
  rotate: -9deg;
  box-shadow: 0 8px 0 0 var(--accent);
}

.strip__list > li:nth-child(even) .strip__link:hover .strip__key,
.strip__list > li:nth-child(even) .strip__link:focus-visible .strip__key {
  rotate: 9deg;
}

.strip__link:active .strip__key {
  translate: 0 2px;
  rotate: 0deg;
  box-shadow: 0 1px 0 0 var(--accent);
  transition-duration: 80ms;
}

/* ═══════════════════════════ THE ROWS ═══════════════════════════ */

.home-rows {
  display: grid;
  gap: clamp(3.5rem, 2rem + 5vw, 7rem);
  padding-block: clamp(3.5rem, 2.5rem + 4vw, 7rem);
}

.home-row {
  display: grid;
  align-items: center;
  justify-items: center;
  gap: 1.5rem 4rem;
  width: min(100% - 2 * var(--gutter), 68rem);
  margin-inline: auto;
  text-align: center;
  scroll-margin-top: 6rem;
}

.home-row__art {
  order: -1;
  width: min(80vw, 22rem);
}

.home-row__text {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.home-row__title {
  color: var(--accent-ink);
  font-size: clamp(2.25rem, 1.6rem + 2.6vw, 3.5rem);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.02em;
}

.home-row__lead {
  max-width: 32ch;
  margin-top: 1rem;
  color: var(--ink-500);
  font-size: 1.125rem;
  line-height: 1.55;
}

.home-row__actions {
  margin-top: 1.75rem;
}

.home-row .more {
  margin-top: 1rem;
}

@media (min-width: 48em) {
  .home-row {
    grid-template-columns: 1fr 1fr;
    justify-items: start;
    text-align: start;
  }

  .home-row__art {
    order: 0;
    justify-self: center;
    width: min(100%, 30rem);
  }

  .home-row--flip .home-row__art {
    order: -1;
  }

  .home-row__text {
    align-items: flex-start;
  }
}

/* ═══════════════════════════ THE CLOSING BAND ═══════════════════════════ */

.closing {
  padding-block: clamp(3rem, 2rem + 4vw, 6rem) clamp(2rem, 1rem + 3vw, 4rem);
  text-align: center;
}

.closing__inner {
  display: grid;
  justify-items: center;
}

.closing__art {
  width: min(100%, 34rem);
}

.closing__title {
  max-width: 22ch;
  margin-top: 1.5rem;
  color: var(--plum-700);
  font-size: clamp(2rem, 1.4rem + 2.6vw, 3.25rem);
  font-weight: 700;
  line-height: 1.08;
  letter-spacing: -0.02em;
  text-transform: lowercase;
}

.closing__lead {
  max-width: 34ch;
  margin-top: 1rem;
  color: var(--ink-500);
  font-size: 1.125rem;
}

.closing__keys {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  margin-top: 2rem;
}

.closing__email {
  color: var(--plum-700);
  font-family: var(--font-display);
  font-size: 1.0625rem;
  font-weight: 600;
  text-decoration: underline;
  text-decoration-color: var(--plum-200);
  text-decoration-thickness: 2px;
  text-underline-offset: 0.25em;
  overflow-wrap: anywhere;
}

.closing__email:hover {
  text-decoration-color: var(--plum-500);
}

.closing .ask--panel {
  width: min(100%, 44rem);
  margin-top: 2.5rem;
  padding: 0;
  border: 0;
  background: none;
  box-shadow: none;
  text-align: center;
}

.closing .ask__title {
  color: var(--ink-500);
  font-size: var(--step-0);
  font-weight: 600;
}

.closing .ask__questions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.6rem;
  margin-top: 0.9rem;
}

/* ═══════════════════════════ THE FOOTER ═══════════════════════════ */

.site-footer {
  margin-top: clamp(1rem, 3vw, 3rem);
  color: var(--white);
}

/* SkullSploit's footer signature: a data tape the Skull reads, in place of the
   parent's conveyor belt and SkullEdu's sea. Clipped sideways only, so the
   probe can lean out of the band upward; sized in its own width so the glide
   is a translate. */
.footer-scan {
  position: relative;
  container-type: inline-size;
  overflow-x: clip;
  overflow-y: visible;
  height: clamp(10rem, 7rem + 8vw, 14rem);
  color: var(--ink);
}

/* A finding, lit in the tape (play.js makes it and removes it). */
.scan-find {
  position: absolute;
  z-index: 2;
  display: grid;
  place-items: center;
  min-width: 1.6rem;
  height: 1.3rem;
  padding: 0 0.3rem;
  border: 2px solid var(--bad);
  border-radius: 6px;
  background: var(--white);
  color: var(--bad);
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 600;
  pointer-events: none;
  animation: scan-find 1200ms var(--ease-out-soft) forwards;
}

@keyframes scan-find {
  0% { opacity: 0; scale: 0.6; }
  15% { opacity: 1; scale: 1.12; }
  30% { scale: 1; }
  78% { opacity: 1; }
  100% { opacity: 0; scale: 1; translate: 0 -6px; }
}

/* The data tape. Two rows of hex drift past on HTML elements moved with
   translate, by exactly one tile per cycle so the loop is seamless and the
   compositor does the work. Clipped sideways only, so the probe is free to
   lean out of the band upward. Measured in its own width (cqw), so the probe's
   glide is a translate too. */
.footer-tape {
  position: absolute;
  left: 0;
  right: 0;
  overflow: hidden;
}

.footer-tape--front {
  bottom: 14px;
  height: 40px;
}

/* The faint row behind, higher up. */
.footer-tape:not(.footer-tape--front) {
  bottom: 70px;
  height: 32px;
}

.footer-tape__roll {
  position: absolute;
  inset: 0 auto 0 0;
  width: calc(100% + 380px);
  will-change: translate;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='380' height='40'%3E%3Ctext x='6' y='27' font-family='monospace' font-size='18' font-weight='600' letter-spacing='3' fill='%2317141c' fill-opacity='.5'%3E4f a2 1b 7c e0 55 9d 3a c1 0b 2e f4%3C/text%3E%3C/svg%3E") repeat-x left center / 380px 40px;
}

.footer-tape__roll--back {
  width: calc(100% + 300px);
  translate: -300px 0;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='32'%3E%3Ctext x='4' y='22' font-family='monospace' font-size='14' font-weight='600' letter-spacing='3' fill='%2317141c' fill-opacity='.24'%3E90 3e 7a 5e d1 33 86 b2 0f cc%3C/text%3E%3C/svg%3E") repeat-x left center / 300px 32px;
}

/* A hairline the tape runs along, so the band reads as a readout, not loose
   text floating on the page. */
.footer-tape--front::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  background: var(--rule);
}

/* The probe: the reticle and the Skull, gliding across the tape. Its window
   sits just above the front row. */
.footer-probe {
  --probe-w: clamp(13rem, 9rem + 11vw, 20rem);
  position: absolute;
  z-index: 1;
  left: 0;
  bottom: 2px;
  width: var(--probe-w);
  translate: 6cqw 0;
}

.footer-probe__ride {
  transform-origin: 50% 90%;
}

/* The scan beam sweeps the reticle window as a one-shot each time the probe
   reads something (play.js adds .is-scanning on the heartbeat and on a fuzz),
   never a loop inside the drawing. */
.probe-beam {
  opacity: 0;
  transform-box: fill-box;
}

/* Sparks (play.js makes them and removes them). */
.line-burst {
  position: absolute;
  z-index: 2;
  width: 0;
  height: 0;
  pointer-events: none;
}

.line-spark {
  position: absolute;
  top: -5px;
  left: -5px;
  width: 10px;
  height: 10px;
  background: var(--sunbeam-300);
  clip-path: polygon(50% 0, 64% 36%, 100% 50%, 64% 64%, 50% 100%, 36% 64%, 0 50%, 36% 36%);
  rotate: var(--rot, 0deg);
  scale: var(--s, 1);
  opacity: 0;
  animation: line-spark 620ms cubic-bezier(0.2, 0.7, 0.4, 1) var(--delay, 0ms) forwards;
}

.line-puff {
  position: absolute;
  top: -8px;
  left: -22px;
  width: 44px;
  height: 16px;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgb(255 255 255 / 0.9), rgb(255 255 255 / 0));
  animation: line-puff 640ms var(--ease-out-soft) forwards;
}

@media (prefers-reduced-motion: no-preference) {
  .footer-tape__roll {
    animation: tape-roll 7s linear infinite;
  }

  .footer-tape__roll--back {
    animation: tape-roll-back 11s linear infinite;
  }

  .footer-probe {
    will-change: translate;
    animation: probe-glide 26s ease-in-out infinite alternate;
  }

  .footer-probe__ride {
    will-change: translate;
    animation: probe-bob 3.2s ease-in-out infinite;
  }

  .footer-scan.is-scanning .probe-beam {
    animation: probe-beam 1.1s var(--ease-out-soft);
  }

  /* The fuzz: a poke flips the bytes. On the tape wrapper, not the rolling
     layer, so the drift never restarts. */
  .footer-scan.is-fuzzing .footer-tape {
    animation: tape-fuzz 560ms steps(7);
  }
}

@keyframes tape-roll {
  from { translate: 0 0; }
  to { translate: -380px 0; }
}

@keyframes tape-roll-back {
  from { translate: -300px 0; }
  to { translate: 0 0; }
}

@keyframes probe-glide {
  from { translate: 5cqw 0; }
  to { translate: calc(95cqw - 100%) 0; }
}

@keyframes probe-bob {
  0%, 100% { translate: 0 0; }
  50% { translate: 0 -4px; }
}

@keyframes probe-beam {
  0% { translate: 0 0; opacity: 0; }
  12% { opacity: 0.6; }
  52% { translate: 146px 0; opacity: 0.6; }
  100% { translate: 146px 0; opacity: 0; }
}

@keyframes tape-fuzz {
  0% { translate: 0 0; }
  20% { translate: 3px -1px; }
  40% { translate: -2px 1px; }
  60% { translate: 2px 1px; }
  80% { translate: -1px -1px; }
  100% { translate: 0 0; }
}

@keyframes line-spark {
  0% { translate: 0 0; opacity: 1; }
  60% { opacity: 1; }
  100% { translate: var(--dx) var(--dy); opacity: 0; }
}

@keyframes line-puff {
  from { scale: 0.3 0.5; opacity: 1; }
  to { scale: 1.8 1; opacity: 0; }
}

.footer-body {
  background: var(--plum-600);
  padding-block: clamp(1.5rem, 1rem + 2vw, 2.5rem) calc(1.5rem + env(safe-area-inset-bottom));
}

.footer-body__inner {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  gap: 2rem;
}

.footer-col--brand {
  grid-column: 1 / -1;
}

@media (min-width: 48em) {
  .footer-col--brand {
    grid-column: auto;
  }
}

.footer-heading {
  margin-bottom: 0.5rem;
  color: var(--white);
  font-family: var(--font-display);
  font-size: 1.1875rem;
  font-weight: 700;
}

.footer-line {
  color: rgb(255 255 255 / 0.88);
}

.footer-link {
  display: inline-block;
  padding-block: 0.4375rem;
  color: rgb(255 255 255 / 0.9);
  text-decoration: underline;
  text-decoration-color: transparent;
  text-decoration-thickness: 2px;
  text-underline-offset: 0.3em;
  overflow-wrap: anywhere;
  transition:
    color 160ms var(--ease),
    text-decoration-color 160ms var(--ease),
    translate 260ms var(--ease-spring);
}

.footer-link:hover,
.footer-link:focus-visible {
  color: var(--white);
  text-decoration-color: rgb(255 255 255 / 0.7);
  translate: 3px 0;
}

.footer-body :focus-visible {
  outline-color: var(--white);
}

.footer-base {
  margin-top: 2rem;
  padding-top: 1.25rem;
  border-top: 1px solid rgb(255 255 255 / 0.35);
  color: rgb(255 255 255 / 0.88);
  font-size: var(--step--1);
}

/* ═══════════════════════════ RIGS ═══════════════════════════

   The engineering scenes at the top of inner pages (components/rigs.mjs).
   Their motion is a boot, not a party: when a rig first comes on screen,
   /js/play.js adds .is-on and the machine assembles. Modules slide in and
   click home, the wires draw, the lights come on, a packet runs down each
   wire. After about three seconds the rig is .is-settled and answers the
   pointer instead: a hovered module lifts and sends a packet. Poking the
   Skull reboots it.

   The drawing is the finished state. Everything below only animates towards
   it, and only inside the no-preference query, so with no script or reduced
   motion a rig is simply complete. Every step is a few hundred milliseconds
   and answers something, so nothing loops inside the drawing. */

.page-head__art--rig {
  width: min(100%, 26rem);
  margin-inline: auto;
}

/* A rig is a drawing of a machine, not a portrait: give it the room. Wins
   over the page head's narrower art column (styles.css) by specificity. */
@media (min-width: 48em) {
  .page-head .page-head__inner:has(.page-head__art--rig) {
    grid-template-columns: minmax(0, 1fr) minmax(0, 26rem);
  }
}

@media (min-width: 64em) {
  .page-head .page-head__inner:has(.page-head__art--rig) {
    grid-template-columns: minmax(0, 1fr) minmax(0, 30rem);
  }

  .page-head__art--rig {
    width: 100%;
  }
}

.rig .rig-label {
  fill: var(--ink-500);
  font-family: var(--font-display);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.rig .rig-label--light {
  fill: var(--white);
}

.rig .rig-label--plate {
  fill: var(--ink);
  font-size: 13px;
  font-weight: 700;
}

.rig .rig-figure {
  fill: var(--ink);
  font-family: var(--font-display);
  font-size: 26px;
  font-weight: 700;
}

/* The binary dump on the home hero (RIGS.decode): a memory view the Skull
   reads. Monospace, because here the bits are the point. */
.rig .rig-addr {
  fill: var(--accent-ink);
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 1px;
}

.rig .rig-bits {
  fill: var(--ink);
  font-family: var(--font-mono);
  font-size: 15px;
  font-weight: 500;
  letter-spacing: 1.5px;
  opacity: 0.72;
}

.rig .rig-bits--faint {
  font-size: 13px;
  opacity: 0.12;
}

.rig .rig-bits--find {
  fill: #c0392b;
  opacity: 1;
}

.rig .rig-packet {
  stroke: var(--accent);
  stroke-dasharray: 0.08 1.2;
  stroke-dashoffset: 0.08;
  opacity: 0;
}

.rig .rig-scan {
  opacity: 0;
}

.rig .rig-unit { --d: 0; }
.rig .rig-d1 { --d: 1; }
.rig .rig-d2 { --d: 2; }
.rig .rig-d3 { --d: 3; }
.rig .rig-d4 { --d: 4; }
.rig .rig-d5 { --d: 5; }

.rig .rig-from-l { --fx: -40px; --fy: 0px; }
.rig .rig-from-r { --fx: 40px; --fy: 0px; }
.rig .rig-from-t { --fx: 0px; --fy: -32px; }
.rig .rig-from-b { --fx: 0px; --fy: 32px; }

.rig :is(.rig-mod, .rig-lift, .rig-led, .rig-keys rect, .sk-shackle) {
  transform-box: fill-box;
  transform-origin: center;
}

/* The rig's Skull does not drop in like the home page's: it rises into
   place beside its machine. Overrides the scene arrival in "Scenes". */
.rig .scene-rig {
  animation: none;
}

@media (prefers-reduced-motion: no-preference) {
  /* Before the boot: modules out of place, wires undrawn, lights off. */
  .play-js .rig:not(.is-on) :is(.rig-mod, .scene-rig) {
    opacity: 0;
  }

  .play-js .rig:not(.is-on) .rig-wire {
    stroke-dasharray: 1;
    stroke-dashoffset: 1;
  }

  .play-js .rig:not(.is-on) .rig-led {
    fill: var(--ink-100);
  }

  /* The boot. Each unit's step comes from its --d. */
  .rig.is-on:not(.is-settled) .rig-mod {
    animation: rig-decode 520ms var(--ease-out-soft) calc(var(--d) * 150ms) backwards;
  }

  .rig.is-on:not(.is-settled) .rig-wire {
    stroke-dasharray: 1;
    animation: rig-draw 520ms var(--ease-out-soft) calc(var(--d) * 160ms + 380ms) backwards;
  }

  .rig.is-on:not(.is-settled) .rig-led {
    animation: rig-led 560ms steps(1) calc(var(--d) * 160ms + 900ms) backwards;
  }

  .rig.is-on:not(.is-settled) .rig-packet {
    animation: rig-run 760ms var(--ease-out-soft) calc(var(--d) * 160ms + 1150ms);
  }

  .rig.is-on .scene-rig {
    animation: rig-rise 760ms var(--ease-spring) 120ms backwards;
  }

  /* Scene details: the scan sweeps the code, the bad line turns good, the
     lock shuts, the calculator's keys are pressed in turn. */
  .rig.is-on:not(.is-settled) .rig-scan {
    animation: rig-scan 1.1s var(--ease-out-soft) 300ms;
  }

  .rig.is-on:not(.is-settled) .rig-flag {
    animation: rig-flag 1.6s steps(1) backwards;
  }

  .rig.is-on:not(.is-settled) .sk-shackle {
    animation: rig-shackle 520ms cubic-bezier(0.2, 1.6, 0.4, 1) 1.5s backwards;
  }

  .rig.is-on:not(.is-settled) .rig-keys rect {
    animation: rig-key 180ms ease-out backwards;
  }

  .rig.is-on:not(.is-settled) .rig-keys rect:nth-of-type(4) { animation-delay: 500ms; }
  .rig.is-on:not(.is-settled) .rig-keys rect:nth-of-type(8) { animation-delay: 680ms; }
  .rig.is-on:not(.is-settled) .rig-keys rect:nth-of-type(2) { animation-delay: 860ms; }
  .rig.is-on:not(.is-settled) .rig-keys rect:nth-of-type(11) { animation-delay: 1040ms; }
  .rig.is-on:not(.is-settled) .rig-keys rect:nth-of-type(12) { animation-delay: 1260ms; }

  /* Settled, the rig answers the pointer. */
  .rig .rig-lift {
    transition: translate 260ms var(--ease-spring);
  }

  .rig.is-settled .rig-unit:is(:hover, .is-active) .rig-lift {
    translate: 0 -5px;
  }

  .rig.is-settled .rig-unit:is(:hover, .is-active) .rig-packet {
    animation: rig-run-again 700ms var(--ease-out-soft);
  }

  .rig.is-settled .rig-unit:is(:hover, .is-active) .rig-led {
    animation: rig-blink 420ms steps(1) 2;
  }

  .rig.is-settled .rig-keys rect:hover {
    animation: rig-key 180ms ease-out;
  }
}

/* A module resolves into place: it fades up with a small jitter and settles,
   the way a value does when a scan locks onto it. No slide from the edge, so
   SkullSploit's rigs read as analysis rather than the parent's assembly. */
@keyframes rig-decode {
  0% { opacity: 0; scale: 0.94; translate: 3px 0; }
  25% { opacity: 1; translate: -2px 0; }
  45% { translate: 2px 0; }
  65% { translate: -1px 0; scale: 1.02; }
  100% { opacity: 1; translate: 0 0; scale: 1; }
}

@keyframes rig-draw {
  from { stroke-dashoffset: 1; }
  to { stroke-dashoffset: 0; }
}

@keyframes rig-led {
  0% { fill: var(--ink-100); }
  30% { fill: #ffffff; }
  45% { fill: var(--ink-100); }
  60% { fill: #ffffff; }
  100% { fill: var(--mint-500); }
}

@keyframes rig-blink {
  0% { fill: #ffffff; }
  50% { fill: var(--mint-500); }
}

@keyframes rig-run {
  0% { stroke-dashoffset: 0.08; opacity: 1; }
  92% { opacity: 1; }
  100% { stroke-dashoffset: -1; opacity: 0; }
}

@keyframes rig-run-again {
  0% { stroke-dashoffset: 0.08; opacity: 1; }
  92% { opacity: 1; }
  100% { stroke-dashoffset: -1; opacity: 0; }
}

@keyframes rig-rise {
  0% { translate: 0 28px; rotate: -6deg; opacity: 0; }
  60% { translate: 0 -4px; rotate: 2deg; opacity: 1; }
  100% { translate: 0 0; rotate: 0deg; opacity: 1; }
}

@keyframes rig-scan {
  0% { translate: 0 0; opacity: 0.9; }
  90% { translate: 0 108px; opacity: 0.9; }
  100% { translate: 0 108px; opacity: 0; }
}

@keyframes rig-flag {
  0% { stroke: #e0453a; }
  80% { stroke: #e0453a; }
  100% { stroke: var(--mint-500); }
}

@keyframes rig-shackle {
  0% { translate: 0 -5px; }
  100% { translate: 0 0; }
}

@keyframes rig-key {
  0% { translate: 0 0; fill: var(--accent); }
  50% { translate: 0 3px; }
  100% { translate: 0 0; }
}

/* ---------- Section rigs ----------
   Beside a section's heading (ui.mjs sectionHeading). On a phone the rig
   sits under the heading; from a laptop up it takes the space the heading
   used to leave empty. */

.heading-row {
  display: grid;
  gap: 0 3rem;
  align-items: center;
  margin-bottom: clamp(2rem, 5vw, 3.5rem);
}

.heading-row > .heading {
  margin-bottom: 0;
}

.heading-row__art {
  width: min(100%, 22rem);
  margin: 1.5rem auto 0;
}

@media (min-width: 64em) {
  .heading-row {
    grid-template-columns: minmax(0, 1fr) minmax(0, 28rem);
  }

  .heading-row__art {
    width: 100%;
    margin: 0;
  }
}

.rig .rig-wire--ok {
  stroke: var(--mint-500);
  stroke-width: 4;
}

.rig :is(.rig-grow, .rig-turn, .rig-sweep, .rig-launch, .rig-fly, .rig-lower) {
  transform-box: fill-box;
}

.rig .rig-grow { transform-origin: 50% 100%; }
.rig .rig-turn { transform-origin: center; }

@media (prefers-reduced-motion: no-preference) {
  /* Bars grow from the axis instead of sliding. */
  .rig.is-on:not(.is-settled) .rig-grow {
    animation: rig-grow 700ms cubic-bezier(0.2, 1.3, 0.4, 1) calc(var(--d) * 160ms) backwards;
  }

  /* Gears turn as the machine starts; the small ones the other way. */
  .rig.is-on:not(.is-settled) .rig-turn {
    animation: rig-turn 1.6s var(--ease-out-soft) calc(var(--d) * 160ms) backwards;
  }

  .rig.is-on:not(.is-settled) .rig-turn--back {
    animation-name: rig-turn-back;
  }

  .rig.is-settled .rig-unit:hover .rig-turn {
    animation: rig-turn 900ms var(--ease-out-soft);
  }

  .rig.is-settled .rig-unit:hover .rig-turn--back {
    animation-name: rig-turn-back;
  }

  /* The lens sweeps across the report. */
  .rig.is-on:not(.is-settled) .rig-sweep {
    animation: rig-sweep 1.6s var(--ease-out-soft) 200ms backwards;
  }

  /* The rocket lifts a little, then settles on the pad. */
  .rig.is-on:not(.is-settled) .rig-launch {
    animation: rig-launch 1.4s var(--ease-out-soft) 1.1s;
  }

  /* The plane flies the dotted path into the inbox. */
  .rig.is-on:not(.is-settled) .rig-fly {
    animation: rig-fly 1.3s var(--ease-out-soft) 500ms backwards;
  }

  /* The crane lowers the next block onto the build. */
  .rig.is-on:not(.is-settled) .rig-lower {
    animation: rig-lower 1.2s cubic-bezier(0.3, 1.2, 0.5, 1) 700ms backwards;
  }
}

@keyframes rig-grow {
  from { scale: 1 0; }
  to { scale: 1 1; }
}

@keyframes rig-turn {
  from { rotate: -120deg; opacity: 0; }
  20% { opacity: 1; }
  to { rotate: 0deg; opacity: 1; }
}

@keyframes rig-turn-back {
  from { rotate: 120deg; opacity: 0; }
  20% { opacity: 1; }
  to { rotate: 0deg; opacity: 1; }
}

@keyframes rig-sweep {
  0% { translate: -170px 170px; opacity: 0; }
  15% { opacity: 1; }
  45% { translate: -150px 40px; }
  70% { translate: -80px 110px; }
  100% { translate: 0 0; opacity: 1; }
}

@keyframes rig-launch {
  0% { translate: 0 0; }
  30% { translate: 0 -14px; }
  60% { translate: 0 -10px; }
  100% { translate: 0 0; }
}

@keyframes rig-fly {
  0% { translate: -270px -80px; rotate: -30deg; opacity: 0; }
  10% { opacity: 1; }
  40% { translate: -170px -40px; rotate: 20deg; }
  70% { translate: -80px 20px; rotate: 30deg; }
  100% { translate: 0 0; rotate: 0deg; opacity: 1; }
}

@keyframes rig-lower {
  from { translate: 0 -90px; }
  to { translate: 0 0; }
}

/* ═══════════════════════════ LIFE ═══════════════════════════

   Every picture is alive while it is on screen, without being clicked, and
   a click changes it more (play.js, "Life"):

   - A heartbeat. Every couple of seconds something in each visible picture
     does something small: a packet runs down a wire, a light blinks, a gear
     nudges, a key wiggles (.is-pulse, .is-nudge). Each is a one-off of under
     a second, never a loop, so a drawing only repaints while it moves.
   - Tilt. The picture leans towards the pointer in 3D. That is a transform
     on the HTML wrapper, which the compositor moves without repainting.
   - Clicks. A module or key is kicked (.is-kicked): it jumps, fires a burst
     of packets, its light runs through the colours and the Skull reacts.
     Clicking empty space in a rig sends a surge through every wire
     (.is-surge). Poking the Skull reboots the whole machine. */

:is(.home-hero__art, .page-head__art, .heading-row__art, .home-row__art, .closing__art, .contact__rig) {
  transition: transform 500ms var(--ease-out-soft);
  will-change: transform;
}

.rig .rig-unit,
.scene .key {
  cursor: pointer;
}

@media (prefers-reduced-motion: no-preference) {
  .rig.is-settled .rig-unit.is-pulse .rig-packet,
  .rig.is-settled.is-surge .rig-packet {
    animation: rig-run-pulse 820ms var(--ease-out-soft);
  }

  .rig.is-settled .rig-unit.is-pulse .rig-led,
  .rig.is-settled.is-surge .rig-led {
    animation: rig-blink 420ms steps(1) 2;
  }

  .rig.is-settled .rig-unit.is-pulse .rig-turn {
    animation: rig-turn 1s var(--ease-out-soft);
  }

  .rig.is-settled .rig-unit.is-pulse .rig-turn--back {
    animation-name: rig-turn-back;
  }

  .rig.is-settled .rig-unit.is-pulse .rig-lift {
    animation: rig-nudge 600ms var(--ease-out-soft);
  }

  .rig.is-settled .rig-unit.is-kicked .rig-lift {
    animation: rig-kick 760ms cubic-bezier(0.2, 1.4, 0.4, 1);
  }

  .rig.is-settled .rig-unit.is-kicked .rig-packet {
    animation: rig-run-burst 380ms linear 3;
  }

  .rig.is-settled .rig-unit.is-kicked .rig-led {
    animation: rig-rainbow 900ms steps(1);
  }

  .rig.is-settled.is-surge .rig-lift {
    animation: rig-nudge 600ms var(--ease-out-soft);
  }

  .scene .key.is-nudge .key-face {
    animation: key-nudge 700ms var(--ease-out-soft);
  }

  .scene .key.is-kicked .key-face {
    animation: key-kick 820ms cubic-bezier(0.2, 1.4, 0.4, 1);
  }
}

@keyframes rig-run-pulse {
  0% { stroke-dashoffset: 0.08; opacity: 1; }
  92% { opacity: 1; }
  100% { stroke-dashoffset: -1; opacity: 0; }
}

@keyframes rig-run-burst {
  0% { stroke-dashoffset: 0.08; opacity: 1; }
  100% { stroke-dashoffset: -1; opacity: 1; }
}

@keyframes rig-nudge {
  0%, 100% { translate: 0 0; }
  35% { translate: 0 -4px; }
  70% { translate: 0 1px; }
}

@keyframes rig-kick {
  0% { translate: 0 0; rotate: 0deg; scale: 1; }
  20% { translate: 0 3px; scale: 1.06 0.92; }
  50% { translate: 0 -18px; rotate: -8deg; scale: 1.04; }
  75% { translate: 0 2px; rotate: 3deg; scale: 0.98 1.03; }
  100% { translate: 0 0; rotate: 0deg; scale: 1; }
}

@keyframes rig-rainbow {
  0% { fill: var(--sunbeam-300); }
  20% { fill: var(--rose-300); }
  40% { fill: var(--aqua-300); }
  60% { fill: var(--plum-300); }
  80% { fill: #ffffff; }
  100% { fill: var(--mint-500); }
}

@keyframes key-nudge {
  0%, 100% { rotate: 0deg; translate: 0 0; }
  25% { rotate: -7deg; translate: 0 -4px; }
  55% { rotate: 5deg; }
  80% { rotate: -2deg; }
}

@keyframes key-kick {
  0% { translate: 0 0; rotate: 0deg; scale: 1; }
  20% { scale: 1.1 0.88; }
  55% { translate: 0 -26px; rotate: 200deg; scale: 1.08; }
  85% { translate: 0 2px; rotate: 360deg; scale: 0.96 1.04; }
  100% { translate: 0 0; rotate: 360deg; scale: 1; }
}

/* ---------- The stages rig ----------
   Which of the seven stages the selected job needs. State, not motion, so it
   applies with reduced motion too; play.js changes the job. */
.rig .rig-d6 { --d: 6; }
.rig .rig-d7 { --d: 7; }
.rig .rig-d8 { --d: 8; }

.rig .rig-job {
  fill: var(--ink);
  font-family: var(--font-display);
  font-size: 16px;
  font-weight: 700;
}

.rig .rig-step {
  cursor: pointer;
}

.rig .rig-stage .rig-lift {
  transition: opacity 300ms var(--ease);
}

.rig .rig-stage.is-yes .rig-led { fill: var(--mint-500); }
.rig .rig-stage.is-some .rig-led { fill: var(--sunbeam-500); }
.rig .rig-stage.is-no .rig-led { fill: var(--ink-100); }
.rig .rig-stage.is-no .rig-lift { opacity: 0.4; }

@media (prefers-reduced-motion: no-preference) {
  .rig .rig-stage .rig-led {
    transition: fill 300ms var(--ease);
  }

  .rig .rig-step {
    transition: translate 200ms var(--ease-spring);
  }

  .rig .rig-step:hover {
    translate: 0 -2px;
  }

  .rig .rig-step:active {
    translate: 0 2px;
  }

  .rig [data-job-name].is-swapping {
    animation: rig-swap 360ms var(--ease-out-soft);
  }
}

@keyframes rig-swap {
  0% { opacity: 0; translate: 0 6px; }
  100% { opacity: 1; translate: 0 0; }
}


  to { background-position: -80% 0; }
}
