/* Terminal-flavoured, but deliberately restrained: the glow lives on the
   chrome and the big readouts, never on text you have to read or type into. */

:root {
  --bg: hsl(220 25% 7%);
  --panel: hsl(220 22% 10%);
  --line: hsl(220 18% 20%);
  --fg: hsl(210 20% 86%);
  --dim: hsl(215 12% 52%);
  --green: hsl(140 65% 58%);
  --amber: hsl(38 92% 62%);
  --cyan: hsl(190 80% 62%);
  --red: hsl(0 72% 62%);
  /* Neutral surfaces: the inside of a control, and what it does on hover.
     Named rather than written inline so a palette only has to set these two
     and every input, button and dot follows. */
  --well: hsl(220 20% 14%);
  --hover: hsl(220 20% 18%);
  --backdrop: hsl(220 30% 4% / 0.7);
  --radius: 4px;
  --gap: 0.75rem;
  /* Set from the text-size slider. Everything else is rem or em, so this one
     number scales the whole app for a stand three metres from the stage. */
  --font-scale: 1;
  font-size: calc(16px * var(--font-scale));
}

* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--fg);
  font-family: 'Fira Code', 'SF Mono', ui-monospace, monospace;
  font-size: calc(14px * var(--font-scale));
  line-height: 1.5;
  /* Checkboxes and sliders are drawn by the browser, so without this they stay
     the platform blue no matter which palette is on — which looked like a bug
     in the settings dialog. Themes redefine --green, so this follows them. */
  accent-color: var(--green);
}

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

/* --- bars --- */
.bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap);
  padding: 0.5rem 1rem;
  border-bottom: 1px solid var(--line);
  background: var(--panel);
}
footer.bar { border-bottom: 0; border-top: 1px solid var(--line); font-size: 12px; }
.bar-right { display: flex; align-items: center; gap: 0.5rem; }

h1 {
  margin: 0;
  font-size: 15px;
  letter-spacing: 0.22em;
  color: var(--green);
  text-shadow: 0 0 12px color-mix(in srgb, var(--green) 40%, transparent);
}

h2 {
  margin: 0 0 0.5rem;
  font-size: 11px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--dim);
}

/* A heading with a right-justified counterpart on the same line — the set
   list heading carries the service name there in live mode. */
h2.split {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.75rem;
}

/* Inherits the heading's size, tracking and case, so the name reads as part
   of the heading line, not as a control. Only exists in live mode. */
#service-name-live {
  display: none;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: right;
  color: var(--fg);
}
body.live #service-name-live { display: block; }

/* --- layout --- */
main {
  flex: 1;
  display: grid;
  grid-template-columns: minmax(190px, 1fr) minmax(340px, 2fr) minmax(220px, 1.2fr);
  gap: var(--gap);
  padding: var(--gap);
  align-items: start;
}

.panel {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  min-width: 0;
}

/* --- controls: one instrument panel, so everything is the same size --- */
button, input, select, textarea {
  font: inherit;
  color: var(--fg);
  background: var(--well);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.35rem 0.55rem;
  min-height: 2.15rem;
}
/* A native select sizes its own box from the font and ignores most of the
   padding, so it came out visibly shorter than the inputs beside it. Dropping
   the native appearance and drawing the arrow here makes a select exactly the
   same box as an input — the "ramp out" row lines up, and so does every other
   dropdown in the app. */
select {
  appearance: none;
  -webkit-appearance: none;
  height: 2.15rem;
  padding-right: 1.6rem;
  background-image: linear-gradient(45deg, transparent 50%, var(--dim) 50%),
                    linear-gradient(135deg, var(--dim) 50%, transparent 50%);
  background-position: right 0.75rem center, right 0.5rem center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
select option { background: var(--panel); color: var(--fg); }
input:not([type='range']):not([type='checkbox']) { height: 2.15rem; }
textarea { resize: vertical; line-height: 1.45; height: auto; }
button {
  cursor: pointer;
  /* Without this, dragging a few pixels across a row of buttons selects their
     labels as text and swallows the click that should have followed. */
  user-select: none;
  -webkit-user-select: none;
  touch-action: manipulation;
}
button:hover:not(:disabled) { border-color: var(--dim); background: var(--hover); }
button:disabled { opacity: 0.4; cursor: not-allowed; }
:focus-visible { outline: 2px solid var(--cyan); outline-offset: 1px; }

button.ghost { background: none; padding: 0.25rem 0.5rem; font-size: 12px; }
/* A link that reads as a ghost button — sign-in is a navigation, not an action. */
a.button-link {
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.25rem 0.5rem;
  min-height: 2.15rem;
  box-sizing: border-box;
  font-size: 12px;
  color: var(--fg);
  text-decoration: none;
  user-select: none;
  -webkit-user-select: none;
}
a.button-link:hover { border-color: var(--dim); background: var(--hover); }
/* display:inline-flex beats the [hidden] attribute's UA display:none, so the
   nav sign-in link would show while signed in without this — the same trap the
   .field[hidden] rule exists for. */
a.button-link[hidden] { display: none; }
label.file { display: inline-flex; align-items: center; cursor: pointer; }
button.wide { width: 100%; }

/* --- library / set list --- */
.list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 42vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.list.ordered { counter-reset: item; }

.list li {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.3rem 0.45rem;
  border: 1px solid transparent;
  border-radius: var(--radius);
  cursor: pointer;
  /* Rows are dragged and shift-clicked; a drag that also sweeps a blue text
     selection across the list reads as a mistake. Wes asked for this. */
  user-select: none;
  -webkit-user-select: none;
}
.list li:hover { background: var(--well); }
.list li.active { border-color: var(--green); background: color-mix(in srgb, var(--green) 18%, var(--panel)); }
.list li .row-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.list li .row-top { display: flex; align-items: baseline; gap: 0.4rem; }
.list li .name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.list li .tempo { color: var(--dim); font-size: 12px; white-space: nowrap; font-variant-numeric: tabular-nums; }
/* Notes on the row: two lines at most, so one wordy song can't push the rest
   of the set off the screen. */
.list li .sub {
  font-size: 11px;
  line-height: 1.35;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.list li .note { color: var(--amber); opacity: 0.8; }

/* --- drag to reorder --- */
.list li[draggable='true'] { cursor: grab; }
.list li .grip { cursor: grab; font-size: 11px; letter-spacing: -1px; user-select: none; }
.list li.dragging { opacity: 0.4; cursor: grabbing; }
.list li.drop-before { box-shadow: inset 0 2px 0 var(--cyan); }
.list li.drop-after { box-shadow: inset 0 -2px 0 var(--cyan); }
.list li .row-btn {
  min-height: auto;
  padding: 0 0.3rem;
  background: none;
  border: none;
  color: var(--dim);
  font-size: 12px;
}
.list li .row-btn:hover { color: var(--red); background: none; }

/* Version groups: one row per title, opening into its arrangements. The
   indent and the rule down the left are what stop the open versions reading as
   three more unrelated songs in the list. */
/* Sized and boxed rather than left to the glyph: at 11px the triangle rendered
   small enough to read as a stray comma beside the title. */
.list li .twisty {
  width: 1em;
  flex: none;
  text-align: center;
  font-size: 13px;
  line-height: 1;
  user-select: none;
}
.list li.group-head .name { color: var(--fg); }
.list li.version-row {
  margin-left: 1.1rem;
  border-left: 2px solid color-mix(in srgb, var(--green) 40%, transparent);
}
.list li.version-row .name { color: var(--dim); font-size: 13px; }
.list.ordered li::before {
  counter-increment: item;
  content: counter(item);
  color: var(--dim);
  font-size: 11px;
  min-width: 1.2em;
}
.empty { padding: 0.5rem; color: var(--dim); font-size: 12px; }

/* --- transport --- */
.now-playing { display: flex; flex-direction: column; }
#now-name { font-size: 15px; color: var(--cyan); }
#now-meta, .now-line { font-size: 12px; }
/* The two double-click targets in the now-playing block. Dotted underline on
   hover rather than a button: it is text that happens to be editable, and it
   is read far more often than it is edited. */
#now-name, #now-artist { cursor: text; }
#now-name:hover, #now-artist:hover { text-decoration: underline dotted; }

/* An input standing in for the text it is over. It inherits the size and the
   colour of what it replaced, so the number does not jump when you start
   typing on it. */
.inline-edit {
  font: inherit;
  color: var(--fg);
  background: var(--well);
  border: 1px solid var(--cyan);
  border-radius: var(--radius);
  padding: 0 0.2em;
  width: 100%;
  max-width: 100%;
  text-align: inherit;
}
/* Editing the tempo happens *in* the big number, not in a small box beside
   where it was — same size, same amber, same tabular digits, with only a
   baseline underline to say it is now a field. Wes: "it should edit with the
   same theme it is in rather than reverting to a tiny text box". */
#bpm + .inline-edit {
  width: 4ch;
  text-align: center;
  font-size: clamp(2.6rem, 9vw, 4.2rem);
  line-height: 1.05;
  /* The shared control rules force every input to a 2.15rem box; without
     clearing both the big amber digits are cropped to a letterbox slot (Wes:
     "a tiny text window much shorter than the height of the text"). */
  height: auto;
  min-height: 0;
  color: var(--amber);
  font-variant-numeric: tabular-nums;
  background: transparent;
  border: none;
  border-bottom: 2px solid var(--cyan);
  border-radius: 0;
  padding: 0;
  outline: none;
  -moz-appearance: textfield;
  appearance: textfield;
}
#bpm + .inline-edit::-webkit-outer-spin-button,
#bpm + .inline-edit::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
/* The name and artist editors inherit the size and colour of the line they
   replace, so typing over them doesn't jump the layout. */
#now-name + .inline-edit { font-size: 15px; color: var(--cyan); }

/* A row picked out for a bulk delete. Distinct from .active, which means
   "this is the song that is loaded" — on a stage those two must never be
   confused, so selection is a fill and active stays the green outline. */
.list li.selected {
  background: color-mix(in srgb, var(--cyan) 22%, var(--panel));
  border-color: color-mix(in srgb, var(--cyan) 55%, transparent);
}
.list li.selected.active { border-color: var(--green); }

/* The artist under a library title. Clipped rather than wrapped: a second
   line per row halves how much of the library fits on a phone. */
.list .artist {
  display: block;
  max-width: 14ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tempo-display {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.5rem 0;
}
.bpm { display: flex; align-items: baseline; gap: 0.35rem; min-width: 6.5ch; justify-content: center; }
#bpm {
  font-size: clamp(2.6rem, 9vw, 4.2rem);
  line-height: 1;
  color: var(--amber);
  /* Toned down from 22px/35%: at 4.2rem the wider halo bled into the nudge
     buttons either side and softened the digits themselves. Wes asked for less. */
  text-shadow: 0 0 10px color-mix(in srgb, var(--amber) 20%, transparent);
  font-variant-numeric: tabular-nums;
}
.unit { color: var(--dim); font-size: 12px; }
.nudge { min-width: 2rem; color: var(--dim); }

.beats {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  justify-content: center;
  min-height: 1.5rem;
}
.beat-dot {
  width: 1.1rem;
  height: 1.1rem;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--well);
  transition: background 90ms linear, box-shadow 90ms linear;
}
.beat-dot.accent { border-color: var(--amber); }
.beat-dot.on { background: var(--green); box-shadow: 0 0 14px var(--green); }
.beat-dot.accent.on { background: var(--amber); box-shadow: 0 0 16px var(--amber); }

.transport { display: flex; gap: 0.5rem; justify-content: center; }
.ctl { min-width: 4rem; }
.ctl.play { min-width: 7rem; color: var(--green); border-color: color-mix(in srgb, var(--green) 55%, var(--line)); }
.ctl.play.playing { color: var(--red); border-color: color-mix(in srgb, var(--red) 55%, var(--line)); }
.position { text-align: center; font-size: 12px; font-variant-numeric: tabular-nums; }

/* The click is running and nothing is coming out. Red rather than amber: this
   is not information, it is the service currently failing. No `display` rule,
   deliberately, so the `hidden` attribute is enough to put it away — see the
   contract test that sweeps for exactly that mistake. */
.audio-stall {
  margin: 0.4rem 0 0;
  text-align: center;
  font-size: 12px;
  line-height: 1.4;
  cursor: pointer;
  color: var(--red);
  border: 1px solid color-mix(in srgb, var(--red) 55%, var(--line));
  padding: 0.35rem 0.5rem;
  background: color-mix(in srgb, var(--red) 12%, transparent);
}

/* The diagnostics readout. Monospace and pre-wrapped because it is meant to be
   read as a column of numbers and pasted into a message. */
.audio-health {
  margin: 0.4rem 0 0;
  font-size: 11px;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
  font-variant-numeric: tabular-nums;
}

/* --- settings form --- */
.settings {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 0.5rem;
  border-top: 1px solid var(--line);
  padding-top: var(--gap);
}
/* display:grid beats the hidden attribute. Both the "your version" and team
   blocks are .settings toggled with `hidden`, so without this they drew as
   empty bordered boxes on a deployment that doesn't bill — which is Wes's
   own tailnet copy, the one that must never mention money. */
.settings[hidden] { display: none; }
.settings label {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim);
  min-width: 0;
}
/* `inline` means "checkbox and its words on one line", and it has to mean that
   everywhere. It used to be defined only under `.settings` and `.mix-head`, so
   the same class on the Planning Center row silently did nothing: the label
   stayed `display: inline`, the checkbox baseline-aligned, and the tick sat a
   line above its own text. Defined here it applies wherever it is written. */
label.inline { display: flex; flex-direction: row; align-items: center; gap: 0.4rem; text-transform: none; }
.settings label.inline { flex-direction: row; align-items: center; gap: 0.4rem; text-transform: none; }
.settings label input:not([type='checkbox']),
.settings label select,
.settings label textarea { color: var(--fg); text-transform: none; letter-spacing: normal; }
.full { grid-column: 1 / -1; }
/* Every control in a settings row lines up along the bottom, whatever its
   label does. Wes sent a screenshot of "voice pitch (hz)" wrapping to two
   lines and dragging its own box a line lower than the two dropdowns beside
   it — a grid cell stretches to the tallest row, so the fix is to push the
   control to the foot of the cell rather than to shorten the words. */
.settings label > :last-child { margin-top: auto; }

/* A named block inside a fold, for the parts Wes asked to be folded together —
   recording the count under "sound & spoken count", recording the phrases
   under "spoken cues in this song". A rule above it does the work a separate
   <summary> used to. */
.fold-head {
  margin: 0.9rem 0 0;
  padding-top: 0.6rem;
  border-top: 1px solid var(--line);
  font-size: 11px;
  font-weight: normal;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
}

/* --- sound & spoken count --- */
/* Collapsed by default: it is set-up-once territory, not something you touch
   mid-service, so it should not push the song settings down the page. */
.sound {
  margin-top: var(--gap);
  border-top: 1px solid var(--line);
  padding-top: var(--gap);
}
.sound > summary {
  cursor: pointer;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim);
  list-style: none;
}
.sound > summary::-webkit-details-marker { display: none; }
.sound > summary::before { content: '▸ '; color: var(--cyan); }
.sound[open] > summary::before { content: '▾ '; }
.sound > summary:hover { color: var(--fg); }
.sound .settings { border-top: 0; }
.sound input[type='range'] { accent-color: var(--cyan); padding: 0; }
.sound label:has(input:disabled) { opacity: 0.45; }

/* --- recording your own count --- */
.hint { font-size: 11px; line-height: 1.5; margin: 0.5rem 0; }
.pack-head { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }
.pack-head input { flex: 1; min-width: 0; }
.pack-head span { font-size: 11px; white-space: nowrap; }

/* Two columns of six, so all twelve numbers are visible without scrolling. */
.pack-grid {
  display: grid;
  /* Wide enough that the nudge slider has usable travel; still two columns on
     a laptop, one on a phone. */
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 0.25rem;
}
.pack-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.15rem;
  padding: 0.15rem 0.35rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
/* The nudge takes the row's second line whole, so the slider gets the width. */
.pack-nudge {
  flex: 1 0 100%;
  display: flex;
  align-items: center;
  gap: 0.35rem;
  min-width: 0;
}
/* No centre tick: a range input's background is covered by the native track,
   and at zero the thumb sits on dead centre anyway — which is the "on the
   beat" cue, and the readout says it in words besides. */
.pack-nudge .nudge { flex: 1; min-width: 0; height: 1.1rem; }
.pack-nudge .nudge-val {
  min-height: auto;
  padding: 0 0.2rem;
  min-width: 5.2rem;
  background: none;
  border: none;
  color: var(--amber);
  font-size: 11px;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.pack-nudge .nudge-val:hover:not(:disabled) { color: var(--fg); background: none; }
.pack-nudge .nudge-val:disabled { opacity: 0.45; color: var(--dim); cursor: default; }
.pack-row.has-clip { border-color: var(--green); }
.pack-row .num { min-width: 1.4em; font-variant-numeric: tabular-nums; }
.pack-row .len { flex: 1; font-size: 11px; text-align: right; padding-right: 0.2rem; }
.pack-row .row-btn {
  min-height: auto;
  padding: 0 0.25rem;
  background: none;
  border: none;
  color: var(--dim);
  font-size: 12px;
}
.pack-row .row-btn:hover:not(:disabled) { color: var(--fg); background: none; }
.pack-row .row-btn:disabled { opacity: 0.3; cursor: default; }
/* Recording in progress: the one control that must be unmistakable. */
.pack-row .rec.on { color: var(--red); animation: blink 1s steps(2) infinite; }
@keyframes blink { 50% { opacity: 0.25; } }
.pack-actions { display: flex; gap: 0.4rem; margin-top: 0.5rem; flex-wrap: wrap; }
/* Cue rows carry a phrase rather than a single digit, so they want more room
   than a count-in row before they wrap to one column. */
.cue-pack-grid { grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); }
.pack-row .cue-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pack-row .cue-name + .len { flex: 0 0 auto; }
/* A button that reads as a link — the way out of a warning line, inline in it. */
.linkish {
  min-height: auto;
  padding: 0;
  background: none;
  border: none;
  color: inherit;
  font-size: inherit;
  text-decoration: underline;
}
.linkish:hover { background: none; color: var(--fg); }

.ts { display: flex; align-items: center; gap: 0.25rem; }
.ts input { width: 3.5rem; }
.slash { color: var(--dim); }

.accents { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.accents button {
  min-width: 2.2rem;
  min-height: 2rem;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
}
.accents button.on { color: var(--amber); border-color: var(--amber); background: color-mix(in srgb, var(--amber) 20%, var(--panel)); }

fieldset.ramp { border: 1px solid var(--line); border-radius: var(--radius); padding: 0.5rem; margin: 0; }
fieldset.ramp legend { padding: 0 0.3rem; font-size: 11px; }
.ramp-fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 0.5rem; }
.ramp-fields[hidden] { display: none; }
/* With the ramp off there is nothing under the legend, and the fieldset's own
   padding left an empty box sitting there looking broken. */
fieldset.ramp:has(.ramp-fields[hidden]) { padding-bottom: 0; border-color: transparent; }

.errors { margin: 0; color: var(--red); font-size: 12px; min-height: 1em; }
.errors:empty { display: none; }

.stack { display: flex; flex-direction: column; gap: 0.2rem; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--dim); }
.stack textarea { text-transform: none; letter-spacing: normal; color: var(--fg); }
.total { text-align: right; font-size: 12px; }

/* --- help --- */
dialog {
  background: var(--panel);
  color: var(--fg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  max-width: 30rem;
}
dialog::backdrop { background: var(--backdrop); }
.shortcuts { list-style: none; margin: 0 0 1rem; padding: 0; }
.shortcuts li { display: flex; justify-content: space-between; gap: 1rem; padding: 0.15rem 0; }
.shortcuts kbd {
  background: var(--hover);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 0 0.35rem;
  color: var(--cyan);
  font-size: 12px;
}

/* --- field groups that are not <label>s ---
   A <label> forwards its click to the first labelable thing inside it, which
   makes it wrong for any group containing buttons. These carry the same look
   without that behaviour. */
.field { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; }
/* `display: flex` beats the hidden attribute, so a .field the app hides —
   the account panel swaps whole blocks — would stay visible without this. */
.field[hidden] { display: none; }
.field .cap {
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim);
}

/* --- lists of people: the admin roster, and the team's members and invites.
   One rule set for both, because they are the same object drawn twice; the
   class is global rather than scoped to the admin panel so the team lists
   don't fall back to an unstyled inline list. --- */
.admin-roster,
.roster {
  list-style: none;
  margin: 0.4rem 0 0;
  padding: 0;
  max-height: 14rem;
  overflow-y: auto;
}
.admin-roster li,
.roster li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  padding: 0.35rem 0;
  border-bottom: 1px solid var(--line);
}
.admin-roster li.empty,
.roster li.empty { border-bottom: 0; color: var(--dim); }
.admin-roster .who,
.roster .who { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.admin-roster .who .hint,
.roster .who .hint { margin: 0; }
.admin-roster .email,
.roster .email { overflow-wrap: anywhere; }
.admin-roster .plan-admin,
.roster .plan-admin {
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--cyan);
}
/* The grant box: the email input takes the room the select and button leave. */
#admin-email { flex: 1; min-width: 10rem; }

/* --- teams (settings → general) --- */
/* The add-by-email box and the minted invite link both take the room the
   buttons beside them leave, the same way the grant box does. */
#team-add-email, #team-rename { flex: 1; min-width: 10rem; }
#team-invite-link { flex: 1; min-width: 12rem; }
/* An invite link is long and must not push the row wider than the dialog. */
#team-invite-row { flex-wrap: wrap; min-width: 0; }

/* --- links --- */
.links { display: flex; flex-direction: column; gap: 0.25rem; margin-bottom: 0.35rem; }
/* Stops "+ add link" stretching the full panel width and reading as the
   primary action of the whole page. */
#btn-add-link { align-self: flex-start; }
.link-row { display: flex; gap: 0.25rem; align-items: center; }
.link-row input:first-child { width: 7rem; flex: none; }
.link-row input:nth-child(2) { flex: 1; min-width: 0; }
.link-row .row-btn {
  min-height: auto;
  padding: 0 0.3rem;
  background: none;
  border: none;
  color: var(--dim);
}
.link-row .row-btn:hover:not(:disabled) { color: var(--fg); background: none; }

/* --- click mixer --- */
.mix-head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin: 0.5rem 0; }
.mix-head label.inline {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim);
}
/* A click voice, drawn as a channel strip on the same console as everything
   else — see .mixer-strip below, which this shares wholesale. Wes: "combine the
   click mixer with the general mixer... They should have the fader sort of
   graphic and also implement the volume level bar that is in the mixer."

   A little wider than a channel because a voice carries the name of the sound
   it plays, and "Pro Tools" clipped to "Pro To" is not a name. */
.click-voice { width: 4.6rem; }
/* A song that has its own sound gets a mark, so "why is this one different"
   has an answer you can see. */
.click-voice.overridden { border-color: var(--amber); }

/* The level fader, drawn rather than native: a vertical slot with rule marks
   and a console-style cap with a line across the grip. Every fader on the
   console wears it — the channels, master and the click voices — because two
   fader graphics in one panel is exactly the split this work removed. */
.mixer-fader input[type="range"] {
  appearance: none;
  -webkit-appearance: none;
  /* `writing-mode: vertical-lr` with `direction: rtl` is the standard way to
     get a vertical range with its minimum at the bottom; the old
     `-webkit-appearance: slider-vertical` is gone from current browsers.
     Falling back to a horizontal slider in anything that does not support it
     is a degradation, not a break — it still sets the level. */
  writing-mode: vertical-lr;
  direction: rtl;
  width: 2.2rem;
  /* The column decides the height, so one number moves every fader on the
     console and the meter beside it stays exactly as tall. */
  height: 100%;
  min-height: 0;
  touch-action: none;
  padding: 0;
  margin: 0;
  border: 0;
  background:
    /* the slot */
    linear-gradient(to right,
      transparent calc(50% - 2px),
      hsl(0 0% 0% / 0.55) calc(50% - 2px) calc(50% + 2px),
      transparent calc(50% + 2px)),
    /* rule marks either side of it, every 10% of travel */
    repeating-linear-gradient(to bottom,
      color-mix(in srgb, var(--dim) 55%, transparent) 0 1px,
      transparent 1px calc(10% - 0px));
  background-size: 100% 100%, 60% calc(100% - 0.9rem);
  background-position: center, center;
  background-repeat: no-repeat;
}
.mixer-fader input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 2rem;
  height: 0.9rem;
  border: 1px solid hsl(0 0% 0% / 0.7);
  border-radius: 2px;
  cursor: ns-resize;
  /* A ridged cap with the position line across the middle of the grip. */
  background: linear-gradient(to bottom,
    hsl(0 0% 34%), hsl(0 0% 16%) 42%,
    var(--fg) 47% 53%,
    hsl(0 0% 16%) 58%, hsl(0 0% 8%));
  box-shadow: 0 1px 3px hsl(0 0% 0% / 0.6);
}
.mixer-fader input[type="range"]::-moz-range-thumb {
  width: 2rem;
  height: 0.9rem;
  border: 1px solid hsl(0 0% 0% / 0.7);
  border-radius: 2px;
  cursor: ns-resize;
  background: linear-gradient(to bottom,
    hsl(0 0% 34%), hsl(0 0% 16%) 42%,
    var(--fg) 47% 53%,
    hsl(0 0% 16%) 58%, hsl(0 0% 8%));
  box-shadow: 0 1px 3px hsl(0 0% 0% / 0.6);
}
.mixer-fader input[type="range"]::-moz-range-track { background: none; }

/* The small trim under the fader: pitch, with a tiny cap. */
.click-voice .mix-mini {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.1rem;
  width: 100%;
}
.click-voice .mix-mini .cap { font-size: 9px; letter-spacing: 0.08em; color: var(--dim); }
/* Pitch: a small horizontal slider with no number beside it, per Wes — the
   hertz reading was not true of a sample, which is played back as a *rate*
   against its voice's preset pitch rather than at any frequency of its own.
   Left is lower, right is higher, and the scale is shared by every strip. */
.click-voice .pitch-slider {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  height: 0.85rem;
  min-height: 0;
  padding: 0;
  margin: 0;
  border: 0;
  background:
    linear-gradient(to bottom,
      transparent calc(50% - 1px),
      hsl(0 0% 0% / 0.55) calc(50% - 1px) calc(50% + 1px),
      transparent calc(50% + 1px));
  /* So a finger drag tunes rather than scrolling the settings dialog. */
  touch-action: none;
  cursor: ew-resize;
}
.click-voice .pitch-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 0.55rem;
  height: 0.85rem;
  border: 1px solid hsl(0 0% 0% / 0.7);
  border-radius: 2px;
  background: linear-gradient(to right,
    hsl(0 0% 34%), hsl(0 0% 16%) 40%,
    var(--fg) 46% 54%,
    hsl(0 0% 16%) 60%, hsl(0 0% 8%));
}
.click-voice .pitch-slider::-moz-range-thumb {
  width: 0.55rem;
  height: 0.85rem;
  border: 1px solid hsl(0 0% 0% / 0.7);
  border-radius: 2px;
  background: linear-gradient(to right,
    hsl(0 0% 34%), hsl(0 0% 16%) 40%,
    var(--fg) 46% 54%,
    hsl(0 0% 16%) 60%, hsl(0 0% 8%));
}
.click-voice .pitch-slider::-moz-range-track { background: none; }

/* A whole click sound under one name. The picker takes the room it needs —
   "Pro Tools marimba" clipped to "Pro Tools m" is two presets that read the
   same. */
.preset-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0.5rem 0;
  flex-wrap: wrap;
}
.preset-row select { flex: 1 1 10rem; min-width: 0; }
.click-voice .row-btn { min-height: auto; padding: 0 0.3rem; background: none; border: none; color: var(--dim); }
.click-voice .row-btn:hover { color: var(--fg); background: none; }
/* The tone picker sits above the pitch. It is a button carrying the current
   sound's name and opening the browser, not a dropdown: with eighteen sample
   packs behind it, a list of names you cannot hear is not a way to choose. */
.click-voice .tone-row { display: flex; width: 100%; gap: 0.1rem; align-items: center; }
.click-voice .mix-tone {
  flex: 1;
  min-width: 0;
  min-height: 0;
  padding: 0.15rem 0.1rem;
  font-size: 10px;
  /* One line: a two-word pack name must not make one strip taller than the
     rest and break the row of faders lining up. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* --- the click sound browser --- */
.sound-browser {
  width: min(46rem, 94vw);
  max-width: none;
  height: min(38rem, 88vh);
}
.sound-browser[open] { display: flex; flex-direction: column; gap: 0.5rem; }
.sb-head { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.sb-head h2 { margin: 0; flex: 0 0 auto; }
.sb-head #sb-search { flex: 1; min-width: 8rem; }
.sb-toolbar { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.sb-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  border: 1px solid var(--line);
  padding: 0.3rem;
}
.sb-group {
  margin: 0.5rem 0 0.2rem;
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--dim);
}
.sb-group:first-child { margin-top: 0.1rem; }
.sb-item { display: flex; align-items: stretch; gap: 0.2rem; }
.sb-row {
  flex: 1;
  min-width: 0;
  display: grid;
  grid-template-columns: minmax(6rem, 10rem) 1fr auto;
  gap: 0.6rem;
  align-items: baseline;
  text-align: left;
  background: none;
  border: 1px solid transparent;
  padding: 0.3rem 0.4rem;
  min-height: 0;
}
.sb-row:hover { border-color: var(--line); background: var(--panel); }
.sb-row.current { border-color: var(--accent); color: var(--accent); }
.sb-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sb-detail { font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sb-row.substituted .sb-detail { color: var(--amber); }
.sb-mark { font-size: 10px; letter-spacing: 0.1em; color: var(--accent); }
.sb-item .sb-hear {
  flex: 0 0 auto;
  min-height: 0;
  padding: 0 0.5rem;
  background: none;
  border: 1px solid transparent;
  color: var(--dim);
}
.sb-item .sb-hear:hover { color: var(--fg); border-color: var(--line); background: none; }
.sb-list .empty { color: var(--dim); padding: 0.6rem 0.4rem; }

/* At phone width the detail column has nowhere to go, so the row stacks and
   the name keeps its own line rather than being squeezed to two characters. */
@media (max-width: 560px) {
  .sb-row { grid-template-columns: 1fr auto; gap: 0.1rem 0.4rem; }
  /* Placed explicitly, not left to auto-flow: the detail spans both columns,
     which starts a new row, and "in use" would then land on a *third* line
     below its own name instead of beside it. */
  .sb-name { grid-area: 1 / 1; }
  .sb-mark { grid-area: 1 / 2; }
  .sb-detail { grid-area: 2 / 1 / 3 / -1; white-space: normal; }
}

/* --- context menu --- */
.ctx-menu {
  position: fixed;
  z-index: 50;
  min-width: 11rem;
  display: flex;
  flex-direction: column;
  padding: 0.2rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 8px 24px var(--backdrop);
}
.ctx-menu[hidden] { display: none; }
.ctx-item {
  background: none;
  border: none;
  border-radius: 3px;
  text-align: left;
  min-height: 1.9rem;
  padding: 0.25rem 0.5rem;
  font-size: 12px;
}
.ctx-item:hover:not(:disabled) { background: var(--hover); }
.ctx-item.danger { color: var(--red); }
.ctx-item:disabled { opacity: 0.35; }

.hint-inline { font-size: 10px; letter-spacing: 0.06em; text-transform: none; }
.sig { display: block; font-variant-numeric: tabular-nums; }
.wide-ctl { flex: 1; max-width: 17rem; font-size: 12px; color: var(--dim); }
#btn-live.on { color: var(--amber); border-color: var(--amber); }

/* --- live mode ---
   Sunday morning: the set list and the transport, nothing that edits a song.
   Hidden rather than shrunk, so a stray tap can't retune the next song. */
body.live #panel-library,
body.live #song-settings,
body.live #details-song,
body.live .sound,
body.live #btn-export,
body.live #wrap-service-notes,
body.live .service-bar,
body.live #set-actions,
body.live label.file { display: none; }
/* ...except the mixer, which is the one `.sound` block that is a *performance*
   control rather than an editing one. "The click is too loud in my ears" and
   "say something to the band" are Sunday-morning problems, and talkback that
   is only reachable by leaving live mode is talkback nobody will reach for
   mid-service. An id beats the `.sound` class rule above on specificity. */
body.live #mixer-box { display: block; }
body.live main { grid-template-columns: minmax(340px, 1fr) minmax(280px, 1fr); }
body.live .list { max-height: 60vh; }
body.live .list li { padding: 0.5rem 0.55rem; }
body.live #bpm { font-size: clamp(3.4rem, 12vw, 5.5rem); }

@media (max-width: 900px) {
  body.live main { grid-template-columns: 1fr; }
  main { grid-template-columns: 1fr; }
  .list { max-height: 30vh; }
}

/*
 * On a phone the title plus the four header buttons need about 433px, and a
 * layout wider than the viewport makes the browser shrink-to-fit: the whole app
 * renders zoomed out and scrolls sideways. Letting the bar wrap keeps the page
 * exactly as wide as the screen.
 */
@media (max-width: 480px) {
  .bar { flex-wrap: wrap; row-gap: 0.4rem; padding: 0.5rem 0.6rem; }
  .bar-right { flex: 1 1 100%; justify-content: flex-end; }
}

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

/* --- themes ---------------------------------------------------------------
   Each palette sets the same handful of custom properties and nothing else.
   --green is the primary accent (playing, active, the logo) and --amber the
   secondary (tempo, accents, notes); they keep those names because that is
   what they are in the default terminal palette, and renaming them across the
   file would have made this a much riskier change than it needs to be. */

/* The default palette also gets an explicit class rather than relying on the
   :root values alone, so every theme in the picker is one rule you can find. */
body.theme-terminal {
  --bg: hsl(220 25% 7%);
  --panel: hsl(220 22% 10%);
  --line: hsl(220 18% 20%);
  --fg: hsl(210 20% 86%);
  --dim: hsl(215 12% 52%);
  --green: hsl(140 65% 58%);
  --amber: hsl(38 92% 62%);
  --cyan: hsl(190 80% 62%);
  --well: hsl(220 20% 14%);
  --hover: hsl(220 20% 18%);
}

body.theme-amber {
  --bg: hsl(28 30% 6%);
  --panel: hsl(28 24% 9%);
  --line: hsl(30 20% 22%);
  --fg: hsl(38 40% 86%);
  --dim: hsl(32 16% 55%);
  --green: hsl(38 92% 62%);
  --amber: hsl(48 96% 70%);
  --cyan: hsl(22 80% 64%);
  --well: hsl(28 22% 13%);
  --hover: hsl(28 22% 18%);
}

body.theme-ice {
  --bg: hsl(215 40% 7%);
  --panel: hsl(214 34% 11%);
  --line: hsl(210 26% 24%);
  --fg: hsl(200 30% 90%);
  --dim: hsl(205 18% 58%);
  --green: hsl(190 85% 62%);
  --amber: hsl(215 90% 72%);
  --cyan: hsl(165 70% 60%);
  --well: hsl(214 30% 15%);
  --hover: hsl(214 30% 20%);
}

/* Flat, maximum contrast, no colour that depends on ambient light. For a dark
   stage and a screen someone is reading at a distance and at an angle. */
body.theme-stage {
  --bg: hsl(0 0% 0%);
  --panel: hsl(0 0% 4%);
  --line: hsl(0 0% 38%);
  --fg: hsl(0 0% 100%);
  --dim: hsl(0 0% 72%);
  --green: hsl(120 100% 68%);
  --amber: hsl(50 100% 66%);
  --cyan: hsl(190 100% 74%);
  --red: hsl(0 100% 68%);
  --well: hsl(0 0% 9%);
  --hover: hsl(0 0% 16%);
  --backdrop: hsl(0 0% 0% / 0.85);
}

/* Daylight, a laptop by a window, and printing a set list to look at. */
body.theme-paper {
  --bg: hsl(45 25% 94%);
  --panel: hsl(45 30% 98%);
  --line: hsl(40 12% 76%);
  --fg: hsl(30 15% 16%);
  --dim: hsl(35 10% 42%);
  --green: hsl(150 60% 30%);
  --amber: hsl(28 85% 38%);
  --cyan: hsl(205 70% 38%);
  --red: hsl(0 65% 44%);
  --well: hsl(45 25% 92%);
  --hover: hsl(45 22% 87%);
  --backdrop: hsl(40 20% 40% / 0.5);
}
/* The one place a light palette needs more than colours: glow on a light
   background reads as a smudge. */
body.theme-paper h1,
body.theme-paper #bpm,
body.theme-paper .big-count { text-shadow: none; }

/* --- screen effects -------------------------------------------------------
   The two effects are independent and additive, which is what Wes asked for:
   scan lines without the glow, glow without the lines, or both at once. They
   are drawn on ::before/::after of <body> so nothing in the layout moves when
   one is switched on. */

:root {
  --scanlines: repeating-linear-gradient(
    to bottom,
    hsl(0 0% 0% / 0.22) 0 1px,
    transparent 1px 3px
  );
}

body.fx-scanlines:not(.fx-scan-under)::before {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 50;
  background: var(--scanlines);
}

/* "Under the text": the same pick as the portal site, per Wes. The lines live
   on the surfaces — the page background and the panels — and everything inside
   a panel is lifted one layer above them, so words, lists and form fields are
   always painted on top of the texture rather than behind it. */
body.fx-scan-under { background-image: var(--scanlines); }
body.fx-scan-under .panel,
body.fx-scan-under .bar { position: relative; }
body.fx-scan-under .panel::before,
body.fx-scan-under .bar::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--scanlines);
  pointer-events: none;
  z-index: 0;
}
body.fx-scan-under .panel > *,
body.fx-scan-under .bar > * { position: relative; z-index: 1; }

/* The one exception: the wordmark keeps its lines in this mode too. It is a
   picture of a CRT, not something being read, and stripping the texture from
   it just makes it look flat — same call the portal makes for its banner. */
body.fx-scan-under h1::after {
  content: '';
  position: absolute;
  inset: -2px -4px;
  background: var(--scanlines);
  pointer-events: none;
}

body.fx-glow h1,
body.fx-glow #bpm,
body.fx-glow #now-name,
body.fx-glow .big-count { text-shadow: 0 0 0.4em currentColor; }
/* The tempo number is by far the largest thing this rule catches — 0.4em of a
   4.2rem glyph at full opacity is a ~27px halo, which is what Wes was looking
   at when he asked for less bloom. Everything else the rule covers is small
   enough that 0.4em reads as a glow rather than a smear, so only #bpm is
   pulled back. Must come after the shared rule to win. */
body.fx-glow #bpm { text-shadow: 0 0 0.16em color-mix(in srgb, currentColor 45%, transparent); }
body.fx-glow .panel { box-shadow: inset 0 0 3rem hsl(0 0% 0% / 0.35); }

/* --- beat flash ---
   The whole transport panel lifts on the beat. Deliberately a background
   change rather than a border or a shadow: at 4 metres you see the block of
   colour and nothing else. */
body.flash .panel { background: color-mix(in srgb, var(--green) 12%, var(--panel)); }

/* --- big beat number --- */
.big-count { display: none; }
body.bigcount .big-count {
  display: block;
  text-align: center;
  font-size: clamp(4rem, 22vw, 11rem);
  line-height: 0.95;
  color: var(--green);
  font-variant-numeric: tabular-nums;
}
/* Nothing is running, so there is no beat to show. Lit in the theme colour at
   this size it reads as a glowing bar rather than as a placeholder, so the
   waiting state is dim and flat — `body` prefix to outrank the fx-glow rule. */
body .big-count.waiting { color: var(--dim); text-shadow: none; opacity: 0.5; }

/* With the number this big the dots are redundant, and both together take up
   the whole panel on a phone. */
body.bigcount .beats { display: none; }

/* Off-pulse beats under compound grouping: same row, visibly lighter, so a
   6/8 bar reads as two groups of three rather than six of anything. */
.beat-dot.offbeat { width: 0.7rem; height: 0.7rem; align-self: center; opacity: 0.7; }

/* --- settings dialog --- */
dialog h3 {
  margin: 1rem 0 0.4rem;
  font-size: 11px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--dim);
}
dialog h3:first-of-type { margin-top: 0.25rem; }

/* Tabbed settings: general / appearance / controls. The head keeps the title
   and the tab strip on one line; each panel is a plain block toggled by the
   [hidden] attribute from the tab wiring. */
.settings-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.5rem 1rem;
  border-bottom: 1px solid var(--line);
  margin-bottom: 0.75rem;
}
.settings-head h2 { margin: 0; }
.settings-tabs { display: flex; gap: 0.2rem; }
.settings-tabs .tab {
  padding: 0.35rem 0.7rem;
  background: none;
  border: 1px solid transparent;
  border-bottom: none;
  border-radius: 4px 4px 0 0;
  color: var(--dim);
  font: inherit;
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  cursor: pointer;
}
.settings-tabs .tab:hover { color: var(--fg); background: var(--hover); }
.settings-tabs .tab.active {
  color: var(--green);
  border-color: var(--line);
  background: var(--panel);
  margin-bottom: -1px;
}
.tabpanel[hidden] { display: none; }
.tabpanel h3:first-of-type { margin-top: 0.25rem; }

/* Wes: "the width and vertical position change significantly when changing
   tabs. It is jarring." They did, because a <dialog> sizes to its content and
   the browser then re-centres it — so a short tab drew a narrow box high up
   and a long one a wide box filling the screen.
   The fix is to stop content being a layout input at all: the settings dialog
   carries its own fixed box, and the panels scroll inside it. The head and the
   close row are pinned; only the middle moves.

   A closed <dialog> is display:none from the UA stylesheet, so the flex box is
   put on [open] only — an unconditional `display: flex` here would beat that
   rule and leave the settings permanently on screen, the same trap that
   `.field[hidden]` and `a.button-link[hidden]` already guard against. */
#help {
  box-sizing: border-box;
  width: min(34rem, calc(100vw - 2rem));
  height: min(42rem, calc(100vh - 4rem));
  max-width: none;
}
#help[open] { display: flex; flex-direction: column; }
.tabpanels { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
/* The scrollbar would otherwise sit on top of the right-hand controls. */
.tabpanels { padding-right: 0.35rem; }

/* --- lite / full ---
   The badge reads as a status, not a button: bracketed like every other badge
   in the theme, amber for lite (something to act on) and green for full. */
.tier-badge {
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--amber);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 0.1rem 0.4rem;
}
.tier-badge.full { color: var(--green); }
.price { color: var(--amber); font-variant-numeric: tabular-nums; }

/* Until the terms link there was no plain <a> anywhere in the app — only
   .button-link — so an ordinary link rendered in the browser's default blue,
   which on this palette is close to unreadable. One rule, globally, or the
   next link added somewhere else has the same problem. */
a { color: var(--cyan); }
a:hover { color: var(--fg); }
a.hint { color: var(--dim); text-decoration-color: var(--line); }
a.hint:hover { color: var(--fg); }

/* The welcome explainer. Sized like the settings dialog so a first-time
   visitor doesn't get a box that jumps as the two plan cards wrap. */
#welcome {
  box-sizing: border-box;
  width: min(38rem, calc(100vw - 2rem));
  max-width: none;
  max-height: calc(100vh - 3rem);
  overflow-y: auto;
}
#welcome h2 { margin-top: 0; }
.welcome-lede { margin: 0 0 1rem; font-size: 13px; color: var(--fg); }
.welcome-try { font-size: 13px; color: var(--fg); }
.plans { display: flex; flex-wrap: wrap; gap: 0.75rem; }
/* min-width:0 on the card AND flex-basis, or a long <li> sets a min-content
   floor wider than a phone and the whole dialog shrink-to-fits. */
.plan {
  flex: 1 1 14rem;
  min-width: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.6rem 0.8rem;
}
.plan-full { border-color: var(--green); }
.plan h3 {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.5rem;
  margin: 0 0 0.5rem;
}
.plan-price { color: var(--amber); letter-spacing: normal; text-transform: none; }
.plan ul { margin: 0; padding-left: 1.1rem; font-size: 12px; }
.plan li { margin-bottom: 0.25rem; }

/* The gear button in the header: line the SVG up with the ghost-button text. */
.gear { display: inline-flex; align-items: center; padding: 0.2rem 0.5rem; }
.gear svg { display: block; }

/* The Planning Center key twisty. openSettings() re-shuts it every open, so the
   id and secret are never on screen unless someone clicks to reveal them. */
.pco-keys > summary {
  cursor: pointer;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dim);
}
.pco-keys[open] > summary { margin-bottom: 0.5rem; color: var(--fg); }
.effects { display: flex; flex-wrap: wrap; gap: 0.35rem 0.9rem; }
.effects label { display: flex; align-items: center; gap: 0.35rem; font-size: 12px; }
.dialog-actions { display: flex; justify-content: space-between; gap: 0.5rem; margin-top: 1rem; }

/* A shortcut row is now label / key / unbind, and the key is a button you
   press to change it rather than a static <kbd>. */
.shortcuts li { align-items: center; }
.shortcuts li > span:first-child { flex: 1; }
.keycap {
  background: var(--hover);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 0.1rem 0.45rem;
  min-height: auto;
  color: var(--cyan);
  font-size: 12px;
  min-width: 7rem;
}
.keycap.capturing { color: var(--amber); border-color: var(--amber); }
.keycap.clash { color: var(--red); border-color: var(--red); }
.shortcuts .row-btn {
  min-height: auto;
  padding: 0 0.3rem;
  background: none;
  border: none;
  color: var(--dim);
  font-size: 12px;
}

/* --- pasted set list import --- */
#import { max-width: 38rem; width: min(38rem, 92vw); }
#import textarea {
  width: 100%;
  font-family: inherit;
  font-size: 13px;
  resize: vertical;
}
#import .hint code {
  background: var(--hover);
  border-radius: 3px;
  padding: 0 0.25rem;
  color: var(--cyan);
}
.import-preview {
  max-height: 40vh;
  overflow-y: auto;
  margin: 0.4rem 0 0;
}
/* The whole row is the hit target, so on a phone a tick is a thumb rather
   than a fingernail on a 13px checkbox. */
.import-preview li { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; }
/* Struck through on .name only. Putting line-through on the row wrapper drew
   it across the "not a song" badge too, and a descendant cannot undo an
   ancestor's text-decoration. */
.import-preview li.skipped .row-main { opacity: 0.5; }
.import-preview li.skipped .name { text-decoration: line-through; }
.badge {
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--cyan);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 0 0.3rem;
  white-space: nowrap;
}
.badge.new { color: var(--amber); }
/* A manual match is coloured differently from an automatic one: "I chose this"
   and "the matcher chose this" carry very different amounts of trust. */
.badge.manual { color: var(--green); border-color: color-mix(in srgb, var(--green) 45%, transparent); }

/* The song / not-a-song toggle. A button rather than a label, because it is
   the one control on the row that changes what gets written. */
button.kind-toggle {
  background: none;
  min-height: 0;
  line-height: 1.6;
  cursor: pointer;
  font-family: inherit;
}
button.kind-toggle:hover { border-color: var(--cyan); color: var(--fg); background: none; }

/* "match to…" — full width under the row, because a library song's name is
   long and truncating it defeats the point of choosing one by name. */
.match-picker {
  margin-top: 0.25rem;
  width: 100%;
  max-width: 100%;
  font-size: 11px;
  height: 1.8rem;
  color: var(--dim);
}
.match-picker:focus { color: var(--fg); }

/* --- markers: the non-song landmarks in a set list --- */
.list li.marker-row {
  margin-left: 1.4rem;
  border-style: dashed;
  border-color: color-mix(in srgb, var(--line) 70%, transparent);
  background: color-mix(in srgb, var(--bg) 55%, var(--panel));
}
.list li.marker-row .name {
  color: var(--dim);
  font-size: 12px;
  font-style: italic;
  letter-spacing: 0.02em;
}
.list li.marker-row:hover { background: color-mix(in srgb, var(--bg) 35%, var(--panel)); }
/* Markers are outside the numbering. The numbers are how you find "song 3" on
   a stage, and counting the offering as one of them makes every number after
   it disagree with the plan everyone else is reading. */
/* The counter is incremented by the ::before, not by the li, so cancelling it
   on the li does nothing at all — which is how the first render came back
   numbered 3, 4, 5, 6, 9. */
.list.ordered li.marker-row::before { counter-increment: none; content: '·'; }

/* Two buttons side by side under a list, each taking half the width. */
/* Four buttons under the set list now, so they wrap rather than shrinking to
   a row of unreadable stubs on a phone. 8ch is about "new service" wrapped. */
.row-buttons { display: flex; gap: 0.4rem; flex-wrap: wrap; }
.row-buttons > button { flex: 1 1 8ch; min-width: 0; }

/* The service name and the switcher, stacked. Side by side they fought over a
   narrow panel and the dropdown showed "Wednesday" for "Wednesday youth" —
   which is exactly the information a switcher exists to give you. Full width
   each, and the dropdown is only in the tree at all once there are two. */
.service-bar { display: flex; flex-direction: column; gap: 0.3rem; }
.service-bar > * { width: 100%; min-width: 0; }

/* Search wide, sort narrow — the search is the thing used every Sunday. */
.library-bar { display: flex; gap: 0.3rem; align-items: stretch; }
.library-bar #search { flex: 1; min-width: 0; }
.library-bar #library-sort { flex: 0 0 auto; max-width: 7.5rem; color: var(--dim); }

/* ------------------------------------------------------- Planning Center */
/* The two ways into the import dialog. They are the same kind of thing — a
   source you can fold away — so they get the same box; the paste one used to
   be an <h3> and reading as a different species of control was confusing when
   they sat one above the other. */
#pco-block,
#paste-block {
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 0.4rem 0.6rem;
  margin-bottom: 0.8rem;
}
#pco-block > summary,
#paste-block > summary { cursor: pointer; color: var(--cyan); }
#pco-block[hidden],
#paste-block[hidden] { display: none; }
.pco-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-top: 0.5rem;
}
/* display:flex beats the hidden attribute, so every .pco-row that is toggled
   with `hidden` stayed on screen — this showed the owner a "leave this team"
   button they can never use, an empty invite-link box, and (already, before
   teams existed) the upgrade button to accounts that had already paid. The
   fourth time this trap has been sprung here, after .field[hidden],
   a.button-link[hidden] and #help[open]. */
.pco-row[hidden] { display: none; }
/* The status line takes the leftover width and wraps rather than pushing the
   dialog wider — a 401 explanation is three lines long on purpose. */
#pco-state { flex: 1 1 12rem; min-width: 0; }

/* ---------------------------------------------------------------- offline */

.offline-row { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.offline-row button[hidden] { display: none; }

/* A network drop mid-service is worth one calm line in the footer, coloured
   so it reads as information rather than as an error — the app carries on. */
.net-badge {
  color: var(--amber);
  border: 1px solid var(--amber);
  border-radius: 3px;
  padding: 0 0.3rem;
  font-size: 11px;
  letter-spacing: 0.06em;
}
.net-badge[hidden] { display: none; }

/* ------------------------------------------------------------------ mixer */

/* A console, not a settings screen. Wes: "make the audio mixer actually look
   like a mixer with audio faders."

   So: one vertical strip per source, faders side by side with their travel
   lined up, a meter beside each fader, and master on the right where a desk
   puts it. The strips scroll horizontally rather than wrapping — a wrapped
   channel strip stops reading as a console at exactly the moment (a phone)
   where it most needs to. */
.mixer {
  display: flex;
  align-items: stretch;
  gap: 0.3rem;
  padding: 0.4rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--fg) 3%, transparent);
  overflow-x: auto;
}
.mixer-strips { display: flex; gap: 0.3rem; }
.mixer-strip {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
  flex: 0 0 auto;
  width: 3.7rem;
  padding: 0.3rem 0.2rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg);
}
/* Master is the one strip that is not a source, so it is set apart — to the
   right of the channels, as on a desk, with a divider rather than a stretch of
   empty panel. It was pushed to the far edge first, and on a wide screen that
   left a hole in the middle of the console that read as a rendering fault. */
.mixer-strip.master {
  border-color: var(--green);
  margin-left: 0.5rem;
  border-left-width: 2px;
  background: color-mix(in srgb, var(--green) 7%, var(--bg));
}
/* Nothing beyond master, so the console does not stretch to fill a wide
   column — a channel strip is a fixed-width object. */
.mixer::after { content: ""; flex: 1 1 auto; }

/* THE CLICK BUS, OPENED UP. Wes asked for the click channel to expand into the
   voices it sums rather than for those to live in a second panel with a second
   set of faders. A panel under the console rather than six more strips inline:
   the console already scrolls sideways on a phone at six strips, and twelve
   would put master a long way off the edge. */
.click-bus {
  display: block;
  margin-top: 0.4rem;
  padding: 0.4rem 0.5rem 0.2rem;
  border: 1px solid var(--line);
  border-left: 2px solid var(--green);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--green) 4%, transparent);
}
/* .click-bus is display:block, which beats the hidden attribute — the same
   trap .field, a.button-link, .pco-row and #help[open] were all bitten by. */
.click-bus[hidden] { display: none; }
/* The twisty, and the name beside it, on the click strip's top line. */
.mixer-head { display: flex; align-items: center; gap: 0.1rem; width: 100%; min-width: 0; }
.mixer-expand {
  min-height: auto;
  padding: 0 0.1rem;
  /* Bigger than the strip's own 10px lettering on purpose: it is the one
     control on the console that is not a fader, and a twisty nobody finds is a
     feature nobody finds. */
  font-size: 13px;
  line-height: 1;
  background: none;
  border: none;
  color: var(--green);
}
.mixer-expand:hover { color: var(--fg); background: none; }
/* The strip the open panel belongs to, marked so the two read as one thing. */
.mixer-strip.expanded { border-color: var(--green); }
.mixer-strip.expanded .mixer-expand { color: var(--green); }
.mixer-name {
  font-size: 10px;
  line-height: 1.2;
  text-align: center;
  width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The fader column: meter on the left, fader on the right, both running the
   same height so a level and its travel can be read against each other. */
.mixer-fader {
  position: relative;
  display: flex;
  align-items: stretch;
  justify-content: center;
  gap: 0.25rem;
  height: 8rem;
}
/* The fader itself is drawn once, up beside the click voices — one graphic for
   every fader on this console, which is the point of the whole exercise. */
.mixer-strip.master .mixer-fader input[type="range"]::-webkit-slider-thumb {
  border-color: color-mix(in srgb, var(--amber) 60%, hsl(0 0% 0%));
}
.mixer-strip.master .mixer-fader input[type="range"]::-moz-range-thumb {
  border-color: color-mix(in srgb, var(--amber) 60%, hsl(0 0% 0%));
}

/* The meter. Post-fader, so it shows what actually left the channel — see
   audio/mixer.js for why this is a diagnostic and not decoration. */
.mixer-meter {
  position: relative;
  display: block;
  width: 0.45rem;
  border: 1px solid var(--line);
  background: color-mix(in srgb, var(--fg) 6%, transparent);
  overflow: hidden;
}
.mixer-meter i {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 0;
  display: block;
  background: linear-gradient(to top,
    var(--green) 0%,
    var(--green) 60%,
    var(--amber) 82%,
    var(--red) 100%);
}
/* The peak-hold line, drawn off a custom property the paint loop writes, so a
   held peak costs no extra element per strip. */
.mixer-meter i::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--hold, 0%);
  height: 1px;
  background: var(--fg);
  opacity: 0.7;
}
.mixer-read {
  font-size: 10px;
  text-align: center;
  color: var(--amber);
  font-variant-numeric: tabular-nums;
}
/* Silent for a reason that is not this channel's own mute — see
   silenceReason() in core/mixer.js. The whole strip dims so "why can't I hear
   the click" is answerable at a glance instead of during a service. */
.mixer-strip.silenced { opacity: 0.55; }
.mixer-strip.silenced .mixer-name,
.mixer-strip.silenced .mixer-read { opacity: 0.6; }

.mixer-btns { display: flex; gap: 0.15rem; }
.mixer-btn {
  min-height: auto;
  padding: 0 0.3rem;
  font-size: 11px;
  line-height: 1.5;
  background: none;
  border: 1px solid var(--line);
  color: var(--dim);
}
.mixer-btn:hover:not(:disabled) { color: var(--fg); background: none; }
.mixer-btn.on { color: var(--bg); background: var(--red); border-color: var(--red); }
.mixer-btn.solo.on { background: var(--amber); border-color: var(--amber); }
/* The master strip has no mute or solo — that is what the fader is for — but
   the empty box has to hold the column so the strips stay the same height. */
.mixer-strip.master .mixer-btns { visibility: hidden; }
#mixer-hint { cursor: pointer; color: var(--amber); }
#output-hint { margin-top: 0.2rem; }

/* A hint that is a warning: the browser opened a microphone other than the one
   that was chosen. Amber rather than red — nothing is broken, but it is not
   doing what was asked, and that must not read as ordinary help text. */
.hint.warn { color: var(--amber); }

/* The mixer's own silence notice, beside the stall one on the transport. Amber,
   not red: this is a fader someone moved, not the browser taking the audio
   away, and the two want telling apart at a glance. */
.audio-stall.quiet {
  color: var(--amber);
  border-color: color-mix(in srgb, var(--amber) 55%, var(--line));
  background: color-mix(in srgb, var(--amber) 12%, transparent);
}

/* ------------------------------------------------------------------- cues */

.cues { margin-top: 0.6rem; border-top: 1px solid var(--line); padding-top: 0.5rem; }
.cue-buttons { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.cue-wrap { display: inline-flex; }
.cue-btn {
  min-height: auto;
  padding: 0.2rem 0.45rem;
  font-size: 12px;
  background: none;
  border: 1px solid var(--line);
  color: var(--fg);
}
.cue-btn:hover:not(:disabled) { border-color: var(--green); color: var(--green); background: none; }
/* A cue in your own voice, which is the one that goes through the fader, the
   meter and the chosen output. Marked because otherwise you find out which
   voice a button speaks in by pressing it in front of the band. */
.cue-btn.recorded { border-color: var(--green); }
.cue-btn.recorded::before { content: '●'; color: var(--green); font-size: 8px; vertical-align: 2px; margin-right: 0.25rem; }
.cue-del {
  min-height: auto;
  padding: 0.2rem 0.3rem;
  font-size: 11px;
  background: none;
  border: 1px solid var(--line);
  border-left: none;
  color: var(--dim);
}
.cue-del:hover { color: var(--red); background: none; }
/* Nothing gets deleted by a stray tap during a service. */
body.live .cue-del { display: none; }
.cue-add { display: flex; gap: 0.3rem; margin-top: 0.4rem; }
.cue-add input { flex: 1 1 auto; min-width: 0; }

/* ------------------------------------------- cues pinned to a bar of a song */

.cuepoints { display: flex; flex-direction: column; gap: 0.4rem; margin-bottom: 0.4rem; }
.cuepoint-row {
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  gap: 0.3rem;
  align-items: center;
}
/* The description sits under its own row rather than beside it: it is a
   sentence ("bar 17, spoken 4 beats early"), and a sentence squeezed into a
   column is a sentence nobody reads. */
.cuepoint-note { grid-column: 1 / -1; font-size: 11px; line-height: 1.3; }
.cuepoint-note.warn { color: var(--amber); }
.cuepoint-at { display: inline-flex; align-items: center; gap: 0.25rem; }
.cuepoint-at .cap { font-size: 10px; }
.cuepoint-at input { width: 3.4rem; padding: 0.2rem 0.3rem; }
/* Wider than its neighbours because its placeholder is a word, not a
   number: "a bar" clipped to "a b" says nothing about the default. */
.cuepoint-at input[data-field="lead"] { width: 5.4rem; }
/* The count-in tick sits with the position controls because that is what it
   is: it decides which bar gets counted, not what gets said. `width: auto`
   because the rule above sizes every input in this row to 3.4rem, and a
   checkbox stretched to that is a rectangle nobody reads as a checkbox. */
.cuepoint-countin { display: inline-flex; align-items: center; gap: 0.2rem; cursor: pointer; }
.cuepoint-countin .cap { white-space: nowrap; }
.cuepoint-at input[type="checkbox"] { width: auto; margin: 0; }
.cuepoint-row input[data-field="text"] { min-width: 0; }
.cuepoint-row .row-btn {
  min-height: auto;
  padding: 0 0.35rem;
  background: none;
  border: none;
  color: var(--dim);
}
.cuepoint-row .row-btn:hover:not(:disabled) { color: var(--fg); background: none; }
/* Same mark, same meaning as on a cue button: this one is in your own voice,
   so it goes through the cue fader and the chosen output rather than the
   browser's. */
.cuepoint-say.recorded { color: var(--green); }

@media (max-width: 620px) {
  /* The position controls take a line of their own rather than shrinking the
     text field to nothing — the words are the part you have to be able to
     read back. */
  .cuepoint-row { grid-template-columns: 1fr auto auto; }
  .cuepoint-at { grid-column: 1 / -1; }
}

/* What is about to be said. On the transport, so live mode keeps it. No
   `display` rule, deliberately, so the `hidden` attribute is enough — the
   contract test sweeps for exactly that mistake. */
.cue-next {
  margin-top: 0.25rem;
  text-align: center;
  font-size: 12px;
  color: var(--dim);
}
/* Inside a bar of it. The one moment the line is worth looking at. */
.cue-next.soon { color: var(--amber); }

/* ---------------------------------------------------- song-length recording

   What "end here" just measured and wrote to the song. Green, because unlike
   the two notices above it this is something that worked — but it is still a
   change made by one tap to a stored setting, so the way back is in the box
   rather than somewhere else on the screen. `[hidden]` guard because the box
   is a flex column; see the contract test that sweeps for exactly that. */
.length-note {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  margin: 0.5rem 0 0;
  padding: 0.4rem 0.5rem;
  font-size: 12px;
  line-height: 1.4;
  text-align: center;
  color: var(--green);
  border: 1px solid color-mix(in srgb, var(--green) 55%, var(--line));
  background: color-mix(in srgb, var(--green) 10%, transparent);
}
.length-note[hidden] { display: none; }
/* How far off the bar line the tap landed. Dim: it qualifies the number above
   it rather than competing with it. */
.length-note .off { color: var(--dim); font-size: 11px; }
.length-note .row { display: flex; gap: 0.35rem; flex-wrap: wrap; justify-content: center; }
.length-note button {
  min-height: auto;
  padding: 0.15rem 0.45rem;
  font-size: 11px;
  background: none;
  border: 1px solid var(--line);
  color: var(--fg);
}
.length-note button:hover:not(:disabled) { border-color: var(--green); color: var(--green); }

/* ------------------------------------------------------------ herding cats

   The click is out front right now, and here is how much of it is left. Amber
   rather than green: this is not a thing that succeeded and went away, it is a
   temporary state of the click that someone needs to be able to see is on.
   `[hidden]` guard because the box is a flex column; see the contract test. */
.herd-note {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.15rem;
  margin: 0.5rem 0 0;
  padding: 0.35rem 0.5rem;
  font-size: 12px;
  line-height: 1.4;
  text-align: center;
  color: var(--amber);
  border: 1px solid color-mix(in srgb, var(--amber) 55%, var(--line));
  background: color-mix(in srgb, var(--amber) 10%, transparent);
}
.herd-note[hidden] { display: none; }
/* What it actually lifted. Dim: it qualifies the countdown above it. */
.herd-note .lift { color: var(--dim); font-size: 11px; }
/* The button while a herd is running, so the state is readable from the
   control as well as from the notice. */
#btn-herd.on {
  border-color: var(--amber);
  color: var(--amber);
}

/* --------------------------------------------------------- the track player

   The reference recording, played from one of the song's links. Neutral rather
   than coloured: unlike the three notices above it this is not a state anyone
   needs warning about, it is a second transport that happens to be on screen.
   `[hidden]` guard because the box is a flex column — see the contract test
   that sweeps for exactly that, and the note in learnings about an author
   `display` rule beating the HTML `hidden` attribute outright. */
.track-player {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  margin: 0.5rem 0 0;
  padding: 0.4rem 0.5rem;
  border: 1px solid var(--line);
  background: color-mix(in srgb, var(--fg) 4%, transparent);
}
.track-player[hidden] { display: none; }
.track-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  /* Without this the row's own min-content floor is set by the children's
     specified widths and the whole block overflows at phone width — the nested
     flex trap already noted for the button rows. */
  min-width: 0;
}
/* The name takes whatever is left and gives it up first: a long file name must
   not push the transport button or the clock off the row. */
.track-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12px;
}
.track-time {
  font-size: 11px;
  /* So the digits don't shuffle sideways once a second while it plays. */
  font-variant-numeric: tabular-nums;
  flex: none;
}
#btn-track-play {
  min-height: auto;
  padding: 0.2rem 0.6rem;
  font-size: 12px;
  flex: none;
}
.track-player .row-btn {
  min-height: auto;
  padding: 0 0.3rem;
  background: none;
  border: none;
  color: var(--dim);
  flex: none;
}
.track-player .row-btn:hover:not(:disabled) { color: var(--fg); background: none; }
.track-seek { width: 100%; margin: 0; }
/* What the player could not deliver: a file whose server won't share it with
   the mixer, or a link that isn't audio at all. Amber, because it is a working
   thing that is working differently — the same weight as the mixer's "playing,
   but silent" line rather than the red of an audio stall. */
.track-note {
  margin: 0;
  font-size: 11px;
  line-height: 1.4;
  color: var(--amber);
}
.track-note[hidden] { display: none; }

/* A disabled ▶ on a link that cannot be played has to *look* unavailable, or
   it reads as a button that does nothing when pressed. */
.link-row .row-btn:disabled { opacity: 0.35; cursor: not-allowed; }

/* --------------------------------------------------------------- talkback */

.talkback { margin-top: 0.6rem; border-top: 1px solid var(--line); padding-top: 0.5rem; }
.talkback-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  margin-bottom: 0.4rem;
}
.talkback-head .ghost.on { color: var(--bg); background: var(--green); border-color: var(--green); }
#btn-talkback-talk.on { background: var(--red); border-color: var(--red); }
#talkback-state { font-size: 11px; }
/* A live microphone is the one state in this app that must never be subtle. */
#talkback-state.talking { color: var(--red); font-weight: 600; }
/* Holding a key to talk must not also select the button's label. */
#btn-talkback-talk { user-select: none; touch-action: none; }

@media (max-width: 480px) {
  /* A fourth tab (history) pushed the strip past the dialog at phone width and
     the last one was simply cut off the right edge — a tab nobody can reach.
     Tighter chrome makes four fit; the wrap is the belt to that brace, so a
     fifth one later drops to a second line rather than disappearing. */
  .settings-tabs { flex-wrap: wrap; }
  .settings-tabs .tab { padding: 0.35rem 0.35rem; letter-spacing: 0.04em; }
}

@media (max-width: 480px) {
  /* Narrower strips, and the row scrolls sideways rather than wrapping. Master
     keeps its place at the right of the scroll, not of the screen — being able
     to swipe to it beats it sitting on top of a channel. */
  .mixer-strip { width: 3.2rem; }
  .mixer-strip.master { margin-left: 0.3rem; }
  /* A voice strip stays wider than a channel: it carries the name of the sound
     it plays, and a clipped pack name is not a name. */
  .click-voice { width: 4.2rem; }
  .mixer-fader { height: 6.5rem; }
  .mixer-fader input[type="range"] { width: 1.9rem; }
  .mixer-fader input[type="range"]::-webkit-slider-thumb { width: 1.7rem; }
  .mixer-fader input[type="range"]::-moz-range-thumb { width: 1.7rem; }
  .click-bus { padding: 0.35rem 0.3rem 0.2rem; }
}

/* --------------------------------------------------------- edit history

   What changed, when and by whom. A dense two-line row rather than a table:
   the log is read by scanning down the left edge for a time, and a table would
   make the change column — the part with the actual words in it — the narrow
   one on a phone. */

.history-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.6rem;
}
.history-head select { max-width: 14rem; }
.history {
  list-style: none;
  margin: 0 0 0.6rem;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}
.hrow {
  border-left: 2px solid var(--line);
  padding: 0.25rem 0 0.25rem 0.5rem;
}
.hrow:hover { background: var(--hover); }
/* The three things that happened to a song read differently at a glance —
   which is the whole reason to scan this list at all. */
.hrow.added { border-left-color: var(--green); }
.hrow.deleted { border-left-color: var(--red); }
.hrow.imported { border-left-color: var(--amber); }
.hwhen { display: flex; gap: 0.5rem; font-size: 11px; color: var(--dim); }
.hwho { color: var(--cyan); }
.hwhat { display: flex; flex-wrap: wrap; gap: 0.4rem; font-size: 13px; }
.htitle { color: var(--fg); }
.hchange { color: var(--dim); }
/* The "last edited …" line under the song form. A button, because it opens the
   log — but it must not look like one, or it competes with the controls it
   sits under for a glance it does not deserve. */
.song-history {
  align-self: start;
  margin: 0.1rem 0 0;
  padding: 0;
  min-height: auto;
  background: none;
  border: none;
  color: var(--dim);
  font: inherit;
  font-size: 11px;
  text-align: left;
  text-decoration: underline dotted;
  cursor: pointer;
}
.song-history:hover:not(:disabled) { color: var(--fg); background: none; border: none; }
/* An author rule setting `display` beats the `hidden` attribute, which is only
   a user-agent style — without this the empty button stays on screen. */
.song-history[hidden] { display: none; }

/* --- music stand ------------------------------------------------------- */
/* The mini player. It used to be drawn into a Picture-in-Picture window as
   well, which is why it was built as a self-contained block; it still is, but
   there is only one place it goes now — floating over the sheet music reader.
   See src/core/musicstand.js. */
.stand {
  display: grid;
  gap: 0.2rem;
  justify-items: center;
  padding: 0.35rem 0.7rem 0.6rem;
  background: var(--panel);
  color: var(--fg);
  font-family: 'Fira Code', 'SF Mono', ui-monospace, monospace;
}
.stand-head {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  width: 100%;
  /* The handle. `touch-action: none` is what makes a drag with a finger a drag
     rather than the browser scrolling the reader underneath it. */
  cursor: grab;
  touch-action: none;
  user-select: none;
}
.stand.dragging .stand-head { cursor: grabbing; }
.stand-grip { color: var(--dim); font-size: 12px; letter-spacing: -0.1em; }
.stand-name { flex: 1; font-size: 1rem; letter-spacing: 0.02em; text-align: center; }
.stand-artist { font-size: 11px; color: var(--dim); }
.stand-artist:empty { display: none; }
.stand-mid {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.8rem;
  width: 100%;
}
/* The number read from furthest away, so it gets the size — the same call the
   transport's big count makes. */
.stand-count { font-size: 2.4rem; line-height: 1; color: var(--green); }
.stand-count.waiting { color: var(--dim); opacity: 0.5; }
.stand-tempo { font-size: 0.9rem; }
.stand-meta { font-size: 0.9rem; color: var(--dim); }
.stand-dots { display: flex; gap: 0.3rem; }
.stand-dots .beat-dot { width: 0.65rem; height: 0.65rem; }
.stand-pos { font-size: 11px; color: var(--dim); }
.stand-flags { font-size: 11px; color: var(--amber); }
.stand-flags[hidden] { display: none; }
.stand-next { font-size: 11px; color: var(--dim); }
.stand-next:empty { display: none; }
.stand-ctl { display: flex; gap: 0.4rem; margin-top: 0.2rem; }
.stand-ctl .ctl { min-width: 3rem; padding: 0.3rem 0.5rem; font-size: 12px; }
.stand-close { min-height: auto; padding: 0 0.3rem; }

/* The layer the player floats in. It covers the viewport so the left/top the
   drag writes are plain viewport coordinates — the same numbers clampStandPos
   is given — but it takes no pointer events of its own, or it would swallow
   every click on the reader underneath it. */
.stand-dock {
  position: fixed;
  inset: 0;
  z-index: 60;
  pointer-events: none;
}
.stand-dock .stand {
  position: absolute;
  width: 260px;
  pointer-events: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 8px 28px hsl(220 40% 2% / 0.6);
}
/* An author `display` rule beats the `hidden` attribute, so without this the
   dock would sit on the page permanently. */
.stand-dock[hidden] { display: none; }

/* --- the sheet music reader -------------------------------------------- */
/* Wes asked for the PDF drawn here rather than downloaded or handed to a
   pop-up window, so this covers the app while it is open. */
.reader {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  flex-direction: column;
  background: var(--bg);
}
.reader[hidden] { display: none; }
.reader-bar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  padding: 0.4rem 0.6rem;
  border-bottom: 1px solid var(--line);
  background: var(--panel);
}
.reader-title {
  flex: 1;
  min-width: 6rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.reader-nav, .reader-tools { display: flex; align-items: center; gap: 0.3rem; }
.reader-tools { flex-wrap: wrap; }
.reader-pos { font-size: 12px; min-width: 8.5rem; text-align: center; }
.reader-zoom { font-size: 12px; min-width: 3rem; text-align: center; }
.reader-page-btn { min-width: 2.2rem; font-size: 1.1rem; line-height: 1; }
/* A toggle that is on has to look on. Without this, "two up" and "dark" read
   exactly the same whether they are engaged or not, which is a control you can
   only understand by looking at the page it changed. */
.reader-tools .ghost.on {
  color: var(--bg);
  background: var(--green);
  border-color: var(--green);
}
.reader-stage {
  flex: 1;
  min-height: 0;
  overflow: auto;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 4px;
}
.reader-pages { display: flex; gap: 16px; align-items: flex-start; }
.reader-pages[hidden] { display: none; }
.reader-canvas { display: block; background: #fff; }
/* Dark mode. A filter rather than a re-render, so it toggles instantly — and
   deliberately on the pages rather than on .reader, because the mini player
   floats above this and must stay the colour the app is. hue-rotate after the
   invert keeps a red chord marking red instead of turning it cyan. */
.reader-pages.inverted .reader-canvas { filter: invert(1) hue-rotate(180deg); }
/* --- marks on the page --- */
/* Absolutely positioned over the canvas and NOT under its filter, so a yellow
   highlight stays yellow in dark mode. Transparent to the pointer unless
   marking is on, or a highlight would swallow a press meant for the page. */
.reader-page { position: relative; }
.reader-marks { position: absolute; inset: 0; pointer-events: none; }
.reader-pages.marking .reader-marks { pointer-events: auto; cursor: crosshair; touch-action: none; }
.reader-mark {
  position: absolute;
  pointer-events: none;
  border-radius: 2px;
}
.reader-pages.marking .reader-mark { pointer-events: auto; }
/* Low alpha on purpose: this goes over the notes you are reading, and a
   highlight you cannot read through is a highlight that hid the music. */
.reader-mark.highlight { background: hsl(48 100% 50% / 0.32); }
.reader-pages.inverted .reader-mark.highlight { background: hsl(48 100% 55% / 0.22); }
.reader-mark.note {
  display: flex;
  align-items: flex-start;
  gap: 0.2rem;
  padding: 0.1rem 0.25rem;
  background: hsl(38 92% 62% / 0.92);
  color: hsl(220 30% 10%);
  font-family: 'Fira Code', 'SF Mono', ui-monospace, monospace;
  font-size: 11px;
  line-height: 1.3;
  box-shadow: 0 1px 4px hsl(220 40% 10% / 0.4);
}
.reader-mark-text { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.reader-mark-ghost { outline: 1px dashed hsl(48 100% 50% / 0.8); }
.reader-mark-kill {
  flex: none;
  min-height: auto;
  padding: 0 0.2rem;
  font-size: 11px;
  line-height: 1.2;
  color: hsl(220 30% 10%);
  background: hsl(0 0% 100% / 0.6);
  border: 0;
  border-radius: 2px;
  cursor: pointer;
}
.reader-mark.highlight .reader-mark-kill { position: absolute; top: 0; right: 0; }

.reader-msg { margin: 2rem auto; max-width: 32rem; text-align: center; }
.reader-msg[hidden] { display: none; }
/* Fit-to-width scrolls down the page, so the pages stop being centred
   vertically and start at the top of a stage that can grow past the box. */
.reader.fit-width .reader-stage { align-items: flex-start; }

@media (max-width: 640px) {
  /* The bar wrapped to three rows on a phone and ate a third of the screen,
     which on the one device most likely to be a music stand is the screen you
     came for. Tighter chrome, and the tab link goes down to the one word that
     matters — a label that long is a label that forces its own row. */
  .reader-title { flex-basis: 100%; text-align: center; font-size: 12px; }
  .reader-bar { gap: 0.25rem; padding: 0.3rem; row-gap: 0.25rem; }
  .reader-bar .ghost { font-size: 11px; padding: 0.2rem 0.35rem; min-height: 1.7rem; }
  .reader-pos { min-width: 6.5rem; font-size: 11px; }
  .reader-page-btn { min-width: 1.8rem; }
  /* Qualified with .reader-bar so it beats `.reader-bar .ghost` above — a
     bare `.reader-tab` loses on specificity and the label stays full length,
     which is how this first rendered as "open in a new tabtab". */
  .reader-bar .reader-tab { font-size: 0; }
  .reader-bar .reader-tab::after { content: 'tab'; font-size: 11px; }
  .stand-dock .stand { width: 230px; }
}

/* --- Planning Center charts and audio --- */

#pco-files { max-width: 40rem; width: min(40rem, 92vw); }

.pco-files-head {
  margin: 1rem 0 0.3rem;
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim);
}
.pco-files-head:first-of-type { margin-top: 0.25rem; }
.pco-files-empty { margin: 0.2rem 0; font-size: 13px; }
.pco-files-summary { margin: 1rem 0 0; font-size: 12px; }
.pco-files-clear { margin-top: 0.35rem; }

.pco-files-list { list-style: none; margin: 0; padding: 0; }
.pco-files-list li {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0.4rem;
  border: 1px solid transparent;
  border-radius: var(--radius);
}
.pco-files-list li + li { margin-top: 0.15rem; }
.pco-files-list .row-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.pco-files-list .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pco-files-list .sub { font-size: 12px; }
/* The reason an unavailable row is unavailable has to wrap — it is a sentence,
   not a label, and truncating it leaves the row looking broken for no reason. */
.pco-files-list .sub.warn { white-space: normal; }
.pco-files-list li.picked {
  border-color: color-mix(in srgb, var(--green) 45%, transparent);
  background: color-mix(in srgb, var(--green) 8%, transparent);
}
.pco-files-list li.unavailable .name { color: var(--dim); }
.pco-files-list li.busy { opacity: 0.6; }
/* min-width:0 on the nested flex container as well as on the child: a flex
   box's min-content floor comes from its children's specified widths, so the
   buttons would otherwise push the filename off the row at phone width. */
.pco-files-actions { display: flex; gap: 0.3rem; flex-shrink: 0; min-width: 0; }


/* The per-row way into the charts. Small and quiet: it sits beside a row whose
   job is to be scanned on a stage, and it must not compete with the song name. */
.row-files {
  flex-shrink: 0;
  opacity: 0.65;
  padding: 0.1rem 0.3rem;
  line-height: 1;
}
.row-files:hover { opacity: 1; }

@media (max-width: 480px) {
  /* The buttons drop under the name rather than squeezing it to nothing —
     a filename is how you tell "Lead G" from "Chords G", so it is the part
     that must survive. */
  .pco-files-list li { flex-wrap: wrap; }
  .pco-files-actions { width: 100%; justify-content: flex-end; }
}
