/* ==========================================================================
   VPlayer · Vane
   A weather-vane instrument panel for music.

   Two rules govern everything below:
   1. Brass never moves. --brass is the brand and stays constant no matter what
      the album art looks like. Only --wind is re-tinted from the cover, and
      only within a clamped lightness range, so the panel never turns to mud.
   2. Angle carries time. There is no progress bar anywhere in this stylesheet.
   ========================================================================== */

:root {
  /* Surfaces — night sky through to instrument housing */
  --ink: #0a0d10;
  --ink-2: #11161b;
  --slate: #1a2128;
  --slate-2: #222c35;

  /* Brass: the vane itself. Fixed. */
  --brass: #c8a24a;
  --brass-hi: #e8ce8a;
  --brass-lo: #8a6f2e;

  /* Wind: the only cover-reactive channel. JS overwrites --wind at runtime. */
  --wind: #6fc5d6;
  --wind-dim: color-mix(in oklab, var(--wind) 40%, var(--ink));

  /* Ink on instruments */
  --paper: #eceff1;
  --gust: #8fa3b0;
  --gust-dim: #4e5b66;
  --hairline: #252f38;

  --danger: #d9705f;

  /* Type. No webfonts — the previous build shipped 41 MB of them.
     Numerals are always mono: this is a gauge, and gauges do not reflow. */
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
    "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", sans-serif;
  --font-num: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Mono",
    Menlo, Consolas, monospace;

  --step--1: 0.75rem;
  --step-0: 0.875rem;
  --step-1: 1rem;
  --step-2: 1.25rem;
  --step-3: 1.625rem;

  --gap-1: 4px;
  --gap-2: 8px;
  --gap-3: 12px;
  --gap-4: 16px;
  --gap-5: 24px;
  --gap-6: 32px;

  --r-sm: 4px;
  --r-md: 8px;
  --r-lg: 14px;

  /* Wind has inertia: it overshoots and settles. Everything moving uses this. */
  --ease-gust: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-settle: cubic-bezier(0.34, 1.42, 0.64, 1);
  --dur-fast: 140ms;
  --dur-base: 260ms;
  --dur-slow: 620ms;

  --shell-max: 1240px;

  /* How much of the docked panel stays visible when collapsed. Declared here
     because the drag handler needs the same number the transform uses, and a
     home indicator must not sit on top of the grab handle. */
  --peek: calc(64px + env(safe-area-inset-bottom, 0px));
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

html,
body {
  height: 100%;
  margin: 0;
  /* Both, not just body: iOS rubber-bands the document while the sheet is being
     dragged, which looks like the page behind coming loose. */
  overscroll-behavior: none;
}

body {
  background: var(--ink);
  /* Grey flashes on tap read as jank on a dark instrument panel. */
  -webkit-tap-highlight-color: transparent;
  color: var(--paper);
  font-family: var(--font-ui);
  font-size: var(--step-0);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior: none;
}

button,
input {
  font: inherit;
  color: inherit;
}

button {
  background: none;
  border: 0;
  cursor: pointer;
  padding: 0;
}

:focus-visible {
  outline: 2px solid var(--brass);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

[hidden] {
  display: none !important;
}

.sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   Instrument voice: uppercase mono microlabels. Used for every reading on the
   panel — bearing, Beaufort, source, track id. Never for prose.
   -------------------------------------------------------------------------- */

.gauge {
  font-family: var(--font-num);
  font-size: var(--step--1);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--gust-dim);
  font-variant-numeric: tabular-nums;
}

.gauge--wind {
  color: var(--wind);
}
.gauge--brass {
  color: var(--brass);
}

.num {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}

/* A soft light that follows the cursor. It is decoration, so it never takes
   pointer events, never appears for touch, and is dropped entirely when the
   viewer has asked for reduced motion. */
.cursor-aura {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 50;
  width: 34px;
  height: 34px;
  border-radius: 999px;
  translate: -50% -50%;
  pointer-events: none;
  background: radial-gradient(circle, rgb(255 255 255 / 0.18), transparent 62%);
  will-change: transform;
  opacity: 0;
  transition: opacity var(--dur-base), width var(--dur-base) var(--ease-gust),
    height var(--dur-base) var(--ease-gust), background var(--dur-base);
}
.cursor-aura.is-on {
  opacity: 1;
}

/* Over the dial it widens and warms, so the light and the needle read as the
   same instrument rather than two unrelated effects. */
.cursor-aura.is-hot {
  width: 76px;
  height: 76px;
  background: radial-gradient(circle, color-mix(in oklab, var(--brass) 34%, transparent), transparent 66%);
}

/* Sky wash: a slow two-point atmospheric field tinted by the cover. It sits
   behind everything and is the only gradient in the build. */
.sky {
  position: fixed;
  inset: 0;
  z-index: -2;
  background:
    radial-gradient(58% 44% at 22% 12%, color-mix(in oklab, var(--wind) 22%, transparent), transparent 70%),
    radial-gradient(48% 52% at 84% 88%, color-mix(in oklab, var(--brass) 10%, transparent), transparent 72%),
    var(--ink);
  transition: background var(--dur-slow) linear;
}

/* --------------------------------------------------------------------------
   Shell — one layout, one DOM. The old build maintained a desktop tree and a
   mobile tree with two virtual scrollers; this is a single grid that reflows.
   -------------------------------------------------------------------------- */

.shell {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  height: 100dvh;
  max-width: var(--shell-max);
  margin-inline: auto;
  padding: 0 var(--gap-4);
}

.mast {
  display: flex;
  align-items: center;
  gap: var(--gap-3);
  padding: var(--gap-4) 0 var(--gap-2);
  min-height: 60px;
}

.mark {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  flex: none;
}

.mark__glyph {
  width: 22px;
  height: 22px;
  border: 1px solid var(--brass);
  border-radius: 50%;
  position: relative;
  flex: none;
}
.mark__glyph::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 2px;
  width: 1px;
  height: 9px;
  background: var(--brass);
  transform: translateX(-50%);
}
.mark__glyph::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 3px;
  width: 5px;
  height: 5px;
  border: 1px solid var(--brass-lo);
  border-radius: 50%;
  transform: translateX(-50%);
}

.mark__word {
  font-family: var(--font-num);
  font-size: var(--step--1);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--paper);
}

.mast__spacer {
  flex: 1;
}

.iconbtn {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  color: var(--gust);
  position: relative;
  transition: color var(--dur-fast), background var(--dur-fast);
}
.iconbtn:hover {
  color: var(--paper);
  background: var(--slate);
}
.iconbtn svg {
  width: 19px;
  height: 19px;
}

/* Sync lamp — rides on the cloud button */
.lamp {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--gust-dim);
  border: 1px solid var(--ink);
}
.lamp[data-state="live"] {
  background: var(--wind);
}
.lamp[data-state="dirty"] {
  background: var(--brass);
}
.lamp[data-state="error"] {
  background: var(--danger);
}

/* --------------------------------------------------------------------------
   Deck: dial on the left, readouts and lyrics on the right.
   -------------------------------------------------------------------------- */

.deck {
  display: grid;
  grid-template-columns: minmax(320px, 0.92fr) minmax(0, 1.08fr);
  gap: var(--gap-6);
  align-items: center;
  min-height: 0;
  padding-bottom: var(--gap-5);
}

.station {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--gap-4);
  min-width: 0;
}

/* ---- The dial ---- */

.dial {
  width: 100%;
  max-width: 380px;
  aspect-ratio: 1;
  touch-action: none;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  /* The dial is round, so its focus ring should be too — an outline follows
     border-radius, and a square ring around a circular instrument reads as a
     rendering artefact. */
  border-radius: 50%;
}

/* The dial is focusable (it is a slider), which means clicking it draws the
   UA's default ring. Keyboard focus still gets a visible brass ring. */
.dial:focus {
  outline: none;
}
.dial:focus-visible {
  outline: 2px solid var(--brass);
  outline-offset: 6px;
}
.dial.is-dragging {
  cursor: grabbing;
}

.dial__tick {
  stroke: var(--hairline);
  stroke-width: 0.5;
}
.dial__tick--major {
  stroke: var(--gust-dim);
  stroke-width: 1;
}

.dial__barb {
  stroke: var(--wind);
  stroke-width: 1.6;
  stroke-opacity: 0.75;
  stroke-linecap: round;
}

.dial__arc {
  fill: none;
  stroke: var(--wind);
  stroke-width: 2.5;
  stroke-linecap: round;
}

.dial__bezel {
  fill: none;
  stroke: var(--slate);
  stroke-width: 0.5;
}

.dial__cardinal {
  font-family: var(--font-num);
  font-size: 11px;
  letter-spacing: 0.1em;
  fill: var(--gust-dim);
}
.dial__cardinal--n {
  fill: var(--brass);
}

.dial__blade {
  fill: var(--ink-2);
}

/* Cover lives inside the turbine hub and freezes on pause — it never resets
   to zero, because a stopped vane holds its bearing.

   Both rotating groups pin their transform semantics explicitly rather than
   relying on UA defaults. They are rotated via the SVG `transform` attribute
   with a baked-in centre, rotate(deg 170 170); `view-box` + a zero origin makes
   that centre mean exactly what it says. Leaving it to the default once let
   `fill-box` resolve the centre against each element's bounding box, which sent
   the hub orbiting a point off the dial. */
.dial__hub,
.dial__vane {
  transform-box: view-box;
  transform-origin: 0 0;
}

/* The mark's path sets no fill, so each <use> decides its own. The gradient is
   in objectBoundingBox space, which means it travels with the mark as the vane
   swings — light stays at the tip, dark stays at the tail, at every bearing.
   The rim is the same glyph fattened by a stroke and laid underneath, which is
   what keeps an open outline readable over an arbitrary sleeve. */
/* .dial__vane-needle carries no rules: its fill is a gradient reference set on
   the element itself, for the resolution reason noted in index.html. The class
   stays in the markup to name which <use> is which. */
.dial__vane-rim {
  fill: var(--ink);
  stroke: var(--ink);
  stroke-width: 1.5;
  stroke-linejoin: round;
  opacity: 0.55;
}

/* Everything below the head is hairline outline, matching the mark: the arm is
   a pair of parallel strokes, the collar and counterweight are rings. Stroke
   colour is the same gradient the head uses, set on the element. */
.dial__vane-arm {
  fill: none;
  stroke-width: 1.35;
  stroke-linecap: round;
}
.dial__vane-pin {
  fill: var(--brass-hi);
}

/* Seeking by angle is imprecise, so hovering previews the landing point. */
.dial__ghost {
  stroke: var(--paper);
  stroke-width: 1.5;
  stroke-linecap: round;
  opacity: 0;
  transition: opacity var(--dur-fast);
  pointer-events: none;
}
.dial__ghost.is-on {
  opacity: 0.55;
}

.dial-tip {
  position: absolute;
  z-index: 2;
  padding: 3px 7px;
  border: 1px solid var(--hairline);
  border-radius: var(--r-sm);
  background: var(--ink);
  color: var(--brass);
  font-size: var(--step--1);
  pointer-events: none;
  transform: translate(-50%, -140%);
  white-space: nowrap;
}

/* On track change the pointer swings to the new bearing and settles. */
.dial__vane {
  transition: none;
}
.dial__vane.is-settling {
  transition: transform var(--dur-slow) var(--ease-settle);
}

/* Loading: the tick ring breathes, like an instrument warming up. A brass dash
   sweeping the progress ring read as the progress arc having come apart — same
   colour, same radius, so it competed with the one thing that ring is for. */
#dialTicks {
  transition: opacity var(--dur-base);
}
.station[data-loading="true"] #dialTicks {
  animation: vane-breathe 1.6s ease-in-out infinite;
}
@keyframes vane-breathe {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.35;
  }
}

/* ---- Readouts ---- */

.readout {
  display: flex;
  flex-direction: column;
  gap: var(--gap-3);
  min-width: 0;
  width: 100%;
  max-width: 420px;
  text-align: center;
}

.readout__title {
  font-size: var(--step-3);
  font-weight: 500;
  line-height: 1.2;
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.readout__artist {
  color: var(--gust);
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A track that will not play says why here. Without it the dial just sits at
   0:00 and the failure looks like a broken pointer. */
.readout__fault {
  margin: 0;
  font-size: var(--step--1);
  color: var(--danger);
}

/* The lyric ticker. Two lines only: what is playing and what is next. Any more
   and it competes with the full column instead of standing in for it. */
.ticker {
  display: block;
  width: 100%;
  padding: 2px 0;
  text-align: center;
  cursor: pointer;
  color: inherit;
}
.ticker__now,
.ticker__next {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  transition: color var(--dur-base) var(--ease-gust);
}
.ticker__now {
  color: var(--paper);
  font-size: var(--step-0);
  min-height: 1.5em;
}
.ticker__next {
  color: var(--gust-dim);
  font-size: var(--step--1);
  min-height: 1.4em;
}
.ticker:hover .ticker__next {
  color: var(--gust);
}

.readout__strip {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: var(--gap-3);
  flex-wrap: wrap;
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 9px;
  border: 1px solid var(--hairline);
  border-radius: var(--r-sm);
  font-family: var(--font-num);
  font-size: var(--step--1);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--gust);
  background: color-mix(in oklab, var(--ink-2) 80%, transparent);
}
.chip--wind {
  border-color: color-mix(in oklab, var(--wind) 45%, transparent);
  color: var(--wind);
}
.chip--brass {
  border-color: color-mix(in oklab, var(--brass) 50%, transparent);
  color: var(--brass);
}

/* Track ids are shown verbatim. QQ mids and KuGou hashes are case sensitive, so
   the chip's uppercasing made the displayed id unusable if anyone read it off
   the screen. */
#idChip {
  text-transform: none;
}
/* A chip that is also a control. Pressed means the fallback is being used
   directly rather than only as a safety net. */
.chip--toggle {
  cursor: pointer;
  transition: color var(--dur-fast), border-color var(--dur-fast), background var(--dur-fast);
}
.chip--toggle:hover {
  color: var(--paper);
  border-color: var(--gust-dim);
}
.chip--toggle[aria-pressed="true"] {
  border-color: var(--brass);
  background: color-mix(in oklab, var(--brass) 14%, transparent);
  color: var(--brass);
}

.chip button {
  color: inherit;
  opacity: 0.6;
}
.chip button:hover {
  opacity: 1;
}

/* Beaufort scale: the quality ladder rendered as wind force 0–12. */
.beaufort {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 14px;
}
.beaufort i {
  width: 3px;
  background: var(--gust-dim);
  border-radius: 1px;
  transition: background var(--dur-base) var(--ease-gust);
}
.beaufort i.on {
  background: var(--wind);
}

/* ---- Transport ---- */

.transport {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--gap-3);
}

.clock {
  display: flex;
  justify-content: space-between;
  width: 100%;
  max-width: 420px;
  font-family: var(--font-num);
  font-size: var(--step--1);
  color: var(--gust);
  font-variant-numeric: tabular-nums;
}
.clock span:nth-child(1) {
  color: var(--brass);
}
.clock span:nth-child(2) {
  color: var(--wind);
  letter-spacing: 0.1em;
}

.tbtn {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  color: var(--gust);
  border: 1px solid transparent;
  transition: color var(--dur-fast), border-color var(--dur-fast), transform var(--dur-fast);
}
.tbtn:hover {
  color: var(--paper);
  border-color: var(--hairline);
}
.tbtn:active {
  transform: scale(0.94);
}
.tbtn svg {
  width: 21px;
  height: 21px;
}

.tbtn--primary {
  width: 58px;
  height: 58px;
  background: var(--brass);
  color: var(--ink);
}
.tbtn--primary:hover {
  background: var(--brass-hi);
  color: var(--ink);
  border-color: transparent;
}
.tbtn--primary svg {
  width: 25px;
  height: 25px;
}

.tbtn[aria-pressed="true"] {
  color: var(--wind);
}

/* --------------------------------------------------------------------------
   Panel: lyrics / queue / search / cloud. Tabs are compass bearings.
   -------------------------------------------------------------------------- */

/* Flex rather than grid: a fixed grid-template-rows breaks the moment a child
   is added — the mini transport pushed .rose into the 1fr row, which stretched
   the tab strip to full height and collapsed the view below it. A column flex
   doesn't care how many children there are. */
.panel {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
  border: 1px solid var(--hairline);
  border-radius: var(--r-lg);
  background: color-mix(in oklab, var(--ink-2) 72%, transparent);
  backdrop-filter: blur(18px);
  overflow: hidden;
}

.rose {
  display: flex;
  gap: 2px;
  padding: var(--gap-2);
  border-bottom: 1px solid var(--hairline);
  flex: none;
}

.rose__tab {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  padding: 7px 4px 6px;
  border-radius: var(--r-md);
  color: var(--gust-dim);
  transition: color var(--dur-fast), background var(--dur-fast);
}
.rose__tab:hover {
  color: var(--gust);
  background: var(--slate);
}
.rose__tab[aria-selected="true"] {
  color: var(--paper);
  background: var(--slate);
}

.rose__bearing {
  font-family: var(--font-num);
  font-size: 10px;
  letter-spacing: 0.2em;
  color: var(--brass);
  opacity: 0.55;
}
.rose__tab[aria-selected="true"] .rose__bearing {
  opacity: 1;
}
.rose__label {
  font-size: var(--step--1);
  letter-spacing: 0.04em;
}

.view {
  flex: 1;
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.view[hidden] {
  display: none;
}

/* ---- Lyrics ---- */

.lyrics {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  scrollbar-width: none;
  padding: 34% var(--gap-5);
  text-align: center;
  mask-image: linear-gradient(transparent, #000 16%, #000 84%, transparent);
  -webkit-mask-image: linear-gradient(transparent, #000 16%, #000 84%, transparent);
}
.lyrics::-webkit-scrollbar {
  display: none;
}

.lyric {
  display: grid;
  grid-template-columns: 46px minmax(0, 1fr) 46px;
  align-items: baseline;
  gap: var(--gap-2);
  padding: 9px 0;
  color: var(--gust-dim);
  font-size: var(--step-1);
  line-height: 1.45;
  transition: color var(--dur-base) var(--ease-gust), transform var(--dur-base) var(--ease-gust);
  transform-origin: center;
  cursor: pointer;
}

/* The timestamp doubles as the affordance: it says these lines are seekable. */
.lyric__time {
  grid-column: 1;
  font-family: var(--font-num);
  font-size: var(--step--1);
  font-variant-numeric: tabular-nums;
  color: var(--gust-dim);
  opacity: 0.4;
  text-align: right;
  transition: opacity var(--dur-fast), color var(--dur-fast);
}
.lyric:hover .lyric__time {
  opacity: 0.8;
}
.lyric.is-active .lyric__time {
  color: var(--brass);
  opacity: 1;
}

.lyric__body {
  grid-column: 2;
}
.lyric:hover {
  color: var(--gust);
}
.lyric.is-active {
  color: var(--paper);
  transform: scale(1.06);
}
.lyric__roma {
  display: block;
  font-family: var(--font-num);
  font-size: var(--step--1);
  letter-spacing: 0.04em;
  color: var(--gust-dim);
  margin-top: 2px;
}
.lyric.is-active .lyric__roma {
  color: var(--gust);
}

.lyric__trans {
  display: block;
  font-size: var(--step--1);
  color: var(--gust-dim);
  margin-top: 2px;
}
.lyric.is-active .lyric__trans {
  color: var(--wind);
}

.empty {
  flex: 1;
  display: grid;
  place-content: center;
  gap: var(--gap-2);
  text-align: center;
  padding: var(--gap-6);
  color: var(--gust-dim);
}
/* The search panel's state line sits between toolbar and rows rather than
   filling the pane, so it doesn't push the scroller out of view. */
#searchNote {
  flex: none;
  padding: var(--gap-5) var(--gap-4);
}

.empty strong {
  display: block;
  color: var(--gust);
  font-weight: 500;
}

/* ---- Track rows (queue + results share one component) ---- */

.toolbar {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  padding: var(--gap-2) var(--gap-3);
  border-bottom: 1px solid var(--hairline);
  flex: none;
}

.field {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  padding: 0 var(--gap-3);
  height: 34px;
  border: 1px solid var(--hairline);
  border-radius: var(--r-md);
  background: var(--ink);
}
.field:focus-within {
  border-color: color-mix(in oklab, var(--wind) 55%, transparent);
}
.field input {
  flex: 1;
  min-width: 0;
  background: none;
  border: 0;
  outline: 0;
}
.field input::placeholder {
  color: var(--gust-dim);
}
.field svg {
  width: 15px;
  height: 15px;
  color: var(--gust-dim);
  flex: none;
}

.btn {
  height: 34px;
  padding: 0 var(--gap-3);
  border: 1px solid var(--hairline);
  border-radius: var(--r-md);
  color: var(--gust);
  font-family: var(--font-num);
  font-size: var(--step--1);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  transition: color var(--dur-fast), border-color var(--dur-fast);
}
.btn:hover {
  color: var(--paper);
  border-color: var(--gust-dim);
}
.btn--brass {
  background: var(--brass);
  border-color: var(--brass);
  color: var(--ink);
}
.btn--brass:hover {
  background: var(--brass-hi);
  border-color: var(--brass-hi);
  color: var(--ink);
}

.srcpick {
  display: flex;
  gap: 2px;
  padding: var(--gap-2) var(--gap-3) 0;
  flex: none;
}
.srcpick button {
  padding: 4px 10px;
  border-radius: var(--r-sm);
  font-family: var(--font-num);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--gust-dim);
}
.srcpick button[aria-pressed="true"] {
  background: var(--slate);
  color: var(--wind);
}

/* Virtual scroller: one implementation, shared by queue and results. */
.scroller {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--slate-2) transparent;
}
.scroller::-webkit-scrollbar {
  width: 6px;
}
.scroller::-webkit-scrollbar-thumb {
  background: var(--slate-2);
  border-radius: 3px;
}
.scroller__sizer {
  position: relative;
}

.row {
  position: absolute;
  left: 0;
  right: 0;
  display: grid;
  grid-template-columns: 30px 40px minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--gap-3);
  height: 56px;
  padding: 0 var(--gap-3);
  cursor: pointer;
  border-radius: var(--r-md);
  transition: background var(--dur-fast);
}
.row:hover {
  background: var(--slate);
}

.row__ord {
  font-family: var(--font-num);
  font-size: var(--step--1);
  color: var(--gust-dim);
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.row__art {
  width: 40px;
  height: 40px;
  border-radius: var(--r-sm);
  object-fit: cover;
  background: var(--slate);
  flex: none;
}

/* No artwork: a small bezel ring, echoing the dial, instead of a blank tile. */
.row__art.is-empty {
  background:
    radial-gradient(circle at 50% 50%, transparent 5px, var(--hairline) 5px, var(--hairline) 5.5px, transparent 6px),
    var(--slate);
}

.row__meta {
  min-width: 0;
}
.row__name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.row__sub {
  font-size: var(--step--1);
  color: var(--gust-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.row__tail {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  opacity: 0;
  transition: opacity var(--dur-fast);
}
.row:hover .row__tail,
.row:focus-within .row__tail {
  opacity: 1;
}
.row__tail button {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  color: var(--gust-dim);
}
.row__tail button:hover {
  color: var(--paper);
  background: var(--slate-2);
}

/* Armed for deletion. One more click removes; it disarms itself after a beat. */
.row__tail button.is-armed {
  color: var(--danger);
  background: color-mix(in oklab, var(--danger) 18%, transparent);
  width: auto;
  padding: 0 10px;
  gap: 5px;
  border-radius: 15px;
  font-family: var(--font-num);
  font-size: 11px;
  letter-spacing: 0.06em;
}
.row__tail button.is-armed::after {
  content: "再点删除";
  font-family: var(--font-ui);
  letter-spacing: 0.02em;
}
.row__tail svg {
  width: 15px;
  height: 15px;
}

/* Download progress lives on the row. A hairline along the bottom edge, so it
   reads as this row filling up rather than as a separate widget. */
.row__prog {
  position: absolute;
  left: var(--gap-3);
  right: var(--gap-3);
  bottom: 4px;
  height: 2px;
  border-radius: 1px;
  background: var(--slate-2);
  opacity: 0;
  transition: opacity var(--dur-fast);
}
.row.is-loading .row__prog {
  opacity: 1;
}
.row__prog i {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--brass);
  transform: scaleX(var(--p, 0));
  transform-origin: left center;
  transition: transform 200ms linear;
}
.row.is-loading .row__ord {
  color: var(--brass);
}

/* Now-playing row wears the vane: a brass bearing needle instead of an index. */
.row.is-current .row__name {
  color: var(--brass);
}
.row.is-current .row__ord {
  color: transparent;
  position: relative;
}
.row.is-current .row__ord::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--brass);
  clip-path: polygon(50% 8%, 68% 62%, 50% 50%, 32% 62%);
}

/* --------------------------------------------------------------------------
   Overlays
   -------------------------------------------------------------------------- */

.scrim {
  position: fixed;
  inset: 0;
  z-index: 40;
  background: color-mix(in oklab, var(--ink) 72%, transparent);
  backdrop-filter: blur(6px);
  display: grid;
  place-items: center;
  padding: var(--gap-4);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-base);
}
.scrim.is-open {
  opacity: 1;
  pointer-events: auto;
}

.sheet {
  width: 100%;
  max-width: 440px;
  max-height: min(86dvh, calc(100dvh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 32px));
  overflow-y: auto;
  border: 1px solid var(--hairline);
  border-radius: var(--r-lg);
  background: var(--ink-2);
  padding: var(--gap-5);
  transform: translateY(12px) scale(0.98);
  transition: transform var(--dur-base) var(--ease-gust);
}
.scrim.is-open .sheet {
  transform: none;
}

.sheet__head {
  display: flex;
  align-items: baseline;
  gap: var(--gap-3);
  margin-bottom: var(--gap-4);
}
.sheet__head h2 {
  margin: 0;
  font-size: var(--step-2);
  font-weight: 500;
}

.stack {
  display: flex;
  flex-direction: column;
  gap: var(--gap-4);
}

.opt {
  display: flex;
  align-items: center;
  gap: var(--gap-3);
  width: 100%;
  padding: var(--gap-3);
  border: 1px solid var(--hairline);
  border-radius: var(--r-md);
  text-align: left;
  transition: border-color var(--dur-fast), background var(--dur-fast);
}
.opt:hover {
  border-color: var(--gust-dim);
}
.opt[aria-pressed="true"] {
  border-color: var(--brass);
  background: color-mix(in oklab, var(--brass) 8%, transparent);
}
.opt__body {
  flex: 1;
  min-width: 0;
}
.opt__name {
  display: block;
}
.opt__note {
  display: block;
  font-size: var(--step--1);
  color: var(--gust-dim);
}

.toast {
  position: fixed;
  left: 50%;
  bottom: calc(24px + env(safe-area-inset-bottom, 0px));
  z-index: 60;
  transform: translate(-50%, 16px);
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  max-width: min(92vw, 420px);
  padding: 10px var(--gap-4);
  border: 1px solid var(--hairline);
  border-radius: 999px;
  background: var(--ink-2);
  font-size: var(--step--1);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-base), transform var(--dur-base) var(--ease-gust);
}
.toast.is-open {
  opacity: 1;
  transform: translate(-50%, 0);
}
.toast[data-tone="error"] {
  border-color: var(--danger);
  color: var(--danger);
}

/* --------------------------------------------------------------------------
   Narrow: the panel becomes a pull-up sheet. Same DOM, same scroller.
   -------------------------------------------------------------------------- */

@media (max-width: 900px) {
  .shell {
    /* Landscape notches eat into the sides, so the insets are honoured on all
       four edges rather than only the bottom. */
    padding-left: max(var(--gap-3), env(safe-area-inset-left, 0px));
    padding-right: max(var(--gap-3), env(safe-area-inset-right, 0px));
    grid-template-rows: auto minmax(0, 1fr);
  }

  .mast {
    padding-top: max(var(--gap-3), env(safe-area-inset-top, 0px));
    min-height: 52px;
  }

  .deck {
    grid-template-columns: 1fr;
    gap: var(--gap-4);
    align-content: center;
    padding-bottom: calc(var(--peek) + var(--gap-5));
  }

  /* Everything tappable clears the 44px that a thumb actually needs. */
  .tbtn {
    width: 48px;
    height: 48px;
  }
  .tbtn--primary {
    width: 62px;
    height: 62px;
  }
  .iconbtn {
    width: 42px;
    height: 42px;
  }
  .rose__tab {
    padding: 10px 4px 9px;
  }
  .btn,
  .field {
    height: 40px;
  }

  /* iOS zooms the viewport when a focused input's text is under 16px, and
     never zooms back out. The field grows to match rather than the text
     shrinking to fit. */
  .field input,
  .sheet input {
    font-size: 16px;
  }
  .row__tail button {
    width: 36px;
    height: 36px;
  }

  .dial {
    max-width: min(74vw, 320px);
  }

  .readout__title {
    font-size: var(--step-2);
  }

  /* The panel docks to the bottom edge and is dragged up by its handle. */
  .panel {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 30;
    height: 88dvh;
    padding-bottom: env(safe-area-inset-bottom, 0px);
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    border-bottom: 0;
    transform: translateY(calc(100% - var(--peek)));
    transition: transform var(--dur-base) var(--ease-gust);
  }
  .panel.is-up {
    transform: translateY(0);
  }
  .panel.is-dragging {
    transition: none;
  }

  /* The bar is 3px tall but the grab area is 30 — a hairline is not a target. */
  .handle {
    display: grid;
    place-items: center;
    height: 30px;
    flex: none;
    touch-action: none;
    cursor: grab;
  }
  .handle:active {
    cursor: grabbing;
  }
  .handle::after {
    content: "";
    width: 38px;
    height: 3px;
    border-radius: 2px;
    background: var(--gust-dim);
  }

  .lyrics {
    padding: 30% var(--gap-4);
  }

  /* Only useful while the sheet is actually covering the transport. */
  .panel.is-up .mini {
    display: flex;
    align-items: center;
    gap: var(--gap-2);
    padding: 0 var(--gap-3) var(--gap-2);
    flex: none;
    /* Part of the drag surface now, not just a status line. */
    touch-action: none;
    user-select: none;
    -webkit-user-select: none;
  }

  .mini__title {
    flex: 1;
    min-width: 0;
    cursor: grab;
    /* It is the sheet's grab area, so give it a thumb's worth of height. */
    padding: 14px 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--step--1);
    color: var(--gust);
  }

  .mini__btn {
    touch-action: manipulation;
    width: 38px;
    height: 38px;
    flex: none;
    display: grid;
    place-items: center;
    border-radius: 50%;
    color: var(--brass);
    background: var(--slate);
  }
  .mini__btn:active {
    transform: scale(0.94);
  }
  .mini__btn svg {
    width: 18px;
    height: 18px;
  }

  /* The time gutter costs width that narrow screens need for the words. */
  .lyric {
    grid-template-columns: 34px minmax(0, 1fr) 34px;
  }

  .row__tail {
    opacity: 1;
  }
}

/* Desktop keeps the full transport in view, so the mini bar is narrow-only. */
.mini {
  display: none;
}

@media (min-width: 901px) {
  .handle {
    display: none;
  }
}

/* Landscape phones are wide but very short, so the dial has to give way or the
   transport ends up below the fold with no way to reach it. */
@media (max-width: 900px) and (max-height: 520px) and (orientation: landscape) {
  .deck {
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
  }
  .dial {
    max-width: min(38vw, 200px);
  }
  .readout__title {
    font-size: var(--step-1);
  }
  .clock {
    max-width: 260px;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .dial__hub {
    transform: none !important;
  }
  .cursor-aura {
    display: none;
  }
}
