/* The app's readout face: Roboto subset to `0123456789.-s`, the same 4 KB file
   the app bundles (assets/fonts/). It carries no letters beyond "s", and that
   is the mechanism rather than a limitation — every other glyph falls through
   to the sans stack per glyph, with no code involved, so "500 ms" gets Roboto's
   digits and the system font's "ms".

   Served as TTF: this machine has no brotli, and 4 KB uncompressed is already
   smaller than the smallest image on the page.

   Two weights, which is the pair the app bundles and for the same reason: the
   readout is 700 and the small figures around it are 300. Ship only one and a
   request for the other silently renders in the weight that happens to exist. */
@font-face {
  font-family: RobotoNumerals;
  src: url("/fonts/RobotoNumerals-Bold.ttf") format("truetype");
  font-weight: 700;
  font-display: swap;
}
@font-face {
  font-family: RobotoNumerals;
  src: url("/fonts/RobotoNumerals-Light.ttf") format("truetype");
  font-weight: 300;
  font-display: swap;
}

:root {
  /* What the page supports, and in which order it would rather be read. This
     stays a *pair*: it is the declaration that lets a visitor with no stored
     choice be served their operating system's scheme. theme.js overrides it
     with a single value when a choice exists, and only a single value
     overrides — measured, see the design note for change 123. (Do not write
     that path with a glob here: a `*` followed by a slash ends this comment
     early and eats the declaration below, which is how this shipped once for
     the length of one test run.) */
  color-scheme: dark light;

  /* Both themes live in the tokens, via light-dark(<light>, <dark>), rather
     than in a `@media (prefers-color-scheme: light)` remap below. A media
     query answers to the operating system and to nothing else, so a button
     could never drive one; `color-scheme` is the switch instead, and it
     reaches places a media query cannot — including the wordmark, which is
     its own document behind an <img> and reads the color-scheme of the
     element embedding it.

     Dark is black, as the app's dark theme is
     (`scaffoldBackgroundColor: Colors.black`) — the previous blue-black put
     the app's own screenshots on a background they were never drawn against. */
  --panel:  light-dark(#F5F5F5, #000000);
  --raised: light-dark(#FFFFFF, #141414);
  --edge:   light-dark(#DCDCDC, #282828);
  --ink:    light-dark(#101010, #F2F2F2);
  --muted:  light-dark(#5A5A5A, #9A9A9A);
  --indigo: #6366F1;
  /* Pro's gold, and the darker variant for light surfaces — gold on white is
     unreadable. Split kept from lib/features/pro/pro_colors.dart. */
  --gold:   #FFCC33;
  --gold-ink: light-dark(#A87C00, #FFCC33);
  /* The measured tempo's colour, and the ink that reads on it. page.js keeps
     both in step with the reading; these are the resting values. */
  --tempo:  #6366F1;
  --tempo-ink: rgba(0, 0, 0, 0.87);
  /* The brand accent as *text*, for the places outside the instrument that
     want it: dark mode can use indigo neat, light mode cannot.

     Deliberately NOT an alias of --tempo. A custom property is substituted
     where it is *declared*, so `--x: var(--tempo)` here would freeze --tempo's
     resting value at :root and inherit that literal downwards — page.js sets
     --tempo on #rig, far below, and could never reach it. That bug shipped
     once and turned the readout permanently indigo. Anything that has to
     follow the measured tempo reads var(--tempo) directly, at the element.

     The same rule is why light-dark() is safe *here* and not for the readout:
     --indigo is a constant at :root, so freezing it at the point of
     declaration costs nothing. */
  --accent-text: light-dark(color-mix(in srgb, var(--indigo) 72%, #000), var(--indigo));
  --ghost:  light-dark(#B4B4B4, #4A4A4A);

  --num:  RobotoNumerals, Roboto, system-ui, sans-serif;
  --mono: ui-monospace, "SF Mono", "Cascadia Mono", "Roboto Mono", Menlo, Consolas, monospace;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;

  --gutter: clamp(1.25rem, 5vw, 3rem);

  /* The gap a block element opens above itself inside prose — a table, a
     screenshot strip, a bordered list, a card. One value rather than the
     1.25/1.5/1.75/2rem spread these used to carry each on their own, because
     four gaps within half a rem of each other read as a wobble, not a
     hierarchy. Captions that belong to a block sit closer than this on
     purpose; everything else keeps the rhythm. */
  --block-gap: 1.75rem;

  /* How wide a line of running text gets. It had been capped well inside the
     container — 34rem for the hero lede, 38 for body copy, the full 54 for
     legal paragraphs whose own lists stopped at 38 — on the usual argument
     that a line past about seventy characters is harder to return from.
     The cap is gone by decision: beside a table, a screenshot strip and a
     store card that all run the container's width, prose that stops three
     quarters of the way across does not read as a measured column, it reads
     as text that failed to fill its box. The container is the measure now,
     and the same edge runs down the whole page.
     What holds the line length in check instead is the container itself:
     54rem less the gutter is about 48rem of text, and the gutter grows with
     the viewport, so the line stops getting longer once the window does. */
  --measure: 100%;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--panel);
  color: var(--ink);
  font: 400 clamp(1rem, 0.95rem + 0.25vw, 1.125rem)/1.65 var(--sans);
  -webkit-font-smoothing: antialiased;
}

a { color: var(--indigo); }

:focus-visible {
  outline: 2px solid var(--indigo);
  outline-offset: 3px;
}

.wrap {
  max-width: 54rem;
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* ---------- masthead ---------- */

/* Wraps rather than overflows: the nav labels differ per page and per language,
   and a German label six pixels too wide would otherwise give the whole page a
   horizontal scrollbar on a 360 px phone. */
.mast {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1.25rem clamp(0.7rem, 3vw, 1.25rem);
  padding-block: 1.75rem 1.25rem;
}
/* `display: block` because the wordmark is a link on every page except the home
   one, where it would only link to itself. An inline image inside that link
   carried a line box, and the descender space under it pushed the pages row
   five pixels further down than on the home page — the same masthead, sitting
   differently on one page in twelve. The row gap above says the distance
   instead. */
.mast-logo {
  display: block;
  height: clamp(1.75rem, 5vw, 2.25rem);
  width: auto;
}
/* The pages and the two "how do I read this" controls, in one wrapper so the
   phone can put them behind a single button. On anything wider the wrapper
   dissolves — `display: contents` hands both halves straight back to the
   masthead's own flex row, where `order` puts them exactly where they sat
   before there was a menu. One masthead, two shapes, no duplicated markup. */
.mast-menu { display: contents; }
/* `margin-left: auto` because `justify-content: space-between` has no say once
   a row holds three items: without it the language switch would sit halfway
   between the wordmark and the badge instead of beside the badge it belongs
   next to. */
.mast-tools {
  flex: none;
  order: 2;
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: clamp(0.7rem, 3vw, 1.25rem);
}
/* The only call to action above the fold, and Google's own badge rather than a
   worded link: it names the platform and the store in one glance, which "Get
   the app" never did — it could as easily have led to an iOS build or a
   download that does not exist. Held at Play's 40 px minimum, which keeps the
   mast a mast; the card at the foot of each page carries it again, at the
   moment the visitor has a reason to want it.
   On every page, not only the two home pages it started on: most visitors will
   arrive on the delay calculator or the genre table straight from a search
   result, and those pages used to offer nothing to click until the very
   bottom. Each carries its own utm_source, so the store can say which page
   earned the install. */
.mast-badge {
  display: inline-block;
  order: 3;
  flex: none;
  line-height: 0;
  border-radius: 8px;
}
.mast-badge img {
  display: block;
  height: 40px;
  width: auto;
}

/* The pages, on a line of their own under the wordmark. This replaces the
   single "Tap a tempo" link the sub-pages carried, which was the whole of the
   site's navigation and led one way only: the wordmark already goes home, and
   nothing led to the other four pages at all.
   Its own row rather than four more items beside the badge and the language
   switch — six items in one flex line wrap unpredictably at 360 px, and the
   badge is the one thing above the fold worth clicking.
   `flex: 1 0 100%` makes the break deliberate instead: two header rows at
   every width, the second one wrapping to three lines on a narrow phone.
   Labels are shorter here than in the footer on purpose — a masthead is
   glanced at, a footer is read. */
.mast-pages {
  order: 4;
  flex: 1 0 100%;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.1rem;
  font: 500 0.9rem/1.4 var(--sans);
}
.mast-pages a {
  color: var(--muted);
  text-decoration: none;
  border-bottom: 1px solid var(--edge);
  padding-bottom: 2px;
}
.mast-pages a:hover {
  color: var(--ink);
  border-bottom-color: var(--indigo);
}
/* The page you are on keeps its place in the row — dropping it would shift
   every other label as you move between pages — but it is not a link, and it
   is the one entry in full ink. */
.mast-pages [aria-current] { color: var(--ink); }

/* The language switch sits next to the download link because someone who
   cannot read this page cannot be asked to scroll to the footer to find that
   out. Boxed rather than underlined so it does not read as a second CTA. */
.mast-lang,
.mast-scheme,
.mast-burger {
  flex: none;
  font: 600 0.78rem/1 var(--sans);
  letter-spacing: 0.05em;
  color: var(--muted);
  text-decoration: none;
  border: 1px solid var(--edge);
  border-radius: 6px;
  padding: 0.35rem 0.45rem;
}
.mast-lang:hover,
.mast-scheme:hover,
.mast-burger:hover { color: var(--ink); border-color: var(--indigo); }

/* Injected by theme.js, so it exists only where it can work. One icon for both
   directions rather than a sun that becomes a moon: the half-filled disc is
   the control, and which way it will go is what the label says. That leaves
   theme.js with a string to swap and no artwork to keep in step. */
.mast-scheme {
  display: inline-flex;
  align-items: center;
  background: none;
  cursor: pointer;
}
.mast-scheme svg {
  display: block;
  width: 0.95rem;
  height: 0.95rem;
}
.mast-burger svg {
  display: block;
  width: 1.15rem;
  height: 1.15rem;
}

/* Injected by theme.js, like the scheme button and for the same reason: a
   button that only reveals what CSS is hiding must not exist where the script
   never ran, or a visitor without JavaScript gets a masthead with its
   navigation locked behind a dead control. Hidden here at every width; the
   phone block below is the only place it appears. */
.mast-burger {
  display: none;
  order: 4;
  align-items: center;
  background: none;
  cursor: pointer;
}

/* The phone masthead: the wordmark and the store badge on one line, everything
   else behind the button.
   The badge beside the wordmark rather than under it — 360 px of viewport was
   about forty short of holding the wordmark, the language switch, the scheme
   button and the badge, so the row wrapped and the badge landed directly below
   the wordmark, reading as a second logo. Taking the three small controls out
   of the row leaves it with room to spare, and the badge stays the one thing
   above the fold worth tapping. */
@media (max-width: 34rem) {
  .mast.has-menu .mast-burger { display: inline-flex; }
  .mast.has-menu .mast-logo { height: 1.5rem; }
  /* The badge and the button travel to the right edge together, so the row
     reads wordmark | download · menu rather than three items spread evenly
     across it by `space-between`. */
  .mast.has-menu .mast-badge { margin-left: auto; }
  .mast.has-menu .mast-badge img { height: 34px; }
  /* In flow under the header rather than floating over the page: a panel that
     pushes the page down can never clip against it, and there is nothing above
     the fold it would be covering. */
  .mast.has-menu .mast-menu {
    order: 5;
    flex: 1 0 100%;
    display: none;
    flex-direction: column;
    /* Flush right, under the button that opened it. Left-aligned, the column
       had nothing to do with the corner it came out of; hanging from the
       burger it reads as that button's own panel. */
    align-items: flex-end;
    gap: 1rem;
    margin-top: 0.25rem;
    padding-top: 1rem;
    border-top: 1px solid var(--edge);
  }
  .mast.has-menu[data-menu="open"] .mast-menu { display: flex; }
  /* The pages first, the two controls under them: `order` put the tools above
     in the column, which is the masthead's own right-to-left reading turned
     ninety degrees and wrong here. */
  .mast.has-menu .mast-pages {
    order: 0;
    flex: none;
    flex-direction: column;
    /* Again on the nav itself: the column shrinks to its longest link, and
       without this the shorter ones sit left inside that box. */
    align-items: flex-end;
    gap: 0.75rem;
    font-size: 1rem;
    text-align: right;
  }
}

/* ---------- the instrument ---------- */

.rig {
  position: relative;
  /* A stacking context of its own, so the discs and rings the two hero controls
     paint at `z-index: -1` stay inside the instrument — behind the readout card
     and the beat row, the way the app renders its ripple in the page
     background rather than over the widgets around the pad. */
  isolation: isolate;
  border: 1px solid var(--edge);
  border-radius: 14px;
  background: var(--raised);
  padding: clamp(1.5rem, 4vw, 2.75rem);
}

/* The page's own name, at a size that says so. It was set smaller than the
   body text below it, which reads as a label on the instrument rather than as
   the title of the page. */
.rig-title {
  margin: 0;
  /* Room for the fullscreen button in the corner above it — the title is the
     one thing long enough to reach that far on a phone. */
  padding-right: 2.5rem;
  font: 700 clamp(1.5rem, 1.2rem + 1.4vw, 2.1rem)/1.15 var(--sans);
  letter-spacing: -0.02em;
}

.rig-lede {
  margin: 0.4rem 0 0;
  max-width: var(--measure);
  color: var(--muted);
}

/* The readout sits in its own card, as it does in the app, so the number reads
   as an instrument's display rather than as a large heading — and it is held to
   a phone's width even on a desktop, because stretched across 54 rem the same
   number reads as an empty banner. */
.readout {
  max-width: 26rem;
  margin: clamp(0.75rem, 2.5vw, 1.25rem) auto 0;
  padding: clamp(1rem, 3vw, 1.5rem) 1rem;
  border: 1px solid var(--edge);
  border-radius: 14px;
  background: var(--panel);
  text-align: center;
}

/* Weight 700 and the app's own numerals, for the reason app_text.dart gives:
   "the system font" is not one thing, and this is the most-looked-at element
   on the page. `ui-monospace` at weight 300 resolved to Consolas, SF Mono or
   Roboto Mono depending on who was looking. */
.readout-value {
  display: block;
  font: 700 clamp(4.5rem, 22vw, 9rem)/0.9 var(--num);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.03em;
  /* Taken down against a bright background, and resolved here at the element
     rather than through a token, so `--tempo` still means whatever page.js
     last set on #rig — a light-dark() sitting in a :root token would freeze
     the resting indigo instead. The tempo palette is built to carry a dark
     glyph, which is the opposite of what it needs to be to read *as* text on
     white. */
  color: light-dark(color-mix(in srgb, var(--tempo) 68%, #000), var(--tempo));
}

.readout-unit {
  display: block;
  margin-top: 0.5rem;
  font: 400 1.05rem/1 var(--sans);
  color: var(--ink);
}

/* A finished run stays in its tempo colour, as it does in the app — the label
   below already says "Final BPM", which is the same thing the app changes at
   the same moment. Only the weight of the whole card drops. */
.readout.is-stale { opacity: 0.86; }
/* The idle dash is a placeholder, not a value — say so, or an empty readout
   reads as a broken one. */
.readout.is-idle .readout-value { color: var(--ghost); }

/* What the reading is worth on its own: the Italian marking, which is what a
   score calls this speed. The beat in milliseconds stood beside it until the
   row read as a meta line rather than as a caption — two facts of different
   kinds, equally bold, joined by a middle dot, under a number the vibe line
   already explains in the reader's own words. Milliseconds have a page of their
   own here, and both tools link it in their prose. The ×½ and ×2 readings used
   to sit either side of them too — they are the app's, and giving them away
   here answered for free the question the app is bought to answer. The FAQ
   still explains why a reading can land at half. */
.facts {
  margin-top: 0.9rem;
  text-align: center;
  font-size: 0.85rem;
  color: var(--muted);
}
.facts b {
  font: 300 0.95rem/1 var(--num);
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}
/* The row is empty until a reading exists, and hiding it outright made the
   readout card grow at the first result — under the finger doing the tapping.
   It holds its space from the start instead: `hidden` still means invisible,
   and `visibility` still takes it out of the accessibility tree, it just no
   longer collapses the box. The dashes in the markup are what reserve the
   height, so the row must keep them.
   The `display` is doing the work here: it overrides the UA's
   `[hidden] { display: none }`, which any author `display` beats regardless of
   specificity. That is the same accident the three-column `display: flex` on
   this row once caused — only there nothing hid it afterwards, so the idle
   readout showed a row of dashes under its dashes. `.done` still needs the
   plain `display: none` guard; it has nothing to reserve. */
.facts[hidden] { display: block; visibility: hidden; }

/* Two lines are reserved whether or not the caption needs them. It is one line
   at 128 BPM and two at 150, and letting the box resize moved the trace, the
   pad and the button down the page on a tap — the one element on the page that
   must not move is the one being tapped. */
.vibe {
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0.6rem 0 0;
  line-height: 1.45;
  min-height: 2.9em;
  text-align: center;
  font-size: 0.95rem;
  color: var(--ink);
  opacity: 0.78;
}

/* One decaying spike per tap. Not the app's timing grid, which plots each tap
   against where the beat actually fell and so can tell you that you rush —
   this only draws where the taps landed relative to each other. Evenly spaced
   spikes mean even playing; bunched ones mean the beat got away. It stays free
   here for that reason, and because it is the only thing on the page that
   moves while you use it. Drawn by page.js. */
.trace {
  height: 32px;
  max-width: 26rem;
  margin: 0.9rem auto 0;
  border-bottom: 1px solid var(--edge);
}
.trace svg {
  display: block;
  width: 100%;
  height: 32px;
}
.trace path {
  fill: color-mix(in srgb, var(--tempo) 26%, transparent);
  stroke: var(--tempo);
  stroke-width: 1;
  /* The viewBox is stretched horizontally, so an unadjusted stroke would be
     drawn thicker on the vertical edges than on the horizontal ones. */
  vector-effect: non-scaling-stroke;
}

/* The two hero controls, lit the way the app lights them.
   `lib/core/pad_material.dart` is the model and this is its translation: one
   light at the top left, a rim ring with the face inset by its width, a matte
   dome falling off toward the lower right, a crease where the face meets the
   rim, and a contact shadow that tightens as the surface is pressed in. Nothing
   here is gloss — the depth is in the edge and the shadow, which is what
   separates a material from a coloured circle.

   The disc is a pseudo-element rather than the button's own background, because
   the tap pad's breath scales the face and not the label. Both pseudos carry
   `z-index: -1` inside the instrument's stacking context, which is also what
   puts the expanding ring behind the readout card — the app renders its ripple
   in the page background for the same reason. */
.pad,
.go {
  --face: var(--tempo);
  --ring: var(--tempo);
  --rim-lit: light-dark(rgb(255 255 255 / 0.22), rgb(255 255 255 / 0.32));
  --rim-far: light-dark(rgb(0 0 0 / 0.26), rgb(255 255 255 / 0.12));
  --rim-w: 1.5px;
  --glow: 35%;

  position: relative;
  display: grid;
  /* One full-width track, so the glyph's percentage width measures against the
     circle rather than against the label standing next to it. */
  grid-template-columns: minmax(0, 1fr);
  place-items: center;
  gap: 0.5rem;
  width: min(230px, 58vw);
  aspect-ratio: 1;
  margin: clamp(2.25rem, 8vw, 3.25rem) auto 0;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--tempo-ink);
  font: 600 1.05rem/1.3 var(--sans);
  letter-spacing: 0.01em;
  /* Icon and label carry the same shadow, so they read as one thing resting on
     the face rather than as marks printed into it. */
  text-shadow: 0 1px 6px rgb(0 0 0 / 0.54);
  cursor: pointer;
  touch-action: manipulation;
  -webkit-user-select: none;
  user-select: none;
  /* Android draws a grey box over whatever you are holding down. On a control
     you tap eight times in a row that box is the thing you end up watching,
     and it is rectangular over a round pad. The press is the feedback. */
  -webkit-tap-highlight-color: transparent;
}
.pad:active,
.go:active { scale: 0.95; }
.pad:focus-visible,
.go:focus-visible { outline: 2px solid var(--ink); outline-offset: 22px; }

.pad-glyph,
.go-glyph {
  fill: currentColor;
  filter: drop-shadow(0 1px 6px rgb(0 0 0 / 0.54));
}
/* The app's proportions: a 48 dp touch glyph and a 72 dp transport icon, both
   on a 200 dp circle. */
.pad-glyph { width: 24%; }
.go-glyph { width: 36%; }

/* The disc.
   The face colour is a plain `background-color` with the dome laid over it as
   transparent white and black, rather than a gradient built out of the colour
   itself: identical output, and it leaves one animatable property to carry the
   pad from one tempo band to the next.
   The rim is the first layer, not the last, for a duller reason — the colour is
   clipped to the box of the *final* layer, so a border-box rim at the end would
   push the face out under the rim and tint it. */
.pad::before,
.go::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border: var(--rim-w) solid transparent;
  border-radius: 50%;
  background:
    linear-gradient(135deg, var(--rim-lit), var(--rim-far)) border-box,
    linear-gradient(transparent, transparent) padding-box var(--face);
  /* Contact shadow only on a light page. The dark scaffold is black, where a
     black shadow can draw nothing at all; there the rim's bounce light closes
     the lower edge instead. */
  box-shadow:
    0 10px 20px light-dark(rgb(0 0 0 / 0.22), transparent),
    0 0 18px 1px color-mix(in srgb, var(--face) var(--glow), transparent);
}
.pad:active::before,
.go:active::before {
  box-shadow:
    0 3px 10px light-dark(rgb(0 0 0 / 0.22), transparent),
    0 0 18px 1px color-mix(in srgb, var(--face) var(--glow), transparent);
}

/* The lit face: sheen, crease, dome. The gradient is deliberately shallow — a
   wide light-to-dark range across a circle is the Web-2.0 gel button, and gloss
   reads as cheap. Matte materials scatter light, which is what good hardware
   pads are made of.
   The tap pad always; the metronome only while it runs. An outlined control is
   how the app says "stopped", and doming it would make a stopped metronome look
   live. */
.pad::before,
.go.is-playing::before {
  background:
    linear-gradient(135deg, var(--rim-lit), var(--rim-far)) border-box,
    radial-gradient(ellipse 33% 13% at 50% 21%, rgb(255 255 255 / 0.1), transparent)
      padding-box,
    radial-gradient(circle at 50% 50%, transparent 78%, rgb(0 0 0 / 0.13) 100%)
      padding-box,
    radial-gradient(
        circle at 33% 28%,
        rgb(255 255 255 / 0.1) 0%,
        transparent 55%,
        rgb(0 0 0 / 0.16) 100%
      )
      padding-box var(--face);
}

/* The ping: one ring per tap, one per beat, flying out and fading. Its duration
   is written by the scripts from the interval they measured, so the ring
   travels with the groove instead of at a fixed speed. */
.pad::after,
.go::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border: 3px solid var(--ring);
  border-radius: 50%;
  opacity: 0;
}

/* The pad's own caption — how many taps have landed, and what to do next — so
   it sits directly under the pad. It used to sit below the copy button, which
   put a disabled control between a control and the sentence explaining it. */
.hint {
  margin: 1.75rem 0 0;
  text-align: center;
  font-size: 0.9rem;
  color: var(--muted);
}

/* The number is what the visitor came for, and until now they had to retype it
   into whatever asked for it. It follows the hint, because it is the step after
   there is a reading rather than part of taking one. */
.tools {
  display: flex;
  justify-content: center;
  margin-top: 1.1rem;
}
.tool {
  font: 500 0.9rem/1 var(--sans);
  color: var(--ink);
  background: none;
  border: 1px solid var(--edge);
  border-radius: 999px;
  padding: 0.6rem 1.1rem;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.tool:hover:not(:disabled) { border-color: var(--tempo); }
.tool:active:not(:disabled) { border-color: var(--tempo); }
.tool:disabled {
  color: var(--ghost);
  border-color: var(--edge);
  cursor: default;
}

/* The one moment on this page where a visitor has certainly got what they came
   for. It used to end in "tap again to start a new count." */
.done {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.75rem 1.5rem;
  margin-top: 1.75rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--edge);
  text-align: center;
}
.done[hidden] { display: none; }
.done p {
  margin: 0;
  flex: 1 1 14rem;
  font-size: 0.95rem;
  color: var(--muted);
  text-align: start;
}
.done img {
  display: block;
  height: 50px;
  width: auto;
}

/* Announced, never shown: one message per finished run. See page.js for why
   the live number must not carry the live region itself. */
.sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Every moving part of the two hero controls, opt-in like the rest of this
   page's motion. The app asks the operating system the same question before it
   starts its ambient loops. */
@media (prefers-reduced-motion: no-preference) {
  /* Asymmetric, as in the app: snap in on contact, spring back a little slower
     with an overshoot, which is what reads as a physical click. */
  .pad,
  .go { transition: scale 120ms cubic-bezier(0.34, 1.56, 0.64, 1); }
  .pad:active,
  .go:active { transition: scale 60ms ease-out; }

  .pad::before,
  .go::before {
    transition: background-color 600ms ease-in-out, box-shadow 200ms ease;
  }

  .pad.is-ping::after,
  .go.is-ping::after {
    animation: pad-ping var(--ring-ms, 400ms) cubic-bezier(0, 0, 0.58, 1);
  }

  /* The tap pad breathes; the metronome does not. In the app that is the
     difference between a pad waiting to be played and a transport that is
     either running or stopped. */
  .pad::before { animation: pad-breathe 1400ms ease-in-out infinite alternate; }
  /* Impact flash: the face lights toward white on contact and decays with the
     ping — the tap's answer to the metronome's beat flash. */
  .pad.is-ping::before {
    animation:
      pad-breathe 1400ms ease-in-out infinite alternate,
      pad-flash 400ms ease-out;
  }

  /* The beat, on the rim and in the glow. `--beat` decays from green back to
     indigo over roughly the first half of the interval, so each tick lands as a
     pulse rather than a square wave — and the disc thumps 4% with it, which is
     what makes the beat legible with the sound off. */
  .go.is-playing.is-ping { animation: beat-rim var(--beat-ms, 200ms) ease-out; }
  .go.is-playing.is-ping::before {
    animation: beat-thump var(--beat-ms, 200ms) ease-out;
  }

  .readout-value { transition: color 200ms ease; }
  .readout { transition: opacity 400ms ease; }
  .trace path { transition: fill 200ms ease, stroke 200ms ease; }
}

@keyframes pad-ping {
  from { scale: 1; opacity: 0.55; }
  to { scale: 1.9; opacity: 0; }
}
@keyframes pad-breathe {
  to { scale: 1.05; }
}
@keyframes pad-flash {
  from { filter: brightness(1.28); }
}
@keyframes beat-rim {
  from { --beat: #22c55e; }
}
@keyframes beat-thump {
  from {
    scale: 1.04;
    box-shadow:
      0 10px 20px light-dark(rgb(0 0 0 / 0.22), transparent),
      0 0 24px 6px color-mix(in srgb, #22c55e 50%, transparent);
  }
}

/* ---------- the metronome ---------- */

/* The same card, the same readout and the same hint line as the tapper: the
   two pages are one instrument in two modes, which is what the app is. What
   differs is what sits under the number — a bar of beats instead of a trace of
   taps, and a transport instead of a pad. */

/* One dot per beat, the downbeat larger because it is the one you count from.
   The lit dot is the beat you are *hearing*: metro.js holds each scheduled beat
   in a queue and lights it when the audio clock reaches it, rather than when it
   was handed over up to 100 ms earlier. */
.bar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(0.7rem, 3vw, 1.1rem);
  min-height: 24px;
  margin-top: clamp(1.25rem, 4vw, 1.75rem);
}
.beat {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--tempo) 40%, transparent);
  background: color-mix(in srgb, var(--tempo) 8%, transparent);
}
.beat.is-accent { width: 22px; height: 22px; }

/* The accent switch, under the bar length it applies to. A plain checkbox, and
   deliberately not an eighth digit in the row above: the bar and its downbeat
   are two choices, and practising against an even click while still watching
   the bar count is a normal thing to want. */
.opt {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  margin-top: 1rem;
  font-size: 0.9rem;
  color: var(--muted);
  cursor: pointer;
}
.opt input {
  width: 1rem;
  height: 1rem;
  accent-color: var(--tempo);
  cursor: pointer;
}

/* The fullscreen button, in the instrument's own top-right corner — where a
   video player puts it, which is the only reason it needs no words. Absolute,
   so it stays out of the column of controls the page is actually about; the
   title beside it is set well clear of that corner at every width. */
.rig-full {
  position: absolute;
  top: clamp(0.75rem, 2vw, 1.1rem);
  right: clamp(0.75rem, 2vw, 1.1rem);
  display: inline-flex;
  align-items: center;
  padding: 0.35rem;
  color: var(--muted);
  background: none;
  border: 1px solid var(--edge);
  border-radius: 6px;
  cursor: pointer;
}
.rig-full:hover { color: var(--ink); border-color: var(--indigo); }
.rig-full svg {
  display: block;
  width: 1.05rem;
  height: 1.05rem;
}

/* Fullscreen, for the metronome on a music stand: from two metres away the
   number and the bar are the instrument, and the prose that explains it is in
   the way. Nothing here keeps the click alive when the screen sleeps — that is
   the line between this page and the app, and the page argues it a section
   further down. */
.rig:fullscreen {
  display: flex;
  flex-direction: column;
  justify-content: center;
  border: 0;
  border-radius: 0;
}
.rig:fullscreen .rig-title,
.rig:fullscreen .rig-lede,
.rig:fullscreen .hint { display: none; }
.rig:fullscreen .readout { max-width: min(46rem, 90vw); }
.rig:fullscreen .readout-value { font-size: clamp(5rem, 26vh, 16rem); }
.rig:fullscreen .beat { width: 22px; height: 22px; }
.rig:fullscreen .beat.is-accent { width: 32px; height: 32px; }
.beat.is-on {
  background: var(--tempo);
  border-color: var(--tempo);
  box-shadow: 0 0 18px 1px color-mix(in srgb, var(--tempo) 45%, transparent);
}

/* Coarse on the slider, exact on the buttons. `accent-color` is the whole of
   the slider's styling: it is one line, it is native on every platform, and a
   hand-built track is the kind of thing that ends up 0 px tall in a browser
   nobody tested. */
.dial {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  max-width: 26rem;
  margin: clamp(1.5rem, 5vw, 2rem) auto 0;
}
.slider {
  flex: 1 1 auto;
  min-width: 0;
  accent-color: var(--tempo);
  cursor: pointer;
}
.step {
  flex: none;
  width: 2.75rem;
  height: 2.75rem;
  border: 1px solid var(--edge);
  border-radius: 50%;
  background: none;
  color: var(--ink);
  font: 400 1.4rem/1 var(--sans);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.step:hover { border-color: var(--tempo); }

/* The metronome's hero control, the app's circle rather than a pill: play and
   pause where every player puts them, a rim that flashes green on each beat, and
   a ring flying out with it. The word it used to carry is in the sentence under
   it, which has to name the space bar anyway.

   `--beat` is registered so the rim colour can be *animated*. An unregistered
   custom property has no type, so it can only ever be swapped at the keyframe
   boundary — a green rim that snaps off rather than a beat that decays. */
@property --beat {
  syntax: "<color>";
  inherits: true;
  initial-value: transparent;
}

.go {
  /* Indigo at rest and while running, green on the beat — the app's split, and
     the reason it exists: the page paints everything else in the tempo's
     colour, and at 120 BPM that colour *is* this green. A beat signal that
     disappears at the default tempo is not a signal. The tap pad keeps the
     tempo palette; a transport is not a pad. */
  --beat: var(--indigo);
  /* The rim carries the beat, so it is tinted rather than neutral: lit toward
     white at the top left, and at the far end toward the page — dark on a light
     one, bounce light on black. */
  --rim-lit: color-mix(in srgb, var(--beat) 78%, #fff);
  --rim-far: light-dark(
    color-mix(in srgb, var(--beat) 78%, #000),
    color-mix(in srgb, var(--beat) 94%, #fff)
  );
  --rim-w: 2px;
  --glow: 25%;
  /* Stopped: a flat neutral face with an indigo glyph on it. */
  --face: light-dark(#ffffff, #1f1f1f);
  --ring: #22c55e;
  color: var(--indigo);
  margin: clamp(1.5rem, 5vw, 2rem) auto 0;
}
.go.is-playing {
  --face: var(--indigo);
  color: #fff;
}
.go .go-pause { display: none; }
.go.is-playing .go-play { display: none; }
.go.is-playing .go-pause { display: block; }

/* Radios, not buttons: the arrow keys move within the group for free, and a
   screen reader is told it is one control with a name rather than six unrelated
   toggles. The input keeps its place in the tab order — hidden with opacity, so
   :focus-visible on it can outline the label standing in for it.
   A `role="group"` div rather than a fieldset, because a `<legend>` is not a
   flex item: it is laid out against the fieldset's border box whatever the
   display is, so it could be neither placed nor centred with the row it names.
   The grouping and the accessible name are the same either way. */
.seg {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.6rem 0.9rem;
  max-width: 26rem;
  margin: clamp(1.75rem, 5vw, 2.25rem) auto 0;
}
/* Its own line, above the digits it names. Beside them it was the one thing on
   the instrument that was not centred on the column the readout, the dial and
   the button share, and at the widths where it did not fit it took the digits
   out of the card with it. `flex: 1 0 100%` rather than a second container: the
   group stays one flex box, which is what `role="group"` is describing. */
.seg-label {
  flex: 1 0 100%;
  text-align: center;
  font-size: 0.9rem;
  color: var(--muted);
}
/* Wraps, because it cannot shrink: six 2.4 rem digits and their gaps come to
   16.4 rem, and the card's own padding leaves less than that below about a
   350 px viewport — or on a wider one with a larger default font size, since
   both figures are in rem. Without the wrap the row is centred and hangs out of
   both sides of the card. The digits themselves stay their size: they are the
   tap target. */
.seg-row { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.4rem; }
.seg label { position: relative; }
.seg input {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
}
.seg span {
  display: grid;
  place-items: center;
  width: 2.4rem;
  height: 2.4rem;
  border: 1px solid var(--edge);
  border-radius: 8px;
  font: 600 0.95rem/1 var(--sans);
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}
.seg input:checked + span {
  background: var(--tempo);
  border-color: var(--tempo);
  color: var(--tempo-ink);
}
.seg input:focus-visible + span {
  outline: 2px solid var(--indigo);
  outline-offset: 3px;
}

@media (prefers-reduced-motion: no-preference) {
  /* Instant on, and a fade over a quarter of a second off, so the bar reads as
     a beat landing rather than as four lights blinking. The state itself is
     information and changes either way — only the fade is motion. */
  .beat { transition: background-color 260ms ease-out, box-shadow 260ms ease-out; }
  .beat.is-on { transition-duration: 0s; }
}

/* ---------- prose sections ---------- */

.band { padding-block: clamp(3rem, 9vw, 5rem); }
/* A section heading brings its own air, because most of them are not the first
   thing in their section: they follow a paragraph, a table or a bordered list,
   and with no top margin the heading sat flush against the block above it. The
   first heading in a section already has the section's padding above it, so
   that one is reset. Sub-headings below use 2rem, which keeps the hierarchy
   readable as spacing and not only as size. */
.band h2 {
  margin: 2.75rem 0 0.75rem;
  font: 600 clamp(1.35rem, 1.1rem + 1vw, 1.75rem)/1.25 var(--sans);
  letter-spacing: -0.01em;
}
.band > h2:first-child { margin-top: 0; }
.band p, .band h2 { max-width: var(--measure); }
.band p { margin: 0 0 1rem; }
.band p:last-child { margin-bottom: 0; }

.perks {
  margin: var(--block-gap) 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 1px;
  background: var(--edge);
  border: 1px solid var(--edge);
  border-radius: 12px;
  overflow: hidden;
}
.perks li {
  background: var(--raised);
  padding: 1rem 1.25rem;
}
.perks b {
  display: block;
  font: 600 0.95rem/1.4 var(--sans);
}
.perks span {
  color: var(--muted);
  font-size: 0.95rem;
}

/* Pro's gold, the same mark the app puts on a locked tile — so the feature
   list says which half of it costs money without being split into two lists
   that then have to be read against each other. Gold on a light surface is
   unreadable, which is what --gold-ink exists for. */
.tag-pro {
  display: inline-block;
  margin-left: 0.5em;
  vertical-align: 0.08em;
  font: 700 0.65rem/1 var(--sans);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--gold-ink);
  border: 1px solid color-mix(in srgb, var(--gold-ink) 45%, transparent);
  border-radius: 4px;
  padding: 0.2em 0.4em;
}

/* The way out of the feature list into the genre reference. It follows a
   bordered block, so it has to bring its own air. */
.lead-on { margin-top: var(--block-gap); }

/* ---------- get-the-app card ---------- */

.getit {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: clamp(1rem, 4vw, 2rem);
  margin-top: var(--block-gap);
  padding: clamp(1.25rem, 4vw, 1.75rem);
  border: 1px solid var(--ghost);
  border-radius: 14px;
  background: var(--raised);
}
.getit-body { flex: 1 1 16rem; }
.getit-name {
  margin: 0;
  font: 600 1.1rem/1.3 var(--sans);
}
.getit-note {
  margin: 0.3rem 0 0;
  font-size: 0.95rem;
  color: var(--muted);
}
.getit-badge {
  display: inline-block;
  flex: none;
  line-height: 0;
  border-radius: 8px;
}
.getit-badge img {
  height: 58px;
  width: auto;
}

/* ---------- screenshots ---------- */

/* Three across wherever they fit. Below that the row scrolls rather than
   shrinking the phones past the point where the numbers on them are legible. */
.shots {
  display: flex;
  gap: 0.75rem;
  margin: var(--block-gap) 0 0;
  padding: 0 0 0.5rem;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  overscroll-behavior-x: contain;
}
.shots figure {
  flex: 1 1 0;
  min-width: 8.5rem;
  margin: 0;
  scroll-snap-align: start;
}
.shots img {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--edge);
  border-radius: 12px;
}
.shots figcaption {
  margin-top: 0.6rem;
  font-size: 0.85rem;
  line-height: 1.45;
  color: var(--muted);
}
.shots:focus-visible { outline: 2px solid var(--indigo); outline-offset: 4px; }

/* Below this three no longer fit, so the row runs to the screen edge: a strip
   that stops short of the gutter reads as clipped, one that runs off the edge
   reads as scrollable. */
@media (max-width: 32rem) {
  .shots {
    margin-right: calc(var(--gutter) * -1);
    padding-right: var(--gutter);
  }
}
/* A caption on the strip above it, so it sits nearer to that than to whatever
   follows. At the old 1.4rem it was almost exactly as far from the screenshots
   as the next block was from it, and read as floating between the two. */
.shots-note {
  margin-top: 0.9rem;
  font-size: 0.85rem;
  color: var(--muted);
}

/* A single screen on a page that is otherwise prose — held to a phone's width
   rather than stretched to the column's. */
.one-shot {
  margin: var(--block-gap) 0 0;
  max-width: 15.5rem;
}
.one-shot img {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--edge);
  border-radius: 12px;
}
.one-shot figcaption {
  margin-top: 0.6rem;
  font-size: 0.85rem;
  line-height: 1.45;
  color: var(--muted);
}

/* ---------- questions ---------- */

.faq h3 {
  margin: 2rem 0 0.4rem;
  font: 600 1.05rem/1.4 var(--sans);
}
.faq h3:first-of-type { margin-top: 1.5rem; }

/* ---------- reference tables ---------- */

/* Wrapped, not squeezed: a tempo range that wraps to three lines is unreadable,
   so on a narrow screen the table scrolls sideways instead. */
/* Air on both sides: `.band p` carries its gap underneath itself and has no
   top margin at all, so a paragraph following a table sat flush against its
   bottom border. */
.tbl-scroll {
  margin: var(--block-gap) 0;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  border: 1px solid var(--edge);
  border-radius: 12px;
}
.tbl {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.95rem;
  background: var(--raised);
}
.tbl th,
.tbl td {
  padding: 0.7rem 1rem;
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--edge);
}
.tbl tr:last-child td { border-bottom: 0; }
.tbl th {
  font: 600 0.85rem/1.3 var(--sans);
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}
.tbl td:first-child { font-weight: 600; }
.tbl .bpm {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.tbl .note {
  color: var(--muted);
  font-size: 0.9rem;
}
/* Below this a third column has about six characters to work with, which turns
   every note into one word per line and every row six lines tall. So the row
   stops being a row: name and tempo share a line, the note goes underneath.
   Scrolling sideways was the other option and it is worse — it takes the genre
   name off screen exactly when you are reading its tempo.

   Only for the tables that survive it: the tier table would collapse to
   "Timing grid First 8 taps Whole session" on one line and the note-length
   table to four unlabelled millisecond figures. A prose `.note` cell is what
   marks a table as loose enough to fold, so that is what selects them, and a
   table added later is judged by the same rule rather than by remembering to
   add a class. Browsers without :has() simply keep the table and scroll it.

   The header folds with the rows rather than being dropped. It was dropped at
   first, on the argument that a genre and a tempo range say what they are — but
   the same phone that hides the header hides the desktop layout that taught you
   the column order, and "115–125" beside "Deep house" is only obviously a tempo
   to someone who already knew. So the header row gets the same flex treatment
   as a data row: the labels land above the values they belong to, one line of
   legend per table.
   The note is the last column in every one of these tables, which is what makes
   the positional selector safe — and if a sixth table ever puts it elsewhere,
   the label lands in the wrong place visibly rather than silently. */
@media (max-width: 34rem) {
  .tbl:has(td.note),
  .tbl:has(td.note) thead,
  .tbl:has(td.note) tbody { display: block; }
  .tbl:has(td.note) tr {
    display: flex;
    flex-wrap: wrap;
    gap: 0 0.75rem;
    padding: 0.7rem 1rem;
    border-bottom: 1px solid var(--edge);
  }
  .tbl:has(td.note) tbody tr:last-child { border-bottom: 0; }
  .tbl:has(td.note) :is(th, td) {
    display: block;
    padding: 0;
    border: 0;
  }
  .tbl:has(td.note) :is(th, td):first-child { flex: 1 1 auto; }
  .tbl:has(td.note) .bpm { flex: none; }
  .tbl:has(td.note) tr > :last-child { flex: 1 0 100%; }
  /* Plenty of genres need no note; an empty cell must not claim a line. */
  .tbl:has(td.note) td:empty { display: none; }

  /* The rest stay tables, because the header is the whole meaning of the row.
     They give up padding and a little type size instead. That is enough for
     three columns on most phones; where a long German label still overruns it,
     .tbl-scroll scrolls the table with its header attached, which is the whole
     difference from the collapse above. */
  .tbl:not(:has(td.note)) {
    font-size: 0.9rem;
  }
  .tbl:not(:has(td.note)) th,
  .tbl:not(:has(td.note)) td {
    padding: 0.55rem 0.6rem;
  }
  .tbl:not(:has(td.note)) th {
    font-size: 0.72rem;
    letter-spacing: 0.02em;
  }
  /* German decides it. The longest word an English row can be asked to fit is
     "Subdivisions"; the German twin has "Nachkommastellen" and, in the next
     column, "Sperrbildschirm" — together about 17 px more than a 360 px screen
     has, so the table scrolled where the English one did not. Hyphenation buys
     back more than that, and it needs nothing but the `lang` both pages already
     declare. `overflow-wrap` is the floor for a browser with no dictionary for
     the language: break the word rather than push the row off the screen. */
  .tbl:not(:has(td.note)) td,
  .tbl:not(:has(td.note)) th {
    -webkit-hyphens: auto;
    hyphens: auto;
    overflow-wrap: break-word;
  }
}

/* The eight bands the app colours by. The swatch carries the colour itself, so
   the row does not have to describe it in words. */
.swatch {
  display: inline-block;
  width: 0.85em;
  height: 0.85em;
  margin-right: 0.5em;
  border-radius: 3px;
  vertical-align: -0.05em;
}

/* ---------- long-form documents ---------- */

.doc h3 {
  margin: 2rem 0 0.5rem;
  font: 600 1.05rem/1.4 var(--sans);
}
.doc h3:first-of-type { margin-top: 1.5rem; }
.doc p, .doc h3 { max-width: var(--measure); }
.doc ul {
  margin: 0 0 1rem;
  padding-left: 1.25rem;
  max-width: var(--measure);
}
.doc li { margin-bottom: 0.35rem; }
.doc .stamp {
  font-size: 0.85rem;
  color: var(--muted);
}
/* An address block, not prose: the lines are the record. */
.doc address {
  font-style: normal;
  margin: 0 0 1rem;
}
/* ---------- in-page navigation ---------- */

/* Three long documents share /legal/, and forty genres share the reference —
   both had anchors and no way to see them. */
.toc {
  margin: var(--block-gap) 0 0;
  padding: 1rem 1.25rem;
  list-style: none;
  border: 1px solid var(--edge);
  border-radius: 12px;
  background: var(--raised);
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.25rem;
  font-size: 0.95rem;
}
.toc li { margin: 0; }

/* ---------- pricing ---------- */

/* The tier table answers the question the FAQ used to answer in prose at the
   bottom of the page: what do I get without paying. */
.tbl .yes { color: var(--accent-text); font-weight: 600; }
.tbl .no  { color: var(--ghost); }
.tbl th.tier-pro { color: var(--gold-ink); }

/* The footnote to the table above it, and spaced as one — see .shots-note. */
.price {
  margin-top: 0.9rem;
  font-size: 0.95rem;
  color: var(--muted);
}
.price b { color: var(--ink); }

/* Where the Play rating goes once there is one worth showing. Styled now so
   that switching it on is a paste rather than a design job — see
   openspec/changes/103-landing-page-conversion/tasks.md, owner step O2. */
.proof {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  margin-top: var(--block-gap);
  font-size: 0.95rem;
  color: var(--muted);
}
.proof b {
  font-family: var(--num);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* ---------- delay time calculator ---------- */

.calc {
  border: 1px solid var(--edge);
  border-radius: 14px;
  background: var(--raised);
  padding: clamp(1.25rem, 4vw, 1.75rem);
  margin-top: var(--block-gap);
}
.calc-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
}
.calc label {
  font: 600 0.95rem/1.4 var(--sans);
}
.calc input {
  width: 7rem;
  font: 700 2rem/1 var(--num);
  font-variant-numeric: tabular-nums;
  color: var(--accent-text);
  background: var(--panel);
  border: 1px solid var(--edge);
  border-radius: 10px;
  padding: 0.4rem 0.6rem;
}
.calc input:invalid { border-color: #EF4444; }
.calc-note {
  margin: 0.75rem 0 0;
  font-size: 0.9rem;
  color: var(--muted);
}

.foot {
  border-top: 1px solid var(--edge);
  padding-block: 2rem 3rem;
  font-size: 0.9rem;
  color: var(--muted);
}
.foot p { margin: 0 0 0.4rem; }
.foot p:last-child { margin-bottom: 0; }

/* The site's only navigation, and it had none: the footer led back to the
   tapper and nowhere else, so /release-notes/ — in the sitemap, linked from the
   404 page and from no other page at all — could be reached only by typing
   the URL, and the three reference pages could not see each other. The
   masthead was the wrong place for it; it holds the store badge, which is the
   one thing above the fold worth clicking, and four more labels would have
   pushed a third line onto a 360 px phone.
   Styling is inherited from .foot on purpose. This is a way out of a page,
   not a feature of it. */
.foot nav { margin: 0 0 0.4rem; }
/* The page you are already on stays in the list — leaving it out would move
   every other label between pages — but it is not a link, and it is the one
   entry in full ink. */
.foot [aria-current] { color: var(--ink); }
