feat(themes): community Theme Store + semantic-token refactor (#1009)

* feat(themes): add semantic tokens for the theme-store contract (B0 P1)

Additive: define --highlight, --accent-2, --bg-deep, --bg-elevated and
--text-on-accent on the :root base as --ctp-* mappings. They resolve
per-theme automatically and nothing consumes them yet (zero behaviour
change) — groundwork for replacing direct --ctp-* use in components.

* refactor(themes): components consume semantic tokens, not --ctp-* (B0 P2)

Replace every direct --ctp-* reference in component/layout/track CSS and
TSX inline styles with the readable semantic token (--bg-app, --accent,
--highlight, --text-on-accent, …). --ctp-* now survives only as the
Catppuccin palette layer the base maps from, and as the deliberate
categorical rainbow in Composers/Genres/artistsHelpers (left untouched).

This is the readable contract surface for the community theme store.
Divergences (theme set a semantic var != its --ctp- source) are
corrections — the element now uses the theme's real semantic colour.

* feat(themes): add player-bar title/artist color tokens

New optional --player-title / --player-artist, defaulting to --text-primary / --text-secondary so nothing changes unless a theme overrides them. Lets a theme give the now-playing readout its own colour as a plain token.

* refactor(themes): token-only theme library (flatten, whitelist, one file per theme)

Turn every built-in theme into a single self-contained [data-theme] var block of semantic whitelist tokens (plus the internal --ctp-* palette layer):

- Flatten all themes: drop structural override rules, @keyframes, and global-token overrides (radius / shadow-elevation / transition / spacing / font / focus-ring). Signature player-bar readout colours are preserved via the new --player-title / --player-artist tokens.

- Normalize the var blocks to the semantic whitelist: drop the alternate token vocabulary (nav-active / scrollbar / bg-input / success / border-default / ...); rename --success -> --positive and --border-default -> --border where no whitelist equivalent was set. Migrate the few components that read those tokens to the whitelist equivalents.

- Split multi-theme files so each theme ships as its own file, making the built-in set 1:1 with the per-theme store packaging.

Kept as-is (built-in, not flattened): the two colour-blind-safe accessibility themes, plus the two curated core skins.

* chore(themes): remove seven themes retired after the token refactor

These themes leaned on heavy structural overrides and were dropped rather than flattened. Full removal each: the CSS file(s), the index.css import, the Theme type union, and the ThemePicker entry.

* feat(themes): granular tokens — track lists

Wire track rows to per-region tokens: row hover (--row-hover), the now-playing
row + indicator (--row-playing-bg / --row-playing-text), track title/artist/
number/duration text, column-header text, row dividers, and the resize-handle
active colour. Covers the desktop tracklist, the shared song-row (Tracks hub /
search), and the mobile tracklist. Drop a baked border fallback. Visual no-op.

* feat(themes): granular tokens — cards

Wire album and artist cards to per-region tokens (--card-hover-border,
--card-title, --card-subtitle, --card-placeholder-bg). Visual no-op.

* fix(themes): drop undefined/baked colour aliases

Replace the undefined --bg-surface (resolved to nothing — broken placeholder
backgrounds and filter input) with --card-placeholder-bg / --input-bg, and the
baked-hex aliases --color-error / --color-warning with --danger / --warning so
themes can actually recolour them.

* feat(themes): Spectrum demo theme + trim unused cascade tokens

Add a loud built-in demo theme that gives each region its own hue (sidebar
green, player pink, lists cyan, cards gold, menus red, controls blue) so the
per-region granularity is obvious when you switch to it. Drop two unused
cascade tokens (--sidebar-text-active, --row-active-bg) and the unused
on-media block (those media surfaces stay static by design).

* style(themes): make Spectrum demo brutally loud

Full-saturation neon per region (toxic green sidebar, magenta player, cyan
lists, acid-yellow cards, blood-red menus, electric-blue controls, purple
scrollbar) so the per-region separation is unmistakable. The earlier soft
tints were too subtle to read.

* feat(themes): name the granular demo theme Braindead

* feat(themes): granular per-region tokens — cascade layer + sidebar

Add an optional per-region token layer (semantic-cascade.css) so a theme can
recolour individual regions — sidebar hover, player controls, list rows,
menus, inputs, on-media surfaces — independently of the global tokens. Every
token defaults to its base token (or a media-safe literal), so this is a
visual no-op until a theme overrides one; it only adds control points.

Wire the sidebar region as the first consumer and drop the baked grey
fallbacks (--bg-tertiary, etc.) that no theme could reach.

* feat(themes): granular tokens — controls, menus, scrollbar

Wire inputs, buttons, sliders, the custom-select, context menus, submenus and
modals to per-region tokens, and tokenise the scrollbar. Complete B0 by
dropping the last direct --ctp-* references in the input/button/progress/
scrollbar utility CSS.

Fix three undefined-token bugs that fell back to nothing (so no theme could
reach them): --surface-2 (context-menu hover had no highlight), --bg-surface
(submenu create-input had no background), and the baked grey fallbacks in the
custom-select. Every other new token defaults to today's value — a visual
no-op that only adds override points.

* feat(themes): granular tokens — player bar

Wire the desktop player bar's transport controls, time toggle, and overflow
menu to per-region tokens (--player-control, --player-time-toggle-*, etc.).
Fix the undefined --surface-hover/--surface-active grey fallbacks on the time
toggle. Visual no-op; defaults match today's values.

* chore(themes): remove empty theme stub files

Six theme CSS files were reduced to comment-only stubs by the flatten
sweep but their files and @import lines remained. Five are empty
structural companions of now-flattened themes (morpheus, p-dvd,
aero-glass, luna-teal) and two are orphans of cut themes
(order-of-the-phoenix, pandora). Removed the files and their imports.

* feat(themes): runtime injection foundation for the theme store

Plumbing for installed community themes ahead of the in-app store UI,
nothing user-visible yet:

- installedThemesStore: persisted (localStorage) record of installed
  community themes incl. their CSS text, so an active community theme is
  available synchronously at startup (no flash, fully offline).
- themeInjection: reconcile <head> <style data-installed-theme> elements
  with the store; lightweight defense-in-depth sanitize on top of CI.
- themeRegistry: jsDelivr registry client with a 12h localStorage cache
  and stale-on-error fallback.
- App: inject installed themes before applying data-theme, in both webviews.
- themeStore: widen the Theme type to accept dynamic installed ids.

* feat(themes): dedicated Themes settings tab

Move theme selection and the day/night scheduler out of Appearance into
a new dedicated Themes tab — the future home of the community Theme Store.
Appearance keeps grid columns, visual options, UI scale, font and seekbar.

- ThemesTab: theme picker + scheduler (relocated verbatim).
- AppearanceTab: drop the two relocated sections + now-unused imports.
- Register the tab in settingsTabs (Tab union, resolveTab, search index)
  and Settings (tab bar, render, label map).
- i18n: settings.tabThemes in all 9 locales.

* feat(themes): community Theme Store browse + install

Add the Theme Store section to the Themes tab:

- Fetch the jsDelivr registry (12h cache, stale-on-error fallback).
- Search by name/author/description + filter by light/dark + refresh.
- Per-row CDN thumbnail, name, author, description and actions:
  Install / Apply / Update / Uninstall. Installing fetches the CSS,
  persists it (localStorage) and the runtime injection applies it;
  uninstalling the active theme falls back to the matching core.
- Rating slot left reserved (deferred).
- i18n: themeStore* keys in all 9 locales.

* feat(themes): slim bundle to fixed cores + flat Themes tab

Remove the 86 store palettes (incl. braindead) from the app bundle — the
CSS files, their index.css imports, the Theme union and the picker data —
leaving only the six fixed cores (Catppuccin Mocha/Latte, Kanagawa Wave,
Stark HUD, Vision Dark/Navy). Everything else installs from the store.

Themes tab is rebuilt flat (no collapsible accordions):
- "Your Themes": one card grid of the fixed cores + installed community
  themes; click to apply, uninstall on community ones (active theme falls
  back to the matching core). Catppuccin prefix on Mocha/Latte; a CVD-safe
  pill on the colour-blind-safe Vision themes.
- Scheduler day/night options include installed themes.
- Theme Store: alphabetical order, thumbnail lightbox, and a submit hint
  above the search linking to the themes repository.
- Nav order: Servers, Library, Audio, Themes, Appearance, Lyrics, …
- ThemePicker accordion removed; fixed-theme data moved to fixedThemes.ts.
- i18n for all new strings across 9 locales.

* feat(themes): reset removed-from-bundle themes to a bundled fallback

After slimming the bundle to the six fixed core themes, a profile upgraded
from an older build may have an active or scheduler theme that is now
store-only and not installed — it has no [data-theme] block and would render
as unstyled :root. Reset any theme/themeDay/themeNight that is neither bundled
nor installed to a bundled fallback: Mocha for the main + night slots, Latte
for the day slot. Runs synchronously in runPreReactBootstrap, rewriting the
persisted selection in localStorage before React mounts (no flash; Zustand
rehydrates after first paint). No auto-install and no network — the fallback
is always a bundled theme, so it works offline.

* feat(themes): floating back-to-top button on the Themes tab

The Themes tab can get long (theme grid + scheduler + full store list), so
add a floating back-to-top affordance that appears once the page is scrolled
and smooth-scrolls to the top. It is portalled into the route host and
positioned absolute against it — the main scroll viewport sets contain: paint,
which would otherwise make position: fixed resolve against the scrolling box
and drift with the content. Reusable component (scroll viewport id + threshold
props); i18n common.backToTop added in all nine locales.

* feat(themes): accessibility + state polish for the Theme Store

- Reuse the shared CoverLightbox for the thumbnail preview instead of a
  second inline dialog — gains a visible close button and a focus-managed,
  portalled dialog, and drops duplicated markup.
- Theme cards expose aria-pressed so assistive tech announces the selected
  theme, not just the visual check.
- Transient store messages get live-region roles (loading/empty/install
  failure = status, fetch error = alert).
- Thumbnails degrade gracefully when offline/missing (hide the broken-image
  glyph; the thumbnail button no longer stretches with the row, so its
  background can't show as letterbox bars).

* feat(themes): larger store-row thumbnails (120x75 -> 200x125)

The list previews were too small to make a theme out; bump the display size
(same 1.6 aspect). Thumbnails are now served at 720x450, so the larger
display stays crisp.

* fix(themes): bust thumbnail cache on registry change

jsDelivr serves theme thumbnails with a 7-day max-age, so when a thumbnail
is updated the webview keeps showing its cached old image (the path is
unchanged). Append the registry's generatedAt as a cache-busting query to
the thumbnail URLs (list + lightbox); it changes on every themes push, so a
registry refresh makes the webview re-fetch and reflect the current CDN
image instead of a stale one.

* docs(themes): changelog + credits for the Theme Store

Add the 1.48.0 "Themes — community Theme Store" changelog entry (PR #1009)
and the matching line in the Psychotoxical credits.

* fix(themes): address PR review (uninstall hygiene, validation, polish)

Uninstall/scheduler & validation:
- uninstallTheme() repairs every selection slot (active + day + night), not
  just the manual one, and is shared by both uninstall buttons (dedup).
- Validate theme CSS at install time and skip persisting CSS that won't inject
  (no more "installed/active but renders nothing" with no feedback).
- Harden the runtime validator: exactly one rule, scoped exactly to the
  theme's [data-theme='<id>'] selector (no unscoped/foreign selectors), no
  at-rules, url() only data:, no expression()/javascript:, size-capped.

Tokens & polish:
- Fix three dangling undefined tokens (--surface-2 x2, --bg-surface).
- Finish the warning/success token sweep (--warning / --positive, themeable).
- Apply the active theme synchronously before React mounts (no first-frame
  flash) and inject installed themes up front.
- One-time, dismissible notice when the slim-bundle migration reset a theme.
- Update badge uses semver, not string inequality.
- Offline/stale indicator in the store; cross-window theme sync; drop the now
  dead REMOVED_THEME_REMAP and Card.mode field.

Tests: themeInjection (validator + sync), themeRegistry (cache/force/stale/
malformed), uninstallTheme (slot repair), migration notice. i18n in all nine
locales. Full suite green (1755 tests).

* test(bootstrap): cover startup theme apply + cross-window sync

The review fixes added applyThemeAtStartup / installCrossWindowThemeSync to
bootstrap.ts (a hot-path file) without tests, dropping its coverage to 68.3%
and failing the frontend hot-path coverage gate (>=70%). Add unit tests for
both (and the no-op / malformed-storage paths); bootstrap.ts is back to ~98%.
This commit is contained in:
Psychotoxical
2026-06-07 02:47:33 +02:00
committed by GitHub
parent 03a1ba9582
commit f9df918c72
221 changed files with 2470 additions and 15759 deletions
+13 -13
View File
@@ -6,7 +6,7 @@
gap: var(--space-6);
padding: 0 var(--space-5);
background: var(--bg-player);
border-top: 1px solid var(--border-subtle);
border-top: 1px solid var(--player-border);
height: var(--player-height);
position: relative;
z-index: 100;
@@ -160,12 +160,12 @@ html[data-platform="windows"] .player-bar.floating {
.player-track-name {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
color: var(--player-title);
}
.player-track-artist {
font-size: 12px;
color: var(--text-secondary);
color: var(--player-artist);
margin-top: 2px;
}
@@ -227,14 +227,14 @@ html[data-platform="windows"] .player-bar.floating {
width: 36px;
height: 36px;
border-radius: 50%;
color: var(--text-muted);
color: var(--player-control);
transition: all var(--transition-fast);
flex-shrink: 0;
}
.player-btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
background: var(--player-control-hover-bg);
color: var(--player-control-hover);
transform: scale(1.12);
}
@@ -246,7 +246,7 @@ html[data-platform="windows"] .player-bar.floating {
/* Star + Last.fm heart — visually separated from track title */
.player-star-btn {
margin: var(--space-1);
color: var(--text-muted);
color: var(--player-control);
transition: color var(--transition-fast);
}
@@ -259,7 +259,7 @@ html[data-platform="windows"] .player-bar.floating {
width: 46px;
height: 46px;
background: var(--accent);
color: var(--ctp-crust);
color: var(--text-on-accent);
border-radius: 50%;
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.4);
flex-shrink: 0;
@@ -267,7 +267,7 @@ html[data-platform="windows"] .player-bar.floating {
.player-btn-primary:hover {
background: var(--accent);
color: var(--ctp-crust);
color: var(--text-on-accent);
transform: scale(1.06);
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4), var(--shadow-glow);
filter: brightness(1.12);
@@ -337,12 +337,12 @@ html[data-platform="windows"] .player-bar.floating {
}
.player-time-toggle:hover {
background-color: var(--surface-hover, rgba(128, 128, 128, 0.2));
background-color: var(--player-time-toggle-hover);
color: var(--text-primary);
}
.player-time-toggle:active {
background-color: var(--surface-active, rgba(128, 128, 128, 0.3));
background-color: var(--player-time-toggle-active);
}
/* Volume section */
@@ -384,7 +384,7 @@ html[data-platform="windows"] .player-bar.floating {
}
.player-eq-btn {
color: var(--text-muted);
color: var(--player-control);
flex-shrink: 0;
transition: color 0.15s;
}
@@ -406,7 +406,7 @@ html[data-platform="windows"] .player-bar.floating {
padding: 10px;
border-radius: var(--radius-md);
border: 1px solid var(--border);
background: var(--bg-card);
background: var(--menu-bg);
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
z-index: 240;
display: flex;
+3 -3
View File
@@ -160,7 +160,7 @@
cursor: default;
}
.queue-round-btn.active {
color: var(--ctp-base);
color: var(--bg-app);
background: var(--accent);
}
@@ -185,7 +185,7 @@
flex-shrink: 0;
border-radius: var(--radius-md);
overflow: hidden;
background: var(--bg-surface);
background: var(--card-placeholder-bg);
display: flex;
align-items: center;
justify-content: center;
@@ -248,7 +248,7 @@
font-size: 9px;
font-family: monospace;
letter-spacing: 0.05em;
background: var(--ctp-surface1);
background: var(--bg-hover);
color: var(--accent);
padding: 3px var(--space-4);
white-space: nowrap;
+1 -1
View File
@@ -19,7 +19,7 @@
right: 0;
height: 4px;
border-radius: 2px;
background: var(--ctp-surface2, rgba(255,255,255,0.12));
background: var(--bg-elevated, rgba(255,255,255,0.12));
}
.mp-scrubber-fill {
@@ -1,8 +1,8 @@
/* ─── 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);
background: color-mix(in srgb, var(--positive, #4ade80) 10%, var(--bg-sidebar));
border-color: color-mix(in srgb, var(--positive, #4ade80) 25%, transparent);
color: var(--positive, #4ade80);
}
@keyframes spin-slow {
@@ -33,13 +33,13 @@
}
.sidebar-playlists-toggle:hover {
background: var(--bg-hover);
background: var(--sidebar-item-hover);
color: var(--text-primary);
}
.sidebar-playlists-toggle.expanded {
transform: rotate(90deg);
color: var(--accent);
color: var(--sidebar-item-active-text);
}
.sidebar-playlists-section {
@@ -54,7 +54,7 @@
width: 100%;
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-md);
color: var(--text-secondary);
color: var(--sidebar-text);
font-size: 14px;
font-weight: 500;
transition: background var(--transition-fast), color var(--transition-fast);
@@ -65,13 +65,13 @@
}
.sidebar-playlists-header:hover {
background: var(--bg-hover);
background: var(--sidebar-item-hover);
color: var(--text-primary);
}
.sidebar-playlists-header.active {
background: var(--accent-dim);
color: var(--accent);
background: var(--sidebar-item-active-bg);
color: var(--sidebar-item-active-text);
}
.sidebar-playlists-chevron {
@@ -99,7 +99,7 @@
gap: var(--space-2);
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-md);
color: var(--text-secondary);
color: var(--sidebar-text);
font-size: 13px;
font-weight: 400;
transition: background var(--transition-fast), color var(--transition-fast);
@@ -115,13 +115,13 @@
}
.sidebar-playlist-item:hover {
background: var(--bg-hover);
background: var(--sidebar-item-hover);
color: var(--text-primary);
}
.sidebar-playlist-item.active {
background: var(--accent-dim);
color: var(--accent);
background: var(--sidebar-item-active-bg);
color: var(--sidebar-item-active-text);
}
.sidebar-playlists-loading {
+32 -32
View File
@@ -4,7 +4,7 @@
display: flex;
flex-direction: column;
background: var(--bg-sidebar);
border-right: 1px solid var(--border-subtle);
border-right: 1px solid var(--sidebar-border);
position: relative;
z-index: 2;
min-height: 0; /* allow 1fr row to shrink freely */
@@ -17,7 +17,7 @@
padding: 0 var(--space-5);
height: 64px;
box-sizing: border-box;
border-bottom: 1px solid var(--border-subtle);
border-bottom: 1px solid var(--sidebar-border);
}
.sidebar-brand .logo-icon {
@@ -31,7 +31,7 @@
font-size: 20px;
font-weight: 700;
letter-spacing: -0.02em;
background: linear-gradient(135deg, var(--accent), var(--ctp-blue));
background: linear-gradient(135deg, var(--accent), var(--accent-2));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
@@ -65,19 +65,19 @@
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: 1px solid var(--sidebar-border);
border-radius: var(--radius-md);
background: var(--bg-tertiary, rgba(0, 0, 0, 0.2));
color: var(--text-secondary);
background: var(--sidebar-trigger-bg);
color: var(--sidebar-text);
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);
background: var(--sidebar-item-hover);
color: var(--text-primary);
border-color: var(--border-default, rgba(255, 255, 255, 0.12));
border-color: var(--border);
}
/* «Все библиотеки»: как секция навигации, без рамки и подложки */
@@ -96,12 +96,12 @@
}
.nav-library-scope-trigger--open:not(.nav-library-scope-trigger--plain) {
border-color: var(--accent, var(--border-default));
border-color: var(--accent, var(--border));
}
.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));
border-color: var(--sidebar-border);
background: var(--sidebar-trigger-bg);
}
.nav-library-scope-icon {
@@ -153,9 +153,9 @@
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));
border: 1px solid var(--border-dropdown);
background: var(--menu-bg);
box-shadow: 0 8px 24px var(--shadow-dropdown);
box-sizing: border-box;
overflow-y: visible;
}
@@ -190,11 +190,11 @@
}
.nav-library-dropdown-item:hover {
background: var(--bg-hover);
background: var(--menu-item-hover);
}
.nav-library-dropdown-item--selected {
color: var(--accent);
color: var(--menu-item-active-text);
}
.nav-library-dropdown-item-label {
@@ -266,9 +266,9 @@
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);
color: var(--danger);
background: color-mix(in srgb, var(--danger) 18%, var(--bg-card));
border: 1px solid color-mix(in srgb, var(--danger) 45%, transparent);
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
}
@@ -278,7 +278,7 @@
gap: var(--space-3);
padding: var(--space-3) var(--space-3);
border-radius: var(--radius-md);
color: var(--text-secondary);
color: var(--sidebar-text);
font-size: 14px;
font-weight: 500;
transition: background var(--transition-fast), color var(--transition-fast);
@@ -288,13 +288,13 @@
}
.nav-link:hover {
background: var(--bg-hover);
background: var(--sidebar-item-hover);
color: var(--text-primary);
}
.nav-link.active {
background: var(--accent-dim);
color: var(--accent);
background: var(--sidebar-item-active-bg);
color: var(--sidebar-item-active-text);
}
.nav-link.active::before {
@@ -305,7 +305,7 @@
transform: translateY(-50%);
width: 3px;
height: 60%;
background: var(--accent);
background: var(--sidebar-item-active-text);
border-radius: 0 var(--radius-full) var(--radius-full) 0;
}
@@ -323,16 +323,16 @@
/* Now Playing nav link — subtly accented but not permanently "active"-looking */
.nav-link-nowplaying {
color: var(--accent);
color: var(--sidebar-item-active-text);
font-weight: 600;
}
.nav-link-nowplaying:hover {
background: var(--bg-hover);
color: var(--accent);
background: var(--sidebar-item-hover);
color: var(--sidebar-item-active-text);
}
.nav-link-nowplaying.active {
background: var(--accent-dim);
color: var(--accent);
background: var(--sidebar-item-active-bg);
color: var(--sidebar-item-active-text);
}
.nav-link-nowplaying svg {
opacity: 1;
@@ -366,7 +366,7 @@
padding: 0 5px;
border-radius: 999px;
background: var(--accent);
color: var(--ctp-crust, #11111b);
color: var(--text-on-accent);
font-size: 10px;
font-weight: 700;
line-height: 18px;
@@ -406,7 +406,7 @@
width: 22px;
height: 22px;
border-radius: 50%;
border: 1px solid var(--border-subtle);
border: 1px solid var(--sidebar-border);
background: var(--bg-card);
color: var(--text-muted);
cursor: pointer;
@@ -418,7 +418,7 @@
}
.collapse-btn:hover {
background: var(--bg-hover);
background: var(--sidebar-item-hover);
color: var(--text-primary);
box-shadow: var(--shadow-md);
}
+2 -2
View File
@@ -37,12 +37,12 @@
width: 64px;
height: 64px;
background: var(--accent);
color: var(--ctp-crust);
color: var(--text-on-accent);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
}
.mp-ctrl-play:hover {
color: var(--ctp-crust);
color: var(--text-on-accent);
filter: brightness(1.1);
transform: scale(1.06);
box-shadow: 0 6px 28px rgba(0, 0, 0, 0.4), var(--shadow-glow);