# AudioPlayerBar — Desktop Redesign Proposal

**Status:** Proposal (design only — no source edited).
**Scope:** Desktop branch of `AudioPlayerBar.razor` (`@if (_isDesktop)`). Mobile branch left untouched.
**Author context:** Written against the wireframe palette currently in `DeepDrftPalettes.cs` and `deepdrft-tokens.css`, not the retired Charleston/Lowcountry identity.

---

## 0. Reframe before you read the rest (the headline)

The brief asks to integrate the player with "the Charleston and Lowcountry themes via raw CSS variables." **Those themes no longer exist.** They were retired when the app migrated to the wireframe palette (navy / green / warm off-white). `DeepDrftPalettes.cs` says so explicitly: *"the coral/lowcountry identity has been retired."*

That changes the nature of this task. This is not "make the messy player prettier." It is:

> **The player bar was left behind by the palette migration. Its entire theming layer references CSS custom properties that are no longer defined anywhere. Migrating it onto MudBlazor's theme system *is* the redesign.**

Concretely, `AudioPlayerBar.razor.css` and `SpectrumVisualizer.razor.css` reference these tokens, **none of which are defined in `deepdrft-tokens.css` (verified by grep — zero definitions):**

| Token referenced in player CSS | Defined anywhere? | Current runtime effect |
| --- | --- | --- |
| `--deepdrft-theme-background-gray` | No | backdrop background resolves to nothing → falls through |
| `--deepdrft-theme-primary` | No | backdrop border colour is invalid → no border / UA default |
| `--deepdrft-theme-secondary` / `-tertiary` | No | minimized dock gradient + spectrum bars have no colour |
| `--charleston-cream` / `-iron` / `-rose` / `-gold` | No | light-mode `:global(.deepdrft-theme-light)` block is dead |
| `--lowcountry-night` / `-coral` / `-twilight` / `-gold` / `-moonlight` | No | dark-mode `:global(.deepdrft-theme-dark)` block is dead |

So **today the desktop player is rendering with broken styling** — invalid `color-mix()`/`var()` references collapse to `transparent` or are dropped, and the `:global(.deepdrft-theme-*)` overrides target a wrapper class that still exists (`MainLayout` sets `deepdrft-theme-dark`/`-light`) but whose custom-property payload was deleted. The bar looks half-styled because it *is* half-styled.

This means the redesign has a clean justification that the brief's own framing obscures: we are not inventing a new look, we are **finishing a migration the rest of the app already completed.** The correct colour source is the MudBlazor theme (`DeepDrftPalettes.Default`), reached through MudBlazor component props and `var(--mud-palette-*)` — not a parallel hand-maintained token set that has already drifted into nonexistence once.

The redesign goals in the brief (rounded `MudPaper`, theme-driven opaque background, MudBlazor layout components, encapsulated zones) are all still right. The palette names in the brief are just stale; substitute the wireframe palette and everything else holds.

---

## 1. Diagnosis — what's specifically wrong

### 1.1 The theming layer points at deleted tokens (load-bearing)
Covered above. This is the single most important finding and the strongest argument for doing the work now: the player is visibly broken against the current palette, not merely inelegant.

### 1.2 The container background is hand-rolled CSS, not a theme surface
`.player-backdrop` builds its own surface from scratch: a `var(...)` background, a hard-coded `backdrop-filter: blur(15px)`, a 2px border in a (now-undefined) theme colour, and a `box-shadow`. MudBlazor already has a surface primitive that reads `--mud-palette-surface` and applies elevation shadows that respond to light/dark automatically — `MudPaper`. The component is reimplementing `MudPaper` badly and then theming it with dead variables.

### 1.3 Layout is raw flex divs with utility-class soup
The desktop branch is six nested `<div>`s carrying `d-flex`, `flex-column`, `align-center`, `gap-3`, `gap-2`, `gap-1`, `mx-3`, `flex-grow-1`. The three-zone structure (left transport / centre seek+spectrum / right volume) is *implied* by div nesting and `min-width: 200px` on `.controls-left`, not *expressed* by any named component. A reader has to mentally execute the flexbox to see the zones.

### 1.4 Zones are not encapsulated
- The left zone (play/stop + loading spinner + timestamp) is an anonymous `<div class="controls-left d-flex flex-column...">` with inline logic for the progress circle.
- The centre zone (seek slider + spectrum) is an anonymous `<div class="d-flex flex-column flex-grow-1">` that *also* owns the pointer-event seek handlers inline in the markup.
- The window controls (minimize/close) are an absolutely-positioned `<div class="player-controls">` floating over the content.

None of these are components or even named slots. The seek pointer-handler block (`@onpointerdown`/`@onpointerup`/`@onpointerleave` with an inline lambda) is duplicated nearly verbatim between desktop and mobile branches.

### 1.5 The outer structure mixes three responsibilities in one tree
`.player-outer-container` (fixed positioning) → `MudContainer` (max-width centring) → `.player-backdrop` (the visible surface) → layout. Fixed-position docking, horizontal centring, and the visible card are three concerns stacked into three divs where MudBlazor offers a cleaner split: scoped CSS owns *only* the fixed dock; `MudPaper` owns the visible surface; a `Style`/`Class` width cap owns centring.

### 1.6 Sub-components each wrap themselves in a bespoke `<div>`
`PlayerControls`, `VolumeControls`, `TimestampLabel`, `SpectrumVisualizer` each open with a hand-rolled `<div class="...">` 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
<MudPaper Elevation="8"
          Class="player-surface"
          Style="border-radius: var(--mud-default-borderradius);">
   ...
</MudPaper>
```

- **`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` (`<div d-flex flex-column>`, `position:fixed`) | Keep a thin scoped-CSS `<div class="player-dock">` **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` (`<div>`) | **`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 `<div class="d-flex align-center gap-3">` with a **`MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3"`**. Each child is a named zone:

```razor
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3" Class="player-row">

    @* LEFT ZONE — transport + timestamp *@
    <PlayerTransportZone IsPlaying="IsPlaying" IsLoaded="IsLoaded"
                         IsLoading="IsLoading" IsStreaming="IsStreaming"
                         LoadProgress="LoadProgress"
                         DisplayTime="DisplayTime" Duration="Duration"
                         TogglePlayPause="TogglePlayPause" Stop="Stop" />

    @* CENTRE ZONE — seek + spectrum (flex-grow) *@
    <PlayerSeekZone DisplayTime="DisplayTime" Duration="Duration"
                    CanSeek="CanSeek"
                    OnSeekStart="OnSeekStart" OnSeekEnd="OnSeekEnd"
                    OnSeekChange="OnSeekChange"
                    Class="flex-grow-1" />

    @* RIGHT ZONE — volume *@
    <VolumeControls Volume="Volume" VolumeChanged="OnVolumeChange" />

</MudStack>
```

| Zone | Current markup | New home |
| --- | --- | --- |
| Left (transport + spinner + timestamp) | `<div class="controls-left d-flex flex-column align-center gap-2">` 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) | `<div class="d-flex flex-column flex-grow-1">` 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) | `<div class="volume-right">` wrapping `VolumeControls` | `VolumeControls` directly — the wrapper div was empty of behaviour (its only CSS rule was commented out) |
| Window controls (top-right) | absolutely-positioned `<div class="player-controls">` | **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 `<div class="player-buttons">` (flex/gap CSS) with `<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">`. Delete `PlayerControls.razor.css` entirely — its only rule is the flex container `MudStack` now provides. Buttons unchanged. |
| **`VolumeControls`** | Minor internal swap | Replace `<div class="volume-controls">` with `<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">`. The `width:140px` / slider `width:100px` sizing can stay as a scoped rule **or** move to `Style="width:140px"` on the stack. Keep the `MudIcon` + `MudSlider`. Trim `VolumeControls.razor.css` to (at most) the width caps. |
| **`TimestampLabel`** | Mostly fine | `<div class="timestamp-display">` → optional `<MudStack Row Justify="Justify.Center">` or just keep the div for centring + min-width; this one is borderline. The monospace font is now redundant with the theme — `Typo.Subtitle1`/`Caption` already map to **Geist Mono** in `DeepDrftPalettes.Typography`. **Recommend:** switch `MudText` to `Typo="Typo.Caption"` (Geist Mono via theme) and drop the `font-family: monospace` scoped rule — that's the migration-to-theme move in microcosm. Keep `min-width:120px` (layout stability so the timestamp doesn't reflow as digits change). |
| **`SpectrumVisualizer`** | **Must change — dead tokens** | The bar colour uses `--deepdrft-theme-secondary` and the `:global(.deepdrft-theme-light/dark)` overrides use `--charleston-*` / `--lowcountry-*` — **all undefined**. Bars currently have no reliable colour. **Fix:** point the bar `background` at `var(--mud-palette-primary)` (themed: navy in light, green-accent in dark) or a `linear-gradient` from `var(--mud-palette-primary)` to `var(--mud-palette-tertiary)`. Drop both `:global` theme-override blocks — a single `var(--mud-palette-*)` rule auto-tracks the theme, so the per-theme duplication is no longer needed. The geometry CSS (`.spectrum-bars` flex, `.spectrum-bar` sizing, `--bar-height` animation) **stays** — it's layout/animation, not colour. |

---

## 5. CSS strategy — what stays scoped vs. moves to MudBlazor props

**Principle:** scoped `.css` owns *geometry, positioning, and animation*. The MudBlazor theme (via component props and `var(--mud-palette-*)`) owns *colour, surface, and elevation*. The dead `--charleston-*` / `--lowcountry-*` / `--deepdrft-theme-*` token references are deleted wholesale.

### Stays in `AudioPlayerBar.razor.css`
- `.player-dock` — fixed positioning (`position:fixed; bottom; left; right; z-index:1200`). No MudBlazor equivalent; correct to keep.
- `.player-surface` — **at most**: `border-radius` (if larger than default) and *one* hairline `border: 1px solid var(--mud-palette-lines-default)` (or `--mud-palette-primary` for a stronger accent). `var(--mud-palette-*)` is allowed here because MudBlazor defines it at runtime.
- `.minimized-dock` positioning — `bottom`/`right`/`z-index` and the `:hover { transform: scale(1.1) }` micro-interaction. Colour/gradient/shadow all go away with the move to `MudFab`.
- `.player-window-controls` — the single `position:absolute; top; right` rule.
- `.player-spacer` — the `height:140px` content-overlap spacer (and its `@media` override). Pure geometry.
- All `@media (max-width: 768px)` blocks — responsive geometry. Keep. (They affect the minimized dock and spacer, which are shared with mobile; do not disturb.)
- `SpectrumVisualizer` bar geometry + `--bar-height` transition.

### Moves out of scoped CSS entirely (deleted)
- `.player-backdrop` background / `backdrop-filter` / `box-shadow` / border → **`MudPaper Elevation` + surface**.
- Both `:global(.deepdrft-theme-light/dark) .player-backdrop` blocks → gone; MudBlazor swaps the palette.
- The `.minimized-dock` gradient + per-theme override blocks → **`MudFab Color="Color.Primary"`**.
- `.minimized-button` `!important` block → gone (was overriding `MudIconButton`; replaced by `MudFab`).
- `PlayerControls.razor.css` (whole file) → `MudStack`.
- `VolumeControls.razor.css` flex rules → `MudStack` (keep only width caps if desired).
- `TimestampLabel` `font-family: monospace` → theme typography (`Typo.Caption`).
- `SpectrumVisualizer` colour rules (`--deepdrft-theme-secondary` + both `:global` theme blocks) → single `var(--mud-palette-*)` rule.

### Net effect
`AudioPlayerBar.razor.css` shrinks from ~176 lines (most of it dead-token theming) to roughly the positioning/spacer/responsive core — maybe 40–50 lines, all of it geometry. Two sub-component `.css` files (`PlayerControls`, `VolumeControls`) can be deleted or reduced to a width cap.

---

## 6. Acceptance criteria

An implementer is done when:

1. **No dead tokens remain.** `grep` for `--charleston-`, `--lowcountry-`, and `--deepdrft-theme-` across `Controls/AudioPlayerBar/**` returns **zero** matches. (This is the migration's definition of done.)
2. **The visible surface is a `MudPaper`** with `Elevation` set, carrying the themed surface colour — no hand-rolled `background`/`box-shadow`/`border` colour in scoped CSS for the surface. Toggling dark mode flips the player surface between off-white (`#FAFAF8`) and navy-mid (`#162437`) **with no player-specific code path** — it rides the `MudThemeProvider` palette swap that `MainLayout` already drives.
3. **The desktop interior is `MudStack`-based**, not `<div class="d-flex gap-*">`. The three zones are named components (`PlayerTransportZone`, `PlayerSeekZone`, `VolumeControls`) and the window controls are a named component (`PlayerWindowControls`).
4. **The seek pointer-handler logic exists in exactly one place** (`PlayerSeekZone`), not duplicated inline in the parent. (Mobile may still call its own copy until mobile is migrated — but the desktop branch no longer carries inline pointer handlers in `AudioPlayerBar.razor`.)
5. **`PlayerControls` and `VolumeControls` wrap in `MudStack`**, not bespoke flex divs; their now-redundant `.razor.css` flex rules are deleted.
6. **`SpectrumVisualizer` bars are visibly coloured in both themes** via `var(--mud-palette-*)`, and its two `:global(.deepdrft-theme-*)` colour blocks are gone.
7. **The minimized state is a `MudFab`** (or themed `MudIconButton`), not a div with a hand-rolled gradient; no `!important` overrides remain on it.
8. **Mobile is byte-for-byte unchanged in behaviour and appearance** — the `@if (_isDesktop)` mobile branch and all mobile `@media` rules render identically to before. (If `PlayerSeekZone` is adopted by mobile, that is a *separate, later* change and out of scope here.)
9. **Scoped CSS contains only geometry/positioning/animation** — a reviewer scanning `AudioPlayerBar.razor.css` finds no colour values except `var(--mud-palette-*)` references (and ideally none at all beyond an optional hairline border).
10. **No "wrong theme flash" regression** — the surface colour is correct on first paint, since it now derives from the same `MudThemeProvider` that the existing prerender/dark-mode round-trip already seeds.

---

## 7. Trade-offs and notes for the implementer

- **`backdrop-filter: blur()` is dropped.** With an opaque themed surface there is nothing to blur. If Daniel specifically wants the frosted-glass look back, that's a deliberate re-add over a *translucent* surface — and it should use a MudBlazor-aware translucent surface, not a re-introduced alpha token. Default recommendation: drop it; opaque reads cleaner and is cheaper.
- **`MudStack` vs `MudToolBar`:** I recommend `MudStack` (§3.3). If the implementer finds `MudToolBar` cleaner for the row, that's an acceptable substitution *provided* the centre zone still flex-grows; flag it if `MudToolBar`'s fixed height fights the stacked seek+spectrum.
- **`PlayerTransportZone` / `PlayerSeekZone` are new files.** They add two components but remove ~four anonymous divs and one duplicated logic block. Net structural win. If Daniel prefers fewer files, the minimum viable version keeps `PlayerSeekZone` (it kills the duplication) and inlines the transport cluster as a `MudStack` without extracting a component. Recommend the full extraction; note the lighter option.
- **This is a desktop-only migration by request, which leaves the app in a mixed state**: desktop on the MudBlazor theme, mobile still on (dead) tokens. Mobile is *also* currently broken against the live palette for the same reason — its spectrum bars and any shared dead-token rules have no colour. Worth surfacing to Daniel: a follow-up to migrate mobile is implied, even though this task explicitly excludes it. Captured here so it isn't lost.

---

## 8. Roadmap placement

This work isn't currently in `PLAN.md`. It fits most naturally as a new item under **Phase 2 — Product surface** (it's a UI-surface correctness + polish task), or as a small standalone entry. Because it's partly a *bug fix* (broken theming against the live palette) and partly a *redesign*, recommend logging it as a Phase 2 item with a note that the dead-token breakage is the triggering defect. If Daniel approves this proposal, I'll draft the `PLAN.md` entry — and flag the implied mobile follow-up (§7) as a sibling item.
