/* Mobile first: the base rules are the phone layout, and the media queries
   only add to it as there is room. Tokens (colour, type, spacing, radius,
   dimensions) live in tokens.css. */

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--ground);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
  line-height: 1.5;
  -webkit-text-size-adjust: 100%;
}

/* A list row that outgrows the viewport drags the whole page sideways, and
   every element then reads as shifted rather than as one row being too wide.
   This is a backstop; rows are expected to truncate on their own. */
html, body { overflow-x: hidden; }

.unit {
  max-width: var(--unit-max);
  margin: 0 auto;
  padding: var(--page-pad-top) max(var(--page-gutter), env(safe-area-inset-left)) var(--page-pad-bottom);
}

.chassis-frame { position: relative; }

/* Above the seam: flat, hard edges, no depth between parts. The unit as a
   whole casts one shadow because it is an object sitting on the page. */
.chassis {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  box-shadow: var(--shadow-unit);
}

/* A real, focusable HTML control laid over the chassis's top-right corner.
   Everything inside the chassis SVG itself is aria-hidden, so search cannot
   live there. Flat and greyscale like the rest of the chassis; no glow,
   since it is not an emissive part of the unit. */
.chassis-search {
  position: absolute;
  /* The row this mark sits on scales with the chassis, so the offset is in
     the chassis's units: 7.6 is the live lamp's cy, 78 the viewBox height
     the frame's own height resolves from. Centred on the row rather than
     hung from it because a 44px target cannot start below a row 11px down;
     the overhang past the panel's top edge is the point, not a bug. */
  top: calc(7.6 / 78 * 100%);
  right: var(--space-4);
  transform: translateY(-50%);
  min-width: var(--tap);
  min-height: var(--tap);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  background: none;
  border: none;
  color: var(--faint);
  text-decoration: none;
}

.chassis-search svg { width: var(--icon); height: var(--icon); }

/* Sized in user units, so it shrinks and stays legible with the viewBox
   rather than at a fixed rem size unrelated to the SVG's own coordinate
   space. */
.chassis__wordmark {
  /* stylelint-disable-next-line scale-unlimited/declaration-strict-value */
  font-size: 4.2px;
  font-weight: 600;
  /* stylelint-disable-next-line scale-unlimited/declaration-strict-value */
  letter-spacing: 1.9px;
  fill: var(--accent);
}

/* Sized in user units, so it shrinks with the viewBox. On a narrow screen the
   smallest labels stop being legible and are simply dropped; they are
   decorative, and every fact they show is real text further down the page. */
.chassis__micro {
  /* stylelint-disable-next-line scale-unlimited/declaration-strict-value */
  font-size: 3.5px;
  /* stylelint-disable-next-line scale-unlimited/declaration-strict-value */
  letter-spacing: 1.2px;
  fill: var(--chassis-label);
  display: none;
}

.chassis__driver { fill: var(--accent); }
.chassis__spine { fill: var(--accent); }
.chassis__needle { stroke: var(--accent); }
.chassis--empty .chassis__driver { fill: var(--chassis-unlit); }
.chassis--empty .chassis__needle { stroke: var(--chassis-needle-unlit); }

/* Only lit segments carry the signal palette. */
.chassis__lamp { fill: var(--signal); }
.chassis__lamp--peak { fill: var(--signal-peak); }
.chassis__live { fill: var(--signal); }
.chassis--empty .chassis__live { fill: var(--chassis-live-unlit); }

/* The output meter, laid over the rendered pattern and dark until a column is
   lit. The two halves run outward from the centre column, so the peak end of
   each is an end of the strip: counted from either end rather than named, so
   the run can change length without this having to hear about it. */
.chassis__meter-column { fill: var(--signal); visibility: hidden; }
.chassis__meter-column--lit { visibility: visible; }

.chassis__meter-column:nth-child(-n + 2),
.chassis__meter-column:nth-last-child(-n + 2) { fill: var(--signal-peak); }

.chassis__readout[data-metering] .chassis__lamps { display: none; }

.banks {
  display: flex;
  background: var(--rail);
  border-bottom: 1px solid var(--line);
}

.bank {
  flex: 1;
  min-height: var(--tap);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-2xs);
  letter-spacing: var(--track-caps);
  color: var(--faint);
  text-decoration: none;
  border-bottom: 2px solid transparent;
}

.bank--active { color: var(--text); border-bottom-color: var(--accent); }

/* In tracklist mode the banks have no chooser to switch, so the strip carries
   the one control that still applies. */
.banks--loaded { justify-content: flex-start; }
.bank--eject { flex: 0 0 auto; padding: 0 var(--space-9); color: var(--muted); }

/* PLAY and NEXT are buttons where the rest of the rail is links, so they
   need the browser's button chrome taken back off. No size or tracking with
   it: these two hold a glyph rather than a word. */
.bank--play, .bank--next {
  flex: 0 0 auto;
  min-width: var(--tap);
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  font: inherit;
  color: var(--text);
  cursor: pointer;
}

/* One cluster, the way the two faces sit on the hardware: tight on the sides
   they face each other across, the rail's own step on the outer ones, so the
   distance to eject is unchanged and is what sets the pair apart. Trimming
   this is what the floor above is there to survive. */
.bank--play { padding: 0 var(--space-3) 0 var(--space-9); }
.bank--next { padding: 0 var(--space-9) 0 var(--space-3); }

/* The glyph and the ring it sits in. The ring is the whole state readout of
   the transport, so it is a real element rather than a border on the button:
   the tap target is 44px and a ring that size would swallow the rail. */
.bank__lamp {
  flex: none;
  width: var(--transport);
  height: var(--transport);
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--text-unlit);
  border-radius: var(--radius-pill);
}

.bank__glyph { flex: none; width: var(--icon-lg); height: var(--icon-lg); }

/* Eject is the one glyph on the rail without a seat, and it is square, so it
   keeps the icon scale above. */
.bank__lamp .bank__glyph { width: auto; height: var(--transport-glyph); }

/* Green is live, the same green the chassis lamps take. Flashing is the
   fetch and the decode in front of a track that has not started yet, which
   otherwise reads as a button that did nothing. */
.bank--play[data-ring] { color: var(--signal); }
.bank--play[data-ring="buffering"] .bank__lamp { animation: lamp-flash 0.6s steps(1, end) infinite; }

/* Cue amber, the colour the incoming row marker already takes. Steady is the
   transition happening now; a flash is one waiting on the boundary the
   attribute names, and the two rates are far enough apart to be read across
   a room. */
.bank--next[data-transition],
.bank--next[data-sounding] { color: var(--signal-peak); }

.bank--next[data-transition="section"] .bank__lamp { animation: lamp-flash 1.4s steps(1, end) infinite; }

.bank--next[data-transition="phrase"] .bank__lamp,
.bank--next[data-transition="beat"] .bank__lamp { animation: lamp-flash 0.45s steps(1, end) infinite; }

.bank--next[data-sounding] .bank__lamp { animation: none; opacity: 1; }

.bank--play[data-ring] .bank__lamp,
.bank--next[data-transition] .bank__lamp,
.bank--next[data-sounding] .bank__lamp { border-color: currentColor; }

/* Stepped, not eased. A lamp is on or it is off, and a fade would read as a
   pulse of its own rather than as a light being switched. */
@keyframes lamp-flash {
  0% { opacity: 1; }
  50% { opacity: 0.2; }
  100% { opacity: 1; }
}

/* Turned down, the same states are levels rather than rates. Three of them,
   as far apart as three flash rates are. */
@media (prefers-reduced-motion: reduce) {
  .bank--play[data-ring="buffering"] .bank__lamp,
  .bank--next[data-transition] .bank__lamp { animation: none; }

  .bank--play[data-ring="buffering"] .bank__lamp { opacity: 0.5; }

  /* A called transition carries the rung it was called at into its own
     window, so the steady state has to be spelled out of these rather than
     left to the order they are written in. */
  .bank--next[data-transition="section"]:not([data-sounding]) .bank__lamp { opacity: 0.5; }

  .bank--next[data-transition="phrase"]:not([data-sounding]) .bank__lamp,
  .bank--next[data-transition="beat"]:not([data-sounding]) .bank__lamp { opacity: 0.75; }
}

/* Disabled means the cut cannot land cleanly yet, nothing more. It reads as
   the same control gone quiet rather than a different one, and it comes last
   because a lit disabled bank is not a state the engine reaches: where the
   two collide the quiet one wins, at any motion setting. */
.bank--next:disabled { color: var(--text-disabled); cursor: default; }

.bank--next:disabled .bank__lamp {
  border-color: var(--text-unlit);
  animation: none;
  opacity: 1;
}

/* Below the seam: dimensional, soft, airy. */
.data { padding-top: var(--gap-data); }

.seed {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  padding-bottom: var(--gap-seed);
}

.seed__label {
  margin: 0 0 var(--space-2);
  font-size: var(--text-2xs);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--faint);
}

.seed__title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-2xl);
  line-height: 1.2;
  letter-spacing: var(--track-tight);
}

.seed__meta { margin: 0; color: var(--muted); font-size: var(--text-base); }

/* States where the running order came from rather than what Duke picked, so
   it never reads as a weave a person can't tell from any other. */
.seed__provenance { margin: var(--space-1) 0 0; color: var(--faint); font-size: var(--text-xs); font-style: italic; }

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

.sequence__reorder-form { display: none; }

.row {
  --row-gap: var(--space-5);
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--row-gap);
  padding: var(--space-4) var(--space-3);
  border-radius: var(--radius-md);
}

.row__no {
  font-variant-numeric: tabular-nums;
  font-size: var(--text-xs);
  color: var(--faint);
  min-width: var(--space-9);
}

/* Which rows play is stated by the number, lit or dark, and nowhere else.
   The row itself stays fully legible either way: a track with no file is
   still a track, and dimming its title would read as a broken row. */
.row--unplayable .row__no { color: var(--text-unlit); }
.row--playing .row__no { color: var(--signal); }
.row--playing { background: var(--panel); }

/* Which of the two decks a row is on, in the slot its number sits in. The
   number is set in tabular digits and the slot is wider than two of them, so
   one letter takes the same space and nothing to its right moves. It carries
   whichever colour the number already had, green sounding and amber incoming,
   and only gains the weight two digits had between them. */
.row__deck { display: none; font-weight: 600; }
.row__no--deck .row__position { display: none; }
.row__no--deck .row__deck { display: inline; }

/* The row the transition is aimed at, once it has been called or has begun.
   Cue amber against the sounding row's green, which is the colour the
   transport's own lamp takes for a called transition: one colour for one
   idea, either side of the seam. A ring rather than a panel, so at a glance
   the lit row is still the one making sound, and the two markers are never
   the same shape. */
.row--incoming { box-shadow: var(--ring-cue); }
.row--incoming .row__no { color: var(--signal-peak); }

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

/* A button standing in for what used to be a plain span, reset back to it. */
.row__trigger {
  min-height: var(--tap);
  justify-content: center;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

/* Font-relative on purpose: the gap between the title and the mismatch
   badge tracks the text they sit beside, not the spacing scale. */
/* stylelint-disable-next-line scale-unlimited/declaration-strict-value */
.row__line { display: flex; align-items: baseline; gap: 0.4em; min-width: 0; }
.row__line .row__title { flex: 0 1 auto; }

.row__title, .row__credits {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.row__title { font-size: var(--text-md); }
.row__meta { display: flex; align-items: baseline; gap: var(--space-3); font-size: var(--text-xs); color: var(--muted); }
.row__credits { min-width: 0; }

/* How long the track runs, and while the row is sounding, how long is left of
   it. Right edge of the meta line, where it is out of the title's way and
   still on the row it describes. Tabular figures in a slot wide enough for
   m:ss, so neither the flip from length to countdown nor the ticking after it
   moves anything. */
.row__time {
  flex: none;
  margin-left: auto;
  min-width: 4ch;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* Five characters held all the way down to 0:00, rather than the slot
   narrowing by one the moment a long track's countdown crosses 9:59. */
.row__time--long { min-width: 5ch; }

/* The same two colours the number takes either side of the seam, green
   sounding and amber incoming, because through a window both rows are counting
   down and the figure has to agree with the number beside it. */
.row--playing .row__time { color: var(--signal); }
.row--incoming .row__time { color: var(--signal-peak); }

/* Reads as a stretch, not a failure: honest feedback on a person's own
   reorder or append, never on a row Duke chose itself. Reuses the signal
   palette's peak amber rather than adding a sixth colour to the interface:
   the same "needs attention" meaning the peak lamp already carries. */
.row__mismatch {
  flex: none;
  /* Font-relative on purpose: the badge tracks the title's own size. */
  /* stylelint-disable-next-line scale-unlimited/declaration-strict-value */
  font-size: 0.75em;
  font-weight: 600;
  letter-spacing: var(--track-slight);
  color: var(--signal-peak);
}

/* Dots and nothing else: no background, no border, no box. It sits between
   the row body and the grip, centred on the row rather than on its glyph's
   baseline, which is why the midline ellipsis is used over an ordinary one. */
.row__menu-toggle {
  flex: none;
  width: var(--tap);
  min-height: var(--tap);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  background: none;
  border: none;
  font-size: var(--icon);
  line-height: 1;
  color: var(--faint);
  cursor: pointer;
}

.row__menu-toggle[aria-expanded="true"] { color: var(--text); }

/* Anchors the menu to the toggle's own box rather than the row: sized to
   hug the 44px toggle, not stretched to the row's width. */
.row__menu-anchor {
  position: relative;
  flex: none;
  display: flex;
}

/* Always visible (dragging has no hidden affordance on desktop) and sized
   to the tap-target floor even though the glyph itself is small. */
.row__grip {
  flex: none;
  width: var(--tap);
  min-height: var(--tap);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--faint);
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  cursor: grab;
  /* Repositions a full-size tap box over the row's gap and right padding;
     do not trade this for a smaller width, --tap is the accessibility floor. */
  margin-left: calc(-1 * var(--row-gap));
  margin-right: calc(-1 * var(--space-7));
}

.row__grip--pinned { visibility: hidden; }

.row--dragging { background: var(--panel); opacity: 0.9; }

/* Hung under the toggle that opens it, via the anchor wrapper, sized to its
   own items rather than stretched across the row. A full-width panel reads
   as a section of the page instead of a menu belonging to one line. */
.row__menu {
  position: absolute;
  top: 100%;
  right: 0;
  min-width: var(--menu-min);
  z-index: 5;
  margin-top: var(--space-1);
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-float);
  overflow: hidden;
}

.row__menu[hidden] { display: none; }

.row__menu-form { margin: 0; }

.row__menu-item {
  width: 100%;
  min-height: var(--tap);
  padding: 0 var(--space-7);
  display: flex;
  align-items: center;
  background: none;
  border: none;
  border-top: 1px solid var(--line);
  font: inherit;
  font-size: var(--text-base);
  color: var(--text);
  text-align: left;
  cursor: pointer;
}

.row__menu-item:first-child { border-top: none; }
.row__menu-item--danger { color: var(--accent); }

/* Takes the bank rail's own row while searching, so it carries the rail's
   chrome (same background and seam) rather than the airy spacing used
   below the seam. */
.search {
  display: flex;
  align-items: center;
  background: var(--rail);
  border-bottom: 1px solid var(--line);
  padding: var(--space-3) var(--space-4);
}

.search__form { position: relative; display: flex; flex: 1; }

/* Keyboard focus has to be visible, and the browser default is a bright blue
   ring on an interface that is otherwise greyscale and one accent. Stated
   once here rather than per control, and only for keyboard focus so a mouse
   click does not leave a ring behind. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-xs);
}

/* Padded on both sides for the close chevron and clear button that sit
   inside the box as prefix/suffix icons. The right side reserves the
   space whether or not the clear button is currently shown, so its
   appearance never shifts the input text. */
.search__input {
  flex: 1;
  min-width: 0;
  min-height: var(--tap);
  padding: 0 var(--tap);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  color: var(--text);
  font-size: var(--text-base);
}

.search__close,
.search__clear {
  position: absolute;
  top: 0;
  min-width: var(--tap);
  min-height: var(--tap);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  background: none;
  border: none;
  color: var(--faint);
  text-decoration: none;
  cursor: pointer;
}

.search__close { left: 0; }
.search__clear { right: 0; }
.search__close svg, .search__clear svg { width: var(--icon); height: var(--icon); }
.search__clear[hidden] { display: none; }

.search__hint { margin: 0; color: var(--muted); font-size: var(--text-base); }

.search__group + .search__group { margin-top: var(--space-8); }

/* Sticky so the heading stays readable while its own group scrolls past;
   the backdrop is opaque because result rows pass underneath it. */
.search__group-heading {
  position: sticky;
  top: 0;
  z-index: 1;
  margin: 0 0 var(--space-2);
  padding: var(--space-2) var(--space-4);
  background: var(--ground);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--muted);
}

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

.search__result-button {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-height: var(--tap);
  justify-content: center;
  padding: var(--space-4);
  background: none;
  border: none;
  border-radius: var(--radius-md);
  font: inherit;
  color: var(--text);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

.search__result-title, .search__result-meta {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.search__result-title { font-size: var(--text-md); }
.search__result-meta { font-size: var(--text-xs); color: var(--muted); }

.search__result-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.search__result-form { margin: 0; }

/* The row is the button; this only labels it, so it carries no tap target
   of its own and is sized to sit beside the text rather than compete. */
.search__result-button--enqueue { flex-direction: row; align-items: center; gap: var(--space-4); }
.search__result-button--enqueue .search__result-body { display: flex; flex-direction: column; flex: 1; min-width: 0; }

.search__result-enqueue {
  flex: none;
  padding: var(--space-1) var(--space-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--muted);
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: var(--track-label);
}

.chooser { margin-top: var(--gap-section); }

.chooser__heading {
  margin: 0 0 var(--space-5);
  font-size: var(--text-2xs);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--faint);
  font-weight: 600;
}

/* Borrows .bank's own variables rather than adding new ones: RDI-51
   rewrites this sheet, so this is deliberately minimal. */
.tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin: 0 0 var(--space-6);
}

.tag {
  min-height: var(--tap);
  padding: 0 var(--space-5);
  display: flex;
  align-items: center;
  font-size: var(--text-2xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--faint);
  text-decoration: none;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
}

.tag--active { color: var(--text); border-color: var(--accent); }

.chooser__empty {
  grid-column: 1 / -1;
  margin: 0;
  padding: var(--space-6) 0;
  font-size: var(--text-sm);
  color: var(--muted);
}

/* Font-relative on purpose: the gap before the clear link tracks the
   message's own text size. */
/* stylelint-disable-next-line scale-unlimited/declaration-strict-value */
.chooser__empty-clear { margin-left: 0.4em; color: var(--accent); }

/* The jumper sits in its own column so it stays beside the list rather than
   floating over the tiles. */
.browse {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-4);
  align-items: start;
}

.tiles {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-3);
}

/* The pages are wrapped in frames, which would otherwise become the grid items
   and let their contents size them: a long credit then widens the column and
   drags the whole page sideways. display:contents hands the tiles back to the
   grid. */
.tiles turbo-frame { display: contents; }

/* The lazy frame is the exception, in both directions. While it is waiting it
   needs a real box, because display:contents leaves nothing for Turbo's
   intersection observer to watch and the next page never loads. Once Turbo
   marks it complete its tiles must rejoin the grid, or they size themselves
   and widen the page. */
.tiles turbo-frame.tiles__more {
  display: block;
  grid-column: 1 / -1;
  min-height: 1px;
}

.tiles turbo-frame.tiles__more[complete] { display: contents; }

.tiles__loading { grid-column: 1 / -1; }

.tiles__loading {
  display: block;
  padding: var(--space-6) 0;
  font-size: var(--text-xs);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--faint);
}

.jumper {
  position: sticky;
  top: var(--space-4);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-hair);
  padding: var(--space-1);
  /* The strip is scrubbed with a finger, so the browser must not claim the
     gesture for panning. */
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}

.jumper--dragging { background: var(--panel); border-radius: var(--radius-pill); }

.jumper__key {
  display: block;
  padding: var(--space-hair) var(--space-2);
  font-size: var(--text-2xs);
  line-height: 1.35;
  letter-spacing: var(--track-slight);
  color: var(--faint);
  text-decoration: none;
  border-radius: var(--radius-xs);
}

.jumper__key--active { color: var(--accent); font-weight: 600; }

/* Grid items default to min-width:auto, so a long unbreakable credit would
   otherwise widen its column rather than truncate. */
.tile { min-width: 0; }

.tile__link {
  display: flex;
  flex-direction: column;
  min-width: 0;
  gap: var(--space-1);
  min-height: var(--tap);
  justify-content: center;
  padding: var(--space-5) var(--space-6);
  background: var(--panel);
  border-radius: var(--radius-md);
  text-decoration: none;
  color: var(--text);
}

.tile__title {
  font-size: var(--text-base);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The artist gives way before the label does. Truncating the line as a whole
   loses the label entirely on tracks with several credited artists, which is
   most of this catalogue. */
.tile__meta {
  display: flex;
  align-items: baseline;
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  font-size: var(--text-xs);
  color: var(--muted);
}

/* Scoped to the tile's own meta line rather than a blanket gap on
   .tile__meta: that would also widen the artist/label separator, which
   already carries its own spacing via .tile__label's padding-left. */
.tile__meta > .chip { margin-left: var(--space-3); }

/* One neutral style for every chip, on the tile's meta line or under the
   modal's title. --signal-peak belongs to MISMATCH and --accent to the
   active filter tag; borrowing either would rank a catalogue fact as
   something it is not. */
.chip {
  flex: none;
  display: inline-flex;
  align-items: center;
  padding: 0 var(--space-4);
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: var(--track-label);
  color: var(--text-dim);
  background: var(--chip);
  border-radius: var(--radius-xs);
}

/* Both parts shrink, the artist far more readily, so the label survives a long
   credit without either being allowed to widen the row. A fixed max-width on
   the artist does not work: the label still took its full width beside it and
   the pair overflowed. */
.tile__artist {
  flex: 0 8 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Padding rather than a space in the markup: flex drops leading whitespace,
   so the separator would sit against the truncated artist name. */
.tile__label {
  flex: 0 1 auto;
  min-width: 0;
  /* Font-relative on purpose: the separator's gap tracks the label's own
     text size. */
  /* stylelint-disable-next-line scale-unlimited/declaration-strict-value */
  padding-left: 0.35em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@media (min-width: 30rem) {
  .tiles { grid-template-columns: repeat(auto-fill, minmax(var(--tile-min), 1fr)); gap: var(--space-4); }
}

@media (min-width: 45rem) {
  .unit { padding: var(--page-pad-top) var(--page-gutter) var(--page-pad-bottom); }
  .chassis { box-shadow: var(--shadow-unit-desktop); }
  .chassis__micro { display: initial; }
  .bank { font-size: var(--text-xs); letter-spacing: var(--track-caps); }
  .data { padding-top: var(--gap-data); }
  .seed { flex-direction: row; align-items: flex-start; justify-content: space-between; gap: var(--space-9); padding-bottom: var(--gap-seed); }
  .seed__title { font-size: var(--text-3xl); }
  .seed__meta { font-size: var(--text-md); }
  .row { --row-gap: var(--space-7); padding: var(--space-5); }
  .row__title { font-size: var(--text-lg); }
  .row__meta { font-size: var(--text-sm); }
  .chooser { margin-top: var(--gap-section); }
}

@media (min-width: 45rem) and (hover: hover) {
  .row:hover { background: var(--panel); }
  .tile__link:hover { background: var(--hover); }

  /* The grip stays visible either way, since reordering has no hover-only
     affordance on desktop; hover only tints it. */
  .row__grip:hover { color: var(--text); }
  .chassis-search:hover { color: var(--text); }
  .search__result-button:hover { background: var(--hover); }
  .search__close:hover, .search__clear:hover { color: var(--text); }
  .search__result-enqueue:hover { background: var(--hover); }
}

/* A track's modal: a bottom sheet on every viewport, wide enough not to
   change shape at the desktop breakpoint. Below the seam, like the rest of
   the data side. */
/* A closed dialog is display:none by default, and setting display on the
   element itself overrides that. The sheet then sits on the page whether it
   is open or not. Only an open dialog gets a box. */
.track-modal:not([open]) { display: none; }

.track-modal {
  position: fixed;
  inset: auto 0 0 0;
  width: 100%;
  max-width: var(--sheet-max);
  /* Height comes from the content. The sheet only scrolls once it would
     outgrow the screen, so a two item list is a short sheet rather than a
     tall one padded with space nothing fills.

     vh first so an engine without dvh still gets a bounded sheet rather
     than dropping the declaration and falling back to UA sizing. Same for
     the padding: a dialog's UA default is 1em, so losing the whole
     shorthand to an unsupported env() would put it back. */
  max-height: 100vh;
  max-height: 100dvh;
  margin: 0 auto;
  padding: 0;
  padding-bottom: env(safe-area-inset-bottom);
  border: none;
  border-radius: var(--radius-xl) var(--radius-xl) 0 0;
  background: var(--panel);
  color: var(--text);
  box-shadow: var(--shadow-sheet);
}

/* The dialog is the initial focus target (see show() in
   track_modal_controller.js), and the container itself isn't a control a
   user operates, so it doesn't need a visible ring. Keyboard users still
   get one on whichever tab or control they move to next. */
.track-modal:focus-visible { outline: none; }

.track-modal::backdrop { background: var(--scrim); }

/* Plain block on purpose. As a zero-basis flex child of an auto-height
   dialog, Safari collapsed this to nothing while Chrome sized it from
   content, an empty sheet on every iPhone. The flex chain served the
   pinned footer that RDI-50 removed; nothing needs it now. */
.track-modal__sheet { display: block; }

/* Native <dialog> already blocks pointer interaction with the rest of the
   page while open, but overflow: hidden alone does not stop iOS Safari
   scrolling the page behind it on a touch device. Pinning body in place at
   its prior scroll offset does; show()/unlockBodyScroll() in
   track_modal_controller.js set and clear the offset this needs. */
body.modal-open {
  position: fixed;
  left: 0;
  right: 0;
  width: 100%;
  overflow: hidden;
}

.track-modal__tabs {
  display: flex;
  align-items: center;
  flex: none;
  border-bottom: 1px solid var(--line);
}

.track-modal__tab {
  flex: 1;
  min-height: var(--tap);
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  font-size: var(--text-xs);
  letter-spacing: var(--track-label);
  color: var(--faint);
  cursor: pointer;
}

.track-modal__tab--active { color: var(--text); border-bottom-color: var(--text); }

/* Deliberately larger than the tap-target floor other controls use: this
   is the one way out of the sheet, so it gets the most generous hit area
   on the page. */
.track-modal__close {
  flex: none;
  min-width: var(--tap-max);
  min-height: var(--tap-max);
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  color: var(--faint);
  cursor: pointer;
}

.track-modal__close svg { width: var(--icon-lg); height: var(--icon-lg); }

.track-modal__panel { padding: var(--space-7) var(--space-7) var(--space-9); overflow-y: auto; }

/* Plain block flow: the flex column this replaced pinned the Replace Track
   bar as a footer below the list, but that same chain forced a floor under
   a short list and, on iOS, collapsed the list under the buttons. The bar
   now scrolls with the list instead of staying pinned below it. The
   overflow-y: auto inherited from .track-modal__panel only engages once
   content outgrows the dialog's max-height, so a short list stays short. */
.track-modal__panel--alternatives:not([hidden]) {
  display: block;
}

.track-modal__eyebrow {
  margin: 0 0 var(--space-2);
  font-size: var(--text-2xs);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--faint);
}

.track-modal__title { margin: 0 0 var(--space-2); font-size: var(--text-xl); line-height: 1.25; }
.track-modal__chips { display: flex; gap: var(--space-3); margin: 0 0 var(--space-3); }
.track-modal__meta { margin: 0; color: var(--muted); font-size: var(--text-base); }

.track-modal__status { margin: var(--space-6) 0 0; font-size: var(--text-sm); color: var(--muted); }

/* Deliberately unaccented: the row already carries the alarm, so the panel
   only needs to read clearly at 390px, not draw attention to itself. */
.track-modal__mismatch {
  margin-top: var(--space-6);
  padding-top: var(--space-6);
  border-top: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.track-modal__mismatch p { margin: 0; font-size: var(--text-sm); color: var(--muted); line-height: 1.45; }

.track-modal__mismatch-note { color: var(--faint); }

/* Same border-top-and-gap shape as .track-modal__mismatch above: another
   block that only sometimes renders, stacking below whichever of the
   details panel's other optional blocks preceded it. */
.track-modal__sets {
  margin-top: var(--space-6);
  padding-top: var(--space-6);
  border-top: 1px solid var(--line);
}

.track-modal__sets-heading {
  margin: 0 0 var(--space-4);
  font-size: var(--text-sm);
  color: var(--muted);
}

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

/* The whole row is the link, like .track-modal__alt-label's whole-row radio;
   this one has no selected state to draw, since tapping it always leaves
   the sheet rather than choosing among the rows. */
.track-modal__set-link {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: var(--tap);
  padding: var(--space-4) 0;
  color: inherit;
  text-decoration: none;
}

.track-modal__set-title {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--text-md);
}

.track-modal__set-meta { font-size: var(--text-xs); color: var(--muted); }

.track-modal__empty { margin: 0; color: var(--muted); font-size: var(--text-base); }

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

/* The radio input stays in the DOM, focusable and functional, so the form
   still submits and the control still has a name a screen reader announces.
   It is only visually replaced by the selected state on the row itself. */
.track-modal__alt-radio {
  position: absolute;
  width: 1px;
  height: 1px;
  /* The standard visually-hidden idiom's own -1px offset, not a spacing step. */
  /* stylelint-disable-next-line scale-unlimited/declaration-strict-value */
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* The whole row is the control: no separate radio dot, a border and tint
   mark the checked one instead. */
.track-modal__alt-label {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  min-height: var(--tap);
  padding: var(--space-4);
  border-radius: var(--radius-md);
  border: 1px solid transparent;
  cursor: pointer;
}

.track-modal__alt-label:has(.track-modal__alt-radio:checked) {
  background: var(--selected);
  border-color: var(--text);
}

.track-modal__alt-body { display: flex; flex-direction: column; flex: 1; min-width: 0; }

.track-modal__alt-title, .track-modal__alt-meta {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.track-modal__alt-title { font-size: var(--text-md); }
.track-modal__alt-meta { font-size: var(--text-xs); color: var(--muted); }

/* Observed and suggested alternatives must read as different things: one a
   transition someone actually played, the other Duke's own guess, so a
   filled tag marks the former and an outlined one the latter. */
.track-modal__alt-tag {
  flex: none;
  font-size: var(--text-xs);
  padding: var(--space-1) var(--space-4);
  border-radius: var(--radius-pill);
  max-width: var(--tag-max);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.track-modal__alt-tag--observed { color: var(--text); background: var(--chip); }
.track-modal__alt-tag--suggested { color: var(--muted); background: none; border: 1px solid var(--line); }

.track-modal__bar {
  padding: var(--space-5) 0 var(--space-1);
  background: var(--panel);
  border-top: 1px solid var(--line);
}

.track-modal__replace {
  width: 100%;
  min-height: var(--tap);
  background: var(--text);
  color: var(--ground);
  border: none;
  border-radius: var(--radius-md);
  font-size: var(--text-base);
  font-weight: 600;
  cursor: pointer;
}

/* The same bar the alternatives panel uses, so the one action the details
   panel offers sits where the eye already looks for one. Given the room the
   panel's own blocks take above it, since here it follows running text
   rather than a list that has already ended. Outlined rather than filled:
   Replace Track is the end of a choice somebody was making, and this is a
   jump out of the list. */
.track-modal__panel--details .track-modal__bar { margin-top: var(--space-6); }

.track-modal__play-now {
  width: 100%;
  min-height: var(--tap);
  background: none;
  color: var(--signal);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  font-size: var(--text-base);
  font-weight: 600;
  cursor: pointer;
}

/* Sits below the replace bar rather than inside it: rejecting the track
   outright is a different kind of choice than picking one of the options
   above, and looking like a third alternative in that list would blur it. */
.track-modal__ditch-form { margin: var(--space-4) 0 0; }

.track-modal__ditch {
  width: 100%;
  min-height: var(--tap);
  background: none;
  color: var(--accent);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  font-size: var(--text-base);
  font-weight: 600;
  cursor: pointer;
}

@media (hover: hover) {
  .track-modal__tab:hover { color: var(--text); }
  .track-modal__close:hover { color: var(--text); }
  .track-modal__alt-label:hover { background: var(--hover); }
  .track-modal__set-link:hover { background: var(--hover); }
  .track-modal__replace:hover { background: var(--text-dim); }
  .track-modal__play-now:hover { border-color: var(--signal); }
  .track-modal__ditch:hover { border-color: var(--accent); }
  .row__menu-item:hover { background: var(--hover); }
}
