/* tactile
   Warp (warp.dev) is still the foundation: near black canvas, depth from
   background steps and never a shadow, monochrome to the point of severity,
   tight negative tracking on display type. The footer is folie's: a full bleed
   photograph behind the wordmark, with the type sitting on the dark part of it.

   The hero is Keeby's mechanic and none of Keeby's skin. A Mac bleeding off the
   left edge with a desktop you can actually type into is the right shape for a
   sound app. Rounded pills, warm greys and 3D keycaps are not, so the machine is
   a wireframe and the only radii on the page are the ones macOS window chrome
   actually has. */

:root {
  /* Pure black, because the switch frames are photographed on pure black and
     any other value leaves a visible seam where the frame ends. Screen
     blending was the first attempt and did not take. */
  --void: #000000;
  --deep: #000000;
  --surface: #161615;
  /* One step above the surface. The fake menu sits on the fake window, and on a
     page with no shadows a second background step is the only way to say so. */
  --raise: #1f1f1e;
  --line: #232322;
  --edge: #34342f;

  --text: #f6f5f2;
  --muted: #8b8b88;
  --dim: #676764;

  --pad: 24px;
  --max: 1080px;

  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
}

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--void);
  color: var(--text);
  font: 400 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img { max-width: 100%; height: auto; display: block; }
a { color: inherit; }

h1, h2, h3 { margin: 0; font-weight: 600; text-wrap: balance; }
h1 { font-size: clamp(32px, 5.2vw, 58px); line-height: 1.03; letter-spacing: -0.035em; }
h2 { font-size: clamp(24px, 3.2vw, 36px); line-height: 1.12; letter-spacing: -0.025em; }
p { margin: 0; text-wrap: pretty; }

.sub {
  max-width: 46ch;
  margin-top: 14px;
  font-size: 16px;
  color: var(--muted);
  letter-spacing: -0.01em;
}

/* ---------------------------------------------------------------- nav */

.nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 14px var(--pad);
  background: color-mix(in oklab, var(--deep) 76%, transparent);
  backdrop-filter: blur(20px);
  border-bottom: 1px solid var(--line);
}

.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -0.01em;
  text-decoration: none;
}

.wordmark img { border-radius: 5px; }

.tally {
  font-size: 13px;
  color: var(--dim);
  white-space: nowrap;
}

/* A fixed box in a tabular face, so the layout cannot twitch as the number
   grows a digit. */
.tally-n {
  display: inline-block;
  min-width: 5ch;
  text-align: right;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.navlink {
  font-size: 14px;
  color: var(--muted);
  text-decoration: none;
}

.navlink:hover { color: var(--text); }

/* --------------------------------------------------------------- hero */

.hero {
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(290px, 30vw, 430px);
  align-items: center;
  gap: clamp(20px, 3vw, 48px);
  /* No left padding: the machine is supposed to run off that edge. */
  padding: clamp(66px, 8vh, 92px) max(var(--pad), calc((100vw - var(--max)) / 2)) 0 0;
}

/* How far the machine hangs off the left of the viewport, and how big it is
   allowed to get. Wider than this and the keyboard below it no longer fits on
   one screen with it, which is the whole point of putting them together. */
.machine {
  max-width: 740px;
  margin-left: clamp(-150px, -7vw, -56px);
  container-type: inline-size;
}

.lid {
  position: relative;
  padding: 9px;
  border: 1px solid var(--edge);
  border-radius: 14px;
  background: var(--deep);
}

.notch {
  position: absolute;
  top: 10px;
  left: 50%;
  z-index: 3;
  width: 17%;
  max-width: 150px;
  height: 11px;
  transform: translateX(-50%);
  background: var(--void);
  border-radius: 0 0 7px 7px;
}

.screen {
  position: relative;
  display: flex;
  flex-direction: column;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  border-radius: 6px;
  background: var(--void);
  /* Everything inside the screen is sized in em off this, so the whole fake
     desktop scales with the machine instead of needing a second set of
     breakpoints. */
  font-size: clamp(9px, 1vw, 13px);
}

/* The base of the machine. A single tapered bar is the whole of a MacBook that
   is visible below the lid, and it is what stops the screen floating. */
.base {
  width: 106%;
  height: 11px;
  margin-left: -3%;
  border: 1px solid var(--edge);
  border-top: none;
  border-radius: 0 0 9px 9px;
  background: var(--surface);
}

/* ------------------------------------------------------------ menubar */

.menubar {
  display: flex;
  align-items: center;
  gap: 1.4em;
  height: 2.1em;
  padding: 0 1em;
  border-bottom: 1px solid var(--line);
  /* One step off pure black, so the notch below reads as a bite taken out of
     the bar rather than as nothing at all. */
  background: #0c0c0b;
  font-size: 0.92em;
  color: var(--muted);
}

.mb-name { font-weight: 600; color: var(--text); }
.mb-gap { flex: 1; }
.mb-clock { font-variant-numeric: tabular-nums; }

/* The app's own menubar item, drawn the way it looks when its menu is open. */
.mb-item {
  display: inline-flex;
  padding: 0.25em 0.45em;
  border-radius: 4px;
  background: var(--text);
  color: var(--void);
}

.mb-item svg { width: 1.55em; height: 1em; }

/* ------------------------------------------------------- fake desktop */

.desktop { position: relative; flex: 1; }

.win {
  position: absolute;
  display: flex;
  flex-direction: column;
  left: 15%;
  top: 16%;
  width: 40%;
  /* Sized off the fake screen rather than off the text, so the window keeps its
     proportions from a phone up to a wide desktop. */
  height: 62%;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: var(--surface);
  overflow: hidden;
}

.win-bar {
  display: flex;
  align-items: center;
  gap: 0.7em;
  padding: 0.5em 0.7em;
  border-bottom: 1px solid var(--line);
  font-size: 0.85em;
  color: var(--dim);
}

.dots { display: inline-flex; gap: 0.4em; }
.dots i { width: 0.62em; height: 0.62em; border-radius: 50%; background: var(--edge); }
.win-title { letter-spacing: -0.01em; }

/* A read-only echo of the typing test below, not an input. That is what lets it
   size with the fake screen: the 16px iOS floor only applies to something you
   can focus, and nothing here can be. */
.win-body {
  flex: 1;
  padding: 0.8em;
  color: var(--text);
  font: 400 1.08em/1.6 ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: -0.01em;
  overflow: hidden;
  overflow-wrap: anywhere;
  cursor: text;
}

/* A block caret, the way a terminal draws one, because the window has no real
   text cursor to borrow. */
.echo-caret {
  display: inline-block;
  width: 0.55em;
  height: 1em;
  vertical-align: -0.14em;
  background: var(--text);
}

/* --------------------------------------------------- the menubar menu */

.menu {
  position: absolute;
  right: 3%;
  top: 0;
  width: 40%;
  max-width: 250px;
  padding: 0.45em 0;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: var(--raise);
  font-size: 0.95em;
}

.menu-row {
  display: flex;
  align-items: center;
  gap: 0.7em;
  padding: 0.42em 0.85em;
  color: var(--text);
}

.menu-rule { height: 1px; margin: 0.35em 0; background: var(--line); }
.menu-dim { color: var(--dim); }
.menu-label { padding: 0.35em 0.85em 0.2em; color: var(--dim); }

.menu-power { cursor: pointer; }
.menu-power input { position: absolute; opacity: 0; pointer-events: none; }

/* The app's on switch, not a pill: a square track with a square knob, so it
   belongs to the same drawing as the rest of the machine. */
.sw {
  position: relative;
  flex: none;
  width: 2em;
  height: 1.1em;
  border: 1px solid var(--edge);
  border-radius: 2px;
  background: var(--surface);
}

.sw::after {
  content: "";
  position: absolute;
  inset: 0.15em auto 0.15em 0.15em;
  width: 0.7em;
  background: var(--dim);
}

.menu-power input:checked + .sw { background: var(--text); border-color: var(--text); }
.menu-power input:checked + .sw::after { inset: 0.15em 0.15em 0.15em auto; background: var(--void); }
.menu-power input:focus-visible + .sw { box-shadow: 0 0 0 2px var(--raise), 0 0 0 4px #6f6f6c; }

.menu-vol { justify-content: space-between; cursor: pointer; }
.menu-vol input { width: 55%; accent-color: var(--text); }

.menu-list {
  /* Positioned so that a row's offsetTop is measured against the list it
     scrolls inside, which is what keeps the selected set scrolled into view. */
  position: relative;
  max-height: 12.5em;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.menu-group {
  padding: 0.5em 0.85em 0.15em;
  color: var(--dim);
  font-size: 0.9em;
}

.menu-set {
  display: block;
  width: 100%;
  padding: 0.35em 0.85em;
  border: 0;
  background: transparent;
  color: var(--muted);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.menu-set:hover { color: var(--text); }
.menu-set.is-on { background: var(--text); color: var(--void); }
.menu-set.is-loading { color: var(--dim); }

/* --------------------------------------------------------- annotations */

.note {
  position: absolute;
  display: flex;
  align-items: center;
  gap: 0.2em;
  color: var(--muted);
  pointer-events: none;
}

.note svg { width: 4.6em; height: 3em; flex: none; }
.scrawl { font: 400 1.5em/1 "Bradley Hand", "Segoe Script", cursive; }

.note-type { left: 19%; bottom: 13%; }
/* The menu is up and to the right of this one, so the same drawn arrow is
   mirrored rather than a second path being invented for it. */
.note-pick { left: 62%; bottom: 5%; }
.note-pick svg { transform: scaleX(-1); }

/* ------------------------------------------------------------- pitch */

.lede {
  max-width: 34ch;
  margin-top: 18px;
  font-size: 17px;
  color: var(--muted);
  letter-spacing: -0.01em;
}

/* -------------------------------------------------------- typing test */

.test {
  max-width: var(--max);
  margin: clamp(34px, 5vh, 64px) auto 0;
  padding: 0 var(--pad);
}

.test-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--line);
}

.restart {
  display: inline-flex;
  align-items: center;
  /* Icon side loses 2px, so the glyph sits optically level with the word. */
  gap: 7px;
  padding: 9px 14px 9px 12px;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: var(--surface);
  color: var(--muted);
  font: 500 13px/1 inherit;
  letter-spacing: -0.01em;
  cursor: pointer;
}

.restart svg { width: 14px; height: 14px; flex: none; }
.restart:hover { color: var(--text); border-color: var(--edge); }
.restart:active { background: var(--text); border-color: var(--text); color: var(--void); }

/* The run is over and the only move left is this one. A static cue, not a
   colour: the page has no second colour to reach for. */
.test.is-done .restart { color: var(--text); border-color: var(--edge); }

.stats { display: flex; align-items: baseline; gap: clamp(16px, 3vw, 34px); }

.stat {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  font-size: 13px;
  color: var(--dim);
  letter-spacing: -0.01em;
}

/* Fixed box and a tabular face. Three digits is the widest any of these get, so
   nothing to the left of them moves as the numbers run. */
.stat b {
  display: inline-block;
  min-width: 3ch;
  text-align: right;
  font: 600 clamp(21px, 2.3vw, 27px)/1 inherit;
  letter-spacing: -0.035em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.words {
  --lh: 1.36em;
  position: relative;
  margin-top: clamp(20px, 2.6vw, 30px);
  /* Three lines, fixed, so the deck below never moves as the run advances. */
  height: calc(3 * var(--lh));
  overflow: hidden;
  font-size: clamp(25px, 3.3vw, 40px);
  cursor: text;
}

.words-scroll {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  gap: 0 0.42em;
  line-height: var(--lh);
  letter-spacing: -0.025em;
  color: var(--dim);
}

.word { white-space: nowrap; }

/* Monochrome, so the three states are weight and strike rather than red and
   green: untyped dim, correct full white, wrong struck through. */
.ch.ok { color: var(--text); }

.ch.bad,
.ch.extra {
  color: var(--muted);
  text-decoration: line-through;
  text-decoration-thickness: 2px;
}

.ch.extra { color: var(--dim); }

.caret {
  position: absolute;
  top: 0;
  left: 0;
  width: 2px;
  height: var(--lh);
  background: var(--text);
}

/* Dim until it has focus, which is the affordance: no line of copy telling you
   to click in. */
.words:not(.live) { opacity: 0.42; }
.words:not(.live) .caret,
.test.is-done .caret { visibility: hidden; }

/* Covers the words exactly. Never visible, always the thing a tap lands on. */
.sink {
  position: absolute;
  inset: 0;
  width: 100%;
  padding: 0;
  border: 0;
  background: transparent;
  /* 16px or iOS zooms the page the moment a finger lands on it. */
  font-size: 16px;
  color: transparent;
  caret-color: transparent;
  opacity: 0;
  cursor: text;
}

/* The ring belongs on the words, not on the invisible box over them. */
.sink:focus-visible { box-shadow: none; }

/* A text field matches :focus-visible even on a mouse click, so that alone
   would draw a box round the words every time anyone clicked in. The ring is
   for someone who arrived by Tab and has nothing else to go on; a click already
   gets the undim and a blinking caret, which is a better indicator than a box.
   `data-kb` is written by app.js off the last thing the reader touched. */
[data-kb] .words:has(.sink:focus) {
  box-shadow: 0 0 0 2px var(--void), 0 0 0 4px #6f6f6c;
}

/* -------------------------------------------------------------- deck */

/* The same key grid the app draws in Settings. One unit is the pitch of a real
   keycap, so on a wide screen these are close to life size, and every row adds
   up to the same 14.625 units so the grid is flush down both edges. */
.deck {
  --u: max(34px, calc((min(100vw, var(--max)) - 2 * var(--pad)) / 14.625));
  --gap: 4px;
  --deckw: calc(14.625 * var(--u) - var(--gap));
  display: grid;
  gap: var(--gap);
  width: var(--deckw);
  margin: clamp(18px, 3.4vh, 40px) auto 0;
  touch-action: manipulation;
}

.row { display: flex; gap: var(--gap); }

.key {
  width: calc(var(--w) * var(--u) - var(--gap));
  height: calc(var(--u) * 0.72 - var(--gap));
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: var(--surface);
  color: var(--dim);
  font: 500 clamp(9px, calc(var(--u) * 0.2), 14px)/1 inherit;
  letter-spacing: -0.01em;
  overflow: hidden;
  cursor: pointer;
  -webkit-user-select: none;
  user-select: none;
}

/* Lit, not coloured. The visualiser in the app inverts the cap, and inverting
   is the only emphasis available on a monochrome page. */
.key.on { background: var(--text); border-color: var(--text); color: var(--void); }

.stack {
  display: grid;
  gap: var(--gap);
  width: calc(var(--w) * var(--u) - var(--gap));
}

.stack .key {
  width: 100%;
  height: calc((var(--u) * 0.72 - var(--gap) - var(--gap)) / 2);
}

/* ------------------------------------------------------- scroll sequence */

/* The tall element is the scroll track. Its height is how much scrolling the
   rotation costs: 300vh gives roughly a full turn at an unhurried speed. */
.rig { height: 300vh; position: relative; margin-top: clamp(60px, 9vw, 120px); }

/* Two columns: the argument on the left, the switch turning to the right of
   it. The copy used to be absolutely positioned over the frame, which put it on
   whatever the switch happened to be showing at that scroll position. */
.stage {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100dvh;
  display: grid;
  grid-template-columns: minmax(0, 30ch) minmax(0, 1fr);
  align-items: center;
  gap: clamp(24px, 4vw, 64px);
  max-width: var(--max);
  margin: 0 auto;
  padding: 0 var(--pad);
}

.copy { z-index: 2; pointer-events: none; }

.seq { display: grid; place-items: center; width: 100%; }

.seq img {
  /* Both frames occupy one cell, so the pair stacks instead of the second one
     landing in a row of its own. */
  grid-area: 1 / 1;
  /* Still the biggest thing on the screen, now measured against its own column
     rather than the viewport. */
  width: min(100%, 78vh);
  /* The frames are square with the switch centred, so the box never reflows
     as the source swaps. */
  aspect-ratio: 1;
  object-fit: contain;
  /* The frames are rendered on pure black and so is the page behind them, so
     this changes no pixels today. It is kept because it is what stops a seam
     appearing at the frame's edge the moment anything behind it is not #000. */
  mix-blend-mode: screen;
}

/* Fades out as the rotation gets going. --p is written by sequence.js; without
   the script it stays unset and the hint simply shows. */
.hint {
  position: absolute;
  right: var(--pad);
  bottom: 28px;
  font-size: 12px;
  color: var(--dim);
  opacity: calc(1 - (var(--p, 0) * 6));
}

/* ---------------------------------------------------------- tone pad */

.tone, .library, .get {
  max-width: var(--max);
  margin: 0 auto;
  padding: clamp(70px, 10vw, 130px) var(--pad) 0;
}

.tone-wrap {
  display: grid;
  grid-template-columns: minmax(0, 400px) minmax(0, 1fr);
  gap: clamp(20px, 4vw, 60px);
  align-items: start;
  margin-top: 34px;
}

/* Card, one step up from the page, holding the two controls that decide what a
   keystroke sounds like. Square corners: the only radii on this page belong to
   the macOS chrome it is drawing. */
.tone-card {
  display: flex;
  align-items: stretch;
  gap: 14px;
  padding: 14px;
  border: 1px solid var(--line);
  background: var(--surface);
}


.read { display: grid; gap: 0; }

.read-row {
  display: flex;
  justify-content: space-between;
  gap: 20px;
  padding: 11px 0;
  border-top: 1px solid var(--line);
  font-size: 14px;
  color: var(--muted);
}

.read-row b { font-weight: 400; color: var(--text); font-variant-numeric: tabular-nums; }

.taps { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 24px; }

.tap {
  padding: 9px 14px;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: var(--surface);
  color: var(--muted);
  font: 500 13px/1 inherit;
  cursor: pointer;
  touch-action: manipulation;
}

.tap:hover { color: var(--text); border-color: var(--edge); }
.tap:active { background: var(--text); border-color: var(--text); color: var(--void); }

/* ---------------------------------------------------------- library */

/* Columns rather than a grid: the groups are four sets long and one set long,
   and a grid row is only as short as its tallest cell, which left holes the
   size of a group under every short one. */
.groups {
  columns: 300px 3;
  column-gap: 44px;
  margin-top: 30px;
}

.group { break-inside: avoid; }

.group h3 {
  padding: 26px 0 10px;
  font-size: 13px;
  font-weight: 600;
  color: var(--dim);
  letter-spacing: -0.005em;
}

.sw-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 2px 16px;
  width: 100%;
  padding: 14px 0;
  border: 0;
  border-top: 1px solid var(--line);
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.sw-name { font-size: 15px; color: var(--text); letter-spacing: -0.01em; }

.sw-blurb {
  grid-column: 1 / -1;
  font-size: 14px;
  color: var(--dim);
}

/* Written by CSS rather than by script, so the row cannot be left saying
   "Loading" by a failed promise that forgot to clear it. */
.sw-state { font-size: 13px; color: var(--dim); }
.sw-state::after { content: "Play"; }
.sw-row:hover .sw-state { color: var(--muted); }
.sw-row.is-loading .sw-state::after { content: "Loading"; }
.sw-row.is-on .sw-state::after { content: "Selected"; }
.sw-row.is-on .sw-name { text-decoration: underline; text-underline-offset: 4px; }

/* --------------------------------------------------------------- get */

.get h2 { max-width: 18ch; }

.cta {
  display: inline-block;
  margin-top: 30px;
  padding: 14px 22px;
  border: 1px solid var(--edge);
  border-radius: 3px;
  color: var(--text);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -0.01em;
  text-decoration: none;
  transition: border-color 150ms var(--ease-out), background-color 150ms var(--ease-out);
}
.cta:hover { border-color: var(--muted); background: var(--surface); }
.cta:focus-visible { outline: 2px solid var(--text); outline-offset: 3px; }

.req { margin-top: 12px; font-size: 13px; color: var(--dim); }

.body {
  max-width: 58ch;
  margin-top: 24px;
  font-size: 16px;
  color: var(--muted);
  letter-spacing: -0.01em;
}

.what {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 0 40px;
  max-width: 860px;
  margin: 44px 0 0;
  padding: 0;
  list-style: none;
}

.what li {
  padding: 16px 0;
  border-top: 1px solid var(--line);
  font-size: 15px;
  color: var(--muted);
}

.status { max-width: 58ch; margin-top: 34px; font-size: 13px; color: var(--dim); }

/* -------------------------------------------------------------- footer */

/* folie's footer: a photograph behind the whole thing, the type sitting on the
   dark part of it, and the wordmark running off the bottom edge. */
footer {
  position: relative;
  overflow: hidden;
  margin-top: clamp(80px, 12vw, 150px);
  border-top: 1px solid var(--line);
  background: var(--deep);
}

footer .backdrop { position: absolute; inset: 0; z-index: 0; pointer-events: none; }

footer .backdrop img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 35%;
  opacity: 0.5;
  /* The ramp starts above the credits so the small type never lands on the
     bright part of the chrome. */
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, transparent 62%);
  mask-image: linear-gradient(to bottom, #000 0%, transparent 62%);
}

.fwrap {
  position: relative;
  z-index: 1;
  max-width: var(--max);
  margin: 0 auto;
  padding: clamp(90px, 14vw, 170px) var(--pad) 0;
}

.credits-line {
  max-width: 66ch;
  font-size: 12px;
  line-height: 1.7;
  color: var(--dim);
}

.credits-line a { color: var(--muted); overflow-wrap: anywhere; }

/* Two stacked copies: the sharp one masked to the top, the blurred one to the
   bottom, so the mark dissolves as it runs off the page. One blur across the
   whole word just fogs it. */
.bigmark {
  position: relative;
  margin: clamp(40px, 7vw, 80px) 0 -0.2em;
  font-size: clamp(96px, 25vw, 330px);
  font-weight: 600;
  line-height: 0.8;
  letter-spacing: -0.055em;
  text-align: center;
  color: #ffffff;
  user-select: none;
  pointer-events: none;
}

.bigmark span {
  display: block;
  opacity: 0.1;
  -webkit-mask-image: linear-gradient(to bottom, #000 8%, transparent 82%);
  mask-image: linear-gradient(to bottom, #000 8%, transparent 82%);
}

.bigmark .soft {
  position: absolute;
  inset: 0;
  filter: blur(14px);
  opacity: 0.08;
  -webkit-mask-image: linear-gradient(to bottom, transparent 34%, #000 88%);
  mask-image: linear-gradient(to bottom, transparent 34%, #000 88%);
}

/* --------------------------------------------------------------- focus */

:focus-visible {
  outline: none;
  transition: none;
  box-shadow: 0 0 0 2px var(--void), 0 0 0 4px #6f6f6c;
  border-radius: 3px;
}

.win-body:focus-visible { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px #6f6f6c; }
.menu-set:focus-visible { box-shadow: 0 0 0 2px var(--raise), 0 0 0 4px #6f6f6c; }

/* --------------------------------------------------------------- motion */

/* Opt in, so a browser that does not understand the query animates nothing.
   Everything here is feedback on something the finger just did, which is why
   none of it goes past 200ms. */
@media (prefers-reduced-motion: no-preference) {
  .navlink, .tap, .sw-state, .sw-name, .menu-set, .restart { transition: color 0.14s var(--ease-out); }
  .key { transition: background-color 0.12s var(--ease-out), color 0.12s var(--ease-out); }
  .key.on { transition-duration: 0s; }
  .tap:active { transition-duration: 0s; }
  .words { transition: opacity 0.16s var(--ease-out); }
  /* The caret moves to a character that already exists, so it slides rather
     than jumping. Well inside the feedback budget. */
  .caret { transition: transform 0.09s var(--ease-out); }
  .words-scroll { transition: transform 0.16s var(--ease-out); }

  .caret, .echo-caret { animation: blink 1.06s steps(1, end) infinite; }
  /* Paused rather than removed, so a keyframe that passes through opacity 0
     can never leave either caret stuck invisible. */
  .words:not(.live) .caret { animation-play-state: paused; opacity: 1; }
}

@keyframes blink { 0%, 54% { opacity: 1 } 55%, 100% { opacity: 0 } }

/* ----------------------------------------------------------- responsive */

@media (max-width: 980px) {
  .hero {
    grid-template-columns: minmax(0, 1fr);
    padding-right: var(--pad);
    padding-left: var(--pad);
  }
  .machine { margin-left: clamp(-90px, -6vw, -24px); }
  .pitch { padding-top: 26px; }
  .lede { max-width: 46ch; }
}

@media (max-width: 820px) {
  /* One column, copy above the switch. Side by side, 30ch of text next to a
     square leaves neither of them enough room. */
  .stage {
    grid-template-columns: minmax(0, 1fr);
    align-content: center;
    gap: clamp(10px, 2vh, 22px);
  }
  .copy { max-width: none; }
  .seq img { width: min(94vw, 52vh); }
  .rig { height: 280vh; }
  .tone-wrap { grid-template-columns: minmax(0, 1fr); }
  /* Capped on the card, not on the pad: the pad is a flex child now and a cap
     on it would leave the card standing wider than its own contents. */
  .tone-card { max-width: 390px; }
  .read { margin-top: 8px; }
}

@media (max-width: 560px) {
  :root { --pad: 20px; }

  .tally { display: none; }

  .machine { margin-left: -22px; }
  .win { left: 8%; width: 58%; }
  .menu { width: 44%; }
  /* The screen is 250px tall on a phone. Settings and Quit are the two rows
     that demonstrate nothing, so they are the two that go. */
  .menu-dim, .menu-rule:last-of-type { display: none; }
  .menu-list { max-height: 7em; }
  .note-type { left: 9%; bottom: 4%; }
  .note-pick { right: 4%; bottom: 18%; }

  /* A phone has no function row and no number row worth tapping at 30px wide,
     and dropping them is what buys the rest of the keys a real hit area. */
  .row-fn, .row-num { display: none; }
  .key { height: 38px; }
  .stack .key { height: 17px; }
  /* An auto margin collapses to zero once the box is wider than its container,
     so the deck would hang off the right edge only. This centres the overhang. */
  .deck { margin-left: calc((100vw - var(--deckw)) / 2); }

  /* Side by side the three figures and the button run past 390px. Stacked, the
     figures keep the full width and stay the size they are worth. */
  .test-bar { flex-direction: column-reverse; align-items: stretch; gap: 13px; }
  .stats { justify-content: space-between; }
  .restart { align-self: flex-start; }

  .what { gap: 0; }
  .groups { columns: 1; }
  .read { max-width: none; }
}
