/*
 * Styles for the web UI. Plain CSS, served by Propshaft — no preprocessing.
 * Kept in one file while the UI is small; split it up when that stops being
 * comfortable.
 */

/* The `hidden` attribute must actually hide things.
 *
 * The browser's own `[hidden] { display: none }` has the same specificity as any
 * class selector, so **every class that sets `display` silently defeats it** —
 * and the failure is quiet: JavaScript sets `hidden = true`, the DOM agrees the
 * element is hidden, and it stays on screen. The upload toast shipped that way
 * and read as a spinner stuck forever.
 *
 * Three elements had already hit this and carried their own `[hidden]` rules.
 * One declaration here is the fix, so the next `display: flex` doesn't have to
 * remember.
 */
[hidden] { display: none !important; }

:root {
  --bg: #fbfaf9;
  --surface: #fff;
  --border: #e5e1dc;
  --text: #1c1a18;
  --muted: #6f6862;
  --accent: #1b54c7;
  /* Studio takes. A complementary orange against the blue accent, picked to
     sit at the same *weight* rather than the same hue distance: 6.58:1 on this
     surface against the accent's 6.69: so neither shouts over the other. */
  --studio: #984508;
  --radius: 10px;
  color-scheme: light dark;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #16150f;
    --surface: #201e18;
    --border: #35322a;
    --text: #f3efe7;
    --muted: #a09889;
    /* The same brand hue as light mode's #1b54c7, lightened to stay readable on
       a dark background — #1b54c7 itself manages only 2.7:1 there, against the
       4.5:1 text needs. Two lightnesses of one hue, which is how this file
       handled its previous accent too. */
    --accent: #628fea;
    /* Lightened for the dark surface exactly as the accent is, and matched to
       it again: 5.14:1 here against the accent's 5.27. */
    --studio: #cd7b35;
  }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 16px/1.5 ui-sans-serif, -apple-system, "Segoe UI", system-ui, sans-serif;
}

a { color: var(--accent); }
a:hover { text-decoration: none; }

/* ----- Masthead ----- */

.masthead {
  border-bottom: 1px solid var(--border);
  background: var(--surface);
}

/* Same max width and horizontal padding as `.page`, so the nav's content lines
   up with the content below it rather than sitting against the window edge. */
.masthead__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  max-width: 60rem;
  margin: 0 auto;
  padding: 0.75rem 1.25rem;
}

.masthead__brand {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
  font-weight: 700;
  font-size: 1.1rem;
  letter-spacing: -0.02em;
  color: var(--text);
  text-decoration: none;
}

.masthead__brand span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }


/* ----- Page ----- */

.page {
  max-width: 60rem;
  margin: 0 auto;
  padding: 2rem 1.25rem 4rem;
}

.page__header { margin-bottom: 2rem; }

h1 { margin: 0 0 0.25rem; font-size: 1.75rem; letter-spacing: -0.02em; }
h2 { margin: 0 0 0.75rem; font-size: 1.05rem; letter-spacing: -0.01em; }

.subtitle { margin: 0; color: var(--muted); font-size: 0.9rem; }

.section { margin-bottom: 2.5rem; }

/* Sections only carry a bottom margin, so one following a form sits right on
   its buttons — as the merge block did under the song form. */
form + .section { margin-top: 2.5rem; }

.section__header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
}

/* ----- Lists ----- */

.list {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}

/* Deliberately **not** `overflow: hidden`. The container's own rounded
   background already clips the corners (rows draw no background of their own),
   and hiding overflow would cut off a row's open "…" menu. */

/* A row with its menu open has to sit above the rows after it, or their
   backgrounds paint over the panel. */
.list__item:has(.menu[open]) {
  position: relative;
  z-index: 30;
}

.list__item {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--border);
}

.list__item:last-child { border-bottom: 0; }

.list__item a { text-decoration: none; font-weight: 500; }
.list__item a:hover { text-decoration: underline; }

.list__index {
  min-width: 1.5rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  font-size: 0.85rem;
}

.list__meta {
  margin-left: auto;
  color: var(--muted);
  font-size: 0.85rem;
}

/* ----- Members ----- */

.members {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

.member {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.9rem;
}

.member__name { color: var(--muted); }

/* ----- Avatars ----- */

.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  object-fit: cover;
  background: var(--border);
  color: var(--muted);
  font-weight: 600;
  letter-spacing: 0.02em;
  flex: none;
}

.avatar--sm { width: 24px; height: 24px; font-size: 0.6rem; }
.avatar--md { width: 36px; height: 36px; font-size: 0.75rem; }
.avatar--lg { width: 64px; height: 64px; font-size: 1.1rem; }

/* ----- Empty states ----- */

.empty {
  margin: 0;
  padding: 1.25rem;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  color: var(--muted);
  font-size: 0.9rem;
}

/* A whole-page dead end, rather than an empty slot within a page: no band
   means there is nothing else on screen to sit beside. */
.empty-state {
  max-width: 26rem;
  margin: 4rem auto;
  text-align: center;
}

.empty-state h1 { margin: 0 0 0.75rem; font-size: 1.4rem; }

.empty-state p { margin: 0; color: var(--muted); line-height: 1.6; }

.empty-state__actions { margin-top: 1.5rem; }

/* ----- Playlist picker ----- */

.field__label { display: block; margin-bottom: 0.3rem; font-size: 0.85rem; color: var(--muted); }

.playlist-picker {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.playlist-picker__count { margin: 0 0 0.5rem; color: var(--muted); font-size: 0.85rem; }

.playlist-picker__row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.4rem 0.6rem;
  border-bottom: 1px solid var(--border);
}

.playlist-picker__row:last-child { border-bottom: 0; }

/* Ticked rows carry the running order, so they should read as the list and the
   rest as the pool they were chosen from. */
.playlist-picker__row--chosen { background: color-mix(in srgb, var(--accent) 8%, transparent); }

.playlist-picker__position {
  flex: 0 0 1.5rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  text-align: right;
  font-size: 0.85rem;
}

.playlist-picker__song { display: flex; align-items: center; gap: 0.5rem; flex: 1; cursor: pointer; }

/* Wider than `.form-narrow`: a song title, its number and two arrows don't fit
   in a column sized for a text field. */
.form-playlist { max-width: 32rem; }

.playlist-picker__move { display: flex; gap: 0.25rem; }

/* A page title with something square beside it — a band and its artwork. */
.page__header--with-avatar {
  display: flex;
  align-items: center;
  gap: 1rem;
}

/* ----- Badges ----- */

.badge {
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  background: var(--border);
  color: var(--muted);
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.badge--pinned { background: var(--accent); color: #fff; }

/* Studio, in the same orange as the play button and title on the same row.
   White text on both accents: `--studio` clears 4.5:1 against white by the same
   measurement that picked it. */
.badge--studio { background: var(--studio); color: #fff; }

/* ----- Forms & buttons ----- */

.button {
  padding: 0.4rem 0.8rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 0.85rem;
  cursor: pointer;
}

.button:hover { border-color: var(--muted); }
.button--quiet { color: var(--muted); }

.button--primary {
  border-color: var(--accent);
  background: var(--accent);
  color: #fff;
}

.field { display: block; margin-bottom: 1rem; }
/* Direct child only — a nested hint stays on the same line as its label. */
.field > span { display: block; margin-bottom: 0.3rem; font-size: 0.85rem; color: var(--muted); }

/* Text-like inputs only. A checkbox stretched to 100% keeps its glyph centred
   in a box the width of the row, which reads as a mysterious gap.
   `.invite-row` rides along because its inputs aren't wrapped in a `.field` —
   they're two to a row, not one per label — and without this they fall back to
   the browser's own styling, which is a smaller font than the rest of the page. */
.field input:not([type="checkbox"]):not([type="radio"]),
.invite-row input {
  width: 100%;
  padding: 0.5rem 0.65rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font: inherit;
}

.form-narrow { max-width: 22rem; }

/* A take that's out of the running for its song's best.
 *
 * Dimmed, deliberately not disabled: it still plays, still opens, still carries
 * its comments — the greying says "this one doesn't count towards best", not
 * "this one is broken". Its own controls stay at full strength so they don't
 * read as unavailable.
 */
.list__item--skipped { opacity: 0.55; }
.list__item--skipped:hover,
.list__item--skipped:focus-within { opacity: 1; }

/* ----- Upload progress ----- */

/* Fixed, so it stays put while the page scrolls under it — an import can run
   for a while and the one thing you want is to see it still moving. */
.upload-progress {
  position: fixed;
  left: 50%;
  bottom: 1.5rem;
  transform: translateX(-50%);
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  max-width: calc(100vw - 2rem);
  padding: 0.7rem 1.1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: 0 6px 24px rgb(0 0 0 / 0.18);
  font-size: 0.9rem;
  /* Tabular figures: the count changes 42 times in a row, and proportional
     digits make the whole toast twitch on every one of them. */
  font-variant-numeric: tabular-nums;
}

.upload-progress__spinner {
  flex: none;
  width: 0.9em;
  height: 0.9em;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: upload-spin 0.7s linear infinite;
}

@keyframes upload-spin {
  to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .upload-progress__spinner { animation-duration: 2.5s; }
}

/* ----- Welcome (the signed-out homepage) ----- */

.welcome {
  max-width: 26rem;
  margin: 6rem auto;
  text-align: center;
}

.welcome__title {
  margin: 0 0 0.5rem;
  font-size: 2.5rem;
  letter-spacing: -0.02em;
}

.welcome__tagline {
  margin: 0;
  color: var(--muted);
  line-height: 1.6;
}

.welcome__actions { margin-top: 2rem; }

/* Apple's guidelines: black fill, white text, the logo at the left of the
   label. Sized off the font so it scales with the page rather than being
   pinned to a pixel height. */
.button-apple {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5em;
  padding: 0.7em 1.4em;
  border-radius: var(--radius);
  background: #000;
  color: #fff;
  font-size: 1rem;
  font-weight: 500;
  text-decoration: none;
}

.button-apple:hover { background: #1a1a1a; }

/* Development only: the button is here so the page looks like the real one,
   but Apple can't complete a sign-in against localhost. */
.button-apple--inert {
  opacity: 0.4;
  cursor: not-allowed;
}

/* The mark reads small next to text, so it sits slightly above the baseline. */
.button-apple__logo {
  width: 1em;
  height: 1.25em;
  margin-top: -0.15em;
}

/* Dark mode: a black button on a near-black page loses its edge. */
@media (prefers-color-scheme: dark) {
  .button-apple {
    background: #fff;
    color: #000;
  }

  .button-apple:hover { background: #e8e8e8; }
}

.welcome__dev { margin: 1.5rem 0 0; font-size: 0.9rem; }
.welcome__dev-note { color: var(--muted); }

/* ----- Invitations ----- */

/* One row per person, wide enough for two names side by side — so it's a form
   of its own width rather than `.form-narrow`. */
.invite-rows {
  list-style: none;
  margin: 0 0 1.5rem;
  padding: 0;
  max-width: 30rem;
}

.invite-row {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
}

.invite-row input { flex: 1; min-width: 0; }

/* The link is the credential, so it's shown in full and selectable rather than
   truncated behind a "copy" affordance. */
.invite-link {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.8rem;
  color: var(--muted);
  user-select: all;
  word-break: break-all;
}

/* ----- Flash ----- */

/* A dismissible message. `align-items: flex-start` is what keeps the × in a
   predictable place: an upload-errors flash runs to several lines, and a
   centred button would sit lower the taller the message got. */
.flash {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  margin: 0 0 1.5rem;
  padding: 0.65rem 0.9rem;
  border-radius: var(--radius);
  font-size: 0.9rem;
}

.flash__body { flex: 1; min-width: 0; }

/* Sized off the text so it stays aligned with the first line, and given a
   generous hit area without drawing a box around itself. */
.flash__dismiss {
  flex: none;
  margin: -0.25rem -0.35rem 0 0;
  padding: 0.25rem 0.45rem;
  border: 0;
  border-radius: var(--radius);
  background: none;
  color: inherit;
  opacity: 0.55;
  font-size: 1.15rem;
  line-height: 1;
  cursor: pointer;
}

.flash__dismiss:hover { opacity: 1; }

.flash--notice { background: color-mix(in srgb, var(--accent) 12%, transparent); }
.flash--alert { background: color-mix(in srgb, #c0392b 15%, transparent); }

/* ----- Dev banner ----- */

.dev-note {
  margin-top: 2rem;
  padding-top: 1rem;
  border-top: 1px solid var(--border);
  color: var(--muted);
  font-size: 0.8rem;
}

/* ----- Forms (create/edit) ----- */

.page__title-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

/* Groups the play button with the heading, so `space-between` above pushes the
   actions to the right rather than pushing the button away from its title. */
.page__title {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-width: 0;
}

.page__title h1 { min-width: 0; }

.field__hint { display: inline; color: var(--muted); font-weight: 400; opacity: 0.8; }

.field input[type="date"] { color-scheme: inherit; }

.form-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 1.25rem;
}

.button--danger { border-color: #c0392b; color: #c0392b; }
.button--danger:hover { background: #c0392b; color: #fff; }

.button--tiny { padding: 0.15rem 0.5rem; font-size: 0.75rem; }

/* Buttons rendered by button_to sit inside their own form. */
.list__meta form,
.form-actions form { display: inline; }

/* ----- Inline comment forms ----- */

.inline-form {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.5rem;
}

.inline-form--wide { margin-top: 1rem; }

.inline-form__input {
  flex: 1;
  padding: 0.4rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 0.9rem;
}

/* A list row that stacks its content above an inline form. */
.list__item--stacked { flex-direction: column; align-items: stretch; }

.list__row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

/* A moment in a take that a comment points at ("0:45"). */
.timestamp {
  padding: 0.05rem 0.35rem;
  border-radius: 4px;
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--accent);
  font-size: 0.8rem;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

/* ----- Take upload drop zone ----- */

.dropzone {
  padding: 2rem 1.25rem;
  border: 2px dashed var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  text-align: center;
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
}

.dropzone--active {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}

.dropzone__prompt { margin: 0 0 0.75rem; }

/* ----- The persistent player -----
 *
 * Fixed to the bottom and outside `.page`, because it belongs to the tab rather
 * than to any page — see the layout's `data-turbo-permanent` note.
 */

.player {
  position: fixed;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 50;
  border-top: 1px solid var(--border);
  background: var(--surface);
}

.player__bar {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 1rem;
}

.player__step {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--text);
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
}

/* ----- The player, playing a studio take -----
 *
 * The whole bar takes the studio colour while one is loaded, so what's playing
 * matches the row it came from. Toggled per **track**, not per queue: a
 * practice's queue is whatever was recorded that day, so the bar can change
 * colour partway through one.
 *
 * Every rule here overrides an accent, and nothing else — shape, size and
 * layout stay exactly where they are, so this can never move the bar around.
 */
.player--studio .player__toggle { background: var(--studio); }

.player--studio .player__seek {
  background: linear-gradient(
    to right,
    var(--studio) 0 var(--progress, 0%),
    color-mix(in srgb, var(--text) 40%, transparent) var(--progress, 0%) 100%
  );
}

.player--studio .player__seek::-webkit-slider-thumb { background: var(--studio); }
.player--studio .player__seek::-moz-range-thumb { background: var(--studio); }

.player__step:hover:not(:disabled) { background: var(--border); }

.player__step:disabled { opacity: 0.3; cursor: default; }

/* ----- Queue panel ----- */

.queue {
  max-height: 40vh;
  overflow-y: auto;
  border-bottom: 1px solid var(--border);
  padding: 0.75rem 1rem;
}

.queue__header {
  margin: 0 0 0.5rem;
  color: var(--muted);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.queue__name::before { content: " · "; }

.queue__list { list-style: none; margin: 0; padding: 0; }

.queue__item { border-bottom: 1px solid var(--border); }

.queue__item:last-child { border-bottom: 0; }

.queue__button {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  width: 100%;
  padding: 0.4rem 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.queue__item--current .queue__title { color: var(--accent); font-weight: 600; }

.queue__subtitle { color: var(--muted); font-size: 0.8rem; }

.player__toggle {
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  border: 0;
  border-radius: 50%;
  background: var(--accent);
  cursor: pointer;
  position: relative;
}

/* Two bars when playing, a triangle when not — drawn rather than iconed, the
   same trick as the play buttons. */
.player__toggle::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  translate: -40% -50%;
  border-top: 6px solid transparent;
  border-bottom: 6px solid transparent;
  border-left: 10px solid var(--surface);
}

.player--playing .player__toggle::before {
  translate: -50% -50%;
  width: 10px;
  height: 12px;
  border: 0;
  background: linear-gradient(
    to right,
    var(--surface) 0 3px,
    transparent 3px 7px,
    var(--surface) 7px 10px
  );
}

.player__what { display: flex; flex-direction: column; min-width: 0; flex: 1; }

.player__title { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.player__subtitle { color: var(--muted); font-size: 0.8rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.player__time { color: var(--muted); font-size: 0.8rem; font-variant-numeric: tabular-nums; }

/* A round thumb on a thin track, rather than the browser's default pill.
 *
 * Styling the thumb at all means `appearance: none`, which also throws away the
 * filled-to-here portion browsers draw for free — so the fill is a gradient,
 * with `--progress` set by the player as it plays. */
/* Elapsed, seek and duration as one unit. It takes the growth the seek input
   used to claim directly, so the wide layout is unchanged — and it gives the
   narrow one something to move as a whole. */
.player__scrubber {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex: 2;
  min-width: 0;
}

.player__seek {
  flex: 1;
  min-width: 6rem;
  height: 4px;
  appearance: none;
  -webkit-appearance: none;
  border-radius: 999px;
  cursor: pointer;
  /* The unplayed portion is mixed from `--text`, **not `--border`**. A border
     colour is meant to sit quietly against its surface, which is exactly wrong
     for a track: in dark mode `--border` (#35322a) on `--surface` (#201e18) is
     about 1.5:1 and simply isn't there. Mixing the text colour instead adapts to
     both themes — dark ink on a light surface, light ink on a dark one — so the
     track is visible either way.

     It shows worst early in a take, when nearly the whole bar is this colour and
     the accent is a sliver. */
  background: linear-gradient(
    to right,
    var(--accent) 0 var(--progress, 0%),
    color-mix(in srgb, var(--text) 40%, transparent) var(--progress, 0%) 100%
  );
}

.player__seek::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  width: 12px;
  height: 12px;
  border: 0;
  border-radius: 50%;
  background: var(--accent);
}

.player__seek::-moz-range-thumb {
  width: 12px;
  height: 12px;
  border: 0;
  border-radius: 50%;
  background: var(--accent);
}

/* Firefox draws its own track under the gradient unless it's transparent. */
.player__seek::-moz-range-track { background: transparent; }

/* Room for the bar, so the last row of a list isn't stuck underneath it. */
.page { padding-bottom: 5rem; }

/* On a narrow screen the scrubber moves to its own row rather than disappearing.
   It used to be `display: none` here, on the reasoning that pausing matters more
   than seeking — but the whole bar is only ~230px of controls and gaps before
   the title, so on a phone the seek bar simply never appeared, and there was no
   way to scrub at all.

   `order` is what does the work: everything else defaults to 0, so ordering this
   after them puts it last, and a 100% basis forces the wrap onto its own line. */
@media (max-width: 600px) {
  .player__bar {
    flex-wrap: wrap;
    row-gap: 0.5rem;
  }

  .player__scrubber {
    order: 1;
    flex: 1 1 100%;
  }

  /* A 4px track is a fine mouse target and a poor thumb one, so padding grows
     the hit area while `background-clip` keeps the painted track its original
     size.

     **`box-sizing: content-box` is what makes that work.** The global reset sets
     `border-box`, under which `height: 4px` *includes* the 24px of padding —
     so the content box collapses to zero, and clipping the gradient to it paints
     nothing at all. The track simply disappeared on phones, while every
     computed style still said it was there. */
  .player__seek {
    height: 6px;
  }

  .player__seek::-webkit-slider-thumb { width: 18px; height: 18px; }
  .player__seek::-moz-range-thumb { width: 18px; height: 18px; }

  /* The waveform costs a row 136px it doesn't have on a phone — the title wraps
     to two lines and the meta wraps after it. Hidden here, unlike the scrubber
     above: **a scrubber is a control** and without it there is no way to seek at
     all, while a waveform is decoration whose absence costs nothing. That
     distinction is the whole reason the old rule hiding the scrubber was wrong,
     so it's worth not blurring it.

     Scoped to list rows: a waveform beside a page title has the width to spare. */
  .list__item .waveform { display: none; }

  /* The practice start time, for the same reason: it pushed the take count into
     wrapping and it's on the practice's own page anyway. Decoration on this
     screen, not a control. */
  .list__item .list__note { display: none; }
}

/* ----- Play buttons -----
 *
 * Filled means a best take, outline means any other. One rule, everywhere a
 * take can be played — see `play_button`.
 */

.play {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 1px solid var(--accent);
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.play:hover { opacity: 0.75; }

/* The triangle, drawn with borders so there's no icon font or SVG to ship. */
.play__icon {
  width: 0;
  height: 0;
  /* Nudged right: a triangle's visual centre isn't its bounding box's. */
  margin-left: 2px;
  border-top: 5px solid transparent;
  border-bottom: 5px solid transparent;
  border-left: 8px solid var(--accent);
}

.play--best { background: var(--accent); }

/* ----- Studio takes -----
 *
 * **Orange means studio**, layered on top of the filled/outline rule rather
 * than replacing it: filled still means "this is what the song plays", and the
 * colour says where it was recorded. So a song whose best take is a studio one
 * gets a filled orange button, and a studio take that *isn't* the best gets an
 * orange outline.
 */
.play--studio { border-color: var(--studio); }
.play--studio .play__icon { border-left-color: var(--studio); }
.play--studio.play--best { background: var(--studio); }
.play--studio.play--best .play__icon { border-left-color: var(--surface); }

/* The title of whatever that button plays, matched to it.

   A sibling selector rather than a class on the link: the button already knows
   whether its take is a studio one, and every row puts the two next to each
   other. So the colours are tied together by construction — there's no second
   place to remember, and a row can't end up with an orange button and a blue
   title. */
.play--studio ~ a { color: var(--studio); }

/* And the shape of the take it plays. Same sibling relationship, which holds in
   every row that draws one: the button, then the title, then the waveform. */
.play--studio ~ .waveform { stroke: color-mix(in srgb, var(--studio) 55%, transparent); }

.list__item--playing .play--studio ~ .waveform { stroke: var(--studio); }

.play--best .play__icon { border-left-color: var(--surface); }

/* A heavier ring on the outline versions, so a non-best take reads as
   deliberately different rather than as a faint version of the filled one. */
.play--other,
.play--empty { border-width: 2px; }

/* Nothing to play yet. Present rather than absent, so a row without a take
   still lines up with the rows that have one. */
.play--empty {
  border-color: var(--border);
  cursor: default;
}

.play--empty .play__icon { border-left-color: var(--border); }

/* Whatever is playing right now shows a pause button instead, wherever it
   appears — the player marks it, including after a Turbo navigation. */
.play--playing .play__icon {
  margin-left: 0;
  width: 8px;
  height: 10px;
  border: 0;
  background: linear-gradient(
    to right,
    var(--accent) 0 3px,
    transparent 3px 5px,
    var(--accent) 5px 8px
  );
}

.play--best.play--playing .play__icon {
  background: linear-gradient(
    to right,
    var(--surface) 0 3px,
    transparent 3px 5px,
    var(--surface) 5px 8px
  );
}

/* ----- Row "…" menu -----
 *
 * A <details>, so it opens, closes on Escape and is keyboard-reachable with no
 * JavaScript. The panel is absolutely positioned, so the row doesn't grow when
 * it opens.
 */

/* No `margin-left: auto` here. `.list__meta` already has one, and two auto
   margins split the free space between them — which parks the duration in the
   middle of the row instead of beside the trigger. The row's own gap is the
   only space wanted between them. */
.menu { position: relative; }

.menu__trigger {
  list-style: none;
  cursor: pointer;
  border-radius: var(--radius);
  color: var(--muted);
}

/* Safari draws its own marker unless both of these are turned off. */
.menu__trigger::-webkit-details-marker { display: none; }

.menu__trigger:hover { background: var(--border); color: var(--text); }

/* The three dots are **drawn**, not typed. A "…" glyph sits on the baseline, so
   in a padded box it reads as bottom-heavy however the box is centred; one dot
   plus two box-shadows is exactly centred by construction. Same technique as the
   play triangle and pause bars. */
.menu__trigger--dots {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
}

.menu__trigger--dots::after {
  content: "";
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: currentcolor;
  box-shadow: -6px 0 currentcolor, 6px 0 currentcolor;
}

/* The account menu wears a face and a name rather than an ellipsis. */
.menu__trigger--user {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.25rem 0.5rem;
  font-size: 0.9rem;
}

.menu--user .menu__panel { min-width: 9rem; }

.menu__panel {
  position: absolute;
  top: 100%;
  right: 0;
  z-index: 20;
  min-width: 13rem;
  margin-top: 0.25rem;
  padding: 0.35rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: 0 6px 20px rgb(0 0 0 / 12%);
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.menu__item,
.menu__panel form { margin: 0; }

.menu__item button,
.menu__item {
  width: 100%;
  padding: 0.4rem 0.5rem;
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.menu__item:hover { background: var(--border); }

.menu__item--danger { color: #c0392b; }

.menu__form {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  padding: 0.4rem 0.5rem;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}

.menu__label { flex: 1 0 100%; color: var(--muted); font-size: 0.8rem; }

.menu__select {
  flex: 1;
  min-width: 0;
  padding: 0.25rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 0.85rem;
}

/* ----- List total ----- */

.list-total {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  margin: 0.75rem 0 0;
  padding: 0.5rem 0.75rem;
  color: var(--muted);
  font-size: 0.9rem;
}

.list-total strong { color: var(--text); font-variant-numeric: tabular-nums; }

.list-total__note { opacity: 0.8; }

/* ----- Next practice ----- */

.next-practice {
  padding: 1rem 1.25rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 1.5rem;
}

.next-practice__label {
  margin: 0 0 0.15rem;
  color: var(--muted);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.next-practice__date { margin: 0 0 0.15rem; font-size: 1.25rem; font-weight: 600; }

.next-practice__date a { text-decoration: none; }

.next-practice--empty { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }

.next-practice--empty .next-practice__label { margin: 0; }

/* ----- Activity grid -----
 *
 * A year as a column per week, seven days down. A practice is the larger
 * circle; listens are a dot drawn over it, in three sizes.
 */

/* Two rows: the month labels, then the weeks. Explicit `grid-column` on both
   keeps a label above the week it belongs to. */
/* Columns come from an inline `grid-template-columns` built by `activity_grid`,
   not from this file: every week is one dot wide, with a narrow blank track
   before each month so the months read as groups.

   That grouping used to happen by accident — columns were implicit and
   auto-sized, so a column holding "Aug" grew to fit the text and shoved
   everything after it along. Pinning the widths fixed the labels, which were
   never misaligned, and removed the grouping, which was the part worth keeping.
   `test/system/activity_grid_test.rb` measures both. */
.activity {
  display: grid;
  grid-auto-flow: column;
  grid-template-rows: auto auto;
  /* Split, not `gap: 3px`: the column gap is the spacing between weeks and is
     load-bearing — it sets the dot rhythm and combines with the blank month
     tracks to make the 13px month break. Only the row gap, between the labels
     and the first row of dots, wanted more room. */
  column-gap: 3px;
  row-gap: 5px;
  overflow-x: auto;
  padding-bottom: 0.25rem;
}

/* Anchored to the left edge of its week column, so the name sits above the week
   the month starts in. It's wider than its 11px column and deliberately
   overflows to the right — the explicit template is what stops that overflow
   from widening the column. */
.activity__month {
  grid-row: 1;
  justify-self: start;
  color: var(--muted);
  font-size: 0.65rem;
  line-height: 1;
  white-space: nowrap;
}

/* ----- Waveform ----- */

/* Sits inline beside a title, so it must never push the row around: a fixed
   box, no shrinking below it, and `preserveAspectRatio="none"` on the SVG lets
   the bar units stretch to whatever width this sets.

   Only drawn where a *single* take is what plays. A practice or playlist row
   queues many takes, and one take's shape would misrepresent the rest. */
.waveform {
  flex: none;
  stroke: color-mix(in srgb, var(--accent) 55%, transparent);
  /* Must equal WAVEFORM_BAR_WIDTH in ApplicationHelper. That constant and
     WAVEFORM_BAR_GAP space the bars 4px apart; this draws them 2px wide,
     leaving the 2px gap. Change one without the other and the bars either
     touch or drift apart. */
  stroke-width: 2;
  stroke-linecap: round;
}

/* No `width` or `height` here on purpose — `waveform_svg` emits both inline.
   The width is the bar count times the pitch, and the height is what the bar
   maths is built around, so neither may drift from the helper. */

/* Whatever is playing gets the full-strength accent, matching the way its play
   button flips to a pause icon. */
.list__item--playing .waveform { stroke: var(--accent); }

.activity__week { grid-row: 2; display: grid; grid-template-rows: repeat(7, 1fr); gap: 3px; }

.activity__day {
  position: relative;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: var(--border);
}

/* Days before the range starts, so the first column keeps its weekday rows. */
.activity__day--blank { background: transparent; }

.activity__day--practice { background: var(--accent); }

.activity__listens {
  position: absolute;
  top: 50%;
  left: 50%;
  translate: -50% -50%;
  border-radius: 50%;
  /* The brand colour rather than ink: listening is the same kind of event as a
     practice, so it should read as part of the same palette. Slightly
     translucent so the empty-day circle underneath still shows through and the
     dot reads as sitting *on* the day rather than replacing it. */
  background: color-mix(in srgb, var(--accent) 70%, transparent);
}

.activity__listens--1 { width: 3px; height: 3px; }
.activity__listens--2 { width: 5px; height: 5px; }
.activity__listens--3 { width: 7px; height: 7px; }

/* On a practice day the dot sits on the accent circle, so it stays the surface
   colour — accent on accent would be invisible, which is exactly what makes the
   two rules look redundant and why they aren't. Opaque, too: a translucent one
   would pick up the blue underneath and wash out. */
.activity__day--practice .activity__listens { background: var(--surface); }

/* ----- Whole-window drop target (the band page) -----
 *
 * Hidden until a drag carrying files enters the window — see
 * drop_overlay_controller.js. The `[hidden]` reset at the top of this file is
 * what makes that stick, despite `display: flex` below.
 */

/* Two halves, so a drop anywhere lands on one of them and the choice is made
   by where you let go rather than by a setting. The studio half carries the
   same orange the studio takes themselves use. */
.drop-overlay__zone {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px dashed transparent;
  border-radius: var(--radius);
  transition: background-color 0.1s ease, border-color 0.1s ease;
}

.drop-overlay__zone--active {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

.drop-overlay__zone--studio.drop-overlay__zone--active {
  border-color: var(--studio);
  background: color-mix(in srgb, var(--studio) 12%, transparent);
}

/* The panels are labels, not targets — the zone around them is what catches the
   drop, and a pointer over the text must still count as over its half. */
.drop-overlay__zone .drop-overlay__panel { pointer-events: none; }

/* The studio half's label wears the studio colour, so the box, the zone
   highlight and the play buttons on the takes it creates all say the same
   thing. */
.drop-overlay__zone--studio .drop-overlay__panel { border-color: var(--studio); }

.drop-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  /* **`stretch`, not `center`.** The two zones have to tile the overlay
     completely — centring them leaves bands of overlay above and below that
     belong to no zone, where a drop would hit the fallback instead of the half
     the pointer is plainly over. */
  align-items: stretch;
  justify-content: center;
  gap: 1rem;
  padding: 2rem;
  background: color-mix(in srgb, var(--bg) 85%, transparent);
}

.drop-overlay__panel {
  max-width: 26rem;
  padding: 2rem;
  /* Solid. The dashed idiom belongs to the *hit area* around it — this box is a
     label explaining what dropping here does, not the target itself. */
  border: 2px solid var(--accent);
  border-radius: var(--radius);
  background: var(--surface);
  text-align: center;
}

.drop-overlay__panel strong { display: block; margin-bottom: 0.5rem; font-size: 1.2rem; }

.drop-overlay__panel span { color: var(--muted); font-size: 0.9rem; }

/* Off-screen but still focusable and still announced — unlike `display: none`,
   which would drop it out of the tab order. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.dropzone__prompt strong { display: block; }
.dropzone__prompt span { color: var(--muted); font-size: 0.9rem; }

/* Kept in the layout (and focusable) rather than hidden: it's the control that
   actually carries the files, and hiding it breaks keyboard use. */
.dropzone__input { max-width: 100%; }

.dropzone__files {
  list-style: none;
  margin: 1rem 0 0;
  padding: 0;
  color: var(--muted);
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
}

/* ----- Upload errors ----- */

.flash--errors { background: color-mix(in srgb, #c0392b 15%, transparent); }
.flash--errors ul { margin: 0.35rem 0 0; padding-left: 1.1rem; }
.flash--errors li { font-variant-numeric: tabular-nums; }

/* A second-tier inline form (moving a take), quieter than the primary one. */
.inline-form--secondary { align-items: center; }

.inline-form__select {
  padding: 0.3rem 0.4rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 0.8rem;
  max-width: 16rem;
}

/* `link_to` with a turbo method renders an <a>, which needs the button look. */
a.button { display: inline-block; text-decoration: none; }

/* Actions beside a page title. */
.page__actions { display: flex; align-items: center; gap: 0.5rem; flex: none; }

/* A quiet annotation next to a list item's link. */
.list__note { color: var(--muted); font-size: 0.85rem; }

/* A row that links somewhere real but has nothing to play yet. Muted to match
   the greyed play placeholder beside it, while staying a link — `.list__item a`
   sets the accent colour, so this has to override it rather than simply not set
   one. Hover still underlines, because it is still a link. */
.list__link--quiet { color: var(--muted); }
