/* =========================================================
   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 ═══════════════════════════ */

/* On a phone the whole hero, both keys included, fits above the browser's
   own bars and the docked tabs: a smaller drawing and tighter gaps. The
   desktop rules below put the room back. */
.home-hero {
  display: grid;
  align-items: center;
  padding-block: 0.5rem 2.5rem;
}

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

.home-hero__art {
  width: min(76vw, 30rem);
}

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

.home-hero__title {
  /* One line at every width: "We secure software." is about nine ems wide,
     so the size follows the text column rather than the window. */
  font-size: min(3rem, 10.4cqi);
  white-space: nowrap;
  font-weight: 700;
  line-height: 1.22;
  color: var(--ink-700);
}

.home-hero__title .home-hero__swap {
  display: inline-block;
  text-align: center;
  transition: opacity 180ms var(--ease), translate 180ms var(--ease);
}

.home-hero__title .home-hero__swap.is-build { color: var(--build); }
.home-hero__title .home-hero__swap.is-deploy { color: var(--deploy); }
.home-hero__title .home-hero__swap.is-secure { color: var(--secure); }
.home-hero__title .home-hero__swap.is-break { color: var(--break); }

.home-hero__title .home-hero__swap.is-out {
  opacity: 0;
  translate: 0 0.15em;
}

.home-hero__lede {
  margin-top: -0.5rem;
  font-family: var(--font-display);
  font-size: clamp(1.375rem, 1rem + 1.2vw, 1.875rem);
  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 64em the pages' tabs are docked along the bottom (--dock), so the
   drawing gives way to them: it is as tall as the screen allows once the
   header, the words, both keys, the tabs and a breath of white above them
   (--hero-rest) are counted, and never wider than it was. The drawing is
   560 by 490, so its width is that height times 8/7. */
@media (max-width: 63.99em) {
  .home-hero {
    --hero-rest: 25.5rem;
  }

  .home-hero__inner {
    gap: 0.75rem;
  }

  .home-hero__art {
    width: min(76vw, 30rem, max(8rem, (100svh - var(--hero-rest) - var(--dock, 0px)) * 8 / 7));
  }

  .home-hero__text {
    gap: 1rem;
  }
}

@media (min-width: 64em) {
  .home-hero {
    min-height: calc(100svh - 4.5rem);
  }

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

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

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

/* The QR code the hero's console opens (play.js): a white card on a dimmed
   page, the code large enough to scan from across a table. */
.qr {
  width: min(calc(100vw - 2 * var(--gutter)), 22rem);
  margin: auto;
  padding: 1.75rem 1.5rem 1.5rem;
  background: var(--white);
  border: 2px solid var(--ink);
  border-radius: 1.5rem;
  box-shadow: 6px 6px 0 0 var(--ink);
  color: var(--ink);
  text-align: center;
}

.qr[open] {
  display: grid;
  gap: 1rem;
  justify-items: center;
}

.qr::backdrop {
  background: rgb(23 20 28 / 0.55);
}

/* The title takes focus when the dialog opens (autofocus), so the Close key
   is not ringed by a mouse click; it is one Tab away. */
.qr__title {
  outline: none;
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 700;
  line-height: 1.2;
}

.qr__code {
  width: 100%;
  padding: 1rem;
  border: 2px solid var(--ink-100);
  border-radius: 1rem;
  color: var(--ink-900);
}

.qr__code svg {
  display: block;
  width: 100%;
  height: auto;
}

.qr__url {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--ink-500);
}

/* The fallback for whoever taps rather than scans: a plain link in the
   house's underline, quieter than the Close key under it. */
.qr__save {
  font-family: var(--font-display);
  font-weight: 700;
  color: var(--secure);
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 0.25em;
}

.qr__save:hover,
.qr__save:focus-visible {
  color: var(--ink-900);
}

@media (prefers-reduced-motion: no-preference) {
  .qr[open] {
    animation: qr-pop 260ms var(--ease-spring);
  }

  .qr[open]::backdrop {
    animation: qr-fade 200ms var(--ease);
  }
}

@keyframes qr-pop {
  from { opacity: 0; scale: 0.9; }
  to { opacity: 1; scale: 1; }
}

@keyframes qr-fade {
  from { opacity: 0; }
}

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

.strip {
  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;
  /* Horizontal only. overflow-x on its own turns overflow-y to auto too, so a
     key hopping or tilting past the row on a tap made the strip scroll up and
     down under a sideways swipe. Hidden clips where auto already did, so
     nothing is drawn differently; it only stops the vertical scroll. */
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}

/* The scroller clips at its padding edge, so the padding is the room a key has
   to hop and tilt into. Scoped under .strip to outrank the ul[role="list"]
   reset in theme.css, which otherwise zeroes it and trims the hop. */
.strip .strip__list {
  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(--periwinkle-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(--periwinkle-700);
  font-family: var(--font-display);
  font-size: 1.0625rem;
  font-weight: 600;
  text-decoration: underline;
  text-decoration-color: var(--periwinkle-200);
  text-decoration-thickness: 2px;
  text-underline-offset: 0.25em;
  overflow-wrap: anywhere;
}

.closing__email:hover {
  text-decoration-color: var(--periwinkle-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);
}

/* The assembly line. Every layer is an HTML element moved with translate,
   by exactly one tile of its background per cycle, so the loop is seamless
   and the compositor does the work. Clipped sideways only: a stunt has to be
   free to leave the line's box upwards. Measured in its own width (cqw), so
   the rover's drift is a translate too. */
.footer-line {
  position: relative;
  container-type: inline-size;
  overflow-x: clip;
  overflow-y: visible;
  height: clamp(11rem, 7rem + 10vw, 16rem);
  color: var(--ink);
}

.footer-belt,
.footer-bus {
  position: absolute;
  left: 0;
  right: 0;
  overflow: hidden;
}

/* The tiles are drawn at 320 by 64 and 240 by 26 and shown 1.4 times that,
   so the belt's top edge is 48px below its own top. */
.footer-belt {
  bottom: -1px;
  height: 90px;
}

.footer-bus {
  bottom: 104px;
  height: 36px;
  opacity: 0.9;
}

.footer-belt__roll,
.footer-bus__roll {
  position: absolute;
  inset: 0 auto 0 0;
  will-change: translate;
}

.footer-belt__roll {
  width: calc(100% + 448px);
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='320' height='64'%3E%3Crect x='40' y='3' width='46' height='30' rx='5' fill='%23ffb36b' stroke='%2317141c' stroke-width='3'/%3E%3Cpath d='M63 3 V33 M40 13 H86' stroke='%2317141c' stroke-width='2' opacity='.45'/%3E%3Crect x='196' y='11' width='36' height='22' rx='5' fill='%237fd2df' stroke='%2317141c' stroke-width='3'/%3E%3Crect x='252' y='17' width='24' height='16' rx='4' fill='%23fbd35a' stroke='%2317141c' stroke-width='3'/%3E%3Crect x='-2' y='33' width='324' height='33' fill='%235562c4'/%3E%3Cpath d='M-2 34.5 H322' stroke='%2317141c' stroke-width='3'/%3E%3Cpath d='M28 43 l-8 6 8 6 M108 43 l-8 6 8 6 M188 43 l-8 6 8 6 M268 43 l-8 6 8 6' stroke='%23ffffff' stroke-opacity='.45' stroke-width='3' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") repeat-x left bottom / 448px 90px;
}

.footer-bus__roll {
  width: calc(100% + 336px);
  translate: -336px 0;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='26'%3E%3Cpath d='M0 7 H240 M0 19 H240' stroke='%23aab8ee' stroke-width='3'/%3E%3Ccircle cx='60' cy='7' r='4' fill='%23ffffff' stroke='%23aab8ee' stroke-width='2.5'/%3E%3Ccircle cx='180' cy='19' r='4' fill='%23ffffff' stroke='%23aab8ee' stroke-width='2.5'/%3E%3Crect x='110' y='4' width='22' height='6' rx='3' fill='%23fbd35a'/%3E%3Crect x='20' y='16' width='16' height='6' rx='3' fill='%237fd2df'/%3E%3C/svg%3E") repeat-x left center / 336px 36px;
}

/* The rover's wheels stand on the belt's top edge, 42px above the line's
   foot: the wheel bottoms are a tenth of the rover's width above its own. */
.footer-rover {
  --rover-w: clamp(15rem, 10rem + 12vw, 24rem);
  position: absolute;
  z-index: 1;
  left: 0;
  bottom: calc(42px - var(--rover-w) * 0.1);
  width: var(--rover-w);
  translate: 6cqw 0;
}

.footer-rover__ride {
  transform-origin: 50% 85%;
}

/* Stunts (play.css "Rover stunts") play on the rover's rig inside the
   drawing; the flame only shows during a boost. */
.rover-flame {
  opacity: 0;
}

/* 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-belt__roll {
    animation: line-belt 3.2s linear infinite;
  }

  .footer-bus__roll {
    animation: line-bus 4.8s linear infinite;
  }

  .footer-rover {
    will-change: translate;
    animation: line-drift 28s ease-in-out infinite alternate;
  }

  .footer-rover__ride {
    will-change: translate, rotate;
    animation: line-bump 1.4s ease-in-out infinite;
  }

  /* The jolt: the belt kicks when something lands on it. On the belt
     element, not the rolling layer, so the roll never restarts. */
  .footer-line.is-jolting .footer-belt {
    animation: line-jolt 520ms var(--ease-out-soft);
  }
}

@keyframes line-belt {
  from { translate: 0 0; }
  to { translate: -448px 0; }
}

@keyframes line-bus {
  from { translate: -336px 0; }
  to { translate: 0 0; }
}

@keyframes line-drift {
  from { translate: 4cqw 0; }
  to { translate: calc(96cqw - 100%) 0; }
}

@keyframes line-bump {
  0%, 100% { translate: 0 0; rotate: 0deg; }
  20% { translate: 0 -2px; rotate: -0.6deg; }
  40% { translate: 0 0; rotate: 0deg; }
  62% { translate: 0 -1.5px; rotate: 0.5deg; }
  80% { translate: 0 0; rotate: 0deg; }
}

@keyframes line-jolt {
  0% { translate: 0 0; }
  25% { translate: 0 4px; }
  55% { translate: 0 -2px; }
  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(--periwinkle-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-screen {
  fill: var(--ink-900);
  font-family: var(--font-display);
  font-size: 16px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.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;
}

.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-slide 620ms cubic-bezier(0.2, 1.5, 0.4, 1) calc(var(--d) * 160ms) 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;
  }
}

@keyframes rig-slide {
  0% { translate: var(--fx) var(--fy); opacity: 0; }
  40% { opacity: 1; }
  100% { translate: 0 0; opacity: 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.
   - 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. */

.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; }
}
