diff --git a/src/main.tsx b/src/main.tsx
index d3c454de..2da1189e 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -4,7 +4,7 @@ import App from './App';
import { runPreReactBootstrap } from './app/bootstrap';
import './i18n';
import './styles/themes/index.css';
-import './styles/layout.css';
+import './styles/layout/index.css';
import './styles/components/index.css';
import './styles/tracks.css';
diff --git a/src/styles/layout.css b/src/styles/layout.css
deleted file mode 100644
index 9754a1dc..00000000
--- a/src/styles/layout.css
+++ /dev/null
@@ -1,3209 +0,0 @@
-/* ─── Psysonic App Shell ─── */
-
-*, *::before, *::after {
- user-select: none;
-}
-
-/* Allow text selection only where the user needs to copy content */
-.artist-bio, [data-selectable] {
- user-select: text;
-}
-
-.app-shell {
- position: relative;
- display: grid;
- grid-template-columns: var(--sidebar-width) 1fr var(--queue-width);
- /* minmax(0, 1fr) instead of plain 1fr: CSS Grid spec treats 1fr as
- minmax(auto, 1fr), meaning the row won't shrink below its min-content
- size. With minmax(0, 1fr) the row can collapse to zero, keeping the
- player bar row always visible regardless of content size or zoom level. */
- grid-template-rows: minmax(0, 1fr) var(--player-height);
- grid-template-areas:
- "sidebar main queue"
- "player player player";
- /* height: 100% inherits from html/body/#root (all 100%) so the grid
- tracks actual window resize events on every platform.
- 100vh was used previously but WebView2 (Windows) does not update the
- CSS viewport synchronously on resize, causing the player-bar grid row
- to drift off-screen. overflow: hidden still prevents the 1fr row from
- pushing the grid taller than the window on Linux WMs that ignore
- the OS minHeight constraint. */
- height: 100%;
- overflow: hidden;
- background: var(--bg-app);
-}
-
-/* When player bar is floating, content extends to bottom */
-.app-shell.floating-player {
- grid-template-rows: minmax(0, 1fr);
- grid-template-areas:
- "sidebar main queue";
-}
-
-/* ─── Custom title bar (Linux only — decorations: false) ─── */
-:root {
- --titlebar-height: 32px;
-}
-
-.app-shell[data-titlebar] {
- grid-template-rows: var(--titlebar-height) minmax(0, 1fr) var(--player-height);
- grid-template-areas:
- "titlebar titlebar titlebar"
- "sidebar main queue"
- "player player player";
-}
-
-/* When player bar is floating with titlebar, content extends to bottom */
-.app-shell[data-titlebar].floating-player {
- grid-template-rows: var(--titlebar-height) minmax(0, 1fr);
- grid-template-areas:
- "titlebar titlebar titlebar"
- "sidebar main queue";
-}
-
-/* Resize grips — replace the native GTK grips lost when decorations: false */
-.app-shell[data-titlebar]::before,
-.app-shell[data-titlebar]::after {
- content: '';
- position: fixed;
- bottom: 0;
- width: 14px;
- height: 14px;
- pointer-events: none;
- background-image: radial-gradient(circle, var(--text-muted) 1.5px, transparent 1.5px);
- background-size: 4px 4px;
- opacity: 0.35;
- z-index: 9999;
-}
-.app-shell[data-titlebar]::before { left: 0; }
-.app-shell[data-titlebar]::after { right: 0; }
-
-.titlebar {
- grid-area: titlebar;
- display: flex;
- align-items: center;
- justify-content: flex-end;
- background: var(--bg-sidebar);
- border-bottom: 1px solid var(--border-subtle);
- padding: 0 6px 0 12px;
- height: var(--titlebar-height);
- user-select: none;
-}
-
-.titlebar-track {
- position: absolute;
- left: 50%;
- transform: translateX(-50%);
- display: flex;
- align-items: center;
- gap: 6px;
- max-width: 40%;
- pointer-events: none;
- padding: 3px 10px;
- background: rgba(0, 0, 0, 0.45);
- border-radius: 999px;
- box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
-}
-
-.titlebar-track:empty {
- display: none;
-}
-
-.titlebar-track-state {
- font-size: 9px;
- color: var(--accent);
- flex-shrink: 0;
- text-shadow: 0 1px 1px rgba(0, 0, 0, 0.6);
-}
-
-.titlebar-track-text {
- font-size: 11px;
- color: #ececec;
- max-width: 100%;
- text-shadow: 0 1px 1px rgba(0, 0, 0, 0.5);
-}
-
-.titlebar-controls {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 0 8px;
- flex: 0 0 auto;
-}
-
-.titlebar-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 14px;
- height: 14px;
- padding: 0;
- border: none;
- border-radius: 50%;
- cursor: pointer;
- box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18);
- transition: filter var(--transition-fast), box-shadow var(--transition-fast);
-}
-
-.titlebar-btn-close {
- background: #ff5f57;
-}
-
-.titlebar-btn-minimize {
- background: #febc2e;
-}
-
-.titlebar-btn-maximize {
- background: #28c840;
-}
-
-.titlebar-btn:hover {
- filter: brightness(1.1);
-}
-
-.titlebar-btn-close:hover { box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18), 0 0 6px rgba(255, 95, 87, 0.75); }
-.titlebar-btn-minimize:hover { box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18), 0 0 6px rgba(254, 188, 46, 0.75); }
-.titlebar-btn-maximize:hover { box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18), 0 0 6px rgba(40, 200, 64, 0.75); }
-
-.titlebar-btn:active {
- filter: brightness(0.92);
-}
-
-.titlebar-btn:focus-visible {
- outline: none;
- box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18), 0 0 0 2px rgba(255, 255, 255, 0.5);
-}
-
-/* Resizer handles must start below the titlebar */
-.app-shell[data-titlebar] .resizer {
- top: var(--titlebar-height);
-}
-
-/* ─── Resizer Handles ─── */
-.resizer {
- position: absolute;
- top: 0;
- bottom: var(--player-height);
- width: 6px;
- cursor: col-resize;
- z-index: 50;
- background: transparent;
- transition: background var(--transition-fast);
-}
-
-.resizer:hover, .resizer:active {
- background: var(--accent);
-}
-
-.resizer-queue { right: calc(var(--queue-width) - 3px); }
-.resizer-queue { overflow: visible; }
-/* Queue handle has its own visual affordance; don't paint seam stripe on hover. */
-.resizer-queue:hover, .resizer-queue:active { background: transparent; }
-
-.resizer-queue-handle {
- z-index: 101;
- width: 22px;
- height: 22px;
- border-radius: 50%;
- border: 1px solid var(--border-subtle);
- background: var(--bg-card);
- color: var(--text-muted);
- cursor: col-resize;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
- box-shadow: var(--shadow-sm);
-}
-
-.resizer-queue-handle:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
- box-shadow: var(--shadow-md);
-}
-
-/* While dragging an overlay scrollbar thumb, hide queue resizer so the
- pointer does not pick up col-resize / queue resize (thumb sits under the
- resizer hit box near the main/queue edge). */
-body.is-overlay-scrollbar-thumb-drag .resizer {
- pointer-events: none !important;
-}
-
-/* In native fullscreen the resizer sits flush against the screen edge and
- would interfere with OS edge-swipe gestures — hide it entirely. */
-.app-shell[data-fullscreen] .resizer { display: none; }
-
-/* ─── Sidebar ─── */
-.sidebar {
- grid-area: sidebar;
- display: flex;
- flex-direction: column;
- background: var(--bg-sidebar);
- border-right: 1px solid var(--border-subtle);
- position: relative;
- z-index: 2;
- min-height: 0; /* allow 1fr row to shrink freely */
-}
-
-.sidebar-brand {
- display: flex;
- align-items: center;
- gap: var(--space-3);
- padding: 0 var(--space-5);
- height: 64px;
- box-sizing: border-box;
- border-bottom: 1px solid var(--border-subtle);
-}
-
-.sidebar-brand .logo-icon {
- width: 32px;
- height: 32px;
- flex-shrink: 0;
-}
-
-.sidebar-brand .brand-name {
- font-family: var(--font-display);
- font-size: 20px;
- font-weight: 700;
- letter-spacing: -0.02em;
- background: linear-gradient(135deg, var(--accent), var(--ctp-blue));
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
-}
-
-.sidebar-nav {
- flex: 1;
- min-height: 0;
- display: flex;
- flex-direction: column;
- overflow: hidden;
-}
-
-.sidebar-nav-scroll.overlay-scroll {
- flex: 1;
- min-height: 0;
-}
-
-.sidebar-nav-scroll .overlay-scroll__viewport.sidebar-nav-viewport {
- padding: var(--space-1) var(--space-3) var(--space-4);
- display: flex;
- flex-direction: column;
- gap: var(--space-1);
-}
-
-/* Library scope: Navidrome-style dropdown trigger + fixed panel (portal) */
-.nav-library-scope-trigger {
- display: flex;
- align-items: flex-start;
- gap: var(--space-2);
- width: calc(100% - 2 * var(--space-3));
- margin: var(--space-3) var(--space-3) var(--space-2);
- padding: var(--space-2) var(--space-2);
- border: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.08));
- border-radius: var(--radius-md);
- background: var(--bg-tertiary, rgba(0, 0, 0, 0.2));
- color: var(--text-secondary);
- cursor: pointer;
- text-align: left;
- transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
-}
-
-.nav-library-scope-trigger:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
- border-color: var(--border-default, rgba(255, 255, 255, 0.12));
-}
-
-/* «Все библиотеки»: как секция навигации, без рамки и подложки */
-.nav-library-scope-trigger--plain {
- border-color: transparent;
- background: transparent;
- align-items: center;
-}
-
-.nav-library-scope-trigger--plain:hover {
- border-color: transparent;
-}
-
-.nav-library-scope-trigger--plain .nav-library-scope-chevron {
- margin-top: 0;
-}
-
-.nav-library-scope-trigger--open:not(.nav-library-scope-trigger--plain) {
- border-color: var(--accent, var(--border-default));
-}
-
-.nav-library-scope-trigger--open.nav-library-scope-trigger--plain {
- border-color: var(--border-subtle, rgba(255, 255, 255, 0.08));
- background: var(--bg-tertiary, rgba(0, 0, 0, 0.2));
-}
-
-.nav-library-scope-icon {
- flex-shrink: 0;
- margin-top: 2px;
- opacity: 0.85;
-}
-
-.nav-library-scope-text {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
-
-.nav-library-scope-title {
- font-size: 10px;
- font-weight: 600;
- letter-spacing: 0.1em;
- text-transform: uppercase;
- color: var(--text-muted);
-}
-
-.nav-library-scope-subtitle {
- font-size: 12px;
- font-weight: 500;
- color: var(--text-primary);
- line-height: 1.25;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.nav-library-scope-chevron {
- flex-shrink: 0;
- margin-top: 2px;
- opacity: 0.7;
- transition: transform var(--transition-fast);
-}
-
-.nav-library-scope-trigger--open .nav-library-scope-chevron {
- transform: rotate(180deg);
-}
-
-.nav-library-dropdown-panel {
- z-index: 10050;
- display: flex;
- flex-direction: column;
- padding: var(--space-1);
- border-radius: var(--radius-md);
- border: 1px solid var(--border-dropdown, rgba(255, 255, 255, 0.12));
- background: var(--bg-card);
- box-shadow: 0 8px 24px var(--shadow-dropdown, rgba(0, 0, 0, 0.45));
- box-sizing: border-box;
- overflow-y: visible;
-}
-
-/* >10 папок: «Все библиотеки» + до 10 строк папок, дальше — прокрутка */
-.nav-library-dropdown-panel--many-libraries {
- --nav-lib-dropdown-row-h: calc(2 * var(--space-2) + 1.35rem);
- max-height: min(
- calc(11 * var(--nav-lib-dropdown-row-h) + 2 * var(--space-1)),
- 70vh
- );
- overflow-y: auto;
-}
-
-.nav-library-dropdown-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: var(--space-3);
- width: 100%;
- margin: 0;
- padding: var(--space-2) var(--space-3);
- border: none;
- border-radius: var(--radius-sm);
- background: transparent;
- color: var(--text-primary);
- font-size: 13px;
- font-weight: 500;
- text-align: left;
- cursor: pointer;
- transition: background var(--transition-fast);
-}
-
-.nav-library-dropdown-item:hover {
- background: var(--bg-hover);
-}
-
-.nav-library-dropdown-item--selected {
- color: var(--accent);
-}
-
-.nav-library-dropdown-item-label {
- flex: 1;
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.nav-library-dropdown-check {
- flex-shrink: 0;
- color: var(--accent);
-}
-
-.nav-library-dropdown-check-spacer {
- flex-shrink: 0;
- width: 16px;
- height: 16px;
-}
-
-.nav-section-label {
- font-size: 10px;
- font-weight: 600;
- letter-spacing: 0.1em;
- text-transform: uppercase;
- color: var(--text-muted);
- padding: var(--space-4) var(--space-3) var(--space-2);
-}
-
-/* Long-press reorder targets (library / system blocks in Sidebar) */
-.sidebar-nav-dnd-row {
- position: relative;
- border-radius: var(--radius-md);
-}
-
-.sidebar-nav-dnd-row--dragging {
- opacity: 0.45;
-}
-
-.sidebar-nav-dnd-row--drop-before::before,
-.sidebar-nav-dnd-row--drop-after::after {
- content: '';
- position: absolute;
- left: 6px;
- right: 6px;
- height: 2px;
- background: var(--accent);
- border-radius: 1px;
- pointer-events: none;
- z-index: 3;
-}
-
-.sidebar-nav-dnd-row--drop-before::before {
- top: 0;
-}
-
-.sidebar-nav-dnd-row--drop-after::after {
- bottom: 0;
-}
-
-/* Drag-out-of-sidebar: hide item — trash follows pointer */
-.sidebar-nav-dnd-trash-hint {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 48px;
- height: 48px;
- border-radius: 50%;
- pointer-events: none;
- z-index: 100002;
- color: var(--danger, #e05555);
- background: color-mix(in srgb, var(--danger, #e05555) 18%, var(--bg-card, #1e1e2e));
- border: 1px solid color-mix(in srgb, var(--danger, #e05555) 45%, transparent);
- box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
-}
-
-.nav-link {
- display: flex;
- align-items: center;
- gap: var(--space-3);
- padding: var(--space-3) var(--space-3);
- border-radius: var(--radius-md);
- color: var(--text-secondary);
- font-size: 14px;
- font-weight: 500;
- transition: all var(--transition-fast);
- cursor: pointer;
- text-decoration: none;
- position: relative;
-}
-
-.nav-link:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-
-.nav-link.active {
- background: var(--accent-dim);
- color: var(--accent);
-}
-
-.nav-link.active::before {
- content: '';
- position: absolute;
- left: 0;
- top: 50%;
- transform: translateY(-50%);
- width: 3px;
- height: 60%;
- background: var(--accent);
- border-radius: 0 var(--radius-full) var(--radius-full) 0;
-}
-
-.nav-link svg {
- flex-shrink: 0;
- opacity: 0.7;
- transition: opacity var(--transition-fast);
-}
-
-.nav-link.active svg,
-.nav-link:hover svg {
- opacity: 1;
-}
-
-/* Now Playing nav link — subtly accented but not permanently "active"-looking */
-.nav-link-nowplaying {
- color: var(--accent);
- font-weight: 600;
-}
-.nav-link-nowplaying:hover {
- background: var(--bg-hover);
- color: var(--accent);
-}
-.nav-link-nowplaying.active {
- background: var(--accent-dim);
- color: var(--accent);
-}
-.nav-link-nowplaying svg {
- opacity: 1;
-}
-
-.nav-np-icon-wrap {
- position: relative;
- display: flex;
- align-items: center;
- flex-shrink: 0;
-}
-.nav-np-dot {
- position: absolute;
- top: -3px;
- right: -4px;
- width: 7px;
- height: 7px;
- border-radius: 50%;
- background: var(--accent);
- animation: np-dot-pulse 1.8s ease-in-out infinite;
-}
-@keyframes np-dot-pulse {
- 0%, 100% { opacity: 1; transform: scale(1); }
- 50% { opacity: 0.4; transform: scale(0.7); }
-}
-
-.sidebar-nav-unread-badge {
- margin-left: auto;
- min-width: 18px;
- height: 18px;
- padding: 0 5px;
- border-radius: 999px;
- background: var(--accent);
- color: var(--ctp-crust, #11111b);
- font-size: 10px;
- font-weight: 700;
- line-height: 18px;
- text-align: center;
- box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 38%, transparent);
-}
-
-/* Collapsed Sidebar Styles */
-.sidebar.collapsed .sidebar-brand {
- justify-content: center;
- padding: 0 var(--space-3);
-}
-
-.sidebar.collapsed .nav-link {
- justify-content: center;
- padding: var(--space-3) 0;
-}
-
-.sidebar.collapsed .sidebar-nav-unread-badge {
- position: absolute;
- top: 3px;
- right: 7px;
- margin-left: 0;
- min-width: 16px;
- height: 16px;
- padding: 0 4px;
- line-height: 16px;
- font-size: 9px;
-}
-
-.collapse-btn {
- position: absolute;
- top: calc(50% - var(--player-height) / 2);
- left: calc(var(--sidebar-width) - 11px);
- transform: translateY(-50%);
- z-index: 101;
- width: 22px;
- height: 22px;
- border-radius: 50%;
- border: 1px solid var(--border-subtle);
- background: var(--bg-card);
- color: var(--text-muted);
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
- box-shadow: var(--shadow-sm);
-}
-
-.collapse-btn:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
- box-shadow: var(--shadow-md);
-}
-
-.queue-toggle-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 32px;
- height: 32px;
- border-radius: var(--radius-md);
- border: none;
- background: transparent;
- color: var(--accent);
- cursor: pointer;
- transition: background 0.15s ease, color 0.15s ease;
- flex-shrink: 0;
-}
-
-.queue-toggle-btn:hover {
- background: var(--accent-dim);
- color: var(--accent);
-}
-
-/* ─── Update Toast ─── */
-@keyframes update-toast-in {
- from { opacity: 0; transform: translateY(6px); }
- to { opacity: 1; transform: translateY(0); }
-}
-
-/* ── AppUpdater floating toast ── */
-.app-updater-toast {
- position: fixed;
- bottom: calc(var(--player-height, 80px) + 12px);
- left: 12px;
- width: 210px;
- padding: var(--space-3);
- border-radius: var(--radius-md);
- background: var(--bg-sidebar);
- border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
- box-shadow: 0 4px 20px rgba(0,0,0,0.35);
- display: flex;
- flex-direction: column;
- gap: 4px;
- z-index: 9000;
- animation: update-toast-in 0.35s ease both;
-}
-.app-updater-header {
- display: flex;
- align-items: center;
- gap: var(--space-2);
- color: var(--accent);
-}
-.app-updater-label {
- font-size: 11px;
- font-weight: 700;
- letter-spacing: 0.04em;
- text-transform: uppercase;
- flex: 1;
-}
-.app-updater-dismiss {
- background: none;
- border: none;
- cursor: pointer;
- color: var(--text-muted);
- padding: 0;
- display: flex;
- align-items: center;
- opacity: 0.6;
- transition: opacity var(--transition-fast);
-}
-.app-updater-dismiss:hover { opacity: 1; }
-.app-updater-version {
- font-size: 12px;
- color: var(--text-secondary);
- padding-left: 19px;
-}
-.app-updater-actions {
- padding-left: 19px;
- margin-top: 2px;
- display: flex;
- flex-direction: column;
- gap: 4px;
-}
-.app-updater-hint {
- font-size: 10px;
- color: var(--text-muted);
- font-style: italic;
- margin: 0 0 2px;
-}
-.app-updater-error {
- font-size: 10px;
- color: var(--danger);
- background: color-mix(in srgb, var(--danger) 12%, transparent);
- border-radius: 4px;
- padding: 4px 6px;
- margin-bottom: 4px;
- word-break: break-all;
-}
-.app-updater-btn-primary {
- display: inline-flex;
- align-items: center;
- gap: 5px;
- font-size: 11px;
- font-weight: 600;
- color: var(--accent);
- background: none;
- border: none;
- cursor: pointer;
- padding: 0;
- opacity: 0.85;
- transition: opacity var(--transition-fast);
-}
-.app-updater-btn-primary:hover { opacity: 1; }
-.app-updater-btn-secondary {
- display: inline-flex;
- align-items: center;
- gap: 5px;
- font-size: 11px;
- font-weight: 400;
- color: var(--text-secondary);
- background: none;
- border: none;
- cursor: pointer;
- padding: 0;
- opacity: 0.75;
- transition: opacity var(--transition-fast);
-}
-.app-updater-btn-secondary:hover { opacity: 1; color: var(--text-primary); }
-.app-updater-progress-wrap {
- display: flex;
- align-items: center;
- gap: var(--space-2);
- padding-left: 19px;
- margin-top: 2px;
-}
-.app-updater-progress-bar {
- flex: 1;
- height: 3px;
- background: var(--bg-glass);
- border-radius: 2px;
- overflow: hidden;
-}
-.app-updater-progress-fill {
- height: 100%;
- background: var(--accent);
- border-radius: 2px;
- transition: width 0.15s ease;
-}
-.app-updater-pct {
- font-size: 10px;
- color: var(--text-muted);
- min-width: 26px;
- text-align: right;
-}
-.app-updater-status {
- font-size: 11px;
- color: var(--text-secondary);
- padding-left: 19px;
- font-style: italic;
-}
-
-/* ── Update Modal ────────────────────────────────────────────────────────── */
-.update-modal {
- width: min(520px, 94vw);
- gap: 0;
- padding: 0;
- max-height: 85vh;
- display: flex;
- flex-direction: column;
-}
-.update-modal-header {
- padding: 14px 16px 14px 18px;
-}
-.update-modal-versions {
- font-size: 12px;
- color: var(--text-muted);
- display: block;
- margin-top: 2px;
-}
-/* Scrollable middle section (changelog + download area combined) */
-.update-modal-body {
- flex: 1;
- overflow-y: auto;
- min-height: 0;
-}
-/* Changelog accordion */
-.update-modal-changelog {
- border-top: 1px solid var(--border-subtle);
- border-bottom: 1px solid var(--border-subtle);
-}
-.update-modal-changelog-toggle {
- width: 100%;
- display: flex;
- align-items: center;
- gap: var(--space-2);
- padding: 9px 18px;
- background: none;
- border: none;
- cursor: pointer;
- color: var(--text-secondary);
- font-size: 12px;
- font-weight: 600;
- text-align: left;
- letter-spacing: 0.03em;
- transition: color var(--transition-fast), background var(--transition-fast);
-}
-.update-modal-changelog-toggle:hover {
- color: var(--text-primary);
- background: var(--bg-glass);
-}
-.update-modal-changelog-body {
- padding: 4px 18px 12px;
-}
-/* Download area */
-.update-modal-download-area {
- padding: 14px 18px;
-}
-.update-modal-asset {
- display: flex;
- align-items: center;
- gap: var(--space-2);
- background: var(--bg-glass);
- border: 1px solid var(--border-subtle);
- border-radius: var(--radius-md);
- padding: 8px 12px;
-}
-.update-modal-asset-name {
- font-size: 12px;
- color: var(--text-secondary);
- flex: 1;
- word-break: break-all;
-}
-.update-modal-asset-size {
- font-size: 11px;
- color: var(--text-muted);
- flex-shrink: 0;
-}
-.update-modal-progress {
- display: flex;
- align-items: center;
- gap: var(--space-2);
-}
-.update-modal-progress .app-updater-progress-bar { flex: 1; height: 4px; }
-.update-modal-dl-bytes {
- font-size: 11px;
- color: var(--text-muted);
- white-space: nowrap;
-}
-.update-modal-done {
- display: flex;
- flex-direction: column;
- gap: var(--space-2);
-}
-.update-modal-done-title {
- font-size: 13px;
- font-weight: 600;
- color: var(--accent);
-}
-.update-modal-done-hint {
- font-size: 12px;
- color: var(--text-secondary);
-}
-.update-modal-folder-btn {
- align-self: flex-start;
- display: inline-flex;
- align-items: center;
- gap: var(--space-2);
- font-size: 12px;
-}
-/* macOS Tauri Updater — idle state info block */
-.update-modal-mac-info {
- display: flex;
- flex-direction: column;
- gap: var(--space-2);
-}
-.update-modal-mac-info-main {
- font-size: 13px;
- font-weight: 600;
- color: var(--text-primary);
-}
-.update-modal-mac-info-sub {
- font-size: 12px;
- color: var(--text-secondary);
- line-height: 1.45;
-}
-.update-modal-trust-badges {
- display: flex;
- flex-wrap: wrap;
- gap: var(--space-2);
- margin-top: var(--space-1);
-}
-.update-modal-trust-badge {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- font-size: 11px;
- color: var(--positive, var(--accent));
- background: var(--bg-glass);
- border: 1px solid var(--border-subtle);
- border-radius: var(--radius-sm);
- padding: 3px 8px;
-}
-.update-modal-trust-badge svg {
- flex-shrink: 0;
-}
-/* macOS Tauri Updater — done state (after install, before/during restart) */
-.update-modal-done-icon {
- align-self: center;
- color: var(--positive, var(--accent));
- margin-bottom: var(--space-1);
-}
-.update-modal-done-countdown {
- font-size: 12px;
- color: var(--text-secondary);
- font-variant-numeric: tabular-nums;
-}
-/* AUR hint */
-.update-modal-aur {
- display: flex;
- flex-direction: column;
- gap: var(--space-2);
-}
-.update-modal-aur-title {
- font-size: 13px;
- color: var(--text-secondary);
-}
-.update-modal-aur-cmd {
- display: block;
- background: var(--bg-glass);
- border: 1px solid var(--border-subtle);
- border-radius: var(--radius-sm);
- padding: 6px 10px;
- font-size: 12px;
- color: var(--accent);
- font-family: monospace;
- user-select: all;
-}
-.update-modal-aur-alt {
- color: var(--text-muted);
-}
-.update-modal-asset-none {
- padding: 4px 0;
-}
-/* Footer */
-.update-modal-footer {
- display: flex;
- align-items: center;
- gap: var(--space-2);
- padding: 12px 18px;
- border-top: 1px solid var(--border-subtle);
- flex-shrink: 0;
-}
-.update-modal-skip {
- font-size: 12px;
- color: var(--text-muted);
- padding: var(--space-1) var(--space-2);
-}
-.update-modal-skip:hover { color: var(--text-secondary); }
-
-.update-toast {
- margin: 0 var(--space-1) var(--space-2);
- padding: var(--space-3) var(--space-3);
- border-radius: var(--radius-md);
- background: var(--accent-dim);
- border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
- display: flex;
- flex-direction: column;
- gap: 3px;
- animation: update-toast-in 0.35s ease both;
-}
-
-.update-toast-header {
- display: flex;
- align-items: center;
- gap: var(--space-2);
- color: var(--accent);
-}
-
-.update-toast-label {
- font-size: 11px;
- font-weight: 700;
- letter-spacing: 0.04em;
- text-transform: uppercase;
- color: var(--accent);
-}
-
-.update-toast-version {
- font-size: 12px;
- color: var(--text-secondary);
- padding-left: 22px;
-}
-
-.update-toast-link {
- font-size: 11px;
- font-weight: 600;
- color: var(--accent);
- text-decoration: none;
- padding-left: 22px;
- opacity: 0.8;
- transition: opacity var(--transition-fast);
-}
-
-.update-toast-link:hover {
- opacity: 1;
- text-decoration: underline;
-}
-
-.update-toast-icon {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: var(--space-2) 0;
- color: var(--accent);
- animation: update-toast-in 0.35s ease both;
- cursor: default;
-}
-
-/* ─── Sidebar offline download queue ─── */
-.sidebar-offline-queue {
- margin: 4px var(--space-1) 0;
- padding: 6px 10px;
- border-radius: var(--radius-md);
- background: var(--accent-dim);
- border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
- display: flex;
- align-items: center;
- gap: 7px;
- font-size: 11px;
- color: var(--accent);
- overflow: hidden;
-}
-
-.sidebar-offline-queue--collapsed {
- justify-content: center;
- padding: 6px;
-}
-
-.sidebar-offline-cancel {
- margin-left: auto;
- flex-shrink: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- width: 16px;
- height: 16px;
- border-radius: 50%;
- border: none;
- background: transparent;
- color: var(--accent);
- cursor: pointer;
- opacity: 0.6;
- padding: 0;
- transition: opacity 0.15s, background 0.15s;
-}
-.sidebar-offline-cancel:hover {
- opacity: 1;
- background: color-mix(in srgb, var(--accent) 20%, transparent);
-}
-
-/* ─── Sidebar device-sync queue ─── */
-.sidebar-sync-queue {
- background: color-mix(in srgb, var(--success, #4ade80) 10%, var(--bg-sidebar));
- border-color: color-mix(in srgb, var(--success, #4ade80) 25%, transparent);
- color: var(--success, #4ade80);
-}
-
-@keyframes spin-slow {
- to { transform: rotate(360deg); }
-}
-
-.spin-slow {
- animation: spin-slow 2s linear infinite;
- flex-shrink: 0;
-}
-
-/* ─── Main Content ─── */
-.main-content {
- grid-area: main;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- min-width: 0;
- min-height: 0; /* allow 1fr row to shrink freely */
- background: var(--bg-app);
- z-index: 1;
-}
-
-/* Scoped UI scaling target. Sidebar / queue / player / titlebar live in
- sibling grid cells of .app-shell and stay at 1:1. The wrapper inherits
- .main-content's flex column layout so .content-header (fixed height) and
- .content-body (flex: 1, scroll) keep working unchanged.
- Zoom is applied via inline style only when uiScale !== 1, so the default
- case has no extra layer or layout cost. */
-.main-content-zoom {
- display: flex;
- flex-direction: column;
- flex: 1;
- min-height: 0;
- min-width: 0;
-}
-
-.content-header {
- display: flex;
- align-items: center;
- gap: var(--space-4);
- padding: 0 var(--space-6);
- height: 64px;
- box-sizing: border-box;
- border-bottom: 1px solid var(--border-subtle);
- background: var(--bg-app);
- position: relative;
- z-index: 10;
-}
-
-.content-header h1 {
- font-family: var(--font-display);
- font-size: 18px;
- font-weight: 600;
- color: var(--text-primary);
-}
-
-.content-header .spacer { flex: 1; }
-
-.content-header > * {
- transition: opacity 0.14s ease, visibility 0.14s ease;
-}
-
-.content-header[data-live-search-overlay] > :not(.live-search):not(.spacer) {
- opacity: 0;
- visibility: hidden;
- pointer-events: none;
-}
-
-.content-body {
- flex: 1;
- overflow-y: auto;
- overflow-x: hidden;
- padding: var(--space-6);
- contain: paint;
-}
-
-/* When floating player is active, add extra bottom padding so content doesn't
- get hidden behind the fixed floating bar (player height + bottom offset + minimal spacing).
- !important needed to override inline padding:0 on App.tsx content-body */
-.app-shell.floating-player .content-body {
- padding-bottom: calc(var(--player-height) + 12px) !important;
-}
-
-/* Every page re-uses .content-body as its outer wrapper, but App.tsx already
- renders one around as the scroll container. The inner one must
- not establish its own scroll ancestor — otherwise `position: sticky`
- children anchor to a container that never scrolls, and the header just
- scrolls along with the content. Padding stays: the outer App.tsx
- content-body has `padding: 0` inline, the inner one is what gives each
- page its breathing room. */
-.content-body .content-body {
- overflow: visible;
-}
-
-/* Main route stack: scroll + overlay thumb live in the inner viewport, not the
- outer .content-body, so sticky headers still anchor to the scrolling element. */
-.content-body.app-shell-route-host {
- flex: 1;
- min-height: 0;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- padding: 0;
- position: relative;
- contain: none;
-}
-
-.app-shell-route-scroll.overlay-scroll {
- flex: 1;
- min-height: 0;
-}
-
-.app-shell-route-scroll .app-shell-route-scroll__viewport {
- contain: paint;
-}
-
-/* Sticky page header: keeps page title + filter/search bar visible while the
- body scrolls. Negative horizontal margins (+ matching padding) make the
- background flush with the scroll container edges so the sticky block looks
- like a real bar, not a floating island. `top: 0` clamps below the content-
- body's own padding-top; the residual 24px gap above the sticky block while
- scrolling is intentional — gives the bar visual breathing room. */
-.page-sticky-header {
- position: sticky;
- top: 0;
- z-index: 5;
- background: var(--bg-app);
- margin: 0 calc(-1 * var(--space-6)) var(--space-5);
- padding: var(--space-4) var(--space-6);
- border-bottom: 1px solid var(--border-subtle);
-}
-
-/* ─── Offline Banner ─── */
-.offline-banner {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 6px 16px;
- background: color-mix(in srgb, var(--accent) 12%, var(--bg-sidebar));
- border-bottom: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
- color: var(--accent);
- font-size: 12px;
- font-weight: 500;
- flex-shrink: 0;
-}
-
-.offline-banner span {
- flex: 1;
-}
-
-.offline-banner-retry {
- display: flex;
- align-items: center;
- gap: 4px;
- background: none;
- border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
- border-radius: var(--radius-sm);
- color: var(--accent);
- font-size: 11px;
- padding: 2px 8px;
- cursor: pointer;
- transition: background var(--transition-fast);
-}
-
-.offline-banner-retry:hover {
- background: color-mix(in srgb, var(--accent) 15%, transparent);
-}
-
-.offline-banner-retry:disabled {
- opacity: 0.5;
- cursor: default;
-}
-
-/* ─── Offline Storage Full Banner ─── */
-.offline-storage-full-banner {
- display: flex;
- align-items: center;
- gap: 0.75rem;
- padding: 10px 16px;
- margin: 0 0 1rem 0;
- background: color-mix(in srgb, var(--color-danger, #e53935) 12%, var(--bg-card));
- border: 1px solid color-mix(in srgb, var(--color-danger, #e53935) 40%, transparent);
- border-radius: var(--radius-md);
- font-size: 13px;
- color: var(--text-primary);
- line-height: 1.4;
-}
-
-.offline-storage-full-banner span {
- flex: 1;
-}
-
-.offline-storage-full-dismiss {
- background: none;
- border: none;
- cursor: pointer;
- color: var(--text-muted);
- font-size: 18px;
- line-height: 1;
- padding: 0 2px;
- flex-shrink: 0;
-}
-
-.offline-storage-full-dismiss:hover {
- color: var(--text-primary);
-}
-
-/* ─── Player Bar ─── */
-.player-bar {
- grid-area: player;
- display: flex;
- align-items: center;
- gap: var(--space-6);
- padding: 0 var(--space-5);
- background: var(--bg-player);
- border-top: 1px solid var(--border-subtle);
- height: var(--player-height);
- position: relative;
- z-index: 100;
- /* WebKitGTK (software compositing) occasionally paints the bar fully
- black for one frame when an unrelated layer in the page invalidates.
- `contain` isolates layout/paint so the bar's region cannot
- participate in the surrounding dirty rect. */
- contain: layout paint;
-}
-
-/* Floating player bar styles - positioning handled by ResizeObserver */
-.player-bar.floating {
- position: fixed;
- bottom: 12px;
- /* left/right handled dynamically by JS */
- z-index: 1000;
- border-radius: 50px;
- width: auto;
- min-width: 100px;
- max-width: none;
- grid-area: unset;
- padding: 0 24px 0 8px;
- gap: 16px;
- background: var(--bg-player);
- border: 1px solid rgba(255, 255, 255, 0.12);
- box-shadow:
- 0 8px 32px rgba(0, 0, 0, 0.8),
- 0 0 0 1px rgba(255, 255, 255, 0.05),
- inset 0 1px 0 rgba(255, 255, 255, 0.08);
-}
-
-/* Internal element adjustments for floating mode */
-.player-bar.floating .player-track-info {
- flex: 0 0 auto;
- min-width: 240px;
- max-width: 320px;
-}
-
-.player-bar.floating .player-album-art-wrap {
- border-radius: 50%;
-}
-
-.player-bar.floating .player-album-art,
-.player-bar.floating .player-album-art-placeholder {
- border-radius: 0;
-}
-
-.player-bar.floating .player-buttons {
- flex-shrink: 0;
- gap: 16px;
-}
-
-.player-bar.floating .player-waveform-section {
- flex: 1;
- min-width: 200px;
-}
-
-.player-bar.floating .player-volume-section {
- flex: 0 0 auto;
- min-width: 120px;
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-/* Liquid-glass floating bar — macOS + Windows only.
- Inspired by PR #211 (kveld9), adapted to theme tokens and reduced
- blur so it stays within WebView2 / WKWebView GPU budgets. Linux
- always keeps the solid look from PR #216, regardless of compositor. */
-html[data-platform="macos"] .player-bar.floating,
-html[data-platform="windows"] .player-bar.floating {
- background: color-mix(in srgb, var(--bg-player) 72%, transparent);
- backdrop-filter: blur(8px);
- -webkit-backdrop-filter: blur(8px);
- border: 1px solid color-mix(in srgb, var(--text-primary) 10%, transparent);
- box-shadow:
- 0 12px 40px rgba(0, 0, 0, 0.45),
- inset 0 1px 0 color-mix(in srgb, var(--text-primary) 12%, transparent);
-}
-
-.player-track-info {
- display: flex;
- align-items: center;
- gap: var(--space-3);
- flex: 0 0 320px;
- min-width: 0;
- overflow: hidden;
-}
-
-.player-album-art {
- width: 52px;
- height: 52px;
- border-radius: var(--radius-md);
- object-fit: cover;
- background: var(--bg-card);
- flex-shrink: 0;
- box-shadow: var(--shadow-md);
-}
-
-.player-album-art-placeholder {
- width: 52px;
- height: 52px;
- border-radius: var(--radius-md);
- background: var(--bg-card);
- flex-shrink: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- color: var(--text-muted);
-}
-
-.player-album-art-wrap {
- position: relative;
- flex-shrink: 0;
- border-radius: var(--radius-sm);
- overflow: hidden;
- background: var(--bg-card);
-}
-.player-album-art-wrap::after {
- content: '';
- position: absolute;
- inset: 0;
- background: rgba(0, 0, 0, 0.3);
- opacity: 0;
- transition: opacity var(--transition-fast);
- pointer-events: none;
-}
-.player-album-art-wrap.clickable { cursor: pointer; }
-.player-album-art-wrap:hover::after { opacity: 1; }
-
-/* Expand icon hint that appears on cover hover */
-.player-art-expand-hint {
- position: absolute;
- inset: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- color: white;
- opacity: 0;
- transition: opacity var(--transition-fast);
- pointer-events: none;
- z-index: 1;
-}
-.player-album-art-wrap.clickable:hover .player-art-expand-hint { opacity: 1; }
-
-.player-track-meta {
- flex: 1 1 0;
- min-width: 0;
- overflow: hidden;
-}
-
-.player-track-name {
- font-size: 14px;
- font-weight: 600;
- color: var(--text-primary);
-}
-
-.player-track-artist {
- font-size: 12px;
- color: var(--text-secondary);
- margin-top: 2px;
-}
-
-.player-track-rating {
- margin-top: 4px;
- width: auto;
- justify-content: flex-start;
-}
-
-.player-track-rating .star {
- font-size: 11px;
- padding: 0 1px;
-}
-
-/* ── Marquee (PlayerBar track name / artist) ── */
-.marquee-wrap {
- overflow: hidden;
- white-space: nowrap;
- width: 100%;
-}
-
-.marquee-scroll {
- display: inline-block;
- animation: player-marquee 12s linear infinite;
- animation-delay: 2s;
-}
-
-@keyframes player-marquee {
- 0%, 12% { transform: translateX(0); }
- 78%, 88% { transform: translateX(var(--marquee-amount)); }
- 88.1%, 100% { transform: translateX(0); }
-}
-
-.player-buttons {
- display: flex;
- align-items: center;
- gap: var(--space-2);
- flex-shrink: 0;
-}
-
-.player-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 36px;
- height: 36px;
- border-radius: 50%;
- color: var(--text-muted);
- transition: all var(--transition-fast);
- flex-shrink: 0;
-}
-
-.player-btn:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
- transform: scale(1.12);
-}
-
-.player-btn-sm {
- width: 28px;
- height: 28px;
-}
-
-/* Star + Last.fm heart — visually separated from track title */
-.player-star-btn {
- margin: var(--space-1);
- color: var(--text-muted);
- transition: color var(--transition-fast);
-}
-
-.player-star-btn:hover,
-.player-star-btn.is-starred {
- color: var(--color-star-active, var(--accent));
-}
-
-.player-btn-primary {
- width: 46px;
- height: 46px;
- background: var(--accent);
- color: var(--ctp-crust);
- border-radius: 50%;
- box-shadow: 0 4px 18px rgba(0, 0, 0, 0.4);
- flex-shrink: 0;
-}
-
-.player-btn-primary:hover {
- background: var(--accent);
- color: var(--ctp-crust);
- transform: scale(1.06);
- box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4), var(--shadow-glow);
- filter: brightness(1.12);
-}
-
-/* Waveform seekbar section */
-.player-waveform-section {
- flex: 1;
- min-width: 0;
- display: flex;
- align-items: center;
- gap: var(--space-2);
- margin-left: 24px;
- margin-right: 24px;
-}
-
-.player-waveform-wrap {
- flex: 1;
- min-width: 0;
-}
-
-.player-time {
- font-size: 11px;
- color: var(--text-muted);
- font-variant-numeric: tabular-nums;
- white-space: nowrap;
- flex-shrink: 0;
- min-width: 2.5rem;
-}
-
-.player-time:first-child {
- text-align: right;
-}
-
-.player-time:last-child {
- text-align: left;
-}
-
-/* Clickable time toggle with swap indicator */
-.player-time-toggle {
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- padding: 2px 4px;
- border-radius: 4px;
- transition: background-color 0.15s, color 0.15s;
-}
-
-.player-time-toggle:hover {
- background-color: var(--surface-hover, rgba(128, 128, 128, 0.2));
- color: var(--text-primary);
-}
-
-.player-time-toggle:active {
- background-color: var(--surface-active, rgba(128, 128, 128, 0.3));
-}
-
-/* Volume section */
-.player-volume-section {
- display: flex;
- align-items: center;
- gap: var(--space-2);
- flex-shrink: 0;
- width: 155px;
-}
-
-.player-volume-slider-wrap {
- flex: 1;
- min-width: 0;
- position: relative;
-}
-
-.player-volume-slider {
- width: 100%;
- display: block;
- margin: 0;
- vertical-align: middle;
-}
-
-.player-volume-pct {
- position: absolute;
- bottom: calc(100% + 6px);
- transform: translateX(-50%);
- font-size: 10px;
- font-weight: 600;
- background: var(--bg-card);
- color: var(--text-primary);
- border: 1px solid var(--border-subtle);
- padding: 2px 6px;
- border-radius: var(--radius-sm);
- pointer-events: none;
- white-space: nowrap;
- box-shadow: var(--shadow-sm);
-}
-
-.player-eq-btn {
- color: var(--text-muted);
- flex-shrink: 0;
- transition: color 0.15s;
-}
-.player-eq-btn:hover,
-.player-eq-btn.active {
- color: var(--accent);
-}
-
-.player-overflow-wrap {
- position: relative;
- flex-shrink: 0;
-}
-
-.player-overflow-menu {
- position: absolute;
- right: 0;
- bottom: calc(100% + 8px);
- width: 238px;
- padding: 10px;
- border-radius: var(--radius-md);
- border: 1px solid var(--border);
- background: var(--bg-card);
- box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
- z-index: 240;
- display: flex;
- flex-direction: column;
- gap: 8px;
-}
-
-.player-overflow-menu-row {
- display: flex;
- gap: 6px;
-}
-
-.player-overflow-menu-btn {
- flex: 1;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 6px;
- height: 30px;
- border-radius: var(--radius-md);
- border: 1px solid var(--border-subtle);
- background: var(--bg-hover);
- color: var(--text-secondary);
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
- transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
-}
-
-.player-overflow-menu-btn:hover {
- background: var(--border);
- color: var(--text-primary);
-}
-
-.player-overflow-menu-btn.active {
- color: var(--accent);
- border-color: color-mix(in srgb, var(--accent) 45%, transparent);
-}
-
-.player-volume-section.player-volume-section--menu {
- width: 100%;
-}
-
-.player-overflow-menu--volume-only {
- padding: 10px;
-}
-
-.player-volume-slider-wrap.player-volume-slider-wrap--menu-only {
- width: 100%;
- min-width: 0;
-}
-
-/* ─── EQ Popup ─── */
-.eq-popup-backdrop {
- position: fixed;
- inset: 0;
- z-index: 200;
- background: rgba(0, 0, 0, 0.35);
- backdrop-filter: blur(2px);
-}
-
-.eq-popup {
- position: fixed;
- z-index: 201;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- width: min(700px, 92vw);
- background: var(--bg-sidebar);
- border: 1px solid var(--border);
- border-radius: 14px;
- box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
- padding: 20px;
- display: flex;
- flex-direction: column;
- gap: 16px;
-}
-.eq-popup .eq-wrap {
- max-width: none;
-}
-
-.eq-popup-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
-}
-
-.eq-popup-title {
- font-size: 15px;
- font-weight: 600;
- color: var(--text-primary);
-}
-
-.eq-popup-close {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 28px;
- height: 28px;
- border-radius: 6px;
- background: var(--bg-hover);
- border: 1px solid var(--border);
- color: var(--text-muted);
- cursor: pointer;
- transition: background 0.15s, color 0.15s;
-}
-.eq-popup-close:hover {
- background: var(--accent);
- color: var(--bg-app);
-}
-
-/* ─── Queue Panel ─── */
-.queue-panel {
- grid-area: queue;
- background: var(--bg-sidebar);
- border-left: 1px solid var(--border-subtle);
- display: flex;
- flex-direction: column;
- overflow: hidden;
- min-width: 0;
- min-height: 0; /* allow 1fr row to shrink freely */
-}
-
-.queue-panel.queue-drop-active {
- box-shadow: inset 0 0 0 2px var(--accent);
- background: linear-gradient(var(--accent-dim), var(--accent-dim)), var(--bg-sidebar);
-}
-
-.queue-header {
- height: 64px;
- box-sizing: border-box;
- padding: 0 var(--space-4);
- background: var(--bg-app);
- border-bottom: 1px solid var(--border-subtle);
- display: flex;
- justify-content: space-between;
- align-items: center;
- flex-shrink: 0;
-}
-
-.queue-action-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 28px;
- height: 28px;
- border-radius: var(--radius-sm);
- color: var(--text-muted);
- cursor: pointer;
- transition: background var(--transition-fast), color var(--transition-fast);
- flex-shrink: 0;
-}
-.queue-action-btn:hover:not(:disabled) {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-.queue-action-btn:disabled {
- opacity: 0.35;
- cursor: default;
-}
-
-.queue-toolbar {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 6px;
- padding: 0 var(--space-3);
- height: 44px;
- flex-shrink: 0;
- border-bottom: 1px solid var(--border-subtle);
- background: var(--bg-app);
-}
-
-.crossfade-popover {
- position: absolute;
- top: calc(100% + 10px);
- right: 0;
- width: 170px;
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- padding: 10px 12px;
- box-shadow: 0 4px 16px rgba(0,0,0,0.3);
- z-index: 200;
- display: flex;
- flex-direction: column;
- gap: 8px;
-}
-
-.crossfade-popover-label {
- display: flex;
- align-items: center;
- gap: 5px;
- font-size: 11px;
- font-weight: 600;
- color: var(--accent);
- letter-spacing: 0.03em;
-}
-
-.crossfade-popover-value {
- margin-left: auto;
- font-size: 12px;
- font-weight: 700;
- font-family: monospace;
- color: var(--text-primary);
-}
-
-.crossfade-popover-slider {
- width: 100%;
- accent-color: var(--accent);
- cursor: pointer;
-}
-
-.crossfade-popover-range {
- display: flex;
- justify-content: space-between;
- font-size: 10px;
- color: var(--text-muted);
-}
-
-.queue-toolbar-sep {
- width: 1px;
- height: 18px;
- background: var(--border-subtle);
- margin: 0 2px;
- flex-shrink: 0;
-}
-
-.queue-round-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 30px;
- height: 30px;
- border-radius: 50%;
- color: var(--text-primary);
- background: var(--bg-hover);
- border: none;
- cursor: pointer;
- transition: background var(--transition-fast), color var(--transition-fast);
- flex-shrink: 0;
-}
-.queue-round-btn:hover:not(:disabled) {
- background: var(--border);
- color: var(--accent);
-}
-.queue-round-btn:disabled {
- opacity: 0.35;
- cursor: default;
-}
-.queue-round-btn.active {
- color: var(--ctp-base);
- background: var(--accent);
-}
-
-.queue-current-track {
- display: flex;
- flex-direction: column;
- flex-shrink: 0;
- border-bottom: 1px solid var(--border-subtle);
-}
-
-.queue-current-track-body {
- padding: var(--space-3) var(--space-4);
- display: flex;
- flex-direction: row;
- align-items: flex-start;
- gap: var(--space-3);
-}
-
-.queue-current-cover {
- width: 90px;
- height: 90px;
- flex-shrink: 0;
- border-radius: var(--radius-md);
- overflow: hidden;
- background: var(--bg-surface);
- display: flex;
- align-items: center;
- justify-content: center;
- box-shadow: 0 2px 8px rgba(0,0,0,0.2);
- position: relative;
-}
-
-.queue-current-cover img {
- width: 100%;
- height: 100%;
- object-fit: cover;
-}
-
-.queue-current-cover .fallback {
- color: var(--text-muted);
-}
-
-.queue-current-info {
- display: flex;
- flex-direction: column;
- gap: 2px;
- min-width: 0;
- flex: 1;
-}
-
-.queue-current-info h3 {
- font-size: 13px;
- font-weight: 700;
- margin: 0;
- color: var(--text-primary);
-}
-
-.queue-current-sub {
- font-size: 12px;
- color: var(--text-secondary);
- transition: color var(--transition-fast);
-}
-
-.queue-current-sub.is-link {
- cursor: pointer;
-}
-.queue-current-sub.is-link:hover {
- color: var(--accent);
-}
-
-.queue-current-tech {
- width: 100%;
- font-size: 9px;
- font-family: monospace;
- letter-spacing: 0.05em;
- background: var(--ctp-surface1);
- color: var(--accent);
- padding: 3px var(--space-4);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- text-align: center;
- backdrop-filter: blur(8px);
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 6px;
-}
-
-.queue-current-tech-source {
- flex-shrink: 0;
- display: inline-flex;
- align-items: center;
- line-height: 0;
- opacity: 0.92;
-}
-
-.queue-current-tech-main {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-/* Two-line layout: base info + RG badge on top, optional ReplayGain
- values underneath when the badge is expanded. The stack wraps both
- rows so the source icon stays vertically centered next to the whole
- text block. */
-.queue-current-tech-stack {
- min-width: 0;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 1px;
-}
-
-.queue-current-tech-row {
- min-width: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 6px;
-}
-
-/* Pill-shaped toggle that signals "this track has ReplayGain metadata"
- without committing screen space to the numbers — values appear in the
- tooltip and on the second line when expanded. */
-.queue-current-tech-rg-badge {
- flex-shrink: 0;
- display: inline-flex;
- align-items: center;
- gap: 2px;
- padding: 1px 5px 1px 6px;
- border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
- background: color-mix(in srgb, var(--accent) 12%, transparent);
- color: var(--accent);
- border-radius: 999px;
- font: inherit;
- font-size: 8px;
- letter-spacing: 0.06em;
- cursor: pointer;
- transition: background 0.12s, border-color 0.12s, transform 0.12s;
-}
-.queue-current-tech-rg-badge:hover {
- background: color-mix(in srgb, var(--accent) 22%, transparent);
- border-color: color-mix(in srgb, var(--accent) 55%, transparent);
-}
-.queue-current-tech-rg-badge svg {
- transition: transform 0.18s ease;
-}
-.queue-current-tech-rg-badge--open svg {
- transform: rotate(180deg);
-}
-
-.queue-current-tech-rg {
- min-width: 0;
- max-width: 100%;
- font-size: 8px;
- opacity: 0.72;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.queue-current-tech-metric {
- margin: 0;
- padding: 0;
- border: none;
- background: transparent;
- font: inherit;
- letter-spacing: inherit;
- line-height: inherit;
- color: inherit;
- cursor: pointer;
- text-decoration: none;
- border-radius: 2px;
-}
-
-.queue-current-tech-metric:hover {
- background: color-mix(in srgb, var(--accent) 14%, transparent);
-}
-
-.queue-current-tech-metric--lufs-reanalyze {
- display: inline-flex;
- align-items: baseline;
- gap: 0;
- padding: 2px 4px;
- margin: -2px -4px;
- max-width: 100%;
- min-width: 0;
- font-variant-numeric: tabular-nums;
-}
-
-.queue-divider {
- padding: var(--space-3) var(--space-4) 0;
- flex-shrink: 0;
-}
-
-/* ─── OverlayScrollArea (shared: queue, mini, sidebar, menus) ─────────────── */
-.overlay-scroll {
- --overlay-rail-top: 0px;
- --overlay-rail-bottom: 0px;
- position: relative;
- overflow: hidden;
- min-height: 0;
- display: flex;
- flex-direction: column;
-}
-
-.overlay-scroll--rail-inset-none {
- --overlay-rail-top: 0px;
- --overlay-rail-bottom: 0px;
-}
-
-.overlay-scroll--rail-inset-mini {
- --overlay-rail-top: 4px;
- --overlay-rail-bottom: 4px;
-}
-
-.overlay-scroll--rail-inset-panel {
- --overlay-rail-top: var(--space-2);
- --overlay-rail-bottom: var(--space-2);
-}
-
-.overlay-scroll--viewport-scroll-auto .overlay-scroll__viewport {
- scroll-behavior: auto;
-}
-
-.overlay-scroll__viewport {
- flex: 1 1 auto;
- min-height: 0;
- overflow-y: auto;
- overscroll-behavior: contain;
- box-sizing: border-box;
- scrollbar-width: none;
-}
-
-.overlay-scroll__viewport::-webkit-scrollbar {
- display: none;
-}
-
-.overlay-scroll__rail {
- position: absolute;
- top: var(--overlay-rail-top);
- right: 0;
- bottom: var(--overlay-rail-bottom);
- width: 14px;
- z-index: 1;
- pointer-events: auto;
-}
-
-.overlay-scroll__thumb {
- position: absolute;
- top: 0;
- right: 2px;
- width: 3px;
- border-radius: 2px;
- background: var(--text-muted);
- opacity: 0;
- transition:
- opacity 0.18s ease,
- width 0.2s ease,
- border-radius 0.2s ease;
- pointer-events: none;
- will-change: transform;
- cursor: grab;
- touch-action: none;
-}
-
-.overlay-scroll__viewport.is-scrolling ~ .overlay-scroll__rail .overlay-scroll__thumb,
-.overlay-scroll:hover .overlay-scroll__rail .overlay-scroll__thumb {
- opacity: 0.55;
- pointer-events: auto;
-}
-
-.overlay-scroll__rail:hover .overlay-scroll__thumb,
-.overlay-scroll__rail:has(.overlay-scroll__thumb.is-thumb-dragging) .overlay-scroll__thumb {
- width: 9px;
- border-radius: 4px;
-}
-
-.overlay-scroll__rail .overlay-scroll__thumb.is-thumb-dragging {
- opacity: 0.9;
- pointer-events: auto;
- cursor: grabbing;
- transition: opacity 0.18s ease;
-}
-
-.queue-list-wrap.overlay-scroll {
- flex: 1 1 auto;
-}
-
-.queue-list.overlay-scroll__viewport {
- padding: var(--space-2);
-}
-
-.queue-empty {
- padding: var(--space-4);
- color: var(--text-muted);
- text-align: center;
- font-size: 14px;
-}
-
-.queue-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: var(--space-2) var(--space-3);
- border-radius: var(--radius-sm);
- cursor: pointer;
- transition: background var(--transition-fast);
- color: var(--text-secondary);
-}
-/* Prevent child elements from stealing dragenter/dragleave events */
-.queue-item > * { pointer-events: none; }
-
-.queue-item:hover, .queue-item.context-active {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-
-.queue-item.active {
- background: var(--accent-dim);
- color: var(--accent);
-}
-
-.queue-item-info {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
-
-.queue-item-title {
- font-size: 12px;
- font-weight: 500;
- color: inherit;
-}
-
-.queue-item-artist {
- font-size: 11px;
- color: var(--text-muted);
- transition: color var(--transition-fast);
-}
-
-.queue-item:hover .queue-item-artist,
-.queue-item.context-active .queue-item-artist {
- color: var(--text-secondary);
-}
-
-.queue-item-duration {
- font-size: 11px;
- color: var(--text-muted);
- flex-shrink: 0;
- margin-left: var(--space-3);
-}
-
-.queue-item.active .queue-item-artist,
-.queue-item.active .queue-item-duration {
- color: inherit;
- opacity: 0.8;
-}
-
-.queue-item-attribution,
-.queue-current-attribution {
- font-size: 10px;
- color: var(--text-muted);
- opacity: 0.75;
- margin-top: 1px;
- font-style: italic;
-}
-
-.queue-item:hover .queue-item-attribution,
-.queue-item.context-active .queue-item-attribution {
- color: var(--text-secondary);
- opacity: 0.9;
-}
-
-.queue-item.active .queue-item-attribution {
- color: inherit;
- opacity: 0.7;
-}
-
-/* ═══════════════════════════════════════════════════════════════════════════
- MOBILE LAYOUT (< 800px)
- Controller: data-mobile attribute set by useIsMobile hook
- ═══════════════════════════════════════════════════════════════════════════ */
-
-/* ─── BottomNav Component ─── */
-.bottom-nav {
- grid-area: bottomnav;
- display: flex;
- align-items: stretch;
- justify-content: space-around;
- background: var(--bg-sidebar);
- border-top: 1px solid var(--border-subtle);
- height: 56px;
- z-index: 100;
- flex-shrink: 0;
-}
-
-.bottom-nav-item {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 3px;
- flex: 1;
- color: var(--text-muted);
- text-decoration: none;
- font-size: 10px;
- font-weight: 500;
- letter-spacing: 0.02em;
- transition: color var(--transition-fast);
- position: relative;
-}
-
-.bottom-nav-item:hover {
- color: var(--text-secondary);
-}
-
-.bottom-nav-item.active {
- color: var(--accent);
-}
-
-.bottom-nav-item.active::before {
- content: '';
- position: absolute;
- top: 0;
- left: 50%;
- transform: translateX(-50%);
- width: 32px;
- height: 2px;
- background: var(--accent);
- border-radius: 0 0 var(--radius-full) var(--radius-full);
-}
-
-.bottom-nav-item svg {
- opacity: 0.65;
- transition: opacity var(--transition-fast);
-}
-
-.bottom-nav-item.active svg,
-.bottom-nav-item:hover svg {
- opacity: 1;
-}
-
-.bottom-nav-icon-wrap {
- position: relative;
- display: flex;
- align-items: center;
-}
-
-.bottom-nav-np-dot {
- position: absolute;
- top: -2px;
- right: -5px;
- width: 6px;
- height: 6px;
- border-radius: 50%;
- background: var(--accent);
- animation: np-dot-pulse 1.8s ease-in-out infinite;
-}
-
-.bottom-nav-label {
- line-height: 1;
-}
-
-/* ─── Mobile More Sheet ─── */
-.mobile-more-backdrop {
- position: fixed;
- inset: 0;
- background: rgba(0, 0, 0, 0.45);
- z-index: 200;
-}
-
-.mobile-more-sheet {
- position: fixed;
- bottom: 0;
- left: 0;
- right: 0;
- background: var(--bg-app);
- border-radius: 16px 16px 0 0;
- border-top: 1px solid var(--border);
- z-index: 201;
- padding: 0.5rem 0.75rem calc(0.75rem + var(--bottom-nav-height, 62px));
- animation: more-sheet-up 0.22s ease;
-}
-
-@keyframes more-sheet-up {
- from { transform: translateY(100%); }
- to { transform: translateY(0); }
-}
-
-.mobile-more-handle {
- width: 36px;
- height: 4px;
- background: var(--border);
- border-radius: 2px;
- margin: 0 auto 0.75rem;
-}
-
-.mobile-more-grid {
- display: grid;
- grid-template-columns: repeat(4, 1fr);
- gap: 0.25rem 0;
-}
-
-.mobile-more-item {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 0.3rem;
- padding: 0.65rem 0.25rem;
- border-radius: var(--radius);
- color: var(--text-secondary);
- text-decoration: none;
- font-size: 11px;
- font-weight: 500;
- cursor: pointer;
- background: none;
- border: none;
- transition: color var(--transition-fast), background var(--transition-fast);
-}
-
-.mobile-more-item:hover {
- color: var(--text-primary);
- background: var(--bg-hover);
-}
-
-.mobile-more-item.active {
- color: var(--accent);
-}
-
-.mobile-more-icon {
- display: flex;
- align-items: center;
- justify-content: center;
- opacity: 0.75;
-}
-
-.mobile-more-item.active .mobile-more-icon,
-.mobile-more-item:hover .mobile-more-icon {
- opacity: 1;
-}
-
-.mobile-more-label {
- line-height: 1.2;
- text-align: center;
- max-width: 72px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-/* ─── Grid Overrides ─── */
-.app-shell[data-mobile] {
- grid-template-columns: 1fr;
- grid-template-rows: minmax(0, 1fr) auto auto;
- grid-template-areas:
- "main"
- "bottomnav"
- "player";
- width: 100vw;
- max-width: 100%;
-}
-
-.app-shell[data-mobile][data-titlebar] {
- grid-template-rows: var(--titlebar-height) minmax(0, 1fr) auto auto;
- grid-template-areas:
- "titlebar"
- "main"
- "bottomnav"
- "player";
-}
-
-/* Hide the sidebar collapse button on mobile */
-.app-shell[data-mobile] .collapse-btn {
- display: none;
-}
-
-/* ─── Content Header — Mobile ─── */
-.app-shell[data-mobile] .content-header {
- padding: 0 var(--space-4);
- height: 52px;
- gap: var(--space-3);
-}
-
-/* Hide queue toggle on mobile */
-.app-shell[data-mobile] .queue-toggle-btn {
- display: none;
-}
-
-.app-shell[data-mobile] .resizer-queue-handle {
- display: none;
-}
-
-/* ─── Player Bar — Mobile ─── */
-.app-shell[data-mobile] .player-bar {
- gap: var(--space-3);
- padding: 0 var(--space-3);
- height: 64px;
-}
-
-.app-shell[data-mobile] .player-track-info {
- flex: 1 1 0;
- min-width: 0;
-}
-
-.app-shell[data-mobile] .player-waveform-section,
-.app-shell[data-mobile] .player-volume-section,
-.app-shell[data-mobile] .player-eq-btn,
-.app-shell[data-mobile] .player-bar > .player-btn.player-btn-sm:not(.player-star-btn):not(.player-love-btn) {
- display: none;
-}
-
-.app-shell[data-mobile] .player-buttons {
- gap: var(--space-1);
- flex-shrink: 0;
-}
-
-.app-shell[data-mobile] .player-btn-primary {
- width: 40px;
- height: 40px;
-}
-
-.app-shell[data-mobile] .player-album-art,
-.app-shell[data-mobile] .player-album-art-placeholder {
- width: 44px;
- height: 44px;
-}
-
-/* Hide star + last.fm love buttons in mobile player to save space */
-.app-shell[data-mobile] .player-star-btn,
-.app-shell[data-mobile] .player-love-btn {
- display: none;
-}
-
-/* ─── App Updater Toast — Mobile ─── */
-.app-shell[data-mobile] .app-updater-toast {
- bottom: calc(64px + 56px + 12px);
-}
-
-/* ─── Mobile Player Takeover ─── */
-/* When the mobile player is active, the app-shell becomes a single full-screen
- area with no BottomNav or PlayerBar — just the main content. */
-.app-shell[data-mobile-player] {
- grid-template-rows: minmax(0, 1fr);
- grid-template-areas: "main";
-}
-
-.app-shell[data-mobile-player] .content-header {
- display: none;
-}
-
-.app-shell[data-mobile-player] .content-body {
- padding: 0 !important;
- overflow: hidden !important;
-}
-
-/* ═══════════════════════════════════════════════════════════════════════════
- MOBILE PLAYER VIEW (MobilePlayerView.tsx)
- ═══════════════════════════════════════════════════════════════════════════ */
-
-.mp-view {
- position: fixed;
- inset: 0;
- display: flex;
- flex-direction: column;
- justify-content: space-evenly;
- background: var(--bg-app); /* fallback; overridden by dynamic gradient via inline style */
- overflow: hidden !important;
- scrollbar-width: none; /* Firefox */
- -ms-overflow-style: none; /* IE/Edge */
- padding: 0 20px env(safe-area-inset-bottom, 0);
- box-sizing: border-box;
- transition: background 0.8s ease;
- z-index: 10;
-}
-
-.mp-view::-webkit-scrollbar {
- display: none; /* Chrome, Safari, Tauri/WebKitGTK */
-}
-
-/* ─── Header ─── */
-.mp-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- height: 52px;
- flex-shrink: 0;
-}
-
-.mp-header-title {
- font-size: 13px;
- font-weight: 600;
- letter-spacing: 0.04em;
- text-transform: uppercase;
- color: var(--text-muted);
-}
-
-.mp-back {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 44px;
- height: 44px;
- border-radius: 50%;
- background: none;
- border: none;
- color: var(--text-secondary);
- cursor: pointer;
- transition: color var(--transition-fast), background var(--transition-fast);
-}
-
-.mp-back:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-
-/* ─── Cover Art ─── */
-.mp-cover-wrap {
- flex: 0 1 auto;
- min-height: 0;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.mp-cover {
- width: min(calc(100vw - 28px), 55vh);
- max-width: 440px;
- aspect-ratio: 1;
- object-fit: cover;
- border-radius: 14px;
- box-shadow: 0 16px 52px rgba(0, 0, 0, 0.7), 0 4px 16px rgba(0, 0, 0, 0.4);
-}
-
-.mp-cover-fallback {
- display: flex;
- align-items: center;
- justify-content: center;
- background: var(--bg-card);
- color: var(--text-muted);
-}
-
-/* ─── Track Metadata ─── */
-.mp-meta {
- display: flex;
- align-items: center;
- gap: var(--space-3);
- padding: 8px 0 4px;
- flex-shrink: 0;
-}
-
-.mp-meta-text {
- flex: 1;
- min-width: 0;
-}
-
-.mp-title {
- font-size: 22px;
- font-weight: 700;
- color: var(--text-primary);
- line-height: 1.3;
-}
-
-.mp-artist {
- font-size: 15px;
- color: var(--accent);
- margin-top: 2px;
-}
-
-.mp-track-info {
- font-size: 12px;
- color: var(--text-muted);
- margin-top: 4px;
- letter-spacing: 0.01em;
-}
-
-.mp-heart {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 44px;
- height: 44px;
- border-radius: 50%;
- background: none;
- border: none;
- color: var(--text-muted);
- cursor: pointer;
- flex-shrink: 0;
- transition: color var(--transition-fast), transform var(--transition-fast);
-}
-
-.mp-heart:hover {
- transform: scale(1.12);
-}
-
-.mp-heart.active {
- color: var(--color-star-active, var(--accent));
-}
-
-/* ─── Scrubber ─── */
-.mp-scrubber-wrap {
- padding: 12px 0 2px;
- flex-shrink: 0;
-}
-
-.mp-scrubber {
- position: relative;
- height: 28px;
- display: flex;
- align-items: center;
- cursor: pointer;
- touch-action: none;
-}
-
-.mp-scrubber-bg {
- position: absolute;
- left: 0;
- right: 0;
- height: 4px;
- border-radius: 2px;
- background: var(--ctp-surface2, rgba(255,255,255,0.12));
-}
-
-.mp-scrubber-fill {
- position: absolute;
- left: 0;
- height: 4px;
- border-radius: 2px;
- background: var(--accent);
- transition: width 0.08s linear;
-}
-
-.mp-scrubber-thumb {
- position: absolute;
- top: 50%;
- width: 14px;
- height: 14px;
- border-radius: 50%;
- background: var(--accent);
- transform: translate(-50%, -50%);
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
- transition: transform 0.1s ease;
-}
-
-.mp-scrubber:active .mp-scrubber-thumb {
- transform: translate(-50%, -50%) scale(1.3);
-}
-
-.mp-scrubber-times {
- display: flex;
- justify-content: space-between;
- padding-top: 4px;
- font-size: 11px;
- color: var(--text-muted);
- font-variant-numeric: tabular-nums;
-}
-
-/* ─── Transport Controls ─── */
-.mp-controls {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: var(--space-4);
- padding: 8px 0 4px;
- flex-shrink: 0;
-}
-
-.mp-ctrl-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 52px;
- height: 52px;
- border-radius: 50%;
- background: none;
- border: none;
- color: var(--text-secondary);
- cursor: pointer;
- transition: color var(--transition-fast), transform var(--transition-fast);
-}
-
-.mp-ctrl-btn:hover {
- color: var(--text-primary);
- transform: scale(1.08);
-}
-
-.mp-ctrl-sm {
- width: 44px;
- height: 44px;
- color: var(--text-muted);
-}
-
-.mp-ctrl-play {
- width: 64px;
- height: 64px;
- background: var(--accent);
- color: var(--ctp-crust);
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
-}
-
-.mp-ctrl-play:hover {
- color: var(--ctp-crust);
- filter: brightness(1.1);
- transform: scale(1.06);
- box-shadow: 0 6px 28px rgba(0, 0, 0, 0.4), var(--shadow-glow);
-}
-
-/* ─── Empty state ─── */
-.mp-empty {
- flex: 1;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: var(--space-4);
- color: var(--text-muted);
- font-size: 15px;
-}
-
-/* ─── Utility Footer ─── */
-.mp-footer {
- display: flex;
- align-items: center;
- justify-content: space-evenly;
- padding: 8px 0 max(12px, env(safe-area-inset-bottom, 12px));
- flex-shrink: 0;
-}
-
-.mp-footer-btn {
- display: flex;
- align-items: center;
- gap: 6px;
- background: none;
- border: none;
- color: var(--text-muted);
- font-size: 13px;
- font-weight: 500;
- cursor: pointer;
- padding: 10px 18px;
- border-radius: var(--radius-full);
- transition: color var(--transition-fast), background var(--transition-fast);
- min-height: 44px;
-}
-
-.mp-footer-btn:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-
-/* ═══════════════════════════════════════════════════════════════════════════
- MOBILE QUEUE / LYRICS DRAWER
- ═══════════════════════════════════════════════════════════════════════════ */
-
-@keyframes mq-slide-up {
- from { transform: translateY(100%); }
- to { transform: translateY(0); }
-}
-
-@keyframes mq-fade-in {
- from { opacity: 0; }
- to { opacity: 1; }
-}
-
-.mq-drawer-backdrop {
- position: fixed;
- inset: 0;
- z-index: 500;
- background: rgba(0, 0, 0, 0.5);
- backdrop-filter: blur(4px);
- animation: mq-fade-in 0.2s ease both;
-}
-
-.mq-drawer {
- position: fixed;
- bottom: 0;
- left: 0;
- right: 0;
- height: 90vh;
- z-index: 501;
- background: var(--bg-sidebar);
- border-radius: 18px 18px 0 0;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- animation: mq-slide-up 0.3s cubic-bezier(0.22, 1, 0.36, 1) both;
- box-shadow: 0 -8px 40px rgba(0, 0, 0, 0.4);
-}
-
-.mq-drawer-header {
- display: flex;
- align-items: center;
- gap: var(--space-3);
- padding: 16px 20px 12px;
- border-bottom: 1px solid var(--border-subtle);
- flex-shrink: 0;
-}
-
-.mq-drawer-header h3 {
- font-size: 17px;
- font-weight: 700;
- color: var(--text-primary);
- margin: 0;
-}
-
-.mq-drawer-count {
- font-size: 13px;
- color: var(--accent);
- flex: 1;
-}
-
-.mq-drawer-close {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 36px;
- height: 36px;
- border-radius: 50%;
- background: var(--bg-hover);
- border: none;
- color: var(--text-muted);
- cursor: pointer;
- flex-shrink: 0;
- transition: background var(--transition-fast), color var(--transition-fast);
-}
-
-.mq-drawer-close:hover {
- background: var(--border);
- color: var(--text-primary);
-}
-
-.mq-drawer-list {
- flex: 1;
- overflow-y: auto;
- padding: var(--space-2);
-}
-
-.mq-drawer-empty {
- padding: var(--space-6);
- text-align: center;
- color: var(--text-muted);
- font-size: 14px;
-}
-
-/* Lyrics drawer — fill entire drawer */
-.mq-drawer-lyrics .mq-drawer-list {
- padding: 0;
-}
-
-/* ── Queue Item ── */
-.mq-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 10px 14px;
- border-radius: var(--radius-md);
- cursor: pointer;
- color: var(--text-secondary);
- transition: background var(--transition-fast);
- min-height: 44px;
-}
-
-.mq-item:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-
-.mq-item.active {
- background: var(--accent-dim);
- color: var(--accent);
-}
-
-.mq-item-info {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
-
-.mq-item-title {
- font-size: 13px;
- font-weight: 500;
- color: inherit;
- display: flex;
- align-items: center;
- gap: 6px;
-}
-
-.mq-item-artist {
- font-size: 11px;
- color: var(--text-muted);
-}
-
-.mq-item.active .mq-item-artist {
- color: inherit;
- opacity: 0.7;
-}
-
-.mq-item-dur {
- font-size: 11px;
- color: var(--text-muted);
- flex-shrink: 0;
- margin-left: var(--space-3);
-}
-
-.mq-item.active .mq-item-dur {
- color: inherit;
- opacity: 0.7;
-}
-
-/* ─── Sidebar Expandable Playlists Section ─── */
-.sidebar-playlists-wrapper {
- display: flex;
- flex-direction: column;
-}
-
-.sidebar-playlists-header-row {
- display: flex;
- align-items: center;
- gap: var(--space-1);
- padding-right: var(--space-2);
-}
-
-.sidebar-playlists-main-link {
- flex: 1;
- min-width: 0;
-}
-
-.sidebar-playlists-toggle {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 24px;
- height: 24px;
- padding: 0;
- border-radius: var(--radius-md);
- background: transparent;
- border: none;
- color: var(--text-muted);
- cursor: pointer;
- transition: all var(--transition-fast);
- flex-shrink: 0;
-}
-
-.sidebar-playlists-toggle:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-
-.sidebar-playlists-toggle.expanded {
- transform: rotate(90deg);
- color: var(--accent);
-}
-
-.sidebar-playlists-section {
- display: flex;
- flex-direction: column;
-}
-
-.sidebar-playlists-header {
- display: flex;
- align-items: center;
- gap: var(--space-3);
- width: 100%;
- padding: var(--space-2) var(--space-3);
- border-radius: var(--radius-md);
- color: var(--text-secondary);
- font-size: 14px;
- font-weight: 500;
- transition: background var(--transition-fast), color var(--transition-fast);
- cursor: pointer;
- background: transparent;
- border: none;
- text-align: left;
-}
-
-.sidebar-playlists-header:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-
-.sidebar-playlists-header.active {
- background: var(--accent-dim);
- color: var(--accent);
-}
-
-.sidebar-playlists-chevron {
- margin-left: auto;
- transition: transform 0.2s ease;
- flex-shrink: 0;
-}
-
-.sidebar-playlists-chevron.expanded {
- transform: rotate(90deg);
-}
-
-.sidebar-playlists-list {
- display: flex;
- flex-direction: column;
- padding-left: var(--space-4);
- padding-top: var(--space-1);
- padding-bottom: var(--space-1);
- gap: var(--space-1);
-}
-
-.sidebar-playlist-item {
- display: flex;
- align-items: center;
- gap: var(--space-2);
- padding: var(--space-1) var(--space-2);
- border-radius: var(--radius-md);
- color: var(--text-secondary);
- font-size: 13px;
- font-weight: 400;
- transition: background var(--transition-fast), color var(--transition-fast);
- overflow: hidden;
-}
-
-.sidebar-playlist-item span {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- flex: 1;
- min-width: 0;
-}
-
-.sidebar-playlist-item:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-
-.sidebar-playlist-item.active {
- background: var(--accent-dim);
- color: var(--accent);
-}
-
-.sidebar-playlists-loading {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: var(--space-2);
-}
-
-.sidebar-playlists-empty {
- padding: var(--space-2);
- font-size: 12px;
- color: var(--text-muted);
- font-style: italic;
-}
diff --git a/src/styles/layout/_intro.css b/src/styles/layout/_intro.css
new file mode 100644
index 00000000..322ec64f
--- /dev/null
+++ b/src/styles/layout/_intro.css
@@ -0,0 +1,42 @@
+/* ─── Psysonic App Shell ─── */
+
+*, *::before, *::after {
+ user-select: none;
+}
+
+/* Allow text selection only where the user needs to copy content */
+.artist-bio, [data-selectable] {
+ user-select: text;
+}
+
+.app-shell {
+ position: relative;
+ display: grid;
+ grid-template-columns: var(--sidebar-width) 1fr var(--queue-width);
+ /* minmax(0, 1fr) instead of plain 1fr: CSS Grid spec treats 1fr as
+ minmax(auto, 1fr), meaning the row won't shrink below its min-content
+ size. With minmax(0, 1fr) the row can collapse to zero, keeping the
+ player bar row always visible regardless of content size or zoom level. */
+ grid-template-rows: minmax(0, 1fr) var(--player-height);
+ grid-template-areas:
+ "sidebar main queue"
+ "player player player";
+ /* height: 100% inherits from html/body/#root (all 100%) so the grid
+ tracks actual window resize events on every platform.
+ 100vh was used previously but WebView2 (Windows) does not update the
+ CSS viewport synchronously on resize, causing the player-bar grid row
+ to drift off-screen. overflow: hidden still prevents the 1fr row from
+ pushing the grid taller than the window on Linux WMs that ignore
+ the OS minHeight constraint. */
+ height: 100%;
+ overflow: hidden;
+ background: var(--bg-app);
+}
+
+/* When player bar is floating, content extends to bottom */
+.app-shell.floating-player {
+ grid-template-rows: minmax(0, 1fr);
+ grid-template-areas:
+ "sidebar main queue";
+}
+
diff --git a/src/styles/layout/app-updater-toast-mobile.css b/src/styles/layout/app-updater-toast-mobile.css
new file mode 100644
index 00000000..808c05f9
--- /dev/null
+++ b/src/styles/layout/app-updater-toast-mobile.css
@@ -0,0 +1,5 @@
+/* ─── App Updater Toast — Mobile ─── */
+.app-shell[data-mobile] .app-updater-toast {
+ bottom: calc(64px + 56px + 12px);
+}
+
diff --git a/src/styles/layout/bottomnav-component.css b/src/styles/layout/bottomnav-component.css
new file mode 100644
index 00000000..4d92a0ff
--- /dev/null
+++ b/src/styles/layout/bottomnav-component.css
@@ -0,0 +1,80 @@
+/* ─── BottomNav Component ─── */
+.bottom-nav {
+ grid-area: bottomnav;
+ display: flex;
+ align-items: stretch;
+ justify-content: space-around;
+ background: var(--bg-sidebar);
+ border-top: 1px solid var(--border-subtle);
+ height: 56px;
+ z-index: 100;
+ flex-shrink: 0;
+}
+
+.bottom-nav-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 3px;
+ flex: 1;
+ color: var(--text-muted);
+ text-decoration: none;
+ font-size: 10px;
+ font-weight: 500;
+ letter-spacing: 0.02em;
+ transition: color var(--transition-fast);
+ position: relative;
+}
+
+.bottom-nav-item:hover {
+ color: var(--text-secondary);
+}
+
+.bottom-nav-item.active {
+ color: var(--accent);
+}
+
+.bottom-nav-item.active::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 32px;
+ height: 2px;
+ background: var(--accent);
+ border-radius: 0 0 var(--radius-full) var(--radius-full);
+}
+
+.bottom-nav-item svg {
+ opacity: 0.65;
+ transition: opacity var(--transition-fast);
+}
+
+.bottom-nav-item.active svg,
+.bottom-nav-item:hover svg {
+ opacity: 1;
+}
+
+.bottom-nav-icon-wrap {
+ position: relative;
+ display: flex;
+ align-items: center;
+}
+
+.bottom-nav-np-dot {
+ position: absolute;
+ top: -2px;
+ right: -5px;
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--accent);
+ animation: np-dot-pulse 1.8s ease-in-out infinite;
+}
+
+.bottom-nav-label {
+ line-height: 1;
+}
+
diff --git a/src/styles/layout/content-header-mobile.css b/src/styles/layout/content-header-mobile.css
new file mode 100644
index 00000000..070658b4
--- /dev/null
+++ b/src/styles/layout/content-header-mobile.css
@@ -0,0 +1,16 @@
+/* ─── Content Header — Mobile ─── */
+.app-shell[data-mobile] .content-header {
+ padding: 0 var(--space-4);
+ height: 52px;
+ gap: var(--space-3);
+}
+
+/* Hide queue toggle on mobile */
+.app-shell[data-mobile] .queue-toggle-btn {
+ display: none;
+}
+
+.app-shell[data-mobile] .resizer-queue-handle {
+ display: none;
+}
+
diff --git a/src/styles/layout/cover-art.css b/src/styles/layout/cover-art.css
new file mode 100644
index 00000000..a1c2ef82
--- /dev/null
+++ b/src/styles/layout/cover-art.css
@@ -0,0 +1,26 @@
+/* ─── Cover Art ─── */
+.mp-cover-wrap {
+ flex: 0 1 auto;
+ min-height: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.mp-cover {
+ width: min(calc(100vw - 28px), 55vh);
+ max-width: 440px;
+ aspect-ratio: 1;
+ object-fit: cover;
+ border-radius: 14px;
+ box-shadow: 0 16px 52px rgba(0, 0, 0, 0.7), 0 4px 16px rgba(0, 0, 0, 0.4);
+}
+
+.mp-cover-fallback {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--bg-card);
+ color: var(--text-muted);
+}
+
diff --git a/src/styles/layout/custom-title-bar-linux-only-decorations-false.css b/src/styles/layout/custom-title-bar-linux-only-decorations-false.css
new file mode 100644
index 00000000..73c25502
--- /dev/null
+++ b/src/styles/layout/custom-title-bar-linux-only-decorations-false.css
@@ -0,0 +1,139 @@
+/* ─── Custom title bar (Linux only — decorations: false) ─── */
+:root {
+ --titlebar-height: 32px;
+}
+
+.app-shell[data-titlebar] {
+ grid-template-rows: var(--titlebar-height) minmax(0, 1fr) var(--player-height);
+ grid-template-areas:
+ "titlebar titlebar titlebar"
+ "sidebar main queue"
+ "player player player";
+}
+
+/* When player bar is floating with titlebar, content extends to bottom */
+.app-shell[data-titlebar].floating-player {
+ grid-template-rows: var(--titlebar-height) minmax(0, 1fr);
+ grid-template-areas:
+ "titlebar titlebar titlebar"
+ "sidebar main queue";
+}
+
+/* Resize grips — replace the native GTK grips lost when decorations: false */
+.app-shell[data-titlebar]::before,
+.app-shell[data-titlebar]::after {
+ content: '';
+ position: fixed;
+ bottom: 0;
+ width: 14px;
+ height: 14px;
+ pointer-events: none;
+ background-image: radial-gradient(circle, var(--text-muted) 1.5px, transparent 1.5px);
+ background-size: 4px 4px;
+ opacity: 0.35;
+ z-index: 9999;
+}
+.app-shell[data-titlebar]::before { left: 0; }
+.app-shell[data-titlebar]::after { right: 0; }
+
+.titlebar {
+ grid-area: titlebar;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ background: var(--bg-sidebar);
+ border-bottom: 1px solid var(--border-subtle);
+ padding: 0 6px 0 12px;
+ height: var(--titlebar-height);
+ user-select: none;
+}
+
+.titlebar-track {
+ position: absolute;
+ left: 50%;
+ transform: translateX(-50%);
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ max-width: 40%;
+ pointer-events: none;
+ padding: 3px 10px;
+ background: rgba(0, 0, 0, 0.45);
+ border-radius: 999px;
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
+}
+
+.titlebar-track:empty {
+ display: none;
+}
+
+.titlebar-track-state {
+ font-size: 9px;
+ color: var(--accent);
+ flex-shrink: 0;
+ text-shadow: 0 1px 1px rgba(0, 0, 0, 0.6);
+}
+
+.titlebar-track-text {
+ font-size: 11px;
+ color: #ececec;
+ max-width: 100%;
+ text-shadow: 0 1px 1px rgba(0, 0, 0, 0.5);
+}
+
+.titlebar-controls {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 0 8px;
+ flex: 0 0 auto;
+}
+
+.titlebar-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 14px;
+ height: 14px;
+ padding: 0;
+ border: none;
+ border-radius: 50%;
+ cursor: pointer;
+ box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18);
+ transition: filter var(--transition-fast), box-shadow var(--transition-fast);
+}
+
+.titlebar-btn-close {
+ background: #ff5f57;
+}
+
+.titlebar-btn-minimize {
+ background: #febc2e;
+}
+
+.titlebar-btn-maximize {
+ background: #28c840;
+}
+
+.titlebar-btn:hover {
+ filter: brightness(1.1);
+}
+
+.titlebar-btn-close:hover { box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18), 0 0 6px rgba(255, 95, 87, 0.75); }
+.titlebar-btn-minimize:hover { box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18), 0 0 6px rgba(254, 188, 46, 0.75); }
+.titlebar-btn-maximize:hover { box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18), 0 0 6px rgba(40, 200, 64, 0.75); }
+
+.titlebar-btn:active {
+ filter: brightness(0.92);
+}
+
+.titlebar-btn:focus-visible {
+ outline: none;
+ box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18), 0 0 0 2px rgba(255, 255, 255, 0.5);
+}
+
+/* Resizer handles must start below the titlebar */
+.app-shell[data-titlebar] .resizer {
+ top: var(--titlebar-height);
+}
+
diff --git a/src/styles/layout/empty-state.css b/src/styles/layout/empty-state.css
new file mode 100644
index 00000000..9d32908c
--- /dev/null
+++ b/src/styles/layout/empty-state.css
@@ -0,0 +1,12 @@
+/* ─── Empty state ─── */
+.mp-empty {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: var(--space-4);
+ color: var(--text-muted);
+ font-size: 15px;
+}
+
diff --git a/src/styles/layout/eq-popup.css b/src/styles/layout/eq-popup.css
new file mode 100644
index 00000000..98d7e1f5
--- /dev/null
+++ b/src/styles/layout/eq-popup.css
@@ -0,0 +1,59 @@
+/* ─── EQ Popup ─── */
+.eq-popup-backdrop {
+ position: fixed;
+ inset: 0;
+ z-index: 200;
+ background: rgba(0, 0, 0, 0.35);
+ backdrop-filter: blur(2px);
+}
+
+.eq-popup {
+ position: fixed;
+ z-index: 201;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: min(700px, 92vw);
+ background: var(--bg-sidebar);
+ border: 1px solid var(--border);
+ border-radius: 14px;
+ box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
+ padding: 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+.eq-popup .eq-wrap {
+ max-width: none;
+}
+
+.eq-popup-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.eq-popup-title {
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.eq-popup-close {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 28px;
+ height: 28px;
+ border-radius: 6px;
+ background: var(--bg-hover);
+ border: 1px solid var(--border);
+ color: var(--text-muted);
+ cursor: pointer;
+ transition: background 0.15s, color 0.15s;
+}
+.eq-popup-close:hover {
+ background: var(--accent);
+ color: var(--bg-app);
+}
+
diff --git a/src/styles/layout/grid-overrides.css b/src/styles/layout/grid-overrides.css
new file mode 100644
index 00000000..285da165
--- /dev/null
+++ b/src/styles/layout/grid-overrides.css
@@ -0,0 +1,26 @@
+/* ─── Grid Overrides ─── */
+.app-shell[data-mobile] {
+ grid-template-columns: 1fr;
+ grid-template-rows: minmax(0, 1fr) auto auto;
+ grid-template-areas:
+ "main"
+ "bottomnav"
+ "player";
+ width: 100vw;
+ max-width: 100%;
+}
+
+.app-shell[data-mobile][data-titlebar] {
+ grid-template-rows: var(--titlebar-height) minmax(0, 1fr) auto auto;
+ grid-template-areas:
+ "titlebar"
+ "main"
+ "bottomnav"
+ "player";
+}
+
+/* Hide the sidebar collapse button on mobile */
+.app-shell[data-mobile] .collapse-btn {
+ display: none;
+}
+
diff --git a/src/styles/layout/header.css b/src/styles/layout/header.css
new file mode 100644
index 00000000..7595bdf3
--- /dev/null
+++ b/src/styles/layout/header.css
@@ -0,0 +1,36 @@
+/* ─── Header ─── */
+.mp-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ height: 52px;
+ flex-shrink: 0;
+}
+
+.mp-header-title {
+ font-size: 13px;
+ font-weight: 600;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: var(--text-muted);
+}
+
+.mp-back {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 44px;
+ height: 44px;
+ border-radius: 50%;
+ background: none;
+ border: none;
+ color: var(--text-secondary);
+ cursor: pointer;
+ transition: color var(--transition-fast), background var(--transition-fast);
+}
+
+.mp-back:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+}
+
diff --git a/src/styles/layout/index.css b/src/styles/layout/index.css
new file mode 100644
index 00000000..61303c3a
--- /dev/null
+++ b/src/styles/layout/index.css
@@ -0,0 +1,29 @@
+@import './_intro.css';
+@import './custom-title-bar-linux-only-decorations-false.css';
+@import './resizer-handles.css';
+@import './sidebar.css';
+@import './update-toast.css';
+@import './sidebar-offline-download-queue.css';
+@import './sidebar-device-sync-queue.css';
+@import './main-content.css';
+@import './offline-banner.css';
+@import './offline-storage-full-banner.css';
+@import './player-bar.css';
+@import './eq-popup.css';
+@import './queue-panel.css';
+@import './overlayscrollarea-shared-queue-mini-sidebar-menus.css';
+@import './bottomnav-component.css';
+@import './mobile-more-sheet.css';
+@import './grid-overrides.css';
+@import './content-header-mobile.css';
+@import './player-bar-mobile.css';
+@import './app-updater-toast-mobile.css';
+@import './mobile-player-takeover.css';
+@import './header.css';
+@import './cover-art.css';
+@import './track-metadata.css';
+@import './scrubber.css';
+@import './transport-controls.css';
+@import './empty-state.css';
+@import './utility-footer.css';
+@import './sidebar-expandable-playlists-section.css';
diff --git a/src/styles/layout/main-content.css b/src/styles/layout/main-content.css
new file mode 100644
index 00000000..fc0fc3a8
--- /dev/null
+++ b/src/styles/layout/main-content.css
@@ -0,0 +1,122 @@
+/* ─── Main Content ─── */
+.main-content {
+ grid-area: main;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ min-width: 0;
+ min-height: 0; /* allow 1fr row to shrink freely */
+ background: var(--bg-app);
+ z-index: 1;
+}
+
+/* Scoped UI scaling target. Sidebar / queue / player / titlebar live in
+ sibling grid cells of .app-shell and stay at 1:1. The wrapper inherits
+ .main-content's flex column layout so .content-header (fixed height) and
+ .content-body (flex: 1, scroll) keep working unchanged.
+ Zoom is applied via inline style only when uiScale !== 1, so the default
+ case has no extra layer or layout cost. */
+.main-content-zoom {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-height: 0;
+ min-width: 0;
+}
+
+.content-header {
+ display: flex;
+ align-items: center;
+ gap: var(--space-4);
+ padding: 0 var(--space-6);
+ height: 64px;
+ box-sizing: border-box;
+ border-bottom: 1px solid var(--border-subtle);
+ background: var(--bg-app);
+ position: relative;
+ z-index: 10;
+}
+
+.content-header h1 {
+ font-family: var(--font-display);
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.content-header .spacer { flex: 1; }
+
+.content-header > * {
+ transition: opacity 0.14s ease, visibility 0.14s ease;
+}
+
+.content-header[data-live-search-overlay] > :not(.live-search):not(.spacer) {
+ opacity: 0;
+ visibility: hidden;
+ pointer-events: none;
+}
+
+.content-body {
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+ padding: var(--space-6);
+ contain: paint;
+}
+
+/* When floating player is active, add extra bottom padding so content doesn't
+ get hidden behind the fixed floating bar (player height + bottom offset + minimal spacing).
+ !important needed to override inline padding:0 on App.tsx content-body */
+.app-shell.floating-player .content-body {
+ padding-bottom: calc(var(--player-height) + 12px) !important;
+}
+
+/* Every page re-uses .content-body as its outer wrapper, but App.tsx already
+ renders one around as the scroll container. The inner one must
+ not establish its own scroll ancestor — otherwise `position: sticky`
+ children anchor to a container that never scrolls, and the header just
+ scrolls along with the content. Padding stays: the outer App.tsx
+ content-body has `padding: 0` inline, the inner one is what gives each
+ page its breathing room. */
+.content-body .content-body {
+ overflow: visible;
+}
+
+/* Main route stack: scroll + overlay thumb live in the inner viewport, not the
+ outer .content-body, so sticky headers still anchor to the scrolling element. */
+.content-body.app-shell-route-host {
+ flex: 1;
+ min-height: 0;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+ position: relative;
+ contain: none;
+}
+
+.app-shell-route-scroll.overlay-scroll {
+ flex: 1;
+ min-height: 0;
+}
+
+.app-shell-route-scroll .app-shell-route-scroll__viewport {
+ contain: paint;
+}
+
+/* Sticky page header: keeps page title + filter/search bar visible while the
+ body scrolls. Negative horizontal margins (+ matching padding) make the
+ background flush with the scroll container edges so the sticky block looks
+ like a real bar, not a floating island. `top: 0` clamps below the content-
+ body's own padding-top; the residual 24px gap above the sticky block while
+ scrolling is intentional — gives the bar visual breathing room. */
+.page-sticky-header {
+ position: sticky;
+ top: 0;
+ z-index: 5;
+ background: var(--bg-app);
+ margin: 0 calc(-1 * var(--space-6)) var(--space-5);
+ padding: var(--space-4) var(--space-6);
+ border-bottom: 1px solid var(--border-subtle);
+}
+
diff --git a/src/styles/layout/mobile-more-sheet.css b/src/styles/layout/mobile-more-sheet.css
new file mode 100644
index 00000000..dfeefaf2
--- /dev/null
+++ b/src/styles/layout/mobile-more-sheet.css
@@ -0,0 +1,87 @@
+/* ─── Mobile More Sheet ─── */
+.mobile-more-backdrop {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.45);
+ z-index: 200;
+}
+
+.mobile-more-sheet {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background: var(--bg-app);
+ border-radius: 16px 16px 0 0;
+ border-top: 1px solid var(--border);
+ z-index: 201;
+ padding: 0.5rem 0.75rem calc(0.75rem + var(--bottom-nav-height, 62px));
+ animation: more-sheet-up 0.22s ease;
+}
+
+@keyframes more-sheet-up {
+ from { transform: translateY(100%); }
+ to { transform: translateY(0); }
+}
+
+.mobile-more-handle {
+ width: 36px;
+ height: 4px;
+ background: var(--border);
+ border-radius: 2px;
+ margin: 0 auto 0.75rem;
+}
+
+.mobile-more-grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 0.25rem 0;
+}
+
+.mobile-more-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.3rem;
+ padding: 0.65rem 0.25rem;
+ border-radius: var(--radius);
+ color: var(--text-secondary);
+ text-decoration: none;
+ font-size: 11px;
+ font-weight: 500;
+ cursor: pointer;
+ background: none;
+ border: none;
+ transition: color var(--transition-fast), background var(--transition-fast);
+}
+
+.mobile-more-item:hover {
+ color: var(--text-primary);
+ background: var(--bg-hover);
+}
+
+.mobile-more-item.active {
+ color: var(--accent);
+}
+
+.mobile-more-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0.75;
+}
+
+.mobile-more-item.active .mobile-more-icon,
+.mobile-more-item:hover .mobile-more-icon {
+ opacity: 1;
+}
+
+.mobile-more-label {
+ line-height: 1.2;
+ text-align: center;
+ max-width: 72px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
diff --git a/src/styles/layout/mobile-player-takeover.css b/src/styles/layout/mobile-player-takeover.css
new file mode 100644
index 00000000..73f14d2d
--- /dev/null
+++ b/src/styles/layout/mobile-player-takeover.css
@@ -0,0 +1,41 @@
+/* ─── Mobile Player Takeover ─── */
+/* When the mobile player is active, the app-shell becomes a single full-screen
+ area with no BottomNav or PlayerBar — just the main content. */
+.app-shell[data-mobile-player] {
+ grid-template-rows: minmax(0, 1fr);
+ grid-template-areas: "main";
+}
+
+.app-shell[data-mobile-player] .content-header {
+ display: none;
+}
+
+.app-shell[data-mobile-player] .content-body {
+ padding: 0 !important;
+ overflow: hidden !important;
+}
+
+/* ═══════════════════════════════════════════════════════════════════════════
+ MOBILE PLAYER VIEW (MobilePlayerView.tsx)
+ ═══════════════════════════════════════════════════════════════════════════ */
+
+.mp-view {
+ position: fixed;
+ inset: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-evenly;
+ background: var(--bg-app); /* fallback; overridden by dynamic gradient via inline style */
+ overflow: hidden !important;
+ scrollbar-width: none; /* Firefox */
+ -ms-overflow-style: none; /* IE/Edge */
+ padding: 0 20px env(safe-area-inset-bottom, 0);
+ box-sizing: border-box;
+ transition: background 0.8s ease;
+ z-index: 10;
+}
+
+.mp-view::-webkit-scrollbar {
+ display: none; /* Chrome, Safari, Tauri/WebKitGTK */
+}
+
diff --git a/src/styles/layout/offline-banner.css b/src/styles/layout/offline-banner.css
new file mode 100644
index 00000000..05b6d845
--- /dev/null
+++ b/src/styles/layout/offline-banner.css
@@ -0,0 +1,41 @@
+/* ─── Offline Banner ─── */
+.offline-banner {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 16px;
+ background: color-mix(in srgb, var(--accent) 12%, var(--bg-sidebar));
+ border-bottom: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
+ color: var(--accent);
+ font-size: 12px;
+ font-weight: 500;
+ flex-shrink: 0;
+}
+
+.offline-banner span {
+ flex: 1;
+}
+
+.offline-banner-retry {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ background: none;
+ border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
+ border-radius: var(--radius-sm);
+ color: var(--accent);
+ font-size: 11px;
+ padding: 2px 8px;
+ cursor: pointer;
+ transition: background var(--transition-fast);
+}
+
+.offline-banner-retry:hover {
+ background: color-mix(in srgb, var(--accent) 15%, transparent);
+}
+
+.offline-banner-retry:disabled {
+ opacity: 0.5;
+ cursor: default;
+}
+
diff --git a/src/styles/layout/offline-storage-full-banner.css b/src/styles/layout/offline-storage-full-banner.css
new file mode 100644
index 00000000..8b465f60
--- /dev/null
+++ b/src/styles/layout/offline-storage-full-banner.css
@@ -0,0 +1,34 @@
+/* ─── Offline Storage Full Banner ─── */
+.offline-storage-full-banner {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 10px 16px;
+ margin: 0 0 1rem 0;
+ background: color-mix(in srgb, var(--color-danger, #e53935) 12%, var(--bg-card));
+ border: 1px solid color-mix(in srgb, var(--color-danger, #e53935) 40%, transparent);
+ border-radius: var(--radius-md);
+ font-size: 13px;
+ color: var(--text-primary);
+ line-height: 1.4;
+}
+
+.offline-storage-full-banner span {
+ flex: 1;
+}
+
+.offline-storage-full-dismiss {
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: var(--text-muted);
+ font-size: 18px;
+ line-height: 1;
+ padding: 0 2px;
+ flex-shrink: 0;
+}
+
+.offline-storage-full-dismiss:hover {
+ color: var(--text-primary);
+}
+
diff --git a/src/styles/layout/overlayscrollarea-shared-queue-mini-sidebar-menus.css b/src/styles/layout/overlayscrollarea-shared-queue-mini-sidebar-menus.css
new file mode 100644
index 00000000..46db9e87
--- /dev/null
+++ b/src/styles/layout/overlayscrollarea-shared-queue-mini-sidebar-menus.css
@@ -0,0 +1,191 @@
+/* ─── OverlayScrollArea (shared: queue, mini, sidebar, menus) ─────────────── */
+.overlay-scroll {
+ --overlay-rail-top: 0px;
+ --overlay-rail-bottom: 0px;
+ position: relative;
+ overflow: hidden;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.overlay-scroll--rail-inset-none {
+ --overlay-rail-top: 0px;
+ --overlay-rail-bottom: 0px;
+}
+
+.overlay-scroll--rail-inset-mini {
+ --overlay-rail-top: 4px;
+ --overlay-rail-bottom: 4px;
+}
+
+.overlay-scroll--rail-inset-panel {
+ --overlay-rail-top: var(--space-2);
+ --overlay-rail-bottom: var(--space-2);
+}
+
+.overlay-scroll--viewport-scroll-auto .overlay-scroll__viewport {
+ scroll-behavior: auto;
+}
+
+.overlay-scroll__viewport {
+ flex: 1 1 auto;
+ min-height: 0;
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ box-sizing: border-box;
+ scrollbar-width: none;
+}
+
+.overlay-scroll__viewport::-webkit-scrollbar {
+ display: none;
+}
+
+.overlay-scroll__rail {
+ position: absolute;
+ top: var(--overlay-rail-top);
+ right: 0;
+ bottom: var(--overlay-rail-bottom);
+ width: 14px;
+ z-index: 1;
+ pointer-events: auto;
+}
+
+.overlay-scroll__thumb {
+ position: absolute;
+ top: 0;
+ right: 2px;
+ width: 3px;
+ border-radius: 2px;
+ background: var(--text-muted);
+ opacity: 0;
+ transition:
+ opacity 0.18s ease,
+ width 0.2s ease,
+ border-radius 0.2s ease;
+ pointer-events: none;
+ will-change: transform;
+ cursor: grab;
+ touch-action: none;
+}
+
+.overlay-scroll__viewport.is-scrolling ~ .overlay-scroll__rail .overlay-scroll__thumb,
+.overlay-scroll:hover .overlay-scroll__rail .overlay-scroll__thumb {
+ opacity: 0.55;
+ pointer-events: auto;
+}
+
+.overlay-scroll__rail:hover .overlay-scroll__thumb,
+.overlay-scroll__rail:has(.overlay-scroll__thumb.is-thumb-dragging) .overlay-scroll__thumb {
+ width: 9px;
+ border-radius: 4px;
+}
+
+.overlay-scroll__rail .overlay-scroll__thumb.is-thumb-dragging {
+ opacity: 0.9;
+ pointer-events: auto;
+ cursor: grabbing;
+ transition: opacity 0.18s ease;
+}
+
+.queue-list-wrap.overlay-scroll {
+ flex: 1 1 auto;
+}
+
+.queue-list.overlay-scroll__viewport {
+ padding: var(--space-2);
+}
+
+.queue-empty {
+ padding: var(--space-4);
+ color: var(--text-muted);
+ text-align: center;
+ font-size: 14px;
+}
+
+.queue-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: var(--space-2) var(--space-3);
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ transition: background var(--transition-fast);
+ color: var(--text-secondary);
+}
+/* Prevent child elements from stealing dragenter/dragleave events */
+.queue-item > * { pointer-events: none; }
+
+.queue-item:hover, .queue-item.context-active {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+}
+
+.queue-item.active {
+ background: var(--accent-dim);
+ color: var(--accent);
+}
+
+.queue-item-info {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.queue-item-title {
+ font-size: 12px;
+ font-weight: 500;
+ color: inherit;
+}
+
+.queue-item-artist {
+ font-size: 11px;
+ color: var(--text-muted);
+ transition: color var(--transition-fast);
+}
+
+.queue-item:hover .queue-item-artist,
+.queue-item.context-active .queue-item-artist {
+ color: var(--text-secondary);
+}
+
+.queue-item-duration {
+ font-size: 11px;
+ color: var(--text-muted);
+ flex-shrink: 0;
+ margin-left: var(--space-3);
+}
+
+.queue-item.active .queue-item-artist,
+.queue-item.active .queue-item-duration {
+ color: inherit;
+ opacity: 0.8;
+}
+
+.queue-item-attribution,
+.queue-current-attribution {
+ font-size: 10px;
+ color: var(--text-muted);
+ opacity: 0.75;
+ margin-top: 1px;
+ font-style: italic;
+}
+
+.queue-item:hover .queue-item-attribution,
+.queue-item.context-active .queue-item-attribution {
+ color: var(--text-secondary);
+ opacity: 0.9;
+}
+
+.queue-item.active .queue-item-attribution {
+ color: inherit;
+ opacity: 0.7;
+}
+
+/* ═══════════════════════════════════════════════════════════════════════════
+ MOBILE LAYOUT (< 800px)
+ Controller: data-mobile attribute set by useIsMobile hook
+ ═══════════════════════════════════════════════════════════════════════════ */
+
diff --git a/src/styles/layout/player-bar-mobile.css b/src/styles/layout/player-bar-mobile.css
new file mode 100644
index 00000000..1d2a5574
--- /dev/null
+++ b/src/styles/layout/player-bar-mobile.css
@@ -0,0 +1,41 @@
+/* ─── Player Bar — Mobile ─── */
+.app-shell[data-mobile] .player-bar {
+ gap: var(--space-3);
+ padding: 0 var(--space-3);
+ height: 64px;
+}
+
+.app-shell[data-mobile] .player-track-info {
+ flex: 1 1 0;
+ min-width: 0;
+}
+
+.app-shell[data-mobile] .player-waveform-section,
+.app-shell[data-mobile] .player-volume-section,
+.app-shell[data-mobile] .player-eq-btn,
+.app-shell[data-mobile] .player-bar > .player-btn.player-btn-sm:not(.player-star-btn):not(.player-love-btn) {
+ display: none;
+}
+
+.app-shell[data-mobile] .player-buttons {
+ gap: var(--space-1);
+ flex-shrink: 0;
+}
+
+.app-shell[data-mobile] .player-btn-primary {
+ width: 40px;
+ height: 40px;
+}
+
+.app-shell[data-mobile] .player-album-art,
+.app-shell[data-mobile] .player-album-art-placeholder {
+ width: 44px;
+ height: 44px;
+}
+
+/* Hide star + last.fm love buttons in mobile player to save space */
+.app-shell[data-mobile] .player-star-btn,
+.app-shell[data-mobile] .player-love-btn {
+ display: none;
+}
+
diff --git a/src/styles/layout/player-bar.css b/src/styles/layout/player-bar.css
new file mode 100644
index 00000000..394b654c
--- /dev/null
+++ b/src/styles/layout/player-bar.css
@@ -0,0 +1,428 @@
+/* ─── Player Bar ─── */
+.player-bar {
+ grid-area: player;
+ display: flex;
+ align-items: center;
+ gap: var(--space-6);
+ padding: 0 var(--space-5);
+ background: var(--bg-player);
+ border-top: 1px solid var(--border-subtle);
+ height: var(--player-height);
+ position: relative;
+ z-index: 100;
+ /* WebKitGTK (software compositing) occasionally paints the bar fully
+ black for one frame when an unrelated layer in the page invalidates.
+ `contain` isolates layout/paint so the bar's region cannot
+ participate in the surrounding dirty rect. */
+ contain: layout paint;
+}
+
+/* Floating player bar styles - positioning handled by ResizeObserver */
+.player-bar.floating {
+ position: fixed;
+ bottom: 12px;
+ /* left/right handled dynamically by JS */
+ z-index: 1000;
+ border-radius: 50px;
+ width: auto;
+ min-width: 100px;
+ max-width: none;
+ grid-area: unset;
+ padding: 0 24px 0 8px;
+ gap: 16px;
+ background: var(--bg-player);
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ box-shadow:
+ 0 8px 32px rgba(0, 0, 0, 0.8),
+ 0 0 0 1px rgba(255, 255, 255, 0.05),
+ inset 0 1px 0 rgba(255, 255, 255, 0.08);
+}
+
+/* Internal element adjustments for floating mode */
+.player-bar.floating .player-track-info {
+ flex: 0 0 auto;
+ min-width: 240px;
+ max-width: 320px;
+}
+
+.player-bar.floating .player-album-art-wrap {
+ border-radius: 50%;
+}
+
+.player-bar.floating .player-album-art,
+.player-bar.floating .player-album-art-placeholder {
+ border-radius: 0;
+}
+
+.player-bar.floating .player-buttons {
+ flex-shrink: 0;
+ gap: 16px;
+}
+
+.player-bar.floating .player-waveform-section {
+ flex: 1;
+ min-width: 200px;
+}
+
+.player-bar.floating .player-volume-section {
+ flex: 0 0 auto;
+ min-width: 120px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+/* Liquid-glass floating bar — macOS + Windows only.
+ Inspired by PR #211 (kveld9), adapted to theme tokens and reduced
+ blur so it stays within WebView2 / WKWebView GPU budgets. Linux
+ always keeps the solid look from PR #216, regardless of compositor. */
+html[data-platform="macos"] .player-bar.floating,
+html[data-platform="windows"] .player-bar.floating {
+ background: color-mix(in srgb, var(--bg-player) 72%, transparent);
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+ border: 1px solid color-mix(in srgb, var(--text-primary) 10%, transparent);
+ box-shadow:
+ 0 12px 40px rgba(0, 0, 0, 0.45),
+ inset 0 1px 0 color-mix(in srgb, var(--text-primary) 12%, transparent);
+}
+
+.player-track-info {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ flex: 0 0 320px;
+ min-width: 0;
+ overflow: hidden;
+}
+
+.player-album-art {
+ width: 52px;
+ height: 52px;
+ border-radius: var(--radius-md);
+ object-fit: cover;
+ background: var(--bg-card);
+ flex-shrink: 0;
+ box-shadow: var(--shadow-md);
+}
+
+.player-album-art-placeholder {
+ width: 52px;
+ height: 52px;
+ border-radius: var(--radius-md);
+ background: var(--bg-card);
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--text-muted);
+}
+
+.player-album-art-wrap {
+ position: relative;
+ flex-shrink: 0;
+ border-radius: var(--radius-sm);
+ overflow: hidden;
+ background: var(--bg-card);
+}
+.player-album-art-wrap::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.3);
+ opacity: 0;
+ transition: opacity var(--transition-fast);
+ pointer-events: none;
+}
+.player-album-art-wrap.clickable { cursor: pointer; }
+.player-album-art-wrap:hover::after { opacity: 1; }
+
+/* Expand icon hint that appears on cover hover */
+.player-art-expand-hint {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ opacity: 0;
+ transition: opacity var(--transition-fast);
+ pointer-events: none;
+ z-index: 1;
+}
+.player-album-art-wrap.clickable:hover .player-art-expand-hint { opacity: 1; }
+
+.player-track-meta {
+ flex: 1 1 0;
+ min-width: 0;
+ overflow: hidden;
+}
+
+.player-track-name {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.player-track-artist {
+ font-size: 12px;
+ color: var(--text-secondary);
+ margin-top: 2px;
+}
+
+.player-track-rating {
+ margin-top: 4px;
+ width: auto;
+ justify-content: flex-start;
+}
+
+.player-track-rating .star {
+ font-size: 11px;
+ padding: 0 1px;
+}
+
+/* ── Marquee (PlayerBar track name / artist) ── */
+.marquee-wrap {
+ overflow: hidden;
+ white-space: nowrap;
+ width: 100%;
+}
+
+.marquee-scroll {
+ display: inline-block;
+ animation: player-marquee 12s linear infinite;
+ animation-delay: 2s;
+}
+
+@keyframes player-marquee {
+ 0%, 12% { transform: translateX(0); }
+ 78%, 88% { transform: translateX(var(--marquee-amount)); }
+ 88.1%, 100% { transform: translateX(0); }
+}
+
+.player-buttons {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ flex-shrink: 0;
+}
+
+.player-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ color: var(--text-muted);
+ transition: all var(--transition-fast);
+ flex-shrink: 0;
+}
+
+.player-btn:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+ transform: scale(1.12);
+}
+
+.player-btn-sm {
+ width: 28px;
+ height: 28px;
+}
+
+/* Star + Last.fm heart — visually separated from track title */
+.player-star-btn {
+ margin: var(--space-1);
+ color: var(--text-muted);
+ transition: color var(--transition-fast);
+}
+
+.player-star-btn:hover,
+.player-star-btn.is-starred {
+ color: var(--color-star-active, var(--accent));
+}
+
+.player-btn-primary {
+ width: 46px;
+ height: 46px;
+ background: var(--accent);
+ color: var(--ctp-crust);
+ border-radius: 50%;
+ box-shadow: 0 4px 18px rgba(0, 0, 0, 0.4);
+ flex-shrink: 0;
+}
+
+.player-btn-primary:hover {
+ background: var(--accent);
+ color: var(--ctp-crust);
+ transform: scale(1.06);
+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4), var(--shadow-glow);
+ filter: brightness(1.12);
+}
+
+/* Waveform seekbar section */
+.player-waveform-section {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ margin-left: 24px;
+ margin-right: 24px;
+}
+
+.player-waveform-wrap {
+ flex: 1;
+ min-width: 0;
+}
+
+.player-time {
+ font-size: 11px;
+ color: var(--text-muted);
+ font-variant-numeric: tabular-nums;
+ white-space: nowrap;
+ flex-shrink: 0;
+ min-width: 2.5rem;
+}
+
+.player-time:first-child {
+ text-align: right;
+}
+
+.player-time:last-child {
+ text-align: left;
+}
+
+/* Clickable time toggle with swap indicator */
+.player-time-toggle {
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ padding: 2px 4px;
+ border-radius: 4px;
+ transition: background-color 0.15s, color 0.15s;
+}
+
+.player-time-toggle:hover {
+ background-color: var(--surface-hover, rgba(128, 128, 128, 0.2));
+ color: var(--text-primary);
+}
+
+.player-time-toggle:active {
+ background-color: var(--surface-active, rgba(128, 128, 128, 0.3));
+}
+
+/* Volume section */
+.player-volume-section {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ flex-shrink: 0;
+ width: 155px;
+}
+
+.player-volume-slider-wrap {
+ flex: 1;
+ min-width: 0;
+ position: relative;
+}
+
+.player-volume-slider {
+ width: 100%;
+ display: block;
+ margin: 0;
+ vertical-align: middle;
+}
+
+.player-volume-pct {
+ position: absolute;
+ bottom: calc(100% + 6px);
+ transform: translateX(-50%);
+ font-size: 10px;
+ font-weight: 600;
+ background: var(--bg-card);
+ color: var(--text-primary);
+ border: 1px solid var(--border-subtle);
+ padding: 2px 6px;
+ border-radius: var(--radius-sm);
+ pointer-events: none;
+ white-space: nowrap;
+ box-shadow: var(--shadow-sm);
+}
+
+.player-eq-btn {
+ color: var(--text-muted);
+ flex-shrink: 0;
+ transition: color 0.15s;
+}
+.player-eq-btn:hover,
+.player-eq-btn.active {
+ color: var(--accent);
+}
+
+.player-overflow-wrap {
+ position: relative;
+ flex-shrink: 0;
+}
+
+.player-overflow-menu {
+ position: absolute;
+ right: 0;
+ bottom: calc(100% + 8px);
+ width: 238px;
+ padding: 10px;
+ border-radius: var(--radius-md);
+ border: 1px solid var(--border);
+ background: var(--bg-card);
+ box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
+ z-index: 240;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.player-overflow-menu-row {
+ display: flex;
+ gap: 6px;
+}
+
+.player-overflow-menu-btn {
+ flex: 1;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ height: 30px;
+ border-radius: var(--radius-md);
+ border: 1px solid var(--border-subtle);
+ background: var(--bg-hover);
+ color: var(--text-secondary);
+ font-size: 12px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
+}
+
+.player-overflow-menu-btn:hover {
+ background: var(--border);
+ color: var(--text-primary);
+}
+
+.player-overflow-menu-btn.active {
+ color: var(--accent);
+ border-color: color-mix(in srgb, var(--accent) 45%, transparent);
+}
+
+.player-volume-section.player-volume-section--menu {
+ width: 100%;
+}
+
+.player-overflow-menu--volume-only {
+ padding: 10px;
+}
+
+.player-volume-slider-wrap.player-volume-slider-wrap--menu-only {
+ width: 100%;
+ min-width: 0;
+}
+
diff --git a/src/styles/layout/queue-panel.css b/src/styles/layout/queue-panel.css
new file mode 100644
index 00000000..0c519a26
--- /dev/null
+++ b/src/styles/layout/queue-panel.css
@@ -0,0 +1,338 @@
+/* ─── Queue Panel ─── */
+.queue-panel {
+ grid-area: queue;
+ background: var(--bg-sidebar);
+ border-left: 1px solid var(--border-subtle);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ min-width: 0;
+ min-height: 0; /* allow 1fr row to shrink freely */
+}
+
+.queue-panel.queue-drop-active {
+ box-shadow: inset 0 0 0 2px var(--accent);
+ background: linear-gradient(var(--accent-dim), var(--accent-dim)), var(--bg-sidebar);
+}
+
+.queue-header {
+ height: 64px;
+ box-sizing: border-box;
+ padding: 0 var(--space-4);
+ background: var(--bg-app);
+ border-bottom: 1px solid var(--border-subtle);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-shrink: 0;
+}
+
+.queue-action-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 28px;
+ height: 28px;
+ border-radius: var(--radius-sm);
+ color: var(--text-muted);
+ cursor: pointer;
+ transition: background var(--transition-fast), color var(--transition-fast);
+ flex-shrink: 0;
+}
+.queue-action-btn:hover:not(:disabled) {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+}
+.queue-action-btn:disabled {
+ opacity: 0.35;
+ cursor: default;
+}
+
+.queue-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ padding: 0 var(--space-3);
+ height: 44px;
+ flex-shrink: 0;
+ border-bottom: 1px solid var(--border-subtle);
+ background: var(--bg-app);
+}
+
+.crossfade-popover {
+ position: absolute;
+ top: calc(100% + 10px);
+ right: 0;
+ width: 170px;
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ padding: 10px 12px;
+ box-shadow: 0 4px 16px rgba(0,0,0,0.3);
+ z-index: 200;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.crossfade-popover-label {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--accent);
+ letter-spacing: 0.03em;
+}
+
+.crossfade-popover-value {
+ margin-left: auto;
+ font-size: 12px;
+ font-weight: 700;
+ font-family: monospace;
+ color: var(--text-primary);
+}
+
+.crossfade-popover-slider {
+ width: 100%;
+ accent-color: var(--accent);
+ cursor: pointer;
+}
+
+.crossfade-popover-range {
+ display: flex;
+ justify-content: space-between;
+ font-size: 10px;
+ color: var(--text-muted);
+}
+
+.queue-toolbar-sep {
+ width: 1px;
+ height: 18px;
+ background: var(--border-subtle);
+ margin: 0 2px;
+ flex-shrink: 0;
+}
+
+.queue-round-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ color: var(--text-primary);
+ background: var(--bg-hover);
+ border: none;
+ cursor: pointer;
+ transition: background var(--transition-fast), color var(--transition-fast);
+ flex-shrink: 0;
+}
+.queue-round-btn:hover:not(:disabled) {
+ background: var(--border);
+ color: var(--accent);
+}
+.queue-round-btn:disabled {
+ opacity: 0.35;
+ cursor: default;
+}
+.queue-round-btn.active {
+ color: var(--ctp-base);
+ background: var(--accent);
+}
+
+.queue-current-track {
+ display: flex;
+ flex-direction: column;
+ flex-shrink: 0;
+ border-bottom: 1px solid var(--border-subtle);
+}
+
+.queue-current-track-body {
+ padding: var(--space-3) var(--space-4);
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ gap: var(--space-3);
+}
+
+.queue-current-cover {
+ width: 90px;
+ height: 90px;
+ flex-shrink: 0;
+ border-radius: var(--radius-md);
+ overflow: hidden;
+ background: var(--bg-surface);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
+ position: relative;
+}
+
+.queue-current-cover img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.queue-current-cover .fallback {
+ color: var(--text-muted);
+}
+
+.queue-current-info {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ min-width: 0;
+ flex: 1;
+}
+
+.queue-current-info h3 {
+ font-size: 13px;
+ font-weight: 700;
+ margin: 0;
+ color: var(--text-primary);
+}
+
+.queue-current-sub {
+ font-size: 12px;
+ color: var(--text-secondary);
+ transition: color var(--transition-fast);
+}
+
+.queue-current-sub.is-link {
+ cursor: pointer;
+}
+.queue-current-sub.is-link:hover {
+ color: var(--accent);
+}
+
+.queue-current-tech {
+ width: 100%;
+ font-size: 9px;
+ font-family: monospace;
+ letter-spacing: 0.05em;
+ background: var(--ctp-surface1);
+ color: var(--accent);
+ padding: 3px var(--space-4);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-align: center;
+ backdrop-filter: blur(8px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+}
+
+.queue-current-tech-source {
+ flex-shrink: 0;
+ display: inline-flex;
+ align-items: center;
+ line-height: 0;
+ opacity: 0.92;
+}
+
+.queue-current-tech-main {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+/* Two-line layout: base info + RG badge on top, optional ReplayGain
+ values underneath when the badge is expanded. The stack wraps both
+ rows so the source icon stays vertically centered next to the whole
+ text block. */
+.queue-current-tech-stack {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1px;
+}
+
+.queue-current-tech-row {
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+}
+
+/* Pill-shaped toggle that signals "this track has ReplayGain metadata"
+ without committing screen space to the numbers — values appear in the
+ tooltip and on the second line when expanded. */
+.queue-current-tech-rg-badge {
+ flex-shrink: 0;
+ display: inline-flex;
+ align-items: center;
+ gap: 2px;
+ padding: 1px 5px 1px 6px;
+ border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
+ background: color-mix(in srgb, var(--accent) 12%, transparent);
+ color: var(--accent);
+ border-radius: 999px;
+ font: inherit;
+ font-size: 8px;
+ letter-spacing: 0.06em;
+ cursor: pointer;
+ transition: background 0.12s, border-color 0.12s, transform 0.12s;
+}
+.queue-current-tech-rg-badge:hover {
+ background: color-mix(in srgb, var(--accent) 22%, transparent);
+ border-color: color-mix(in srgb, var(--accent) 55%, transparent);
+}
+.queue-current-tech-rg-badge svg {
+ transition: transform 0.18s ease;
+}
+.queue-current-tech-rg-badge--open svg {
+ transform: rotate(180deg);
+}
+
+.queue-current-tech-rg {
+ min-width: 0;
+ max-width: 100%;
+ font-size: 8px;
+ opacity: 0.72;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.queue-current-tech-metric {
+ margin: 0;
+ padding: 0;
+ border: none;
+ background: transparent;
+ font: inherit;
+ letter-spacing: inherit;
+ line-height: inherit;
+ color: inherit;
+ cursor: pointer;
+ text-decoration: none;
+ border-radius: 2px;
+}
+
+.queue-current-tech-metric:hover {
+ background: color-mix(in srgb, var(--accent) 14%, transparent);
+}
+
+.queue-current-tech-metric--lufs-reanalyze {
+ display: inline-flex;
+ align-items: baseline;
+ gap: 0;
+ padding: 2px 4px;
+ margin: -2px -4px;
+ max-width: 100%;
+ min-width: 0;
+ font-variant-numeric: tabular-nums;
+}
+
+.queue-divider {
+ padding: var(--space-3) var(--space-4) 0;
+ flex-shrink: 0;
+}
+
diff --git a/src/styles/layout/resizer-handles.css b/src/styles/layout/resizer-handles.css
new file mode 100644
index 00000000..8e7ec13a
--- /dev/null
+++ b/src/styles/layout/resizer-handles.css
@@ -0,0 +1,54 @@
+/* ─── Resizer Handles ─── */
+.resizer {
+ position: absolute;
+ top: 0;
+ bottom: var(--player-height);
+ width: 6px;
+ cursor: col-resize;
+ z-index: 50;
+ background: transparent;
+ transition: background var(--transition-fast);
+}
+
+.resizer:hover, .resizer:active {
+ background: var(--accent);
+}
+
+.resizer-queue { right: calc(var(--queue-width) - 3px); }
+.resizer-queue { overflow: visible; }
+/* Queue handle has its own visual affordance; don't paint seam stripe on hover. */
+.resizer-queue:hover, .resizer-queue:active { background: transparent; }
+
+.resizer-queue-handle {
+ z-index: 101;
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ border: 1px solid var(--border-subtle);
+ background: var(--bg-card);
+ color: var(--text-muted);
+ cursor: col-resize;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
+ box-shadow: var(--shadow-sm);
+}
+
+.resizer-queue-handle:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+ box-shadow: var(--shadow-md);
+}
+
+/* While dragging an overlay scrollbar thumb, hide queue resizer so the
+ pointer does not pick up col-resize / queue resize (thumb sits under the
+ resizer hit box near the main/queue edge). */
+body.is-overlay-scrollbar-thumb-drag .resizer {
+ pointer-events: none !important;
+}
+
+/* In native fullscreen the resizer sits flush against the screen edge and
+ would interfere with OS edge-swipe gestures — hide it entirely. */
+.app-shell[data-fullscreen] .resizer { display: none; }
+
diff --git a/src/styles/layout/scrubber.css b/src/styles/layout/scrubber.css
new file mode 100644
index 00000000..541a6506
--- /dev/null
+++ b/src/styles/layout/scrubber.css
@@ -0,0 +1,58 @@
+/* ─── Scrubber ─── */
+.mp-scrubber-wrap {
+ padding: 12px 0 2px;
+ flex-shrink: 0;
+}
+
+.mp-scrubber {
+ position: relative;
+ height: 28px;
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ touch-action: none;
+}
+
+.mp-scrubber-bg {
+ position: absolute;
+ left: 0;
+ right: 0;
+ height: 4px;
+ border-radius: 2px;
+ background: var(--ctp-surface2, rgba(255,255,255,0.12));
+}
+
+.mp-scrubber-fill {
+ position: absolute;
+ left: 0;
+ height: 4px;
+ border-radius: 2px;
+ background: var(--accent);
+ transition: width 0.08s linear;
+}
+
+.mp-scrubber-thumb {
+ position: absolute;
+ top: 50%;
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ background: var(--accent);
+ transform: translate(-50%, -50%);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
+ transition: transform 0.1s ease;
+}
+
+.mp-scrubber:active .mp-scrubber-thumb {
+ transform: translate(-50%, -50%) scale(1.3);
+}
+
+.mp-scrubber-times {
+ display: flex;
+ justify-content: space-between;
+ padding-top: 4px;
+ font-size: 11px;
+ color: var(--text-muted);
+ font-variant-numeric: tabular-nums;
+}
+
diff --git a/src/styles/layout/sidebar-device-sync-queue.css b/src/styles/layout/sidebar-device-sync-queue.css
new file mode 100644
index 00000000..71c7fda3
--- /dev/null
+++ b/src/styles/layout/sidebar-device-sync-queue.css
@@ -0,0 +1,16 @@
+/* ─── Sidebar device-sync queue ─── */
+.sidebar-sync-queue {
+ background: color-mix(in srgb, var(--success, #4ade80) 10%, var(--bg-sidebar));
+ border-color: color-mix(in srgb, var(--success, #4ade80) 25%, transparent);
+ color: var(--success, #4ade80);
+}
+
+@keyframes spin-slow {
+ to { transform: rotate(360deg); }
+}
+
+.spin-slow {
+ animation: spin-slow 2s linear infinite;
+ flex-shrink: 0;
+}
+
diff --git a/src/styles/layout/sidebar-expandable-playlists-section.css b/src/styles/layout/sidebar-expandable-playlists-section.css
new file mode 100644
index 00000000..f67a2aa2
--- /dev/null
+++ b/src/styles/layout/sidebar-expandable-playlists-section.css
@@ -0,0 +1,140 @@
+/* ─── Sidebar Expandable Playlists Section ─── */
+.sidebar-playlists-wrapper {
+ display: flex;
+ flex-direction: column;
+}
+
+.sidebar-playlists-header-row {
+ display: flex;
+ align-items: center;
+ gap: var(--space-1);
+ padding-right: var(--space-2);
+}
+
+.sidebar-playlists-main-link {
+ flex: 1;
+ min-width: 0;
+}
+
+.sidebar-playlists-toggle {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ border-radius: var(--radius-md);
+ background: transparent;
+ border: none;
+ color: var(--text-muted);
+ cursor: pointer;
+ transition: all var(--transition-fast);
+ flex-shrink: 0;
+}
+
+.sidebar-playlists-toggle:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+}
+
+.sidebar-playlists-toggle.expanded {
+ transform: rotate(90deg);
+ color: var(--accent);
+}
+
+.sidebar-playlists-section {
+ display: flex;
+ flex-direction: column;
+}
+
+.sidebar-playlists-header {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ width: 100%;
+ padding: var(--space-2) var(--space-3);
+ border-radius: var(--radius-md);
+ color: var(--text-secondary);
+ font-size: 14px;
+ font-weight: 500;
+ transition: background var(--transition-fast), color var(--transition-fast);
+ cursor: pointer;
+ background: transparent;
+ border: none;
+ text-align: left;
+}
+
+.sidebar-playlists-header:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+}
+
+.sidebar-playlists-header.active {
+ background: var(--accent-dim);
+ color: var(--accent);
+}
+
+.sidebar-playlists-chevron {
+ margin-left: auto;
+ transition: transform 0.2s ease;
+ flex-shrink: 0;
+}
+
+.sidebar-playlists-chevron.expanded {
+ transform: rotate(90deg);
+}
+
+.sidebar-playlists-list {
+ display: flex;
+ flex-direction: column;
+ padding-left: var(--space-4);
+ padding-top: var(--space-1);
+ padding-bottom: var(--space-1);
+ gap: var(--space-1);
+}
+
+.sidebar-playlist-item {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ padding: var(--space-1) var(--space-2);
+ border-radius: var(--radius-md);
+ color: var(--text-secondary);
+ font-size: 13px;
+ font-weight: 400;
+ transition: background var(--transition-fast), color var(--transition-fast);
+ overflow: hidden;
+}
+
+.sidebar-playlist-item span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1;
+ min-width: 0;
+}
+
+.sidebar-playlist-item:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+}
+
+.sidebar-playlist-item.active {
+ background: var(--accent-dim);
+ color: var(--accent);
+}
+
+.sidebar-playlists-loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: var(--space-2);
+}
+
+.sidebar-playlists-empty {
+ padding: var(--space-2);
+ font-size: 12px;
+ color: var(--text-muted);
+ font-style: italic;
+}
+
diff --git a/src/styles/layout/sidebar-offline-download-queue.css b/src/styles/layout/sidebar-offline-download-queue.css
new file mode 100644
index 00000000..be7129af
--- /dev/null
+++ b/src/styles/layout/sidebar-offline-download-queue.css
@@ -0,0 +1,42 @@
+/* ─── Sidebar offline download queue ─── */
+.sidebar-offline-queue {
+ margin: 4px var(--space-1) 0;
+ padding: 6px 10px;
+ border-radius: var(--radius-md);
+ background: var(--accent-dim);
+ border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ font-size: 11px;
+ color: var(--accent);
+ overflow: hidden;
+}
+
+.sidebar-offline-queue--collapsed {
+ justify-content: center;
+ padding: 6px;
+}
+
+.sidebar-offline-cancel {
+ margin-left: auto;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ border: none;
+ background: transparent;
+ color: var(--accent);
+ cursor: pointer;
+ opacity: 0.6;
+ padding: 0;
+ transition: opacity 0.15s, background 0.15s;
+}
+.sidebar-offline-cancel:hover {
+ opacity: 1;
+ background: color-mix(in srgb, var(--accent) 20%, transparent);
+}
+
diff --git a/src/styles/layout/sidebar.css b/src/styles/layout/sidebar.css
new file mode 100644
index 00000000..fec6e23b
--- /dev/null
+++ b/src/styles/layout/sidebar.css
@@ -0,0 +1,444 @@
+/* ─── Sidebar ─── */
+.sidebar {
+ grid-area: sidebar;
+ display: flex;
+ flex-direction: column;
+ background: var(--bg-sidebar);
+ border-right: 1px solid var(--border-subtle);
+ position: relative;
+ z-index: 2;
+ min-height: 0; /* allow 1fr row to shrink freely */
+}
+
+.sidebar-brand {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ padding: 0 var(--space-5);
+ height: 64px;
+ box-sizing: border-box;
+ border-bottom: 1px solid var(--border-subtle);
+}
+
+.sidebar-brand .logo-icon {
+ width: 32px;
+ height: 32px;
+ flex-shrink: 0;
+}
+
+.sidebar-brand .brand-name {
+ font-family: var(--font-display);
+ font-size: 20px;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ background: linear-gradient(135deg, var(--accent), var(--ctp-blue));
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.sidebar-nav {
+ flex: 1;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.sidebar-nav-scroll.overlay-scroll {
+ flex: 1;
+ min-height: 0;
+}
+
+.sidebar-nav-scroll .overlay-scroll__viewport.sidebar-nav-viewport {
+ padding: var(--space-1) var(--space-3) var(--space-4);
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-1);
+}
+
+/* Library scope: Navidrome-style dropdown trigger + fixed panel (portal) */
+.nav-library-scope-trigger {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--space-2);
+ width: calc(100% - 2 * var(--space-3));
+ margin: var(--space-3) var(--space-3) var(--space-2);
+ padding: var(--space-2) var(--space-2);
+ border: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.08));
+ border-radius: var(--radius-md);
+ background: var(--bg-tertiary, rgba(0, 0, 0, 0.2));
+ color: var(--text-secondary);
+ cursor: pointer;
+ text-align: left;
+ transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
+}
+
+.nav-library-scope-trigger:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+ border-color: var(--border-default, rgba(255, 255, 255, 0.12));
+}
+
+/* «Все библиотеки»: как секция навигации, без рамки и подложки */
+.nav-library-scope-trigger--plain {
+ border-color: transparent;
+ background: transparent;
+ align-items: center;
+}
+
+.nav-library-scope-trigger--plain:hover {
+ border-color: transparent;
+}
+
+.nav-library-scope-trigger--plain .nav-library-scope-chevron {
+ margin-top: 0;
+}
+
+.nav-library-scope-trigger--open:not(.nav-library-scope-trigger--plain) {
+ border-color: var(--accent, var(--border-default));
+}
+
+.nav-library-scope-trigger--open.nav-library-scope-trigger--plain {
+ border-color: var(--border-subtle, rgba(255, 255, 255, 0.08));
+ background: var(--bg-tertiary, rgba(0, 0, 0, 0.2));
+}
+
+.nav-library-scope-icon {
+ flex-shrink: 0;
+ margin-top: 2px;
+ opacity: 0.85;
+}
+
+.nav-library-scope-text {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.nav-library-scope-title {
+ font-size: 10px;
+ font-weight: 600;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ color: var(--text-muted);
+}
+
+.nav-library-scope-subtitle {
+ font-size: 12px;
+ font-weight: 500;
+ color: var(--text-primary);
+ line-height: 1.25;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.nav-library-scope-chevron {
+ flex-shrink: 0;
+ margin-top: 2px;
+ opacity: 0.7;
+ transition: transform var(--transition-fast);
+}
+
+.nav-library-scope-trigger--open .nav-library-scope-chevron {
+ transform: rotate(180deg);
+}
+
+.nav-library-dropdown-panel {
+ z-index: 10050;
+ display: flex;
+ flex-direction: column;
+ padding: var(--space-1);
+ border-radius: var(--radius-md);
+ border: 1px solid var(--border-dropdown, rgba(255, 255, 255, 0.12));
+ background: var(--bg-card);
+ box-shadow: 0 8px 24px var(--shadow-dropdown, rgba(0, 0, 0, 0.45));
+ box-sizing: border-box;
+ overflow-y: visible;
+}
+
+/* >10 папок: «Все библиотеки» + до 10 строк папок, дальше — прокрутка */
+.nav-library-dropdown-panel--many-libraries {
+ --nav-lib-dropdown-row-h: calc(2 * var(--space-2) + 1.35rem);
+ max-height: min(
+ calc(11 * var(--nav-lib-dropdown-row-h) + 2 * var(--space-1)),
+ 70vh
+ );
+ overflow-y: auto;
+}
+
+.nav-library-dropdown-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-3);
+ width: 100%;
+ margin: 0;
+ padding: var(--space-2) var(--space-3);
+ border: none;
+ border-radius: var(--radius-sm);
+ background: transparent;
+ color: var(--text-primary);
+ font-size: 13px;
+ font-weight: 500;
+ text-align: left;
+ cursor: pointer;
+ transition: background var(--transition-fast);
+}
+
+.nav-library-dropdown-item:hover {
+ background: var(--bg-hover);
+}
+
+.nav-library-dropdown-item--selected {
+ color: var(--accent);
+}
+
+.nav-library-dropdown-item-label {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.nav-library-dropdown-check {
+ flex-shrink: 0;
+ color: var(--accent);
+}
+
+.nav-library-dropdown-check-spacer {
+ flex-shrink: 0;
+ width: 16px;
+ height: 16px;
+}
+
+.nav-section-label {
+ font-size: 10px;
+ font-weight: 600;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ color: var(--text-muted);
+ padding: var(--space-4) var(--space-3) var(--space-2);
+}
+
+/* Long-press reorder targets (library / system blocks in Sidebar) */
+.sidebar-nav-dnd-row {
+ position: relative;
+ border-radius: var(--radius-md);
+}
+
+.sidebar-nav-dnd-row--dragging {
+ opacity: 0.45;
+}
+
+.sidebar-nav-dnd-row--drop-before::before,
+.sidebar-nav-dnd-row--drop-after::after {
+ content: '';
+ position: absolute;
+ left: 6px;
+ right: 6px;
+ height: 2px;
+ background: var(--accent);
+ border-radius: 1px;
+ pointer-events: none;
+ z-index: 3;
+}
+
+.sidebar-nav-dnd-row--drop-before::before {
+ top: 0;
+}
+
+.sidebar-nav-dnd-row--drop-after::after {
+ bottom: 0;
+}
+
+/* Drag-out-of-sidebar: hide item — trash follows pointer */
+.sidebar-nav-dnd-trash-hint {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ pointer-events: none;
+ z-index: 100002;
+ color: var(--danger, #e05555);
+ background: color-mix(in srgb, var(--danger, #e05555) 18%, var(--bg-card, #1e1e2e));
+ border: 1px solid color-mix(in srgb, var(--danger, #e05555) 45%, transparent);
+ box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
+}
+
+.nav-link {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ padding: var(--space-3) var(--space-3);
+ border-radius: var(--radius-md);
+ color: var(--text-secondary);
+ font-size: 14px;
+ font-weight: 500;
+ transition: all var(--transition-fast);
+ cursor: pointer;
+ text-decoration: none;
+ position: relative;
+}
+
+.nav-link:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+}
+
+.nav-link.active {
+ background: var(--accent-dim);
+ color: var(--accent);
+}
+
+.nav-link.active::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 3px;
+ height: 60%;
+ background: var(--accent);
+ border-radius: 0 var(--radius-full) var(--radius-full) 0;
+}
+
+.nav-link svg {
+ flex-shrink: 0;
+ opacity: 0.7;
+ transition: opacity var(--transition-fast);
+}
+
+.nav-link.active svg,
+.nav-link:hover svg {
+ opacity: 1;
+}
+
+/* Now Playing nav link — subtly accented but not permanently "active"-looking */
+.nav-link-nowplaying {
+ color: var(--accent);
+ font-weight: 600;
+}
+.nav-link-nowplaying:hover {
+ background: var(--bg-hover);
+ color: var(--accent);
+}
+.nav-link-nowplaying.active {
+ background: var(--accent-dim);
+ color: var(--accent);
+}
+.nav-link-nowplaying svg {
+ opacity: 1;
+}
+
+.nav-np-icon-wrap {
+ position: relative;
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+}
+.nav-np-dot {
+ position: absolute;
+ top: -3px;
+ right: -4px;
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: var(--accent);
+ animation: np-dot-pulse 1.8s ease-in-out infinite;
+}
+@keyframes np-dot-pulse {
+ 0%, 100% { opacity: 1; transform: scale(1); }
+ 50% { opacity: 0.4; transform: scale(0.7); }
+}
+
+.sidebar-nav-unread-badge {
+ margin-left: auto;
+ min-width: 18px;
+ height: 18px;
+ padding: 0 5px;
+ border-radius: 999px;
+ background: var(--accent);
+ color: var(--ctp-crust, #11111b);
+ font-size: 10px;
+ font-weight: 700;
+ line-height: 18px;
+ text-align: center;
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 38%, transparent);
+}
+
+/* Collapsed Sidebar Styles */
+.sidebar.collapsed .sidebar-brand {
+ justify-content: center;
+ padding: 0 var(--space-3);
+}
+
+.sidebar.collapsed .nav-link {
+ justify-content: center;
+ padding: var(--space-3) 0;
+}
+
+.sidebar.collapsed .sidebar-nav-unread-badge {
+ position: absolute;
+ top: 3px;
+ right: 7px;
+ margin-left: 0;
+ min-width: 16px;
+ height: 16px;
+ padding: 0 4px;
+ line-height: 16px;
+ font-size: 9px;
+}
+
+.collapse-btn {
+ position: absolute;
+ top: calc(50% - var(--player-height) / 2);
+ left: calc(var(--sidebar-width) - 11px);
+ transform: translateY(-50%);
+ z-index: 101;
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ border: 1px solid var(--border-subtle);
+ background: var(--bg-card);
+ color: var(--text-muted);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
+ box-shadow: var(--shadow-sm);
+}
+
+.collapse-btn:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+ box-shadow: var(--shadow-md);
+}
+
+.queue-toggle-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ border-radius: var(--radius-md);
+ border: none;
+ background: transparent;
+ color: var(--accent);
+ cursor: pointer;
+ transition: background 0.15s ease, color 0.15s ease;
+ flex-shrink: 0;
+}
+
+.queue-toggle-btn:hover {
+ background: var(--accent-dim);
+ color: var(--accent);
+}
+
diff --git a/src/styles/layout/track-metadata.css b/src/styles/layout/track-metadata.css
new file mode 100644
index 00000000..26719c9f
--- /dev/null
+++ b/src/styles/layout/track-metadata.css
@@ -0,0 +1,57 @@
+/* ─── Track Metadata ─── */
+.mp-meta {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ padding: 8px 0 4px;
+ flex-shrink: 0;
+}
+
+.mp-meta-text {
+ flex: 1;
+ min-width: 0;
+}
+
+.mp-title {
+ font-size: 22px;
+ font-weight: 700;
+ color: var(--text-primary);
+ line-height: 1.3;
+}
+
+.mp-artist {
+ font-size: 15px;
+ color: var(--accent);
+ margin-top: 2px;
+}
+
+.mp-track-info {
+ font-size: 12px;
+ color: var(--text-muted);
+ margin-top: 4px;
+ letter-spacing: 0.01em;
+}
+
+.mp-heart {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 44px;
+ height: 44px;
+ border-radius: 50%;
+ background: none;
+ border: none;
+ color: var(--text-muted);
+ cursor: pointer;
+ flex-shrink: 0;
+ transition: color var(--transition-fast), transform var(--transition-fast);
+}
+
+.mp-heart:hover {
+ transform: scale(1.12);
+}
+
+.mp-heart.active {
+ color: var(--color-star-active, var(--accent));
+}
+
diff --git a/src/styles/layout/transport-controls.css b/src/styles/layout/transport-controls.css
new file mode 100644
index 00000000..eac11fb7
--- /dev/null
+++ b/src/styles/layout/transport-controls.css
@@ -0,0 +1,50 @@
+/* ─── Transport Controls ─── */
+.mp-controls {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--space-4);
+ padding: 8px 0 4px;
+ flex-shrink: 0;
+}
+
+.mp-ctrl-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 52px;
+ height: 52px;
+ border-radius: 50%;
+ background: none;
+ border: none;
+ color: var(--text-secondary);
+ cursor: pointer;
+ transition: color var(--transition-fast), transform var(--transition-fast);
+}
+
+.mp-ctrl-btn:hover {
+ color: var(--text-primary);
+ transform: scale(1.08);
+}
+
+.mp-ctrl-sm {
+ width: 44px;
+ height: 44px;
+ color: var(--text-muted);
+}
+
+.mp-ctrl-play {
+ width: 64px;
+ height: 64px;
+ background: var(--accent);
+ color: var(--ctp-crust);
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
+}
+
+.mp-ctrl-play:hover {
+ color: var(--ctp-crust);
+ filter: brightness(1.1);
+ transform: scale(1.06);
+ box-shadow: 0 6px 28px rgba(0, 0, 0, 0.4), var(--shadow-glow);
+}
+
diff --git a/src/styles/layout/update-toast.css b/src/styles/layout/update-toast.css
new file mode 100644
index 00000000..e66403ac
--- /dev/null
+++ b/src/styles/layout/update-toast.css
@@ -0,0 +1,394 @@
+/* ─── Update Toast ─── */
+@keyframes update-toast-in {
+ from { opacity: 0; transform: translateY(6px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+/* ── AppUpdater floating toast ── */
+.app-updater-toast {
+ position: fixed;
+ bottom: calc(var(--player-height, 80px) + 12px);
+ left: 12px;
+ width: 210px;
+ padding: var(--space-3);
+ border-radius: var(--radius-md);
+ background: var(--bg-sidebar);
+ border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
+ box-shadow: 0 4px 20px rgba(0,0,0,0.35);
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ z-index: 9000;
+ animation: update-toast-in 0.35s ease both;
+}
+.app-updater-header {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ color: var(--accent);
+}
+.app-updater-label {
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ flex: 1;
+}
+.app-updater-dismiss {
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: var(--text-muted);
+ padding: 0;
+ display: flex;
+ align-items: center;
+ opacity: 0.6;
+ transition: opacity var(--transition-fast);
+}
+.app-updater-dismiss:hover { opacity: 1; }
+.app-updater-version {
+ font-size: 12px;
+ color: var(--text-secondary);
+ padding-left: 19px;
+}
+.app-updater-actions {
+ padding-left: 19px;
+ margin-top: 2px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+.app-updater-hint {
+ font-size: 10px;
+ color: var(--text-muted);
+ font-style: italic;
+ margin: 0 0 2px;
+}
+.app-updater-error {
+ font-size: 10px;
+ color: var(--danger);
+ background: color-mix(in srgb, var(--danger) 12%, transparent);
+ border-radius: 4px;
+ padding: 4px 6px;
+ margin-bottom: 4px;
+ word-break: break-all;
+}
+.app-updater-btn-primary {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--accent);
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 0;
+ opacity: 0.85;
+ transition: opacity var(--transition-fast);
+}
+.app-updater-btn-primary:hover { opacity: 1; }
+.app-updater-btn-secondary {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ font-size: 11px;
+ font-weight: 400;
+ color: var(--text-secondary);
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 0;
+ opacity: 0.75;
+ transition: opacity var(--transition-fast);
+}
+.app-updater-btn-secondary:hover { opacity: 1; color: var(--text-primary); }
+.app-updater-progress-wrap {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ padding-left: 19px;
+ margin-top: 2px;
+}
+.app-updater-progress-bar {
+ flex: 1;
+ height: 3px;
+ background: var(--bg-glass);
+ border-radius: 2px;
+ overflow: hidden;
+}
+.app-updater-progress-fill {
+ height: 100%;
+ background: var(--accent);
+ border-radius: 2px;
+ transition: width 0.15s ease;
+}
+.app-updater-pct {
+ font-size: 10px;
+ color: var(--text-muted);
+ min-width: 26px;
+ text-align: right;
+}
+.app-updater-status {
+ font-size: 11px;
+ color: var(--text-secondary);
+ padding-left: 19px;
+ font-style: italic;
+}
+
+/* ── Update Modal ────────────────────────────────────────────────────────── */
+.update-modal {
+ width: min(520px, 94vw);
+ gap: 0;
+ padding: 0;
+ max-height: 85vh;
+ display: flex;
+ flex-direction: column;
+}
+.update-modal-header {
+ padding: 14px 16px 14px 18px;
+}
+.update-modal-versions {
+ font-size: 12px;
+ color: var(--text-muted);
+ display: block;
+ margin-top: 2px;
+}
+/* Scrollable middle section (changelog + download area combined) */
+.update-modal-body {
+ flex: 1;
+ overflow-y: auto;
+ min-height: 0;
+}
+/* Changelog accordion */
+.update-modal-changelog {
+ border-top: 1px solid var(--border-subtle);
+ border-bottom: 1px solid var(--border-subtle);
+}
+.update-modal-changelog-toggle {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ padding: 9px 18px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: var(--text-secondary);
+ font-size: 12px;
+ font-weight: 600;
+ text-align: left;
+ letter-spacing: 0.03em;
+ transition: color var(--transition-fast), background var(--transition-fast);
+}
+.update-modal-changelog-toggle:hover {
+ color: var(--text-primary);
+ background: var(--bg-glass);
+}
+.update-modal-changelog-body {
+ padding: 4px 18px 12px;
+}
+/* Download area */
+.update-modal-download-area {
+ padding: 14px 18px;
+}
+.update-modal-asset {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ background: var(--bg-glass);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-md);
+ padding: 8px 12px;
+}
+.update-modal-asset-name {
+ font-size: 12px;
+ color: var(--text-secondary);
+ flex: 1;
+ word-break: break-all;
+}
+.update-modal-asset-size {
+ font-size: 11px;
+ color: var(--text-muted);
+ flex-shrink: 0;
+}
+.update-modal-progress {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+}
+.update-modal-progress .app-updater-progress-bar { flex: 1; height: 4px; }
+.update-modal-dl-bytes {
+ font-size: 11px;
+ color: var(--text-muted);
+ white-space: nowrap;
+}
+.update-modal-done {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-2);
+}
+.update-modal-done-title {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--accent);
+}
+.update-modal-done-hint {
+ font-size: 12px;
+ color: var(--text-secondary);
+}
+.update-modal-folder-btn {
+ align-self: flex-start;
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-2);
+ font-size: 12px;
+}
+/* macOS Tauri Updater — idle state info block */
+.update-modal-mac-info {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-2);
+}
+.update-modal-mac-info-main {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+.update-modal-mac-info-sub {
+ font-size: 12px;
+ color: var(--text-secondary);
+ line-height: 1.45;
+}
+.update-modal-trust-badges {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-2);
+ margin-top: var(--space-1);
+}
+.update-modal-trust-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 11px;
+ color: var(--positive, var(--accent));
+ background: var(--bg-glass);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-sm);
+ padding: 3px 8px;
+}
+.update-modal-trust-badge svg {
+ flex-shrink: 0;
+}
+/* macOS Tauri Updater — done state (after install, before/during restart) */
+.update-modal-done-icon {
+ align-self: center;
+ color: var(--positive, var(--accent));
+ margin-bottom: var(--space-1);
+}
+.update-modal-done-countdown {
+ font-size: 12px;
+ color: var(--text-secondary);
+ font-variant-numeric: tabular-nums;
+}
+/* AUR hint */
+.update-modal-aur {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-2);
+}
+.update-modal-aur-title {
+ font-size: 13px;
+ color: var(--text-secondary);
+}
+.update-modal-aur-cmd {
+ display: block;
+ background: var(--bg-glass);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-sm);
+ padding: 6px 10px;
+ font-size: 12px;
+ color: var(--accent);
+ font-family: monospace;
+ user-select: all;
+}
+.update-modal-aur-alt {
+ color: var(--text-muted);
+}
+.update-modal-asset-none {
+ padding: 4px 0;
+}
+/* Footer */
+.update-modal-footer {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ padding: 12px 18px;
+ border-top: 1px solid var(--border-subtle);
+ flex-shrink: 0;
+}
+.update-modal-skip {
+ font-size: 12px;
+ color: var(--text-muted);
+ padding: var(--space-1) var(--space-2);
+}
+.update-modal-skip:hover { color: var(--text-secondary); }
+
+.update-toast {
+ margin: 0 var(--space-1) var(--space-2);
+ padding: var(--space-3) var(--space-3);
+ border-radius: var(--radius-md);
+ background: var(--accent-dim);
+ border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+ animation: update-toast-in 0.35s ease both;
+}
+
+.update-toast-header {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ color: var(--accent);
+}
+
+.update-toast-label {
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: var(--accent);
+}
+
+.update-toast-version {
+ font-size: 12px;
+ color: var(--text-secondary);
+ padding-left: 22px;
+}
+
+.update-toast-link {
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--accent);
+ text-decoration: none;
+ padding-left: 22px;
+ opacity: 0.8;
+ transition: opacity var(--transition-fast);
+}
+
+.update-toast-link:hover {
+ opacity: 1;
+ text-decoration: underline;
+}
+
+.update-toast-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: var(--space-2) 0;
+ color: var(--accent);
+ animation: update-toast-in 0.35s ease both;
+ cursor: default;
+}
+
diff --git a/src/styles/layout/utility-footer.css b/src/styles/layout/utility-footer.css
new file mode 100644
index 00000000..02e81921
--- /dev/null
+++ b/src/styles/layout/utility-footer.css
@@ -0,0 +1,191 @@
+/* ─── Utility Footer ─── */
+.mp-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-evenly;
+ padding: 8px 0 max(12px, env(safe-area-inset-bottom, 12px));
+ flex-shrink: 0;
+}
+
+.mp-footer-btn {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ background: none;
+ border: none;
+ color: var(--text-muted);
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ padding: 10px 18px;
+ border-radius: var(--radius-full);
+ transition: color var(--transition-fast), background var(--transition-fast);
+ min-height: 44px;
+}
+
+.mp-footer-btn:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+}
+
+/* ═══════════════════════════════════════════════════════════════════════════
+ MOBILE QUEUE / LYRICS DRAWER
+ ═══════════════════════════════════════════════════════════════════════════ */
+
+@keyframes mq-slide-up {
+ from { transform: translateY(100%); }
+ to { transform: translateY(0); }
+}
+
+@keyframes mq-fade-in {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+.mq-drawer-backdrop {
+ position: fixed;
+ inset: 0;
+ z-index: 500;
+ background: rgba(0, 0, 0, 0.5);
+ backdrop-filter: blur(4px);
+ animation: mq-fade-in 0.2s ease both;
+}
+
+.mq-drawer {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 90vh;
+ z-index: 501;
+ background: var(--bg-sidebar);
+ border-radius: 18px 18px 0 0;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ animation: mq-slide-up 0.3s cubic-bezier(0.22, 1, 0.36, 1) both;
+ box-shadow: 0 -8px 40px rgba(0, 0, 0, 0.4);
+}
+
+.mq-drawer-header {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ padding: 16px 20px 12px;
+ border-bottom: 1px solid var(--border-subtle);
+ flex-shrink: 0;
+}
+
+.mq-drawer-header h3 {
+ font-size: 17px;
+ font-weight: 700;
+ color: var(--text-primary);
+ margin: 0;
+}
+
+.mq-drawer-count {
+ font-size: 13px;
+ color: var(--accent);
+ flex: 1;
+}
+
+.mq-drawer-close {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ background: var(--bg-hover);
+ border: none;
+ color: var(--text-muted);
+ cursor: pointer;
+ flex-shrink: 0;
+ transition: background var(--transition-fast), color var(--transition-fast);
+}
+
+.mq-drawer-close:hover {
+ background: var(--border);
+ color: var(--text-primary);
+}
+
+.mq-drawer-list {
+ flex: 1;
+ overflow-y: auto;
+ padding: var(--space-2);
+}
+
+.mq-drawer-empty {
+ padding: var(--space-6);
+ text-align: center;
+ color: var(--text-muted);
+ font-size: 14px;
+}
+
+/* Lyrics drawer — fill entire drawer */
+.mq-drawer-lyrics .mq-drawer-list {
+ padding: 0;
+}
+
+/* ── Queue Item ── */
+.mq-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 10px 14px;
+ border-radius: var(--radius-md);
+ cursor: pointer;
+ color: var(--text-secondary);
+ transition: background var(--transition-fast);
+ min-height: 44px;
+}
+
+.mq-item:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+}
+
+.mq-item.active {
+ background: var(--accent-dim);
+ color: var(--accent);
+}
+
+.mq-item-info {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.mq-item-title {
+ font-size: 13px;
+ font-weight: 500;
+ color: inherit;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.mq-item-artist {
+ font-size: 11px;
+ color: var(--text-muted);
+}
+
+.mq-item.active .mq-item-artist {
+ color: inherit;
+ opacity: 0.7;
+}
+
+.mq-item-dur {
+ font-size: 11px;
+ color: var(--text-muted);
+ flex-shrink: 0;
+ margin-left: var(--space-3);
+}
+
+.mq-item.active .mq-item-dur {
+ color: inherit;
+ opacity: 0.7;
+}
+