/* kit.css — the arcade objects, and the only place any of them is defined.

   Phase A3, 2026-09-03. Split out of styles.css, which had grown to 62KB with
   the reusable objects and the screen-specific layout interleaved. DESIGN.md
   §17 named the missing component layer as the reason visual fidelity does not
   stick: "make every pill look like the mockup's pill" was a hunt through one
   file for whichever of its 27 sections happened to hold pills. The mockup has
   **one** .tile rule and **one** .chip rule, which is exactly why it looks
   coherent, and this file is the app's version of that.

   **The line: an object goes here, a screen stays in styles.css.** A card, a
   row, a pill, a chip, a button, a seg, a slider, a text input, a tile, a part
   row — the things more than one screen builds, and the things phase A4's
   kit.html will render. The Editor's step grid, counter mode's big type and
   Flashcards' deck are one screen each and stayed. `.notice` and `.confirm` are
   near the line and stayed on the far side of it: A3 named a list of objects
   and widening it mid-phase is how a refactor turns into a repaint.

   **Load order is theme.css, kit.css, styles.css, and it is load-bearing.**
   The blocks below are in the order they had in styles.css, so every
   object-to-object override still resolves the way it did (.part-btn over
   .btn-primary, .btn[aria-pressed] over .btn, .screen-meta over .card-sub).
   Two rules in styles.css deliberately sit *after* everything here — the dim-ink
   block and the uppercase block — and both still do. That is why .field-label
   below carries no `color` and why nothing here sets `text-transform`: those
   two facts are stated once each, in styles.css, and this file must not restate
   them. Adding a `color` to .field-label here would win on source order and put
   bone where dim belongs.

   No build step. It is a second <link>, in index.html and in tests.html.       */
/* ---------- Cards — one card family, used everywhere ---------- */

/* The tile frame, and the one place it is defined. Two inset rings - bone
   outside, near-black inside - replace the old hairline border. Inset shadows
   rather than two nested elements so no screen's markup changes, and rather
   than a border so the frame does not eat into the padding budget. */
.card {
  background: var(--surface);
  color: var(--text);
  border: 0;
  border-radius: var(--radius);
  box-shadow:
    inset 0 0 0 var(--frame-w) var(--text),
    inset 0 0 0 calc(var(--frame-w) * 2) var(--frame-lo);
  padding: var(--gap);
  margin-bottom: var(--gap);
}

.card-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--display-md);
  font-weight: 400;
  letter-spacing: 0.03em;
  line-height: 1.5;
}

.card-sub {
  color: var(--text-dim);
  font-size: var(--text-small);
  margin: var(--gap-xs) 0 0;
}

/* A .card-sub standing on its own directly under the header rather than inside
   a card: the Rhythm page's "2 parts", the Editor's "Maracatu · groove · 2
   bars". The title those belong to is in the header now. Declared after
   .card-sub because it overrides it and both are one class deep. */
.screen-meta { margin: 0 0 var(--gap); }
/* Rows inside a card: a pattern under its rhythm */
.row {
  display: flex;
  align-items: center;
  gap: var(--gap-sm);
  min-height: var(--tap-min);
  padding: var(--gap-sm) 0;
  border-top: 1px solid var(--border);
}

.row:first-of-type { border-top: 0; }
.row .row-name { flex: 1 1 auto; min-width: 0; }
.row .row-meta { color: var(--text-dim); font-size: var(--text-small); flex: 0 0 auto; }
/* ---------- Pills and chips — the two badge components ---------- */

/* There are two, and the line between them is what a reader is being told.
   A **pill** is filled and says what a thing *is*: the kind of a part, fixed
   for as long as the part exists. A **chip** is outlined and says what the app
   is *doing right now*: the mix is cut for Surdo 1, the screen is being held
   awake. Phase 27 added the second one to replace two sentences on counter
   mode, and it is the last badge family - a third would mean the distinction
   had stopped meaning anything. */

.pill {
  display: inline-block;
  padding: 5px 6px;
  border-radius: var(--radius-pill);
  font-family: var(--font-display);
  font-size: var(--display-xs);
  font-weight: 400;
  letter-spacing: 0.04em;
  background: var(--surface-raised);
  color: var(--text-dim);
  border: 0;
  white-space: nowrap;
}

/* A drawn icon and one short label inside a frame - the same two-tone inset the
   rest of the chrome wears, at one --frame-w rather than two, because at chip
   size the second ring closes the middle up. */
.chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-sm);
  margin-top: var(--gap-sm);
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--gap-sm);
  margin: 0;
  padding: var(--gap-sm);
  font-family: var(--font-display);
  font-size: var(--display-xs);
  font-weight: 400;
  letter-spacing: 0.06em;
  line-height: 1.4;
  background: var(--surface);
  color: var(--text-dim);
  box-shadow: inset 0 0 0 var(--frame-w) var(--border);
  white-space: nowrap;
}

/* The mix is the one that is worth a colour: it is the setting most likely to
   be wrong, and its failure sounds exactly like its success (DESIGN.md §15). */
.chip-mix {
  color: var(--accent);
  box-shadow: inset 0 0 0 var(--frame-w) var(--accent-dim);
}

/* Half a part row's 32px: a chip is read, not tapped. 16px rather than the
   13 it was, so the bitmap lands one grid unit per pixel - see .part-btn svg
   for what a fractional scale does to a diagonal. **Only the chip icons (drum,
   screen) are drawn here, and they are the two that stayed on the 16 grid** -
   a 32-grid icon at 16px would be half a unit per pixel, which is the jog
   again. js/icons.js has the table. */
.chip svg { display: block; width: 16px; height: 16px; }

/* Filled rather than outlined (DESIGN.md §6): groove/break/intro is the thing you
   scan a rhythm's parts for, and an outline at 15px is not a glance. Ink is --bg
   on all three, which is 6.3:1 or better against every fill. */
.pill.kind-groove { background: var(--kind-groove); color: var(--bg); border-color: var(--kind-groove); }
.pill.kind-break  { background: var(--kind-break);  color: var(--bg); border-color: var(--kind-break); }
.pill.kind-intro  { background: var(--kind-intro);  color: var(--bg); border-color: var(--kind-intro); }
.pill.kind-drop   { background: var(--kind-drop);   color: var(--bg); border-color: var(--kind-drop); }
/* ---------- Buttons ---------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--gap-sm);
  min-height: var(--tap-min);
  padding: 0 var(--gap);
  font-family: var(--font-display);
  font-size: var(--display-md);
  font-weight: 400;
  letter-spacing: 0.04em;
  /* The face has no descenders and sets tight; two-word captions wrap and need
     the leading, and centring keeps them balanced in the box. */
  line-height: 1.6;
  text-align: center;
  color: var(--text);
  background: var(--surface-raised);
  border: 0;
  border-radius: var(--radius);
  box-shadow: inset 0 0 0 var(--frame-w) var(--text);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.btn:active { background: var(--border); }
.btn-primary { background: var(--accent); color: var(--bg); box-shadow: inset 0 0 0 var(--frame-w) var(--frame-lo), 0 0 0 var(--frame-w) var(--accent); }
.btn-primary:active { background: var(--accent-dim); color: var(--text); }
.btn-danger { color: var(--danger); box-shadow: inset 0 0 0 var(--frame-w) var(--danger); }

/* Amber means *on* or *go* — phase A2. It stopped meaning that when "+ Add a
   part" shipped as a full amber slab at the foot of the Rhythm page: the
   loudest object on a screen whose whole point is the parts above it, louder
   than Play. In the mockup it is a quiet outline.

   A third weight rather than a plain .btn, because "not primary" and "the same
   as every other button" are different things: this is the one that should be
   found only when looked for. --surface rather than transparent so it still
   reads as a control on a screen that is now --bg edge to edge. */
.btn-quiet {
  background: var(--surface);
  color: var(--text-dim);
  box-shadow: inset 0 0 0 var(--frame-w) var(--frame-dim);
}
.btn-quiet:active { background: var(--surface-raised); color: var(--text); }

/* The fourth weight — 2026-09-14, Bill's word: "make the backup button a little
   more visible… a bold border and maybe a little orange in there. It should be
   at least as visible as the rest of the boxes in there."

   **"The boxes" is the literal brief, and it is also the construction.** The
   only other thing on Settings is three .cards, and a card's frame is a bone
   3px with a near-black 3px inside it — a 6px pixel-art double frame. This is
   that frame in amber. So the row now weighs what the cards around it weigh,
   which is what was asked, without inventing a fifth way for a box to have an
   edge on a screen that already has one.

   **It does not break §6b's "amber means on or go".** Amber as a *slab* is what
   that rule polices — "+ Add a part" shipped as one and was the loudest object
   on a screen about its own parts. This is an amber frame on a bone caption,
   and the thing it does is go: it is the only control on Settings that opens a
   screen, and every other control there sets a number in place. It is the one
   button on Settings that is not a setting.

   Not a replacement for .btn-quiet, which keeps its caller on the Rhythm page:
   "+ Add a part" is one of many things you could do next, and this is the only
   one. The two are the same shape and opposite arguments. */
.btn-go {
  background: var(--surface-raised);
  box-shadow:
    inset 0 0 0 var(--frame-w) var(--accent),
    inset 0 0 0 calc(var(--frame-w) * 2) var(--frame-lo);
}
.btn-go:active { background: var(--accent-dim); }

/* There are **three** of these now — phase C2 put "Drill with flashcards" and
   "Make a practice file" beside the Share row — and three amber frames stacked
   flush double their edges into a 6px band that reads as one control with lines
   drawn on it. --gap-sm rather than --gap: tighter inside the group than the
   16px the cards above are separated by, which is what says these three are one
   family and not three more cards. The last one's margin costs nothing; the
   tab bar follows it. */
.btn-go { margin-bottom: var(--gap-sm); }

.btn-block { display: flex; width: 100%; }
/* ---------- The popup — GB.ui.popup(), 2026-10-04 ----------

   A <dialog> that is the whole viewport and is its own scrim, holding one
   card on the bottom edge where a thumb reaches. The scrim is on the element
   and not on ::backdrop because ::backdrop could not read a custom property
   until iOS 17.4, and a scrim has to be a token. The UA's dialog box —
   centred, bordered, max-width'd — is undone wholesale here.

   The panel is a .card, so it is the card family's frame and not a new one;
   only its margin and its edge change. It is capped at --app-max so on a
   desktop it sits under the column it belongs to. */
.popup {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: var(--scrim);
  color: var(--text);
}
.popup[open] {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: center;
}
.popup-panel {
  width: 100%;
  max-width: var(--app-max);
  max-height: 100%;
  overflow-y: auto;
  margin: 0;
  /* Below the home indicator once installed; 0 in a browser tab. */
  padding-bottom: calc(var(--gap) + env(safe-area-inset-bottom, 0px));
  outline: none;
}
.popup-panel > :last-child { margin-bottom: 0; }

/* ---------- Button grids ---------- */

.btn-grid {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-sm);
  margin-bottom: var(--gap);
}

.btn-grid .btn { flex: 1 1 45%; }
.btn-grid .btn-block { flex: 1 1 100%; }
/* ---------- Sliders and labelled fields ---------- */

.field { display: block; margin-bottom: var(--gap); }

/* A .field-label straight under a .card-title is two lines of the same face
   four pixels apart, which reads as one wrapped heading. It did not need
   saying while the label was 18px system; phase 28 made it need saying. */
.card-title + .field,
.card-title + .btn-grid { margin-top: var(--gap-sm); }
/* The .btn-grid half arrived with the set strip's "..." menu (2026-10-07),
   whose title is followed by a button: flush, the name sat on its frame. */

/* ⚠️ **The same collision one line lower, and it had shipped** — found by
   `cmp` on 03-import-offer.png in phase B3b, 2026-09-22, and on the backlog as
   a nit since 2026-09-09.

   A `.card-sub` carries a top margin and **no bottom one**, which is right
   under a title with a paragraph after it and wrong the moment a control
   follows. On the import offer card the sentence "This file has 1 rhythm and 2
   parts in it." sat at 0px against the amber button under it — close enough
   that the button's frame clipped the line's descenders — and it had done since
   the Share screen was built. The nit that recorded it said the one-line fix
   was `[data-share=offer] .card-sub { margin-bottom: … }` but that **"the same
   question applies to every other .card-sub immediately above a control in the
   app, so it may be an object-level fix rather than a screen-level one"**. It
   is: this is the object-level one, here beside its sibling, and the cloud
   card was simply the second host to meet it.

   `.card-sub + .field-help` below is a third instance of the same family and
   was fixed the same way in phase 15. */
.card-sub + .field,
.card-sub + .btn-block { margin-top: var(--gap-sm); }

/* The caption over a control, everywhere: "Rhythm name", "What kind of part is
   it?", the wizard's and Settings' rows. On the display face since phase 28 - a
   label is exactly what that face is for, and leaving these on the system stack
   was what made the Rhythm page read as two apps stacked on each other. Not the
   input underneath it, which stays --font at --text-body: you never type into a
   bitmap face, and what you typed is content rather than chrome. */
.field-label {
  display: block;
  /* No `color`: this stands on the field, and the on-field block near the
     top of this file sets it. Declaring it again here would win on source
     order and put bone at 3.4:1 on the grey-blue. */
  font-family: var(--font-display);
  font-size: var(--display-sm);
  font-weight: 400;
  letter-spacing: 0.04em;
  line-height: 1.5;
  margin-bottom: var(--gap-xs);
}

/* An explanation that belongs to the option above it, not a label for the one
   below. Body size rather than --text-small: it is prose to be read, and this
   is still a screen read at arm's length. */
.field-help {
  margin: calc(-1 * var(--gap-sm)) 0 var(--gap);
  color: var(--text-dim);
  font-size: var(--text-body);
}

/* That negative top margin exists to hug the field above it, and it collides
   with anything that is not one. Phase 15 put help text directly under a
   .card-sub twice - the Practice hub's empty rotation and the wizard's summary
   card - where the two lines overlapped outright. */
.card-sub + .field-help { margin-top: var(--gap-sm); }

/* A picker and the button that fires it, side by side — GB.ui.selectField()'s
   `go` half (phase B4a). It is here rather than in styles.css because a GB.ui
   builder emits it, which is this file's line: if GB.ui draws it, kit.html has
   a specimen of it.

   `.select-input` itself is still in styles.css, and that is not a load-order
   bug waiting to happen — `.action-row .select-input` is two classes deep, so
   it wins on specificity whichever file comes first. The base rule is left
   where the Editor's fields section put it.

   The button is sized rather than left to its label: "Go" is two characters and
   an auto-width button would be a 30px target beside a full-width wheel. */
.action-row { display: flex; align-items: stretch; gap: var(--gap-sm); }
.action-row .select-input { flex: 1 1 auto; min-width: 0; }
.action-row .btn { flex: 0 0 auto; min-width: 76px; }

/* Minus, the count, plus. The number is not typeable on purpose - a keyboard
   opening over the grid to set "3" is worse than two taps.

   ⚠️ **It lived in styles.css until phase C4, and moving it was the point of
   the move.** Two screens hand-built this — the Editor's bars and the wizard's
   four repeat counts — and C4's bulk panel would have been the third, which is
   the count §17 named when `segField()` reached six. It is emitted by
   `GB.ui.stepperField()` now, so by this file's own line (see `.action-row`
   above) it belongs here with a specimen on kit.html. */
.stepper { display: flex; align-items: center; gap: var(--gap-sm); }
.stepper .btn { flex: 0 0 var(--tap-min); padding: 0; font-size: var(--text-lg); }

.stepper-value {
  flex: 1 1 auto;
  text-align: center;
  font-size: var(--text-body);
  font-weight: 600;
}

/* Tall enough to grab one-handed, and the thumb is deliberately oversized -
   this gets dragged while the app is playing, often in a moving car. */
.slider {
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 100%;
  height: var(--tap-min);
  margin: 0;
  background: transparent;
  cursor: pointer;
}

.slider::-webkit-slider-runnable-track {
  height: 10px;
  border-radius: var(--radius-pill);
  background: var(--surface-raised);
  border: 1px solid var(--border);
}

.slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 34px;
  height: 34px;
  margin-top: -13px;
  border-radius: var(--radius);
  background: var(--accent);
  border: 2px solid var(--bg);
}

.slider::-moz-range-track {
  height: 10px;
  border-radius: var(--radius-pill);
  background: var(--surface-raised);
  border: 1px solid var(--border);
}

.slider::-moz-range-thumb {
  width: 34px;
  height: 34px;
  border-radius: var(--radius);
  background: var(--accent);
  border: 2px solid var(--bg);
}

.btn[aria-pressed="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg);
}
/* ---------- The Library: a stage select ---------- */

/* The Library lists rhythms and nothing else (DESIGN.md §15), and since phase
   26 it lists them as tiles rather than rows: three columns of framed squares,
   each carrying a portrait of that rhythm's own groove. The whole tile plus its
   label is one button, so a target is ~105px square at 375px — nearly twice the
   56px floor — and there is nothing else on it to tap. DESIGN.md §10. */
.stage-grid {
  display: grid;
  /* minmax(0, 1fr), never a bare 1fr: an fr track's floor is min-content, and a
     tile's min-content is the longest *word* of its name - .tile-label breaks a
     word only once its box is already too narrow, which is a wrap decision and
     not a sizing one. So a rhythm called "Maracatu Nacao Pernambuco" pushed its
     own column out to 112px and squeezed the other two to 107px: one tile in the
     row a different size from its siblings, which is the other half of the
     misalignment .stage-cell's flex fixes. The zero floor lets every column take
     its equal share and hands the overflow back to break-word. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--gap) var(--gap-sm);
  margin-bottom: var(--gap);
}

/* A column flex, not a block, and that is not cosmetic. A grid item stretches
   to its row's height, and a <button> vertically *centres* its own content in
   whatever height it is given. So the moment one name in a row wrapped to a
   line more than its neighbours', every shorter-labelled tile in that row sank
   by half the surplus - 8.8px for one line at 375px, which reads as the top-left
   tile being slightly out of true. Laying the button out as a column with the
   content packed to the start pins every tile to the top of its row whatever
   the names do. The button still fills the row, so the slack below a short
   label stays part of the target rather than becoming a dead gap. */
.stage-cell {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
  width: 100%;
  padding: 0;
  background: none;
  border: 0;
  color: var(--text);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

/* The boss-tile frame, and the one place it is drawn. Not the two-tone inset
   ring the cards use (.card, above): this is the reference's own tile — a dark
   surround with four bone corner blocks cut into it, and a dim ring set in from
   the edge. The corners are four non-repeating background gradients rather than
   four elements, so a tile is three nodes plus its portrait. */
.tile {
  position: relative;
  padding: 6px;
  background-color: var(--frame-lo);
  background-image:
    linear-gradient(var(--text), var(--text)),
    linear-gradient(var(--text), var(--text)),
    linear-gradient(var(--text), var(--text)),
    linear-gradient(var(--text), var(--text));
  background-size: 9px 9px;
  background-position: 0 0, 100% 0, 0 100%, 100% 100%;
  background-repeat: no-repeat;
}

/* --text-dim reads as a lighter ring on the dark frame and a darker one on the
   inverted tile below, so one rule covers both. It is a token rather than a
   translucent bone because there is no alpha on a hex custom property without
   color-mix(), which is newer than the phone this is tested on. */
.tile::after {
  content: '';
  position: absolute;
  inset: 3px;
  box-shadow: inset 0 0 0 2px var(--text-dim);
  pointer-events: none;
}

/* The frame flashes rather than the tile: the portrait is the thing you were
   looking at, and dimming it under a thumb is the wrong feedback. */
.stage-cell:active .tile { background-color: var(--accent); }

.tile-inner {
  aspect-ratio: 1 / 1;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: var(--bg);
}

/* Dr. Wily's tile: the one square in MM2's 3×3 framed in white instead of
   black. "+ New rhythm" takes it. */
.tile.is-special { background-color: var(--text); }
.tile.is-special .tile-inner {
  color: var(--accent);
  font-family: var(--font-display);
  font-size: var(--text-xl);
  line-height: 1;
}

/* One flush-left block, not MM2's staggered pair. The reference sets its second
   line flush right and gets away with it because every name ends in the same
   word; with real names it reads as jitter and crowds the tile to its right.
   Two lines' height is reserved whether the name uses them or not, so a row of
   tiles starts level. No `color`: it is the rhythm's name, which is data
   rather than a label, so it inherits bone from .screen. It was the dark ink
   from phase 25 until A2, for standing on the blue. */
.tile-label {
  display: block;
  margin-top: var(--gap-sm);
  min-height: calc(var(--display-tile) * 1.6 * 2);
  font-family: var(--font-display);
  font-size: var(--display-tile);
  font-weight: 400;
  line-height: 1.6;
  letter-spacing: 0.02em;
  overflow-wrap: break-word;
}

/* The portrait: one bar of the rhythm's groove, nine drums deep. Squares,
   which they have always been — at ~6px a ring is mud — and since 2026-09-03
   the grid's cells are squares too, so this is no longer the odd surface out.
   Colour says which drum, and that is now the only thing it has to say.

   Sixteen columns straight and twelve on a triplet pattern, in the *same box*:
   the cells come out a third wider rather than the picture coming out
   narrower, which is what makes a triplet groove recognisable as one at a
   glance (phase B1, DESIGN.md §20). The count arrives as `data-steps` from
   GB.ui.portrait() and never as an inline style — style-src carries no
   'unsafe-inline'. Sixteen is the default, so an element without the
   attribute draws as it always did. */
.thumb {
  display: grid;
  grid-template-columns: repeat(16, 1fr);
  grid-auto-rows: 1fr;
  gap: 1px;
  width: 100%;
  height: 100%;
  padding: 3px;
}

.thumb[data-steps="12"] { grid-template-columns: repeat(12, 1fr); }

/* A hit sits at 0.8 and **that is not left over from the accent** — it is the
   headroom the beat lift below needs. Take it to 1 and a sounding hit has
   nowhere brighter to go. */
.thumb-cell { background: var(--cell-off); }
.thumb-cell.is-hit { background: var(--hue, var(--cell-hit)); opacity: 0.8; }

/* The beat, phase 19: the column that is sounding right now. Scheduled off the
   audio clock exactly as the Editor's playhead is (DESIGN.md §4, §11) — the
   same signal, pointed at a second surface.

   It **lifts** rather than sweeping, which is where it parts company with the
   grid's playhead. .cell.is-now paints the whole column --playhead and wins
   over .cell.is-hit on source order, so in the Editor a passing playhead
   momentarily erases the hits it crosses. On a 120px portrait that reads as a
   shadow crossing the picture, and the thing being reported is a drum being
   struck. So the off cells take --playhead (brighter than --cell-off, which is
   what makes the column findable at all) and the *hits* go to full strength in
   their own hue instead of losing it. Three cells' worth of brightness, no
   movement, no transition.

   The is-hit rule has to be repeated at two classes' worth of specificity to
   win back the background --now takes off it. */
.thumb-cell.is-now { background: var(--playhead); }
.thumb-cell.is-hit.is-now { background: var(--hue, var(--cell-hit)); opacity: 1; }

/* Wider than a phone: a fourth column, and a label sized for it.

   Three columns is a phone measurement - it is what puts a ~105px tile on a
   375px screen. Past the column cap the same three tiles are 220px each, which
   is a poster of a rhythm rather than a way to pick one; four at ~166px reads
   as the reference does. The breakpoint is the grid's own (480px), so the two
   sizes meet: four tiles at 480px are 106px, the same tile the phone gets.

   Not `auto-fill`: the count is a design decision, not a function of the
   viewport. */
@media (min-width: 480px) {
  :root { --display-tile: 13px; }
  .stage-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* A fifth column past 820px — phase A5b, DESIGN.md §19, and Bill's call on
   2026-09-14 against both mocked at 1280 and at 834.

   **The old comment above said a fifth column "would put the label below
   anything readable", and that was true of the column it was written under.**
   At the 720px cap five tiles were 131px; at 880 they are **163.19px**, which
   is within three pixels of the ~166px the four-column line already calls the
   size that reads right. The number moved out from under the rule, so the rule
   went rather than being carried forward — it is not restated above any more.

   Measured, both counts, at both widths:

     880 cap   four columns  206px      five columns  163.19px
     834       four columns  194.5px    five columns  154px

   206px is nearer the 220px this section already rejected as a poster of a
   rhythm than it is to the tile it wants. And the count buys a row: ten
   rhythms plus `+ New rhythm` land in two rows at five, so the Share row is
   above the fold where at four columns both it and the new-rhythm tile fall
   below it.

   The label survives it. `.tile-label` reserves two lines (min-height) and
   breaks on a word, so the longest real name in the seed bank sets on one line
   at 1280 and takes the second line it already had at 834.

   820 rather than 880 is the density pass's one breakpoint, shared with
   .part-list below and with --display-xl in styles.css: three screens, one
   number, and none of it can reach 375px. */
@media (min-width: 820px) {
  .stage-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}
/* ---------- The Rhythm page: one row per part ---------- */

/* A part is a 136px row, and phase 19 is why. It was 64px from phase 13 to
   phase 28: name and meta on the left, then Play, the pencil and the star in a
   single line of three 56px squares. The thumbnail slot sat at the front of
   that line at zero width, waiting for this phase.

   **It could not be filled where it stood, and that is the whole of the new
   shape.** The old line left 141px of text column:

     375 - 32 (screen padding) - 16 (row padding) - 168 (3 x 56) - 18 (3 gaps)
       = 141px

   and "Groove (demo)" measures 141px at --display-md, which is not a
   coincidence — phase 28 picked 10px over 11px against exactly that number. A
   40px portrait in the slot takes the column to 95px, which clips every real
   name to an ellipsis and wraps the groove row's kind pill and bar count onto a
   second line while the break row's stays on one, so the rows stop matching.
   Measured, at 375px, before any of this was written.

   So the row turns the corner instead. The left column stacks: the text on top,
   the three controls under it. The portrait is a square on the right edge.

     height: 56 (the text target) + 8 + 56 (the controls) = 120px of content
     width:  375 - 32 - 16 - 120 (the square) - 6 = 201px for the left column

   Both numbers come out better than what they replaced. The name gets 201px
   rather than 141px, so a name that used to ellipsise now fits; the controls
   get their 180px with 21px to spare rather than 14px; and the portrait is
   120px, which is a *bigger* cell than the Library tile's ~105px and lands on
   the same ~6.5px grid. The cost is the row height, and it is a real cost: six
   parts is 816px of scrolling where it used to be 432px. DESIGN.md §15.

   The 64px floor stays as a floor. Nothing renders that short any more, but a
   row is still never allowed to be a target you have to aim at. */
.part-list { margin-bottom: var(--gap); }

.part-row {
  display: flex;
  align-items: stretch;
  gap: var(--gap-row);
  min-height: 64px;
  padding: var(--gap-sm);
  margin-bottom: var(--gap-sm);
  background: var(--surface);
  border: 0;
  box-shadow:
    inset 0 0 0 var(--frame-w) var(--text),
    inset 0 0 0 calc(var(--frame-w) * 2) var(--frame-lo);
  border-radius: var(--radius);
}

/* Two-up past 820px, and the row itself is untouched — phase A5b, DESIGN.md
   §19. Widening the column to 880 (A5a) fixed the Editor's grid and left this
   screen stretching rather than reflowing: at 880 a part row is 848px wide with
   a 201px text column, a 120px portrait and roughly 500px of dead black between
   them. The fix is the container, not the row. Every number inside .part-row
   was measured hard at 375px (§15 — 201px text, 120px portrait, 56+8+56 of left
   column), and re-deriving them at a second width is how a fidelity pass turns
   into a repaint.

   So .part-list becomes a 2-column grid and each row renders at

     (880 - 32 screen padding - 8 gap) / 2 = 420px

   which is much nearer the 343px it was designed at than 848 is. Six parts are
   on screen at 1280px where four were, which also answers "is 136px per part
   too much scrolling?" from the other side on anything wider than a phone.

   820 rather than 880 because that is where two rows first have room; below it
   nothing changes, and the query cannot reach 375px, which is why all twenty
   screenshots stay byte-identical.

   The gap is --gap-sm because that is the row's own margin-bottom, so the
   vertical rhythm is the one it already had; align-items: start so a row whose
   meta has wrapped stands taller without stretching its neighbour to match.
   `+ Add a part` is a sibling of .part-list rather than a child, so it stays
   full width under both columns. */
@media (min-width: 820px) {
  .part-list {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--gap-sm);
    align-items: start;
  }
  .part-list .part-row { margin-bottom: 0; }
}

/* Everything that is not the portrait, stacked. min-width: 0 is what lets
   .part-name's ellipsis work at all - without it the flex child takes its
   content's width and the row overflows instead of truncating. */
.part-main {
  display: flex;
  flex-direction: column;
  gap: var(--gap-sm);
  flex: 1 1 auto;
  min-width: 0;
}

/* The biggest target on the screen, and it opens the Editor.

   flex: 1 1 auto against a 56px floor: it takes whatever height the row has
   spare, so on a row whose meta has wrapped the text target grows rather than
   the portrait going lopsided. The floor is the one that matters - every button
   in this app is >=56px tall and tools/check-app.js checks all of them at once,
   this one included. */
.part-open {
  display: flex;
  flex-direction: column;
  justify-content: center;
  flex: 1 1 auto;
  min-height: var(--tap-min);
  min-width: 0;
  padding: 0;
  font-family: inherit;
  font-size: var(--text-body);
  color: var(--text);
  text-align: left;
  background: transparent;
  border: 0;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

/* The three controls, on their own line since phase 19. Left-aligned rather
   than spread: they stay square at exactly --tap-min because the drawn icons
   are 32px of a 56px button and that ratio is a device-pixel argument, not a
   preference (see .part-btn svg below). The ~21px left over at 375px goes
   nowhere, which is correct - a stretched star is not a better star. */
.part-controls {
  display: flex;
  gap: var(--gap-row);
  flex: 0 0 auto;
}

/* The portrait, on the right edge, square. Filled by phase 19 with the same
   GB.ui.portrait() the Library's tiles use — one object, one builder, and
   .thumb above does all the drawing.

   Square is enforced here rather than left to aspect-ratio: a flex item with
   `flex: 0 0 auto` and only an aspect-ratio collapses to its content's
   intrinsic width, which measured 21px. Both dimensions are written down.

   align-self: center, not stretch, so that a row whose meta has wrapped gets a
   taller text target and a portrait that is still square rather than a portrait
   stretched into a rectangle. */
.part-thumb {
  flex: 0 0 auto;
  align-self: center;
  width: var(--part-thumb);
  height: var(--part-thumb);
}

.part-text { flex: 1 1 auto; min-width: 0; }

/* Two lines rather than one: name, then kind and bars underneath. A name, a
   pill and "2 bars" side by side leaves about 60px for the name, which is not
   a name. */
/* The display face since phase 28, which is what --display-md was always
   described as being for ("part names in a row") - it just never got applied.
   10px and not 11: the text column is 141px at 375px, and this face runs about
   one em per character, so 13 characters of "Groove (demo)" is 135px at 10px
   and 149px at 11px. The ellipsis below is the fallback for a longer name, not
   the normal case. */
.part-name {
  display: block;
  font-family: var(--font-display);
  font-size: var(--display-md);
  font-weight: 400;
  letter-spacing: 0.03em;
  line-height: 1.5;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A row of short facts, not a paragraph. Each one stays whole and the line
   breaks between them — without the nowrap, phase 24's third fact took the
   line to exactly the 189px available and both "2 bars" and "1 cycle" broke
   across two lines each, taking a 56px row to 85px. */
/* The same 6px as the row, and measured the same way (phase 27). With the pencil
   on the row the text column is 141px, and a groove pill (56) plus "2 bars" (50)
   plus an "8x" override (20) needs 138 of it at --gap-row and 148 at --gap-sm -
   so at the old gap the busiest row wrapped and stood 91px tall next to its 72px
   neighbours, which is precisely what phase 24 abbreviated "8 cycles" to "8x" to
   avoid. The wrap stays as the fallback it was built to be: a 16-bar part with
   an override still breaks the line, and a 91px row is legible, not broken. */
.part-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--gap-row);
  margin-top: var(--gap-xs);
  /* Phase 28: the facts beside the pill are set in the display face too. They
     were 18px system, which put a bitmap pill and a system sentence-height
     number on the same line and was the loudest mismatch on the screen.

     --display-xs, the pill's own size, and that is a measurement rather than a
     tidiness. This face runs about 1.04em per character including its tracking,
     so at 9px a groove pill (56), "2 bars" (56) and an "8x" override (19) plus
     two 6px gaps come to 142.6px of the 141px column - two tenths of a pixel
     over, which wraps the line and stands the row 3px taller than its
     neighbour. At 7px the same three are 126px. It also fixes the hierarchy the
     bigger size lost: the part name above is 10px, and a fact you glance at
     should not be within a pixel of the name you scan by. */
  font-family: var(--font-display);
  font-size: var(--display-xs);
  letter-spacing: 0.04em;
  line-height: 1.6;
}

.part-meta > * { white-space: nowrap; }

/* Square, and exactly --tap-min. Three of them, and none carries a word any
   more: since phase 27 each holds a 16x16 drawn icon (see js/icons.js for why a
   picture rather than a character). The button keeps its .btn frame, so the
   target and its focus ring are unchanged - only the contents moved. */
.part-btn {
  flex: 0 0 var(--tap-min);
  width: var(--tap-min);
  padding: 0;
}

/* 32px of a 56px button, and **the whole ratio matters more than the 32**:
   crispEdges snaps every rect to a device pixel, so at 26px - 1.625 grid units
   per pixel - the same one-pixel step of a diagonal came out one device pixel
   wide in one row and two in the next, and every staircase in the set turned
   into a jog. That is what "the icons aren't drawn very cleanly" was in phase
   27, and no amount of redrawing fixes it.

   **32px is what makes two grids possible in one button, which is why it is the
   number that must not move.** play and stop are 16-unit bitmaps drawn here at
   2 units per pixel; the pencil and the star are 32-unit bitmaps at 1 - both
   whole, both crisp, and the finer pair is what let those two shapes finally be
   drawn properly on 2026-09-04 (js/icons.js has the reasoning). Any other size
   breaks one grid or the other: 48px is 3x and 1.5x, and does not fit a 56px
   target anyway. (A chip's copy is 16px and only ever draws 16-unit icons -
   see .chip svg.) */
.part-btn svg {
  display: block;
  width: 32px;
  height: 32px;
}

/* Play is the one that changes what it draws rather than only what colour it
   is: a square is the other half of the transport pair and needs no reading. */
.part-btn.btn-primary { color: var(--bg); }

/* The dim frame — phase A2. All three of these shipped in .btn's bone frame, so
   a part row was four objects of equal weight (the name, Play, the pencil, the
   star) and the eye had nothing to land on. In the mockup the frames are dim
   grey and amber appears exactly twice on the screen: Play while it is playing,
   and a star that is lit.

   The *glyphs* stay bone. The frame is the thing that was shouting; a dim pencil
   on a dim frame would be two steps back, and the pencil is still a target you
   have to find one-handed in a dim room. Only the star's glyph is dim, and it
   was already - dim-to-amber is what the star's state has always been.

   Not applied to .btn generally: a full-width button with a caption is one
   object with one frame, and it is the row of three squares that had the
   problem. .btn-quiet below is the caption half of the same idea. */
.part-btn { box-shadow: inset 0 0 0 var(--frame-w) var(--frame-dim); }

/* ...but not when it is lit. Both of these paint their own frame and have to
   win on source order, which is why they sit here rather than above. */
.part-btn.btn-primary {
  box-shadow: inset 0 0 0 var(--frame-w) var(--frame-lo),
              0 0 0 var(--frame-w) var(--accent);
}

/* The rotation toggle. A star rather than a word: it is a *state*, not an
   action, and it has to read at a glance in a dark rehearsal room. The
   explainer above the list is what teaches it.

   One drawn star since phase 27, not the ★/☆ pair - the state is carried by
   three things changing at once (the star's own colour, the frame around it and
   the ground behind it) rather than by the shape. */
.part-star { color: var(--text-dim); }

/* The frame lights with the star. `border-color` was written here before phase
   25 turned every frame into a box-shadow, so from then until phase 27 it set
   nothing at all and the "in my rotation" state was a colour change on the star
   alone. Two channels now, as .btn-danger already does it. */
.part-star.is-on {
  color: var(--accent);
  background: var(--surface-raised);
  box-shadow: inset 0 0 0 var(--frame-w) var(--accent);
}
/* ---------- The set list: one line per thing that plays ---------- */

/* Phase C2, 2026-09-16. DESIGN.md §24, and it was mocked at 375px twice before
   a line of it was written — `mocks/c2-set-list.html` keeps all eight
   candidates.

   **This is deliberately not a .part-row.** The Rhythm page's row is 136px
   because it holds a 120px portrait (§15), and a set of nine is 1,224px of
   scroll before you reach anything. The set list is a *list*: one line per
   thing that plays, name flush left so nine names scan down one edge, and the
   whole set on one 375×812 screen.

   Hairline rules rather than frames, and that was decided by looking. Candidate
   B gave each row the card family's double frame and the dim return rows ended
   up floating *between* framed cards, reading as gaps in the list rather than
   as part of it — the one-card-family instinct working against itself, because
   these are lines in a list, not cards you tap into. */
.setlist { margin-bottom: var(--gap); }

/* ⚠️ **A <div> holding two buttons since phase C3b, and a single <button>
   before it.** Bill asked for the count to become the way to change it, and a
   button cannot nest inside a button — so the row stopped being one target.
   What that costs is C2's "the whole row is the target", chosen so an older
   thumb never has to aim inside a 343px row; .set-open is still ~250px of it. */
.set-row {
  display: flex;
  align-items: center;
  gap: var(--gap-sm);
  width: 100%;
  min-height: 72px;
  border-top: 1px solid var(--border);
}
.set-row:first-child { border-top: 0; }

/* The disclosure half: everything the C2 row was, minus the count. It carries
   the row's vertical padding so the whole of it is tappable rather than just
   the text inside it. */
.set-open {
  display: flex;
  align-items: center;
  flex: 1 1 auto;
  min-width: 0;
  min-height: 72px;
  padding: var(--gap-sm) 0;
  color: inherit;
  text-align: left;
  background: transparent;
  border: 0;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.set-main { flex: 1 1 auto; min-width: 0; }

/* ⚠️ **No wrap, and the rhythm name is the half that gives way.** These two
   came off .rotation-row, the object this row replaces, where they were
   measured rather than assumed (phase 15): .part-meta grew flex-wrap in phase
   24 for the Rhythm page, where *three* short facts share 189px. Here there are
   two and the second is an unbounded rhythm name. Left wrapping it never has to
   shrink, so it never truncates — it takes a second line instead, and
   "Samba Reggae do Bloco Afro Ile Aiye" grew the row from 73px to 106px. A
   .row-meta does not shrink by default either, so the kind pill stays
   fixed-width and the rhythm ellipses. tools/check-app.js GATEs it with a name
   long enough to force the question. */
.set-main .part-meta { flex-wrap: nowrap; }
.set-main .row-meta {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The effective repeat count, and ⚠️ **dim versus bone is the override mark** —
   §24. On the Rhythm page the mark is the number *existing at all*:
   hasRepeatsOverride() is checked and a part with no override shows nothing
   (§15, phase 24). That signal cannot work here, because "see how many repeats
   are on an item" is what this screen is for and so every row carries a number.
   Dim means the count came from the kind; bone means this part overrides it.
   One token, no new object, and the same dim-versus-data distinction §6b
   already draws for .barcount and .tile-label. */
.set-reps {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  padding: 0 var(--gap-xs);
  background: transparent;
  /* ⚠️ **A dim frame, and it is what makes the number look tappable.** §6b: a
     dim frame is how something recedes, and it is also how the part row's Play
     and pencil say "control" without claiming amber. §24 kept candidate C's
     framed count box in reserve for exactly the phase where the number stopped
     being a label; this is that phase. */
  box-shadow: inset 0 0 0 var(--frame-w) var(--frame-dim);
  cursor: pointer;
}
.set-reps-num {
  font-family: var(--font-display);
  font-size: var(--display-md);
  color: var(--text-dim);
}
.set-reps.is-own .set-reps-num { color: var(--text); }

/* A groove interposed after a break — what actually plays next with "Back to
   the groove" on. Drawn dim because you cannot move one, cannot remove one on
   its own, and did not put it there.

   ⚠️ **44px, and deliberately under the 56px rule.** It is not a tap target.
   A 56px row that does nothing reads as a control that is broken, where a 44px
   dim line reads as a consequence of the rows around it — and three of them at
   44 buy back 36px of list. This is the app's one exception to the tap-target
   floor and it is an exception because the object is not tappable at all. */
.set-return {
  display: flex;
  align-items: center;
  gap: var(--gap-sm);
  min-height: 44px;
  padding: var(--gap-sm) 0 var(--gap-sm) var(--gap);
  border-top: 1px solid var(--border);
  color: var(--text-dim);
  font-family: var(--font-display);
  font-size: var(--display-md);
  letter-spacing: 0.03em;
}

/* The row you are on. A 3px amber bar down the left and the name in amber —
   two channels, the way .part-star.is-on already does it, so it survives being
   looked at across a kitchen.

   The bar is drawn *inside* with an inset shadow and paid for with padding, so
   the row's own box does not move and the eight rows below it stay aligned. */
.set-row.is-now {
  box-shadow: inset 3px 0 0 0 var(--accent);
  padding-left: var(--gap-sm);
}
.set-row.is-now .part-name { color: var(--accent); }

/* The control strip, disclosed under the row it acts on — Bill, 2026-09-16.

   ⚠️ **This is not a new object and must not become one.** It is
   `.part-controls`' geometry — 56px squares, left-aligned, --gap-row between
   them — holding .part-btn children, and all this rule adds is *where it sits*:
   under the row, carrying the row's amber bar, with no rule between the two so
   the pair reads as one disclosed thing rather than as a tenth row.

   Left-aligned rather than spread, and the ~170px left over at 375px goes
   nowhere. That is the same call .part-controls already made (§15: "a stretched
   star is not a better star") — a square button is square because the icon in
   it is drawn on a square grid.

   ⚠️ **Phase C3 briefly stacked this into .strip-line / .strip-field to hold a
   repeats <select>, and C3b deleted both objects rather than leaving them
   unbuilt.** Bill's answer on a 13 mini was that the select was too big and the
   count belonged on the row; the strip is two pictures again. §18 is why the
   dead objects went with it — an object nothing builds is the drift kit.html
   exists to catch.

   ⚠️ **There is no transport on this screen and there is not going to be one
   below the fold.** The fixed bar above the tab bar lasted one round of mocks;
   §16's "a button aimed at without looking must not scroll away" is met by
   scrolling the current row into view instead. §24. */
.set-panel {
  display: flex;
  align-items: center;
  gap: var(--gap-row);
  padding: 0 0 var(--gap-sm) var(--gap-sm);
  box-shadow: inset 3px 0 0 0 var(--accent);
}

/* The Take-out confirm, under the strip rather than in place of it, so the two
   pictures stay visible and it is obvious which row is being talked about. It
   carries the amber bar for the same reason the strip does: the question and
   the row it is about are one disclosed object. */
.set-confirm {
  padding: 0 0 var(--gap-sm) var(--gap-sm);
  box-shadow: inset 3px 0 0 0 var(--accent);
}
.set-confirm .confirm { margin-top: var(--gap-sm); }

/* ---------- Reorder mode: the set list with arrows (phase C3, rebuilt C6) ---------- */

/* ⚠️ **This block was deleted by accident in phase C3b and nobody saw for three
   weeks.** C3b removed the dead .strip-line objects and took the five rules
   under them along, so Reorder rendered in the body face with its arrows
   jammed against the names — every check-app assertion still green, because
   the 56px floor was met by the buttons alone. tools/check-kit.js now fails if
   kit.html names a class in <code> that no stylesheet defines; that is the
   guard this block did not have.

   **Rebuilt to be the set list's row, not a compact cousin of it** (Bill,
   2026-10-08: "look more like the practice screen"). The same .set-main —
   name, kind pill, rhythm — so a part looks the same in both modes and the
   eye does not have to re-find it; the two arrows take the place the count box
   holds on the list, and are the same 56px dim-framed square. The returns are
   .set-return, drawn exactly as the list draws them. */
.move-row {
  display: flex;
  align-items: center;
  gap: var(--gap-row);
  min-height: 72px;
  padding: var(--gap-sm) 0;
  border-top: 1px solid var(--border);
}
.move-row:first-child { border-top: 0; }

/* ⚠️ **U+2191 and U+2193, and they were measured before they were used.**
   §24's sketch said ▲▼ and both are absent from the face: at 48px U+25B2
   renders at 70 distinct alpha levels and U+25BC at 19, against the face's own
   six, so both arrive as fallback outlines — §12's U+2192 bug and C2's U+21B3
   bug a third time. These two measure six levels at the full 48-unit advance.
   Set at --display-xl because they are the control, not a caption on one. */
.move-glyph {
  font-family: var(--font-display);
  font-size: var(--display-xl);
  line-height: 1;
}

.reorder-done { margin-top: var(--gap); }

/* ---------- Inline forms (no browser prompts anywhere) ---------- */

.text-input {
  display: block;
  width: 100%;
  min-height: var(--tap-min);
  padding: 0 var(--gap-sm);
  font-family: inherit;
  font-size: var(--text-body);
  color: var(--text);
  background: var(--surface-raised);
  border: 0;
  box-shadow: inset 0 0 0 var(--frame-w) var(--border);
  border-radius: var(--radius-sm);
}

.text-input:focus {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* Three equal buttons on one line - used for the groove/break/intro choice. */
.seg { display: flex; gap: var(--gap-sm); }
.seg .btn { flex: 1 1 0; min-width: 0; padding: 0 var(--gap-sm); }
