Files
deepdrft/COMPLETED.md
T

253 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# COMPLETED.md — DeepDrftHome
Archive of items that have moved out of `PLAN.md` and `CMS-PLAN.md`. Per `CONTEXT.md §6`, completed items are moved here rather than deleted. Each entry preserves the original "What / Why / Shape" body so this file reads as a decision record, not just an outcome list.
Newest entries at the top. Group by phase/wave header (mirroring `PLAN.md` / `CMS-PLAN.md` themes) when there are enough entries to warrant it.
---
## Phase 2 — Product surface: player and theming
**Status:** AudioPlayerBar responsive unification and SpectrumVisualizer fix landed on 2026-06-05.
### AudioPlayerBar Responsive Unification
**Landed 2026-06-05.**
Collapsed the two divergent Razor trees in `AudioPlayerBar.razor` (`@if (_isDesktop)` / `@else`) into a single markup tree where CSS — not a runtime breakpoint flag — drives the responsive layout. Removed `IBrowserViewportService`, the `_isDesktop` field, `OnAfterRenderAsync`, and the viewport subscription/unsubscription from the code-behind.
**Structural changes:**
- Single `.player-layout` flex container (in `AudioPlayerBar.razor.css`) replaces the dual-branch conditional. Three children (`PlayerTransportZone`, `VolumeControls`, `PlayerSeekZone`) in source order; media query at 600px (`Sm` breakpoint) reorders via CSS `order` property and forces `SeekZone` to full-width below the transport/volume row on narrow viewports.
- `PlayerTransportZone` flips its internal axis (vertical ↔ horizontal) via scoped CSS override of `MudStack` `flex-direction` at the 600px boundary — no parameter added to the component.
- `::deep` prefix removed from `MudBlazor` component-class selectors in `PlayerTransportZone.razor.css` now that axis is purely CSS-driven and no runtime flag determines structure.
- **SpectrumVisualizer bars now appear on first expand** — fixed by subscribing to the multicast `StateChanged` event (same pattern used by `AudioPlayerBar`), ensuring animation is initialized after mount.
**Scope:**
- Unified responsive layout (desktop/mobile branches merged into single tree).
- Both `AudioPlayerBar` and `SpectrumVisualizer` components affected.
- Build clean: 0 errors, 0 new warnings.
**Notes for future work:**
- First-render layout flash eliminated by construction (CSS media query evaluates at paint, not async subscription).
**Status:** Desktop AudioPlayerBar redesign landed on 2026-06-04.
### Desktop AudioPlayerBar — migrate to MudBlazor theme system
**Landed 2026-06-04.**
Desktop branch of `AudioPlayerBar.razor` migrated off dead CSS palette tokens (`--charleston-*`, `--lowcountry-*`, `--deepdrft-theme-*` — none of which are defined in the live stylesheet) onto the active MudBlazor theme system. This was simultaneously a bug fix (player styling broken against the current palette) and a structural redesign.
**Structural changes:**
- `.player-backdrop` div replaced with `MudPaper Elevation="8"` — surface colour now derives from `--mud-palette-surface` via the live theme, and flips automatically with dark mode (off-white in light, navy in dark).
- Three new zone sub-components extracted: `PlayerTransportZone` (left transport cluster), `PlayerSeekZone` (centre seek+spectrum, owns the seek pointer-handler logic), `PlayerWindowControls` (minimize/close buttons). These remove duplication (seek handlers no longer inline-copied) and name the layout zones explicitly.
- `MudStack` replaces all raw `<div class="d-flex gap-*">` throughout the desktop branch and sub-components (`PlayerControls`, `VolumeControls`, `TimestampLabel`).
- `SpectrumVisualizer` bar colour fixed: `var(--mud-palette-primary)` replaces the undefined `--deepdrft-theme-secondary` token.
- Minimized dock replaced with `MudFab Color="Color.Primary"` — rounded button picking up themed primary colour with no hand-rolled gradient.
- `AudioPlayerBar.razor.css` shrunk from ~176 lines (mostly dead-token theming) to ~74 lines (geometry and positioning only).
**Scope:**
- Desktop branch only (`@if (_isDesktop)`). Mobile branch unchanged by design.
- Build clean: 0 errors, 0 new warnings.
**Notes for future work:**
- Mobile branch is also currently broken against the live palette for the same reason (spectrum bars + shared dead-token rules have no colour). A companion migration for mobile is implied but out of scope for this task — marked for future Phase 2 work.
---
## Deployment Infrastructure
**Status:** CD pipeline infrastructure landed on 2026-06-04.
### CD pipeline infrastructure (Gitea workflows + remote host installer)
**Landed 2026-06-04.**
Continuous deployment infrastructure for DeepDrftHome dual-app deployment. Consists of four Gitea workflows (`.gitea/workflows/`) — `deploy-public.yml`, `deploy-manager.yml`, `deploy-api.yml`, `package-install.yml` — all triggered by `dev` branch (beta) and `master` branch (prod) pushes, path-filtered to deploy only on changes to the affected service and its dependencies. Five installer scripts (`deploy/`) — `install.sh` (one-shot host provisioner), `bootstrap.sh` (curl-and-run entry point), `ssh-wrapper.sh` (forced-command dispatcher), three `deploy-*.sh` per-service deployment scripts — plus systemd service templates (`deploy/systemd/`) and nginx vhost templates (`deploy/nginx/`), and credential template files (`deploy/credentials/`). One auxiliary setup script `setup-step10-creds.sh` for interactive credential entry on the host. The installer creates users, directories, systemd services, PostgreSQL databases, nginx vhosts, and loads credential files via systemd `LoadCredential=` into the credential sandbox. The deploy scripts swap binaries in-place, run the EF migrations bundle for the API metadata database, and restart services without touching persistent vault data. Enables hands-off pushes to beta and prod with full CI/CD orchestration.
---
## Two-app split Wave 2 — Phase 4
**Status:** Phase 4 (project rename) landed on 2026-05-19.
### Phase 4 — Two-app split: rename `DeepDrftWeb` → `DeepDrftPublic`
**Landed 2026-05-19.**
Renamed `DeepDrftWeb` to `DeepDrftPublic` and `DeepDrftWeb.Client` to `DeepDrftPublic.Client` across all project files, `.csproj` files, namespace declarations, using directives, solution file, and deploy scripts. Updated all references in `CLAUDE.md` agent guidance to reflect the new names. Also updated prior references to `DeepDrftWeb.Services` to `DeepDrftData` to align with the Phase 2 library rename. The solution builds cleanly with all endpoints functional.
---
## CMS Wave 1 — Auth + scaffolding + parity
**Status:** All sub-items landed on 2026-05-18.
### W1.0 `DeepDrftContext` Postgres migration
**Landed 2026-05-18.**
Rewrite all existing EF Core migrations from SQLite to PostgreSQL. Update the `DeepDrftWeb` and `DeepDrftCli` connection strings in config. Migrate any existing data from `../Database/deepdrft.db` to Postgres. Verify the existing `api/track/page` and `api/track/{id}` endpoints function against the new backend. This is a prerequisite for W1.2 (which also runs migrations for AuthDbContext against the same Postgres instance).
### W1.1 `DeepDrftCms` RCL skeleton
**Landed 2026-05-18.**
Project created, added to solution, referenced from `DeepDrftWeb`. Empty `Pages/Cms/Index.razor` mounted at `/cms` returning a "CMS — under construction" placeholder, proving the mount works.
### CMS RCL inlined into `DeepDrftManager`
**Landed 2026-05-21.**
The `DeepDrftCms` Razor Class Library has been inlined into `DeepDrftManager` and the standalone project deleted from the solution. All Razor pages, components, and layouts (CmsLayout, DeleteTrackDialog, TrackList, TrackNew, TrackEdit, and the CMS index page) now live directly in `DeepDrftManager/Components/Pages/Cms/`, `DeepDrftManager/Components/Pages/Tracks/`, `DeepDrftManager/Components/Layout/`, and `DeepDrftManager/Components/Shared/`. The `DeepDrftManager.csproj` no longer references the now-deleted `DeepDrftCms` project. `DeepDrftManager/Program.cs` no longer calls `AddCmsServices()` or references the CMS assembly. Solution builds cleanly with all CMS endpoints and pages functional.
### W1.2 AuthBlocks integration + login
**Landed 2026-05-18.**
Reference `Cerebellum.AuthBlocks`, `Cerebellum.AuthBlocks.Web`, `Cerebellum.AuthBlocks.Models` from `DeepDrftWeb`; reference `Cerebellum.AuthBlocks.Web` from `DeepDrftWeb.Client`. Call `AddAuthBlocks(...)` in `Program.cs` with JWT secret/issuer/audience, Mailtrap email connection, Postgres connection string, and `AdminUserSettings` from `environment/authblocks.json`. Call `await app.Services.UseAuthBlocksStartupAsync()` post-build. Call `app.MapAuthBlocks()` to mount `/api/auth/*` routes. Add the `AuthBlocksWeb` assembly to `AddAdditionalAssemblies` so the bundled `/account/login` and `/account/logout` pages resolve. In `DeepDrftWeb.Client.Startup`, call `AuthBlocksWeb.Client.Startup.ConfigureServices(builder.Services)` for the prerender→WASM auth-state bridge. Add `CreatedByUserId : long?` column to `TrackEntity` via a nullable migration. Provision local Postgres (docker-compose) and document the dev setup. Includes `CmsStealthRoutingHandler` — a custom `IAuthorizationMiddlewareResultHandler` that returns 404 for any `/cms/*` hit that fails authorization, honouring the stealth-routing constraint: unauthorized access to admin routes returns 404, not 401 or redirect.
---
## CMS Wave 1 (legacy section header for reference)
**Status:** All sub-items landed on 2026-05-18.
Goal was: A logged-in collective member can do everything the CLI does today, from a browser.
### W1.3 CMS track list
**Landed in CMS Wave 3.**
`/cms/tracks` consuming the same `GET api/track/page` endpoint as the public gallery. Different rendering (table with admin affordances), same VM. No new SQL endpoint.
### W1.4 CMS upload endpoint + add page
**Landed in CMS Wave 3.**
New `POST api/cms/track` on `DeepDrftWeb` (auth-gated, see §5 for the transport decision). `/cms/tracks/new` page wires `InputFile` to the endpoint. Note: Option B is confirmed — this requires a new `POST api/track/upload` endpoint on `DeepDrftContent` (raw WAV in, unpersisted `TrackEntity` out) in addition to the CMS page and controller.
### W1.5 CMS delete endpoint + delete UI
**Landed in CMS Wave 3.**
New `DELETE api/cms/track/{id}` on `DeepDrftWeb`. Removes the SQL row and the vault entry; logs orphans if vault delete fails after SQL delete succeeds. Delete button + confirmation in the list and detail pages.
### W1.6 CMS edit endpoint + edit page
**Landed in CMS Wave 3.**
New `PUT api/cms/track/{id}` (metadata only — no binary replacement in Wave 1). `/cms/tracks/{id}` page.
---
## Phase 2 — Product surface: gallery, browsing, ingestion
### 2.4 Web-side track upload
**Landed in CMS Wave 1 (subsumed by `CMS-PLAN.md`).**
The CLI is the only producer of tracks today. A web upload UI would pair with `TrackService.AddTrackFromWavAsync` and the existing `PUT api/track/{id}` (already `[ApiKeyAuthorize]`-protected).
- **Why it matters:** Lowers the barrier to adding content. The collective can publish without shell access to the host.
- **Shape:**
- New page or modal on the web client, drag-and-drop file input.
- Upload streams to a `POST` endpoint on `DeepDrftWeb` (not `DeepDrftContent` — the web host orchestrates the dual-write, then forwards bytes to content with the API key it already holds).
- Authentication: this is the first user-facing action that needs to be gated. A new question — see open question below.
- **Prerequisite:** **Authentication model for the web side**. Currently the site has no user concept. Cookie-with-shared-password? OAuth? Per-collective-member account? Decide before building the UI.
- **Open question:** Same as above. This may also bring forward a wider session/identity decision that other features (favourites, listening history) will need eventually.
- **Constraint:** Today's dual-write has no compensating rollback — if content-side succeeds and SQL-side fails, the audio is orphaned in the vault. The CLI inherits this; pushing this onto a web upload increases the rate at which orphans can occur. A simple `DeadLetterLog` of orphaned `entryKey`s (suggested in the audit) becomes more pressing once the web upload exists.
---
## Phase 0 — Wireframe-driven home page redesign
**Status:** All sub-items landed on 2026-05-17.
A design wireframe (`deepdrft-wireframe.html` at the project root) is the source of truth for a full visual reskin of the public site. The current `Home.razor` is a MudPaper/MudGrid composition with a generic "purple-tint" feature card aesthetic that doesn't match the collective's intended voice. The wireframe replaces it with a layout-first, editorial design: 50/50 hero, frosted-glass nav, dark feature band, green origin/connect split, navy CTA banner with ghost-watermark, and an italic-serif accent treatment throughout.
Scope here is **the home page and the chrome that wraps it** (nav, layout container, theme palette, font loading). The track gallery (`TracksView.razor`), the audio player dock (`AudioPlayerBar.razor`), and the FileDatabase/streaming substrate are **out of scope** for Phase 0 — they keep working through the existing MudBlazor theme, which is being recoloured under them. The "Now Playing" card in the hero is a *new* surface that reads from the existing `IPlayerService` cascade; it is a view onto the player, not a replacement for the dock.
Phase 0 sub-items decompose into worktree-sized tracks. 0.1 is the foundation everything else inherits — land it first. 0.20.4 can proceed in parallel against that foundation. 0.5 is a follow-on tuning pass once the light theme is in.
### 0.1 Light palette + font system
- **What:** Replace the "Charleston in the Day" `PaletteLight` in `DeepDrftWeb.Client/Layout/MainLayout.razor` with the wireframe palette (`--white #FAFAF8`, `--navy #0D1B2A`, `--green #1A3C34`, `--green-accent #3D7A68`, `--muted #8A9BB0`), expressed as MudBlazor `PaletteLight` properties. Update the corresponding CSS custom properties in `DeepDrftWeb/wwwroot/styles/deepdrft-styles.css` so the `deepdrft-*` utility classes still resolve. Add `Geist Mono` to the Google Fonts `<link>` in `DeepDrftWeb/Components/App.razor`. Upgrade the existing `Cormorant` link to `Cormorant Garamond` with the italic + 300/400/600 weight set used by the wireframe. Remove the `Bodoni Moda` link (and its `--font-hero` reference) if no remaining surface uses it.
- **Why it matters:** Every other Phase 0 sub-item consumes these tokens. Fonts and palette landing first means 0.2/0.3/0.4 can render at intended fidelity from the moment they're built, not approximate-then-correct. The font swap is also the only Phase 0 change that affects HTML served by the host project (`App.razor`), so isolating it cleanly keeps the render-mode seam clear.
- **Shape:**
- MudBlazor palette mapping (light): `Primary = navy`, `Secondary = green`, `Tertiary = green-accent`, `Background = white`, `Surface = white`, `AppbarBackground = "rgba(250,250,248,0.88)"`, `AppbarText = navy`, `TextPrimary = navy`, `TextSecondary = muted`, `Divider = "rgba(13,27,42,0.10)"`, `LinesDefault / TableLines` to match. Semantic colours (`Info/Success/Warning/Error`) stay at MudBlazor defaults.
- Typography block (light): `H1``H6` and a new wireframe-specific display class use `Cormorant Garamond`; `Button` / `Default` keep `DM Sans`; introduce a `Subtitle1` / `Caption` family pointing at `Geist Mono` for label/eyebrow text.
- CSS variables: rename or alias the existing `--deepdrft-primary/--deepdrft-secondary/etc.` to the wireframe palette in `:root`. Add `--font-mono: "Geist Mono", monospace;` and update `--font-hero` / `--font-headers` to `"Cormorant Garamond", serif`. Where the legacy palette has no wireframe equivalent (e.g. `--deepdrft-quaternary` warm gold), prefer mapping it to the closest wireframe colour rather than inventing a new one — the goal is convergence on the new vocabulary, not coexistence.
- Font loading: a single Google Fonts link, ideally one combined request with `family=Cormorant+Garamond:ital,wght@…&family=Geist+Mono:wght@…&family=DM+Sans:…`. One round-trip, three families.
- **Prerequisite:** None — this is the foundation.
- **Constraint:** The dark palette ("Lowcountry Summer Nights") must stay functional after this change even if visually mismatched — 0.5 is the dedicated pass for re-harmonising it. Do not edit the dark palette in 0.1. The dark-mode cookie + `PersistentComponentState` round-trip described in `CLAUDE.md` must be preserved unchanged.
### 0.2 Frosted-glass top nav
- **What:** Replace the current MudBlazor `MudAppBar`-based `DeepDrftMenu.razor` chrome (logo + nav stack + dark-mode toggle, default Material elevation) with the wireframe's fixed frosted-glass nav: 88% opacity off-white background, `backdrop-filter: blur(18px)`, 1px navy-alpha bottom border, no elevation shadow, navy-on-white "Stream Now" CTA pinned right, nav links in Geist Mono uppercase with the muted-to-navy hover transition.
- **Why it matters:** The nav sits across every page, so its visual language sets expectations for the rest of the site. The Material elevation + dropdown menu pattern is the strongest "this is a stock MudBlazor app" tell currently; replacing it is the single largest perceived-quality move of Phase 0.
- **Shape:**
- Keep `DeepDrftMenu.razor` as the file (the existing render-mode wiring and viewport-subscription mobile branch are reused) — rewrite the markup inside it.
- Wrap a styled `<nav>` element (or `MudAppBar` with heavy CSS override) and bind nav links to `Pages.AllPages`. The link text should render via Geist Mono with the wireframe's letter-spacing and uppercase transform.
- The "Stream Now" CTA is a new affordance — wire it to `/tracks` for now (it is functionally a "browse the gallery" action since live streaming isn't a Phase 0 surface).
- Dark-mode toggle stays — the gas-lamp icon button moves to the right of the CTA. Confirm visual treatment works against both the frosted-white nav (light) and whatever the dark-mode nav becomes after 0.5.
- Mobile branch: the `MudMenu` dropdown pattern persists, but the activator + items should adopt Geist Mono and the new colour vocabulary. No drawer.
- **Prerequisite:** 0.1 (palette + Geist Mono load).
- **Constraint:** The nav is rendered through `MainLayout.razor` and therefore participates in server prerender. `backdrop-filter` is CSS-only and renders identically in both passes, so this is safe — but any JS-driven scroll/show behaviour added later must be gated on `OnAfterRenderAsync`. `IBrowserViewportService` is already used here for breakpoints and must continue to work after the rewrite. Do not regress the dark-mode toggle wiring (`DarkModeCookieService.ToggleDarkModeAsync` → cookie → `IsDarkModeChanged` event up).
### 0.3 Split hero with live Now-Playing card
- **What:** Replace the current centered MudPaper hero in `DeepDrftWeb.Client/Pages/Home.razor` with the wireframe's 50/50 split:
- **Left:** eyebrow ("Charleston, South Carolina"), display title ("Deep / *Drft*" with italic green emphasis on "Drft"), italic-serif subtitle, body description, and the two CTAs (`Start Streaming` filled / `Browse Tracks` ghost). All entering via the existing `fade-up` CSS animation pattern with staggered delays.
- **Right:** dark navy panel with three concentric pulsing rings (CSS keyframe `pulse-ring`), a frosted "Now Playing" card (label + blinking dot + track title + sub + animated waveform bars), and the stat row (47+ / 2 / ∞).
- **Why it matters:** This is the page. Hero is what a first-time visitor sees, and it is the only sub-item that wires the new design back into the live audio system — making the design feel inhabited rather than decorative.
- **Shape:**
- **Now-Playing data source:** `Home.razor` consumes `[CascadingParameter] IPlayerService Player` (cascaded by `AudioPlayerProvider` from `MainLayout`). The card binds to `Player.IsLoaded`, `Player.IsPlaying`, `Player.CurrentTime`, `Player.Duration`. `IPlayerService` does not currently expose the selected `TrackEntity` as a public property — add `IPlayerService.CurrentTrack { get; }` (nullable `TrackEntity`) and surface the backing field in `AudioPlayerService`. Additive, no existing consumer is affected — implement it as part of this sub-item without a separate approval gate.
- **Empty state:** when `Player.CurrentTrack is null`, render a placeholder ("Nothing playing — pick a track" or similar) inside the card with the same chrome but no waveform animation. The card is permanent layout, not conditional on selection.
- **Animated waveform bars:** Phase 0 uses the wireframe's pure-CSS `wave-dance` keyframe animation with randomised `--h-lo` / `--h-hi` / `--dur` per bar — driven by no real audio data. A later phase can wire `SpectrumAnalyzer` data through `AudioInteropService.GetSpectrumData()` to drive bar heights, but that path is already used by `SpectrumVisualizer.razor` in the dock and duplicating it here is out of scope.
- **Stat row:** static markup with hard-coded "47+", "2", "∞" and TODO comments. The first two could plausibly become real numbers (track count, member count from a future identity model) — flag those at the markup site for Phase 2/identity work to pick up.
- **Pulsing-ring decoration:** three absolutely-positioned divs as in the wireframe, with the `pulse-ring` keyframe. These are decorative and live in `deepdrft-styles.css` or a `Home.razor.css` scoped stylesheet — pick scoped CSS for anything home-page-specific to keep the global stylesheet from accreting.
- **Render mode:** `Home.razor` lives in `DeepDrftWeb.Client/Pages/`, so it is already WASM-interactive end-to-end. The cascading `IPlayerService` works in both server prerender (no track loaded → empty state) and post-WASM (live state). No `OnAfterRenderAsync` gymnastics needed.
- **Prerequisite:** 0.1 (fonts + palette for the markup to render correctly).
- **Constraint:** Do not introduce a second player implementation or a separate state store. The "Now Playing" card is **a view onto the same `IPlayerService` instance** the dock uses (see `user_one_source_multiple_views`). If the dock plays a track, the hero card reflects it; if the hero card eventually grows controls, those calls go through the same cascade. The hero's CTAs route to `/tracks` and (eventually) trigger `Player.SelectTrack` from there — they do not become a parallel selection surface.
### 0.4 Marketing content sections (sound / features / origin+connect / CTA / footer)
- **What:** Replace the remainder of `Home.razor` with five wireframe sections in order:
1. Section divider (`The Sound` tag between horizontal rules).
2. Sound section — `Genres & Moods` label, `Every / Frequency / Explored` title with italic green emphasis, body copy, 6-column genre grid (House / Techno / Trance / IDM / Progressive / Ambient) with the scaleX-from-left bottom border hover affordance.
3. Dark features section — navy background, `What We Offer` label, 4-card feature grid (`Lossless Audio Streaming`, `Live Sessions Broadcast`, `Studio Video Content`, `Growing Archive`) with stroked SVG icons.
4. Split origin + connect — green-panel origin copy on the left with a soft-circle decoration, white-panel "Stay Connected" on the right with Newsletter + Live Alerts option rows and a `Subscribe Free` CTA.
5. Navy CTA banner with the ghost `DRFT` watermark, headline, sub, and dual CTAs (`Explore the Archive` filled-white / `View Live Schedule` outline-white).
6. Footer with logo, link list, copyright. Replaces nothing today (there is no footer in the current layout) — add it inside `MainLayout.razor` so it appears site-wide, or inside `Home.razor` if Phase 0 wants it on the home page only. Recommend site-wide.
- **Why it matters:** These sections are what carries the editorial voice. They are decorative-but-load-bearing — without them, the home page is just a hero floating in whitespace.
- **Shape:**
- **Genre grid:** static cards. Each `genre-card` is a Razor markup block (or a small `<GenreCard />` component if the duplication grates). Phase 2.2 (album/genre views) will wire these to real filtered routes; for Phase 0, an `href="#"` placeholder is acceptable, flagged with a `TODO: wire to /genres/{slug} in Phase 2.2` comment.
- **Features grid:** the four cards mirror the existing copy on the current `Home.razor` ("High-Quality Streaming", "Live Sessions", "Video Content", "Growing Archive"). Keep the copy intent; reskin to the wireframe. Inline the four SVG icons from the wireframe (they are already 24-box `viewBox` stroked paths and fit `DDIcons.cs` if a static-icon home is preferred — but inline is fine for Phase 0; only promote to `DDIcons` if reuse appears).
- **Origin + Connect split:** the origin copy is editorial — adapt the existing "Charleston, SC" copy from the current `Home.razor` to the new section. The Connect side has two non-functional rows for Phase 0: Newsletter and Live Alerts are decorative pending an identity/subscription system. Flag them.
- **CTA banner:** the `DRFT` ghost watermark uses `::before` with a `22rem` font size — verify it doesn't trigger layout overflow on narrow viewports (the wireframe uses `overflow: hidden` on the parent; replicate that).
- **Footer:** new site-wide affordance. Site root `MainLayout.razor` is the right home for it (after `MudMainContent`, before the closing `MudLayout`). Use `Pages.AllPages` for the link list to keep the source of truth in one place.
- **Scoped CSS:** these sections are home-page-specific decorative styling. Use `Home.razor.css` (scoped stylesheet) for anything that doesn't generalise; reserve `deepdrft-styles.css` for things genuinely shared across pages.
- **Prerequisite:** 0.1 (palette + fonts).
- **Constraint:** The footer added to `MainLayout.razor` renders on **every** page, including `/tracks`. The dock is the bottom-fixed surface; the footer must be in the document flow above it. **Confirmed:** the `AudioPlayerBar` already starts minimized (`_isMinimized = true`) and expands only on track selection — footer coexistence is acceptable as-is. No suppression logic needed.
### 0.5 Dark theme harmony pass
- **What:** Review the existing "Lowcountry Summer Nights" `PaletteDark` against the Phase 0 light palette and update it so the dark variant feels like a sibling of the new design vocabulary rather than the old one. The current dark palette is coral/sunset/firefly-gold over deep twilight — that may or may not still read as cohesive once the light side has been pulled to navy/green/off-white.
- **Why it matters:** Dark mode is a first-class affordance (cookie-persisted, prerender-aware). If the dark theme reads as a different product after 0.10.4 land, the toggle becomes a surprise rather than a preference. This sub-item is the explicit budget for re-harmonising it instead of letting drift accumulate.
- **Shape:** **Confirmed: Option B (mirror).** Rebuild the dark palette as a dark-navy ground — `--navy` as background, deeper navy as surface, `--green-accent` as primary accent, `--white` (#FAFAF8) as text. Visually consistent with the light theme; the "Lowcountry Summer Nights" coral/sunset identity is retired. Adjust contrast values so text and interactive targets meet WCAG thresholds on the darker ground — the light palette's tokens are a starting point, not a direct copy.
- **Prerequisite:** 0.10.4 ideally landed so the harmony evaluation has the actual artefact to look at. Can run in a sketch worktree against 0.1 alone if speed matters.
- **Constraint:** The dark-mode cookie + `PersistentComponentState` round-trip is untouched. Only the palette values in `PaletteDark` and the `.deepdrft-theme-dark` CSS-variable block change. Do not refactor the toggle, the cookie service, or the prerender bridge — those are tested and load-bearing.
### Phase 0 deferred (not in scope)
These would naturally appear when scoping a redesign, and are explicitly **not** Phase 0:
- **Real "Now Playing" waveform from `SpectrumAnalyzer`.** CSS-keyframe waveform is good enough for Phase 0. Wiring real spectrum data into the hero card duplicates work already done in the dock and is better folded into a future "shared spectrum hook" refactor.
- **Real stat-row numbers.** Track count would need a `GET api/track/count` endpoint or a count column in the paged response; member count needs an identity model. Hard-coded with TODO is intentional.
- **Genre-filter routes.** Genre cards are decorative in 0.4. Real `/genres/{slug}` is Phase 2.2 work.
- **Subscribe / Live Alerts functionality.** Both rows are visual placeholders. Real subscription requires email collection + storage + an identity decision (see "Cross-cutting / not yet themed").
- **`TracksView.razor` reskin.** The gallery has its own composition (`TracksGallery``TrackCard`) that deserves its own design pass, not a Phase 0 retrofit. It continues to work under the recoloured MudBlazor theme.
- **`AudioPlayerBar.razor` reskin.** Same logic. The dock works against the new palette via MudBlazor tokens; a dedicated dock redesign is out of scope.
- **Animation library / scroll-triggered fades.** The wireframe's `fade-up` is CSS-only with hard-coded delays. Anything richer (IntersectionObserver, framer-motion-equivalent) is post-Phase 0.