/* 00-tokens.css */
/* Tokens.

   Black, and glass on top of it.

   The ground is actually black, not a very dark grey and not a gradient. Nothing is
   shaded and nothing fades into anything: every surface is one flat value. What
   separates one plane from another is not a change of colour, it is that the near plane
   is translucent and bends what is behind it.

   That bending is real. The chrome carries an SVG displacement filter, so content
   scrolling under the rail or the player is pushed sideways as it passes and a straight
   edge crossing behind a panel visibly bows. A blur on its own reads as frosted plastic,
   because blurring is what distance does, not what glass does. Refraction is the thing
   the eye actually recognises.

   Glass only says anything when there is something behind it, which is why the chrome is
   glass and the content is not. A translucent card floating on a black page over nothing
   is a grey card with extra steps.

   Plant green stays. It is the one colour in the room and it belongs to whatever is
   playing. */

:root {
  /* ── the ground ─────────────────────────────────────────────────────────── */
  --bg:          #000000;
  --bg-deep:     #000000;
  --bg-high:     #000000;

  /* Solid planes, for the few things that must not be seen through. */
  --surface:     #0B0B0C;
  --surface-2:   #121214;
  --elevated:    #1A1A1D;
  --hover:       rgba(255, 255, 255, 0.06);
  --pressed:     rgba(255, 255, 255, 0.11);

  --line:        rgba(255, 255, 255, 0.09);
  --line-2:      rgba(255, 255, 255, 0.16);

  --text:        #F2F3F4;
  --text-2:      #9EA1A6;
  --text-3:      #6B6E74;
  --text-on-accent: #05130B;

  --accent:      #54B37A;
  --accent-hi:   #74D398;
  --accent-lo:   #38835A;
  --accent-soft: rgba(84, 179, 122, 0.16);
  --accent-line: rgba(84, 179, 122, 0.42);

  --warn:        #D9A441;
  --bad:         #D4685E;
  --bad-hi:      #F0A79E;
  --text-on-color: #FFFFFF;

  /* ── glass ──────────────────────────────────────────────────────────────── */

  /* The tint. Barely a colour: a pane is mostly whatever is behind it, lifted just
     enough that type has something to sit on. */
  --glass:       rgba(255, 255, 255, 0.055);
  --glass-2:     rgba(255, 255, 255, 0.085);
  --glass-line:  rgba(255, 255, 255, 0.13);

  /* Distort, then blur, then lift. The order matters: displacing an image that has
     already been blurred bends a smudge, which looks like nothing at all. */
  --glass-filter:      url(#glass) blur(22px) saturate(180%) brightness(1.06);
  --glass-filter-thin: url(#glass-thin) blur(14px) saturate(170%) brightness(1.05);

  /* The lit top edge every real pane has, and the shadow it sits in. */
  --edge-top: inset 0 1px 0 rgba(255, 255, 255, 0.14);
  --glass-shadow: 0 12px 40px rgba(0, 0, 0, 0.55);

  /* ── gradients, off ─────────────────────────────────────────────────────────

     These names are spent all over the stylesheets. Rather than chase every one, they
     are flat values now, so "the gradients are off" is true at the single place that
     decides it and cannot quietly come back on somewhere else. */
  --grad-app:       none;
  --grad-surface:   var(--surface);
  --grad-raised:    var(--elevated);
  --grad-raised-hi: #232327;
  --grad-sunken:    #08080A;
  --grad-player:    var(--surface);
  --grad-accent:    var(--accent);
  --grad-accent-hi: var(--accent-hi);

  --font:  "Manrope", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  /* "Jriter Display" is whatever font has been uploaded in Settings, registered at runtime.
     Orbitron is the openly licensed stand in until one is. */
  --display: "Jriter Display", "Orbitron", "Manrope", ui-sans-serif, system-ui, sans-serif;

  /* Rounder than before. Glass with tight corners looks like a cut tile. */
  --r-pill: 999px;
  --r-xl:  24px;
  --r-lg:  18px;
  --r-md:  13px;
  --r-sm:  9px;

  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
  --s5: 20px; --s6: 28px; --s7: 40px; --s8: 56px;

  --rail-w: 244px;
  --player-h: 84px;

  --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.6);
  --shadow-2: 0 10px 30px rgba(0, 0, 0, 0.6);
  --shadow-3: 0 24px 70px rgba(0, 0, 0, 0.75);
  --glow:     0 0 0 1px var(--accent-line), 0 8px 30px rgba(84, 179, 122, 0.2);

  --ease: cubic-bezier(0.22, 0.7, 0.3, 1);
  --fast: 130ms var(--ease);
  --med:  220ms var(--ease);
  --slow: 420ms var(--ease);
}

@media (prefers-reduced-motion: reduce) {
  :root { --fast: 1ms linear; --med: 1ms linear; --slow: 1ms linear; }
  * { animation-duration: 1ms !important; scroll-behavior: auto !important; }
}

/* The filter definitions carry no picture of their own. */
.glass-defs { position: absolute; width: 0; height: 0; pointer-events: none; }


/* 10-base.css */
/* Base: the ground, the type, and the things every screen uses. */

* { box-sizing: border-box; }

/* The hidden attribute has to win against the layout.
 *
 * A browser's own stylesheet says [hidden] { display: none }, but that is a user agent
 * rule and any author rule beats it. So .sheet-backdrop { display: grid } quietly turned
 * the hidden attribute off, and the modal backdrop sat over the whole app at 60% black
 * with nothing in it: the page looked dimmed and swallowed every click. The player did
 * the same thing with nothing playing.
 *
 * Anything given a display by its class needs this to stay hideable. */
[hidden] { display: none !important; }

html, body {
  height: 100%;
  margin: 0;
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 14px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overscroll-behavior: none;
}

h1, h2, h3 {
  font-family: var(--display);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.1;
  margin: 0;
  text-wrap: balance;
}
h1 { font-size: clamp(30px, 5vw, 46px); font-weight: 800; }
h2 { font-size: 21px; }
h3 { font-size: 16px; letter-spacing: -0.01em; }

a { color: inherit; text-decoration: none; }

button, input, textarea, select { font: inherit; color: inherit; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

::selection { background: var(--accent-soft); }

/* Numbers that sit in columns get to line up. */
.num, .dur, .meta-num { font-variant-numeric: tabular-nums; }

.grow { flex: 1; }
.muted { color: var(--text-2); }
.faint { color: var(--text-3); }
.bad-text { color: var(--bad); }
.nowrap { white-space: nowrap; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }

.eyebrow {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-3);
}

.stack { display: flex; flex-direction: column; }
.row { display: flex; align-items: center; gap: var(--s3); }
.row.wrap { flex-wrap: wrap; }

/* Nothing here yet.
 *
 * A dashed outline is the convention for "drop something here", which is not what most
 * of these are. They are a sentence explaining what this screen will hold and a way to
 * put the first thing in it, so they read as a panel with a little light on it. */
.empty {
  padding: var(--s8) var(--s6);
  text-align: center;
  color: var(--text-2);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background:
    radial-gradient(120% 100% at 50% 0%, rgba(84, 179, 122, 0.05), transparent 62%),
    var(--grad-surface);
  box-shadow: var(--edge-top);
}
.empty h3 { margin-bottom: var(--s2); color: var(--text); }
.empty p { max-width: 46ch; margin-inline: auto; }

/* Scrollbars, quiet but present. */
* { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.16) transparent; }
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.14);
  border-radius: var(--r-pill);
  border: 3px solid transparent;
  background-clip: content-box;
}
*::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.24); background-clip: content-box; }
*::-webkit-scrollbar-track { background: transparent; }


/* ── moving between screens ───────────────────────────────────────────────── */

/* The new screen arrives rather than appearing. A few pixels of travel and a fade is
   enough to say "this replaced that" without anybody waiting for an animation. */
.view.entering {
  opacity: 0;
  transform: translate3d(0, 8px, 0);
}
.view {
  opacity: 1;
  transform: none;
  transition: opacity var(--med), transform var(--med);
}

/* The line at the top, for a navigation slow enough to notice. It fills most of the way
   and waits, because the honest thing is that we do not know how long is left. */
.nav-bar {
  position: fixed; top: 0; left: 0; right: 0; height: 2px;
  z-index: 100;
  pointer-events: none;
}
.nav-bar span {
  display: block; height: 100%; width: 0;
  background: linear-gradient(90deg, var(--accent-lo), var(--accent-hi));
  box-shadow: 0 0 10px rgba(84, 179, 122, 0.6);
}
.nav-bar.running span { width: 82%; transition: width 2.4s cubic-bezier(0.1, 0.8, 0.2, 1); }
.nav-bar.done span { width: 100%; opacity: 0; transition: width 140ms linear, opacity 220ms 120ms linear; }

/* ── the shape of a screen that has not arrived yet ───────────────────────── */

.sk { padding: var(--s6) var(--s7); animation: sk-in var(--med) both; }
@keyframes sk-in { from { opacity: 0; } to { opacity: 1; } }

.sk-hero { display: flex; gap: var(--s5); align-items: flex-end; margin-bottom: var(--s7); }
.sk-stack { display: flex; flex-direction: column; gap: var(--s3); flex: 1; min-width: 0; }
.sk-block { display: flex; flex-direction: column; gap: var(--s3); margin-bottom: var(--s6); }
.sk-row { display: flex; align-items: center; gap: var(--s3); }

.sk-line, .sk-cover, .sk-thumb {
  display: block;
  border-radius: var(--r-sm);
  /* A slow sweep rather than a pulse. It should read as "still coming" from the corner
     of your eye and never ask to be looked at directly. */
  background:
    linear-gradient(100deg, transparent 20%, rgba(255, 255, 255, 0.045) 40%, transparent 62%),
    var(--surface-2);
  background-size: 260% 100%, auto;
  animation: sk-sweep 1.9s var(--ease) infinite;
}
.sk-cover { width: 148px; height: 148px; border-radius: var(--r-lg); flex: none; }
.sk-thumb { width: 40px; height: 40px; border-radius: var(--r-md); flex: none; }

@keyframes sk-sweep {
  from { background-position: 130% 0, 0 0; }
  to   { background-position: -30% 0, 0 0; }
}

@media (prefers-reduced-motion: reduce) {
  .sk-line, .sk-cover, .sk-thumb { animation: none; }
}

@media (max-width: 900px) {
  .sk { padding: var(--s4); }
  .sk-cover { width: 96px; height: 96px; }
  .sk-hero { gap: var(--s4); }
}


/* ── keyboard ─────────────────────────────────────────────────────────────── */

/* Only for people actually using a keyboard: :focus-visible leaves mouse clicks alone.
   The browser's own ring is a thin dark outline, which against a near black ground is
   the same as having no ring at all. */
:focus-visible {
  outline: 2px solid var(--accent-hi);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}
.btn:focus-visible, .icon-btn:focus-visible, .pill:focus-visible { outline-offset: 3px; }

/* ── glass ────────────────────────────────────────────────────────────────

   Applied directly on the four surfaces that have the page moving behind them: the
   rail, the bar across the top, the player and dialogs. Not offered as a utility class,
   because glass is not a decoration to sprinkle: it means something only where content
   passes underneath, and everywhere else it is a grey box that costs a compositor layer.

   Where the filter cannot be had at all, those surfaces fall back to solid. */
@supports not (backdrop-filter: blur(1px)) {
  .rail, .topbar, .player, .sheet, .slot-menu { background: var(--surface); }
  /* The card carries text rather than chrome, so it falls back to the panel colour
     it used to be rather than to a nearly invisible film. */
  .lyric-card { background: var(--surface-2); }
}


/* 20-shell.css */
/* The shell: a rail, a scrolling main column, a docked player. */

/* The whole page's ground when a song is open.
 *
 * It used to live inside the panel, which meant the artwork stopped at the panel's edge
 * and the rail and the player sat on flat black with nothing behind them to bend. Fixed
 * to the viewport and behind everything, it is the room the app is standing in. */
.page-wash {
  position: fixed;
  inset: 0;
  z-index: 0;
  background-size: cover;
  background-position: center;
  filter: blur(90px) saturate(160%);
  opacity: 0;
  transform: scale(1.3);
  pointer-events: none;
  transition: opacity var(--slow);
}
.page-wash.on { opacity: 0.32; }

/* With a song open the panel stops being the ground and becomes another pane over it,
   or the artwork would stop dead at the panel's edge, which is the thing being fixed. */
.page-wash.on ~ .shell .main { background: rgba(11, 11, 12, 0.62); }
.page-wash.flat {
  background-image:
    radial-gradient(80% 60% at 25% 10%, hsl(var(--hue) 45% 30%), transparent 70%),
    radial-gradient(70% 60% at 85% 60%, hsl(calc(var(--hue) + 50) 40% 24%), transparent 65%);
  filter: blur(70px);
}

.shell {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: var(--rail-w) 1fr;
  grid-template-rows: 1fr auto;
  grid-template-areas: "rail main" "player player";
  height: 100dvh;
  gap: var(--s2);
  padding: var(--s2);
}

/* ── rail ─────────────────────────────────────────────────────────────────── */
.rail {
  grid-area: rail;
  background: var(--glass);
  -webkit-backdrop-filter: var(--glass-filter);
  backdrop-filter: var(--glass-filter);
  border: 1px solid var(--glass-line);
  box-shadow: var(--edge-top), var(--glass-shadow);
  isolation: isolate;
  border-radius: var(--r-lg);
  padding: var(--s4) var(--s3) var(--s3);
  display: flex;
  flex-direction: column;
  gap: var(--s4);
  min-height: 0;
  overflow: hidden;
}
.rail-top { display: flex; align-items: center; justify-content: space-between; padding: 0 var(--s2); }
.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-family: var(--display);
  font-weight: 800;
  font-size: 23px;
  letter-spacing: -0.03em;
}
/* The accent lives in the rest now. There used to be a green dot after the name here,
   which was only ever a way of getting the accent into the mark. The name ends in a
   letter that already has a dot under it, so keeping both reads as a stray character. */
.wordmark .rest { flex: none; color: var(--accent); }
/* The rail buttons carry .icon-btn too, and .icon-btn sets display: grid from a later
   file. Two single class rules tie on specificity and file order decides, which is how
   both of these ended up permanently visible on desktop doing nothing. Two classes in the
   selector settles it wherever the files happen to sit. */
.icon-btn.rail-close { display: grid; }   /* always: it is how you put the rail away */
.icon-btn.rail-open { display: none; }
.shell.rail-shut .icon-btn.rail-open { display: grid; }

.nav { display: flex; flex-direction: column; gap: 2px; }
.nav a {
  display: flex; align-items: center; gap: var(--s3);
  padding: 9px var(--s3);
  border-radius: var(--r-pill);
  color: var(--text-2);
  font-weight: 600;
  transition: background var(--fast), color var(--fast);
}
.nav a svg { flex: none; opacity: 0.9; }
.nav a:hover { background: var(--hover); color: var(--text); }
.nav a.on {
  background: linear-gradient(90deg, var(--accent-soft) 0%, rgba(84, 179, 122, 0.04) 100%);
  color: var(--accent-hi);
}
/* A lit edge on the side the eye starts from, so which screen you are on is readable
   from the shape of the rail rather than from reading the labels. */
.nav a.on::before {
  content: "";
  position: absolute; left: 0; top: 50%;
  width: 3px; height: 18px;
  border-radius: var(--r-pill);
  background: var(--accent);
  transform: translateY(-50%);
  box-shadow: 0 0 10px var(--accent);
}
.nav a { position: relative; }

.rail-section {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-right: 2px;
}
.rail-section .eyebrow { padding: var(--s3) var(--s3) var(--s2); }
.rail-album {
  display: flex; align-items: center; gap: var(--s3);
  padding: 6px var(--s2);
  border-radius: var(--r-md);
  transition: background var(--fast);
}
.rail-album:hover { background: var(--hover); }
.rail-album .cover { width: 38px; height: 38px; border-radius: var(--r-sm); flex: none; }
.rail-album .t { font-weight: 600; font-size: 13px; }
.rail-album .s { font-size: 11.5px; color: var(--text-3); }

.rail-foot {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s2);
  padding-top: var(--s3);
  border-top: 1px solid var(--line);
}
.rail-version { font-size: 11px; color: var(--text-3); }

.rail-scrim { display: none; }

/* Collapsing the rail on a wide screen: the column goes to nothing and the main panel
   takes the room. This is what the hamburger does when there is no overlay to open.

   Held inside a min-width query rather than left loose. Two class selectors outrank the
   one class rule that gives a phone its single column, so as a bare rule this pinned the
   main column to zero on every phone and collapsed the entire page behind it. */
@media (min-width: 901px) {
  .shell.rail-shut { grid-template-columns: 0 1fr; }
  .shell.rail-shut .rail { display: none; }
}

/* ── main ─────────────────────────────────────────────────────────────────── */
.main {
  grid-area: main;
  /* Solid. This is the plane everything else is glass *against*, and a pane that is
     itself see through has nothing to refract. */
  background: var(--surface);
  transition: background var(--slow);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  position: relative;
}

/* Sticky over the list, which is exactly the condition glass is for: rows pass under it
   and bend as they go. Thin rather than full strength, because a bar this shallow with
   heavy refraction reads as a smear. */
.topbar {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3) var(--s5);
  flex: none;
  position: sticky; top: 0; z-index: 5;
  background: var(--glass);
  -webkit-backdrop-filter: var(--glass-filter-thin);
  backdrop-filter: var(--glass-filter-thin);
  border-bottom: 1px solid var(--glass-line);
  isolation: isolate;
}

.search {
  display: flex; align-items: center; gap: var(--s2);
  background: var(--grad-sunken);
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  padding: 0 var(--s4);
  height: 40px;
  width: min(360px, 46vw);
  color: var(--text-3);
  transition: border-color var(--fast), background var(--fast);
}
.search:focus-within { border-color: var(--accent-line); background: var(--bg-deep); }
.search input {
  background: none; border: 0; outline: none; width: 100%;
  font-size: 13.5px; color: var(--text);
}
.search input::placeholder { color: var(--text-3); }
.search input::-webkit-search-cancel-button { -webkit-appearance: none; }

.update-dot {
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 4px var(--accent-soft);
}

.view {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  /* Vertical panning belongs to the browser; sideways comes to us.
     Without this a phone owns the whole gesture, decides a sideways drag is a scroll
     attempt and fires pointercancel before it has travelled far enough to count, so the
     rail swipe could never fire on a touch screen however far you dragged. Synthetic
     pointer events do not go through any of that machinery, which is why it looked
     like it worked. */
  touch-action: pan-y;
  padding: var(--s2) var(--s5) var(--s8);
  scroll-behavior: smooth;
}

.section { margin-top: var(--s7); }
.section:first-child { margin-top: var(--s3); }
.section-head {
  display: flex; align-items: baseline; gap: var(--s3);
  /* Wrapping so a heading with two buttons after it does not run off the side of a
     phone. The spacer pushes them to a second line rather than off the edge. */
  flex-wrap: wrap;
  margin-bottom: var(--s4);
}
.section-head .grow { flex: 1; }

/* ── responsive ───────────────────────────────────────────────────────────── */
@media (max-width: 900px) {
  .shell { grid-template-columns: 1fr; grid-template-areas: "main" "player"; }
  .rail {
    position: fixed; inset: var(--s2) auto var(--s2) var(--s2);
    width: var(--rail-w); z-index: 40;
    /* One number says where the rail is, and the class below and the finger both write
       it. The transform used to live only in the rail-shut rule, which is why a drag had
       nothing to push: there were two positions and no way to be between them. */
    --rail-x: 0px;
    transform: translate3d(var(--rail-x), 0, 0);
    transition: transform var(--med);
    box-shadow: var(--shadow-3);
    /* Sideways on the rail is ours, up and down still belongs to the browser. Without
       this the browser claims the whole gesture and sends pointercancel partway through,
       which is the same thing that stopped the swipe working on a phone until .view was
       given its own. */
    touch-action: pan-y;
  }
  /* touch-action does not inherit, and this is the part that actually scrolls. */
  .rail-section { touch-action: pan-y; }

  /* Narrow: shut means parked off screen rather than gone, so it can slide back. */
  .shell.rail-shut .rail {
    display: flex;
    --rail-x: calc(-100% - var(--s3));
  }

  .rail-scrim {
    display: block; position: fixed; inset: 0; z-index: 30;
    background: rgba(0, 0, 0, 0.5);
    transition: opacity var(--med);
    /* Nothing behind a scrim should scroll while it is up, and a drag across it is how
       the rail gets pushed back, so both directions come here. */
    touch-action: none;
  }
  /* Shut is invisible and untouchable rather than absent. It was display: none, and a
     rail being dragged open had no dim to bring with it: the room stayed bright until
     the finger let go and then went dark all at once. */
  .shell.rail-shut .rail-scrim { opacity: 0; pointer-events: none; }

  /* While a finger is on it, the rail and the dim are wherever the finger put them. A
     transition here is the browser animating towards a position that has already moved
     again, which reads as lag rather than as a drag. */
  .shell.rail-dragging .rail,
  .shell.rail-dragging .rail-scrim { transition: none; }
  .shell.rail-dragging .rail { will-change: transform; }
}


/* 30-controls.css */
/* Controls: round, and pills where a pill says "one of these".

   Three shapes only. A filled green pill for the action a screen is actually for, a
   quiet pill for everything else, and a circle for transport. */

.btn {
  box-shadow: var(--edge-top), var(--shadow-1);
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
  height: 38px;
  padding: 0 var(--s5);
  border: 0;
  border-radius: var(--r-pill);
  background: var(--grad-raised);
  color: var(--text);
  font-weight: 600;
  font-size: 13.5px;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--fast), transform var(--fast), opacity var(--fast);
}
.btn:hover { background: var(--grad-raised-hi); }
.btn:active { transform: scale(0.97); }
.btn[disabled] { opacity: 0.45; cursor: default; transform: none; }

.btn.primary {
  background: var(--grad-accent);
  color: var(--text-on-accent);
  box-shadow: var(--edge-top), 0 2px 10px rgba(84, 179, 122, 0.25);
}
.btn.primary:hover { background: var(--grad-accent-hi); }
.btn.ghost { background: transparent; color: var(--text-2); box-shadow: none; }
.btn.ghost:hover { background: var(--hover); color: var(--text); }
.btn.danger { background: transparent; color: var(--bad); }
.btn.danger:hover { background: rgba(212, 104, 94, 0.12); }
.btn.sm { height: 30px; padding: 0 var(--s4); font-size: 12.5px; }
.btn.lg { height: 46px; padding: 0 var(--s6); font-size: 15px; }

/* Round transport and icon buttons. */
.icon-btn {
  display: grid; place-items: center;
  width: 36px; height: 36px;
  border: 0; border-radius: 50%;
  background: transparent;
  color: var(--text-2);
  cursor: pointer;
  transition: background var(--fast), color var(--fast), transform var(--fast);
}
.icon-btn:hover { background: var(--hover); color: var(--text); }
.icon-btn:active { transform: scale(0.94); }
.icon-btn.on { color: var(--accent-hi); }
.icon-btn[disabled] { opacity: 0.35; cursor: default; }

.play-btn {
  display: grid; place-items: center;
  width: 52px; height: 52px;
  border: 0; border-radius: 50%;
  background: var(--accent);
  color: var(--text-on-accent);
  cursor: pointer;
  box-shadow: var(--shadow-2);
  transition: transform var(--fast), background var(--fast);
}
.play-btn:hover { background: var(--accent-hi); transform: scale(1.05); }
.play-btn:active { transform: scale(0.97); }
.play-btn.sm { width: 38px; height: 38px; box-shadow: none; }

/* Pills: a row where exactly one is chosen. */
.pills { display: flex; gap: var(--s2); flex-wrap: wrap; }
.pill {
  display: inline-flex; align-items: center; gap: 6px;
  height: 32px; padding: 0 var(--s4);
  border: 1px solid var(--line-2);
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--text-2);
  font-size: 13px; font-weight: 600;
  cursor: pointer;
  transition: background var(--fast), color var(--fast), border-color var(--fast);
}
.pill:hover { background: var(--hover); color: var(--text); }
.pill.on {
  background: var(--accent); border-color: var(--accent); color: var(--text-on-accent);
}
.pill.quiet { border-color: transparent; background: var(--surface-2); }
.pill.quiet.on { background: var(--accent); }

/* A read only tag, not a control. */
.tag {
  white-space: nowrap;        /* "43 waiting" is one thing, never two lines */
  display: inline-flex; align-items: center; gap: 5px;
  height: 24px; padding: 0 10px;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  color: var(--text-2);
  font-size: 11.5px; font-weight: 600;
}
.tag.accent { background: var(--accent-soft); color: var(--accent-hi); }
.tag.warn { background: rgba(217, 164, 65, 0.14); color: var(--warn); }

.field {
  width: 100%;
  background: var(--grad-sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 10px var(--s4);
  font-size: 14px;
  outline: none;
  transition: border-color var(--fast), background var(--fast);
}
.field:hover { border-color: var(--line-2); }
.field:focus { border-color: var(--accent-line); background: var(--bg-deep); }
textarea.field { min-height: 120px; resize: vertical; line-height: 1.7; }

.switch {
  position: relative; width: 44px; height: 26px; flex: none;
  border-radius: var(--r-pill); border: 0;
  background: var(--grad-sunken);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.5);
  cursor: pointer;
  transition: background var(--fast);
}
.switch::after {
  content: ""; position: absolute; top: 3px; left: 3px;
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--text-2);
  box-shadow: var(--shadow-1);
  transition: transform var(--med), background var(--fast);
}
.switch.on { background: var(--grad-accent); }
/* 44 wide, 20 of knob, 3 of gap either side: the travel is what is left. */
.switch.on::after { transform: translateX(18px); background: var(--text-on-accent); }

/* Slider used by the limiter and the volume control. */
.range {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 20px; background: none; cursor: pointer;
}
.range::-webkit-slider-runnable-track {
  height: 4px; border-radius: var(--r-pill);
  background: linear-gradient(var(--accent), var(--accent)) 0 / var(--fill, 0%) 100% no-repeat,
              var(--elevated);
}
.range::-moz-range-track { height: 4px; border-radius: var(--r-pill); background: var(--elevated); }
.range::-moz-range-progress { height: 4px; border-radius: var(--r-pill); background: var(--accent); }
.range::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 14px; height: 14px; margin-top: -5px;
  border-radius: 50%; background: var(--text); border: 0;
  box-shadow: var(--shadow-1);
  transition: transform var(--fast);
}
.range::-moz-range-thumb {
  width: 14px; height: 14px; border: 0; border-radius: 50%; background: var(--text);
}
.range:hover::-webkit-slider-thumb { transform: scale(1.18); }

/* A plain row in a settings or folder list. Generic, so it lives with the controls
   rather than inside one screen's stylesheet, which is where it used to be until that
   screen was rewritten out from under it. */
.list-row {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3);
  border-radius: var(--r-md);
  background: var(--surface-2);
  margin-bottom: var(--s2);
}
.list-row .grow { min-width: 0; flex: 1; }

/* Clear of the player when there is one, and near the bottom when there is not. Without
   the second case a message floats with eighty four pixels of nothing under it, holding
   room for a bar that is not on screen. */
.toast-stack {
  position: fixed; left: 50%; bottom: var(--s5);
  transform: translateX(-50%);
  display: flex; flex-direction: column; gap: var(--s2);
  z-index: 90; pointer-events: none;
  transition: bottom var(--med);
}
.shell:has(.player:not([hidden])) ~ .toast-stack {
  bottom: calc(var(--player-h) + var(--s5));
}
.toast {
  background: var(--grad-raised);
  border: 1px solid var(--line-2);
  border-radius: var(--r-pill);
  padding: 10px var(--s5);
  font-size: 13.5px; font-weight: 600;
  box-shadow: var(--shadow-2), var(--edge-top);
  /* Blurred behind, so a message that lands over the equaliser is still readable
     without having to be opaque enough to hide it. */
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  animation: toast-in 260ms var(--ease);
}
.toast.bad {
  border-color: rgba(212, 104, 94, 0.5);
  color: var(--bad-hi);
  box-shadow: var(--shadow-2), var(--edge-top), 0 0 24px rgba(212, 104, 94, 0.15);
}
@keyframes toast-in { from { opacity: 0; transform: translateY(10px) scale(0.97); } }

/* ── choosing a colour ────────────────────────────────────────────────────

   A bare <input type="color"> is a full width block of flat colour with the operating
   system's own border on it, which is the one thing on the settings page that looked
   like nobody had touched it. The input is still there, and still opens the system
   picker, but it is invisible and the swatch in front of it is what you see. */

.swatch {
  position: relative;
  display: inline-flex; align-items: center; gap: var(--s3);
  /* Its parent is a stretching column, so without this the pill runs the full width of
     the form for no reason: it is a swatch, it should be the size of a swatch. */
  align-self: flex-start;
  height: 42px; padding: 0 var(--s4) 0 6px;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--grad-sunken);
  cursor: pointer;
  transition: border-color var(--fast);
}
.swatch:hover { border-color: var(--line-2); }
.swatch:focus-within { border-color: var(--accent-line); }

.swatch input {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  opacity: 0;
  border: 0; padding: 0;
  cursor: pointer;
}
.swatch-dot {
  width: 30px; height: 30px; flex: none;
  border-radius: 50%;
  background: var(--picked, var(--accent));
  box-shadow: var(--edge-top), 0 2px 8px rgba(0, 0, 0, 0.5);
}
.swatch-hex {
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
  color: var(--text-2);
  text-transform: uppercase;
}

/* The sort control.
 *
 * A button that says what the list is in order by, not what pressing it will do, because
 * a list's order is a state you read at a glance far more often than you change it. The
 * arrow is the direction, and it is absent entirely for an order that cannot be
 * reversed: a running order somebody chose has a right way round and no other. */
.sort-pick { gap: 6px; }
.sort-pick .sort-mark { flex: none; color: var(--text-3); transition: transform var(--fast); }
.sort-pick:hover .sort-mark { color: var(--accent-hi); }
.sort-pick .sort-mark.down { transform: scaleY(-1); }
.sort-pick span { white-space: nowrap; }

/* On a phone the label goes and the icon stays: the menu says the rest. */
@media (max-width: 560px) {
  .sort-pick span { display: none; }
}


/* 40-library.css */
/* Library and albums: artwork carries the page, song lists stay dense. */

.cover {
  position: relative;
  border-radius: var(--r-md);
  background: var(--grad-surface);
  background-size: cover;
  background-position: center;
  overflow: hidden;
  flex: none;
  display: grid;
  place-items: center;
  /* The placeholder letter is sized from the box, not from the inherited font size.
     A percentage font-size resolves against the parent's font size, which made the
     letter six pixels tall on a two hundred pixel cover. */
  container-type: size;
}
/* No artwork yet: a calm two tone built from the title, so a library with nothing
   uploaded still looks like a shelf rather than a row of empty boxes. */
/* Tinted graphite rather than colour.
   The room is black and green now, and a magenta album cover was the one thing in it
   shouting. The hue still comes from the title, so covers stay tellable apart, but at a
   saturation that belongs on this page. */
.cover[data-hue] {
  background-color: hsl(var(--hue) 12% 15%);
}
/* A lit top edge, the same as every other raised thing on the page. */
.cover { box-shadow: var(--edge-top); }
.cover .letter {
  font-family: var(--display);
  font-weight: 800;
  color: rgba(255, 255, 255, 0.38);
  font-size: 44cqmin;
  line-height: 1;
  letter-spacing: -0.03em;
  user-select: none;
}

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
  gap: var(--s4);
}

.album-card {
  padding: var(--s3);
  border-radius: var(--r-lg);
  background: transparent;
  transition: background var(--fast);
  cursor: pointer;
  position: relative;
}
.album-card:hover { background: var(--grad-raised); border-color: var(--line-2); }
.album-card .cover { width: 100%; aspect-ratio: 1; border-radius: var(--r-md); margin-bottom: var(--s3); }
.album-card .t { font-weight: 700; font-size: 14px; }
.album-card .s { font-size: 12.5px; color: var(--text-3); }
.album-card .play-btn {
  position: absolute; right: var(--s5); top: 46%;
  opacity: 0; transform: translateY(6px);
  transition: opacity var(--fast), transform var(--fast);
}
.album-card:hover .play-btn, .album-card:focus-within .play-btn { opacity: 1; transform: none; }

/* ── song rows ────────────────────────────────────────────────────────────── */
.tracks { display: flex; flex-direction: column; }
.track {
  display: grid;
  grid-template-columns: var(--track-cols, 40px 1fr 84px 62px);
  align-items: center;
  gap: var(--s4);
  padding: var(--s2) var(--s3);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: background var(--fast);
}
/* The row lights from the left, where the eye starts, rather than filling flat. */
.track:hover {
  background: linear-gradient(90deg,
    rgba(255, 255, 255, 0.055) 0%, rgba(255, 255, 255, 0.022) 46%, transparent 100%);
}
.track:active { background: var(--pressed); }
.track:focus-visible { outline: 2px solid var(--accent-hi); outline-offset: -2px; }
.track .lead { display: grid; place-items: center; width: 40px; }
.track .cover { width: 40px; height: 40px; border-radius: var(--r-sm); }
.track .index { color: var(--text-3); font-size: 13px; font-variant-numeric: tabular-nums; }
.track .title {
  font-weight: 600;
  display: block;
  color: inherit;
  text-decoration: none;
  /* It is a link, so it should look like one when you are on it, and like the row's own
     title the rest of the time. */
  text-underline-offset: 3px;
}
.track .title:hover { text-decoration: underline; color: var(--text); }
.track .sub { font-size: 12.5px; color: var(--text-3); }
.track .dur, .track .ver { color: var(--text-3); font-size: 12.5px; font-variant-numeric: tabular-nums; }
.track .ver { text-align: left; }
.track.playing .title { color: var(--accent-hi); }
.track.playing .index { color: var(--accent-hi); }
/* The one that is sounding gets a lit edge, so it is findable in a long list without
   scanning every title. */
.track.playing {
  background: linear-gradient(90deg, var(--accent-soft) 0%, transparent 62%);
  box-shadow: inset 2px 0 0 var(--accent);
}

/* The equaliser bars that mark the row currently sounding. */
.playing-bars { display: flex; align-items: flex-end; gap: 2px; height: 13px; }
.playing-bars i {
  width: 2.5px; background: var(--accent-hi); border-radius: 1px;
  animation: bars 900ms var(--ease) infinite alternate;
}
.playing-bars i:nth-child(1) { height: 40%; animation-delay: -200ms; }
.playing-bars i:nth-child(2) { height: 90%; animation-delay: -520ms; }
.playing-bars i:nth-child(3) { height: 60%; animation-delay: -80ms; }
@keyframes bars { to { height: 20%; } }
.paused .playing-bars i { animation-play-state: paused; }

.track-head {
  display: grid;
  grid-template-columns: var(--track-cols, 40px 1fr 84px 62px);
  gap: var(--s4);
  padding: 0 var(--s3) var(--s2);
  border-bottom: 1px solid var(--line);
  margin-bottom: var(--s2);
}

/* ── album page header ────────────────────────────────────────────────────── */
.page-head {
  display: flex; gap: var(--s6); align-items: flex-end;
  padding: var(--s5) 0 var(--s6);
}
.page-head .cover {
  width: clamp(140px, 20vw, 208px);
  aspect-ratio: 1;
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-3);
}
.page-head .meta { min-width: 0; display: flex; flex-direction: column; gap: var(--s3); }
.page-head .stats { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; color: var(--text-2); font-size: 13px; }
.page-head .stats .dot { width: 3px; height: 3px; border-radius: 50%; background: var(--text-3); }
.page-actions { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }

@media (max-width: 700px) {
  .page-head { flex-direction: column; align-items: flex-start; gap: var(--s4); }
  .track { --track-cols: 40px 1fr 56px !important; }
  .track .ver { display: none; }
  .track-head { display: none; }
}


/* ── the list arriving ─────────────────────────────────────────────────────

   Rows come in together with a short stagger. Enough that the list assembles rather
   than blinking into place, and short enough that by the time the eye reaches the
   bottom of the screen it is already finished. Only the first dozen are staggered:
   past that nobody is watching, and a hundred delayed rows is a hundred animations. */

.track {
  animation: row-in 260ms var(--ease) both;
}
@keyframes row-in {
  from { opacity: 0; transform: translate3d(0, 6px, 0); }
}
.track:nth-child(1)  { animation-delay: 0ms; }
.track:nth-child(2)  { animation-delay: 18ms; }
.track:nth-child(3)  { animation-delay: 36ms; }
.track:nth-child(4)  { animation-delay: 54ms; }
.track:nth-child(5)  { animation-delay: 72ms; }
.track:nth-child(6)  { animation-delay: 90ms; }
.track:nth-child(7)  { animation-delay: 108ms; }
.track:nth-child(8)  { animation-delay: 126ms; }
.track:nth-child(9)  { animation-delay: 144ms; }
.track:nth-child(10) { animation-delay: 162ms; }
.track:nth-child(n+11) { animation-delay: 176ms; }

@media (prefers-reduced-motion: reduce) {
  .track { animation: none; }
}

/* ── the first screen ─────────────────────────────────────────────────────

   Shown only when there is nothing at all. It explains what a song is here, because
   that is the one idea the whole app rests on and it is not the usual one, and then it
   gives the two or three steps in the order they happen with the button for each.
   Nobody reads a paragraph and then goes looking for where to start. */

.start-here { text-align: left; padding: var(--s7) var(--s6); }
.start-here h3 { font-size: 22px; }
.start-here > p { margin-inline: 0; max-width: 56ch; }

.steps {
  list-style: none;
  margin: var(--s6) 0 0;
  padding: 0;
  display: flex; flex-direction: column; gap: var(--s5);
}
.steps li { display: flex; gap: var(--s4); align-items: flex-start; }

.step-n {
  display: grid; place-items: center;
  width: 28px; height: 28px; flex: none;
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent-hi);
  font-size: 13px; font-weight: 700;
  font-variant-numeric: tabular-nums;
  box-shadow: inset 0 0 0 1px var(--accent-line);
}
.step-body { display: flex; flex-direction: column; gap: 5px; align-items: flex-start; }
.step-body b { color: var(--text); font-size: 14.5px; }
.step-body > span { color: var(--text-2); max-width: 54ch; }
.step-body .btn, .step-body a.btn { margin-top: var(--s2); }

@media (max-width: 900px) {
  .start-here { padding: var(--s5) var(--s4); }
  .steps { gap: var(--s4); }
}

/* A section head carrying only a control. The album's track list has no title of its
   own, because the album's title is already the page. */
.section-head.bare { margin-bottom: var(--s2); min-height: 0; }

/* The albums section stays even with nothing in it, because "you have no albums" and
   "this library does not do albums" look identical when the whole section is absent,
   and the way to make the first one was hidden behind having one already. */
.album-none { margin: 0 0 var(--s3); font-size: 13px; max-width: 52ch; }


/* 45-renders.css */
/* ── the renders list, and choosing where one goes ────────────────────────── */

.render-row {
  display: flex; align-items: center; gap: var(--s3);
  flex-wrap: wrap;              /* so the arrangement strip gets a line of its own */
  padding: var(--s3);
  border-radius: var(--r-md);
  background: var(--grad-surface);
  border: 1px solid transparent;
  margin-bottom: var(--s2);
  transition: border-color var(--fast), background var(--fast);
}
/* The row is the button. Forty four copies of the same green pill down a list is a wall
   of shouting, and the thing you are choosing between is the render, not the pill. */
.render-row[role="button"] { cursor: pointer; }
.render-row:hover {
  border-color: var(--line-2);
  background: var(--grad-raised);
}
.render-row:focus-visible { outline: 2px solid var(--accent-hi); outline-offset: 2px; }

/* Says what pressing the row does.

   It used to rest on --elevated with --text-3 on it, which is the pairing the meta line
   uses and close enough to a disabled control that it was read as one. On a phone that
   was permanent: the accent only arrived on hover and a finger cannot hover.

   So it rests in the quiet green .pick-row.new already uses for "you can press this,
   said quietly", and fills in only when the pointer is on the row or a finger is on it.
   Still not forty four filled pills down a list, which is the thing that was rejected.

   Its height is .btn.sm's height, so a waiting row and a row with Put back on it line up
   down a mixed list rather than sitting two pixels apart. */
.row-go {
  flex: none;
  display: inline-flex; align-items: center;
  height: 30px;
  padding: 0 var(--s4);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-pill);
  background: var(--accent-soft);
  color: var(--accent-hi);
  font-size: 12.5px; font-weight: 650;
  white-space: nowrap;
  transition: background var(--fast), color var(--fast), border-color var(--fast);
}
.render-row:hover .row-go,
.render-row:focus-visible .row-go,
.render-row[role="button"]:active .row-go {
  background: var(--grad-accent);
  border-color: var(--accent);
  color: var(--text-on-accent);
}

/* The action and the tools, held together only so that a phone can put them on one line.

   On a wide row this wrapper is not there at all: display: contents hands its children
   straight to the row, so the layout is the flat one the rule describes and the tools'
   auto margin still measures against the whole row. On a narrow one it becomes a real
   box asking for the full width, which is what forces the break. Without it the name
   column shrank just enough to let the pill stay up on the first line and the two icons
   wrapped on their own, which is a tall row with a hole in it. */
.row-actions { display: contents; }

/* The two things a row can do that are not the row's own job, kept together at the right
   edge so they read as a pair of tools rather than as two more steps in the sentence.
   Their own gap is tighter than the row's and there is more space in front of them: at
   the row's uniform twelve pixels, five controls all read as five peers, which is what
   made the order look scattered.

   The auto margin does nothing on a wide row, where the name column has already taken
   the slack. It earns its place on a phone, where the row wraps and this is what stops
   the buttons trailing off into the middle of the second line. */
.row-tools {
  display: flex; align-items: center; gap: 2px;
  flex: none;
  margin-left: auto;
  padding-left: var(--s3);
}
/* Round, never squeezed into ovals. Only the play button said flex: none. */
.row-tools .icon-btn { flex: none; }
/* Throwing a render away is the one thing in this row that cannot be taken back, so it
   is last and it is the only icon that changes colour under the pointer. */
.row-tools [data-act="dismiss"]:hover { color: var(--bad-hi); }
.render-row .grow { min-width: 0; flex: 1; display: block; }
/* One that has found its song stays readable but stops asking for attention. */
.render-row.used { opacity: 0.62; }
.render-row.used:hover { opacity: 1; }

/* The one thing on the row you can change, sized to itself.

   It was display: block, so it filled the whole name column: the hover underline ran the
   width of the screen and clicking the empty space to the right of a short name opened a
   sheet you had not aimed at. max-width keeps .truncate's ellipsis working, and
   vertical-align stops an inline-block leaving a gap under the text. */
.render-name {
  display: inline-block;
  max-width: 100%;
  vertical-align: bottom;
  font-weight: 650;
  cursor: text;
  border-bottom: 1px solid transparent;
}
.render-name:hover { border-bottom-color: var(--line-2); }
.render-row .s {
  display: block;
  font-size: 12px;
  color: var(--text-3);
  margin-top: 1px;
}
.render-row .s b { color: var(--text-2); font-weight: 650; }

.icon-btn.play {
  background: var(--elevated);
  color: var(--text);
  flex: none;
}
.icon-btn.play:hover { background: var(--accent); color: var(--text-on-accent); }
.icon-btn.play.sm { width: 30px; height: 30px; }

/* ── picking: a song for a render, or a render for a song ─────────────────── */

.pick-list {
  max-height: 46vh;
  overflow-y: auto;
  margin-top: var(--s2);
  /* Its own scroll, so the sheet keeps its buttons on screen with fifty songs in it. */
  padding-right: 2px;
}

.pick-row {
  display: flex; align-items: center; gap: var(--s3);
  width: 100%;
  padding: var(--s2) var(--s3);
  border: 1px solid transparent;
  border-radius: var(--r-md);
  background: transparent;
  color: inherit;
  text-align: left;
  cursor: pointer;
  transition: background var(--fast), border-color var(--fast);
}
.pick-row:hover { background: var(--hover); border-color: var(--line); }
.pick-row .grow { min-width: 0; flex: 1; display: block; }
.pick-row .t { display: block; font-weight: 650; }
.pick-row .s { display: block; font-size: 12px; color: var(--text-3); }

.pick-row.new {
  background: var(--accent-soft);
  border-color: var(--accent-line);
}
.pick-row.new:hover { background: rgba(84, 179, 122, 0.2); }
.pick-plus {
  display: grid; place-items: center;
  width: 38px; height: 38px; flex: none;
  border-radius: var(--r-md);
  background: var(--accent);
  color: var(--text-on-accent);
  font-size: 20px; font-weight: 600; line-height: 1;
}

.pick-go {
  flex: none;
  font-size: 12px;
  font-weight: 650;
  color: var(--text-3);
  padding: 5px var(--s3);
  border-radius: var(--r-pill);
  background: var(--elevated);
}
.pick-row:hover .pick-go { background: var(--accent); color: var(--text-on-accent); }

.cover.sm { width: 38px; height: 38px; }

/* ── how many are still waiting, on the rail ──────────────────────────────── */

.nav-badge {
  margin-left: auto;
  min-width: 20px;
  padding: 0 6px;
  border-radius: var(--r-pill);
  background: var(--accent);
  color: var(--text-on-accent);
  font-size: 11px;
  font-weight: 700;
  line-height: 19px;
  text-align: center;
}

@media (max-width: 900px) {
  /* On a phone the row wraps on purpose. The name asks for sixty per cent so the break
     happens after it: cover, play and name take the first line, the action and the tools
     take the second.

     The buttons used to be told to grow into whatever was left on that second line,
     which is how Put back ended up as a two hundred pixel pill with two icons stuck on
     the end of it. They keep their own size now, and .row-tools' auto margin is what
     puts the space between them. Wrapping itself is unconditional, up at the top of the
     file, so it is not restated here. */
  .render-row .grow { flex: 1 1 60%; }
  .row-actions {
    display: flex; align-items: center; gap: var(--s3);
    flex: 1 1 100%;
  }
}


/* The render's own square. It is a generated letter tile, never artwork, and it is not a
   control. It used to be a button that made a song out of the render, which meant the
   biggest press target in a row whose job is "add this to a song" did something else,
   and on a row that had already found its song it made a second one. No display here:
   .cover already sets its own, and two single class rules disagreeing about display on
   one element is the thing that put the name's cursor 109 lines from its own rule. */
.render-art { width: 40px; height: 40px; border-radius: var(--r-sm); }

/* The two dates a render can carry. They sit inside the name column, under the size
   line, so a long name never pushes them out of the row and nothing here is the first
   thing to be clipped. This used to say flex: 0 0 100%, which did nothing at all: its
   parent is a block, and the line it gets is ordinary block flow. */
.render-dates {
  display: flex; align-items: center; gap: var(--s2);
  margin-top: 2px;
  font-size: 11.5px; color: var(--text-3);
  overflow: hidden;
}
.render-dates span { overflow: hidden; text-overflow: ellipsis; }
.render-dates .dot {
  width: 3px; height: 3px; border-radius: 50%;
  background: currentColor; opacity: 0.5; flex: none;
}

/* The shape of the audio, behind the row.
 *
 * Recognising a bounce by its shape is faster than reading its name, and a name like
 * "Pattern 1_5" carries nothing at all. It sits behind the row's own content at low
 * contrast, stretched to the full width, because it is for glancing at rather than
 * reading: what it has to show is whether there is anything there and roughly where the
 * loud part is. */
.render-row { position: relative; overflow: hidden; }
.render-row > * { position: relative; z-index: 1; }

.wave { display: block; }
.wave polygon { fill: currentColor; }

.render-wave {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  z-index: 0;
  color: var(--accent);
  opacity: 0.13;
  pointer-events: none;
  transition: opacity var(--fast);
}
.render-row:hover .render-wave { opacity: 0.2; }
.render-row.used .render-wave { color: var(--text-3); opacity: 0.1; }

/* Something is wrong with this file, said before anything is played. */
.render-row.in-trouble { border-color: var(--bad); }
.render-row.in-trouble .render-wave { color: var(--bad); opacity: 0.16; }

.trouble {
  display: inline-flex; align-items: center; gap: 4px;
  margin-left: var(--s2);
  padding: 1px 7px 1px 5px;
  border-radius: var(--r-pill);
  border: 1px solid var(--bad);
  color: var(--bad-hi);
  font-size: 11px; font-weight: 700;
  letter-spacing: 0.02em;
  white-space: nowrap;
  vertical-align: middle;
}

/* The same shape in the sheets where a render is chosen. */
.pick-wave {
  flex: none;
  width: 74px; height: 26px;
  color: var(--accent);
  opacity: 0.4;
}
.pick-row:hover .pick-wave { opacity: 0.65; }


/* 50-song.css */
/* The song page.
 *
 * One page, read top to bottom, with no tabs. Everything a song has sits in its own
 * place: the cover, the name, the version, then the words, then the sound, then the
 * renders. Nothing is behind a button that only says what is behind it.
 *
 * The things themselves are the controls. The title is an editable line, not a Rename
 * button. The cover takes a pen when you point at it. The version reads as a version and
 * opens the list when you press it.
 */

/* The song's ground now lives at the page level, in 20-shell.css, so the artwork runs
   behind the rail and the player instead of stopping at this panel's edge. What is left
   here is the stacking that put the page's content above it. */
.view > * { position: relative; z-index: 1; }

/* ── the header ───────────────────────────────────────────────────────────── */
.hero {
  position: relative;
  /* Out past the view's padding, so the artwork reaches the panel edges. */
  margin: calc(var(--s2) * -1) calc(var(--s5) * -1) var(--s6);
  padding: var(--s8) var(--s5) var(--s6);
  isolation: isolate;
  /* The artwork behind this is deliberately larger than the header so the blur has no
     hard edge. Without clipping it here that overhang becomes page width, and the whole
     library scrolls sideways. */
  overflow: hidden;
}

/* The song's own artwork, enormous and out of focus, moving slower than the page.
   It is the only decoration on the page and it is made of the song's own content. */
.hero-art {
  position: absolute;
  inset: -30% -15% -10%;
  z-index: -2;
  background-size: cover;
  background-position: center;
  filter: blur(64px) saturate(170%);
  opacity: 0.5;
  transform: translate3d(0, 0, 0) scale(1.3);
  will-change: transform;
  /* Faded out rather than cut off.
   *
   * The header is clipped, so without this the artwork ends in a hard horizontal line
   * across the page. Veiling that line meant painting the bottom of the header a solid
   * colour and hoping it matched the ground below, which it stopped doing the moment
   * the ground became a gradient. Fading the art itself needs no such luck. */
  -webkit-mask-image: linear-gradient(180deg, black 0%, black 52%, transparent 97%);
  mask-image: linear-gradient(180deg, black 0%, black 52%, transparent 97%);
}
.hero-art.flat {
  /* No artwork yet: a wash built from the title, so the page still has a temperature. */
  background-image: radial-gradient(120% 90% at 20% 0%,
      hsl(var(--hue) 42% 22%), transparent 70%),
    radial-gradient(120% 90% at 90% 20%,
      hsl(calc(var(--hue) + 48) 38% 17%), transparent 65%);
  filter: blur(40px);
}
/* Only a darkener now, so the title reads against whatever the artwork happens to be.
   It fades out with the art rather than ending in a colour of its own. */
.hero-veil {
  position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(180deg,
    rgba(0, 0, 0, 0.30) 0%,
    rgba(0, 0, 0, 0.52) 46%,
    rgba(0, 0, 0, 0.34) 78%,
    transparent 100%);
}

.hero-row { display: flex; gap: var(--s6); align-items: flex-end; }

/* The cover, and the pen that appears on it. */
.cover-edit {
  position: relative;
  width: clamp(132px, 19vw, 208px);
  aspect-ratio: 1;
  border: 0; padding: 0;
  border-radius: var(--r-lg);
  overflow: hidden;
  cursor: pointer;
  flex: none;
  background: none;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.45);
  transition: transform var(--fast);
}
.cover-edit .cover { width: 100%; height: 100%; border-radius: var(--r-lg); }
.cover-edit:hover { transform: translateY(-2px); }
.cover-pen {
  position: absolute; top: var(--s2); right: var(--s2);
  width: 30px; height: 30px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: rgba(10, 13, 11, 0.66);
  backdrop-filter: blur(6px);
  color: var(--text-on-color);
  opacity: 0;
  transform: scale(0.9);
  transition: opacity var(--fast), transform var(--fast);
}
.cover-edit:hover .cover-pen,
.cover-edit:focus-visible .cover-pen { opacity: 1; transform: none; }

.hero-meta { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: var(--s3); }

/* The name. Bigger and bolder than anything else in the product, because it is the
   thing the page is about. */
.song-title {
  font-family: var(--display);
  font-size: clamp(38px, 6.4vw, 84px);
  font-weight: 900;
  line-height: 0.94;
  letter-spacing: -0.03em;
  margin: 0;
  color: var(--text);
  word-break: break-word;
  cursor: text;
  border-radius: var(--r-sm);
  transition: background var(--fast);
}
.song-title:hover { background: rgba(255, 255, 255, 0.05); }
.song-title-input {
  font-family: var(--display);
  font-size: clamp(38px, 6.4vw, 84px);
  font-weight: 900;
  line-height: 0.94;
  letter-spacing: -0.03em;
  width: 100%;
  background: rgba(0, 0, 0, 0.25);
  border: 0;
  border-bottom: 2px solid var(--accent);
  color: var(--text);
  padding: 0;
  outline: none;
}

/* Every name it used to have, the way an account remembers its old handles. */
.also-known {
  position: relative;
  align-self: flex-start;
  font-size: 12.5px;
  color: var(--text-3);
  background: none; border: 0; padding: 2px 0;
  cursor: pointer;
  text-align: left;
}
.also-known:hover { color: var(--text-2); }
.also-known b { font-weight: 600; color: var(--text-2); }
.name-list {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 20;
  min-width: 230px;
  background: var(--elevated);
  border: 1px solid var(--line-2);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-3);
  padding: var(--s2);
  animation: pop 140ms var(--ease);
}
@keyframes pop { from { opacity: 0; transform: translateY(-4px); } }
.name-list li {
  display: flex; justify-content: space-between; gap: var(--s4);
  padding: 6px var(--s3); border-radius: var(--r-sm);
  font-size: 13px;
}
.name-list ul { list-style: none; margin: 0; padding: 0; }
.name-list .when { color: var(--text-3); font-size: 11.5px; white-space: nowrap; }

.hero-line { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }
.hero-facts { display: flex; align-items: center; gap: var(--s2); color: var(--text-2); font-size: 13.5px; }
.hero-facts .dot { width: 3px; height: 3px; border-radius: 50%; background: var(--text-3); }
.hero-facts a:hover { color: var(--text); text-decoration: underline; }

/* The version reads as a version and opens the list. Not a button labelled Versions. */
.version-pick {
  display: inline-flex; align-items: center; gap: 7px;
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid var(--line-2);
  border-radius: var(--r-pill);
  padding: 5px var(--s3) 5px var(--s4);
  font-family: var(--display);
  font-weight: 700;
  font-size: 15px;
  letter-spacing: -0.01em;
  color: var(--text);
  cursor: pointer;
  transition: background var(--fast), border-color var(--fast);
}
.version-pick:hover { background: rgba(255, 255, 255, 0.12); border-color: var(--accent-line); }
.version-pick .caret { opacity: 0.5; }

/* ── the blocks below ─────────────────────────────────────────────────────── */
.block { margin-bottom: var(--s8); scroll-margin-top: var(--s5); }
.block-head {
  display: flex; align-items: center; gap: var(--s3);
  margin-bottom: var(--s4);
}
.block-head h2 {
  font-family: var(--display);
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-3);
}
.block-head .grow { flex: 1; }
.block-tools { display: flex; align-items: center; gap: var(--s2); opacity: 0; transition: opacity var(--fast); }
.block:hover .block-tools, .block:focus-within .block-tools { opacity: 1; }

/* ── A and B, in the header ───────────────────────────────────────────────── */
.ab-inline { display: flex; align-items: stretch; gap: var(--s2); flex-wrap: wrap; }
.slot-pick {
  position: relative;
  display: inline-flex; align-items: center; gap: var(--s2);
  min-width: 148px;
  padding: 7px var(--s3) 7px var(--s2);
  border: 1px solid var(--line-2);
  border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.06);
  color: var(--text);
  font-size: 13px; font-weight: 600;
  cursor: pointer;
  text-align: left;
  transition: background var(--fast), border-color var(--fast);
}
.slot-pick:hover { background: rgba(255, 255, 255, 0.12); }
.slot-pick .k {
  display: grid; place-items: center;
  width: 22px; height: 22px; flex: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.12);
  font-family: var(--display); font-weight: 800; font-size: 11px;
}
.slot-pick .v { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Two targets in one chip: the chip chooses the deck, the caret opens its menu. A
   13px arrow is not something to aim at, so the box around it is padded out to a real
   size and only the arrow inside it is drawn. */
.slot-pick .caret {
  opacity: 0.45; flex: none;
  box-sizing: content-box;
  padding: 6px 4px 6px 6px; margin: -6px -2px -6px 0;
  border-radius: var(--r-sm);
}
.slot-pick .caret:hover { opacity: 1; background: var(--hover); }
/* The deck a chosen equaliser lands on, playing or not. Only an outline, because at rest
   there is nothing coming out of the speakers to point at. Above .live so that when both
   are set, which is the usual case, the louder one wins on file order. */
.slot-pick.sel { border-color: var(--accent-line); }
/* The side you are actually hearing. */
.slot-pick.live { border-color: var(--accent); background: var(--accent-soft); }
.slot-pick.live .k { background: var(--accent); color: var(--text-on-accent); }

.ab-switch {
  display: grid; place-items: center;
  width: 40px; flex: none;
  border: 1px solid var(--line-2);
  border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.06);
  color: var(--text-2);
  cursor: pointer;
  transition: background var(--fast), color var(--fast), transform var(--fast);
}
.ab-switch:hover { background: var(--accent-soft); color: var(--accent-hi); }
.ab-switch:active { transform: scale(0.94); }

/* One menu for both things a slot carries. */
/* Placed on the body rather than inside the header.
   .hero clips its children so the oversized blurred artwork has no hard edge, and that
   clipping took the dropdown with it: the menu opened inside the header and was cut off
   at its bottom edge. Fixed positioning puts it above every stacking context there is. */
.slot-menu {
  position: fixed; z-index: 80;
  min-width: 260px; max-height: 320px; overflow-y: auto;
  background: var(--glass-2);
  -webkit-backdrop-filter: var(--glass-filter-thin);
  backdrop-filter: var(--glass-filter-thin);
  border: 1px solid var(--glass-line);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-3), var(--edge-top);
  isolation: isolate;
  padding: var(--s2);
  animation: pop 140ms var(--ease);
  cursor: default;
}
.menu-group {
  font-size: 10px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--text-3); padding: var(--s3) var(--s3) var(--s2);
}
.menu-row {
  display: flex; align-items: center; gap: var(--s3); width: 100%;
  padding: 7px var(--s3);
  border: 0; border-radius: var(--r-sm);
  background: none; color: var(--text-2);
  font-size: 13px; text-align: left; cursor: pointer;
  transition: background var(--fast), color var(--fast);
}
.menu-row:hover { background: var(--hover); color: var(--text); }
.menu-row.on { background: var(--accent-soft); color: var(--accent-hi); }
.menu-row .tagline {
  font-family: var(--display); font-weight: 800; font-size: 12px;
  min-width: 30px; flex: none;
}
.menu-row .grow { flex: 1; min-width: 0; }
.menu-row .when { font-size: 11.5px; color: var(--text-3); font-variant-numeric: tabular-nums; }
.menu-row.add { color: var(--text-3); }
.menu-row.add:hover { color: var(--accent-hi); }
/* Taking a render away lives on its row, since the section that used to hold that is
   gone and a render is the only thing that knows which render it is. */
.row-drop {
  display: grid; place-items: center;
  width: 20px; height: 20px; flex: none;
  border: 0; border-radius: 50%;
  background: none; color: var(--text-3);
  cursor: pointer; opacity: 0;
  transition: opacity var(--fast), background var(--fast), color var(--fast);
}
.menu-row:hover .row-drop { opacity: 1; }
.row-drop:hover { background: rgba(212, 104, 94, 0.16); color: var(--bad); }

/* ── lyrics, as cards you move between ───────────────────────────────────── */
.lyric-deck {
  display: flex; align-items: center; gap: var(--s3);
  justify-content: center;
}
.deck-window {
  flex: 1;
  /* Wide enough that a long line of a verse is a line rather than a wrap. The cards are
     the middle of the page, so they get the room the page has. */
  max-width: 940px;
  overflow: hidden;
  border-radius: var(--r-lg);
  touch-action: pan-y;          /* vertical scrolling still belongs to the page */
}
.deck-track { display: flex; will-change: transform; }

/* Glass, over the song's own wash.
 *
 * Eighty percent transparent, so what is behind is most of what you see, and then the
 * same refraction the rail and the player use: url(#glass) is feTurbulence driving an
 * feDisplacementMap, which pushes the backdrop sideways as it passes rather than only
 * softening it. Blurring is what distance does; bending is what glass does.
 *
 * The card keeps its own colour rather than the white tint the chrome uses, because the
 * chrome floats over the library and this floats over one song's artwork, and a white
 * veil over that would wash the colour out of the thing it is meant to be showing. */
.lyric-card {
  flex: 0 0 100%;
  min-width: 100%;
  padding: var(--s6);
  border-radius: var(--r-lg);
  background: rgba(18, 18, 20, 0.2);
  -webkit-backdrop-filter: var(--glass-filter);
  backdrop-filter: var(--glass-filter);
  border: 1px solid var(--glass-line);
  box-shadow: var(--edge-top), var(--glass-shadow);
  isolation: isolate;
  opacity: 0.55;
  transform: scale(0.985);
  transition: opacity var(--med), transform var(--med);
}
.lyric-card.on { opacity: 1; transform: none; }
.lyric-card.reading { border-color: var(--accent-line); }
.lyric-card.editing { opacity: 1; transform: none; }

.card-title {
  font-family: var(--display);
  font-size: 22px; font-weight: 800; letter-spacing: -0.02em;
  margin: 0 0 var(--s4);
  color: var(--text);
}
.card-body {
  font-size: 16.5px; line-height: 1.85;
  color: var(--text);
  min-height: 120px;
  cursor: text;
  border-radius: var(--r-sm);
}
.card-body.empty-words { color: var(--text-3); font-style: italic; }

/* Markdown output. Every line is a line: a verse is not a paragraph to be reflowed. */
.md-line { min-height: 1.85em; }
.md-gap { height: 0.9em; }
.card-body h1, .card-body h2, .card-body h3,
.card-body h4, .card-body h5, .card-body h6 {
  font-family: var(--display); font-weight: 700; letter-spacing: -0.01em;
  margin: var(--s4) 0 var(--s2); font-size: 17px; color: var(--text);
}
.card-body ul, .card-body ol { margin: var(--s2) 0; padding-left: var(--s5); }
.card-body blockquote {
  margin: var(--s3) 0; padding-left: var(--s4);
  border-left: 2px solid var(--accent-line); color: var(--text-2);
}
.card-body blockquote p { margin: 0; }
.card-body hr { border: 0; border-top: 1px solid var(--line-2); margin: var(--s4) 0; }
/* Translucent, not black. On a glass card an opaque pill reads as a hole punched
   through it, which is the one thing that gives the effect away. */
.card-body code {
  font-size: 0.9em; background: rgba(0, 0, 0, 0.38); padding: 1px 5px;
  border-radius: var(--r-sm);
}
.card-body a { color: var(--accent-hi); text-decoration: underline; }

/* Almost not there.
 *
 * Reading and writing are the same view: the same type, the same measure, the same place
 * on the card. A bordered box with its own background made writing feel like filling in
 * a form laid over the page, when the whole point is that the card is the page and the
 * words are the thing. What is left is a caret and a faint line under the block so the
 * card still reads as live rather than merely rendered. */
textarea.card-edit {
  display: block;
  width: 100%;
  min-height: 220px;
  font: inherit;
  font-size: 16.5px;
  line-height: 1.85;
  color: var(--text);
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: 0;
  resize: none;
  overflow: hidden;          /* it grows to the words instead */
  outline: none;
}
textarea.card-edit::placeholder { color: var(--text-3); font-style: italic; }
/* The one mark that says this card is being written in rather than read. */
.lyric-card.editing { border-color: var(--accent-line); }
.lyric-card.editing .card-foot {
  border-top: 1px solid var(--line);
  padding-top: var(--s3);
  margin-top: var(--s4);
}
/* It names both ways out now, which is longer than it was, and on a phone the line
   count and the hint no longer fit side by side. Wrapping is right here: the hint is the
   only line in the editor that teaches a gesture, and clipping it would take away the
   half that says the words can be left behind. */
.edit-hint { font-size: 11.5px; text-align: right; }
.card-foot {
  display: flex; align-items: center; gap: var(--s2);
  flex-wrap: wrap;
  margin-top: var(--s3);
}

.deck-arrow {
  display: grid; place-items: center;
  width: 40px; height: 40px; flex: none;
  border: 1px solid var(--line-2); border-radius: 50%;
  background: var(--surface-2); color: var(--text-2);
  cursor: pointer;
  transition: background var(--fast), color var(--fast), opacity var(--fast);
}
.deck-arrow:hover { background: var(--elevated); color: var(--text); }
.deck-arrow[disabled] { opacity: 0.25; cursor: default; }

.deck-dots { display: flex; justify-content: center; gap: 6px; margin-top: var(--s4); }
/* A dot is 7px because that is the right size to look at, and 7px is far too small to
   hit with a thumb. The dot keeps its size; the target around it does not. */
.deck-dot::after {
  content: "";
  position: absolute;
  left: 50%; top: 50%;
  width: 30px; height: 30px;
  transform: translate(-50%, -50%);
}
.deck-dot {
  position: relative;
  width: 7px; height: 7px; padding: 0;
  border: 0; border-radius: 50%;
  background: var(--line-2);
  cursor: pointer;
  transition: background var(--fast), width var(--fast);
}
.deck-dot.on { background: var(--accent); width: 22px; border-radius: var(--r-pill); }

/* Looking at an old version is looking, not restoring. */
.time-bar {
  display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap;
  padding: var(--s3) var(--s4);
  margin-bottom: var(--s4);
  border-radius: var(--r-md);
  background: rgba(217, 164, 65, 0.1);
  border: 1px solid rgba(217, 164, 65, 0.3);
  font-size: 13px;
}
.time-bar .grow { flex: 1; }

.history-rail {
  display: flex; flex-direction: column; gap: 1px;
  max-height: 300px; overflow-y: auto;
  max-width: 640px; margin: var(--s5) auto 0;
}
.history-entry {
  display: flex; align-items: center; gap: var(--s3);
  padding: 7px var(--s3);
  border-radius: var(--r-sm);
  cursor: pointer;
  font-size: 13px; color: var(--text-2);
  transition: background var(--fast), color var(--fast);
}
.history-entry:hover { background: var(--hover); color: var(--text); }
.history-entry.on { background: var(--accent-soft); color: var(--accent-hi); }
.history-entry .when { font-variant-numeric: tabular-nums; }
.history-entry .grow { flex: 1; }
.history-entry .size { color: var(--text-3); font-size: 11.5px; }

/* ── versions ─────────────────────────────────────────────────────────────── */
.ab-bar {
  display: grid; grid-template-columns: 1fr auto 1fr;
  gap: var(--s3); align-items: stretch; margin-bottom: var(--s4);
}
.ab-slot {
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface-2);
  padding: var(--s4) var(--s5);
  cursor: pointer;
  transition: border-color var(--fast), background var(--fast);
  text-align: left;
}
.ab-slot:hover { background: var(--elevated); }
.ab-slot.on { border-color: var(--accent); background: var(--accent-soft); }
.ab-slot .k { font-size: 11px; font-weight: 700; letter-spacing: 0.16em; color: var(--text-3); }
.ab-slot.on .k { color: var(--accent-hi); }
.ab-slot .v { font-family: var(--display); font-size: 22px; font-weight: 800; letter-spacing: -0.02em; }
.ab-slot .d { font-size: 12px; color: var(--text-3); font-variant-numeric: tabular-nums; }
.ab-mid { display: grid; place-items: center; gap: var(--s2); }
.ab-hint { font-size: 11.5px; color: var(--text-3); text-align: center; }

.version-row {
  display: grid;
  grid-template-columns: 62px 1fr 92px 58px auto;
  align-items: center; gap: var(--s3);
  padding: var(--s2) var(--s3);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: background var(--fast);
}
.version-row:hover { background: var(--hover); }
.version-row.current { background: var(--accent-soft); }
.version-row .n { font-family: var(--display); font-weight: 800; font-size: 15px; }
/* The name the file arrived under. Six renders later it is often the only thing
   that says which session it came out of, so it is the line, not a footnote. */
.version-row .og-name { display: block; font-size: 13.5px; color: var(--text); }
.version-row.current .n { color: var(--accent-hi); }
.version-row .when, .version-row .dur { font-size: 12.5px; color: var(--text-3); font-variant-numeric: tabular-nums; }
.version-row .acts { display: flex; gap: var(--s1); opacity: 0; transition: opacity var(--fast); }
.version-row:hover .acts, .version-row:focus-within .acts { opacity: 1; }

/* ── artwork strip ────────────────────────────────────────────────────────── */
.art-strip { display: flex; gap: var(--s2); flex-wrap: wrap; }
/* A tile is a frame with a corner control sitting on its edge, so the tile itself must
   not clip: the clipping happens one level in, on the frame that holds the picture. */
.art-tile {
  position: relative; width: 92px; height: 92px;
  border-radius: var(--r-md);
  cursor: pointer;
}
.art-frame {
  position: absolute; inset: 0;
  display: block;
  border-radius: var(--r-md);
  overflow: hidden;
}
.art-tile img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Removing sits on the corner, not across the bottom.
 *
 * It used to be a bar the full width of the tile and about a third of its height,
 * invisible until the pointer was already over it, so clicking a picture to choose it
 * deleted it instead. Choosing happens forty times; deleting happens once and cannot be
 * taken back. */
.art-tile .art-drop {
  position: absolute; top: -7px; right: -7px;
  width: 22px; height: 22px; padding: 0;
  display: grid; place-items: center;
  border: 1px solid var(--line-2); border-radius: 50%;
  background: var(--elevated); color: var(--text-2);
  opacity: 0; transform: scale(0.8);
  transition: opacity var(--fast), transform var(--fast), background var(--fast),
              color var(--fast), border-color var(--fast);
  cursor: pointer;
  z-index: 2;
}
.art-tile:hover .art-drop, .art-tile:focus-within .art-drop { opacity: 1; transform: none; }
.art-tile .art-drop:hover {
  background: var(--bad); color: var(--text-on-color); border-color: var(--bad);
}

/* A finger cannot hover, so on a touch screen it is simply always there. Sitting off the
   corner rather than over the picture is what keeps it out of the way of choosing. */
@media (hover: none) {
  .art-strip { padding: 8px 8px 0; margin: -8px -8px 0; }
  .art-tile .art-drop { opacity: 1; transform: none; }
}

/* Which one is chosen, said in words rather than only by an outline. */
.art-badge {
  position: absolute; left: 4px; bottom: 4px;
  padding: 1px 7px; border-radius: var(--r-pill);
  font-size: 10.5px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
  background: var(--accent); color: var(--text-on-accent);
  pointer-events: none;
  opacity: 0; transform: translateY(3px);
  transition: opacity var(--fast), transform var(--fast);
}
.art-tile.is-cover .art-badge { opacity: 1; transform: none; }

.art-tile.is-cover .art-frame { outline: 2px solid var(--accent); outline-offset: -2px; }
.art-tile:focus-visible { outline: 2px solid var(--accent-hi); outline-offset: 3px; }
.art-tile:not(.is-cover):hover .art-frame {
  outline: 2px solid var(--line-2); outline-offset: -2px;
}

.art-add {
  width: 92px; height: 92px;
  border: 1.5px dashed var(--line-2);
  border-radius: var(--r-md);
  background: none; color: var(--text-3);
  display: grid; place-items: center;
  cursor: pointer;
  transition: border-color var(--fast), color var(--fast);
}
.art-add:hover { border-color: var(--accent); color: var(--accent-hi); }

/* ── phone ────────────────────────────────────────────────────────────────── */
@media (max-width: 720px) {
  .hero { padding: var(--s6) var(--s4) var(--s5); margin-inline: calc(var(--s4) * -1); }
  .hero-row { flex-direction: column; align-items: flex-start; gap: var(--s4); }
  .cover-edit { width: 132px; }
  .song-title { font-size: clamp(30px, 11vw, 46px); }
  .song-title-input { font-size: clamp(30px, 11vw, 46px); }

  /* A and B go full width and stack, so neither is a squeezed chip. */
  .ab-inline { width: 100%; display: grid; grid-template-columns: 1fr auto 1fr; }
  .slot-pick { min-width: 0; }
  .slot-menu { left: 0; right: auto; min-width: min(280px, 84vw); }
  .slot-pick[data-slot="B"] .slot-menu { left: auto; right: 0; }

  .block { margin-bottom: var(--s7); }
  /* Nothing to hover with on a phone, so the tools are always shown, and they take
     their own line rather than running off the edge of the screen. */
  .block-head { flex-wrap: wrap; }
  .block-tools { opacity: 1; width: 100%; flex-wrap: wrap; }
  .block-head .grow { display: none; }
  .preset-row { width: 100%; }

  /* The deck fills the width and the arrows step aside for the swipe. */
  .deck-arrow { display: none; }
  .deck-window { max-width: none; }
  .lyric-card { padding: var(--s4); }
  .card-title { font-size: 19px; }
  .card-body { font-size: 16px; }
  .deck-dot { width: 9px; height: 9px; }
  .deck-dot.on { width: 26px; }

  .version-row { grid-template-columns: 52px 1fr auto; }
  .version-row .when, .version-row .dur { display: none; }
  .version-row .acts { opacity: 1; }

  .ab-bar { grid-template-columns: 1fr; }
  .ab-mid { padding: var(--s2) 0; }

  .art-tile, .art-add { width: 76px; height: 76px; }
  .time-bar { font-size: 12.5px; }
}


/* ── adding a render from the facts line ──────────────────────────────────── */

.fact-add {
  position: relative;
  display: inline-grid; place-items: center;
  width: 24px; height: 24px;
  margin-left: 2px;
  vertical-align: -5px;
  border: 0; border-radius: 50%;
  background: rgba(255, 255, 255, 0.09);
  color: var(--text-2);
  cursor: pointer;
  transition: background var(--fast), color var(--fast), transform var(--fast);
}
/* Same again: small to look at, big enough to press. */
.fact-add::after {
  content: ""; position: absolute; inset: -8px;
}
.fact-add:hover { background: var(--accent); color: var(--text-on-accent); }
.fact-add:active { transform: scale(0.9); }

/* Offered instead of an empty B chip and a swap arrow that could not do anything. */
.ab-add { white-space: nowrap; }

/* ── the shared menu ──────────────────────────────────────────────────────── */

.app-menu { min-width: 200px; }
.app-menu .menu-row { gap: var(--s3); }
.app-menu .menu-row:focus-visible { background: var(--hover); color: var(--text); outline: none; }
.app-menu .menu-row[disabled] { opacity: 0.4; cursor: default; }
.app-menu .menu-row[disabled]:hover { background: none; color: var(--text-2); }
.app-menu .menu-row.danger { color: var(--bad); }
.app-menu .menu-row.danger:hover { background: rgba(212, 104, 94, 0.12); color: var(--bad-hi); }

.menu-icon { width: 15px; height: 15px; flex: none; opacity: 0.75; }
.menu-hint {
  font-size: 11px; color: var(--text-3);
  font-variant-numeric: tabular-nums;
  flex: none;
}
.menu-divider {
  height: 1px; margin: var(--s2) var(--s2);
  background: var(--line);
}

/* ── the running orders a song is in, and where it is on YouTube ──────────── */

.in-none, .yt-none { margin: 0; font-size: 13px; max-width: 60ch; }

.in-list, .yt-list { display: flex; flex-direction: column; gap: var(--s2); }

/* A row is a button, because its whole job is to start playing from here. */
.in-row {
  display: flex; align-items: center; gap: var(--s3);
  width: 100%; padding: var(--s3);
  background: var(--grad-surface);
  border: 1px solid transparent; border-radius: var(--r-md);
  color: var(--text); font: inherit; text-align: left; cursor: pointer;
  transition: border-color var(--fast), background var(--fast);
}
.in-row:hover { border-color: var(--line-2); background: var(--grad-raised); }
.in-row:focus-visible { outline: 2px solid var(--accent-hi); outline-offset: 2px; }

.in-go {
  flex: none; width: 30px; height: 30px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--accent-soft); color: var(--accent-hi);
  transition: background var(--fast), color var(--fast);
}
.in-row:hover .in-go { background: var(--accent); color: var(--text-on-accent); }

.in-row .t { display: block; font-weight: 650; font-size: 13.5px; }
.in-row .s { display: block; font-size: 11.5px; color: var(--text-3); }

/* Opening the list instead of playing it. Its own target inside the row. */
.in-open {
  flex: none; padding: 4px 10px; border-radius: var(--r-pill);
  font-size: 11.5px; color: var(--text-2); text-decoration: none;
  border: 1px solid var(--line);
}
.in-open:hover { border-color: var(--accent-line); color: var(--accent-hi); }

.yt-row {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3); border-radius: var(--r-md);
  background: var(--grad-surface); border: 1px solid transparent;
  transition: border-color var(--fast);
}
.yt-row:hover { border-color: var(--line-2); }
.yt-thumb {
  flex: none; width: 84px; height: 47px;
  border-radius: var(--r-sm); object-fit: cover; display: block;
  background: var(--grad-sunken);
}
.yt-row .t { display: block; font-weight: 650; font-size: 13.5px; }
.yt-row .s { display: block; font-size: 11.5px; color: var(--text-3); }

@media (max-width: 640px) {
  .yt-thumb { width: 64px; height: 36px; }
  .in-open { display: none; }
}


/* 60-player.css */
/* The player: docked, and able to grow into the version comparison workspace. */

.player {
  grid-area: player;
  height: var(--player-h);
  background: var(--glass);
  -webkit-backdrop-filter: var(--glass-filter);
  backdrop-filter: var(--glass-filter);
  border: 1px solid var(--glass-line);
  box-shadow: var(--edge-top), var(--glass-shadow);
  isolation: isolate;
  border-radius: var(--r-lg);
  padding: 0 var(--s5);
  display: grid;
  grid-template-columns: minmax(180px, 1fr) minmax(280px, 2.2fr) minmax(180px, 1fr);
  align-items: center;
  gap: var(--s4);
}

/* A link, because clicking the cover or the words should open what is playing,
   not only the title. */
.now-playing {
  display: flex; align-items: center; gap: var(--s3); min-width: 0;
  color: inherit; text-decoration: none; border-radius: var(--r-md);
  padding: var(--s1); margin: calc(var(--s1) * -1);
  transition: background var(--fast);
}
.now-playing:hover { background: var(--hover); }
.now-playing:hover .t { text-decoration: underline; text-underline-offset: 2px; }
.now-playing:focus-visible { outline: 2px solid var(--accent-line); outline-offset: 2px; }
.now-playing .cover { width: 52px; height: 52px; border-radius: var(--r-md); }
.now-playing .t { font-weight: 700; font-size: 13.5px; }
.now-playing .s { font-size: 12px; color: var(--text-3); }

.transport { display: flex; flex-direction: column; align-items: center; gap: 6px; min-width: 0; }
.transport-row { display: flex; align-items: center; gap: var(--s2); }

.scrubber { display: flex; align-items: center; gap: var(--s3); width: 100%; }
.scrubber .t { font-size: 11.5px; color: var(--text-3); font-variant-numeric: tabular-nums; width: 38px; flex: none; }
.scrubber .t.right { text-align: right; }
/* Taller than it looks. The line is 4px because that is what suits the bar; the area
   you can grab is 18px, because a 4px target is a game rather than a control. */
.bar {
  position: relative; flex: 1; height: 18px;
  display: flex; align-items: center;
  cursor: pointer;
  touch-action: none;
}
.bar .track-line {
  position: absolute; inset: 7px 0; height: 4px; border-radius: var(--r-pill);
  background: var(--grad-sunken);
  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.6);
}
.bar .fill {
  position: absolute; left: 0; top: 7px; height: 4px; border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--accent-lo), var(--accent));
}
.bar .knob {
  position: absolute; top: 3px; width: 12px; height: 12px; margin-left: -6px;
  border-radius: 50%; background: var(--text);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.6);
  opacity: 0; transform: scale(0.6);
  transition: opacity var(--fast), transform var(--fast);
}
.bar:hover .fill, .bar.scrubbing .fill {
  background: linear-gradient(90deg, var(--accent), var(--accent-hi));
}
.bar:hover .knob, .bar.scrubbing .knob { opacity: 1; transform: none; }
.bar .buffered {
  position: absolute; left: 0; top: 7px; height: 4px; border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.07);
}

.player-right { display: flex; align-items: center; justify-content: flex-end; gap: var(--s2); }
.volume { display: flex; align-items: center; gap: var(--s2); width: 108px; }

/* The A and B chips live in the player, because comparing versions is a thing you do
   while listening rather than a thing you go to a page for. */
.ab-chips { display: flex; gap: 4px; align-items: center; }
.ab-chip {
  min-width: 34px; height: 26px; padding: 0 9px;
  border-radius: var(--r-pill);
  border: 1px solid var(--line-2);
  background: transparent;
  color: var(--text-2);
  font-size: 11.5px; font-weight: 700;
  cursor: pointer;
  transition: background var(--fast), color var(--fast), border-color var(--fast);
}
.ab-chip:hover { border-color: var(--accent-line); color: var(--text); }
.ab-chip.on {
  background: var(--grad-accent);
  border-color: transparent;
  color: var(--text-on-accent);
  box-shadow: var(--edge-top), 0 2px 8px rgba(84, 179, 122, 0.25);
}
.ab-chip[disabled] { opacity: 0.4; cursor: default; }

/* ── expanded ─────────────────────────────────────────────────────────────── */
.stage {
  position: fixed; inset: var(--s2) var(--s2) calc(var(--player-h) + var(--s3)) var(--s2);
  z-index: 50;
  background: var(--surface);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-3);
  display: flex; flex-direction: column;
  overflow: hidden;
  animation: stage-in 240ms var(--ease);
}
@keyframes stage-in { from { opacity: 0; transform: translateY(18px); } }
.stage-head {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s4) var(--s5);
  border-bottom: 1px solid var(--line);
}
.stage-body { flex: 1; min-height: 0; overflow-y: auto; padding: var(--s5); }
.stage-grid {
  display: grid; grid-template-columns: minmax(220px, 320px) 1fr; gap: var(--s6);
  align-items: start;
}
.stage-grid .cover { width: 100%; aspect-ratio: 1; border-radius: var(--r-lg); box-shadow: var(--shadow-2); }
@media (max-width: 820px) {
  .stage-grid { grid-template-columns: 1fr; }
  .stage-grid .cover { max-width: 220px; }
}

@media (max-width: 760px) {
  .player { grid-template-columns: 1fr auto; grid-template-rows: auto auto; gap: var(--s2) var(--s3); height: auto; padding: var(--s3) var(--s4); }
  .player .transport { grid-column: 1 / -1; order: 3; }
  .player-right .volume { display: none; }
  :root { --player-h: 116px; }
}


/* ── the one honest wait: reading a whole render for the compositor ───────── */

.preparing {
  display: flex; align-items: center; gap: var(--s3);
  width: 100%;
  font-size: 12px;
  color: var(--text-3);
}
.preparing-bar {
  flex: 1;
  height: 4px;
  border-radius: var(--r-pill);
  background: var(--elevated);
  overflow: hidden;
}
.preparing-fill {
  display: block; height: 100%; width: 0;
  border-radius: var(--r-pill);
  background: var(--accent);
  transition: width 120ms linear;
}
.preparing-pct { font-variant-numeric: tabular-nums; min-width: 34px; text-align: right; }


/* The player arriving, when something starts for the first time. It slides up from
   under the edge of the window rather than appearing beneath the content. */
.player { animation: player-in var(--med) var(--ease) both; }
@keyframes player-in { from { opacity: 0; transform: translate3d(0, 14px, 0); } }
@media (prefers-reduced-motion: reduce) { .player { animation: none; } }


/* 70-eq.css */
/* The equaliser and the limiter.
 *
 * Both are displays you work on rather than banks of sliders. The equaliser is a curve
 * you place points on; the limiter is a meter with the two deciding lines drawn on it.
 * Everything that can be dragged with a mouse can be dragged with a thumb, which is why
 * Q lives on a knob rather than only on the scroll wheel.
 */

/* ── presets ──────────────────────────────────────────────────────────────── */
/* A row where one is chosen. This lived in the lyrics stylesheet until that file was
   rewritten around it, at which point the preset names became unstyled default buttons:
   white squares on a dark panel. It belongs next to the thing that uses it. */
.sheet-tabs { display: flex; align-items: center; gap: var(--s1); flex-wrap: wrap; }
.sheet-tab {
  background: none;
  border: 0;
  color: var(--text-3);
  font-size: 13px; font-weight: 600;
  padding: 4px var(--s3);
  border-radius: var(--r-pill);
  cursor: pointer;
  white-space: nowrap;
  transition: color var(--fast), background var(--fast);
}
.sheet-tab:hover { color: var(--text); background: var(--hover); }
.sheet-tab.on { color: var(--accent-hi); background: var(--accent-soft); }

.preset-row { display: flex; align-items: center; gap: 2px; flex-wrap: wrap; }
.preset-tools {
  display: inline-flex; align-items: center; gap: 1px;
  margin-left: var(--s2); padding-left: var(--s2);
  border-left: 1px solid var(--line);
}
.icon-btn.tiny { width: 28px; height: 28px; }

/* ── the curve ────────────────────────────────────────────────────────────── */
.eq-wrap {
  position: relative;
  border-radius: var(--r-lg);
  background: var(--bg-deep);
  border: 1px solid var(--line);
  overflow: hidden;
  user-select: none;
  touch-action: none;
}
.eq-canvas {
  display: block; width: 100%; height: 300px; cursor: crosshair;
  /* touch-action does not inherit, so the canvas needs it itself. */
  touch-action: none;
}
.eq-canvas.dragging { cursor: grabbing; }

.eq-readout {
  position: absolute; top: var(--s3); left: var(--s4);
  display: flex; gap: var(--s4);
  font-size: 11.5px; color: var(--text-3);
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}
.eq-readout b { color: var(--text); font-weight: 600; }

.eq-hint {
  position: absolute; right: var(--s4); bottom: var(--s3);
  font-size: 11px; color: var(--text-3);
  pointer-events: none;
}

.eq-toolbar {
  display: flex; align-items: center; gap: var(--s2);
  flex-wrap: wrap;
  margin: var(--s4) 0;
}

/* ── one card per band, with a knob for Q ─────────────────────────────────── */
.band-list {
  display: flex; gap: var(--s2);
  overflow-x: auto;
  padding-bottom: var(--s2);
  scroll-snap-type: x proximity;
}
.band-card {
  flex: none; width: 156px;
  background: var(--surface-2);
  border: 1px solid transparent;
  border-radius: var(--r-md);
  padding: var(--s3);
  cursor: pointer;
  scroll-snap-align: start;
  transition: border-color var(--fast), background var(--fast);
}
.band-card:hover { background: var(--elevated); }
.band-card.on { border-color: var(--accent); }
.band-card.off { opacity: 0.45; }
.band-card .head { display: flex; align-items: center; justify-content: space-between; gap: var(--s2); }
.band-card .kind {
  font-size: 10.5px; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--text-3);
}
.band-card .freq {
  font-family: var(--display); font-weight: 800; font-size: 19px;
  letter-spacing: -0.02em; margin-top: 2px;
}
.band-card .freq .unit { font-family: var(--font); font-size: 11px; color: var(--text-3); margin-left: 3px; }
.band-bottom { display: flex; align-items: center; justify-content: space-between; gap: var(--s2); margin-top: 2px; }
.band-card .gain { font-size: 12px; color: var(--text-2); font-variant-numeric: tabular-nums; }
.band-card select {
  width: 100%; margin-top: var(--s2);
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: 4px 6px; font-size: 11.5px; color: var(--text-2); outline: none;
}
.band-card .band-remove { width: 100%; margin-top: 6px; height: 26px; }

/* Q, as a dial you turn. The wheel still works, but a thumb has no wheel. */
.q-knob {
  position: relative;
  display: grid; place-items: center;
  width: 34px; height: 34px;
  cursor: ns-resize;
  touch-action: none;
  flex: none;
}
.q-knob svg { display: block; }
.q-knob .q-value {
  position: absolute;
  font-size: 9px; font-weight: 700; color: var(--text-2);
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}
.q-knob.turning .q-value, .q-knob:hover .q-value { color: var(--text); }
.q-knob:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 50%; }

.switch.tiny { width: 30px; height: 17px; }
.switch.tiny::after { width: 11px; height: 11px; }
.switch.tiny.on::after { transform: translateX(13px); }

/* ── the limiter ──────────────────────────────────────────────────────────── */
.limiter-head {
  display: flex; align-items: center; gap: var(--s3);
  margin: var(--s7) 0 var(--s3);
}
.limiter-head h3 {
  font-family: var(--display); font-size: 13px; font-weight: 700;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-3);
}
.limiter-head .grow { flex: 1; }
.limiter-nums {
  display: flex; gap: var(--s4);
  font-size: 11.5px; color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.limiter-nums b { color: var(--text-2); font-weight: 600; }

.limiter-wrap {
  position: relative;
  border-radius: var(--r-lg);
  background: var(--bg-deep);
  border: 1px solid var(--line);
  overflow: hidden;
  user-select: none;
  touch-action: none;
}
.limiter-canvas {
  display: block; width: 100%; height: 180px;
  touch-action: none;       /* the same: the finger lands here, not on the wrapper */
}
.limiter-hint {
  position: absolute; right: var(--s4); bottom: var(--s3);
  font-size: 11px; color: var(--text-3); pointer-events: none;
}

.limiter-knobs {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--s4) var(--s5);
  margin-top: var(--s4);
}
.knob-row { display: flex; flex-direction: column; gap: 4px; }
.knob-row .lab { display: flex; justify-content: space-between; align-items: baseline; }
.knob-row .lab span:first-child { font-size: 12.5px; color: var(--text-2); }
.knob-row .lab b { font-size: 12.5px; font-variant-numeric: tabular-nums; font-weight: 600; }

.sound-note { margin-top: var(--s4); font-size: 12.5px; color: var(--text-3); }

/* ── phone ────────────────────────────────────────────────────────────────── */
@media (max-width: 720px) {
  .eq-canvas { height: 210px; }
  .limiter-canvas { height: 150px; }
  .eq-hint, .limiter-hint { display: none; }
  .eq-toolbar { gap: 6px; }
  .eq-toolbar .btn { flex: 1 1 auto; }
  /* Cards get wider and snap, so one thumb swipe lands on one band. */
  .band-card { width: 60vw; max-width: 220px; }
  .q-knob { width: 42px; height: 42px; }
  .q-knob svg { width: 42px; height: 42px; }
}

/* Folded sections.
 *
 * The whole head is the control, because a chevron on its own is a five pixel target and
 * the words next to it are the thing the eye goes to anyway. The limiter's head already
 * carries its on/off switch, so there the fold is only the chevron and the word, and the
 * switch stays a separate press: turning a limiter on and looking at one are different
 * intentions and must not share a target. */
.fold-head {
  display: flex; align-items: center; gap: var(--s2);
  width: 100%; padding: var(--s3) 0; margin: 0;
  background: none; border: 0; color: var(--text);
  font: inherit; text-align: left; cursor: pointer;
}
.fold-head.bare { width: auto; padding-right: var(--s2); }
.fold-head h3 { margin: 0; font-size: 13.5px; font-weight: 700; letter-spacing: 0.01em; }
.fold-head:hover h3 { color: var(--accent-hi); }
.fold-head:focus-visible { outline: 2px solid var(--accent-line); outline-offset: 2px; border-radius: var(--r-sm); }

.fold-mark { flex: none; color: var(--text-3); transition: transform var(--fast), color var(--fast); }
.fold-mark.open { transform: rotate(90deg); color: var(--accent); }
.fold-head:hover .fold-mark { color: var(--accent-hi); }

/* What is inside, without opening it. */
.fold-sub { margin-left: auto; font-size: 12px; color: var(--text-3); }


/* 76-compositor.css */
/* ── the compositor ───────────────────────────────────────────────────────── */

/* The drawer. It is closed by default and takes no room at all when closed, which is
   what makes it something you open when you need it rather than a permanent panel. */
.comp-drawer {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  transition: grid-template-rows var(--med), opacity var(--fast);
}
.comp-drawer > .comp-inner {
  overflow: hidden;
  min-height: 0;
  /* Without this the track's width, which is the whole song in pixels, pushes every
     ancestor wide and the panel stops scrolling inside itself: a grid or flex item
     refuses to shrink below its content unless it is told it may. */
  min-width: 0;
}
.comp-drawer.open {
  grid-template-rows: 1fr;
  opacity: 1;
}
.comp-drawer.open > .comp-inner {
  /* Only once open, so the shadow of a clip is not clipped while it is in use. */
  overflow: visible;
}
@media (prefers-reduced-motion: reduce) {
  .comp-drawer { transition: none; }
}

.comp-body {
  min-width: 0;
  max-width: 100%;
  margin-top: var(--s4);
  padding: var(--s4);
  border-radius: var(--r-lg);
  background: var(--grad-surface);
  border: 1px solid var(--line);
  box-shadow: var(--edge-top), var(--shadow-2);
}

.comp-head {
  display: flex; align-items: center; gap: var(--s2);
  flex-wrap: wrap;
  margin-bottom: var(--s3);
}
.comp-tempo { display: flex; align-items: center; gap: var(--s2); }
.comp-bpm {
  display: flex; align-items: baseline; gap: 5px;
  padding: 0 var(--s2);
}
.comp-bpm .field {
  width: 76px;
  padding: 6px var(--s2);
  font-variant-numeric: tabular-nums;
  font-weight: 650;
  text-align: right;
}
.comp-bpm span { font-size: 12px; color: var(--text-3); }
.comp-zoom { display: flex; gap: 2px; }
.icon-btn.sm { width: 28px; height: 28px; font-size: 15px; }

/* ── the track ────────────────────────────────────────────────────────────── */

.comp-scroll {
  width: 100%;
  min-width: 0;
  max-width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  border-radius: var(--r-md);
  background: var(--grad-sunken);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.6);
  outline: none;
  /* Held so the clips do not fight the page for a horizontal drag. */
  overscroll-behavior-x: contain;
}
.comp-scroll:focus-visible { box-shadow: inset 0 0 0 2px var(--accent-line); }

.comp-track {
  position: relative;
  height: 104px;
  min-width: 100%;
}
.comp-bars {
  position: absolute; inset: 0;
  background-repeat: repeat-x;
  opacity: 0.5;
  pointer-events: none;
}
.comp-clips {
  position: relative;
  display: flex;
  height: 100%;
  align-items: stretch;
  padding: var(--s2) 0;
  gap: 2px;
}

.comp-clip {
  position: relative;
  width: var(--w);
  min-width: 14px;
  flex: none;
  border-radius: var(--r-sm);
  /* Lit from above like everything else, and darker than before so the waveform drawn
     on top of it is the thing you read rather than the block underneath. */
  background: linear-gradient(180deg,
    hsl(var(--hue) 44% 24%) 0%, hsl(var(--hue) 40% 15%) 100%);
  border: 1px solid hsl(var(--hue) 38% 30%);
  box-shadow: var(--edge-top);
  overflow: hidden;
  cursor: grab;
  user-select: none;
  transition: filter var(--fast), box-shadow var(--fast);
}
.comp-clip:hover { filter: brightness(1.14); }
.comp-clip.on { box-shadow: 0 0 0 2px var(--accent), var(--edge-top); }
.comp-clip.holding { cursor: grabbing; filter: brightness(1.2); z-index: 2; }
/* The one being heard right now. */
.comp-clip.live {
  box-shadow: inset 0 2px 0 var(--accent-hi), 0 0 20px rgba(84, 179, 122, 0.2);
}

.comp-wave {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  pointer-events: none;
  opacity: 0.75;
}
.comp-name {
  position: absolute; left: 7px; top: 5px; right: 7px;
  font-size: 11.5px; font-weight: 650;
  color: var(--text-on-color);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
  pointer-events: none;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.comp-beats {
  position: absolute; right: 7px; bottom: 4px;
  font-size: 10.5px;
  font-variant-numeric: tabular-nums;
  color: rgba(255, 255, 255, 0.7);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
  pointer-events: none;
}

/* Wide enough to hit with a mouse, and wider still on a touch screen. */
.comp-grip {
  position: absolute; top: 0; bottom: 0;
  width: 9px;
  cursor: ew-resize;
  z-index: 3;
}
.comp-grip.start { left: 0; }
.comp-grip.end { right: 0; }
.comp-grip::after {
  content: ""; position: absolute; top: 30%; bottom: 30%;
  width: 2px; border-radius: 2px;
  background: rgba(255, 255, 255, 0.55);
  left: 50%; transform: translateX(-50%);
  opacity: 0; transition: opacity var(--fast);
}
.comp-clip:hover .comp-grip::after { opacity: 1; }

.comp-playhead {
  position: absolute; top: 0; bottom: 0; width: 2px;
  background: var(--accent-hi);
  box-shadow: 0 0 8px var(--accent);
  pointer-events: none;
  z-index: 4;
}

.comp-foot {
  display: flex; align-items: center; gap: var(--s3);
  margin-top: var(--s3);
  font-size: 12px;
}
.comp-hint { opacity: 0.7; }
.comp-empty { padding: var(--s6) var(--s4); }

/* ── the button that opens it ─────────────────────────────────────────────── */

.comp-onoff {
  display: inline-flex; align-items: center; gap: var(--s2);
  margin-left: var(--s2);
  font-size: 12.5px; font-weight: 600;
  color: var(--text-2);
  cursor: pointer;
}

.comp-toggle.on {
  background: var(--accent-soft);
  color: var(--accent-hi);
}

/* Says out loud that A and B mean something different while this is on. */
.ab-note {
  font-size: 10.5px;
  color: var(--text-3);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* ── the read only picture, in the renders list ───────────────────────────── */

.render-shape {
  display: flex;
  gap: 1px;
  /* Its own line under the row. As a flex sibling of the buttons it was squeezed to
     nothing, which drew ten segments none of which had any width. */
  flex: 0 0 100%;
  width: 100%;
  height: 22px;
  margin-top: 6px;
  border-radius: var(--r-sm);
  overflow: hidden;
  /* Nothing here is clickable. It is there to be recognised, not used. */
  pointer-events: none;
  opacity: 0.75;
}
.render-shape .seg {
  flex: var(--grow) 1 0;
  background: linear-gradient(180deg,
    hsl(var(--hue) 42% 26%) 0%, hsl(var(--hue) 38% 16%) 100%);
}
.render-shape canvas { width: 100%; height: 100%; display: block; }

/* ── lyrics that know which section they belong to ────────────────────────── */

.card-part {
  display: inline-flex; align-items: center; gap: 6px;
  height: 26px; padding: 0 10px;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--text-3);
  font-size: 12px; font-weight: 600;
  cursor: pointer;
  transition: background var(--fast), color var(--fast), border-color var(--fast);
}
.card-part:hover { background: var(--hover); color: var(--text); }
.card-part .dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: hsl(var(--hue, 140) 45% 52%);
}
.card-part.set { color: var(--text-2); border-color: hsl(var(--hue) 40% 40%); }

/* The card lights up when its section is the one playing. */
.lyric-card.sounding {
  border-color: var(--accent);
  /* --edge-top first: this rule replaces box-shadow outright and loads after the card's
     own, so without it the pane loses its lit top edge exactly while it is playing. */
  box-shadow: var(--edge-top), 0 0 0 1px var(--accent-line),
              0 12px 40px rgba(84, 179, 122, 0.14);
}

@media (max-width: 900px) {
  /* The spacer would otherwise hold the zoom and detect buttons out on a line of their
     own miles to the right. On a phone the header is simply two rows. */
  .comp-head > .grow { flex-basis: 100%; height: 0; }
  .comp-track { height: 92px; }
  .comp-grip { width: 16px; }
  .comp-hint { display: none; }
  .comp-body { padding: var(--s3); }
}


/* The drawer opening. The clips come in after the panel has finished making room, so
   the two movements read as one thing arriving rather than two things competing. */
.comp-drawer.open .comp-clip {
  animation: clip-in 240ms var(--ease) both;
  animation-delay: 90ms;
}
@keyframes clip-in {
  from { opacity: 0; transform: translate3d(0, 8px, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .comp-drawer.open .comp-clip { animation: none; }
}

/* The row of things that act on one card, sitting on the card they act on. */
.card-foot-tools {
  display: flex; align-items: center; gap: var(--s2);
  margin-top: var(--s4);
}
/* Quiet until wanted. Deleting a set of words is not something to be invited into. */
.card-drop {
  width: 30px; height: 30px;
  color: var(--text-3);
  opacity: 0;
  transition: opacity var(--fast), color var(--fast), background var(--fast);
}
.lyric-card:hover .card-drop,
.lyric-card:focus-within .card-drop { opacity: 1; }
.card-drop:hover { color: var(--bad-hi); background: rgba(212, 104, 94, 0.12); }

/* A pointer is not always there to hover with. */
@media (hover: none) {
  .card-drop { opacity: 1; }
}

/* The way into the compositor. Named rather than left as a glyph nobody can be expected
   to recognise, and it says which of its two states it is in. */
.comp-toggle { display: inline-flex; align-items: center; gap: 6px; }
.comp-toggle.on {
  background: var(--accent-soft);
  color: var(--accent-hi);
  box-shadow: inset 0 0 0 1px var(--accent-line);
}
.comp-toggle.on:hover { background: rgba(84, 179, 122, 0.2); }

/* On a phone the row already holds A, Compare and this; the word is the first thing
   that can go. */
@media (max-width: 560px) {
  .comp-toggle-label { display: none; }
  .comp-toggle { padding: 0 var(--s3); }
}


/* 78-youtube.css */
/* The upload page.
 *
 * Three cards down the page in the order the decisions happen: what the file will be,
 * what YouTube is told about it, and which account it goes to. Not a wizard, because
 * every one of these can be revisited before anything leaves, and a wizard would hide
 * the render settings behind a Next the moment you wanted to change them.
 */

.yt-title {
  font-family: var(--display);
  font-size: clamp(26px, 4vw, 38px);
  letter-spacing: -0.02em;
  margin: 0 0 var(--s5);
}

.yt-form { display: grid; gap: var(--s5); max-width: 720px; }

.yt-card {
  padding: var(--s5);
  background: var(--grad-surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  display: grid;
  gap: var(--s4);
}
.yt-card h2 {
  margin: 0;
  font-size: 14px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
  font-weight: 700;
}
.yt-card > p { margin: 0; font-size: 13.5px; }

/* A checkbox that is a whole row, so it can be hit with a thumb. */
.yt-check {
  display: flex; align-items: center; gap: var(--s3);
  font-size: 14px; cursor: pointer;
}
.yt-check input { width: 18px; height: 18px; accent-color: var(--accent); flex: none; }
.yt-check input:disabled { opacity: 0.4; }
.yt-check input:disabled + span { color: var(--text-3); }

/* What is about to be rendered, in one line, before anything is. */
.yt-summary {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2);
  padding: var(--s3);
  background: var(--grad-sunken);
  border-radius: var(--r-md);
  font-size: 12.5px; color: var(--text-2);
}
.yt-summary .dot {
  width: 3px; height: 3px; border-radius: 50%;
  background: currentColor; opacity: 0.5; flex: none;
}

.yt-progress { display: flex; align-items: center; gap: var(--s3); }
.yt-status { font-size: 12.5px; color: var(--text-2); min-width: 12ch; }
.yt-bar {
  flex: 1; height: 4px; border-radius: 999px;
  background: var(--grad-sunken); overflow: hidden;
}
.yt-bar span {
  display: block; height: 100%; width: 0;
  background: var(--accent);
  transition: width var(--med);
}

/* The result, and whether it survived the trip. */
.yt-made {
  display: grid; gap: var(--s3);
  padding: var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--grad-sunken);
}
.yt-made.hot { border-color: var(--bad); }
.yt-made-head { display: flex; align-items: center; gap: var(--s2); font-size: 13.5px; }
.yt-peak { font-size: 12px; color: var(--text-3); font-variant-numeric: tabular-nums; }
.yt-made.hot .yt-peak { color: var(--bad-hi); }
.yt-warn { margin: 0; font-size: 13px; color: var(--bad-hi); max-width: 58ch; }
.yt-audition { width: 100%; height: 34px; }

.yt-desc { min-height: 92px; resize: vertical; font-family: inherit; }

.yt-still { display: flex; align-items: center; gap: var(--s3); }
.yt-still img {
  width: 72px; height: 72px; object-fit: cover;
  border-radius: var(--r-md); display: block;
}

/* Connecting an account: three real steps, not a button that fails. */
.yt-steps { margin: 0; padding-left: var(--s5); font-size: 13.5px; display: grid; gap: var(--s2); }
.yt-steps b { color: var(--text); }
.yt-note { margin: 0; font-size: 12.5px; max-width: 60ch; }

.yt-code { display: grid; gap: var(--s3); }
.yt-usercode {
  margin: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 26px; letter-spacing: 0.18em; text-align: center;
  padding: var(--s4);
  background: var(--grad-sunken);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-md);
  color: var(--accent-hi);
  user-select: all;
}

/* A button that reads as a sentence, for an aside inside a paragraph. */
.linkish {
  background: none; border: 0; padding: 0;
  font: inherit; color: var(--accent-hi);
  text-decoration: underline; cursor: pointer;
}

/* A card head that carries a control as well as a title. */
.yt-card-head { display: flex; align-items: center; gap: var(--s3); }
.yt-card-head h2 { margin: 0; }

/* The compositor, mounted open rather than hanging off a button. */
.yt-comp { border: 1px solid var(--line); border-radius: var(--r-md); overflow: hidden; }

/* Visibility as three pills rather than a dropdown.
 *
 * A dropdown shows the chosen one and hides what the other two would mean. These are the
 * three states a video can be in for as long as it exists, so all three say what they do
 * and the chosen one is obvious without opening anything. */
.yt-privacy { display: grid; gap: var(--s2); }
.yt-privacy-label {
  font-size: 12px; color: var(--text-2);
  letter-spacing: 0.02em;
}
.yt-pills { display: flex; flex-wrap: wrap; gap: var(--s2); }
.yt-pill {
  flex: 1 1 140px;
  display: grid; gap: 1px;
  padding: var(--s3) var(--s4);
  text-align: left;
  background: var(--grad-sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  color: var(--text-2);
  font: inherit; cursor: pointer;
  transition: border-color var(--fast), background var(--fast), color var(--fast);
}
.yt-pill .t { font-weight: 700; font-size: 13.5px; }
.yt-pill .s { font-size: 11.5px; color: var(--text-3); }
.yt-pill:hover { border-color: var(--line-2); color: var(--text); }
.yt-pill.on {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--text);
}
.yt-pill.on .t { color: var(--accent-hi); }
.yt-pill:focus-visible { outline: 2px solid var(--accent-hi); outline-offset: 2px; }

/* The accounts, as a list you choose from rather than one slot you overwrite. */
.yt-accounts { display: grid; gap: var(--s2); }
.yt-account {
  display: flex; align-items: center; gap: var(--s3);
  width: 100%; padding: var(--s3);
  background: var(--grad-sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  color: var(--text); font: inherit; text-align: left; cursor: pointer;
  transition: border-color var(--fast), background var(--fast);
}
.yt-account:hover { border-color: var(--line-2); }
.yt-account.on { border-color: var(--accent); background: var(--accent-soft); }
.yt-account:focus-visible { outline: 2px solid var(--accent-hi); outline-offset: 2px; }
.yt-account .t { display: block; font-weight: 650; font-size: 13.5px; }
.yt-account .s { display: block; font-size: 11.5px; color: var(--text-3); }

.yt-avatar {
  flex: none; width: 34px; height: 34px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--elevated); color: var(--text-2);
  font-weight: 700; font-size: 14px;
}
.yt-account.on .yt-avatar { background: var(--accent); color: var(--text-on-accent); }

.yt-chosen, .yt-pick {
  flex: none; font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase;
  padding: 3px 9px; border-radius: var(--r-pill);
}
.yt-chosen { background: var(--accent); color: var(--text-on-accent); font-weight: 700; }
.yt-pick { border: 1px solid var(--line-2); color: var(--text-3); }
.yt-account:hover .yt-pick { border-color: var(--accent-line); color: var(--accent-hi); }

/* The card that says what is not built.
 *
 * A page carrying a title field, a description, three visibility choices and a connected
 * account reads as a page that can publish. It cannot yet, and leaving somebody to
 * discover that by looking for a button which is not there is worse than saying so. */
.yt-last { border-color: var(--line-2); }
.yt-last p { margin: 0; font-size: 13.5px; max-width: 60ch; }
.yt-last b { color: var(--text); }
.yt-last .btn[disabled] { opacity: 0.45; cursor: not-allowed; }

/* On a narrow screen the head and the control beside it stop competing for one line.
   "The sound that goes out" was breaking across three lines to leave room for the preset
   tabs; below 560px they stack and each gets the full width. */
@media (max-width: 560px) {
  .yt-card-head { flex-wrap: wrap; }
  .yt-card-head h2 { flex: 1 1 100%; }
  .yt-card-head .grow { display: none; }
  .yt-card-head .preset-row { flex: 1 1 100%; }
  .yt-card { padding: var(--s4); }
}

/* When there is no arrangement to show. */
.yt-nocomp { margin: 0; padding: var(--s4); font-size: 13px; max-width: 60ch; }


/* 80-sheet.css */
/* Dialogs. One shape, centred, and it closes on Escape or a click outside. */

.sheet-backdrop {
  position: fixed; inset: 0; z-index: 60;
  /* Darker and blurred, because the ground behind is now nearly black: a flat 60% veil
     over it barely separated the dialog from the page at all. */
  background: rgba(0, 0, 0, 0.55);
  display: grid; place-items: center;
  padding: var(--s4);
  animation: fade-in 160ms var(--ease);
}
@keyframes fade-in { from { opacity: 0; } }

.sheet {
  width: min(520px, 100%);
  max-height: 84dvh;
  overflow-y: auto;
  background: var(--glass-2);
  -webkit-backdrop-filter: var(--glass-filter);
  backdrop-filter: var(--glass-filter);
  border: 1px solid var(--glass-line);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-3), var(--edge-top);
  isolation: isolate;
  padding: var(--s6);
  animation: sheet-in 240ms var(--ease);
}
.sheet.wide { width: min(760px, 100%); }
/* It comes up from the page rather than appearing on top of it. */
@keyframes sheet-in { from { opacity: 0; transform: translateY(18px) scale(0.975); } }

.sheet h2 { margin-bottom: var(--s2); }
.sheet .sub { color: var(--text-2); margin-bottom: var(--s5); }
.sheet-fields { display: flex; flex-direction: column; gap: var(--s4); }
.sheet-label { display: flex; flex-direction: column; gap: 6px; font-size: 12.5px; color: var(--text-2); font-weight: 600; }
.sheet-foot { display: flex; justify-content: flex-end; gap: var(--s2); margin-top: var(--s6); }

.candidate {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3);
  border-radius: var(--r-md);
  background: var(--grad-surface);
  margin-bottom: var(--s2);
}
.candidate .grow { min-width: 0; }
.candidate .name { font-weight: 600; font-size: 13.5px; }
.candidate .path { font-size: 11.5px; color: var(--text-3); }

/* ── what the keyboard does ───────────────────────────────────────────────── */

.key-row {
  display: flex; align-items: center; gap: var(--s4);
  padding: var(--s2) 0;
  color: var(--text-2);
  font-size: 13.5px;
}
.key-row + .key-row { border-top: 1px solid var(--line); }
.keys { display: flex; gap: 4px; flex: none; min-width: 132px; }

kbd {
  display: inline-grid; place-items: center;
  min-width: 26px; height: 26px; padding: 0 7px;
  border-radius: var(--r-sm);
  background: var(--grad-raised);
  box-shadow: var(--edge-top), 0 1px 0 rgba(0, 0, 0, 0.6);
  border: 1px solid var(--line-2);
  color: var(--text);
  font: inherit; font-size: 12px; font-weight: 650;
}

/* The quiet mention in the rail, so the shortcuts can be found by someone who does not
   already know they exist. */
.rail-keys {
  width: 22px; height: 22px;
  border: 0; border-radius: 50%;
  background: transparent;
  color: var(--text-3);
  font-size: 12px; font-weight: 700;
  cursor: pointer;
  transition: background var(--fast), color var(--fast);
}
.rail-keys:hover { background: var(--hover); color: var(--text); }

/* ── what changed ─────────────────────────────────────────────────────────── */

/* A release, in the popup and again in Settings. One rule for both: a second copy of
   these is where the two stop looking like the same thing. */
.log-entry + .log-entry {
  margin-top: var(--s5);
  padding-top: var(--s5);
  border-top: 1px solid var(--line);
}
.log-head { display: flex; align-items: baseline; gap: var(--s3); }
.log-version {
  font-family: var(--display); font-weight: 700; font-size: 13px;
  color: var(--accent-hi);
  font-variant-numeric: tabular-nums;
}
.log-title { font-weight: 600; color: var(--text); }
.log-date { margin-left: auto; flex: none; font-size: 11.5px; color: var(--text-3); }
/* The name the project had at the time. It was called J-ong for the whole of the 1.x
   line, and a history that renames its own past is one you cannot use to work out what
   you were running. */
.log-name { margin-top: 2px; font-size: 11px; color: var(--text-3); letter-spacing: 0.04em; }
.log-notes {
  margin: var(--s2) 0 0; padding-left: var(--s4);
  color: var(--text-2); font-size: 13.5px;
}
.log-notes li { margin-top: var(--s1); }
.log-more { margin-top: var(--s4); }
.log-earlier { margin-top: var(--s5); }
