/* ============================================================================
   Sonorus - stylesheet.

   The visual idea is a piece of audio equipment: a deep ink chassis, one warm
   amber lamp colour, and hi-fi style labelling (small, wide-tracked, uppercase
   monospace) over the section titles. Numbers - durations, bitrates, track
   numbers, the transport counter - are always monospace and tabular, the way a
   display on a hi-fi front panel is.

   Dark is the default and the theme the layout is designed for; light exists
   for daytime and is derived from the same tokens.
   ========================================================================== */

:root {
  --bg: #100e14;
  --surface: #1a171f;
  --surface-2: #221d29;
  --surface-3: #2b2434;
  --line: #2a2431;
  --line-soft: #201b27;
  --text: #f2eef4;
  --text-dim: #9b90a6;
  --text-faint: #6d6479;
  --accent: #f5a524;
  --accent-hi: #ffbe57;
  --accent-soft: rgba(245, 165, 36, 0.14);
  --accent-line: rgba(245, 165, 36, 0.35);
  --accent-ink: #24170a;
  /* The lamp turned down: what marks the song that is playing, in a list it is
     not playing from. Deliberately the same hue - a second colour would say
     "something else", and this is the same thing seen from further away. */
  --accent-dim: rgba(245, 165, 36, 0.52);
  --accent-ghost: rgba(245, 165, 36, 0.05);
  --danger: #f0705f;
  --danger-soft: rgba(240, 112, 95, 0.15);
  --ok: #63c98a;
  --ok-soft: rgba(99, 201, 138, 0.15);
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5);
  --shadow-md: 0 6px 20px rgba(0, 0, 0, 0.45);
  --shadow-lg: 0 18px 48px rgba(0, 0, 0, 0.6);
  /* Measured off Firefox's own bar in a dark GTK theme, so both browsers agree. */
  --scrollbar-thumb: #a4a4a3;
  --scrollbar-track: #313131;
  --radius: 12px;
  --radius-sm: 8px;
  --radius-xs: 5px;
  --sidebar-w: 264px;
  /* What is left of the sidebar when it is folded away: wide enough for
     the brand mark and the button that brings the rest back. */
  --sidebar-rail: 62px;
  /* How long the fold takes, in one place so the width, the brand name and
     the list underneath it all move together. Its own curve rather than
     --sheet-ease: that one is built to shoot off the mark, which reads as a
     snap on a 200 px move and is not what a panel folding away should look
     like. This is the even one. */
  --fold-ms: 0.26s;
  --fold-ease: cubic-bezier(0.4, 0, 0.2, 1);
  --player-h: 92px;
  /* What the phone's own bars cover. Zero everywhere else, so it can be added
     unconditionally - the transport is the one thing that must not end up under
     the gesture bar of an installed app. */
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-top: env(safe-area-inset-top, 0px);
  /* The curve a sheet moves on: quick off the mark, long and soft into place. */
  --sheet-ease: cubic-bezier(0.32, 0.72, 0, 1);
  --font-ui: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, "JetBrains Mono", "SFMono-Regular", "Cascadia Code", Menlo, Consolas, monospace;
}

:root[data-theme="light"] {
  --bg: #f6f3f0;
  --surface: #ffffff;
  --surface-2: #efeae5;
  --surface-3: #e4ddd6;
  --line: #e2dbd4;
  --line-soft: #ece6e0;
  --text: #1c1720;
  --text-dim: #6b6270;
  --text-faint: #8b8290;
  --accent: #a86400;
  --accent-hi: #8a5100;
  --accent-soft: rgba(168, 100, 0, 0.12);
  --accent-line: rgba(168, 100, 0, 0.3);
  --accent-ink: #ffffff;
  --accent-dim: rgba(168, 100, 0, 0.55);
  --accent-ghost: rgba(168, 100, 0, 0.06);
  --danger: #c0392b;
  --danger-soft: rgba(192, 57, 43, 0.12);
  --ok: #2e7d52;
  --ok-soft: rgba(46, 125, 82, 0.13);
  --shadow-sm: 0 1px 2px rgba(30, 20, 10, 0.07);
  --shadow-md: 0 6px 20px rgba(30, 20, 10, 0.12);
  --shadow-lg: 0 18px 48px rgba(30, 20, 10, 0.2);
  --scrollbar-thumb: #8b8290;
  --scrollbar-track: #e4ddd6;
}

* { box-sizing: border-box; }

/* Author display rules otherwise beat the UA [hidden] rule at equal
   specificity, leaving "hidden" elements visible. */
[hidden] { display: none !important; }

html {
  -webkit-text-size-adjust: 100%;
  /* Android draws its own grey flash over anything tapped, on top of whatever
     the element already shows for a press. One answer per press is enough. */
  -webkit-tap-highlight-color: transparent;
}

/* No waiting to see whether a second tap follows and means "zoom" - there is
   nothing here to zoom into, and that wait is what makes a web app feel a
   moment slower than an app. */
button, a, input, select, textarea, .card, .track-row.item, .queue-item {
  touch-action: manipulation;
}

body {
  margin: 0;
  font-family: var(--font-ui);
  font-size: 15px;
  line-height: 1.5;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; }
h1, h2, h3 { margin: 0; letter-spacing: -0.02em; }
h1 { font-size: 1.7rem; font-weight: 700; }
h2 { font-size: 1.15rem; font-weight: 650; }
h3 { font-size: 0.98rem; font-weight: 600; }

::selection { background: var(--accent-soft); }

/* Chromium - and with it the desktop client - otherwise draws a light grey bar
   with arrow buttons. This copies Firefox's overlay bar on Linux, which keeps
   its own and ignores these: hidden at rest, a 3 px line while the pointer or
   the page moves (see app.js), the full bar under the pointer. */
@media (hover: hover) {
  ::-webkit-scrollbar { width: 12px; height: 12px; }
  ::-webkit-scrollbar-track, ::-webkit-scrollbar-corner { background: transparent; }
  ::-webkit-scrollbar-thumb {
    background: transparent padding-box;
    border: solid transparent; border-radius: 999px;
  }
  ::-webkit-scrollbar-thumb:vertical { border-width: 0 1px 0 8px; }
  ::-webkit-scrollbar-thumb:horizontal { border-width: 8px 0 1px; }
  .scrollbars-shown ::-webkit-scrollbar-thumb { background-color: var(--scrollbar-thumb); }
  /* The fade, in the steps app.js sets. Chromium animates nothing on a
     scrollbar, and a custom property transition restyles the whole page every
     frame - 139 ms for the first one on a list of 8000 songs. */
  [data-scrollbar-fade="1"] ::-webkit-scrollbar-thumb { background-color: color-mix(in srgb, var(--scrollbar-thumb) 75%, transparent); }
  [data-scrollbar-fade="2"] ::-webkit-scrollbar-thumb { background-color: color-mix(in srgb, var(--scrollbar-thumb) 50%, transparent); }
  [data-scrollbar-fade="3"] ::-webkit-scrollbar-thumb { background-color: color-mix(in srgb, var(--scrollbar-thumb) 25%, transparent); }
  .scrollbar-hover::-webkit-scrollbar-track { background: var(--scrollbar-track); }
  .scrollbar-hover::-webkit-scrollbar-thumb { background-color: var(--scrollbar-thumb); border-width: 3px; }
}

:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

.icon { display: block; flex: none; }
.muted { color: var(--text-dim); }
.num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* The equipment label: every section in the app is titled like a control on a
   hi-fi front panel. This is the one type gesture the whole design leans on. */
.rack-label {
  font-family: var(--font-mono);
  font-size: 0.66rem;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* ============================================================================
   Auth pages (login, setup, error)
   ========================================================================== */

body.auth { display: flex; flex-direction: column; min-height: 100vh; }
.auth-main { flex: 1; display: flex; align-items: center; justify-content: center; padding: 24px; }
.auth-card {
  width: 100%; max-width: 400px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow-md);
  padding: 32px;
}
.auth-brand { display: flex; align-items: center; justify-content: center; gap: 11px; margin-bottom: 18px; }
.auth-card h1 { text-align: center; font-size: 1.35rem; margin-bottom: 6px; }
.auth-card .page-sub { text-align: center; color: var(--text-dim); font-size: 0.9rem; margin: 0 0 20px; }
.auth-footer { display: flex; justify-content: center; padding: 20px; color: var(--text-faint); font-size: 0.82rem; }

.brand-mark {
  display: grid; place-items: center;
  width: 36px; height: 36px; border-radius: 10px;
  background: var(--accent); color: var(--accent-ink);
  flex: none;
}
.brand-mark svg { width: 21px; height: 21px; }
.brand-name { font-weight: 700; font-size: 1.2rem; letter-spacing: -0.02em; }

/* ============================================================================
   Forms
   ========================================================================== */

.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 15px; }
.field label { font-size: 0.82rem; font-weight: 600; }
input[type="text"], input[type="password"], input[type="search"], input[type="number"],
select, textarea {
  width: 100%; font: inherit; font-size: 0.94rem;
  padding: 9px 12px;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface-2); color: var(--text);
}
input::placeholder { color: var(--text-faint); }
input:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
textarea { resize: vertical; min-height: 70px; }
.checkbox { display: flex; align-items: center; gap: 9px; font-size: 0.9rem; cursor: pointer; }
.checkbox input { width: auto; accent-color: var(--accent); }
.form-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 6px; }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 0 14px; }

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  font-weight: 600; font-size: 0.89rem;
  padding: 9px 16px; border-radius: var(--radius-sm);
  border: 1px solid transparent; cursor: pointer; white-space: nowrap;
}
.btn-primary { background: var(--accent); color: var(--accent-ink); }
.btn-ghost { background: var(--surface-2); border-color: var(--line); color: var(--text); }
.btn-quiet { background: transparent; color: var(--text-dim); }
.btn-danger { background: var(--danger-soft); color: var(--danger); border-color: transparent; }
.btn-block { width: 100%; }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
.btn-sm { padding: 6px 11px; font-size: 0.82rem; }
/* A toggle that is on: the pin button of a playlist */
.btn.is-pinned { color: var(--accent); border-color: var(--accent-line); }

.icon-btn {
  display: inline-grid; place-items: center;
  width: 34px; height: 34px; border-radius: var(--radius-sm);
  border: none; background: transparent; color: var(--text-dim); cursor: pointer;
}
.icon-btn:disabled { color: var(--text-faint); opacity: 0.5; cursor: default; }
.icon-btn.is-on { color: var(--accent); }
.icon-btn-sm { width: 28px; height: 28px; }

/* Hover belongs to a pointer, and only to a pointer. A phone keeps :hover on
   the last thing it touched until something else is touched, so every one of
   these tints would stay behind on the button that was just used - which is
   exactly the "Buttons sind danach immer noch grau hinterlegt". What a tap gets
   instead is the :active block at the end of this file. */
@media (hover: hover) {
  .btn-primary:hover { background: var(--accent-hi); }
  .btn-ghost:hover { background: var(--surface-3); }
  .btn-quiet:hover { background: var(--surface-2); color: var(--text); }
  .btn-danger:hover { background: var(--danger); color: var(--bg); }
  .icon-btn:hover { background: var(--surface-2); color: var(--text); }
  /* A switched-on button keeps its accent under the pointer. Without this the
     hover colour wins (same specificity, later in the file) and shuffle and
     repeat look identical whether they are on or off - which is the one thing
     the colour is there to say. The background tint still answers the hover. */
  .icon-btn.is-on:hover { color: var(--accent); }
  .icon-btn:disabled:hover { background: transparent; color: var(--text-faint); }
  .icon-btn.danger:hover { background: var(--danger-soft); color: var(--danger); }
}

.flash { padding: 11px 14px; border-radius: var(--radius-sm); margin-bottom: 16px; font-size: 0.89rem; }
.flash.err { background: var(--danger-soft); color: var(--danger); }
.flash.ok { background: var(--ok-soft); color: var(--ok); }

/* ============================================================================
   App shell
   ========================================================================== */

/* dvh, not vh: on a phone `100vh` is the tallest the viewport ever gets, so
   with the address bar showing, the bottom of the app - the transport - sits
   below the fold. The vh line stays as the fallback for anything that does not
   know dvh yet. `overscroll-behavior` is what stops a scroll that has reached
   the end of a list from turning into a pull-to-refresh of the whole app. */
body.app { height: 100vh; height: 100dvh; overflow: hidden; overscroll-behavior: none; }

/* Deliberately not ".app": that class is also on <body>, and a shared rule
   would turn the body itself into the grid, making the audio element, the
   toast host and the modal host into grid cells over the content. */
.shell {
  display: grid;
  /* `auto` rather than the width itself: the column follows the sidebar, and the
     sidebar animates its own width. Animating the track directly would work in
     Firefox but is the less portable of the two, and this way the drawer on a
     phone - which is out of flow - changes nothing here. */
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr) var(--player-h);
  height: 100vh;
  height: 100dvh;
}

/* --- Sidebar -------------------------------------------------------------- */

.sidebar {
  grid-row: 1; grid-column: 1;
  width: var(--sidebar-w);
  background: var(--surface);
  border-right: 1px solid var(--line);
  display: flex; flex-direction: column;
  min-height: 0;
  z-index: 30;
  transition: width var(--fold-ms) var(--fold-ease);
}

.sidebar-head {
  display: flex; align-items: center; gap: 11px;
  padding: 16px 18px 14px;
  /* Wrapping is what carries the fold: as the rail narrows past the brand mark
     plus the button, the button drops onto a line of its own under the mark
     instead of being cut off. No second layout to switch to. */
  flex-wrap: wrap; justify-content: center;
  transition: padding var(--fold-ms) var(--fold-ease);
}
/* The fold button sits at the end of the brand row and only exists where the
   sidebar is a column - on a phone it is a drawer and the hamburger closes it. */
.sidebar-head .sidebar-toggle { margin-left: auto; }

/* Folded away: the sidebar narrows to a rail instead of disappearing, so the
   mark stays where it was and the way back is where it went. The name and the
   list fade out first, so nothing is still being read while the width moves. */
.shell.sidebar-folded .sidebar { width: var(--sidebar-rail); }
.shell.sidebar-folded .sidebar-head { padding: 16px 0 14px; }
.shell.sidebar-folded .sidebar-head .sidebar-toggle { margin-left: 0; }
.sidebar .brand-name {
  overflow: hidden; white-space: nowrap;
  transition: opacity calc(var(--fold-ms) * 0.6) ease, max-width var(--fold-ms) var(--fold-ease);
  max-width: 12ch;
}
.shell.sidebar-folded .brand-name { opacity: 0; max-width: 0; }
/* `visibility` and not `display`, or there would be nothing to animate - and it
   is what keeps a folded list out of the tab order all the same. */
.shell.sidebar-folded .sidebar-scroll {
  opacity: 0; visibility: hidden;
  transition: opacity calc(var(--fold-ms) * 0.5) ease, visibility 0s linear var(--fold-ms);
}
.sidebar-scroll { transition: opacity var(--fold-ms) ease var(--fold-ms); }
/* Restoring the saved state is not a fold, so it does not get the animation. */
.shell.fold-instant .sidebar,
.shell.fold-instant .sidebar-head,
.shell.fold-instant .brand-name,
.shell.fold-instant .sidebar-scroll { transition: none; }
.sidebar-scroll { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 0 10px 20px; }

.nav-group { margin-bottom: 22px; }
.nav-group-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; padding: 0 9px 7px;
}
.nav-item {
  display: flex; align-items: center; gap: 11px;
  padding: 8px 10px; border-radius: var(--radius-sm);
  color: var(--text-dim); font-size: 0.92rem; font-weight: 500;
  cursor: pointer; width: 100%; border: none; background: none; text-align: left;
}
@media (hover: hover) {
  .nav-item:hover { background: var(--surface-2); color: var(--text); }
}
.nav-item.active { background: var(--surface-2); color: var(--text); font-weight: 600; }
/* The amber lamp marks where you are - the one place colour is used in the nav */
.nav-item.active::before {
  content: ""; position: absolute; left: 0;
  width: 3px; height: 18px; border-radius: 0 3px 3px 0; background: var(--accent);
}
.nav-item { position: relative; }
.nav-item .icon { color: currentColor; }
.nav-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav-count {
  font-family: var(--font-mono); font-size: 0.72rem;
  font-variant-numeric: tabular-nums; color: var(--text-faint);
}

/* Star playlists: the row of stars is the label, so it reads as a rating */
.nav-stars { display: flex; gap: 1px; color: var(--accent); }
.nav-stars .icon { width: 12px; height: 12px; }
.nav-stars .off { color: var(--text-faint); opacity: 0.4; }

/* Playlist folders: a disclosure row with its playlists nested underneath */
.folder-row { display: flex; align-items: center; gap: 4px; }
.folder-row .nav-item { flex: 1; }
.folder-caret { transition: transform 0.15s ease; }
.folder.collapsed .folder-caret { transform: rotate(-90deg); }
.folder.collapsed .folder-children { display: none; }
.folder-children { padding-left: 16px; border-left: 1px solid var(--line); margin-left: 19px; }
.nav-item.drop-target { background: var(--accent-soft); color: var(--text); box-shadow: inset 0 0 0 1px var(--accent-line); }

/* Dragging a playlist to another place in the sidebar: into another spot, into
   a folder, or back out to the top level. */
.playlist-item { cursor: grab; }
.nav-item.dragging { opacity: 0.4; }
.nav-item.drop-above { box-shadow: inset 0 2px 0 var(--accent); }
.nav-item.drop-below { box-shadow: inset 0 -2px 0 var(--accent); }
/* The top level is a drop target of its own, so a list can leave a folder even
   when there is no loose playlist to drop it next to. */
.playlist-root { min-height: 12px; border-radius: var(--radius-sm); }
.playlist-root.drop-target { background: var(--accent-soft); box-shadow: inset 0 0 0 1px var(--accent-line); }
/* A pinned playlist wears the pin instead of the list icon */
.playlist-item .icon { color: currentColor; }

.sidebar-empty { padding: 4px 10px 0; color: var(--text-faint); font-size: 0.82rem; }

/* --- Main column ---------------------------------------------------------- */

.main {
  grid-row: 1; grid-column: 2;
  display: flex; flex-direction: column; min-height: 0; min-width: 0;
}

.topbar {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 22px;
  border-bottom: 1px solid var(--line);
  background: var(--bg);
  flex: none;
}
.topbar .menu-toggle { display: none; }

.nav-arrows { display: flex; gap: 2px; }

.search {
  position: relative; flex: 1; max-width: 380px;
}
.search input[type="search"] {
  height: 36px; padding-left: 34px; border-radius: 999px;
  background: var(--surface); font-size: 0.9rem;
}
.search-icon {
  position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
  color: var(--text-faint); pointer-events: none;
}
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 6px; }

/* Statistik and Einstellungen, left of the avatar. Einstellungen is the icon
   alone - the cog says it - while Statistik carries its word as long as the
   window is wide enough for it. The badge is the count of open import notices,
   which used to hang on the sidebar entry. */
.top-link {
  position: relative;
  display: inline-flex; align-items: center; gap: 7px;
  height: 34px; padding: 0 11px; border-radius: var(--radius-sm);
  color: var(--text-dim); font-size: 0.88rem; font-weight: 500;
}
.top-link.icon-only { padding: 0; width: 34px; justify-content: center; }
.top-link.active { background: var(--surface-2); color: var(--text); }
.top-link .nav-badge { position: absolute; top: 2px; right: 2px; }
@media (hover: hover) {
  .top-link:hover { background: var(--surface-2); color: var(--text); text-decoration: none; }
}

/* The segmented switch, now only the time range on the statistics page and the
   theme picker under Einstellungen. */
.seg-switch {
  display: inline-flex; background: var(--surface); border: 1px solid var(--line);
  border-radius: 999px; padding: 2px;
}
.seg-switch button {
  border: none; background: transparent; color: var(--text-dim);
  font-size: 0.78rem; font-weight: 600; padding: 4px 11px;
  border-radius: 999px; cursor: pointer;
}
@media (hover: hover) {
  .seg-switch button:hover { color: var(--text); }
}
.seg-switch button.active { background: var(--surface-3); color: var(--text); }

/* The account menu carries the two topbar links only where they do not fit. */
.dropdown-item.only-compact { display: none; }

.avatar { display: inline-grid; place-items: center; border-radius: 50%; overflow: hidden; background: var(--accent); color: var(--accent-ink); font-weight: 700; }
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.avatar-sm { width: 32px; height: 32px; font-size: 0.85rem; }
.avatar-md { width: 42px; height: 42px; font-size: 1.05rem; }

.menu-wrap { position: relative; }
.avatar-btn { border: none; background: none; padding: 0; cursor: pointer; border-radius: 50%; }

.dropdown {
  position: absolute; right: 0; top: calc(100% + 8px);
  min-width: 220px; background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow-lg); padding: 7px; z-index: 60;
}
.dropdown-user { display: flex; align-items: center; gap: 10px; padding: 8px; border-bottom: 1px solid var(--line); margin-bottom: 5px; }
.dropdown-user-text { display: flex; flex-direction: column; min-width: 0; }
.dropdown-user-name { font-weight: 600; font-size: 0.92rem; }
.dropdown-user-sub { font-size: 0.78rem; color: var(--text-dim); }
.dropdown-item {
  display: flex; align-items: center; gap: 10px;
  width: 100%; padding: 8px 10px; border-radius: var(--radius-xs);
  font-size: 0.9rem; text-align: left; border: none; background: none; cursor: pointer;
}
.dropdown-item.danger { color: var(--danger); }
@media (hover: hover) {
  .dropdown-item:hover { background: var(--surface-2); }
  .dropdown-item.danger:hover { background: var(--danger-soft); }
}
.dropdown-sep { height: 1px; background: var(--line); margin: 5px 0; }
.dropdown form { margin: 0; }

/* `contain` keeps a scroll that has hit the end of the list inside the list:
   without it the gesture carries on into the page behind and Chrome answers a
   pull at the top with a reload of the whole app. */
.content { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 26px 26px 40px; }
.content-inner { max-width: 1280px; margin: 0 auto; }

/* --- Page heads ----------------------------------------------------------- */

.page-head { margin-bottom: 24px; }
.page-head .rack-label { display: block; margin-bottom: 7px; }
.page-head-row { display: flex; align-items: flex-end; justify-content: space-between; gap: 18px; flex-wrap: wrap; }
.page-meta { color: var(--text-dim); font-size: 0.88rem; margin-top: 5px; }
.page-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* The generic form rule stretches a select across whatever it is in, which on
   the album page pushed the view switch onto a line of its own. */
.page-actions select { width: auto; }

.section { margin-bottom: 34px; }
.section-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 13px; }

/* --- Home: the equipment readout ------------------------------------------ */
/* The library as a front-panel display: hairline-separated readouts, mono
   numbers, no cards. This is the home page's opening statement. */

.readout {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface); overflow: hidden;
  margin-bottom: 30px;
}
.readout-cell { padding: 16px 18px; border-right: 1px solid var(--line); }
.readout-cell:last-child { border-right: none; }
.readout-value {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size: 1.5rem; font-weight: 600; letter-spacing: -0.02em; line-height: 1.2;
  color: var(--text); display: block; margin-top: 5px; white-space: nowrap;
}
.readout-cell.accent .readout-value { color: var(--accent); }
.readout-sub { display: block; margin-top: 5px; font-size: 0.78rem; color: var(--text-dim); }

/* Two kinds of value do not share a row of equal columns: five plain counts,
   and the ones that need the width - a playtime heading for four digits of
   hours, a date under the number. Ten columns are what makes both rows divide
   evenly, five cells of two and then two of five. */
.readout.split { grid-template-columns: repeat(10, 1fr); }
.readout.split > .readout-cell { grid-column: span 2; }
.readout.split > .readout-cell:nth-child(5) { border-right: none; }
.readout.split > .span-5,
.readout.split > .span-10 { border-top: 1px solid var(--line); }
.readout.split > .span-5 { grid-column: span 5; }
.readout.split > .span-10 { grid-column: span 10; border-right: none; }

.hero-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 32px; }

/* --- Statistics ------------------------------------------------------------ */
/* Same front-panel language as the home readout: a column chart drawn from
   plain divs (no chart library, and the CSP would block one anyway) and top
   lists whose bar is part of the row. */

.panel .readout { margin-bottom: 0; }
.panel-head-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 18px; flex-wrap: wrap; }
.panel-head-row h2 { margin: 0; }

/* Which single period is on screen, with the arrows that walk to its
   neighbours. Centred like the readout it sits on, so the name of the period
   reads as the heading of everything below it. */
.period-nav { display: flex; align-items: center; justify-content: center; gap: 10px; margin-bottom: 16px; }
.period-title {
  font-size: 0.95rem; font-weight: 550; text-align: center;
  min-width: 0; flex: 0 1 auto;
}
/* The whole block is swapped out at once while the next period is fetched -
   dimming it says "these numbers are the old ones" without moving anything. */
#period-view.busy { opacity: 0.55; pointer-events: none; }
#period-view { transition: opacity 0.15s ease; }

.chart { display: flex; align-items: flex-end; gap: 8px; height: 210px; overflow-x: auto; padding-bottom: 2px; }
/* A width cap, or a single day of history would draw one bar across the whole
   panel. The labels above and below each bar need the min-width. */
.chart-col {
  flex: 1 1 0; min-width: 54px; max-width: 96px;
  display: flex; flex-direction: column; align-items: center; gap: 5px; height: 100%;
}
/* The value rides on top of its own bar, so a short bar does not leave its
   number floating at the ceiling. The tallest bar shrinks by the height of that
   line instead of overflowing - flex does that by itself. */
.chart-track {
  flex: 1; width: 100%;
  display: flex; flex-direction: column; justify-content: flex-end; align-items: center; gap: 3px;
}
.chart-bar {
  display: block;
  width: 100%; min-height: 2px; border-radius: 3px 3px 0 0;
  background: linear-gradient(180deg, var(--accent), var(--accent-soft));
  transition: height 0.25s ease;
}
@media (hover: hover) {
  .chart-col:hover .chart-bar { background: var(--accent-hi); }
  .chart-col:hover .chart-value { color: var(--accent); }
  .top-row:hover { background: var(--surface); }
}
/* The value lines keep their height when they are empty, so every bar in the
   row still stands on the same baseline. */
.chart-value, .chart-plays { flex: none; line-height: 1.2; min-height: 1.2em; white-space: nowrap; }
.chart-value { font-size: 0.64rem; color: var(--text-dim); }
.chart-plays { font-size: 0.6rem; color: var(--text-faint); }
/* Nothing was played: a neutral tick instead of a glowing sliver of amber. */
.chart-col.quiet .chart-bar { background: var(--surface-2); }
.chart-key {
  font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.04em;
  color: var(--text-faint); white-space: nowrap;
}

/* A day is 24 bars and a long month is 31, so the columns have to give up most
   of their width. The numbers stay - a value you only see after hovering is a
   value you do not see - they are just allowed to wrap onto a second line
   instead of pushing the whole chart into a horizontal scroll. */
.chart.dense { gap: 3px; }
.chart.dense .chart-col { min-width: 26px; max-width: 56px; }
.chart.dense .chart-value { white-space: normal; font-size: 0.58rem; }
.chart.dense .chart-plays { white-space: normal; font-size: 0.55rem; }
.chart.dense .chart-key { font-size: 0.58rem; letter-spacing: 0; }

.top-list { margin-bottom: 30px; }
.top-row {
  display: grid; grid-template-columns: 26px 40px minmax(0, 1fr) minmax(0, 1.1fr) 64px 90px;
  align-items: center; gap: 12px;
  padding: 7px 10px; border-radius: var(--radius-sm);
}
.top-rank { font-size: 0.8rem; color: var(--text-faint); text-align: right; }
.top-art { width: 40px; height: 40px; border-radius: var(--radius-xs); overflow: hidden; background: var(--surface-2); }
.top-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.top-art .art-fallback { font-size: 0.85rem; }
.top-text { display: flex; flex-direction: column; min-width: 0; }
.top-title { font-size: 0.91rem; font-weight: 550; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.top-sub { font-size: 0.8rem; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.top-meter { height: 6px; border-radius: 3px; background: var(--surface-2); overflow: hidden; }
.top-meter .chart-bar { height: 100%; min-height: 0; border-radius: 3px; transition: width 0.25s ease; }
.top-count, .top-time { font-size: 0.82rem; color: var(--text-dim); text-align: right; white-space: nowrap; }

/* The two tables that split a number by library: the playtime per kind, and the
   spoken libraries in the library panel. Same row shape as a top list minus the
   cover, so the three blocks read as one page rather than as three widgets. */
.kind-list { display: flex; flex-direction: column; }
.kind-row {
  display: grid; grid-template-columns: minmax(84px, 1fr) minmax(0, 1.3fr) 46px 62px 92px;
  align-items: center; gap: 12px;
  padding: 9px 10px; border-radius: var(--radius-sm);
  color: inherit; text-decoration: none;
}
/* Nothing was listened to in this library in this period. The row stays - it is
   what says the library is counted here at all - but it does not compete with
   the ones that have something to show. */
.kind-row.quiet { color: var(--text-faint); }
.kind-row.quiet .kind-time, .kind-row.quiet .top-count { color: var(--text-faint); }
.kind-row.total { border-top: 1px solid var(--line); margin-top: 4px; padding-top: 12px; font-weight: 600; }
.kind-row.total .kind-time { color: var(--accent); }
/* The total has no share to draw, but it keeps the cell: the row has to line up
   with the four above it, and a narrow screen drops the meter by that class. */
.kind-row.total .top-meter { background: none; }
.kind-name { font-size: 0.91rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kind-share { font-size: 0.82rem; color: var(--text-dim); text-align: right; white-space: nowrap; }
.kind-time {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size: 0.86rem; text-align: right; white-space: nowrap;
}
.kind-row.wide { grid-template-columns: minmax(0, 1fr) 92px; }
.kind-row.wide:hover { background: var(--surface-2); }
.kind-text { display: flex; flex-direction: column; min-width: 0; gap: 2px; }
.kind-sub { font-size: 0.8rem; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* --- Card grids (albums, artists, genres) --------------------------------- */

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(164px, 1fr));
  gap: 18px 16px;
}
.grid.row { display: flex; gap: 16px; overflow-x: auto; padding-bottom: 6px; }
/* Firefox only: in Chromium `scrollbar-width` switches the ::-webkit-scrollbar
   rules off and brings back the native bar with its arrows. */
@supports not selector(::-webkit-scrollbar) {
  .grid.row { scrollbar-width: thin; }
}
/* min-width has to be said out loud: a flex item is at least as wide as its
   content, and a long title on one line (nowrap, for the ellipsis) is content.
   Without this one card in a shelf grows past the 164px and stands out. */
.grid.row > * { flex: 0 0 164px; min-width: 0; }

/* The other way to show a collection: one row per entry instead of a tile.
   Built to the height of a track row on purpose - a list of albums has to read
   like the list of songs it leads to, so `min-height` and padding are the ones
   from .track-row and nothing else. */
.list-rows { display: flex; flex-direction: column; }
.list-row {
  display: flex; align-items: center; gap: 12px;
  padding: 7px 10px; border-radius: var(--radius-sm);
  min-height: 46px; color: var(--text);
}
.list-art { width: 32px; height: 32px; border-radius: var(--radius-xs); overflow: hidden; background: var(--surface-2); flex: none; }
.list-art.round { border-radius: 50%; }
.list-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.list-art .art-fallback { font-size: 0.8rem; }
.list-art .mosaic { width: 100%; height: 100%; }
.list-text { display: flex; flex-direction: column; min-width: 0; flex: 1; }
/* The type of a track row, to the tenth of a rem: the two lines are what makes
   the row its height, so they have to be the same two lines. */
.list-title { font-size: 0.91rem; font-weight: 550; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.list-sub { font-size: 0.8rem; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.list-meta { font-size: 0.82rem; color: var(--text-dim); font-variant-numeric: tabular-nums; flex: none; }
/* The play button keeps its slot whether or not the row has one, so the counts
   line up down the whole list. */
.list-play, .list-play-gap { width: 28px; height: 28px; flex: none; }
.list-play { opacity: 0; }
.list-row:focus-within .list-play { opacity: 1; }
@media (hover: hover) {
  .list-row:hover { background: var(--surface-2); text-decoration: none; }
  .list-row:hover .list-play { opacity: 1; }
}
/* A finger has no hover to reveal it with. */
@media (hover: none) {
  .list-play { opacity: 1; }
}

/* Tiles or rows: two icons, the segmented switch the statistics page uses.
   The padding has to be reset, and that is the whole reason this rule sets it:
   .seg-switch button carries 4px 11px for the text buttons it was written for,
   and with box-sizing: border-box that leaves 8px of content width in a 30px
   box. The 16px icon then overflows its own track and sits 11px from the left
   and 3px from the right instead of centred - visible as a switch whose icons
   lean right. */
.view-switch .switch-icon {
  display: inline-grid; place-items: center; width: 30px; height: 26px;
  padding: 0; border: none; background: transparent; color: var(--text-dim);
  border-radius: 999px; cursor: pointer;
}
.view-switch .switch-icon.active { background: var(--surface-3); color: var(--text); }
@media (hover: hover) {
  .view-switch .switch-icon:hover { color: var(--text); }
}

.card { cursor: pointer; border: none; background: none; padding: 0; text-align: left; display: block; width: 100%; }
/* The card is built from spans, and an inline box ignores width, aspect-ratio
   and overflow - so every part of it has to be told to be a block. Without
   that the artwork keeps the natural ratio of the image file instead of the
   square tile, and a cover-less card collapses around its fallback. */
.card-art {
  display: block;
  position: relative; width: 100%; aspect-ratio: 1;
  border-radius: var(--radius-sm); overflow: hidden;
  background: var(--surface-2); margin-bottom: 10px;
  box-shadow: var(--shadow-sm);
}
.card.round .card-art { border-radius: 50%; }
/* A book, not a record: its cover is a page and it is taller than it is wide, so
   a square tile cuts the title off the top of it. 2:3 is the proportion nearly
   every cover is drawn to, and it is the one the phone already uses. */
.card.portrait .card-art { aspect-ratio: 2 / 3; }
.card-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.card-title { display: block; font-size: 0.9rem; font-weight: 600; line-height: 1.35; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.card-sub { display: block; font-size: 0.8rem; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.card.round .card-title, .card.round .card-sub { text-align: center; }

/* The play button rises out of the artwork on hover, tinted like the lamp */
.card-play {
  position: absolute; right: 9px; bottom: 9px;
  width: 42px; height: 42px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--accent); color: var(--accent-ink);
  border: none; cursor: pointer;
  opacity: 0; transform: translateY(8px);
  transition: opacity 0.18s ease, transform 0.18s ease;
  box-shadow: var(--shadow-md);
}
.card-play:focus-visible { opacity: 1; transform: translateY(0); }
@media (hover: hover) {
  .card:hover .card-play { opacity: 1; transform: translateY(0); }
}
/* Nothing to hover over, so the button is simply there - without this rule the
   play button on an album, a genre or the singles folder could not be reached
   from a phone at all. */
@media (hover: none) {
  .card-play { opacity: 1; transform: none; }
}

/* Placeholder artwork: the initial over a tinted panel, no stock icon */
.art-fallback {
  width: 100%; height: 100%;
  display: grid; place-items: center;
  background: linear-gradient(150deg, var(--surface-3), var(--surface-2));
  color: var(--text-faint);
  font-size: 1.6rem; font-weight: 700; letter-spacing: -0.02em;
  text-transform: uppercase;
}

/* ============================================================================
   Track list
   ========================================================================== */

.tracks { display: flex; flex-direction: column; }

.track-row {
  display: grid;
  grid-template-columns: 34px minmax(0, 2.4fr) minmax(0, 1.5fr) minmax(0, 1fr) 92px 58px 32px;
  align-items: center; gap: 12px;
  padding: 7px 10px; border-radius: var(--radius-sm);
  min-height: 46px;
}
.track-head {
  border-bottom: 1px solid var(--line); border-radius: 0;
  padding-bottom: 9px; margin-bottom: 5px; min-height: 0;
  position: sticky; top: -26px; background: var(--bg); z-index: 5;
}
.track-head > * { font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--text-faint); }
.th-sort { background: none; border: none; padding: 0; cursor: pointer; text-align: left; display: inline-flex; align-items: center; gap: 5px; font: inherit; letter-spacing: inherit; text-transform: inherit; color: inherit; }
.th-sort.active { color: var(--accent); }
.sort-caret { font-size: 0.6rem; }

/* The file is gone, the rating is not: the row stays, dimmed and unplayable,
   and its title attribute says where the file used to be. */
.track-row.item.missing { opacity: 0.5; }
.track-row.item.missing .track-title { text-decoration: line-through; text-decoration-color: var(--text-faint); }

.track-row.item.playing { background: var(--accent-soft); }
.track-row.item.playing .track-title { color: var(--accent); }
/* The same song, in a list it is not playing from: still found at a glance, but
   it never competes with the list that really is running. */
.track-row.item.playing-elsewhere { background: var(--accent-ghost); }
.track-row.item.playing-elsewhere .track-title { color: var(--accent-dim); }
/* A list of a few thousand songs is a list the browser may skip most of: only
   what is near the viewport is laid out and painted, and the placeholder height
   is a row, so the scrollbar does not jump while it scrolls. */
.track-row.item { content-visibility: auto; contain-intrinsic-size: auto 46px; }

.track-index { font-family: var(--font-mono); font-size: 0.8rem; font-variant-numeric: tabular-nums; color: var(--text-faint); text-align: right; }
.track-index .play-hint { display: none; color: var(--text); }
.track-index button { background: none; border: none; padding: 0; cursor: pointer; display: grid; place-items: center; width: 100%; }

/* Under a pointer the row answers to being pointed at, and the running number
   turns into the play symbol. A finger gets neither - it taps the row itself,
   see the touch block further down. */
@media (hover: hover) {
  .track-row.item:hover { background: var(--surface); }
  .track-row.item.missing:hover { background: var(--surface); opacity: 0.75; }
  .track-row.item:hover .track-index .num-label { display: none; }
  .track-row.item:hover .track-index .play-hint { display: block; }
  .th-sort:hover { color: var(--text-dim); }
}

.track-main { display: flex; align-items: center; gap: 11px; min-width: 0; }
.track-art { width: 38px; height: 38px; border-radius: var(--radius-xs); overflow: hidden; background: var(--surface-2); flex: none; }
.track-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.track-art .art-fallback { font-size: 0.85rem; }
.track-text { min-width: 0; display: flex; flex-direction: column; }
.track-title { font-size: 0.91rem; font-weight: 550; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.track-artist { font-size: 0.8rem; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.track-cell { font-size: 0.84rem; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.track-cell.col-year { font-size: 0.82rem; }
.track-time { font-family: var(--font-mono); font-size: 0.82rem; font-variant-numeric: tabular-nums; color: var(--text-dim); text-align: right; }

.track-row .row-menu { opacity: 0; }
.row-menu:focus-visible { opacity: 1; }
@media (hover: hover) {
  .track-artist a:hover, .track-cell a:hover { color: var(--text); text-decoration: underline; }
  .track-row.item:hover .row-menu { opacity: 1; }
}

/* ----------------------------------------------------------------------------
   Episodes
   A podcast episode is a track row with different questions asked of it. What a
   song answers with an album, a genre and five stars, an episode answers with
   one thing: how much of it is left.
   -------------------------------------------------------------------------- */

.track-row.episode-row { grid-template-columns: 46px minmax(0, 1fr) 150px 66px 32px; }
/* Heard all the way through: still there, still playable, just no longer
   asking to be. */
.track-row.item.heard .track-title { color: var(--text-dim); font-weight: 500; }

.episode-state { font-size: 0.76rem; color: var(--text-dim); min-width: 0; }
.ep-done { display: inline-flex; align-items: center; gap: 5px; color: var(--text-faint); }
.ep-done .icon { flex: none; }

.ep-progress { display: flex; flex-direction: column; gap: 4px; }
.ep-bar {
  height: 3px; border-radius: 999px; background: var(--surface-3); overflow: hidden;
}
.ep-bar > span { display: block; height: 100%; background: var(--accent); border-radius: 999px; }
.ep-left { font-variant-numeric: tabular-nums; color: var(--text-faint); }

/* How far into a book the listener is. One bar for the whole thing, because a
   book is one thing - the files it is made of are never drawn. */
.book-progress { max-width: 520px; margin: 0 0 18px; }
.book-bar {
  height: 5px; border-radius: 999px; background: var(--surface-3); overflow: hidden;
}
.book-bar > span { display: block; height: 100%; background: var(--accent); border-radius: 999px; }
.book-place {
  margin: 7px 0 0; font-size: 0.82rem; color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

/* The show description and the line that says what is half-finished. Both are
   prose under the head, so they read at the width prose reads at. */
.podcast-note {
  max-width: 68ch; margin: 0 0 14px; color: var(--text-dim);
  font-size: 0.88rem; line-height: 1.6;
}
.podcast-resume { margin: 0 0 14px; font-size: 0.86rem; color: var(--text-dim); }
.podcast-resume strong { color: var(--text); font-weight: 600; }

/* Now-playing indicator: three bars moving like a level meter */
.eq { display: flex; align-items: flex-end; gap: 2px; height: 13px; justify-content: center; }
.eq span { width: 2.5px; background: var(--accent); border-radius: 1px; animation: eq-bounce 0.9s ease-in-out infinite; }
.eq span:nth-child(1) { height: 60%; animation-delay: -0.4s; }
.eq span:nth-child(2) { height: 100%; animation-delay: -0.15s; }
.eq span:nth-child(3) { height: 45%; animation-delay: -0.7s; }
.eq.paused span { animation-play-state: paused; }
.eq.elsewhere span { background: var(--accent-dim); }
@keyframes eq-bounce { 0%, 100% { transform: scaleY(0.35); } 50% { transform: scaleY(1); } }

/* Star rating: five buttons. The stars sit in the DOM as 5..1 and are flipped
   back with row-reverse, so hovering star N can light N and everything before
   it through a plain sibling selector - no JavaScript for the hover state. */
.stars { display: flex; flex-direction: row-reverse; justify-content: flex-end; gap: 1px; }
.stars .star {
  background: none; border: none; padding: 1px; cursor: pointer;
  color: var(--text-faint); display: grid; place-items: center;
  line-height: 0;
}
.stars .star .icon { width: 15px; height: 15px; }
.stars .star.on { color: var(--accent); }
/* A rating the server has not confirmed yet: the same amber, turned down. The
   tap has been registered and is queued, and the full colour is what says it
   really arrived. A rating being taken away keeps its stars lit here until the
   server agrees, which is why this can show while the value is already 0. */
.stars.waiting .star.on { color: var(--accent-dim); }
/* A read-only widget is spans, so there is nothing to click anyway - but the
   rule stays for the one that is buttons: the rating dialog renders the same
   widget disabled. pointer-events is what lets a click through to the album
   card underneath. */
.stars.readonly .star { cursor: default; pointer-events: none; }
@media (hover: hover) {
  .stars:not(.readonly):hover .star { color: var(--text-faint); }
  .stars:not(.readonly) .star:hover,
  .stars:not(.readonly) .star:hover ~ .star { color: var(--accent-hi); }
}

/* The record's own rating, shown on an album tile and on an album row. It is
   never set from here - the album's own page is where the stars are clickable -
   so this is a line of five and nothing more. */
/* The alignment is inherited on purpose: .stars is row-reverse, so its own
   justify-content: flex-end is what puts the first star on the left. Setting
   flex-start here would push the row to the right edge of the tile. */
.card-stars { display: block; margin-top: 3px; }
/* Slightly smaller than in a track row: a tile is 164px wide and the title
   under it has to stay the thing you read first. */
.card-stars .stars .star .icon,
.list-stars .stars .star .icon { width: 13px; height: 13px; }
/* In a row the stars sit next to the count on the right, before the play
   button, so a list of records reads title - stars - songs. */
.list-stars { flex: none; }
/* An unrated record shows five empty stars rather than nothing, so the column
   does not go ragged down the list - but they stay quiet. */
.card-stars .stars.readonly .star:not(.on),
.list-stars .stars.readonly .star:not(.on) { opacity: 0.45; }

/* The dialog behind "Bewerten …" in a track's menu: the same widget, at a size
   a thumb can hit. It exists because the rating column is dropped from the
   track list on a narrow screen. */
.rate-picker { display: flex; justify-content: center; padding: 2px 0 14px; }
.rate-picker .stars .star { padding: 6px; }
.rate-picker .stars .star .icon { width: 30px; height: 30px; }

/* Star playlists and genres can both be combined ("4 und 5 Sterne", "Rock und
   Jazz"), so what the list is made of is a row of switches, not links: the
   amber ones are in, the others are out. */
.chip-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; }
.chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 12px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--surface);
  color: var(--text-dim); font-size: 0.82rem; font-weight: 600; cursor: pointer;
}
.chip .icon { width: 13px; height: 13px; }
@media (hover: hover) {
  .chip:hover { color: var(--text); border-color: var(--accent-line); }
}
.chip.active { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent); }

/* Five ratings fit on one line, a hundred genres do not - so the genre picker
   is capped at three lines and folds open, and the switches that are on are
   drawn first so the current selection is never the part that is hidden.
   `--chip-h` is the height of one chip written the way the chip is built, so
   the cap lands exactly between two rows instead of cutting one in half:
   line box (1.5 x 0.82rem) plus its padding and its two borders. */
.chip-picker { --chip-h: calc(1.23rem + 12px); margin-bottom: 20px; }
.chip-picker .chip-row {
  margin-bottom: 0;
  max-height: calc(3 * var(--chip-h) + 2 * 8px);
  overflow: hidden;
}
.chip-picker.open .chip-row { max-height: 46vh; overflow-y: auto; overscroll-behavior: contain; }
/* Only there when something really is behind the fold - app.js measures that
   after the render and sets `.clipped`. */
.chip-more {
  display: none; margin-top: 10px; padding: 4px 0;
  border: none; background: none; cursor: pointer;
  color: var(--text-dim); font-size: 0.82rem; font-weight: 600;
}
.chip-picker.clipped .chip-more { display: inline-block; }
@media (hover: hover) {
  .chip-more:hover { color: var(--accent); }
}

/* Drag to reorder inside a playlist */
.track-row.dragging { opacity: 0.4; }
.track-row.drop-above { box-shadow: inset 0 2px 0 var(--accent); }
.track-row.drop-below { box-shadow: inset 0 -2px 0 var(--accent); }
.drag-handle { cursor: grab; color: var(--text-faint); }

/* --- Detail headers (album, artist, playlist) ----------------------------- */

.detail-head {
  display: flex; align-items: flex-end; gap: 24px;
  margin-bottom: 28px; flex-wrap: wrap;
}
.detail-art {
  width: 190px; height: 190px; flex: none;
  border-radius: var(--radius); overflow: hidden;
  background: var(--surface-2); box-shadow: var(--shadow-md);
}
.detail-art.round { border-radius: 50%; }
/* The same 2:3 as the shelf's tiles, so a book is the same shape wherever it is
   drawn. Only the height changes - the row it sits in keeps its width. */
.detail-art.portrait { height: 285px; }
/* The artwork opens at full size, so the tile is a button. It is a flex item
   and therefore already a block, but a button says nothing about that itself. */
button.detail-art { display: block; padding: 0; border: none; cursor: zoom-in; }
@media (hover: hover) {
  button.detail-art:hover { box-shadow: var(--shadow-lg); }
}
.detail-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.detail-art .art-fallback { font-size: 3.2rem; }
.detail-text { flex: 1; min-width: 240px; }
.detail-text h1 { margin: 6px 0 8px; font-size: 2.1rem; line-height: 1.15; }
.detail-facts { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; color: var(--text-dim); font-size: 0.88rem; }
.detail-facts .dot { color: var(--text-faint); }
.detail-actions { display: flex; gap: 9px; margin-top: 18px; flex-wrap: wrap; align-items: center; }
/* The record's own stars, set from here and nowhere else. Set apart from the
   buttons next to them by a gap of their own and their own little title: those
   change the shared library, this one belongs to the account. */

/* Cover picker in the album editor: the artwork next to what it can be */
.cover-edit { display: flex; gap: 16px; align-items: flex-start; margin-bottom: 20px; flex-wrap: wrap; }
.cover-edit-art {
  width: 152px; height: 152px; flex: none;
  border-radius: var(--radius-sm); overflow: hidden; background: var(--surface-2);
}
.cover-edit-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* A picture that is not square is dragged in here to pick its square section.
   `touch-action: none` is what stops the drag from scrolling the dialog. */
.cover-edit-art.movable { cursor: grab; touch-action: none; }
.cover-edit-art.dragging { cursor: grabbing; }
.cover-edit-art .art-fallback { font-size: 2rem; }
.cover-edit-side { flex: 1; min-width: 190px; }
.cover-edit-side .panel-hint { margin-bottom: 10px; }
.cover-edit-side .form-actions { margin-top: 0; justify-content: flex-start; }

/* A collection without artwork of its own - a playlist, a star playlist, a
   genre - shows the covers of the first four albums in it as a 2x2 mosaic.
   Both tracks are stated: an implicit `auto` row does not take the free height
   of the tile, so the pictures would size themselves instead of the box. */
.mosaic {
  display: grid;
  grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr;
  width: 100%; height: 100%;
}
.mosaic img { width: 100%; height: 100%; object-fit: cover; display: block; }
.mosaic.single { grid-template-columns: 1fr; grid-template-rows: 1fr; }

/* --- Empty states --------------------------------------------------------- */

.empty {
  text-align: center; padding: 56px 24px;
  border: 1px dashed var(--line); border-radius: var(--radius);
  color: var(--text-dim);
}
.empty h3 { margin-bottom: 6px; color: var(--text); }
.empty p { margin: 0 0 18px; font-size: 0.9rem; }
/* The bottom margin is the gap before an action below. Without one it would
   push the content off the vertical centre of the padded box. */
.empty > :last-child { margin-bottom: 0; }
.empty.small { padding: 28px 20px; }

.loading { padding: 60px; text-align: center; color: var(--text-faint); }

/* ============================================================================
   Player bar - the transport
   ========================================================================== */

.player {
  grid-row: 2; grid-column: 1 / -1;
  position: relative;
  background: var(--surface);
  border-top: 1px solid var(--line);
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(320px, 1.5fr) minmax(0, 1fr);
  align-items: center;
  gap: 16px;
  padding: 0 18px;
  z-index: 40;
}

/* The signature: the top edge of the transport IS the seek bar. Full width,
   edge to edge, amber fill - grab it anywhere. */
.seek-rail {
  position: absolute; top: -1px; left: 0; right: 0;
  height: 12px; transform: translateY(-5px);
  cursor: pointer; padding: 0; border: none; background: none;
  display: flex; align-items: center;
  /* The rail handles the gesture itself. Without this the browser claims a
     drag that starts here as a scroll and the seek never gets it. */
  touch-action: none;
}
.seek-track { position: relative; width: 100%; height: 3px; background: var(--line); }
.seek-fill { position: absolute; inset: 0 auto 0 0; background: var(--accent); width: 0; }
.seek-buffer { position: absolute; inset: 0 auto 0 0; background: var(--surface-3); width: 0; }
.seek-knob {
  position: absolute; top: 50%; left: 0;
  width: 11px; height: 11px; border-radius: 50%;
  background: var(--accent); transform: translate(-50%, -50%) scale(0);
  transition: transform 0.13s ease; box-shadow: 0 0 0 3px var(--surface);
}
.seek-rail:focus-visible .seek-track { height: 5px; }
.seek-rail:focus-visible .seek-knob { transform: translate(-50%, -50%) scale(1); }
.seek-rail.dragging .seek-track { height: 5px; }
.seek-rail.dragging .seek-knob { transform: translate(-50%, -50%) scale(1.3); }
@media (hover: hover) {
  .seek-rail:hover .seek-track { height: 5px; }
  .seek-rail:hover .seek-knob { transform: translate(-50%, -50%) scale(1); }
}
/* A thumb needs a taller strip to find than the hairline a mouse aims at. The
   knob stays hidden here: on the top edge of the bar it would be cut in half by
   the edge of the screen at the start of a track. The full screen shows it (see
   .player.expanded), where the rail sits inside the padding. */
@media (hover: none) {
  .seek-rail { height: 16px; transform: translateY(-7px); }
}

/* The head of the fullscreen player on a phone - the bar itself never shows it,
   see .player.expanded in the responsive section. */
.now-head { display: none; align-items: center; gap: 10px; }
.now-source { color: var(--text-dim); }

/* Left: what is playing */
.now { display: flex; align-items: center; gap: 13px; min-width: 0; }
.now-art {
  width: 56px; height: 56px; flex: none;
  border-radius: var(--radius-sm); overflow: hidden;
  background: var(--surface-2); cursor: pointer; border: none; padding: 0;
}
.now-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.now-art .art-fallback { font-size: 1.2rem; }
.now-text { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
/* The title is the way into the big view, so it says so under the pointer. */
.now-title { font-size: 0.92rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
@media (hover: hover) {
  .now-title:hover { text-decoration: underline; }
}
.now-artist { font-size: 0.81rem; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (hover: hover) {
  .now-artist a:hover { color: var(--text); text-decoration: underline; }
}
/* The rating of what is playing, and next to it the one thing you may want to
   do with a song you just heard: put it on a list. Deliberately nothing else -
   everything further lives in the track's own menu. */
.now-actions { display: flex; align-items: center; gap: 7px; margin-top: 3px; }
.now .stars .star .icon { width: 13px; height: 13px; }
.now-add { width: 22px; height: 22px; border-radius: var(--radius-xs); color: var(--text-faint); }
.now-add .icon { width: 15px; height: 15px; }
@media (hover: hover) {
  .now-add:hover { color: var(--accent); background: var(--accent-soft); }
}

/* Centre: transport + counter */
.transport { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.transport-buttons { display: flex; align-items: center; gap: 6px; }
.transport-buttons .icon-btn { width: 36px; height: 36px; }
.play-btn {
  width: 42px; height: 42px; border-radius: 50%;
  background: var(--accent); color: var(--accent-ink);
  border: none; cursor: pointer; display: grid; place-items: center;
  flex: none;
}
@media (hover: hover) {
  .play-btn:hover { background: var(--accent-hi); }
}
.counter {
  display: flex; align-items: center; gap: 9px;
  font-family: var(--font-mono); font-size: 0.75rem;
  font-variant-numeric: tabular-nums; color: var(--text-dim);
}
.counter .sep { color: var(--text-faint); }
.counter .elapsed { color: var(--text); }

/* Right: level meter, volume, queue */
.player-right { display: flex; align-items: center; justify-content: flex-end; gap: 6px; }

/* The live level meter - a real AnalyserNode, not decoration */
.meter { width: 72px; height: 26px; display: block; opacity: 0.9; }

.volume { display: flex; align-items: center; gap: 7px; }
.volume-slider {
  -webkit-appearance: none; appearance: none;
  width: 88px; height: 3px; border-radius: 2px;
  background: var(--line); cursor: pointer; padding: 0;
}
.volume-slider::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 11px; height: 11px; border-radius: 50%; background: var(--accent); border: none;
}
.volume-slider::-moz-range-thumb { width: 11px; height: 11px; border-radius: 50%; background: var(--accent); border: none; }
.volume-slider:focus { outline: none; }
.volume-slider:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

.repeat-one { position: relative; }
.repeat-one::after {
  content: "1"; position: absolute; right: 4px; bottom: 3px;
  font-family: var(--font-mono); font-size: 0.55rem; font-weight: 700;
  background: var(--accent); color: var(--accent-ink);
  border-radius: 3px; padding: 0 2px; line-height: 1.3;
}

/* ============================================================================
   Queue panel
   ========================================================================== */

.queue {
  position: fixed; right: 0; top: 0; bottom: var(--player-h);
  width: 340px; z-index: 45;
  background: var(--surface); border-left: 1px solid var(--line);
  display: flex; flex-direction: column;
  transform: translateX(100%); transition: transform 0.22s ease, box-shadow 0.22s ease;
}
/* The shadow only while open: parked off-screen, its 48 px blur still reached
   into the window and darkened the whole right edge, scrollbar included. */
.queue.open { transform: translateX(0); box-shadow: var(--shadow-lg); }
.queue-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 15px 16px 12px; border-bottom: 1px solid var(--line); }
.queue-list { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 8px; }
.queue-section { padding: 14px 8px 6px; }
.queue-item {
  display: grid; grid-template-columns: 30px 1fr auto; align-items: center; gap: 10px;
  padding: 7px 8px; border-radius: var(--radius-sm); cursor: pointer;
}
@media (hover: hover) {
  .queue-item:hover { background: var(--surface-2); }
}
.queue-item.playing { background: var(--accent-soft); }
.queue-item.playing .queue-title { color: var(--accent); }
.queue-pos { font-family: var(--font-mono); font-size: 0.75rem; color: var(--text-faint); text-align: right; }
.queue-text { min-width: 0; }
.queue-title { font-size: 0.87rem; font-weight: 550; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.queue-artist { font-size: 0.78rem; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.queue-actions { display: flex; align-items: center; }
/* The handle is the only part of the row a drag may start on. On a touch screen
   a drag anywhere else is the list being scrolled - the same gesture - and
   `touch-action: none` is what hands this one to the app instead of to the
   browser's own panning. */
.queue-drag { cursor: grab; touch-action: none; }
.queue-drag:active { cursor: grabbing; }
.queue-item.dragging { opacity: 0.4; }
.queue-item.drop-above { box-shadow: inset 0 2px 0 var(--accent); }
.queue-item.drop-below { box-shadow: inset 0 -2px 0 var(--accent); }

/* A chapter row is a queue row, so it takes the grid and the amber above and
   only has to undo what a <button> brings with it. */
.chapter-row {
  width: 100%; text-align: left; font: inherit; color: inherit;
  background: none; border: 0; appearance: none;
}
.chapter-row .queue-title { white-space: normal; }
.chapter-row.playing .queue-title { color: var(--accent); }

/* Where the chapters of a book begin. Hairlines rather than notches: on a
   fifty-hour book there are hundreds of them, and anything heavier turns the
   rail into a comb. They sit under the knob and over the fill, so the amber
   still reads as one continuous run. */
.seek-chapters { position: absolute; inset: 0; pointer-events: none; }
.seek-chapter {
  position: absolute; top: 0; bottom: 0; width: 1px;
  background: var(--bg); opacity: 0.75;
}

/* ============================================================================
   Lyrics
   ========================================================================== */

/* Moving the text against the song. It floats over the bottom right of whichever
   text view is up - the side panel or the big view's Songtext tab - and has no
   backdrop, because the lines behind it have to keep running while the number
   moves. Above the panel it sits on (z 46) and clear of the transport. */
.lyric-offset {
  position: fixed; z-index: 47;
  right: 16px; bottom: calc(var(--player-h) + 16px);
  width: 292px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: 12px 14px 11px;
  display: flex; flex-direction: column; gap: 9px;
}
.lyric-offset[hidden] { display: none; }
.lyric-offset-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.lyric-offset-set { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.lyric-offset-set .icon-btn {
  width: 32px; height: 32px; flex: none;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface);
}
@media (hover: hover) {
  .lyric-offset-set .icon-btn:hover { color: var(--accent); border-color: var(--accent-line); }
}
/* The number is the thing being read while the eye is really on the text, so it
   is big, monospace and never moves - tabular figures and the minus sign U+2212,
   which is as wide as a digit where a hyphen is not. */
.lyric-offset-value {
  flex: 1; text-align: center;
  font-family: var(--font-mono); font-size: 1.15rem; font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}
.lyric-offset-range { width: 100%; margin: 0; accent-color: var(--accent); }
.lyric-offset-foot {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  font-size: 0.76rem; color: var(--text-faint);
}
.lyric-offset-hint { flex: none; }
.lyric-offset-reset {
  background: none; border: none; padding: 0; cursor: pointer;
  color: var(--text-dim); font-size: 0.76rem; text-decoration: underline;
  text-underline-offset: 2px;
}
@media (hover: hover) {
  .lyric-offset-reset:hover { color: var(--text); }
}

/* The panel is the queue panel: same place, same slide, same way out. It is the
   same kind of thing - a side panel about the track that is playing - and the
   two are mutually exclusive, so they can share the geometry outright. */
.lyrics-panel { z-index: 46; }
.lyrics-body {
  /* offsetTop of a line is read against this box to centre it, so the box has
     to be the positioning context. */
  position: relative;
  flex: 1; overflow-y: auto; overscroll-behavior: contain;
  padding: 16px 20px 40vh;
}
/* The tail of padding above is deliberate: without it the last lines can never
   reach the middle of the panel, and the text stops following along right where
   a song usually says the thing you looked it up for. */

.lyric-lines { display: flex; flex-direction: column; gap: 2px; }
/* A timed line is a button - clicking it jumps the song there - and an untimed
   one a paragraph. They have to read as one text either way, so the button is
   stripped back to the paragraph it replaces. The padding sits inside it, so
   the whole strip is the target and not just the glyphs. */
.lyric-line {
  margin: 0; padding: 5px 0;
  border: none; background: none; width: 100%; text-align: left;
  font-family: inherit;
  font-size: 1rem; line-height: 1.45; font-weight: 550;
  color: var(--text-faint);
  transition: color 0.2s ease;
}
button.lyric-line { cursor: pointer; }
button.lyric-line:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* An empty line is an instrumental gap. It keeps the timing honest but must not
   take a whole row of a phone screen. */
.lyric-line.is-gap { padding: 0; height: 8px; }
/* A line already sung stays readable - it is the context for the one that is
   running - while everything still ahead is quieter. */
.lyric-line.is-past { color: var(--text-dim); }
.lyric-line.is-now { color: var(--accent); font-size: 1.12rem; }
@media (hover: hover) {
  /* Brightening a line on the way to it is what says it can be aimed at. The
     one being sung already carries the accent and must not be dulled to it. */
  button.lyric-line:hover { color: var(--text); }
  button.lyric-line.is-now:hover { color: var(--accent); }
}
/* Without timestamps nothing is highlighted, so the whole text reads at once
   instead of sitting there greyed out waiting for a cue that never comes. */
.lyric-lines.is-plain .lyric-line { color: var(--text); font-weight: 400; }

/* The line being sung, between the artwork and the title of the fullscreen
   player. Not part of the bar - there is no room, and it is a phone feature. */
.now-line {
  display: none;
  border: none; background: none; padding: 0;
  color: var(--accent); text-align: center; cursor: pointer;
  font-size: 1.05rem; line-height: 1.4; font-weight: 600;
  font-family: inherit;
}

/* ============================================================================
   Lightbox - artwork at full size
   ========================================================================== */

/* No chrome and no title bar: the picture is the whole dialog, and anywhere you
   click is the way out. */
.lightbox {
  position: fixed; inset: 0; z-index: 250;
  background: rgba(8, 6, 11, 0.88);
  display: grid; place-items: center;
  padding: 6vh 5vw; cursor: zoom-out;
  animation: lightbox-in 0.15s ease;
}
.lightbox img {
  display: block; max-width: 100%; max-height: 88vh;
  border-radius: var(--radius); box-shadow: var(--shadow-lg);
}
.lightbox-close { position: fixed; top: 16px; right: 16px; color: var(--text); }
@keyframes lightbox-in { from { opacity: 0; } }

/* ============================================================================
   The big view: the song, its text, the visualizer
   ========================================================================== */
/* Takes the place of the content area rather than covering the screen: the
   sidebar and the topbar stay where they are, so the library is one click away.
   It is a flex child of .main next to #content, which is hidden while it is up -
   that way the page underneath keeps its scroll position and nothing is layered
   over anything. */

.bigview {
  flex: 1; min-height: 0;
  display: flex; flex-direction: column;
  background: var(--bg);
}
.bigview-head {
  display: flex; align-items: center; gap: 12px;
  padding: 14px 22px; border-bottom: 1px solid var(--line);
}
.bigview-tabs { display: inline-flex; gap: 2px; background: var(--surface); border: 1px solid var(--line); border-radius: 999px; padding: 2px; }
.bigview-tab {
  border: none; background: transparent; color: var(--text-dim);
  font-size: 0.82rem; font-weight: 600; padding: 6px 14px;
  border-radius: 999px; cursor: pointer;
}
.bigview-tab.active { background: var(--surface-3); color: var(--text); }
@media (hover: hover) {
  .bigview-tab:hover { color: var(--text); }
}
.bigview-head .icon-btn { margin-left: auto; }
/* Two buttons on the right of the head: only the first one needs pushing over. */
.bigview-head .offset-toggle + .icon-btn { margin-left: 0; }

.bigview-body { flex: 1; min-height: 0; display: flex; }
.bigview-pane { flex: 1; min-height: 0; min-width: 0; }

/* The song itself: artwork as large as the height allows, its name under it. */
.pane-player {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 26px; padding: 30px 24px; text-align: center;
}
.bigview-art {
  width: min(46vh, 380px); aspect-ratio: 1;
  border-radius: var(--radius); overflow: hidden;
  background: var(--surface-2); box-shadow: var(--shadow-lg);
  display: grid; place-items: center;
}
.bigview-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.bigview-art .art-fallback { font-size: 4rem; }
.bigview-meta { display: flex; flex-direction: column; gap: 6px; max-width: 60ch; }
.bigview-title { font-size: 1.9rem; font-weight: 700; letter-spacing: -0.02em; margin: 0; }
.bigview-sub { color: var(--text-dim); font-size: 1rem; margin: 0; }
/* The line being sung, when the file brought a timed text with it. It reserves
   no room at all while there is none - same rule as the phone's `.now-line`. */
.bigview-line { display: none; color: var(--accent); font-size: 1.05rem; font-weight: 600; margin: 8px 0 0; }
.bigview-line.is-on { display: block; }

/* The text, in the same box the side panel uses - one renderer fills both. It
   has a whole page here instead of a 320 px panel, so the type is set to match
   the room: this is a text to read across the room, not out of the corner. */
.pane-lyrics { display: flex; justify-content: center; overflow: hidden; }
.pane-lyrics .lyrics-body { width: 100%; max-width: 720px; overflow-y: auto; overscroll-behavior: contain; padding: 34px 24px 40vh; }
.pane-lyrics .lyric-lines { gap: 6px; }
.pane-lyrics .lyric-line { font-size: 1.45rem; line-height: 1.4; padding: 7px 0; }
.pane-lyrics .lyric-line.is-now { font-size: 1.6rem; }
.pane-lyrics .lyric-line.is-gap { height: 14px; padding: 0; }

/* Flex rather than a percentage height: the canvas is stretched by the pane,
   which is the one thing a percentage inside a flex item cannot be trusted to
   do the same way in every browser. */
.pane-visual { display: flex; padding: 20px 24px 26px; }
.pane-visual canvas { flex: 1; min-width: 0; display: block; }

/* ============================================================================
   Modals, menus, toasts
   ========================================================================== */

.modal-backdrop {
  position: fixed; inset: 0; z-index: 100;
  background: rgba(8, 6, 11, 0.66);
  display: flex; align-items: flex-start; justify-content: center;
  padding: 8vh 16px 16px; overflow-y: auto;
}
.modal {
  width: 100%; max-width: 480px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow-lg);
}
.modal.wide { max-width: 660px; }
.modal-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 20px; border-bottom: 1px solid var(--line); }
.modal-body { padding: 20px; }
.modal-body > p:first-child { margin-top: 0; }
.modal-foot { display: flex; justify-content: flex-end; gap: 10px; padding: 0 20px 20px; }

.picker-list { max-height: 46vh; overflow-y: auto; overscroll-behavior: contain; margin: 0 -6px; }
.picker-item {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 10px 12px; border-radius: var(--radius-sm);
  border: none; background: none; cursor: pointer; text-align: left; font-size: 0.92rem;
}
@media (hover: hover) {
  .picker-item:hover { background: var(--surface-2); }
}
.picker-item .count { margin-left: auto; font-family: var(--font-mono); font-size: 0.75rem; color: var(--text-faint); }

.context-menu {
  position: fixed; z-index: 150; min-width: 210px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-sm); box-shadow: var(--shadow-lg); padding: 6px;
}

/* --- Searching inside the list on screen (Ctrl+F) -------------------------- */

/* Over the page rather than in it: the field appears and goes without the list
   under it moving a pixel, which is the whole point of a thing you open for one
   word and close again. Below the overlays and above the page. */
.find-bar {
  position: fixed; top: 14px; right: 26px; z-index: 48;
  display: none; align-items: center; gap: 8px;
  padding: 7px 8px 7px 12px; border-radius: 999px;
  background: var(--surface-2); border: 1px solid var(--line);
  box-shadow: var(--shadow-lg);
}
.find-bar.is-on { display: flex; }
.find-icon { display: grid; place-items: center; color: var(--text-faint); }
.find-input {
  width: 230px; border: none; background: none; color: var(--text);
  font-size: 0.9rem; font-family: inherit; outline: none;
}
.find-count {
  font-family: var(--font-mono); font-size: 0.7rem; color: var(--text-faint);
  white-space: nowrap;
}
/* A row is a grid, and a class beats the attribute - so the plain `hidden` the
   filter sets would do nothing at all without this. */
.track-row[hidden] { display: none; }
@media (max-width: 720px) {
  .find-bar { left: 12px; right: 12px; top: 10px; }
  .find-input { width: auto; flex: 1; min-width: 0; }
}

.toasts { position: fixed; bottom: calc(var(--player-h) + 18px); left: 50%; transform: translateX(-50%); z-index: 300; display: flex; flex-direction: column; gap: 8px; align-items: center; pointer-events: none; }
.toast {
  background: var(--surface-3); color: var(--text);
  border: 1px solid var(--line); border-radius: 999px;
  padding: 9px 18px; font-size: 0.87rem; font-weight: 550;
  box-shadow: var(--shadow-md);
  animation: toast-in 0.18s ease;
}
.toast.err { background: var(--danger-soft); color: var(--danger); border-color: transparent; }
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } }

/* ============================================================================
   Settings
   ========================================================================== */

.panel {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 22px; margin-bottom: 18px;
}
.panel > h2 { margin-bottom: 4px; }
.panel-hint { color: var(--text-dim); font-size: 0.87rem; margin: 0 0 18px; }
.setting-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 11px 0; border-bottom: 1px solid var(--line-soft); }
.setting-row:last-child { border-bottom: none; }
.setting-label { font-size: 0.91rem; font-weight: 550; }
.setting-sub { font-size: 0.82rem; color: var(--text-dim); }
/* The layout a library root is read by, under the path it applies to.
   Fainter than the path so the two mono lines are not read as two
   folders. */
.setting-layout { color: var(--text-faint); }

.progress { height: 4px; border-radius: 2px; background: var(--surface-2); overflow: hidden; margin-top: 10px; }
.progress-fill { height: 100%; width: 0; background: var(--accent); transition: width 0.3s ease; }

/* Until the walk has counted the files there is no percentage to show, so the
   bar sweeps instead of sitting at 0 %. */
.progress.indeterminate .progress-fill { width: 32%; transition: none; animation: progress-sweep 1.2s ease-in-out infinite; }
@keyframes progress-sweep {
  from { transform: translateX(-100%); }
  to { transform: translateX(320%); }
}

.scan-progress { padding: 12px 0 2px; }
.scan-progress-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }

/* Spacing utilities. The Content-Security-Policy forbids inline style
   attributes, so anything that would have been a one-off style="..." gets a
   class instead. */
.mt-sm { margin-top: 8px; }
.mt-md { margin-top: 12px; }
.mt-lg { margin-top: 18px; }
.field-bottom { justify-content: flex-end; }
.push-right { margin-left: auto; }
.inline-actions { display: flex; gap: 2px; }

.user-row { display: flex; align-items: center; gap: 12px; padding: 11px 0; border-bottom: 1px solid var(--line-soft); }
.user-row:last-child { border-bottom: none; }
.user-row-text { flex: 1; min-width: 0; }
.user-row-name { font-weight: 600; display: flex; align-items: center; gap: 7px; flex-wrap: wrap; font-size: 0.92rem; }
.user-row-sub { font-size: 0.8rem; color: var(--text-dim); }
.badge { font-size: 0.66rem; font-weight: 700; padding: 1px 7px; border-radius: 999px; letter-spacing: 0.04em; }
.badge.admin { background: var(--accent-soft); color: var(--accent); }
.badge.you { background: var(--surface-2); color: var(--text-dim); }
.badge.gone { background: var(--surface-2); color: var(--text-faint); }
.badge.ok { background: var(--ok-soft); color: var(--ok); }

/* Import notices: a persistent list of what the CSV import could not find */
.issue-list { max-height: 460px; overflow-y: auto; margin: 0 -6px; }
.issue {
  display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 12px;
  padding: 9px 12px; border-radius: var(--radius-sm);
}
@media (hover: hover) {
  .issue:hover { background: var(--surface-2); }
}
.issue-text { min-width: 0; }
.issue-title { font-size: 0.89rem; font-weight: 550; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.issue-sub { font-size: 0.8rem; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.issue-playlist { font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-faint); }
/* A missing song carries one column more than an import notice: its stars,
   between what it is and the button that lets go of it. */
.issue.missing { grid-template-columns: 1fr auto auto; }
.issue.missing .stars { flex: none; }
/* Where the file used to be. Quiet and monospaced, because it is there to be
   recognised rather than read - and it is the one line that says why this entry
   is not simply a song. */
.issue-path {
  font-family: var(--font-mono); font-size: 0.7rem; color: var(--text-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The record it was on, where that record still exists. Underlined, because
   nothing else in this row is clickable and a bare colour would not say so. */
.issue-sub a { text-decoration: underline; text-underline-offset: 2px; }
@media (hover: hover) {
  .issue-sub a:hover { color: var(--accent); }
}
@media (max-width: 640px) {
  .issue.missing { grid-template-columns: 1fr auto; }
  .issue.missing .stars { grid-column: 1; }
}
/* Counted down from the page while entries go, so it has to be able to hide -
   and `display` on the class would beat the attribute on its own. */
.issue-count[hidden] { display: none; }
.issue-count {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--danger-soft); color: var(--danger);
  border-radius: 999px; padding: 2px 10px; font-size: 0.78rem; font-weight: 650;
}
.nav-badge {
  min-width: 18px; height: 18px; padding: 0 5px;
  border-radius: 999px; background: var(--danger); color: var(--bg);
  font-size: 0.68rem; font-weight: 700; display: grid; place-items: center;
  font-family: var(--font-mono);
}

.drop-zone {
  border: 1px dashed var(--line); border-radius: var(--radius);
  padding: 26px; text-align: center; color: var(--text-dim);
  font-size: 0.89rem; cursor: pointer;
}
/* .icon is display:block, so text-align does not reach it. */
.drop-zone .icon { margin: 0 auto; }
.drop-zone.over { border-color: var(--accent); background: var(--accent-soft); color: var(--text); }
@media (hover: hover) {
  .drop-zone:hover { border-color: var(--accent); background: var(--accent-soft); color: var(--text); }
}

.import-summary { display: flex; gap: 22px; flex-wrap: wrap; margin: 16px 0; }
.import-stat { display: flex; flex-direction: column; gap: 3px; }
.import-stat .value { font-family: var(--font-mono); font-size: 1.35rem; font-weight: 600; }
.import-stat.matched .value { color: var(--ok); }
.import-stat.missing .value { color: var(--danger); }

/* ============================================================================
   Responsive
   ========================================================================== */

/* The scrim under the sidebar drawer. It only exists while the drawer is open,
   and it is what makes tapping next to the sidebar close it - without it the
   only way out is picking something from the list. */
.sidebar-backdrop { display: none; position: fixed; inset: 0; background: rgba(8, 6, 11, 0.6); z-index: 25; }

@media (max-width: 1100px) {
  .track-row { grid-template-columns: 34px minmax(0, 2.4fr) minmax(0, 1.4fr) 92px 58px 32px; }
  .track-row .col-genre { display: none; }
  /* An episode has no genre column to drop, so it keeps its five. */
  .track-row.episode-row { grid-template-columns: 46px minmax(0, 1fr) 130px 62px 32px; }
}

@media (max-width: 900px) {
  .shell, .shell.sidebar-folded { grid-template-columns: minmax(0, 1fr); }
  /* Here the sidebar is a drawer, so folding it away is meaningless - and the
     preference is the account's, so a phone can well arrive with it set. Every
     part of the folded look is undone rather than only the toggle hidden. */
  .shell.sidebar-folded .sidebar { display: flex; width: var(--sidebar-w); }
  .shell.sidebar-folded .sidebar-head { padding: 16px 18px 14px; }
  .shell.sidebar-folded .sidebar-head .sidebar-toggle { margin-left: auto; }
  .shell.sidebar-folded .brand-name { opacity: 1; max-width: 12ch; }
  .shell.sidebar-folded .sidebar-scroll { opacity: 1; visibility: visible; }
  .sidebar-toggle { display: none !important; }
  .sidebar {
    position: fixed; left: 0; top: 0; bottom: 0; width: var(--sidebar-w);
    transform: translateX(-100%); transition: transform 0.22s var(--sheet-ease);
    padding-top: var(--safe-top);
  }
  .lyric-offset { left: 12px; right: 12px; width: auto; }
  .sidebar.open { transform: translateX(0); }
  .sidebar.open ~ .sidebar-backdrop { display: block; }
  .main { grid-column: 1; }
  .topbar { padding-top: calc(12px + var(--safe-top)); }
  .topbar .menu-toggle { display: inline-grid; }
  .content { padding: 20px 16px 32px; }
  /* The sticky header cancels the padding above it, so the offset has to be the
     padding this layout uses - with the desktop's 26 px it would stick 6 px too
     high and cut its own labels off. */
  .track-head { top: -20px; }
  .detail-art { width: 140px; height: 140px; }
  .detail-art.portrait { height: 210px; }
  .detail-text h1 { font-size: 1.6rem; }
  .queue { width: 300px; padding-top: var(--safe-top); }
  /* A dialog on a phone comes up from the bottom edge like every other sheet in
     the system, instead of hanging in the middle of the screen. */
  .modal-backdrop { align-items: flex-end; padding: 0; }
  .modal, .modal.wide {
    max-width: none; max-height: 92dvh;
    border-radius: var(--radius) var(--radius) 0 0;
    display: flex; flex-direction: column;
    animation: sheet-up 0.26s var(--sheet-ease);
  }
  .modal-body { overflow-y: auto; overscroll-behavior: contain; padding-bottom: calc(20px + var(--safe-bottom)); }
  .modal-foot { padding-bottom: calc(20px + var(--safe-bottom)); }
}

@media (max-width: 760px) {
  /* The transport keeps its height and adds whatever the phone's gesture bar
     covers, so the play button never ends up under it. */
  :root { --player-h: calc(76px + var(--safe-bottom)); }
  /* The album and the year go, the rating stays: it is what the star playlists
     are built from, and a window at half a screen has room for it. What gives
     way instead is the title, which cuts off with an ellipsis and says its full
     name on hover (see the truncation handler in app.js). */
  .track-row { grid-template-columns: 30px minmax(0, 1fr) 92px 58px 32px; }
  .track-row .col-album, .track-row .col-year { display: none; }
  .track-row.episode-row { grid-template-columns: 40px minmax(0, 1fr) 96px 58px 32px; }
  /* Three columns again, and the outer two equal: that is the whole reason the
     transport sits in the middle of the bar instead of being pushed to one
     side by however long the title next to it happens to be. */
  .player { grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); gap: 10px; padding: 0 12px var(--safe-bottom); }
  /* The slider is the one control here that can give way; the buttons keep
     their size, so mute, the visualizer and the queue stay reachable. */
  .volume-slider { width: 64px; }
  .now-art { width: 46px; height: 46px; }
  /* The bar has room for the interpret, not for the album behind it. */
  .player:not(.expanded) .now-album { display: none; }
  /* Six numbers over a full screen before the library starts: the readout says
     the same thing in half the height here. */
  .readout-cell { padding: 11px 14px; }
  .readout-value { font-size: 1.2rem; }
  /* The word next to the chart icon is the first thing to give way; the icon
     alone still says which page it leads to. */
  .top-link-label { display: none; }
  .top-link { padding: 0; width: 34px; justify-content: center; }
  /* Five widths at full size are wider than a phone. The switch itself stays -
     it is the only way to change what the whole page below it shows. */
  .seg-switch button { padding: 4px 8px; font-size: 0.73rem; }
  .period-title { font-size: 0.86rem; }
  .readout { grid-template-columns: 1fr 1fr; }
  /* The split readout keeps its two kinds of row here, they just get narrower:
     the counts two to a row, the wide values across the whole width. */
  .readout.split { grid-template-columns: 1fr 1fr; }
  .readout.split > .readout-cell { grid-column: span 1; }
  .readout.split > .span-5,
  .readout.split > .span-10 { grid-column: span 2; }
  .grid { grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: 14px 12px; }
  .queue { width: 100%; }
  /* The bar is the first thing to go when the row gets narrow - the number
     next to it says the same thing. */
  .top-row { grid-template-columns: 22px 36px minmax(0, 1fr) 46px; gap: 10px; }
  .top-meter, .top-time { display: none; }
  .kind-row { grid-template-columns: minmax(0, 1fr) 56px 86px; gap: 10px; }
  .kind-row .kind-share { display: none; }
  .kind-row.wide { grid-template-columns: minmax(0, 1fr) 86px; }
}

/* The level meter is the widest thing in the bar and the one that says the
   least; the fullscreen visualizer shows the same signal with room for it. */
@media (max-width: 900px) {
  .meter { display: none; }
}

/* Below this the volume slider has nowhere left to shrink to. The mute button
   stays, and the mouse wheel over it still changes the volume.
   The rating and the plus go with it: from here on the title next to them has
   less width than the two controls need, and both are still a tap away - in
   the row's own menu, and on a phone in the full screen. */
@media (max-width: 640px) {
  .volume-slider { display: none; }
  .now .now-actions { display: none; }
}

/* A real phone. Here the bar cannot be a transport and a panel at the same
   time, and it does not have to be: tapping what is playing opens the full
   screen, which has room for the rating, the counter and the rest. */
@media (max-width: 560px) {
  .track-row { grid-template-columns: 30px minmax(0, 1fr) 58px 32px; }
  .track-row .col-stars { display: none; }
  /* The bar survives where the words do not: it is the whole message. The
     columns around it give up what they can, because the title is what is
     really being read here and it has an extra column to compete with. */
  .track-row.episode-row { grid-template-columns: 34px minmax(0, 1fr) 44px 54px 30px; gap: 8px; }
  .episode-row .ep-left, .episode-row .ep-word { display: none; }
  .player { grid-template-columns: minmax(0, 1fr) auto; }
  .player-right { display: none; }
  .transport .counter { display: none; }
  /* Not even the two icons fit next to the search field here, so the account
     menu carries both pages instead. */
  .top-link { display: none; }
  .dropdown-item.only-compact { display: flex; }
}

/* --- The transport as a screen of its own (phones only) -------------------- */
/* Tapping what is playing blows the bar up into a full screen, the way a
   streaming app does. Deliberately the same element: every control in here is
   the one from the bar, so state, wiring and rendering are shared and only the
   layout changes. */
@media (max-width: 900px) {
  .player.expanded {
    position: fixed; inset: 0; z-index: 60;
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "head" "now" "seek" "transport" "extras";
    grid-template-rows: auto minmax(0, 1fr) auto auto auto;
    align-items: stretch;
    gap: 0;
    padding: calc(12px + var(--safe-top)) 20px calc(24px + var(--safe-bottom));
    border-top: none;
    /* Comes up and goes down like a sheet. `.sheet-out` is the closed position
       just below the screen, so the same transition carries both directions -
       and a finger on the artwork writes its own translate over it while it
       drags, which is what `.sheet-moving` switches the transition off for. */
    transform: translateY(0);
    transition: transform 0.32s var(--sheet-ease);
  }
  .player.expanded.sheet-out { transform: translateY(100%); }
  .player.expanded.sheet-moving { transition: none; }
  .player.expanded .now-head { display: flex; grid-area: head; }

  .player.expanded .now {
    grid-area: now; min-height: 0;
    flex-direction: column; justify-content: center; gap: 24px; text-align: center;
  }
  .player.expanded .now-art {
    width: min(74vw, 42vh); height: auto; aspect-ratio: 1;
    border-radius: var(--radius); box-shadow: var(--shadow-lg);
  }
  .player.expanded .now-art .art-fallback { font-size: 3.2rem; }
  /* The gap between the artwork and the title is where the line being sung
     goes - the one bit of the lyric a phone has room for without leaving the
     player. `.is-on` and not `hidden`: a display rule beats the attribute, and
     a song without a timed lyric must not reserve the space. */
  .player.expanded .now-line.is-on {
    display: block; width: 100%; flex: none;
    max-height: 4.2em; overflow: hidden;
  }
  .player.expanded .now-text { width: 100%; align-items: center; gap: 4px; }
  .player.expanded .now-title { font-size: 1.35rem; white-space: normal; }
  .player.expanded .now-artist { font-size: 0.95rem; white-space: normal; }
  /* Where a phone hands out stars: the track list has no room for the column,
     and this is the screen a listener rates from anyway. The plus next to them
     rides along, for the same reason - this is where a song is judged. */
  .player.expanded .now .now-actions { display: flex; justify-content: center; gap: 16px; margin-top: 10px; }
  .player.expanded .now .stars .star { padding: 5px; }
  .player.expanded .now .stars .star .icon { width: 26px; height: 26px; }
  .player.expanded .now-add { width: 40px; height: 40px; border-radius: var(--radius-sm); }
  .player.expanded .now-add .icon { width: 24px; height: 24px; }

  /* Out of the top edge and into the layout, at a height a thumb can hit. */
  .player.expanded .seek-rail {
    grid-area: seek; position: static; height: 26px; transform: none;
  }
  .player.expanded .seek-track { height: 4px; border-radius: 2px; }
  .player.expanded .seek-knob { transform: translate(-50%, -50%) scale(1); }

  /* column-reverse: the counter belongs under the bar it reads from, not under
     the buttons. */
  .player.expanded .transport { grid-area: transport; flex-direction: column-reverse; gap: 12px; }
  .player.expanded .transport-buttons { gap: 16px; }
  .player.expanded .transport-buttons .icon-btn { width: 46px; height: 46px; }
  .player.expanded .transport-buttons .icon-btn .icon { width: 22px; height: 22px; }
  .player.expanded .play-btn { width: 62px; height: 62px; }
  .player.expanded .play-btn .icon { width: 26px; height: 26px; }
  .player.expanded .counter { display: flex; font-size: 0.8rem; }

  /* Of the right-hand controls only these two mean anything here: volume is a
     hardware button on a phone and the meter needs a width this screen has not
     got. */
  .player.expanded .player-right {
    display: flex; grid-area: extras; justify-content: center; gap: 18px; padding-top: 14px;
  }
  .player.expanded .meter, .player.expanded .volume { display: none; }
  /* Opened from the full screen, the queue has to cover it. */
  .player.expanded ~ .queue { bottom: 0; z-index: 65; }
}

/* ============================================================================
   Touch
   ========================================================================== */
/* Everything a finger needs and a pointer does not: nothing is revealed by
   hovering, a press has to answer for itself, and a target has to be big enough
   to hit without aiming. */

@media (hover: none) {
  /* The row menu is where a phone reaches a track's actions - the rating among
     them - and it was invisible until hovered. */
  .track-row .row-menu { opacity: 1; }

  /* A row is tapped, not pointed at, so it needs the height of a target and no
     text selection under the long press that opens its menu. */
  .track-row.item { min-height: 56px; user-select: none; -webkit-user-select: none; }
  /* The row's job here is to play. The interpret under the title is a link two
     lines high in a row that is one tap wide, so it swallowed taps meant for
     the song - it stops being a target and the row's menu keeps the way to the
     interpret ("Zum Interpreten"). */
  .track-row.item .track-artist a { pointer-events: none; }
  .track-row.item .row-menu { width: 34px; height: 34px; }
  .nav-item { padding: 11px 10px; }
  .picker-item, .dropdown-item { padding: 13px 12px; }
  .chip { padding: 9px 14px; }
  /* The taller chip a thumb gets, so the three-line cap moves with it */
  .chip-picker { --chip-h: calc(1.23rem + 20px); }

  /* The press itself is the feedback. Kept short and small on purpose - it has
     to be over before the next screen arrives. */
  .btn:active, .icon-btn:active, .play-btn:active, .card-play:active, .avatar-btn:active {
    transform: scale(0.94);
  }
  /* Except the queue handle: it is held down for the whole drag, and a button
     that stays pressed-in that long reads as stuck rather than as pressed. */
  .queue-drag:active { transform: none; }
  .btn, .icon-btn, .play-btn, .card-play, .avatar-btn { transition: transform 0.08s ease; }
  .track-row.item:active, .queue-item:active, .picker-item:active,
  .dropdown-item:active, .nav-item:active, .issue:active, .top-row:active {
    background: var(--surface-2);
  }
  .card:active .card-art { opacity: 0.72; }
}

/* A menu with nothing to anchor it to: it comes up from the bottom edge over a
   scrim, full width, with rows a thumb can hit - the same items, laid out for
   the hand instead of for the pointer. */
.context-menu.sheet {
  left: 0; right: 0; bottom: 0; top: auto;
  min-width: 0; width: 100%;
  border-radius: var(--radius) var(--radius) 0 0;
  border-left: none; border-right: none; border-bottom: none;
  padding: 8px 8px calc(8px + var(--safe-bottom));
  animation: sheet-up 0.22s var(--sheet-ease);
}
.context-menu.sheet .dropdown-item { padding: 14px 14px; font-size: 0.95rem; }
.sheet-scrim { position: fixed; inset: 0; z-index: 149; background: rgba(8, 6, 11, 0.5); }

@keyframes sheet-up { from { transform: translateY(100%); } }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
  /* A sweep with no duration would park the bar off-screen, so hold it still. */
  .progress.indeterminate .progress-fill { animation: none !important; width: 100%; opacity: 0.5; }
}

/* --- The reading view -------------------------------------------------------
   The page inside the frame brings its own typography - it is the same page the
   Android app shows - so everything here is the furniture around it. The frame
   itself is given a height rather than left to grow: the columns inside it are
   laid out against the viewport, and a frame without a height has none to lay
   them against. */

/* The content area is a scroller everywhere else; for the reading view it is a
   column that hands its leftover height to the page. `height: 100%` would not
   do - it would be the height *before* the padding is taken off, and the last
   line of the bar would end up under the player. */
.content.is-full { display: flex; flex-direction: column; overflow: hidden; }
.content.is-full .content-inner { max-width: none; margin: 0; flex: 1; min-height: 0; }

/* The one view that is sized rather than scrolled: the columns inside the frame
   are laid out against its height, so the height has to be a number before the
   book is broken into pages. */
.reader {
  display: flex;
  flex-direction: column;
  gap: 10px;
  height: 100%;
  min-height: 420px;
}

.reader-top,
.reader-foot {
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 8px 12px;
}

.reader-foot { flex-direction: column; align-items: stretch; gap: 6px; }

.reader-title {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  line-height: 1.3;
}

.reader-title strong,
.reader-title span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.reader-title span { color: var(--text-dim); font-size: 0.85rem; }

.reader-stage {
  position: relative;
  flex: 1;
  min-height: 0;
  border: 1px solid var(--line);
  border-radius: 12px;
  overflow: hidden;
}

.reader-frame {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
  background: var(--bg);
}

/* The two thirds that turn a page. Transparent on purpose: the book is what the
   reader looks at, and a visible button either side would be furniture in the
   middle of it. */
.reader-zone {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 22%;
  border: 0;
  background: transparent;
  cursor: pointer;
}

.reader-zone-prev { left: 0; }
.reader-zone-next { right: 0; }
.reader-zone:hover { background: rgba(255, 255, 255, 0.03); }

.reader-back {
  position: absolute;
  left: 50%;
  bottom: 16px;
  transform: translateX(-50%);
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 8px 16px;
  cursor: pointer;
}

.reader-panel {
  max-height: 280px;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.reader-panel label { display: flex; align-items: center; gap: 10px; color: var(--text-dim); }
.reader-panel label input[type='range'] { flex: 1; }
.reader-fonts { display: flex; gap: 8px; flex-wrap: wrap; }

.reader-chapter {
  text-align: left;
  background: transparent;
  border: 0;
  color: var(--text);
  padding: 8px 6px;
  border-radius: 8px;
  cursor: pointer;
}

.reader-chapter:hover { background: var(--surface-2); }

.reader-numbers {
  display: flex;
  justify-content: space-between;
  color: var(--text-dim);
  font-size: 0.85rem;
}

/* The bar is a real range input with the fill drawn behind it, so dragging it
   is the browser's own job and the colour is still ours. */
.reader-bar { position: relative; height: 18px; }

.reader-bar input[type='range'] {
  position: absolute;
  inset: 0;
  width: 100%;
  margin: 0;
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  height: 18px;
  top: 0;
}

.reader-bar input[type='range']::-webkit-slider-runnable-track {
  height: 3px;
  margin-top: 8px;
  background: var(--line);
  border-radius: 2px;
}

.reader-bar input[type='range']::-moz-range-track {
  height: 3px;
  background: var(--line);
  border-radius: 2px;
}

.reader-bar input[type='range']::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  margin-top: 2.5px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--accent);
  cursor: pointer;
}

.reader-bar input[type='range']::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border: 0;
  border-radius: 50%;
  background: var(--accent);
  cursor: pointer;
}

/* Firefox paints its own filled track, which would sit under ours and show
   through as a second, differently coloured progress. */
.reader-bar input[type='range']::-moz-range-progress { background: transparent; }

.reader-fill {
  position: absolute;
  left: 0;
  top: 8px;
  height: 3px;
  background: var(--accent);
  border-radius: 2px;
  pointer-events: none;
}

.reader-chapter-page {
  text-align: center;
  color: var(--text-faint);
  font-size: 0.8rem;
  margin: 0;
}

/* The tape measure: laid out at the size of the real frame, because a page
   count is a fact about a window - and never seen. */
.reader-measure {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  pointer-events: none;
  /* `visibility: hidden` and not `display: none`: a frame that is not laid out
     has no viewport, and a page with no viewport has no page count. */
  visibility: hidden;
}
