`
`PlayerControls`, `VolumeControls`, `TimestampLabel`, `SpectrumVisualizer` each open with a hand-rolled `
` and a sibling scoped `.css` that just re-implements `display:flex; align-items:center; gap:...`. That is `MudStack` with parameters. Each is a small, mechanical swap.
---
## 2. Visual design — the rounded container
### 2.1 Target look
A single rounded card floating above the bottom edge of the viewport, horizontally centred, capped at a comfortable reading width, with a **mostly-opaque themed surface** so content behind it doesn't bleed through and hurt the seek/spectrum legibility. Soft elevation shadow, theme-coloured hairline accent, generous internal padding. It should read as the same material family as the menu bar and track cards — because it will now share their palette source.
### 2.2 Where the colour comes from
**`MudPaper` carrying the theme surface, not a hand-built background.** `MudPaper` paints `var(--mud-palette-surface)` and applies a Material elevation shadow that already differs between the light and dark palettes (MudBlazor swaps the whole palette when `MudThemeProvider IsDarkMode` flips, which `MainLayout` already drives). So:
- **Light (wireframe):** surface resolves to `#FAFAF8` (warm off-white) — `Surface` in `PaletteLight`. Elevation shadow is the standard Material dark-on-light.
- **Dark (wireframe):** surface resolves to `#162437` (navy-mid) — `Surface` in `PaletteDark`. Elevation shadow is the deeper dark-mode Material shadow.
Both are *already mostly opaque solids* in the palette — which is exactly what the brief asks for and what the seek/spectrum legibility wants. We do **not** need the old `color-mix(... 88%, transparent)` trick; the palette surfaces are opaque by design. If a hint of translucency is still wanted, prefer a single MudBlazor-aware rule (`background-color: var(--mud-palette-surface)` with an `opacity` on a pseudo-layer) over re-introducing alpha tokens — but the default recommendation is **opaque surface, no backdrop-filter**, because `backdrop-filter: blur()` over an opaque surface is wasted GPU work and was only ever there to rescue the translucent look.
### 2.3 Concrete prop choices
```razor
...
```
- **`Elevation="8"`** — high enough to read as a floating dock above page content; MudBlazor's elevation system supplies the light/dark-appropriate shadow so the hand-rolled `box-shadow: 0 4px 20px rgba(0,0,0,0.9)` goes away entirely.
- **Rounding** — `MudPaper` rounds by default via `--mud-default-borderradius`. If a *larger* radius is wanted (the old CSS used `1rem`), set it via `Style="border-radius:16px"` or a single scoped `.player-surface { border-radius: 16px; }` rule. One line, theme-independent — radius is geometry, not colour, so scoped CSS is the right home (see §5).
- **Accent hairline** — the old design had a theme-coloured border (iron in light, coral in dark). To keep a hairline accent that *tracks the theme*, use `var(--mud-palette-primary)` in a single scoped rule: `.player-surface { border: 1px solid var(--mud-palette-lines-default); }` for a neutral hairline, or `var(--mud-palette-primary)` for a stronger accent (navy in light, green-accent in dark). This is the **one** place we keep a `var(--mud-palette-*)` reference in scoped CSS, and it is legitimate because `--mud-palette-*` *is* defined by `MudThemeProvider` at runtime — unlike the dead `--charleston-*` tokens. The distinction is the whole point: theme-driven via MudBlazor's own variables, not a parallel hand-maintained set.
### 2.4 The minimized dock and window controls
- **Minimized dock** (`.minimized-dock`) currently builds a 3-stop gradient from the dead tokens. Replace with a `MudFab` (`Color="Color.Primary"`, `Icon="@Icons.Material.Filled.ExpandLess"`) — a circular floating action button is exactly this control, it picks up the themed primary colour for free, and it carries its own elevation/hover. The bespoke `.minimized-button` `!important` overrides disappear. The only scoped CSS that survives is the **fixed positioning** (`bottom`/`right`/`z-index`) and the responsive position shift.
- **Window controls** (minimize/close, top-right) stay as two `MudIconButton`s but move into a named slot/component (§3) and are positioned with a `MudStack Row` + absolute-position scoped rule, unchanged in behaviour.
---
## 3. Layout blueprint — MudBlazor components replacing raw divs
### 3.1 Outer shell
| Current | Replacement | Owns |
| --- | --- | --- |
| `.player-outer-container` (`
`, `position:fixed`) | Keep a thin scoped-CSS `
` **or** a `MudPaper` with the fixed positioning in scoped CSS | Fixed docking to viewport bottom, z-index, full-width |
| `MudContainer MaxWidth="Large"` | `MudContainer MaxWidth="MaxWidth.Large"` (keep — it's already the right component) | Horizontal centring + max width |
| `.player-backdrop` (`
`) | **`MudPaper Elevation="8"`** | The visible rounded themed surface (§2) |
Positioning (`position: fixed; bottom: 0; left/right: 0; z-index: 1200`) is geometry and **stays in scoped CSS** — MudBlazor has no fixed-dock primitive and shouldn't. Everything *visual* about the surface moves to `MudPaper`.
### 3.2 The three-zone desktop interior
Replace the top-level `
` with a **`MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3"`**. Each child is a named zone:
```razor
@* LEFT ZONE — transport + timestamp *@
@* CENTRE ZONE — seek + spectrum (flex-grow) *@
@* RIGHT ZONE — volume *@
```
| Zone | Current markup | New home |
| --- | --- | --- |
| Left (transport + spinner + timestamp) | `
` with inline spinner + `TimestampLabel` | **New `PlayerTransportZone` sub-component** wrapping `PlayerControls`, the `MudProgressCircular`, and `TimestampLabel` in a `MudStack Column AlignItems="Center" Spacing="2"` |
| Centre (seek + spectrum) | `
` with inline pointer handlers + `MudSlider` + `SpectrumVisualizer` | **New `PlayerSeekZone` sub-component** owning the seek pointer-handler logic once, plus the `MudSlider` and `SpectrumVisualizer`, in a `MudStack Column` |
| Right (volume) | `
` wrapping `VolumeControls` | `VolumeControls` directly — the wrapper div was empty of behaviour (its only CSS rule was commented out) |
| Window controls (top-right) | absolutely-positioned `
` | **New `PlayerWindowControls` sub-component** (`MudStack Row`) positioned via one scoped absolute rule |
### 3.3 Why `MudStack` over `MudGrid` here
`MudGrid`/`MudItem` is a 12-column responsive grid — overkill and clumsy for a single fixed-height toolbar row where the centre should simply *take the remaining space*. `MudStack` (flexbox wrapper) with `flex-grow-1` on the centre zone is the natural fit and is the direct, idiomatic replacement for the existing `d-flex gap-*` divs. **`MudToolBar`** was considered (it's literally a player-bar-shaped component) but it forces a fixed height and dense horizontal layout that fights the centre zone's stacked seek-over-spectrum arrangement; `MudStack` inside `MudPaper` gives more control. Note that recommendation for the implementer.
### 3.4 New sub-components worth extracting (summary)
1. **`PlayerTransportZone`** — left cluster. Removes inline spinner logic from the parent and gives the left zone a name. Reused by mobile? No — keep desktop-only for now; mobile composes its own arrangement (brief says leave mobile alone).
2. **`PlayerSeekZone`** — centre cluster. **Highest-value extraction**: it ends the duplicated pointer-handler block between desktop and mobile by owning that logic in one place. Even though mobile stays as-is for layout, mobile could later consume this same component (one source, multiple views — consistent with `CONTEXT.md §6` and the project's "same VM, divergence only in rendering" instinct). Design it so mobile *can* adopt it later without a rewrite, even though we don't wire mobile now.
3. **`PlayerWindowControls`** — minimize/close cluster. Trivial but gives the absolutely-positioned controls a name and a home for their one positioning rule.
---
## 4. Sub-component dispositions
| Component | Verdict | Change |
| --- | --- | --- |
| **`PlayerControls`** | Minor internal swap | Replace `