/* ============================================================================
   Verdant — the app's design system.

   Two themes from one set of tokens. Light is the default; dark comes from an
   explicit choice, or from the OS when the choice is "system". Nothing below
   hard-codes a colour; everything reads a variable, so a re-skin is an edit to
   the blocks at the top of this file.

   How the three states resolve:
     data-theme="light"  → this block only; the media query is excluded
     data-theme="dark"   → the explicit dark block, whatever the OS says
     data-theme="system" → this block, flipped by the media query on a dark OS
                           (also what you get with no attribute at all, i.e.
                           before the pre-paint script runs or without JS)

   The dark values are written out twice, once per selector. CSS has no way to
   share a declaration block between a media query and a plain rule, and the
   alternative — a second layer of alias variables — costs more clarity than
   the repetition does.
   ========================================================================== */

:root {
  color-scheme: light;

  /* Surfaces, lightest to darkest. bg-2 is for chrome that should sit *under*
     the page (sidebar, bottom bar); surface-1..3 stack on top of it. */
  --bg: #F7F7F4;
  --bg-2: #FFFFFF;
  --surface: #FFFFFF;
  --surface-2: #F1F1ED;
  --surface-3: #E6E6E0;

  /* Hairlines. Translucent rather than solid so they read correctly against
     every surface above without a variant per layer. */
  --line: rgba(20, 18, 10, .10);
  --line-2: rgba(20, 18, 10, .18);

  --text: #131315;
  --muted: #61616B;

  /* The accent splits in two, because yellow can't do both jobs in both
     themes: --accent-fill is the yellow you SIT ON (buttons, pills, the play
     button), --accent is the yellow you WRITE IN (active labels, links,
     borders). Yellow text on white is unreadable, so the written accent
     darkens to a gold that passes contrast; the fill stays yellow, because
     black on yellow reads just as well here as it does on black. */
  --accent: #8A6A00;
  --accent-fill: #FFC91F;
  --accent-fill-2: #F0B800;
  --accent-ink: #14120A;               /* text/icons drawn ON the fill */
  --accent-soft: rgba(255, 201, 31, .26);
  --accent-glow: rgba(255, 201, 31, .45);

  /* Duplicate rows. Deliberately NOT the accent: "this is playing" and "this is
     a second copy" are different states and must not look alike. */
  --dup: #4A54D6;

  --danger: #C62F2F;
  --danger-soft: rgba(198, 47, 47, .10);
  /* Warm orange, not amber — amber this close to the accent would read as it. */
  --warn: #B45309;
  --warn-soft: rgba(180, 83, 9, .12);

  --shadow-1: 0 1px 2px rgba(20, 18, 10, .07);
  --shadow-2: 0 10px 28px rgba(20, 18, 10, .10);
  --shadow-3: 0 26px 60px rgba(20, 18, 10, .16);
  --glass: rgba(247, 247, 244, .80);
  --scrim: rgba(15, 15, 10, .42);

  --r-sm: 10px;
  --r-md: 14px;
  --r-lg: 20px;
  --r-xl: 26px;

  /* Height of the phone bottom bar. Everything that must clear it — the
     mini-player, the page's own bottom padding — measures from here. */
  --navh: 64px;
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-t: env(safe-area-inset-top, 0px);
  --side-w: 258px;

  /* One easing for everything, so the whole app moves the same way. */
  --ease: cubic-bezier(.22, .8, .3, 1);
}

/* Dark, when the OS asks for it and the user hasn't explicitly picked light. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;

    --bg: #0A0A0B;
    --bg-2: #050506;
    --surface: #141417;
    --surface-2: #1C1C21;
    --surface-3: #26262C;

    --line: rgba(255, 255, 255, .08);
    --line-2: rgba(255, 255, 255, .17);

    --text: #F5F5F6;
    --muted: #9A9AA3;

    /* On black the written accent can be the same yellow as the fill — there's
       nothing to lose contrast against. */
    --accent: #FFCE3A;
    --accent-fill: #FFCE3A;
    --accent-fill-2: #F0BE1E;
    --accent-ink: #14120A;
    --accent-soft: rgba(255, 206, 58, .14);
    --accent-glow: rgba(255, 206, 58, .28);

    --dup: #8A93FF;

    --danger: #FF6B6B;
    --danger-soft: rgba(255, 107, 107, .13);
    --warn: #FF9F45;
    --warn-soft: rgba(255, 159, 69, .14);

    --shadow-1: 0 1px 2px rgba(0, 0, 0, .4);
    --shadow-2: 0 10px 30px rgba(0, 0, 0, .45);
    --shadow-3: 0 26px 70px rgba(0, 0, 0, .6);
    --glass: rgba(10, 10, 11, .74);
    --scrim: rgba(0, 0, 0, .66);
  }
}

/* Dark, chosen deliberately — wins whatever the OS is set to. */
:root[data-theme="dark"] {
  color-scheme: dark;

  --bg: #0A0A0B;
  --bg-2: #050506;
  --surface: #141417;
  --surface-2: #1C1C21;
  --surface-3: #26262C;

  --line: rgba(255, 255, 255, .08);
  --line-2: rgba(255, 255, 255, .17);

  --text: #F5F5F6;
  --muted: #9A9AA3;

  --accent: #FFCE3A;
  --accent-fill: #FFCE3A;
  --accent-fill-2: #F0BE1E;
  --accent-ink: #14120A;
  --accent-soft: rgba(255, 206, 58, .14);
  --accent-glow: rgba(255, 206, 58, .28);

  --dup: #8A93FF;

  --danger: #FF6B6B;
  --danger-soft: rgba(255, 107, 107, .13);
  --warn: #FF9F45;
  --warn-soft: rgba(255, 159, 69, .14);

  --shadow-1: 0 1px 2px rgba(0, 0, 0, .4);
  --shadow-2: 0 10px 30px rgba(0, 0, 0, .45);
  --shadow-3: 0 26px 70px rgba(0, 0, 0, .6);
  --glass: rgba(10, 10, 11, .74);
  --scrim: rgba(0, 0, 0, .66);
}

/* ---- reset / base -------------------------------------------------------- */

* { box-sizing: border-box; }

/* The `hidden` attribute must always win. The browser's own [hidden] rule is a
   user-agent rule, so ANY author rule that sets `display` silently beats it. */
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  min-height: 100vh;
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI",
    Roboto, Inter, system-ui, sans-serif;
  font-size: 14.5px;
  line-height: 1.5;
  background: var(--bg);
  color: var(--text);
  -webkit-tap-highlight-color: transparent;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior-y: contain;
}

.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

.ic {
  width: 20px; height: 20px; flex: none;
  vertical-align: -.22em; pointer-events: none;
}

h1, h2, h3 { margin: 0; letter-spacing: -.02em; font-weight: 700; }
h1 { font-size: 20px; }
h2 { font-size: 22px; }
h3 { font-size: 16px; }
p { margin: 0; }

a { color: var(--accent); }

.muted { color: var(--muted); font-size: 13.5px; }
.hint { color: var(--muted); font-size: 12.5px; }
code {
  background: var(--surface-2); border: 1px solid var(--line);
  padding: 1px 6px; border-radius: 6px; font-size: 12px;
}

/* Empty states read as a deliberate message, not a stray sentence. */
.empty {
  margin: 8px 0; padding: 40px 24px; text-align: center;
  color: var(--muted); font-size: 14px;
  background: var(--surface); border: 1px dashed var(--line-2);
  border-radius: var(--r-lg);
}

.countline { margin: 0 0 12px; font-size: 12.5px; }

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

/* Scrollbars: thin and unobtrusive, so long lists don't look like a web form. */
* { scrollbar-width: thin; scrollbar-color: var(--line-2) transparent; }
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 999px; border: 3px solid transparent; background-clip: content-box; }
*::-webkit-scrollbar-track { background: transparent; }

/* ---- buttons ------------------------------------------------------------- */

button {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  cursor: pointer; font: inherit; font-size: 14px; font-weight: 600;
  border: 1px solid transparent; border-radius: 999px;
  padding: 11px 18px;
  background: var(--accent-fill); color: var(--accent-ink);
  -webkit-user-select: none; user-select: none; white-space: nowrap;
  transition: transform .12s var(--ease), background .16s, border-color .16s,
    color .16s, box-shadow .16s, opacity .16s;
}
button:disabled { opacity: .45; cursor: not-allowed; }
button:active:not(:disabled) { transform: scale(.965); }
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (hover: hover) {
  button:not(:disabled):hover { background: var(--accent-fill-2); }
}

/* Secondary: a translucent surface that works on any background. */
.ghost {
  background: var(--surface-2); border-color: var(--line);
  color: var(--text); padding: 9px 15px; font-size: 13.5px;
}
@media (hover: hover) {
  .ghost:not(:disabled):hover { background: var(--surface-3); border-color: var(--line-2); }
}

.danger { background: var(--danger-soft); border-color: transparent; color: var(--danger); }
@media (hover: hover) {
  .danger:not(:disabled):hover { background: var(--danger); color: #fff; }
}

/* Icon-only: square-ish tap target, no label. */
.iconbtn { padding: 0; width: 38px; height: 38px; border-radius: 50%; }
.iconbtn .ic { width: 18px; height: 18px; }

.backbtn { padding: 7px 14px 7px 10px; font-size: 13px; margin-bottom: 12px; }
.backbtn .ic { width: 16px; height: 16px; }

.playall { padding: 11px 20px 11px 17px; box-shadow: 0 6px 20px var(--accent-glow); }
.playall .ic { width: 16px; height: 16px; }

.download { width: 100%; margin-top: 20px; padding: 15px; font-size: 15.5px; border-radius: var(--r-md); }

/* ---- form controls ------------------------------------------------------- */

input, select, textarea { font: inherit; }

select, .field input, .prompt-input, .pl-create input, .fmtpath-row input, .lookup-field input {
  width: 100%; padding: 12px 14px;
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--r-sm); color: var(--text); font-size: 14.5px; outline: none;
  transition: border-color .16s, box-shadow .16s, background .16s;
}
select:focus, .field input:focus, .prompt-input:focus, .pl-create input:focus,
.fmtpath-row input:focus, .lookup-field input:focus {
  border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft);
}
select {
  cursor: pointer; appearance: none; -webkit-appearance: none;
  padding-right: 34px;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 17px) 52%, calc(100% - 12px) 52%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}

.field { display: flex; flex-direction: column; gap: 7px; margin-top: 16px; }
.field > span { font-size: 12.5px; font-weight: 600; color: var(--muted); }

/* Search box with a leading icon. */
.searchwrap {
  position: relative; display: flex; align-items: center;
  flex: 1 1 220px; min-width: 0;
}
.searchwrap .ic { position: absolute; left: 13px; width: 17px; height: 17px; color: var(--muted); }
.libsearch {
  width: 100%; padding: 11px 14px 11px 39px;
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: 999px; color: var(--text); font-size: 14px; outline: none;
  transition: border-color .16s, box-shadow .16s;
}
.libsearch:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.libsearch::-webkit-search-cancel-button { filter: grayscale(1) opacity(.5); }

.libselect {
  width: auto; padding: 10px 32px 10px 14px; border-radius: 999px;
  font-size: 13.5px; background-color: var(--surface-2);
}

/* ---- banners ------------------------------------------------------------- */

.error, .notice {
  margin: 0 0 16px; padding: 13px 16px; border-radius: var(--r-md);
  font-size: 14px; line-height: 1.5; border: 1px solid transparent;
}
.error { background: var(--danger-soft); border-color: var(--danger); color: var(--danger); }
.notice { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }

.badge {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
  padding: 3px 9px; border-radius: 999px;
  background: var(--surface-3); color: var(--muted); margin-right: 7px;
}
.badge.ok { background: var(--accent-soft); color: var(--accent); }
.badge.warn { background: var(--warn-soft); color: var(--warn); }

/* ============================================================================
   App shell — sidebar + main column on desktop, bottom bar on phones.
   ========================================================================== */

.app { display: grid; grid-template-columns: var(--side-w) 1fr; min-height: 100vh; }

.side {
  position: sticky; top: 0; align-self: start;
  height: 100vh; display: flex; flex-direction: column; gap: 6px;
  padding: 18px 14px 18px 18px;
  background: var(--bg-2); border-right: 1px solid var(--line);
}

.brand {
  display: flex; align-items: center; gap: 11px;
  padding: 8px 10px 18px; text-decoration: none; color: var(--text);
}
.brand-mark { border-radius: 8px; display: block; }
.brand-name { font-size: 16.5px; font-weight: 700; letter-spacing: -.02em; }

/* Sidebar nav items. */
.tabs { display: flex; flex-direction: column; gap: 3px; }
.tab {
  position: relative; justify-content: flex-start; width: 100%;
  gap: 13px; padding: 11px 14px; border-radius: var(--r-sm);
  background: transparent; border: none; color: var(--muted);
  font-size: 14.5px; font-weight: 600;
}
.tab .ic { width: 21px; height: 21px; }
.tab-label { overflow: hidden; text-overflow: ellipsis; }
@media (hover: hover) {
  /* --surface-2, not --surface: in the light palette --surface is #FFFFFF and
     so is --bg-2, which the sidebar is painted with — so hovering a tab
     changed the background to exactly the background it already had, and only
     the text moved. --surface-2 is a step away from the sidebar in BOTH
     palettes, which is the whole job of a hover state. */
  .tab:not(.active):hover { background: var(--surface-2); color: var(--text); }
  .tab:not(.active):hover .ic { color: var(--text); }
  /* The active tab is not inert either — it just keeps its accent. */
  .tab.active:hover { background: var(--accent-glow); }
}
.tab.active { background: var(--accent-soft); color: var(--accent); }

.tabbadge {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 20px; height: 20px; margin-left: auto; padding: 0 6px;
  font-size: 11px; font-weight: 700; border-radius: 999px;
  background: var(--danger); color: #fff;
}

.side-foot { margin-top: auto; padding: 0 12px; font-size: 11.5px; line-height: 1.5; }

.main { min-width: 0; display: flex; flex-direction: column; }

.topbar {
  position: sticky; top: 0; z-index: 60;
  display: flex; align-items: center; gap: 14px;
  padding: 12px 32px; min-height: 62px;
  background: var(--glass);
  backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%);
  border-bottom: 1px solid var(--line);
}
/* The brand belongs in the sidebar on desktop; the top bar carries it only
   when the sidebar is gone. */
.brand-compact { display: none; padding: 0; }

/* Light / dark / system, as a segmented control in the top bar. It carries the
   auto margin that pushes the right-hand group over, because #userbox — which
   used to carry it — is hidden when auth is off. */
.themeswitch {
  display: inline-flex; gap: 2px; margin-left: auto; padding: 3px;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: 999px;
}
.themeswitch button {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 28px; padding: 0; border: 0; border-radius: 999px;
  background: transparent; color: var(--muted);
}
.themeswitch button .ic { width: 15px; height: 15px; }
@media (hover: hover) {
  .themeswitch button:not(.active):hover { background: var(--surface-3); color: var(--text); }
}
.themeswitch button.active { background: var(--accent-fill); color: var(--accent-ink); }

/* The switcher took the auto margin, so the user box just follows it. */
.themeswitch + .userbox { margin-left: 12px; }

.userbox { display: flex; align-items: center; gap: 10px; margin-left: auto; }
.userbox #whoami { font-size: 13px; }
.avatar {
  width: 36px; height: 36px; flex: none; padding: 0; border-radius: 50%;
  background: var(--surface-2); border: 1px solid var(--line);
  font-size: 17px; line-height: 1; overflow: hidden;
}
.avatar img { width: 100%; height: 100%; object-fit: cover; }
@media (hover: hover) { .avatar:hover { border-color: var(--accent); background: var(--surface-3); } }

.content { flex: 1; width: 100%; max-width: 1440px; padding: 26px 32px 140px; }

.pane-head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 16px; flex-wrap: wrap; margin-bottom: 20px;
}
.pane-head h2 { font-size: 28px; }
.pane-head .muted { margin-top: 6px; max-width: 68ch; }
.libactions { display: flex; gap: 8px; align-items: center; }

/* Tab panes animate in, so moving between them reads as changing screen. */
.pane-in { animation: panein .26s var(--ease); }
@keyframes panein { from { opacity: 0; transform: translateY(10px); } }

.foot { display: none; margin-top: 40px; text-align: center; font-size: 12px; }
.foot .muted { font-size: 12px; }

/* ---- phone bottom bar + more sheet --------------------------------------- */

.bottomnav { display: none; }

.navbtn {
  position: relative; flex: 1; min-width: 0;
  flex-direction: column; gap: 4px;
  background: none; border: none; border-radius: 0; padding: 8px 2px;
  color: var(--muted); font-size: 10.5px; font-weight: 600; letter-spacing: .01em;
}
.navbtn .ic { width: 22px; height: 22px; transition: transform .2s var(--ease); }
.nav-label { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
.navbtn.active { color: var(--accent); }
.navbtn.active .ic { transform: translateY(-1px); }
.navbtn:active:not(:disabled) { transform: none; }
.navbtn:active .ic { transform: scale(.88); }
.navdot {
  position: absolute; top: 8px; right: calc(50% - 16px);
  width: 8px; height: 8px; border-radius: 50%; background: var(--danger);
  box-shadow: 0 0 0 2px var(--bg-2);
}

body.sheet-open { overflow: hidden; }
.sheet { position: fixed; inset: 0; z-index: 130; display: flex; align-items: flex-end; }
.sheet-backdrop { position: absolute; inset: 0; background: var(--scrim); backdrop-filter: blur(6px); animation: fadein .18s ease; }
.sheet-box {
  position: relative; z-index: 1; width: 100%;
  background: var(--surface); border-top: 1px solid var(--line);
  border-radius: var(--r-xl) var(--r-xl) 0 0;
  padding: 10px 14px calc(18px + var(--safe-b));
  box-shadow: var(--shadow-3);
  animation: slideup .28s var(--ease);
}
.sheet-grip { width: 38px; height: 4px; border-radius: 999px; background: var(--line-2); margin: 0 auto; }
.sheet-box h3 {
  margin: 14px 4px 12px; font-size: 12px; font-weight: 700;
  color: var(--muted); text-transform: uppercase; letter-spacing: .08em;
}
.sheet-list { display: flex; flex-direction: column; gap: 6px; }
.sheet-item {
  justify-content: flex-start; width: 100%; gap: 14px;
  background: var(--surface-2); border: 1px solid var(--line); color: var(--text);
  padding: 15px 16px; border-radius: var(--r-md); font-size: 15px;
}
.sheet-item .ic { width: 20px; height: 20px; color: var(--muted); }
.sheet-item.active { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.sheet-item.active .ic { color: var(--accent); }

@keyframes fadein { from { opacity: 0; } }
@keyframes slideup { from { transform: translateY(100%); } }
@keyframes pop { from { opacity: 0; transform: translateY(10px) scale(.985); } }

/* ============================================================================
   Download tab
   ========================================================================== */

.lookup { display: flex; gap: 10px; margin-bottom: 22px; max-width: 860px; }
.lookup-field { position: relative; flex: 1; display: flex; align-items: center; min-width: 0; }
.lookup-icon { position: absolute; left: 15px; color: var(--muted); }
.lookup-field input { padding: 15px 16px 15px 44px; border-radius: var(--r-md); font-size: 15px; }
.lookup > button { padding: 15px 24px; border-radius: var(--r-md); font-size: 15px; }

.card {
  margin-top: 20px; padding: 22px; max-width: 860px; background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--r-lg); box-shadow: var(--shadow-1);
}

.preview { display: flex; gap: 18px; }
.preview img {
  width: 190px; aspect-ratio: 16/9; object-fit: cover; border-radius: var(--r-md);
  background: var(--surface-3); flex: none; box-shadow: var(--shadow-2);
}
.meta h2 { margin: 0 0 8px; font-size: 19px; line-height: 1.35; }

.expander {
  margin: 22px 0 4px; background: var(--surface-2);
  border: 1px solid var(--line); border-radius: var(--r-md);
}
.expander > summary {
  display: flex; align-items: center; gap: 12px; cursor: pointer;
  padding: 14px 16px; list-style: none; user-select: none;
}
.expander > summary::-webkit-details-marker { display: none; }
.exp-label { font-size: 12.5px; font-weight: 600; color: var(--muted); }
.exp-value { font-weight: 600; }
.exp-hint { margin-left: auto; font-size: 13px; font-weight: 600; color: var(--accent); }
.expander[open] .exp-hint::after { content: ' ⌃'; }
.expander:not([open]) .exp-hint::after { content: ' ⌄'; }
.expander-body { padding: 0 14px 14px; }
.expander .segmented { margin: 4px 0 14px; }

/* iOS-style segmented control. */
.segmented {
  display: flex; gap: 4px; padding: 4px; margin: 18px 0 14px;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: 999px;
}
.segmented button {
  flex: 1; background: transparent; color: var(--muted);
  padding: 9px 12px; font-size: 13.5px; border-radius: 999px; border: none;
}
@media (hover: hover) { .segmented button:not(.active):hover { background: var(--surface-3); color: var(--text); } }
.segmented button.active {
  background: var(--surface); color: var(--text);
  box-shadow: var(--shadow-1); border: 1px solid var(--line);
}

.formatlist { display: flex; flex-direction: column; gap: 8px; max-height: 290px; overflow-y: auto; padding-right: 2px; }
.fmt {
  display: flex; align-items: center; gap: 12px; padding: 12px 15px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-sm); cursor: pointer; transition: border-color .16s, background .16s;
}
@media (hover: hover) { .fmt:hover { border-color: var(--line-2); } }
.fmt.sel { border-color: var(--accent); background: var(--accent-soft); }
.fmt input { accent-color: var(--accent); }
.fmt-main { font-weight: 600; font-size: 14px; }
.fmt-sub { color: var(--muted); font-size: 12.5px; margin-left: auto; text-align: right; }

.destlist { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
.destopt {
  display: flex; align-items: center; gap: 11px; padding: 12px 14px;
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--r-sm); cursor: pointer; font-size: 14px;
}
.destopt input { accent-color: var(--accent); }
@media (hover: hover) { .destopt:hover { border-color: var(--line-2); } }

/* ---- progress ------------------------------------------------------------ */

.progress { margin-top: 16px; }
.bar { height: 8px; background: var(--surface-3); border-radius: 999px; overflow: hidden; }
.fill {
  position: relative; height: 100%; width: 0; border-radius: 999px;
  background: linear-gradient(90deg, var(--accent-fill-2), var(--accent-fill));
  transition: width .3s var(--ease); overflow: hidden;
}
.progress-status { display: flex; align-items: center; justify-content: center; gap: 10px; margin-top: 12px; }
#progressText { margin: 0; text-align: center; }

.spinner {
  width: 15px; height: 15px; flex: none; display: none;
  border: 2px solid var(--line-2); border-top-color: var(--accent);
  border-radius: 50%; animation: spin .7s linear infinite;
}
.progress.busy .spinner { display: inline-block; }
@keyframes spin { to { transform: rotate(360deg); } }

.progress.busy .fill::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .4), transparent);
  transform: translateX(-100%); animation: sweep 1.2s ease-in-out infinite;
}
@keyframes sweep { to { transform: translateX(100%); } }

.progress.indeterminate .fill { width: 40% !important; animation: indet 1.2s ease-in-out infinite; }
.progress.indeterminate .fill::after { display: none; }
@keyframes indet { 0% { margin-left: -45%; } 100% { margin-left: 100%; } }

/* ---- running downloads --------------------------------------------------- */

.downloads { margin-top: 30px; }
.dlhead { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.dllist { display: flex; flex-direction: column; gap: 10px; }
.dlrow {
  display: flex; gap: 14px; align-items: center; padding: 14px 16px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-md); box-shadow: var(--shadow-1);
}
.dlrow.done { border-color: var(--accent); }
.dlrow.error { border-color: var(--danger); }
.dl-thumb {
  width: 68px; height: 42px; flex: none; border-radius: 8px; object-fit: cover;
  background: var(--surface-3); display: grid; place-items: center;
  color: var(--muted); overflow: hidden;
}
.dl-body { flex: 1; min-width: 0; }
.dl-title { font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dlrow .progress { margin-top: 10px; }
.dl-statusline { display: flex; align-items: center; gap: 8px; margin-top: 9px; }
.dl-status { font-size: 12.5px; color: var(--muted); overflow-wrap: anywhere; }
.dl-status .ok { color: var(--accent); }
.dl-status .err { color: var(--danger); }
.dl-dismiss { flex: none; padding: 7px 12px; font-size: 12px; }

/* ============================================================================
   Library
   ========================================================================== */

.subtabs {
  display: inline-flex; gap: 4px; padding: 4px; margin-bottom: 18px;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: 999px;
}
.subtab { background: transparent; color: var(--muted); padding: 8px 22px; font-size: 13.5px; }
@media (hover: hover) { .subtab:not(.active):hover { background: var(--surface-3); color: var(--text); } }
.subtab.active { background: var(--accent-fill); color: var(--accent-ink); }

/* Server songs builds one sub-tab per mounted folder, so unlike Discover's
   three fixed tabs this row has no fixed width — it scrolls sideways rather
   than wrapping into a second line and pushing the list down the screen. */
#songTabs { max-width: 100%; overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
#songTabs::-webkit-scrollbar { display: none; }
#songTabs .subtab {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 8px 16px; white-space: nowrap; flex: 0 0 auto;
}
#songTabs .subtab .ic { width: 15px; height: 15px; }
/* Item count, shown once the tab's folder has been walked. */
#songTabs .tabcount {
  font-style: normal; font-size: 11px; font-weight: 700; line-height: 1;
  padding: 3px 6px; border-radius: 999px;
  background: var(--surface-3); color: var(--muted);
}
#songTabs .subtab.active .tabcount {
  background: color-mix(in srgb, var(--accent-ink) 16%, transparent); color: var(--accent-ink);
}

.libcontrols { display: flex; flex-wrap: wrap; align-items: center; gap: 9px; margin-bottom: 18px; }

/* "Show all 1,204" sits with the page numbers but is not one — wider, and
   clearly a different kind of thing to press. */
.pagebtn.pageall { width: auto; padding: 0 14px; font-weight: 600; }

/* "These downloads are still on this site" — an offer while idle, a progress
   readout while a move is running. Deliberately quiet: it is a tidy-up, not a
   problem, so it sits below the controls rather than shouting above them. */
.consolidate {
  display: flex; align-items: center; gap: 12px;
  padding: 11px 14px; margin-bottom: 16px;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-md);
}
.consolidate > .ic { width: 18px; height: 18px; flex: none; color: var(--muted); }
.consolidate.busy > .ic { color: var(--accent); }
.consolidate-main { flex: 1; min-width: 0; }
.consolidate-text { font-size: 13.5px; }
.consolidate-note { font-size: 12.5px; margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.consolidate-actions { display: flex; gap: 8px; flex: none; }
.consolidate-bar {
  height: 5px; margin-top: 7px; border-radius: 3px; overflow: hidden;
  background: color-mix(in srgb, var(--muted) 22%, transparent);
}
.consolidate-bar > span {
  display: block; height: 100%; width: 0;
  background: var(--accent); border-radius: 3px; transition: width .3s ease;
}
@media (max-width: 640px) {
  .consolidate { flex-wrap: wrap; }
  .consolidate-main { flex-basis: 100%; order: 2; }
  .consolidate-actions { order: 3; margin-left: auto; }
}

.viewtoggle { display: inline-flex; gap: 3px; padding: 3px; background: var(--surface-2); border: 1px solid var(--line); border-radius: 999px; }
.viewtoggle button { background: transparent; color: var(--muted); padding: 8px 13px; border-radius: 999px; }
.viewtoggle button.active { background: var(--surface); color: var(--text); box-shadow: var(--shadow-1); }

.library.view-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 18px; }
.library.view-list { display: block; overflow-x: auto; }

.libcard {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg);
  overflow: hidden; display: flex; flex-direction: column;
  transition: transform .2s var(--ease), border-color .2s, box-shadow .2s;
}
@media (hover: hover) {
  /* The background lift matters more than the shadow: against a near-black
     page a drop shadow is invisible, so without it a hovered card in dark mode
     reads as barely changed. Rows have always lifted their background this way;
     cards now match. */
  .libcard:hover {
    transform: translateY(-4px); border-color: var(--line-2);
    background: var(--surface-2); box-shadow: var(--shadow-2);
  }
}
.libcard.playing { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent), var(--shadow-2); }
.thumb-wrap { position: relative; aspect-ratio: 16/9; background: var(--surface-3); cursor: pointer; }
.thumb-wrap img { width: 100%; height: 100%; object-fit: cover; display: block; }
.noimg { width: 100%; height: 100%; display: grid; place-items: center; color: var(--muted); }
.noimg .ic { width: 34px; height: 34px; }
.playbtn {
  position: absolute; right: 12px; bottom: 12px;
  width: 46px; height: 46px; display: grid; place-items: center;
  color: var(--accent-ink); background: var(--accent-fill); border-radius: 50%;
  box-shadow: 0 8px 22px var(--accent-glow);
  opacity: 0; transform: translateY(8px) scale(.9);
  transition: opacity .2s var(--ease), transform .2s var(--ease);
}
.playbtn .ic { width: 18px; height: 18px; }
.thumb-wrap:hover .playbtn, .libcard.playing .playbtn { opacity: 1; transform: none; }
@media (hover: none) { .playbtn { opacity: 1; transform: none; } }
.card-body { padding: 13px 15px 15px; display: flex; flex-direction: column; gap: 9px; }
.card-title { font-weight: 650; font-size: 14px; line-height: 1.35; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.card-meta { font-size: 12px; color: var(--muted); overflow-wrap: anywhere; }
.actions { display: flex; gap: 7px; flex-wrap: wrap; }
/* A row of icons, not a row of pills. Five labelled buttons under every card
   turned a wall of thumbnails into a wall of text; the actions are the same
   ones the list view has always shown as icons, so they read the same here. */
.actions-icons { gap: 6px; }
.actions-icons a, .actions-icons button {
  flex: 0 0 auto; width: 34px; height: 34px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
}
.actions-icons .ic { width: 16px; height: 16px; }
.actions a, .actions button {
  font-size: 12px; padding: 7px 13px; flex: 1 1 auto; text-align: center;
  justify-content: center; text-decoration: none;
  background: var(--surface-2); border: 1px solid var(--line); color: var(--text);
  border-radius: 999px; font-weight: 600;
}
@media (hover: hover) { .actions a:hover { background: var(--surface-3); } }
.actions .danger { background: var(--danger-soft); border-color: transparent; color: var(--danger); }

/* List view */
.libtable { width: 100%; border-collapse: collapse; font-size: 14px; }
.libtable th {
  text-align: left; color: var(--muted); font-size: 11px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em;
  padding: 10px 12px; border-bottom: 1px solid var(--line); white-space: nowrap;
}
.libtable td { padding: 11px 12px; border-bottom: 1px solid var(--line); vertical-align: middle; }
@media (hover: hover) { .lt-row:hover > td { background: var(--surface); } }
.lt-title { display: flex; align-items: center; gap: 11px; min-width: 160px; }
.lt-title > span { overflow-wrap: anywhere; }
.lt-play {
  flex: none; width: 32px; height: 32px; padding: 0; border-radius: 50%;
  background: var(--surface-2); border: 1px solid var(--line); color: var(--text);
}
.lt-play .ic { width: 13px; height: 13px; }
@media (hover: hover) { .lt-play:hover { background: var(--accent-fill); border-color: var(--accent-fill); color: var(--accent-ink); } }
.lt-row.playing > td { background: var(--accent-soft); }
.lt-row.playing .lt-play { background: var(--accent-fill); border-color: var(--accent-fill); color: var(--accent-ink); }
.lt-actions { white-space: nowrap; text-align: right; }
.lt-actions a, .lt-actions button {
  padding: 7px 10px; font-size: 12px; margin-left: 5px; border-radius: 999px;
  background: var(--surface-2); border: 1px solid var(--line); color: var(--text);
  text-decoration: none; display: inline-flex;
}
.lt-actions .danger { background: var(--danger-soft); border-color: transparent; color: var(--danger); }
.lt-playrow > td { padding: 0 12px 12px; border-bottom: 1px solid var(--line); }

.checkresult { margin-top: 9px; font-size: 12.5px; line-height: 1.65; }
.checkresult .ok { color: var(--accent); }
.checkresult .miss { color: var(--danger); }
.checkresult .sendgo { width: auto; margin-top: 10px; padding: 9px 16px; font-size: 13px; }

.pagination { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; margin-top: 22px; }
.pagebtn {
  min-width: 38px; padding: 9px 13px; font-size: 13px;
  background: var(--surface-2); border: 1px solid var(--line); color: var(--text);
}
.pagebtn.active { background: var(--accent-fill); border-color: var(--accent-fill); color: var(--accent-ink); }
.pagegap { color: var(--muted); align-self: center; padding: 0 4px; }

/* ============================================================================
   Server songs
   ========================================================================== */

.songbar { display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
.songbar .field { margin-top: 0; flex: 1 1 260px; min-width: 0; }
.songbar > button { padding: 12px 18px; }

.dirchips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 18px; }
.dirchip {
  display: inline-flex; align-items: center; gap: 9px; max-width: 100%;
  background: var(--accent-soft); border: 1px solid transparent; color: var(--accent);
  border-radius: 999px; padding: 6px 7px 6px 15px; font-size: 13px;
  font-weight: 600; overflow-wrap: anywhere;
}
.dirchip button {
  padding: 0; width: 22px; height: 22px; flex: none; border-radius: 50%;
  background: transparent; color: inherit; border: none; opacity: .75;
}
.dirchip button .ic { width: 13px; height: 13px; }
@media (hover: hover) { .dirchip button:hover { background: var(--danger); color: #fff; opacity: 1; } }

.songlist { display: flex; flex-direction: column; gap: 4px; max-width: 1100px; }
.songrow {
  display: flex; align-items: center; gap: 14px; padding: 9px 14px;
  border: 1px solid transparent; border-radius: var(--r-md);
  transition: background .16s, border-color .16s;
}
@media (hover: hover) { .songrow:hover { background: var(--surface); border-color: var(--line); } }
.songrow.playing { background: var(--accent-soft); border-color: transparent; }
.songrow.playing .song-title { color: var(--accent); }

/* Cover art. See .art below — the row just sizes it and hangs the play button
   on top. */
.song-art {
  position: relative; width: 46px; height: 46px; flex: none;
  border-radius: 10px; overflow: hidden; box-shadow: var(--shadow-1);
}
.song-play {
  position: absolute; inset: 0; width: 100%; height: 100%; padding: 0;
  border: none; border-radius: 0; background: rgba(0, 0, 0, .5); color: #fff;
  opacity: 0; transition: opacity .16s;
}
.song-play .ic { width: 16px; height: 16px; color: #fff; }
@media (hover: hover) { .songrow:hover .song-play { opacity: 1; } }
.songrow.playing .song-play, .song-play:focus-visible { opacity: 1; }
@media (hover: none) { .song-play { opacity: 1; background: rgba(0, 0, 0, .34); } }

.song-main { flex: 1; min-width: 0; }
.song-title { font-weight: 600; font-size: 14.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.song-sub { display: flex; align-items: center; gap: 7px; font-size: 12.5px; color: var(--muted); margin-top: 3px; overflow: hidden; }
.song-sub .badge { margin: 0; }
.song-where { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Paging footer: "show the next hundred" beside "load the lot", the second
   being what makes a whole-tab de-duplication possible. */
.songmore { display: flex; flex-wrap: wrap; gap: 9px; margin-top: 12px; }
.songmore button { flex: 1 1 200px; padding: 13px; }

.installbtn { display: inline-flex; align-items: center; gap: 7px; padding: 8px 13px; font-size: 13px; }
@media (max-width: 620px) { .installbtn span { display: none; } .installbtn { padding: 8px; } }

/* ---- who may use this app -------------------------------------------------- */
.people { margin-top: 34px; padding-top: 6px; border-top: 1px solid var(--line); }
.peoplelist { display: flex; flex-direction: column; gap: 2px; }
.person-pending { background: var(--accent-soft); border-radius: var(--r-md); }
.person-blocked { opacity: .6; }
.personrow .file-actions { gap: 6px; }

/* ---- what just arrived, and what looks like it ----------------------------- */

/* Five files copied into a folder of nine hundred are five files you cannot
   find. These two states last until the pane is browsed elsewhere. */
.dualrow.justmoved { background: var(--accent-soft); box-shadow: inset 3px 0 0 var(--accent); }
.dualrow.alike { box-shadow: inset 3px 0 0 var(--warn); }
/* A group of likely copies of one thing. The tint marks the group; the badge
   says how many and how much could be freed. */
.dualrow.dupe { box-shadow: inset 3px 0 0 var(--dup); }
.dualrow.dupe-first { margin-top: 6px; }
.badge.dupbadge2 { background: var(--surface-3); color: var(--dup); }
.dualdel { color: var(--danger); }
.badge.warnbadge { background: var(--warn-soft); color: var(--warn); }

/* ---- picking several videos to compress ------------------------------------ */

.cmp-pick { display: flex; align-items: center; padding-right: 4px; cursor: pointer; }
.cmp-pick input { accent-color: var(--accent); }
/* A queued row has no checkbox; the slot keeps the columns lined up anyway. */
.cmp-pick-slot { width: 15px; flex: none; }
.filerow.picked { background: var(--accent-soft); }
#compressBulk { margin-bottom: 10px; }

/* ---- the history of copies and moves --------------------------------------- */

/* A panel under the header rather than a fold at the foot of the page: "what
   did I already move?" is asked BEFORE moving the next thing, and the old
   position was below both file panes where nobody scrolls. */
.xferhistory {
  margin: 0 0 14px; padding: 12px 14px;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-md);
}
.xferhist-tools { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 8px; }
.xferhist-title { font-size: 14px; }
.xferhist-tools button { font-size: 12.5px; }
.xferhist-tools #xferHistClose { margin-left: auto; }
.xferhist-list { max-height: min(46vh, 420px); overflow-y: auto; }
/* An expanded batch scrolls inside itself rather than pushing every other
   batch out of reach: 250 files in one row would otherwise be 250 rows of
   scrolling before the next batch. */
.hist-filelist { max-height: 240px; overflow-y: auto; overscroll-behavior: contain; }
#dualHistBtn.on { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
#dualHistBtn .tabbadge { margin-left: 6px; background: var(--surface-3); color: var(--muted); }

/* One batch per row. Two files moved this morning and three moved now are two
   rows, because they were two decisions. */
.histrow { border-bottom: 1px solid var(--line); }
.histrow:last-child { border-bottom: 0; }
.hist-top {
  display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
  font-size: 13px; padding: 9px 0; cursor: pointer; list-style: none;
}
.hist-top::-webkit-details-marker { display: none; }
.hist-top::before {
  content: '▸'; flex: none; font-size: 10px; opacity: .55;
  transition: transform .15s var(--ease);
}
.histrow[open] .hist-top::before { transform: rotate(90deg); }
.hist-route { font-weight: 600; }
.hist-kind { font-size: 11.5px; }
/* The timestamp goes to the right edge: it is the column the eye scans when the
   question is "when", and a ragged one is no column at all. */
.hist-when { margin-left: auto; font-size: 12px; white-space: nowrap; }
.hist-top .xfer-tag { margin-left: 0; }
.hist-meta { font-size: 12px; padding: 0 0 6px 16px; }
.hist-filelist { padding: 0 0 10px 16px; }
.hist-filelist .xf-row { display: grid; grid-template-columns: 14px 1fr auto auto; gap: 4px 10px; }
/* The route goes on its own line under the name, spanning the columns: full
   paths are long, and squeezing them into the name column would ellipsis away
   the half that matters. */
.xf-row-wide { grid-template-columns: 14px 1fr auto auto; }
.xf-route {
  grid-column: 2 / -1; font-size: 11px; line-height: 1.4;
  overflow-wrap: anywhere; padding-bottom: 2px;
}
.xf-route .xr-arrow { opacity: .7; padding: 0 3px; }

/* A footnote under a list of files, not another row in it.
 *
 * These were .xf-row, which inside .hist-filelist is a four-column grid — so
 * the sentence landed in the 14px first column and wrapped into a 292px-tall
 * stack one or two characters wide. */
.xf-note {
  margin: 6px 0 0; padding-left: 24px; font-size: 11.5px; line-height: 1.45;
}
/* Per-file times line up in their own column — the point of opening a batch is
   scanning down it for the one you are looking for. */
.xf-time { font-size: 11.5px; white-space: nowrap; min-width: 62px; text-align: right; }
@media (max-width: 560px) {
  .hist-when { margin-left: 0; width: 100%; }
  .hist-filelist .xf-row { grid-template-columns: 14px 1fr auto; }
  .hist-filelist .xf-time { grid-column: 2 / -1; text-align: left; }
}

/* ---- minimising the dock, and what a cancel left behind --------------------- */

/* Minimised is a small pill in the corner, not a full-width bar with its body
   hidden.
 *
 * The point of minimising is to get the panel out of the way, and a 360px
 * strip lying across the corner is still in the way — which is why people were
 * cancelling transfers instead. It shrinks to its title, the whole header
 * becomes the target, and the jobs carry on behind it. */
.updock.minimised {
  max-height: none; width: auto; max-width: min(320px, calc(100vw - 36px));
  border-radius: 999px; box-shadow: var(--shadow-2);
}
.updock.minimised .updock-list,
.updock.minimised .updock-foot { display: none; }
.updock.minimised .updock-head {
  border-bottom: 0; background: var(--surface); padding: 7px 8px 7px 14px; gap: 8px; cursor: pointer;
}
.updock.minimised .updock-title { font-size: 12.5px; white-space: nowrap; }
/* Only the restore control stays; clearing and cancelling belong to the open
   panel, where you can see what you are acting on. */
.updock.minimised .updock-head button:not([id$="Min"]) { display: none; }
.updock.minimised .uprow-x { display: none; }
@media (hover: hover) { .updock.minimised:hover { border-color: var(--accent); } }

/* Stacked so two minimised docks never sit on top of each other. */
.updock.zipdock { bottom: 18px; }

#xferDockMin .ic { transition: transform .18s var(--ease); }
#xferDockMin.flipped .ic { transform: rotate(180deg); }
#xferDockStopAll { color: var(--danger); }

.xfer-partial {
  margin-top: 8px; padding: 9px 10px; border-radius: var(--r-sm);
  background: var(--warn-soft); border: 1px solid var(--warn);
}
.xfer-partial-head {
  display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
  font-size: 12.5px; color: var(--warn);
}
.xfer-partial-head .ic { width: 15px; height: 15px; flex: none; }
.xfer-partial-list { margin: 6px 0; max-height: 130px; overflow-y: auto; }
.xfer-partial-acts { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 12px; }
.xfer-partial-acts button { font-size: 12px; padding: 4px 9px; }

/* ---- a transfer's own file list -------------------------------------------- */

.xfer-files { margin-top: 8px; }
.xfer-files-sum {
  display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
  font-size: 12px; list-style: none; padding: 3px 0;
}
.xfer-files-sum::-webkit-details-marker { display: none; }
.xfer-files-sum .ic { width: 13px; height: 13px; }
/* Capped and scrolled rather than growing the dock: a match can be thousands
   of files, and a panel that grows to fit them pushes the app off screen. */
/* No scrollbar of its own.
 *
 * This had max-height:220px and overflow-y:auto, which put a 219px scroll box
 * inside the dock's 360px scroll box inside a 423px panel — two nested
 * scrollbars, on a screen with 800px going spare. The dock already scrolls;
 * the list just needs to be part of it. The row cap (200) is what keeps it
 * finite. */
.xfer-files-list {
  margin-top: 6px; border-top: 1px solid var(--line); padding-top: 6px;
}
.xf-row {
  display: flex; align-items: baseline; gap: 8px; font-size: 12px;
  padding: 3px 0; line-height: 1.4;
}
/* A row that also carries a route line becomes a small grid, so the route can
   sit under the name rather than after the timestamp. */
.updock .xf-row-wide {
  display: grid; grid-template-columns: 14px 1fr auto; gap: 2px 8px; align-items: baseline;
}
.xf-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.xf-size { flex: none; }
.xf-dot { flex: none; width: 12px; text-align: center; font-size: 11px; }
.xf-done .xf-name { opacity: .55; }
.xf-dot.xf-done { color: var(--accent); }
.xf-dot.xf-running { color: var(--accent); animation: xfpulse 1.1s ease-in-out infinite; }
.xf-dot.xf-failed { color: var(--danger); }
.xf-err { flex: none; max-width: 45%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@keyframes xfpulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
@media (prefers-reduced-motion: reduce) { .xf-dot.xf-running { animation: none; } }

/* ---- shared downloads ------------------------------------------------------ */

.badge.sharedbadge {
  background: var(--accent-soft); color: var(--accent);
  display: inline-flex; align-items: center; gap: 4px;
}
.badge.sharedbadge .ic { width: 12px; height: 12px; }
/* The share toggle reads as pressed when it is on — it is a state, not an
   action you repeat. */
button[data-share].on { color: var(--accent); background: var(--accent-soft); border-color: var(--accent); }
.song-act.on { color: var(--accent); background: var(--accent-soft); }

/* ---- files this app downloaded -------------------------------------------- */

/* A left edge, not a background wash: the row already uses background for
   "selected" and "duplicate", and a third meaning on the same property would
   make all three unreadable together. */
.songrow.fromapp, .songcard.fromapp { box-shadow: inset 3px 0 0 var(--accent); }
.songcard.fromapp { border-radius: var(--r-md); }
.badge.appbadge {
  background: var(--accent-soft); color: var(--accent);
  display: inline-flex; align-items: center; gap: 4px; white-space: nowrap;
}
.badge.appbadge .ic { width: 12px; height: 12px; }
.songmine.on { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.songmine .tabbadge { margin-left: 6px; }

/* ---- installing on an iPhone ----------------------------------------------- */

.install-box { width: min(480px, 94vw); }
.install-steps { margin: 12px 0; padding-left: 22px; line-height: 1.65; font-size: 14px; }
.install-steps li { margin-bottom: 6px; }
/* The real Share glyph inline in the sentence: "tap the share button" is much
   easier to follow when the button is drawn where the words are. */
.ios-share {
  display: inline-flex; vertical-align: -4px; width: 20px; height: 20px;
  margin: 0 2px; color: var(--accent);
}
.ios-share svg { width: 100%; height: 100%; }
.install-note, .install-warn {
  font-size: 13px; line-height: 1.55; padding: 10px 12px; border-radius: var(--r-sm);
}
.install-note { background: var(--surface-2); border: 1px solid var(--line); }
.install-warn { background: var(--warn-soft); border: 1px solid var(--warn); color: var(--warn); }
.install-url { display: flex; gap: 8px; margin: 12px 0 4px; }
.install-url input {
  flex: 1; min-width: 0; font-size: 13px; font-family: ui-monospace, monospace;
}

/* ---- Match all: picking the files ------------------------------------------ */

.match-box { width: min(760px, 94vw); }
/* Where you are in scan → choose → copy. The question a list of files you did
   not ask for provokes is "has it already started?", so the answer is at the
   top of the box. */
.match-steps { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 6px; }
.match-step {
  display: inline-flex; align-items: center; gap: 5px; font-size: 12px;
  padding: 3px 9px; border-radius: 999px; background: var(--surface-3); color: var(--muted);
}
.match-step .ic { width: 13px; height: 13px; }
.match-step.done { background: var(--accent-soft); color: var(--accent); }
.match-step.now { background: var(--surface-2); color: var(--text); border: 1px solid var(--line-2); font-weight: 600; }
/* The scanning button: the refresh icon turns while it works, so a slow folder
   reads as busy rather than stuck. */
.is-busy .ic { animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .is-busy .ic { animation: none; } }

.match-tools {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 10px 0 8px;
}
.match-search { flex: 1 1 200px; min-width: 160px; }
/* Scrolled, not grown: a match can be hundreds of files and a box that grows
   to fit them walks off the bottom of the screen. */
.matchlist {
  max-height: min(46vh, 420px); overflow-y: auto;
  border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--surface-2);
}
.matchrow {
  display: grid; grid-template-columns: auto auto 1fr auto; gap: 10px; align-items: center;
  padding: 7px 11px; font-size: 13px; cursor: pointer; border-bottom: 1px solid var(--line);
}
.matchrow:last-child { border-bottom: 0; }
.matchrow input { accent-color: var(--accent); }
@media (hover: hover) { .matchrow:hover { background: var(--surface-3); } }
/* Direction is the thing you scan for, so it is a chip rather than an arrow
   buried in the text. */
.match-dir {
  font-size: 11px; font-weight: 700; padding: 2px 7px; border-radius: 999px;
  white-space: nowrap; background: var(--surface-3);
}
.match-dir.to-right { background: var(--accent-soft); color: var(--accent); }
/* Same thing, different name: shown, scored, and deliberately not ticked. */
.matchlikely { border-top: 2px solid var(--line-2); padding: 10px 11px; background: var(--surface-3); }
.matchlikely-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 12.5px; margin-bottom: 6px; }
.matchlikely-head .ic { width: 15px; height: 15px; }
.likelyrow { display: flex; align-items: baseline; gap: 10px; font-size: 12px; padding: 3px 0; }
.likely-pct { flex: none; font-weight: 700; color: var(--warn); min-width: 38px; }
.likely-pair { display: flex; gap: 8px; min-width: 0; flex-wrap: wrap; }
.likely-pair > span { overflow: hidden; text-overflow: ellipsis; }
.match-dir.to-left { background: var(--surface-3); }
.match-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.match-size { white-space: nowrap; font-size: 12px; }
.matchnote { margin-top: 8px; font-size: 12px; }
@media (max-width: 560px) {
  .matchrow { grid-template-columns: auto 1fr auto; }
  .matchrow .match-dir { grid-column: 2; justify-self: start; }
  .matchrow .match-name { grid-column: 2; }
  .matchrow .match-size { grid-column: 3; }
}

/* ---- moving one account's downloads to another ---------------------------- */

.movebox .moverow {
  display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap;
  margin-top: 8px; padding: 10px 11px;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-sm);
}
.movebox .field { margin: 0; }
.movebox select { min-width: 160px; }

/* ---- transfer outcome ----------------------------------------------------- */

.xfer-tag {
  margin-left: auto; flex: none; font-size: 12px; font-weight: 700;
  padding: 2px 8px; border-radius: 999px; background: var(--surface-3);
}
.xfer-tag.ok { background: var(--accent-soft); color: var(--accent); }
.xfer-tag.miss { background: var(--danger-soft); color: var(--danger); }
/* The cancel button is the last child while a job runs; the tag takes that
   slot once it has finished, so they never both push at the margin. */
.uprow-top .xfer-tag + .uprow-x { margin-left: 8px; }
.matchplan { margin: 0 0 10px; padding-left: 18px; line-height: 1.7; }

/* ---- which pages a member may see ---------------------------------------- */

.personwrap { border-bottom: 1px solid var(--line); }
.personwrap .personrow { border-bottom: 0; }
.pagesbox { margin: 0 0 10px 44px; }
.pagessum {
  display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
  font-size: 13px; padding: 5px 10px; border-radius: var(--r-sm);
  border: 1px solid var(--line); background: var(--surface-2); width: max-content;
  list-style: none;
}
.pagessum::-webkit-details-marker { display: none; }
.pagessum .ic { width: 15px; height: 15px; }
@media (hover: hover) { .pagessum:hover { border-color: var(--line-2); } }
.pagegrid {
  display: grid; gap: 6px; margin-top: 8px;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
}
.pageopt {
  display: grid; grid-template-columns: auto 1fr; gap: 2px 10px; align-items: start;
  padding: 9px 11px; background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--r-sm); cursor: pointer; font-size: 13px;
}
.pageopt input { accent-color: var(--accent); grid-row: 1 / span 2; margin-top: 2px; }
.pageopt-name { font-weight: 600; }
.pageopt-note { font-size: 12px; line-height: 1.35; }
/* Locked rows stay legible rather than greyed to the edge of readable: the
   owner is meant to READ why they cannot be ticked, not merely notice that
   they cannot. */
.pageopt-locked { cursor: default; opacity: .72; background: transparent; border-style: dashed; }
@media (hover: hover) { .pageopt:not(.pageopt-locked):hover { border-color: var(--line-2); } }
@media (max-width: 560px) { .pagesbox { margin-left: 0; } }

/* ---- sign in with Google ---------------------------------------------------
   Google's brand guidance: their mark, unmodified, on a neutral surface, with
   the wording they specify. */
.login-or {
  display: flex; align-items: center; gap: 12px;
  margin: 18px 0 14px; color: var(--muted); font-size: 12.5px;
}
.login-or::before, .login-or::after {
  content: ''; flex: 1; height: 1px; background: var(--line);
}
.googlebtn {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  width: 100%; padding: 12px 16px;
  border: 1px solid var(--line-2); border-radius: var(--r-md);
  background: var(--surface); color: var(--text);
  font-size: 14px; font-weight: 600; text-decoration: none;
  transition: background .16s, border-color .16s;
}
@media (hover: hover) { .googlebtn:hover { background: var(--surface-2); border-color: var(--line-2); } }
.googlebtn .gicon { width: 18px; height: 18px; flex: none; }

/* ---- the file manager ----------------------------------------------------- */

/* Breadcrumbs. Every ancestor is a click target; the folder you're in is not. */
.crumbs {
  display: flex; flex-wrap: wrap; align-items: center; gap: 2px;
  margin-bottom: 12px; font-size: 13px;
}
.crumb {
  padding: 4px 8px; border: 0; border-radius: var(--r-sm);
  background: transparent; color: var(--accent); font-weight: 500;
}
@media (hover: hover) { .crumb:not(.current):hover { background: var(--surface-2); } }
.crumb.current { color: var(--text); font-weight: 600; cursor: default; }
.crumb-sep { color: var(--muted); opacity: .5; }

.filelist { display: flex; flex-direction: column; gap: 2px; }
.filerow {
  display: flex; align-items: center; gap: 12px; padding: 8px 12px;
  border: 1px solid transparent; border-radius: var(--r-md);
  transition: background .16s, border-color .16s;
}
@media (hover: hover) { .filerow:hover { background: var(--surface); border-color: var(--line); } }
.filerow.picked { background: var(--accent-soft); border-color: var(--accent); }

.file-pick { flex: none; display: flex; align-items: center; cursor: pointer; }
.file-pick input { width: 17px; height: 17px; accent-color: var(--accent-fill); cursor: pointer; }

.file-icon {
  flex: none; display: flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 9px;
  background: var(--surface-2); color: var(--muted);
}
.file-icon .ic { width: 17px; height: 17px; }
/* A folder is the thing you click most, so it gets the accent. */
.file-folder { color: var(--accent); background: var(--accent-soft); }
.file-audio, .file-video { color: var(--dup); background: color-mix(in srgb, var(--dup) 14%, transparent); }

.file-main { flex: 1; min-width: 0; }
.filerow.isdir .file-main, .file-icon { cursor: pointer; }
.file-name {
  font-size: 14px; font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.filerow.isdir .file-name { font-weight: 600; }
.file-meta { font-size: 12.5px; margin-top: 2px; }
.file-actions { display: flex; gap: 4px; flex: none; }
/* Actions stay out of the way until the row matters, as in the songs list. */
@media (hover: hover) {
  .file-actions .song-act { opacity: 0; }
  .filerow:hover .file-actions .song-act, .file-actions .song-act:focus-visible { opacity: 1; }
}

@media (max-width: 620px) {
  .file-icon { width: 30px; height: 30px; }
  .file-actions .song-act { opacity: 1; }
}

/* The listing is the drop target. Dashed accent border only while something is
   actually over it, so it isn't shouting at you the rest of the time. */
.filelist.dropping {
  outline: 2px dashed var(--accent);
  outline-offset: 4px;
  background: var(--accent-soft);
  border-radius: var(--r-md);
}

/* ---- side by side ----------------------------------------------------------
   Two independent panes with a column of controls between them. Each pane
   scrolls on its own — the whole point is to keep both folders on screen, and a
   single page scroll would drag one out of view to reach the other. */

.dualwrap {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  gap: 12px;
  /* Both panes take the height of the taller one. Two boxes of different
     heights read as one being unfinished. */
  align-items: stretch;
}

.dualpane {
  min-width: 0;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface);
  padding: 12px;
  transition: opacity .16s;
}
/* Grey the source out while its drop is being carried out, so a second drag
   onto a half-finished move is visibly not on. */
.dualpane.dualbusy { opacity: .55; pointer-events: none; }

.dualbar { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; }
.dualbar .libselect { flex: 1; min-width: 0; }
.dualbar .iconbtn { flex: none; }

/* Narrower than the file manager's, because it sits in a half-width pane. */
.dualsearch { margin-bottom: 10px; }
.dualsearch .libsearch { font-size: 13px; }

.dualpane .crumbs { margin-bottom: 8px; font-size: 12.5px; }
.dualpane .countline { margin-bottom: 8px; }

.duallist {
  max-height: min(58vh, 560px);
  overflow: auto;
  overscroll-behavior: contain;
  border-radius: var(--r-md);
}
.dualrow { cursor: default; user-select: none; }
.dualrow .file-name { pointer-events: none; }
.dualrow.dragging { opacity: .4; }

/* A folder lights up when a drag is over it, because dropping there means
   "inside this one" rather than "in the folder I'm looking at". */
.dualrow.dual-over {
  background: var(--accent-soft);
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}
.crumb.crumb-drop { background: var(--accent-soft); color: var(--text); }

.dualmid {
  display: flex; flex-direction: column; gap: 8px;
  align-self: center;
}
.dualarrow {
  display: flex; align-items: center; gap: 6px;
  padding: 10px 12px; white-space: nowrap;
}
.dualarrow:disabled { opacity: .4; }
.dualarrow-label { font-size: 13px; font-weight: 600; }
/* One arrow glyph, turned to point at the side it sends to. */
#dualToRight .ic { transform: rotate(90deg); }
#dualToLeft .ic { transform: rotate(-90deg); }

.dualhint { margin-top: 14px; }

/* Below this width two panes are narrower than a filename, so they stack. The
   arrows turn to match: on a phone the other side is below, not beside. */
@media (max-width: 900px) {
  .dualwrap { grid-template-columns: minmax(0, 1fr); }
  .dualmid { flex-direction: row; justify-content: center; }
  /* Stacked, the other side is below rather than beside. */
  #dualToRight .ic { transform: rotate(180deg); }
  #dualToLeft .ic { transform: none; }
  .duallist { max-height: min(44vh, 420px); }
}

/* ---- phones: buttons become their icons ------------------------------------
   A toolbar of "New folder / Upload / Zip / Send to server… / Copy to… /
   Move to…" is wider than a phone, so it wrapped onto three lines and pushed
   the list off the screen. Every one of these already carries an icon that says
   the same thing, so on a narrow screen the words go and the icon stays.

   font-size: 0 rather than hiding a span, because most of these buttons hold a
   bare text node next to their <svg> with nothing to hang a selector on. The
   icon is immune: .ic sets its size in pixels.

   :has(> svg.ic) is the safety catch — a button with no icon (Clear, Done,
   Cancel) is left completely alone, since collapsing one of those would leave
   an empty circle. The name survives in title/aria-label, so it is still
   announced and still available on a long press. */
@media (max-width: 620px) {
  .songbar > button:has(> svg.ic),
  .selacts > button:has(> svg.ic),
  .libactions > button:has(> svg.ic),
  .dualmid .dualarrow {
    font-size: 0; gap: 0;
    width: 44px; min-width: 44px; padding: 0;
    display: inline-flex; align-items: center; justify-content: center;
  }
  .songbar > button:has(> svg.ic) .ic,
  .selacts > button:has(> svg.ic) .ic,
  .libactions > button:has(> svg.ic) .ic { width: 18px; height: 18px; }
  /* The side-by-side arrows keep a label — there the word IS the instruction,
     and two bare arrows on a stacked layout say nothing about what they do. */
  .dualmid .dualarrow { width: auto; min-width: 0; padding: 10px 14px; font-size: 13px; }
}

/* ---- the upload page -------------------------------------------------------
   A target you cannot miss, because the alternative is hunting for a button. */

.updrop {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 6px; padding: 40px 24px; margin-bottom: 18px;
  border: 2px dashed var(--line-2); border-radius: var(--r-lg);
  background: var(--surface); color: var(--muted);
  cursor: pointer; text-align: center;
  transition: border-color .16s, background .16s, color .16s;
}
@media (hover: hover) { .updrop:hover { border-color: var(--accent); background: var(--surface-2); } }
.updrop:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
/* Only while something is genuinely over it — a target that shouts all the time
   stops meaning anything. */
.updrop.dropping {
  border-color: var(--accent); background: var(--accent-soft); color: var(--text);
}
.updrop-mark {
  display: grid; place-items: center; width: 48px; height: 48px;
  border-radius: 50%; background: var(--surface-2); color: var(--accent);
}
.updrop-mark .ic { width: 22px; height: 22px; }
.updrop-title { font-size: 16px; font-weight: 650; color: var(--text); }
.updrop-hint { font-size: 13px; }

.uplist { display: flex; flex-direction: column; gap: 10px; }
.uplist .uprow {
  padding: 12px 14px; border-radius: var(--r-md);
  background: var(--surface-2); border: 1px solid var(--line);
}
/* An upload waiting for its file to be pointed at again. */
.uplist .up-paused { border-style: dashed; }
.uplist .up-failed { border-color: var(--danger); }
/* Waiting to try again, which is not the same as stopped. The bar keeps the
   ground it has taken and the border says something is still happening. */
.uplist .up-retrying { border-color: var(--warn, var(--accent)); }
.uplist .up-retrying .uprow-bar span { opacity: .55; }
.uplist .up-done .uprow-bar span { background: var(--accent-fill); }

@media (max-width: 620px) {
  .updrop { padding: 28px 16px; }
}

/* The file moving right now, under the overall bar. A big file is otherwise a
   bar that does not move and a count that does not change. */
/* Stalled, not merely slow. The bar keeps whatever ground it took — the bytes
   really did arrive — but the row stops claiming a rate it no longer has. */
.stalled { color: var(--danger); font-weight: 600; }
.up-stalled, .job-stalled { border-color: var(--danger); }
.up-stalled .uprow-bar span, .job-stalled .uprow-bar span {
  background: repeating-linear-gradient(45deg,
    var(--danger) 0 6px, color-mix(in srgb, var(--danger) 55%, transparent) 6px 12px);
}

.xfer-now {
  display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
  margin-top: 7px; font-size: 12px;
}
.xfer-now-name { flex: 1; min-width: 0; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.xfer-now-pct { flex: none; font-variant-numeric: tabular-nums; }
.uprow-bar-sub { margin-top: 4px; height: 3px; opacity: .75; }

/* ---- what was uploaded recently -------------------------------------------- */

.uphistory { margin-top: 34px; padding-top: 6px; border-top: 1px solid var(--line); }
.uphistory-head { margin-bottom: 14px; }
.uphistory-head h3 { font-size: 19px; }
.uphistory-head .muted { margin-top: 4px; font-size: 13px; max-width: 62ch; }

.uppast { display: flex; flex-direction: column; gap: 2px; }
/* A day heading, so a morning's uploads read as one batch rather than a list
   of timestamps to compare. */
.uppast-day {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  margin: 16px 0 4px; padding-bottom: 6px;
  border-bottom: 1px solid var(--line);
}
.uppast-day:first-child { margin-top: 4px; }
.uppast-daylabel { font-size: 13px; font-weight: 700; letter-spacing: .01em; }
.uppast-daymeta { font-size: 12px; }

/* A file that has since been moved or binned stays listed — "where did that go"
   is a fair question — but cannot be picked, because there is nothing to act
   on. */
.pastgone { opacity: .55; }
.pastgone .file-pick input { cursor: not-allowed; }

.serverpick { display: flex; flex-direction: column; gap: 8px; }
.serverpick-opt { justify-content: flex-start; text-align: left; padding: 12px 14px; }

/* ---- compressing video -----------------------------------------------------
   Two surfaces: the quality picker, which has to make a size trade-off legible
   at a glance, and the jobs panel, which has to be readable while four things
   are moving at once. */

.shrinkopts { display: flex; flex-direction: column; gap: 8px; margin: 4px 0 12px; }
.shrink-shape { font-size: 12.5px; margin: 0 0 4px; }
.shrinkopt {
  display: grid; grid-template-columns: auto auto 1fr; align-items: baseline;
  gap: 10px; width: 100%; text-align: left;
  padding: 14px 16px; border-radius: var(--r-md);
  background: var(--surface-2); border: 1px solid var(--line); color: var(--text);
  transition: border-color .16s, background .16s;
}
@media (hover: hover) { .shrinkopt:not(:disabled):hover { background: var(--surface-3); border-color: var(--accent); } }
.shrinkopt:disabled { opacity: .55; }
.shrinkopt-q { font-size: 16px; font-weight: 700; }
.shrinkopt-size { font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums; }
.shrinkopt-note { font-size: 12px; }

/* The jobs panel is taller than a normal modal on purpose — four encodes with
   progress bars is the case it exists for, not the exception. */
.jobslist { display: flex; flex-direction: column; gap: 10px; max-height: 56vh; overflow-y: auto; margin-bottom: 6px; }
.jobrow {
  padding: 12px 14px; border-radius: var(--r-md);
  background: var(--surface-2); border: 1px solid var(--line);
}
.jobrow-top { display: flex; align-items: center; gap: 8px; }
.jobrow-name { flex: 1; min-width: 0; font-size: 13.5px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.jobrow-q {
  flex: none; font-size: 11px; font-weight: 700; letter-spacing: .04em;
  padding: 2px 7px; border-radius: 999px;
  background: var(--accent-soft); color: var(--accent);
}
/* GPU and CPU are told apart by colour as well as by the word, so a glance down
   a list of jobs answers "which of these used the card?" without reading. */
.jobrow-hw { background: color-mix(in srgb, var(--dup) 16%, transparent); color: var(--dup); }
.jobrow-cpu { background: var(--surface-3); color: var(--muted); }
.jobrow-fell { background: var(--danger-soft); color: var(--danger); }
.jobrow-meta { font-size: 12px; margin-top: 5px; }
.job-failed { border-color: var(--danger); }
.job-failed .jobrow-meta { color: var(--danger); }
.job-done .uprow-bar span { background: var(--accent-fill); }

/* Original beside compressed, so the decision to remove one is made by looking
   at both rather than by trusting a number. */
.jobcompare { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 10px; }
.jobside {
  display: flex; flex-direction: column; gap: 3px; align-items: flex-start; min-width: 0;
  padding: 10px 12px; border-radius: var(--r-sm);
  background: var(--surface); border: 1px solid var(--line);
}
.jobside-new { border-color: var(--accent); }
.jobside-label { font-size: 10.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
.jobside-size { font-size: 16px; font-weight: 700; font-variant-numeric: tabular-nums; }
.jobside-name { font-size: 11.5px; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.jobside-acts { display: flex; align-items: center; gap: 6px; margin-top: 6px; }
.jobside-act { padding: 5px 11px; font-size: 12px; }
.jobside-bin { padding: 5px 8px; color: var(--danger); background: var(--danger-soft); border-color: transparent; }
.jobside-bin .ic { width: 14px; height: 14px; }
/* A side that has gone to the Bin stays on screen, greyed — seeing which one
   you kept is the point of the comparison. */
.jobside-gone { opacity: .6; }

/* The "N finished while you were away" note carries its own way in. */
.notice-act {
  margin-left: 10px; padding: 3px 10px; font-size: 12px; font-weight: 700;
  border-radius: 999px; border: 1px solid currentColor;
  background: transparent; color: inherit; cursor: pointer;
}

@media (max-width: 620px) {
  .jobcompare { grid-template-columns: 1fr; }
  .shrinkopt { grid-template-columns: auto 1fr; }
  .shrinkopt-note { grid-column: 1 / -1; }
}

/* ---- the upload queue ------------------------------------------------------
   Fixed, so it stays put while you carry on browsing — an upload in progress
   must not pin you to one screen. */
.updock {
  position: fixed; right: 18px; bottom: 18px; z-index: 95;
  width: min(360px, calc(100vw - 36px));
  max-height: min(52vh, 460px); display: flex; flex-direction: column;
  transition: max-height .18s var(--ease), width .18s var(--ease);
  background: var(--surface); border: 1px solid var(--line-2);
  border-radius: var(--r-lg); box-shadow: var(--shadow-3); overflow: hidden;
}
body.player-open .updock { bottom: calc(18px + 84px); }
/* Something is expanded inside it, so give it the room it needs rather than
   making a list of forty files share 52vh with everything else. */
.updock.expanded { max-height: min(84vh, 760px); width: min(460px, calc(100vw - 36px)); }
/* What the dock IS, said once, where the question is asked. People were
   cancelling transfers to get the panel off the screen. */
.updock-foot {
  padding: 8px 12px; border-top: 1px solid var(--line); background: var(--surface-2);
  font-size: 11.5px; line-height: 1.45;
}
.updock-head {
  display: flex; align-items: center; gap: 10px; padding: 11px 12px 11px 15px;
  border-bottom: 1px solid var(--line); background: var(--surface-2);
}
.updock-title { flex: 1; font-size: 13.5px; font-weight: 600; }
.updock-list { overflow-y: auto; padding: 8px; display: flex; flex-direction: column; gap: 6px; }

.uprow { padding: 8px 10px; border-radius: var(--r-sm); background: var(--surface-2); }
.uprow-top { display: flex; align-items: center; gap: 8px; }
.uprow-name {
  flex: 1; min-width: 0; font-size: 13px; font-weight: 500;
  display: flex; align-items: baseline; gap: 6px; overflow: hidden;
}
/* Two ends and an arrow. Each end shrinks on its own, so a long source folder
   cannot squeeze the destination out of the row entirely. */
.xr-end { display: flex; align-items: baseline; gap: 4px; min-width: 0; flex: 1 1 auto; }
.xr-end > .xr-dir { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11.5px; font-weight: 400; }
.xr-arrow { flex: none; opacity: .75; }
.uprow-x { width: 26px; height: 26px; flex: none; }
.uprow-bar {
  height: 4px; margin: 7px 0 5px; border-radius: 999px;
  background: var(--surface-3); overflow: hidden;
}
.uprow-bar span {
  display: block; height: 100%; border-radius: 999px;
  background: var(--accent-fill); transition: width .25s var(--ease);
}
.uprow-meta { font-size: 11.5px; font-variant-numeric: tabular-nums; }
.up-done .uprow-bar span { background: var(--accent); }
.up-failed .uprow-bar span, .up-cancelled .uprow-bar span { background: var(--danger); }
.up-queued .uprow-bar span { background: var(--muted); }

@media (max-width: 620px) {
  .updock { right: 10px; left: 10px; width: auto; bottom: calc(var(--navh) + var(--safe-b) + 10px); }
  body.player-open .updock { bottom: calc(var(--navh) + var(--safe-b) + 94px); }
}

/* ---- selecting rows in bulk ---------------------------------------------- */

.song-pick { flex: none; display: flex; align-items: center; cursor: pointer; }
.song-pick input { width: 17px; height: 17px; accent-color: var(--accent-fill); cursor: pointer; }
.songrow.picked { background: var(--accent-soft); border-color: var(--accent); }

/* A duplicate: same name and byte size as another file in this tab. Low alpha
   on purpose — a folder that is mostly duplicates should still be readable —
   with an inset bar rather than a border so nothing shifts sideways. */
.songrow.dup {
  background: color-mix(in srgb, var(--dup) 13%, transparent);
  box-shadow: inset 3px 0 0 var(--dup);
}
@media (hover: hover) {
  .songrow.dup:hover { background: color-mix(in srgb, var(--dup) 20%, transparent); }
}
/* Selected wins over duplicate — you need to see what you are about to act on. */
.songrow.dup.picked { background: var(--accent-soft); box-shadow: inset 3px 0 0 var(--accent); }
.badge.dupbadge {
  display: inline-flex; align-items: center; gap: 4px;
  color: var(--dup); background: color-mix(in srgb, var(--dup) 15%, transparent);
}
.badge.dupbadge .ic { width: 12px; height: 12px; }

/* An auto-mounted folder — derived from where this app uploads, so there is no
   remove button on it, and the chip says why. */
.dirchip.autochip { border-style: dashed; }
.dirchip.autochip em {
  font-style: normal; font-size: 10.5px; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--accent); opacity: .85;
}

/* Video cards. Selection sits on the thumbnail so it can't be missed, and the
   duplicate flag rides the same corner the duration badge uses elsewhere. */
.card-pick {
  position: absolute; top: 8px; left: 8px; z-index: 2;
  display: flex; padding: 5px; border-radius: 7px;
  background: rgba(0, 0, 0, .55); cursor: pointer;
}
.card-pick input { width: 17px; height: 17px; accent-color: var(--accent-fill); cursor: pointer; }
.dupflag {
  position: absolute; left: 8px; bottom: 8px; z-index: 2;
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 8px; border-radius: 999px;
  font-size: 11.5px; font-weight: 600; color: #fff;
  background: color-mix(in srgb, var(--dup) 82%, black);
}
.dupflag .ic { width: 12px; height: 12px; }
.libcard.dup { border-color: var(--dup); }
.libcard.picked { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent), var(--shadow-2); }
.songcard .actions { margin-top: 10px; }

.selbar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
  padding: 9px 12px; margin-bottom: 14px;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-md);
}
/* Duplicate review is a mode you're in, so the button reads as switched on. */
.selbar .ghost.on { border-color: var(--dup); color: var(--dup); background: color-mix(in srgb, var(--dup) 14%, transparent); }
.selcheck { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 13.5px; }
.selcheck input { width: 17px; height: 17px; accent-color: var(--accent-fill); cursor: pointer; }
.selcount { font-size: 13px; color: var(--accent); font-weight: 600; }
.selacts { display: inline-flex; flex-wrap: wrap; gap: 7px; margin-left: auto; }
.selacts button { padding: 8px 13px; font-size: 13px; }
.selacts .seldanger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 45%, transparent); }
@media (hover: hover) {
  .selacts .seldanger:hover { background: color-mix(in srgb, var(--danger) 14%, transparent); border-color: var(--danger); }
}
@media (max-width: 620px) {
  .selacts { margin-left: 0; width: 100%; }
  .selacts button { flex: 1 1 auto; }
}

/* ============================================================================
   Playlists / failed / past downloads / servers
   ========================================================================== */

.plcard .plcover { display: grid; }
.plcard .noimg .ic { width: 40px; height: 40px; }
.pl-picker { display: flex; flex-direction: column; gap: 8px; margin: 6px 0 18px; max-height: 46vh; overflow-y: auto; }
.pl-pickrow {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  background: var(--surface-2); border: 1px solid var(--line); color: var(--text);
  padding: 13px 15px; border-radius: var(--r-sm); font-size: 14px; text-align: left; width: 100%;
}
@media (hover: hover) { .pl-pickrow:hover { border-color: var(--line-2); } }
.pl-pickrow.on { border-color: var(--accent); background: var(--accent-soft); }
.pl-pickname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.pl-pickmark { flex: none; font-size: 12px; color: var(--muted); }
.pl-pickrow.on .pl-pickmark { color: var(--accent); }
.pl-create { display: flex; gap: 8px; margin-bottom: 18px; }
.pl-create input { flex: 1; min-width: 0; }
.pl-create button { padding: 12px 16px; font-size: 13.5px; }

.faillist { display: flex; flex-direction: column; gap: 10px; margin-top: 4px; max-width: 980px; }
.failrow {
  display: flex; gap: 14px; align-items: flex-start; padding: 14px 16px;
  background: var(--surface); border: 1px solid var(--danger-soft);
  border-left: 3px solid var(--danger); border-radius: var(--r-md);
}
.fail-thumb {
  width: 76px; height: 47px; flex: none; border-radius: 8px; object-fit: cover;
  background: var(--surface-3); display: grid; place-items: center;
  color: var(--muted); overflow: hidden;
}
.fail-body { flex: 1; min-width: 0; }
.fail-title { font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fail-meta { font-size: 12px; color: var(--muted); margin-top: 4px; }
.fail-attempts { color: var(--warn); }
.fail-err { font-size: 12.5px; color: var(--danger); margin-top: 7px; word-break: break-word; }
.fail-url {
  display: block; font-size: 11.5px; color: var(--muted); margin-top: 6px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-decoration: none;
}
@media (hover: hover) { .fail-url:hover { color: var(--accent); } }
.fail-result { font-size: 12px; margin-top: 8px; color: var(--muted); }
.fail-actions { display: flex; flex-direction: column; gap: 7px; flex: none; }
.fail-actions button { padding: 8px 15px; font-size: 12.5px; }
.fail-retry { background: var(--accent-fill); color: var(--accent-ink); }

.reclist { display: flex; flex-direction: column; gap: 8px; max-width: 980px; }
/* A past download reads like every other list in the app: art, then what it
   was, then the link — rather than a bare URL you have to decode. */
.recrow {
  display: flex; align-items: center; gap: 13px; padding: 9px 12px;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-md);
  transition: border-color .16s, background .16s;
}
@media (hover: hover) { .recrow:hover { border-color: var(--line-2); background: var(--surface-2); } }
.rec-thumb {
  position: relative; width: 84px; aspect-ratio: 16/9; flex: none;
  border-radius: 8px; overflow: hidden; background: var(--surface-3);
}
.rec-body { flex: 1; min-width: 0; cursor: pointer; }
.rec-title { font-weight: 600; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rec-meta { display: flex; align-items: center; gap: 7px; font-size: 12.5px; margin-top: 3px; }
.rec-meta .badge { margin: 0; }
.recurl-text {
  margin-top: 3px; font-size: 12px; color: var(--accent); opacity: .85;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rec-actions { display: flex; gap: 6px; flex: none; }
.reccopy.copied { color: var(--accent); border-color: var(--accent); }

@media (max-width: 620px) {
  .rec-thumb { width: 64px; }
  .recurl-text { display: none; }
}

.serverlist { display: flex; flex-direction: column; gap: 10px; margin: 4px 0 26px; max-width: 860px; }
.server {
  display: flex; flex-direction: column; align-items: flex-start; gap: 7px;
  padding: 16px 18px; background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-md); box-shadow: var(--shadow-1);
}
.s-main { font-weight: 700; font-size: 15px; }
.s-sub { color: var(--muted); font-size: 12.5px; overflow-wrap: anywhere; }
.s-sub .ic { width: 15px; height: 15px; }
.s-actions { display: flex; gap: 8px; margin-top: 6px; }
.s-actions button { padding: 8px 15px; font-size: 12.5px; }

.serverform { padding: 22px; max-width: 860px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); }
.serverform h3 { margin-bottom: 4px; }
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 16px; }
.formactions { display: flex; gap: 10px; align-items: center; }

.fmtpaths { border-top: 1px solid var(--line); margin: 6px 0 20px; padding-top: 18px; }
.fmtpaths-head { display: flex; flex-direction: column; gap: 5px; margin-bottom: 12px; }
.fmtpaths-head > span:first-child { font-size: 12.5px; font-weight: 600; color: var(--muted); }
.fmtpath-rows { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; }
.fmtpath-row { display: flex; align-items: center; gap: 8px; }
.fmtpath-row input { padding: 11px 13px; font-size: 14px; }
.fmt-ext { width: 104px; flex: none; text-align: center; }
.fmt-dir { flex: 1; }
.fmt-arrow { color: var(--muted); flex: none; }
.fmtpath-row button { flex: none; width: 38px; height: 38px; padding: 0; border-radius: 50%; }
.fmtpath-row button .ic { width: 16px; height: 16px; }

/* ============================================================================
   Mini-player
   ========================================================================== */

body.player-open .content { padding-bottom: 190px; }

.miniplayer {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 90;
  display: flex; align-items: center; gap: 18px;
  padding: 12px 22px calc(12px + var(--safe-b));
  background: var(--glass);
  backdrop-filter: blur(24px) saturate(180%); -webkit-backdrop-filter: blur(24px) saturate(180%);
  border-top: 1px solid var(--line);
  box-shadow: 0 -10px 40px rgba(0, 0, 0, .28);
  /* How loud the track is right now, 0–1, written by the visualiser loop. The
     artwork and the glow below are driven off it, so the bar breathes with the
     music instead of animating on a timer that has nothing to do with it. */
  --mp-level: 0;
}
/* Every direct child sits above the canvas. Without this the spectrum would be
   painted over the controls rather than behind them. */
.miniplayer > *:not(.mp-viz) { position: relative; z-index: 1; }

/* The spectrum. Absolutely positioned across the whole bar, never interactive,
   and clipped so the bars stop at the top edge rather than bleeding over it. */
.mp-viz {
  position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none; z-index: 0; opacity: .5;
  mask-image: linear-gradient(to top, #000 0%, #000 55%, transparent 100%);
  -webkit-mask-image: linear-gradient(to top, #000 0%, #000 55%, transparent 100%);
}
.miniplayer.viz-off .mp-viz { display: none; }

/* A glow along the top edge that swells with the level — the cheapest way to
   make the bar feel alive without anything moving under the cursor. */
.miniplayer::before {
  content: ""; position: absolute; left: 0; right: 0; top: -1px; height: 2px;
  background: var(--accent-fill);
  opacity: calc(.25 + var(--mp-level) * .75);
  box-shadow: 0 0 calc(6px + var(--mp-level) * 26px) var(--accent-glow);
  pointer-events: none; z-index: 0;
}
.miniplayer.viz-off::before { display: none; }
/* Hairline progress across the very top of the bar — the at-a-glance readout
   that stays legible even when the seek row is scrolled off on a phone. */
.mp-progress { position: absolute; top: 0; left: 0; right: 0; height: 2px; background: var(--line); }
.mp-progress-fill { display: block; height: 100%; width: 0; background: var(--accent-fill); transition: width .25s linear; }

.mp-now { display: flex; align-items: center; gap: 13px; min-width: 0; width: 26%; }
.mp-art {
  width: 50px; height: 50px; flex: none; border-radius: 10px;
  background: var(--surface-3) center/cover no-repeat;
  display: grid; place-items: center; overflow: hidden; box-shadow: var(--shadow-1);
  /* Pulses on the level. Kept small on purpose — at 1.06 it reads as a beat, at
     1.2 it reads as a fault. transform and box-shadow only, so this is a
     compositor job and costs nothing per frame. */
  transform: scale(calc(1 + var(--mp-level) * .06));
  box-shadow: var(--shadow-1), 0 0 calc(var(--mp-level) * 26px) var(--accent-glow);
  transition: transform .09s linear, box-shadow .09s linear;
}
.miniplayer.viz-off .mp-art { transform: none; box-shadow: var(--shadow-1); }

/* The note on a track with no artwork gets the same beat, so the bar does not
   look half-dead when a cover is missing. */
.mp-note { transform: scale(calc(1 + var(--mp-level) * .12)); }

@media (prefers-reduced-motion: reduce) {
  .mp-art, .mp-note { transform: none; transition: none; }
  .miniplayer::before { box-shadow: none; }
}
.mp-note { color: rgba(255, 255, 255, .9); width: 22px; height: 22px; }
.mp-meta { min-width: 0; }
.mp-title { font-size: 14px; font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mp-channel { font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.mp-center { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 7px; max-width: 620px; margin: 0 auto; }
.mp-buttons { display: flex; align-items: center; gap: 8px; }
.mp-btn {
  background: transparent; border: none; color: var(--text);
  padding: 0; border-radius: 50%; width: 38px; height: 38px;
}
.mp-btn .ic { width: 18px; height: 18px; }
@media (hover: hover) { .mp-btn:hover { background: var(--surface-2); } }
.mp-btn.active { color: var(--accent); }
.mp-play { background: var(--accent-fill); color: var(--accent-ink); width: 44px; height: 44px; box-shadow: 0 6px 18px var(--accent-glow); }
.mp-play .ic { width: 19px; height: 19px; }
@media (hover: hover) { .mp-play:hover { background: var(--accent-fill-2); transform: scale(1.05); } }
.mp-seek { display: flex; align-items: center; gap: 11px; width: 100%; }
.mp-time { font-size: 11px; color: var(--muted); width: 40px; text-align: center; flex: none; font-variant-numeric: tabular-nums; }

.mp-range {
  flex: 1; -webkit-appearance: none; appearance: none;
  height: 4px; border-radius: 999px; background: var(--surface-3); cursor: pointer;
}
.mp-range::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; width: 13px; height: 13px;
  border-radius: 50%; background: var(--text); border: none;
  box-shadow: 0 1px 4px rgba(0, 0, 0, .4);
}
.mp-range::-moz-range-thumb { width: 13px; height: 13px; border: none; border-radius: 50%; background: var(--text); }
@media (hover: hover) {
  .mp-range:hover::-webkit-slider-thumb { background: var(--accent-fill); }
}

.mp-side { display: flex; align-items: center; gap: 8px; width: 26%; justify-content: flex-end; }
.mp-volume { display: flex; align-items: center; gap: 6px; }
.mp-vol-range { width: 74px; flex: none; transition: width .18s var(--ease); }
.mp-volume:hover .mp-vol-range, .mp-vol-range:focus-visible { width: 104px; }
.mp-count {
  font-size: 11.5px; font-weight: 600; padding: 6px 11px; border-radius: 999px;
  background: var(--surface-2); border: 1px solid var(--line); color: var(--muted);
  font-variant-numeric: tabular-nums;
}
@media (hover: hover) { .mp-count:hover { color: var(--text); border-color: var(--accent); background: var(--surface-3); } }

/* ============================================================================
   Modals
   ========================================================================== */

body.modal-open { overflow: hidden; }
.modal { position: fixed; inset: 0; z-index: 120; display: flex; align-items: center; justify-content: center; padding: 24px; }
/* Modals are opened FROM other modals, so a single z-index is not enough: with
   everything equal, whichever happens to come later in the markup wins, which
   is arbitrary and was putting the video — and then the delete confirmation —
   behind the thing that launched it.
   
   A deliberate order instead, from what sits on top of what:
     120  ordinary dialogs
     200  the player, opened from a list or a job
     300  the photo lightbox
     400  anything that ASKS A QUESTION and blocks until answered — a
          confirmation must never be hidden behind what it is asking about. */
#playerModal { z-index: 200; }
#confirmModal, #promptModal, #removeModal, #adminDelModal { z-index: 400; }
.modal-backdrop { position: absolute; inset: 0; background: var(--scrim); backdrop-filter: blur(8px); animation: fadein .18s ease; }
.modal-box {
  position: relative; z-index: 1; width: 100%; max-width: 920px;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-xl);
  box-shadow: var(--shadow-3); overflow: hidden; animation: pop .24s var(--ease);
}
.modal-close {
  position: absolute; top: 12px; right: 12px; z-index: 2;
  width: 38px; height: 38px; padding: 0; border-radius: 50%;
  background: rgba(0, 0, 0, .5); color: #fff; border: none;
}
@media (hover: hover) { .modal-close:hover { background: var(--danger); } }
.modal-media { background: #000; }
.modal-media video { display: block; width: 100%; max-height: 78vh; }
.modal-media .audio-hero {
  position: relative; aspect-ratio: 16/9; overflow: hidden;
  display: flex; align-items: flex-end; justify-content: center;
  padding: 0 18px 18px; background: radial-gradient(circle at 50% 35%, var(--surface-3), var(--bg-2));
}
.modal-media .audio-hero img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: .35; }
.modal-media .audio-hero .noimg { position: absolute; inset: 0; display: grid; place-items: center; color: var(--muted); }
.modal-media .audio-hero audio { position: relative; z-index: 1; width: 100%; }
.modal-info { padding: 16px 20px 20px; }
.modal-info h3 { margin-bottom: 6px; font-size: 17px; overflow-wrap: anywhere; }

/* A plain dialog: title, some fields, a row of buttons. .modal-box brings the
   solid surface, the border and the shadow; this only adds the padding and the
   rhythm between the parts. Several dialogs used to carry a .modal-card class
   that was never written, so they rendered as bare text floating over the page
   with nothing behind them. */
/* Resizable, like the player. A dialog that lists things — jobs, folders, a
   file being edited — is one you may want bigger, and the size is the reader's
   business rather than mine. `overflow: auto` is not decoration: `resize` does
   nothing without it. */
.dialog-box, .jobs-box, .folder-box, .editor-card {
  resize: both; overflow: auto;
  min-width: 320px; min-height: 220px; max-width: 98vw; max-height: 94vh;
}
/* The corner is easier to find with something drawn in it. */
.dialog-box, .jobs-box, .folder-box, .editor-card { background-clip: padding-box; }

.dialog-box { max-width: 520px; padding: 24px; display: flex; flex-direction: column; gap: 12px; }
.dialog-box h3 { font-size: 19px; }
.dialog-box > .muted { margin-top: -4px; font-size: 13.5px; }
.dialog-box .modal-actions { justify-content: flex-end; margin-top: 4px; }

/* The jobs panel is the same dialog, sized for a list that scrolls. */
.jobs-box { width: min(640px, 94vw); padding: 24px; display: flex; flex-direction: column; gap: 12px; }
.jobs-box h3 { font-size: 19px; }
.jobs-box > .muted { margin-top: -6px; font-size: 13px; }
.jobs-box .modal-actions { justify-content: flex-end; }

.confirm-box { max-width: 440px; padding: 26px 26px 22px; }
.confirm-box h3 { margin-bottom: 10px; font-size: 19px; }
.confirm-box > p { margin-bottom: 20px; font-size: 14.5px; line-height: 1.55; color: var(--muted); }
.confirm-actions { display: flex; justify-content: flex-end; gap: 10px; }

/* Remote folder picker */
.folder-box { max-width: 620px; padding: 24px; display: flex; flex-direction: column; gap: 14px; }
.folder-box h3 { overflow-wrap: anywhere; }
.fb-bar { display: flex; align-items: center; gap: 11px; }
.fb-path {
  flex: 1; min-width: 0; font-size: 13px; font-weight: 600; color: var(--muted);
  background: var(--surface-2); border: 1px solid var(--line);
  padding: 9px 14px; border-radius: 999px; overflow-wrap: anywhere;
}
.fb-list {
  display: flex; flex-direction: column; gap: 4px;
  max-height: 46vh; overflow-y: auto; background: var(--bg);
  border: 1px solid var(--line); border-radius: var(--r-md); padding: 8px;
}
.fb-row { display: flex; align-items: center; gap: 8px; }
.fb-open {
  flex: 1; min-width: 0; justify-content: flex-start; gap: 11px;
  background: transparent; border: none; color: var(--text);
  padding: 11px 12px; font-size: 14px; border-radius: var(--r-sm); font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fb-open .ic { color: var(--muted); }
@media (hover: hover) { .fb-open:hover { background: var(--surface-2); } }
.fb-add { flex: none; padding: 8px 13px; font-size: 12px; }
.fb-add.on { background: var(--accent-soft); border-color: transparent; color: var(--accent); }
.fb-file {
  display: flex; align-items: center; gap: 11px; padding: 8px 12px;
  font-size: 13px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fb-file .ic { width: 15px; height: 15px; flex: none; }
.fb-error { color: var(--danger); font-size: 13px; }
.folder-box .dirchips { margin-bottom: 0; max-height: 22vh; overflow-y: auto; }

/* ============================================================================
   Responsive — tablet & phone
   ========================================================================== */

@media (max-width: 1100px) {
  :root { --side-w: 216px; }
  .content { padding: 24px 24px 140px; }
  .topbar { padding: 12px 24px; }
}

/* Below this the sidebar gives way to the bottom bar. */
@media (max-width: 900px) {
  .app { grid-template-columns: 1fr; }
  .side { display: none; }
  .brand-compact { display: flex; }

  .bottomnav {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 95;
    display: flex; align-items: stretch;
    height: calc(var(--navh) + var(--safe-b));
    padding-bottom: var(--safe-b);
    background: var(--glass);
    backdrop-filter: blur(24px) saturate(180%); -webkit-backdrop-filter: blur(24px) saturate(180%);
    border-top: 1px solid var(--line);
  }

  .topbar { padding: calc(var(--safe-t) + 10px) 16px 10px; min-height: 0; }
  /* No sidebar down here, so the footer note earns its place again. */
  .foot { display: block; }
  .content { padding: 20px 16px calc(var(--navh) + var(--safe-b) + 28px); }
  body.player-open .content { padding-bottom: calc(var(--navh) + var(--safe-b) + 170px); }

  .pane-head h2 { font-size: 23px; }
  /* Keep the pane's actions beside the title instead of letting them wrap onto
     a line of their own, which reads as a stray button. */
  .pane-head { align-items: flex-start; margin-bottom: 16px; flex-wrap: nowrap; }
  .pane-head > div:first-child { flex: 1; min-width: 0; }
  .libactions { flex-wrap: wrap; justify-content: flex-end; }

  /* Mini-player: a floating card riding above the bottom bar. */
  .miniplayer {
    flex-wrap: wrap; gap: 10px; padding: 11px 13px;
    left: 8px; right: 8px; bottom: calc(var(--navh) + var(--safe-b) + 8px);
    border: 1px solid var(--line); border-radius: var(--r-lg);
    box-shadow: var(--shadow-3);
  }
  .mp-progress { border-radius: var(--r-lg) var(--r-lg) 0 0; overflow: hidden; }
  .mp-now { width: auto; flex: 1; }
  .mp-side { width: auto; }
  .mp-art { width: 44px; height: 44px; }
  /* Phones have hardware volume keys and no hover, so the slider only crowds
     the bar — keep the mute button, drop the slider. */
  .mp-vol-range { display: none; }
  .mp-center { order: 3; flex-basis: 100%; max-width: none; }

  /* Dialogs become bottom sheets — the shape a phone user expects. */
  .modal { padding: 0; align-items: flex-end; }
  .modal-box {
    max-width: 100%; max-height: 92vh; overflow-y: auto;
    border-radius: var(--r-xl) var(--r-xl) 0 0; border-bottom: none;
    padding-bottom: calc(20px + var(--safe-b));
    animation: slideup .28s var(--ease);
  }
  /* The video player is the exception: a centred, full-bleed viewer. Resizing
     is a pointer gesture, so it's off here — the phone decides the size. */
  #playerModal { align-items: center; }
  #playerModal .modal-box {
    border-radius: 0; width: 100%; height: 100%; max-height: 100%; max-width: 100%;
    padding-bottom: 0; resize: none; justify-content: center;
    animation: pop .2s var(--ease);
  }
  #playerModal .modal-media { flex: 0 1 auto; }
  .resize-grip { display: none; }
  /* No dragging a corner on a touch screen, and the size is the screen's. */
  .dialog-box, .jobs-box, .folder-box, .editor-card { resize: none; max-height: 92vh; }
  .modal-tools { opacity: 1; }
  .confirm-box { padding: 24px 18px calc(18px + var(--safe-b)); }
  .confirm-actions button { flex: 1; padding: 14px 10px; }
  .folder-box { padding: 20px 16px calc(16px + var(--safe-b)); }
  .fb-list { max-height: 38vh; }
}

@media (max-width: 620px) {
  .userbox #whoami { display: none; }
  .lookup { flex-direction: column; }
  .lookup > button { width: 100%; }
  .grid2 { grid-template-columns: 1fr; }
  .preview { flex-direction: column; }
  .preview img { width: 100%; }
  .libcontrols { gap: 8px; }
  /* Search gets its own line; the selects pair up two to a row. */
  .searchwrap { flex-basis: 100%; }
  .libselect { flex: 1 1 calc(50% - 4px); min-width: 0; }
  .viewtoggle { flex: 1 1 calc(50% - 4px); justify-content: center; }
  .playall { width: 100%; }
  .library.view-cards { grid-template-columns: 1fr 1fr; gap: 12px; }
  .libtable .col-type, .libtable .col-date { display: none; }
  .libtable { font-size: 13px; }
  .card { padding: 17px; }
  .serverform { padding: 18px; }
  .songbar > button { width: 100%; }
  /* Format + its buttons on one line, the path full-width beneath. */
  .fmtpath-row { display: grid; grid-template-columns: 100px 1fr auto auto; gap: 8px; }
  .fmt-ext { width: auto; grid-area: 1 / 1; }
  .fmt-arrow { display: none; }
  .fmt-browse { grid-area: 1 / 3; }
  .fmt-del { grid-area: 1 / 4; }
  .fmt-dir { grid-area: 2 / 1 / 3 / -1; }
  .failrow { flex-wrap: wrap; }
  .fail-actions { flex-direction: row; width: 100%; }
  .fail-actions .fail-retry { flex: 1; }
}

@media (max-width: 420px) {
  .library.view-cards { grid-template-columns: 1fr; }
  .segmented { flex-direction: column; }
  .segmented button { width: 100%; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important; animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
}

/* ============================================================================
   Sign-in page
   ========================================================================== */

.login-body {
  display: grid; place-items: center; min-height: 100vh; padding: 24px;
  background:
    radial-gradient(900px 520px at 50% -12%, var(--accent-soft), transparent 70%),
    var(--bg);
}
.logincard {
  width: 100%; max-width: 380px; padding: 34px 30px 30px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-xl); box-shadow: var(--shadow-3);
  text-align: center; animation: pop .3s var(--ease);
}
.login-mark { border-radius: 15px; margin-bottom: 16px; box-shadow: var(--shadow-2); }
.logincard h1 { font-size: 24px; }
.login-sub { color: var(--muted); font-size: 14px; margin-top: 6px; }
.logincard .field { text-align: left; margin-top: 18px; }
.logincard .error { margin: 18px 0 0; text-align: left; }
.logincard button[type="submit"] { width: 100%; margin-top: 24px; padding: 14px; font-size: 15px; }

/* ============================================================================
   Video player (Plyr) + resizable window
   ========================================================================== */

/* Plyr reads its own custom properties, so the player picks up this app's
   accent instead of shipping a second colour system. */
.plyr {
  --plyr-color-main: var(--accent-fill);
  --plyr-video-control-color: #fff;
  --plyr-video-control-color-hover: var(--accent-ink);
  --plyr-video-background: #000;
  --plyr-menu-background: var(--surface);
  --plyr-menu-color: var(--text);
  --plyr-menu-border-color: var(--line);
  --plyr-tooltip-background: var(--surface);
  --plyr-tooltip-color: var(--text);
  --plyr-range-thumb-background: #fff;
  --plyr-font-family: inherit;
  --plyr-font-size-small: 12px;
  --plyr-font-size-base: 14px;
  --plyr-control-radius: 8px;
  height: 100%;
}
.plyr--video { background: #000; }
.plyr__control--overlaid { background: var(--accent-fill); color: var(--accent-ink); }
.plyr__control--overlaid:hover { background: var(--accent-fill-2); }

#playerModal .modal-box {
  /* Resizable from the corner; the chosen size is remembered in app.js. */
  display: flex; flex-direction: column;
  width: min(1240px, 92vw); height: min(760px, 88vh);
  min-width: 380px; min-height: 260px; max-width: 98vw; max-height: 94vh;
  resize: both; overflow: hidden;
}
/* Plyr keeps the video's aspect ratio by shrinking itself, so centre it —
   otherwise a 16:9 clip in a taller window sits off to one side. */
#playerModal .modal-media { flex: 1; min-height: 0; display: flex; justify-content: center; align-items: center; overflow: hidden; }
/* Plyr wraps the <video>; without these it sizes itself from the video's
   intrinsic dimensions and overflows the box we just made resizable. */
#playerModal .modal-media .plyr { min-width: 0; max-width: 100%; width: 100%; height: 100%; }
#playerModal .modal-media .plyr__video-wrapper { height: 100%; display: flex; }
#playerModal .modal-media video { width: 100%; height: 100%; max-height: none; object-fit: contain; }
#playerModal .modal-info { flex: none; }

.modal-tools {
  position: absolute; top: 12px; right: 12px; z-index: 3;
  display: flex; gap: 8px; opacity: .55; transition: opacity .2s var(--ease);
}
#playerModal .modal-box:hover .modal-tools,
#playerModal .modal-box:focus-within .modal-tools { opacity: 1; }
.modal-tools .ghost {
  background: rgba(0, 0, 0, .55); border-color: rgba(255, 255, 255, .18); color: #fff;
}
@media (hover: hover) { .modal-tools .ghost:hover { background: rgba(0, 0, 0, .8); } }
.modal-tools .modal-close { position: static; }

/* A visible hint for the native resize corner (which the UA draws on top). */
.resize-grip {
  position: absolute; right: 2px; bottom: 2px; width: 16px; height: 16px;
  pointer-events: none; opacity: .5;
  background:
    linear-gradient(135deg, transparent 48%, var(--muted) 48%, var(--muted) 56%, transparent 56%),
    linear-gradient(135deg, transparent 68%, var(--muted) 68%, var(--muted) 76%, transparent 76%);
}

/* ============================================================================
   Play queue + shortcuts
   ========================================================================== */

.queue-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 12px 4px; }
.queue-head h3 { margin: 0; font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); }
.queue-actions { display: flex; align-items: center; gap: 8px; }
.queue-actions .ghost { padding: 7px 12px; font-size: 12.5px; }
.queue-actions .iconbtn { width: 32px; height: 32px; }

.queue-list { display: flex; flex-direction: column; gap: 2px; max-height: 56vh; overflow-y: auto; }
.queue-row {
  display: flex; align-items: center; gap: 12px; padding: 8px 10px;
  border-radius: var(--r-sm); border: 1px solid transparent;
}
@media (hover: hover) { .queue-row:hover { background: var(--surface-2); } }
.queue-row.current { background: var(--accent-soft); }
.queue-row.current .queue-title { color: var(--accent); font-weight: 700; }
.queue-idx {
  width: 26px; flex: none; text-align: center; font-size: 12px;
  color: var(--muted); font-variant-numeric: tabular-nums;
}
.queue-row.current .queue-idx .ic { width: 13px; height: 13px; color: var(--accent); }
.queue-jump {
  flex: 1; min-width: 0; justify-content: flex-start; text-align: left;
  background: transparent; border: none; color: var(--text); padding: 4px 0; font-weight: 600;
}
.queue-main { min-width: 0; }
.queue-title { font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.queue-sub { font-size: 12px; font-weight: 500; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.queue-drop {
  flex: none; width: 30px; height: 30px; padding: 0; border-radius: 50%;
  background: transparent; border: none; color: var(--muted);
}
.queue-drop .ic { width: 14px; height: 14px; }
@media (hover: hover) { .queue-drop:hover { background: var(--danger-soft); color: var(--danger); } }

/* The queue is a right-hand panel on desktop, a bottom sheet on phones. */
@media (min-width: 901px) {
  .queue-sheet { align-items: stretch; justify-content: flex-end; }
  .queue-sheet .sheet-box {
    width: 400px; height: 100%; border-radius: 0; border-left: 1px solid var(--line);
    border-top: none; padding: 16px 16px 24px; overflow-y: auto;
    animation: slidein .26s var(--ease);
  }
  .queue-sheet .sheet-grip { display: none; }
  .queue-list { max-height: none; }
}
@keyframes slidein { from { transform: translateX(100%); } }

.keys { display: grid; grid-template-columns: auto 1fr; gap: 10px 18px; margin: 0 4px 8px; align-items: center; }
.keys dt { display: flex; gap: 5px; }
.keys dd { margin: 0; font-size: 13.5px; color: var(--muted); }
kbd {
  display: inline-block; min-width: 26px; text-align: center;
  padding: 4px 7px; border-radius: 7px; font-size: 11.5px; font-weight: 700;
  font-family: inherit; background: var(--surface-2);
  border: 1px solid var(--line); border-bottom-width: 2px; color: var(--text);
}

/* Row actions on the Songs tab: quiet until you're on the row. */
.song-actions { display: flex; align-items: center; gap: 2px; flex: none; }
.song-act {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0; border-radius: 50%;
  background: transparent; border: none; color: var(--muted); text-decoration: none;
}
.song-act .ic { width: 16px; height: 16px; }
@media (hover: hover) {
  .song-actions .song-act { opacity: 0; transition: opacity .16s, background .16s, color .16s; }
  .songrow:hover .song-act, .song-act:focus-visible { opacity: 1; }
  .song-act:hover { background: var(--surface-2); color: var(--text); }
}

/* On a wide screen a full-width bottom sheet is a lot of empty space — the
   shortcuts list reads better as a centred card. (The More sheet never opens
   here: the sidebar shows every tab.) */
@media (min-width: 901px) {
  #keysSheet .sheet-box, #moreSheet .sheet-box {
    max-width: 540px; margin: 0 auto 28px; border-radius: var(--r-xl);
    border: 1px solid var(--line); padding: 14px 22px 22px;
  }
  #keysSheet .sheet-grip, #moreSheet .sheet-grip { display: none; }
}

/* ============================================================================
   Home — stat tiles, then the music
   ========================================================================== */

/* The stat board.
   One surface with a headline figure and a row of cells under it, rather than a
   grid of separate boxes — the figures belong together, so they share a card and
   are divided by hairlines instead of gaps. Numerals are large, tight and
   tabular so they line up and read as data rather than text. */
.statboard {
  margin-bottom: 32px; border-radius: var(--r-lg); overflow: hidden;
  background: var(--surface); border: 1px solid var(--line); box-shadow: var(--shadow-1);
}
.sb-hero { padding: 26px 26px 20px; }
.sb-hero-value {
  font-size: clamp(34px, 6vw, 46px); font-weight: 700; line-height: 1.05;
  letter-spacing: -.03em; font-variant-numeric: tabular-nums;
}
.sb-hero-label { margin-top: 6px; font-size: 13.5px; color: var(--muted); }
/* Why a figure is what it is — shown when the count is zero or short, so a
   wrong path can't quietly masquerade as an empty library. */
.sb-hint {
  margin-top: 10px; padding: 8px 11px; border-radius: var(--r-sm);
  font-size: 12.5px; color: var(--warn);
  background: var(--warn-soft); border: 1px solid color-mix(in srgb, var(--warn) 35%, transparent);
}
.sb-sub { font-size: 11.5px; color: var(--muted); font-variant-numeric: tabular-nums; }

/* Capacity of the volume, not of the media counted above — the two differ by
   however much else lives on the disk. */
.sb-disk { margin-top: 14px; max-width: 460px; }
.sb-disk-bar {
  height: 6px; border-radius: 999px; overflow: hidden;
  background: var(--surface-3);
}
.sb-disk-bar span {
  display: block; height: 100%; border-radius: 999px;
  background: var(--accent-fill); transition: width .4s var(--ease);
}
.sb-disk.tight .sb-disk-bar span { background: var(--danger); }
.sb-disk-text { margin-top: 6px; font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
.sb-disk.tight .sb-disk-text { color: var(--danger); }

/* A folder sitting among the files it contains. A show with a poster inside it,
   or a season whose first episode can give up a frame, shows that; anything
   else falls back to the glyph underneath. The picture is layered OVER the
   glyph rather than replacing it, so a cover that never arrives degrades to a
   folder icon instead of a grey hole. */
.folder-thumb {
  position: relative; display: grid; place-items: center;
  background: var(--surface-2); border-bottom: 1px solid var(--line);
}
.folder-glyph { color: var(--muted); }
.folder-glyph svg { width: 40px; height: 40px; }
.folder-card { cursor: pointer; }
.folder-card:hover .folder-thumb { background: var(--surface-3); }
.folder-row { cursor: pointer; }
.folder-art {
  position: relative; overflow: hidden;
  display: grid; place-items: center;
  background: var(--surface-2); color: var(--muted); border-radius: var(--r-sm);
}
.folder-thumb img[data-art], .folder-art img[data-art] {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block;
  opacity: 0; transition: opacity .18s var(--ease);
}
.folder-thumb img[data-art].ready, .folder-art img[data-art].ready { opacity: 1; }

/* Full-screen photo viewer. Deliberately dark in both themes: a light frame
   around a photograph changes how the photograph reads. */
.lightbox {
  position: fixed; inset: 0; z-index: 300;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: clamp(12px, 4vw, 48px);
  background: rgba(8, 8, 10, .94);
  backdrop-filter: blur(6px);
}
.lightbox[hidden] { display: none; }
body.lb-open { overflow: hidden; }
.lb-stage {
  margin: 0; display: flex; flex-direction: column; align-items: center; gap: 12px;
  max-width: 100%; max-height: 100%; min-width: 0;
}
.lb-img {
  max-width: 100%; max-height: calc(100vh - 140px);
  object-fit: contain; border-radius: 10px;
  background: rgba(255, 255, 255, .04);
}
.lb-cap {
  color: rgba(255, 255, 255, .78); font-size: 13px; text-align: center;
  font-variant-numeric: tabular-nums; max-width: 70ch;
  overflow-wrap: anywhere;
}
.lb-close, .lb-nav {
  flex: none; display: grid; place-items: center;
  width: 44px; height: 44px; border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, .16);
  background: rgba(255, 255, 255, .08); color: #fff; cursor: pointer;
  transition: background .15s var(--ease), transform .15s var(--ease);
}
.lb-close:hover, .lb-nav:hover { background: rgba(255, 255, 255, .18); }
.lb-nav:active { transform: scale(.94); }
.lb-close { position: absolute; top: 16px; right: 16px; }
.lb-nav[hidden] { display: none; }
@media (max-width: 640px) {
  .lb-nav { width: 38px; height: 38px; }
  .lb-img { max-height: calc(100vh - 180px); }
}

/* How far a measurement has got. Striped and moving, so a bar that sits at the
   same percentage for a while still reads as working rather than stuck — one
   large folder can take far longer than the twenty before it. */
.measure-bar {
  height: 6px; border-radius: 999px; overflow: hidden;
  background: var(--surface-3); margin-top: 14px; max-width: 460px;
}
.measure-bar span {
  display: block; height: 100%; border-radius: 999px;
  background: var(--accent-fill); transition: width .4s var(--ease);
  background-image: linear-gradient(90deg,
    rgba(255, 255, 255, .28) 25%, transparent 25%, transparent 50%,
    rgba(255, 255, 255, .28) 50%, rgba(255, 255, 255, .28) 75%, transparent 75%);
  background-size: 16px 16px;
  animation: measure-stripes 1s linear infinite;
}
@keyframes measure-stripes { to { background-position: 16px 0; } }
@media (prefers-reduced-motion: reduce) { .measure-bar span { animation: none; } }

/* Mounted folders, listed under Server songs in the sidebar. */
.subnav { display: flex; flex-direction: column; gap: 1px; margin: 2px 0 6px 14px; }
.subnav-item {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 10px 7px 12px; border: 0; border-radius: var(--r-sm);
  border-left: 2px solid var(--line);
  background: transparent; color: var(--muted); text-align: left;
  font-size: 13px; font-weight: 500;
}
.subnav-item .ic { width: 14px; height: 14px; flex: none; }
.subnav-item span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (hover: hover) {
  /* One step further than the tabs above, because the subnav sits inside them
     and --surface-2 would make a sub-item look like a hovered parent. */
  .subnav-item:hover { color: var(--text); background: var(--surface-3); }
}
.subnav-item.active { color: var(--accent); border-left-color: var(--accent); }

/* No sidebar on a phone — these live inside the Server songs tab there. */
@media (max-width: 900px) { .subnav { display: none; } }
.sb-cells {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  border-top: 1px solid var(--line);
}
/* min-width on both the cell and the label is doing real work here, not
   belt-and-braces. A grid item and a flex item both default to min-width:auto,
   which means neither may shrink below its own content — so a label like
   MUSIC-VIDEOS or PUNJABI-MOVIES couldn't wrap, and instead pushed the whole
   row wider than the card it sits in. Allowing them to shrink is what lets the
   text wrap inside its cell where it belongs. */
.sb-cell {
  display: flex; flex-direction: column; gap: 5px; align-items: flex-start;
  padding: 16px 20px; text-align: left; background: transparent;
  border: 0; border-left: 1px solid var(--line); border-radius: 0;
  color: var(--text); font-weight: 400; transition: background .16s;
  min-width: 0;
}
.sb-cell > * { max-width: 100%; }
/* No stray edge on the first cell of each row — the card's own border is there. */
.sb-cell:first-child { border-left: 0; }
button.sb-cell { cursor: pointer; }
@media (hover: hover) { button.sb-cell:hover { background: var(--surface-2); } }
.sb-value { font-size: 21px; font-weight: 650; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.sb-label {
  display: flex; align-items: flex-start; gap: 6px;
  font-size: 11.5px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
  color: var(--muted);
  /* white-space is the one that actually mattered: the label inherited nowrap,
     so no amount of shrinking would ever have broken the line. */
  min-width: 0; overflow-wrap: anywhere; white-space: normal;
}
/* The icon keeps its size while the words wrap beside it, and sits on the
   first line rather than floating in the middle of a two-line label. */
.sb-label .ic { width: 13px; height: 13px; flex: none; margin-top: 1px; }
.sb-value, .sb-sub { overflow-wrap: anywhere; }
.sb-warn .sb-value, .sb-warn .sb-label { color: var(--danger); }

@media (max-width: 620px) {
  .sb-hero { padding: 20px 18px 16px; }
  .sb-cell { padding: 13px 16px; }
}

.stats {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(158px, 1fr));
  gap: 12px; margin-bottom: 32px;
}
/* Most played: song, video, playlist. Sits under the stats and follows the same
   tile language, with room for artwork — these name one specific thing, so the
   cover is what you recognise before you read the title. */
.topcards {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 12px; margin-bottom: 32px;
}
.topcard {
  display: flex; align-items: center; gap: 14px; text-align: left;
  padding: 12px 14px; border-radius: var(--r-lg);
  background: var(--surface); border: 1px solid var(--line);
  color: var(--text); font-weight: 400; box-shadow: var(--shadow-1); cursor: pointer;
  transition: transform .18s var(--ease), border-color .18s, box-shadow .18s;
}
@media (hover: hover) {
  .topcard:hover {
    transform: translateY(-2px); border-color: var(--line-2);
    background: var(--surface-2); box-shadow: var(--shadow-2);
  }
}
.topcard-art { position: relative; width: 58px; height: 58px; flex: none; border-radius: 10px; overflow: hidden; }
.topcard-body { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.topcard-label {
  display: flex; align-items: center; gap: 6px;
  font-size: 11.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--muted);
}
.topcard-label .ic { width: 13px; height: 13px; }
.topcard-title {
  font-size: 14.5px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.topcard-count { font-size: 12.5px; color: var(--accent); font-weight: 600; }

.stat {
  display: flex; align-items: center; gap: 13px; text-align: left;
  padding: 16px 18px; border-radius: var(--r-lg);
  background: var(--surface); border: 1px solid var(--line);
  color: var(--text); font-weight: 400; box-shadow: var(--shadow-1);
  transition: transform .18s var(--ease), border-color .18s, box-shadow .18s;
}
button.stat { cursor: pointer; }
@media (hover: hover) {
  button.stat:hover { transform: translateY(-2px); border-color: var(--line-2); box-shadow: var(--shadow-2); background: var(--surface); }
}
.stat-icon {
  width: 40px; height: 40px; flex: none; border-radius: 12px;
  display: grid; place-items: center;
  background: var(--accent-soft); color: var(--accent);
}
.stat-icon .ic { width: 20px; height: 20px; }
.stat-warn .stat-icon { background: var(--danger-soft); color: var(--danger); }
.stat-body { display: flex; flex-direction: column; min-width: 0; }
.stat-value { font-size: 22px; font-weight: 700; letter-spacing: -.02em; line-height: 1.15; }
.stat-label { font-size: 12px; color: var(--muted); }

.home-section { margin-bottom: 34px; }
.home-section-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 14px; margin-bottom: 14px;
}
.home-section-head h3 { font-size: 18px; }
.home-section .library.view-cards { grid-template-columns: repeat(auto-fill, minmax(196px, 1fr)); }



/* ============================================================================
   Discover
   ========================================================================== */

.discover-bar {
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  margin: 4px 0 20px;
}
.discover-format { margin: 0; width: auto; flex: none; }
.discover-format button { padding: 9px 18px; }
#discoverWhy { font-size: 12.5px; font-style: italic; }
#discoverSearchForm { margin-bottom: 16px; }

.disccard .thumb-wrap { cursor: default; }
.disccard .dur {
  position: absolute; right: 8px; bottom: 8px;
  background: rgba(0, 0, 0, .82); color: #fff;
  font-size: 11.5px; font-weight: 700; padding: 2px 7px; border-radius: 6px;
  font-variant-numeric: tabular-nums;
}
.disccard .owned {
  position: absolute; left: 8px; top: 8px;
  display: inline-flex; align-items: center; gap: 5px;
  background: var(--accent-fill); color: var(--accent-ink);
  font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  padding: 4px 9px; border-radius: 999px;
}
.disccard .owned .ic { width: 12px; height: 12px; }
.disccard .actions { flex-wrap: nowrap; }
.disccard .grab {
  flex: 1; background: var(--accent-fill); color: var(--accent-ink);
  border-color: transparent; font-size: 12.5px; padding: 8px 12px;
}
.disccard .grab .ic { width: 15px; height: 15px; }
.disccard .iconbtn { flex: none; width: 34px; height: 34px; }
.grab-result { font-size: 12px; line-height: 1.5; }
.grab-result .ok { color: var(--accent); display: inline-flex; align-items: center; gap: 5px; }
.grab-result .ok .ic { width: 13px; height: 13px; }
.grab-result .miss { color: var(--danger); }
.linkbtn {
  background: none; border: none; padding: 0; font: inherit;
  color: var(--accent); text-decoration: underline; font-weight: 600;
}

/* ============================================================================
   Artwork, skeletons, empty states
   ========================================================================== */

/* The gradient and the track's initial sit underneath; a remote thumbnail is
   laid over the top. If the image 404s, app.js removes it and what's beneath
   is already the right shape and colour — no broken-image box, no flash. */
.art {
  position: absolute; inset: 0;
  display: grid; place-items: center; overflow: hidden;
  background: var(--surface-3) center/cover no-repeat;
}
.art-initial {
  font-size: 1.1em; font-weight: 800; letter-spacing: -.02em;
  color: rgba(255, 255, 255, .92); text-shadow: 0 1px 3px rgba(0, 0, 0, .35);
  user-select: none;
}
.art img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block;
  /* Revealed by .ready once decoded — see wireArt. The gradient and initial
     sit underneath, so a slow or missing thumbnail degrades to those rather
     than to a flash of broken image. */
  opacity: 0; transition: opacity .18s var(--ease);
}
.art img.ready { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .art img { transition: none; } }
/* Cover-sized artwork (cards) carries a bigger initial than a 46px row does. */
.art-cover .art-initial { font-size: 34px; }
.song-art .art-initial { font-size: 17px; }
.dl-thumb .art-initial, .fail-thumb .art-initial { font-size: 15px; }

/* Four-up playlist cover. */
.art-mosaic { display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; gap: 1px; }
.art-mosaic img { position: static; width: 100%; height: 100%; }

/* The tiles that hold artwork just need a stacking context and a shape. */
.thumb-wrap, .dl-thumb, .fail-thumb, .mp-art { position: relative; overflow: hidden; }
.dl-thumb, .fail-thumb { padding: 0; }

/* Skeletons — the shape of the answer while it loads. */
.skeletons { display: flex; flex-direction: column; gap: 10px; }
.sk-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 18px; }
.sk-row { display: flex; align-items: center; gap: 14px; padding: 9px 14px; }
.sk-lines { flex: 1; display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.sk-card {
  display: flex; flex-direction: column; gap: 9px; padding-bottom: 12px;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg);
  overflow: hidden;
}
.sk-card .sk-line { margin: 0 14px; }
.sk {
  display: block; border-radius: 8px;
  background: linear-gradient(100deg, var(--surface-2) 30%, var(--surface-3) 50%, var(--surface-2) 70%);
  background-size: 300% 100%;
  animation: shimmer 1.4s ease-in-out infinite;
}
.sk-art { width: 46px; height: 46px; flex: none; border-radius: 10px; }
.sk-thumb { width: 100%; aspect-ratio: 16/9; border-radius: 0; }
.sk-line { height: 11px; width: 100%; }
.sk-line.short { width: 45%; }
@keyframes shimmer { to { background-position: -200% 0; } }
@media (prefers-reduced-motion: reduce) { .sk { animation: none; } }

/* Empty states: a mark, a headline, and what to do next. */
.empty {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 44px 24px;
}
.empty-mark {
  display: grid; place-items: center; width: 56px; height: 56px; margin-bottom: 6px;
  border-radius: 50%; background: var(--surface-2); color: var(--muted);
}
.empty-mark .ic { width: 26px; height: 26px; }
.empty-title { font-size: 15.5px; font-weight: 650; color: var(--text); }
.empty-hint { font-size: 13.5px; color: var(--muted); max-width: 46ch; line-height: 1.55; }

/* ============================================================================
   Sticky pane headers
   ========================================================================== */

/* Everything above a long list holds still while the list scrolls under it —
   the search box you're filtering with shouldn't disappear the moment you
   look at the results. It sticks below the top bar, whose height is published
   as --topbar-h by app.js (it changes with the safe-area inset on a phone).
   Sticky rather than a nested scroll container on purpose: an inner scroller
   costs you momentum scrolling, pull-to-refresh and the address bar hiding. */
.pane-sticky {
  position: sticky; top: var(--topbar-h, 62px); z-index: 40;
  margin: -26px -32px 16px; padding: 26px 32px 14px;
  background: var(--glass);
  backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%);
}
/* The hairline only earns its place once something has scrolled under it. */
.pane-sticky::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 1px;
  background: var(--line); opacity: 0; transition: opacity .2s var(--ease);
}
.pane-sticky.stuck::after { opacity: 1; }
.pane-sticky > *:last-child { margin-bottom: 0; }
.pane-sticky .pane-head { margin-bottom: 16px; }
.pane-sticky .subtabs, .pane-sticky .libcontrols, .pane-sticky .songbar,
.pane-sticky .dirchips, .pane-sticky .discover-bar { margin-bottom: 12px; }
.pane-sticky .countline { margin-bottom: 0; }

/* Inside a page section (Home), it sticks within that section only, so the
   next section still scrolls it away. */
.pane-sticky-inline { margin-top: 0; padding-top: 12px; }

@media (max-width: 900px) {
  .pane-sticky { margin: -20px -16px 14px; padding: 20px 16px 12px; }
  .pane-sticky-inline { margin-top: 0; padding-top: 10px; }
  /* A pinned header must not eat the list on a phone: the blurb goes once
     it's competing for space. */
  .pane-sticky .pane-head .muted { display: none; }
  .pane-sticky .pane-head { margin-bottom: 12px; }
}

.song-act-danger:hover { background: var(--danger-soft) !important; color: var(--danger) !important; }

/* ============================================================================
   Playlist ordering
   ========================================================================== */

.droghint { margin-bottom: 12px; font-size: 12.5px; }
.pl-tracks { max-width: 1100px; }

.plrow { background: var(--surface); border-color: var(--line); }
.plrow.dragging {
  position: relative; z-index: 5;
  background: var(--surface-2); border-color: var(--accent);
  box-shadow: var(--shadow-2); cursor: grabbing;
}
/* Only the dragged row should animate; the ones shuffling around it snap. */
body.is-dragging { cursor: grabbing; user-select: none; }
body.is-dragging .plrow:not(.dragging) { transition: transform .16s var(--ease); }

.pl-grip {
  flex: none; width: 30px; height: 34px; padding: 0; border-radius: 8px;
  background: transparent; border: none; color: var(--muted);
  cursor: grab;
  /* The handle owns the gesture, so the page doesn't scroll out from under a
     drag on touch. The rest of the row still scrolls normally. */
  touch-action: none;
}
.pl-grip .ic { width: 17px; height: 17px; }
.pl-grip:active { cursor: grabbing; }
@media (hover: hover) { .pl-grip:hover { background: var(--surface-2); color: var(--text); } }

.pl-idx {
  flex: none; width: 22px; text-align: right; font-size: 12.5px;
  color: var(--muted); font-variant-numeric: tabular-nums;
}

@media (max-width: 620px) {
  /* The index is the first thing to go when space is tight — the handle and
     the artwork already say which row you're holding. */
  .pl-idx { display: none; }
  .pl-grip { width: 34px; height: 40px; }
}

/* A pinned header has to earn its height. At rest it shows everything; once it
   sticks it collapses to what you actually need while scrolling a list — the
   way a large iOS title shrinks into the bar. Without this, the header on a
   phone was taking half the screen and the list had nowhere to go. */
.pane-sticky { transition: padding .18s var(--ease); }
.pane-sticky .pane-head, .pane-sticky .libcontrols,
.pane-sticky .songbar, .pane-sticky .dirchips { transition: opacity .16s var(--ease); }

/* Pinned on a wide screen: the blurb and the one-time setup controls go, the
   title and the things you actually use while scrolling stay. */
.pane-sticky.stuck .pane-head .muted,
.pane-sticky.stuck .songbar,
.pane-sticky.stuck .dirchips,
.pane-sticky.stuck .countline { display: none; }
.pane-sticky.stuck .pane-head h2 { font-size: 21px; }
.pane-sticky.stuck .pane-head { margin-bottom: 10px; }

@media (max-width: 900px) {
  .pane-sticky.stuck { padding-top: 10px; padding-bottom: 8px; }
  /* The top bar already names the section, so the title and its blurb go. */
  .pane-sticky.stuck .pane-head,
  .pane-sticky.stuck .playall,
  .pane-sticky.stuck .songbar,
  .pane-sticky.stuck .dirchips,
  .pane-sticky.stuck .countline,
  .pane-sticky.stuck .discover-bar { display: none; }
  /* Search and the selects stay reachable, on one horizontally-scrolling row. */
  .pane-sticky.stuck .libcontrols {
    flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
    margin-bottom: 0; padding-bottom: 2px;
  }
  .pane-sticky.stuck .libcontrols::-webkit-scrollbar { display: none; }
  .pane-sticky.stuck .searchwrap { flex: 1 1 55%; min-width: 150px; }
  .pane-sticky.stuck .libselect { flex: 0 0 auto; }
  .pane-sticky.stuck .subtabs { margin-bottom: 8px; }
}

/* Discover preview: YouTube's own embed, filling the player window. */
.yt-embed { width: 100%; height: 100%; border: 0; display: block; background: #000; }
#playerModal .modal-media:has(.yt-embed) { display: block; }

.modal-info { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.modal-info-text { flex: 1; min-width: 0; }
.modal-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.modal-actions .grab { background: var(--accent-fill); color: var(--accent-ink); border-color: transparent; }
.modal-actions a.ghost { text-decoration: none; }
.modal-actions .grab-result { flex-basis: 100%; }

/* The preview button on a Discover card sits where it does on a library card. */
.disccard .thumb-wrap { cursor: pointer; }

/* Marker sitting at a sticky header's natural place in the flow; app.js watches
   it to know when the header has actually pinned. It is 1px rather than 0 so
   the header's negative top margin cannot collapse through it and drag the
   marker up with it — which made the header read as pinned before any
   scrolling had happened. */
.sticky-sentinel { height: 1px; margin: 0; padding: 0; visibility: hidden; }

/* Format picker — the resolutions and bitrates a Discover result actually has. */
.fmt-box { max-width: 460px; }
.fmt-box > .muted { margin-bottom: 4px; overflow-wrap: anywhere; }
.fmt-box .segmented { margin: 14px 0 12px; }
.fmt-box .formatlist { max-height: 46vh; margin-bottom: 16px; }
.fmt-box .fmt { cursor: pointer; }

/* ============================================================================
   Desktop: a fixed application frame
   ----------------------------------------------------------------------------
   The sidebar, the top bar, the pane header (title, stats, search, sub-tabs)
   and the player all stay put; only the list in the middle scrolls.

   Deliberately desktop-only. The note on .pane-sticky above explains why a
   nested scroll container is the wrong answer on a phone — it costs momentum
   scrolling, pull-to-refresh and the address bar hiding — so below 900px the
   page keeps scrolling as a whole and the header pins with position: sticky
   instead. Same visual result, the right mechanism for each input model.
   ========================================================================== */

@media (min-width: 901px) {
  html, body { height: 100%; overflow: hidden; }
  .app { height: 100dvh; overflow: hidden; }
  .main { height: 100dvh; min-height: 0; overflow: hidden; }

  /* The frame holds no padding of its own — the scrolling pane inside does, so
     that the scrollbar sits at the edge of the window rather than inset. */
  .content, body.player-open .content {
    flex: 1; min-height: 0; display: flex; flex-direction: column;
    overflow: hidden; padding: 0;
  }
  .tabpane { flex: 1; min-height: 0; overflow-y: auto; padding: 26px 32px 40px; }
  body.player-open .tabpane { padding-bottom: 120px; }

  /* The pane now scrolls itself, so its header pins to the pane's own top
     rather than to the window under the top bar.

     -26px, not 0, because sticky constrains the MARGIN box and this header
     carries margin-top: -26px to bleed its background over the pane padding.
     At top: 0 the border box would settle 26px lower and rows would scroll
     visibly through the gap above it. */
  .pane-sticky { top: -26px; }
}

@media (min-width: 901px) and (max-width: 1100px) {
  .tabpane { padding: 24px 24px 40px; }
  body.player-open .tabpane { padding-bottom: 120px; }
}

/* "Try the rest again" on a transfer that partly failed. Full width so it reads
   as an action on the row above it rather than a stray control. */
.xfer-again { width: 100%; margin-top: 7px; padding: 7px; font-size: 12px; }
/* Two docks can be open at once; the transfers one sits above the uploads one. */
.xferdock { bottom: auto; top: 84px; }
@media (max-width: 620px) { .xferdock { top: auto; bottom: calc(var(--navh) + var(--safe-b) + 10px); } }

/* A picture or video in the file manager shows itself rather than an icon. */
.file-icon.file-thumb { padding: 0; overflow: hidden; background: var(--surface-3); }
.file-icon.file-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* ---- storage: where the space went ---------------------------------------- */
.storage-hero { margin-bottom: 14px; }
.usagelist { display: flex; flex-direction: column; gap: 2px; }
.usagerow {
  display: flex; align-items: center; gap: 12px; padding: 10px 12px;
  border: 1px solid transparent; border-radius: var(--r-md);
  transition: background .16s, border-color .16s, opacity .2s;
}
@media (hover: hover) { .usagerow:hover { background: var(--surface); border-color: var(--line); } }
/* Dimmed once its file is gone — the scan behind it is now out of date. */
.usagerow.stale { opacity: .45; }
.usage-icon {
  flex: none; display: flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 9px;
  background: var(--surface-2); color: var(--muted);
}
.usagerow.isdir .usage-icon { color: var(--accent); background: var(--accent-soft); cursor: pointer; }
.usage-icon .ic { width: 17px; height: 17px; }
.usage-main { flex: 1; min-width: 0; }
.usagerow.isdir .usage-main { cursor: pointer; }
.usage-top { display: flex; align-items: baseline; gap: 12px; }
.usage-name {
  flex: 1; min-width: 0; font-size: 14px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.usage-size { flex: none; font-size: 13.5px; font-weight: 700; font-variant-numeric: tabular-nums; }
.usage-bar {
  height: 6px; margin: 6px 0 4px; border-radius: 999px;
  background: var(--surface-3); overflow: hidden;
}
.usage-bar span { display: block; height: 100%; border-radius: 999px; background: var(--accent-fill); }
.usage-meta { font-size: 12px; }
.usage-actions { display: flex; gap: 4px; flex: none; }
@media (hover: hover) {
  .usage-actions .song-act { opacity: 0; }
  .usagerow:hover .usage-actions .song-act { opacity: 1; }
}

/* ---- activity log ---------------------------------------------------------- */
.logs-facets { margin-bottom: 14px; }
.loglist { display: flex; flex-direction: column; gap: 2px; }
.logrow {
  display: flex; align-items: flex-start; gap: 12px; padding: 9px 12px;
  border: 1px solid transparent; border-left-width: 3px; border-radius: var(--r-md);
}
@media (hover: hover) { .logrow:hover { background: var(--surface); border-color: var(--line); } }
/* Severity reads off the left edge, so a wall of entries is still scannable. */
.log-warn { border-left-color: var(--warn); }
.log-error { border-left-color: var(--danger); }
.log-icon {
  flex: none; display: flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border-radius: 8px; margin-top: 1px;
  background: var(--surface-2); color: var(--muted);
}
.log-icon .ic { width: 14px; height: 14px; }
.log-main { flex: 1; min-width: 0; }
.log-top { display: flex; align-items: baseline; gap: 12px; }
.log-msg { flex: 1; min-width: 0; font-size: 13.5px; font-weight: 500; }
.log-when { flex: none; font-size: 11.5px; font-variant-numeric: tabular-nums; }
.log-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 3px; font-size: 11.5px; }
.log-meta .badge { margin: 0; }
.log-error .log-level { color: var(--danger); background: var(--danger-soft); }
.log-warn .log-level { color: var(--warn); background: var(--warn-soft); }
.log-target {
  margin-top: 3px; font-size: 11.5px; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---- text editor ----------------------------------------------------------- */
.editor-card { width: min(860px, 94vw); padding: 24px; display: flex; flex-direction: column; gap: 12px; }
#editText {
  width: 100%; height: min(56vh, 460px); margin-top: 10px; padding: 12px 14px;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--bg); color: var(--text);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 13px; line-height: 1.55; resize: vertical;
  /* Code is read in columns; wrapping it would misrepresent the file. */
  white-space: pre; overflow: auto;
}
#editText:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }

/* Search sits above the listing and replaces it while active. */
.files-search { margin-bottom: 12px; }
/* Three docks can coexist; they stack up the right-hand edge. */
.zipdock { bottom: auto; top: 200px; }
@media (max-width: 620px) { .zipdock { top: auto; bottom: calc(var(--navh) + var(--safe-b) + 10px); } }
