diff --git a/CLAUDE.md b/CLAUDE.md index 00a8cc50..81cd376c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,6 +14,7 @@ This file is an **index, not a manual**. It carries the map plus the one-line fo | How a feature actually works | [`docs/features/`](docs/README.md#features) | | The visual direction (tokens, shapes) | [`docs/design/fluid-editorial.md`](docs/design/fluid-editorial.md) | | The second direction, Liquid's glass | [`docs/design/liquid-aurora-glass.md`](docs/design/liquid-aurora-glass.md) | +| The third material, Pulse's neon | [`docs/design/pulse-neon-deck.md`](docs/design/pulse-neon-deck.md) | | Crate split, audio topology, DB layout | [`docs/architecture/`](docs/README.md#architecture) | | Design decisions not yet built | [`docs/rfcs/`](docs/README.md#rfcs) | | Release / packaging procedure | [`docs/RELEASING.md`](docs/RELEASING.md) | @@ -48,7 +49,7 @@ React 19 + TypeScript. Entry: `src/main.tsx` → `src/App.tsx`. - **Hooks** wrap each context: `useTheme`, `usePlayer`, `useLibrary`, `usePlaylist`, `useProfile`, `usePageScroll`. The playback position is the exception: it ticks at 4 Hz, so it lives outside `PlayerContext` in [`usePlayerPosition`](src/hooks/usePlayerPosition.ts) ([why](docs/architecture/invariants.md#events)). - **Tauri wrappers** (`src/lib/tauri/`): one typed `invoke()` per backend command. - **Views**: `HomeView`, `LibraryView`, `PlaylistView`, `AlbumDetailView`, `ArtistDetailView`, `LikedView`, `HistoryView`, `StatisticsView`, `WrappedView`, `SettingsView`, … -- **Layout**: a skin preset declares its `layout` (`classic` | `fluid`, the 2.0 interface) and the views branch on `skin.layout`, never on a skin id; the fluid layout's structure is [`fluid.css`](src/styles/skins/fluid.css), keyed on `data-layout`, and Liquid paints its glass over it. Classic: Apple-Music-style sidebar, TopBar with search, PlayerBar at the bottom, right-edge panels (`NowPlayingPanel` / `QueuePanel` / `LyricsPanel`) mutex'd via `PlayerContext`. A second `WebviewWindow` (label `mini`, `?mini=1`) ships the always-on-top mini-player — [`docs/features/ui.md`](docs/features/ui.md#mini-player) — and a third (label `lyrics`, `?lyrics=1`, created by the backend) the transparent desktop lyrics overlay — [`#desktop-lyrics`](docs/features/ui.md#desktop-lyrics). +- **Layout**: a skin preset declares its `layout` (`classic` | `fluid`, the 2.0 interface) and the views branch on `skin.layout`, never on a skin id; the fluid layout's structure is [`fluid.css`](src/styles/skins/fluid.css), keyed on `data-layout`, and Liquid paints its glass over it, Pulse its neon. Classic: Apple-Music-style sidebar, TopBar with search, PlayerBar at the bottom, right-edge panels (`NowPlayingPanel` / `QueuePanel` / `LyricsPanel`) mutex'd via `PlayerContext`. A second `WebviewWindow` (label `mini`, `?mini=1`) ships the always-on-top mini-player — [`docs/features/ui.md`](docs/features/ui.md#mini-player) — and a third (label `lyrics`, `?lyrics=1`, created by the backend) the transparent desktop lyrics overlay — [`#desktop-lyrics`](docs/features/ui.md#desktop-lyrics). ### Backend (`src-tauri/`) — Cargo workspace, two members diff --git a/docs/README.md b/docs/README.md index cbcb42ae..d332da0a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -28,10 +28,11 @@ User-facing references and per-feature deep dives. The top-level [README](../REA ## Design -| Doc | Scope | -| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [Fluid Editorial](design/fluid-editorial.md) | The visual direction since 2.0: principles, colour / type / shape / motion tokens, the wave mark, every component's shape, and how to carry it to the web app, the Android app and the landing site | -| [Liquid — Aurora Glass](design/liquid-aurora-glass.md) | The second direction on the same interface: music as light and material — pastel pools tinted by the cover, four glass levels, separation by light, states as light, the mood capsules and the light progress | +| Doc | Scope | +| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Fluid Editorial](design/fluid-editorial.md) | The visual direction since 2.0: principles, colour / type / shape / motion tokens, the wave mark, every component's shape, and how to carry it to the web app, the Android app and the landing site | +| [Liquid — Aurora Glass](design/liquid-aurora-glass.md) | The second direction on the same interface: music as light and material — pastel pools tinted by the cover, four glass levels, separation by light, states as light, the mood capsules and the light progress | +| [Pulse — Neon Deck](design/pulse-neon-deck.md) | The third material on the same interface: a club control panel — OLED black, the chrome drawn as hairlines, the theme's accent as neon on whatever is live, the tempo deck, the floating pill of a player bar, the spring | ## RFCs diff --git a/docs/design/fluid-editorial.md b/docs/design/fluid-editorial.md index 5261b4ea..7ed19e21 100644 --- a/docs/design/fluid-editorial.md +++ b/docs/design/fluid-editorial.md @@ -106,7 +106,7 @@ The structure above — the canvas, the band, the spectrum, the rail, the contro | rails and bars are flush | the rail, the bar and the panel float: inset `0.75–1 rem` from the window, `1.5–1.75 rem` corners | | a hover is a tone | a hover is a tone, and a press is instant — `scale(0.94–0.97)` on pointer-down, before anything else | -What does not change between the two: the base type system (the serif italic line excepted, as the row above says), the mark, the accent rule, the rhythm, the ease. A third material (a paper, a neon) would be written the same way — the tokens redeclared, the few surface rules added, nothing moved. +What does not change between the two: the base type system (the serif italic line excepted, as the row above says), the mark, the accent rule, the rhythm, the ease. A third material is written the same way — the tokens redeclared, the few surface rules added, nothing moved — and the neon one exists: [Pulse — Neon Deck](./pulse-neon-deck.md), OLED black, the chrome as hairlines, the accent lit on whatever is live, its own spring in place of the ease. ## Carrying it to another surface diff --git a/docs/design/liquid-aurora-glass.md b/docs/design/liquid-aurora-glass.md index c35341e4..88f4c0ae 100644 --- a/docs/design/liquid-aurora-glass.md +++ b/docs/design/liquid-aurora-glass.md @@ -1,6 +1,6 @@ # Liquid — Aurora Glass -The second direction of WaveFlow 2.0, beside [Fluid Editorial](./fluid-editorial.md). Both sit on the same interface — the canvas, the rail, the control rail, the tabbed panel, the page heroes — and both are written down here so the web app, the Android app and the landing site can carry them. The desktop implementation is the `liquid` skin ([`src/styles/skins/liquid.css`](../../src/styles/skins/liquid.css) over [`fluid.css`](../../src/styles/skins/fluid.css), [`src/lib/skins.ts`](../../src/lib/skins.ts)); this page is the intent and the values. +The second direction of WaveFlow 2.0, beside [Fluid Editorial](./fluid-editorial.md) (and, since, [Pulse — Neon Deck](./pulse-neon-deck.md)). Both sit on the same interface — the canvas, the rail, the control rail, the tabbed panel, the page heroes — and both are written down here so the web app, the Android app and the landing site can carry them. The desktop implementation is the `liquid` skin ([`src/styles/skins/liquid.css`](../../src/styles/skins/liquid.css) over [`fluid.css`](../../src/styles/skins/fluid.css), [`src/lib/skins.ts`](../../src/lib/skins.ts)); this page is the intent and the values. The one-line brief: **music as light and material — a luminous, airy glass, tinted by what is playing, with a light that answers the hand.** diff --git a/docs/design/pulse-neon-deck.md b/docs/design/pulse-neon-deck.md new file mode 100644 index 00000000..47b224eb --- /dev/null +++ b/docs/design/pulse-neon-deck.md @@ -0,0 +1,69 @@ +# Pulse — Neon Deck + +The third material on the 2.0 interface, beside [Fluid Editorial](./fluid-editorial.md) and [Liquid — Aurora Glass](./liquid-aurora-glass.md). All three sit on the same structure — the canvas, the rail, the control rail, the tabbed panel, the page heroes — and this page writes down what Pulse makes of it, so the web app, the Android app and the landing site can carry it. The desktop implementation is the `pulse` skin ([`src/styles/skins/pulse.css`](../../src/styles/skins/pulse.css) over [`fluid.css`](../../src/styles/skins/fluid.css), [`src/lib/skins.ts`](../../src/lib/skins.ts)); this page is the intent and the values. + +The one-line brief: **a club control panel — OLED black, the chrome drawn as hairlines, and the accent as neon on whatever is live.** + +Where Fluid stages the music and Liquid diffuses it, Pulse wires it: nothing is filled, everything is outlined, and the thing in use lights up. It is the 1.x Pulse's "OLED minimalism with neon edge lighting" carried onto the 2.0 structure — the floating pill of a player bar, the vinyl that turns, the `///` marker, the spring — with one change of principle: the neon is no longer its own. + +## Principles + +1. **One neon, the theme's.** The 1.x Pulse owned two hard-coded neons (cyan for hover, magenta for active). The 2.0 Pulse takes the theme's accent — the picked colour, or the cover's, through the same accent modes as every other skin — for every live state: hover, open, playing, focus. The second light is the record's own (`glow`), in the atmosphere and under the artwork and the player. Two colours, as before; both the music's. +2. **The chrome reads as absent.** The rail and the panel are the page's black with a 1 px hairline for their edge. A module on them is transparent and ringed; a card on the page is a step of black and ringed; nothing has a fill at rest. +3. **Live means lit.** A hovered thing takes the accent's ring and a halo outside it, and lifts a pixel. An open or playing thing is filled 7 % with the accent, ringed in it, with a halo outside and a little light inside — the one place a fill is allowed. A focused thing takes the ring and the halo, without the fill. +4. **OLED black, not near-black.** The page is `#000`. The two pools of light at its corners and the hairlines are what keep it from reading as a void. No grain, no scanlines: the black is the texture. +5. **Two faces.** Space Grotesk for the display and the body, at the layout's settled weights; Space Mono for the utility voice — labels and kickers with the `///` marker in the accent, time codes, the search, the inputs, the audio facts, and the greeting line, which Fluid sets in a serif italic. +6. **The spring.** One easing, `cubic-bezier(0.34, 1.56, 0.64, 1)`: a slight overshoot that lands as a snap — the rise on the Home, the lift on a hover, the deck opening a segment. Reduced motion puts the layout's ease-out back. +7. **Blur on what never scrolls only.** The floating player bar blurs the page running under it; the rail and the panel sit over the fixed pools, which do not move, so a translucent black is enough for them. + +## Tokens + +| Token | Value | Role | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | +| `bg` | `#000000` | The page. | +| `surface` | `#0a0a0c` · `#121216` (second step) · `#050507` (what scrolls under a sticky header) | Cards, menus, dialogs, the table header. | +| `chrome` | black at 55 % | The rail and the panel, over the fixed pools. Solid `surface` in the fallbacks. | +| `bar` | `rgb(6 6 8)` at 74 %, blurred 20 px | The floating player bar. Solid in the fallbacks. | +| `ink` | `#f4f4f5` · `#a9a9b2` · `#7a7a85` | Title · body · label. Cool, not warm: Fluid's inks are warm. | +| `hairline` | white 10 % · 18 % (strong) | The edge of everything: rails, rings, dividers. | +| `accent` | the theme's 400 stop, black ink | The neon. | +| `glow` | sampled from the cover; default `#4a1f5a` | The second pool, the halo under the artwork and the player bar, the pool at the record's end of it. | +| `ring` | `inset 0 0 0 1px hairline` · `inset 0 0 0 1px accent` · the accent at 55 % for hover | A surface's edge, a live surface's edge. | +| `halo` | `0 0 20px -6px` accent 55 % (hover) · `0 0 28px -4px` accent 70 % + `inset 0 0 14px -8px` accent 45 % (open) · `0 0 28px -6px` accent 75 % (CTA) | The light outside a live surface. | +| `fill` | accent at 7 % (open, playing) · 11 % (a playing row hovered) | The one fill. | +| `neon line` | `drop-shadow(0 0 4px accent 80 %)` | On the mark: the flow line, the wave, the chosen mood's wave, the tab's underline, the brand's mark. | +| `ease` | `cubic-bezier(0.34, 1.56, 0.64, 1)`, the layout's ease-out under reduced motion | Every transition. | +| `font` / `mono` | Space Grotesk (400–700, the layout's weights map onto 500 and 600) / Space Mono (400, 700) | Display and body / the utility voice. | + +The atmosphere: two static pools at the corners of the window — the accent at 14 % top-left, the glow at 55 % bottom-right — fading over 900 ms when the record changes. Under the canvas text and the suggestion band, the layout's pool of the glow stays (at 0.45 and 0.35) but its blurred copy of the cover goes: on pure black a pale sleeve reads as a grey smudge. + +### States + +| State | Treatment | +| ------- | --------------------------------------------------------------------------------------------- | +| idle | transparent (a module) or `surface` (a card), the hairline ring | +| hover | the accent's ring at 55 %, the hover halo, 1 px lift (2 px for a card); the accent as ink | +| active | `scale(0.94–0.97)` on pointer-down | +| open | the 7 % fill, the accent's ring, the open halo; the icon chip filled 14 % | +| focused | the accent's ring and the hover halo, no fill | +| playing | the row filled 7 % with the accent's edge and its light inset; the record's thumbnail turning | + +## Components + +- **Rail**: the chrome black with a hairline on its right; the wordmark as a tracked capital label with the mark lit; the profile a ringed capsule, its avatar in the two lights; the navigation modules two to a row, ringed, the icon in a ringed disc, the label in the mono voice; the open module filled and lit; playlists as rows, the open one filled with the accent's edge glowing. +- **Control rail**: transparent; the navigation squares ringed and lit on hover; the search a black field with a hairline, the mono voice inside, the accent's ring and halo on focus with the cover's light under it; theme switch and profile on one ringed capsule. +- **Canvas** (Home): the layout's grid; the greeting in Space Mono; the kicker with the `///` marker; the hero cover ringed, on the record's light; the CTA the accent filled and lit; the strip of recent records ringed, the current one ringed in the accent. +- **Mood Radio — the tempo deck**: Fluid's band, dimmed to 42 % and saturated under a black scrim; the segments parted by hairlines; the chosen one filled 7 %, ringed in the accent and lit inside, its wave in the accent as a lit tube (the others' in white, softly lit); the start pill the accent filled. Not Liquid's capsules: the moods stay one band whose axis is tempo — a deck, not five objects. +- **Player bar — the floating pill**: inset 1.25 rem from the sides and 0.75 rem from the foot, 1.75 rem corners, the chrome blurred; the cover's light pooled at the record's end, the glow's halo under it; the wave along its top as the neon tube with a lit thumb; the record a disc that turns while the music plays; the transport in ink, the accent on hover, a snap on press; the play button the accent filled and lit; the audio facts in the mono voice, following the pill's corners. +- **Side panel**: the chrome black with a hairline on its left and the cover's light from its top; the tabs in the mono voice, capitals, the open one's underline lit; Details with the layout's cover edge to edge, the pills ringed; Lyrics with the sung line carrying the accent's light; Queue with the playing row filled and lit. +- **Cards and tables**: cards `surface` with a hairline ring, the accent's ring and halo on hover; the folder tile's icon in a ringed disc, in the accent; the table's playing row filled and lit, the hover from the accent at 8 %. +- **Menus, popovers, dialogs**: `surface`, 1–1.25 rem corners, a strong hairline ring, a faint halo of the accent, the deep shadow; a row takes the accent's fill and ink on hover. +- **Immersive**: shared with Fluid, unchanged here. + +## What not to do + +Nothing runs forever except the vinyl, and only while the music plays (off under reduced motion). No second neon of Pulse's own: the hover and the open state are the same colour at different strengths. No fill at rest, no fill on hover — a fill is a state. No grain, no scanlines, no gradient on a surface (the player bar's pool of the cover's light and the panel's are the two exceptions, and they are the record's). No glass on the page: a card is black with an edge, never a sheet. The marker `///` opens a block; it never decorates a title or a button. + +## Carrying it to another surface + +The tokens map to CSS variables as Fluid's do: the three surfaces, the hairline, the accent, the glow, the ring and halo recipes. The two faces ship as webfonts (Space Grotesk 400–700, Space Mono 400 and 700). The neon line is one `drop-shadow` on the mark. On Android, the halos are a coloured elevation shadow and the rings a 1 dp stroke; the vinyl is a rotation tied to the playback state, as the desktop's `data-is-playing` mark is. The mark, the type scale and the rhythm come from [Fluid Editorial](./fluid-editorial.md#the-mark) unchanged. diff --git a/docs/features/ui.md b/docs/features/ui.md index a58e1294..1d9c8576 100644 --- a/docs/features/ui.md +++ b/docs/features/ui.md @@ -546,21 +546,21 @@ Skins are an **orthogonal axis to the 14 colour themes**: a skin re-skins surfac -| Skin | Direction | Signature | -| ----------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `studio` | Apple Music baseline | Inter / system-ui, soft shadows, rounded-12 — the calm baseline, and the skin safe mode and the crash marker fall back to (`RECOVERY_SKIN_ID`) | -| `editorial` | Broadsheet "WaveFlow Gazette" | Playfair Display 900 + Lora body, `` with locale-aware `Intl.DateTimeFormat`, `::first-letter` drop caps, halftone N&B covers via `mix-blend-luminosity`, ruler-tick progress bar via `repeating-linear-gradient`, `counter-reset` page numbering ("P. 1", "P. 2") | -| `lounge` | "Listening Room" warm-burgundy glass | Inter, [`SkinAmbientBackdrop`](../../src/components/layout/SkinAmbientBackdrop.tsx) blurs the cover (`filter: blur(140px) saturate(280%)`) into the body, white-overlay tint over the cover dominates the chrome | -| `pulse` | OLED "club control panel" | Space Grotesk + Space Mono, dual-neon magenta/cyan aurora blobs, `///` mono pills, vinyl-spin cover via [`SkinPlayingState`](../../src/components/layout/SkinPlayingState.tsx) mirroring `usePlayer().isPlaying` to `[data-is-playing]`, floating pill PlayerBar | -| `liquid` | Aurora Glass — music as light | Its own direction, written in [`docs/design/liquid-aurora-glass.md`](../design/liquid-aurora-glass.md), on the same `layout: "fluid"` as Fluid. `liquid.css` redeclares the `--fe-*` tokens and adds the material: three pools of pastel light (lavender, ice, mint, each mixed with the cover's `--fe-glow`) always behind the page; four glass levels as semantic tokens (`--glass-canvas` / `--glass-structural` / `--glass-elevated` / `--glass-floating`: tint, edge, shadow; only structural and floating blur, because they never scroll) with the rule that two sheets of one level never stack; a sheet reads as glass through its edge — rim, inner reflection, foot shade, contour, a reflection along the top, the opal (`--glass-ambient-tint`) — rather than its blur; states as light — hover catches a sheen, focus charges the sheet with the accent once (`--glass-focus-ring`), the playing row carries the accent's edge and its light, the active module is luminous rather than filled; nothing loops but the aurora's drift and the chosen mood capsule's wave. Two pieces are its own through the preset's `fluid` field: the progress as a luminous line on the player plate (`fluid.progress: "inline"`) and Mood Radio as five capsules of tinted glass ([`MoodCapsules`](../../src/components/views/home/MoodCapsules.tsx), `fluid.moodRadio: "capsules"`) with an inner light wave each. **Theme-aware**: the light theme is the direction's home, the dark theme the same material at dusk. Opaque sheets under `prefers-reduced-transparency`, the WebKitGTK safeguard and high contrast | -| `fluid` | Cinematic editorial, music first | **The default since 2.0** (`DEFAULT_SKIN_ID`; first in the picker, and the picker is the first card of Appearance). DM Sans Variable + Playfair italic on the greeting only; depth by tone (no borders); the playing cover's light behind the Home ([`useCoverGlow`](../../src/components/views/home/fluid/useCoverGlow.ts) → `--fe-glow`, the accent stays `--accent-400` so the #895 accent modes drive it); the flow-line mark under section titles; a **different Home** ([`FluidHome`](../../src/components/views/home/fluid/FluidHome.tsx): listening canvas, the suggestion of the day as a band, [`MoodSpectrum`](../../src/components/views/home/MoodSpectrum.tsx) — Mood Radio as a tempo band, editorial mosaic, shelf, history list) and the progress as the player bar's top edge (`ProgressBar variant="edge"`); the three right-edge panels as the tabs of one shell ([`FluidSidePanel`](../../src/components/layout/fluid/FluidSidePanel.tsx): Details with the cover edge to edge and the identity over it, the album it plays from, radio and artist as pills and a glimpse of the lyrics; Lyrics with the sung line sharp and the others fading with their distance; Queue); the immersive view in the same voice (the display face for the title, text tabs, the wave for the progress, the lyrics fading with their distance from the sung line) — the one place where `data-immersive`, which otherwise shields the view from every skin, is read on purpose; the sidebar as a navigation rail (the wordmark, the profile as a small panel with the library's size under the name, the open row on a tone with the accent's edge, the liked / recently played pair as two tiles, the playlists as rows with a larger cover) through the `data-sidebar-*` and `data-nav-item` landmarks the view and [`NavItem`](../../src/components/common/NavItem.tsx) carry; the top bar as a control rail (transparent, the navigation squares as one quiet cluster, the search shorter and on the bar's own middle with the accent and the cover's light on focus, the theme switch and the profile as one cluster, the popovers in the panel tone) through the `data-topbar-*` landmarks on [`TopBar`](../../src/components/layout/TopBar.tsx); every other page keeps its markup and gets the same header family from the stylesheet — kicker, display title, one meta line, a pill CTA and ghost secondaries — through a `data-page-hero` attribute on the header of the library, album, artist, playlist, genre, year, liked and history views. Dark-only like Lounge / Pulse. Its stylesheet is keyed on `data-layout="fluid"`, not on the skin id: it is the **structure** of the 2.0 interface for every skin whose preset says so (Liquid today), with Fluid's own material as the default token values; a rule there never hard-codes a tone (`--fe-tone-1..4` at white 4 / 6 / 8 / 10 %, `--fe-edge*` for the light a top edge catches, `--fe-surface-0` and `--fe-bg-rgb` for what fades into the page) so another skin can repaint it, on a light ground too | +| Skin | Direction | Signature | +| ----------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `studio` | Apple Music baseline | Inter / system-ui, soft shadows, rounded-12 — the calm baseline, and the skin safe mode and the crash marker fall back to (`RECOVERY_SKIN_ID`) | +| `editorial` | Broadsheet "WaveFlow Gazette" | Playfair Display 900 + Lora body, `` with locale-aware `Intl.DateTimeFormat`, `::first-letter` drop caps, halftone N&B covers via `mix-blend-luminosity`, ruler-tick progress bar via `repeating-linear-gradient`, `counter-reset` page numbering ("P. 1", "P. 2") | +| `lounge` | "Listening Room" warm-burgundy glass | Inter, [`SkinAmbientBackdrop`](../../src/components/layout/SkinAmbientBackdrop.tsx) blurs the cover (`filter: blur(140px) saturate(280%)`) into the body, white-overlay tint over the cover dominates the chrome | +| `pulse` | Neon Deck — club control panel | Neon Deck — its own direction, written in [`docs/design/pulse-neon-deck.md`](../design/pulse-neon-deck.md), on the same `layout: "fluid"` as Fluid and Liquid, with `fluid: { progress: "edge", moodRadio: "spectrum" }`: Fluid's two shapes painted as neon. `pulse.css` redeclares the `--fe-*` tokens (OLED `#000`, cool inks, white 10 % hairlines, the accent from `--accent-400` so the #895 accent modes drive it, Space Grotesk as `--fe-font` and Space Mono as the editorial slot) and adds the material: the chrome drawn as hairlines (the rail and the panel a translucent black with a 1 px edge, modules transparent and ringed, never filled), the accent as neon on whatever is live (a hover takes its ring and a halo; an open or playing item the 7 % fill, the ring, a halo outside and a little light inside), two static pools of light at the corners (the accent top-left, the cover's `--fe-glow` bottom-right), the floating pill of a player bar (the one blurred surface — the page scrolls under it) with the wave along its edge as a lit tube and the vinyl-spin cover via [`SkinPlayingState`](../../src/components/layout/SkinPlayingState.tsx) mirroring `usePlayer().isPlaying` to `[data-is-playing]`, the mood band as a tempo deck (the chosen segment ringed and lit, its wave in the accent), the `///` marker in the accent on the labels that open a block, and the spring `cubic-bezier(0.34, 1.56, 0.64, 1)` as `--fe-ease`. Dark-only. Reduced motion puts the layout's ease-out back and stops the vinyl; reduced transparency, the Linux safeguard and high contrast turn the bar, the rail, the panel and the search solid, and high contrast drops the halos | +| `liquid` | Aurora Glass — music as light | Its own direction, written in [`docs/design/liquid-aurora-glass.md`](../design/liquid-aurora-glass.md), on the same `layout: "fluid"` as Fluid. `liquid.css` redeclares the `--fe-*` tokens and adds the material: three pools of pastel light (lavender, ice, mint, each mixed with the cover's `--fe-glow`) always behind the page; four glass levels as semantic tokens (`--glass-canvas` / `--glass-structural` / `--glass-elevated` / `--glass-floating`: tint, edge, shadow; only structural and floating blur, because they never scroll) with the rule that two sheets of one level never stack; a sheet reads as glass through its edge — rim, inner reflection, foot shade, contour, a reflection along the top, the opal (`--glass-ambient-tint`) — rather than its blur; states as light — hover catches a sheen, focus charges the sheet with the accent once (`--glass-focus-ring`), the playing row carries the accent's edge and its light, the active module is luminous rather than filled; nothing loops but the aurora's drift and the chosen mood capsule's wave. Two pieces are its own through the preset's `fluid` field: the progress as a luminous line on the player plate (`fluid.progress: "inline"`) and Mood Radio as five capsules of tinted glass ([`MoodCapsules`](../../src/components/views/home/MoodCapsules.tsx), `fluid.moodRadio: "capsules"`) with an inner light wave each. **Theme-aware**: the light theme is the direction's home, the dark theme the same material at dusk. Opaque sheets under `prefers-reduced-transparency`, the WebKitGTK safeguard and high contrast | +| `fluid` | Cinematic editorial, music first | **The default since 2.0** (`DEFAULT_SKIN_ID`; first in the picker, and the picker is the first card of Appearance). DM Sans Variable + Playfair italic on the greeting only; depth by tone (no borders); the playing cover's light behind the Home ([`useCoverGlow`](../../src/components/views/home/fluid/useCoverGlow.ts) → `--fe-glow`, the accent stays `--accent-400` so the #895 accent modes drive it); the flow-line mark under section titles; a **different Home** ([`FluidHome`](../../src/components/views/home/fluid/FluidHome.tsx): listening canvas, the suggestion of the day as a band, [`MoodSpectrum`](../../src/components/views/home/MoodSpectrum.tsx) — Mood Radio as a tempo band, editorial mosaic, shelf, history list) and the progress as the player bar's top edge (`ProgressBar variant="edge"`); the three right-edge panels as the tabs of one shell ([`FluidSidePanel`](../../src/components/layout/fluid/FluidSidePanel.tsx): Details with the cover edge to edge and the identity over it, the album it plays from, radio and artist as pills and a glimpse of the lyrics; Lyrics with the sung line sharp and the others fading with their distance; Queue); the immersive view in the same voice (the display face for the title, text tabs, the wave for the progress, the lyrics fading with their distance from the sung line) — the one place where `data-immersive`, which otherwise shields the view from every skin, is read on purpose; the sidebar as a navigation rail (the wordmark, the profile as a small panel with the library's size under the name, the open row on a tone with the accent's edge, the liked / recently played pair as two tiles, the playlists as rows with a larger cover) through the `data-sidebar-*` and `data-nav-item` landmarks the view and [`NavItem`](../../src/components/common/NavItem.tsx) carry; the top bar as a control rail (transparent, the navigation squares as one quiet cluster, the search shorter and on the bar's own middle with the accent and the cover's light on focus, the theme switch and the profile as one cluster, the popovers in the panel tone) through the `data-topbar-*` landmarks on [`TopBar`](../../src/components/layout/TopBar.tsx); every other page keeps its markup and gets the same header family from the stylesheet — kicker, display title, one meta line, a pill CTA and ghost secondaries — through a `data-page-hero` attribute on the header of the library, album, artist, playlist, genre, year, liked and history views. Dark-only like Lounge / Pulse. Its stylesheet is keyed on `data-layout="fluid"`, not on the skin id: it is the **structure** of the 2.0 interface for every skin whose preset says so (Liquid and Pulse today), with Fluid's own material as the default token values; a rule there never hard-codes a tone (`--fe-tone-1..4` at white 4 / 6 / 8 / 10 %, `--fe-edge*` for the light a top edge catches, `--fe-surface-0` and `--fe-bg-rgb` for what fades into the page) so another skin can repaint it, on a light ground too | **Architecture** : - Token system + `SkinId` union live in [`src/lib/skins.ts`](../../src/lib/skins.ts); `applySkin()` writes `data-skin` on ``. -- Per-skin overrides live in `src/styles/skins/{editorial,lounge,pulse,liquid,fluid}.css` (Studio = no overrides, baseline). The five files are imported from `src/app.css`, `fluid.css` before `liquid.css`: the first is the 2.0 layout's structure and default material, keyed on `data-layout`, and the second only repaints it, winning the `!important` ties by source order. Four of them also carry a high-contrast block (see above) — a skin that adds a glass surface has to add it there too, or that surface becomes unreadable in the mode. +- Per-skin overrides live in `src/styles/skins/{editorial,lounge,pulse,liquid,fluid}.css` (Studio = no overrides, baseline). The five files are imported from `src/app.css`, `fluid.css` before `liquid.css` and `pulse.css`: the first is the 2.0 layout's structure and default material, keyed on `data-layout`, and the other two only repaint it, winning the `!important` ties by source order. Four of them also carry a high-contrast block (see above) — a skin that adds a glass surface has to add it there too, or that surface becomes unreadable in the mode. - [`src/app.css`](../../src/app.css) extends the Tailwind `dark` variant via `@custom-variant dark (.dark, .dark *, :root[data-skin="lounge"] *, :root[data-skin="pulse"] *, :root[data-skin="fluid"] *)` — Lounge/Pulse/Fluid fire `dark:*` utilities automatically because they're always "dark" by design (Liquid stays theme-aware). - **Local-first typography** — all skin fonts are bundled via `@fontsource` / `@fontsource-variable` (Playfair Display, Lora, Space Grotesk, Space Mono, DM Sans Variable) and imported at the top of [`src/main.tsx`](../../src/main.tsx). Zero network at runtime — no Google Fonts request. - **Motion** — each skin declares its own `MotionConfig` spring in `SkinMotionWrapper`. Skins with strong identity (Pulse, Editorial) override the spring; calmer skins (Studio, Lounge, Liquid, Fluid) inherit the soft default. diff --git a/index.html b/index.html index e30e0067..648f4625 100644 --- a/index.html +++ b/index.html @@ -89,7 +89,7 @@ studio: "classic", editorial: "classic", lounge: "classic", - pulse: "classic", + pulse: "fluid", liquid: "fluid", fluid: "fluid", }; diff --git a/src/app.css b/src/app.css index 0796b895..08f034d9 100644 --- a/src/app.css +++ b/src/app.css @@ -232,10 +232,6 @@ box-shadow: none !important; } -:root[data-skin="pulse"] { - --track-row-hover: rgb(var(--pulse-cyan-rgb) / 0.08); -} - :root[data-skin="lounge"] { --track-row-hover: rgb(255 45 121 / 0.12); } @@ -244,6 +240,12 @@ --track-row-hover: var(--fe-tone-1); } +/* Pulse sits on the fluid layout: same specificity as the rule above, + so it has to come after it to win. */ +:root[data-skin="pulse"] { + --track-row-hover: color-mix(in srgb, var(--fe-accent) 8%, transparent); +} + /* Override substring-based surface rules only for unselected, idle rows. */ :root:is( [data-skin="pulse"], @@ -414,10 +416,11 @@ iterations fragile. */ @import "./styles/skins/editorial.css"; @import "./styles/skins/lounge.css"; -@import "./styles/skins/pulse.css"; @import "./styles/skins/fluid.css"; -/* Liquid paints its material over the fluid layout, so it comes after. */ +/* Liquid and Pulse paint their material over the fluid layout, so they + come after it. */ @import "./styles/skins/liquid.css"; +@import "./styles/skins/pulse.css"; /* The desktop lyrics window (#582) is transparent at the OS level, and the document has to be too, or it paints an opaque box over the diff --git a/src/components/views/home/MoodRadioGrid.tsx b/src/components/views/home/MoodRadioGrid.tsx index 608fb243..87bb7ae7 100644 --- a/src/components/views/home/MoodRadioGrid.tsx +++ b/src/components/views/home/MoodRadioGrid.tsx @@ -86,7 +86,6 @@ export function MoodRadioGrid() { }, []); const isLoungeSkin = skin.id === "lounge"; const isEditorialSkin = skin.id === "editorial"; - const isPulseSkin = skin.id === "pulse"; const isFluidLayout = skin.layout === "fluid"; useEffect(() => { @@ -200,9 +199,7 @@ export function MoodRadioGrid() { ? "lounge-mood-section" : isEditorialSkin ? "editorial-mood-section" - : isPulseSkin - ? "pulse-mood-section" - : undefined + : undefined } >
@@ -244,9 +241,7 @@ export function MoodRadioGrid() { ? "lounge-mood-grid grid grid-cols-1 sm:grid-cols-6 gap-4 auto-rows-[10.75rem]" : isEditorialSkin ? "editorial-mood-grid grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-0 auto-rows-[9.75rem]" - : isPulseSkin - ? "pulse-mood-grid grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 auto-rows-[10rem]" - : "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4" + : "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4" } > {TILES.map(({ mood, icon: Icon, cardClass, iconClass }, index) => { @@ -272,9 +267,7 @@ export function MoodRadioGrid() { ? `lounge-mood-tile lounge-mood-${mood} flex flex-col ${loungeSpan}` : isEditorialSkin ? `editorial-mood-tile editorial-mood-${mood} flex flex-col ${editorialSpan}` - : isPulseSkin - ? `pulse-mood-tile pulse-mood-${mood} flex flex-col` - : cardClass + : cardClass }`} aria-label={t(`home.moodRadio.${mood}.title`)} > @@ -284,9 +277,7 @@ export function MoodRadioGrid() { ? "bg-white/10 text-rose-100" : isEditorialSkin ? "editorial-mood-icon" - : isPulseSkin - ? "pulse-mood-icon" - : iconClass + : iconClass }`} > @@ -296,7 +287,7 @@ export function MoodRadioGrid() { (isLoungeSkin || isEditorialSkin) && index === 0 ? "text-xl" : "text-base" - } ${isPulseSkin ? "text-xl" : ""}`} + }`} > {t(`home.moodRadio.${mood}.title`)}
@@ -305,9 +296,7 @@ export function MoodRadioGrid() {
{disabled diff --git a/src/i18n/locales/ar.json b/src/i18n/locales/ar.json index d5ba7e80..7547c3ed 100644 --- a/src/i18n/locales/ar.json +++ b/src/i18n/locales/ar.json @@ -2555,7 +2555,7 @@ }, "pulse": { "label": "Pulse", - "description": "نادي نيون — قاعدة OLED، هالات ملوّنة حول العناصر النشطة، شارات كبسولية أحادية، حركات نابضة." + "description": "Neon Deck — أسود OLED، إطار بخطوط دقيقة، لون التمييز في السمة نيونًا على ما هو نشط، تسميات أحادية المسافة، حركات نابضة." }, "liquid": { "label": "Liquid", diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index e4b341a1..ed42645b 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -2332,7 +2332,7 @@ }, "pulse": { "label": "Pulse", - "description": "Neon-Club — OLED-Basis, farbige Halos um aktive Elemente, einfarbige Kapseln, federnde Animationen." + "description": "Neon Deck — OLED-Schwarz, Chrome aus Haarlinien, der Akzent des Themas als Neon auf allem Aktiven, Mono-Labels, federnde Animationen." }, "liquid": { "label": "Liquid", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index bebcd5e3..718d76f9 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -2332,7 +2332,7 @@ }, "pulse": { "label": "Pulse", - "description": "Neon club — OLED base, coloured halos around active items, monochrome pills, springy motion." + "description": "Neon Deck — OLED black, hairline chrome, the theme's accent as neon on whatever is live, mono labels, springy motion." }, "liquid": { "label": "Liquid", diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 2fb4a8f9..f1253623 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -2396,7 +2396,7 @@ }, "pulse": { "label": "Pulse", - "description": "Club neón — base OLED, halos de color en los elementos activos, cápsulas monocromáticas, animaciones con rebote." + "description": "Neon Deck — negro OLED, cromo en líneas finas, el acento del tema como neón en lo que está activo, etiquetas mono, animaciones con rebote." }, "liquid": { "label": "Liquid", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index a04ab0bd..e4757021 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -1938,7 +1938,7 @@ }, "pulse": { "label": "Pulse", - "description": "Club néon — base OLED, halos colorés autour des éléments actifs, capsules monochromes, animations élastiques." + "description": "Neon Deck — noir OLED, chrome en filets, l'accent du thème en néon sur ce qui est actif, étiquettes mono, animations élastiques." }, "liquid": { "label": "Liquid", diff --git a/src/i18n/locales/hi.json b/src/i18n/locales/hi.json index 0fc4a52c..96c24664 100644 --- a/src/i18n/locales/hi.json +++ b/src/i18n/locales/hi.json @@ -2332,7 +2332,7 @@ }, "pulse": { "label": "Pulse", - "description": "नियॉन क्लब — OLED आधार, सक्रिय आइटम्स के चारों ओर रंगीन प्रभा, एकरंगी पिल्स, स्प्रिंग एनिमेशन।" + "description": "Neon Deck — OLED काला, पतली रेखाओं का क्रोम, सक्रिय चीज़ों पर थीम का एक्सेंट नियॉन की तरह, मोनो लेबल, स्प्रिंग एनिमेशन।" }, "liquid": { "label": "Liquid", diff --git a/src/i18n/locales/id.json b/src/i18n/locales/id.json index 74222b56..11eea823 100644 --- a/src/i18n/locales/id.json +++ b/src/i18n/locales/id.json @@ -2330,7 +2330,7 @@ }, "pulse": { "label": "Pulse", - "description": "Klub neon — basis OLED, halo warna di sekitar elemen aktif, kapsul mono, animasi lenting." + "description": "Neon Deck — hitam OLED, krom bergaris tipis, aksen tema sebagai neon pada yang aktif, label mono, animasi lenting." }, "liquid": { "label": "Liquid", diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index d6d05e9d..1cac1b62 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -2396,7 +2396,7 @@ }, "pulse": { "label": "Pulse", - "description": "Club neon — base OLED, aloni colorati attorno agli elementi attivi, capsule monocromatiche, animazioni elastiche." + "description": "Neon Deck — nero OLED, cromature a filo, l'accento del tema come neon su ciò che è attivo, etichette mono, animazioni elastiche." }, "liquid": { "label": "Liquid", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 88be2760..971a167b 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -2262,7 +2262,7 @@ }, "pulse": { "label": "Pulse", - "description": "ネオン・クラブ — OLED ベース、アクティブ項目を彩るグロー(光輪)、モノクロのカプセル型ボタン、スプリング感のあるモーション。" + "description": "Neon Deck — OLED ブラック、細線のクローム、アクティブな要素にテーマのアクセントをネオンで、モノスペースのラベル、スプリング感のあるモーション。" }, "liquid": { "label": "Liquid", diff --git a/src/i18n/locales/ko.json b/src/i18n/locales/ko.json index 4cde111b..c15ec06c 100644 --- a/src/i18n/locales/ko.json +++ b/src/i18n/locales/ko.json @@ -2330,7 +2330,7 @@ }, "pulse": { "label": "Pulse", - "description": "네온 클럽 — OLED 기반, 활성 요소 주변의 컬러 헤일로, 단색 캡슐, 탄력 있는 모션." + "description": "Neon Deck — OLED 블랙, 가는 선의 크롬, 활성 요소에 네온으로 비추는 테마 강조색, 모노 라벨, 탄력 있는 모션." }, "liquid": { "label": "Liquid", diff --git a/src/i18n/locales/nl.json b/src/i18n/locales/nl.json index 40e2f580..889e71da 100644 --- a/src/i18n/locales/nl.json +++ b/src/i18n/locales/nl.json @@ -2332,7 +2332,7 @@ }, "pulse": { "label": "Pulse", - "description": "Neonclub — OLED-basis, gekleurde halo's rond actieve items, monochrome capsules, veerkrachtige animaties." + "description": "Neon Deck — OLED-zwart, chroom in haarlijnen, het accent van het thema als neon op wat actief is, mono-labels, veerkrachtige animaties." }, "liquid": { "label": "Liquid", diff --git a/src/i18n/locales/pt-BR.json b/src/i18n/locales/pt-BR.json index 4a3a9ba4..d26aebb4 100644 --- a/src/i18n/locales/pt-BR.json +++ b/src/i18n/locales/pt-BR.json @@ -2397,7 +2397,7 @@ }, "pulse": { "label": "Pulse", - "description": "Clube neon — base OLED, halos coloridos ao redor dos itens ativos, cápsulas mono, animações com efeito de mola." + "description": "Neon Deck — preto OLED, cromado em linhas finas, o acento do tema em neon sobre o que está ativo, rótulos mono, animações com efeito de mola." }, "liquid": { "label": "Liquid", diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index c497cd30..721506b9 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -2397,7 +2397,7 @@ }, "pulse": { "label": "Pulse", - "description": "Clube néon — base OLED, halos coloridos em torno dos itens ativos, cápsulas mono, animações elásticas." + "description": "Neon Deck — preto OLED, cromado em linhas finas, o acento do tema em néon sobre o que está ativo, etiquetas mono, animações elásticas." }, "liquid": { "label": "Liquid", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 00106c08..ac0f2a21 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -2464,7 +2464,7 @@ }, "pulse": { "label": "Pulse", - "description": "Неоновый клуб — OLED-основа, цветные ореолы вокруг активных элементов, однотонные капсулы, пружинящие анимации." + "description": "Neon Deck — OLED-чёрный, тонкие линии хрома, акцент темы неоном на всём активном, моноширинные подписи, пружинящие анимации." }, "liquid": { "label": "Liquid", diff --git a/src/i18n/locales/tr.json b/src/i18n/locales/tr.json index 398cf9f9..2a03927e 100644 --- a/src/i18n/locales/tr.json +++ b/src/i18n/locales/tr.json @@ -2332,7 +2332,7 @@ }, "pulse": { "label": "Pulse", - "description": "Neon kulüp — OLED zemin, aktif öğeler etrafında renkli haleler, tek renkli etiketler, yaylı animasyonlar." + "description": "Neon Deck — OLED siyahı, ince çizgili krom, temanın vurgu rengi aktif öğelerde neon olarak, mono etiketler, yaylı animasyonlar." }, "liquid": { "label": "Liquid", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 3e6dd558..372f1f0c 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -2262,7 +2262,7 @@ }, "pulse": { "label": "Pulse", - "description": "霓虹俱乐部——OLED 底色,活动元素周围的彩色光晕,单色胶囊标签,弹性动效。" + "description": "Neon Deck——OLED 纯黑,细线勾勒的界面,主题强调色化作霓虹点亮活动元素,等宽标签,弹性动效。" }, "liquid": { "label": "Liquid", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index 47d06fd9..347eae16 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -2262,7 +2262,7 @@ }, "pulse": { "label": "Pulse", - "description": "霓虹俱樂部——OLED 底色、使用中項目周圍的彩色光暈、單色膠囊標籤與彈性動態效果。" + "description": "Neon Deck——OLED 純黑、細線勾勒的介面、主題強調色化作霓虹點亮使用中項目、等寬標籤與彈性動態效果。" }, "liquid": { "label": "Liquid", diff --git a/src/lib/skins.ts b/src/lib/skins.ts index 846509dd..c266345d 100644 --- a/src/lib/skins.ts +++ b/src/lib/skins.ts @@ -49,7 +49,8 @@ export interface SkinPreset { * How the fluid layout draws its two signature pieces; read only when * `layout` is `fluid`. Fluid's wave runs along the player bar's top * edge and its Mood Radio is one tempo band; Liquid's light line sits - * on the player plate and its moods are five glass capsules. + * on the player plate and its moods are five glass capsules; Pulse + * keeps Fluid's two shapes and paints them as neon (`pulse.css`). */ fluid?: { progress: "edge" | "inline"; @@ -378,7 +379,12 @@ export const SKIN_PRESETS: SkinPreset[] = [ }, { id: "pulse", - layout: "classic", + // The 2.0 interface, as a neon deck: the wave along the player bar's + // edge glows, the mood band is a tempo deck. `pulse.css` is the + // material over `fluid.css`; the direction is written in + // `docs/design/pulse-neon-deck.md`. + layout: "fluid", + fluid: { progress: "edge", moodRadio: "spectrum" }, labelKey: "settings.appearance.skins.pulse.label", descriptionKey: "settings.appearance.skins.pulse.description", density: { @@ -400,31 +406,30 @@ export const SKIN_PRESETS: SkinPreset[] = [ pill: "9999px", }, surface: { - // Neon glow shadows — these are what make Pulse feel - // distinct. Each card carries a coloured halo via the - // accent palette so the cover art / accent ring around - // selected items pulses into the surrounding chrome. + // Neon halos — what makes Pulse distinct. The accent is the + // theme's (so the custom and cover accent modes of #895 drive + // it), and every active element wears it as a halo rather than + // a fill. The hairline is Pulse's edge: a 1 px rail on black. shadowCard: - "0 0 0 1px rgb(255 255 255 / 0.06), 0 0 24px -4px var(--accent-500)", + "inset 0 0 0 1px rgb(255 255 255 / 0.1), 0 0 24px -8px var(--accent-400)", shadowElevated: - "0 0 0 1px rgb(255 255 255 / 0.08), 0 0 48px -8px var(--accent-400)", + "inset 0 0 0 1px rgb(255 255 255 / 0.12), 0 0 48px -12px var(--accent-400)", backdrop: "none", // Subtle white divider — Pulse is dark-only so the lines // need to lift off the OLED base. - divider: "rgb(255 255 255 / 0.08)", + divider: "rgb(255 255 255 / 0.1)", grain: "", }, typography: { // Space Grotesk for the body / display — a geometric sans // with mechanical character ticks that read as "tech / // club". Space Mono ALSO ships in main.tsx for the utility - // chrome (eyebrows, nav pills, time codes, `///` markers), - // but it is NOT exposed via this preset — pulse.css - // references `"Space Mono"` directly in its selectors. The - // preset only carries the two roles the SkinPreset shape - // declares (display + body), so the mono family lives at - // the CSS layer where it's used. Both families bundled via - // `@fontsource/*` in main.tsx. + // chrome (labels, time codes, the `///` markers), but it is + // NOT exposed via this preset — pulse.css points the layout's + // `--fe-font-editorial` at it. The preset only carries the + // two roles the SkinPreset shape declares (display + body), + // so the mono family lives at the CSS layer where it's used. + // Both families bundled via `@fontsource/*` in main.tsx. display: '"Space Grotesk", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', body: '"Space Grotesk", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', diff --git a/src/main.tsx b/src/main.tsx index 2c015671..8240af67 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -26,7 +26,11 @@ import "@fontsource/lora/700.css"; // the tech-track utility chrome (eyebrows, nav pills, time codes, // the `///` section markers). Loaded eagerly so the skin doesn't // FOUT when the user flips into it from Studio. +// 500 and 600 serve the 2.0 layout's settled weights (480–600): +// without them every title fell to 700. import "@fontsource/space-grotesk/400.css"; +import "@fontsource/space-grotesk/500.css"; +import "@fontsource/space-grotesk/600.css"; import "@fontsource/space-grotesk/700.css"; import "@fontsource/space-mono/400.css"; import "@fontsource/space-mono/700.css"; diff --git a/src/styles/skins/pulse.css b/src/styles/skins/pulse.css index d28c168e..bb1946f1 100644 --- a/src/styles/skins/pulse.css +++ b/src/styles/skins/pulse.css @@ -1,991 +1,1016 @@ /* ============================================================ - Pulse skin — Club control panel + Pulse — Neon Deck ============================================================ - Direction: OLED minimalism with neon edge lighting. Three - strict chromatic states, never more than one on the same - element at a time: - - idle = white text on transparent / black, hairline white/10 - hover = cyan halo (#00f3ff) — outline + glow + 4 px lift - active = magenta halo (#ff00ea) — outline + glow + 5 % fill - - Pulse owns its palette outright: the magenta and cyan are - hard-coded, NOT pulled from `--accent-500`. The theme picker - keeps influencing Studio + Editorial + Lounge; Pulse is the - "neon skin" by design and the duality (cyan hover ⇄ magenta - active) only works with these two specific neons. - - Signature element: spring motion. Every interactive surface - uses `cubic-bezier(0.34, 1.56, 0.64, 1)` — slightly past-end - bounce that lands as kinetic energy in every click. - - Selectors target stable landmarks + ARIA roles + the - `.skin-sidebar-shell` opt-in that Lounge/Editorial also use. - Tailwind class substring matching is intentionally limited - to a small audit-able set. */ - + The direction is written in `docs/design/pulse-neon-deck.md`; this + file is its material on the 2.0 interface (`fluid.css`, keyed on + `data-layout="fluid"`, imported before this one). One line: a club + control panel — OLED black, the chrome drawn as hairlines, and the + accent as neon on whatever is live. Where Fluid stages the music and + Liquid diffuses it, Pulse wires it. + + The decision, first: Pulse moves from the classic layout to + `layout: "fluid"`, with `fluid: { progress: "edge", moodRadio: + "spectrum" }` (`src/lib/skins.ts`). The 2.0 structure gives it the + canvas, the rail, the control rail and the tabbed panel for free; + its two signature pieces are Fluid's shapes painted as neon — the + wave along the player bar's edge becomes a lit tube, the mood band + a tempo deck with the chosen segment ringed and lit. Nothing is + moved: every rule below is a tone, an edge, a halo or a face over + the structure, as `liquid.css` does for its glass. + + What this file decides, in order of weight: + + 1. One neon, the theme's. The old Pulse owned two hard-coded neons + (cyan hover, magenta active); the 2.0 Pulse takes the accent + (`--accent-400`, so the custom and cover accent modes of #895 + drive it) for every live state — hover, open, playing, focus — + and the cover's own light (`--fe-glow`, written on the root by + `useCoverGlow`) for the other pool of the atmosphere and the + halos under the artwork and the player. Two colours, as before, + but both the music's. + + 2. The chrome reads as absent. The rail and the panel are the page's + black with a 1 px hairline for their edge; the modules on them are + transparent and ringed, never filled. A live element is filled + 7 % and ringed in the accent, with a halo outside and a little + light inside — the one place a fill is allowed. + + 3. OLED black, not near-black. The page is `#000`: the two pools of + light (the accent top-left, the cover bottom-right) are what + keep it from reading as a void, and the hairlines are what give + it an edge. No grain, no scanlines — the black is the texture. + + 4. Two faces. Space Grotesk carries the display and the body; Space + Mono carries the utility voice — the labels and kickers (with + the `///` marker in the accent), the time codes, the search, the + inputs, the audio facts, the greeting line (the layout's + editorial slot, which Fluid sets in a serif italic). + + 5. The spring. Every transition runs on `cubic-bezier(0.34, 1.56, + 0.64, 1)` — a slight overshoot that lands as a snap: the rise + on the Home, the lift on a hover, the deck opening a segment. + Reduced motion puts the layout's ease-out back and stops the + vinyl. + + 6. Blur on what never scrolls only: the floating player bar (the + page runs under it) and nothing else. The rail and the panel sit + over the fixed pools, which do not move, so a translucent black + is enough. + + Dark-only, like Fluid and Lounge: `app.css` lists it in the extended + `dark` variant, the scrollbar rules and the high-contrast dark block. + Rules that must beat a `!important` in `fluid.css` carry one and win + by source order. */ + +/* ── Tokens ─────────────────────────────────────────────────── */ :root[data-skin="pulse"] { - --pulse-bg: #000000; - --pulse-surface: #0a0a0a; - --pulse-surface-raised: #111114; - --pulse-ink: rgb(244 244 245); - --pulse-muted: rgb(160 160 160); - --pulse-faint: rgb(255 255 255 / 0.4); - --pulse-rail: rgb(255 255 255 / 0.1); - --pulse-rail-soft: rgb(255 255 255 / 0.06); - --pulse-rail-strong: rgb(255 255 255 / 0.18); - - /* Pulse's two neons — hard-coded so the duality always - reads. Not theme-bound. */ - --pulse-magenta: #ff00ea; - --pulse-magenta-rgb: 255 0 234; - --pulse-cyan: #00f3ff; - --pulse-cyan-rgb: 0 243 255; - - /* Spring motion — the kinetic signature. */ - --pulse-spring: cubic-bezier(0.34, 1.56, 0.64, 1); - --pulse-fast: 180ms; - --pulse-slow: 400ms; -} - -:root[data-skin="pulse"], -:root[data-skin="pulse"] body { - color: var(--pulse-ink); - background-color: var(--pulse-bg); - font-family: "Space Grotesk", system-ui, sans-serif; - font-feature-settings: "ss01", "ss02"; -} - -/* Atmospheric corner blobs: a single fixed overlay paints two - huge soft neon ellipses at opposite corners. position:fixed - so they stick to the viewport instead of scrolling with main - content. Z-index below everything else, pointer-events off - so they never block interaction. The opacity stays low (8 %) - — they're ambient lighting, not the subject. */ + --fe-bg: #000000; + --fe-bg-rgb: 0 0 0; + --fe-surface: #0a0a0c; + --fe-surface-2: #121216; + --fe-surface-0: #050507; + --fe-ink: #f4f4f5; + --fe-ink-2: #a9a9b2; + --fe-ink-3: #7a7a85; + --fe-hairline: rgb(255 255 255 / 0.1); + --fe-hairline-strong: rgb(255 255 255 / 0.18); + --fe-tone-1: rgb(255 255 255 / 0.035); + --fe-tone-2: rgb(255 255 255 / 0.06); + --fe-tone-3: rgb(255 255 255 / 0.09); + --fe-tone-4: rgb(255 255 255 / 0.12); + --fe-edge: rgb(255 255 255 / 0.05); + --fe-edge-strong: rgb(255 255 255 / 0.08); + --fe-accent: var(--accent-400); + --fe-accent-ink: #000000; + /* The cover's light before the first sample lands: a violet that + sits beside any accent. */ + --fe-glow: #4a1f5a; + --fe-glow-deep: #26102e; + --fe-ease: cubic-bezier(0.34, 1.56, 0.64, 1); + --fe-font: "Space Grotesk", system-ui, -apple-system, "Segoe UI", sans-serif; + --fe-font-editorial: var(--pu-mono); + + /* Pulse's own: the utility face, the rings, the halos. */ + --pu-mono: "Space Mono", ui-monospace, "Cascadia Mono", Menlo, monospace; + --pu-ring: inset 0 0 0 1px var(--fe-hairline); + --pu-ring-strong: inset 0 0 0 1px var(--fe-hairline-strong); + --pu-ring-accent: inset 0 0 0 1px var(--fe-accent); + --pu-ring-accent-soft: inset 0 0 0 1px + color-mix(in srgb, var(--fe-accent) 55%, transparent); + --pu-halo: 0 0 20px -6px color-mix(in srgb, var(--fe-accent) 55%, transparent); + --pu-halo-strong: + 0 0 28px -4px color-mix(in srgb, var(--fe-accent) 70%, transparent), + inset 0 0 14px -8px color-mix(in srgb, var(--fe-accent) 45%, transparent); + --pu-halo-accent-fill: 0 0 28px -6px + color-mix(in srgb, var(--fe-accent) 75%, transparent); + --pu-halo-glow: 0 0 40px -12px + color-mix(in srgb, var(--fe-glow) 85%, transparent); + --pu-fill-accent: color-mix(in srgb, var(--fe-accent) 7%, transparent); + --pu-fill-accent-strong: color-mix( + in srgb, + var(--fe-accent) 11%, + transparent + ); + --pu-neon-line: drop-shadow( + 0 0 4px color-mix(in srgb, var(--fe-accent) 80%, transparent) + ); + --pu-bar-bg: rgb(6 6 8 / 0.74); + --pu-chrome-bg: rgb(0 0 0 / 0.55); + --pu-field-bg: rgb(0 0 0 / 0.6); + --pu-blur: 20px; +} + +/* ── The atmosphere: two pools, the accent and the cover ────── */ +/* Static, at the corners of the window, as the old Pulse's blobs were; + the pools fade over the layout's ease-out, not the spring. */ :root[data-skin="pulse"] body::before { - content: ""; - position: fixed; inset: 0; - z-index: 0; - background-image: + background: radial-gradient( - ellipse at top left, - rgb(var(--pulse-magenta-rgb) / 0.1), - transparent 38% + ellipse 44% 42% at 0% 0%, + color-mix(in srgb, var(--fe-accent) 14%, transparent), + transparent 70% ), radial-gradient( - ellipse at bottom right, - rgb(var(--pulse-cyan-rgb) / 0.1), - transparent 38% + ellipse 48% 46% at 100% 100%, + color-mix(in srgb, var(--fe-glow) 55%, transparent), + transparent 70% ); - pointer-events: none; + transition: background 900ms cubic-bezier(0.22, 1, 0.36, 1); } - -/* Center column + ambient wrapper neutralisation. The - `bg-zinc-900/50` slab would otherwise grey out the neon - blobs. */ -:root[data-skin="pulse"] [class*="bg-app-ambient"], -:root[data-skin="pulse"] div:has(> header):has(> main) { - background-color: transparent !important; - background-image: none !important; +/* No grain: the black is the texture. */ +:root[data-skin="pulse"] body::after { + display: none; +} +/* The record's light under the canvas text and the band: the layout's + pool of the glow stays, its blurred copy of the cover goes — on pure + black a pale sleeve read as a grey smudge under the counters. */ +:root[data-skin="pulse"] .fe-canvas__aura img, +:root[data-skin="pulse"] .fe-suggest__aura img { + display: none; +} +:root[data-skin="pulse"] .fe-canvas__aura { + opacity: 0.45; +} +:root[data-skin="pulse"] .fe-suggest__aura { + opacity: 0.35; + background: radial-gradient( + ellipse 70% 80% at 30% 50%, + color-mix(in srgb, var(--fe-glow) 70%, transparent), + transparent 70% + ); } -/* ============================================================ - Chrome — sidebar, header, footer - ============================================================ - All chrome reads as "absent": no fill, just a hairline rail - on the inside edge. The cover-corner blobs paint behind. */ +/* ── Type: the mono voice, the `///` marker ─────────────────── */ +:root[data-skin="pulse"] .fe-editorial { + font-style: normal; + font-weight: 400; + letter-spacing: -0.02em; +} +:root[data-skin="pulse"] .fe-display { + font-weight: 600; + letter-spacing: -0.03em; +} +:root[data-skin="pulse"] .fe-title { + font-weight: 600; +} +:root[data-skin="pulse"] .fe-mono, +:root[data-skin="pulse"] footer [class*="tabular"], +:root[data-skin="pulse"] footer > div:last-child[class*="border-t"] { + font-family: var(--pu-mono); +} +/* Labels: the mono at 700, tracked wide. */ +:root[data-skin="pulse"] .fe-label, +:root[data-skin="pulse"] [data-sidebar-section-title], +:root[data-skin="pulse"] + main + [data-page-hero] + [class*="tracking-widest"][class*="uppercase"], +:root[data-skin="pulse"] + main + [class*="text-[10px]"][class*="uppercase"]:not([class*="text-white"]), +:root[data-skin="pulse"] .fe-panel [class*="text-[10px]"][class*="uppercase"], +:root[data-skin="pulse"] .fe-panel__tab { + font-family: var(--pu-mono) !important; + font-weight: 700 !important; + letter-spacing: 0.18em !important; +} +:root[data-skin="pulse"] .fe-label { + font-size: 0.6875rem; +} +/* The marker, in the accent, on the labels that open a block: the + section eyebrows, the rail's section titles, a page's kicker. */ +:root[data-skin="pulse"] .fe-label::before, +:root[data-skin="pulse"] + [data-sidebar-section-title] + > span:first-child::before, +:root[data-skin="pulse"] + main + [data-page-hero] + [class*="tracking-widest"][class*="uppercase"]::before { + content: "/// "; + white-space: pre; + color: var(--fe-accent); +} -:root[data-skin="pulse"] .skin-sidebar-shell { - background-color: transparent !important; - border-right: 1px solid var(--pulse-rail); - box-shadow: none; +/* ── The mark: the flow line as a lit tube ──────────────────── */ +:root[data-skin="pulse"] svg.fe-accent, +:root[data-skin="pulse"] main :is(h1, h2)[class*="border-b-4"]::after, +:root[data-skin="pulse"] main h1 + div[class*="bg-emerald-500"][class*="h-1"], +:root[data-skin="pulse"] + main + button[class*="border-b-2"][class*="border-emerald-500"]::after, +:root[data-skin="pulse"] .fe-panel__tab.is-active::after { + filter: var(--pu-neon-line); } +/* ── The rail: black, a hairline edge, ringed modules ───────── */ :root[data-skin="pulse"] .skin-sidebar-shell > aside { - margin: 0 !important; - width: 100% !important; - height: 100% !important; - padding: 1.5rem 1rem; - color: var(--pulse-ink); - background-color: transparent !important; + color: var(--fe-ink-2); + background-color: var(--pu-chrome-bg) !important; background-image: none !important; - border: 0 !important; - border-radius: 0 !important; - box-shadow: none !important; -} - -/* Brand block: the WaveFlow logo gets a neon outline + glow. - The label stays uppercase Space Grotesk with tracking — the - monogram is the focal point, the wordmark is a caption. */ -:root[data-skin="pulse"] .skin-sidebar-shell > aside > div:first-child { - padding: 0.5rem 0.75rem 1.25rem; - border-bottom: 1px solid var(--pulse-rail); + box-shadow: 1px 0 0 var(--fe-hairline) !important; } - -:root[data-skin="pulse"] - .skin-sidebar-shell - > aside - > div:first-child - > div:first-child { - gap: 0.75rem; - margin-bottom: 1.5rem; - font-family: "Space Grotesk", system-ui, sans-serif; +/* The product: the wordmark as a tracked capital label, the mark lit. */ +:root[data-skin="pulse"] [data-sidebar-brand] { + font-size: 0.8125rem; font-weight: 700; letter-spacing: 0.22em; text-transform: uppercase; - font-size: 0.95rem; } - -:root[data-skin="pulse"] - .skin-sidebar-shell - > aside - > div:first-child - > div:first-child - > :first-child { - width: 2rem; - height: 2rem; - padding: 0.4rem; - color: var(--pulse-ink); - background-color: var(--pulse-bg); - border: 2px solid var(--pulse-ink); - border-radius: 9999px; - box-shadow: 0 0 18px -4px rgb(255 255 255 / 0.6); +:root[data-skin="pulse"] [data-sidebar-brand] svg { + filter: var(--pu-neon-line); } - -/* Profile pill = magenta→cyan gradient avatar + transparent - capsule. */ -:root[data-skin="pulse"] .skin-sidebar-shell > aside > div:first-child button { - border: 1px solid var(--pulse-rail); - border-radius: 9999px !important; +/* The person: a ringed capsule, the avatar in the two lights. */ +:root[data-skin="pulse"] [data-sidebar-profile] { background-color: transparent !important; - font-family: "Space Mono", ui-monospace, monospace !important; - font-size: 0.78rem; - transition: all var(--pulse-fast) var(--pulse-spring); + box-shadow: var(--pu-ring); + transition: + background-color 180ms var(--fe-ease), + box-shadow 180ms var(--fe-ease); } - -:root[data-skin="pulse"] - .skin-sidebar-shell - > aside - > div:first-child - button:hover { - border-color: var(--pulse-cyan); - color: var(--pulse-cyan); - box-shadow: 0 0 16px -6px rgb(var(--pulse-cyan-rgb) / 0.6); - transform: translateY(-1px); +:root[data-skin="pulse"] [data-sidebar-profile]:hover { + background-color: var(--fe-tone-1) !important; + box-shadow: var(--pu-ring-accent-soft), var(--pu-halo); } - -:root[data-skin="pulse"] - .skin-sidebar-shell - > aside - > div:first-child - button - > div:first-child - > div:first-child { +:root[data-skin="pulse"] [data-sidebar-profile] > div > div:first-child { background-image: linear-gradient( 135deg, - var(--pulse-magenta), - var(--pulse-cyan) + var(--fe-accent), + color-mix(in srgb, var(--fe-glow) 70%, var(--fe-accent)) ) !important; background-color: transparent !important; - color: var(--pulse-bg) !important; + color: var(--fe-accent-ink) !important; + box-shadow: 0 0 12px -2px + color-mix(in srgb, var(--fe-accent) 60%, transparent); +} +/* The places and the pinned pair: transparent, ringed; hover takes the + accent's ring and halo and lifts a pixel; the open one is filled 7 %, + ringed and lit inside. The label in the mono voice. */ +:root[data-skin="pulse"] [data-sidebar-nav] [data-nav-item], +:root[data-skin="pulse"] [data-sidebar-row="pinned"] { + border-radius: 1rem !important; + background-color: transparent !important; + box-shadow: var(--pu-ring); + transition: + color 180ms var(--fe-ease), + background-color 180ms var(--fe-ease), + box-shadow 180ms var(--fe-ease), + transform 180ms var(--fe-ease); +} +:root[data-skin="pulse"] [data-sidebar-nav] [data-nav-item] > div > span { + font-family: var(--pu-mono); + font-size: 0.75rem; + font-weight: 400; + letter-spacing: 0.01em; +} +:root[data-skin="pulse"] [data-sidebar-nav] [data-nav-item] > span:first-child { border-radius: 9999px; - box-shadow: 0 0 12px -2px rgb(var(--pulse-magenta-rgb) / 0.5); + background-color: transparent; + box-shadow: var(--pu-ring); + transition: + color 180ms var(--fe-ease), + background-color 180ms var(--fe-ease), + box-shadow 180ms var(--fe-ease); } - -/* Section eyebrows — "/// MY MUSIC", "/// PLAYLISTS". Mono - font, tracking-widest, with three slashes injected via - ::before so we don't have to touch every translation file. */ -:root[data-skin="pulse"] - .skin-sidebar-shell - > aside - [class*="uppercase"][class*="tracking"] { - display: flex !important; - align-items: center; - gap: 0.5rem; - padding: 0 0.5rem !important; - color: var(--pulse-faint) !important; - font-family: "Space Mono", ui-monospace, monospace !important; - font-size: 0.65rem !important; - font-weight: 700 !important; - letter-spacing: 0.28em !important; - text-transform: uppercase; +:root[data-skin="pulse"] [data-sidebar-nav] [data-nav-item]:hover, +:root[data-skin="pulse"] [data-sidebar-row="pinned"]:hover { + background-color: transparent !important; + box-shadow: var(--pu-ring-accent-soft), var(--pu-halo); + transform: translateY(-1px); } - -:root[data-skin="pulse"] - .skin-sidebar-shell - > aside - [class*="uppercase"][class*="tracking"]::before { - content: "///"; - color: var(--pulse-magenta); - font-weight: 700; +:root[data-skin="pulse"] [data-sidebar-nav] [data-nav-item]:active, +:root[data-skin="pulse"] [data-sidebar-row="pinned"]:active { + transform: translateY(0) scale(0.97); } - -/* Mono pill nav items. Three states: idle (white text, white/10 - border), hover (cyan halo + lift), active (magenta halo + - 5 % magenta fill). */ -:root[data-skin="pulse"] - .skin-sidebar-shell - > aside - button:where(:has(> div.flex.flex-col)) { - padding: 0.55rem 1rem !important; - margin: 0.25rem 0 !important; - color: var(--pulse-muted) !important; - background-color: transparent !important; - border: 1px solid var(--pulse-rail) !important; - border-radius: 9999px !important; - font-family: "Space Mono", ui-monospace, monospace !important; - font-size: 0.78rem !important; - letter-spacing: 0.02em; - transition: - color var(--pulse-fast) var(--pulse-spring), - border-color var(--pulse-fast) var(--pulse-spring), - background-color var(--pulse-fast) var(--pulse-spring), - box-shadow var(--pulse-fast) var(--pulse-spring), - transform var(--pulse-fast) var(--pulse-spring); +:root[data-skin="pulse"] [data-sidebar-nav] [data-nav-item][data-active], +:root[data-skin="pulse"] [data-sidebar-row="pinned"][data-active] { + background-color: var(--pu-fill-accent) !important; + box-shadow: var(--pu-ring-accent), var(--pu-halo-strong); } - :root[data-skin="pulse"] - .skin-sidebar-shell - > aside - button:where(:has(> div.flex.flex-col)):hover { - color: var(--pulse-cyan) !important; - border-color: var(--pulse-cyan) !important; - box-shadow: 0 0 18px -6px rgb(var(--pulse-cyan-rgb) / 0.5); - transform: translateY(-1px); + [data-sidebar-nav] + [data-nav-item][data-active] + > span:first-child { + background-color: color-mix(in srgb, var(--fe-accent) 14%, transparent); + box-shadow: var(--pu-ring-accent-soft); } - -:root[data-skin="pulse"] - .skin-sidebar-shell - > aside - button[class*="bg-emerald"], -:root[data-skin="pulse"] - .skin-sidebar-shell - > aside - button[aria-pressed="true"] { - color: var(--pulse-magenta) !important; - background-color: rgb(var(--pulse-magenta-rgb) / 0.05) !important; - border-color: var(--pulse-magenta) !important; - box-shadow: - 0 0 20px -4px rgb(var(--pulse-magenta-rgb) / 0.5), - inset 0 0 10px -8px rgb(var(--pulse-magenta-rgb) / 0.4); - font-weight: 700; +/* Playlist rows: a tone on hover, the open one filled with the accent's + light and its edge lit. */ +:root[data-skin="pulse"] [data-sidebar-row="playlist"][data-active] { + background-color: var(--pu-fill-accent) !important; } - -/* SidebarRow + PlaylistSidebarRow — pinned items + real - playlists. Same pill treatment but no page-counter overhead. */ -:root[data-skin="pulse"] - .skin-sidebar-shell - > aside - button:where(:not(:has(> div.flex.flex-col))):where(:not(:first-child)) { - border: 1px solid var(--pulse-rail) !important; - border-radius: 9999px !important; - background-color: transparent !important; - transition: all var(--pulse-fast) var(--pulse-spring); +:root[data-skin="pulse"] [data-nav-item][data-active]::before, +:root[data-skin="pulse"] [data-sidebar-row="playlist"][data-active]::before { + box-shadow: 0 0 16px color-mix(in srgb, var(--fe-accent) 85%, transparent); } - :root[data-skin="pulse"] - .skin-sidebar-shell - > aside - button:where(:not(:has(> div.flex.flex-col))):where( - :not(:first-child) - ):hover { - border-color: var(--pulse-cyan) !important; - box-shadow: 0 0 16px -6px rgb(var(--pulse-cyan-rgb) / 0.5); - transform: translateY(-1px); -} - -/* Playlist + Liked thumbnails get a hairline ring so they - read as inset capsules instead of floating squares. */ -:root[data-skin="pulse"] .skin-sidebar-shell > aside img { - border: 1px solid var(--pulse-rail); + [data-sidebar-row="playlist"] + > div:first-child + > :is(img, div) { + box-shadow: 0 0 0 1px var(--fe-hairline); } - -/* ============================================================ - TopBar - ============================================================ */ -:root[data-skin="pulse"] header[class*="sticky"] { - height: 3.5rem !important; - margin: 0; - padding: 0 1.5rem !important; - color: var(--pulse-ink); - background-color: transparent !important; - background-image: none !important; - border-bottom: 1px solid var(--pulse-rail); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); +:root[data-skin="pulse"] [data-nav-item]:focus-visible, +:root[data-skin="pulse"] [data-sidebar-row]:focus-visible { + outline: 0; + box-shadow: var(--pu-ring-accent), var(--pu-halo); } -:root[data-skin="pulse"] header :where(button, [class*="rounded-full"]) { +/* ── The control rail: ringed squares, a black field ────────── */ +:root[data-skin="pulse"] [data-topbar-left] button { border-radius: 9999px !important; - border: 1px solid var(--pulse-rail) !important; + transition: + color 180ms var(--fe-ease), + background-color 180ms var(--fe-ease), + box-shadow 180ms var(--fe-ease), + transform 180ms var(--fe-ease); +} +:root[data-skin="pulse"] [data-topbar-left] button:hover:not(:disabled) { + color: var(--fe-accent) !important; background-color: transparent !important; - color: var(--pulse-ink) !important; - box-shadow: none !important; - transition: all var(--pulse-fast) var(--pulse-spring); + box-shadow: var(--pu-ring-accent-soft), var(--pu-halo) !important; } - -:root[data-skin="pulse"] - header - :where(button:hover, [class*="rounded-full"]:hover) { - color: var(--pulse-cyan) !important; - border-color: var(--pulse-cyan) !important; - box-shadow: 0 0 16px -6px rgb(var(--pulse-cyan-rgb) / 0.5); +:root[data-skin="pulse"] [data-topbar-left] button:active:not(:disabled), +:root[data-skin="pulse"] [data-topbar-profile]:active { + transform: scale(0.94); } - -:root[data-skin="pulse"] header [class*="rounded-full"]:has(input) { - border-color: var(--pulse-rail) !important; +:root[data-skin="pulse"] [data-topbar-search-field] { + background-color: var(--pu-field-bg) !important; + box-shadow: var(--pu-ring) !important; + transition: + background-color 180ms var(--fe-ease), + box-shadow 180ms var(--fe-ease); } - -:root[data-skin="pulse"] header [class*="rounded-full"]:focus-within { - border-color: var(--pulse-cyan) !important; - box-shadow: 0 0 20px -6px rgb(var(--pulse-cyan-rgb) / 0.5); +:root[data-skin="pulse"] [data-topbar-search-field]:hover { + background-color: var(--pu-field-bg) !important; + box-shadow: var(--pu-ring-strong) !important; } - -:root[data-skin="pulse"] header input { - color: var(--pulse-ink) !important; - background-color: transparent !important; - font-family: "Space Mono", ui-monospace, monospace !important; - font-size: 0.82rem; +:root[data-skin="pulse"] [data-topbar-search-field]:focus-within { + background-color: var(--pu-field-bg) !important; + box-shadow: + var(--pu-ring-accent), + var(--pu-halo), + 0 18px 40px -22px color-mix(in srgb, var(--fe-glow) 90%, rgb(0 0 0)) !important; } - -:root[data-skin="pulse"] header input::placeholder { - color: var(--pulse-faint) !important; +:root[data-skin="pulse"] [data-topbar-search-field] input { + font-family: var(--pu-mono); + font-size: 0.8125rem; } - -/* ============================================================ - Main + body cards - ============================================================ */ -:root[data-skin="pulse"] main { - position: relative; - z-index: 1; - padding: 1.5rem 2.5rem 8rem !important; +:root[data-skin="pulse"] [data-topbar-right] { + background-color: transparent; + box-shadow: var(--pu-ring); } - -/* Root section spacing. HomeView's default branch uses - `space-y-8` (2 rem) between sections — that gap reads as - "breathing" when the headings are chunky `text-2xl font-bold` - blocks, but FLATTENS visually when the headings are the thin - monospace `///` eyebrows Pulse uses. Bump the gap to 4 rem - so each section block stands clearly apart. `!important` - because Tailwind's `.space-y-8 > :not([hidden]) ~ - :not([hidden])` lands at (0,3,0) specificity — higher than - ours without it. - - The same applies to the section's internal heading-to-content - gap (`mb-6` on the heading wrapper) — bump it so the eyebrow - doesn't sit right on top of its grid. */ -:root[data-skin="pulse"] main > div > * + * { - margin-top: 4rem !important; +:root[data-skin="pulse"] [data-topbar-theme] > div { + box-shadow: 0 0 8px -2px color-mix(in srgb, var(--fe-ink) 60%, transparent); } - -:root[data-skin="pulse"] - section - > div:where([class*="flex"][class*="items-end"][class*="justify-between"]) { - margin-bottom: 2rem !important; +:root[data-skin="pulse"] [data-topbar-profile]:hover, +:root[data-skin="pulse"] [data-topbar-profile][aria-expanded="true"] { + color: var(--fe-accent) !important; + background-color: var(--pu-fill-accent) !important; } - -:root[data-skin="pulse"] - main - :where( - [class*="rounded-xl"], - [class*="rounded-2xl"], - [class*="rounded-3xl"] - ):where(:not([data-artwork-hit-target])) { - border-radius: 1.75rem !important; - border: 1px solid var(--pulse-rail) !important; - background-color: var(--pulse-surface) !important; - background-image: none !important; - box-shadow: none !important; - backdrop-filter: none; +:root[data-skin="pulse"] [data-topbar-popover] { + background-color: var(--fe-surface) !important; + box-shadow: + var(--pu-ring-strong), + 0 0 36px -8px color-mix(in srgb, var(--fe-accent) 25%, transparent), + 0 30px 60px -24px rgb(0 0 0 / 0.9) !important; } - :root[data-skin="pulse"] - main - :where( - [class*="rounded-xl"], - [class*="rounded-2xl"], - [class*="rounded-3xl"] - ):where(:not([data-artwork-hit-target], [data-track-table])):hover { - border-color: var(--pulse-cyan) !important; - box-shadow: 0 0 24px -8px rgb(var(--pulse-cyan-rgb) / 0.4); - transform: translateY(-2px); - transition: all var(--pulse-fast) var(--pulse-spring); -} - -/* ============================================================ - Section heading: "/// MOOD RADIOS" with trailing rail - ============================================================ */ -:root[data-skin="pulse"] section h2 { - display: flex !important; - align-items: center; - gap: 1rem; - color: var(--pulse-faint) !important; - font-family: "Space Mono", ui-monospace, monospace !important; - font-size: 0.78rem !important; - font-weight: 700; - letter-spacing: 0.28em; - text-transform: uppercase; - border: 0 !important; - padding-bottom: 0 !important; - margin-bottom: 1.5rem; -} - -:root[data-skin="pulse"] section h2::before { - content: "///"; - color: var(--pulse-magenta); + [data-topbar-popover] + :is([class*="hover:bg-zinc-50"], [class*="hover:bg-zinc-100"]):hover { + background-color: var(--pu-fill-accent) !important; + color: var(--fe-accent) !important; } -:root[data-skin="pulse"] section h2::after { - content: ""; - flex: 1; - height: 1px; - background-color: var(--pulse-rail); +/* ── The player bar: the floating pill, the wave lit ────────── */ +/* The one blurred surface: the page scrolls under it. The cover's + light pools at the record's end; the glow's halo sits under the + pill; the facts strip follows its corners. */ +:root[data-skin="pulse"] footer { + position: relative; + margin: 0 1.25rem 0.75rem; + border-radius: 1.75rem !important; + background-color: var(--pu-bar-bg) !important; + background-image: radial-gradient( + ellipse 30% 150% at 0% 50%, + color-mix(in srgb, var(--fe-glow) 42%, transparent), + transparent 70% + ) !important; + backdrop-filter: blur(var(--pu-blur)) saturate(140%); + -webkit-backdrop-filter: blur(var(--pu-blur)) saturate(140%); + box-shadow: + var(--pu-ring), + 0 14px 40px -18px rgb(0 0 0 / 0.9), + var(--pu-halo-glow) !important; + overflow: visible; } - -/* ============================================================ - Welcome banner hero — rounded-[2rem] image with magenta - pulse-dot eyebrow + big bold display headline - ============================================================ */ -:root[data-skin="pulse"] .editorial-hero, -:root[data-skin="pulse"] .lounge-hero { - /* Already handled via component class; do nothing for now. */ +:root[data-skin="pulse"] footer > div:last-child[class*="border-t"] { + border-color: var(--fe-hairline) !important; + border-bottom-left-radius: 1.75rem; + border-bottom-right-radius: 1.75rem; + font-size: 0.6875rem; + letter-spacing: 0.04em; } - -/* Welcome banner generic — the home view default banner block. */ -:root[data-skin="pulse"] - main - > div - > div - > div[class*="overflow-hidden"][class*="rounded-3xl"]:first-of-type { - border-radius: 2rem !important; - background-color: var(--pulse-surface) !important; - background-image: - radial-gradient( - ellipse at top left, - rgb(var(--pulse-magenta-rgb) / 0.2), - transparent 50% - ), - radial-gradient( - ellipse at bottom right, - rgb(var(--pulse-cyan-rgb) / 0.12), - transparent 50% - ) !important; - border: 1px solid var(--pulse-rail) !important; +/* The record: a disc that turns while the music plays, under the + `data-is-playing` mark `SkinPlayingState` writes on the root. */ +:root[data-skin="pulse"] footer img { + border-radius: 9999px; + box-shadow: + 0 0 0 1px var(--fe-hairline-strong), + 0 0 18px -6px color-mix(in srgb, var(--fe-glow) 90%, transparent); + animation: pu-vinyl 12s linear infinite; + animation-play-state: paused; } - -/* Eyebrow pill ("Ready to listen") — magenta capsule. */ -:root[data-skin="pulse"] - main - .inline-flex[class*="rounded-full"][class*="bg-emerald-50"] { - background-color: rgb(var(--pulse-magenta-rgb) / 0.08) !important; - border-color: var(--pulse-magenta) !important; - color: var(--pulse-magenta) !important; - font-family: "Space Mono", ui-monospace, monospace !important; - font-size: 0.65rem !important; - font-weight: 700; - letter-spacing: 0.22em; - text-transform: uppercase; - box-shadow: 0 0 14px -4px rgb(var(--pulse-magenta-rgb) / 0.5); +:root[data-skin="pulse"][data-is-playing="true"] footer img { + animation-play-state: running; } - -:root[data-skin="pulse"] - main - .inline-flex[class*="rounded-full"][class*="bg-emerald-50"] - > div { - background-color: var(--pulse-magenta) !important; - box-shadow: 0 0 8px rgb(var(--pulse-magenta-rgb) / 0.8); +@keyframes pu-vinyl { + to { + transform: rotate(360deg); + } } - -/* Display headlines in welcome banner / view titles. */ -:root[data-skin="pulse"] main h1, -:root[data-skin="pulse"] main h2:not(section h2) { - font-family: "Space Grotesk", system-ui, sans-serif !important; - font-weight: 700; - letter-spacing: -0.025em; +/* The wave: the accent as a lit tube, the baseline a hairline, the + thumb a bright point. Stroke and fill are CSS here, so they beat the + component's presentation attributes. */ +:root[data-skin="pulse"] .fe-progress svg line { + stroke: rgb(255 255 255 / 0.12); } - -/* Stat cards — pill capsules with magenta icon. */ -:root[data-skin="pulse"] main [class*="rounded-xl"]:has(> div[class*="w-12"]) { - border-radius: 1.5rem !important; - background-color: var(--pulse-surface) !important; +:root[data-skin="pulse"] .fe-progress svg path { + filter: var(--pu-neon-line); + transition: stroke-width 180ms var(--fe-ease); } - -:root[data-skin="pulse"] main [class*="rounded-xl"] > div[class*="w-12"] { - border-radius: 9999px !important; - color: var(--pulse-magenta) !important; - background-color: rgb(var(--pulse-magenta-rgb) / 0.1) !important; - border: 1px solid var(--pulse-magenta) !important; - box-shadow: 0 0 12px -4px rgb(var(--pulse-magenta-rgb) / 0.5); +:root[data-skin="pulse"] .fe-progress:hover svg path { + stroke-width: 2.4; } - -/* ============================================================ - Mood Radio — per-tile colored halo - ============================================================ */ -:root[data-skin="pulse"] .pulse-mood-grid { - border: 0 !important; - gap: 1rem !important; +:root[data-skin="pulse"] .fe-progress__thumb { + fill: var(--fe-ink); + filter: drop-shadow(0 0 6px var(--fe-accent)); } - -/* Per-tile colored halo: a soft blur disk in the top-right - corner, scaled + lit on hover. Each mood gets its own - accent. */ -:root[data-skin="pulse"] .pulse-mood-tile { - padding: 1.5rem !important; - color: var(--pulse-ink) !important; - background-color: var(--pulse-surface) !important; - background-image: none !important; - border: 1px solid var(--pulse-rail) !important; - border-radius: 2rem !important; - box-shadow: none !important; - transition: all var(--pulse-fast) var(--pulse-spring); - position: relative; - overflow: hidden; +:root[data-skin="pulse"] footer .fe-mono { + font-size: 0.6875rem; + letter-spacing: 0.04em; } - -:root[data-skin="pulse"] .pulse-mood-tile::before { - content: ""; - position: absolute; - top: -2rem; - right: -2rem; - width: 8rem; - height: 8rem; - border-radius: 9999px; - filter: blur(40px); - opacity: 0.25; - background-color: var(--pulse-cyan); +/* The transport: ink, the accent on hover, a snap on press; the play + button the accent filled and lit. */ +:root[data-skin="pulse"] footer :where(button) { transition: - opacity var(--pulse-slow) var(--pulse-spring), - transform var(--pulse-slow) var(--pulse-spring); - pointer-events: none; + color 180ms var(--fe-ease), + background-color 180ms var(--fe-ease), + box-shadow 180ms var(--fe-ease), + transform 180ms var(--fe-ease); } - -/* Per-mood halo colour. */ -:root[data-skin="pulse"] .pulse-mood-focus::before { - background-color: var(--pulse-cyan); +:root[data-skin="pulse"] footer :where(button:hover:not(:disabled)) { + color: var(--fe-accent) !important; } -:root[data-skin="pulse"] .pulse-mood-chill::before { - background-color: #bd00ff; +:root[data-skin="pulse"] footer :where(button:active:not(:disabled)) { + transform: scale(0.94); } -:root[data-skin="pulse"] .pulse-mood-workout::before { - background-color: var(--pulse-magenta); +:root[data-skin="pulse"] + footer + button:where([class*="bg-emerald-500"], [class*="bg-emerald-600"]) { + background-color: var(--fe-accent) !important; + color: var(--fe-accent-ink) !important; + box-shadow: var(--pu-halo-accent-fill) !important; } -:root[data-skin="pulse"] .pulse-mood-party::before { - background-color: #00ff66; +:root[data-skin="pulse"] + footer + button:where([class*="bg-emerald-500"], [class*="bg-emerald-600"]):hover:not( + :disabled + ) { + color: var(--fe-accent-ink) !important; + transform: scale(1.08); + box-shadow: 0 0 36px -4px + color-mix(in srgb, var(--fe-accent) 85%, transparent) !important; } -:root[data-skin="pulse"] .pulse-mood-sleep::before { - background-color: #4d8dff; +:root[data-skin="pulse"] footer :where(button:focus-visible) { + outline: 0; + box-shadow: var(--pu-ring-accent), var(--pu-halo); } - -:root[data-skin="pulse"] .pulse-mood-tile:hover { - border-color: var(--pulse-cyan) !important; - box-shadow: 0 0 28px -10px rgb(var(--pulse-cyan-rgb) / 0.6); - transform: translateY(-3px); +/* The play button's resting halo is `!important`, so its focus light + has to be declared the same way, on top of it — as the CTA's is. */ +:root[data-skin="pulse"] + footer + button:where( + [class*="bg-emerald-500"], + [class*="bg-emerald-600"] + ):focus-visible { + outline: 0; + box-shadow: + 0 0 0 2px var(--fe-bg), + 0 0 0 3px var(--fe-accent), + var(--pu-halo-accent-fill) !important; } -:root[data-skin="pulse"] .pulse-mood-tile:hover::before { - opacity: 0.55; - transform: scale(1.2); +/* ── The side panel: black, a hairline edge, ringed pills ───── */ +:root[data-skin="pulse"] .fe-panel { + background-color: var(--pu-chrome-bg); + background-image: linear-gradient( + 180deg, + color-mix(in srgb, var(--fe-glow) 22%, transparent), + transparent 40% + ); + box-shadow: -1px 0 0 var(--fe-hairline); +} +:root[data-skin="pulse"] .fe-panel__tab { + font-size: 0.6875rem; + text-transform: uppercase; } - -/* Icon in mood tile — small mono pill. */ -:root[data-skin="pulse"] .pulse-mood-tile > div:first-child { - width: 2.5rem !important; - height: 2.5rem !important; - color: var(--pulse-ink) !important; - background-color: transparent !important; - border: 1px solid var(--pulse-rail) !important; - border-radius: 9999px !important; +:root[data-skin="pulse"] .fe-panel__tab:hover { + color: var(--fe-ink); + background-color: transparent; } - -:root[data-skin="pulse"] .pulse-mood-tile > div:nth-child(2) { - font-family: "Space Grotesk", system-ui, sans-serif !important; - font-size: 1.25rem !important; - font-weight: 700; +:root[data-skin="pulse"] .fe-panel__pill { + background-color: transparent; + box-shadow: var(--pu-ring); + transition: + color 180ms var(--fe-ease), + background-color 180ms var(--fe-ease), + box-shadow 180ms var(--fe-ease), + transform 180ms var(--fe-ease); +} +:root[data-skin="pulse"] .fe-panel__pill:hover:not(:disabled) { + background-color: transparent; + box-shadow: var(--pu-ring-accent-soft), var(--pu-halo); + color: var(--fe-ink); +} +:root[data-skin="pulse"] .fe-panel__pill:active:not(:disabled) { + transform: scale(0.96); +} +:root[data-skin="pulse"] .fe-panel__lyrics { + background-color: transparent; + box-shadow: var(--pu-ring); + transition: + background-color 180ms var(--fe-ease), + box-shadow 180ms var(--fe-ease); } - -:root[data-skin="pulse"] .pulse-mood-tile > div:nth-child(3) { - color: var(--pulse-muted) !important; - font-family: "Space Mono", ui-monospace, monospace !important; - font-size: 0.78rem; - font-style: normal; +:root[data-skin="pulse"] .fe-panel__lyrics:hover { + background-color: var(--fe-tone-1); + box-shadow: var(--pu-ring-accent-soft), var(--pu-halo); } - -:root[data-skin="pulse"] .pulse-mood-tile > div:last-child { - margin-top: auto; - padding-top: 0; - border-top: 0 !important; - color: var(--pulse-faint) !important; - font-family: "Space Mono", ui-monospace, monospace !important; - font-size: 0.65rem !important; - font-weight: 700; - letter-spacing: 0.18em; - text-transform: uppercase; +/* The sung line carries the accent's light. */ +:root[data-skin="pulse"] + .fe-panel + ul + > li:has(> button[class*="scale-[1.02]"]) + > button { + text-shadow: 0 0 20px color-mix(in srgb, var(--fe-accent) 45%, transparent); } - -/* ============================================================ - PlayerBar — floating pill capsule - ============================================================ - The footer keeps its DOM position (above the AudioQualityFooter) - but visually pulls back into a centered floating pill with a - blurred glass treatment. Margin around it pushes the - AudioQualityFooter into a thin colophon below. */ -:root[data-skin="pulse"] footer { - margin: 0 1.25rem 0.75rem; - padding: 0.5rem 1.25rem; - color: var(--pulse-ink); - background-color: rgb(0 0 0 / 0.7) !important; - background-image: none !important; - border: 1px solid var(--pulse-rail) !important; - border-radius: 9999px !important; - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); +/* The queue's playing row: filled with the accent's light, its edge lit. */ +:root[data-skin="pulse"] .fe-panel [class*="bg-emerald-50 "] { + background-color: var(--pu-fill-accent) !important; box-shadow: - 0 0 0 1px rgb(255 255 255 / 0.04) inset, - 0 12px 36px -16px rgb(0 0 0 / 0.9); - overflow: visible; + inset 2px 0 0 var(--fe-accent), + inset 12px 0 24px -12px + color-mix(in srgb, var(--fe-accent) 30%, transparent); } - -:root[data-skin="pulse"] footer :where(button) { - border-radius: 9999px !important; - border: 0 !important; - background-color: transparent !important; - color: var(--pulse-ink) !important; - box-shadow: none !important; - transition: all var(--pulse-fast) var(--pulse-spring); +:root[data-skin="pulse"] + .fe-panel + :is([class*="hover:bg-zinc-50"], [class*="hover:bg-zinc-100"]):hover { + background-color: var(--pu-fill-accent) !important; } -:root[data-skin="pulse"] footer :where(button:hover) { - color: var(--pulse-cyan) !important; - transform: scale(1.1); +/* ── The page: ringed cards, lit on hover ───────────────────── */ +:root[data-skin="pulse"] + main + :where( + [class*="rounded-xl"], + [class*="rounded-2xl"], + [class*="rounded-3xl"] + ):where([class*="border"]):where( + :not( + button, + a, + input, + select, + textarea, + [data-artwork-hit-target], + [data-track-table], + [role="switch"], + [role="radio"] + ) + ), +:root[data-skin="pulse"] main details.settings-group, +:root[data-skin="pulse"] main [data-folder-card], +:root[data-skin="pulse"] main [class*="from-violet"], +:root[data-skin="pulse"] + main + button[class*="rounded-2xl"][class*="border"][class*="p-4"] { + border-radius: 1.25rem !important; + background-color: var(--fe-surface) !important; + box-shadow: var(--pu-ring) !important; + transition: + background-color 180ms var(--fe-ease), + box-shadow 180ms var(--fe-ease), + transform 180ms var(--fe-ease); } - -/* The center play button — solid white circle. */ +:root[data-skin="pulse"] main [data-folder-card]:hover, :root[data-skin="pulse"] - footer - button:where([class*="bg-emerald-500"], [class*="bg-emerald-600"]) { - background-color: var(--pulse-ink) !important; - color: var(--pulse-bg) !important; - border: 0 !important; - box-shadow: 0 0 24px -6px rgb(255 255 255 / 0.5); + main + button[class*="rounded-2xl"][class*="border"][class*="p-4"]:hover { + background-color: var(--fe-surface) !important; + box-shadow: var(--pu-ring-accent-soft), var(--pu-halo) !important; + transform: translateY(-2px); +} +:root[data-skin="pulse"] main [data-folder-card]:focus-within { + box-shadow: var(--pu-ring-accent), var(--pu-halo) !important; } - :root[data-skin="pulse"] - footer - button:where([class*="bg-emerald-500"], [class*="bg-emerald-600"]):hover { - transform: scale(1.1); - box-shadow: 0 0 28px -4px rgb(255 255 255 / 0.7); + main + [data-folder-card] + > div:first-child + > div:first-child { + background-color: transparent !important; + box-shadow: var(--pu-ring); + color: var(--fe-accent) !important; } - -/* Cover thumbnail — spins like a vinyl when isPlaying. The - `data-is-playing` attribute is set by PulsePlaybackState (a - tiny effect component mounted by App) on the documentElement - so we don't have to thread per-component state. */ -:root[data-skin="pulse"] footer img { - border-radius: 9999px !important; - border: 1px solid var(--pulse-rail); - animation: pulse-vinyl 12s linear infinite; - animation-play-state: paused; +/* The suggestion band: ringed, the record's light at its end. */ +:root[data-skin="pulse"] .fe-suggest { + box-shadow: var(--pu-ring); } - -:root[data-skin="pulse"][data-is-playing="true"] footer img { - animation-play-state: running; +/* Artwork: a hairline ring; the accent's ring and light on hover; the + hero cover on the record's own light. */ +:root[data-skin="pulse"] .fe-tile { + box-shadow: + 0 0 0 1px var(--fe-hairline), + 0 10px 30px -18px rgb(0 0 0 / 0.9); } - -@keyframes pulse-vinyl { - to { - transform: rotate(360deg); - } +:root[data-skin="pulse"] .fe-tile-hover:hover .fe-tile, +:root[data-skin="pulse"] .fe-tile-hover:focus-visible .fe-tile { + box-shadow: + 0 0 0 1px color-mix(in srgb, var(--fe-accent) 55%, transparent), + 0 0 28px -8px color-mix(in srgb, var(--fe-accent) 55%, transparent), + 0 26px 44px -22px rgb(0 0 0 / 0.9); } - -@media (prefers-reduced-motion: reduce) { - :root[data-skin="pulse"] footer img { - animation: none !important; - } +:root[data-skin="pulse"] .fe-tile-hover[aria-current="true"] .fe-tile { + box-shadow: + 0 0 0 2px var(--fe-accent), + 0 0 18px color-mix(in srgb, var(--fe-accent) 45%, transparent); } - -/* Progress slider — OLED line: white/10 rail + cyan fill that - turns magenta on hover. Tabular mono time codes left and - right. */ -:root[data-skin="pulse"] footer [role="slider"] { - height: 4px !important; - background-color: rgb(255 255 255 / 0.1) !important; - background-image: none !important; - border: 0 !important; - border-radius: 9999px !important; - overflow: visible; - transition: all var(--pulse-fast) var(--pulse-spring); +:root[data-skin="pulse"] .fe-hero-cover { + box-shadow: + 0 0 0 1px var(--fe-hairline-strong), + 0 0 0 2px rgb(0 0 0 / 0.6), + 0 0 70px -20px color-mix(in srgb, var(--fe-glow) 95%, transparent), + 0 30px 60px -30px rgb(0 0 0 / 0.95); } - -:root[data-skin="pulse"] footer [role="slider"]:hover { - height: 6px !important; +:root[data-skin="pulse"] + main + [data-page-hero] + :is([class*="w-44"], [class*="w-48"], [class*="w-40"])[class*="rounded"] { + box-shadow: + 0 0 0 1px var(--fe-hairline-strong), + 0 0 48px -16px color-mix(in srgb, var(--fe-glow) 90%, transparent), + 0 30px 60px -28px rgb(0 0 0 / 0.95) !important; } - -:root[data-skin="pulse"] footer [role="slider"] > div { - background-color: var(--pulse-cyan) !important; - background-image: none !important; - border-radius: 9999px !important; - height: 100% !important; - /* Outer glow kept very tight (6 px spread, no offset) so the - neon halo stays inside the floating pill's rounded edge. - The previous 12 px spread leaked past the pill's bottom- - right curve and read as a teal "border" outside the - capsule. `overflow: visible` on the footer is intentional - (DeviceMenu / SleepTimerMenu / MoreActions popovers need - to escape) so the glow has to clip itself. */ - box-shadow: 0 0 6px rgb(var(--pulse-cyan-rgb) / 0.7); +/* The primary action: the accent filled, lit under; a snap on press. */ +:root[data-skin="pulse"] .fe-cta, +:root[data-skin="pulse"] .fe-spectrum__cta, +:root[data-skin="pulse"] + main + [data-page-hero] + button:where( + [class*="bg-emerald-500"], + [class*="bg-pink-500"], + [class*="rounded-full"][class*="text-white"]:not([class*="w-10"]):not( + [class*="absolute"] + ) + ) { + box-shadow: var(--pu-halo-accent-fill) !important; } - -:root[data-skin="pulse"] footer [role="slider"]:hover > div { - background-color: var(--pulse-magenta) !important; - box-shadow: 0 0 6px rgb(var(--pulse-magenta-rgb) / 0.8); +:root[data-skin="pulse"] .fe-cta:hover, +:root[data-skin="pulse"] + main + [data-page-hero] + button:where( + [class*="bg-emerald-500"], + [class*="bg-pink-500"], + [class*="rounded-full"][class*="text-white"]:not([class*="w-10"]):not( + [class*="absolute"] + ) + ):hover:not(:disabled) { + filter: brightness(1.1); + box-shadow: 0 0 36px -4px + color-mix(in srgb, var(--fe-accent) 85%, transparent) !important; +} +:root[data-skin="pulse"] .fe-cta:active { + transform: scale(0.96); +} +:root[data-skin="pulse"] .fe-spectrum__cta { + background-color: var(--fe-accent) !important; + color: var(--fe-accent-ink) !important; + font-weight: 600; +} +:root[data-skin="pulse"] .fe-spectrum__cta:hover:not(:disabled) { + filter: brightness(1.1); +} +:root[data-skin="pulse"] .fe-cta:focus-visible, +:root[data-skin="pulse"] .fe-spectrum__cta:focus-visible, +:root[data-skin="pulse"] + main + [data-page-hero] + button:where( + [class*="bg-emerald-500"], + [class*="bg-pink-500"], + [class*="rounded-full"][class*="text-white"]:not([class*="w-10"]):not( + [class*="absolute"] + ) + ):focus-visible { + outline: 0; + box-shadow: + 0 0 0 2px var(--fe-bg), + 0 0 0 3px var(--fe-accent), + var(--pu-halo-accent-fill) !important; } - -/* Time codes + audio meta — Space Mono. */ -:root[data-skin="pulse"] footer [class*="tabular"] { - font-family: "Space Mono", ui-monospace, monospace !important; - color: var(--pulse-muted) !important; +/* Links and icon buttons take the accent on hover. */ +:root[data-skin="pulse"] .fe-link:hover, +:root[data-skin="pulse"] .fe-icon-btn:hover { + color: var(--fe-accent); } - -/* Hi-Res badge: cyan mono pill. */ -:root[data-skin="pulse"] footer [class*="text-[10px]"][class*="uppercase"] { - padding: 0.2rem 0.55rem !important; - border: 1px solid var(--pulse-cyan) !important; - border-radius: 9999px !important; - background-color: rgb(var(--pulse-cyan-rgb) / 0.08) !important; - color: var(--pulse-cyan) !important; - font-family: "Space Mono", ui-monospace, monospace !important; - font-weight: 700; - letter-spacing: 0.16em; - box-shadow: 0 0 12px -4px rgb(var(--pulse-cyan-rgb) / 0.4); +:root[data-skin="pulse"] .fe-icon-btn:hover { + background-color: var(--pu-fill-accent); } - -/* AudioQualityFooter — hidden under Pulse. It's a flat 20-px - strip that lives as the last child of `