379 lines
27 KiB
Markdown
379 lines
27 KiB
Markdown
# PLAN.md — DeepDrftHome forward roadmap
|
||
|
||
Forward-looking roadmap. Sits alongside `CONTEXT.md` (architecture orientation) and `COMPLETED.md` (history). Per `CONTEXT.md §6`, items move from here to `COMPLETED.md` when work lands; do not delete completed entries.
|
||
|
||
Organised by **theme**, not by date. Themes are roughly ordered by current product weight, not commitment. Nothing here carries a timeline unless it explicitly says so.
|
||
|
||
---
|
||
|
||
## 0. Baseline — what just landed
|
||
|
||
A two-part audit (design + streaming) ran on 2026-05-17 and the fixes for Critical, Major, and Minor findings are now on `dev`. The remainder of this plan assumes that baseline. In summary the audit-pass fixed:
|
||
|
||
- **Index concurrency** — `VaultIndexDirectory` no longer drops the lock before its async disk write; the index file can no longer be clobbered by interleaved writers.
|
||
- **Repository semantics** — `TrackRepository.Update` now fails-fast when an `Id` is not found instead of silently issuing an `INSERT`.
|
||
- **Streaming Criticals** — concurrent-seek race in the client, dirty trailing bytes leaking out of the `ArrayPool`-rented buffer, final-tail audio dropped at EOF below the minimum decode frame, and the assumption that the first network chunk contains the whole WAV header.
|
||
- **17 design and streaming Majors/Minors** across all eight projects — format-validation alignment between processor/offset/decoder, `IAsyncDisposable` on the player provider, cancellation tokens threaded through the HTTP path, structured logging into the FileDatabase subsystem, sort-sentinel cleanup, sundry DRY/SRP tightenings.
|
||
|
||
What this means for the roadmap: the streaming substrate is solid. Future work can build on top of it rather than around it. The remaining items in `TODO-V2.md` that did not land are **deferred as features, not bugs** — they are captured below under Phase 1.
|
||
|
||
---
|
||
|
||
## Phase 1 — Streaming features deferred from the audit
|
||
|
||
These were flagged during the audit but classified as feature work, not defect fixes. They are listed in rough order of user-visible impact.
|
||
|
||
### 1.3 Preload / prefetch of the next track
|
||
|
||
- **What:** No mechanism to begin the next track's stream during the tail of the current. Each play is a cold fetch.
|
||
- **Why it matters:** Prerequisite for both crossfade (1.4) and gapless (1.5). Also a perceived-latency win on its own — track-change feels instant when the bytes are already in flight.
|
||
- **Shape:** A second `HttpClient` request kicked off when the current track passes a configurable threshold (e.g. last 10 seconds). Bytes accumulate into a staged `StreamDecoder` instance rather than the live one. Promotion to "current" happens at end-of-stream or on user-selected next.
|
||
- **Prerequisite:** Requires a notion of "next track" — today the player only knows the current one. That implies either a playlist/queue model in `IPlayerService` or a passive "what was the next row in the gallery" inference.
|
||
- **Open question:** Does a queue model belong in `IPlayerService`, or is the player a single-slot device that a future `PlaylistService` orchestrates above? Worth a design note before implementation. Capture in product notes when picked up.
|
||
|
||
### 1.4 Crossfade
|
||
|
||
- **What:** Smooth A→B transition with overlapping fade-out / fade-in.
|
||
- **Why it matters:** DJ/mix aesthetic that fits the DeepDrft collective's electronic-music context. Distinguishing UX from generic "next track."
|
||
- **Shape:** Architecturally two simultaneous `PlaybackScheduler` instances suffice — each owns its own gain node, crossfaded via `GainNode.gain.linearRampToValueAtTime`. The wiring is the work, not the audio graph itself.
|
||
- **Prerequisite:** **1.3 (Preload)** — there is nothing to fade *into* without prefetch.
|
||
|
||
### 1.5 Gapless playback
|
||
|
||
- **What:** Eliminate the inter-track silence that exists today.
|
||
- **Why it matters:** Important for live-set rips, mix tapes, anything authored to flow continuously.
|
||
- **Shape:** The decoder must be able to start the next track's first buffer scheduled exactly at the end of the current one's last buffer (sample-accurate, not wall-clock). With `PlaybackScheduler`'s existing 500 ms lookahead this is mechanically achievable — the next track's first `AudioBufferSourceNode.start(t)` is set to the previous track's end time.
|
||
- **Prerequisite:** **1.3 (Preload)**. Also needs to play nicely with **1.2** because gapless across formats is hard (encoder padding/priming on MP3 in particular).
|
||
- **Constraint:** Truly sample-accurate gapless requires knowing the priming/padding sample counts of the source format. Out of scope for WAV-only; revisit when format diversity lands.
|
||
|
||
### 1.6 Track-skip on error
|
||
|
||
- **What:** A failed `processStreamingChunk` aborts the entire load with no recovery path.
|
||
- **Why it matters:** One corrupt frame at byte 4M of a 100 MB stream currently means the listener loses the entire track. Should at minimum surface a clear error and (optionally) skip past the bad region.
|
||
- **Shape:** Two-level response.
|
||
- Cheap: catch in the streaming loop, surface a user-visible error, advance the gallery to the next track if a queue exists.
|
||
- Richer: byte-scan forward to the next valid frame header for the format and resume. Format-dependent — only worth doing once **1.2** lands.
|
||
|
||
### 1.7 Safari compatibility
|
||
|
||
- **What:** Two known Safari edge cases.
|
||
- `webkitAudioContext.close()` is async-but-not-Promise on older Safari (≤ ~14); `await` resolves immediately and the next `initialize()` can run against a not-yet-closed context.
|
||
- iOS Safari < 15 had streaming-fetch quirks; `HttpCompletionOption.ResponseHeadersRead` behaviour is not guaranteed there.
|
||
- **Why it matters:** Real listener share. iOS in particular is a primary listening surface for music.
|
||
- **Shape:** For the `close()` race — detect `webkitAudioContext` and poll `state === "closed"` with a short timeout instead of trusting the `await`. For the fetch quirks — first decide the minimum supported iOS version; if pre-15 is in scope, fall back to a non-streaming fetch path and accept the latency.
|
||
- **Open question:** What's the floor? Decide before designing the fallback. iOS 15+ as the floor would let us drop the second concern entirely.
|
||
|
||
---
|
||
|
||
## Phase 2 — Product surface: gallery, browsing, ingestion
|
||
|
||
These follow from `CONTEXT.md §5`. Direction is strongly implied but no specific UI has been committed.
|
||
|
||
---
|
||
|
||
## Phase 6 — CMS Enhancements (Completed)
|
||
|
||
See `COMPLETED.md` for Phase 6 (§6.1, §6.3) and entity-prep (§6.2 model layer) which landed on dev in June 2026.
|
||
|
||
---
|
||
|
||
### 6.2 Card-contextual filtering of the Tracks page — `[superseded by §8]`
|
||
|
||
- **What:** Make the Album and Genre dashboard cards navigate into a *filtered* `/tracks` view (e.g. clicking an album card shows only that album's tracks), rather than the unfiltered table.
|
||
- **Why:** Turns the dashboard from a read-only summary into a navigation hub — the natural next step once the cards exist.
|
||
- **Why deferred:** The dashboard cards aggregate *across all* albums/genres — there is no single album/genre to filter to from a top-level count card. Meaningful per-album/per-genre navigation needs an intermediate browse surface (a list of albums, a list of genres) for the admin to pick from — i.e. it's really a CMS analogue of the public `AlbumsView`/`GenresView`, not a property of the summary cards. That's a larger surface than the dashboard itself and shouldn't be smuggled in. The `GET api/track/page` endpoint already accepts `album=` and `genre=` query filters, so the API substrate is ready; the missing piece is the CMS browse UI and the filter plumbing in `TrackList`.
|
||
- **Superseded:** **§8 (CMS Track Browser)** builds exactly the intermediate browse surface this item was waiting on — Album Mode and Genre Mode *are* the CMS analogue of `AlbumsView`/`GenresView`, and the filter plumbing into `GetPagedAsync` is part of §8's data contract. This item folds into §8; do not implement it separately.
|
||
|
||
---
|
||
|
||
## Phase 3 — New content kinds
|
||
|
||
### 3.1 Live / session content
|
||
|
||
- **What:** The home page advertises "Live Sessions" and "Video Content (coming soon)". No data model exists for these.
|
||
- **Why it matters:** Honour the home page copy. Also differentiates the site from a generic track gallery — live sessions and video are the collective's authored output.
|
||
- **Shape:** Speculative; no commitment yet.
|
||
- Likely new entity table(s) sibling to `TrackEntity` (`SessionEntity`, `VideoEntity`?) — or a polymorphic `MediaEntity` with discriminator. The choice affects how much code in `TrackService` / `TrackController` can be reused.
|
||
- New vault type(s). `MediaVaultType.Media` exists and is the obvious home for video; sessions are probably still `Audio`.
|
||
- New routes, new UI surfaces, new player considerations (video has its own playback element and does not go through the WAV decoder).
|
||
- **Prerequisite:** Probably **2.1** (vault wiring proof) and a decision on the entity model before any code lands.
|
||
- **`[speculative]`** — direction inferred from home-page copy, not a Daniel-confirmed commitment.
|
||
|
||
---
|
||
|
||
## Phase 4 — Infrastructure / delivery
|
||
|
||
### 4.3 Dual-write rollback / dead-letter log
|
||
|
||
- **What:** If content-side write succeeds and SQL-side write fails, audio is orphaned in the vault. No compensating mechanism exists.
|
||
- **Why it matters:** A latent data-integrity issue. Materially riskier once web upload (2.4) exists.
|
||
- **Shape:** Audit suggested a `DeadLetterLog` recording orphaned `entryKey`s for a periodic maintenance pass. Lighter than full transactional rollback (which the dual-database split fundamentally cannot give us).
|
||
- **Prerequisite:** None. Worth landing alongside or just before 2.4.
|
||
|
||
---
|
||
|
||
## Phase 5 — Documentation backlog
|
||
|
||
### 5.1 Folder-level CLAUDE.md sweep
|
||
|
||
- **What:** Eight folder-level `CLAUDE.md` files need writing/rewriting per the brief in `DOC_PLAN.md`. Five are rewrites (drift from the `.NET 10` upgrade and structural moves); three are new (`DeepDrftWeb.Services`, `DeepDrftContent.Services` — the two libraries where most domain logic now lives — plus the open question on `DeepDrftContent.Services/FileDatabase/README.md`).
|
||
- **Why it matters:** The agent guidance files are how every future implementer (human or agent) gets oriented in a directory. They are currently misleading in ways that will cause wrong assumptions on first contact — claiming `.NET 9`, referencing `MediaPath` that has been `EntryKey` for two migrations, describing a `FileDatabase/` tree inside `DeepDrftContent` that has moved out, and missing entirely for the two `*.Services` libraries.
|
||
- **Shape:** Doc-keeper executes against `DOC_PLAN.md`. Order of operations and the per-folder briefs are already specified there.
|
||
- **Prerequisite:** None. Can run fully in parallel with any feature work.
|
||
- **Constraint:** Wait on Daniel for the `DeepDrftContent.Services/FileDatabase/README.md` judgement call before that file changes (retire, keep + refresh, or replace with a CLAUDE.md). The other seven can proceed without that decision.
|
||
|
||
---
|
||
|
||
## Phase 7 — Shared UI Components
|
||
|
||
Reusable presentational components in `DeepDrftShared.Client` (the RCL consumed by both the public site and the CMS). Distinct from the player stack and CMS surfaces — these are host-agnostic building blocks both apps compose.
|
||
|
||
---
|
||
|
||
## Phase 8 — CMS Track Browser
|
||
|
||
Three browse modes for the CMS `/tracks` page — **Track**, **Album**, **Genre** — selected by a toggle, each deep-linkable so the public home page can link straight into a mode. One view-model (DI-scoped, matching the `TracksViewModel` pattern) feeds all three views; the divergence is in rendering, not data paths (per the standing "same data, different uses" preference). This supersedes the deferred §6.2 — Album and Genre modes *are* the intermediate browse surface that item was waiting on. Full spec: `product-notes/phase-8-cms-track-browser.md` (normalization gate, component decomposition, VM design, URL scheme, data contracts, open questions).
|
||
|
||
**§8.0 landed on 2026-06-11** — a breaking `TrackEntity` normalization has been completed and is stable on dev. §8.1–§8.5 are now unblocked. The Waveform Pre-Processing tab is **removed**, folded into an in-grid status column + per-row/page-level generate actions (see §8.2).
|
||
|
||
---
|
||
|
||
## Phase 8.6 — "Music through Every Medium" Section
|
||
|
||
Replaces the "Genres & Moods" block in `DeepDrftPublic.Client/Pages/Home.razor` (current lines 43–86 — the `<section class="section">` containing the `.genre-grid`). The 6 text-only genre cards become **3 image-first cards** keyed on release format: Studio, Live, DJ Mix. The pivot is taxonomy → medium: instead of "what scene is this," the section answers "in what form does the music reach you."
|
||
|
||
The section-divider tag stays "The Sound." The `.section-divider` and `.section-header-grid` wrappers (Home.razor lines 36–57) are **untouched** — only the header copy inside the grid and the card grid below it change. Everything from `.section-dark` onward (line 88+) is untouched.
|
||
|
||
**Design intent.** The current section is a flat, typographic palette grid — appropriate when the message was "we span many genres." The new message is fewer, weightier, photographic: three distinct *ways* the collective produces, each earning a full image pane. This trades the dense 6-up rhythm for a confident 3-up editorial spread, closer in spirit to the dark `.features-grid` (icon + title + desc) but image-led rather than icon-led. The card is the unit of interest now, not the grid texture.
|
||
|
||
### 1. Section header copy
|
||
|
||
| Slot | Class | Copy |
|
||
| --- | --- | --- |
|
||
| Label | `.section-label` | `Format & Medium` |
|
||
| Title | `.section-title` | `Music through<br /><em>Every</em><br />Medium` |
|
||
| Body | `.section-body` | `The same hands, three different rooms. A studio cut is built; a live set is risked; a DJ mix is woven. We release in every form the music asks for — each one a different relationship between the moment and the record of it.` |
|
||
|
||
The `<em>Every</em>` carries the italic-green emphasis the existing `.section-title em` rule already styles — no change needed there. (Title echoes the prior "Every Frequency Explored" cadence deliberately, so the replacement reads as an evolution of the same voice, not a rewrite.)
|
||
|
||
### 2. Card copy
|
||
|
||
| Card | Type label (`.medium-type`, mono) | Title (`.medium-name`, serif) | One-line descriptor (`.medium-desc`) |
|
||
| --- | --- | --- | --- |
|
||
| Studio | `Produced` | `Studio Releases` | `Composed, layered, and finished — tracks built to be returned to.` |
|
||
| Live | `Captured` | `Live Releases` | `Performances caught in the moment, unrepeatable and unedited.` |
|
||
| DJ Mix | `Continuous` | `DJ Mix Releases` | `Uninterrupted sets — one track bleeding into the next, start to finish.` |
|
||
|
||
The type labels (`Produced` / `Captured` / `Continuous`) play the same one-word-essence role the genre `.genre-count` labels did ("Foundation," "Architecture," …) — kept deliberately to preserve that tic of the original design.
|
||
|
||
### 3. HTML structure sketch
|
||
|
||
Replaces Home.razor lines 43–86. Header grid block (lines 44–57) keeps its existing structure with only the copy swapped; the grid below is new:
|
||
|
||
```razor
|
||
@* Medium section *@
|
||
<section class="section">
|
||
<div class="section-header-grid">
|
||
<MudGrid Style="margin-bottom: 5rem;">
|
||
<MudItem xs="12" md="4">
|
||
<div class="section-label">Format & Medium</div>
|
||
<h2 class="section-title">Music through<br /><em>Every</em><br />Medium</h2>
|
||
</MudItem>
|
||
<MudItem xs="12" md="8">
|
||
<p class="section-body"> ...body copy from §1... </p>
|
||
</MudItem>
|
||
</MudGrid>
|
||
</div>
|
||
|
||
<div class="medium-grid">
|
||
@* TODO Phase 3.x: wire each card to its format-filtered browse route once /tracks?format= exists *@
|
||
<div class="medium-card">
|
||
<div class="medium-image" style="background-image: url('img/medium-studio.jpeg');">
|
||
<div class="medium-scrim"></div>
|
||
</div>
|
||
<div class="medium-body">
|
||
<div class="medium-type">Produced</div>
|
||
<div class="medium-name">Studio Releases</div>
|
||
<div class="medium-desc">Composed, layered, and finished — tracks built to be returned to.</div>
|
||
</div>
|
||
</div>
|
||
@* …Live card (medium-live.jpeg) and DJ Mix card (medium-djmix.jpeg) follow the same shape… *@
|
||
</div>
|
||
</section>
|
||
```
|
||
|
||
Notes for the implementer:
|
||
- **Image as CSS `background-image`, not `<img>`.** This makes `cover`-cropping, the scrim overlay, and the hover scale trivial without a wrapper-overflow dance, and keeps these decorative-but-branded photos out of the document's content image flow. (If alt-text/SEO is later wanted, revisit — but these are mood images, not informational, so background is the right call here.) The card is one block: image pane on top, text body below, matching the brief's "image area + text below."
|
||
- The three cards are structurally identical — implementer can author one and repeat. Leave the `TODO` comment so the future format-filter routing has a home (mirrors the existing `@* TODO Phase 2.2 *@` convention in the current genre grid).
|
||
- Whether the card is a `<div>` or an `<a>` is deferred: there is no format-filtered route yet (the genre grid had the same unresolved `/genres/{slug}` TODO). Author as `<div>` now; the `.medium-card` hover styles already assume `cursor` affordance so promoting to `<a>` later is a one-line change.
|
||
|
||
### 4. CSS additions (`Home.razor.css`)
|
||
|
||
Add a new block after the genre-grid rules (lines 106–165 can stay or be removed once the genre markup is gone — recommend **removing** the now-dead `.genre-grid` / `.genre-card` / `.genre-name` / `.genre-count` rules in the same change to avoid dead CSS, since nothing else on the page uses them; confirm no other consumer with a grep before deleting). New classes:
|
||
|
||
```css
|
||
/* ── MEDIUM GRID (Music through Every Medium) ── */
|
||
.medium-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 1px;
|
||
background: var(--deepdrft-border);
|
||
border: 1px solid var(--deepdrft-border);
|
||
margin-bottom: 4rem;
|
||
}
|
||
|
||
.medium-card {
|
||
background: var(--deepdrft-white); /* fixed white ground — matches .section, see §9 */
|
||
cursor: pointer;
|
||
overflow: hidden; /* clips the hover image scale */
|
||
text-decoration: none;
|
||
display: block;
|
||
}
|
||
|
||
.medium-image {
|
||
position: relative;
|
||
width: 100%;
|
||
aspect-ratio: 4 / 3; /* consistent crop across all three; ~240px tall at 1-col, scales with column width */
|
||
background-size: cover;
|
||
background-position: center;
|
||
transition: transform 0.5s ease;
|
||
}
|
||
|
||
.medium-card:hover .medium-image { transform: scale(1.05); }
|
||
|
||
.medium-scrim {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: linear-gradient(to bottom,
|
||
rgba(17, 35, 56, 0.0) 40%,
|
||
rgba(17, 35, 56, 0.35) 100%); /* navy scrim, weighted to the lower edge near the text seam */
|
||
transition: opacity 0.3s;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.medium-card:hover .medium-scrim { opacity: 1; }
|
||
|
||
.medium-body {
|
||
padding: 2rem 1.5rem;
|
||
position: relative;
|
||
}
|
||
|
||
/* Green underline sweep — same mechanic as the old .genre-card::after */
|
||
.medium-card::after {
|
||
content: '';
|
||
position: absolute;
|
||
bottom: 0; left: 0; right: 0;
|
||
height: 2px;
|
||
background: var(--deepdrft-green-accent);
|
||
transform: scaleX(0);
|
||
transform-origin: left;
|
||
transition: transform 0.3s;
|
||
z-index: 1;
|
||
}
|
||
|
||
.medium-card:hover::after { transform: scaleX(1); }
|
||
|
||
.medium-type {
|
||
font-family: var(--deepdrft-font-mono);
|
||
font-size: 0.58rem;
|
||
letter-spacing: 0.2em;
|
||
color: var(--deepdrft-muted);
|
||
text-transform: uppercase;
|
||
margin-bottom: 0.6rem;
|
||
}
|
||
|
||
.medium-name {
|
||
font-family: var(--deepdrft-font-display);
|
||
font-size: 1.6rem;
|
||
font-weight: 400;
|
||
color: var(--deepdrft-navy);
|
||
margin-bottom: 0.75rem;
|
||
line-height: 1.1;
|
||
}
|
||
|
||
.medium-desc {
|
||
font-family: var(--deepdrft-font-body);
|
||
font-size: 0.82rem;
|
||
line-height: 1.65;
|
||
color: var(--deepdrft-navy);
|
||
opacity: 0.6;
|
||
}
|
||
```
|
||
|
||
Reuse decisions:
|
||
- `.section`, `.section-divider`, `.section-header-grid`, `.section-label`, `.section-title`, `.section-body` — all reused unchanged.
|
||
- `.medium-type` / `.medium-name` / `.medium-desc` are new but are deliberate near-clones of `.genre-count` / `.genre-name` (bumped from 1.5→1.6rem to suit the larger card) / a new descriptor line the genre cards never had. Kept as distinct classes rather than reusing the `.genre-*` names so the dead genre CSS can be removed cleanly.
|
||
- The underline-sweep `::after` is copied from `.genre-card::after` verbatim except for the added `z-index: 1` (needed because the card now has a stacking context from the image).
|
||
|
||
### 5. Responsive breakpoints
|
||
|
||
| Viewport | `.medium-grid` columns | Behaviour |
|
||
| --- | --- | --- |
|
||
| ≥ 960px | `repeat(3, 1fr)` | Three cards in a row — the primary editorial layout. |
|
||
| 600–959px | `repeat(2, 1fr)` + third card spans both columns | Two on top, the third full-width below. Reads better than a lone 1-col orphan on tablet and keeps the image panes generous. |
|
||
| < 600px | `1fr` | Single column, cards stack. Each image pane is full content-width; `aspect-ratio: 4/3` keeps them generous (~260px tall at a typical mobile width). |
|
||
|
||
```css
|
||
@media (max-width: 959px) {
|
||
.medium-grid { grid-template-columns: repeat(2, 1fr); }
|
||
.medium-card:last-child { grid-column: 1 / -1; } /* third card spans full width */
|
||
}
|
||
|
||
@media (max-width: 599px) {
|
||
.medium-grid { grid-template-columns: 1fr; }
|
||
.medium-card:last-child { grid-column: auto; } /* reset the span at 1-col */
|
||
}
|
||
```
|
||
|
||
Note the breakpoint boundary is `959px` here (the existing genre grid used `960px` for its `max-width` query; `.section-header-grid` uses `min-width: 960px`). Using `max-width: 959px` avoids the 1px both-rules-fire overlap at exactly 960px. Implementer may keep `960` for consistency with the surrounding file if preferred — the `last-child` span makes the 960 edge case harmless either way.
|
||
|
||
### 6. Image placeholder names
|
||
|
||
All three in `DeepDrftPublic.Client/wwwroot/img/` (same dir as existing hero images), referenced as `img/<name>` to match the existing `Image1="img/..."` convention:
|
||
|
||
- `medium-studio.jpeg`
|
||
- `medium-live.jpeg`
|
||
- `medium-djmix.jpeg`
|
||
|
||
`.jpeg` extension matches every existing photo on the page (`dd-duo-hero.jpeg`, `kp-shoulder-bw.jpeg`). Recommend source images at least 800px wide (rendered up to ~430px wide at the 3-col desktop layout on a 1440px viewport, so 800px covers 2× displays). Consistent landscape orientation across all three — the `4/3 aspect-ratio` crop will center-cover whatever is supplied, but landscape sources avoid heavy cropping.
|
||
|
||
### 7. Hover and overlay spec
|
||
|
||
- **Underline sweep** (preserved from genre cards): on `:hover`, a 2px green-accent bar sweeps in from the left along the card's bottom edge (`scaleX(0)→(1)`, 0.3s). Unchanged mechanic.
|
||
- **Image scale** (new, additive): on `:hover`, the background image scales to `1.05` over 0.5s, clipped by the card's `overflow: hidden`. Slow and subtle — a breath, not a zoom. This is the "parallax-scale" the brief allowed; pure CSS transform, no JS.
|
||
- **Scrim** (always-on, subtle): a navy gradient (`--deepdrft-navy` at 0%→35% alpha, top→bottom) sits over the image at `opacity: 0.7`, deepening to `1.0` on hover. Two jobs: (a) it weights the image toward its lower edge so the transition into the text body feels intentional rather than abrupt, and (b) it future-proofs for overlaying white text on the image if a later iteration wants the title *on* the photo. Today all text sits in `.medium-body` below the image, so the scrim is purely tonal — keep it light; it should never read as a dark box. If during implementation the supplied photos are already dark/low-key, dial the base opacity down to `0.4` rather than fighting them.
|
||
|
||
The hover bundle (underline + scale + scrim-deepen) fires together as one gesture. Don't stagger them.
|
||
|
||
### 8. Dark-mode awareness
|
||
|
||
The raw `--deepdrft-white` and `--deepdrft-navy` tokens are **literal** in both themes — they are *not* remapped under `.deepdrft-theme-dark` (verified in `deepdrft-tokens.css`; only the alias tokens like `--deepdrft-surface`/`--theme-*` flip). The existing `.section` and `.genre-card` both hardcode `background: var(--deepdrft-white)`, so **this whole section is a fixed off-white ground in both light and dark mode today** — it does not invert.
|
||
|
||
The new `.medium-card` follows that same convention deliberately: white card ground, navy text, in both themes. This keeps Phase 8.6 consistent with its untouched siblings (`.section` above it stays white; only `.section-dark` below it is dark). **Do not** introduce theme-aware surface tokens here — that would make this one section invert while the rest of the white `.section` stays put, which is a larger and out-of-scope design decision (if Daniel wants the public home page to genuinely respond to dark mode, that is a separate roadmap item spanning every `.section`, not a Phase 8.6 concern).
|
||
|
||
- **Images:** unaffected by theme — same `.jpeg` assets render identically. The navy scrim also reads correctly against the off-white card in both modes.
|
||
- **Text & backgrounds:** `--deepdrft-navy` text on `--deepdrft-white` card in both modes. No `.deepdrft-theme-dark` overrides needed or wanted for this section.
|
||
|
||
### 9. Out of scope / deferred
|
||
|
||
- **Format-filtered routing.** Cards are non-navigating today (no `/tracks?format=` route exists). The `TODO` comment marks where it lands. This mirrors the genre grid's never-resolved `/genres/{slug}` TODO — don't build the route as part of 8.6.
|
||
- **A real format field on `TrackEntity`.** "Studio / Live / DJ Mix" is presentational copy here, not yet a data dimension. If these cards are ever to filter real tracks, the entity needs a `Format`/`ReleaseType` discriminator — that is Phase 3 (new content kinds) territory, not this cosmetic swap. Flagging so the copy isn't mistaken for an existing capability.
|
||
|
||
---
|
||
|
||
A small set of items that are real but don't fit a phase yet. Surface them when they become relevant rather than committing now.
|
||
|
||
- **Identity / accounts.** Currently no user concept. Needed before web upload (2.4); also a precondition for favourites, listening history, per-user playlists. Decide the shape before any of those lands. `[speculative]` until Daniel signals interest.
|
||
- **`ITrackService` interface.** Audit-suggested. Low value today (one consumer pair); higher value when the test surface expands beyond FileDatabase.
|
||
- **Test coverage outside FileDatabase.** Tests today cover the FileDatabase subsystem comprehensively and nothing else. As features in Phases 1–4 land, test scope should expand — at minimum `WavOffsetService`, `AudioProcessor`, `TrackService` (both sides), and the streaming player services. Not a phase of its own; an attached cost to feature work.
|
||
|
||
---
|
||
|
||
## Working with this file
|
||
|
||
- **Add items by extending an existing phase first**; only create a new phase when the addition genuinely doesn't fit any of 1–5. Phase numbers are organisational, not sequencing.
|
||
- **When something lands, move it to `COMPLETED.md`** rather than deleting it. Keep the original "What / Why / Shape" body intact so the history reads as a record of the decision, not just the outcome.
|
||
- **Mark genuinely uncertain items `[speculative]`** so future readers can tell what is direction vs. commitment.
|
||
- **Open questions belong in the item that raises them**, not in a separate "questions" list — they expire when the item does.
|
||
|