/* Art Deco theme — black & silver, in the spirit of Poirot's title cards.
   Dependency-free: no web fonts or images, just CSS. */
:root {
  --bg: #0d0f12;          /* near-black, cool */
  --panel: #14181d;       /* dark lacquer */
  --panel-2: #1b2026;     /* raised surface */
  --text: #e8ecf0;        /* cool white */
  --muted: #8a939d;       /* steel grey */
  --accent: #aab8c6;      /* brushed silver */
  --accent-hi: #d6dee6;   /* polished chrome (hover) */
  --accent-soft: rgba(170, 184, 198, 0.34);   /* hairline rules */
  --accent-softer: rgba(170, 184, 198, 0.13); /* fills */
  --danger: #cf5b46;      /* warm pop against the cool palette */
  --ok: #8fb3ad;          /* muted sea-green */
  --warn: #d4a44c;        /* amber: needs a look, nothing lost yet */
  /* Each Metropolis app has a colour of its own on the logo and title, so the
     tabs of a browser tell them apart at a glance: Home gold, Docs jade, Papers
     steel blue. Media is the silver the whole family's palette starts from. It
     must match the fill in logo.svg, which an <img> cannot take from here. */
  --app: #aab8c6;
  --display: "Century Gothic", "Futura", "Twentieth Century",
             "Avenir Next", "Trebuchet MS", system-ui, sans-serif;
}

* {
  box-sizing: border-box;
  /* iOS: never flash the grey tap box on any element (buttons and clickable
     containers like .album-head alike). Inherited in WebKit, but set on every
     element so non-button click targets are covered too. */
  -webkit-tap-highlight-color: transparent;
}

body {
  margin: 0;
  font-family: "Avenir Next", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif;
  background: var(--bg);
  color: var(--text);
  /* never scroll the page sideways; `clip` (not `hidden`) so the body doesn't
     become a scroll container, which would break the sticky tab/filter bars. */
  overflow-x: clip;
  /* App chrome, not a document: tapping a control on iOS shouldn't select the
     text around it or pop the selection callout. Form fields opt back in below. */
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
/* Keep real text entry selectable. */
input, textarea, [contenteditable] {
  -webkit-user-select: text;
  user-select: text;
}
/* Reserve room for the fixed audio bar so it never covers the last list item;
   only while it's showing, to avoid dead space when nothing is playing. */
body.audio-playing { padding-bottom: calc(64px + env(safe-area-inset-bottom, 0px)); }

header {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 1rem 1.5rem;
  background: var(--panel);
  border-bottom: 2px solid var(--accent);
}

header .logo {
  height: 2.1rem;
  width: auto;
  display: block;
  flex: 0 0 auto;
}

header h1 {
  font-family: var(--display);
  font-size: 1.35rem;
  font-weight: 400;
  margin: 0;
  text-transform: uppercase;
  letter-spacing: 0.32em;
  color: var(--app);
}
/* Deco diamond flourishes around the wordmark. */
header h1::before,
header h1::after {
  content: "\25C6";
  font-size: 0.55em;
  vertical-align: 0.2em;
  color: var(--app);
  opacity: 0.75;
}
header h1::before { margin-right: 0.6em; }
header h1::after { margin-left: 0.6em; }

.status { color: var(--muted); font-size: 0.85rem; letter-spacing: 0.04em; }
.status.bad { color: var(--danger); }
.status.good { color: var(--ok); }

main { padding: 1.5rem; }

.tabs {
  display: flex;
  gap: 0.25rem;
  padding: 0 1.5rem;
  background: var(--panel);
  border-bottom: 1px solid var(--accent-soft);
  /* Keep the tab nav in reach on long lists: pin it to the top once the title
     scrolls past. Sits below the modal overlays (z 10) but above page content;
     the filter bar (z 4) slides under it. */
  position: sticky;
  top: 0;
  z-index: 5;
  /* If the tabs don't fit (narrow phones), scroll the bar itself rather than
     widening the whole page. */
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;       /* Firefox */
}
.tabs::-webkit-scrollbar { display: none; }  /* WebKit */
.tab {
  background: transparent;
  border: none;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  color: var(--muted);
  font-family: var(--display);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  padding: 0.75rem 1rem;
  white-space: nowrap;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
}

@media (max-width: 520px) {
  /* Trim the outer gutters on a phone so the video panel nearly fills the
     screen width instead of sitting in a narrow column. */
  main { padding: 0.75rem 0.5rem; }
  main .panel { padding: 0.85rem 0.7rem; }
  /* A phone has no room to spend on a title bar. The wordmark is set in caps
     with a third of an em of tracking, which is handsome at a desk and, on a
     390px screen, a banner across the top of a screen that has none to give.
     Here it is a label rather than a banner, and forbidden from wrapping: a
     name that has to break is a name too big for where it is. The same sizes
     as Docs, Papers and Home, so the four read as one family on a phone too. */
  header { padding: 0.4rem 0.75rem; gap: 0.55rem; }
  header .logo { height: 1.3rem; }
  header h1 { font-size: 0.75rem; letter-spacing: 0.16em; white-space: nowrap; }
  header h1::before { margin-right: 0.35em; }
  header h1::after  { margin-left: 0.35em; }
  /* The chip shrinks with it, and a long name gives way rather than pushing:
     it is the one part of the row whose width nobody here chose. */
  .user-menu-btn { font-size: 0.78rem; padding: 0.28rem 0.7rem; }
  .user-name {
    display: block; max-width: 5.5rem;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .tabs { padding: 0 0.75rem; }
  .tab { padding: 0.4rem 0.55rem; letter-spacing: 0.06em; }
  /* Stack the watch-state filter below the subscription picker so the row
     doesn't overflow and scroll horizontally on a phone. */
  .sub-picker { flex-wrap: wrap; }
  .sub-picker label:last-child { flex-basis: 100%; }
  .sub-picker select { width: 100%; max-width: 100%; }
  /* In the compact filter row, put each select to the right of its label
     text instead of stacking it below, one filter per line. */
  .sub-picker.compact label { flex: 1 1 100%; flex-direction: row; align-items: center; gap: 0.5rem; }
  .sub-picker.compact label select { flex: 1; width: auto; }
  /* The date-window inputs keep their natural width on desktop, but on a phone
     let them share a row and the Clear button take its own line, so nothing
     overlaps. */
  /* Two filters per row: Title + Director on one line, from + to on the next. */
  .sub-picker label:has(> input[type="search"]) { flex: 1 1 40%; }
  #tab-video .sub-picker label:has(> input[type="date"]) { flex: 1 1 40%; min-width: 0; flex-direction: row; align-items: center; gap: 0.5rem; }
  .sub-picker label:has(> input[type="date"]) input { flex: 1; width: auto; min-width: 0; }
  .sub-picker button { flex: 0 0 100%; }
}

/* Below this the wordmark is the thing that will not fit: the ornaments go
   first, then the tracking, because a diamond either side is decoration and
   the name is not. */
@media (max-width: 380px) {
  header h1 { font-size: 0.68rem; letter-spacing: 0.1em; }
  header h1::before, header h1::after { content: none; }
  .user-name { max-width: 4.5rem; }
}
.tab:hover { color: var(--text); filter: none; }
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }
.tab-icon { display: block; }  /* gear icon inherits the tab's color */

.tab-panel { display: flex; flex-direction: column; gap: 1.5rem; }

.banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin: 0 1.5rem;
  margin-top: 1.5rem;
  padding: 0.75rem 1rem;
  border-radius: 2px;
  background: rgba(196, 84, 60, 0.14);
  border: 1px solid var(--danger);
  color: var(--text);
  font-size: 0.9rem;
}
.banner button { background: var(--danger); border-color: var(--danger); color: #11151a; }
.banner .banner-actions { display: flex; gap: 0.5rem; }
.banner button.ghost { background: transparent; border-color: var(--accent-soft); color: var(--text); }
/* Amber for a storage warning; red (the default) is kept for real trouble. */
.banner.warn { background: rgba(212, 164, 76, 0.13); border-color: var(--warn); }
.banner.warn button:not(.ghost) { background: var(--warn); border-color: var(--warn); }

/* Storage (Settings tab): the issues first, then one block per ZFS pool. */
.badge.ok { color: var(--ok); background: rgba(143, 179, 173, 0.14); }
.badge.warn { color: var(--warn); background: rgba(212, 164, 76, 0.14); }
.badge.critical { color: var(--danger); background: rgba(196, 84, 60, 0.16); }
.storage-issues { margin: 0 0 0.75rem; padding-left: 1.2rem; font-size: 0.9rem; }
.storage-issues li.warn { color: var(--warn); }
.storage-issues li.critical { color: var(--danger); }
.storage-ok { color: var(--ok); margin: 0 0 0.75rem; font-size: 0.9rem; }
.pool {
  border-top: 1px solid var(--accent-soft);
  padding: 0.85rem 0 0.4rem;
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
}
.pool-head { display: flex; align-items: center; flex-wrap: wrap; gap: 0.4rem 0.6rem; }
.pool-name { font-family: var(--display); text-transform: uppercase; letter-spacing: 0.12em; }
/* Scoped under .pool to outrank .meta's break-all, which comes later and
   would split ordinary words in these sentences mid-word on a phone. */
.pool .pool-usage { margin-left: auto; word-break: normal; }
.pool-bar { height: 6px; background: var(--accent-softer); border-radius: 2px; overflow: hidden; }
.pool-bar span { display: block; height: 100%; background: var(--accent); }
.pool-bar span.warn { background: var(--warn); }
.pool-bar span.critical { background: var(--danger); }
.pool .pool-text { margin: 0; white-space: pre-line; word-break: normal; overflow-wrap: anywhere; }
.pool-sub {
  margin: 0.35rem 0 0;
  color: var(--muted);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
}
.pool-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.3rem; font-size: 0.85rem; }
.pool-list li { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.15rem 0.6rem; }
.pool-list .pool-item { overflow-wrap: anywhere; }
.pool-list .badge { font-size: 0.62rem; }

.subs-grid {
  display: grid;
  grid-template-columns: 360px 1fr;
  gap: 1.5rem;
  align-items: start;
}

.subs-left {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

@media (max-width: 800px) {
  .subs-grid { grid-template-columns: 1fr; }
}

.panel {
  background: var(--panel);
  border: 1px solid var(--accent-soft);
  border-radius: 3px;
  padding: 1rem 1.25rem;
}

.panel h2 {
  font-family: var(--display);
  font-size: 0.95rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--accent);
  margin: 0 0 0.85rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--accent-soft);
  display: flex;
  justify-content: space-between;
  align-items: center;
}

form { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem; }
form label { display: flex; flex-direction: column; gap: 0.2rem; font-size: 0.8rem; color: var(--muted); }
/* Checkbox rows lay out horizontally with a compact box, not full-width. */
form label.check { flex-direction: row; align-items: center; gap: 0.5rem; }
form label.check input { width: auto; flex: none; }
.field { display: flex; flex-direction: column; gap: 0.2rem; font-size: 0.8rem; color: var(--muted); margin-bottom: 1rem; }

input, select, button, textarea {
  font: inherit;
  padding: 0.5rem 0.6rem;
  border-radius: 2px;
  border: 1px solid var(--accent-soft);
  background: #0c0a06;
  color: var(--text);
}
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--accent);
}
input.invalid, input.invalid:focus { border-color: var(--danger); }
/* Inline hint shown when the add-episode number is already taken. */
.field-hint { font-size: 0.75rem; color: var(--danger); margin-top: 0.2rem; }
.field-hint.hidden { display: none; }

textarea {
  width: 100%;
  resize: vertical;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.8rem;
}

button {
  cursor: pointer;
  background: var(--accent);
  border: 1px solid var(--accent);
  color: #11151a;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  /* Stop iOS from flashing a grey box and selecting the glyph/label on tap. */
  -webkit-tap-highlight-color: transparent;
  -webkit-user-select: none;
  user-select: none;
}
button:hover { filter: none; background: var(--accent-hi); border-color: var(--accent-hi); }
button.ghost { background: transparent; border-color: var(--accent-soft); color: var(--accent); }
button.ghost:hover { background: transparent; border-color: var(--accent); color: var(--text); }
button.ghost.on { color: var(--ok); border-color: rgba(155, 176, 131, 0.6); }

.list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }

/* Trailing control for a windowed (paged) list. */
.show-more { display: flex; align-items: center; justify-content: center; gap: 0.75rem; flex-wrap: wrap; padding: 0.75rem 0 0.25rem; }

.list li {
  background: var(--panel-2);
  border: 1px solid var(--accent-softer);
  border-radius: 2px;
  padding: 0.6rem 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.row { display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.title { font-weight: 600; word-break: break-word; }
.title.link { cursor: pointer; }
.title.link:hover { color: var(--accent); }

/* Channel avatar (round, thin silver ring) next to a name / above the play list. */
.sub-icon {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  object-fit: cover;
  flex: none;
  background: var(--panel-2);
  border: 1px solid var(--accent-soft);
}
.sub-head { display: flex; align-items: center; gap: 0.5rem; }

/* Fixed "Other" bucket: a built-in icon (inline SVG) in the same round frame,
   and a left accent stripe so it reads as a distinct, pinned section. */
.other-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--accent);
  padding: 5px;
}
.other-icon svg { width: 100%; height: 100%; }
.list li.sub-other { border-left: 3px solid var(--accent); }
.sub-picker { display: flex; align-items: flex-end; gap: 0.6rem; margin-bottom: 0.6rem; }
.sub-picker label { flex: 1; }
/* A compact row sizes its controls to content and groups them at the left,
   rather than stretching each to an equal share of the width. */
.sub-picker.compact { justify-content: flex-start; }
.sub-picker.compact label { flex: 0 0 auto; }
.sub-picker input { width: 100%; }
/* The date-window inputs are narrow; don't let them grab equal width. */
.sub-picker label:has(> input[type="date"]) { flex: 0 0 auto; }
#sub-detail-controls .search-wrap { margin-bottom: 1rem; }

/* Search box with an inline clear (×) button. The wrap takes the layout slot;
   the input fills it and leaves room on the right for the button. */
.search-wrap { position: relative; display: block; }
.search-wrap input[type="search"] { width: 100%; margin-bottom: 0; padding-right: 2rem; }
/* Hide the browser's native clear button; we render our own. */
.search-wrap input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; }
.search-clear {
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  width: 2rem;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: none;
  color: var(--muted);
  font-size: 1.2rem;
  line-height: 1;
  cursor: pointer;
}
.search-clear:hover { color: var(--text); }
/* No text in the box: nothing to clear, so hide the button. */
.search-wrap.empty .search-clear { display: none; }
/* Search box + sort controls above the audiobook grid. */
.audiobook-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 1rem;
}
.audiobook-toolbar .search-wrap { flex: 1 1 12rem; min-width: 0; margin-bottom: 0; }
.audiobook-sort {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  white-space: nowrap;
  color: var(--muted);
  font-size: 0.85rem;
}
#audiobook-sort-dir { flex: 0 0 auto; }

/* Compact icon toggles (watched state, sort order) below the filter form. */
.filter-icons {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  align-items: center;
  /* Pin the filter controls right under the sticky tab bar. --tab-bar-h is the
     tab bar's live height, published from app.js so the offset tracks the
     responsive breakpoint and late font loads (fallback covers first paint). */
  position: sticky;
  top: var(--tab-bar-h, 44px);
  z-index: 4;
  /* Opaque, so list rows scroll out of sight beneath the bar (the list spans
     the same content width, so no horizontal bleed is needed). */
  background: var(--panel);
  padding: 0.55rem 0;
  margin-bottom: 0.8rem;
  border-bottom: 1px solid var(--accent-soft);
}
/* Permanent search box on the Audio tab spans its own row, with the toggle
   buttons wrapping onto the line below it. */
.filter-icons .search-wrap { flex: 1 1 100%; min-width: 0; margin-bottom: 0; }
/* Series mode: the serie + season dropdowns sit inline with the filter icons. */
.serie-picker { display: inline-flex; gap: 0.5rem; align-items: center; min-width: 0; }
#videos-serie-chip { max-width: 16rem; }
.icon-toggle {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border-radius: 2px;
  background: transparent;
  border: 1px solid var(--accent-soft);
  color: var(--muted);
}
.icon-toggle:hover { background: transparent; border-color: var(--accent); color: var(--text); }
/* An active (non-default) toggle. Hue alone can't carry "on" in this monochrome
   palette (silver-on-black reads much like the grey-on-black off state, and like
   hover), so the active state gets a filled background plus chrome-bright glyph —
   the fill is what distinguishes it at a glance. */
.icon-toggle.on {
  color: var(--accent-hi);
  border-color: var(--accent);
  background: var(--accent-soft);
}
.icon-toggle svg { display: block; width: 20px; height: 20px; }

/* Custom filter dropdowns (channel / serie / season): a chip showing the
   current selection's icon + name acts as the trigger, opening an anchored menu
   of icon+name rows. Used where a native <select> can't render the avatars. */
.dd { position: relative; display: inline-flex; min-width: 0; }
.dd-chip {
  flex: 0 1 auto;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  height: 34px;
  padding: 0 0.5rem 0 0.25rem;
  border-radius: 2px;
  background: transparent;
  border: 1px solid var(--accent-soft);
  color: var(--text);
  min-width: 0;
  text-transform: none;
  letter-spacing: normal;
  font-weight: 400;
}
.dd-chip:hover { background: transparent; border-color: var(--accent); }
.dd-chip[aria-expanded="true"] { border-color: var(--accent); }
.dd-chip img { width: 26px; height: 26px; }
.dd-chip > span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.85rem;
}
.dd-chip .dd-caret { flex: none; font-size: 0.7rem; color: var(--muted); }
.dd-chip[aria-expanded="true"] .dd-caret { color: var(--accent); }

/* The anchored menu: a scrollable list of rows dropping below the chip. */
.dd-menu {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  min-width: 100%;
  max-width: 18rem;
  max-height: 60vh;
  overflow-y: auto;
  padding: 0.3rem;
  background: var(--panel-2);
  border: 1px solid var(--accent-soft);
  border-radius: 2px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4);
}
.dd-menu.hidden { display: none; }
.dd-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  padding: 0.35rem 0.5rem;
  background: transparent;
  border: 1px solid var(--accent-soft);
  border-radius: 2px;
  color: var(--text);
  text-align: left;
  text-transform: none;
  letter-spacing: normal;
  font-weight: 400;
}
.dd-row:hover { border-color: var(--accent); }
.dd-row.on { border-color: var(--accent); color: var(--accent-hi); background: var(--accent-softer); }
.dd-row img { width: 26px; height: 26px; flex: none; }
.dd-row span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Option popup for the cycling filter toggles (long press / right click). Reuses
   .dd-row for items but is viewport-anchored (position set inline in JS). */
.option-menu {
  position: fixed;
  z-index: 30;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  max-width: 16rem;
  max-height: 70vh;
  overflow-y: auto;
  padding: 0.3rem;
  background: var(--panel-2);
  border: 1px solid var(--accent-soft);
  border-radius: 2px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4);
}
.option-menu.hidden { display: none; }
.option-menu .dd-row { text-transform: capitalize; }
.option-menu .opt-icon { display: inline-flex; flex: none; color: var(--muted); }
.option-menu .opt-icon svg { display: block; width: 20px; height: 20px; }
.option-menu .dd-row.on .opt-icon { color: var(--accent); }

/* Dialog with stacked input fields (e.g. the tag and user-edit overlays). */
.search-fields { display: flex; flex-direction: column; gap: 0.75rem; margin-bottom: 1rem; }
.search-fields input { width: 100%; }

/* Video rows with a thumbnail on the left. */
.list li.vid { flex-direction: row; align-items: flex-start; gap: 0.6rem; }
.thumb {
  width: 140px;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: 2px;
  flex: none;
  background: #000;
  border: 1px solid var(--accent-soft);
}
.vid-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.25rem; }

/* Thumbnail wrapper carrying a "watched" progress bar along its bottom edge. */
.thumb-wrap { position: relative; width: 140px; flex: none; line-height: 0; }
.thumb-wrap .thumb { width: 100%; display: block; }
.progress {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 4px;
  background: rgba(0, 0, 0, 0.5);
  border-radius: 0 0 2px 2px;
  overflow: hidden;
}
.progress-bar { display: block; height: 100%; background: var(--accent); }

/* Click-to-play thumbnails: a play triangle fades in over the cover on hover. */
.thumb-wrap.playable { cursor: pointer; }
.thumb-play {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%) scale(0.85);
  width: 44px; height: 44px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  opacity: 0;
  transition: opacity 0.12s, transform 0.12s;
  pointer-events: none;
}
.thumb-play svg { width: 22px; height: 22px; margin-left: 2px; }
.thumb-wrap.playable:hover .thumb-play {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}

@media (max-width: 520px) {
  .list li.vid { flex-direction: column; }
  .thumb, .thumb-wrap { width: 100%; }
}

.list li.selected {
  border-color: var(--accent);
  background: var(--accent-softer);
}
.meta { color: var(--muted); font-size: 0.8rem; word-break: break-all; }

.badge {
  font-size: 0.7rem;
  padding: 0.12rem 0.5rem;
  border-radius: 2px;
  border: 1px solid currentColor;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  white-space: nowrap;
}
.badge.new { color: var(--accent); background: rgba(201, 162, 39, 0.12); }
.badge.downloaded { color: var(--ok); background: rgba(155, 176, 131, 0.14); }
.badge.pending { color: var(--muted); background: rgba(171, 156, 119, 0.14); }
.badge.downloading { color: var(--accent-hi); background: rgba(201, 162, 39, 0.16); }
.badge.error { color: var(--danger); background: rgba(196, 84, 60, 0.16); }
.badge.deleted { color: var(--muted); background: rgba(171, 156, 119, 0.10); }
.badge.members { color: #c79a4b; background: rgba(199, 154, 75, 0.15); }
.badge.unavailable { color: var(--danger); background: rgba(196, 84, 60, 0.10); }

/* Download queue (Settings tab): numbered list in planned download order. */
.queue { margin: 0 0 0.5rem; padding-left: 1.6rem; display: flex; flex-direction: column; gap: 0.4rem; }
.queue li { padding-left: 0.2rem; }
.queue li::marker { color: var(--muted); font-size: 0.8rem; }
.queue .qtitle { display: block; }
.queue li.downloading .qtitle { color: var(--accent); }
.queue li.downloading::marker { content: "▶ "; color: var(--accent); }

.head-actions { display: flex; gap: 0.5rem; align-items: center; }
.head-actions button { padding: 0.3rem 0.55rem; font-size: 0.8rem; }

.actions { display: flex; gap: 0.4rem; flex-wrap: wrap; }
.actions.wrap { flex-wrap: wrap; }
.actions button, .actions select { padding: 0.3rem 0.55rem; font-size: 0.8rem; }
.ghost.danger { color: var(--danger); border-color: rgba(196, 84, 60, 0.6); }
.ghost.danger:hover { background: rgba(196, 84, 60, 0.14); color: var(--danger); border-color: var(--danger); }

.overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.85);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  z-index: 10;
}
.overlay.hidden { display: none; }

/* The shared confirm/alert dialog is always a response to something already on
   screen, so it has to outrank every other layer: the audio bar (50), the photo
   viewer (20) and any other .overlay. Without this it keeps its .overlay z-index
   of 10 and, sitting earlier in the DOM than the later overlays, gets painted
   underneath the very thing that opened it — an invisible dialog that silently
   swallows clicks. */
#dialog-overlay { z-index: 100; }
.hidden { display: none; }

.player-box {
  background: var(--panel);
  border: 1px solid var(--accent);
  border-radius: 3px;
  padding: 1rem;
  width: min(960px, 100%);
  position: relative;
}
.player-box h3 { margin: 0; }
.player-box video { width: 100%; border-radius: 2px; background: #000; }

/* Video description under the player: preserve the source line breaks/links
   layout, but cap the height so a long description doesn't push the controls
   off-screen. */
.player-desc {
  margin: 0.75rem 0 0;
  color: var(--muted);
  font-size: 0.85rem;
  line-height: 1.4;
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 9rem;
  overflow-y: auto;
}

.player-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  margin: 0 2rem 0.75rem 0;
}
/* Give the title a width floor so the trailing controls (cast status, cast
   button, speed pills) wrap to a second line on narrow screens instead of
   crushing the h3 to a sliver — otherwise word-break wraps it one letter per
   line into a tall thin column (notably while casting on a phone, when the
   status text and cast button are both present). */
.player-head h3 { flex: 1 1 10rem; min-width: 10rem; word-break: break-word; }

/* Cast button; shown/hidden from JS by the live CastContext state (see app.js).
   .hidden (display:none) handles "no devices"; .connected highlights an active
   cast session. */
.cast-btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  width: 28px;
  height: 28px;
  background: transparent;
  border: none;
  cursor: pointer;
  color: var(--text);
}
.cast-btn:hover { color: var(--accent); }
.cast-btn.connected { color: var(--accent); }
/* .cast-btn sets display, so it would otherwise beat the .hidden utility
   (same specificity, declared later) and the button could never be hidden —
   e.g. it would show inertly on AirPlay-only browsers. Win explicitly. */
.cast-btn.hidden { display: none; }

/* Short cast status line ("Connecting…", "Casting", or a load error) shown
   next to the player title. Empty -> no box, so it's invisible when idle. */
.cast-status {
  flex: none;
  font-size: 0.75rem;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 40%;
}
.cast-status:empty { display: none; }

/* Playback-speed buttons next to the player title. A compact pill group; the
   active rate is highlighted. */
.speed-control {
  flex: none;
  display: inline-flex;
  gap: 2px;
}
.speed-btn {
  padding: 2px 7px;
  font-size: 0.75rem;
  font-weight: 600;
  background: transparent;
  border: 1px solid var(--accent-soft);
  color: var(--muted);
  cursor: pointer;
  border-radius: 3px;
}
/* Override the base button:hover fill so an inactive button stays outlined. */
.speed-btn:hover { background: transparent; color: var(--text); border-color: var(--accent); }
/* Declared after :hover so the active rate stays filled even while hovered. */
.speed-btn.is-active {
  color: #11151a;
  background: var(--accent);
  border-color: var(--accent);
}

/* Cast control card: shown in place of the <video> while casting. Mirrors the
   video box's aspect/feel (dark, full width) and centers a "Casting to …"
   label over its own play/pause + scrubber, all driven by the RemotePlayer. */
.cast-panel {
  width: 100%;
  aspect-ratio: 16 / 9;
  background: #000;
  border-radius: 2px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  padding: 1rem;
  box-sizing: border-box;
}
.cast-panel.hidden { display: none; }
.cast-panel-icon { color: var(--accent); }
.cast-panel-label {
  color: var(--text);
  font-size: 0.95rem;
}
.cast-panel-label span { color: var(--accent); font-weight: 600; }
.cast-controls {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: min(560px, 100%);
}
.cast-playpause {
  flex: none;
  width: 36px;
  height: 36px;
  font-size: 1rem;
  line-height: 1;
  background: transparent;
  border: 1px solid var(--accent-soft);
  color: var(--text);
  border-radius: 50%;
  cursor: pointer;
}
.cast-playpause:hover { border-color: var(--accent); color: var(--accent); background: transparent; }
.cast-seek { flex: 1; min-width: 0; cursor: pointer; accent-color: var(--accent); }
.cast-time {
  flex: none;
  font-size: 0.8rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.close {
  position: absolute;
  top: 0.5rem;
  right: 0.75rem;
  background: transparent;
  border: none;
  color: var(--muted);
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
}
.close:hover { color: var(--accent); }

.empty { color: var(--muted); font-size: 0.85rem; padding: 0.5rem 0; }

.confirm-box {
  background: var(--panel);
  border: 1px solid var(--accent);
  border-radius: 3px;
  padding: 1.25rem;
  width: min(440px, 100%);
}
.confirm-box h3 {
  margin: 0 0 0.5rem;
  font-family: var(--display);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--accent);
  font-weight: 500;
}
/* .meta breaks anywhere so long paths wrap mid-token; a dialog message is
   running prose, where that reads as "give it a ne / w one". overflow-wrap
   still breaks a long filename that genuinely cannot fit. */
.confirm-box .meta {
  display: block;
  margin-bottom: 1rem;
  line-height: 1.4;
  word-break: normal;
  overflow-wrap: break-word;
}
/* Names the track being added in the add-to-playlist picker. */
.dialog-sub {
  margin: -0.25rem 0 1rem;
  color: var(--text);
  font-weight: 600;
  overflow-wrap: anywhere;
}
.confirm-actions { display: flex; justify-content: flex-end; gap: 0.5rem; flex-wrap: wrap; }
#confirm-delete { background: var(--danger); border-color: var(--danger); color: #11151a; }
#confirm-delete:hover { background: var(--danger); filter: brightness(1.12); }

.import-path-row { display: flex; gap: 0.5rem; }
.import-path-row input { flex: 1; min-width: 0; }

/* Season/Episode number inputs sit side by side (add-episode + episode Info). */
.episode-num-row { display: flex; gap: 0.5rem; }
.episode-num-row label { flex: 1; min-width: 0; }

.group { margin-bottom: 1.25rem; }
.group:last-child { margin-bottom: 0; }
.group-title {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--display);
  font-size: 0.9rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  margin: 0 0 0.5rem;
  padding-bottom: 0.3rem;
  border-bottom: 1px solid var(--accent-soft);
  color: var(--accent);
}
.group-title .count {
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--accent-soft);
  border-radius: 2px;
  padding: 0.05rem 0.5rem;
  font-size: 0.72rem;
  letter-spacing: 0.05em;
}

/* --- Tags ------------------------------------------------------------------ */

/* Read-only tag chips shown on video cards and under the player. */
.tag-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin: 0.25rem 0 0.1rem;
}
.actions .tag-chips {
  margin: 0;
  align-items: center;
  min-width: 0;
}
.tag-chip {
  --notch: 7px;
  --edge: var(--accent-soft);
  --fill: var(--accent-softer);
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.05rem 0.65rem;
  font-size: 0.72rem;
  letter-spacing: 0.03em;
  color: var(--text);
  background: transparent;
  border: none;
  border-radius: 0;
  white-space: nowrap;
  clip-path: polygon(0 50%, var(--notch) 0, calc(100% - var(--notch)) 0,
                     100% 50%, calc(100% - var(--notch)) 100%, var(--notch) 100%);
}
.tag-chip::before, .tag-chip::after {
  content: ""; position: absolute; inset: 0; z-index: -1; clip-path: inherit;
}
.tag-chip::before { background: var(--edge); }
.tag-chip::after { inset: 1px; background: var(--fill); }
.tag-chip.auto { --edge: var(--accent-soft); color: var(--muted); }
/* A manually-removed tag: visible but struck through and dimmed. */
.tag-chip.removed {
  color: var(--muted);
  text-decoration: line-through;
  opacity: 0.6;
}
.tag-chip.removed .tag-dot { opacity: 0.5; }
.meta.removed { font-style: italic; text-decoration: line-through; }
.tag-dot {
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 50%;
  flex: none;
}

/* The 🏷 button on cards / player. */
.tag-btn, .info-btn {
  padding: 0.1rem 0.4rem;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  color: var(--muted);
  border-color: var(--accent-soft);
}
.tag-btn:hover, .info-btn:hover { color: var(--text); border-color: var(--accent); }
.tag-btn svg, .info-btn svg { width: 1.05em; height: 1.05em; }

/* Info dialog definition list. */
.info-list {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.35rem 0.9rem;
  margin: 0.25rem 0 0;
}
.info-list dt { color: var(--muted); font-size: 0.8rem; }
.info-list dd { margin: 0; }

/* Tri-state tag filter on the Videos tab. */
.tag-filter {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem;
  margin: 0.5rem 0 0.25rem;
}
.tag-filter-label { color: var(--muted); font-size: 0.8rem; }
button.tag-chip.filter {
  cursor: pointer;
  --fill: var(--panel);
}
button.tag-chip.filter.include {
  --edge: var(--accent);
  --fill: var(--accent-softer);
  color: var(--accent-hi);
}
button.tag-chip.filter.exclude {
  --edge: var(--danger);
  color: var(--danger);
  text-decoration: line-through;
}
.tag-match-toggle { font-size: 0.72rem; padding: 0.05rem 0.5rem; }

/* Player tag row. */
.player-tags {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.3rem;
  margin-top: 0.4rem;
}
.player-tags .tag-chips { margin: 0; }

/* Player: offer to subscribe to a channel stored from a YouTube search. */
.player-channel {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.4rem;
}
.player-channel .meta { word-break: normal; }
.player-channel.hidden { display: none; }

/* Audio tab: YouTube songs, each with its sources and the album it is on. */
.music-results { gap: 0.8rem; }
.music-song { flex-direction: column; align-items: stretch; gap: 0.4rem; }
.music-song-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; }
.music-source {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
}
.music-thumb {
  width: 64px;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: 2px;
  flex: none;
  background: #000;
}
.music-source-body { flex: 1; min-width: 10rem; display: flex; flex-direction: column; gap: 0.15rem; }
.music-source .meta, .music-album .meta { word-break: normal; }
.badge.official { color: var(--ok); background: rgba(155, 176, 131, 0.14); }
.music-album {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  padding-top: 0.4rem;
  border-top: 1px solid var(--accent-soft);
}
.music-album .actions { margin-left: auto; }
.music-album-tracks {
  flex-basis: 100%;
  margin: 0.2rem 0 0;
  padding-left: 1.6rem;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  font-size: 0.85rem;
}
.music-album-tracks li { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: baseline; }
.music-album-tracks li.match .music-track-title { color: var(--accent-hi); font-weight: 600; }

/* Videos tab: YouTube search results, below the library's own. */
.yt-results { margin-top: 1.5rem; }
.yt-head {
  margin: 0 0 0.6rem;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--muted);
}

/* Settings: tag management. */
#tag-form { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }
#tag-form #tag-name { flex: 1; min-width: 12rem; }

/* Curated Art Deco colour palette (swatch buttons). */
.swatches { display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; }
.swatch {
  width: 1.25rem;
  height: 1.25rem;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--accent-soft);
  cursor: pointer;
}
.swatch:hover { border-color: var(--accent-hi); }
.swatch.selected {
  /* gold ring against the panel so the pick reads clearly */
  box-shadow: 0 0 0 2px var(--panel), 0 0 0 3px var(--accent-hi);
}
#tags-list { margin-top: 0.75rem; display: flex; flex-direction: column; gap: 0.4rem; }
.tag-admin {
  border: 1px solid var(--accent-soft);
  border-radius: 3px;
  background: var(--panel-2);
}
.tag-admin > summary {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.6rem;
  cursor: pointer;
  list-style: none;
}
.tag-admin > summary::-webkit-details-marker { display: none; }
.tag-admin-name { font-weight: 600; }
.tag-admin-body {
  padding: 0.6rem;
  border-top: 1px solid var(--accent-soft);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.tag-admin-row { display: flex; gap: 0.5rem; align-items: center; }
.tag-admin-row .tag-name-input { flex: 1; }
.tag-rules { display: flex; flex-direction: column; gap: 0.35rem; }
.rule-row { display: flex; gap: 0.5rem; align-items: center; }
.rule-row .rule-pattern { flex: 1; }
.rule-row .rule-lang-custom { width: 6rem; }
.rule-regex-lbl {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  color: var(--muted);
  font-size: 0.8rem;
  white-space: nowrap;
}
.rule-remove { padding: 0.1rem 0.45rem; line-height: 1; }
button.ghost.danger { color: var(--danger); border-color: rgba(207, 91, 70, 0.5); }
button.ghost.danger:hover { color: var(--danger); border-color: var(--danger); }

/* Manual tag picker modal. */
.tag-picker-list {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  margin: 0.5rem 0 0.75rem;
  max-height: 50vh;
  overflow: auto;
}
.tag-pick-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.3rem 0.25rem;
  cursor: pointer;
  border-radius: 2px;
}
.tag-pick-row:hover { background: var(--accent-softer); }
.tag-pick-name { flex: 1; }

/* Rescan progress bar in the Tags panel. */
.rescan-progress {
  width: 100%;
  height: 0.5rem;
  margin: 0.4rem 0 0.2rem;
  border: 1px solid var(--accent-soft);
  border-radius: 999px;
  background: var(--panel-2);
  padding: 0;
  -webkit-appearance: none;
  appearance: none;
  overflow: hidden;
}
.rescan-progress::-webkit-progress-bar { background: var(--panel-2); }
.rescan-progress::-webkit-progress-value { background: var(--accent); }
.rescan-progress::-moz-progress-bar { background: var(--accent); }

/* --- Log viewer (Settings tab) --- */
.log-content {
  margin: 0;
  padding: 0.6rem 0.75rem;
  background: #0c0a06;
  border: 1px solid var(--accent-soft);
  border-radius: 4px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.75rem;
  line-height: 1.5;
  white-space: pre;
  overflow: auto;
  max-height: 28rem;
  color: var(--text);
}

/* --- Audio tab: albums grouped with inline players --- */
.audio-controls { margin-bottom: 1rem; }
#album-list { display: flex; flex-direction: column; gap: 1rem; }
.album {
  border: 1px solid var(--accent-soft);
  border-radius: 8px;
  background: var(--panel-2);
  overflow: hidden;
}
.album-head { display: flex; gap: 0.7rem; align-items: center; padding: 0.7rem; }
@media (max-width: 520px) {
  .album-head { padding: 0.9rem 0.7rem; }
  .album-caret { font-size: 1.35rem; }
}
#album-list .album-head,
#sub-video-list .album-head { cursor: pointer; }
.album-info-form { display: flex; flex-direction: column; gap: 0.5rem; padding: 0 0.7rem 0.7rem; }
.album-info-form.hidden { display: none; }
.album-info-form label { display: flex; flex-direction: column; gap: 0.2rem; font-size: 0.85rem; color: var(--muted); }
.album-info-form label.check { flex-direction: row; align-items: center; gap: 0.4rem; }
.album-info-form input[type="text"], .album-info-form input[type="date"] { width: 100%; }
.album-info-actions { display: flex; gap: 0.5rem; margin-top: 0.3rem; }
.video-info-form { display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.5rem; }
.video-info-form.hidden { display: none; }
.video-info-form label { display: flex; flex-direction: column; gap: 0.2rem; font-size: 0.85rem; color: var(--muted); }
.video-info-form label.check { flex-direction: row; align-items: center; gap: 0.4rem; }
.video-info-form input[type="text"], .video-info-form select { width: 100%; }
.video-info-actions { display: flex; gap: 0.5rem; margin-top: 0.3rem; }
/* Per-video subtitle track manager inside the Info form. */
.subs-manager { display: flex; flex-direction: column; gap: 0.35rem; padding-top: 0.5rem; border-top: 1px solid var(--border); }
.subs-manager-label { font-size: 0.85rem; color: var(--muted); }
.subs-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.25rem; }
.subs-list li { display: flex; align-items: center; gap: 0.4rem; }
.subs-empty { font-size: 0.85rem; color: var(--muted); font-style: italic; }
.subs-lang-tag { font-size: 0.8rem; padding: 0.1rem 0.4rem; border-radius: 0.25rem; background: var(--surface-2, rgba(127,127,127,0.15)); text-transform: uppercase; letter-spacing: 0.02em; }
.subs-remove { flex: none; border: none; background: none; color: var(--muted); cursor: pointer; font-size: 0.9rem; line-height: 1; padding: 0.1rem 0.3rem; }
.subs-remove:hover { color: var(--danger, #c0392b); }
.subs-add { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; }
.subs-add .subs-lang { width: 4.5rem; flex: none; }
.subs-add .subs-file { flex: 1; min-width: 0; font-size: 0.8rem; }
.subs-msg { font-size: 0.8rem; color: var(--muted); min-height: 1em; }
.album-caret { flex: none; color: var(--muted); font-size: 1.25rem; transition: transform 0.15s; }
.album:not(.collapsed) .album-caret { transform: rotate(90deg); }
.album.collapsed .track-list { display: none; }
/* A song the search found, in an album opened on it; and the button that
   shows the album's other tracks. */
.track.match .track-title { color: var(--accent-hi); font-weight: 600; }
.album-more { list-style: none; }
.album-more .album-show-all { font-size: 0.8rem; padding: 0.15rem 0.6rem; }
.album-thumb {
  width: 72px; height: 54px; object-fit: cover;
  border-radius: 5px; flex: none; background: var(--bg);
}
.album-thumb-empty { background: var(--accent-softer); }
/* Album cover with a hover play-overlay, mirroring the video thumbnails. */
.album-thumb-wrap { position: relative; width: 72px; height: 54px; flex: none; }
.album-cover-play {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%) scale(0.85);
  width: 34px; height: 34px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: none; border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  color: #fff; cursor: pointer;
  opacity: 0;
  transition: opacity 0.12s, transform 0.12s;
}
.album-cover-play svg { width: 18px; height: 18px; margin-left: 2px; }
.album-thumb-wrap:hover .album-cover-play,
.album-cover-play:focus-visible {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}
.album-info h3 { margin: 0 0 0.2rem; font-size: 1rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.album-info .meta { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Artist and date render inline with a separator on wide screens. */
.album-info .meta-extra::before { content: " · "; }
.track-list { list-style: none; margin: 0; padding: 0 0.7rem 0.6rem; display: flex; flex-direction: column; gap: 0.4rem; }
.track { display: flex; align-items: center; gap: 0.5rem; padding-top: 0.4rem; border-top: 1px solid var(--accent-softer); }
.track-num { color: var(--muted); font-variant-numeric: tabular-nums; min-width: 1.4rem; text-align: right; }
/* Per-track album cover in playlists; small square so a row of them reads as a
   strip of covers without crowding the title. Clicking it plays the track, so
   it carries a border that lights up while its track is the one playing. */
.track-thumb {
  flex: none; width: 36px; height: 36px; border-radius: 0.3rem;
  object-fit: cover; background: var(--accent-softer); display: block;
  cursor: pointer; border: 2px solid transparent;
}
.track-thumb-empty { background: var(--accent-softer); }
.track-thumb:hover { border-color: var(--accent-soft); }
.track.playing .track-thumb { border-color: var(--accent); }
/* Album rows keep a play button; it takes the same accent highlight as the
   playlist cover while its track is playing. */
.track.playing .track-play {
  border-color: var(--accent); background: var(--accent-softer);
}
/* Title + its tag cluster (genre pill, duration/BPM/key). The cluster wraps
   under the title on narrow screens rather than squeezing the title to a sliver:
   the title can shrink (min-width:0) and wrap its words, while the fixed-width
   tags move to a second line when there's no room. */
.track-main {
  flex: 1 1 auto; min-width: 0;
  display: flex; flex-wrap: wrap; align-items: center;
  column-gap: 0.5rem; row-gap: 0.15rem;
}
/* The title is a play trigger. */
.track-title { flex: 1 1 auto; min-width: 0; word-break: break-word; cursor: pointer; }
.track-title:hover { text-decoration: underline; }
/* Meta line and secondary controls stay tucked away until the row's "⋯" is
   pressed (adds .expanded), keeping a resting row down to cover/play + title. */
.track-tags { flex: 0 0 auto; display: none; align-items: center; gap: 0.4rem; }
.track-extra { display: none; align-items: center; gap: 0.5rem; }
/* Expanded, the row stacks: the band/title line runs full width, the meta
   (genre · duration · BPM · key) drops onto its own line beneath it, and the
   controls sit on a third line, right-aligned. The cover/play and "⋯" stay on
   the top line (top-aligned), so "⋯" keeps its resting position. */
.track.expanded { flex-wrap: wrap; align-items: flex-start; }
.track.expanded .track-main { flex-direction: column; align-items: stretch; }
.track.expanded .track-tags { display: flex; }
.track.expanded .track-extra {
  display: flex; flex-basis: 100%; justify-content: flex-end;
}
.track-more {
  flex: none; width: 26px; height: 26px; padding: 0; border-radius: 50%;
  border: 1px solid transparent; background: none; color: var(--muted);
  cursor: pointer; font-size: 1rem; line-height: 1;
}
.track-more:hover { background: var(--accent-softer); color: var(--text); }
.track.expanded .track-more { color: var(--accent); }
/* Analyzed top-genre pill; the BPM/key meta stays on one line so
   "142 BPM · A minor" doesn't break mid-token (the global .meta allows break-all). */
.track-genre {
  font-size: 0.7rem; color: var(--muted);
  background: var(--accent-softer); border-radius: 0.6rem;
  padding: 0.05rem 0.45rem; white-space: nowrap;
}
.track .meta { white-space: nowrap; }
.track-play {
  flex: none; width: 28px; height: 28px; padding: 0; border-radius: 50%;
  border: 1px solid var(--accent-soft); background: var(--panel);
  color: var(--accent); cursor: pointer; font-size: 0.7rem; line-height: 1;
}
.track-play:hover { background: var(--accent-softer); }
/* Push the play button to the right of the album head and match the Videos-tab
   Play button (plain button, compact padding). */
.album-info { flex: 1 1 auto; min-width: 0; }
.album-play { flex: none; padding: 0.3rem 0.55rem; font-size: 0.8rem; }

/* --- Playlists -------------------------------------------------------------
   Composite cover: a 2x2 mosaic of up to 4 album covers, sized to match the
   album thumbnail so playlists and albums line up in the same grid. */
.playlist-cover {
  width: 72px; height: 54px; flex: none;
  border-radius: 5px; overflow: hidden;
  display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr;
  gap: 1px; background: var(--accent-softer);
}
.pl-tile { background-size: cover; background-position: center; background-color: var(--bg); }
.pl-tile.empty { background-color: var(--accent-softer); }
/* The cover sits inside an .album-thumb-wrap, so the play overlay is inherited. */

/* Album/playlist badge tucked before the title. */
.kind-badge {
  display: inline-flex; vertical-align: -2px; margin-right: 0.35rem;
  color: var(--muted);
}
.kind-badge svg { width: 14px; height: 14px; }

/* Owner controls in a playlist head and on its track rows. */
/* Wrap so the action cluster always stays within .album-info and never runs
   under the head's Play button on narrow (phone) widths. */
.pl-actions { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.25rem; max-width: 100%; }
.pl-actions .ghost { padding: 0.15rem 0.4rem; font-size: 0.75rem; }
/* Destructive row in the Edit popup. */
.option-menu .dd-row.danger { color: var(--danger, #c33); }
.track-add, .pl-up, .pl-down, .pl-remove, .track-edit {
  flex: none; width: 24px; height: 24px; padding: 0; border-radius: 50%;
  border: 1px solid var(--accent-soft); background: var(--panel);
  color: var(--accent); cursor: pointer; font-size: 0.8rem; line-height: 1;
}
.track-add:hover, .pl-up:hover, .pl-down:hover, .pl-remove:hover,
.track-edit:hover { background: var(--accent-softer); }
/* Inline track editor: artist + title inputs fill the row, with save/cancel. */
.track.editing { gap: 0.4rem; }
.track-edit-artist, .track-edit-title {
  min-width: 0; padding: 0.25rem 0.4rem; background: var(--bg);
  border: 1px solid var(--accent-soft); border-radius: 2px; color: var(--text);
}
.track-edit-artist { flex: 1 1 35%; }
.track-edit-title { flex: 1 1 65%; }
.track-edit-save, .track-edit-cancel { flex: none; }
.pl-up:disabled, .pl-down:disabled { opacity: 0.35; cursor: default; }
.pl-remove { color: var(--danger, #c33); }
.track.empty { border-top: none; }
/* Drag-to-reorder grip on owner playlist rows. touch-action:none lets a
   touch-drag that starts here reorder instead of scrolling the page. */
.pl-drag {
  flex: none; width: 24px; height: 24px; display: inline-flex;
  align-items: center; justify-content: center; color: var(--accent);
  cursor: grab; font-size: 0.95rem; line-height: 1;
  touch-action: none; user-select: none; -webkit-user-select: none;
}
.pl-drag:active { cursor: grabbing; }
.track.dragging { opacity: 0.55; background: var(--accent-softer); border-radius: 4px; }
/* MusicBrainz release-date lookup in the album Info form. */
.album-lookup { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.album-lookup-note { color: var(--muted); font-size: 0.85rem; }

/* Shared audio queue bar, pinned to the bottom of the viewport. */
.audio-bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 50;
  display: flex; flex-direction: column; gap: 0.4rem;
  padding: 0.5rem 0.9rem;
  background: var(--panel-2); border-top: 1px solid var(--accent-soft);
}
/* Keep the controls clear of the iPhone home-indicator strip and of the notch
   in landscape — a button sitting in the indicator strip swallows its taps.
   Separate declaration so browsers without env() keep the padding above. */
.audio-bar {
  padding-bottom: calc(0.5rem + env(safe-area-inset-bottom, 0px));
  padding-left: max(0.9rem, env(safe-area-inset-left, 0px));
  padding-right: max(0.9rem, env(safe-area-inset-right, 0px));
}
.audio-bar.hidden { display: none; }
.audio-bar-main { display: flex; align-items: center; gap: 0.8rem; }
.audio-bar-info {
  display: flex; align-items: center; gap: 0.5rem;
  min-width: 0; flex: 1 1 320px;
}
/* Queue chip, shuffle, key/tempo, device and clear: the non-transport controls. */
.audio-bar-tools {
  display: flex; align-items: center; gap: 0.5rem; flex: none;
}
/* Cover art of the track now playing, at the left of the bar. */
.audio-bar-art {
  flex: none; align-self: center;
  width: 40px; height: 40px; border-radius: 5px;
  object-fit: cover; background: var(--bg);
}
.audio-bar-art.hidden { display: none; }
/* Song over artist. Each line ellipsises on its own rather than wrapping, so a
   long title can't grow the bar and push the page content up. */
.audio-bar-title {
  flex: 1; min-width: 0;
  display: flex; flex-direction: column; justify-content: center;
}
.audio-bar-song, .audio-bar-artist {
  display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The song gets two lines before it gives up — the transport buttons leave it
   only ~170px on a phone, which one line often can't hold. The artist stays on
   a single line under it; it is almost always short. */
.audio-bar-song {
  font-weight: 600; font-size: 0.98rem; line-height: 1.3;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  white-space: normal; overflow: hidden; overflow-wrap: anywhere;
}
.audio-bar-artist { font-size: 0.85rem; line-height: 1.3; color: var(--muted); }
/* Tracks with no artist recorded keep a single centred line. */
.audio-bar-artist:empty { display: none; }

/* Play/pause. Stretches to the height of the song display (the cover art and
   both text lines) so the phone gets a full-height target instead of the native
   element's ~15px glyph. */
.audio-bar-play {
  flex: none; align-self: stretch;
  width: 42px; min-height: 42px;
  display: flex; align-items: center; justify-content: center;
  padding: 0; border: none; border-radius: 50%;
  background: var(--accent); color: var(--panel-2);
  cursor: pointer; -webkit-tap-highlight-color: transparent; outline: none;
}
.audio-bar-play svg { width: 55%; height: 55%; display: block; pointer-events: none; }
.audio-bar-play:hover, .audio-bar-play:active { background: var(--accent-hi); }
.audio-bar-play:disabled { opacity: 0.4; cursor: default; }
.audio-bar-play:focus, .audio-bar-play:focus-visible { outline: none; box-shadow: none; }
.audio-bar-queue {
  flex: none; align-self: center; padding: 0;
  background: none; border: none; color: var(--muted);
  font-size: 0.85rem; cursor: pointer;
}
/* These bar controls are bare <button>s, so the global button:hover bleeds a
   light-grey accent background in — and on touch that :hover sticks after a tap,
   leaving a grey box until you tap elsewhere. Keep them transparent in every
   state and brighten only the glyph for feedback. */
.audio-bar-queue:hover, .audio-bar-queue:active {
  background: none; border-color: transparent; color: var(--accent-hi);
}
.audio-bar-next, .audio-bar-clear, .audio-bar-shuffle, .audio-bar-fx {
  flex: none; align-self: center; padding: 0 0.3rem;
  background: none; border: none; color: var(--muted);
  font-size: 1rem; line-height: 1; cursor: pointer;
}
.audio-bar-fx { font-size: 1.2rem; padding: 0 0.4rem; }
.audio-bar-fx:hover { color: var(--accent-hi); }
.audio-bar-fx:disabled { opacity: 0.4; cursor: default; }
/* Lit when a non-default key/tempo is applied. */
.audio-bar-fx.on { color: var(--accent-hi); }
/* Key/tempo popover, anchored above the bar's right edge. */
.audio-bar-fx-panel {
  position: absolute; right: 0.6rem; bottom: 100%; margin-bottom: 0.4rem;
  background: var(--panel-2); border: 1px solid var(--accent-soft);
  border-radius: 4px; padding: 0.6rem 0.8rem; z-index: 30;
  display: flex; flex-direction: column; gap: 0.5rem; min-width: 16rem;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);
}
/* The panel sets display:flex, which ties on specificity with the global
   .hidden{display:none} and wins on source order — so without this the popover
   never hides. Re-assert the hide at higher specificity. */
.audio-bar-fx-panel.hidden { display: none; }
.fx-row { display: flex; align-items: center; gap: 0.5rem; }
.fx-label { width: 3.2rem; color: var(--muted); }
.fx-row .fx-down, .fx-row .fx-up {
  flex: none; width: 26px; height: 26px; padding: 0; border-radius: 50%;
  border: 1px solid var(--accent-soft); background: var(--panel);
  color: var(--accent); cursor: pointer; line-height: 1;
}
.fx-row .fx-down:hover, .fx-row .fx-up:hover { background: var(--accent-softer); }
.fx-value { min-width: 2.4rem; text-align: center; font-variant-numeric: tabular-nums; }
.fx-unit { color: var(--muted); font-size: 0.85rem; }
.fx-bpm { margin-left: auto; color: var(--muted); font-size: 0.85rem; }
.fx-actions { display: flex; align-items: center; gap: 0.6rem; }
.fx-status { color: var(--muted); font-size: 0.85rem; }
/* Shuffle and skip are the primary transport controls, so make them a touch
   larger than the bare ✕ clear button. */
.audio-bar-shuffle, .audio-bar-next { font-size: 1.3rem; padding: 0 0.4rem; }
.audio-bar-shuffle svg, .audio-bar-next svg { width: 1.3rem; height: 1.3rem; display: block; pointer-events: none; }
.audio-bar-next:hover, .audio-bar-next:active,
.audio-bar-clear:hover, .audio-bar-clear:active,
.audio-bar-shuffle:hover, .audio-bar-shuffle:active {
  background: none; border-color: transparent; color: var(--accent-hi);
}
.audio-bar-next:disabled, .audio-bar-shuffle:disabled { opacity: 0.4; cursor: default; }
/* iOS leaves a button focused after a tap, showing a grey focus box around the
   skip/shuffle glyphs (and matches :focus-visible on touch, so guarding on that
   doesn't help). Kill the outline/box-shadow on these transport controls and pin
   the tap highlight transparent on the elements themselves. */
.audio-bar-next, .audio-bar-shuffle, .audio-bar-clear, .audio-bar-queue {
  -webkit-tap-highlight-color: transparent;
  outline: none;
}
.audio-bar-next:focus, .audio-bar-shuffle:focus,
.audio-bar-clear:focus, .audio-bar-queue:focus,
.audio-bar-next:focus-visible, .audio-bar-shuffle:focus-visible,
.audio-bar-clear:focus-visible, .audio-bar-queue:focus-visible {
  outline: none;
  box-shadow: none;
}
/* Kept in the DOM and playing, but never drawn — it carries no controls now.
   Taken out of flow rather than display:none, so nothing about its rendering
   can affect whether it plays. */
.audio-bar audio {
  position: absolute; width: 1px; height: 1px;
  opacity: 0; pointer-events: none;
}

/* Wrapper that anchors the picker above the device button (not the bar edge). */
.audio-device-wrap { position: relative; flex: none; align-self: center; display: inline-flex; }
/* Which device is producing sound; doubles as the transfer-picker toggle. */
.audio-bar-device {
  flex: none; padding: 0 0.4rem;
  display: inline-flex; align-items: center; gap: 0.3rem; max-width: 11rem;
  background: none; border: none; color: var(--muted);
  font-size: 0.85rem; line-height: 1; cursor: pointer;
  white-space: nowrap; -webkit-tap-highlight-color: transparent; outline: none;
}
.audio-bar-device[hidden] { display: none; }
.audio-device-ico { flex: none; }
.audio-device-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* Match the other bar buttons: the global button:hover bleeds a grey box in, so
   keep it transparent and brighten only the glyph. */
.audio-bar-device:hover, .audio-bar-device:active {
  background: none; border-color: transparent; color: var(--accent-hi);
}
.audio-bar-device:focus, .audio-bar-device:focus-visible { outline: none; box-shadow: none; }
/* Highlighted when this device is only a remote (playback is elsewhere). */
.audio-bar-device.remote { color: var(--accent); }

/* Device picker, anchored above the device button via .audio-device-wrap. */
.audio-bar-devices {
  position: absolute; right: 0; bottom: 100%; margin-bottom: 0.4rem;
  list-style: none; margin-left: 0; margin-right: 0; padding: 0.3rem;
  background: var(--panel-2); border: 1px solid var(--accent-soft);
  border-radius: 4px; z-index: 40; min-width: 14rem;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);
}
.audio-bar-devices.hidden { display: none; }
.audio-device-item {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.4rem 0.5rem; border-radius: 4px; cursor: pointer;
}
.audio-device-item:hover { background: var(--accent-softer); }
.audio-device-item.is-current { cursor: default; }
.audio-device-item.offline { opacity: 0.5; cursor: default; }
.audio-device-item.offline:hover { background: none; }
.audio-device-dot {
  flex: none; width: 8px; height: 8px; border-radius: 50%;
  background: var(--accent);
}
.audio-device-item.offline .audio-device-dot { background: var(--muted); }
.audio-device-name { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.audio-device-badge { flex: none; color: var(--accent); font-size: 0.75rem; }
.audio-device-empty { padding: 0.4rem 0.5rem; color: var(--muted); }

/* Read-only transport shown on a remote device (in place of the <audio>). */
/* Transport. Shared by the playing device and the remotes watching it. */
/* min-width keeps the track usable in a narrow window: the title column has the
   larger flex basis and would otherwise starve the scrubber down to a few
   pixels, and a title that ellipsises still reads where an 8px track doesn't. */
.audio-scrub { flex: 1; display: flex; align-items: center; gap: 0.6rem; min-width: 120px; }
.audio-scrub.hidden { display: none; }
/* The tap strip is taller than the painted track: ::before stretches the
   target over the whole row so a thumb doesn't have to find 8 pixels. */
.audio-scrub-bar {
  flex: 1; min-width: 0; height: 8px; border-radius: 4px;
  background: var(--accent-soft); cursor: pointer; position: relative;
  -webkit-tap-highlight-color: transparent; outline: none;
}
.audio-scrub-bar::before {
  content: ""; position: absolute; left: 0; right: 0;
  top: -14px; bottom: -14px;
}
.audio-scrub-fill {
  position: absolute; left: 0; top: 0; bottom: 0; width: 0;
  border-radius: 4px; background: var(--accent);
}
.audio-scrub-time {
  flex: none; color: var(--muted); font-size: 0.8rem;
  font-variant-numeric: tabular-nums;
}

/* Upcoming-tracks panel; expands upward above the controls. */
.audio-bar-list {
  list-style: none; margin: 0; padding: 0;
  max-height: 40vh; overflow-y: auto;
  border-bottom: 1px solid var(--accent-soft);
}
.audio-bar-list.hidden { display: none; }
.audio-q-item {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.3rem 0.1rem;
}
.audio-q-item + .audio-q-item { border-top: 1px solid var(--border, var(--accent-soft)); }
/* Queue rows carry the same two-line split as the now-playing display: title
   on top, artist under it. One combined "Artist - Title" line ellipsised into
   something you couldn't read on a phone. */
.audio-q-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.audio-q-title, .audio-q-artist {
  display: block; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.audio-q-artist { font-size: 0.8rem; color: var(--muted); }
.audio-q-artist:empty { display: none; }
.audio-q-up, .audio-q-down, .audio-q-del {
  flex: none; padding: 0 0.35rem;
  background: none; border: none; color: var(--muted);
  font-size: 1rem; line-height: 1; cursor: pointer;
}
.audio-q-up:hover, .audio-q-up:active,
.audio-q-down:hover, .audio-q-down:active,
.audio-q-del:hover, .audio-q-del:active { color: #11151a; }
.audio-q-up:disabled, .audio-q-down:disabled { opacity: 0.3; cursor: default; }

/* Between the phone layout and a roomy window the bar is still a single row,
   and the two word-labels are the first things that stop fitting. Drop them
   there — the tools group can't shrink, so without this the scrubber is
   squeezed to nothing long before the window is actually narrow. */
@media (max-width: 900px) {
  .audio-device-label, .audio-queue-word { display: none; }
}

@media (max-width: 560px) {
  /* Player bar: stack so the title and the native scrubber each get the full
     width — otherwise iOS truncates the title and hides the progress bar. */
  .audio-bar-main { flex-direction: column; align-items: stretch; gap: 0.4rem; }
  .audio-bar-info { flex: none; }
  .audio-bar audio { width: 100%; }
  /* Give the now-playing display real size on the small screen it's read on:
     a bigger cover, legible song/artist lines, and a play button to match. */
  .audio-bar-art { width: 56px; height: 56px; }
  .audio-bar-song { font-size: 1.05rem; }
  .audio-bar-artist { font-size: 0.9rem; }
  .audio-bar-play { width: 56px; min-height: 56px; }
  /* Next is a transport control, so it stays the widest thing in the tools row
     even though it matches its neighbours' height. */
  .audio-bar-next {
    align-self: stretch; min-width: 60px; min-height: 48px;
    display: flex; align-items: center; justify-content: center;
    font-size: 1.75rem; padding: 0;
  }
  .audio-bar-next svg { width: 1.75rem; height: 1.75rem; }
  /* Stacked order: song display, then the scrubber under it, then the tools —
     so the two rows a phone actually reads sit together at the top. */
  .audio-scrub { order: 2; }
  .audio-bar-tools { order: 3; }

  /* Scrubber: a thicker track and a taller row, so seeking is a thumb gesture
     rather than a fingernail one. Inset from the bar's own padding as well, so
     the two timestamps aren't pressed up against the edges of the screen. */
  .audio-scrub { gap: 0.7rem; min-height: 44px; padding: 0 0.5rem; }
  .audio-scrub-bar { height: 14px; border-radius: 7px; }
  .audio-scrub-fill { border-radius: 7px; }
  .audio-scrub-time { font-size: 0.95rem; }

  /* Tools row: bigger glyphs, every control given a full 48px target, and the
     group centred rather than pushed into the bottom corners — a control in the
     corner is the hardest thing on the screen to hit. The queue chip is matched
     with :not(.hidden), since a bare `display` here would outrank the .hidden
     rule that takes it away when the queue is empty. */
  .audio-bar-tools { min-height: 48px; gap: 0.35rem; justify-content: center; }
  .audio-bar-queue:not(.hidden), .audio-bar-shuffle, .audio-bar-fx,
  .audio-bar-clear, .audio-bar-device {
    align-self: stretch; min-width: 48px; min-height: 48px;
    display: flex; align-items: center; justify-content: center;
  }
  .audio-device-wrap { align-self: stretch; }
  .audio-bar-queue { font-size: 1rem; padding: 0 0.6rem; }
  .audio-bar-shuffle, .audio-bar-fx, .audio-bar-clear {
    font-size: 1.6rem; padding: 0 0.6rem;
  }
  .audio-bar-shuffle svg { width: 1.6rem; height: 1.6rem; }
  /* Collapse the device chip to just its icon on a phone (it would otherwise
     push the row past the edge); the picker still spells out each device name.
     The queue chip loses its "queued" for the same reason. */
  .audio-device-label, .audio-queue-word { display: none; }
  .audio-bar-device { max-width: none; padding: 0 0.6rem; font-size: 1.5rem; }
  /* Three stacked rows: song display (56), scrubber (44), tools (48), plus the
     safe-area inset the bar itself now adds along the bottom. */
  body.audio-playing {
    padding-bottom: calc(196px + env(safe-area-inset-bottom, 0px));
  }

  /* Manage view: the thumbnail + title row can't also fit the action buttons on
     a phone, so let the buttons wrap to their own full-width row below instead
     of squeezing the title to an unreadable sliver. */
  #sub-video-list .album-head { flex-wrap: wrap; }
  #sub-video-list .album-head .actions { flex-basis: 100%; }
  /* A nowrap title gives .album-info a huge intrinsic width that, combined with
     the wrapping head, overflows the viewport. Let the title wrap, and stack the
     meta so the date sits under the artist instead of widening the row. */
  #sub-video-list .album-info h3 { white-space: normal; overflow: visible; overflow-wrap: anywhere; }
  #sub-video-list .album-info .meta { white-space: normal; overflow: visible; }
  #sub-video-list .album-info .meta-artist,
  #sub-video-list .album-info .meta-extra { display: block; }
  #sub-video-list .album-info .meta-extra::before { content: none; }
}

/* --- Auth: user chip, login & change-password, users panel --------------- */

.user-chip {
  margin-left: auto;
  position: relative;
}
.user-chip.hidden { display: none; }
/* Art Deco tab: angled (chamfered) left/right ends with a hairline silver
   outline, drawn via two clipped layers since a border can't follow clip-path. */
.user-menu-btn {
  --notch: 10px;
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  background: transparent;
  border: none;
  border-radius: 0;
  padding: 0.32rem 1rem;
  color: var(--text);
  letter-spacing: 0.04em;
  cursor: pointer;
  clip-path: polygon(0 50%, var(--notch) 0, calc(100% - var(--notch)) 0,
                     100% 50%, calc(100% - var(--notch)) 100%, var(--notch) 100%);
}
.user-menu-btn::before,
.user-menu-btn::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  clip-path: inherit;
}
.user-menu-btn::before { background: var(--accent-soft); }   /* hairline edge */
.user-menu-btn::after { inset: 1px; background: var(--panel); } /* inner fill */
.user-menu-btn:hover::before { background: var(--accent); }
.user-menu-btn .user-caret { font-size: 0.7rem; color: var(--muted); }
.user-menu {
  position: absolute;
  top: calc(100% + 0.4rem);
  right: 0;
  display: flex;
  flex-direction: column;
  min-width: 11rem;
  background: var(--panel);
  border: 1px solid var(--accent-soft);
  border-radius: 0;
  padding: 0.3rem;
  z-index: 20;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
}
.user-menu.hidden { display: none; }
.user-menu button { text-align: left; }

/* Keep the app shell hidden until a session is established so it doesn't show
   behind the login gate. */
body:not(.authed) nav.tabs,
body:not(.authed) main { display: none; }

.user-name { font-weight: 600; color: var(--text); }

.login-box { max-width: 22rem; }
.login-box form { display: flex; flex-direction: column; gap: 0.7rem; margin-top: 0.6rem; }
.login-box label { display: flex; flex-direction: column; gap: 0.25rem; }
.login-box input { width: 100%; box-sizing: border-box; }

#user-list { margin-top: 0.8rem; }
li.user-row {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  flex-wrap: wrap;
}
li.user-row.disabled { opacity: 0.55; }
.user-row-main { display: flex; flex-direction: column; gap: 0.15rem; }
.user-row-actions { display: flex; gap: 0.35rem; flex-wrap: wrap; }
.user-devices { list-style: none; margin: 0.25rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.15rem; }
.user-devices li { display: flex; align-items: center; gap: 0.35rem; font-size: 0.78rem; color: var(--muted); }
.device-dot { width: 0.45rem; height: 0.45rem; border-radius: 50%; background: var(--muted); flex: 0 0 auto; }
.device-dot.current { background: var(--ok); }
.device-name { color: var(--text); }
.device-badge {
  padding: 0 0.4rem;
  border-radius: 0.7rem;
  background: var(--accent-softer);
  border: 1px solid var(--accent-soft);
  color: var(--accent);
  font-size: 0.68rem;
  letter-spacing: 0.03em;
}

/* Hide the per-video tag button for users without tag permission. */
body:not(.can-tag) .tag-btn { display: none; }

/* --- Audiobooks --------------------------------------------------------- */
.audiobook-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 1rem;
}
.audiobook-card {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding: 0;
  background: none;
  border: none;
  text-align: left;
  cursor: pointer;
  color: inherit;
}
/* Folder imports that are still assembling (or failed) are not playable. */
.audiobook-card-importing, .audiobook-card-failed { cursor: default; }
.audiobook-card-failed { opacity: 0.7; }
.audiobook-card-failed .audiobook-card-sub { color: var(--danger); }
.audiobook-card-art {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: 3px;
  overflow: hidden;
  background: var(--accent-softer);
}
.audiobook-card-cover {
  width: 100%;
  height: 100%;
  /* contain, not cover: book/audiobook covers vary in aspect ratio, so show the
     whole image rather than cropping the top and bottom off a portrait cover. */
  object-fit: contain;
  display: block;
}
.audiobook-card-nocover {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 2.4rem;
  color: var(--muted);
}
.audiobook-card-title {
  font-size: 0.85rem;
  font-weight: 600;
  line-height: 1.25;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.audiobook-card-sub { font-size: 0.75rem; color: var(--muted); }

/* Player */
.audiobook-box { width: min(720px, 100%); }
.audiobook-head { display: flex; gap: 1rem; align-items: flex-start; }
.audiobook-cover {
  /* Fixed width, natural height so the full cover shows without the bottom being
     clipped (covers are often portrait). max-height guards an extreme aspect. */
  width: 140px;
  height: auto;
  max-height: 220px;
  object-fit: contain;
  align-self: flex-start;
  border-radius: 3px;
  background: var(--accent-softer);
  flex: 0 0 auto;
}
.audiobook-meta { min-width: 0; }
.audiobook-meta h3 { margin: 0 0 0.2rem; }
#audiobook-audio { width: 100%; margin: 0.8rem 0 0.4rem; }
.audiobook-skip {
  display: flex;
  justify-content: center;
  gap: 0.6rem;
  margin-bottom: 0.6rem;
}
.audiobook-skip button {
  font: inherit;
  font-variant-numeric: tabular-nums;
  padding: 0.3rem 0.9rem;
  border: 1px solid var(--border, rgba(255, 255, 255, 0.15));
  border-radius: 999px;
  background: var(--accent-softer);
  color: inherit;
  cursor: pointer;
}
.audiobook-skip button:hover { border-color: var(--accent); color: var(--accent); }
.audiobook-chapters {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 16rem;
  overflow-y: auto;
  border-top: 1px solid var(--border, rgba(255, 255, 255, 0.1));
}
.audiobook-chapter {
  display: flex;
  justify-content: space-between;
  gap: 0.6rem;
  padding: 0.4rem 0.5rem;
  cursor: pointer;
  border-bottom: 1px solid var(--border, rgba(255, 255, 255, 0.08));
  font-size: 0.85rem;
}
.audiobook-chapter:hover { background: var(--accent-softer); }
.audiobook-chapter.active { background: var(--accent-softer); font-weight: 600; }
.audiobook-chapter-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.audiobook-chapter-time { color: var(--muted); flex: 0 0 auto; }

/* Manage-audiobooks modal list */
.manage-list {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  max-height: 60vh;
  overflow-y: auto;
}
.manage-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--border, rgba(255, 255, 255, 0.1));
  border-radius: 3px;
}
.manage-row-cover {
  flex: 0 0 auto;
  width: 2.6rem;
  height: 2.6rem;
  border-radius: 2px;
  object-fit: contain;
  background: #0c0a06;
}
.manage-row-nocover {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.3rem;
  color: var(--muted);
}
.manage-row-main { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; flex: 1 1 auto; }
.manage-row-title { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.manage-row-sub { font-size: 0.8rem; color: var(--muted); }
.manage-row-actions { display: flex; gap: 0.35rem; flex: 0 0 auto; }

/* Upload progress bar (inline, distinct from the absolute overlay .progress). */
.upload-progress { display: flex; align-items: center; gap: 0.6rem; margin: 0.6rem 0; }
.upload-bar {
  flex: 1;
  height: 6px;
  background: var(--accent-softer);
  border-radius: 3px;
  overflow: hidden;
}
.upload-bar-fill { display: block; height: 100%; width: 0; background: var(--accent); transition: width 0.1s linear; }

/* --- Phone tokens (Settings) --- */
.token-new { display: flex; gap: 0.5rem; margin-bottom: 0.5rem; }
.token-new input { flex: 1; }
.token-result {
  border: 1px solid var(--accent-soft);
  border-radius: 2px;
  padding: 0.6rem;
  margin-bottom: 0.5rem;
}
.token-result code {
  display: block;
  overflow-wrap: anywhere;
  margin-bottom: 0.5rem;
  color: var(--ok);
}
.token-result.hidden { display: none; }
.token-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.35rem 0;
  border-bottom: 1px solid var(--accent-softer);
  font-size: 0.85rem;
}
.token-row .meta { margin-left: auto; }

/* --- News -------------------------------------------------------------------
   Headlines as a card list, and a reader overlay for the article body. The
   palette is the app's own: silver rules and muted steel, so a news card reads
   as part of Metropolis rather than as an embedded website. */

/* Unread count on the News tab itself, visible from any other tab. */
.tab-badge {
  display: inline-block;
  margin-left: 0.4rem;
  padding: 0.05rem 0.35rem;
  border-radius: 999px;
  background: var(--accent);
  color: var(--bg);
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  vertical-align: text-top;
}

.news-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem 1rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--accent-soft);
}
.news-shelves { display: flex; gap: 0.25rem; flex-wrap: wrap; }
.shelf {
  background: transparent;
  border: 1px solid var(--accent-softer);
  border-radius: 999px;
  color: var(--muted);
  font-family: var(--display);
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 0.35rem 0.85rem;
  cursor: pointer;
}
.shelf:hover {
  background: transparent;
  border-color: var(--accent);
  color: var(--text);
}
.shelf.is-active {
  background: var(--accent-softer);
  border-color: var(--accent);
  color: var(--accent);
}
.news-tools {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.news-tools .check { white-space: nowrap; }
.news-status { margin: 0; min-height: 1em; }

.news-list { display: flex; flex-direction: column; gap: 0.6rem; }
.news-card {
  display: flex;
  gap: 0.85rem;
  align-items: flex-start;
  padding: 0.75rem;
  background: var(--panel);
  border: 1px solid var(--accent-softer);
  border-radius: 3px;
  cursor: pointer;
}
.news-card:hover { border-color: var(--accent-soft); }
/* Read stories stay in the list but step back, so what is left to read is what
   catches the eye. */
.news-card.is-read { opacity: 0.55; }
.news-card.is-read .news-title { color: var(--muted); font-weight: 500; }
.news-thumb {
  width: 132px;
  height: 88px;
  flex: 0 0 auto;
  object-fit: cover;
  border-radius: 2px;
  background: var(--panel-2);
}
.news-text { min-width: 0; display: flex; flex-direction: column; gap: 0.3rem; }
.news-meta {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
  color: var(--muted);
  font-size: 0.75rem;
}
.news-feed { color: var(--accent); font-weight: 600; }
.news-dot { opacity: 0.6; }
.news-shelf-tag {
  border: 1px solid var(--accent-softer);
  border-radius: 999px;
  padding: 0 0.4rem;
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.news-title {
  margin: 0;
  font-family: var(--display);
  font-size: 1.02rem;
  font-weight: 600;
  line-height: 1.3;
}
.news-summary {
  margin: 0;
  color: var(--muted);
  font-size: 0.86rem;
  line-height: 1.45;
  /* Two lines is enough to tell whether the story is worth opening. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* --- The reader ----------------------------------------------------------- */

/* A long article scrolls inside its own box; the page behind it must not
   scroll with it. */
body.modal-open { overflow: hidden; }

.reader-box {
  position: relative;
  width: min(720px, 100%);
  max-height: calc(100vh - 3rem);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  background: var(--panel);
  border: 1px solid var(--accent);
  border-radius: 3px;
  padding: 1.5rem;
  /* Reading, not app chrome: let the text be selected and copied. */
  -webkit-user-select: text;
  user-select: text;
}
.reader-head { padding-right: 2rem; }
.reader-source {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  color: var(--accent);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.reader-head h2 {
  margin: 0.4rem 0 0;
  font-family: var(--display);
  font-size: 1.5rem;
  line-height: 1.25;
}
.reader-head .meta { margin: 0.3rem 0 0; }
.reader-image {
  width: 100%;
  margin-top: 1rem;
  border-radius: 2px;
  background: var(--panel-2);
}
.reader-body {
  margin-top: 1rem;
  /* A comfortable measure for prose, wider than the app's usual dense lists. */
  font-size: 1rem;
  line-height: 1.65;
}
.reader-body p { margin: 0 0 1rem; }
.reader-body h3 {
  margin: 1.5rem 0 0.6rem;
  font-family: var(--display);
  font-size: 1.1rem;
  color: var(--accent);
}
/* The first heading is usually the article's own title repeated; the reader
   already shows it above, so it needs no gap of its own. */
.reader-body h3:first-child { margin-top: 0; }
.reader-bullet { padding-left: 1rem; border-left: 2px solid var(--accent-softer); }
.reader-loading, .reader-note { color: var(--muted); font-style: italic; }
.reader-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin-top: 0.5rem;
  padding-top: 1rem;
  border-top: 1px solid var(--accent-soft);
}
.reader-foot-actions { display: flex; gap: 0.5rem; }
.reader-link {
  color: var(--accent);
  text-decoration: none;
  font-weight: 600;
}
.reader-link:hover { color: var(--accent-hi); text-decoration: underline; }

/* --- Settings: the feed list ---------------------------------------------- */

/* `.list li` is a flex column; a feed row is a row, and needs the same
   specificity to say so. */
.list li.feed-row {
  flex-direction: row;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
}
.feed-row.disabled { opacity: 0.55; }
.feed-main { display: flex; flex-direction: column; gap: 0.15rem; flex: 1 1 auto; min-width: 0; }
.feed-url {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 46ch;
}
.feed-actions { display: flex; gap: 0.4rem; flex: 0 0 auto; }
/* A failing feed's status often quotes the URL: let it break anywhere. */
.feed-bad { color: var(--warn); overflow-wrap: anywhere; }

@media (max-width: 560px) {
  .news-bar { align-items: stretch; }
  .news-tools { width: 100%; }
  .news-tools .search-wrap { flex: 1 1 100%; }
  .news-thumb { width: 92px; height: 68px; }
  .news-title { font-size: 0.95rem; }
  .reader-box { padding: 1.1rem; max-height: calc(100vh - 1.5rem); }
  .reader-head h2 { font-size: 1.25rem; }
  .feed-url { max-width: 100%; }
  /* Stretched, not shrink-to-fit: a nowrap URL would otherwise size the row
     to its full length and push the page wider than the screen. */
  .list li.feed-row { flex-direction: column; align-items: stretch; }
}

/* Watch & listen history (user menu → History). A taller dialog than the
   confirm box: a day heading over each day's plays, each row the item's art,
   what it was, when, and how far it got. */
.history-box {
  position: relative;
  width: min(620px, 100%);
  max-height: calc(100vh - 3rem);
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--accent);
  border-radius: 3px;
  padding: 1.25rem;
}
.history-box h3 {
  margin: 0 0 0.75rem;
  font-family: var(--display);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--accent);
  font-weight: 500;
}
.history-filter { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-bottom: 0.5rem; }
.history-filter.hidden { display: none; }
.history-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  margin: 0 -0.25rem;
  padding: 0 0.25rem;
}
.history-day {
  margin: 0.9rem 0 0.35rem;
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}
.history-day-list { list-style: none; margin: 0; padding: 0; }
.history-row {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  border-bottom: 1px solid var(--accent-softer);
}
.history-row:last-child { border-bottom: none; }
/* The whole row but the remove button plays it; undo the base button look. */
.history-play {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.45rem 0.25rem;
  background: transparent;
  border: none;
  color: var(--text);
  font: inherit;
  font-weight: normal;
  text-transform: none;
  letter-spacing: normal;
  text-align: left;
}
.history-play:hover { background: var(--accent-softer); }
.history-play:disabled { cursor: default; opacity: 0.55; }
.history-play:disabled:hover { background: transparent; }
.history-thumb {
  flex: none;
  width: 64px;
  height: 40px;
  border-radius: 2px;
  overflow: hidden;
  background: var(--panel-2);
}
/* Square art for music and books; a video keeps its 16:10-ish frame. */
.history-thumb.kind-track, .history-thumb.kind-audiobook { width: 40px; margin: 0 12px; }
.history-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.history-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.1rem; }
.history-title, .history-sub, .history-meta {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.history-title { font-weight: 600; }
.history-sub, .history-meta { font-size: 0.8rem; color: var(--muted); }
.history-meta { font-variant-numeric: tabular-nums; }
.history-bar {
  display: block;
  height: 2px;
  margin-top: 0.2rem;
  background: var(--accent-softer);
  border-radius: 1px;
  overflow: hidden;
}
.history-bar > span { display: block; height: 100%; background: var(--accent); }
.history-remove {
  flex: none;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  background: transparent;
  border: none;
  color: var(--muted);
  font-size: 1.3rem;
  font-weight: normal;
  line-height: 1;
}
.history-remove:hover { background: transparent; color: var(--danger); }
.history-actions {
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
  padding-top: 0.75rem;
}
/* Clear sits on the right even when there is nothing more to load. */
.history-actions #history-clear { margin-left: auto; }

@media (max-width: 520px) {
  .history-box { padding: 1rem 0.85rem; max-height: calc(100vh - 1.5rem); }
  .history-thumb { width: 52px; height: 32px; }
  .history-thumb.kind-track, .history-thumb.kind-audiobook { width: 32px; margin: 0 10px; }
}
