/* Eigendrum - Bauhaus preliminary-course study sheet.
 *
 * Plaster ground, hard fields of vermilion / ultramarine / chrome yellow, 3px
 * black rules, one geometric sans set lowercase. No cards, no shadows, no
 * gradients, no rounded corners: the composition is made of fields and rules.
 *
 * The colour plan gives every primary a job, and each owns a whole region rather
 * than accenting one:
 *   vermilion  = the bench, where you act
 *   ultramarine = the ledger, where the measurements are
 *   chrome yellow = the remarkable thing, and whatever is selected
 *   plaster + ink = the study itself
 */

:root {
  --plaster: #f0ede6;
  --plate: #e8e3d6;
  --ink: #14120f;
  --ink-soft: #55504a;
  --vermilion: #c62d1a;
  --vermilion-bright: #d8321c;
  --ultramarine: #1b39a8;
  --ultramarine-tint: #c9d2f5;
  --chrome: #f2c300;

  --rule: 3px;
  --hair: 1px;
  --masthead-h: 4.75rem;

  --font: 'Jost', 'Futura', 'Century Gothic', 'Avenir Next', system-ui, sans-serif;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

/* A class selector that sets `display` outranks the UA stylesheet's
   `[hidden] { display: none }`, which once left an invisible overlay on top of
   the page swallowing every pointer event. Make the attribute win, always. */
[hidden] {
  display: none !important;
}

html {
  background: var(--plaster);
}

body {
  margin: 0;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  background: var(--plaster);
  color: var(--ink);
  font-family: var(--font);
  font-size: 16px;
  font-weight: 400;
  line-height: 1.5;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1,
h2,
h3 {
  margin: 0;
  font-weight: 500;
  letter-spacing: -0.01em;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 40;
  background: var(--chrome);
  color: var(--ink);
  padding: 0.7rem 1.1rem;
  font-weight: 600;
  text-decoration: none;
}
.skip:focus {
  left: 0;
}

:focus-visible {
  outline: var(--rule) solid var(--ink);
  outline-offset: 2px;
}
.bench :focus-visible,
.ledger :focus-visible {
  outline-color: #fff;
}

/* ------------------------------------------------------------ status banner */

/* `flex: none` in the same column as .masthead and .sheet: it takes its own
   height and .sheet's `flex: 1 1 auto` absorbs the rest, so this can never push
   the vermilion bench off a `100svh` screen the way a body-level sibling would.
   Chrome yellow rather than the quiet ink-on-plaster of every other aside, since
   the One Job Rule in DESIGN.md reserves that hue for the remarkable thing, and a
   real risk to whether the site stays up qualifies. Links inherit the banner's
   own ink colour rather than the page's blue-link default, since this sits on a
   saturated field like the ledger does. */
.status-banner {
  flex: none;
  margin: 0;
  padding: 0.6rem 1.5rem;
  background: var(--chrome);
  color: var(--ink);
  font-size: 0.85rem;
  line-height: 1.45;
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
}

.status-banner a {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
}

.status-banner button {
  margin-left: auto;
  flex: none;
  border: none;
  background: none;
  color: inherit;
  font-size: 1.05rem;
  line-height: 1;
  cursor: pointer;
  padding: 0 0.2rem;
}

.status-banner button:hover {
  opacity: 0.6;
}

@media (max-width: 34rem) {
  .status-banner {
    flex-wrap: wrap;
    padding: 0.55rem 1rem;
  }
  .status-banner button {
    margin-left: 0;
  }
}

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

.masthead {
  flex: none;
  display: flex;
  align-items: center;
  gap: 0.9rem;
  padding: 0.85rem 1.5rem;
  min-height: var(--masthead-h);
}

.mark {
  width: 2.35rem;
  height: 2.35rem;
  flex: none;
}

/* On a prose page the h1 belongs to the article, so the wordmark demotes to a link
   and the mark itself becomes the way home. Same size and weight as the instrument's
   h1, because the masthead should not visibly change between pages. */
.mark-link {
  display: block;
  flex: none;
  line-height: 0;
}

.masthead-eyebrow {
  margin: 0;
  font-size: clamp(1.45rem, 2.4vw, 2rem);
  line-height: 1;
  font-weight: 500;
  letter-spacing: -0.01em;
}
.masthead-eyebrow a {
  color: var(--ink);
  text-decoration: none;
}
.masthead-eyebrow a:hover {
  border-bottom: var(--rule) solid var(--vermilion);
}

.masthead-titles {
  min-width: 0;
}

.masthead-titles h1 {
  font-size: clamp(1.45rem, 2.4vw, 2rem);
  line-height: 1;
  letter-spacing: -0.035em;
  font-weight: 600;
}

.masthead-titles p {
  margin: 0.15rem 0 0;
  font-size: 0.95rem;
  letter-spacing: 0.02em;
  color: var(--ink-soft);
}

.masthead-actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  justify-content: flex-end;
}

/* A live headcount, not a thing you can click, so it takes no keyline - just a small
   dot and a number, sitting quietly between the wordmark and the action chips. Hidden
   by default (see index.html); mountPresence() in src/app/presence.js reveals it only
   once a real count arrives, and hides it again the instant the connection drops. */
.masthead-live {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0 0 0 0.25rem;
  font-size: 0.85rem;
  letter-spacing: 0.01em;
  color: var(--ink-soft);
  white-space: nowrap;
}

/* A pseudo-element rather than a child span, since presence.js sets this element's
   textContent directly - a real child node would be wiped out on every update. */
.masthead-live::before {
  content: '';
  display: block;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: #2f8f4e;
  flex: none;
}

@media (prefers-reduced-motion: no-preference) {
  .masthead-live::before {
    animation: masthead-live-pulse 2.4s ease-in-out infinite;
  }
}

@keyframes masthead-live-pulse {
  0%, 100% {
    opacity: 1;
  }
  50% {
    opacity: 0.4;
  }
}

@media (max-width: 34rem) {
  .masthead {
    flex-wrap: wrap;
  }
  .masthead-actions {
    min-width: 0;
    flex: 1 1 100%;
  }
  .masthead-live {
    order: 3;
    flex: 1 0 100%;
    margin: -0.25rem 0 0;
  }
}

/* A hint, not a warning: nothing is broken, so it takes no ink colour and no border -
   just the soft background every quiet aside on this sheet uses. Full width of the
   action row on its own line, since it needs room for a full sentence and a dismiss
   control without crowding the chips beside it. */
.ios-sound-hint {
  flex: 1 0 100%;
  margin: 0;
  padding: 0.5rem 0.75rem;
  border-radius: 4px;
  background: rgba(20, 18, 15, 0.06);
  color: var(--ink-soft);
  font-size: 0.82rem;
  line-height: 1.4;
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
}

.ios-sound-hint button {
  margin-left: auto;
  flex: none;
  border: none;
  background: none;
  color: inherit;
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  padding: 0 0.15rem;
}

.ios-sound-hint button:hover {
  color: var(--ink);
}

/* The source address and the support link are chips, not underlined text. They were
   underlined text, which was readable but did not look pressable, and on this sheet a
   thing you can do carries a keyline. `.chip` already resets text-decoration and
   inverts on hover, so an <a> needs nothing beyond it. Neither takes a hue: vermilion
   is the bench, ultramarine the ledger and chrome yellow selection, and a link in the
   masthead is none of those. */

/* The address is spelled out where there is room, so a viewer of a screen recording
   can read it and type it in, and collapses to one word where there is not. */
.narrow-only {
  display: none;
}
@media (max-width: 78rem) {
  .wide-only {
    display: none;
  }
  .narrow-only {
    display: inline;
  }
}

/* ------------------------------------------------------------------- chips */

.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font: inherit;
  font-size: 0.85rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  /* Chips are also used on <a>, where the UA underline has to go: the keyline is
     already carrying "this is pressable", and both marks together reads as a link
     wearing a button. */
  text-decoration: none;
  padding: 0.4rem 0.75rem;
  border: var(--hair) solid var(--ink);
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  transition: background-color 110ms linear, color 110ms linear;
}
/* Hover tints, pressed fills. They used to render identically, which made a
   pressed chip indistinguishable from a hovered unpressed one. */
.chip:hover {
  background: rgba(20, 18, 15, 0.09);
}
.chip svg {
  width: 1.05rem;
  height: 1.05rem;
  flex: none;
}
.chip[aria-pressed='true'] {
  background: var(--ink);
  color: var(--plaster);
}


.chip-solid {
  background: var(--ink);
  color: var(--plaster);
}
.chip-solid:hover {
  background: var(--vermilion);
  color: #fff;
}

/* On the saturated fields the chip inverts: white keyline, white text. */
.chip-invert {
  border-color: #fff;
  color: #fff;
}
.chip-invert:hover {
  background: #fff;
  color: var(--ink);
}

/* ------------------------------------------------------------------ sheet */

/* A bounded app shell: the first row takes whatever height is left after the
   masthead and the bench, so the mode index scrolls inside itself rather than
   pushing the vermilion band off the bottom of the screen. */
.sheet {
  flex: 1 1 auto;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 21.5rem;
  grid-template-rows: minmax(0, 1fr) auto;
  border-top: var(--rule) solid var(--ink);
  min-height: 0;
}

/* On a wide screen this is an instrument, not a document: the drum, the index and
   the bench are all present at once, and the columns scroll inside themselves.
   Below the breakpoint the page stacks and scrolls normally.

   The sheet is pinned to the viewport rather than the body, which is the whole
   trick. `body { overflow: hidden }` used to do this job, and it did it by making
   the document unscrollable - fine when the sheet was the entire site, fatal once
   there is a footer under it. Giving the sheet a definite height keeps the first
   viewport byte-identical (masthead, then a full-height study sheet, nothing else
   visible) while letting the document scroll to reach what follows. `flex: none`
   matters: the documented trap is that `flex: 1 1 auto` lets the shell grow past
   the viewport and shoves the vermilion band off the bottom. */
@media (min-width: 62.0625rem) {
  .screen {
    height: 100svh;
    display: flex;
    flex-direction: column;
    min-height: 0;
  }
  /* Takes whatever the masthead leaves, which is the original behaviour and the only
     version that does not depend on knowing the masthead's height in advance. */
  .sheet {
    flex: 1 1 auto;
    min-height: 0;
  }
}

/* -------------------------------------------------------------- the study */

/* Big square plate flush left, text column beside it. Two things fall out: the
   drum fills its plate instead of floating in a wide box, and the readout and the
   form list cost no vertical space, which is what keeps the vermilion bench
   inside the first viewport. */
.study {
  grid-row: 1;
  display: grid;
  /* A definite track, not `auto`. With an auto track, the plate's own
     `min(..., 100%)` width would depend on a column width that depends on the
     plate, and the whole thing collapses to zero. */
  grid-template-columns: min(56vh, 600px) minmax(0, 1fr);
  gap: 1.1rem 1.75rem;
  align-content: start;
  min-width: 0;
  min-height: 0;
  overflow-y: auto;
  padding: 1.15rem 1.5rem 1.25rem;
  border-right: var(--rule) solid var(--ink);
}

/* Square, sized from the viewport rather than from the grid, so nothing can
   stretch it out of shape. Explicit width and height beat aspect-ratio here:
   a stretched grid item silently overrides aspect-ratio. */
.plate {
  position: relative;
  width: 100%;
  height: min(56vh, 600px);
  background: var(--plate);
  border: var(--hair) solid var(--ink);
}

/* Anchored to the bottom of the column, so its baseline lines up with the foot of
   the plate. That turns the leftover space into a deliberate interval instead of
   a hole in the middle of the sheet. */
.forms {
  margin-top: auto;
  padding-top: 1.25rem;
}

#board {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
  cursor: crosshair;
}
/* Inset, so the focus ring does not read as a second border on the plate. */
#board:focus-visible {
  outline-offset: -4px;
}

.study-side {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
  min-width: 0;
}

/* A vertical study-sheet annotation rather than a floating pill. */
.prompt {
  position: absolute;
  left: 0.55rem;
  bottom: 0.9rem;
  margin: 0;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.26em;
  color: var(--ink-soft);
  pointer-events: none;
}

.solving {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  background: var(--plate);
  font-size: 0.8rem;
  font-weight: 500;
  letter-spacing: 0.22em;
  color: var(--ink-soft);
}
.solving-rule {
  display: block;
  width: min(52%, 18rem);
  height: var(--rule);
  background: var(--ink);
  transform-origin: left center;
  animation: sweep 1.15s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
@keyframes sweep {
  0% {
    transform: scaleX(0);
  }
  55% {
    transform: scaleX(1);
  }
  100% {
    transform: scaleX(1);
    opacity: 0;
  }
}

/* An ink field, not a yellow one. Chrome yellow already means "selected" and
   "the remarkable thing" here, and rendering a failure in the celebration colour
   is a lie about what happened. */
.notice {
  margin: 0;
  padding: 0.6rem 0.85rem;
  background: var(--ink);
  color: var(--plaster);
  font-size: 0.95rem;
  font-weight: 500;
}

.readout {
  margin: 0;
  font-size: 1.02rem;
  line-height: 1.45;
  max-width: 40ch;
  min-height: 4.4rem;
  color: var(--ink);
}
.readout b {
  font-weight: 600;
}
.readout .note {
  color: var(--ink-soft);
}

/* The spectrum as a comb on a log-frequency axis. Bars measured from zero cannot
   show interval, and interval is the whole character of a drum: this shows the
   overtones crowding together as they climb. For the isospectral pair the
   partner's spectrum is drawn above the same axis, so its coincidence is
   something you see rather than something we assert. */
.comb {
  margin: 0;
  padding-top: 0.8rem;
  border-top: var(--hair) solid rgba(20, 18, 15, 0.25);
}
.comb figcaption {
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.2em;
  color: var(--ink-soft);
}
.comb-axis {
  position: relative;
  height: 3rem;
  margin-top: 0.6rem;
}
.comb-axis::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 2px;
  margin-top: -1px;
  background: var(--ink);
}
.comb-tick {
  position: absolute;
  top: 50%;
  width: 2px;
  height: 0.8rem;
  margin-left: -1px;
  background: var(--ink);
}
.comb-tick.is-selected {
  width: 4px;
  margin-left: -2px;
  top: 20%;
  height: 1.9rem;
}
.comb-tick.is-partner {
  top: auto;
  bottom: 50%;
  background: var(--ultramarine);
}
.comb-ends {
  display: flex;
  justify-content: space-between;
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
}

.aside-note {
  margin: 0;
  font-size: 0.82rem;
  line-height: 1.5;
  max-width: 44ch;
  color: var(--ink-soft);
}

.key {
  list-style: none;
  margin: 0;
  padding: 0.7rem 0 0;
  border-top: var(--hair) solid rgba(20, 18, 15, 0.25);
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  font-size: 0.82rem;
  letter-spacing: 0.03em;
  color: var(--ink-soft);
}
.key li {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.sw {
  width: 0.9rem;
  height: 0.9rem;
  flex: none;
}
.sw-pos {
  background: var(--vermilion-bright);
}
.sw-neg {
  background: var(--ultramarine);
}
.sw-nodal {
  background: var(--plate);
  box-shadow: inset 0 0 0 1px var(--ink);
}

.forms h2 {
  margin-bottom: 0.5rem;
}

h2 {
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.2em;
  color: var(--ink-soft);
  margin-bottom: 0.6rem;
}

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

/* Forces the wrap: everything after it starts a new line, so "draw your own" and
   "from an equation" read as their own row above the preset gallery rather than
   blending into a wall of a dozen similarly-shaped chips. A zero-size flex item
   that is 100% wide does this without any JS layout logic. */
.forms-break {
  flex: 0 0 100%;
  height: 0;
}

/* Preset buttons carry a drawn glyph of the form itself. */
.form-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font: inherit;
  font-size: 0.85rem;
  font-weight: 500;
  padding: 0.32rem 0.7rem 0.32rem 0.4rem;
  border: var(--hair) solid var(--ink);
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  transition: background-color 110ms linear, color 110ms linear;
}
.form-chip svg {
  width: 1.15rem;
  height: 1.15rem;
  flex: none;
}
.form-chip:hover {
  background: var(--ink);
  color: var(--plaster);
}
.form-chip:hover svg [data-fill] {
  fill: var(--plaster);
}
.form-chip[aria-pressed='true'] {
  background: var(--chrome);
  border-color: var(--ink);
  color: var(--ink);
  font-weight: 600;
}
/* Solid, because on this surface drawing your own is the real invitation.
   #btn-draw additionally takes vermilion, the bench's own colour and the one
   the system already assigns to "where you act", so the real call to action
   stands out from the ink-and-plaster gallery around it rather than blending
   into every other form chip. #btn-formula stays ink: two vermilion chips side
   by side would dilute the one-job rule this colour exists to enforce. */
.form-chip-draw {
  background: var(--ink);
  color: var(--plaster);
}
#btn-draw.form-chip-draw {
  background: var(--vermilion);
  border-color: var(--vermilion);
  color: #fff;
}
#btn-draw.form-chip-draw:hover {
  background: var(--vermilion-bright);
  border-color: var(--vermilion-bright);
  color: #fff;
}
.form-chip-draw:hover {
  background: var(--vermilion);
  border-color: var(--vermilion);
  color: #fff;
}
.form-chip-draw[aria-pressed='true'] {
  background: var(--chrome);
  border-color: var(--ink);
  color: var(--ink);
}

/* ------------------------------------------------------ the equation panel */

/* A region, not a card: plaster ground with a 3px ink rule above it, so it is
   separated by adjacency and rule weight like everything else here. It takes no
   hue of its own, because the three primaries already have jobs and this is a
   place to write rather than a bench, a ledger or a remarkable thing. */
.formula {
  border-top: var(--rule) solid var(--ink);
  padding-top: 0.85rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}
.formula h2 {
  margin-bottom: 0;
}
.formula-kinds {
  display: flex;
  gap: 0.4rem;
}

.formula-fields {
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
}
.formula-field {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
/* Tabular figures and a fixed width, so r(t), x(t) and y(t) put their inputs on
   the same left edge and the two parametric rows read as one pair. */
.formula-var {
  flex: none;
  width: 3.1rem;
  font-size: 0.85rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--ink-soft);
}

/* White ground on plaster: this is the one place on the sheet you write, and
   paper is what you write on. No radius, 1px keyline like every other component
   keyline, and the page's own face - there is no monospace in this system. */
.formula input[type='text'] {
  flex: 1 1 auto;
  min-width: 0;
  font: inherit;
  font-size: 1rem;
  font-weight: 500;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--ink);
  background: #fff;
  border: var(--hair) solid var(--ink);
  border-radius: 0;
  padding: 0.34rem 0.5rem;
}
.formula input[type='text']:focus-visible {
  outline: var(--rule) solid var(--ink);
  outline-offset: 1px;
}

.formula-hint {
  font-size: 0.8rem;
  line-height: 1.45;
  color: var(--ink-soft);
  max-width: 48ch;
}

.formula-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}
.formula-examples {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
}
/* The gallery is a set of starting points, not a set of presets, so its chips are
   deliberately quieter than the form chips above. */
.chip-tiny {
  font-size: 0.78rem;
  padding: 0.24rem 0.5rem;
}

/* -------------------------------------------------------------- the index */

/* One scroll region for the whole right column. Two nested scrollers (a list
   inside a panel) is worse than one, and this stays predictable on short
   viewports where the ledger would otherwise be clipped out of reach. */
.index {
  grid-row: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  overflow-y: auto;
}

/* Takes the column's slack and scrolls inside itself, so the ledger below it is
   always fully visible and never painted over the list. Previously the ledger was
   sticky here and covered the last five rows: they were unreachable and, since the
   rows became playable, un-pressable - elementFromPoint on mode 16 returned a
   ledger term. The narrow-viewport rules already avoided this; desktop did not. */
.modes {
  padding: 1.25rem 1.25rem 1rem;
  flex: 1 1 auto;
  /* A floor, so a tall Kac panel can never squeeze the list away to nothing. If the
     three sections together still do not fit, `.index` scrolls as a backstop. */
  min-height: 8rem;
  display: flex;
  flex-direction: column;
}
.modes h2 {
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
}
#modes-count {
  font-weight: 400;
  letter-spacing: 0.08em;
}

/* The rows do not read as buttons at a glance, and their most interesting property
   - that each one sounds - should not have to be discovered by accident. */
.modes-hint {
  margin: 0.35rem 0 0.55rem;
  font-size: 0.72rem;
  letter-spacing: 0.02em;
  color: var(--ink-soft);
  line-height: 1.6;
}

/* Small key caps for the shortcut map. A hairline box rather than a soft
   drop-shadow "keycap", to stay inside this sheet's flat, ruled vocabulary. The
   whole run sits in its own inline-flex block so the sixteen keys wrap together
   as a strip instead of splitting mid-run onto two lines. */
.modes-hint .keymap {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 0.2rem;
  vertical-align: middle;
  margin-top: 0.3rem;
}
.modes-hint kbd {
  display: inline-block;
  font: inherit;
  font-size: 0.68rem;
  font-weight: 600;
  line-height: 1;
  padding: 0.05rem 0.32rem;
  border: var(--hair) solid var(--ink-soft);
  border-radius: 2px;
  color: var(--ink);
  background: rgba(20, 18, 15, 0.05);
}

.spectrum {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: var(--hair) solid var(--ink);
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

/* A caption, not a field. The solid ink field means "something went wrong and here
   is what to do"; staleness is a quieter kind of truth and must not borrow that
   voice. It heads the whole column, because the mode list, the comb and the
   measurements are all equally out of date while you are tracing. */
.stale-note {
  flex: none;
  margin: 0;
  padding: 0.6rem 1.25rem;
  border-bottom: var(--hair) solid rgba(20, 18, 15, 0.3);
  color: var(--ink);
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.02em;
}

.mode-row {
  position: relative; /* anchors the excitation rule */
  display: grid;
  grid-template-columns: 1.6rem minmax(0, 1fr) 4.4rem;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  padding: 0.3rem 0.35rem;
  border: 0;
  border-bottom: var(--hair) solid rgba(20, 18, 15, 0.22);
  background: transparent;
  font: inherit;
  font-size: 0.82rem;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
  transition: background-color 110ms linear;
}
.mode-row:hover {
  background: rgba(20, 18, 15, 0.07);
}
.mode-row[aria-pressed='true'] {
  background: var(--chrome);
  font-weight: 600;
}

.mode-n {
  font-variant-numeric: tabular-nums;
  font-size: 0.74rem;
  color: var(--ink-soft);
}
.mode-row[aria-pressed='true'] .mode-n {
  color: var(--ink);
}

/* The ratio to the fundamental, which is the number that actually characterises a
   drum. A bar measured from zero looked like information and was not: at these
   frequencies eight consecutive rows differ by a few percent of their length. */
.mode-ratio {
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
  font-size: 0.82rem;
}
.mode-row[aria-pressed='true'] .mode-ratio {
  color: var(--ink);
}

.mode-hz {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* How much of the sound now ringing this mode actually is. The one quantity that
   ties the mode list to the drum: a strike is not one of these rows, it is this
   whole set of lengths at once. A bar is honest here in a way it was not for
   frequency - these values really do run from zero to one, and the exact zeros are
   the interesting part.

   Drawn as a field filling the row from the left, not as a rule along its foot. At
   30px per row a footer rule lands 3px from the divider and 12px from its own
   numbers, so proximity says "divider" and the eye assigns it to the row below. I
   misattributed it that way twice while reviewing screenshots and concluded the
   readout contradicted the bars when it did not. If the author cannot attribute the
   mark, a reader cannot either; inside the row there is nothing to confuse. */
.mode-drive {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 100%;
  background: rgba(20, 18, 15, 0.14);
  /* Scaled rather than resized: transform stays off the layout path, so sixteen of
     these updating on every strike cost nothing. */
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 90ms linear;
}
/* Positioned, and later in DOM order than the field, so the numbers stay above it. */
.mode-n,
.mode-ratio,
.mode-hz {
  position: relative;
}

/* A mode the strike could not reach: its nodal line was under the mallet. A
   full-height stub at the left edge, at full ink, because a measured zero is a
   result and should not look like missing data. Deliberately not expressed by
   fading the numbers - that would drop real information below AA contrast. The
   readout names these modes in words too, so the mark is never the only channel. */
.mode-row.is-mute::after {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--ink);
}

/* Chrome-yellow field: the theorem. It sits at the top of the column, because
   when it applies it is the most interesting thing on the page and its action
   must not be pushed below the fold. */
.kac {
  flex: none;
  background: var(--chrome);
  color: var(--ink);
  padding: 1.1rem 1.25rem 1.25rem;
  border-bottom: var(--rule) solid var(--ink);
}
.kac h2 {
  color: var(--ink);
  letter-spacing: 0.12em;
}
.kac p {
  margin: 0 0 0.85rem;
  font-size: 0.86rem;
  line-height: 1.45;
}
.kac .kac-match {
  font-weight: 600;
  border-top: var(--hair) solid rgba(20, 18, 15, 0.4);
  padding-top: 0.6rem;
}
.kac .chip-invert {
  border-color: var(--ink);
  color: var(--ink);
}
.kac .chip-invert:hover {
  background: var(--ink);
  color: var(--chrome);
}

/* Ultramarine field: the measurements. */
/* Pinned to the foot of the scrolling column: the measurements are the honesty of
   this thing, so they stay in view while the mode list scrolls behind them. */
.ledger {
  flex: none;
  background: var(--ultramarine);
  color: #fff;
  padding: 1.1rem 1.25rem 1.3rem;
  border-top: var(--rule) solid var(--ink);
}
.ledger h2 {
  color: var(--ultramarine-tint);
}
.ledger dl {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 0.28rem 0.9rem;
  margin: 0;
  font-size: 0.82rem;
}
.ledger dt {
  color: var(--ultramarine-tint);
}
.ledger dd {
  margin: 0;
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-weight: 500;
}
.ledger dd.flag {
  color: var(--chrome);
  font-weight: 600;
}

/* -------------------------------------------------------------- the bench */

.bench {
  grid-row: 2;
  grid-column: 1 / -1;
  background: var(--vermilion);
  color: #fff;
  border-top: var(--rule) solid var(--ink);
  padding: 1.05rem 1.5rem 1.15rem;
  display: grid;
  grid-template-columns: minmax(0, 17rem) minmax(0, 1fr) auto;
  gap: 0.9rem 2.25rem;
  align-items: start;
}

/* Full white, no opacity: this is the heading that names the honesty boundary, and
   at 0.85 it fell to 4.37:1 against the vermilion. */
.bench h2 {
  color: #fff;
}
.bench-head p {
  margin: 0;
  font-size: 0.88rem;
  line-height: 1.5;
  max-width: 46ch;
}
.bench-head em {
  font-style: normal;
  font-weight: 600;
  border-bottom: 2px solid #fff;
}

.dials {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: 1rem 1.75rem;
}

.dial {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 0.1rem 0.6rem;
}
.dial label {
  font-size: 0.8rem;
  letter-spacing: 0.08em;
}
.dial output {
  font-size: 0.8rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

/* Ruled track, square handle. */
input[type='range'] {
  grid-column: 1 / -1;
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 1.25rem;
  margin: 0.15rem 0 0;
  background: transparent;
  cursor: pointer;
}
/* 0.9 rather than 0.55: the track is the only mark showing a slider's extent, so
   it is a UI component and owes 3:1. At 0.55 it was 2.24:1. */
input[type='range']::-webkit-slider-runnable-track {
  height: 2px;
  background: rgba(255, 255, 255, 0.9);
}
input[type='range']::-moz-range-track {
  height: 2px;
  background: rgba(255, 255, 255, 0.9);
}
input[type='range']::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 1rem;
  height: 1rem;
  margin-top: -0.44rem;
  background: #fff;
  border: 2px solid var(--ink);
  border-radius: 0;
}
input[type='range']::-moz-range-thumb {
  width: 1rem;
  height: 1rem;
  background: #fff;
  border: 2px solid var(--ink);
  border-radius: 0;
}

.bench-side {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.55rem;
}

.toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.85rem;
  cursor: pointer;
}
.toggle input {
  appearance: none;
  -webkit-appearance: none;
  width: 1.05rem;
  height: 1.05rem;
  margin: 0;
  border: 2px solid #fff;
  background: transparent;
  cursor: pointer;
}
.toggle input:checked {
  background: #fff;
  box-shadow: inset 0 0 0 3px var(--vermilion);
}

.takeaways {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.bench-status {
  margin: 0;
  font-size: 0.8rem;
  min-height: 1.1rem;
  text-align: right;
}

/* ------------------------------------------------------------------ about */

dialog {
  border: var(--rule) solid var(--ink);
  background: var(--plaster);
  color: var(--ink);
  padding: 0;
  max-width: 46rem;
  width: calc(100% - 2rem);
  max-height: 86svh;
  overflow: auto;
}
dialog::backdrop {
  background: rgba(20, 18, 15, 0.62);
}

.sheet-prose {
  padding: 1.6rem 1.75rem 0.5rem;
}
.sheet-prose h2 {
  font-size: 1.6rem;
  letter-spacing: -0.03em;
  color: var(--ink);
  margin-bottom: 1rem;
}
.sheet-prose h3 {
  margin: 1.75rem 0 0.4rem;
  font-size: 0.74rem;
  font-weight: 600;
  letter-spacing: 0.2em;
  color: var(--vermilion);
}
.sheet-prose p {
  margin: 0 0 0.8rem;
  font-size: 0.95rem;
  line-height: 1.6;
  max-width: 68ch;
}
/* The embedded face ships upright only, so italic would be a synthesised oblique.
   Emphasis comes from weight instead. */
.sheet-prose em {
  font-style: normal;
  font-weight: 600;
}
.sheet-prose strong {
  font-weight: 600;
}

/* Notation set in the page's own face, tracked out, with lining figures. A system
   monospace here would be the one voice on the page that was not chosen - and a
   geometric sans is the right register for this world anyway: the Bauhaus set its
   own mathematics this way. */
.equation {
  background: var(--ink);
  color: var(--plaster);
  font-size: 1.05rem !important;
  font-weight: 500;
  letter-spacing: 0.07em;
  font-variant-numeric: tabular-nums lining-nums;
  text-align: center;
  padding: 0.95rem 0.75rem;
  max-width: none !important;
}
.tex {
  font-weight: 500;
  letter-spacing: 0.05em;
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--ultramarine);
}

dialog form {
  display: flex;
  justify-content: flex-end;
  padding: 0 1.75rem 1.6rem;
}

.dialog-close {
  position: absolute;
  top: 0.8rem;
  right: 0.8rem;
  padding: 0;
}

.dialog-close button {
  width: 2.2rem;
  height: 2.2rem;
  padding: 0;
  justify-content: center;
  font-size: 1.25rem;
  line-height: 1;
}

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

@media (max-width: 62rem) {
  /* Stacked and free-flowing: the page scrolls, so nothing is bounded or clipped. */
  .sheet {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto;
  }
  .study {
    grid-row: auto;
    grid-template-columns: minmax(0, 1fr);
    gap: 1.1rem;
    overflow: visible;
    border-right: 0;
    padding-bottom: 1.25rem;
  }
  .index {
    grid-row: auto;
    overflow: visible;
  }
  .bench {
    grid-row: auto;
  }
  .plate {
    width: 100%;
    height: auto;
    aspect-ratio: 1 / 1;
  }
  .readout {
    max-width: 60ch;
    min-height: 0;
  }
  .key {
    flex-direction: row;
    flex-wrap: wrap;
    gap: 0.3rem 1.2rem;
  }
  .index {
    border-top: var(--rule) solid var(--ink);
  }
  /* No nested scrollers on touch: the page itself scrolls, so the mode list is let
     out to full height and every row is reached by scrolling the document. Bound the
     measure too, or a full-width row leaves the ratio and the frequency half a
     screen apart. */
  .modes {
    padding: 1.1rem 1.25rem 1rem;
    flex: none;
    min-height: 0;
    display: block;
  }
  .spectrum {
    max-width: 26rem;
    flex: none;
    overflow-y: visible;
  }
  .ledger dl,
  .kac p,
  .kac .chip-invert {
    max-width: 34rem;
  }
  .kac {
    border-top: var(--rule) solid var(--ink);
  }
  .bench {
    grid-template-columns: minmax(0, 1fr);
    gap: 1.1rem;
  }
}

@media (max-width: 34rem) {
  :root {
    --masthead-h: auto;
  }
  .masthead {
    padding: 0.8rem 1rem;
    gap: 0.7rem;
  }
  .masthead-titles p {
    display: none;
  }
  /* Four chips do not fit beside the wordmark on a phone with their labels intact, so
     source and explainer both shed theirs and shrink to an icon-only square. The
     source chip stays reachable rather than vanishing: it is the only link off the
     page that a mobile visitor could otherwise miss entirely, since the explainer's
     colophon copy is easy to skip past without opening the dialog. */
  #link-source span,
  #link-contact span,
  #btn-about span {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  #link-source,
  #link-contact,
  #btn-about {
    padding: 0.4rem;
  }
  #btn-sound {
    font-size: 0.8rem;
  }
  .study,
  .modes,
  .kac,
  .ledger {
    padding-left: 1rem;
    padding-right: 1rem;
  }
  .bench {
    padding: 1.1rem 1rem 1.35rem;
  }
  .readout {
    font-size: 0.95rem;
  }
  .bench-status {
    text-align: left;
  }
  .sheet-prose {
    padding: 1.25rem 1.15rem 0.5rem;
  }
  dialog form {
    padding: 0 1.15rem 1.25rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  .solving-rule {
    animation: none;
    transform: scaleX(1);
  }
}

/* ------------------------------------------------------- the footer band */

/* Below the study sheet, so it costs the instrument nothing: on a wide screen you
   have to scroll past a full viewport of drum to reach it. Plaster ground, because
   the three primaries already have jobs and a fourth field competing with them
   would flatten the composition. This is the sheet's margin, not another panel. */
/* Three columns: "read more", the ad, built-by/support - in that order, side by side.
   "Between read more and built by" means between those two columns, not below the
   taller of two stacked blocks, which is what a two-column layout with a full-width
   row underneath actually produces. The ad column takes the width that is left over
   once the other two claim what they need, since it is the one column with no fixed
   content to size against. */
.colophon {
  flex: none;
  border-top: var(--rule) solid var(--ink);
  padding: 1.6rem 1.5rem 2rem;
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 2rem;
}

/* Three simple flex children instead of a grid: nav on the left, the ad in the
   middle, the built-by/support block on the right. Each has a comfortable minimum
   width and grows to fill leftover space, and the whole row wraps to a stack once
   it can no longer fit three abreast - no separate "collapse to one column" media
   query needed, the box model does it. */
.colophon-nav,
.colophon-side {
  flex: 1 1 15rem;
  min-width: 15rem;
}

.ad-home {
  flex: 1.6 1 22rem;
  min-width: 18rem;
  max-width: 32rem;
}

.colophon h2 {
  font-size: 0.78rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: 0.7rem;
}

.colophon-nav ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.45rem;
}

.colophon-nav a {
  color: var(--ink);
  text-decoration: none;
  border-bottom: var(--hair) solid var(--ink);
  font-size: 1.02rem;
}
.colophon-nav a:hover {
  background: var(--chrome);
}

.colophon-nav span {
  display: block;
  font-size: 0.86rem;
  color: var(--ink-soft);
  border: 0;
}

/* The Direct Link unit. It fills the reserved frame, so it inherits the frame's
   min-height and there is no layout shift when it appears.
   It deliberately does not look like the site's own controls. A visitor must be able to
   tell an advert from the instrument at a glance, so this is a dashed keyline rather than
   the solid 3px ink rule every real region uses, and it takes no primary hue - The One
   Job Rule again. Hover fills ink, which is the same "this is pressable" language as the
   chips, because it is in fact pressable. */
.ad-direct {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0.3rem;
  /* The frame is a centring flex box, so the unit has to opt into filling it. Stretching
     rather than setting a height keeps one source of truth for the reserved height: the
     frame's own min-height, which differs between the footer band and the article slots. */
  align-self: stretch;
  flex: 1 1 auto;
  width: 100%;
  padding: 0.85rem 1rem;
  border: 2px dashed var(--ink-soft);
  color: var(--ink);
  text-decoration: none;
  transition: background-color 120ms linear, color 120ms linear;
}
.ad-direct strong {
  font-weight: 600;
  font-size: 0.94rem;
  letter-spacing: 0.01em;
}
.ad-direct span {
  font-size: 0.8rem;
  color: var(--ink-soft);
}
.ad-direct:hover,
.ad-direct:focus-visible {
  background: var(--ink);
  color: var(--plaster);
  border-style: solid;
}
.ad-direct:hover span,
.ad-direct:focus-visible span {
  color: var(--plaster);
}

/* In the wide footer band the copy is centred; stacked hard left across 1200px of
   plaster would leave the unit looking unfinished rather than deliberate. */
.ad-home .ad-direct {
  align-items: center;
  text-align: center;
}

@media (prefers-reduced-motion: reduce) {
  .ad-direct {
    transition: none;
  }
}

/* No longer needs to stretch against a sibling column now the footer is a single
   stack, but the block still has its own internal layout: meta text, then the
   support region pinned below it. */
.colophon-side {
  display: flex;
  flex-direction: column;
  max-width: 34rem;
}

/* A region, not a card: plaster ground, told apart by a rule and by adjacency, like
   every other region on the sheet. `margin-top: auto` is what turns the leftover
   height into an interval instead of a gap. */
.colophon-support {
  margin-top: auto;
  padding-top: 0.95rem;
  border-top: var(--rule) solid var(--ink);
}

.colophon-support p {
  margin: 0 0 0.8rem;
  font-size: 0.86rem;
  color: var(--ink-soft);
  max-width: 34ch;
}

.colophon-meta {
  margin: 0.9rem 0 0;
  font-size: 0.86rem;
  color: var(--ink-soft);
  max-width: 52ch;
}
.colophon-meta a {
  color: var(--ink);
}

/* ------------------------------------------------------------------- ads */

/* Labelled, because an ad that could be mistaken for the instrument's own readout
   would be dishonest and is also against the network's policy. Set in the same
   uppercase micro-caps as every other field label on the sheet, so it reads as
   part of the furniture rather than as an apology.
   The frame reserves its height up front: `min-height` on the box means the ad
   arrives into space that was always there, so nothing below it ever moves. */
.ad {
  margin: 0;
}

.ad-label {
  display: block;
  font-size: 0.68rem;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: 0.4rem;
}

.ad-frame {
  min-height: 250px;
  width: 100%;
  background: var(--plate);
  border: var(--hair) solid var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.ad-frame .adsbygoogle {
  width: 100%;
}

/* The middle column of the footer, so it takes a squarer shape than the old
   full-width band did - roughly the height of the nav and side columns beside it,
   since all three sit in one row and should look like three columns, not a tall
   box next to two short ones. */
.ad-home .ad-frame {
  min-height: 280px;
}

/* Below this the three items can no longer sit side by side at a readable width,
   so let them go full width and drop the ad back to its normal height. */
@media (max-width: 48rem) {
  .colophon-nav,
  .ad-home,
  .colophon-side {
    flex-basis: 100%;
    max-width: none;
  }
  .ad-home {
    order: -1;
  }
  .ad-home .ad-frame {
    min-height: 250px;
  }
}

@media (max-width: 34rem) {
  .colophon {
    padding: 1.35rem 1rem 1.75rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  .colophon-nav a:hover {
    background: transparent;
  }
}
