/* ============================================================
   Tone Clock — "The web book"
   Typography after Matthew Butterick's Practical Typography:
   a white page, Charter for everything, one brick-red accent,
   letterspaced-caps captions, hairline rules, no boxes.
   One register since the quiet pass (docs/stage-and-deck.md §6): the
   instrument keeps its feel — sound on touch, springs, detents,
   latches — and gives up its look. No LCD glass, no key edges, no LED
   strips, no monospaced words: Plex Mono sets pitch-class data only.
   ============================================================ */

/* ---------- Fonts ----------
   Charter — Matthew Carter, Bitstream, 1987. Distributed freely by
   Matthew Butterick (see fonts/charter-license.txt). */

@font-face {
  font-family: "Charter";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/charter-regular.woff2") format("woff2");
}
@font-face {
  font-family: "Charter";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/charter-italic.woff2") format("woff2");
}
@font-face {
  font-family: "Charter";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/static/fonts/charter-bold.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/ibm-plex-mono-400.woff2") format("woff2");
}

/* ---------- Tokens ----------
   Every color token is light-dark(light, dark): the page follows the OS
   theme, and any subtree can force a side by setting color-scheme (the
   notation component's "printed cutting" does exactly that). */

:root {
  color-scheme: light dark;
  --paper: light-dark(#fffefb, #181714);
  --face: light-dark(#f5f4ee, #232219);
  --ink: light-dark(#201f1d, #e9e6dd);
  --ink-soft: light-dark(#504e48, #c2beb2);
  /* The faintest text ink, still 4.5:1 on the paper and on the key field
     (the quiet pass sets the tabs, the HUD and the readout's edit word
     in it). */
  --ink-faint: light-dark(#716e66, #918d80);
  --rule: light-dark(#e6e4dc, #34322b);
  --rule-2: light-dark(#c6c3b8, #56534a);
  --accent: light-dark(#ae2b14, #e2694a);
  --accent-dark: light-dark(#8c2110, #d0552f);
  /* The ground behind the clock (docs/stage-and-deck.md §6): the paper
     today, its own token so the intent rings' gap and Phase 4's stage
     read one name. */
  --stage: var(--paper);
  /* One register (docs/stage-and-deck.md §6, the quiet pass): the deck
     is the stage's paper, set off by a hairline and its lift. A key is a
     word on a faint field — ink at about 5% over the paper — and a press
     darkens it to about 12%; nothing else marks it as a key. Contrast
     roles are pinned by view_contract's contrast test. */
  --deck: var(--paper);
  --veil: color-mix(in srgb, var(--deck) 90%, transparent);
  --key: light-dark(#f3f2ee, #23221f);
  --key-down: light-dark(#e4e3df, #31302c);
  /* The one mark for "on": a 2 px ink bar under the label — the tab's
     underline, a latched key's lit strip, the active chip's fill before
     the quiet pass. Drawn as a background image, so any control can wear
     it without a pseudo-element. */
  --bar: linear-gradient(var(--ink), var(--ink));
  --lift-1: 0 -1px 2px rgb(0 0 0 / .10), 0 -8px 24px rgb(0 0 0 / .14);
  /* The intent fills (§6 states): a pitch pressed toward selected reads
     85% accent, toward released 15%, so intent never passes for truth.
     Dark mode mixes toward white, which keeps the paper label above
     4.5:1 on the lighter dark-mode accent. */
  --intent-on: light-dark(
    color-mix(in srgb, var(--accent) 85%, var(--paper)),
    color-mix(in srgb, var(--accent) 85%, white)
  );
  --intent-off: color-mix(in srgb, var(--accent) 15%, var(--stage));
  /* The Effect spring (ζ 1, k 1600, §4.3) sampled every 30 ms: rings and
     fills settle on it without overshoot. */
  --spring-effect: linear(0, .337, .692, .874, .952, .983, .994, 1);
  --spring-effect-dur: .21s;
  --live: light-dark(#33702e, #79b573);
  --fret-line: light-dark(#8f8b81, #86827a);
  --fret-nut: light-dark(#201f1d, #cfcbc0);
  --subset-0: light-dark(#ad6d12, #d29a3f);
  --subset-1: light-dark(#1e6f72, #4fa8ac);
  --subset-2: light-dark(#6b4a9e, #a184d1);
  --subset-3: light-dark(#35793a, #5cb168);
  --subset-4: light-dark(#b5402f, #e07b70);
  --subset-5: light-dark(#1d689c, #5ba3d8);
  --subset-6: light-dark(#a98200, #d9b23c);
  --subset-7: light-dark(#93386f, #c46cb0);
  --subset-8: light-dark(#59712f, #8fb277);
  --subset-9: light-dark(#c25626, #e08a67);
  --subset-10: light-dark(#4d5a95, #8b9ad0);
  --subset-11: light-dark(#85683a, #bfa06a);
  --serif: "Charter", "Bitstream Charter", "Sitka Text", "Iowan Old Style", Georgia, serif;
  --mono: "IBM Plex Mono", "SFMono-Regular", ui-monospace, monospace;
  /* Letterspaced caps for captions and labels — caps or bold, never both. */
  --caps-tracking: .12em;
  /* Spacing scale: rhythm properties (gap, section padding/margins) draw
     from this ladder; optical one-offs (clock geometry, safe areas,
     borders) stay literal. */
  --space-1: .3rem;
  --space-2: .45rem;
  --space-3: .6rem;
  --space-4: .8rem;
  --space-5: 1.2rem;
  --space-6: 1.5rem;
  --space-7: 2.4rem;
  /* Type scale below body size: three caps registers for labels, three
     text registers for annotation/aside copy. */
  --text-caps-sm: .62rem;
  --text-caps: .68rem;
  --text-caps-lg: .74rem;
  --text-sm: .8rem;
  --text-md: .85rem;
  --text-lg: .92rem;
  /* Interaction tokens: one focus ring (theme.js mirrors it for shadow
     roots), one disabled register, WCAG tap-target floors. */
  --focus-ring: 2px solid var(--accent);
  --focus-offset: 2px;
  --disabled-opacity: .4;
  --tap-min: 2.1rem;
  --tap-min-coarse: 2.75rem;
  /* Motion: three curves + one spring. Entries decelerate (--ease-out),
     hover feedback snaps (--ease-snap), state changes glide
     (--ease-in-out), and the few tactile micro-interactions get a
     spring with a whisper of overshoot (--spring-snappy). CSS keyword
     easings are banned here — an unspecified timing function silently
     means `ease`. */
  --ease-out: cubic-bezier(.16, 1, .3, 1);
  --ease-snap: cubic-bezier(.22, 1, .36, 1);
  --ease-in-out: cubic-bezier(.65, 0, .35, 1);
  --spring-snappy: linear(
    0, 0.009, 0.035 2.1%, 0.141 4.4%, 0.723 12.9%, 0.938 16.7%,
    1.017 19.4%, 1.067 22.5%, 1.089 26%, 1.079 30.3%, 1.049 36%,
    1.024 42.6%, 1.011 50.3%, 1.004 59.2%, 1.001 69.3%, 1
  );
}

/* Any element classed split-N / subset-N / split-legend-N exposes its
   subset color as --subset; consumers read `var(--subset, fallback)` once
   instead of twelve per-index rules. (theme.js repeats this mapping for
   the shadow roots — document styles don't cross the shadow boundary,
   custom properties do.) */
.split-0, .subset-0, .split-legend-0 { --subset: var(--subset-0); }
.split-1, .subset-1, .split-legend-1 { --subset: var(--subset-1); }
.split-2, .subset-2, .split-legend-2 { --subset: var(--subset-2); }
.split-3, .subset-3, .split-legend-3 { --subset: var(--subset-3); }
.split-4, .subset-4, .split-legend-4 { --subset: var(--subset-4); }
.split-5, .subset-5, .split-legend-5 { --subset: var(--subset-5); }
.split-6, .subset-6, .split-legend-6 { --subset: var(--subset-6); }
.split-7, .subset-7, .split-legend-7 { --subset: var(--subset-7); }
.split-8, .subset-8, .split-legend-8 { --subset: var(--subset-8); }
.split-9, .subset-9, .split-legend-9 { --subset: var(--subset-9); }
.split-10, .subset-10, .split-legend-10 { --subset: var(--subset-10); }
.split-11, .subset-11, .split-legend-11 { --subset: var(--subset-11); }

/* ---------- Base ---------- */

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
/* No pull-to-refresh or vertical rubber band under a finger that is
   playing the dial (docs/stage-and-deck.md Phase 1). The horizontal axis
   keeps the browser's back swipe for now: Back is Decision 18's open
   question (it re-imports the pre-Apply set), not this phase's, and
   nothing in today's shell pans sideways at the page level. The score
   (notation.js) and the CT/census strips, which do scroll sideways,
   contain their own overscroll, so a swipe through them never chains
   into navigation. */
html, body { overscroll-behavior-y: none; }
body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--serif);
  font-size: 19px;
  line-height: 1.45;
  font-variant-numeric: tabular-nums;
  text-rendering: optimizeLegibility;
}
::selection { background: var(--accent); color: var(--paper); }
button, select { font: inherit; }
input { font: inherit; }
:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
a {
  color: var(--accent);
  text-decoration: none;

  &:hover { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: .15em; }
}
kbd {
  font-family: var(--mono);
  font-size: var(--text-caps);
  border: 1px solid var(--rule-2);
  padding: .05rem .32rem;
  background: var(--face);
  color: var(--ink-soft);
}

/* Listener-only chrome: one element carries one window keydown
   listener (page.gleam). Fixed with zero footprint — the busy-bar
   pattern — so it never becomes a grid item. */
.sel-keys, .escape-keys { position: fixed; width: 0; height: 0; }
/* The intent channel's settle effect host (page.gleam): a listener, never
   a participant in the shell grid. */
.settle-effect, .fold-actions { position: fixed; width: 0; height: 0; }
/* The event stream's host and the posting keys' host (docs/stage-and-deck.md
   §5, Phase 2): listeners with their own element so datastar-fetch names
   them, never grid items. */
#stream-host, .key-actions { position: fixed; width: 0; height: 0; }
/* Focus rescue's landmark target (tabindex -1): focusable from script so
   a keyboard user is not dropped at the top of the page, but never ringed
   as if it were a control. */
/* Focus rescue's sinks (app.js tcRescueFocus): landmarks, not controls. */
#main:focus, #deck-more-title:focus { outline: none; }

/* ---------- More card groups ---------- */

.control-group {
  margin: 0;
  padding: 0 0 var(--space-6);
  border: 0;
  border-bottom: 1px solid var(--rule);
  /* A fieldset's UA min-inline-size is min-content, which once let one
     non-wrapping chip row push the whole Display group past the sidebar. */
  min-inline-size: 0;
}
legend, .eyebrow {
  color: var(--ink-faint);
  font-size: var(--text-caps);
  letter-spacing: var(--caps-tracking);
  text-transform: uppercase;
}
legend { padding: 0; margin-bottom: .2rem; }
.eyebrow { margin: 0 0 .35rem; }

/* ---------- Buttons & chips ----------
   Words, not boxes (the quiet pass). Five states, one vocabulary:
     rest     — --ink-soft text, no border, no fill
     hover    — ink text over the key field (--key)
     active   — persistent server state: ink, bold, the 2 px bar beneath
     armed    — pending client preview: an accent ring and a faint accent
                tint (rhymes with the clock's ghost ring; see
                .transform-button.armed)
     disabled — --disabled-opacity, everywhere
   The one primary action is solid in red. */

.button-row, .chip-row { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: .6rem; }
.button, .chip {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--ink-soft);
  border-radius: 8px;
  padding: .34rem .7rem;
  min-height: var(--tap-min);
  font-family: var(--serif);
  font-size: 15px;
  line-height: 1.1;
  letter-spacing: .01em;
  cursor: pointer;
  transition: color .15s var(--ease-snap), background-color .15s var(--ease-snap);
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;

  &:hover:not(:disabled):not([aria-disabled="true"]) { color: var(--ink); background-color: var(--key); }
  &:disabled { opacity: var(--disabled-opacity); cursor: not-allowed; }
}
/* Trim the line box to cap height and baseline where the engine can, so
   a one-line label centres optically. */
@supports (text-box: trim-both cap alphabetic) {
  .button, .chip { text-box: trim-both cap alphabetic; }
}
/* The bar stops 8 px short of each side, inside the corner radius, so a
   chip's hover field and a segmented field's rounded ends never cut it. */
.chip.active {
  color: var(--ink);
  font-weight: 700;
  background-image: var(--bar);
  background-size: calc(100% - 16px) 2px;
  background-position: 50% 100%;
  background-repeat: no-repeat;

  &:hover:not(:disabled) { color: var(--ink); }
}
/* The Play as latch (docs/stage-and-deck.md §5): the segment a tap asked
   for, until the stream brings it — the pads' intent colour, as the bar
   in accent, never the truth's ink bar, which stays on the mode that is
   still true. */
.chip.latched {
  color: var(--ink);
  background-image: linear-gradient(var(--accent), var(--accent));
  background-size: calc(100% - 16px) 2px;
  background-position: 50% 100%;
  background-repeat: no-repeat;
}
.button.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--paper);

  &:hover:not(:disabled) {
    background: var(--accent-dark);
    border-color: var(--accent-dark);
    color: var(--paper);
  }
}
.button[href] { display: inline-flex; align-items: center; justify-content: center; text-decoration: none; }

/* ---------- The transport row (docs/stage-and-deck.md Phase 3) ----------
   Undo · Redo · Play as · Loop · Play, in the deck's Row B. Every target
   is 44 px. Loop and Play carry the latch bar under their label (once an
   LED strip along their top edge): hidden unlit, ink lit (§6 — lit means
   luminance), and the Play bar pulses on the beat (pulse.js writes its
   opacity). Play prints its tempo under its glyph. Static shell: nothing
   here is morphed. */
.transport {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1);
}
.transport .button, .transport .chip {
  position: relative;
  min-height: var(--tap-min-coarse);
  min-width: var(--tap-min-coarse);
  padding: .25rem .5rem;
}
.transport .play-as {
  display: flex;
  gap: var(--space-1);
  margin-inline: var(--space-2);
  padding-inline: var(--space-2);
  border-inline: 1px solid var(--rule);
}
.transport-led {
  position: absolute;
  left: 10px;
  right: 10px;
  bottom: 0;
  height: 2px;
  border-radius: 1px;
  background: transparent;
  pointer-events: none;
}
.transport-key.lit .transport-led { background: var(--ink); }
/* A key whose bar is its LED keeps its weight when lit: Loop never
   widens and shoves Play along the row. */
.transport-key.lit { color: var(--ink); }
.transport .transport-play { min-width: 3.4rem; }
/* Block-level, so the key's cap-height trim never reaches the column
   and the glyph over the tempo centres in the 44 px key. */
.transport-ink { display: flex; flex-direction: column; align-items: center; gap: 4px; line-height: 1; }
/* The glyph and the tempo are figures: Plex Mono. */
.transport-glyph { font: 13px/1 var(--mono); }
.transport-tempo { font: 11px/1 var(--mono); letter-spacing: 0; font-variant-numeric: tabular-nums; }
.transport-hint {
  flex-basis: 100%;
  margin: 0;
  font-size: var(--text-sm);
  color: var(--ink-soft);
}
.transport-hint[hidden] { display: none; }
/* A hint, never a target: a tap through it reaches what it overlays. */
.transport-hint { pointer-events: none; }
/* The tempo strip (Phase 3b): opened by a hold on Play, a row of its own
   above the transport keys — ◆ −5 · ♩120 · +5 · Tap. */
.transport {
  position: relative;
  /* Play takes a hold (the tempo strip): no callout or selection under a
     held finger. */
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
.tempo-strip {
  flex-basis: 100%;
  display: flex;
  align-items: center;
  gap: var(--space-1);
}
.tempo-strip[hidden] { display: none; }
.tempo-glyph { font-size: var(--text-sm); color: var(--ink-soft); }
.tempo-slider {
  font-family: var(--mono);
  min-width: 4.5rem;
  min-height: var(--tap-min-coarse);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 10px;
  background: var(--key);
  font-variant-numeric: tabular-nums;
  cursor: ew-resize;
  /* The drag is horizontal (2 px per bpm, Phase 6); a vertical pan and a
     pinch stay the browser's. */
  touch-action: pan-y pinch-zoom;
  user-select: none;
}
.tempo-slider:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
/* Hear and Listen (Phase 3b), under the dial: keys, 44 px. */
.dial-keys {
  display: flex;
  justify-content: center;
  gap: var(--space-1);
  margin-top: var(--space-2);
}
.dial-key {
  min-height: var(--tap-min-coarse);
  min-width: var(--tap-min-coarse);
}
.dial-key.lit { color: var(--ink); font-weight: 700; }
/* A chip whose target is the selection itself: "=" after its name. */
.arm-same { font-weight: 700; }
@media (forced-colors: active) {
  .transport-led { background: transparent; forced-color-adjust: none; }
  .transport-key.lit .transport-led { background: ButtonText; }
}
.button[aria-disabled="true"],
.chip[aria-disabled="true"] { opacity: var(--disabled-opacity); cursor: not-allowed; }

/* ---------- Disclosure summaries ----------
   Shared chrome for the sidebar <details> panels (room links, share,
   the View popover): a plain flex summary with a rotating ▸ in place of
   the browser marker. */

.room-link-summary, .share-summary,
.strings-details > summary, .instrument-details > summary {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  cursor: pointer;
  list-style: none;
  user-select: none;
  padding: .25rem 0;

  &::-webkit-details-marker { display: none; }
  &::after {
    content: "▸";
    margin-left: auto;
    color: var(--ink-faint);
    font-size: var(--text-caps);
    transition: transform .2s var(--ease-snap);
  }
}
:is(.room-link-control, .share-control, .strings-details, .instrument-details)[open] > summary::after { transform: rotate(90deg); }
/* The Score header's instrument line (the quiet pass): the settings
   folded behind their own read-back, the ▸ after it. */
.instrument-details > summary.chip { display: inline-flex; gap: var(--space-3); justify-content: flex-start; }
.instrument-details[open] > summary.chip { color: var(--ink); }
.instrument-details > .score-row-instrument { margin-top: var(--space-2); }
/* The summary stands in for the fieldset legend it replaced. */
.share-summary-label {
  color: var(--ink-faint);
  font-size: var(--text-caps);
  letter-spacing: var(--caps-tracking);
  text-transform: uppercase;
}

/* ---------- Room links ---------- */

.room-hint { margin: .3rem 0 .65rem; color: var(--ink-faint); font-size: var(--text-md); font-style: italic; }
.room-links { display: grid; gap: 1rem; margin-top: .65rem; }
.room-link { display: grid; gap: var(--space-2); }
.room-link-control { padding: 0; }
.room-link-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}
.room-link-label {
  color: var(--ink-faint);
  font-size: var(--text-caps-sm);
  letter-spacing: var(--caps-tracking);
  text-transform: uppercase;
}
.room-link-meta { color: var(--ink-faint); font-size: var(--text-sm); font-style: italic; margin: 0; }
.room-link-warning {
  color: var(--accent);
  font-size: var(--text-caps-sm);
  letter-spacing: var(--caps-tracking);
  text-transform: uppercase;
}
/* The copy/open UI (input + buttons) lives inside the <tc-share-link>
   shadow root (priv/static/share-link.js). */

/* ---------- Busy bar & status ---------- */

.app-shell[aria-busy="true"] { cursor: progress; }

.status-strip {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  padding: 0 0 var(--space-4);
  margin-bottom: var(--space-6);
  border-bottom: 1px solid var(--rule);
  font-size: var(--text-md);
  color: var(--ink-soft);

  strong {
    font-weight: 400;
    font-size: var(--text-caps);
    letter-spacing: var(--caps-tracking);
    text-transform: uppercase;
    color: var(--ink);
  }
  &.viewer .status-dot { background: var(--live); animation: status-pulse 2.4s ease-in-out infinite; }
}
.status-hint { color: var(--ink-faint); font-style: italic; }
.status-dot {
  align-self: center;
  flex: 0 0 auto;
  width: .5rem;
  height: .5rem;
  border-radius: 50%;
  background: var(--accent);
}
@keyframes status-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: .35; }
}

/* ---------- Workspace & masthead ---------- */

h1 {
  margin: .1rem 0 0;
  line-height: 1;
  font-family: var(--serif);
  font-size: clamp(2.1rem, 4.5vw, 2.9rem);
  font-weight: 700;
  letter-spacing: -.01em;

  &::after { content: "."; color: var(--accent); }
}
h2 {
  margin: 0 0 .7rem;
  font-family: var(--serif);
  font-size: var(--text-caps-lg);
  font-weight: 400;
  letter-spacing: var(--caps-tracking);
  text-transform: uppercase;
  color: var(--ink);
}
/* Provenance: the operation that created the current state (head history
   entry). Italic aside, never a control. */
.provenance { font-size: var(--text-sm); font-style: italic; color: var(--ink-faint); }

figure { margin: 0; }
.clock-section, .notation-section, .analysis-section, .fretboard-section, .piano-section {
  background: transparent;
  border: 0;
  padding: 0;
}
/* The settling affordance (docs/felt-latency.md F3): while a stream
   event's notation payload is still rendering server-side, the score
   host dims slightly. The 300ms delay applies only on ENTERING the
   settling state, so sub-300ms renders never flicker; clearing is
   immediate via the base transition. */
tc-notation { transition: opacity .15s ease; }
tc-notation[data-settling] { opacity: .75; transition-delay: .3s; }

/* ---------- Side panel tabs (notation / analysis) ---------- */

.side-tabs {
  display: flex;
  gap: var(--space-6);
  border-bottom: 1px solid var(--rule);
}
/* Side-tabs are a secondary register — italic Charter, ink underline —
   quieter than the letterspaced-caps labels elsewhere in the chrome. */
.side-tab {
  appearance: none;
  background: none;
  border: 0;
  margin: 0;
  padding: 0 0 .5rem;
  font-family: var(--serif);
  font-style: italic;
  font-size: var(--text-md);
  color: var(--ink-faint);
  border-bottom: 1px solid transparent;
  margin-bottom: -1px;
  cursor: pointer;

  &:hover { color: var(--ink); }
  &.active { color: var(--ink); border-bottom-color: var(--ink); }
}
/* The tab strip is the shared header; the panels' own headings would
   repeat it directly beneath (they stay in the DOM for aria-labelledby). */
.reader .notation-section > h2,
.reader .analysis-section > .section-head { display: none; }
.reader .notation-section,
.reader .analysis-section { margin-top: 1.1rem; }

/* ---------- Clock ---------- */

/* The stage stacks the decorative SVG and the twelve HTML pitch buttons;
   button centers are percentage offsets of the stage, which the SVG fills
   exactly, so they track the viewBox node points at any size. */
.clock-stage { position: relative; }
#clock { width: 100%; height: auto; display: block; pointer-events: none; }
.clock-buttons { position: absolute; inset: 0; }
.clock-face {
  fill: var(--paper);
  stroke: var(--ink);
  stroke-width: 1.5;
}
.edge { stroke: var(--subset, var(--accent)); stroke-width: 1.5; opacity: .7; }
/* Split marks are the traveling subset-color discs; stepping the split only
   rewrites each mark's rotation, so this transition glides them along the
   circle (clock.js keeps the angle on the shortest arc). */
.split-mark {
  transform-origin: 220px 220px;
  transform-box: view-box;
  transition: transform .35s cubic-bezier(.25, .8, .35, 1);

  circle { fill: var(--subset); stroke: var(--subset); stroke-width: 1; }
}
/* Sub-clocks (docs/voice-leading-cycles.md §12): one small clock per
   chord source the score moves between — a progression's chords, or the
   split's subsets in their colors — in a strip under the main clock. The
   one whose chord is sounding lights up (.sounding, from $voiceSource). */
.sub-clocks {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-3) var(--space-4);
  margin: .8rem 0 0;
}
.sub-clock {
  --sub: var(--subset, var(--ink-soft));
  margin: 0;
  min-width: 5.5rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .25rem;
  transition: opacity .25s ease-out;

  svg { width: 4.5rem; height: 4.5rem; display: block; }
}
/* A key since Phase 5 (Decision 6): the whole figure is the target, at
   rest it reads as the display it was; a chosen subset's key latches. */
.sub-clock-key {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .25rem;
  min-width: 44px;
  min-height: 44px;
  padding: var(--space-1) var(--space-2);
  border: 0;
  border-radius: 12px;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
  &:hover { background: var(--key); }
  /* Chosen: the field, and the bar in the subset's colour. */
  &.active {
    background: linear-gradient(var(--sub), var(--sub)) 50% 100% / calc(100% - 24px) 2px no-repeat, var(--key);
  }
  &:active { scale: .96; }
}
.sub-clock-caption {
  display: flex;
  flex-direction: column;
  align-items: center;
  line-height: 1.25;
  text-align: center;
}
.sub-clock-name { font-size: var(--text-sm); color: var(--ink-soft); }
.sub-clock-name, .sub-clock-detail { white-space: nowrap; }
.sub-clock-detail { font-family: var(--mono); font-size: var(--text-sm); color: var(--ink-faint); }
.sub-face { fill: none; stroke: var(--rule-2); stroke-width: 1.5; transition: stroke .2s ease-out, stroke-width .2s ease-out; }
.sub-tick { fill: var(--rule-2); }
.sub-edge { stroke: var(--sub); stroke-width: 2; opacity: .7; }
.sub-dot { fill: var(--sub); stroke: var(--paper); stroke-width: 1.5; }
/* While any chord sounds, the others step back and the sounding one
   draws its face in its color — the strip names which chord the
   travelling voices on the main clock belong to. */
.sub-clocks:has(.sub-clock.sounding) .sub-clock:not(.sounding) { opacity: .45; }
.sub-clock.sounding {
  .sub-face { stroke: var(--sub); stroke-width: 3; }
  .sub-clock-name { color: var(--ink); font-weight: 700; }
}
@media (prefers-reduced-motion: reduce) {
  .sub-clock, .sub-face { transition: none; }
}
/* Pitch buttons: node diameter is 40/440 of the viewBox (r=20), and the
   44px floor keeps the target WCAG-sized however small the clock renders.
   Selection is never color-alone: fill + border weight + bold label. */
.pc-button {
  position: absolute;
  /* The translate property, not transform: individual transforms apply
     translate, then scale, about the box's centre, so the hover grow and
     the press shrink stay centred on the node (stage.js measures the hit
     circle from the rect's centre). */
  translate: -50% -50%;
  /* The dial owns every touch on a pad (docs/stage-and-deck.md Decision
     2): no pan, no zoom, so a chord held with two fingers never turns
     into a pinch. A pinch between the pads still zooms. */
  touch-action: none;
  /* Three ring channels share one box-shadow, stacked first over last:
     the intent ring (wider, over everything while a tap is in flight),
     then the selected subset's steady ring, then playback's sounding ring
     — equal 3 px rings, so a sounding pad of the selected subset keeps its
     subset colour, as it did before the channels existed (S1). */
  --ring-pending: 0 0 #0000;
  --ring-sound: 0 0 #0000;
  --ring-sel: 0 0 #0000;
  box-shadow: var(--ring-pending), var(--ring-sel), var(--ring-sound);
  width: max(10%, 44px);
  aspect-ratio: 1;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--rule-2);
  background: var(--paper);
  color: var(--ink-soft);
  /* Pads as keys (docs/stage-and-deck.md §6, Phase 3b): the numeral in
     Plex Mono with tabular figures, the note name under it at 70%. */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  font-family: var(--mono);
  font-size: 15px;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  cursor: pointer;
  /* The authoritative fill arrives with a 120 ms colour fade (§4.3) —
     no View Transition masks it since Phase 1. The hover grow and the
     release ride the spring; rings settle on the Effect spring.
     Durations below are POSITIONAL against this list: background,
     border-color, color, scale, box-shadow. */
  transition: background .12s var(--ease-snap), border-color .12s var(--ease-snap),
    color .12s var(--ease-snap), scale .3s var(--spring-snappy),
    box-shadow var(--spring-effect-dur) var(--spring-effect);

  /* Hover only where a pointer hovers: a tap leaves no sticky grow. */
  @media (hover: hover) {
    &:hover { border-color: var(--ink); scale: 1.1; }
  }
  /* The press lands at once (.96), and the release springs back. */
  &:active { scale: .96; transition-duration: .12s, .12s, .12s, 0s, var(--spring-effect-dur); }
  /* Deliberate exception to --focus-ring: an accent outline is invisible
     on the accent-filled .selected button, so the dial rings in ink. */
  &:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
  &.selected {
    background: var(--accent);
    border: 2.5px solid var(--accent);
    color: var(--paper);
    font-weight: 700;
  }
  /* In split mode the fill comes from the .split-mark disc underneath; the
     button only keeps the subset-colored border (an invisible seam at
     rest, a colored ring when hover grows the button). :is() keeps the
     three-class specificity so that ring outranks the hover-ink rule. */
  &.selected:is(.split-0, .split-1, .split-2, .split-3, .split-4, .split-5, .split-6, .split-7, .split-8, .split-9, .split-10, .split-11) {
    background: transparent;
    border-color: var(--subset);
  }
  &.sounding { --ring-sound: 0 0 0 3px var(--accent); }
  /* A held pad listens (Phase 3b): a static ink ring from the 250 ms cue,
     in the intent ring's slot — a hold has given its intent back. */
  &.holding { --ring-pending: 0 0 0 2px var(--stage), 0 0 0 4.5px var(--ink); }

  /* Selection-first (S1): while a subset is the selected noun, its
     members carry a steady subset-colored ring and everything else on
     the dial steps back — the highlight travels with the object across
     clock, score, and analysis. */
  &.sel-focus { --ring-sel: 0 0 0 3px var(--subset, var(--accent)); }

  /* The intent channel (docs/stage-and-deck.md §5, §6 states): set by
     stage.js through $pendingOn/$pendingOff the instant a pitch is
     pressed, gone when the authoritative fill lands. Never colour alone:
     a "+" or "−" notch at 12 o'clock and an outer ring with a stage gap.
     Both forms (unselected and .selected) because an intent can point
     either way on either pad; after the split rule so a split member's
     intent shows. The entry is instant (0 durations), the retraction
     settles on the base transition. */
  &.pending-on,
  &.selected.pending-on {
    background: var(--intent-on);
    border: 2.5px solid var(--intent-on);
    color: var(--paper);
    font-weight: 700;
  }
  &.pending-off,
  &.selected.pending-off {
    background: var(--intent-off);
    border: 2.5px solid var(--accent);
    color: var(--ink);
    font-weight: 400;
  }
  &:is(.pending-on, .pending-off) {
    --ring-pending: 0 0 0 2px var(--stage), 0 0 0 4.5px var(--accent);
    transition-duration: 0s, 0s, 0s, .3s, 0s;
  }
  &:active:is(.pending-on, .pending-off) { transition-duration: 0s, 0s, 0s, 0s, 0s; }
  &:is(.pending-on, .pending-off)::before {
    position: absolute;
    left: 50%;
    top: -7px;
    translate: -50% -100%;
    font: 700 11px/1 var(--mono);
    color: var(--accent);
  }
  &.pending-on::before { content: '+'; }
  &.pending-off::before { content: '\2212'; }
  /* The focus outline would sit exactly on the intent ring: push it out. */
  &:focus-visible:is(.pending-on, .pending-off) { outline-offset: 6px; }
  /* A viewer's press is acknowledged, neutrally: their taps commit
     nothing, so no intent, no accent. */
  &[aria-disabled="true"] { cursor: default; }
  &[aria-disabled="true"]:active {
    --ring-pending: 0 0 0 2px var(--stage), 0 0 0 4.5px color-mix(in srgb, var(--ink) 45%, transparent);
  }

  /* Ghost preview: while a relation/transform control is hovered or
     focused, the clock shows the set it would produce. Kept tones get a
     heavy ring, incoming tones a dashed accent ring, dropped tones fade.
     After .sounding so a hovered preview outranks playback highlight. */
  &.preview:not(.selected) {
    border: 2.5px dashed var(--accent);
    background: color-mix(in srgb, var(--accent) 12%, var(--paper));
    color: var(--accent);
  }
  &.selected.preview { border: 3px solid var(--ink); }

  /* The armed transform's persistent ghost (its own channel, bound to
     $armedPcs): an accent dot below the node that stays put while a
     transient hover preview borrows the dashed-ring channel above. */
  &.armed::after {
    content: '';
    position: absolute;
    left: 50%;
    bottom: -11px;
    transform: translateX(-50%);
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--accent);
  }
}
.pc-button .pad-num { font-size: 15px; }
/* The root (Decision 23(a), Phase 11): its numeral underlined — a shape,
   never a colour alone, so forced colours keep it. */
.pc-button.root .pad-num {
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}
.pc-button .pad-name {
  font-size: 11px;
  opacity: .7;
  letter-spacing: .02em;
}
/* The dial's gestures (Phase 6) are one-finger drags — the twist from the
   annulus, the swipe from the centre — so a single finger on the dial is
   the dial's, never a page pan (which would cancel the pointer mid-twist);
   a pinch stays the browser's. Flow, one plain scroll, keeps its pans. Zoomed mode gives panning back
   (below). */
.app-shell:is([data-layout="sheet"], [data-layout="rail"], [data-layout="cards"], [data-layout="present"]) #clock-panel .clock-stage { touch-action: pinch-zoom; }
/* The wheel's scrub (Phase 6): the discs dim inside the viewfinder while
   it moves and until its settle lands, so the HUD's target reads first. */
.clock-stage.scrubbing :is(.clock-edges, .clock-split-marks) { opacity: .35; }
.clock-stage.previewing {
  .pc-button.selected:not(.preview) { opacity: .35; }
  .clock-edges { opacity: .35; }
  .clock-split-marks { opacity: .35; }
}

/* Touch hygiene on the instrument (docs/stage-and-deck.md Phase 1): no
   grey tap flash on the panel, and no callout or text selection under a
   held finger on the dial and the top bar. The callout is inherited, so
   it stays off the action surface: its text inputs keep iOS's
   long-press edit menu (§8.2: never on page bodies). */
#clock-panel, .deck-header, #stage {
  -webkit-tap-highlight-color: transparent;
}
.clock-stage, .sub-clocks, .deck-header, #stage {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
@media (forced-colors: active) {
  /* The latch keeps a mark where tints and shadows vanish: a dashed
     border, which no focus ring or active state uses. */
  .chip.latched { border: 2px dashed ButtonText; }
}
@media (forced-colors: active) {
  /* Box shadows vanish in forced colours; the intent keeps a ring of its
     own (double, so it never reads as the focus outline) and its notch. */
  .pc-button:is(.pending-on, .pending-off) {
    outline: 3px double ButtonText;
    outline-offset: 4px;
  }
}

/* Selection-first (S1): a dial with a selected subset dims the rest;
   the analysis card is a gesture surface and mirrors the focus ring. */
.clock-buttons:has(.sel-focus) .pc-button.selected:not(.sel-focus) {
  opacity: 0.45;
}
.half-card {
  cursor: pointer;
}
.half-card.sel-focus {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Selection-first (S2): the action surface — the selection's verbs,
   anchored under the clock. Rows reuse the rail grammar; the hairline
   sets it off from the dial without boxing it. */
.action-surface {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
/* Section grouping (mobile shell M2): the set and partition verb groups
   are wrapped structure. Each section is a column with the surface's own
   row gap, so desktop reads exactly as before; the phone media block
   adds hairline separators and air between sections. */
.surface-section {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* ---------- Score ---------- */

/* The score DOM and its styles live inside the <tc-notation> shadow root
   (priv/static/notation.js), including the dark-mode paper-tone override. */

.toolbar-row {
  display: grid;
  grid-template-columns: 4.6rem minmax(0, 1fr);
  gap: var(--space-1) var(--space-4);
  align-items: baseline;

  > :not(.toolbar-label) { grid-column: 2; }
}
.toolbar-label {
  color: var(--ink-faint);
  font-size: var(--text-caps-sm);
  letter-spacing: var(--caps-tracking);
  text-transform: uppercase;
}

/* ---------- Split nav & filters ---------- */

.splitnav {
  min-height: 2rem;
  margin-bottom: .8rem;

  > div { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-3); }
}
/* Piano and Fretboard are on/off, not a fourth and fifth style: a gap
   sets them apart from Staff | Tab | Staff + Tab. */
.score-row > .style-chip + .view-chip { margin-left: var(--space-5); }
.splitpos { color: var(--ink-faint); font-size: var(--text-sm); }
.split-jump {
  width: 5.5rem;
  border: 0;
  border-bottom: 1px solid var(--rule-2);
  background: transparent;
  color: var(--ink);
  padding: .3rem .45rem;
  font-size: var(--text-md);
  min-height: 2rem;
  border-radius: 0;
}
.split-shape-row { margin-top: .3rem; }
.filter-pill {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
  font-size: var(--text-md);
  color: var(--ink-soft);
}
.filter-pill-kind {
  color: var(--ink-faint);
  font-size: var(--text-md);
  font-style: italic;
}
.filter-pill-target { color: var(--ink); }
.filter-pill-clear { align-self: center; }
.splitnav-empty {
  margin: .35rem 0;
  color: var(--ink-faint);
  font-style: italic;
  font-size: var(--text-md);
}
.subset-filter-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-top: .6rem;
}
.filter-input {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  border-bottom: 1px solid var(--rule-2);
  background: transparent;
  color: var(--ink);
  padding: .35rem .55rem;
  font-family: var(--mono);
  font-size: var(--text-sm);
  min-height: var(--tap-min);
  border-radius: 0;
}

/* ---------- The stack as scale degrees (docs/voice-leading-cycles.md §13) ----------
   One card per chord (Billy, 2026-10-19: which controls belong together
   must read from where they sit). A hairline box, the tiles' edge,
   holds everything that acts on one chord, with more air between cards
   than inside one:
   - the header: the chord's colour (its sub-clock's), its name and its
     pitch classes, and at its trailing edge the keys that act on the
     whole chord, ‹ › and ×;
   - the bar of the scale's degrees 1..n: cells side by side, a held
     degree barred like any active chip, the root marked by a dot above
     its number. A lone template's root is a static cell that wears the
     same bar and dot but is never a control (and never dimmed).
     --degree-cols is written by the server: n cells on one line up to
     seven notes, ceil(n/2) columns from eight, so every chord wraps the
     same way and the columns stay aligned;
   - in a progression, the Root row: the held degrees as keys, the root
     pressed. */
.stack-chords {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin-top: var(--space-3);
}
.stack-chord {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 4px 8px 8px 12px;
  border-radius: 10px;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ink) 15%, transparent);
}
.stack-chord-head {
  display: flex;
  align-items: center;
  gap: .45rem;
  min-height: var(--tap-min);
}
/* The name and the pitch classes wrap inside their own box, so the
   chord's keys always stay on the header's line. */
.stack-chord-name {
  display: flex;
  flex: 1 1 auto;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 .7rem;
  min-width: 0;
}
.stack-chord-title {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--ink);
  font-size: 15px;
  font-weight: 700;
}
.stack-chord-actions {
  display: flex;
  flex: none;
  align-items: center;
  gap: 2px;

  & .toolbar-label { margin-right: 2px; }
  & .stack-remove { margin-left: var(--space-3); }
}
/* On the narrowest phones the word gives its room to the pitch classes;
   the keys still name themselves ("Chord 2 up a degree"). */
@media (width < 360px) {
  .stack-chord-actions .toolbar-label { display: none; }
}
/* At the chips' serif size ‹ › read as stray commas, so the chord's
   keys set their glyphs large. */
.stack-step, .stack-remove {
  min-width: var(--tap-min);
  padding-inline: .5rem;
  color: var(--ink);
  font-size: 1.35rem;
  line-height: 1;
}
.chord-pcs {
  font-family: var(--mono);
  font-size: var(--text-sm);
  color: var(--ink-faint);
}
/* The Root row reads as the bar does: the same mono numbers in the
   same cell width, so the root keys sit like a smaller bar under it. */
.stack-root-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 2px;

  & .toolbar-label { min-width: 2.6rem; }
  & .stack-root-key {
    min-width: var(--tap-min);
    padding-inline: .35rem;
    font-family: var(--mono);
  }
}
.degree-bar {
  display: grid;
  grid-template-columns: repeat(var(--degree-cols), minmax(var(--tap-min), max-content));
  width: max-content;
  max-width: 100%;

  & > * {
    position: relative;
    margin: 0;
    min-width: var(--tap-min);
    min-height: var(--tap-min);
    padding-inline: .35rem;
    font-family: var(--mono);
  }
  & .chip.active { font-weight: 700; }
  /* Cells paint in document order (a disabled cell's opacity makes its
     own stacking context), so the focused cell lifts above its
     neighbours or they paint over its ring. */
  & > :focus-visible { position: relative; z-index: 1; }
}
/* The root cell is on and never a control: the active chip's mark. */
.degree-root {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--ink);
  background: var(--bar) 50% 100% / calc(100% - 16px) 2px no-repeat;
  font-size: 15px;
  font-weight: 700;
  cursor: default;
}
/* The root's mark: a dot above its number, the one thing that tells it
   from the other held degrees (both wear the bar). It sits a fixed gap
   above the numeral's cap height (centred by the chip's text trim), so
   it never crowds the digit in a short cell or floats off it in a tall
   one. */
.degree-is-root::before, .degree-root::before {
  content: "";
  position: absolute;
  top: max(2px, calc(50% - 14px));
  left: calc(50% - 2px);
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: currentColor;
  pointer-events: none;
}
/* Add chord is the next card's empty place: a dashed outline where the
   chord it adds will sit. */
.stack-add {
  display: flex;
  justify-content: center;
  width: 100%;
  margin-top: var(--space-4);
  border: 1px dashed color-mix(in srgb, var(--ink) 30%, transparent);
  border-radius: 10px;
}

/* ---------- Legend, toolbar, empty states ---------- */

.split-legend {
  display: flex;
  gap: var(--space-5);
  justify-content: center;
  margin-top: .6rem;
  font-size: var(--text-sm);
  color: var(--ink-soft);
}
.split-legend-item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);

  &::before {
    content: "";
    display: inline-block;
    width: .65rem;
    height: .65rem;
    border-radius: 50%;
    background: var(--subset);
  }
}
/* ---------- Surface rows ----------
   The rail grammar survives as the action surface's row vocabulary
   (selection-first S3: the verb rail itself is gone). */
.rail-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);

  .button { padding: .3rem .7rem; min-height: 1.9rem; }
}
.rail-hint {
  margin: .5rem 0 0;
  font-size: var(--text-sm);
  font-style: italic;
  color: var(--ink-faint);
  max-width: 60ch;
}
/* The armed transform: visibly marked as a pending preview, not a state.
   The tint matches the clock's preview ghost so both read as the same
   pending thing; no bold — a weight change would jitter the button width
   the moment it arms. The grid chip it arms wears the same mark. */
.transform-button.armed,
.arm-grid .grid-chip.armed {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 8%, var(--paper));
  box-shadow: inset 0 0 0 1.5px var(--accent);
}
.rail-commit-row { margin-top: .6rem; }
.rail-commit[disabled] { opacity: var(--disabled-opacity); cursor: default; }
.rail-armed-status {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--accent);
}
.rail-armed-keys { color: var(--ink-faint); }
/* ---------- Query inputs ----------
   .dc-input is the query-field grammar the partition filter's kind
   select uses (the Decompose browser and its result cards were deleted
   with the /decompose route). */
.dc-input {
  font: inherit;
  font-size: 15px;
  padding: .25rem .45rem;
  border: 0;
  border-bottom: 1px solid var(--rule-2);
  border-radius: 0;
  background-color: transparent;
  color: var(--ink);
  width: 100%;
  /* Tap-target floor, matching the tuning selects and the .button/.chip
     convention. */
  min-height: var(--tap-min);
}
/* Selects are a word on a baseline rule, like the inputs: the native part
   is opted out (it once also dodged this Charter build's flipped
   descender, which clipped a native label — fonts/ now carries the
   corrected metrics) and the chevron is redrawn with two currentColor
   gradients, which follow --ink across light/dark. Forced colours drop
   gradients, so there the native select comes back (below). */
select.dc-input,
.tuning select,
.score-header select {
  appearance: none;
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position:
    calc(100% - .95rem) 50%,
    calc(100% - .6rem) 50%;
  background-size: .35rem .35rem;
  background-repeat: no-repeat;
  padding-right: 1.7rem;
}
input.dc-input { font-family: var(--mono, monospace); }
/* History list (vNext §2.4): a display list, newest first. The head is
   inked; older entries are plain text (navigating the tree is URL-only
   since the cut list). */
.history-list {
  list-style: none;
  margin: .55rem 0 0;
  padding: 0;
  font-size: var(--text-sm);
}
/* The history-reads-like-music rule, stated under the list: preferences
   change in place and never enter the tree. */
.history-hint {
  color: var(--ink-faint);
  font-size: var(--text-sm);
  font-style: italic;
  margin: .55rem 0 0;
}
.history-item { border-top: 1px solid var(--rule); }
.history-entry {
  display: block;
  width: 100%;
  text-align: left;
  padding: .25rem .1rem;
  color: var(--ink-faint);
}
/* The head carries an accent tick — a second, non-typographic signal on
   top of the ink/weight change (aria-current="step" in the markup). */
.history-entry.head {
  color: var(--ink);
  font-weight: 700;
  border-left: 2px solid var(--accent);
  padding-left: .35rem;
}
/* Canonical-text set editor under the clock: an equivalent input method
   for the twelve buttons, server-synced. Mono like the filter inputs so
   the 0-9te symbols read as notation, not prose. */
.set-editor { flex: 1 1 11rem; max-width: 16rem; }
.set-editor-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
/* The page is already titled Set: the label stays for assistive tech. */
.set-editor-label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.set-input {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  border-bottom: 1px solid var(--rule-2);
  background: transparent;
  color: var(--ink);
  padding: .35rem .55rem;
  font-family: var(--mono);
  font-size: var(--text-lg);
  letter-spacing: .12em;
  min-height: var(--tap-min);
  border-radius: 0;

  &[aria-invalid="true"] { border-color: var(--accent); outline-color: var(--accent); }
}
.clock-empty-hint {
  margin: .8rem auto 0;
  max-width: 22rem;
  text-align: center;
  color: var(--ink-faint);
  font-style: italic;
  font-size: var(--text-lg);
}
.empty-card {
  border: 0;
  background: transparent;
  padding: .2rem 0;
  color: var(--ink-faint);

  p { margin: 0; font-style: italic; font-size: var(--text-lg); line-height: 1.45; max-width: 44ch; }
}
.score-empty { width: 100%; }

/* ---------- Analysis ---------- */

.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  border-bottom: 1px solid var(--rule);
  padding-bottom: .45rem;
  margin-bottom: .4rem;

  h2 { margin: 0; }
  p { margin: 0; color: var(--ink-faint); font-style: italic; font-size: var(--text-md); }
}
.analysis-grid, .half-card dl, .almanac-card dl { margin: 0; display: grid; gap: 0; }
.analysis-grid { grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); column-gap: 2.5rem; }
.analysis-row {
  display: grid;
  gap: .05rem;
  padding: .5rem 0;
  border-bottom: 1px solid var(--rule);

  dt {
    color: var(--ink-faint);
    font-size: var(--text-caps-sm);
    letter-spacing: var(--caps-tracking);
    text-transform: uppercase;
  }
  dd { margin: 0; color: var(--ink); font-size: var(--text-lg); }
}

/* ---------- Relations: common-tone strip, Z-partner, inclusion ---------- */

.relations { margin-top: 1.6rem; }
.relations h3 {
  margin: 0 0 .55rem;
  font-family: var(--serif);
  font-weight: 400;
  font-size: var(--text-caps);
  letter-spacing: var(--caps-tracking);
  text-transform: uppercase;
  color: var(--ink-faint);
  border-bottom: 1px solid var(--rule);
  padding-bottom: .35rem;
}
/* On narrow panels the strip scrolls sideways instead of crushing its
   twelve columns below tap size. */
.ct-strip {
  display: grid;
  grid-template-columns: auto repeat(12, minmax(2rem, 1fr));
  align-items: center;
  margin-bottom: .9rem;
  border-bottom: 1px solid var(--rule);
  padding-bottom: .5rem;
  overflow-x: auto;
  /* A swipe through the strip never chains into back navigation. */
  overscroll-behavior-x: contain;
}
.ct-head,
.ct-rowlabel {
  font-size: var(--text-caps-sm);
  color: var(--ink-faint);
  letter-spacing: var(--caps-tracking);
  text-align: center;
}
/* Column heads keep the clock's lowercase t/e; only row labels are caps. */
.ct-rowlabel {
  text-transform: uppercase;
  text-align: left;
  padding-right: .8rem;
}
/* Each cell carries --ct (0–100, common tones as a percentage of the
   cardinality) from the server; the tint caps at 30% accent so the ink
   count stays readable. The counts remain text — the print stylesheet
   flattens the strip back to a plain table. */
.ct-cell {
  appearance: none;
  font: inherit;
  font-size: var(--text-md);
  color: var(--ink);
  background: color-mix(in srgb, var(--accent) calc(var(--ct, 0) * .3%), transparent);
  border: 1px solid transparent;
  border-radius: 0;
  padding: .28rem 0;
  text-align: center;
  cursor: pointer;
  /* background in the list so heatmap tints cross-fade when a patch
     reshades the strip. */
  transition: border-color .15s var(--ease-snap), color .15s var(--ease-snap),
    background .3s var(--ease-out);
}
.ct-identity { cursor: default; color: var(--ink-faint); background: transparent; }
.ct-invariant { color: var(--accent); font-weight: 700; border-color: var(--accent); }
.ct-zero { color: var(--ink-faint); }
.relation-row { border-bottom: 1px solid var(--rule); }
.relation-value { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.rel-chips { display: flex; flex-wrap: wrap; gap: .15rem .9rem; font-size: var(--text-lg); }
.rel-chip { white-space: nowrap; }
.rel-count { color: var(--ink-faint); font-size: var(--text-sm); }
.rel-none { color: var(--ink-faint); }
.rel-inventory-wrap { padding: .5rem 0; }
.rel-inventory-wrap .linklike { font-size: var(--text-sm); color: var(--ink-soft); }
.rel-inventory { margin-top: .7rem; display: grid; gap: var(--space-4); }
.rel-inventory-group {
  h4 {
    margin: 0 0 .3rem;
    font-family: var(--serif);
    font-weight: 400;
    font-size: var(--text-caps-sm);
    letter-spacing: var(--caps-tracking);
    text-transform: uppercase;
    color: var(--ink-faint);
  }
  > .rel-chips { display: grid; gap: var(--space-1); }
}
.rel-inventory-card {
  display: flex;
  align-items: baseline;
  gap: .15rem .9rem;
  padding: .25rem 0;
  border-bottom: 1px solid var(--rule);
}
.rel-card-label {
  flex: 0 0 4.2rem;
  font-size: var(--text-caps-sm);
  letter-spacing: var(--caps-tracking);
  text-transform: uppercase;
  color: var(--ink-faint);
}

.split-analysis {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 0 2.5rem;
  margin-top: 1.5rem;
}
/* ---------- The Almanac apparatus (docs/voice-leading-cycles.md §5) ----------
   One card per chord source under Emit: Cycles. Split cards carry a
   split-N class so the heading takes the subset color; the census strip
   reuses the CT-strip cell grammar with its own column count (inline
   grid-template-columns), and the emitted cycle's cell is outlined. */
.almanac { margin-top: 1.6rem; }
.almanac-card {
  margin-top: 1rem;

  h4 {
    margin: 0 0 .1rem;
    font-family: var(--serif);
    font-weight: 400;
    font-size: var(--text-caps);
    letter-spacing: var(--caps-tracking);
    text-transform: uppercase;
    color: var(--subset, var(--ink));
  }
}
.strand-lines { margin: .6rem 0 .4rem; }
.strand-line {
  display: flex;
  gap: .8rem;
  align-items: baseline;
  font-size: var(--text-md);
}
.strand-label {
  flex: none;
  font-size: var(--text-caps-sm);
  color: var(--ink-faint);
  letter-spacing: var(--caps-tracking);
  text-transform: uppercase;
}
.strand-pcs { font-family: var(--mono); overflow-wrap: anywhere; }
.census-strip { margin-top: .5rem; border-bottom: 0; padding-bottom: 0; }
.census-current { border-color: var(--accent); }
/* The Tune section (docs/tune.md §2, C5): one similarity card per join;
   the join leaving the focused step carries the accent rule, as the
   census strip outlines the emitted cycle. */
.tune-card.tune-current {
  border-left: 2px solid var(--accent);
  padding-left: .6rem;
  h4 { color: var(--accent); }
}
/* Canon text is case-sensitive (t = 10): the sets escape the heading's
   caps transform and take the set editor's face. */
.tune-card h4 .tune-sets {
  text-transform: none;
  letter-spacing: 0;
  font-family: var(--mono);
}
.almanac-card .provenance {
  margin: .3rem 0 0;
  font-size: var(--text-caps-sm);
  color: var(--ink-faint);
  letter-spacing: .02em;
}

.split-analysis-empty { margin-top: 1rem; }
.half-card {
  border: 0;
  background: transparent;
  padding: 0;

  /* Half-cards always carry a split-N class, so --subset is set. */
  h3 {
    margin: 0 0 .1rem;
    font-family: var(--serif);
    font-weight: 400;
    font-size: var(--text-caps);
    letter-spacing: var(--caps-tracking);
    text-transform: uppercase;
    color: var(--subset, var(--ink));
  }
}

/* ---------- Fretboard ---------- */

.fretboard-section { margin-top: 2.6rem; }
.fretboard-section.hidden { display: none; }

.piano-section { margin-top: 2.6rem; }
.piano-section.hidden { display: none; }
.tuning { margin: 0 0 1rem; display: flex; flex-direction: column; gap: var(--space-3); }
.tuning-presets-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); }
.capo-stepper { display: flex; align-items: center; gap: var(--space-2); }
.capo-stepper .eyebrow { margin: 0; }
.capo-step { padding: .1rem .55rem; min-height: 1.7rem; line-height: 1; }
.capo-value {
  min-width: 3.4rem;
  text-align: center;
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
}
.tuning label { display: grid; gap: .2rem; font-size: var(--text-caps-lg); color: var(--ink-faint); }
.tuning select {
  min-width: 4.5rem;
  border: 1px solid var(--rule-2);
  /* background-color, not the shorthand: the shared select rule above
     draws the chevron as a background-image. */
  background-color: var(--paper);
  color: var(--ink);
  padding: .35rem .45rem;
  min-height: var(--tap-min);
  border-radius: 0;
}
/* Fretboard SVG styles live inside the <tc-fretboard> shadow root
   (priv/static/fretboard.js); they consume the --fret- and --subset-
   custom properties defined on :root above. */

/* ---------- First-paint reveal ---------- */

@media (prefers-reduced-motion: no-preference) {
  /* Opacity only: a transform or filter on an ancestor of the fixed
     reader would break it (§8.4), so the reveal touches leaves only. */
  #clock-panel, .deck-header { animation: fade-in .4s var(--ease-out) backwards; }
  .deck-header { animation-delay: .06s; }
}
/* Blur-to-sharp reads as the page "focusing" into existence — a
   one-shot cost at first paint only. */
@keyframes settle {
  from {
    opacity: 0;
    transform: translateY(6px);
    filter: blur(4px);
  }
}
@keyframes fade-in {
  from { opacity: 0; }
}

@media (hover: none) and (pointer: coarse) {
  .button, .chip, .rail-row .button { min-height: var(--tap-min-coarse); padding: .55rem 1rem; }
  .degree-bar {
    grid-template-columns: repeat(var(--degree-cols), var(--tap-min-coarse));
  }
  .degree-bar > *, #action-surface .degree-bar .chip {
    min-width: var(--tap-min-coarse);
    min-height: var(--tap-min-coarse);
    padding: 0;
  }
  .stack-step, .stack-remove { min-width: var(--tap-min-coarse); }
  .stack-root-row .stack-root-key { min-width: var(--tap-min-coarse); padding-inline: .3rem; }
  .dc-input { min-height: var(--tap-min-coarse); }
  .ct-cell { min-height: 2.5rem; }
  /* Touch has no hover: a pressed tint is the acknowledgment (M2).
     An active chip keeps its bar (the shorthand would wipe it) and the
     primary button its fill (the tint would wash out its paper text). */
  .button:active:not(:disabled):not(.primary),
  .chip:active:not(:disabled):not(.active) {
    background: color-mix(in srgb, var(--ink) 8%, transparent);
  }
}

@media print {
  body { background: #fff; }
  .deck-header,
  .deck-tabs,
  .stage-corner,
  .stage-hud,
  .stage-unpark,
  .status-strip,
  .connection-banner,
  .toast,
  .help-overlay,
  .credits,
  button {
    display: none !important;
  }
  .app-shell, tc-deck, .deck-body, .reader { display: block; position: static; }
  #stage-camera { position: static; transform: none; }
  /* On paper there are no tabs: print both side panels in full. */
  .side-tabs { display: none !important; }
  .notation-section, .analysis-section { display: block !important; }
  /* Common-tone counts are content, not chrome — resurrect them from the
     blanket button hide (borderless so they print as a plain table), and
     print the full class inventory regardless of its on-screen state. */
  .ct-cell { display: revert !important; border: 0 !important; }
  .rel-inventory { display: grid !important; }
  .rel-inventory-wrap .linklike { display: none !important; }
}

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* ---------- Hero (first-visit landing) ----------
   The typographic poster shown at `/` before a session exists. Words
   are the visual: viewport-scale Charter, diagonal line flow, one
   brick-red period. Zero JavaScript — entry motion is the settle
   keyframe with a poster-paced stagger. */

.hero-page {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: var(--space-7) clamp(1.5rem, 6vw, 5rem) var(--space-6);
  overflow: hidden;
}
.hero-eyebrow {
  margin: 0;
  font-size: var(--text-caps);
  letter-spacing: var(--caps-tracking);
  text-transform: uppercase;
  color: var(--ink-faint);
}
.hero-heading {
  margin: 0;
  display: flex;
  flex-direction: column;
  font-size: clamp(3rem, 11vw, 12rem);
  font-weight: 700;
  letter-spacing: -.03em;
  line-height: .95;

  em { font-style: italic; font-weight: 400; }
  /* The masthead h1 rule appends a red period globally; the poster
     places its own inside line two. */
  &::after { content: none; }
}
/* Diagonal flow: line one anchors left, line two anchors right. */
.hero-line { display: block; }
.hero-line-2 { text-align: right; }
.hero-period { color: var(--accent); }
.hero-subtext {
  margin: var(--space-5) 0 0;
  max-width: 45ch;
  font-size: clamp(1rem, 1.25vw, 1.25rem);
  line-height: 1.6;
  color: var(--ink-soft);
}
.hero-cta-row { margin-top: var(--space-5); }
.hero-cta {
  font-size: var(--text-md);
  padding: .8rem 1.6rem;
  min-height: 2.75rem;
}
.hero-foot {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-4);
}
.hero-meta {
  margin: 0;
  font-family: var(--mono);
  font-size: var(--text-caps);
  letter-spacing: var(--caps-tracking);
  color: var(--ink-faint);
}
.hero-dial {
  width: clamp(80px, 9vw, 120px);
  height: auto;
  flex: 0 0 auto;

  circle { fill: var(--rule-2); }
  path { fill: none; stroke: var(--accent); stroke-width: 1.5; opacity: .5; }
}
@media (prefers-reduced-motion: no-preference) {
  .hero-eyebrow,
  .hero-center > *,
  .hero-foot { animation: settle .45s var(--ease-out) backwards; }
  .hero-center > *:nth-child(1) { animation-delay: .08s; }
  .hero-center > *:nth-child(2) { animation-delay: .2s; }
  .hero-center > *:nth-child(3) { animation-delay: .32s; }
  .hero-foot { animation-delay: .44s; }
}
@media (width <= 40rem) {
  /* Poster diagonals wrap awkwardly at phone widths: both lines left. */
  .hero-line-2 { text-align: left; }
}

/* ---------- Set-class pages (docs/set-class-pages.md) ----------
   The pages a search engine reads beside the hero: zero JavaScript, the
   hero's register, the Analysis tab's count-shaded strips and the deck's
   clocks. Every rule sits under .class-page or a class-* name, so nothing
   here reaches the workspace. */

.class-page {
  --sub: var(--ink);
  max-width: 68rem;
  margin: 0 auto;
  padding: var(--space-6) clamp(1rem, 5vw, 4rem) var(--space-7);

  .mono { font-family: var(--mono); }
}
.class-eyebrow {
  margin: 0;
  font-size: var(--text-caps);
  letter-spacing: var(--caps-tracking);
  text-transform: uppercase;
  color: var(--ink-faint);

  a { color: inherit; }
}
.class-head {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--space-5);
  align-items: end;
  margin: var(--space-5) 0 var(--space-6);
}
.class-number {
  margin: 0;
  font-size: clamp(3.5rem, 14vw, 10rem);
  letter-spacing: -.03em;
  line-height: .9;
}
.class-number-index { font-size: clamp(2.6rem, 9vw, 6.5rem); }
.class-name {
  margin: .25em 0 0;
  font-size: clamp(1.5rem, 4vw, 2.6rem);
  font-style: italic;
  line-height: 1.1;
}
.class-data {
  margin: var(--space-4) 0;
  font-family: var(--mono);
  color: var(--ink-soft);
  overflow-wrap: anywhere;
}
.class-note, .class-lede, .class-caption { max-width: 40rem; color: var(--ink-soft); }
.class-caption { margin: var(--space-3) 0 var(--space-4); }
.class-picture svg { --sub: var(--accent); width: min(34vw, 13rem); height: auto; }
.class-section {
  border-top: 1px solid var(--rule);
  padding: var(--space-5) 0;

  h2 {
    margin: 0 0 var(--space-4);
    font-size: var(--text-caps);
    font-weight: 400;
    letter-spacing: var(--caps-tracking);
    text-transform: uppercase;
    color: var(--ink-faint);
  }
  h3 { margin: var(--space-4) 0 var(--space-2); font-size: 1rem; font-weight: 400; font-style: italic; }
}
.class-shapes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: var(--space-4);
}
.class-shape {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-4);
  align-items: center;

  svg { width: 5.5rem; height: auto; }
  h3 { margin: 0; }
  p { margin: .2em 0; color: var(--ink-soft); }
}
.class-strip {
  border-collapse: separate;
  border-spacing: 2px;
  table-layout: fixed;
  width: 100%;
  max-width: 40rem;

  th { padding: 0 0 2px; font-size: .7rem; font-weight: 400; color: var(--ink-faint); }
  td {
    padding: .35rem 0;
    text-align: center;
    font-family: var(--mono);
    background: color-mix(in srgb, var(--accent) var(--ct, 0%), var(--key));
  }
  td.strong { color: var(--paper); }
}
.class-sets {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
  gap: var(--space-2);
  margin: 0 0 var(--space-4);
  padding: 0;
  list-style: none;

  a {
    display: block;
    padding: .45rem .6rem;
    border-radius: 6px;
    background: var(--key);
    color: var(--ink);
    text-decoration: none;

    &:hover { background: var(--key-down); }
  }
  small { display: block; font-size: var(--text-sm); color: var(--ink-faint); }
}
.class-facts {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: .5rem var(--space-5);
  margin: 0;

  dt { color: var(--ink-faint); }
  dd { margin: 0; }
}
.class-group { margin: 0 0 .3rem; }
/* A Forte number never breaks at its hyphen. */
.class-facts a, .class-nav a, .class-table a { white-space: nowrap; }
/* The index table scrolls inside its own box if a phone is still too
   narrow for it; the page itself never scrolls sideways. */
.class-table-wrap { overflow-x: auto; }
.class-group-size { color: var(--ink-faint); }
.class-foot {
  border-top: 1px solid var(--rule);
  padding-top: var(--space-5);
  font-size: var(--text-sm);
  color: var(--ink-soft);

  p { max-width: 44rem; }
}
.class-nav { display: flex; flex-wrap: wrap; gap: var(--space-4); margin-top: var(--space-4); }
.class-table {
  width: 100%;
  border-collapse: collapse;

  th, td { padding: .4rem .8rem .4rem 0; text-align: left; border-bottom: 1px solid var(--rule); }
  th {
    font-size: var(--text-caps);
    font-weight: 400;
    letter-spacing: var(--caps-tracking);
    text-transform: uppercase;
    color: var(--ink-faint);
  }
  td em { color: var(--ink-soft); }
}
@media (width <= 40rem) {
  .class-head { grid-template-columns: 1fr; }
  .class-picture svg { width: 9rem; }
  .class-facts { grid-template-columns: 1fr; gap: .2rem; }
  .class-facts dd { margin-bottom: var(--space-3); }
  .class-strip th { font-size: .55rem; }
  .class-strip td { font-size: .8rem; }
  .class-table .optional { display: none; }
  .class-table th, .class-table td { padding-right: .5rem; }
  .class-table th { letter-spacing: .04em; }
  .class-table .mono { font-size: .8rem; }
  .class-table td em { overflow-wrap: anywhere; }
}

/* ---------- Credits ---------- */

.credits {
  margin-top: 3rem;
  padding-top: 1rem;
  border-top: 1px solid var(--rule);
  font-size: var(--text-sm);
  color: var(--ink-faint);

  p { margin: .3rem 0; }
  a {
    color: inherit;
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: .15em;

    &:hover { color: var(--ink-soft); }
  }
}
.key-hint { letter-spacing: .01em; }

.linklike {
  border: 0;
  padding: 0;
  background: none;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: .15em;

  &:hover { color: var(--ink-soft); }
}

/* ---------- Inline input validation ---------- */

.filter-error {
  margin: .35rem 0 0;
  font-size: var(--text-sm);
  color: var(--accent);
}
.filter-input[aria-invalid="true"] {
  border-color: var(--accent);
  outline-color: var(--accent);
}

/* ---------- Connection-lost banner ---------- */

.connection-banner {
  position: fixed;
  inset: 0 0 auto;
  z-index: 150;
  padding: .45rem 1rem;
  text-align: center;
  font-size: var(--text-md);
  background: var(--accent);
  color: var(--paper);
}
.banner-retry {
  margin-left: .5rem;
  padding: .1rem .6rem;
  font: inherit;
  font-size: var(--text-sm);
  color: var(--paper);
  background: transparent;
  border: 1px solid var(--paper);
  border-radius: 2px;
  cursor: pointer;
}
.banner-retry:hover { background: rgb(255 255 255 / .15); }

/* ---------- Keyboard-help overlay ---------- */

.help-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  background: color-mix(in srgb, var(--paper) 88%, transparent);
}
.help-panel {
  background: var(--paper);
  border: 1px solid var(--rule-2);
  box-shadow: 0 2px 24px rgba(0, 0, 0, .08);
  padding: 1.6rem 2rem 1.8rem;
  max-width: 28rem;
  width: 100%;
  /* The vocabulary section makes the panel taller than small screens —
     the panel scrolls, the page behind it does not. */
  max-height: min(85vh, 85dvh);
  overflow-y: auto;
}
.help-section {
  margin: 1.2rem 0 .4rem;
  font-size: var(--text-caps);
  letter-spacing: var(--caps-tracking);
  text-transform: uppercase;
  color: var(--ink-soft);
}
/* Sound and touch (this device's feel settings): plain labelled inputs,
   one per row, each with a one-line hint. */
.feel-settings { border: 0; margin: 0; padding: 0; display: grid; gap: var(--space-3); }
.feel-row { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); align-items: baseline; }
.feel-row[hidden], .feel-option[hidden] { display: none; }
.feel-label { color: var(--ink); }
.feel-option { font-size: .9rem; color: var(--ink); min-height: var(--tap-min); }
.feel-option input { width: 1.1rem; height: 1.1rem; vertical-align: -.15rem; accent-color: var(--accent); }
.feel-hint { display: block; margin-left: 1.6rem; font-size: .8rem; color: var(--ink-soft); }
.feel-hint[hidden] { display: none; }
.feel-row > .feel-hint { flex-basis: 100%; margin-left: 0; }
/* The toast (toast.js, docs/stage-and-deck.md §5): one transient line at a
   time — a refusal, "= same set", the Steady touch offer, the silent-mode
   note. Out of flow, above the phone tab bar and clear of the home
   indicator; --toast-bottom is the one place Phase 3's transport row will
   raise it from. Under the help overlay (200), over the banners (150). */
.toast {
  --toast-bottom: calc(4.4rem + env(safe-area-inset-bottom));
  position: fixed;
  left: max(1rem, env(safe-area-inset-left));
  right: max(1rem, env(safe-area-inset-right));
  bottom: var(--toast-bottom);
  z-index: 160;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  max-width: 32rem;
  margin: 0 auto;
  padding: .7rem 1rem;
  background: var(--paper);
  border: 1px solid var(--rule-2);
  box-shadow: 0 6px 24px rgb(0 0 0 / .18);
  font-size: var(--text-md);
}
/* The box takes the pointer whole: it is opaque, so a tap on its text must
   never reach a control it hides, and a hover anywhere on it pauses its
   timer (toast.js). It is exactly as large as it looks (max-width, auto
   margins), and a touch scroll that starts on it still chains to the page. */
.toast[hidden], .toast .button[hidden] { display: none; }
.toast .toast-text { margin: 0; flex: 1 1 14rem; }
.toast .button { min-height: var(--tap-min-coarse); }
/* Glossary terms are words, not keys: italic, never key-caps. */
.help-glossary dt { font-style: italic; color: var(--ink); }
.help-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1rem;
  padding-bottom: .6rem;
  border-bottom: 1px solid var(--rule);

  h2 { font-size: 1.05rem; margin: 0; }
}
.help-grid {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-3) var(--space-5);
  margin: 0;
  align-items: baseline;

  dt { white-space: nowrap; }
  dd { margin: 0; font-size: .9rem; color: var(--ink-soft); }
}

/* ---------- State reveals ----------
   Blocks toggled by data-show (inline display flips) enter via
   @starting-style and exit via the discrete display transition:
   modern browsers fade/rise them, older browsers snap exactly as
   before, and the reduced-motion block disables all of it. Only
   signal-revealed blocks belong here — server-gated rows appear via
   morph, which is not this mechanism. */

.rail-armed-status {
  transition:
    opacity .3s var(--ease-out),
    translate .3s var(--ease-out),
    display .3s allow-discrete;
}
@starting-style {
  .rail-armed-status {
    opacity: 0;
    translate: 0 6px;
  }
}

.connection-banner {
  transition:
    opacity .3s var(--ease-out),
    translate .35s var(--spring-snappy),
    display .35s allow-discrete;
}
@starting-style {
  .connection-banner {
    opacity: 0;
    translate: 0 -100%;
  }
}

.help-overlay {
  transition:
    opacity .25s var(--ease-in-out),
    display .25s allow-discrete;
}
.help-overlay .help-panel {
  transition:
    scale .3s var(--spring-snappy),
    opacity .25s var(--ease-out);
}
@starting-style {
  .help-overlay { opacity: 0; }
  .help-overlay .help-panel {
    scale: .96;
    opacity: 0;
  }
}

/* Example sets on the empty workspace (vNext 2.13) and the Verovio
   render-failure fallback: the material stays readable as text. */
.example-sets {
  flex-wrap: wrap;

  & .example-set {
    text-decoration: none;
  }
}


/* ============================================================
   Stage and deck (docs/stage-and-deck.md Phase 4)
   ============================================================
   Two fixed layers plus out-of-flow chrome: the stage (main#main) holds
   the clock, the deck (<tc-deck>) holds the identity, the transport and
   the pages. deck.js measures everything and writes the layout
   (data-layout on the deck and the shell: cards | rail | sheet | flow),
   the detent, px custom properties and the camera; this sheet reads only
   those. Before the element upgrades — and in Flow — the page is one
   plain vertical scroll: stage, then deck. Nothing here writes on a
   morphed element, and no ancestor of the reader carries a transform,
   filter or containment where the reader is fixed (§8.4). */

html { background: var(--deck); }
body { background: var(--stage); }

/* ---------- Flow: the fallback and the smallest screens ---------- */

.stage-main { display: block; }
.stage {
  position: relative;
  padding: max(.5rem, env(safe-area-inset-top)) 16px 12px;
}
/* Never wider than the page: below 280 px the dial parks (deck.js), and
   an overflowing dial would widen a phone's layout viewport, which deck.js
   would then read as more room — a feedback loop. */
#stage-camera {
  width: clamp(264px, min(100vw - 32px, 100svh - 16px), 560px);
  width: var(--dial-size, clamp(264px, min(100vw - 32px, 100svh - 16px), 560px));
  max-width: 100%;
  margin: 52px auto 0;
  position: relative;
}
#clock-panel .clock-stage { width: 100%; }
#clock-panel :is(figcaption, .sub-clock-caption) { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
.stage-corner {
  position: absolute;
  top: max(.5rem, env(safe-area-inset-top));
  z-index: 12;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--ink-soft);
  font: 400 1.25rem/1 var(--serif);
  cursor: pointer;
  transition: scale .3s var(--spring-snappy), background-color .12s var(--ease-snap);
  &:hover { color: var(--ink); }
}
.stage-help { left: max(8px, env(safe-area-inset-left)); }
.stage-more { right: max(8px, env(safe-area-inset-right)); }
.stage-corner:active { scale: .92; background: var(--key); transition-duration: 0s, 0s; }
.room-pill {
  position: absolute;
  top: max(.5rem, env(safe-area-inset-top));
  left: 50%;
  translate: -50% 0;
  z-index: 11;
  margin: 0;
  padding: .3rem .8rem;
  border: 0;
  border-radius: 999px;
  background: none;
  font-variant: small-caps;
  letter-spacing: .04em;
  max-width: calc(100% - 120px);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  .status-hint { display: none; }
}
.stage-hud {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  pointer-events: none;
  font: italic 400 16px/1.2 var(--serif);
  color: var(--ink-faint);
}
.stage-hud-line:empty { display: none; }
.stage-unpark {
  position: fixed;
  left: var(--cam-x, 8px);
  top: var(--cam-y, 8px);
  z-index: 13;
  width: 48px;
  height: 48px;
  border: 0;
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
}
.stage-unpark[hidden] { display: none; }
/* The strip under the dial: the sub-clocks, the empty stage's examples or
   the plain split legend — shown or hidden, never faded at rest (§3.2). */
.stage-examples {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px 12px;
  margin-top: 12px;

  .button { padding: .4rem .9rem; }
}
#stage-camera[data-strip="hidden"] :is(.sub-clocks, .stage-examples, .split-legend) { visibility: hidden; }
tc-deck[data-moving] ~ * #stage-camera :is(.sub-clocks, .stage-examples, .split-legend),
.app-shell:has(tc-deck[data-moving]) #stage-camera :is(.sub-clocks, .stage-examples, .split-legend) { opacity: var(--strip, 1); }

tc-deck { display: block; background: var(--deck); border-top: 1px solid var(--rule-2); }
/* Header rows (§3.3): Row A at +12 to +60, Row B at +60 to +112, so the
   header is 112 px at default text (164 in the compact tier, where Play
   as wraps to Row C). Side padding p is 16 px, 12 px below 375 px. */
.deck-header {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0;
  padding: 12px 16px 0;
  container-type: inline-size;
  container-name: deck-head;
  touch-action: pinch-zoom;
}
.deck-body { position: relative; }

/* ---------- The fixed layouts ---------- */

.app-shell:is([data-layout="sheet"], [data-layout="rail"], [data-layout="cards"], [data-layout="present"]) {
  min-height: 100dvh;
  overflow: hidden;
  .stage { position: static; padding: 0; }
  .stage-corner { position: fixed; }
  .room-pill { position: fixed; }
  #stage-camera {
    position: fixed;
    left: var(--cam-x);
    top: var(--cam-y);
    width: var(--dial-size);
    margin: 0;
    transform-origin: 0 0;
    z-index: 2;
  }
}
/* The stage's top corners sit over the stage region, beside a deck card. */
.app-shell[data-layout="rail"],
.app-shell[data-layout="cards"] {
  .stage-help { left: calc(var(--deck-w, 0px) + 8px); }
}
.app-shell[data-layout="cards"] .stage-more { right: calc(var(--reader-w, 0px) + 8px); }
.app-shell[data-layout="cards"] tc-deck[data-reader-collapsed] ~ * .stage-more,
.app-shell[data-layout="cards"]:has(tc-deck[data-reader-collapsed]) .stage-more { right: 56px; }

/* The sheet (§3.3): a bottom sheet the full visible height, moved only by
   the transform deck.js writes; the body's height is written at rest. */
tc-deck[data-layout="sheet"] {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 20;
  height: var(--vh-px, 100dvh);
  max-width: 640px;
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  border-radius: 20px 20px 0 0;
  box-shadow: var(--lift-1);
  will-change: transform;
}
@media (width >= 430px) {
  tc-deck[data-layout="sheet"]:not([data-detent="full"]) { left: 8px; right: 8px; }
}
tc-deck[data-layout="sheet"][data-detent="full"] { border-radius: 12px 12px 0 0; }
tc-deck[data-layout="sheet"] .deck-body {
  flex: none;
  height: var(--deck-body-h, auto);
  overflow-y: auto;
  overscroll-behavior: contain;
}
tc-deck[data-layout="sheet"][data-detent="tip"] :is(#deck-tabs, .deck-pages, #reader) { display: none; }
/* Touch-action follows the detent (§3.3 table): at tip and half a
   vertical drag on a page moves the sheet; at full it scrolls. */
tc-deck[data-layout="sheet"]:is([data-detent="tip"], [data-detent="half"]) .deck-body { touch-action: pinch-zoom; }
tc-deck[data-layout="sheet"]:is([data-detent="tip"], [data-detent="half"]) :is(.chip-row, .tile-strip, .deck-tabs) { touch-action: pan-x pinch-zoom; }
tc-deck[data-layout="sheet"][data-detent="full"] .deck-body { touch-action: pan-y pinch-zoom; }
tc-deck[data-layout="sheet"][data-detent="full"] :is(.chip-row, .tile-strip) { touch-action: manipulation; }
/* The veil (§6): partial heights at rest show the stage through the
   deck, on ::before, opaque while it moves and under the user's
   contrast or transparency preferences. */
@supports (backdrop-filter: blur(1px)) {
  tc-deck[data-layout="sheet"]:not([data-detent="full"]):not([data-moving]) {
    background: transparent;
    &::before {
      content: "";
      position: absolute;
      inset: 0;
      z-index: -1;
      border-radius: inherit;
      background: var(--veil);
      backdrop-filter: blur(24px);
    }
  }
}
@media (prefers-contrast: more), (prefers-reduced-transparency: reduce) {
  tc-deck[data-layout="sheet"] { background: var(--deck) !important; }
  tc-deck[data-layout="sheet"]::before { display: none; }
}

/* The rail (§3.2): a full-height leading card, no detents. */
tc-deck[data-layout="rail"],
tc-deck[data-layout="cards"] {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  z-index: 20;
  width: var(--deck-w, 390px);
  display: flex;
  flex-direction: column;
  border-top: 0;
  border-right: 1px solid var(--rule-2);
  background: var(--paper);
  padding-top: env(safe-area-inset-top);
}
tc-deck:is([data-layout="rail"], [data-layout="cards"]) .deck-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* Collapsed to its chevron (deck.js's COLLAPSED_W, 48 px). In cards the
   reader is fixed but still inside .deck-body, so only the tabs and the
   pages hide there — never an ancestor of the reader (§8.4). */
tc-deck:is([data-layout="rail"], [data-layout="cards"])[data-collapsed] {
  width: 48px;
  :is(.deck-row, #deck-tabs, .deck-pages, .armed-status) { display: none; }
  .deck-header { padding-inline: 2px; }
}
tc-deck[data-layout="rail"][data-collapsed] #reader { display: none; }
/* Cards (§3.3 Rail and cards): the reader is its own trailing card,
   fixed — and no ancestor of it is transformed. */
tc-deck[data-layout="cards"] #reader {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 19;
  width: var(--reader-w, 552px);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: max(16px, env(safe-area-inset-top)) 20px 24px;
  background: var(--paper);
  border-left: 1px solid var(--rule-2);
  box-shadow: 0 8px 24px rgb(0 0 0 / .08);
}
tc-deck[data-layout="cards"][data-reader-collapsed] #reader {
  width: 48px;
  padding-inline: 0;
  #reader-body { display: none; }
}
.reader-toggle { display: none; }
tc-deck[data-layout="cards"] .reader-toggle {
  display: inline-grid;
  place-items: center;
  float: right;
}
tc-deck[data-layout="cards"][data-reader-collapsed] .reader-toggle { float: none; margin: 8px 2px; transform: scaleX(-1); }

/* ---------- Pages ---------- */

.deck-tabs {
  display: flex;
  overflow-x: auto;
  scrollbar-width: none;
  position: sticky;
  top: 0;
  z-index: 3;
  /* Opaque in every layout: the pages scroll under the tabs, and a
     transparent strip let a Voice page's chips read through them in the
     rail and the cards (mobile_test's sticky-chrome check). */
  background: var(--deck);
  border-bottom: 1px solid var(--rule);
}
.deck-tab {
  flex: 1 0 auto;
  min-width: 44px;
  min-height: 44px;
  appearance: none;
  border: 0;
  border-bottom: 2px solid transparent;
  background: none;
  font: 400 12.5px/1 var(--serif);
  letter-spacing: var(--caps-tracking);
  text-transform: uppercase;
  color: var(--ink-faint);
  cursor: pointer;

  &[aria-selected="true"], &[aria-current="location"] { color: var(--ink); font-weight: 700; border-bottom-color: var(--ink); }
  &[hidden] { display: none; }
}
.tab-mark, .tab-mark-text { display: none; }
tc-deck:has(#action-surface .filter-pill) #deck-tab-split {
  .tab-mark { display: inline; color: var(--accent); }
  .tab-mark-text { display: inline; }
}
.deck-pages, #reader { padding: 12px 16px 24px; }
@media (width < 375px) {
  .deck-header { padding-inline: 12px; }
  .deck-pages, #reader { padding-inline: 12px; }
}
tc-deck[data-layout="cards"] .deck-pages { padding-bottom: 48px; }
.deck-page {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
}
/* Disclosure by detent (docs/stage-and-deck.md §11.16): on a phone's
   sheet a page at half shows what you play against the dial, and full
   adds what you set up (typed fields, the filter, the chord editor,
   display settings) — [data-disclose="full"]. The cue, one quiet line at
   the end of the page ([data-disclose="half"]), names those rows and
   raises the deck. Without detents (rail, cards, present, flow), and on a
   sheet 600 px wide or more, whose half holds every row (deck.js's
   TABLET_W), every row shows and no cue does. The hide is !important so
   no row's own display rule can show it early. */
[data-disclose="half"] { display: none; }
@media (width < 600px) {
  tc-deck[data-layout="sheet"]:not([data-detent="full"]) [data-disclose="full"] { display: none !important; }
  tc-deck[data-layout="sheet"][data-detent="half"] .disclose-cue[data-disclose="half"] { display: inline-flex; }
  /* A page's cue is its last line, and at half a finger moves the sheet
     rather than the page, so a cue below the fold could never be seen:
     it sticks to the bottom of the page's view on paper, over whatever
     the fold cuts (the drag up reaches that too). The Score page's cue
     sits in its header, at the top, and stays a plain line. */
  tc-deck[data-layout="sheet"][data-detent="half"] .deck-page .disclose-cue[data-disclose="half"] {
    display: flex;
    position: sticky;
    bottom: 0;
    z-index: 1;
    align-self: stretch;
    padding-bottom: env(safe-area-inset-bottom, 0px);
    border-top: 1px solid var(--rule);
    border-radius: 0;
    background: var(--deck);
  }
}
.disclose-cue {
  align-self: flex-start;
  align-items: center;
  gap: .4em;
  min-height: 44px;
  margin: 0;
  padding: 0 .15rem;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--ink-faint);
  font: italic 15px/1.2 var(--serif);
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: color .15s var(--ease-snap);

  &:hover { color: var(--ink); }
  &:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
  .disclose-glyph { font-style: normal; }
}
.deck-page-title {
  margin: 0;
  font: 400 12px/1 var(--serif);
  letter-spacing: var(--caps-tracking);
  text-transform: uppercase;
  color: var(--ink-faint);
}
/* Tabbed: one page at a time, its heading repeating the tab (hidden). */
tc-deck[data-pages="tabs"] {
  .deck-page-title { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
  &:not([data-page="set"]) #deck-page-set,
  &:not([data-page="split"]) #deck-page-split,
  &:not([data-page="tune"]) #deck-page-tune,
  &:not([data-page="voice"]) #deck-page-voice,
  &:not([data-page="room"]) #deck-page-room { display: none; }
  &:not([data-card="more"]) #deck-more { display: none; }
  &[data-card="more"] :is(#deck-page-host, #deck-tabs) { display: none; }
}
tc-deck[data-pages="tabs"]:not([data-layout="cards"]):not([data-layout="present"]) {
  &[data-page="score"] #deck-page-host { display: none; }
  /* The pages' box itself, with More closed: its empty padding kept 36 px
     between the tabs and the score (§11.16 gives the score that room). */
  &[data-page="score"]:not([data-card="more"]) #deck-pages { display: none; }
  &:not([data-page="score"]) #reader,
  &[data-card="more"] #reader { display: none; }
}
/* Stacked (rail and cards with a tall body): every page in one scroll,
   each under a sticky 28 px heading; More last. */
tc-deck[data-pages="stacked"] {
  .deck-page + .deck-page { margin-top: 20px; padding-top: 12px; border-top: 1px solid var(--rule); }
  .deck-page-title, #deck-more .deck-card-head {
    position: sticky;
    top: 45px;
    z-index: 2;
    min-height: 28px;
    display: flex;
    align-items: center;
    background: var(--paper);
  }
  #deck-more { margin-top: 24px; padding-top: 12px; border-top: 1px solid var(--rule); }
  .deck-card-back { display: none; }
}
tc-deck[data-pages="stacked"]:not([data-layout="cards"]) #reader { display: none; }
tc-deck[data-pages="stacked"][data-read] #reader { display: block; }
tc-deck[data-pages="stacked"][data-read] .deck-pages { display: none; }

/* The More card (§3.3): history, share, room, sound, help, credits. */
.deck-card-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
/* ‹ is a small guillemet in Charter: set large on its key. */
.deck-card-back { min-width: 44px; min-height: 44px; }
.deck-card-head h2 { margin: 0; }
.deck-more { display: flex; flex-direction: column; gap: var(--space-5); }
.deck-more .control-group { border-bottom: 1px solid var(--rule); }
tc-deck[data-layout="sheet"][data-card="more"] #deck-more { animation: more-in .24s var(--ease-out); }
@keyframes more-in { from { transform: translateX(24px); opacity: 0; } }

/* ---------- The header (§3.3) ---------- */

.deck-grabber {
  position: absolute;
  top: -28px;
  left: 50%;
  translate: -50% 0;
  width: 120px;
  height: 44px;
  display: grid;
  place-items: end center;
  padding: 0 0 18px;
  border: 0;
  background: transparent;
  cursor: grab;
  z-index: 4;
  /* One finger drags the deck (deck.js); two still zoom the page. */
  touch-action: pinch-zoom;

  .grabber-bar {
    width: 36px;
    height: 5px;
    border-radius: 3px;
    background: color-mix(in srgb, var(--ink) 30%, transparent);
  }
  &.busy .grabber-bar {
    background: linear-gradient(90deg, var(--ink) 0 40%, color-mix(in srgb, var(--ink) 30%, transparent) 40%);
    background-size: 200% 100%;
    animation: busy-slide 1s linear infinite;
  }
}
@keyframes busy-slide { to { background-position: -200% 0; } }
tc-deck:is([data-layout="rail"], [data-layout="cards"]) .deck-grabber {
  position: static;
  translate: none;
  width: 44px;
  place-items: center;
  padding: 0;
  align-self: flex-end;
  margin: -8px -8px 0 0;

  .grabber-bar { width: 14px; height: 14px; border-radius: 0; background: none; border: solid var(--ink-soft); border-width: 0 2px 2px 0; rotate: 135deg; }
}
tc-deck[data-collapsed] .deck-grabber .grabber-bar { rotate: -45deg; }
.deck-row { display: flex; align-items: center; gap: 4px; min-height: 48px; }
.deck-row-a .deck-step {
  flex: 0 0 48px;
  min-height: 48px;
}
/* Without a partition the steppers leave the row, and the readout's
   edit word moves to its trailing edge. */
.deck-step.off { display: none; }

/* The readout (§6, the quiet pass): type on the deck's paper where the
   LCD's dark glass stood — the set in Plex Mono figures at 22 px, its
   name and line 2 in Charter. The set text never truncates; the Forte
   number drops first, the provenance ellipsizes. The whole readout is
   the edit (a viewer's info) key; its word sits at the row's trailing
   edge in letterspaced small caps. The class names stay (.lcd, .lcd-*):
   tests and stage.js read them. */
.lcd {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  min-height: 48px;
  display: flex;
  align-items: center;
  /* Row A's 48 px holds both lines: 22 px figures on a 1.05 line, 13 px
     words on 1.35, 2 px of air — the header stays 112 px (§3.3). */
  padding: 2px 64px 2px 0;
  color: var(--ink);
}
.readout {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
  font-family: var(--mono);
  color: var(--ink);
}
/* Clipped sideways only (the ellipsis), never vertically: a descender or
   an ascender may leave a line box without being cut. */
.lcd-line { display: block; white-space: nowrap; overflow: hidden; overflow: clip visible; text-overflow: ellipsis; }
.lcd-line-1 { display: flex; align-items: baseline; gap: 0; font-size: 22px; line-height: 1.05; }
.lcd-line-1 .pc-set { flex: none; color: var(--ink); }
/* The words on line 1 scale with its figures (22 px; 15 px in the
   compact tier), so the set always reads largest. */
.lcd-line-1 .pc-set-empty { font: italic calc(20em / 22)/1.05 var(--serif); color: var(--ink-soft); }
/* pre: the name's leading space survives as the flex item's first
   character, so the figures and " · 3-11" keep a word space. */
.lcd-line-1 .lcd-name { flex: 0 1 auto; min-width: 0; overflow: hidden; overflow: clip visible; text-overflow: clip; white-space: pre; color: var(--ink-soft); font: calc(17em / 22)/1.3 var(--serif); }
.lcd-line-2 { font: 13px/1.35 var(--serif); color: var(--ink-soft); }
.lcd-line-2 :is(.iv, .provenance) { color: var(--ink-soft); font-style: normal; font-size: 13px; }
.lcd-glyph { color: var(--ink-soft); }
.readout-open {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding: 0 2px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--ink-faint);
  cursor: pointer;
  font: 400 15px/1 var(--serif);
  font-variant: small-caps;
  text-transform: lowercase;
  letter-spacing: var(--caps-tracking);
  &:hover { color: var(--ink); }
}
.readout-open:focus-visible { outline-offset: 2px; }
.lcd-status { position: absolute; right: 64px; top: 6px; font: 12px/1 var(--mono); color: var(--ink); pointer-events: none; }

/* Row B, the transport (§3.3 tier table): one line in the regular and
   narrow tiers, Play as wrapping to Row C in compact. */
.deck-row-b.transport {
  flex-wrap: nowrap;
  gap: 8px 4px;
  min-height: 52px;
  padding-block: 4px;
}
.rowb-rest { display: contents; }
.deck-row-b .transport-key, .deck-row-b .chip {
  min-width: 44px;
  min-height: 44px;
  flex: 0 0 auto;
  padding: .2rem .4rem;
}
/* At W 358 (§3.3 tier table) the row is Undo, Redo, Play as (3 × 48 on
   one field), Loop and Play: 353 px in Charter, 5 px in hand, so no key
   overhangs the gutter on a 390 px phone. */
/* Play as: three words on one key field, the true mode barred. */
.deck-row-b .play-as {
  display: flex;
  gap: 0;
  margin: 0 2px;
  padding: 0;
  border: 0;
  border-radius: 10px;
  background: var(--key);
}
.deck-row-b .play-as .chip { min-width: 48px; width: 48px; padding-inline: 0; font-size: 14px; }
/* A viewer's Play as is a status line, not a control: no field. */
.deck-row-b .play-as.play-as-status { background: none; border-radius: 0; }
.deck-row-b .transport-loop { margin-left: auto; }
.deck-row-b .transport-play { min-width: 50px; }
.deck-row-b .transport-cancel { min-width: 64px; }
.deck-row-b .transport-apply {
  flex: 1 1 auto;
  min-width: 120px;
  background: var(--ink);
  color: var(--paper);
  font-weight: 700;
}
/* Only a shown Apply takes the free space (data-show hides it inline). */
.deck-row-b .transport-apply:not([style*="none"]) + .transport-loop { margin-left: 0; }
/* Apply keeps its ink through hover and press. */
#transport-apply:is(:hover, :active) { background-color: var(--ink); color: var(--paper); }
/* Route B's line floats above the deck (above Row A as well), never over
   the LCD. */
.transport-hint {
  position: absolute;
  bottom: calc(100% + 72px);
  right: 0;
  left: auto;
  flex-basis: auto;
  margin: 0;
  padding: .25rem .7rem;
  border-radius: 999px;
  background: var(--paper);
  box-shadow: 0 1px 8px rgb(0 0 0 / .08);
  font-style: italic;
  white-space: nowrap;
}
/* At full the deck covers the stage the hint points from. */
tc-deck[data-detent="full"] .transport-hint { display: none; }
.tempo-strip {
  position: absolute;
  bottom: calc(100% + 6px);
  right: 0;
  z-index: 6;
  width: auto;
  padding: 6px;
  border: 1px solid var(--rule-2);
  border-radius: 12px;
  background: var(--deck);
  box-shadow: var(--lift-1);
}
.armed-status { margin: 0; }
.armed-status:not([style*="none"]) {
  position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
@container deck-head (width < 358px) {
  .deck-row-b.transport { column-gap: 2px; }
  .deck-row-b .play-as { margin-left: 0; padding-right: 2px; }
  .deck-row-b .play-as .chip { min-width: 44px; width: 44px; font-size: 12px; padding-inline: 0; }
  .deck-row-b .transport-play { min-width: 44px; }
  .deck-row-b .transport-cancel { min-width: 56px; }
}
@container deck-head (width < 322px) {
  .deck-row-b.transport { flex-wrap: wrap; }
  .deck-row-b .play-as {
    order: 9;
    flex: 1 0 100%;
    margin: 0;
    border: 0;
    padding: 0;
    .chip { flex: 1 1 0; font-size: inherit; }
  }
  .deck-row-b .transport-cancel { min-width: 44px; }
  .deck-row-b .transport-apply { min-width: 112px; }
  .armed-status:not([style*="none"]) {
    position: static; width: auto; height: auto; overflow: visible; clip-path: none;
    order: 10; font-size: var(--text-sm); color: var(--ink-soft);
  }
  .lcd-line-1 { font-size: 15px; }
  .deck-row-a .deck-step { flex-basis: 44px; min-height: 44px; }
}

/* ---------- Keys, chips and the bar (§6) ----------
   A key is a word on the key field: no edge, no lip, Charter like every
   other word. The press lands at once (.96) and the release springs;
   that, and the sound, is what makes it a key. On is the bar beneath the
   label and a bold word — the latch strip's successor; a key with a
   .key-led or .transport-led draws its bar there, so pulse.js can still
   clock it to the beat. */

.key, .transport-key, .stage-examples .button, .deck-row-a .deck-step {
  border: 0;
  border-radius: 10px;
  background-color: var(--key);
  color: var(--ink);
  font-family: var(--serif);
  font-size: 15px;
  letter-spacing: 0;
  line-height: 1.1;
  transition: scale .3s var(--spring-snappy), background-color .12s var(--ease-snap);
}
.key:active:not(:disabled), .transport-key:active:not(:disabled), .deck-step:active:not(:disabled),
.stage-examples .button:active {
  background-color: var(--key-down);
  scale: .96;
  transition-duration: 0s, 0s;
}
@supports (text-box: trim-both cap alphabetic) {
  .key, .transport-key, .stage-examples .button, .deck-row-a .deck-step { text-box: trim-both cap alphabetic; }
}
.key.active, .key[aria-pressed="true"] {
  font-weight: 700;
  background-image: var(--bar);
  background-size: calc(100% - 20px) 2px;
  background-position: 50% 100%;
  background-repeat: no-repeat;
}
/* A key that draws its own bar never wears the background one too. */
.key:has(> .key-led, > .transport-led) { background-image: none; }
/* The armed transform is pending, not on: it never wears the truth's bar
   or weight (aria-pressed tells assistive technology it is engaged). */
.key.transform-button.armed { background-image: none; font-weight: 400; }
/* ‹ and › are small guillemets in Charter: the steppers (Row A's and the
   tune's) and the card's back key set them large, like the root row's. */
.deck-row-a .deck-step, .key.deck-card-back, .key.stepper { font-size: 26px; }
/* Hover, where a pointer can: the field darkens half a step toward the
   press (touch has the press alone). An armed key keeps its tint. */
@media (hover: hover) {
  :is(.key, .transport-key, .stage-examples .button, .deck-row-a .deck-step):hover:not(:disabled):not([aria-disabled="true"]):not(.armed) {
    background-color: color-mix(in srgb, var(--key) 50%, var(--key-down));
  }
}
.transform-keys {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(44px, 1fr));
  gap: 6px;
}
.transform-keys .key { min-height: 48px; padding: 2px; }
.transform-keys .key-sub { display: block; font: italic 12px/1.1 var(--serif); color: var(--ink-soft); }
/* The shape strip (Decision 27(a), Phase 10): one role=slider row under
   the Transform keys, 44 px detents, the current shape centred. The track
   sits at left 50% and slides by --shape-at detents; stage.js moves it
   while a drag lasts and the server's render re-centres it. Each cell
   names its shape in canon text, shrinking with its length so a 12-note
   name still fits 44 px. */
.shape-row { display: flex; align-items: center; gap: 8px; }
/* The root and mode stepping (Phase 11): ‹ Root D › ×. */
.root-row { display: flex; align-items: center; gap: 8px; }
.root-row .root-label {
  min-width: 6.5rem;
  text-align: center;
  font: 16px/1.2 var(--serif);
  color: var(--ink-soft);
}
/* ‹ and › are small guillemets in Charter: set large, on the key field. */
.root-row .root-step {
  min-width: 44px;
  min-height: 44px;
  background-color: var(--key);
  border-radius: 10px;
  color: var(--ink);
  font-size: 26px;
  text-decoration: none;
  transition: scale .3s var(--spring-snappy), background-color .12s var(--ease-snap);
  &:active:not([aria-disabled="true"]) { background-color: var(--key-down); scale: .96; transition-duration: 0s, 0s; }
}
.shape-glyph { font-size: 14px; color: var(--ink-faint); flex: 0 0 auto; }
/* No frame: the strip fades at its ends, a wheel seen edge-on, and the
   current detent is the bar beneath its name. The fade is a mask on the
   static window inside the slider, never on the slider itself: a mask
   clips everything outside its box, the focus ring with it. */
.shape-strip {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  height: 44px;
  border: 0;
  touch-action: pan-y pinch-zoom;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
}
.shape-strip:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.shape-strip::after {
  content: '';
  position: absolute;
  left: calc(50% - 16px);
  bottom: 4px;
  width: 32px;
  height: 2px;
  border-radius: 1px;
  background: var(--ink);
  pointer-events: none;
}
.shape-strip-window {
  position: absolute;
  inset: 0;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 18%, #000 82%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 18%, #000 82%, transparent);
}
.shape-strip-track {
  position: absolute;
  left: 50%;
  top: 0;
  height: 100%;
  display: flex;
  transform: translateX(calc((var(--shape-at, 0) + 0.5) * -44px));
}
.shape-strip[data-moving] .shape-strip-track { transition: none; }
.shape-strip:not([data-moving]) .shape-strip-track { transition: transform .12s var(--ease-out); }
.shape-cell {
  flex: 0 0 44px;
  width: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0;
  color: var(--ink-faint);
  overflow: hidden;
  white-space: nowrap;
}
.shape-cell[data-len="7"], .shape-cell[data-len="8"] { font-size: 8px; }
.shape-cell[data-len="9"], .shape-cell[data-len="10"] { font-size: 7px; }
.shape-cell[data-len="11"], .shape-cell[data-len="12"] { font-size: 6px; }
.shape-cell.current { color: var(--ink); font-weight: 700; }
.shape-cell.target { color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
  .shape-strip .shape-strip-track { transition: none; }
}
@media (forced-colors: active) {
  .shape-strip { border: 1px solid CanvasText; }
  .shape-strip::after { background: CanvasText; }
}
.arm-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(max(44px, calc((100% - 30px) / 6)), 1fr));
  gap: 6px;
}
.arm-grid .grid-chip { min-height: 44px; padding: 2px; background-color: var(--key); font-size: 14px; }
.arm-grid .ct { color: var(--ink-soft); font-size: 10px; }
.arm-grid .grid-chip.active .ct, .arm-grid .grid-chip.armed .ct { color: inherit; }
.set-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.set-row .set-editor { flex: 1 1 9rem; min-width: 0; }
.set-row .set-input { width: 100%; }
.text-key {
  border: 0;
  background: none;
  min-height: 44px;
  padding: 0 .4rem;
  color: var(--ink);
  text-decoration: underline transparent;
  text-underline-offset: .2em;
  &:active { text-decoration-color: currentColor; }
}
.dial-keys { margin: 0; gap: 6px; }
.dial-key { position: relative; }
.key-led {
  position: absolute; left: 10px; right: 10px; bottom: 0; height: 2px; border-radius: 1px;
  background: transparent;
  pointer-events: none;
}
.lit > .key-led { background: var(--ink); }
.visually-hidden-focusable:not(:focus):not(:focus-within) {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}
/* ---------- Tiles (§3.3, Phase 5) ----------
   A strip of keys that scrolls sideways (the drag rule's horizontal strip,
   app.css's touch-action rows): the Split page's subset tiles (104×64) and
   the Tune page's step tiles (72×64), each a mini-clock over its facts. */
.tile-strip {
  display: flex;
  flex-wrap: nowrap;
  gap: 6px;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
  padding: 2px 2px 6px;
  align-items: stretch;
}
.tile {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: 64px;
  padding: 4px 8px;
  border: 0;
  border-radius: 12px;
  background-color: var(--key);
  color: var(--ink);
  font-family: var(--mono);
  font-size: 11px;
  cursor: pointer;
  transition: scale .3s var(--spring-snappy), background-color .12s var(--ease-snap);
  &:active:not(:disabled) { background-color: var(--key-down); scale: .96; transition-duration: 0s, 0s; }
  svg { display: block; }
}
.tile, .sub-clock-key { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
.tile-facts { display: flex; align-items: center; gap: 4px; white-space: nowrap; }
.subset-tile {
  --sub: var(--subset, var(--ink-soft));
  width: auto;
  min-width: 104px;
  /* Chosen: the bar, in the subset's colour. */
  &.active {
    font-weight: 700;
    background-image: linear-gradient(var(--sub), var(--sub));
    background-size: calc(100% - 24px) 2px;
    background-position: 50% 100%;
    background-repeat: no-repeat;
  }
}
.subset-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--subset, var(--ink-soft));
  flex: none;
}
.tile .sub-dot, .tile .sub-edge { --sub: var(--subset, var(--ink-soft)); }
.tune-tile {
  width: 72px;
  .tile-facts { flex-wrap: wrap; justify-content: center; row-gap: 0; }
  .tile-set { color: var(--ink-soft); max-width: 60px; overflow: hidden; text-overflow: ellipsis; }
  &.current { background: var(--ink); color: var(--paper); border-color: var(--ink); }
  &.current .tile-set { color: inherit; }
  &.current .sub-face { stroke: var(--paper); }
  &.current .sub-tick { fill: color-mix(in srgb, var(--paper) 60%, transparent); }
  &.current :is(.sub-dot, .sub-edge) { --sub: var(--paper); }
}
/* The insertion caret: where Add step puts its copy, after the focused
   step. Ink, 3 px, the height of a tile. */
.tune-caret {
  flex: 0 0 3px;
  align-self: stretch;
  margin-inline: -2px;
  border-radius: 2px;
  background: var(--accent);
}
.tune-ghost {
  width: 72px;
  cursor: default;
  background: none;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ink) 15%, transparent);
}
.key-sub { font: italic 12px/1.1 var(--serif); color: color-mix(in srgb, currentColor 70%, transparent); }
.subset-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.subset-row .toolbar-label { display: inline-flex; align-items: center; gap: 6px; }
.subset-facts { font-family: var(--mono); font-size: var(--text-sm); color: var(--ink-soft); flex-basis: 100%; }
@media (forced-colors: active) {
  .tile { border: 1px solid ButtonText; }
  .tune-tile.current, .subset-tile.active { outline: 3px solid Highlight; outline-offset: -3px; }
  .tune-caret { background: Highlight; }
  .sub-clock-key.active { outline: 3px solid Highlight; outline-offset: -3px; }
  .subset-dot { forced-color-adjust: none; }
}
@media (prefers-reduced-motion: reduce) {
  .tile { transition: none; }
}
/* The latch is a key and the parts are words; air keeps them apart. */
#split-latch { min-width: 64px; margin-right: var(--space-4); }
.splitnav { display: flex; align-items: center; gap: 8px; }
.splitnav .split-jump { width: 5rem; min-height: 44px; }
.display-only { font-size: var(--text-sm); color: var(--ink-soft); font-style: italic; }
.score-header { display: flex; flex-direction: column; gap: 4px; margin-bottom: 8px; }
.score-header .score-row { margin-top: 0; align-items: center; }
.score-header select {
  max-width: 100%;
  min-height: var(--tap-min);
  font: 15px / 1.2 var(--serif);
  color: var(--ink);
  background-color: transparent;
  border: 0;
  border-bottom: 1px solid var(--rule-2);
}
.score-header .tuning-preset-select { flex: 1 1 14rem; min-width: 0; }
.score-header select { padding: .3rem .4rem; line-height: 1.3; height: auto; }
/* Beside a card the deck has no stage above it: the hint sits under Row B. */
tc-deck:is([data-layout="rail"], [data-layout="cards"], [data-layout="flow"]) .transport-hint {
  bottom: auto;
  top: calc(100% + 4px);
  z-index: 5;
}
/* Stacked sections scroll to sit below the sticky tabs, never under. */
tc-deck [data-deck-page], tc-deck #deck-more { scroll-margin-top: 52px; }
.strings-details { display: inline-block; }
.strings-details > summary.chip { display: inline-flex; align-items: center; }
.deck-page .chip-row { margin-top: 0; }

/* Coarse pointers: every deck control is a 44 px target. */
@media (pointer: coarse) {
  tc-deck :is(.button, .chip, select, input) { min-height: 44px; }
}

/* ---------- Stage under the deck: parking and zoom ---------- */

#stage-camera[data-parked] { pointer-events: none; }
#stage-camera[data-parked] :is(.sub-clocks, .stage-examples, .split-legend, .stage-hud, tc-dial-fx) { display: none; }
/* Parked the dial is 48 px: its pads are dots, unlabelled, so twelve
   44 px keys never pile up in the corner. */
#stage-camera[data-parked] .pc-button { width: 16%; min-width: 0; border-width: 1px; box-shadow: none; }
#stage-camera[data-parked] .pc-button :is(.pad-num, .pad-name) { display: none; }
/* Nor do the armed dots and preview rings, sized for 44 px pads. */
#stage-camera[data-parked] .pc-button::after { display: none; }
#stage-camera[data-parked] .pc-button.preview { border-width: 1px; }
.app-shell[data-zoomed] :is(#stage, tc-deck, .deck-header, .deck-body, #clock-panel .clock-stage) { touch-action: pan-x pan-y pinch-zoom; }
.app-shell[data-zoomed] .pc-button { touch-action: manipulation; }
.app-shell[data-zoomed] .stage-hud { display: none; }

/* The Read state (§3.3): the score at full, the header one 52 px row. */
tc-deck[data-read] {
  .deck-header { gap: 0; padding-block: 4px; }
  .deck-row { min-height: 44px; }
  .deck-row-b.transport { padding-block: 0; min-height: 44px; }
  .lcd { min-height: 44px; }
  .deck-header { flex-direction: row; flex-wrap: wrap; align-items: center; }
  .deck-row-a { flex: 1 1 10rem; min-width: 0; }
  .deck-row-a .lcd-line-2 { display: none; }
  .deck-row-b { flex: 0 0 auto; min-height: 44px; }
  .transport-apply, .transport-cancel { display: none !important; }
  .deck-tabs, .side-tabs, .score-header-host { position: static; }
}
/* The sheet's Read row drops Undo, Redo and Play as (one detent down);
   the rail's carries them (§3.3 Rail and cards). */
tc-deck[data-layout="sheet"][data-read] .rowb-rest { display: none !important; }
/* Parked at the sheet's full detent the mini-dial takes the top-leading
   corner, so Help docks beside More (§3.2). */
.app-shell[data-layout="sheet"]:has(#stage-camera[data-parked]) .stage-help {
  left: auto;
  right: calc(max(8px, env(safe-area-inset-right)) + 52px);
}
/* The rail's Read state: the card covers all but a 64 px trailing strip,
   which stacks More, Help and the parked mini-dial. */
.app-shell[data-layout="rail"]:has(tc-deck[data-read]) {
  .stage-more { right: max(10px, env(safe-area-inset-right)); top: max(8px, env(safe-area-inset-top)); }
  .stage-help { left: auto; right: max(10px, env(safe-area-inset-right)); top: calc(max(8px, env(safe-area-inset-top)) + 52px); }
}

/* The toast sits just above the deck header on the sheet, beside the
   deck card on wide screens, and never covers the header's controls. */
.app-shell[data-layout="sheet"] .toast { --toast-bottom: calc(var(--vh-px, 100dvh) - var(--deck-top, 100dvh) + 12px); }
.app-shell:is([data-layout="rail"], [data-layout="cards"]) .toast {
  left: calc(var(--deck-w, 0px) + 16px);
  --toast-bottom: max(16px, env(safe-area-inset-bottom));
}
/* Present: inside the stage column, above the docked header (never over
   its keys or the score card). */
.app-shell[data-layout="present"] .toast {
  left: 16px;
  right: calc(100vw - var(--deck-w, 576px) + 16px);
  --toast-bottom: calc(var(--deck-head, 112px) + 12px + env(safe-area-inset-bottom));
}

/* ---------- Forced colours and reduced motion (§6, §4.8) ---------- */

@media (forced-colors: active) {
  .key, .transport-key, .stage-corner, .deck-step, .tile, .chip, .button,
  .sub-clock-key, button.room-pill, .disclose-cue { border: 1px solid ButtonText; }
  /* Gradients drop here, the redrawn chevron with them: the native
     select comes back, boxed. */
  select.dc-input, .tuning select, .score-header select {
    appearance: auto;
    border: 1px solid ButtonText;
    padding-right: .45rem;
  }
  .key.active, .key[aria-pressed="true"], .transport-key.lit, .dial-key.lit, .chip.active, .subset-tile.active {
    border: 1px solid Highlight;
    border-bottom: 3px solid Highlight;
    font-weight: 700;
  }
  .transform-button.armed, .arm-grid .grid-chip.armed { border: 2px dashed ButtonText; }
  /* Box shadows vanish here, so each chord's card draws a real edge. */
  .stack-chord { border: 1px solid CanvasText; }
  /* The root's dot keeps its ink where backgrounds are forced away. */
  .degree-is-root::before { forced-color-adjust: none; background: ButtonText; }
  .degree-root::before { forced-color-adjust: none; background: CanvasText; }
  .deck-row-b .transport-apply {
    forced-color-adjust: none;
    background: Highlight;
    color: HighlightText;
    border-color: Highlight;
  }
  tc-deck { border-top: 1px solid CanvasText; }
  .key-led { background: transparent; forced-color-adjust: none; }
  .lit > .key-led { background: ButtonText; }
}
@media (prefers-reduced-motion: reduce) {
  .deck-grabber.busy .grabber-bar { animation: none; background: color-mix(in srgb, var(--ink) 70%, transparent); }
}

/* ---------- Rooms and Present (docs/stage-and-deck.md §7, Phase 7) ---------- */

/* A viewer's Row B and Score header are status text, not controls. */
.play-as-status {
  margin: 0;
  flex: 1 1 auto;
  align-self: center;
  min-width: 0;
  font: italic 15px/1.3 var(--serif);
  color: var(--ink-soft);
  white-space: nowrap;
  overflow: hidden;
  overflow: clip visible;
  text-overflow: ellipsis;
}
.score-status {
  margin: 0 0 var(--space-3);
  font: 15px/1.3 var(--serif);
  color: var(--ink-soft);
}

/* The presenter's pill is a key: Presenting · 12 connected. */
button.room-pill {
  min-height: 44px;
  font: inherit;
  font-variant: small-caps;
  color: inherit;
  background: var(--key);
  cursor: pointer;
  &[aria-disabled="true"] { cursor: default; }
  &:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
}
.room-pill .status-presence { color: var(--ink-soft); }

/* The banners are stage pills under the room pill (§7), keeping their
   ids and their copy. */
.connection-banner {
  inset: auto;
  top: calc(max(.5rem, env(safe-area-inset-top)) + 52px);
  left: 50%;
  translate: -50% 0;
  max-width: calc(100% - 32px);
  border-radius: 999px;
  box-shadow: var(--lift-1);
}
@starting-style {
  .connection-banner { translate: -50% -100%; }
}

/* Follow room sound's locked-audio chip, in Row A. */
.hear-room {
  flex: none;
  align-self: center;
  min-height: 44px;
  padding-inline: 12px;
  box-shadow: inset 0 0 0 1.5px var(--accent);
  color: var(--accent);
  font-weight: 700;
  &[hidden] { display: none; }
}

/* The Room page and the Present key: a key on its field, like every
   other key, at the start of its row. */
.present-control { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-2); }
.present-control .present-toggle { background-color: var(--key); padding-inline: 1rem; }
.room-present-note { color: var(--ink); }

/* A fresh home-screen launch (Phase 7): one line, standalone only. */
.hero-standalone { display: none; }
@media (display-mode: standalone) {
  .hero-standalone {
    display: block;
    margin: 0 auto var(--space-4);
    max-width: 32rem;
    text-align: center;
    color: var(--ink-soft);
  }
}

/* Present (§7): the stage takes the leading 45%, the deck shrinks to its
   header docked under it, and the reader is a fixed card on the trailing
   55%. deck.js writes --deck-w (the stage column) and --reader-w. */
.app-shell[data-layout="present"] {
  .stage-more { left: calc(var(--deck-w, 576px) - 52px); right: auto; }
  .room-pill { left: calc(var(--deck-w, 576px) / 2); }
  .connection-banner { left: calc(var(--deck-w, 576px) / 2); }
}
tc-deck[data-layout="present"] {
  position: fixed;
  left: 0;
  bottom: 0;
  z-index: 20;
  width: var(--deck-w, 576px);
  display: flex;
  flex-direction: column;
  padding-bottom: env(safe-area-inset-bottom);
  background: var(--paper);
  border-top: 1px solid var(--rule-2);
  transition: border-color .6s var(--ease-out);
  .deck-body { position: static; }
  #deck-tabs, .reader-toggle { display: none; }
  &:not([data-card]) .deck-pages { display: none; }
  #reader {
    position: fixed;
    top: 8px;
    bottom: 8px;
    right: 13px;
    z-index: 19;
    width: var(--reader-w, 678px);
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 16px 20px 24px;
    background: var(--paper);
    border: 1px solid var(--rule-2);
    border-radius: 12px;
  }
  /* More (a viewer's Room) opens as a card over the reader. */
  &[data-card] .deck-pages {
    position: fixed;
    top: 8px;
    bottom: 8px;
    right: 13px;
    z-index: 21;
    width: var(--reader-w, 678px);
    overflow-y: auto;
    overscroll-behavior: contain;
    background: var(--paper);
    border: 1px solid var(--rule-2);
    border-radius: 12px;
    box-shadow: var(--lift-1);
  }
}
/* The docked header's fade: after 3 s of stillness its hairline and key
   fields fall to 30%; text and controls keep full contrast. An armed
   Apply never fades: its ink field is what carries its paper label. */
tc-deck[data-layout="present"][data-faded] {
  border-top-color: color-mix(in srgb, var(--rule-2) 30%, transparent);
  :is(.key, .transport-key, .play-as):not(#transport-apply) {
    background-color: color-mix(in srgb, var(--key) 30%, transparent);
    transition: background-color .6s var(--ease-out);
  }
}
@media (prefers-reduced-motion: reduce) {
  tc-deck[data-layout="present"], tc-deck[data-layout="present"][data-faded] :is(.key, .transport-key, .play-as):not(#transport-apply) { transition: none; }
}

/* ---------- Light on the beat (docs/stage-and-deck.md §4.6, Phase 8) ---------- */

/* pulse.js writes these from the one musical phase, on the smallest static
   hosts of their readers (#deck-page-host and .stage-hud, never the whole
   shell, since each write restyles everything beneath it):
   --pulse-breath while a transform is armed and nothing plays (the armed
   key and the HUD's "● T5"), --pulse-blink while a wheel target waits.
   Opacity stays between 60% and 100%; unset, everything is steady. The
   Play LED's opacity is written on its own strip. */
.transform-button.armed,
.arm-grid .grid-chip.armed,
.stage-hud-line { opacity: var(--pulse-breath, 1); }
.app-shell:has(#clock-panel .clock-stage.scrubbing) .stage-hud-line { opacity: var(--pulse-blink, 1); }
@media (prefers-reduced-motion: reduce) {
  .transform-button.armed,
  .arm-grid .grid-chip.armed,
  .stage-hud-line { opacity: 1; }
  /* A queued wheel target is underlined instead of blinking (§4.8). */
  .app-shell:has(#clock-panel .clock-stage.scrubbing) .stage-hud-line {
    opacity: 1;
    text-decoration: underline;
    text-underline-offset: 3px;
  }
}
