/* MuTube v2 — light / e-ink-friendly theme.
 *
 * Design principles (chosen so this reads cleanly on a Boox / Kindle
 * web browser as well as a regular display):
 *
 *   * Near-pure white surfaces. Pure #fff feels harsh on regular
 *     displays so we use #fafaf7 for the page bg with #ffffff for
 *     raised panels — barely-there contrast for sighted users, but
 *     enough for an e-ink panel border to register at all.
 *
 *   * Near-black text (#111). Pure #000 isn't worth the contrast
 *     headache on LCD; e-ink renders both identically.
 *
 *   * NO color-as-meaning. State (selected, pressed, owner, error)
 *     uses borders / weights / outlined badges, not coloured fills.
 *     One single accent (#2b2b2b for primary buttons) and one error
 *     tone (#7a1a1a, dark enough to still read on e-ink greyscale).
 *
 *   * Borders 1.5px and 'solid' — e-ink loses sub-pixel hairlines.
 *
 *   * No gradients. No transitions. No box-shadows (a single 1px
 *     hard border is the entire "elevation" vocabulary).
 *
 *   * Generous spacing, 16px base type. Long-form readability
 *     matters more than information density on a 6"–10" panel.
 *
 *   * Sharp corners — `border-radius` capped at 2px so panel edges
 *     don't dither into the bg on a low-DPI greyscale render.
 *
 *   * Print stylesheet at the bottom: pure black on white, no
 *     borders on form controls, hide nav/buttons. Makes paper-out
 *     pairing instructions trivial.
 */

:root {
  /* Surfaces */
  --bg:          #fafaf7;
  --surface:     #ffffff;
  --surface-2:  #f3f3ef;

  /* Ink */
  --ink:         #111111;
  --ink-soft:   #333333;
  --muted:       #555555;
  --faint:       #888888;

  /* Lines */
  --border:      #d4d4d0;
  --border-strong: #888888;

  /* States — used only on borders / outlined badges, never as fills. */
  --error-ink:   #7a1a1a;
  --warn-ink:    #5a4a00;

  /* Single accent. Buttons use it as a 1.5px border + bold weight;
   * the only "filled" use is `.btn-primary` (dark ink on light bg). */
  --accent-ink: #2b2b2b;

  /* Brand colour — matches the `μ` glyph in `mu_logo.svg`. Used
   * sparingly: the "In library" badge picks it up so the most-
   * visited card state has a soft visual tie to the wordmark.
   * Kept as a 1.5px border + bold mono text rather than a fill
   * to stay within the "no colour-as-meaning, outlined only"
   * rule. On e-ink readers it falls to a mid-grey, which is fine
   * — the badge still reads as "different from the others"
   * because of the heavier border weight inherited from `.badge`.
   *
   * `--brand-ink` is a darker shade for text-on-white at AA
   * contrast (the raw baby blue is too light to set type in). */
  --brand:      #89cff0;
  --brand-ink:  #2a6f96;

  /* Typography */
  --font-sans:   ui-sans-serif, system-ui, -apple-system, "Segoe UI",
                 sans-serif;
  --font-mono:   ui-monospace, SFMono-Regular, "JetBrains Mono", Menlo,
                 monospace;

  color-scheme: light;
}

/* ── Reset / base ────────────────────────────────────────────────── */

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

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  /* Prevent any descendant (long invite codes, overflowing pre
   * blocks, fixed-width inputs) from pushing the body wider than
   * the viewport on mobile. `overflow-x: clip` is the modern
   * "hard stop" — it doesn't create a containing block for
   * position:fixed children like `hidden` would, so no surprise
   * stacking-context interactions. Pair with the per-element
   * `min-width: 0` rules below so flex children actually shrink
   * instead of forcing the parent wider. */
  overflow-x: clip;
  /* iOS Safari: stop pull-to-refresh from bouncing the whole
   * shell when the user is just trying to scroll a long list.
   * `contain` only blocks pull-to-refresh + nav-gestures at the
   * edges; in-page scroll-chaining still works. */
  overscroll-behavior-y: contain;
}

a {
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 0.15em;
  text-decoration-thickness: 1px;
}
a:hover { text-decoration-thickness: 2px; }

h1, h2, h3 {
  margin: 0 0 0.5rem;
  font-weight: 600;
  letter-spacing: -0.005em;
}
h1 { font-size: 1.4rem; }
h2 { font-size: 1.1rem; }
h3 { font-size: 1rem; }

p { margin: 0 0 0.75rem; }
p:last-child { margin-bottom: 0; }

code, kbd, samp { font-family: var(--font-mono); font-size: 0.95em; }

/* ── App shell ──────────────────────────────────────────────────── */

.app {
  max-width: 56rem;
  margin: 0 auto;
  padding: 1.5rem 1.25rem 4rem;
}

.boot {
  padding: 2rem 1rem;
  color: var(--muted);
}

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

.brand {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

.brand .logo {
  display: block;
  /* The v1 logo's viewBox is 400×280 (wider than square), so we pin
   * height and let width follow. ~1.8rem matches the wordmark cap
   * height closely. */
  height: 1.8rem;
  width: auto;
  flex: 0 0 auto;
}

.brand h1 {
  margin: 0;
  font-size: 1.25rem;
  letter-spacing: 0;
}

/* ── Panel — the only "card" surface ────────────────────────────── */

.panel {
  background: var(--surface);
  border: 1.5px solid var(--border);
  border-radius: 2px;
  padding: 1.25rem;
  margin-bottom: 1rem;
}

.panel > h2:first-child,
.panel > h3:first-child {
  margin-top: 0;
}

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

/* Section divider inside a panel (e.g. "Paired proxies" + "Pair new"). */
.panel .divider {
  margin: 1.25rem -1.25rem;
  border-top: 1.5px solid var(--border);
}

/* ── Forms ──────────────────────────────────────────────────────── */

.row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}
.row + .row { margin-top: 0.5rem; }
.row.spaced > * + * { margin-left: 0; }

label.field {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  font-size: 0.8rem;
  color: var(--muted);
  /* Don't let the column stretch the input to fill the row's height —
   * with `align-items: center` on `.row` and uneven sibling heights
   * (e.g. a single field next to a stacked one), unconstrained inputs
   * grow to match. Pin the input to its intrinsic height. */
  line-height: 1.25;
}

.input,
input[type="text"],
input[type="search"] {
  font: inherit;
  font-family: var(--font-mono);
  font-size: 0.9rem;
  padding: 0.3rem 0.55rem;
  background: var(--surface);
  color: var(--ink);
  border: 1.5px solid var(--border);
  border-radius: 2px;
  min-width: 8rem;
  /* Critical: stop inputs inheriting line-height from prose elsewhere
   * — the parent `.field` is a flex column and an inflated line-height
   * blows the input up to ~3rem tall. */
  line-height: 1.4;
  height: auto;
}
.input:focus,
input[type="text"]:focus,
input[type="search"]:focus {
  outline: none;
  border-color: var(--ink);
}

/* IMPORTANT: don't use `flex: 1 1 <basis>` here. These inputs live
 * inside `label.field`, which is a `flex-direction: column` container.
 * `flex-basis` resolves on the parent's *main* axis — vertical for
 * a column — so a `flex-basis: 14rem` blows the input up to 14rem
 * TALL (~3.5cm). Width-based sizing only. */
.input.wide  { width: 14rem;  min-width: 12rem; max-width: 100%; }
.input.code  { width: 14rem;  min-width: 12rem; max-width: 100%; font-weight: 600; }
.input.label { width: 9rem;   min-width: 6rem;  max-width: 100%; font-family: var(--font-sans); }

/* ── Buttons ────────────────────────────────────────────────────── */

.btn {
  font: inherit;
  font-size: 0.95rem;
  padding: 0.45rem 0.85rem;
  background: var(--surface);
  color: var(--ink);
  border: 1.5px solid var(--border-strong);
  border-radius: 2px;
  cursor: pointer;
  line-height: 1.2;
}
.btn:hover:not(:disabled) {
  border-color: var(--ink);
  background: var(--surface-2);
}
.btn:active:not(:disabled) {
  background: var(--border);
}
.btn:disabled {
  color: var(--faint);
  border-color: var(--border);
  cursor: not-allowed;
}

.btn-primary {
  border-color: var(--ink);
  font-weight: 600;
  background: var(--surface);
}
.btn-primary:hover:not(:disabled) {
  background: var(--ink);
  color: var(--surface);
}

.btn-danger {
  border-color: var(--error-ink);
  color: var(--error-ink);
}
.btn-danger:hover:not(:disabled) {
  background: var(--error-ink);
  color: var(--surface);
}

.btn-small {
  font-size: 0.85rem;
  padding: 0.25rem 0.55rem;
}

/* ── Lists (paired proxies, etc.) ───────────────────────────────── */

ul.list {
  list-style: none;
  margin: 0;
  padding: 0;
}
ul.list li {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 0.75rem;
  border: 1.5px solid var(--border);
  border-radius: 2px;
  margin-bottom: 0.4rem;
  background: var(--surface);
}
ul.list li:last-child { margin-bottom: 0; }

ul.list .li-main {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 0;
}
ul.list .li-title {
  font-weight: 500;
}
ul.list .li-meta {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

ul.list.empty {
  color: var(--muted);
  font-size: 0.9rem;
  padding: 0.25rem 0;
}

/* Compact thumbnail used in list-style routes like /history. The
 * row uses `align-items: center` so this just needs a fixed 16:9
 * box on the left. Same proxy-served thumbnail machinery as
 * `view_library_card`. */
.history-thumb {
  flex: 0 0 auto;
  width: 7rem;
  aspect-ratio: 16 / 9;
  display: block;
  overflow: hidden;
  border: 1.5px solid var(--border);
  border-radius: 2px;
  background: var(--surface-2);
}
.history-thumb img,
.history-thumb mutube-thumbnail {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ── Badges — outlined, no color fills ──────────────────────────── */

.badge {
  display: inline-block;
  font-size: 0.75rem;
  font-weight: 600;
  padding: 0.1rem 0.45rem;
  border: 1.5px solid var(--ink);
  border-radius: 2px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  line-height: 1.3;
}
/* "In library" / owner badge — tinted with the brand colour so
 * the most common card state has a visible tie to the logo.
 * Border picks up the baby-blue glyph colour; text uses the
 * darker `--brand-ink` so it still reads at AA contrast on the
 * white card surface. */
.badge.owner {
  border-color: var(--brand);
  color: var(--brand-ink);
}
.badge.friend {
  border-color: var(--muted);
  color: var(--muted);
}

/* ── Code / id displays ─────────────────────────────────────────── */

.code-display,
.id-display {
  font-family: var(--font-mono);
  background: var(--surface-2);
  border: 1.5px solid var(--border);
  border-radius: 2px;
  padding: 0.35rem 0.55rem;
  user-select: all;
  overflow-wrap: anywhere;
}

.code-display {
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: 0.04em;
}

.id-display {
  font-size: 0.85rem;
  color: var(--ink-soft);
}

/* ── Inline messages ────────────────────────────────────────────── */

.message {
  font-size: 0.9rem;
  padding: 0.5rem 0.7rem;
  border: 1.5px solid var(--border);
  border-radius: 2px;
  background: var(--surface);
  margin-top: 0.5rem;
}
.message.error {
  border-color: var(--error-ink);
  color: var(--error-ink);
}
.message.muted {
  color: var(--muted);
  border-style: dashed;
}

/* ── Connection status + RPC test panel (Phase 3b) ──────────────── */

.status-line {
  font-family: var(--font-mono);
  font-size: 0.9rem;
  color: var(--ink-soft);
  /* No background, no border — this is a caption, not a banner. */
  padding: 0.1rem 0;
}

/* Pre-block used by the RPC smoke panel. Soft-wrap so a long
 * library-list response doesn't blow out the panel width on an
 * e-ink panel. */
.rpc-output pre {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  background: var(--surface-2);
  border: 1.5px solid var(--border);
  border-radius: 2px;
  padding: 0.6rem 0.75rem;
  margin: 0.4rem 0 0;
  max-height: 18rem;
  overflow: auto;
  white-space: pre-wrap;
  word-break: break-word;
}

.rpc-output .muted {
  font-size: 0.85rem;
}

/* Tighten the paired-list rows now that they carry two action
 * buttons (Connect + Forget) on the right side. */
ul.list li .btn-small + .btn-small {
  margin-left: 0.4rem;
}

/* ── Misc ───────────────────────────────────────────────────────── */

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

/* Top-margin utilities. Used to space siblings vertically without
 * inserting an empty spacer element — historical `.spacer-1` /
 * `.spacer-2` were height-based and silently squashed any row they
 * were applied to. Don't bring those back. */
.mt-1 { margin-top: 0.5rem; }
.mt-2 { margin-top: 1rem; }

.right { margin-left: auto; }
.hidden { display: none !important; }

details > summary {
  cursor: pointer;
  color: var(--muted);
  font-size: 0.9rem;
  user-select: none;
}
details[open] > summary { color: var(--ink); }
details > .details-body { margin-top: 0.6rem; }

/* ════════════════════════════════════════════════════════════════
 * Phase 3c — connected-mode shell
 *
 * Adds: top nav, video grid + cards, channel page (banner+avatar),
 * search bar, downloads strip, invite call-out, video-route
 * placeholder.
 *
 * Stays inside the design vocab (1.5px borders, no fills-for-state,
 * no transitions/gradients/shadows).
 * ──────────────────────────────────────────────────────────────── */

.app-connected {
  /* Connected mode benefits from a wider canvas — video grids look
   * silly squeezed into 56rem. Cap at 80rem so column line lengths
   * inside individual cards stay readable. */
  max-width: 80rem;
}

.header-connected {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
}

.header-connected .nav {
  display: flex;
  gap: 0.25rem;
  align-items: center;
  margin-left: 1rem;
  flex: 1 1 auto;
}

.header-connected .header-right {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-left: auto;
}

.nav-link {
  display: inline-block;
  padding: 0.3rem 0.7rem;
  border: 1.5px solid transparent;
  border-radius: 2px;
  text-decoration: none;
  color: var(--muted);
  font-size: 0.95rem;
}
.nav-link:hover {
  color: var(--ink);
  border-color: var(--border);
}
.nav-link.active {
  color: var(--ink);
  font-weight: 600;
  border-color: var(--ink);
}

/* The dot before the connected-to label. Pure ink, no glow. */
.header-right .status-line {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  color: var(--ink-soft);
}

/* ── Spaced row (h2 on the left, button on the right) ──────────── */

.row.spaced {
  width: 100%;
  justify-content: space-between;
}
.row.spaced > .right { margin-left: auto; }

/* ── Video grid + card ─────────────────────────────────────────── */

.video-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 1rem;
  margin-top: 0.5rem;
}

.video-card {
  display: flex;
  flex-direction: column;
  border: 1.5px solid var(--border);
  border-radius: 2px;
  background: var(--surface);
  overflow: hidden;
  min-width: 0;
}

.video-thumb {
  display: block;
  background: var(--surface-2);
  border-bottom: 1.5px solid var(--border);
  /* Hold a 16:9 box even before the image loads so the grid doesn't
   * reflow as thumbnails arrive. */
  aspect-ratio: 16 / 9;
  overflow: hidden;
}
.video-thumb img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.video-meta {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: 0.6rem 0.7rem 0.7rem;
  min-width: 0;
  /* Fill the card's remaining height so `.card-foot`'s
   * `margin-top: auto` actually has space to push into. Without
   * `flex: 1 1 auto` here, short-title cards leave the foot row
   * floating in the middle while tall-title cards push it to the
   * bottom — that's the "download button drifts" bug. */
  flex: 1 1 auto;
}

.video-title {
  font-size: 0.95rem;
  font-weight: 500;
  color: var(--ink);
  text-decoration: none;
  /* Clamp to 2 lines so cards stay even-height. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  line-height: 1.3;
}
a.video-title:hover { text-decoration: underline; }

.video-channel {
  font-size: 0.85rem;
  color: var(--muted);
  text-decoration: none;
}
.video-channel:hover { text-decoration: underline; color: var(--ink); }

/* ── Per-card quality picker + Download cluster ────────────────── */

/* Sits at the right edge of each `.video-card`'s foot row, paired
 * with the Download button. The picker is intentionally TINY so it
 * doesn't dominate the card; users who want a different quality on
 * one specific card pick it before clicking Download. */
.card-download {
  display: flex;
  align-items: center;
  gap: 0.3rem;
}

.quality-picker-inline {
  font: inherit;
  font-family: var(--font-mono);
  font-size: 0.75rem;
  padding: 0.15rem 0.3rem;
  background: var(--surface);
  color: var(--ink);
  border: 1.5px solid var(--border);
  border-radius: 2px;
  line-height: 1.2;
  /* Native chevron is fine on e-ink — no `appearance: none` hacks. */
  max-width: 5rem;
}
.quality-picker-inline:focus {
  outline: none;
  border-color: var(--ink);
}

/* `.card-foot` is the bottom row of `.video-meta` (duration on the
 * left, [picker + Download] cluster on the right). Two layout jobs:
 *   1. `margin-top: auto` pushes the row to the *bottom* of the
 *      card so every card's Download button lines up across the
 *      grid, regardless of how many lines the title clamped to.
 *      Pairs with `.video-meta { flex: 1 1 auto }` above — the
 *      meta has to fill the card for `auto` to have any room.
 *   2. `space-between` pins duration to the left edge and the
 *      [picker + Download] cluster to the right; `.card-download`
 *      itself doesn't need a margin since space-between handles it. */
.card-foot {
  justify-content: space-between;
  margin-top: auto;
}

/* Right-side action cluster inside `.card-foot`. Wraps the favorite
 * star + the download / "In library" / status badge so they travel
 * together against the right edge — otherwise `space-between` with
 * 3 children would strand the star in the middle of the row. */
.card-actions {
  display: flex;
  align-items: center;
  gap: 0.3rem;
}

/* The "In library" / status badge sits next to a `.btn-small`
 * (Download, Delete, the favorite star). The default `.badge`
 * uses smaller font + tighter padding, which leaves it visibly
 * thinner than the neighbouring button. Scope the override so
 * `.badge` everywhere else (e.g. download-row status, account
 * status) keeps its compact look. */
.card-actions .badge {
  font-size: 0.85rem;
  line-height: 1.2;
  padding: 0.25rem 0.55rem;
}

/* ── Search bar ────────────────────────────────────────────────── */

.search-bar {
  margin-top: 0.5rem;
  gap: 0.5rem;
}
.search-bar .input.wide { width: 28rem; max-width: 100%; }

/* ── Channel page (banner + avatar + meta + actions) ───────────── */

.channel-page { padding: 0; }
.channel-page > .channel-header { border-bottom: 1.5px solid var(--border); }
.channel-page > *:not(.channel-header) { padding-left: 1.25rem; padding-right: 1.25rem; }
.channel-page > *:last-child { padding-bottom: 1.25rem; }

.channel-banner {
  width: 100%;
  /* Cap banner height so a 2560×400 YouTube banner doesn't dominate
   * the page. 12rem keeps it visible without crowding the videos. */
  max-height: 12rem;
  overflow: hidden;
}
.channel-banner img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.channel-header-body {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 1rem 1.25rem;
  flex-wrap: wrap;
}

.channel-avatar {
  width: 5rem;
  height: 5rem;
  border-radius: 50%;
  border: 1.5px solid var(--border);
  background: var(--surface-2);
  object-fit: cover;
  flex: 0 0 auto;
}

.channel-meta {
  flex: 1 1 18rem;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
}
.channel-meta h2 { margin: 0; }
.channel-meta .channel-desc {
  /* Long channel descriptions blow the panel out if rendered raw.
   * Clamp to 3 lines; the user can refresh / open the channel page
   * for the full text via yt-dlp if they really need it. */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin-top: 0.3rem;
}

.channel-actions { margin-left: auto; flex: 0 0 auto; }

/* ── Channel hits row (search panel) ───────────────────────────── */

.channel-hits {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
  gap: 0.6rem;
  margin-top: 0.5rem;
}

.channel-hit {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.6rem;
  border: 1.5px solid var(--border);
  border-radius: 2px;
  background: var(--surface);
  text-decoration: none;
  color: var(--ink);
  min-width: 0;
}
.channel-hit:hover { border-color: var(--ink); }
.channel-hit-avatar {
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 50%;
  object-fit: cover;
  flex: 0 0 auto;
  background: var(--surface-2);
  border: 1.5px solid var(--border);
}
.channel-hit-meta {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 0;
}
.channel-hit-name {
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── Downloads strip ───────────────────────────────────────────── */

.panel.downloads { padding-bottom: 0.6rem; }
.download-row .li-meta.error { color: var(--error-ink); }

/* ── Invite call-out (admin) ───────────────────────────────────── */

.invite-callout {
  border-color: var(--ink);
  /* Slightly heavier feel without resorting to a fill — owners
   * shouldn't have to hunt for the freshly-minted code. */
  border-width: 2px;
}

/* ── Video page (Phase 3d — MSE player + progress bar) ─────────── */

.video-page h2 { margin-bottom: 0.2rem; }
.video-page .subtitle { margin-bottom: 0.6rem; }

/* Black wrapper so the letterboxing of varying aspect ratios reads
 * as "this is the player frame" rather than "the panel has a hole
 * in it". The wrapper, not the <video>, is what carries the border. */
.video-player-wrap {
  background: #000;
  border: 1.5px solid var(--border);
  border-radius: 2px;
  margin: 0.5rem 0;
  /* Reserve a 16:9 box even before <video> has metadata, so the
   * layout doesn't jump when MSE first reports duration / size. */
  aspect-ratio: 16 / 9;
  overflow: hidden;
}

.video-player {
  display: block;
  width: 100%;
  height: 100%;
  /* Letterbox the video into the 16:9 frame regardless of the
   * source aspect ratio. `contain` keeps the whole frame visible
   * (vs `cover` which would crop). */
  object-fit: contain;
  background: #000;
}

/* ── Playback progress bar ─────────────────────────────────────── */

.playback-progress { margin-top: 0.5rem; }

.playback-progress-bar {
  width: 100%;
  height: 0.4rem;
  border: 1.5px solid var(--border);
  border-radius: 2px;
  background: var(--surface-2);
  overflow: hidden;
  /* No transitions — width changes snap to the new value. Smooth
   * animations would draw the eye on every appended range; users
   * who want a flowing UX have the native <video> controls. */
}

.playback-progress-fill {
  height: 100%;
  background: var(--ink);
}

/* ── Format table (video page, below the player) ──────────────────
 *
 * Two-column key/value layout. Key column is muted + right-aligned
 * for a compact look; value column is monospace because codec
 * strings like `av01.0.05M.08` and `mp4a.40.2` only read sanely in
 * a fixed-width font. We don't draw cell borders — the surrounding
 * `.panel` already provides the visual containment, and inner
 * lines would clash with the rest of the design vocab. */
.format-table {
  border-collapse: collapse;
  margin: 0;
  /* Keep keys close to values; the key column gets the leftover
   * width, value column shrinks to content. `auto` fills the panel
   * without forcing the table to stretch full-width on a wide
   * viewport where the rows would otherwise look adrift. */
  width: auto;
  min-width: 18rem;
}
.format-table th {
  font-weight: 500;
  color: var(--muted);
  text-align: left;
  padding: 0.2rem 1rem 0.2rem 0;
  white-space: nowrap;
  vertical-align: top;
}
.format-table td {
  font-family: var(--font-mono);
  font-size: 0.95rem;
  padding: 0.2rem 0;
  /* Long codec strings can run past the panel on mobile — wrap
   * anywhere rather than overflowing the cell. */
  word-break: break-all;
}

.playback-progress-meta {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  color: var(--muted);
  margin-top: 0.3rem;
}

/* ── Subscription rows: channel avatar ─────────────────────────── */

.subscription-row {
  display: flex;
  align-items: center;
  gap: 0.7rem;
}

.subscription-avatar {
  flex: 0 0 auto;
  display: block;
  width: 2.2rem;
  height: 2.2rem;
  border-radius: 50%;
  overflow: hidden;
  background: var(--surface-2);
  border: 1px solid var(--border);
}

.subscription-avatar img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ── Continue Watching strip (on /library) ─────────────────────── */

.continue-watching {
  margin-bottom: 1.5rem;
}

.continue-watching-heading {
  font-size: 1.05rem;
  margin: 0 0 0.6rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

/* The progress bar and dismiss button overlay the thumbnail. The
 * wrapper provides the positioning context; the link inside fills
 * it so clicks on the thumbnail still navigate to the video. */
.continue-thumb-wrap {
  position: relative;
}

.continue-thumb-link {
  display: block;
  width: 100%;
  height: 100%;
}

.continue-progress {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 0.25rem;
  border: 0;
  border-radius: 0;
  background: rgba(0, 0, 0, 0.35);
}

.continue-progress .playback-progress-fill {
  /* Slightly brighter than the regular progress bar so the strip
   * reads as "in progress" at a glance against the thumbnail. */
  background: var(--accent, var(--ink));
}

/* Dismiss-from-Continue-Watching X. Floats over the top-right
 * corner of the thumbnail. Matches the rest of the app's button
 * vocabulary: white surface, 1.5px ink-tone border, square with
 * 2px corner radius. Always visible — it's quiet enough not to
 * compete with the title. */
.continue-dismiss {
  position: absolute;
  top: 0.3rem;
  right: 0.3rem;
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  font-size: 1rem;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--surface);
  color: var(--ink);
  border: 1.5px solid var(--border);
  border-radius: 2px;
  cursor: pointer;
}

.continue-dismiss:hover,
.continue-dismiss:focus-visible {
  border-color: var(--border-strong);
}

/* ── Tweaks: code inline ───────────────────────────────────────── */

p code, span code {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 2px;
  padding: 0.02rem 0.3rem;
}

/* ── Account panel (Phase 3e — encrypted-blob sync) ────────────── */

.account-panel .account-form { margin-top: 0.5rem; }

.account-status-badge {
  display: inline-flex;
  align-items: center;
  font-family: var(--font-mono);
  font-size: 0.75rem;
  font-weight: 600;
  padding: 0.1rem 0.5rem;
  border: 1.5px solid var(--border-strong);
  border-radius: 2px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  line-height: 1.4;
  color: var(--muted);
}
.account-status-badge.loading {
  border-color: var(--ink);
  color: var(--ink);
}
/* When rendered as a <button> (retry affordance on sync error)
 * we don't want browser button chrome — keep the same look as
 * the <span> version, just add a pointer cursor. */
button.account-status-badge {
  font: inherit;
  cursor: pointer;
  text-align: inherit;
}
button.account-status-badge:hover { filter: brightness(1.08); }

.account-status-badge.error {
  border-color: var(--error-ink);
  color: var(--error-ink);
}

/* The header's account badge sits just before the "● connected to
 * <proxy>" line; tighten its left margin so the two read as one
 * status bar. */
.header-right .account-status-badge { margin-right: 0.4rem; }

/* ── Inline error in li-meta (download rows) ───────────────────── */

ul.list li .li-meta.error {
  color: var(--error-ink);
  white-space: normal;
  overflow: visible;
}

/* ════════════════════════════════════════════════════════════════
 * Mobile — narrow viewports (phones, narrow tablets in portrait).
 *
 * Strategy: keep the design vocab (ink, 1.5px borders, no fills /
 * gradients / transitions). Just relax the desktop layout so:
 *
 *   * the app shell uses the full screen width with tight padding
 *   * the connected-mode header lets nav + status wrap onto a
 *     second row instead of squishing the brand off-screen
 *   * grids collapse to single column (already 1-col anyway at the
 *     auto-fill threshold; we drop the min so cards don't leave a
 *     dead band of whitespace next to them)
 *   * list rows let action buttons wrap below the meta so a long
 *     channel name doesn't get truncated to "Bo…" between two
 *     buttons
 *   * tap targets bump to at least 36–40px on a finger-sized
 *     hit area
 *
 * Breakpoint at 640px (≈ a 5–6" phone in portrait). Tablets in
 * landscape (>= 768px) keep the desktop layout — they have room
 * for the grid + sidebar pattern.
 * ──────────────────────────────────────────────────────────────── */

@media (max-width: 640px) {
  /* ── Shell ────────────────────────────────────────────────── */

  .app, .app-connected {
    /* Half the side padding — the existing 1.25rem eats 40px of a
     * 375px viewport. */
    padding: 1rem 0.75rem 3rem;
  }

  /* ── Header ───────────────────────────────────────────────── */

  /* Connected-mode header: stack nav + status under the brand,
   * but keep each cluster on ONE line instead of wrapping into a
   * 4-5-row monster that eats half the viewport. Strategy:
   *   - brand: shrink to logo-only (hide the "MuTube" wordmark);
   *   - nav: horizontal scroll on a single row (touch-swipe to
   *     reach links that don't fit);
   *   - header-right: shrink padding and let it wrap as needed
   *     but don't force flex-wrap so the typical case stays one row.
   *
   * Net result on a 375px viewport: ~2 rows of header total
   * (logo+nav scroll on one, status+disconnect on the next)
   * instead of the previous 4-5. */
  .header-connected {
    gap: 0.4rem;
    padding-bottom: 0.75rem;
    margin-bottom: 0.75rem;
  }

  /* Logo-only brand. The wordmark next to a 5-link nav is what
   * pushed the header into multi-row hell. The μ-glyph is
   * recognisable on its own and we save ~5rem of horizontal real
   * estate. */
  .header-connected .brand h1,
  .header .brand h1 {
    display: none;
  }
  .brand .logo {
    height: 1.5rem;
  }

  .header-connected .nav {
    margin-left: 0;
    /* `flex: 1 1 0` lets the nav claim the leftover row width
     * AFTER the brand, but `min-width: 0` is what actually lets
     * it shrink below its intrinsic content width — without it
     * the nav refuses to fit and forces the header-right onto a
     * new row. */
    flex: 1 1 0;
    min-width: 0;
    /* Single-row nav with horizontal scroll for any links that
     * overflow. Swipe to reach Subscriptions / History / etc.
     * `flex-wrap` reset to nowrap overrides the desktop default. */
    flex-wrap: nowrap;
    overflow-x: auto;
    /* Hide the scrollbar — the row is short enough that the
     * affordance is obvious from partial-clipping of the last
     * visible link. */
    scrollbar-width: none;
    gap: 0.1rem;
  }
  .header-connected .nav::-webkit-scrollbar { display: none; }

  .header-connected .nav-link {
    padding: 0.4rem 0.55rem;
    /* Don't let individual link text wrap mid-word — that would
     * make the scrollable row taller than one line. */
    white-space: nowrap;
    flex: 0 0 auto;
  }

  .header-connected .header-right {
    margin-left: 0;
    width: 100%;
    /* Push the cluster to the right edge of its row. On mobile the
     * row sits underneath the nav (because header-right gets
     * width:100%), so right-aligning here puts the Disconnect
     * button comfortably near the user's thumb in the bottom-right
     * of the typical phone hold. */
    justify-content: flex-end;
    /* Allow wrap but keep gaps tight — the common case (sync
     * badge + connected dot + Disconnect) fits one row at 375px. */
    flex-wrap: wrap;
    gap: 0.4rem;
  }
  .header-right .status-line {
    /* Truncate the proxy id / hostname if it's too long instead of
     * pushing the Disconnect button off-screen. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 14rem;
  }

  /* Disconnected-mode header (logo + signed-in badge): same
   * story — let the right-side badge wrap underneath. */
  .header {
    flex-wrap: wrap;
    gap: 0.5rem;
    padding-bottom: 0.75rem;
    margin-bottom: 0.75rem;
  }

  /* ── Panels ──────────────────────────────────────────────── */

  .panel {
    padding: 1rem;
  }
  .panel .divider {
    margin: 1rem -1rem;
  }

  /* ── Forms ───────────────────────────────────────────────── */

  /* Force the fixed-width inputs to span the full row so a 14rem
   * `.input.wide` doesn't push the submit button off the side. */
  .input.wide,
  .input.code,
  .input.label {
    width: 100%;
    min-width: 0;
  }

  /* Search bar: keep the input and the Search button on the same
   * row but let the input claim the leftover space. */
  .search-bar {
    flex-wrap: wrap;
  }
  .search-bar .input.wide {
    flex: 1 1 12rem;
    min-width: 0;
  }

  /* ── Buttons (touch targets) ──────────────────────────────── */

  /* Bump base button padding so the standard `.btn` hits a real
   * ~40px tap target. `.btn-small` stays slightly more compact
   * for in-row actions (Connect / Forget / Subscribe). */
  .btn {
    padding: 0.55rem 0.85rem;
    font-size: 1rem;
  }
  .btn-small {
    padding: 0.35rem 0.65rem;
    font-size: 0.9rem;
  }

  /* ── Lists (paired proxies, accounts, invites, downloads) ──── */

  /* Let the action buttons wrap UNDER the meta on narrow screens —
   * `.li-main` claims the full row, buttons drop to a second line.
   * Without this, a long channel name gets ellipsed between two
   * buttons even when they would fit on their own line. */
  ul.list li {
    flex-wrap: wrap;
    gap: 0.5rem;
  }
  ul.list .li-main {
    flex: 1 1 100%;
  }

  /* History thumb: 7rem is ~112px, which competes with the title
   * column on a 320px screen. Shrink to 5rem (~80px) so the title
   * gets room to wrap onto two lines. */
  .history-thumb {
    width: 5rem;
  }

  /* ── Grids ──────────────────────────────────────────────── */

  /* Drop the auto-fill min so a single column gets the full row
   * width instead of leaving a dead band next to a 15rem card
   * on a 24rem viewport. */
  .video-grid {
    grid-template-columns: 1fr;
    gap: 0.75rem;
  }
  .channel-hits {
    grid-template-columns: 1fr;
  }

  /* ── Channel page (banner + avatar + subscribe) ─────────── */

  .channel-header-body {
    gap: 0.6rem;
    padding: 0.8rem 1rem;
  }
  .channel-page > *:not(.channel-header) {
    padding-left: 1rem;
    padding-right: 1rem;
  }
  /* Shrink the avatar so it sits comfortably above the meta on a
   * stacked layout (flex-wrap already handles the stacking). */
  .channel-avatar {
    width: 4rem;
    height: 4rem;
  }
  /* On stacked layout, Subscribe goes full width so the user
   * doesn't have to thumb-stretch to the right edge. */
  .channel-actions {
    margin-left: 0;
    width: 100%;
  }
  .channel-actions .btn {
    width: 100%;
  }
  /* Cap banner height tighter on phones — 12rem is half the
   * screen on a typical phone in portrait. */
  .channel-banner {
    max-height: 8rem;
  }

  /* ── Continue Watching ──────────────────────────────────── */

  /* Bigger tap target for the dismiss-X overlay (1.75rem ≈ 28px
   * is below the 40px-ish finger comfortable threshold). */
  .continue-dismiss {
    width: 2.25rem;
    height: 2.25rem;
    font-size: 1.1rem;
  }

  /* ── Video page ─────────────────────────────────────────── */

  /* The video player wrap already adapts via aspect-ratio. Just
   * trim title spacing so the player sits closer to the top of
   * the viewport when the URL bar collapses. */
  .video-page h2 {
    font-size: 1.1rem;
  }

  /* ── Sync / status badges ───────────────────────────────── */

  /* Account status badge: when it lives in the header-right and
   * the header wraps, give it a bit of bottom margin so it doesn't
   * butt up against the "● connected to …" line below. */
  .header-right .account-status-badge {
    margin-right: 0;
  }

  /* ── Code / id display blocks ──────────────────────────── */

  /* Invite codes and account_id hex strings get long. Break
   * anywhere so a 64-char hex doesn't push the panel out. The
   * desktop CSS already sets `overflow-wrap: anywhere`; the
   * mobile-specific tweak is just to drop letter-spacing so
   * the broken line doesn't look like a typo. */
  .code-display {
    letter-spacing: 0.02em;
  }
}

/* Extra-narrow viewports (< 380px — older / smaller phones,
 * also landscape browsers with a side panel open). Mostly the
 * same rules but with one more squeeze on side padding. */
@media (max-width: 380px) {
  .app, .app-connected {
    padding: 0.75rem 0.5rem 3rem;
  }
  .panel {
    padding: 0.85rem;
  }
  .header-connected .nav-link {
    /* Tighter padding on very narrow screens so more links are
     * visible without swiping the (single-row, horizontally-
     * scrollable) nav. */
    padding: 0.4rem 0.5rem;
    font-size: 0.9rem;
  }
  /* Cap the status-line truncation tighter on extra-narrow viewports
   * so the Disconnect button keeps its row. */
  .header-right .status-line {
    max-width: 9rem;
  }
}

/* ── Print (paper-out invite codes, account-code recovery) ──────── */

@media print {
  :root {
    --bg: #ffffff;
    --surface: #ffffff;
    --ink: #000000;
    --muted: #000000;
    --border: #000000;
  }
  body { background: #ffffff; color: #000; font-size: 12pt; }
  .btn, .header .right, .pair-form, .no-print { display: none !important; }
  .panel { border: 1px solid #000; box-shadow: none; }
}

/* ── AnySoul: format picker + tracklist preview ──────────────────────
 * The search-card additions introduced in the search-preview phase.
 * The base `.album-card` / `.track-list` / `.btn-small` rules earlier
 * in the file do the heavy lifting; this block only handles the new
 * sub-elements and their compact variants. */

.album-formats {
  font-size: 0.85rem;
  margin-top: 0.15rem;
}
.album-quality {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.25rem;
  font-size: 0.85rem;
}
.quality-select {
  font: inherit;
  padding: 0.1rem 0.3rem;
  border-radius: 3px;
  border: 1px solid var(--border, #ccc);
  background: var(--surface, transparent);
  color: inherit;
}

/* Ghost-style toggle button: lives next to the primary Save action
 * on the same row, so we want it visually quieter. */
.btn-ghost {
  background: transparent;
  border: 1px solid transparent;
  color: inherit;
  opacity: 0.75;
}
.btn-ghost:hover {
  opacity: 1;
  background: rgba(0, 0, 0, 0.05);
}

/* Compact track list rendered inside an expanded search card.
 * Tighter than the library view so a 14-track preview doesn't push
 * the rest of the grid off-screen. */
.track-list-compact {
  list-style: none;
  margin: 0.6rem 0 0;
  padding: 0;
  border-top: 1px solid var(--border, rgba(0, 0, 0, 0.1));
  font-size: 0.85rem;
}
.track-row-compact {
  display: grid;
  /* N · title · duration · tier · (unavailable hint, optional) */
  grid-template-columns: 2rem 1fr auto auto auto;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.2rem 0;
}
.track-row-compact .track-no {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.track-row-compact .track-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Unavailable tracks: MB knows about them but no peer is sharing.
   Dimmed + struck-through so the user sees the canonical album
   skeleton without being misled into thinking they can play it. */
.track-row-unavailable {
  opacity: 0.5;
}
.track-row-unavailable .track-title {
  text-decoration: line-through;
  text-decoration-color: rgba(0, 0, 0, 0.4);
}
.track-unavailable {
  font-style: italic;
}
.ml-1 {
  margin-left: 0.25rem;
}

/* ── Search-result multi-album candidates ──────────────────────────
 *
 * The aggregator now returns up to 10 candidate albums per query
 * (canonical MusicBrainz match + peer-only variants). The verified
 * badge marks the MB-validated pick so the user can spot it
 * instantly in the grid. .album-card-verified gets a subtle
 * accent border so the canonical card stands out as a card, not
 * just by virtue of the badge inside it. */
.album-title-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
}
.badge.verified {
  border-color: var(--brand);
  color: var(--brand-ink);
  /* Slightly tighter than the default .badge so it doesn't tower
     over the album title it sits next to. */
  font-size: 0.7rem;
  padding: 0.05rem 0.35rem;
}
.album-card-verified {
  /* Use box-shadow rather than border so the verified card
     doesn't shift adjacent siblings by the border width when
     the grid wraps. */
  box-shadow: 0 0 0 1.5px var(--brand);
}

/* ── AnySoul: search results (vertical list, not a grid) ─────────────
 *
 * Search results render as a vertical list — distinct from the
 * library's album-art wall — because every search card is
 * expandable into a full canonical tracklist (up to 16+ rows)
 * and a fixed grid cell would cramp them.
 *
 * Each card is a 3-column / 2-row CSS grid:
 *
 *   col-widths: [cover 9rem] [meta 1fr] [actions auto]
 *   row 1:        cover     meta        actions
 *   row 2:        cover     tracks      tracks   (tracks span 2 cols)
 *
 * When the tracklist is collapsed (no children inside .sc-tracks)
 * row 2 collapses to 0 height. When expanded, the tracks block
 * spans from below the meta to the right edge of the card with
 * the cover column to its left, giving 16-track previews room to
 * breathe. */

.search-results {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin-top: 0.5rem;
}

.search-card {
  display: grid;
  grid-template-columns: 9rem 1fr auto;
  grid-template-areas:
    "cover meta    actions"
    "cover tracks  tracks";
  gap: 0.5rem 1rem;
  padding: 0.75rem;
  border: 1.5px solid var(--border);
  border-radius: 2px;
  background: var(--surface);
  min-width: 0;
}

.search-card-verified {
  /* Same subtle accent as library `.album-card-verified` —
   * box-shadow so it doesn't push siblings around. */
  box-shadow: 0 0 0 1.5px var(--brand);
}

.search-card .sc-cover {
  grid-area: cover;
  align-self: start;
}
.search-card .sc-cover .cover {
  width: 9rem;
  aspect-ratio: 1 / 1;
}

.search-card .sc-meta {
  grid-area: meta;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  min-width: 0;
  /* Don't let long titles/artists stretch the meta column wide
   * enough to push the actions column off the row. */
  overflow: hidden;
}
.search-card .sc-meta .album-title {
  font-size: 1.05rem;
  font-weight: 500;
  /* No clamp — the search card has horizontal room to print
   * the whole title on one row (or wrap to two as needed). */
  display: block;
  overflow: visible;
  white-space: normal;
}
.search-card .sc-meta .album-artist {
  font-size: 0.9rem;
}

.search-card .sc-actions {
  grid-area: actions;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.35rem;
  white-space: nowrap;
}

.search-card .sc-tracks {
  grid-area: tracks;
  min-width: 0;
}
/* Pad the tracks block from the row above only when it
 * actually has content; otherwise the empty wrapper would
 * inject a phantom gap when collapsed. */
.search-card .sc-tracks:not(:empty) {
  margin-top: 0.5rem;
  padding-top: 0.5rem;
  border-top: 1px solid var(--border);
}

/* Compact tracklist already exists higher up in the file
 * (`.track-list-compact` / `.track-row-compact`). Inside the
 * new card it sits in the wide tracks row, no extra rules
 * needed — but we override the default 5-column compact grid
 * so the title gets the bulk of the now-wider row. */
.search-card .track-row-compact {
  grid-template-columns: 2rem minmax(0, 1fr) auto auto auto;
}
.search-card .track-list-compact {
  /* Lift the 200-px-ish font default; in the wider card the
   * tracks read better at the same scale as the meta. */
  font-size: 0.9rem;
  /* No top border — the wrapper above already drew one. */
  border-top: 0;
  margin-top: 0;
}

/* ── Mobile: stack the search card so the cover sits above the meta. */
@media (max-width: 640px) {
  .search-card {
    grid-template-columns: 1fr;
    grid-template-areas:
      "cover"
      "meta"
      "actions"
      "tracks";
    gap: 0.5rem;
  }
  .search-card .sc-cover .cover {
    /* Smaller, left-aligned thumbnail on mobile so the meta
     * column gets the full row width. */
    width: 7rem;
  }
  .search-card .sc-actions {
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
  }
}

/* ── AnySoul: loading banner ─────────────────────────────────────────
 *
 * Shown in every panel that's waiting on a slow RPC (library
 * fetch, search, album fetch). Three design rules from the rest
 * of this stylesheet still apply: no gradients, no shadows, no
 * colour-as-meaning. We allow ONE concession to motion — a quiet
 * pulse on the square + a three-dot ellipsis — because a fully
 * static "Searching" feels indistinguishable from "frozen" when
 * the slsk window is 12s long.
 *
 * The whole rule is wrapped in `@media (prefers-reduced-motion:
 * no-preference)` so users who opted out of animations (and
 * e-ink browsers that signal it) get the static form. */

.loading-banner {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.75rem 0.85rem;
  border: 1.5px dashed var(--border-strong);
  border-radius: 2px;
  background: var(--surface);
  color: var(--ink-soft);
  font-size: 0.95rem;
  margin: 0.5rem 0;
}
.loading-banner::after {
  content: "";
  display: inline-block;
  width: 1.5em;
  text-align: left;
  /* The ellipsis grows from "" → "." → ".." → "..." driven by
   * the `loading-dots` keyframes. Width is pinned so the
   * trailing chars don't shift the row's other content. */
  font-family: var(--font-mono);
}

@keyframes loading-dots {
  0%   { content: ""; }
  25%  { content: "."; }
  50%  { content: ".."; }
  75%  { content: "..."; }
  100% { content: ""; }
}

@media (prefers-reduced-motion: no-preference) {
  .loading-banner::after {
    animation: loading-dots 1.4s steps(1, end) infinite;
  }
}

/* ── AnySoul: library + album grids and covers ───────────────────────
 *
 * `.cover` is the canonical album-art element used by every panel
 * (search card, library card, album header, now-playing, transport
 * bar). We size it via aspect-ratio so a missing image (CAA 404,
 * placeholder gradient) still holds its place in the layout instead
 * of collapsing the surrounding flex/grid cell.
 *
 * Placeholder fall-back: when the proxy doesn't ship a cover_url
 * the view emits `.cover.cover-placeholder` which gets the soft
 * surface-2 fill so the spot reads as "intentional empty art"
 * rather than "broken layout". */

.cover {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  background: var(--surface-2);
  border: 1.5px solid var(--border);
  border-radius: 2px;
}

.cover-placeholder {
  /* Subtle diagonal hatching so an empty cover slot reads
   * distinct from "image still loading". E-ink friendly: no
   * gradients, just a 1px-stripe pattern via linear-gradient
   * (the only gradient in the file — kept here because the
   * alternative is a half-baked SVG asset).  */
  background-image: repeating-linear-gradient(
    45deg,
    var(--surface-2),
    var(--surface-2) 8px,
    var(--surface) 8px,
    var(--surface) 16px
  );
}

/* ── Library / search album grids ─────────────────────────────────── */

.album-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: 1rem;
  margin-top: 0.5rem;
}

.album-card {
  display: flex;
  flex-direction: column;
  border: 1.5px solid var(--border);
  border-radius: 2px;
  background: var(--surface);
  padding: 0.6rem;
  gap: 0.5rem;
  min-width: 0;
  /* Grid cells stretch to row height by default; we want the
   * actions row to sit flush at the bottom of every card so the
   * Forget buttons line up across the grid regardless of how
   * many lines the title wraps to. The `margin-top: auto` below
   * on `.album-card-actions` does the actual pushing — this just
   * makes sure the card itself fills its cell so there's
   * somewhere for the auto-margin to expand into. */
  height: 100%;
}

/* Bottom-right action strip. `margin-top: auto` consumes all the
 * free vertical space between meta and actions so the buttons
 * sit flush at the bottom even when titles are short.
 * `justify-content: flex-end` right-aligns the button(s) within
 * the strip. */
.album-card-actions {
  margin-top: auto;
  display: flex;
  justify-content: flex-end;
  gap: 0.4rem;
}

.album-card .cover {
  /* Covers in the grid live in a square slot; the card body's
   * padding handles spacing on all four sides. */
  width: 100%;
}

.album-meta {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 0;
}
.album-artist {
  font-size: 0.85rem;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.album-title {
  font-size: 1rem;
  font-weight: 500;
  color: var(--ink);
  text-decoration: none;
  /* Clamp to 2 lines so card heights line up across the grid. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  line-height: 1.25;
}
a.album-title:hover { text-decoration: underline; }
.album-counts {
  font-size: 0.8rem;
}

/* ── Album header (per-album route) ───────────────────────────────── */

.album-header {
  display: flex;
  align-items: flex-start;
  gap: 1.25rem;
  margin-bottom: 1rem;
  flex-wrap: wrap;
}
.album-header .cover {
  /* Fixed 12rem (~192px) square for the header cover. Cap with
   * max-width so it shrinks on narrow viewports instead of
   * pushing the meta column off-screen. */
  width: 12rem;
  max-width: 40vw;
  flex: 0 0 auto;
}
.album-header .album-meta {
  flex: 1 1 18rem;
  min-width: 0;
  gap: 0.3rem;
}
.album-header .album-artist {
  font-size: 0.95rem;
}
.album-header h2.album-title {
  font-size: 1.5rem;
  font-weight: 600;
  margin: 0;
  /* No clamp here — the header has room for the full title. */
  display: block;
  overflow: visible;
}

/* ── Track list (album route) ────────────────────────────────────────
 *
 * A 5-column grid so the columns line up across rows regardless of
 * title length: [N | title | duration | state badge | actions].
 * The track number column is fixed-width with tabular numerals so
 * "1" and "16" right-align. The actions cell is `auto`, hugging
 * its content; duration + badge are `auto` too. Title is the only
 * `1fr` so long titles ellipsize rather than push the row sideways. */

ol.track-list,
ul.track-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.track-row {
  display: grid;
  /* Columns: [N | title | duration | format-chip | state-badge | actions]
   * The two `auto` chips (format + state) sit between duration and
   * actions; either can be element.none() and the grid collapses the
   * empty cell cleanly. */
  grid-template-columns: 2.5rem 1fr auto auto auto auto;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0.6rem;
  border-bottom: 1px solid var(--border);
}
.track-row:last-child { border-bottom: 0; }
.track-row:hover {
  background: var(--surface-2);
}

/* Available tracks are the whole-row click target — there's no
 * dedicated Play button anymore, so we advertise the affordance
 * with cursor:pointer and a slightly heavier hover. Nested
 * action buttons stop propagation so Like/Retry don't trip the
 * row click. Keyboard parity via the `role="button"` +
 * tabindex="0" attrs set in the view; focus ring matches the
 * project's standard outline so it works on e-ink too. */
.track-row-clickable {
  cursor: pointer;
}
.track-row-clickable:hover {
  background: var(--surface-2);
}
.track-row-clickable:focus-visible {
  outline: 1.5px solid var(--ink);
  outline-offset: -1.5px;
}

.track-row .track-no {
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-family: var(--font-mono);
  font-size: 0.85rem;
}
.track-row .track-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.track-row .track-duration {
  font-variant-numeric: tabular-nums;
  font-family: var(--font-mono);
  font-size: 0.85rem;
}
.track-actions {
  display: flex;
  align-items: center;
  gap: 0.35rem;
}

/* Failed/missing tracks: dim the row so available tracks visually
 * dominate. Keep the Retry button at full contrast (the action
 * lives inside .track-actions which we explicitly un-dim). */
.track-row-failed {
  opacity: 0.7;
}
.track-row-failed .track-actions {
  opacity: 1;
}

/* ── Format chips (file-type summary) ────────────────────────────────
 *
 * Used in two places: the album-header chip row (album-level summary,
 * "FLAC + MP3" etc.) and inline per-track in the album view. Riding
 * the existing `.badge` base so border-weight + casing stay
 * consistent with state badges. Sized down slightly so the per-track
 * chip doesn't dominate the row next to the state badge. */
.album-formats-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin-top: 0.3rem;
}
.format-chip {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  padding: 0.05rem 0.4rem;
  /* Quieter border than the bold .badge default so the chip
   * reads as informational rather than alarming. */
  border-color: var(--border-strong);
  color: var(--ink-soft);
  text-transform: uppercase;
}
/* In the track row the chip sits next to the state badge; tighten
 * the per-track variant a touch so the two together don't crowd the
 * actions column. */
.track-row .format-chip {
  font-size: 0.65rem;
  padding: 0.05rem 0.35rem;
}

/* ── Liked list (dedicated layout, NOT track-row) ──────────────────
 *
 * The Liked panel is its own visual language: small album-art
 * thumbnail on the left, a two-line title/artist column, an
 * Unlike control on the right. Deliberately not riding the
 * `.track-row` grid (that grid is sized for the album-detail
 * panel where state badges + format chips + per-row actions
 * occupy multiple columns; reusing it here squeezed the cover
 * down to a sliver and pushed a phantom Play button into the
 * wrong cell).
 *
 * Title links to the album. Per-row playback affordance is
 * intentionally absent — open the album to play. */
.liked-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.liked-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0.6rem;
  border-bottom: 1px solid var(--border);
}
.liked-row:last-child { border-bottom: 0; }
.liked-row:hover {
  background: var(--surface-2);
}

/* Fixed thumbnail size; the inner .cover is `width:100%` so it
 * conforms. The wrapper is `flex: 0 0 3rem` so it never shrinks
 * even on very long titles. */
.liked-row .liked-thumb {
  flex: 0 0 3rem;
  width: 3rem;
  display: block;
  /* Anchor element wraps the cover; reset link styling so it
   * doesn't add underline / colour to the image itself. */
  text-decoration: none;
  color: inherit;
}
.liked-row .liked-thumb .cover {
  width: 100%;
}
.liked-row .liked-meta {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}
.liked-row .liked-title {
  font-weight: 500;
  color: inherit;
  text-decoration: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.liked-row .liked-title:hover {
  text-decoration: underline;
}
.liked-row .liked-artist {
  font-size: 0.85rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── Transport bar (fixed bottom strip) ─────────────────────────── */

.transport-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 1rem;
  background: var(--surface);
  border-top: 1.5px solid var(--border-strong);
  z-index: 20;
}
.transport-bar .cover {
  width: 2.5rem;
  height: 2.5rem;
  aspect-ratio: 1 / 1;
  flex: 0 0 auto;
}
.transport-bar .tb-meta {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}
.transport-bar .tb-title {
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Now Playing route: oversized cover + meta column. */
.now-playing {
  display: flex;
  gap: 1.5rem;
  align-items: flex-start;
  flex-wrap: wrap;
}
.now-playing .cover {
  width: 18rem;
  max-width: 60vw;
  flex: 0 0 auto;
}
.now-playing .np-meta {
  flex: 1 1 16rem;
  min-width: 0;
}

/* Search-card cover sits above the meta block. The shared `.cover`
 * rule already squares it; this just nudges spacing inside the
 * card so the cover doesn't crowd the title. */
.album-card > .cover + .album-meta {
  margin-top: 0.25rem;
}

/* ── Mobile: collapse track-row actions onto a second line ─────── */
@media (max-width: 640px) {
  .album-header {
    gap: 0.75rem;
  }
  .album-header .cover {
    width: 8rem;
  }
  .album-header h2.album-title {
    font-size: 1.2rem;
  }
  .track-row {
    /* Stack: [N + title row] / [duration + badge + actions]. The
     * grid collapses to a two-row layout where the title spans
     * the full row and the secondary metadata wraps underneath. */
    grid-template-columns: 2rem 1fr auto;
    grid-template-areas:
      "no title actions"
      ".  meta  meta";
    row-gap: 0.25rem;
  }
  .track-row .track-no { grid-area: no; }
  .track-row .track-title { grid-area: title; }
  .track-row .track-duration {
    grid-area: meta;
    justify-self: start;
  }
  .track-row .badge {
    grid-area: meta;
    justify-self: start;
    margin-left: 4rem;
  }
  .track-actions { grid-area: actions; }
}
